backend-agents C (plan #789): reserved meta.turnRunId/turnStatus + host detach (unmount ≠ abort run) - #790
Conversation
…iers + host detach (unmount != abort run)
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
Implemented plan #789 (backend-agents slice C). Adds reserved Verification: typecheck exit 0; full |
btipling
left a comment
There was a problem hiding this comment.
Adversarial review — PR #790
Verdict: PASS WITH NOTES
Repo: btipling/invincible
Scope: main ← plan/turns-detach · 13 files · backend-agents C (reserved meta.turnRunId/turnStatus + host detach seam)
Lenses run: L1, L2, L3, L5, L6, L7, L8 (skip: L4 — no GHA/CI/artifact files; L9 — no palette/UX chrome)
AGENTS.md read: yes (docs/feature-divide.md unmount=detach row; SECURITY.md session envelope trust)
Findings
| Sev | Lens | Finding | Break scenario | Refutation attempt | Confidence |
|---|---|---|---|---|---|
| Minor | L1 | Plan edge case + docs/feature-divide.md claim session switch / New / Clear mid-turn = detach. onSwitchSession / onNewSession / onClear still return on inflightRef.current before detachTurn() (app/harness/HarnessHost.tsx). The only tear-down that can fire while a turn is in flight is unmount. |
Start a turn. Click another rail session / New / Clear. Host no-ops; viewport stays Busy. Detach-vs-abort never runs. Tab close is the only live-turn detach. Post-E, a durable run still cannot be abandoned by switch — user is stuck until the Function ends or the tab unmounts. | Inflight guards are the #642 leftover-Send lock and pre-exist this PR; step 4 of the plan listed unmount/switch/clear calling detach without saying “lift the guard.” Fails: the plan’s own edge case (“Session switch / New / Clear mid-turn: same detach discipline — abandonment is not cancellation”) and the feature-divide sentence state it as current truth. The new detachTurn() lines on those three paths are dead for a live turn. |
high |
| Minor | L6 | No Host wiring proof that unmount uses detachTurn(). Plan row 7 allowed an extracted helper; lib/detachTurn.test.ts covers decideDetach only. Unmount cleanup in HarnessHost.tsx is untested. This repo already source-locks Host call sites (lib/harnessHostModelPersist.test.ts, lib/sessionSummaryLabel.test.ts). |
Revert the unmount cleanup from detachTurn() back to abortRef.current?.abort(). decideDetach tests stay green. Default (no turnRunId) still aborts, so C looks the same — the durable-run close-reader path is the untested wiring. |
Helper extraction was an allowed DoD option; the four call sites are one-liners. Fails: the #710-class behavior lives in the cleanup, not the helper. A source lock that unmount/switch/new/clear call detachTurn() and that the remaining abortRef.abort() sites are only runPrompt (replace controller) and takePendingCancel (Stop/Esc) would pin the seam. |
high |
Residual risk
decideDetach keys on presence of a client-writable meta.turnRunId (envelope PUT accepts any Redis-safe opaque; drop-to-unset, never 400). At C the host never mints an id (/api/agent is still the turn owner), so a session that already carries a valid-looking id will not abort on unmount — the plan-review Major (unpersisted 1800s Function) returns for that session. Default snapshots omit the field → abort (today’s behavior). close-reader is a documented no-op until F. Logout relies on unmount after signOut (LogoutButton does not call detachTurn). getRun is not consulted (hint vs authority is docs-only this slice).
Merge guidance
- PASS WITH NOTES: safe to merge from this attack; nits optional
- E/F should lift or justify the inflight guards when switch-away must detach a durable run, and should not trust a client-supplied id without a real Workflow run
What was not attacked
Live Workflow turn owner (E). Viewport attach / reader close (F). Stop→Workflow cancel (H). Prod Gateway. Self-hosted runner. Dual-chat. Wasm protocol (no native/ in diff).
Plan to address feedback on PR #790 (Status: DONE — all round-1 + round-2 items addressed)Last updated: 2026-08-23 Feedback items (most complex → least)
#1 — switch/New/Clear
|
…lock L1 (Minor): feature-divide row now states that during an in-flight turn only unmount fires detach — switch/New/Clear are gated by the pre-existing #642 inflight lock and their detachTurn() runs on the idle-viewport teardown; E/F lifts or justifies the guards for durable-abandon. L6 (Minor): lib/detachTurn.test.ts gains a HarnessHost wiring source lock asserting the four detachTurn() call sites (unmount/switch/New/Clear) and that the only abortRef.abort() sites are detachTurn, runPrompt, and takePendingCancel.
btipling
left a comment
There was a problem hiding this comment.
Adversarial review — PR #790 (round 2)
Verdict: PASS WITH NOTES
Repo: btipling/invincible
Scope: main ← plan/turns-detach · 13 files · HEAD 31d7f78 (backend-agents C)
Lenses run: L1, L2, L3, L5, L6, L7, L8 (skip: L4 — no GHA/CI/artifact files; L9 — no palette/UX chrome)
AGENTS.md read: yes (docs/feature-divide.md detach row; SECURITY.md session envelope trust)
Round 1 PASS WITH NOTES: inflight guards make switch/New/Clear detach dead for a live turn (docs overclaim); no Host wiring lock that unmount calls detachTurn().
Prior items
| Prior | Status on 31d7f78 |
|---|---|
| Minor L1 switch/New/Clear mid-turn detach is dead; feature-divide overclaim | Closed (docs). Row now states: seam wired on four sites; during an in-flight turn only unmount fires detach; #642 inflight lock holds the other three; E/F lifts or justifies. Host behavior unchanged (correct at C). |
Minor L6 no Host proof unmount uses detachTurn() |
Closed. lib/detachTurn.test.ts source-locks 4× detachTurn(); with comment anchors + 3× abortRef.current?.abort(); (detachTurn / runPrompt replace / takePendingCancel). Reverting unmount to a direct abort drops the detach count to 3 and bumps abort to 4. |
Findings
| Sev | Lens | Finding | Break scenario | Refutation attempt | Confidence |
|---|---|---|---|---|---|
| Nit | L8 | docs/feature-divide.md detach row cites “PR #790 review L1” (issue/PR archaeology in a living ownership table). |
A clone-and-run reader has to open this PR to decode the sentence. The inflight-lock fact is already in the same cell. | Plan #789 living-docs rule was timeless / no phase theater; the repo’s tables routinely cite plan numbers. Fails only as a PR-review citation, not a plan id. Optional trim. | high |
Residual risk
decideDetach still keys on presence of a client-writable meta.turnRunId (envelope PUT accepts any Redis-safe opaque). At C the host never mints an id, so a session that already carries a valid-looking id will not abort on unmount — the plan-review Major (unpersisted 1800s Function) returns for that session. Default snapshots omit the field → abort (today’s behavior). close-reader is a no-op until F. Logout still relies on unmount after signOut. Source lock is regex-shaped (abortRef.current?.abort();) — an abort() without ?. would not bump the count.
Merge guidance
- PASS WITH NOTES: safe to merge from this attack; nits optional
- E/F: lift or justify the inflight guards when switch-away must detach a durable run; do not trust a client-supplied id without a real Workflow run
What was not attacked
Live Workflow turn owner (E). Viewport attach / reader close (F). Stop→Workflow cancel (H). Prod Gateway. Self-hosted runner. Dual-chat. Wasm protocol (no native/ in this diff).
btipling
left a comment
There was a problem hiding this comment.
adversarial-review — PASS (VERDICT: PASS)
Target commit: 4a6f3cf (current head). Fresh review after the head was pushed for the round-2 Nit L8 doc fix.
Head delta reviewed (31d7f78..4a6f3cf)
Single commit, docs-only: docs(feature-divide) removes the (PR #790 review L1) archaeology citation from the detach row (docs/feature-divide.md:47). One line changed; the substantive clause E/F lifts or justifies those guards when switch-away must detach a durable run is retained verbatim. No TS/Zig/code surface touched.
Round findings — all closed, re-confirmed on this head
- L1 (Minor): detach dead-behind-inflight-guard docs overclaim — fixed; doc now states only
unmountfires detach during an in-flight turn. ✓ - L6 (Minor): HarnessHost wiring source lock added in
lib/detachTurn.test.ts(4 detach / 3 abort sites). ✓ - L8 (Nit): PR archaeology dropped from ownership table. ✓
Independent verification on this head
npm run typecheck→ exit 0- Full
node_modules/vitest/vitest.mjs run→ 133 files / 2062 tests passed, exit 0 (direct vitest, no wrapper) node scripts/di-gate.mjs→di-gate OK; nonew PGlite(/createDbConnection(in any changed test file
Verdict: PASS. No blocking/major/minor findings survive on head 4a6f3cf; the change is a safe, non-behavioral doc clean-up matching the intended round-2 alignment.
Summary
Implements plan #789 (backend-agents slice C, source #766): stop treating
the tab as the turn lifetime.
meta.turnRunIdsession carrier (Redis-safe opaque via theexisting
isRedisSafeOpaqueIdrule,^[A-Za-z0-9_-]{1,512}$) + optionalmeta.turnStatus(idle|running|cancelling, a cached hint only —getRun(runId)is the authority when an id is present). Both follow themeta.selectedModeldrop-to-unset-on-poison, never 400 discipline.HarnessHostunmount / session switch / New/Clear now call asingle
detachTurn()seam (lib/detachTurn.tsdecideDetach) that keys onturnRunIdpresence:run, never a server cancel. The run identity survives on the envelope.
/api/agentfetch so a detached busy tab never leaves an unpersisted 1800 sFunction burning with no writer (the plan-review Major guard).
takePendingCancel) stillcancels, isolated from the detach path.
those). This slice ships the carrier + detach seam only.
Files
lib/sessionCloudCaps.ts— client-safesanitizeTurnRunId/sanitizeTurnStatus,TurnStatustype, NEW capTURN_STATUS_MAX_BYTES = 32.lib/sessions/sessionStore.ts— both keys inRESERVED_META_KEYS+ dedicateddrop-to-unset branches in
validateMeta(never 400; STRICT unknown-key intact).lib/sessionStore.ts(local snapshot) — fields + sanitize-on-load.lib/sessionRepository.ts— fold intocloudMetaFor, restore on pull/adopt(
parseCloudSessionSnapshot,overlayEnvelopeMeta); emit-when-set, omit=clear.app/harness/HarnessHost.tsx—detachTurn()seam wired to unmount/switch/Clear/New; Stop/Esc (
takePendingCancel) unchanged.lib/detachTurn.ts(+ test) — pure detach-vs-abort guard.docs/session-model.md(reserved-meta table + schema list),docs/feature-divide.md(unmount = detach row). AGENTS/README/SECURITY/.env.example N/A (one-line whys in plan).Verification
npm run typecheck→ exit 0.vitest run→ 2046 passed (merge feat(backend-agents): turns runtime spike — resumable AgentStreamEvent stream (plan #787) #788); now 2061 passed — +15 new test cases across the added/changed files (rows 1–9 of the plan's test matrix).vitest run: 133 files / 2061 tests passed, failed=0, exit 0.npm run test:di-gate→ OK (no new I/O construction).TURN_STATUS_MAX_BYTES=32is a NEW generous cap (plan Caps table);turnRunIdreusesREDIS_SAFE_OPAQUE_ID_MAX=512. Both ride the existing 1 MiB whole-meta budget / 4.5 MB Function ceiling.workflow_dispatchchange.Closes #789 · Refs #766 (source) · #764 (parent) · #710 (umbrella) · #765 / #767 (registry B/D).