Repository navigation
fix(ui-server): commit authentication's per-request principal writes on the write worker - #2992
Merged
Merged
Conversation
…on the write worker Authentication no longer commits a principal row on the app connection on the request path (#2850). A principal's last_seen_at touch, terminal-principal pruning, ambient principal resolution and a login's new principal commit on the ui.db write worker at synchronous=FULL, through the synchronous writers in principals.ts run on the write connection (db/principal-writes.ts). A touch is never waited for, and one pending is not submitted again; an ambient principal that is current is read at once; a login answers once its principal has committed, and its refusals keep their classes across the worker. Revocations and administrative principal writes still commit on the app connection. Refs #2850 Co-Authored-By: Claude Opus 5.5 <[email protected]>
The upgrade resolves its ambient principal on the write worker (#2850); expect().resolves waits in a nested loop that never delivers the worker's reply (docs/process/testing.md). Refs #2850 Co-Authored-By: Claude Opus 5.5 <[email protected]>
Authentication prunes through principalWrites(db).pruneIfDue now, so prunePrincipalsIfDue and its per-database map have no caller. Refs #2850 Co-Authored-By: Claude Opus 5.5 <[email protected]>
Refs #2850 Co-Authored-By: Claude Opus 5.5 <[email protected]>
schlessera
marked this pull request as ready for review
October 26, 2026 07:13
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
Fifth slice of #2850: authentication's per-request principal writes (group 3, "auth sessions").
What changed
Authentication no longer commits a principal row on the app connection on the request path. Four writes now commit on the
ui.dbwrite worker atsynchronous=FULL:last_seen_attouch on a signed-in request;none,tailscaleandproxymodes and on WebSocket upgrades;The procedures are the synchronous writers in
principals.ts, run on the write connection (packages/ui-server/src/db/principal-writes.ts). The app registers them for its database (registerPrincipalWrites). Without a registration (tests, the cron runner) the same writers run inline.What changes for a caller:
PrincipalLimitError,PrincipalCredentialNotFoundError,PrincipalCreatorNotUsableError) keep their classes across the worker.Revocations and administrative principal writes (agent creation, rename) still commit on the app connection. A revocation must be in force for every request from the moment it returns, so it moves with an overlay in a later slice.
Why this shape
authGuard,isWsAuthorizedandissueLoginSessionkeep their signatures, and so do the many tests that call them. The writer is looked up per database, as the per-database prune throttle it replaces was (the unusedprunePrincipalsIfDueis removed). The alternative, threading a writer through every auth entry point, would have touched every auth test without changing behaviour.Proof
Head
c31daf063: the change6f34086a2, a merge oforigin/main4c97b68dc(#2916), one test-only commit, one dead-code removal (prunePrincipalsIfDue, no caller left), the re-pointedhttp-websocket-admissioncitations indocs/http-api-coverage.md(5dcc26ced), and a merge oforigin/main2d4671ac1(#2985, #2983; no conflicts). Every heavy run went through the host slot wrapper.bun run check:pr --base origin/mainonc31daf063bun run test ./testsonc31daf063bun run test packages/ui-serveronc31daf063886211a91failed 4http-websocket-admissionreal-upgrade cases: they asserted withexpect(...).resolves, whose nested loop never delivers the worker's reply now that the upgrade resolves its ambient principal there, the trap indocs/process/testing.md. The test-only commit awaits plainly.)8f89d7046:principal-writes-offloopplus the 13 auth, principal, cookie, passkey, login and WebSocket filestsc --noEmit,bun run lint,decision-citationsonc31daf063prove-native-host.ts, all four cells, underflockon the fixture lock,dd … conv=fsyncwriters outside the lock) on886211a91Focused tests (
packages/ui-server/tests/principal-writes-offloop.test.ts, new). The app connection refuses writes toprincipals; the worker refuses any below FULL. The cases:authGuard) and login commit on the worker.resolveCookiePrincipalis answered without waiting for its touch.Mutations (each applied, its named test run, restored)
APP_CONNECTION_WRITE principalsSQLITE_WRITE_BELOW_FULLPRINCIPAL_PENDING_TOUCH_SUBMITTED_ONCEPRINCIPAL_COOKIE_REQUEST_DOES_NOT_WAIT_FOR_TOUCHPRINCIPAL_LOGIN_REFUSAL_CLASS_KEPTAPP_CONNECTION_WRITE principalsP4 and P6 survived the first pass: no test drove the cookie path, and the guard's principal already existed. The cookie case and a reordered guard case now catch them. A duplicate touch at the same instant matches no row in SQL, so the dedupe case submits the second touch 61 s later.
Lag attribution (#2416 method with the turn wait joined, as on #2884–#2949; diagnostic not committed). Native pi UID 1000 cell, 3 fsync writers, 3 runs each.
origin/main2577504d2): 5 windows ≥ 250 ms afterready.last_seen_attouch: 287 ms, its own commit (principals.ts:365←auth.ts:438).principals.ts: 5 (max 402 ms).principals.tsat all. One window remains afterready, 361 ms. It overlaps only a workerupsertSessioncommit, with no app-connection turn, so it is recorded as unattributed.Checklist
.changeset/principal-writes-off-event-loop.md)principal-writes.tsis 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
Still on the app connection:
readyin the before-runs here);history-store,governed-manifest, schedules, inbox and settings.🤖 Generated with Claude Code