Parent: #794
Umbrella: C14 — #808
Label: backend-agents
Status: HANDOFF-READY (plan-review 2026-08-24; one-turn sub-issue of the C14 split)
Blocked on: #835 (C14b start+stream)
Part of breaking C14 (POST /api/turns) into small single-turn, one-issue/one-PR rows. C14d is the third row: make the started run durable (post-start running marker + warning contract) and land the full C14 route test matrix + next build deploy-gate. One issue → one PR → one adversarial review → one merge → close. Do not expand scope.
Slice
Two parts, both small and local to the C14b route + the B8 overlay seam:
1. Durable running PATCH (the missing "real start surface" piece)
The pre-return PATCH persists turnRunId=run.runId + turnStatus='running' into the session envelope after start() returns (you cannot persist the id before start returns it). Reuse the existing B8 seam overlayWorkerMeta (lib/agent/workerMetaOverlay.ts:164, supports turnStatus:'running', copy-forward preserves host siblings byte-for-byte, LWW with a strictly-newer clock) — a separate write from the terminal B13 seam (which writes completed); do not reuse the terminal seam for the running marker. Same construction pattern as createTurnPersistSeam.
Post-start persist-failure contract (locked): if the running PATCH fails AFTER start() succeeded, the run IS started — do not 500. Return the stream/JSON {runId} and surface the persist failure as a warning (x-workflow-run-warning header or warning field). Durable truth is getRun/C16 (attach reconciles).
2. Full C14 route test matrix + deploy build gate
app/api/turns/route.test.ts — the complete 8-row matrix (including C14b's rows, now asserted against the durable route): 400 guard · start called + runId · 503 on start throw · running-PATCH-fails-after-start → non-500 + {runId} + warning (run considered started) · SSE contract · JSON {runId} · auth gate · in-workflow runId equality. Plus next build exit 0 as the mandatory deploy gate for the Workflows-bundle layer (the umbrella: green tests never substitute for the build/review).
Architecture lock (from #794 / C14 #808 — copy, do not weaken)
- One run = one prompt; one step = one model round / one tool / one persist (unchanged).
turnRunId = Workflow run.runId, never sessionId.
- Terminal
turnStatus='completed' (B13 seam) stays distinct from the running PATCH (B8) — two separate writes, ordered by the strictly-newer LWW clock so they never self-conflict.
'use workflow' file must not statically import db/mcp/blob/node:crypto/node:dns; the B11 real-entry closure regression still walks zero banned modules.
DoD
Testing matrix (complete — app/api/turns/route.test.ts)
| # |
Case |
| 1 |
Authorized; valid sessionId+prompt → start called, run.runId PATCHed 'running', JSON {runId} + header |
| 2 |
Missing sessionId → 400 |
| 3 |
start throws → 503 fail-closed, no fallback |
| 4 |
running PATCH fails after start → non-500, still {runId} + warning, run considered started |
| 5 |
Accept: text/event-stream → content-type: AGENT_STREAM_CONTENT_TYPE, body is run.getReadable(), header set |
| 6 |
Accept: application/json → JSON {runId} |
| 7 |
Unauthorized → auth gate 401/403 before any persist/start |
| 8 |
In-workflow terminal persist uses getWorkflowMetadata().workflowRunId, equals route-side run.runId (spy the terminal seam arg) |
Cloud ops
N/A. Same request-scoped route as C14b — no Production data/secret/env mutate, no GHA, no cutover. Durable-turn ops documentation lands in H23–H25 (deferred).
Living docs
N/A for this row. H23–H25 (docs rows) cover durable-turn ownership/wire; C14d adds no user/operator rule a doc must describe (attach/cancel are later rows C16/G22).
Caps
| Cap |
Value |
Status |
Rationale |
maxDuration |
1800 s |
Reused constant (C14b) |
#808 caps lock. No change → no human gate. |
MAX_WORKFLOW_STEPS |
256 |
Existing (B12) |
Not reopened. |
Risks & mitigations
- Running vs terminal PATCH self-conflict: B8 strictly-newer
overlayClock (B13) keeps the running write (earlier clock) from being clobbered by a stale terminal overlay on a real run (locked; LWW conflict test).
- Persist failure post-start: never 500 — warning + attach-reconciles (locked).
- Deploy regressions in the bundle layer:
next build exit 0 is a mandatory DoD row, not optional.
Refs
Parent: #794
Umbrella: C14 — #808
Label:
backend-agentsStatus: HANDOFF-READY (plan-review 2026-08-24; one-turn sub-issue of the C14 split)
Blocked on: #835 (C14b start+stream)
Part of breaking C14 (
POST /api/turns) into small single-turn, one-issue/one-PR rows. C14d is the third row: make the started run durable (post-startrunningmarker + warning contract) and land the full C14 route test matrix +next builddeploy-gate. One issue → one PR → one adversarial review → one merge → close. Do not expand scope.Slice
Two parts, both small and local to the C14b route + the B8 overlay seam:
1. Durable running PATCH (the missing "real start surface" piece)
The pre-return PATCH persists
turnRunId=run.runId+turnStatus='running'into the session envelope afterstart()returns (you cannot persist the id beforestartreturns it). Reuse the existing B8 seamoverlayWorkerMeta(lib/agent/workerMetaOverlay.ts:164, supportsturnStatus:'running', copy-forward preserves host siblings byte-for-byte, LWW with a strictly-newer clock) — a separate write from the terminal B13 seam (which writescompleted); do not reuse the terminal seam for the running marker. Same construction pattern ascreateTurnPersistSeam.Post-start persist-failure contract (locked): if the
runningPATCH fails AFTERstart()succeeded, the run IS started — do not 500. Return the stream/JSON{runId}and surface the persist failure as a warning (x-workflow-run-warningheader orwarningfield). Durable truth isgetRun/C16 (attach reconciles).2. Full C14 route test matrix + deploy build gate
app/api/turns/route.test.ts— the complete 8-row matrix (including C14b's rows, now asserted against the durable route): 400 guard · start called +runId· 503 onstartthrow · running-PATCH-fails-after-start → non-500 +{runId}+ warning (run considered started) · SSE contract · JSON{runId}· auth gate · in-workflow runId equality. Plusnext buildexit 0 as the mandatory deploy gate for the Workflows-bundle layer (the umbrella: green tests never substitute for the build/review).Architecture lock (from #794 / C14 #808 — copy, do not weaken)
turnRunId= Workflowrun.runId, neversessionId.turnStatus='completed'(B13 seam) stays distinct from therunningPATCH (B8) — two separate writes, ordered by the strictly-newer LWW clock so they never self-conflict.'use workflow'file must not statically import db/mcp/blob/node:crypto/node:dns; the B11 real-entry closure regression still walks zero banned modules.DoD
runningPATCH via B8overlayWorkerMeta(turnRunId=run.runId+turnStatus='running', separate from terminal seam), immediately afterstart()returns, before the route returns the stream/{runId}{runId}+ warning header/field (attach reconciles); no/api/agentfallbackapp/api/turns/route.test.ts(400 · start+runId · 503 · running-PATCH-fail non-500 · SSE · JSON · auth · runId-equality)vitest run+next build(Workflows-bundle) greenTesting matrix (complete —
app/api/turns/route.test.ts)sessionId+prompt→startcalled,run.runIdPATCHed'running', JSON{runId}+ headersessionId→ 400startthrows → 503 fail-closed, no fallbackrunningPATCH fails afterstart→ non-500, still{runId}+ warning, run considered startedAccept: text/event-stream→content-type: AGENT_STREAM_CONTENT_TYPE, body isrun.getReadable(), header setAccept: application/json→ JSON{runId}getWorkflowMetadata().workflowRunId, equals route-siderun.runId(spy the terminal seam arg)Cloud ops
N/A. Same request-scoped route as C14b — no Production data/secret/env mutate, no GHA, no cutover. Durable-turn ops documentation lands in H23–H25 (deferred).
Living docs
N/A for this row. H23–H25 (docs rows) cover durable-turn ownership/wire; C14d adds no user/operator rule a doc must describe (attach/cancel are later rows C16/G22).
Caps
maxDurationMAX_WORKFLOW_STEPSRisks & mitigations
overlayClock(B13) keeps the running write (earlier clock) from being clobbered by a stale terminal overlay on a real run (locked; LWW conflict test).next buildexit 0 is a mandatory DoD row, not optional.Refs
workerMetaOverlay) + B13.