Repository navigation
automation resume door: the 400 FLOW_FAILED envelope drops the engine's status: 'stranded' verdict — the wire mirror's member is unreachable on the wire #15221
Description
Activity
Discharging #13807's ruling item 1 against this card: measured, and it is NOT the same seam — so this stays yours rather than being folded into #13807's stroke.
The batch #37 ruling on #13807 said the generic door "is measured for the same discard and fixed in the same stroke if it is the same seam, filed if it is not". It is filed — here. What I measured, on
a56baa2bd:1. No shared code.
packages/runtime/src/domains/automation.tscontains zero occurrences ofserviceResume,RESUME_FAILED, orhandleApprovalError. The two doors share no narrowing function and no envelope builder.2. The two discards have different mechanisms. In approvals the loss is a lossy internal relay:
ApprovalService.serviceResumecasts the engine result to{ success, code, error }and the door downstream never gets the chance to seestatus. Here the door reads the engine result directly — it already forwardserrorMessageandsummaryoff the same object — and simply does not forwardstatus. That is a wire-shaping choice at the point of shaping, not a value destroyed in transit; the fix shapes are correspondingly different.3. Different code, owner and status. Approvals answers 500
RESUME_FAILED; this door answers 400FLOW_FAILED, which the ADR-0112 ledger registers to@objectstack/runtime— "this door, not the engine's, is where the wire vocabulary is named", as the site's own comment puts it.4. Three of #13807's four fields have no referent here.
finalizedanddecisionare approvals-domain facts. A generic resume has no decision to report, so only the repairability half is even shared.5. An approval node cannot reach this arm at all.
approval-node.ts:126declaresresumeAuthority: 'service', so a generic resume of an approval suspension is answered 403PERMISSION_DENIEDby the arm above, long beforeFLOW_FAILED— measured independently in #13807's measurement round. The populations do not overlap.6. The contract change is genuinely separate.
FLOW_FAILEDcovers two engine exits — "the flow itself failed" and "a subflow child failed terminally" — and only one of them can be stranded. Adding a repairability discriminator changes whatFLOW_FAILEDmeans for every plain terminal failure too, on a second published route with a different owner ⇒ its own Clause-② review, not a rider on #13807's.⇒ #13807's PR carries the approvals door only. After it lands,
AutomationResult.status: 'stranded'will have exactly one consumer; this card remains the second, and the shared piece it can reuse isstrandedDecisionDetails/strandedDecisionFailurein@objectstack/types(new there in that PR) if a maintainer wants the same field vocabulary on a 400.
Generated by Claude Code
- addedpriority:p2Medium: important, M3Medium: important, M3
on Sep 4, 2026 分诊路由(本评论来自分诊座位)· R+150 ·
date -u实测 2026-09-04T20:09:35Z 一轮domain:cli·finding·priority:p2。落点:
packages/runtime/src/domains/automation.ts(REST resume 门)⇒ 车道表把runtime归 cli 车道(cli/runtime/verify/qa/types/rest/mcp/client*)。⚠️ 卡面自己也点明了这一点(「packages/runtime是那张卡宣称文件面之外的」),这正是它没被 #13937 顺手修掉的原因。p2 判据:契约上有一个成员(
status: 'stranded')在线上不可达 —— 只有 HTTP 的运维对「这条 run 已经够不着了」和「这条 run 跑了但被拒了」看到的是同一个400 FLOW_FAILED。⇒ 声明 ≠ 可达(spec 侧的TriggerFlowResponseSchema.data.status甚至有 parity pin 钉着四个成员,而其中一个永远出不来)。⛔ 不是 p1:没有错误行为、没有数据问题,缺的是区分度;而且修复动作(#13953 的操作动词门)本身还不存在,所以今天即使分辨出来了也无处可去。⛔ 不是 p3:它是发布出去的线上词表上的一个洞。⭐ 卡面给的两个方向里,第二个有明确的支持证据,值得在派发词里点名:控制台把 400
FLOW_FAILED当终态处理(objectuiflowResponse.ts读success === false || status === 'failed',且 #8684 的危害 pin 钉着这条),⇒ 一个独立的 ADR-0112 码(FLOW_STRANDED)才是让客户端不靠正则匹配消息就能分支的形状;把status塞进 400 的 details 里,客户端仍要先信任那个 details 结构。⚠️ 但新增错误码是 ADR-0112 台账上的契约新增 ⇒ 走 Clause-② 复审,⛔ 不是顺手加个字符串。⚠️ 与 #13953(操作动词无门)是同族但不是重复:本卡是「线上看不出它是 stranded」,那张是「看出来了也没有门可敲」。⇒ 两张都做才有意义,而先后顺序上本卡在前(先有区分度,门才值得建)。
Generated by Claude Code
Family ruling landed on #16472 (director seat, decision batch #76, maintainer 「同意」): option A — the resume door's
400 FLOW_FAILEDdetails carrystatus: 'stranded'andrepairableso a client can branch without a message regex; ⛔ noFLOW_STRANDEDsibling code is minted under this ruling (a new code is a ledger event; if the console needs a distinct code, that is its own card). Joins the queue:pm:queueadded.
Generated by Claude Code
Claim: PM loop round R70
Session:session_01YFY46JydE1gMxQG1TqBcMZ
Branch:claude/issue-15221-flow-failed-stranded-details
Worktree:objectstack-issue-15221
Domain:domain:cli
File surface:packages/runtime/src/domains/automation.ts, plus whatever the ADR-0112 details shape requires inpackages/spec/src/api/**andpackages/types(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: CONTRACT_REVIEW_TIER
Clause-②: yes — see the grading note below; ⛔ 不预挂 on this card, the PR carriesneeds:contract-reviewat creation and the seat pairs the carriers then
Serial constraints cleared:nonefor the door — measured now, ⛔ not inheritedThe ruling — landed 90 minutes ago, and the card body does not carry it
Family ruling on #16472, director seat, decision batch #76, maintainer verbatim 「同意」, recorded here at
5567698235(2026-09-07T08:28:29Z) — option A:the resume door's
400 FLOW_FAILEDdetails carrystatus: 'stranded'andrepairableso a client can branch without a message regex; ⛔ noFLOW_STRANDEDsibling code is minted under this ruling (a new code is a ledger event; if the console needs a distinct code, that is its own card).⛔ Do not mint a new ADR-0112 code. Triage's own analysis (
5545913117) argued for a distinctFLOW_STRANDEDcode and the ruling declined it. That analysis is now historical; ⛔ do not resurrect it from the thread and ⛔ do not treat its reasoning as an open option. Two independent fences point the same way: the ruling forbids it, andpackages/spec/src/api/error-code-ledger.zod.tsis held by open PR #16531 right now.Serial — measured across all 14 open PRs, ⛔ not inherited
packages/runtime/src/domains/automation.ts : NO HOLDER ← the door packages/spec/src/api/** (automation/api) : NO HOLDER packages/spec/src/api/error-code-ledger.zod.ts : PR #16531 ← and the ruling bars you from it anyway CONTROL: the same probe names 4 holders of packages/spec/src/** (#16548, #16531, #16376, #15612)⭐ The control is what makes "no holder" a reading rather than a broken predicate.
The premise, re-confirmed at source before dispatch
FLOW_FAILED in packages/runtime/src/domains/automation.ts : 7 sites (the 400 emitted at :1698) "stranded" in the same file : 0⇒ The engine's verdict is genuinely not forwarded at that door.
⚠️ Those line numbers are today's reading — locate by the sentence and the code literal, ⛔ never by line.What the card measured that you should not re-derive, but must re-confirm on today's tree
5542754478settled that this is NOT the same seam as #13807's approvals door, with five separate readings — no shared code (automation.tshas zero occurrences ofserviceResume/RESUME_FAILED/handleApprovalError), different mechanisms (approvals loses the value in a lossy internal relay; this door reads the engine result directly and simply does not forwardstatus), different code and owner (500RESUME_FAILEDvs 400FLOW_FAILED, the latter registered to@objectstack/runtime), and non-overlapping populations (approval-node.tsdeclaresresumeAuthority: 'service', so a generic resume of an approval suspension is answered 403 beforeFLOW_FAILEDis reachable).⭐ The reusable piece it names:
strandedDecisionDetails/strandedDecisionFailurein@objectstack/types, introduced by #13807's PR. Check whether they landed and whether the same field vocabulary fits a 400 — ⛔ but do not force it if the shapes disagree; report instead.⚠️ FLOW_FAILEDcovers two engine exits — "the flow itself failed" and "a subflow child failed terminally" — and only one of them can be stranded. So adding the discriminator changes whatFLOW_FAILEDmeans for every plain terminal failure too. Be explicit in the PR about what a client sees on the non-stranded arm; a details member that is present-and-false is a different contract from one that is absent.⚠️ And the consumer fact that motivated the whole card: the console treats400 FLOW_FAILEDas terminal (objectuiflowResponse.tsreadssuccess === false || status === 'failed', pinned by #8684). The ruling's whole point is that a client can branch without a message regex — so the details must be reachable and typed, not prose.Clause ② — graded
yeshere, and you re-derive it from the delivered diffThis widens a published error envelope on a published route.
SKILL.mdis explicit: 凡改变契约接受/拒绝行为或扩大公开面的卡一律契约复审档, and 拿不准就升一档. ⇒ Dispatched at contract tier.⛔ That is my grading, not your conclusion. Re-derive it from what you actually ship, both limbs stated separately: the path limb is
packages/spec/src/**(including*.zod.tscontract schemas and the error-code ledger), and the content limb is your declaration line. If the delivered diff turns out to touch no published contract face, say so and grade itnowith the reading that supports it — ⛔ an inheritedyesis as much a false declaration as an inheritedno.Mechanics
- Derive gates with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands— no paths; it derives the change set from the merge base. Report the Reconciliation total, run the artifact rosters separately, and record anyPREREQUISITE NOT MET/ exit 3 as NOT MEASURED, ⛔ never as a pass. - Relation:
Fixes #15221. ⚠️ Sibling card service-automation: the two operator run-lifecycle verbs (cancelRun, restoreConsumedSuspension) have no door — no REST route, no CLI command, and not on IAutomationService #13953 (the operation-verb door) is the same family but ⛔ not this card's work: this one is "you cannot see it is stranded", that one is "you can see it but there is no door to knock on". This card comes first — ⛔ do not widen into it.- Report back as structured JSON per the role file, including any measurement that falsifies this dispatch.
Generated by Claude Code
- Derive gates with
os-dev-report
{ "issue": 15221, "status": "done", "branch": "claude/issue-15221-flow-failed-stranded-details", "pr": "https://github.com/objectstack-ai/objectstack/pull/16587", "premise_still_valid": true, "summary": "Implemented the #16472 option-A ruling at the resume door: `POST /automation/:name/runs/:runId/resume` now carries `{ runId, status?: 'failed' | 'stranded', repairable }` in its 400 FLOW_FAILED details beside `errorMessage` / `summary`, declared once as `ResumeFailureDetailsSchema` in `@objectstack/spec/api` (spec minor), with no FLOW_STRANDED code minted. Contract stated arm by arm in the PR: `status` is the engine's verdict forwarded verbatim and never synthesised (the subflow-child exit stamps none today, measured on `resumeInternal`), `repairable` is `status === 'stranded'` and is ALWAYS present on this arm (present-and-false on the plain terminal exit, deliberately), `runId` is the path's `:runId`; the trigger door and /actions are unchanged (absent there means not-a-resume). `strandedDecisionDetails` in @objectstack/types was checked and NOT reused (all-four-or-nothing envelope; `finalized` / `decision` have no referent at a generic resume). Clause-②: re-derived `yes` from the delivered diff — path limb hit (`packages/spec/src/api/automation-api.zod.ts`), content limb hit (a published error envelope widened, a new exported wire schema); `needs:contract-review` hung on the PR at creation via the additive REST endpoint and read back; `check-clause2-carriers.mjs --pair 16587` exit 0 (both carriers agree). Serial re-measured across all 18 open PRs before push: door, automation-api.zod.ts, types, client — no holder; contracts/automation-service.ts (#16563) and error-code-ledger (#16531) held and untouched. Draft PR open; CI status: in_progress at report time. The dispatch's `packages/runtime` premise re-confirmed on d1c86a745a (FLOW_FAILED 7 sites, stranded 0, no shared code with the approvals door); origin/main moved 6 commits during the run with zero file overlap against this diff.", "tests": "All at cde77edcc9 in worktree objectstack-issue-15221, closure built first (turbo build --filter='@objectstack/verify^...' 32/32, then client + client-react 34/34), heavy steps under scripts/pm/os-verify-lock.sh, exits captured before any pipe. spec: vitest src/api/automation-api.zod.test.ts + src/contracts/automation-result-status.pin.test.ts → 'Test Files 2 passed (2) / Tests 58 passed (58)'; typecheck (tsc + scripts-typecheck + test-typecheck) → 'VERDICT command-exit 0'. runtime: FULL pnpm --filter @objectstack/runtime test → 'Test Files 239 passed (239) / Tests 3375 passed (3375)' VERDICT command-exit 0; typecheck → exit 0 (test-typecheck ledger unchanged 27/191/69); new domains/automation-resume-stranded-details.test.ts = 14 pins (stranded arm equality + schema parse + message carries no 'stranded'; plain arm exactly { runId, repairable: false }; regex CONTROL; stamped 'failed' relayed; runId source; six coded refusals with no details; both 200 arms; trigger door unchanged). verify (real AutomationEngine through the real HttpDispatcher, runtime via built dist): new automation-resume-stranded-details.test.ts → stranded resume answers 400 FLOW_FAILED with { runId, status: 'stranded', repairable: true } beside the artefacts, resume again 404, restoreConsumedSuspension restored: true, CONTROL non-throwing tail → 200; 'Test Files 2 passed (2) / Tests 6 passed (6)'; typecheck OK. client: typecheck OK; check:skill-examples after building client + client-react → '257 prose examples type-check across 3 surface(s)'. eslint --no-inline-config on the six changed source/test files exit 0; check:nul-bytes OK + control-byte self-scan zero hits. Reverse verification + ablation (implementation committed first; restore by git checkout HEAD -- path, proven by git diff HEAD empty and git hash-object == HEAD blob after each leg; trap-guarded): Leg 1 door disagrees with the spec type (repairable: 'yes'; injected=1 removed=0 on disk) → runtime tsc RED 'automation.ts(971,9): error TS2322: Type string is not assignable to type boolean' (direction: turns red, reads the rebuilt .d.ts); Leg 2 spread removed (removed=0 on disk) → new runtime file '5 failed | 9 passed (14)' — exactly the five new-behaviour pins red, nine untouched-arm pins green. Gates: dispatch-gates --commands (no paths) at cde77edcc9 → 'Run reconciliation — 99 derived, 99 run, 0 UNRUN' (--ran ledger); 95 exit 0; check-system-context-census exit 1 = pure line rot from the helper insertion, repaired by its own --fix (4 anchors in content/docs/permissions/system-context.mdx, committed) and re-run OK; check:skill-examples exit 1 until client dists existed, then green; NOT MEASURED (exit 3, prerequisite not met — dist outside the built closure): check:dual-build-cjs-loads, check:type-check-debt — declared to CI, not passes. spec check:generated --fix regenerated exactly the 5 it proved stale (api-surface, export-origins, declaration-map, references page, strictness-ledger 450 → 451 in the untriaged api/ dir); check:authorable-surface green. Declared narrowing: turbo ls --affected against BASE = 76 of 79 packages (spec moved); locally runtime full + spec/verify/client targeted as above, the rest declared to CI (additive spec export; no other package reads the resume door). The derivation printed STALE TREE (origin/main +6 commits, 5 derived-from files changed) — none of those commits touch a file in this diff.", "mcp_calls": "1 — create_pull_request. Every read (card body + 4 comments, #16472 body + comments, 18 open-PR file lists for the serial re-check, labels, PR body read-back) and both other writes (the additive label POST on the PR, this report comment) went through the repo-scoped REST channel after a 200 probe; no search_issues was needed (nothing to file, so no dedup search).", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: engine.ts resumeInternal's subflow-child-failed-terminally exit returns { success: false, error, durationMs } with no status, while execute()'s failure exits stamp status: 'failed' — within contract (absent means terminal); it is why the door's plain arm forwards no status today; an asymmetry for the services lane, not a defect.", "noted, not filed: StrandedDecisionDetails (@objectstack/types, #13807) and the new spec ResumeFailureDetailsSchema now share the runId / repairable vocabulary as two declarations; the ruling asks #15556 / #15970 to reuse the spec structure's members — whether the types-side envelope is re-based on it is the services lane's call.", "noted, not filed: the console-side branch on details.repairable (objectui flowResponse.ts) is the client half the ruling anticipates and depends on #13953's operator door; the objectui tree was not readable from this container (403 on the pinned tree API), so the console reading is cited from the card's pins, not re-read.", "noted, not filed: the closure rebuilt fully a second time when client + client-react were added (turbo treated the regenerated spec artifacts as changed inputs) — 8m09s under the lock; a shared-box reading, not a defect." ] }
Generated by Claude Code
os-dev-report
{ "issue": 15221, "status": "done", "branch": "claude/issue-15221-flow-failed-stranded-details", "pr": "https://github.com/objectstack-ai/objectstack/pull/16587", "premise_still_valid": true, "summary": "Patch round for the red `Check Changeset` on #16587, scoped to the two fixes and nothing else. (1) `.changeset/automation-resume-stranded-details.md`: `@objectstack/client` raised from `patch` to `minor` (commit aa5b987ae2, pushed) — the 2026-09-04 ruling (decision batch #35, on #15294) binds per PR, and this clause-② PR moved `packages/client/src/**`; I accept that reading, nothing routed around. (2) PR body: the literal declaration line `Clause-②: yes` added as its own line directly under the kept `## Clause ② — …` section; the edit was sent with no footer of my own (the platform appended its bare block once, read back: one footer), and the session URL restored to body prose in a second PATCH that first stripped the appended block. Code, tests, spec schema and generated artefacts untouched; PR stays draft; `needs:contract-review` survived the size-labeler's writes (read-back union: documentation, size/l, tests, tooling, needs:contract-review). Carrier lesson recorded: the pair predicate reads the card's claim comment, `Check Changeset` reads the PR body — a green from one says nothing about the other. Worktree removed again after a clean status and the pushed tip.", "tests": "Re-run only what the coordinator named, at aa5b987ae2 (merge base d1c86a745a): `node scripts/check-changeset-no-major.mjs --self-test` exit 0 (157 assertions); `node scripts/check-changeset-no-major.mjs --base d1c86a745a --event event.json` (event built from the read-back PR payload: labels + body) exit 0 — '✓ This diff introduces no major bump' and '✓ LEVEL AXIS: this PR declares clause-② yes, and no package whose packages/*/src/** it moves is graded patch · carrier: needs:contract-review IS on this PR · declaration line: Clause-②: yes'; `node scripts/check-empty-changeset.mjs --self-test` exit 0 (118 assertions) and `--base d1c86a745a` exit 0 — '✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)'; `node scripts/pm/check-clause2-carriers.mjs --pair 16587` after the body edit exit 0 — 'the clause-② declaration is readable in the fixed spelling and both carriers agree'. PR body read back after each PATCH: `Clause-②: yes` present as its own line under the heading, session URL in prose, exactly one footer block. The 99-command union was not re-run (as instructed); CI on aa5b987ae2: in_progress at report time.", "mcp_calls": "0 this round (1 for the whole run — the original create_pull_request); every read and write here went through the repo-scoped REST channel (PR GET/PATCH, comment POST).", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: none new this round — the four observations in report comment 5569871013 stand unchanged." ] }
Generated by Claude Code
os-dev-report
{ "issue": 15221, "status": "done", "branch": "claude/issue-15221-flow-failed-stranded-details", "pr": "https://github.com/objectstack-ai/objectstack/pull/16587", "premise_still_valid": true, "summary": "Round 3 (contract-review follow-ups) plus the census fallout, head now c3208dfe0e, PR still draft. Fix 1: `resumeFailureDetails` returns `ResumeFailureDetails` and relays `status` through `isTerminalFailureStatus`, a guard on `TERMINAL_FAILURE_STATUSES = ['failed', 'stranded']` that is `satisfies`-bound to the schema's enum — still a relay, never a synthesised verdict: a `success: false` result stamped with a `success: true` member (unreachable per the contract) is neither forwarded under a schema that refuses it nor turned into anything else; docblock updated to say exactly that (782b92ceb5). Fix 2: `/actions` negative pin added in `actions-flow-dispatch-status.test.ts` — exact `details` equality `{ errorMessage, summary }` — and the docblock pointer now names both pin files (trigger door in the door-pins file, /actions in #9585's artefacts pin); nothing weakened. Fallout: the guard + constant + docblocks moved the anonymous-deny read `ec.isSystem` from `automation.ts:1057` to `:1079`; `check-system-context-census --fix` rewrote exactly that one anchor in `content/docs/permissions/system-context.mdx` (row 52), refused nothing, population unchanged at 106 census sites / 141 anchors (27 declared non-read), committed as c3208dfe0e. The phantom `Test Core` red on the superseded head aa5b987ae2 was not investigated, as instructed. Untouched: spec schema, subset pin, generated artefacts, changeset (`@objectstack/client: minor` stays — the live gate demanded it). Report comment for this round posted on the card; worktree removed again after a clean status and the pushed tip.", "tests": "At 782b92ceb5 (code) and c3208dfe0e (docs anchor), closure build 29/29 cached (FULL TURBO), under scripts/pm/os-verify-lock.sh with && chaining and per-part markers: `pnpm --filter @objectstack/runtime typecheck` → RUNTIME_TC_OK (test-typecheck ledger unchanged 27/191/69); `vitest run src/domains/automation-resume-stranded-details.test.ts src/actions-flow-dispatch-status.test.ts` → 'Test Files 2 passed (2) / Tests 33 passed (33)' — the door pins (14) plus the actions suite with its new exact-equality negative pin; VERDICT command-exit 0. eslint --no-inline-config on both touched files exit 0. Ablation Leg 3 (committed first; trap-restored; `git diff HEAD` empty and `git hash-object` == HEAD blob after): dropping the guard (`const status = result.status;`, injected=1 removed=0 on disk) reds `tsc --noEmit` at 'automation.ts(994,5): error TS2322: Type { status?: completed | paused | failed | stranded | refused | undefined; … } is not assignable to type { runId: string; repairable: boolean; status?: failed | stranded }' plus TS6133 on the now-unused guard — the return-type binding is load-bearing, direction: turns red. Census: `check-system-context-census` exit 1 (2 problems: site-without-a-row :1079, anchor-is-not-a-read-site :1057) → `--fix` rewrote 1 anchor (`:1057` → `:1079`) → re-run exit 0, 'OK — 106 elevation read sites in 20 packages across 45 files, all anchored; 141 anchors resolve, 27 declared non-read'. `check-clause2-carriers --pair 16587` after the final push exit 0 — 'readable in the fixed spelling and both carriers agree'. The 99-command union was not re-run (as instructed); CI on c3208dfe0e: in_progress at report time.", "mcp_calls": "0 this round (1 for the whole run — the original create_pull_request); this round's only GitHub writes are git pushes and the report comment via the repo-scoped REST channel.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: none new this round — the four observations in report comment 5569871013 stand; the {runId, repairable} two-declaration convergence stays with #15556 / #15970 per the reviewer." ] }
Generated by Claude Code
os-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:dispatched.- Closing pull request: feat(runtime, spec): the resume door's 400 FLOW_FAILED details carry the engine's stranded verdict #16587, merged.
- Closing commit
68437d4d95, merged intomain. - Left untouched:
priority:p2,domain:cli,finding— ownership, priority and outcome are not state claims. - The label set was read back after the write and matched.
A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.posted by half-state-patrol run 34128942053 · trigger
scheduleGenerated by Claude Code
Found while delivering the services half of #13937 (branch
claude/issue-13937-stranded-run-operator-verb, draft PR pending). ⛔ Not fixed there:packages/runtimeis outside that card's claimed file surface, and the wire vocabulary is the runtime door's own (ADR-0112 ledger).The reading, on
origin/main50d6c92 plus that branchAutomationResult.statuscarries'stranded'(spec: name the terminally-failed run state onAutomationResult.status— contract half of #13937 (shape 4 ruling) #14384), and the wire mirrorTriggerFlowResponseSchema.data.status(packages/spec/src/api/automation-api.zod.ts) carries the same four members, parity pinned byautomation-result-status.pin.test.ts.AutomationEngine.resume()returns{ success: false, status: 'stranded', ... }for the resume that consumed the pause and then failed downstream (the one exit that journals a consumed suspension).packages/runtime/src/domains/automation.ts,POST /:name/runs/:runId/resume) answers everysuccess: falseresult that is not a coded refusal withdeps.error(result.error, 400, { code: 'FLOW_FAILED', errorMessage?, summary? }). The details object copieserrorMessageandsummaryand nothing else —statusis not copied.resumereaches that catch arm), and the resume door's 200 body (deps.success(result)) is reached only for a paused or completed result.⇒
data.status: 'stranded'cannot appear on the wire through any door today. An HTTP-only operator sees400 FLOW_FAILEDfor a stranded run and for a plain ran-and-was-rejected run alike, so they cannot tell "this run is beyond reach" from "this run has a repair verb waiting" — exactly the information the console (objectuiflowResponse.ts, which readssuccess === false || status === 'failed'as terminal) would need in order to offer a repair action.What a fix has to decide (not decided here)
status(and possiblyrunId) on the stranded arm, or whether that arm gets its own ADR-0112 code (aFLOW_STRANDEDsibling ofFLOW_FAILED). The console treats 400FLOW_FAILEDas terminal (the automation: the run-resume route still answers HTTP 200 wrapping an inner {success:false} — the route #3962's status-code unification left behind #8684 hazard pins inautomation-resume-envelope.test.ts), so a distinct code is the shape that lets a client branch without a message regex.Related
#13937 (the ruling; services half on the named branch) · #14384 (the contract half) · #13953 (doorless operator verbs) · #8684 (the resume door's 400 FLOW_FAILED arm)