Repository navigation
fix(ui-server): move storage scans to the write worker and guard against app-connection writes - #3029
Merged
Merged
Conversation
schlessera
marked this pull request as ready for review
October 26, 2026 19:17
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.
Closes #3004
Closes #3028
Refs #2850
This is the final slice of epic #2850. It moves the last hosted-path
ui.dbwriters to the write worker (#3028), proves by inventory that none remain on the app connection, and adds a guard that fails the hosted test suites if one returns.What changed
The last writers (#3028). The inventory found one group no sub-issue owned: shared storage scan leases and storage maintenance operations. #2997 had left them out. Their transactions now run verbatim on the worker (
packages/ui-server/src/storage/shared-scan-writes.ts,packages/ui-server/src/storage/maintenance-writes.ts):gpt-6-astra drafted this part; I reviewed it and added the mutation receipts.
Observing the app connection (
observeAppWrites,markAppPhase,packages/ui-server/src/db/write-worker.ts).takeTurnsalready wraps every outermost app-connection transaction and autocommit write. When an observer is registered, it compares SQLite'stotal_changes()before and after each one, and reports any turn that changed rows, with its stack and the app's phase. The phase isbootwhilecreateAppbuilds the app,servingonce it returns, andstandalonefor a component built outside it. With no observer registered, nothing is measured. The API is not exported from any package entry, so the API report is unchanged.The inventory (
scripts/app-write-audit-preload.ts,scripts/app-write-inventory.ts). The preload records every observed write while a suite runs, and the script groups them by the nearest server frame. A stack that holds a test frame counts as a fixture: a test's own write, or a product call a test made directly on the app's database to seed it. The method, the result and the named exceptions are indocs/decisions/sqlite-write-worker.md("The inventory and its guard").The guard (
guardAppWrites,packages/ui-server/tests/helpers/app-write-guard.ts).httpContractAppregisters it on every hosted test app once the app serves. It records each app-connection write reached from server code alone, and the helper'sclose()fails the test withAPP_CONNECTION_WRITE_WHILE_SERVING, naming the site. The 28 route and mount suites built on that helper are now the guard.Inventory result
Run over the ui-server suite and
./testsona1dcc134e(2,452 observed writes; full table under Proof). The serving phase holds only three kinds of site, none of them a hosted path:INBOX_DISPATCH_FIXTURE.createAppsupplies no production dispatcher (packages/ui-server/src/inbox/runtime.ts:29).ws-turn-budgets.test.ts:77builds budgets on the app's database without its writer.Boot holds only the schedule root's first insert (
rootInfo). Standalone components (the writer gate, turn scope and reverts built by tests outsidecreateApp) take the app's writer when hosted.Named exceptions:
rootInfo.Proof
Head
791624100. It contains main2f10e0407(#3024): the #3028 slice, merged into this branch, then the observer, guard, inventory and docs. Every heavy run went through the host slot wrapper. The suites, native proof and attribution ran ona1dcc134e, which is this head minus the docs commit that records the inventory.check:prran again on this head.bun run check:pr --base origin/mainon791624100(and ona1dcc134e)bun run test packages/ui-server, with the guard active in everyhttpContractAppsuite and the inventory preloadbun run test ./tests, with the inventory preloadtsc --noEmit,bun run lint,decision-citations,api-surfaceprove-native-host.ts, all four cells, underflockon the fixture lock,dd … conv=fsyncwriters outside the lock)7e3ab6a1b: native proof, no load and 3 fsync writersFailing first. The guard first ran over the ui-server suite on main
2f10e0407, before #3028 was merged into this branch. 110 hosted-suite tests failed withAPP_CONNECTION_WRITE_WHILE_SERVING, namingstorage/shared-scan.ts(register,unregister,reap),storage/coordinator.ts(start, finish) andstorage/maintenance.ts(write,start). That run also showed two classification problems, both fixed before this head:app.db(roadblock seeding,rollupRun,revokePrincipal) had been counted as hosted;Mutations (each applied, its named test run, restored):
APP_WRITE_GUARD_REFUSES_SERVING_WRITEAPP_WRITE_GUARD_REFUSES_SERVING_WRITEAPP_WRITE_GUARD_REFUSES_SERVING_WRITEAPP_WRITE_GUARD_REFUSES_SERVING_WRITEAPP_WRITE_GUARD_FIXTURE_AND_BOOT_WRITES_ALLOWEDAPP_CONNECTION_WRITE storage_scan_leasesSTORAGE_SCAN_START_WAITS_FOR_LEASESTORAGE_MAINTENANCE_ROUTE_WAITS_FOR_RECEIPT: the inline insert is refused on the app connection, so no receipt is answeredK3 (collector registration not awaited) survived as an equivalent mutant. The collector process runs from spawn either way, and the worker FIFO keeps the awaited
unregisterbehindregister, so no observable state differs.Native lag, after every slice (#2416 method with the turn wait joined; diagnostic not committed). Native pi UID 1000 cell, 3
dd … conv=fsyncwriters, 3 runs ona1dcc134e:readyper run set, all from app-connection writers. They came from the notifier tick (up to 819 ms), governed-admission records (472 ms), turn scope, the session catalog and the writer gate (receipts on fix(ui-server): commit governance events and turn scope on the write worker #2884–fix(ui-server): commit reference sources and managed worktrees on the write worker #3026).readyremains: 339 ms in run 1. It overlaps only a workerupsertSessioncommit, with no app-connection turn, so it is recorded as unattributed. Every other window falls beforeready, during boot.Checklist
.changeset/storage-scans-off-event-loop.mdfor ui-server: storage scan leases and maintenance operations commit on the write worker #3028; the observer, guard and scripts change nothing a consumer sees.)api-report/ui-server.txtis unchanged.)docs/reflect the change, or nothing documented moved. (docs/decisions/sqlite-write-worker.md; shifted citations.)docs/decisions/provider-history.md): the provider-history guard passes, or nothing on a turn's path changed.Anything left open
Nothing for #3004 or #3028. Epic #2850's definition of done is for the coordinator to check after this merges. All of the epic's writer sub-issues are closed or close here. The inventory and the native receipts are recorded above and in the decision doc, and that doc no longer lists any "other app-connection writers".
🤖 Generated with Claude Code