← Reviews
Estate briefing · diagnosis only

The estate, by state — and where it produces a confident zero

Every surface, feed, store and job in the Mallory estate, colour-coded by whether it is carrying traffic, dark, paused or retiring. Then the Demand Board arc, and a ranked gap list split by what the 17 August measurement gate depends on.

Superseded — 2026-08-13

This page's diagnosis is wrong in several places and should not be relied on. It concluded the Tower had died and the vault had gone silent. Neither was true: the Tower ran throughout, and the vault took 670 commits in the period this page calls silent. The error was a stale local clone read as if it were live.

The estate map, the state colour-coding and the gap list below still hold. The outage narrative and the gate arithmetic do not.

Read the correction →

Update, 2026-08-13 — a live producer was masking four dead ones

The vault's last commit is 2026-08-08 07:30. There are no daily notes for 8–12 August. The nightly reflection, the miss log and the weekly review have all been silent for five days, and nothing reported it — it was found by the operator trying to use the estate.

The first diagnosis was wrong, and the correction is more interesting. This page originally concluded the Tower had died, on the reasoning that cron-watchdog.sh and status-digest.sh both run on it and would have died with it. A live read of GET /status on 13 Aug disproved that: hub.lastContact was eleven minutes old and fresh: true. The Tower is up. context-push has been running the whole time.

The failure is partial — the job plane that writes the vault stopped while the VM-side jobs carried on — and the reason nobody saw it is a design fault in the health check itself. signalsHealth() reads four signals from different producers on different hosts and collapses them:

lastContact = max(ts across all four)
fresh       = ANY one within its own TTL

So one healthy producer reports the whole estate healthy. That is the same error as view recording / for every surface, and the same error as a push_sent row written before the fan-out: an aggregate that conceals the member you actually need to see. It is this briefing's thesis holding in a form sharper than the original draft — not a monitor that died, but a monitor that was built to average away the failure it was watching for.

The gate arithmetic below is corrected in place and is materially worse than first reported.

Before anything else

What was read, and what could not be

All four repos were readable for this briefing

The working assumption going in was that only mallory could be read, and that mallory-runtime, mallory-pebble and the vault would have to be taken on trust. All four were in fact readable. Everything here about the Tower jobs, the Goose recipes and the vault is therefore Confirmed from source. Two of the largest findings below could only be reached that way.

What is still out of reach

Live production state. No wrangler --remote was attempted. So: which Tower host actually runs each cron, current pause flags, prod KV contents (config:response-slas, board:editorial), row counts in calendar_events, and whether any push subscriptions exist. Anything resting on those is marked Unverifiable here.

How claims are marked


Prior art

What the existing decks already say

A large part of this ground is already covered by the three decks in the archive. Their framing is reused rather than redrawn, and none of it is restated here as discovery.

QuestionAlready answered by
The 142 / 130 / 12 / 0 founding audit2026-07-18 deck situation[0]; spec §1.
The reframe to "a demand on my time"2026-08-01 deck decisions[0]; spec §1.
triage defaults to 'off', never enabled2026-08-01 spec §1.3, citing responseSla.ts:204.
The board has never read loop_items2026-08-01 spec §1.3; v1 §9 fenced them out.
"No usage instrumentation of any kind"2026-08-01 spec §1.4 — the reason the measurement layer exists.
The responseSlaPoll transport gap2026-07-27 addendum §4, in full.
Retiring the ~$70/mo triage as the wrong feed2026-08-01 spec §3 and "On cost".
The gate, and that it may retire the board2026-08-01 spec §2, §6 step 5.
"A clean zero indistinguishable from a broken job"2026-08-01 deck risks[3], naming three instances.
The one place the decks are now wrong

The 2026-08-01 deck's delivery[2] reads: "stampNudgeUrl behind tests, then notify mirrors the send and ships the stamped link." The mirror shipped. The stamped link did not — no caller anywhere sets nudgeId. The deck records an intention as a completed state, which is exactly the failure its own risks[3] warns about.

New in this pass


Part one

The ecosystem map

Read top to bottom: producers push into ingest ports, ports write stores, jobs judge stores, surfaces read the judgement, delivery goes back out. Each node names what it feeds. is a live edge; is an edge that exists in design but carries nothing.

Colour follows the guide's functional-colour rule — health reads as the absence of colour, and oxblood is spent only where something genuinely demands attention. The red nodes are the ones to look at.

Producers

Tower / Goose container / VM — mallory-runtime
Livejobs/goose/calendar-sync.shCalendar pull → agenda artifact. Since 3 Aug also posts near-term events to the board. Confirmed at :277. POST /ingest/calendar
Dark twinjobs/calendar-sync.sh (VM)The VM original. No board post — zero hits for post_calendar. If this is the live host, "Coming up" is still empty. /ingest/calendar
Live · gate instrumentjobs/goose/eod-reflect.shNightly reflection plus the miss question. Guarded by misses_drafted — a run that drafts nothing is downgraded to fail. vault · Daily notes · ## Misses
Dark twinjobs/eod-reflect.sh (VM)No miss question. Its successor's header records that it "could never work on this host… ran twice and failed both times". ## Misses
Paused → retiringjobs/goose/slack-mentions.shSlack ball-in-court pull plus one cheap classify. is_paused gate at :31; live flag Unverifiable here. Missed roughly half of real asks Given. POST /ingest/pending
Retiredjobs/slack-email-monitor.shAgentic pull. Failed "unparseable pull" from 17 Jul, died outright 24 Jul when the work→personal switch took the connector plane. anything
Livejobs/context-push.shShips rendered signal markdown to edge KV every ~15 min. POST /context → CONTEXT_KV
Stalledjobs/goose/weekly-review.shPorted to a Goose recipe so it "runs somewhere again". The vault's last weekly review is 2026-W29 (13–19 Jul). Today is W33. Confirmed. W30, W31, W32

Ingest ports

Worker mallory-core · Access-gated
LivePOST /ingest/pendingThe only path that actually fills the court. channel_pending
Live · caller as of 3 AugPOST /ingest/calendarBuilt, tested, Access-gated. No longer caller-less — the Goose twin calls it. Whether that twin is the live host: Unverifiable here. calendar_events
LivePOST /eventsThe measurement beacon. Batch-capped at 50; unknown actions skipped, not rejected. usage_events
LivePOST /nudgeThe pacing engine — quiet hours, caps, dedup, mode gate. routes/nudge.ts:103. nudges → notify()

Stores

D1 · KV · R2
Livechannel_pendingThe court. 219 rows at the July audit. Fed only by ingest. board court zone
Live · 100 rowsusage_eventsMigration 025, append-only, 365-day prune. Four of seven verbs written; three never. weekly rollup (not running)
Live · 3-day prunenudgesSpec §4.2 called a ≥90-day retention "the one retention bug to fix first". The code kept the 3-day prune and mirrored into usage_events instead. Better answer, never written down. GET /updates
Dark to the boardloop_itemsAlex's own to-dos. Zero references across board.ts, routes/board.ts, boardCompose.ts. Confirmed. board
Dark to the boardq_tasks · needs_youAlready structured, already tracked, invisible to the board. board
Live · 283 rowsremindersAll mined from phone notifications. Remember zone · reminderPush
Dark · configKV config:response-slastriage resolves to 'off' by default (responseSla.ts:204) and was never switched on. summary · wants_action
GateddemandsThe point of the redesign. Deliberately not built — spec §6 step 5 is "a hard gate, not a pause".

Judgement & delivery jobs

Worker cron — dispatched in index.ts:280
Live · 3×/dayboardCompose07:30 / 12:00 / 21:00 London. Writes the ranked shortlist and editor's note to KV. board:editorial
Retiring · ~$70/moslaEnrichThe per-item classify tier. Retires only after the board reads demands (spec §6 step 9). channel_pending.summary
Confident zeroresponseSlaPollSkips every connection whose transport isn't edge-poll; every live connection is ingest. Iterates to zero and logs nothing. anything
Confident zeroresponseSlaEval · breach pushThe safety net for the ~12 that aged. Never fired across 142 items. Being demoted anyway, with eyes open. notify()
Live · coalescednudgeFlush2-hourly, 07–21. Collapses N queued nudge rows into one digest push (nudgeFlush.ts:43) — which is why attribution cannot work. See gap A6. notify() × 1
Uncapped fan-outreminderPush2-hourly. Loops due reminders and calls notify() once per reminder — no coalescing, and it bypasses shouldSendNow entirely (reminderPush.ts:25–33). Against 283 reminders, the likeliest source of ~16 pushes/day. notify() × N

Surfaces

Pages mallory-web · Android shell · watch · vault
Live · 6 opensPWA · debrief.mallory.chatSvelteKit. Its own Pages project, built separately from the Worker. GET /board
Renders · unmeasuredSituationBoard.svelteNot a route — a panel on / opened by ?view=updates (+page.svelte:614). GET /board records nothing. No instrument knows it was opened. usage_events
Live · legacy/waiting · UpdatesView · WaitingCardThe surfaces the board was meant to collapse. Still present. channel_pending
LivePebble watch · mallory-pebbleTimeline pins, opt-in per message by data.pebbleKind. Token bug fixed 2 Aug. Timeline API
StalledWeekly review · vault /reviewSpec §4.6: "the numbers land in the weekly review, not a dashboard." The rollup query exists (SKILL.md:65). No review written since W29. W30, W31, W32
Client-onlyFocusTimer.svelteNo backend at all — records nothing about where the day went. any store

Part two

The Demand Board arc

Three cycles in six weeks, each re-diagnosing the last. The through-line is that the question kept getting better while the instrument kept getting worse.

CycleDiagnosisWhat it changed
18 Jul
Situation Board
The SLA engine works but is invisible; pushes are a slower echo of native ones; three surfaces fragment the view.One merged self-draining board. Ambient by default, breach escalates.
27 Jul
Availability addendum
Post-ship: the board renders and judges nothing. 0 of 219 triaged, 0 deadlines, 0 breach pushes, empty board:editorial.Enrichment moved off the dead poll onto a transport-agnostic sweep. Deferral verbs surfaced.
1 Aug
The reframe
Not under-commissioned — aimed at the wrong question. Invested entirely in the one axis Slack wins, with no evidence anywhere.Unit becomes "a demand on my time". Measurement ships first and gates everything.
Now
The gate
Measurement shipped 2 Aug. Miss log started 3 Aug. Window closes ~16–17 Aug.Nothing downstream is built. The gate may conclude "retire the board and its feed".

Where it sits in the map

The Demand Board is not a node — it is a planned replacement for the court zone. Today the board reads one source. It was designed to read five.

ZoneDesigned to readActually reads
On your courtdemands, projected from four sources plus capturechannel_pending only — untriaged, so rules-order
Coming upcalendar_eventsCaller exists since 3 Aug in the Goose twin only Unverifiable
RememberremindersWorking
Handled / FYIResolved and dismissed, newest firstWorking since PR #172
Editor's noteKV board:editorial, availability-awareComposer runs 3×/day; availability-awareness not built
Alex's own to-dosloop_itemsNever — fenced out by v1 §9, still fenced
Delegated workq_tasks where status='needs_you'Never wired

What the gate is actually collecting

The miss log is not coming back empty

Read directly from the vault's daily notes. The window opened 3 Aug and runs ~14 nights:

Night## Misses
3 AugCE Plus cert expired Saturday; Bruno's remediation email ~5d old, no reply sentReal miss
4 AugSection absent. Reflection empty too.Night lost
5 AugNone.Answered
6 AugNone.Answered
7 AugSchema migration due 10 Aug arrived urgent and unassigned while on leave; no owner going into the weekendReal miss
8–12 AugNo daily note at all — Tower outage, five consecutive nights.Lost

Corrected 12 Aug: four usable nights out of ten elapsed. The window was to close ~16–17 Aug on fourteen nights. Even if the Tower returned immediately it would reach eight, half of them produced by a system that was intermittently dead. The 17 August gate cannot be met on its original basis and needs an explicit decision — extend it, or change what it rests on.

The five lost nights must be marked lost, not backfilled as None. The spec is explicit that inventing a miss is worse than reporting none; the inverse holds harder, because a fabricated None. would report that curation was working on nights when nothing was watching.

Two real misses in four answered nights, and both are the kind the board was reframed to catch — a demand on time, not a message awaiting a reply. On this evidence the gate is trending toward "build it", not "retire it". Confirmed.

But 4 Aug is gone, silently, from a 14-night sample — a 7% hole in the only evidence the decision rests on, and nothing surfaced it.

The push ratio

EventRowsWhat it actually countsTrustworthy
push_sent82Calls to notify(), logged before fan-out and outside every send's error handling — so it counts even with no subscriptions and every send throwing.Upper bound only
view12Route navigations recorded as pathname. Board, chat, brief, Updates and R&D are all panels on /.Cannot separate surfaces
open6PWA session starts, one per mount.Yes — the only clean number
push_tapped0Nothing. The branch is unreachable.Not a behaviour

So the headline stands, but as a floor rather than a measurement: up to ~16 push attempts a day against 2 app opens a day. Against a design intent of "few, calendar-aware pushes rather than going ambient", the estate is going ambient by roughly eight to one.

Two caveats, both load-bearing

The windows aren't the same. push_sent spans 3–7 Aug (5 days, 16.4/day); open spans 4–6 Aug (3 days, 2.0/day). They overlap only on 4–6 Aug. A like-for-like ratio needs the per-day split.

That one-day skew is itself a finding. The measurement layer merged on 2 Aug in a single PR. The Worker half started logging on 3 Aug; the browser half on 4 Aug. Two independent build pipelines from one merge — the deploy-split question, answered by the estate's own data.


The two chains that matter

How the zeros are manufactured

nudgeFlush · reminderPush · briefing … 13 call sites no nudgeId passed stampNudgeUrl(url, undefined) returns the URL unchanged usage_events · push_sent subject_id = NULL × 82 push lands on the phone deep link carries no ?n= +layout.svelte reads ?n= null — branch never taken push_tapped = 0
The tap metric is cut at the first link, not the last. The break is one step earlier than it looks: not that stampNudgeUrl fails, but that no caller ever supplies a nudgeId for it to stamp. All 82 push_sent rows carry a NULL subject because the field is optional and nothing sets it.
THE HOP THE INSTRUMENT a push goes out 14:52 push_sent counts intent, not delivery did he open the app open clean — 6 rows did he reach the board a panel on / — not a route none view records "/" did he act on it resolve · snooze · not today none acted has no writer 2 of 4 hops instrumented · the 2 that decide the gate are the dark ones
The spec's own justification for one table was this chain. §4.3 argued for a single append-only log so it could answer "the push landed at 14:52 — did he open, did he reach the board, did he act?" The table is right and the argument is right. Only the first two hops were ever wired.

Part three

The gap list

The split, and how strictly it was drawn

Group A is only things where the gate's own evidence is at risk — a number that will be read on 17 August and is currently wrong, missing or unreadable. Every one is additive instrumentation or a diagnostic read. None builds anything downstream, none touches the demand model, none changes what Mallory sends.

Four candidates that felt urgent were pushed to Group B, because fixing them changes behaviour the gate is currently measuring. Changing the thing you are measuring mid-window is worse than a small gap in the data.

Group A — measurement integrity · safe now

A1

Nothing records that the board was looked at

Broken
The gate's central question is whether the board earns its keep. There is no instrument that can answer it.
Evidence
GET /board (src/routes/board.ts:21) writes no usage event and no last-read timestamp. The board is a panel on / opened by ?view=updates (+page.svelte:614). The beacon logs pathname (+layout.svelte:36), so every surface records /. Confirmed
Radius
One recordUsageEvent in handleBoard, plus an explicit subjectId from openUpdates(). Additive; nothing reads usage_events at runtime.
Verdict
Do now. Correction 12 Aug: the window has since lost five more nights to the Tower outage, so this instrument matters more, not less — whatever basis the gate ends up using.
A2

4 August is missing from the miss log, and nothing said so

Broken
One night of a fourteen-night sample vanished. The gate's only judgement signal has a hole in it, invisible from inside the vault.
Evidence
Daily notes/2026-08-04.md has no ## Misses heading and an empty ## End-of-Day Reflection. Every other night 3–7 Aug has both. Confirmed The misses_drafted guard should have downgraded that run to fail and alerted, so either the job never ran or the alert never landed — Unverifiable here.
Radius
None — a read. Check eod-reflect.json for 4 Aug.
Verdict
Do now, and check daily until 17 Aug. A second lost night puts the sample below what the decision can carry.
A3

The rollup the numbers land in has not run for three weeks

Broken
Spec §4.6 chose the weekly review over a dashboard so the numbers would be read. The review is not being produced, so nothing is being read.
Evidence
The rollup exists — .claude/skills/review/SKILL.md:65 queries usage_events grouped by action. The vault's most recent weekly review is 2026-W29; W30, W31 and W32 were never written. Confirmed
Radius
Run /review. Separately, find out why jobs/goose/weekly-review.sh stopped.
Verdict
Do now. Otherwise the gate arrives with raw rows and no read-out.
A4

push_sent counts intentions, not deliveries

Broken
The 82 is the number the whole "going ambient" argument rests on, and it is an upper bound of unknown tightness.
Evidence
recordUsageEvent runs at src/webpush.ts:187–198, before the web-push fan-out (:201) and the FCM fan-out (:218). Each send is separately try/caught and swallowed. Confirmed
Radius
Add a delivered/failed count to the event's detail after fan-out rather than moving the write — moving it would lose the record of attempted-and-failed pushes, which is itself worth knowing.
Verdict
Do now, but note the discontinuity — rows before and after are not comparable. Stamp the changeover date in the weekly review.
A5

Three of the seven event verbs have no writer

Broken
surfaced, acted and expired are defined and validated but never emitted. "Cost per acted demand" has a structurally zero denominator.
Evidence
Declared at src/db.usage.ts:10, accepted at src/routes/events.ts:9. A global grep for recordUsageEvent returns exactly two call sites: webpush.ts and events.ts. boardAction posts to /response-slas/action, which records nothing. Confirmed
Radius
One insert in the /response-slas/action handler with the verb in detail.
Verdict
Do acted now — it pairs with A1. Leave surfaced and expired; they belong to the demand model and are properly gated.
A6

push_tapped cannot fire — and the fix is not a one-liner

Broken
The cut is one link earlier than it appears, and the obvious fix does not work on the main path.
Evidence
nudgeId is optional on PushMessage (webpush.ts:22) and no production caller sets it — a grep across all 13 notify() call sites returns nothing. Confirmed
Deeper
nudgeFlush deliberately coalesces N nudge rows into one digest push (nudgeFlush.ts:43) — a headline decision of the 18 Jul spec. So there is no single nudgeId to stamp, and subject_id TEXT cannot hold a set. A schema gap, not a missing argument. Inferred
Verdict
Wire the single-nudge paths now; hold the digest decision — the gate may retire the feed that generates digests anyway.

Group B — everything else · waits for the gate

B1

"Coming up" depends on which host runs calendar-sync

Status
The caller was built on 3 Aug — but only into the Goose twin. mallory-runtime PR #118 adds post_calendar to lib/common.sh:148, called from jobs/goose/calendar-sync.sh:277. The VM original has zero hits. Confirmed
Verdict
Check now, change nothing. One query against calendar_events may retire a known gap and date two specs.
B2

reminderPush fans out one push per reminder, uncapped

Status
Likeliest source of ~16 pushes/day, and a direct contradiction of "few, calendar-aware pushes". reminderPush.ts:25–33, 8×/day, bypassing shouldSendNow. Confirmed
Verdict
Wait. Coalescing changes the exact volume the gate is measuring. Confirm it is the source first, from the per-day split.
B3

The board still cannot see Alex's own work

Status
loop_items and q_tasks · needs_you are both structured, both tracked, both invisible. Confirmed
Verdict
Wait. This is the projection layer — explicitly gated as step 7.
B4

The two-pipeline deploy is real, but not the trap it looks like

Status
D17 (docs/estate/05-decisions.md:124–132) connected both mallory-web and mallory-core to main, verified 2026-07-27 with the proving commands recorded. Merge is the deploy for both. Confirmed
Cost
Two independent builds from one merge, which can succeed and fail separately — visible as the 3 Aug / 4 Aug skew. And no pause between merge and production.
Verdict
No action. Documented, verified, trade-off taken deliberately.
B5

Retiring feeds still running

Status
triage off at responseSla.ts:204; slaEnrich retires only after the board reads demands; slack-mentions carries its pause gate. Correctly parked.
Verdict
Leave everything off. The 27 Jul addendum's warning stands: enabling active posture on stale items emits high-priority pushes that override quiet hours before any revert can land.
B6

Documented state has drifted from actual state again

Status
Two fresh instances, both inside the measurement cycle. (i) Spec §4.2 called the nudges prune change "one line"; the code kept the 3-day prune and mirrored into usage_events at 365 days instead — a better answer, never written down. (ii) The deck says notify "ships the stamped link"; it does not. Confirmed
Verdict
Amend the decks when the gate closes, alongside whatever it decides.

The one thing that, if wrong, makes the rest wrong

usage_events measures the Worker's intentions, not Alex's behaviour

push_sent counts calls to notify(), logged before anything is delivered. view collapses the board, chat, the brief and R&D into a single /. acted, surfaced and expired have no writer at all. push_tapped is unreachable. Only open — six rows — is a clean record of something Alex actually did.

Everything in Group A follows from that reading, and so does the shape of the 17 August decision. If it is right, the gate is on course to be settled by the miss log alone — the vault half, not the D1 half — and the instrumentation shipped on 2 August will have contributed almost nothing to a decision it was built to inform.

If it is wrong — if the beacon was mis-traced, or there is a writer that was not found — then Group A dissolves, the 82:6 ratio is a real measurement, and 17 August can proceed on the data as it stands.

Checkable in one query, and worth running before anything here is acted on: SELECT action, subject_type, subject_id, COUNT(*) FROM usage_events GROUP BY 1,2,3. If every view row carries subject_id = '/' and no row carries action = 'acted', this briefing is right.

Mallory
design.mallory.chat/reviews/briefings · estate briefing, 2026-08-08, updated 2026-08-12 · diagnosis only.