Skip to content

Commit 68437d4

Browse files
os-salesclaude
andauthored
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>
1 parent e692515 commit 68437d4

19 files changed

Lines changed: 791 additions & 13 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/runtime": minor
4+
"@objectstack/client": minor
5+
---
6+
7+
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.

content/docs/api/client-sdk.mdx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,37 @@ try {
750750
}
751751
```
752752

753+
**The resume door says one thing more.** `client.automation.resume()` rejects
754+
with the same `400` `FLOW_FAILED` when a run consumed its pause and then failed
755+
— and there the `details` also carry the engine's verdict, because one of those
756+
failures is *stranded*: terminal like any failure, but the pause a durable
757+
decision was waiting on is gone with it and only an explicit operator verb can
758+
re-arm the run. The shape is `ResumeFailureDetailsSchema` from
759+
`@objectstack/spec/api`; branch on it, never on the message text:
760+
761+
```typescript
762+
import { ResumeFailureDetailsSchema } from '@objectstack/spec/api';
763+
764+
try {
765+
await client.automation.resume('order_approval', runId, { inputs });
766+
} catch (err) {
767+
if (!isApiError(err) || err.code !== 'FLOW_FAILED') throw err;
768+
const verdict = ResumeFailureDetailsSchema.safeParse(err.details);
769+
if (verdict.success && verdict.data.repairable) {
770+
// verdict.data.status === 'stranded' — verdict.data.runId names the run
771+
// an operator can re-arm; offer that instead of closing as terminal.
772+
}
773+
}
774+
```
775+
776+
`repairable` is always present on the resume door's `400``false` on a plain
777+
terminal failure, deliberately, so an absent member reads as an older server
778+
rather than as "not repairable". `status` is the engine's own stamp
779+
(`'stranded'` or `'failed'`), forwarded when the engine set one and never
780+
invented by the door. The code stays `FLOW_FAILED`: there is no `FLOW_STRANDED`
781+
sibling, and the trigger door's `400` carries `errorMessage` / `summary` only —
782+
it never resumes, so "repairable" has no referent there.
783+
753784
---
754785

755786
## Configuration

content/docs/automation/flows.mdx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,32 @@ POST /api/v1/automation/{flow}/runs/{runId}/resume
688688
segment](#retry-pause-boundary).
689689
</Callout>
690690

691+
### A resume that fails downstream says whether the run is stranded
692+
693+
The suspension is consumed *before* the downstream nodes run (that is what buys
694+
exactly-once across a crash), so a node that throws after the resume leaves a
695+
run that is terminal **and** whose pause is gone. The engine names that state
696+
`status: 'stranded'` — distinct from a plain `failed`, because an explicit
697+
operator verb can still re-arm it — and the resume route forwards the verdict
698+
in the `400` `FLOW_FAILED` details, beside the author's `errorMessage` and the
699+
per-node `summary`:
700+
701+
```json
702+
400 { "error": { "code": "FLOW_FAILED", "message": "Node 'tail' failed: …",
703+
"details": { "runId": "run_42", "status": "stranded", "repairable": true,
704+
"errorMessage": "", "summary": { } } } }
705+
```
706+
707+
Branch on `details.repairable`, never on the message: it is `true` exactly when
708+
`status` is `stranded`, and it is **always present** on this route's `400`
709+
`false` on a plain terminal failure, so an absent member reads as an older
710+
server rather than as "not repairable". `status` is the engine's own stamp,
711+
forwarded when it set one and never invented by the route. The code does not
712+
change (there is no `FLOW_STRANDED`), and the trigger route's `400` carries
713+
`errorMessage` / `summary` only — it never resumes, so the question has no
714+
referent there. The shape is `ResumeFailureDetailsSchema` in
715+
`@objectstack/spec/api`.
716+
691717
### Who may resume — the gate is the suspended node
692718

693719
The resume route is generic, so **the node the run is parked on** decides

content/docs/permissions/system-context.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,12 @@ The largest single consumer — **17 of the 106 sites**.
160160
| 49 | Action `requiredPermissions` bypassed | runtime | Get: engine self-invocation runs any action | `action-execution.ts:401` |
161161
| 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` |
162162
| 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` |
164164
| 53 | MCP principal check satisfied | runtime | Get: MCP surface reachable with no user | `domains/mcp.ts:61` |
165165
| 54 | Package REST route capability gate bypassed | rest | Get: package read/write over REST without `manage_metadata` / `studio.access` / `setup.access` | `package-routes.ts:102` |
166166
| 55 | Package domain capability gates bypassed | runtime | Get: package management and package-inventory reads without the capability | `domains/packages.ts:250`, `:283` |
167167
| 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` |
169169
| 58 | Audience-binding suggestion recording skipped | plugin-security | Lose: install-time suggestions are not recorded for system callers | `suggested-audience-bindings.ts:703` |
170170
| 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` |
171171
| 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` |

content/docs/references/api/automation-api.mdx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ GET /api/automation/:name/runs/:runId — Get single execution run
3030
## TypeScript Usage
3131

3232
```typescript
33-
import { AutomationApiErrorCode, AutomationFlowPathParamsSchema, AutomationRunPathParamsSchema, CreateFlowRequestSchema, CreateFlowResponseSchema, DeleteFlowRequestSchema, DeleteFlowResponseSchema, FlowSummarySchema, GetFlowRequestSchema, GetFlowResponseSchema, GetRunRequestSchema, GetRunResponseSchema, ListFlowsRequestSchema, ListFlowsResponseSchema, ListRunsRequestSchema, ListRunsResponseSchema, ToggleFlowRequestSchema, ToggleFlowResponseSchema, TriggerFlowRequestSchema, TriggerFlowResponseSchema, UpdateFlowRequestSchema, UpdateFlowResponseSchema } from '@objectstack/spec/api';
34-
import type { AutomationApiErrorCode, AutomationFlowPathParams, AutomationRunPathParams, CreateFlowRequest, CreateFlowResponse, DeleteFlowRequest, DeleteFlowResponse, FlowSummary, GetFlowRequest, GetFlowResponse, GetRunRequest, GetRunResponse, ListFlowsRequest, ListFlowsResponse, ListRunsRequest, ListRunsResponse, ToggleFlowRequest, ToggleFlowResponse, TriggerFlowRequest, TriggerFlowResponse, UpdateFlowRequest, UpdateFlowResponse } from '@objectstack/spec/api';
33+
import { AutomationApiErrorCode, AutomationFlowPathParamsSchema, AutomationRunPathParamsSchema, CreateFlowRequestSchema, CreateFlowResponseSchema, DeleteFlowRequestSchema, DeleteFlowResponseSchema, FlowSummarySchema, GetFlowRequestSchema, GetFlowResponseSchema, GetRunRequestSchema, GetRunResponseSchema, ListFlowsRequestSchema, ListFlowsResponseSchema, ListRunsRequestSchema, ListRunsResponseSchema, ResumeFailureDetailsSchema, ToggleFlowRequestSchema, ToggleFlowResponseSchema, TriggerFlowRequestSchema, TriggerFlowResponseSchema, UpdateFlowRequestSchema, UpdateFlowResponseSchema } from '@objectstack/spec/api';
34+
import type { AutomationApiErrorCode, AutomationFlowPathParams, AutomationRunPathParams, CreateFlowRequest, CreateFlowResponse, DeleteFlowRequest, DeleteFlowResponse, FlowSummary, GetFlowRequest, GetFlowResponse, GetRunRequest, GetRunResponse, ListFlowsRequest, ListFlowsResponse, ListRunsRequest, ListRunsResponse, ResumeFailureDetails, ToggleFlowRequest, ToggleFlowResponse, TriggerFlowRequest, TriggerFlowResponse, UpdateFlowRequest, UpdateFlowResponse } from '@objectstack/spec/api';
3535

3636
// Validate data
3737
const result = AutomationApiErrorCode.parse(data);
@@ -511,6 +511,19 @@ const result = AutomationApiErrorCode.parse(data);
511511
| **hasMore** | `boolean` || Whether more runs are available |
512512

513513

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 |
525+
526+
514527
---
515528

516529
## ToggleFlowRequest

content/docs/references/index.mdx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Protocol Reference
3-
description: Every schema published by @objectstack/spec — 1574 schemas across 14 protocol modules
3+
description: Every schema published by @objectstack/spec — 1575 schemas across 14 protocol modules
44
---
55

66
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -20,7 +20,7 @@ counts are sums of the rows they head. Regenerate with
2020
| Module | Pages | Schemas | Description |
2121
| :--- | ---: | ---: | :--- |
2222
| [AI Protocol](/docs/references/ai) | 11 | 66 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. |
23-
| [API Protocol](/docs/references/api) | 31 | 436 | REST contracts, endpoints, routing, realtime, batch, discovery. |
23+
| [API Protocol](/docs/references/api) | 31 | 437 | REST contracts, endpoints, routing, realtime, batch, discovery. |
2424
| [Automation Protocol](/docs/references/automation) | 13 | 73 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
2525
| [Cloud Protocol](/docs/references/cloud) | 11 | 94 | Environments, packages and versions, marketplace, developer portal, tenancy. |
2626
| [Data Protocol](/docs/references/data) | 29 | 166 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
@@ -33,7 +33,7 @@ counts are sums of the rows they head. Regenerate with
3333
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
3434
| [System Protocol](/docs/references/system) | 33 | 272 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
3535
| [UI Protocol](/docs/references/ui) | 16 | 153 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
36-
| **Total** | **198** | **1574** | 14 protocol modules |
36+
| **Total** | **198** | **1575** | 14 protocol modules |
3737

3838
---
3939

@@ -61,7 +61,7 @@ Agents, tools, skills, RAG and knowledge sources, model registry, conversations.
6161

6262
## API Protocol
6363

64-
**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **31 pages, 436 schemas**
64+
**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **31 pages, 437 schemas**
6565

6666
REST contracts, endpoints, routing, realtime, batch, discovery.
6767

@@ -70,7 +70,7 @@ REST contracts, endpoints, routing, realtime, batch, discovery.
7070
| [`analytics.zod.ts`](/docs/references/api/analytics) | `AnalyticsEndpoint`, `AnalyticsMetadataResponse`, `AnalyticsQueryRequest`, `AnalyticsResultResponse`, `AnalyticsSqlResponse`, `GetAnalyticsMetaRequest` |
7171
| [`auth.zod.ts`](/docs/references/api/auth) | `AuthProvider`, `LoginRequest`, `LoginType`, `RefreshTokenRequest`, `RegisterRequest`, `Session`, `SessionResponse`, `SessionUser`, `UserProfileResponse` |
7272
| [`auth-endpoints.zod.ts`](/docs/references/api/auth-endpoints) | `AuthEndpoint`, `AuthFeaturesConfig`, `AuthProviderInfo`, `DeviceRequestResponse`, `DeviceTokenResponse`, `EmailPasswordConfigPublic`, `GetAuthConfigResponse` |
73-
| [`automation-api.zod.ts`](/docs/references/api/automation-api) | `AutomationApiErrorCode`, `AutomationFlowPathParams`, `AutomationRunPathParams`, `CreateFlowRequest`, `CreateFlowResponse`, `DeleteFlowRequest`, `DeleteFlowResponse`, `FlowSummary`, `GetFlowRequest`, `GetFlowResponse`, `GetRunRequest`, `GetRunResponse`, `ListFlowsRequest`, `ListFlowsResponse`, `ListRunsRequest`, `ListRunsResponse`, `ToggleFlowRequest`, `ToggleFlowResponse`, `TriggerFlowRequest`, `TriggerFlowResponse`, `UpdateFlowRequest`, `UpdateFlowResponse` |
73+
| [`automation-api.zod.ts`](/docs/references/api/automation-api) | `AutomationApiErrorCode`, `AutomationFlowPathParams`, `AutomationRunPathParams`, `CreateFlowRequest`, `CreateFlowResponse`, `DeleteFlowRequest`, `DeleteFlowResponse`, `FlowSummary`, `GetFlowRequest`, `GetFlowResponse`, `GetRunRequest`, `GetRunResponse`, `ListFlowsRequest`, `ListFlowsResponse`, `ListRunsRequest`, `ListRunsResponse`, `ResumeFailureDetails`, `ToggleFlowRequest`, `ToggleFlowResponse`, `TriggerFlowRequest`, `TriggerFlowResponse`, `UpdateFlowRequest`, `UpdateFlowResponse` |
7474
| [`batch.zod.ts`](/docs/references/api/batch) | `BatchConfig`, `BatchOperationResult`, `BatchOperationType`, `BatchOptions`, `BatchRecord`, `BatchUpdateRequest`, `BatchUpdateResponse`, `CrossObjectBatchDroppedFields`, `CrossObjectBatchOperation`, `CrossObjectBatchRequest`, `CrossObjectBatchResponse`, `DeleteManyRequest`, `UpdateManyRecord`, `UpdateManyRequest` |
7575
| [`contract.zod.ts`](/docs/references/api/contract) | `ApiError`, `BaseResponse`, `BatchLoadingStrategy`, `BulkRequest`, `BulkResponse`, `CreateRequest`, `DataLoaderConfig`, `DeleteResponse`, `ExportRequest`, `IdRequest`, `ListRecordResponse`, `ModificationResult`, `QueryOptimizationConfig`, `RecordData`, `SingleRecordResponse`, `UpdateRequest` |
7676
| [`discovery.zod.ts`](/docs/references/api/discovery) | `ApiRoutes`, `CapabilityDescriptor`, `Discovery`, `DiscoveryEnvironment`, `RouteHealthEntry`, `RouteHealthReport`, `ServiceInfo`, `ServiceSelfInfo`, `ServiceStatus`, `WellKnownCapabilities` |

docs/audits/2026-07-unknown-key-strictness-ledger.counts.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ directory rather than per file.
257257
| Dir | Sites |
258258
|---|---|
259259
| `ai/` | 77 |
260-
| `api/` | 450 |
260+
| `api/` | 451 |
261261
| `cloud/` | 83 |
262262
| `identity/` | 32 |
263263
| `integration/` | 8 |

packages/client/src/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4794,9 +4794,27 @@ export class ObjectStackClient {
47944794
* err.message; // the node failure, verbatim
47954795
* err.details?.errorMessage; // the flow author's `errorMessage`
47964796
* err.details?.summary; // per-node accounting of the failed run
4797+
* err.details?.runId; // the run this resume was addressed to
4798+
* err.details?.status; // 'stranded' | 'failed' — the engine's verdict, when it stamped one
4799+
* err.details?.repairable; // true exactly when `status` is 'stranded'
47974800
* }
47984801
* ```
47994802
*
4803+
* **Since #15221 the 400 tells a stranded run from a plain failure.**
4804+
* A resume that consumed the pause and then failed downstream leaves
4805+
* the run *stranded* — terminal like any failure, but the pause a
4806+
* durable decision was waiting on is gone with it and only an explicit
4807+
* operator verb can re-arm it. The engine says so
4808+
* (`AutomationResult.status: 'stranded'`), and the door forwards that
4809+
* verdict in `err.details`: branch on `err.details.repairable`, never
4810+
* on the message text. `repairable` is always present on this 400 —
4811+
* `false` on a plain terminal failure, deliberately, so an absent
4812+
* member reads as an older server rather than as "not repairable".
4813+
* The shape is `ResumeFailureDetailsSchema` (`@objectstack/spec/api`);
4814+
* the code stays `FLOW_FAILED` (no `FLOW_STRANDED` sibling). The
4815+
* trigger door's 400 carries `errorMessage` / `summary` only — it never
4816+
* resumes, so "repairable" has no referent there.
4817+
*
48004818
* A **stale** suspension (the flow deregistered, or the node edited away
48014819
* under a live pause) rejects with **404** rather than 400: nothing ran,
48024820
* and the pause is gone for good. The refusals that leave the suspension

0 commit comments

Comments
 (0)