Repository navigation
fix(server): t3_get_session reads the session on the SQLite reader - #349
Conversation
getThreadDetailSnapshot, the whole-session read behind the t3_get_session MCP tool, ran its transaction on the writer. On bkt3 it held the writer, and so the event loop, for 3.4-5.2 s and caused two of the three freezes in the first 34 minutes after the 2026-10-23 deploy. The transcript import still runs first on the writer, because it can write. The session read then runs under NodeSqliteClient.readOnly on the reader worker, the same boundary as PR #344. Refs TEC-1502 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…INSERT writes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merging into
Next: validate the stagebkt3 deploy and the 🤖 Generated with Claude Code |
Stage validation (stagebkt3, merge
|
Promote stage to bkt3 (2026-10-23 evening): t3_get_session on the SQLite reader (#349)
Production proof (bkt3, 2026-10-24)This PR has been live on bkt3 since the #350 deploy at 2026-10-23 19:38:42 UTC. bkt3 now runs
Freeze rate: 0.44 per hour over 20.4 h with #344 and this PR, against 1.43 per hour in the 24 h before #344. The remaining freezes come from upstream paths. The full table and causes are in the PR #344 measurement. 🤖 Generated with Claude Code |
Refs TEC-1502
Problem
The early production check after PR #344 (evidence) found 3 freezes in the first 34 minutes after the 2026-10-23 deploy. Two of them came from the
t3_get_sessionMCP tool:T3ControlToolkit.getSession→ProjectionSnapshotQuery.getThreadDetailSnapshot→sql.transactionon the writer →readCanonicalProjection. The turn-items query took 3.4 s at 16:17:44 UTC and 3.9 s at 16:40:06 UTC. The server logged freezes of 2.8 s and 3.7 s.Agents call this tool often, because the global session protocol tells every session to call
t3_get_sessionbefore each final reply.Fix
getThreadDetailSnapshot(fork-only fileorchestration-v2/Layers/ProjectionSnapshotQuery.ts) now runs its read transaction underNodeSqliteClient.readOnly, on the SQLite reader worker:importer.ensureTranscript(threadId)still runs first, on the writer, because the v1 transcript import can write. After the first import, the importer's in-memory cache makes this check free.detail(threadId, false)and the snapshot sequence) then runs on the reader.detail(..., false)does not import again, and every query in it is a SELECT.This is the same boundary as #344. The HTTP thread snapshot route already reads on the reader through
ProjectionStore.getThreadSnapshot. Authorization, the result shape and the audit (withMcpToolAnalytics) do not change: the tool handler is unchanged.Both changed files are fork-only, so no fork markers are necessary.
Not in this PR (Manager decision for Tushar, 2026-10-23): the upstream
OrchestratorMcpService.loadCalleralso read the caller's thread on the writer (3.0 s at 16:16:19 UTC).How I tested
This host runs no tests (the dev-server compute policy). CI runs the checks on the head.
New
ProjectionSnapshotQuery.expbkt3.test.tsbuilds the real snapshot query layer on a temporary file database with reader workers. It records the connection of every SQL statement:After the merge into
stage, I check stagebkt3:t3_get_sessionspans must showdb.connection=reader, and the result must stay the same. Thebkmainpromotion waits for Tushar's go.Review
A fresh-context review of
fbddc9c085found that the runtime change is correct. Nothing inside the read-only transaction writes.snapshotSequenceand the detail come from one reader snapshot. The only caller ist3_get_session, and no caller needs read-your-writes.80e17c6f17: the test passed a plain string whereactivities[].idis a brandedEventId(TS2769). This failedBuild and Typecheck,validateandCheck. The test now usesEventId.make. The write filter also matches the import'sWITH … INSERT.event loop stalledand thegetSessionspans after the deploy. If the decode stays long, the next step is to read a window (getThreadSnapshotWindow), not the whole session. That change is outside this PR.🤖 Generated with Claude Code