Repository navigation
fix(ui-server): commit the Activity notifier and push bookkeeping on the write worker - #2994
Merged
Merged
Conversation
…the write worker The hosted server no longer commits the Activity notifier on its app connection (#2850). The notifier's tick (intent detection under its cursor and the stuck-run watchdog), intent delivery status, acknowledgement and retention, and push delivery's subscription bookkeeping (last_used_at, a gone subscription) commit on the ui.db write worker at synchronous=FULL (activity/notify-writes.ts). The tick runs the same notifier on the write connection, with who is watching taken from the Activity stream where the tick was submitted, and its log records come back to the app's log; a tick still committing is not submitted again. A push delivery pass settles only once its bookkeeping and delivery status have committed, so the next pass never resends an intent or retries a gone device. The inbox and the pending queue are still read on the app connection. Refs #2850 Co-Authored-By: Claude Opus 5.5 <[email protected]>
…face ActivityStream.watched() exists for the runtime's off-loop notifier; tag it @internal so the public API report is unchanged. Refs #2850 Co-Authored-By: Claude Opus 5.5 <[email protected]>
…loop-044 # Conflicts: # docs/decisions/sqlite-write-worker.md
schlessera
marked this pull request as ready for review
October 26, 2026 08:08
This was referenced Oct 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #2850
Sixth slice of #2850: the Activity notifier and push delivery bookkeeping (group 4, "notifier and push ticks").
What changed
The hosted server no longer commits the Activity notifier on its app connection. These now commit on the
ui.dbwrite worker atsynchronous=FULL(packages/ui-server/src/activity/notify-writes.ts):last_used_at, and dropping a gone device.The tick runs the same
createActivityNotifieron the write connection. Who is watching the run or its session is taken from the Activity stream when the tick is submitted (ActivityStream.watched()), asisWatchedanswered before. The tick's log records come back to the app's log. A tick still committing is not submitted again; the next interval runs after it. The inbox and the pending queue are still read on the app connection. Without a write worker (tests, the cron runner) the runtime keeps the synchronous notifier.What callers wait for:
Action and storage notices (their own delivery state machines), push subscribe/unsubscribe and the lazy VAPID key insert stay on the app connection for later slices.
Why this shape
The tick's detection reads what it writes: the cursor, tag dedupe and the hourly rate cap. So the whole transaction runs where it commits, rather than splitting reads from writes.
isWatchedis the one input that lives only in the app process; passing it as data keeps the worker's notifier identical.Proof
Head
b86abb765: the change3882428ed, a merge oforigin/main4c97b68dc,d5423301b(tagsActivityStream.watched()@internal, so the public API report is unchanged), and a merge oforigin/main71503546f(#2992; one doc conflict insqlite-write-worker.md, resolved by keeping both bullets). Every heavy run went through the host slot wrapper, on this head.bun run check:pr --base origin/mainbun run test ./testsbun run test packages/ui-servermaintenance-cancellation-cleanup's control case: its child run'sgit initwas killed by the case's 250 ms timeout under host load. Rerun alone on this head: 2/2 passes, twice.notify-offloop, the notifier, push and Activity files,principal-writes-offloop,http-websocket-admission,api-surface,autonomous-runtimetsc --noEmit,bun run lint,decision-citationsprove-native-host.ts, all four cells, underflockon the fixture lock,dd … conv=fsyncwriters outside the lock)An earlier
./testsrun ond5423301bfailed one timing case,autonomous-runtime's revocation abort: 3,235 ms against a 3,000 ms bound, while fsync writers ran. It passes alone and in the run above.Focused tests (
packages/ui-server/tests/notify-offloop.test.ts, new). The app connection refuses writes tonotification_intents,settingsandpush_subscriptions; the worker refuses any below FULL. The cases:Mutations (each applied, its named test run, restored)
NOTIFY_TICK_COMMITS_ON_WORKER_AT_FULL(APP_CONNECTION_WRITE notification_intents)SQLITE_WRITE_BELOW_FULLNOTIFY_TICK_COMMITS_ON_WORKER_WITH_WATCHED_SNAPSHOTNOTIFY_PENDING_TICK_NOT_RESUBMITTEDPUSH_PASS_SETTLES_AFTER_DELIVERY_COMMITSNOTIFY_TICK_LOGS_REACH_THE_APPNOTIFY_RUNTIME_TICKS_ON_WORKERN1 and N2 first failed only on the intents assertion, because the notifier swallows its own error into a "tick failed" log. That log's error is now asserted first, so the cause is named.
Lag attribution (#2416 method with the turn wait joined, as on #2884–#2992; diagnostic not committed). Native pi UID 1000 cell, 3 fsync writers, 3 runs each.
origin/main2577504d2, the runs recorded on fix(ui-server): commit authentication's per-request principal writes on the write worker #2992): the notifier tick ran on the app connection 3 times, up to 819 ms (activity/notify.ts:205←activity/runtime.ts:144), its own commit. Windows ≥ 250 ms afterreadyfrom it: 376, 810 and 350 ms.06604d3a8): no app-connection turn from the notifier or push bookkeeping at all. Windows ≥ 250 ms afterready:provider-exposure.ts:54) waiting for a workerupsertSession. That's the governed-admission group, the next PR.governed-turn.ts:301), same group.ready: the proof fixture's own setup write (hosted-app-child.ts:295), not the server.Checklist
.changeset/notifier-off-event-loop.md)ActivityNotifier,ActivityStreamandcreatePushSenderare internal.)docs/reflect the change, or nothing documented moved. (docs/decisions/sqlite-write-worker.md; shifted and re-pointed citations.)docs/decisions/provider-history.md): the provider-history guard passes, or nothing on a turn's path changed.Anything left open
Authentication's request-path writes landed in #2992. Next, as their own PRs: local exchanges, classified blocks and saved worker preferences; storage history and notices; a governed turn's admission records. Still to move after those (the inventory in the #2850 comment of 2026-10-26): retained retries, drafts, policy proposals, voice transcription claims and settings; reference sources and managed worktrees; action notices; inbox lifecycle; account and security administration (revocation, passkeys, push subscribe/unsubscribe, VAPID keys); schedules.
🤖 Generated with Claude Code