Skip to content

fix(ui-server): commit local exchanges, classified blocks and saved worker preferences on the write worker - #3011

Merged
schlessera merged 15 commits into
mainfrom
fix/2850-session-leftovers-offloop-044
Oct 26, 2026
Merged

schlessera merged 15 commits into
mainfrom
fix/2850-session-leftovers-offloop-044

Conversation

@schlessera

Copy link
Copy Markdown
Owner

Closes #2996
Refs #2850

Eighth slice of #2850: local exchanges, a turn's classified blocks and classifier confidence, and a saved worker preference.

What changed

The hosted server no longer commits these on its app connection. They now commit on the ui.db write worker at synchronous=FULL, each the statement or transaction that ran inline:

  • Local exchanges (a command the client answered itself, stored against the session), and the record that the next prompt carried them. These go through the session catalog's write procedures (packages/ui-server/src/ws/session-catalog-writes.ts).
  • A turn's classified blocks, the blocks that follow a promoted link, and the classifier's confidence (packages/ui-server/src/classification/classification-writes.ts).
  • A saved worker preference (PUT …/profile): checked and stored in one immediate transaction (saveProfile, packages/ui-server/src/ws/session-profile-writes.ts).

What callers wait for:

  • Local exchange: the client is answered once the exchange is stored. Until it is, the next prompt handed over still takes it, exactly once. The catalog keeps exchanges submitted but not yet committed, and claims the ones a prompt took. The hand-over is recorded on the worker after the exchange's own record.
  • Classification: a pass settles once its blocks have committed. A replayed history (WsHost.historySettled()) waits for every classifier write submitted before it, as it already did for the catalog and policy proposals.
  • Saved preference: the route answers once the row has committed, reading it back as before. Its refusals (SessionProfileError with status 401, 404, 409 or 507) keep their class and status across the worker.

Retained retries, drafts, policy proposal inserts and saves, voice transcription claims and settings still commit on the app connection. They are the next slices.

Why this shape

A local exchange is recorded on one message and carried by the next prompt. Committing it later must not let a prompt miss it or carry it twice. So the catalog keeps the pending exchange in memory and serves it to the next prompt, the way it already does for other submitted catalog writes. The saved preference's checks (session known, origin, policy profile, draft cap) read the rows the write depends on. They run inside the same immediate transaction on the worker. The brain's policy profile per origin is resolved where the request arrived and passed in as data.

Proof

Head a907f62a0. The work commits e8080257d…4bf8cc0ff; merges of origin/main 71503546f (#2992), e69ade405 (#2994) and c47483196 (#3005, #3006), with doc conflicts only, resolved; and a docs-only commit (a907f62a0) that removes a duplicated "other writers" paragraph. Every heavy run went through the host slot wrapper. The suites ran on 7d55f72fc, which is this head minus that docs commit. check:pr ran again on this head.

Gate Result
bun run check:pr --base origin/main on a907f62a0 (and on 7d55f72fc) passed (local preflight)
bun run test ./tests on 7d55f72fc 1966 pass, 0 fail
bun run test packages/ui-server on 7d55f72fc 4086 pass, 2 fail. Both failures are the ws-turn-budgets host-token-fallback cases, filed as #3009. That timing race also fails on main: 2 of 22 alone on c47483196, against 6 of 33 on this branch; the gap is not significant at these counts. Nothing in this branch touches budgets.
focused: app-writes-offloop, classification, ws-local-exchange, the retry/turn-failure replays, dispatch, governance-policy, turn-lifecycle, api-surface 218/218
tsc --noEmit, bun run lint, decision-citations clean; 44/44
native hosted proof (prove-native-host.ts, all four cells, under flock on the fixture lock, dd … conv=fsync writers outside the lock) on 7d55f72fc no load: passed; 3 fsync writers: passed (both with the deliberate retained-index-namespace failure verified)

An earlier ui-server run, on fcb02e5ea, failed one saved-transcription timing case (a fixed 20 ms wait under host load). It passed 3/3 alone and in the run above.

Focused tests. The app connection refuses writes to the watched tables. The worker refuses any commit below FULL.

  • app-writes-offloop.test.ts:
    • A local exchange commits on the worker. While a slow commit holds the queue it is not on disk, the next prompt still takes it once, it is marked carried after its record, and it is never taken again.
    • A saved preference commits on the worker for a session and a draft, and a refusal keeps its class and status.
    • A replayed history waits for pending classified blocks.
  • classification.test.ts: with a write worker, a pass commits its blocks and confidence there, settles once its blocks have, and logs no write failure.

Mutations (each applied, its named test run, restored)

# Mutation Fails
L1 a local exchange commits on the app connection APP_CONNECTION_WRITE local_exchanges
L2 a pending exchange is not taken by the next prompt EXCHANGE_PENDING_TAKEN_BY_NEXT_PROMPT
L3 a pending exchange is taken twice EXCHANGE_TAKEN_ONCE
K1 classified blocks and confidence commit on the app connection CLASSIFY_WORKER_WRITES_DID_NOT_FAIL (APP_CONNECTION_WRITE classification_confidence, … message_blocks)
K2 a pass settles before its blocks commit CLASSIFY_BLOCKS_COMMITTED_BEFORE_RUN_SETTLES
K3 classified blocks commit below FULL CLASSIFY_WORKER_WRITES_DID_NOT_FAIL (SQLITE_WRITE_BELOW_FULL)
K4 replay does not wait for classified blocks HISTORY_WAITS_FOR_PENDING_CLASSIFIED_BLOCKS
P1 a saved preference commits on the app connection APP_CONNECTION_WRITE session_worker_profiles
P2 a preference refusal loses its class PROFILE_REFUSAL_CLASS_KEPT

K1 and K3 first failed only on a missing block: the pass turns a write failure into a log line. The test now asserts that log is empty first, so the failure names its cause.

Lag attribution (#2416 method with the turn wait joined, as on #2884–#2994; diagnostic not committed). Native pi UID 1000 cell, 3 fsync writers, 3 runs each, before (origin/main 2d4671ac1) and after (this change).

So this slice's evidence is the focused tests and mutations above, not the native cell.

Checklist

  • Focused local preflight/behavioral receipts and complete selected hosted proof passed; actual head/base/job checkout evidence is in Proof.
  • Acceptance criteria from the issue are all met, or the gap is named below. (Met for these writers; the remaining groups are listed below.)
  • The PR and completed/advanced issues have verified release milestones under docs/process/github.md.
  • A changeset is included, or this changes nothing a consumer can see. (.changeset/app-writes-chat-leftovers-off-event-loop.md)
  • No personal data anywhere in the diff — the leakage gate covers the whole tree.
  • The integration contract index or relevant component is updated in this same commit, or nothing in the machine surface moved. (Nothing moved: the API report is unchanged.)
  • Docs under docs/ reflect the change, or nothing documented moved. (docs/decisions/sqlite-write-worker.md; shifted and re-pointed citations.)
  • Provider history stays append-only (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

…overs-offloop-044

# Conflicts:
#	docs/decisions/http-api-boundary.md
#	docs/decisions/session-principals.md
#	docs/decisions/sqlite-write-worker.md
#	docs/plans/async-collaboration-requirements.md
#	docs/plans/async-collaboration.md
#	docs/plans/navigation-recovery.md
…overs-offloop-044

# Conflicts:
#	docs/decisions/sqlite-write-worker.md
…overs-offloop-044

# Conflicts:
#	docs/decisions/governed-writable-workers.md
#	docs/decisions/http-api-boundary.md
#	docs/decisions/session-principals.md
#	docs/decisions/sqlite-write-worker.md
#	docs/plans/async-collaboration-requirements.md
#	docs/plans/async-collaboration.md
#	docs/plans/navigation-recovery.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ui-server: local exchanges, classified blocks and saved worker preferences commit on the write worker

1 participant