Repository navigation
fix(ui-server): commit governance events and turn scope on the write worker - #2884
Merged
Merged
Conversation
…worker Governance events raised outside a journal transaction, the session's first taint, roadblock classification, the journal's steering replay and every turn_scope row now commit on the ui.db write worker at synchronous=FULL, in submission order with journal and Activity writes, instead of on the event loop (#2850). Events are validated and their clock taken where they are raised. Reads that followed those writes wait for them: a connection's first byte waits for its taint's commit while admission counts a pending taint; a scope decision runs against the committed row and settles after its own writes; a turn's outcome waits for its egress records; the scope summary is broadcast once its row commits. Refs #2850 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This was referenced Oct 25, 2026
…cope-offloop-044 # Conflicts: # docs/decisions/governed-writable-workers.md # docs/decisions/session-principals.md # docs/decisions/voice-permission.md # docs/plans/async-collaboration-requirements.md # docs/plans/async-collaboration.md # docs/plans/navigation-recovery.md
…cope-offloop-044 # Conflicts: # docs/decisions/governed-writable-workers.md # docs/decisions/managed-worktrees.md # docs/decisions/scheduled-tasks.md # docs/decisions/session-principals.md # docs/decisions/voice-permission.md # docs/plans/async-collaboration-requirements.md # docs/plans/async-collaboration.md # docs/plans/navigation-recovery.md # packages/ui-server/src/inbox/autonomous-turn.ts
…cope-offloop-044 # Conflicts: # docs/decisions/governed-writable-workers.md
schlessera
marked this pull request as ready for review
October 25, 2026 08:09
schlessera
added a commit
that referenced
this pull request
Oct 25, 2026
Layer 2 of #2850, re-parented onto main after #2884's squash merge. The tree is byte-identical to the proven head 50354cf. Co-Authored-By: Claude Opus 5.5 <[email protected]>
schlessera
added a commit
that referenced
this pull request
Oct 25, 2026
Layer 2 of #2850, re-parented onto main after #2884's squash merge. The tree is byte-identical to the proven head 50354cf. Co-authored-by: Claude Opus 5.5 <[email protected]>
This was referenced Oct 25, 2026
Merged
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
First slice of #2850 (option 3 from #2814): the governance-event and turn-scope writers.
What changed
The hosted server no longer commits a governance event or a turn-scope row on its app connection. Every governance event raised outside a journal transaction (the Activity runtime's events, egress records, scope caps, bridge and catalog events, policy changes, revert conflicts, scratch overage), the session's first taint, roadblock classification, the journal's startup steering replay and every
turn_scopeinsert, save, end and restart sweep now commit on theui.dbwrite worker from #2830, atsynchronous=FULL, in submission order with the journal's and Activity's writes. Each event is validated, and its clock taken, where it is raised, so an invalid event still throws at its call site. The transaction bodies are the ones that ran inline (insertGovernanceEvent,insertFirstTaint,recordTurnRoadblocks, the scope statements); events the journal records inside its own transactions stay there.Four reads used to follow those writes on the same connection, so each now waits for what it read:
admitted, so a connection's first upstream byte still waits for its taint's commit. Admission counts a taint from the moment it is submitted (GovernanceWrites.taint), so a second connection during that commit is narrowed as before, and the command policy's live taint read uses the same view.TurnScope.decideis now async. It waits until the turn's earlier scope writes have committed, decides against the committed row with nothing awaited between that check and the decision, and settles after its own writes, so the summarydispatch.tssends next reads them.changed()runs once the saved row commits, because the journal summary it is broadcast with reads that row.prepareTurnEgress(...).settled(), so a failed record still fails the turn and contacted hosts are visible to the next turn.A process without a write worker (the cron runner, tests on
:memory:) runs the same procedures on its own connection behind the same promises (procedureCaller).Why this shape
It follows #2830's model: writers move as procedure modules (
governance/event-writes.ts,brain/turn-scope-writes.ts), not as replayed SQL, so reads inside each transaction still happen against the state it commits. The alternative for scope decisions was reading the live in-memory summary instead of the row;turn-scope-headroom.test.ts("refuses a tampered ceiling") showed the row read is deliberate, so the decision waits for the commit instead. Governance and scope ship together because scope caps recordscope_capevents and restart roadblocks, and the ordering test covers both.The new #2850 finding (an auth-session write in
principals.tswaiting 384 ms behind a workerbindModel) is in scope as its own group: once that writer is on the worker queue, it waits there instead of on the loop, without reworking the journal ledger's locks. The decision record now says so.Proof
Refresh onto
main. Mergedorigin/mainthree times while proof ran (no force-push):e03c3c68c,5eb26d1dcand then6e3d41723(onto6814f2ce5). Conflicts were doc citations, plus imports ininbox/autonomous-turn.ts(this PR's governance writes beside main's newproviderDeniedTypes). Citations were re-pointed after each merge. On the final head,tsc --noEmit,bun run lintanddecision-citations44/44 pass. Per the stack rule, the full re-proof ran on #2896's top, which contains this branch: see #2896.Head
365e82960, one commit onb192ca0c0(#2830 merged). Receipts are local; draft CI covers the rest.Local gates
bun run check:pr --base origin/mainbun scripts/check-packages.ts(pack smoke; new worker-loaded modules resolve fromdist/)tsc --noEmit,bun run lint,tests/decision-citations.test.tsbun run test packages/ui-server packages/ui-sdkhttp-supported-mounts("a dirty failed mount discards its resources…") andgovernance-policy("a pre-existing unpinned peer…"), the load-sensitive pair #2830 also recorded. Rerun alone: 131/131 on this head and 131/131 onorigin/main.bun run test packages/ui-backend-claude packages/ui-backend-piprovider-history.test.tsbun run test ./testsintegration-command(1 + theposix_spawn 'git'error) andnative-diagram-inputs(4) fail identically onorigin/main. The 4policy-write-boundarycases pass when rerun; that file fails 3 other cases on bothorigin/mainand this head in the same rerun, so it is environmental here.Focused tests (
packages/ui-server/tests/app-writes-offloop.test.ts, new;turn-egress.test.ts, two new cases). Temporary triggers make the app connection refuse every write to the moved tables (APP_CONNECTION_WRITE), and a probe on the worker connection refuses any write below FULL and logs commit order. They cover: governance events, first taint, binding, roadblocks and the Activity runtime's events commit on the worker; a pending taint counts for admission; scope rows and their cap event commit in submission order after an Activity write;changed()fires after the commit; a connection's first byte waits behind a held worker commit for its taint while a second host is narrowed;settled()surfaces a late record failure; the recorder'ssettled()waits for its roadblock commit.Mutations (each applied on this head, its named tests run, restored; logs kept locally)
APP_CONNECTION_WRITE governance_eventsSQLITE_WRITE_BELOW_FULLAPP_CONNECTION_WRITE turn_scopeSCOPE_CAP_EVENT_BEFORE_ITS_SUMMARY: the refused save is missing from the commit log)SCOPE_CHANGED_AFTER_COMMITSCOPE_CAP_EVENT_BEFORE_ITS_SUMMARYGOVERNANCE_PENDING_TAINT_IS_IN_FORCE; the egress case fails tooTURN_EGRESS_BYTES_AFTER_TAINT_COMMITsettled()does not wait for egress recordsTURN_EGRESS_LATE_RECORD_FAILURE_IS_FINAL_AFTER_SETTLED,TURN_EGRESS_RECORDS_COMMITTED_BY_SETTLEDws-scope-caps.test.ts, both adapters:SCOPE_ADAPTER_FIRST_GRANT_AND_DUPLICATE_ACCEPTEDsettled()does not wait for its roadblocksRECORDER_SETTLED_WAITS_FOR_ROADBLOCKSTest changes that follow the async writes.
ws-main-model.test.tswaits for the governance row to commit before reading it; the row's model is still taken when the event is raised, soMODEL_LIVE_EARLY_UNKNOWNasserts the same thing.ws-scope-caps.test.tswaits for the pause summary that counts every call seen so far: each summary now reaches the client once its row commits, so the backlogged one arrives after the first rather than in the same tick; the assertions are unchanged.interactive-share.test.tsmoves its bridge-promise assertions from.resolves/.rejectsto plainawaitandexpectRejection. A pending worker write inside Bun 1.4.2's nestedexpectloop never settled (the trap indocs/process/testing.md). Theturn-scope*tests await the now-asyncdecide.command-taint.test.tsis unchanged. Twoui-sdktests that copyscratch-prune.tsrewrite its new import path.Native hosted proof (
prove-native-host.ts, all four cells, underflockon the fixture lock,dd … conv=fsyncwriters outside the lock), on a tree identical to this head:schedule Git publication inherits active native peer folder admission:verifyWorkerFolderMountsStepsread "invalid worker mount record" (ui-sdk launcher, not changed here)origin/main, 3 fsync writersHOSTED_ADMISSION_HEALTH_WITHIN_1_SECONDLag attribution (the #2416 method with the turn wait joined, as on #2814: 20 ms timer-drift probe,
bun --cpu-profat 500 µs, every app-connection turn > 20 ms logged with its call site and every worker call > 20 ms; diagnostic not committed). Native pi UID 1000 cell, 3 fsync writers.origin/mainb192ca0c0, 2 runs, plus 3 runs on CONTRACT: fix(ui-server): commit journal, Activity and native-lease SQLite off the event loop #2830's head633663dab). Windows ≥ 250 ms afterreadyattributed to this PR's writers: the Activity runtime's governance event (451 ms, own commit), turn-scopesave(404 ms, own commit) and the first taint (317 ms, waiting 331 ms behind a workerappendEvent). Over the twoorigin/mainruns, 26 app-connection turns > 20 ms were these writers (turn-scope.ts×22,session-taint.ts×2,activity/runtime.ts×4).governance/events.ts,session-taint.ts,activity/runtime.tsorturn-scope.tsat all. The windows ≥ 250 ms left afterreadyaresession-work.tsvia the session catalog (299 ms) and the session catalog's own commit (254 ms), which are later groups of this issue.Changes that are not writes
TurnScope.decideandbeginare async,prepareTurnEgressgainedsettled(), and the egress proxy'sadmittedhook may return a promise. All of these are internal, sodocs/integration-contract/is unchanged.Checklist
.changeset/governance-scope-off-event-loop.md)@internalor under/internal.)docs/reflect the change, or nothing documented moved. (docs/decisions/sqlite-write-worker.md, taint citations, shifted code citations.)docs/decisions/provider-history.md): the provider-history guard passes, or nothing on a turn's path changed.Anything left open
Still on the app connection, in the order the attribution suggests: turn budgets (
observeatrun-session.ts:676, up to 366 ms before), session catalog with message sources and session work, the writer gate, auth sessions (principals.ts, the #2850 finding), the Activity notifier and push ticks, and the rest the inventory found (storage/history-store.ts,governed-manifest.ts,session-profiles.ts, schedules, inbox, settings). Each is a following PR on this issue.🤖 Generated with Claude Code