Repository navigation
fix(server): fast session-webhook ack (Toolyard callbacks no longer time out) - #318
Conversation
Session webhook receive: the admission transaction called the Toolyard integration's owner check, which reads environment users through Effect.runPromise on a fresh fiber. The SQLite client has one connection behind a one-permit semaphore held by the open transaction, so that read waited for the transaction that waited for it. Every push callback hung until Toolyard's 20 s client timeout, the request was interrupted and the insert rolled back. The transaction now runs SQL only; the binding is checked before it and again by the dispatch loop. Upstream MCP proxy: a personal T3 token (t3usr_) may call /mcp/upstream/toolyard as the token's own user, with that user's managed Toolyard credential, when the server and user external MCP switches are on. No x-t3-session-id is sent for these calls. Provider sessions are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he live clock - Reuse one payload for the duplicate delivery. Under it.live a second h.event() carries a new timestamp, so the retry was a 409 content conflict instead of a duplicate. - Keep the fresh-fiber Effect.runPromise read, which replicates the production owner check, behind a reasoned oxlint suppression. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
The host relay timer that needed a personal t3usr_ token on /mcp/upstream/toolyard was cancelled (2026-10-15), so the new access path has no user. Keep only the fast session-webhook ack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged at head |
Production check (2026-10-31)Read-only count of
The network errors stop just after this merge (the last one is at 12:48 UTC, before the deploy finished). The newest delivery is on 2026-10-29 16:58 UTC. Sent by: BK-T3-Code session (toolyard audit). |
Why
B1: session webhooks time out. Toolyard sends a signed decision callback to
POST /api/session-webhooks/:webhookId. Since 2026-10-04, 536 of 542 callback attempts failed withnetwork_error. The T3 trace shows the request took 19 963 ms (http.server POST, route/api/session-webhooks/:webhookId, user agentGo-http-client/1.1, 2026-10-15 14:49Z and 14:53Z). Toolyard's sender stops after 20 s (internal/callbacks/service.go:531). The client then disconnects, T3 interrupts the request, and the transaction rolls back. Sosession_webhook_eventsstays empty and no decision reaches the session.Root cause, from the trace spans:
sessionWebhook.receiveruns the firstSELECT, twoServerSecretStore.getcalls, thensql.transaction. Inside the transaction, the twoSELECTs finish in under 1 ms, and then nothing happens until the interrupt at about 19.9 s.integration.callbackBinding(...)(SessionWebhookService.ts, old lines 434-436).callbackBindingcallscore.assertConnectionOwner→verifyUser. That function reads the user withEffect.runPromise(users.get(...))(toolyard/ToolyardIntegration.ts:176). This starts a new root fiber that does not share the transaction's connection.packages/shared/src/nodeSqliteClient.ts:262-272). The open transaction holds that permit. The new fiber waits for the permit, and the transaction waits for the new fiber. This is a deadlock. It ends only when the client disconnects.Scope
This PR first also had "B2", a personal-token route to
/mcp/upstream/toolyardfor a host relay timer. That timer was cancelled on 2026-10-15, so commit962e5f40removes B2: the PR now changes onlySessionWebhookService.tsand its test.What changed
B1 (
apps/server/src/session-webhooks/SessionWebhookService.ts)activeand thetrust_bindingis the same as the row that the handler verified before the transaction.queuedand returns 202. The existing dispatch loop (drain, every 2 s) sends it into the thread. That loop already re-checks the integration binding before dispatch (line ~570), andretireReplacedReceiverends queued events of a replaced receiver.receive, so this also fixes pulled callbacks. Before the fix, the 2 s pull timeout cut them off.SessionWebhookService.test.ts): a new harness flag makes the owner check read the database on a fresh fiber, like production. The newit.livetest receives a callback, then a duplicate, within a 2 s bound, and then drains it. With the old code this test times out.Risk and undo
integration-disabled-or-replaced) and does not deliver it. No path that delivered events before now skips a check.How I tested
CI on the current head
962e5f4009a6b1843dd340e87e766928055f6c11(B2 removed), CI run 38769006567 and Validate and deploy bkt3 38769006468: Lint, validate, Typecheck, Build, Test, Test Web, Test Server 1-6, Rust, Release Smoke, markers, Mobile Native Changes and Native fingerprint diff pass. Transfer report artifact stays queued (pre-existing, below).Nothing ran on the host (dev-server-1 runs no tests, typechecks or builds). CI ran the checks on the earlier head
e87c1a7e14144a6d390d560e5eb995c7493a8b7a(B1 and B2): CI run 38763240414 and the "Validate and deploy bkt3" run of the same head.Result per job on that head:
SessionWebhookService.test.ts: 45 tests pass. This includes the new test "acknowledges a push callback when the owner check reads the database".On the first head
6d1f3e1a, four jobs failed: Lint, validate, Check and Test Server 2. This PR caused all four.Effect.runPromise, whicht3code/no-manual-effect-runtime-in-testsrefuses in tests.it.livetest. The test calledh.event()two times. The live clock gave the second payload a new timestamp, so the second delivery was a 409 content conflict and not a duplicate.e87c1a7efixes both. The test now sends one payload two times. The fresh-fiber read stays, because it copies the production owner check, behind an oxlint suppression that gives the reason.Pre-existing, not from this PR: "Transfer report artifact" stays queued on the Blacksmith runner. The same job is also queued on other PRs, since 11:47Z and 13:48Z today (run 38729075691, run 38744996435). The aggregate
Checkneeds that job, soCheckdoes not report.bkmainhas no CI run to compare with. CI runs only on pull requests and on pushes tomain. The latestbkmain"Validate and deploy bkt3" run passed (run 38561344711).After deploy, the proof for B1 is one row in Toolyard
callback_attemptswith a 2xx status for a T3 session webhook, and the matching row in T3session_webhook_events.Merge blockers
Checkdoes not report. This is a known problem that this PR did not cause.iamtushar324/*repositories, so the Toolyard approval route cannot submit it.🤖 Generated with Claude Code