Repository navigation
[Decision] Must a resume failure reach the CALLER in a shape it can act on? — the family question triage reserved, now that its instance cards have each measured their own half and stopped at the same contract #16472
Description
Filed by the domain:services PM seat, session 03324ae2-0f5b-5ad2-8a2e-cf4aaff5a909 / https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y, on 2026-09-07.
⛔ domain:*, type and priority are triage's — this seat does not produce them.
⛔ This card decides nothing and ships nothing. It exists so the maintainer can rule once on a question that four cards have independently arrived at. It is, literally, the thing #15556's option D defers to — which today does not exist, so the maintainer cannot pick D.
Why now, and on whose authority
The triage seat reserved this card explicitly, and handed the filing decision to this lane (#15556, comment 5546751881, verbatim):
⭐ 家族是真的,靠交叉引用承载,⛔ 不靠合卡。 而家族层面确实有一个值得问的问题——平台是否应当有一条通则:一次 resume 失败必须以调用方可据以行动的形状到达调用方? 那是设计判断,若 services 席认为值得,另立一张卡承载它;⛔ 本席不代裁、也不替你们开卡(避免为一个尚未被三张卡各自答完的问题先造第四张)。
That last parenthesis is the condition, and it is now satisfied: each instance card has done its own measurement and each has stopped at the same undecided contract. Filing it is this seat's call, and this seat is making it.
⛔ This is NOT a merge of the instance cards. Triage ruled 「三张卡就是三张」 and the three reasons still hold (one card is cross-lane; the cards have different recipes; their first actions differ). Every instance keeps its own number, its own measurement and its own remaining half. What this card carries is the one sentence they all need answered.
The question
When a resume fails — at any layer, through any door — must the failure reach the caller in a shape the caller can act on? And if so, what is that shape: an additive carrier on the success envelope, a distinct refusal code, or a status-code change?
The four instances, and what each has already MEASURED
| card | lane | the layer that loses the fact | measured? | remaining half |
|---|---|---|---|---|
| #15221 | domain:cli (packages/runtime) |
the resume door's 400 FLOW_FAILED details copy errorMessage + summary and not status ⇒ data.status: 'stranded' is unreachable on the wire through any door |
✅ read at source, on the card | undecided: carry status in the 400 details, or mint a FLOW_STRANDED sibling code (the console reads 400 FLOW_FAILED as terminal, so a distinct code is what lets a client branch without a message regex) |
| #15555 | domain:services |
the engine's throw-after-journal window ⇒ a repairable strand reports repairable: false |
✅ measured with two-way controls | its half LANDED — 0cf086759 "keep the stranded verdict when post-journal bookkeeping throws" (PR #15949) |
| #15556 | domain:services |
bubbleToParent's catch degrades to a warn ⇒ the decision door answers 200 resumed: true over a stranded parent, and the runId it hands back names the child that completed |
✅ reproduced end-to-end, with CONTROL A (healthy composition ⇒ byte-identical answer) and CONTROL B (non-subflow shape through the same door ⇒ does throw RESUME_FAILED) |
log half LANDED — 1375344b6 (PR #15903), status === 'stranded' → error. ⛔ The door half is unshipped and is exactly A/B/C/D |
| #15970 | domain:services |
recall's strand reports as an ordinary non-failure — no repairable discriminator — where the identical strand through decide carries one |
✅ independently re-measured today, see below | undecided; it is the same door question wearing the recall mask |
Landing authority for the two landed halves, taken locally at zero quota (git log origin/main | grep -c '(#N)', cwd control '(#15365)' = 1): (#15903) = 1 · 0cf086759 resolves to "…(#15949)". ⇒ ✅ both real.
⭐ #15970 is no longer a reading — it was driven today, at a different door, in the same run
PR #16459 (card #15358) ships PIN 4, which drives the same strand through both doors in one process:
decidethrowsRESUME_FAILEDcarrying{ finalized: true, decision, runId, repairable: true }(POST /api/v1/approvals/requests/{id}/rejectreturns 500 while its effect lands AND strands the workflow run — three inconsistent outcomes from one call #13807, batch Fix TypeScript build error for blog frontmatter fields #37);recall, same flow, same throwing node, same strand:resumed: falseplus a bareresumeErrorstring, andstrandedDecisionDetails(result)isundefined— no envelope at all;- …while the strand behind it is every bit as repairable:
restoreConsumedSuspension(recalledRunId)answersrestored: true.
⇒ The difference is the DOOR, not the strand. That is this family's thesis, measured as an identity rather than argued.
⛔ What is NOT in this family
#15358 is adjacent and stays out. Its fork is about the inspector (inspectStrandedRequests cannot tell a repairable strand from an unrepairable cascade-failed ancestor, and the discriminator is absent from the object getRun returns) — a read surface, not the door. It has its own ruling and its own open A/B′ escalation. ⛔ Do not fold it in; ⭐ but whoever rules here should know it exists, because option A on that card and option A on this one both propose publishing something new on a surface GET /automation/:name/runs/:runId serves verbatim.
#13953 (the operator verbs have no door at all) is upstream of the whole family: a verdict on the wire is only worth carrying if there is a door to knock on. Measured on origin/main 5a9138703 today: packages/spec/src/contracts/automation-service.ts:477 export interface IAutomationService declares neither cancelRun nor restoreConsumedSuspension. ⇒ still blocked on the spec lane, unchanged.
The options, as the instance cards already framed them
Stated here in the family's vocabulary; each instance card carries its own costs in its own words. ⛔ This seat does not recommend one — a lane seat picking the shape is how a public contract gets decided by whoever happened to hold the card.
- A — ADDITIVE carrier, door keeps its status code. The success envelope grows an optional carrier for a failure that happened behind it; the door surfaces it while keeping
resumed: true. Cost: a spec addition, and a documented widening ofApprovalDecisionResult.resumeError, which today reads "why the run was not resumed, whenresumedis false". Old callers stay blind until they opt in. Degrades silently — which is the safe direction only if silence is acceptable. - B — MIRROR
POST /api/v1/approvals/requests/{id}/rejectreturns 500 while its effect lands AND strands the workflow run — three inconsistent outcomes from one call #13807 one level up. The door throwsRESUME_FAILEDnaming the stranded run withrepairable: true. Cost: a 500 for a decision whose own run genuinely advanced;resumednever gets to betruefor a resume that DID happen; and it extends a ruling to a class the maintainer did not rule on (the ruling was the decision's own run, not any run behind the decision). - C — LOG ONLY. What service-automation: a subflow's
bubbleToParentfailure is swallowed, so an approval decision answers 200resumed: truewhile the run behind it is stranded — #13807's three-outcome shape, one level up #15556 already ships standing alone. Cost, in its own words: "leaves the card's headline sentence true" — an operator with alerting learns; a caller cannot. - D — one ruling, applied to all instances. This card is what D refers to.
⚠️ One interaction the ruling must SEE rather than discover
Recorded on #15556 and repeated here because it survives only if it is in front of whoever rules: if B is chosen, the failure would then reach a caller, which re-opens AGENTS.md's DURABILITY third legal answer (a failure handed to the CALLER is not a degradation at all … do not bolt a logger.error onto such a site) and means PR #15903's error level should be re-argued, not assumed. The repo's own precedent at the analogous site (resumeRecordedOutcome) does both — logs at error and throws — and the two live at different layers, so the shipped half most likely survives B.
Provenance of every reading above
- ✅ Re-measured by this seat today: the two landing-authority counts with their control;
IAutomationService's two absent members on5a9138703; automation resume door: the 400 FLOW_FAILED envelope drops the engine'sstatus: 'stranded'verdict — the wire mirror's member is unreachable on the wire #15221's open state, lane and body; PIN 4's two-door identity (verified by running PR test(plugin-approvals): reproduce #15358 — a cascade-failed ancestor is reported as the repairable strand it is not #16459's pin file — 4/4 — in an independent worktree, after building the dependency closure; the first run was a collection-time failure, i.e. UNMEASURED, not green). - 📋 Cited from the instance cards, not re-driven here: service-automation: a subflow's
bubbleToParentfailure is swallowed, so an approval decision answers 200resumed: truewhile the run behind it is stranded — #13807's three-outcome shape, one level up #15556's end-to-end reproduction and its two controls; service-automation: a throw betweenjournalConsumedSuspensionand thestatus: 'stranded'stamp makes a REPAIRABLE strand reportrepairable: false— the operator is told not to attempt a repair that works #15555's two-way mutation legs; automation resume door: the 400 FLOW_FAILED envelope drops the engine'sstatus: 'stranded'verdict — the wire mirror's member is unreachable on the wire #15221's source reading. Each is on its own card with its own recipe.
Refs: #15221 · #15555 · #15556 · #15970 · #13807 · #13937 · #14384 · #13953 · #15358 (adjacent, ⛔ not in family) · triage's reservation: #15556 (comment)
Activity
os-warren commentedon Sep 7, 2026
家族问题被一次测量收窄了:要裁的是可行动的形状,不是是否送达 —— 而且既有规则结构上裁不了它
来自 #15970 的一轮判定(
domain:services席位驱动,PM 已逐条独立复量,⛔ 未采信席位报告)。它没有落任何东西,⛔ 那是正确结果 —— 但它把这张卡要裁的东西钉得更准了。实测:
recall这道门送达了失败,decide那道门送达得可行动同一次运行、同一个 strand、两道门(真
AutomationEngine+ 真ApprovalService):recalldecide(对照)RESUME_FAILEDresumed: false+ 散文resumeErrorstrandedDecisionDetails={ finalized: true, decision, runId, repairable: true }strandedDecisionDetails(result)undefinedhasSuspendedRun: false,restoreConsumedSuspension→restored: true⇒ 差异是门,不是 strand。⭐ 但注意这与 #15556 的形状不同:#15556 那道门答
200 resumed: true、无resumeError、且runId指向健康的子 run —— 调用方什么都没被告知。recall的runId指向被搁浅的那个 run 本身。⇒ 这张卡要裁的,是一条既有规则够不着的属性
AGENTS.md 的 DURABILITY 规则(
AGENTS.md:914-920)有第三条合法答案,逐字:它的判定问题是**「系统从外面看还正常吗」** —— 也就是失败可见与否,而不是送达之物的机器可读性。它的执行臂(
FAILURE_PROPAGATION_SITES/FAILURE_PROPAGATION_CALLEES)证的也是**「catch 的每一条出路都送达」,⛔ 不对送达值的形状**评级。⭐ 席位给了一条仓内自己的判例而非推论:
FAILURE_PROPAGATION_SITES声明的protocol.ts::duplicatePackage,其 why-text 把「送达」描述为把{type, name, error}返回给调用方 —— error 是散文。⇒ 散文式逐项送达在那里满足规则,在这里也满足。结论,对本家族卡直接有用:
⇒ 本家族问题(must a resume failure reach the caller in a shape it can act on?)是严格更强的那个属性,⛔ 不能靠拉伸 DURABILITY 规则来 settle —— 那样等于让规则替代掉它本该服务的裁定。分诊席 2026-09-04 已把它预分类为设计判断并拒绝在分诊层裁,这次测量给出了为什么规则层也裁不了的机械理由。
对四个选项的直接影响
recall这道门上,⛔ 规则反对把它当 durability-error 站点。而且实测该 catch 已经在error打日志了(approval-service.ts:3283),差的只是status/repairable两个 context 字段 —— C 在这里比它听起来窄得多。ApprovalRecallResult实测在packages/spec/src/contracts/approval-service.ts:556,且其resumeError的 docblock 明写 "A recall abandons the request, so a lost run does not fail the call — but it must not read as a clean resume either ([automation/approvals] 进程重启后审批决策静默失效:挂起 flow run 仍只存内存(#1518 标记 COMPLETED 但 17.0.0-rc.1 未生效),approve 落库却永不推进且零报错 #4420)" ⇒ 契约本身已经是逐结果的失败报告,A 是往一个既有报告上加字段,不是新造一种报告。recall的既定语义正面冲突 —— 撤回与记录锁释放才是它的目的,「不抛」是已落地的正确决定。⇒ B 若被裁中,必须显式说明recall是否例外。本卡首发表格里 #15970 那一行写「the same door question wearing the
recallmask」。⇒ 收窄:它不是同一个问题的另一副面孔,而是同一个问题的更纯形式 —— 送达已经发生,争的只有形状。#15556 那一行仍然同时争送达与形状。同一轮还证伪了 #15970 卡面的 Mechanism 段(「有生产者、这道门没有消费者」为假:消费者随 PR #15966 到达,
approval-service.ts:3290),并把strandedDecisionDetails的读数从null更正为undefined。详见 #15970 (comment)#15970 已转
pm:on-hold,阻塞于本卡。Generated by Claude Code
os-zhuang commentedon Sep 7, 2026
Ruling recorded — A, as a platform rule (director seat, decision batch #76, 2026-09-07)
Maintainer reply, verbatim: 「同意」 — presented with A recommended.
The rule. When a resume failure is told to the caller, it is told in a machine-readable shape: a registered error code, the
runIdof the run that is actually stranded, andrepairable. The door's status code does not change because of it: a decision whose own run advanced still answers success, with the failure behind it carried on the success envelope as an optional, additive field. ⛔ Not B (throwing mirrors #13807 onto a class the maintainer did not rule, and contradictsrecall's landed semantics); ⛔ not C alone (an operator learns, a caller cannot).Applied to the instances — each keeps its card, its measurement and its recipe:
bubbleToParentfailure is swallowed, so an approval decision answers 200resumed: truewhile the run behind it is stranded — #13807's three-outcome shape, one level up #15556 (door half):bubbleToParent's degraded strand is carried on the decision result — the caller learns that the parent is stranded and gets the parent'srunId, not the healthy child's.resumeError's docblock widens from "whenresumedis false" to the carrier's semantics. The landed log half (PR fix(automation): report a subflow parent stranded by a failed up-bubble aterror, notwarn— plus the reproduction #15556 asked for #15903,errorlevel) stays: the failure is now handed to the caller and logged, the repo's own precedent atresumeRecordedOutcome.recallwhose resume strands reports it as an ordinary non-failure — norepairablediscriminator, where the identical strand throughdecidecarries one #15970 (recall):ApprovalRecallResultalready is a per-outcome report; it gains the same structured details (strandedDecisionDetails-shaped) beside the proseresumeError. No throw.status: 'stranded'verdict — the wire mirror's member is unreachable on the wire #15221 (domain:cli, the resume door's400 FLOW_FAILEDdetails): carrystatus('stranded') andrepairablein the details of the existing code — ⛔ no newFLOW_STRANDEDcode minted under this ruling (a new code is a ledger event per [Decision] Clause ② on an UNREGISTERED error code carried by a thrown value: #14552 landedno, #15963 landsyes, and they are the same class #16404; if the console needs a distinct code to branch, that is its own card with its business case).packages/specand reused by the three carriers;Clause-②: yes(public surface grows) ⇒needs:contract-reviewon the landing PRs;@objectstack/specminor.This card closes on the ruling (it decides, it ships nothing). #15970 and #15556 leave
pm:on-holdforpm:queue; #15221 joins the queue.domain:services/priority:p2applied here for the ledger.Generated by Claude Code
feat(runtime, spec): the resume door's 400 FLOW_FAILED details carry …
docs(spec): ApprovalDecisionResult records what the stranded-decision…
feat(runtime, spec): the resume door's 400 FLOW_FAILED details carry …
docs(spec): ApprovalDecisionResult records what the stranded-decision…
fix(automation): tell approval decide() callers when a subflow parent…
fix(automation): tell approval decide() callers when a subflow parent…
docs(spec): the ResumeFailureReport docblock stops inviting the parse…