Skip to content

fix(runtime): /actions answers the #9378 flow-dispatch status table, from one shared definition - #9584

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-9446-actions-flow-status-table
Aug 18, 2026
Merged

fix(runtime): /actions answers the #9378 flow-dispatch status table, from one shared definition#9584
os-zhuang merged 2 commits into
mainfrom
claude/issue-9446-actions-flow-status-table

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#9446

POST /api/v1/actions/:object/:action with a type: '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: false automation 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 own result.code was 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.

engine exitbefore, at /actionsnowtrigger door
flow not found400FLOW_FAILED404RESOURCE_NOT_FOUNDsame
flow disabled400FLOW_FAILED409FLOW_DISABLEDsame
flow has no start node400FLOW_FAILED422FLOW_NO_START_NODEsame
ran and failed400FLOW_FAILED400FLOW_FAILEDsame

Retry semantics changed, and that is the point

The status is a contract with the console, not just a number. All four answers were 400 before, and 400 FLOW_FAILED is 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.ts plus its test siblings. Two files outside it were touched, both deliberately:

What deliberately did NOT change

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_FAILED restored at door 2, with the module and the trigger door left alone — and the outcome predicted before the run:

predictedactual
9 red: rows 1/2/3, the distinguishability set, the classify-off-verdict case, the no-getFlow case, and 3 of the 4 cross-door parity casesexactly those 9expected 200 to be 404, expected 400 to be 409, expected 400 to be 422
4 green: row 4, the success 200, the unclassified-residual guard, the parity case for the row that already agreedexactly those 4
the trigger door's own suite fully green26/26 green

The 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 tsc refuse it — "types ... and '"FLOW_DISABLED_TYPO"' have no overlap" — which proves the closed AutomationResult.code union is being read from a rebuilt declaration rather than a stale or any one.

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/runtime full suite — 170 files / 2528 tests, all green
  • packages/runtime typecheck (tsc --noEmit) — green
  • Consumer packages (downstream dependents, --filter '...@objectstack/runtime'): @objectstack/client 23 files / 310 tests green - @objectstack/verify 7 / 32 green (includes the real-AutomationEngine trigger-route integration test) - @objectstack/cloud-connection 23 / 191 green - @objectstack/http-conformance 4 / 72 green - @objectstack/hono 2 / 73 green

Gates

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-measure timed out under contention for the shared build lock. The thing it would catch was measured directly instead — tsc over packages/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

…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>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime, touching 10 documentable anchor(s).

1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via /:name/toggle (route))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see

Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 7e202882092bf460b0045f2be9a281797a849462packageMentionDocs.

Which tree this was computed on

This run read content/docs from c51a82aff612af37e80ac68e0336716304b055c6 — the merge of head 41ff10ed751228be62eb5fca6b72ce292420ec60 into base 7e202882092bf460b0045f2be9a281797a849462, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 7e202882092bf460b0045f2be9a281797a849462 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 18, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 18, 2026 12:21
@os-zhuang
os-zhuang enabled auto-merge August 18, 2026 12:21
@os-zhuang
os-zhuang added this pull request to the merge queueAug 18, 2026
Merged via the queue into main with commit ca2e020Aug 18, 2026
26 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-9446-actions-flow-status-table branch August 18, 2026 12:38
os-elon pushed a commit that referenced this pull request Aug 18, 2026
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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-zhuang@claude