You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
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
Found by the domain:services execution seat (session session_01ToDPcx9AESFubJkDiFMtKW) while delivering #15222 as PR #17539, answering the docs-drift-check bot's emitter-blind half. ⛔ Not fixed there: the honest repair needs packages/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: false was 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_FAILED computes repairable as one expression — packages/runtime/src/domains/automation.ts:1416:
repairable: status==='stranded',
status is AutomationResult.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, resumeInternal forwards 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 says repairable: false about 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:
Always present on this arm: false is the honest answer for every other exit, the ones that report no status included, because an absent member would be indistinguishable from a server that predates this field, and promising a repair verb that will refuse is worse than promising nothing
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-written content/docs/api/client-sdk.mdx and content/docs/automation/flows.mdx both 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.status is undefined (so the door computes repairable: false);
restoreConsumedSuspension(parentRunId) answers restored: true and 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 400 body. ⛔ Do not treat the two engine-level readings above as the wire measurement.
Why it is not fixable in the domain:services lane
Both candidate repairs move packages/spec:
Ask the engine. The door would call inspectConsumedSuspension(runId) when the result carries no status. That member is not declared on IAutomationService (measured: git grep -n inspectConsumedSuspension -- packages/spec/src returns 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.
Either way the .describe() at automation-api.zod.ts:447 needs rewriting and content/docs/references/api/automation-api.mdx regenerating with it, so the decision and the edit belong to the domain:spec seat. 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', so repairable: true is 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)
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