MalloryEstate Briefing
M·030Approved
← All briefings

Notifications & Nudges

Demand Board — Measurement

The Situation Board was aimed at the wrong question. Re-points it at demands on Alex's time, and instruments the estate before rebuilding anything.

Dated 2026-08-01Programme Notifications & Nudges

Situation

The board was not under-commissioned. It was aimed at the wrong question. Its own founding audit found 142 ball-in-court items, of which 130 auto-resolved the moment Alex replied, 12 aged, and none were ever nudged. Confirmed in use: *"by the time the updates flagged the issue, I had already replied or planned to reply."* The court zone lags a system Alex already sits inside all day.

Everything Mallory built pointed at the single axis Slack wins — *has this been replied to* — and nothing at the axes Slack cannot see. triage still defaults to 'off' and was never switched on; POST /ingest/calendar is built, tested and has no caller; the board has never read loop_items, Alex's own to-dos, because v1 fenced the Loops tray out of scope. And there was no usage instrumentation anywhere, so every retire/keep decision to date was argued from architecture rather than evidence.

Key decisions

The unit is a demand

A demand on Alex's time — anything wanting a slice of his day, reply or not — ranked by cost of ignoring rather than age of non-reply.

Derived *and* authored

Manual capture is in. The v1 *"not a to-do app"* principle was aimed at manual ticking and banned manual telling as collateral — which is precisely what left the board a partial view.

Mallory takes the interrupt budget

Few, calendar-aware pushes instead of ambient silence, with a watch glance underneath. Precondition, agreed: Alex turns Slack's notifications down. Without it this is a second, slower notifier and should not be built.

Measurement first, and gated

Instrument before redesigning. A two-week miss log runs before the demand store is built — if the misses come back empty, the rest should not be built at all.

Append-only, one table

A count yields a number; the log yields the sequence — surfaced 08:12, deferred 08:13, deferred 12:40, expired. That cannot be reconstructed from counters afterwards. One table, not two, so the push→open→act chain is answerable.

No dashboard

The numbers land in the weekly review. A dashboard would be a surface for numbers Alex will not read, inside an app he does not open — the exact failure this cycle exists to correct.

Architecture

One append-only D1 table written from three places — the PWA beacon, the push helper, and the board's own actions — plus a nightly question on the VM and a weekly rollup in the vault. Nothing new is read by the app itself.

migrations/025_usage_events.sql
The behavioural log: at, surface, action, subject_type, subject_id, detail. Never updated, only appended.
src/db.usage.ts
recordUsageEvent, countByAction for the rollup, and a 365-day pruneUsageEvents on the existing cron tick.
src/routes/events.ts
POST /events — the beacon endpoint. Batch-capped at 50; an unknown action is skipped, not rejected, so a stale client never fails the batch.
web/src/lib/api.ts + +layout.svelte
Fire-and-forget beacon for opens and route views. Never throws — losing an event beats degrading the app to measure it.
src/webpush.ts
nudgeId on PushMessage; notify mirrors a push_sent row and stamps ?n= onto the deep link so a tap is attributable.
jobs/eod-reflect.sh (VM)
The miss question — *did anything land today Mallory should have surfaced and didn't?* — with a misses_drafted guard that fails on an empty section.
Obsidian vault · /review
The weekly rollup: opens, views, agent turns, pushes sent and tapped, misses, and Slack notification volume self-reported.

Risks & guardrails

A net that never fired

Demoting breach pushes removes the only safety net for the ~12 items that did age. Accepted with eyes open — a net that has never fired across 142 items is not a net — but it is the decision most likely to need reversing.

An empty miss log looks like a broken one

The estate's known failure mode: eod-reflect recorded ok on Goose's startup banner for three nights because the guard tested for a heading the template always emits. The miss log must answer None. literally, and the guard has a negative case proving it bites.

Measurement degrading the thing measured

The beacon is fire-and-forget and swallows every error; the push_sent mirror is wrapped so it can never block a push. Instrumentation must never be the reason something fails.

Documented state ≠ actual state

Three times now the docs have described a state the code never reached — triage: 'off', the calendar endpoint with no caller, and nudges still being read by GET /updates despite being "demoted". Both plan deviations came from re-reading the code rather than the spec.

The Slack precondition slipping

If the real-time notifications stay on, Mallory always loses on speed and the interrupt budget is noise. The weekly review carries a self-reported Slack volume specifically so the precondition is visible rather than assumed.

Delivery

The log

Migration 025, db.usage.ts behind unit tests, POST /events, and the retention prune folded into the cron tick that already runs pruneUpdates.

The beacon

postEvent in the API client; open on mount, view on afterNavigate, and push_tapped read from the ?n= stamp on load.

Push attribution

stampNudgeUrl behind tests, then notify mirrors the send and ships the stamped link. Attribution by URL, not a service-worker fetch whose Access cookie may not ride along.

The miss log (VM)

The question appended to the reflection prompt, plus misses_drafted and two new cases in the existing bash test — one passing on None., one failing on blank.

The weekly rollup (vault)

Counts queried from D1 and messages, misses counted from the week's daily notes, Slack volume typed by hand.

The two-week stop

A hard gate, not a pause. Nothing downstream is planned or built until the miss log has produced evidence — including the demands table itself.

Out of scope

The Slack act-vs-catch-up digest — the replacement for the retired per-item triage, and its own cycleInterrupt delivery and the calendar ingest caller it depends onDay-shaping and the focus timer (client-only today, so it records nothing about where the day went)The `demands` table itself — gated behind the two-week observation windowAny analytics dashboard surfaceDaily-note integration — parked as a distraction from the demand model
spec · docs/superpowers/specs/2026-08-01-demand-board-measurement-design.md
plan · docs/superpowers/plans/2026-08-02-demand-board-measurement.md
Mallory · Estate briefing