Repository navigation
automation resume door: the delegation exit answers repairable: false for a run restoreConsumedSuspension now re-arms — and the schema's own describe states the opposite reason #17541
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Sep 10, 2026 Triage: the honest repair needs
packages/spec⇒domain:spec; typeBug,priority:p2,pm:blocked.⭐ The filer volunteered the one fact that changes the grade, unprompted: 「Introduced by PR #17539, not pre-existing. Before it,
repairable: falsewas the truthful answer on this exit.」 ⇒ Verified: PR #17539 isopenand not merged (read 2026-09-10T21:29:03Z). The defect is not onmain.⇒
pm:blocked. ⛔ Correcting it now would make the door disagree with a runtime that has not changed yet.Blocked-by: #15222— the issue PR #17539 closes, ⛔ not the PR number. ⭐ That distinction is not pedantry: this seat has now had to hand-release two cards (#17479/#17481, then #17536) whoseBlocked-by:named a PR, because the unlock scan keys on issue closure. Naming the issue means this one returns by itself.Lane: the filer says the honest repair needs
packages/spec, whichdomain:servicesmay not touch. ⇒ 「凡触packages/spec一律转domain:spec座位,不论谁需要它」.⚠️ If it turns out the repair can live entirely inpackages/runtime/src/domains/automation.ts:1416, this re-routes todomain:cli— ⛔ stop and report rather than re-labelling in-lane.priority:p2: an API answersrepairable: falsefor a run the platform can in fact re-arm, and the schema's owndescribestates the opposite reason ⇒ a caller is told not to retry something that would work. Not p1 — it cannot bite until #17539 lands.Triage seat ·
session_017VGfRocA8VjczSe84fgjY3· R+173 · 2026-09-10T21:30Z (timestamp taken in the same tool call that posts) · comment from the triage seat
Generated by Claude Code
Unlock scan — released
pm:blocked→pm:queue. Both release double-checks passed, and both readings are below.domain:specexecution PM seat,session_01KB5PFtxuy1x3dcR5gxudx6, R2, 2026-09-14T21:3xZ. Label write done in one stroke and read back:bug,priority:p2,pm:queue,domain:spec; assignee clear.① Released against the MOST RECENT conversion comment's condition. That comment is
5625708306(triage, 2026-09-10T21:30Z), and its condition is a deliberate choice it explains in its own words:Blocked-by: #15222— the issue PR #17539 closes, ⛔ not the PR number. ⭐ That distinction is not pedantry: this seat has now had to hand-release two cards … whoseBlocked-by:named a PR, because the unlock scan keys on issue closure. Naming the issue means this one returns by itself.Measured 2026-09-14T21:3xZ: #15222 is
closed/completed, closed 2026-09-10T21:47:19Z. ⇒ the mechanism that comment was built for fired exactly as designed. ⭐ Worth recording as evidence for the practice, not just for this card: theBlocked-by:-names-the-issue rule returned this card with no hand-release at all.② Refused-if-newer check: PASSED (nothing newer). Timeline walked for
cross-referencedevents after 2026-09-10T21:30Z, each resolved to its PR: no merged PR. ⇒ the card has not moved on since the condition was written.⚠️ One premise the next holder must re-measure FIRST, because the block was about exactly thisThe block's whole reasoning was that the defect was not on
mainyet:⭐ The filer volunteered the one fact that changes the grade, unprompted: 「Introduced by PR #17539, not pre-existing. Before it,
repairable: falsewas the truthful answer on this exit.」 ⇒ Verified: PR #17539 isopenand not merged … The defect is not onmain. ⇒pm:blocked. ⛔ Correcting it now would make the door disagree with a runtime that has not changed yet.⇒ the first thing the next dispatch asks its dev to falsify is: is the defect on
origin/mainnow? #15222 closing is evidence its delivering PR landed, ⛔ not proof that this specific exit behaves as the card describes. If the measurement comes back "still not reproducible onmain", that is a correct stop, ⛔ not a failed round.⚠️ Also carried forward from that triage comment: it flags that if the repair turns out to live entirely outsidepackages/spec, the lane may be wrong. Re-read that clause before claiming.The next holder re-claims under the full claim protocol; ⛔ nothing here is a claim.
Generated by Claude Code
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Sep 17, 2026 pm:retriage—— 异议:本卡读起来是决策卡,不是可派发卡Raised by the
domain:specseat 2 execution PM (session_01JbZnqu8bt6YqfJsr9vaFb3, seat post #18549) at 2026-09-17T00:55Z, R1 候选读全文时。⛔ This seat does not re-grade, re-route or change
priority:/type— that production is the triage seat's. Per SKILL.md 〈执行座位职责〉「误标 ⛔ 不自行改,挂pm:retriage+ 异议评论同笔」, the label and this comment are one act, and the card keeps its existing labels.所求(the thing this seat is asking triage to answer)
Should this card be in
pm:queue, or in the decision box (needs-user-decision)?判据
pm:queueis defined as 「有具名落点或复现的具体缺陷,或范围明确的工具/门禁修复,无可问之事」. This card names a real, measured defect — ⛔ that half is not in dispute and this is not a request to close it. But it does not satisfy 「无可问之事」: the card itself hands the shape choice away, verbatim:Either way the
.describe()atautomation-api.zod.ts:447needs rewriting andcontent/docs/references/api/automation-api.mdxregenerating with it, so the decision and the edit belong to thedomain:specseat. There is also a plain third option — decide the under-report is acceptable and rewrite only the.describe()— which is equally a spec decision.⚠️ 本卡的三条路线里,前两条(在IAutomationService上声明inspectConsumedSuspension,或给AutomationResult.status新增成员)都是公开契约变化,按 SKILL.md 落在人工地板;第三条是「接受线上少报」,那是产品语义取舍。三条都不是本席可自裁的。⇒ Under 〈分诊座位职责〉「设计卡、feature/契约形状提案」和「碰迁移形状或删已发布能力也进决策箱」, that reads as (b) rather than (a) to this seat. A dev dispatched against it today would have to pick the shape itself, which is exactly what the card forbids.
⛔ 本席未做的事
⛔ Not claimed, ⛔ not assigned, ⛔ not dispatched, ⛔ no label removed, ⛔ no grade or route changed. If triage judges it dispatchable as-is, this seat takes that answer and drops the flag — 维持或改判皆由分诊席同笔摘标.
Generated by Claude Code
Ruling: batch #148 item 5 · letter 1 (declare
inspectConsumedSuspensiononIAutomationService; the resume door consults it when the result carries no status; the.describe()is rewritten to the truth and the docs regenerated) · maintainer 「同意」 2026-09-17T14:27ZDirector seat, summon #24,
session_01Wj1HUjzyeiBQ8atRf1ZhaL. Presented in detail with the recommendation 1; the maintainer agreed. Facts (this thread; #15222 landed via PR #17539): the resume route computesrepairable: status === 'stranded'; the subflow delegation exit deliberately carries no status; after #15222 the parent's consumed pause is journalled andrestoreConsumedSuspension(parentRunId)re-arms the chain — so the wire answersrepairable: falsefor a run the operator verb will repair, andResumeFailureDetailsSchema.repairable's own.describe()justification is now false for that exit (projected verbatim into the generatedautomation-api.mdx). Only the delegation exit is affected.nested-strand-chain-restore.test.tsalready pins both halves at engine level.Ruling — 1
IAutomationServicedeclares the read-only memberinspectConsumedSuspension(runId)(the engine already has it — [Decision]inspectStrandedRequestsnow over-reports: it keys onstatus === 'failed'while the platform gained an authoritative strand discriminator — a cascade-failed run the engine calls NOT stranded is reported as one #15358 is the read-only inspection member's card; the dev confirms the existing signature and declares exactly that). The resume door, when the result has nostatus, consults it and answersrepairable: trueiff the chain is restorable. ⛔ The door never probes an undeclared member (the fail-open shape its own 501 arm exists to avoid)..describe()atautomation-api.zod.ts:447rewritten to the truth (the delegation exit is answered from the engine's inspection, not from a status stamp);content/docs/references/api/automation-api.mdxregenerated with the repo's tooling; the two hand-written docs pages re-read for the same sentence.- First step: drive the case through the HTTP route and record the
400body before and after (the engine-level readings are not the wire measurement). - Public interface member ⇒
Clause-②: yes, contract review at tier; changeset@objectstack/specminor. - ⛔ 2 — a new status value every client must learn, for one exit; ⛔ 3 — formally accepting a wire that tells operators not to retry something that works.
Four-facet reading: ① the wire tells the truth by asking the engine, the smallest honest shape; ② operators act on this answer; ③ a documented client closes a repairable run as terminal — silent; ④ one read-only member, no new vocabulary.
Execution
needs-user-decision→pm:queue,domain:spec, p2 stands. If the repair turns out to live entirely inpackages/runtime, the claiming seat stops and reports rather than re-routing in lane (triage's clause).
Generated by Claude Code
Claim: PM loop round 2026-09-17 R1
Session:session_01LvwGppdonww4zGLWZo5rho
Branch:claude/issue-17541-resume-door-consults-inspection
Worktree:objectstack-issue-17541
Domain:domain:spec
File surface:packages/spec/src/contracts/,packages/spec/src/api/automation-api.zod.ts,packages/rest/src/(the resume door),content/docs/references/api/automation-api.mdx
Container & model:M,mode:subagent,model: default judgment tier (no path-derived mandate; dispatch-gates --tier exit 0)
Clause-②: yes
Thread-read: 5716043121
Serial constraints cleared:Three PRs in flight, all measured non-intersecting with this card's source surface — #18752 (card #18058) holds packages/spec/src/api/package-api.zod.ts + .test.ts; #18704's patch round (card #16066) holds packages/spec/src/data/ + packages/metadata-protocol/; #18729's patch round (card #18670) holds packages/spec/scripts/. ⚠️ The GENERATED packages/spec/api-surface/api.json is touched by #18752 and will be touched here too — that is ordinary concurrency, not a single-writer path (SINGLE_CLAIM_PATHS holds only .objectui-sha), so the later lander resolves it.
三面前提过时检查(
origin/main,本席自取)① 动作面:
packages/spec/src/api/automation-api.zod.ts末次触碰2f1a6f696,2026-09-13T11:39Z —— 早于今日 14:27Z 的裁决 ⇒ 被点名的动作仍未被做掉。② 卡引用面(逐个现读):#15222 closed / completed;#15358 closed / completed(裁决称它是「the read-only inspection member's card」);#17539 closed(PR,已并入)。三者都已了结,本卡不等任何人。
③ 工作项面(对树核验):
- LIT —— 引擎侧已经有
inspectConsumedSuspension:命中packages/plugins/plugin-approvals/src/approval-service.ts与packages/services/service-automation/(含两个专门的 stranded 测试文件)。⇒ 裁决那句「the engine already has it」成立。 - DARK ——
packages/spec/src/**内inspectConsumedSuspension0 命中 ⇒ 契约面尚未声明,工作项成立。 - 同题 LIT 对照 —— 同一探针在
packages/spec/src/**搜IAutomationService命中(api/automation-api.zod.ts等)⇒ 仪器够得着 spec 树,零是真缺席,⛔ 不是探针没扫到。
裁决分区(
5716043121,批次 #148 item 5,letter 1,维护者「同意」)IAutomationService声明只读成员inspectConsumedSuspension(runId);dev 先确认引擎侧既有签名,再照抄声明。resume 门在结果没有status时查询它,并在链可恢复时答repairable: true。⛔ 门永不探测一个未声明的成员(那正是它自己那条 501 分支要避免的 fail-open 形态)。automation-api.zod.ts:447的.describe()改写为真相;content/docs/references/api/automation-api.mdx用仓库自己的工具重生成;两处手写文档页按同一句话重读。- ⭐ 第一步(裁决指定):把该用例走 HTTP 路由驱动一遍,记录改动前后的
400响应体。引擎级读数不是线上读数。 - ⛔ 2(为一个出口新增一个所有客户端都要学的状态值)· ⛔ 3(正式接受一条告诉运维「别重试」而它其实能修的线)。
档位与复核
dispatch-gates --tierexit 0:无路径强制,档位归 PM 逐卡判断。裁决写明「Public interface member ⇒Clause-②: yes, contract review at tier;changeset@objectstack/specminor」⇒ 施工走默认判断档,交付后复核走CONTRACT_REVIEW_TIER的隔离子代理。
Generated by Claude Code
- LIT —— 引擎侧已经有
os-dev-report
{ "issue": 17541, "status": "done", "branch": "claude/issue-17541-resume-door-consults-inspection", "pr": "https://github.com/objectstack-ai/objectstack/pull/18792", "session": "session_01LvwGppdonww4zGLWZo5rho", "premise_still_valid": true, "summary": "The premise was re-measured on origin/main (f1c9bb3056) and REPRODUCED at the wire before any edit: POST /automation/parent_flow/runs/:parentRunId/resume on a parked subflow delegation answered 400 FLOW_FAILED with details {runId, repairable: false} while the same engine answered inspectConsumedSuspension -> {repairable: true, witness: 'journal'} and restoreConsumedSuspension -> {restored: true, chain: [child, parent]}. Implemented ruling letter 1: IAutomationService now declares the optional read-only inspectConsumedSuspension(runId) that AutomationEngine already implements (declared narrower than the implementation, on the restoreConsumedSuspension precedent, so the engine's wider type satisfies it under implements); the resume door consults that declared member ONLY when the engine stamped no status, relays its repairable, and is fail-closed both when the member is absent and when the read rejects (warn, never a 500 replacing the 400). ResumeFailureDetailsSchema.repairable's .describe() was rewritten to the truth, content/docs/references/api/automation-api.mdx was REGENERATED with the repo tooling (one-line diff), and both hand-written pages (automation/flows.mdx, api/client-sdk.mdx) carried the same false sentence and were corrected by hand. Changeset: @objectstack/spec minor + @objectstack/runtime minor, Clause-2: yes (widening). No new AutomationResult.status member (ruled-out option 2); the #15222 fence is untouched (an ancestor is still never STAMPED 'stranded'). Assignee was os-litant on arrival and I never wrote it. The dispatch's three ZONE-2 assumptions were all re-measured and all CONFIRMED.", "tests": "All readings from the final commit 228275a473. WIRE before/after (the ruling's step one, driven through HttpDispatcher.handleAutomation, not the engine): BEFORE details = {runId, repairable: false}; AFTER details = {runId, repairable: true}; both 400 FLOW_FAILED, both quoted in full in the PR body. ABLATION (behaviour): pre-change expression put back on the committed tree, on-disk proof (injected marker count 1, deleted call count 0, blob hash moved off HEAD's), rebuilt @objectstack/runtime, scripts/ablation-dist-preflight.mjs confirmed the marker PRESENT in 2 built dist files, then the pins ran: direction observed = TURNED RED and only there (1 failed | 4 passed, the failure being the DELEGATION wire case; both controls and the two pre-existing #15221 wire cases stayed green). Restore leg: git checkout HEAD -- path, whole-tree git status --porcelain clean, blob hash == HEAD's, marker count 0, rebuilt, --absent preflight clean over all 6 built files, suite green again (5 passed). ABLATION (types): AutomationEngine implements IAutomationService, so with ConsumedSuspensionInspection mutated to drop runId from its repairable arm, pnpm --filter @objectstack/service-automation exec tsc --noEmit exits 1 naming TS2416 on Property 'inspectConsumedSuspension'; restored it exits 0 - which is also the proof tsc reads the rebuilt spec .d.ts and not a cache. Tree restore proven by observed state (hash + porcelain), not by exit code. SUITES, exit codes read from $? after a redirect, never through a pipe, BOTH projects run where a package has two: spec test (--project local) 0 [486 files / 14017 tests]; spec test:repo (--project repo) 0 [31 files / 536 tests]; runtime test (local) 0 [266 files / 3667 passed, 1 skipped]; runtime test:repo (repo) 0 [2 files / 69 tests]; verify test 0 [110 tests]; spec typecheck 0; runtime typecheck 0; verify typecheck 0; service-automation typecheck 0; pnpm --filter '@objectstack/verify^...' build 0. GATES (all exit 0): spec check:generated (15/15 up to date after the regeneration), check:nul-bytes, check-adr-0087-registration --base origin/main, check-changeset-no-major --base origin/main, check-empty-changeset --base origin/main, check:objectui-changeset, check:pm-widening-tells, check-closing-keyword-parity, check-spec-docblock-symbol-anchors, check-doc-frontmatter, check:docs-single-h1, check:doc-anchors, check-docs-section-name, docs-audit/check-affected-docs, check:route-envelope, check:dispatcher-error-vocabulary, check:cross-package-test-inputs, check:test-source-alias, check-undeclared-dep-imports, check:tier-file-adoption, check:type-check-coverage, check:type-check-debt. The three changeset gates first exited 1 with 'no merge base between origin/main and HEAD' - a shallow-clone PREREQUISITE NOT MET, not a verdict; git fetch --deepen 300 origin main restored the merge base (f1c9bb3056) and all three then exited 0. LINT: not a narrowing - the WHOLE population ran. node --stack-size=4000 node_modules/eslint/bin/eslint.js . --no-inline-config --format json exits 0 over 6834 files (count read from the JSON output), 0 errors / 0 warnings. NOT MEASURED: the rest of CI's farm - dispatch-gates --commands derives 107 runnable commands plus 50 artifact-roster families, 10 declared-wide families, 5 workflow-valued commands and 6 path-scheduled CI jobs (Test Core, Temporal Conformance, Dogfood x2, Build Core, Build Docs), none of which has a local invocation. Also NOT MEASURED: CI convergence on PR 18792 itself - reported at local-verification completion per the dispatch contract. CONTRACT-REVIEW READING (not an act - I neither hung nor removed the label): card #17541 carries needs:contract-review, PR 18792 does NOT; node scripts/pm/check-clause2-carriers.mjs --pair 18792 exits 4 with row C1 (half-hung dual carrier). PR labels after the auto-labeler: documentation, size/l, tests, tooling.", "mcp_calls": "0 - no MCP GitHub tool was called, read or write; every GitHub touch was the REST proxy with curl", "api_writes": "2 REST writes: POST /repos/objectstack-ai/objectstack/pulls (draft PR 18792); POST /repos/objectstack-ai/objectstack/issues/17541/comments (this os-dev-report). Plus 3 git pushes to the feature branch (empty-branch route probe, implementation commit, docs+changeset commit). POST /issues/{n}/labels was NOT spent: the only label mechanically in play here is needs:contract-review, which the dispatch reserves to the seat, and skip-changeset does not apply because this PR ships a changeset.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: registerSubflowNode is exported from packages/services/service-automation/src/builtin/index.ts but is NOT re-exported from that package's root index.ts, while five sibling registrars are. Not a defect class - nothing is broken and installBuiltinNodes reaches the same executor (that is what the new wire test uses). Next party to meet it: an out-of-package test author who wants the subflow node alone.", "noted, not filed: ResumeFailureDetailsSchema.status's own .describe() names 'a subflow child that failed terminally' as the status-less example and does not also name the delegation frame. Incomplete, not false, and the ruling named only the repairable describe - an omission is not class (a). Next party to meet it: whoever next edits that enum's text." ] }
Generated by Claude Code
✅ 已落地 —— PR #18792 MERGED,squash
182bbde36adomain:spec席位,session_01LvwGppdonww4zGLWZo5rho,2026-09-17T22:25Z。判据取自主记录:在取回的
origin/main上grep -F '(#18792)'命中 1;LIT 对照(#18729)= 1、(#18724)= 1、(#18676)= 1,DARK 对照(#99999999)= 0,窗口 500 个提交。⛔ 未用 API 的merged字段。⚠️ 一条仪器记录:等待器的倒数第二跳报 「BOTH LIT controls read 0 — the window is a dud, this run proves nothing」。那是一次 fetch 静默失败,不是「还没合并」。⭐ 这正是本班次早些时候修好的那个坏掉:旧模板--depth=30取、-400读,一旦参照 PR 落后超过 30 个提交,LIT 对照就变成哑的,而读数看起来完全正常。现在双对照全 0 会自报哑掉而不是报「未合并」。落地前三条前检的读数(逐条,现读)
读数 ① 席内条款②复核 PASS 在案 记录 5721708429,同形,Head-sha独占码段指228275a473,VERDICT: PASS档位核验 自 grep 复核轮转录的 harness 盖戳 message.model字段:199/199 在档,0 脱档。⛔ 不读派发时传的model参数② --pair 18792exit 0,双载体一致,记录落在本 head 上 ③ 受管面 check-governed-merges --pr objectstack-ai/objectstack#18792⇒ 0 / 9 命中登记册,普通队列落地入队资格 36 success / 7 预期 skip / 0 failure;四类 skip 全在 check-expected-skips.mjs名单上⚠️ 一条 NOT MEASURED,写明而不含糊:check-expected-skips.mjs本身在本容器跑不起来(无node_modules,yaml缺失,exit = ERR_MODULE_NOT_FOUND)。⇒ 那四类 skip 是本席直接读该脚本源码里的名单判的(:235:242:257:307),⛔ 不是跑出来的。Auto Label/Check PR Size各一 success 两 skip,与该文件:89-90docblock 描述的「PR body 被编辑过的 PR 带三次PR Automation运行」完全吻合。生成物路径的合并驱动:危险为什么不适用
本 PR 触碰一条 os-regen 路由路径(
content/docs/references/api/automation-api.mdx,由.gitattributes:153的content/docs/references/**命中)。实测 main 自合并基以来动过的 os-regen 路由路径 = 0 条(匹配器的阳性对照:本 PR 自己那条路径命中)。⇒ git 根本不会为任何路径调起该驱动 —— 「静默丢掉一侧」需要两侧都动过同一条路由路径。⛔ 不是「我觉得没事」,是驱动不会被调起。复核留下的两条边界旗,⛔ 未在本 PR 处理
packages/client/src/index.ts:5551的 JSDoc 仍带那句已退役的散文,随@objectstack/client的.d.ts作为悬停文档发运,不投影进content/。裁决点名的是.describe()、重生成的参考页与两张手写页,不含它 ⇒ 非阻塞,归一张 docs-only 后续卡。- spec 的
IAutomationService.inspectConsumedSuspension?与 plugin-approvals 自己的ApprovalResumeSurface.inspectConsumedSuspension?现在并存且不可互相赋值(reason?: stringvs 必填字面量联合)。今天没有任何一处跨赋值;留给下一个让 plugin-approvals 改用 spec 声明的人。
Generated by Claude Code
github-actions commented
on Sep 18, 2026 on Sep 18, 2026 – with GitHub ActionsContributorMore actionsos-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: fix(runtime,spec): the resume door asks the engine whether a status-less exit is repairable #18792, merged.
- Closing commit
182bbde36a, merged intomain. - Left untouched:
bug,priority:p2,domain:spec— 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 35297049699 · trigger
scheduleGenerated by Claude Code
- added 3 commits that reference this issue
on Sep 28, 2026
Found by the
domain:servicesexecution seat (sessionsession_01ToDPcx9AESFubJkDiFMtKW) while delivering #15222 as PR #17539, answering the docs-drift-check bot's emitter-blind half. ⛔ Not fixed there: the honest repair needspackages/spec, which that lane may not touch. ⛔domain:*, type and priority are triage's — this seat does not produce them.Introduced by PR #17539, not pre-existing. Before it,
repairable: falsewas the truthful answer on this exit. Stating that plainly so nobody spends a lap looking for it on an older tree.The shape
The resume route's
400 FLOW_FAILEDcomputesrepairableas one expression —packages/runtime/src/domains/automation.ts:1416:statusisAutomationResult.status, forwarded verbatim and never synthesised. On the subflow delegation exit the engine stamps no status at all: a caller resumes the PARENT run id,resumeInternalforwards the signal down to the child, the child strands, and the parent frame answers{ success: false, error, durationMs }— deliberately status-less, because nothing re-arms an ancestor by resuming it and stamping'stranded'there would send an operator to retry a recovery that cannot succeed (the fence #15222 was dispatched with).After #15222 that parent's consumed pause is journalled, and
restoreConsumedSuspension(parentRunId)re-arms it — together with the leaf, as one chain. So the wire now saysrepairable: falseabout a run the operator verb will repair, and a client written exactly as the docs instruct closes it as terminal.Why it is a contract violation and not only a wire gap
ResumeFailureDetailsSchema.repairable's own.describe()—packages/spec/src/api/automation-api.zod.ts:447— states the reason in the schema itself:The verb no longer refuses on this exit, so the stated justification is false for it. That text projects verbatim into
content/docs/references/api/automation-api.mdx:531(AUTO-GEN, never hand-edited), and the hand-writtencontent/docs/api/client-sdk.mdxandcontent/docs/automation/flows.mdxboth restate it for authors.Reproduction
The composition is already committed by PR #17539 and needs no new fixture —
packages/services/service-automation/src/nested-strand-chain-restore.test.ts, the test named "DELEGATION — the parent frame carries NO status, and the chain is still restorable from either end". It measures both halves that make this card:frame.statusisundefined(so the door computesrepairable: false);restoreConsumedSuspension(parentRunId)answersrestored: trueand re-arms the chain.What is NOT yet measured, and is the first step for whoever takes this: drive it through the HTTP route rather than through the engine, and record the
400body. ⛔ Do not treat the two engine-level readings above as the wire measurement.Why it is not fixable in the
domain:serviceslaneBoth candidate repairs move
packages/spec:inspectConsumedSuspension(runId)when the result carries no status. That member is not declared onIAutomationService(measured:git grep -n inspectConsumedSuspension -- packages/spec/srcreturns nothing), so declaring it is a spec change — and a door that probes an undeclared member is the fail-open shape the restore door's own 501 arm exists to avoid.AutomationResult.statusmember for "cascade-consumed but repairable" is spec vocabulary. ⛔ It must not be'stranded': that word means a run that consumed its OWN pause and then threw, and service-automation:restoreConsumedSuspensioncannot reach a nested run — a stranded child's cascade-failed ancestors are consumed without a snapshot, so restoring the child continues into a dead parent #15222's dispatch fenced the reuse explicitly.Either way the
.describe()atautomation-api.zod.ts:447needs rewriting andcontent/docs/references/api/automation-api.mdxregenerating with it, so the decision and the edit belong to thedomain:specseat. There is also a plain third option — decide the under-report is acceptable and rewrite only the.describe()— which is equally a spec decision.Scope note
Only the delegation exit is affected. On the up-bubble path the caller resumes the child, whose result does carry
status: 'stranded', sorepairable: trueis answered correctly and the ancestors are not the resumed run at all.Refs
#15222 / PR #17539 (the change that introduces it) · #15221 (the door dropping
status— closed, the mechanism this rides on) · #15358 (the read-only inspection member) · #13937 (the shape-4 ruling and the'stranded'stamp) · #15556 (the sibling divergence one level up: a decision door answering 200 while the run behind it is stranded)Blocked-by: #15222
Generated by Claude Code