Uh oh!
There was an error while loading. Please reload this page.
fix(runtime): /actions answers the #9378 flow-dispatch status table, from one shared definition - #9584
Conversation
…9446) `dispatchFlowAction` mapped every `success: false` automation result to `400 FLOW_FAILED` under a comment asserting "The flow RAN and rejected" — false for two of the four exits it caught. A disabled flow invoked through an action told the caller a run had failed when no node ever executed, and the producer's own `result.code` was available and ignored. The table now lives in one module, `flow-dispatch-status.ts`, read by both the `/actions` door and the trigger door: 404 (flow not found) / 409 FLOW_DISABLED / 422 FLOW_NO_START_NODE / 400 FLOW_FAILED. Maintainer ruling 2026-08-18, verbatim 「同意」: the table is a property of the flow-dispatch contract, not of the trigger route. Co-Authored-By: Claude <noreply@anthropic.com>
…dd the changeset (#9446) `ui/actions.mdx` documented the divergence this change closes; it now states the converged table. `automation/flows.mdx` gains the cross-door note, and `http-protocol.mdx`'s declared-endpoint row points at #9462 — the door that is still unconverged — instead of at this card, which would read as "fixed" once this closes. Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not seeCoarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c51a82aff612af37e80ac68e0336716304b055c6 && git checkout c51a82aff612af37e80ac68e0336716304b055c6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7e202882092bf460b0045f2be9a281797a849462 41ff10ed751228be62eb5fca6b72ce292420ec60 && git checkout -B drift-repro 7e202882092bf460b0045f2be9a281797a849462 && git merge --no-ff 41ff10ed751228be62eb5fca6b72ce292420ec60
node scripts/docs-audit/affected-docs.mjs --json 7e202882092bf460b0045f2be9a281797a849462
|
Uh oh!
There was an error while loading. Please reload this page.
Since #9446 (PR #9584), POST /api/v1/actions/:object/:action also emits both codes through dispatchFlowAction (packages/runtime/src/action-execution.ts), not just the trigger door's respondToFlowTrigger. Update the ledger's provenance notes to record the second emitter, following the in-file PACKAGE_DELETE_PARTIAL precedent ("Second EMITTER of the code ... Provenance, not identity"). Comment-only: the registration rows themselves are unchanged, so no code is registered or unregistered. Fixes#9586 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016D9wdJR14KKCxz1WgdAzcw
Fixes#9446
POST /api/v1/actions/:object/:actionwith atype: 'flow'action now answers the #9378 flow-dispatch status table, from one definition the trigger door reads too.The defect
dispatchFlowAction(packages/runtime/src/action-execution.ts) mapped everysuccess: falseautomation result to one answer —400 FLOW_FAILED— under a comment asserting "The flow RAN and rejected". That is false for two of the four exits it caught: a DISABLED flow invoked through an action told the caller a run had failed when no node ever executed, and the producer's ownresult.codewas available and ignored.The ruling this implements
Maintainer, 2026-08-18, verbatim 「同意」 to the triage recommendation: the #9378 table is a property of the flow-dispatch contract, not of the trigger route, converged in stages. Door 2 converges here; door 3 (declared endpoints) is #9462 and is deliberately untouched.
/actions400FLOW_FAILED404RESOURCE_NOT_FOUND400FLOW_FAILED409FLOW_DISABLED400FLOW_FAILED422FLOW_NO_START_NODE400FLOW_FAILED400FLOW_FAILEDRetry semantics changed, and that is the point
The status is a contract with the console, not just a number. All four answers were
400before, and400 FLOW_FAILEDis terminal to the console — so three refusals that are not terminal-for-the-same-reason were being reported as one. After this change:409 FLOW_DISABLED— reversible operational state. Enable the flow and the identical request succeeds. A caller may legitimately retry after an operator acts.422 FLOW_NO_START_NODE— an authoring defect in the stored definition. Retrying cannot help.404— the action exists, the flow it targets does not. Retrying cannot help; the metadata needs fixing.400 FLOW_FAILED— unchanged, still terminal, still the only row that says a run happened.A caller that treats every non-2xx as "the action failed" is unaffected: all four are still refusals, still
success: false, still with no inner envelope.One definition, not a fourth copy
The mapping lives in a new module,
packages/runtime/src/flow-dispatch-status.ts, and both doors read it. This is the part that matters more than the mapping: three doors each holding their own reading of one engine result is exactly what let them answer differently in the first place, so a second copy of the rule is a defect by construction. #9462 gets the same module to call.respondToFlowTrigger(packages/runtime/src/domains/automation.ts) was refactored to delegate — a pure refactor with no behaviour change, pinned by its own untouched suite (automation-trigger-route-status.test.ts, 26 tests) and by the real-engine integration test in@objectstack/verify. Both stayed green throughout.Declared file-surface expansion, stated rather than smuggled
The dispatch named
action-execution.tsplus its test siblings. Two files outside it were touched, both deliberately:domains/automation.ts— the delegation above. Without it there are still two definitions of one rule, which is the thing the ruling forbids. The file carries no in-flight claim (verified: automation: the run-resume route still answers HTTP 200 wrapping an inner {success:false} — the route #3962's status-code unification left behind #8684 and automation resume: the request body's OUTER envelope is lenient — an unknown top-level key is silently ignored and the submission is treated as empty #8796 both landed), andpackages/runtimeis serialised to this card this round.content/docs/**—ui/actions.mdxdocumented the old divergence and cited this very issue; leaving it would ship documentation stating the opposite of the behaviour.automation/flows.mdxgains the cross-door note.protocol/kernel/http-protocol.mdx's declared-endpoint row now points at Declared endpoints (type: 'flow'): converge onto the #9378 status table — the ruled follow-up to #9446's staged execution #9462 rather than at this card — once this lands, a reader following that link from a line describing an unconverged door would conclude the door was fixed. That row's own statement (declared endpoints still answer200) is unchanged. To be explicit: Declared endpoints (type: 'flow'): converge onto the #9378 status table — the ruled follow-up to #9446's staged execution #9462 is not addressed here.What deliberately did NOT change
400message keeps its wording (Flow '...' failed: ...). It has been on the wire since [17.0.0-rc] /actions reports business failures as HTTP 200 inside a double envelope — the only route of 12 that does; unify on real status codes before GA #3962, the ruling is about status and code, and it names the flow — which this door needs because the flow name is in the trigger route's URL and nowhere in this one.success: falsestill refuses with400 FLOW_FAILEDhere, deliberately not the trigger door's200. This route settled in [17.0.0-rc] /actions reports business failures as HTTP 200 inside a double envelope — the only route of 12 that does; unify on real status codes before GA #3962 that failures speak HTTP; the alternative residual is the200 {"success":true,"data":{"success":false}}double envelope that ruling removed. The two doors differ here on purpose and each states why at its own site.packages/specedit. All three codes are already registered under@objectstack/runtimeinERROR_CODE_LEDGER; nothing needed widening. Their ledger notes still describe only the trigger door as the emitter — a provenance note that is now incomplete rather than wrong, left to the spec seat rather than edited here.error.details. The trigger door carrieserrorMessage/summarythere; this door throws, and a throw's structured context is whateverresolveThrownHttpErrorreads off it. Out of this card's scope, recorded as a finding.Reverse verification — both directions, predicted before running
A suite that only asserted the new codes would stay green under a regression that collapsed every exit back to one code. Both halves are pinned: each row answers its own status and code, and the rows are asserted as a set so a collapse reddens whatever the surviving answer is.
Then the fix was ablated — the blanket
FLOW_FAILEDrestored at door 2, with the module and the trigger door left alone — and the outcome predicted before the run:getFlowcase, and 3 of the 4 cross-door parity casesexpected 200 to be 404,expected 400 to be 409,expected 400 to be 422200, the unclassified-residual guard, the parity case for the row that already agreedThe four that stay green do not claim to pin the defect: three are regression guards for behaviour that must not change (including the guard against re-introducing the #3962 double envelope, which the ablation restores identically), and the fourth is the parity row that already agreed before this change.
A second reverse check, on the type side: renaming the compared member to a non-member made
tscrefuse it — "types ... and '"FLOW_DISABLED_TYPO"' have no overlap" — which proves the closedAutomationResult.codeunion is being read from a rebuilt declaration rather than a stale oranyone.Verification
All of the below ran at commit
41ff10ed7— the final commit — on a tree whose dependency closure was built first. The gate list was re-derived from the actual changed paths (scripts/pm/dispatch-gates.mjs), not taken from the dispatch's initial list, which named four families; the re-derivation named seventeen plus five convention-triggered ones.Tests
packages/runtimefull suite — 170 files / 2528 tests, all greenpackages/runtimetypecheck (tsc --noEmit) — green--filter '...@objectstack/runtime'):@objectstack/client23 files / 310 tests green -@objectstack/verify7 / 32 green (includes the real-AutomationEnginetrigger-route integration test) -@objectstack/cloud-connection23 / 191 green -@objectstack/http-conformance4 / 72 green -@objectstack/hono2 / 73 greenGates
Green:
check:nul-bytes-check:cross-package-test-inputs-check:route-envelope-check:dispatcher-error-vocabulary-check:docs-audit-scope-check:docs-redirects-check:role-word-check:changeset-gate-self-tests-check:objectui-changeset-check-adr-0087-registration-check-changeset-no-major-check-empty-changeset-check-affected-docs-check:query-options-erasure-check:engine-double-contract-check:where-matcher-check:type-check-coverage- spec liveness:check:liveness,check:empty-state,check:strictness-ledger,check:variant-docs.Not completed locally:
check:type-check-debt --re-measuretimed out under contention for the shared build lock. The thing it would catch was measured directly instead —tscoverpackages/runtimeincluding test files (which its own tsconfig excludes) reports 227 pre-existing errors and 0 in the new test file, so this diff cannot push the frozen count up. Its structural half (check:type-check-coverage) is green above. CI runs the ratchet.Generated by Claude Code