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
feat(runtime, spec): the resume door's 400 FLOW_FAILED details carry the engine's stranded verdict (#16587)
* feat(runtime, spec): the resume door's 400 FLOW_FAILED details carry the engine's stranded verdict
`POST /automation/:name/runs/:runId/resume` copied `errorMessage` and
`summary` off the engine result and dropped `status`, so
`AutomationResult.status: 'stranded'` (terminally failed but repairable by an
operator verb) reached the wire as the same 400 FLOW_FAILED a plain terminal
failure does. The #16472 family ruling (option A): carry `status` and
`repairable` in the details of the existing code, no FLOW_STRANDED sibling.
- spec: `ResumeFailureDetailsSchema` (`@objectstack/spec/api`) declares the
structure once — `{ runId, status?: 'failed' | 'stranded', repairable }`.
- runtime: the resume door forwards `status` verbatim when the engine stamped
one, names the resumed run as `runId`, and answers `repairable` as
`status === 'stranded'` — always present on this arm, present-and-false on
the plain terminal exit. Trigger door and /actions unchanged.
- client: `automation.resume` docblock; docs: flows.mdx, client-sdk.mdx.
- pins: spec schema + type-level subset pin; runtime door pins (fake engine,
every arm); verify wire pin through the real engine.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
* chore(spec): regenerate the artifacts the new ResumeFailureDetailsSchema export moves
api-surface, export-origins, declaration-map, the generated api reference
page, and the unknown-key strictness ledger count (450 -> 451 in api/) —
each regenerated by `check:generated --fix`, only the five it proved stale.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
* docs(permissions): re-anchor the system-context census rows the resume-door helper moved
Pure line rot: the helper and its import shift four `ec.isSystem` read
sites in domains/automation.ts; rewritten by the gate's own --fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
* chore(changeset): grade @objectstack/client minor — a clause-② PR may not grade a package it grew as patch
Check Changeset's finding on #16587: the PR declares clause-② yes and moved
packages/client/src/**, and the 2026-09-04 ruling (decision batch #35, on
#15294) binds per PR — at least `minor` for a package whose public surface
this PR moved, whatever the commit type says.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
* fix(runtime): bind the relayed status to the published enum — a guard on the two terminal-failure members, and the /actions negative pin
Contract-review follow-ups on #16587: resumeFailureDetails now returns
ResumeFailureDetails, relaying status through a guard on 'failed' |
'stranded' (satisfies-bound to the schema's enum) so the compile-time
binding is true by construction — still a relay, never a synthesised
verdict. actions-flow-dispatch-status.test.ts gains the exact-equality
negative pin the docblock claimed for /actions, and the docblock now names
both pin files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
* docs(permissions): re-anchor the census row the terminal-failure guard moved
Pure line rot again: the guard, its constant and their docblocks sit above
the anonymous-deny read in domains/automation.ts, shifting it :1057 -> :1079;
rewritten by the gate's own --fix, population unchanged (106 sites / 141 anchors).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
The automation resume route's `400 FLOW_FAILED` now says whether the run is stranded.
8
+
9
+
`POST /api/v1/automation/:name/runs/:runId/resume` answers a run that consumed its pause and then failed with `400 FLOW_FAILED`, and until now its `error.details` carried the run's two artefacts only (`errorMessage`, `summary`). The engine's own verdict was dropped at the door: `AutomationResult.status: 'stranded'` — a run that is terminally failed *but* repairable by an explicit operator verb, because the pause a durable decision was waiting on is gone with the failure — reached the wire as the same `400` a plain terminal failure does, so an HTTP-only caller could not tell "beyond reach" from "repair waiting".
10
+
11
+
-**`@objectstack/spec`** declares `ResumeFailureDetailsSchema` (`@objectstack/spec/api`): `{ runId, status?: 'failed' | 'stranded', repairable }` — the machine-readable shape of a resume failure told to the caller, declared once so every carrier of the family ruling spells the same members.
12
+
-**`@objectstack/runtime`**: the resume door's `400 FLOW_FAILED` details now carry that structure beside `errorMessage` / `summary`. `runId` is the run the resume was addressed to; `status` is the engine's own stamp, forwarded verbatim when it set one and never synthesised (the subflow-child-failed exit stamps none today); `repairable` is `status === 'stranded'` and is **always present on this arm** — present-and-false on a plain terminal failure, deliberately, so an absent member reads as an older server rather than as "not repairable". The code stays `FLOW_FAILED` (no `FLOW_STRANDED` sibling is minted), so a client that treats it as terminal keeps working and one that wants to offer a repair branches on `details.repairable`, never on the message text. The trigger door and `/actions` are unchanged: they never resume, so the member is absent there and absent means "not a resume".
13
+
-**`@objectstack/client`**: `automation.resume` documents the new members.
| 50 |`manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability |`domains/meta.ts:471`, `:874`, `rest-server.ts:5520`, `:6977`, `:7225`, `:7656`, `:7849`|
162
162
| 51 | The shared metadata-write verdict itself returns `allowed`| metadata-core | Get: the one function all of row 50's doors consult answers yes before any capability is examined |`meta-write-capability.ts:134`|
163
-
| 52 | Anonymous-deny seam satisfied on the domain dispatchers and the package/federation routes | runtime, rest | Get: passes with no `userId`|`domains/actions.ts:421`, `domains/ai.ts:60`, `domains/automation.ts:989`, `domains/meta.ts:232`, `domains/security.ts:78`, `domains/packages.ts:552`, `external-datasource-routes.ts:302`, `package-routes.ts:97`|
163
+
| 52 | Anonymous-deny seam satisfied on the domain dispatchers and the package/federation routes | runtime, rest | Get: passes with no `userId`|`domains/actions.ts:421`, `domains/ai.ts:60`, `domains/automation.ts:1079`, `domains/meta.ts:232`, `domains/security.ts:78`, `domains/packages.ts:552`, `external-datasource-routes.ts:302`, `package-routes.ts:97`|
164
164
| 53 | MCP principal check satisfied | runtime | Get: MCP surface reachable with no user |`domains/mcp.ts:61`|
| 55 | Package domain capability gates bypassed | runtime | Get: package management and package-inventory reads without the capability |`domains/packages.ts:250`, `:283`|
167
167
| 56 | Activation write / authoring refusals do not fire | runtime | Get: activation artifacts writable and authorable without the activation-authoring capability |`activation-gate.ts:177`, `:268`|
168
-
| 57 | Automation run-state read, flow-authoring write and unrelated-screen read all pass | runtime | Get: run state, flow writes and screen reads with no grant |`domains/automation.ts:254`, `:545`, `:635`|
168
+
| 57 | Automation run-state read, flow-authoring write and unrelated-screen read all pass | runtime | Get: run state, flow writes and screen reads with no grant |`domains/automation.ts:255`, `:546`, `:636`|
169
169
| 58 | Audience-binding suggestion recording skipped | plugin-security | Lose: install-time suggestions are not recorded for system callers |`suggested-audience-bindings.ts:703`|
170
170
| 59 | Email-template / webhook provenance stamps skipped | plugin-email, plugin-webhooks | Lose: the row is not marked as an admin customization |`email-template-provenance.ts:77`, `webhook-provenance.ts:68`|
171
171
| 60 |**Automation flow data nodes re-add the `owner_id` stamp** (the one place row 2's gap is compensated inline) | service-automation | Get: a flow-authored INSERT under system elevation still lands owned, when the run resolved a user. Fill-only — flow-authored values win |`runtime-identity.ts:279`, called from `builtin/crud-nodes.ts:319`|
@@ -511,6 +511,19 @@ const result = AutomationApiErrorCode.parse(data);
511
511
|**hasMore**|`boolean`| ✅ | Whether more runs are available |
512
512
513
513
514
+
---
515
+
516
+
## ResumeFailureDetails
517
+
518
+
### Properties
519
+
520
+
| Property | Type | Required | Description |
521
+
| :--- | :--- | :--- | :--- |
522
+
|**runId**|`string`| ✅ | The run the resume was addressed to - the run that failed, and on the `stranded` arm the run an operator verb can re-arm. Named so a caller acts on an identifier instead of parsing one out of the message |
523
+
|**status**|`Enum<'failed' \| 'stranded'>`| optional | The engine's own lifecycle verdict for the run, forwarded verbatim when the producer stamped one and never synthesised by the door - absent when the engine reported no status (a subflow child that failed terminally, an engine that predates the discriminator). `stranded` is the terminally-failed-but-repairable run of `AutomationResult.status`; `failed` says the run ran and was rejected. These two terminal-failure members of that union are the only ones that can reach a 400 |
524
+
|**repairable**|`boolean`| ✅ | Whether the engine says this run can still be re-armed by an operator verb - `true` exactly when `status` is `stranded`, derived from the engine's discriminator and never from the message text. 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 |
|[Automation Protocol](/docs/references/automation)| 13 | 73 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
0 commit comments