Repository navigation
fix(ui-server): commit storage history and storage notices on the write worker - #3022
Merged
Merged
Conversation
…s-offloop-044 # Conflicts: # docs/decisions/sqlite-write-worker.md
…s-offloop-044 # Conflicts: # docs/decisions/sqlite-write-worker.md # packages/ui-server/src/activity/push-sender.ts
…s-offloop-044 # Conflicts: # docs/decisions/sqlite-write-worker.md
…s-offloop-044 # Conflicts: # docs/decisions/sqlite-write-worker.md
…s-offloop-044 # Conflicts: # docs/decisions/sqlite-write-worker.md
…s-offloop-044 # Conflicts: # docs/decisions/sqlite-write-worker.md
…s-offloop-044 # Conflicts: # docs/decisions/sqlite-write-worker.md
schlessera
marked this pull request as ready for review
October 26, 2026 16:17
This was referenced Oct 26, 2026
Open
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.
Closes #2997
Refs #2850
Ninth slice of #2850: storage history and storage notices, the 20-second storage tick's writes.
What changed
The hosted server no longer commits these on its app connection. They now commit on the
ui.dbwrite worker atsynchronous=FULL, each the transaction that ran inline, moved verbatim:packages/ui-server/src/storage/history-writes.ts);beginand itsfinish(packages/ui-server/src/storage/notify-writes.ts).The history's types, schemas and reads moved to
history-data.ts, so the worker module can load them without the store.history-store.tsre-exports them.What callers wait for:
close()awaits the final flush, and so does app shutdown.history.status()) from where it was submitted. It reads the durable history under its own transaction, so a newer observation another process committed while it waited still counts./api/storage/historyand/api/storage/guidanceread once every flush submitted before them has committed (StorageHistory.settled()).deliverStorageawaits begin and finish.Without a writer (the cron runner, tests, fixtures) the same transactions run inline and synchronously, as before. Operational restore still writes inside its caller's transaction. The storage coordinator, maintenance, removal and the shared scan still commit on the app connection.
Why this shape
A flush reads the stored history, merges the buffered evidence and writes it back. That has to be one transaction where it commits, so the buffers travel to the worker as data, and the result is applied back only if no newer evidence arrived meanwhile. A delivery's
begindecides from durable state that another process may change. So the durable read stays inside its transaction, and only what lives in this process is captured at submission.The first implementation was drafted by gpt-6-astra; I reviewed it and added two fixes it missed:
beginon the worker had decided from the history snapshot taken at submission.storage-guidance.test.ts's mounted history case caught that one.Proof
Head
aadfc2ca4. The change (drafted by gpt-6-astra, reviewed and fixed here; see "Why this shape"), then merges oforigin/mainup tofd51cadf4(#3015), with doc conflicts only. The full suites and native proof ran ond37185754, which contains mainb99489b3c(#3014). The last merge brought in #3015 (schedules:app.tswiring and this decision doc); on this head I rerancheck:pr, typecheck, lint, citations and the focused tests. Every heavy run went through the host slot wrapper.bun run check:pr --base origin/mainonaadfc2ca4(and ond37185754)bun run test ./testsond37185754bun run test packages/ui-serverond37185754close-cancels-shared-runstiming out waiting for the fixture'sstatsread to start, the open #2866. The app there uses an in-memory database. Alone the file failed once in 7 runs on this branch and 0 in 6 on main, all under host load.aadfc2ca4: everystorage-*file,storage-offloop,app-writes-offloop, push,notify-offloop,inbox-notify, schedulestsc --noEmit,bun run lint,decision-citations,api-surfaceonaadfc2ca4prove-native-host.ts, all four cells, underflockon the fixture lock,dd … conv=fsyncwriters outside the lock) ond37185754Focused tests (
packages/ui-server/tests/storage-offloop.test.ts, new; a real file-backedui.dband write worker). The app connection refuses writes tostorage_historyandstorage_notice_receipts, and the worker refuses any commit below FULL. The cases:close()awaits the final flush.beginreturns the same shape as inlinebeginon the same inputs.begindecides on the durable history at its commit: another process's newer observation, committed while it waited, refuses it.Mutations (each applied, its named test run, restored)
STORAGE_INITIALIZE_COMMITS_ON_WORKER: no worker commit. The inline insert hit the app-connection refusal, which the store records ashistory-write-unavailableSTORAGE_FLUSH_COMMITS_ON_WORKER: the worker refused the commit (SQLITE_WRITE_BELOW_FULL), which the store turns into a failed flushSTORAGE_FLUSH_REQUESTS_COALESCEDAPP_CONNECTION_WRITE storage_notice_receiptsSQLITE_WRITE_BELOW_FULLSTORAGE_TICK_NEVER_OVERLAPS_FLUSHbegindecides on the submitted historySTORAGE_QUEUED_BEGIN_READS_DURABLE_HISTORY_AT_COMMITstorage-guidance:observationshas length 0, expected 3S4 (reporting a flush in flight as pending) survived as an equivalent mutant. The buffered observation stays set until its commit applies, so the extra in-flight term never changed the answer, and I removed it. S8's test failed first on the drafted code. S9's test is the existing
storage-guidancecase, which failed on the drafted code before the views waited.Lag attribution (#2416 method with the turn wait joined; diagnostic not committed). Native pi UID 1000 cell, 3 fsync writers, 3 runs each.
origin/main71503546f): the only storage app-connection turn was the boot-time history insert (history-store.ts:58←app.ts, 26 ms). No window ≥ 250 ms afterreadycame from storage.ready(338 ms) was the Activity notifier's tick, on a head before fix(ui-server): commit the Activity notifier and push bookkeeping on the write worker #2994 moved it.The cell is shorter than a few sampling ticks, so it barely exercises this group. The focused tests carry the evidence.
Checklist
.changeset/storage-ticks-off-event-loop.md)docs/reflect the change, or nothing documented moved. (docs/decisions/sqlite-write-worker.md.)docs/decisions/provider-history.md): the provider-history guard passes, or nothing on a turn's path changed.Anything left open
The remaining groups are the open sub-issues of epic #2850.
🤖 Generated with Claude Code