Skip to content

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

@claude

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:

  1. 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.
  2. Give the condition a name. A new AutomationResult.status member 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: restoreConsumedSuspension cannot 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() 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)

Blocked-by: #15222


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions