Skip to content

Commit 228275a

Browse files
committed
docs(spec): regenerate the automation API reference for the rewritten repairable describe
Adds the changeset for the spec contract widening and the runtime door change. Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <noreply@anthropic.com>
1 parent 28f20f9 commit 228275a

2 files changed

Lines changed: 65 additions & 1 deletion

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/runtime': minor
4+
---
5+
6+
the resume door's `repairable` is answered by the engine on the exits that stamp no status — `IAutomationService` declares the read-only `inspectConsumedSuspension` (#17541)
7+
8+
Clause-②: yes (widening)
9+
10+
The resume route's `400 FLOW_FAILED` details computed `repairable` as the single
11+
expression `status === 'stranded'`. That word is stamped on exactly one exit —
12+
the run that consumed its OWN pause and then threw downstream. The subflow
13+
DELEGATION exit stamps nothing on purpose: a caller resumes the PARENT, the
14+
signal is forwarded down, the child strands, and the parent frame answers
15+
`{ success: false, error, durationMs }`, because nothing re-arms an ancestor by
16+
resuming it and stamping `'stranded'` there would send an operator to retry a
17+
recovery that cannot succeed.
18+
19+
Since the nested-chain restore landed, that parent's consumed pause IS
20+
journalled and one `restoreConsumedSuspension(parentRunId)` re-arms the whole
21+
chain leaf-first. So the wire answered `repairable: false` about a run the
22+
operator verb WILL repair, and a client written exactly as the reference page
23+
instructs closed it as terminal. Measured through the HTTP route, before and
24+
after, on the same parked delegation:
25+
26+
```json
27+
before 400 { "error": { "code": "FLOW_FAILED",
28+
"details": { "runId": "run_…", "repairable": false } } }
29+
after 400 { "error": { "code": "FLOW_FAILED",
30+
"details": { "runId": "run_…", "repairable": true } } }
31+
```
32+
33+
…while at that same instant the engine answered
34+
`inspectConsumedSuspension(runId) → { repairable: true, witness: 'journal' }`
35+
and `restoreConsumedSuspension(runId) → { restored: true, chain: [child, parent] }`.
36+
37+
**`@objectstack/spec` — additive, `minor`.** `IAutomationService` declares the
38+
optional read-only member `inspectConsumedSuspension(runId)`, which
39+
`AutomationEngine` already implements publicly: would the restore verb have a
40+
consumed suspension to put back for this run? It re-arms nothing and reads the
41+
same two witnesses that verb reads, so what it calls repairable IS what that
42+
verb restores. The declared result is deliberately narrower than the
43+
implementation's, the way `restoreConsumedSuspension`'s already is — `reason` is
44+
typed as the string the implementation answers, not as an enumeration this
45+
contract would have to keep in step, and the engine's wider type satisfies it
46+
under `implements`. `ResumeFailureDetailsSchema.repairable`'s `.describe()` is
47+
rewritten to the truth and the generated reference page regenerated with it. No
48+
key is added, renamed or retired on any wire schema.
49+
50+
**`@objectstack/runtime` — the door.** On a `400 FLOW_FAILED` whose result
51+
carries a `status`, that stamp still decides, and the engine is not consulted at
52+
all. On a result that carries none, the door asks the declared member and relays
53+
its `repairable`. Both ways of not getting an answer are FAIL-CLOSED: a service
54+
that declares no inspection member answers `false` exactly as it did before, and
55+
an inspection that REJECTS (a store it could not read) answers `false` and says
56+
so once at `warn` — an unreadable store is UNKNOWN, not "nothing to restore",
57+
and it is never allowed to replace the `400` the caller asked for with a `500`.
58+
59+
⛔ The fence is untouched: a cascade-failed ancestor is still never STAMPED
60+
`'stranded'`. Its repairability is carried by the journal and REPORTED by the
61+
inspection, which is exactly why the door asks instead of reading a word. ⛔ And
62+
no new `AutomationResult.status` member is minted for this exit — there is
63+
nothing new for a client to learn, and `details.repairable` is the member a
64+
client was already told to branch on.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ const result = AutomationApiErrorCode.parse(data);
583583
| :--- | :--- | :--- | :--- |
584584
| **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 |
585585
| **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 |
586-
| **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 |
586+
| **repairable** | `boolean` | ✅ | Whether the engine says this run can still be re-armed by an operator verb. Answered from the engine, two ways, and never from the message text: where the engine stamped a `status`, that word decides (`stranded` is the run whose OWN pause a resume consumed before a downstream node threw); where it stamped none, the door asks the engine's read-only inspection (`IAutomationService.inspectConsumedSuspension`) and relays its verdict. The second half is not a fallback but the honest answer for a real exit: a subflow DELEGATION failure carries no status, because nothing re-arms an ancestor by resuming it, and yet the ancestor's consumed pause is journalled and the restore verb re-arms that chain as one unit. Always present on this arm: an absent member would be indistinguishable from a server that predates this field. Every way of not getting an answer is fail-closed - a service that declares no inspection member, and a store the inspection could not read - because promising a repair verb that will refuse is worse than promising nothing |
587587

588588

589589
---

0 commit comments

Comments
 (0)