Commit 2826d1e
* fix(automation,approvals): an approval decision can no longer succeed while its flow stays parked (#4420)
A flow paused at an `approval` node, a deploy, then an approver clicking
Approve: the request row flipped to `approved`, the UI toasted success — and
the flow never moved. No next-stage request, no error, the record's mirrored
status frozen mid-workflow. Approval flows pause for days by design, so a
restart mid-flight is the normal case, not the edge one.
Durable suspended runs (#1518) had shipped and were not the missing piece.
Two other things were.
The wiring could enable a store over a table nobody had created. Object
registration and store activation resolve different services in different
phases — `manifest` at init(), `objectql` at start() — and the plugin declared
no ordering. Composed ahead of ObjectQL, init() found no `manifest`, warned,
and continued; start() attached the DB-backed store anyway. Every suspend then
failed with `no such table: sys_automation_run` into a log line nobody read.
Now: `optionalDependencies: ['com.objectstack.engine.objectql']` (order-if-
present per ADR-0116 — an engine-less kernel must still boot), a missed
registration retried at start() while it still precedes schema sync, no store
attached when registration did not happen (at error level), a boot-time table
probe, and a failed durable write logged at error rather than warn.
A reported resume failure read as success. `AutomationEngine.resume()` answers
a lost run by RETURNING `{ success: false }`, never by throwing; approvals
discarded that value and counted only throws as failure. Resume failures are
now classified — RUN_NOT_FOUND, STORE_UNAVAILABLE, RESUME_IN_PROGRESS —
so a run gone for good is distinguishable from a store merely unreachable,
and the raw resume route maps them to 404 / 503 / 409.
Approvals acts on them. The new `hasSuspendedRun(runId)` — which reads the
suspension store, unlike `getRun()`, and throws rather than answering false
when the store is unreadable — pre-flights decide / sendBack / resubmit BEFORE
their first write, so the zombie half-state is never created rather than
merely reported: RESUME_TARGET_LOST (409), request left actionable. A resume
that fails after the decision is durable throws RESUME_FAILED (500) naming the
stranded run. A concurrent duplicate stays benign via the new `resumeError`
field; recall and revise-window cancellation stay non-fatal but log at error.
Compositions with no automation engine attached are unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EPRaZTNbACfvGKytYESX2h
* docs(spec): regenerate the API reference for the new resume error codes (#4420)
`check:docs` compares `content/docs/references/` against what
`gen:schema && gen:docs` produces. The five codes this branch registers in the
ADR-0112 ledger — RUN_NOT_FOUND, STORE_UNAVAILABLE, RESUME_IN_PROGRESS,
RESUME_TARGET_LOST, RESUME_FAILED — feed `ErrorCode`, which the generator
inlines into every response-envelope table. Hence eleven files, all of it the
same union widening: the ledger page gains the five entries and every page
that renders an ApiError picks them up.
Generated, not hand-edited:
pnpm --filter @objectstack/spec gen:schema && pnpm --filter @objectstack/spec gen:docs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EPRaZTNbACfvGKytYESX2h
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 83cf2d3 commit 2826d1e
26 files changed
Lines changed: 1288 additions & 161 deletions
File tree
- .changeset
- content/docs/references/api
- packages
- plugins/plugin-approvals/src
- rest/src
- runtime/src/domains
- services/service-automation/src
- spec/src
- api
- contracts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
280 | 280 | | |
281 | 281 | | |
282 | 282 | | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
283 | 286 | | |
284 | 287 | | |
285 | 288 | | |
286 | 289 | | |
287 | 290 | | |
288 | 291 | | |
289 | 292 | | |
| 293 | + | |
290 | 294 | | |
291 | 295 | | |
292 | 296 | | |
| |||
303 | 307 | | |
304 | 308 | | |
305 | 309 | | |
| 310 | + | |
306 | 311 | | |
307 | 312 | | |
308 | 313 | | |
| |||
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
0 commit comments