|
| 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. |
0 commit comments