fix(turns): skip getReadable on already-cancelled attach - #920
Conversation
F5 on a dashboard-cancelled run was wrapping getReadable() (pre-headers Failed to fetch) and then hot-resuming because subscribe-fail stamped envelope C onto heapApplied. bodyForRun returns synthetic SSE for cancelled/failed without touching the SDK readable; attach Stop keep-running requires an aborted signal so producer cancel clears the envelope.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
implement-plan for #919.
|
btipling
left a comment
There was a problem hiding this comment.
Adversarial review — PR #920
Verdict: PASS WITH NOTES
Repo: btipling/invincible
Scope: main ← plan/f5-cancelled-attach · 13 files · F5 cancelled-attach unstick (bodyForRun + host hot-resume gate)
Lenses run: L1, L2 (API trust), L3, L5, L6, L8 (skip: L4 no workflow/artifact; L7 no config-seam change; L9 no palette/Wasm chrome)
AGENTS.md read: yes
Findings
| Sev | Lens | Finding | Break scenario | Refutation attempt | Confidence |
|---|---|---|---|---|---|
| Minor | L8 | app/api/turns/[runId]/stream/route.ts file header + handler comment still describe attach as always getRun().getReadable(). Plan-review locked only bodyForRun may call getReadable, and only on the wrap branch. |
Next implementer reads the route comment, writes const readable = run.getReadable({ startIndex }); return new Response(await bodyForRun(run, readable)) (or calls getReadable then wraps). Cancelled-hours-ago F5 hits the SDK handle again → pre-headers Failed to fetch (#918). |
Tests on cancelled (row 13b / POST row 5b / bodyForRun unit) would fail if the current route called getReadable. They do not stop a future agent who “fixes” the comment-shaped code. Living docs/architecture.md prose is already correct; the route comment is what implement-plan reads first. |
high |
| Nit | L8 | docs/architecture.md mermaid still labels attach getRun.getReadable. Prose four lines later is the new contract. |
Diagram-only readers keep wrapping the SDK readable. | Prose is the operator/agent contract; mermaid is schematic. | high |
| Nit | L6 | Route tests lock cancelled skip-getReadable. failed skip is only in lib/agent/pipeRunReadable.test.ts. GET row 13 is named “completed run” but uses default status: 'running'. |
A route-local branch that special-cases only 'cancelled' would stay green. C16 wrap-on-completed is not locked at the route. |
Routes do not branch on status; bodyForRun unit tests cover failed + completed wrap. |
medium |
Residual risk
Wrapped running/completed streams still call getReadable() + wrapper start()/getReader() inside the Function before the Response is returned — same shape as #917. If Preview ever proves every wrapped SDK readable pre-header-fails, this PR does not unwrap those paths (plan lock; follow-up). bodyForRun races run.status for ≤1s; hung/rejected status falls through to wrap (or GET 503) and Goal 2 (shouldKickHotResume) still stops the Busy loop, but Goal 1 (clear running) then depends on a later synthetic attach or G22 (#816). Synthetic cancelled/failed drops any buffered frames (hours-ago cancel does not need replay). Operator attach Stop/Esc still keep-running (6i/6j); dashboard cancel without abort now clears.
Merge guidance
- PASS WITH NOTES: safe to merge from this attack; nits optional (route comment + mermaid +
failed/completed route locks). - Do not merge until an explicit merge ask (
merge-pr). Fullvitest runis the merge gate, not this review.
What was not attacked
Live DO runner, prod Gateway, Vercel Workflows getReadable() of a real hours-ago cancelled run, Preview network waterfall, G22 cancel POST (#816).
Adversarial-review #920 follow-up: route comments and architecture mermaid no longer describe always-getReadable attach. Route tests cover failed skip-getReadable and completed wrap. Source-lock GET/POST never call getReadable themselves.
|
Adversarial follow-up landed on
Targeted vitest: 3 files / 85 passed. Still PASS WITH NOTES; not merged. |
|
Operator still broken on production (Preview skipped, this PR not merged): #922 — hang + stacked |
Summary
Fixes F5 on a durable run that was cancelled hours ago: GET/POST no longer call
getReadable()whenrun.statusis alreadycancelled/failed(plain synthetic SSE). Subscribe-fail (Failed to fetch) no longer hot-resumes. ProducerRequest cancelled.without an abort clearsrunning(operator attach Stop/Esc still keep-running). Liverunning/completedstill use the #917 wrap.Fixes #919
Fixes #918
Refs #915
Refs #916
Refs #816
Refs #794
Plan locks
bodyForRun: cancelled/failed → synthetic; elsepipeRunReadable(getReadable(…)). Routes do not callgetReadable.attachOperatorStoprequiressignal.aborted.shouldKickHotResume/streamOpened: no hot-resume unless the GET opened.Tests
Targeted +
--changed: 10 files, 494 passed (includes +11 new cases:bodyForRun×6,shouldKickHotResume×4, producer-cancel fold ×1).tsc --noEmitgreen.Baseline last merge-pr full suite was 3011. Full
vitest runis the merge-pr gate, not this PR-open round.Docs
docs/agent-stream.mdEnd of turndocs/architecture.mdattachgetReadableDo not merge
implement-plan stop. Next: adversarial-review, then explicit merge.