Uh oh!
There was an error while loading. Please reload this page.
feat(spec): name the terminally-failed-but-repairable run 'stranded' on AutomationResult.status (#14384) - #14636
Conversation
…onResult.status as 'stranded' Contract half of the #13937 shape-4 ruling (maintainer 2026-09-01): the run whose resume consumed its suspension and then had a downstream node throw is recorded as failed and can be re-armed only by an explicit operator verb. `AutomationResult.status` now carries `'stranded'` beside `'completed' | 'paused' | 'failed'`, the wire mirror `TriggerFlowResponseSchema.data.status` carries the same four, and `contracts/automation-result-status.pin.test.ts` binds the two at the type level and the value level and reads the JSDoc that names the condition. No engine, route or client behaviour changes; plugin-approvals' report-only `StrandedRunState` is deliberately not promoted. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RbbUMnxkUnWhE4j94v8FE
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin fcdd5f04c1fffa48b2df2a94e9348af681e8c85a && git checkout fcdd5f04c1fffa48b2df2a94e9348af681e8c85a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d8024f050e1f457117f914d6960a204d1af03966 2c4bc2903701f758f1c6c5aa107a3c597888e9de && git checkout -B drift-repro d8024f050e1f457117f914d6960a204d1af03966 && git merge --no-ff 2c4bc2903701f758f1c6c5aa107a3c597888e9de
node scripts/docs-audit/affected-docs.mjs --json d8024f050e1f457117f914d6960a204d1af03966
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#14384
Contract half of the #13937 shape-4 ruling (director seat, comment 5491060439; maintainer 2026-09-01 「其他同意」), quoted verbatim on the card and untranslated here:
What changed — one contract increment
packages/spec/src/contracts/automation-service.ts—AutomationResult.statusis now'completed' | 'paused' | 'failed' | 'stranded', with a JSDoc paragraph naming the condition: a resume CONSUMED the suspension, a downstream node threw, the run is recorded as failed and can be re-armed only by an explicit operator verb (never byresume, which answersRUN_NOT_FOUNDfor it, and never automatically). Distinct from'failed'on purpose: that one says the run ran and was rejected; this one says a recorded continuation stopped mid-flight and an operator has something to repair. The JSDoc also states that plugin-approvals'StrandedRunStateis NOT promoted.packages/spec/src/api/automation-api.zod.ts— the wire mirrorTriggerFlowResponseSchema.data.statuscarries the same four (the existingTriggerFlowDataMatchesContractpin goes red if only one side moves; see the reverse verification). The.describe()prose names the condition without an issue id:check:doc-authoringrefused the first spelling (customer-facing text must not carry#NNNN), so the card references moved to an adjacent line comment.packages/spec/src/contracts/automation-result-status.pin.test.ts(new) — pins, each on its own: (1) the union's exact membership at the type level (anAssertover anEqidentity, theautomation-api.zod.test.tsform, compiled bycheck:test-typecheck, plus asatisfieslist); (2) wire-to-contract parity at both levels (the Zod enum's.optionsread at runtime, and its inferred type bound to the contract's); (3) that the doc block above the declaration names the condition (source read); (4) a stranded envelope parses AND is preserved through the strip-mode object; (5) a literal outside the four is refused atdata.statuswithinvalid_value.content/docs/references/api/automation-api.mdx— regenerated bygen:docs(check:generated --fix; the one artifact it proved stale).api-surface/is unchanged: the snapshot records thatAutomationResultexists, not its members..changeset/automation-result-stranded-status.md—@objectstack/spec: minor, additive (no BREAKING declaration, so no ADR-0087 marker is owed;check:adr-0087-registrationexit 0).Not in this PR, by the card: no engine change, no operator verb, no REST route, no
ExecutionStatuswidening. Those are #13937 (services half, stays open) and #13953.The literal — what was measured before choosing
Vocabularies read (
packages/spec/src,packages/services/service-automation,packages/plugins/plugin-approvals):AutomationResult.status(this union)completedpausedfailedExecutionStatus(automation/execution.zod.ts, the run-row vocabularylistRunsfilters on)pendingrunningpausedcompletedfailedcancelledtimed_outretryingTaskStatus(system/worker.zod.ts)pendingqueuedprocessingcompletedfailedcancelledtimeoutdeaddead= moved to the dead-letter queueJobExecutionStatus(system/job.zod.ts)runningsuccessfailedtimeoutdegradedStrandedRunState(plugin-approvals, report-only)missingfailedinspectStrandedRequests,StrandedApprovalRequest; 8 comment sites inengine.tsCandidates:
'stranded'— chosen. The word the whole measured lineage already uses for exactly this condition (service-automation: a resume consumes the pause BEFORE running downstream nodes, so any node that throws leaves the run terminally unresumable — and the only inspector for it reports all clear #13909 itself: "There is no run state calledstranded"); a single-word participle beside its three siblings; it names the operator-facing STATE, which survives a later shape-2 re-ordering, rather than the mechanism. It is not a promotion ofStrandedRunState: that type's members are'missing' | 'failed', it stays plugin-local and report-only, and the JSDoc says so.'dead'— the one in-spec precedent (TaskStatus), but there it means retries exhausted and dead-lettered; here nothing was retried, and "dead" reads as final where the ruling's whole point is re-armable.'failed_resume'/'resume_failed'— snake_case is the house shape for two words (timed_out), but both name the mechanism (the consumption ordering that [Decision] Workflow resume ordering: a thrown node today leaves the run terminally unresumable — which of three shapes, given that the current order buys exactly-once across a crash? #13937 may still change under shape 2) rather than the state an operator acts on.'unresumable'— false the moment the re-arm verb exists.'suspended'— the spec uses it for admin-suspended tenant / license / environment, and it collides with "suspension", which is the PAUSE.'stalled'/'stuck'/'halted'/'incident'— zero uses in the three trees;incidentis a noun (Camunda's shape), not a participle.Producer and consumer measurements
AutomationResult.statusis a TypeScript interface member, not Zod — verified. Its one Zod mirror inpackages/spec/srcisTriggerFlowResponseSchema.data.status; both updated, parity pinned.AutomationEngine.resumeInternal's catch arm records the run ROWfailedand returns{ success: false, error, durationMs, errorMessage, summary }— nostatusat all on the resumed-failure envelope (onlyexecute()'s dispatched-and-rejected exit stampsstatus: 'failed'). So stamping'stranded'on the result is the services half's, and whether the run ROW (ExecutionStatus) ever carries the word is that half's to measure; this PR does not widenExecutionStatus.AutomationResult.statusinpackages/{runtime,services,plugins,rest,cli,client}: zero. The onlycase 'completed' | 'paused' | 'failed'outside spec is plugin-approvals'classifyStrandedRunState, which switches over the run ROW's status string with a deliberately silentdefault, not over this union. Narrow re-declarations of the three-member union outside spec: zero in code (two in comments,engine.ts:4182andapproval-service.ts:315).app-shell/src/utils/flowResponse.ts:108typesstatus?: 'completed' | 'paused' | 'failed' | stringand branches onsuccess === false— tolerant of the new literal; nothing to change there for this half.Verification — all readings on head 2c4bc29, exit codes captured before any pipe
@objectstack/spec:pnpm --filter @objectstack/spec build— exit 0 (check-dts-emitted: 34/34 declared declaration file(s) present).pnpm --filter @objectstack/spec test(whole suite) —Test Files 453 passed | 1 skipped (454) · Tests 12214 passed | 1 skipped (12215)on the committed head. The one skip isscripts/root-entry-type-nameability.pin.test.ts: itsinspectDistFreshnesspredicate sawdistolder thansrcbecause the reverse verification below touched the contract file's mtime after the build (content byte-identical), and that pin skips by default on a stale dist. After rebuilding the dist that file re-ran on the same head:Test Files 1 passed (1) · Tests 2 passed (2);check:api-surfaceon the rebuilt dist: exit 0. The earlier full run on the pre-.describe()-edit tree with a fresh dist was454 passed · 12216 passed, 0 skipped.pnpm --filter @objectstack/spec typecheck— exit 0;check:test-typecheck: OK — 54 file(s) / 262 error(s) / 146 pinned signature(s)(ledger unchanged; the new pin file compiles undertsconfig.test.json).contracts/automation-service.test.ts,api/automation-api.zod.test.ts) viaexec vitest run --maxWorkers=2—Test Files 3 passed (3) · Tests 61 passed (61).check:generated— exit 0 after--fixregenerated the one stale artifact;check:api-surfaceexit 0; the 15 spec-scoped gatesdispatch-gates.mjsderived (api-surface, authorable-surface, browser-reachable-entries, docs, dual-source-exports, empty-state, entry-nameability, export-origins, exported-any, liveness, llms-txt, skill-refs, strictness-ledger, variant-docs, yaml-examples) — every one exit 0.Root gates, derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackfrom the merge-base change set (the derivation named this checkout at a5b95ee):@objectstack/lint check:doc-formula-expressions(exit 0 once the closure was built),pnpm check:doc-authoring(14504 customer-facing string(s) across 710 spec sources clean),pnpm check:nul-bytes(scanned 7981 text file(s) … no raw ASCII control bytes).--re-measureneeds the full ledgered closure), check-dev-prereqs (exit 1: asks for a fullpnpm build), pm/check-half-states (timed out at 240 s on the GitHub network leg — a PM-side gate). CI owns these.Consumers — filter direction is DOWNSTREAM of spec; their dependency closures were built first (
turbo run buildover the^...of each, 55 tasks, exit 0):@objectstack/runtime—typecheckexit 0; the 6 test files that readAutomationResult(flow-dispatch-status,automation-trigger-route-status,automation-trigger-paused-run,action-activation-dispatch,endpoint-flow-dispatch-status,actions-flow-dispatch-status) —Test Files 5 passed (5) · Tests 81 passed (81)(one pattern matched no file on its own; narrowed to these files, declared).@objectstack/service-automation— has notypecheckscript (types NOT MEASURED here; the package is ledgered bycheck:type-check-coverage, exit 0); the 9 test files that readAutomationResult—Test Files 9 passed (9) · Tests 239 passed (239)(narrowed, declared).@objectstack/plugin-approvals—typecheckexit 0 (check:test-typecheck: OK); wholetest—Test Files 36 passed (36) · Tests 662 passed (662).@objectstack/client—typecheckexit 0 (check:test-typecheck: OK); wholetest—Test Files 32 passed (32) · Tests 420 passed (420).@objectstack/verify—typecheckexit 0;automation-trigger-terminal-messages.test.ts—Test Files 1 passed (1) · Tests 4 passed (4).@objectstack/rest—typecheckexit 0 (check:test-typecheck: OK);@objectstack/cli—typecheckexit 0. Neither importsAutomationResult.@objectstack/dogfood— NOT MEASURED locally (its closure is the whole app); CI.Reverse verification — from the committed state
Mutation leg: the contract union narrowed back to three members on disk (confirmed by anchor counts: four-member 0 / three-member 1;
git diff --stat1 file, 1 insertion, 1 deletion), thentsc --noEmit -p packages/spec/tsconfig.test.json: exit 2 with 266 error lines against the ledgered 262 on the restored tree — the 4 new ones name exactly the two pins:src/api/automation-api.zod.test.ts(52,54)(TriggerFlowDataMatchesContract, TS2344) andsrc/contracts/automation-result-status.pin.test.tsat lines 64 (TS2322,'stranded'not assignable), 72 and 74 (TS2344). Direction: red, as predicted. Restore leg:git checkout HEAD -- FILEfrom a trap with an absolute path, proven bygit diff HEAD --quietexit 0 andgit hash-objectequal to the HEAD blob85b3d15a….Review tier
needs:contract-review— clause ②, contract tier, as the ruling says in as many words. Draft; no seat marks it ready or arms auto-merge.🤖 Generated with Claude Code
Generated by Claude Code
Generated by Claude Code