You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found while implementing #9446 (PR #9584). Filed unassigned — not fixed there: closing it needs a decision about a shared resolver's contract, which is wider than that card's ruling.
What is missing
When a flow that ran and failed is reported by the trigger door, the ADR-0112 envelope carries two things beside the message:
error.details.summary — the run's per-node accounting, i.e. WHICH node failed.
The same flow, dispatched through POST /api/v1/actions/:object/:action as a type: 'flow' action, answers neither. After #9446 both doors agree on the status and the code (400 FLOW_FAILED), so the divergence that is left is exactly the payload beside them.
Why, mechanically
The two doors reach the envelope differently:
the trigger door RETURNS a built response and passes details straight to deps.error(message, status, details);
/actions THROWS (dispatchFlowAction, packages/runtime/src/action-execution.ts) and the route serves the throw through HttpDispatcher.errorFromThrown, which delegates to resolveThrownHttpError (packages/types/src/thrown-http-error.ts).
resolveThrownHttpError does not read a thrown .details. It BUILDS one, from a closed list:
So errorMessage and summary stamped on the thrown error would be dropped silently. Stamping them anyway is the shape that reads as fixed and is not.
Why it matters
A metadata app that dispatches its flow through an ACTION (the documented way to expose one on a record page) shows the caller the raw engine error instead of the message its author wrote for exactly this case. The author's text is declared, and at this door it is declared-but-never-delivered — the declared ≠ enforced shape, in the payload rather than in the schema.
The decision this needs
resolveThrownHttpError is shared by two doors (the dispatcher and packages/rest's direct-mount registrar), and #8016 moved the rule there precisely so one door cannot hold its own copy. So widening what a throw may declare is a change to that shared contract, not a local fix:
B — give dispatchFlowAction a typed refusal carrier the /actions route recognises before the generic catch, leaving the shared resolver untouched. Keeps the widening out of the shared rule, at the cost of a second path into the envelope at one door.
C — rule that the run artefacts are a property of the TRIGGER route (the door that owns runs) and that the action door is not expected to carry them. Then the docs and objectui's reader should say so, and this closes as declared-by-design rather than as residue.
No option is obviously right, which is why this is filed rather than decided: the same reasoning that made #9446 a ruling ("is this a property of the contract or of one route?") applies here to the payload rather than to the status.
This gap predates it — /actions has never carried these fields. What #9446 changes is that it is now the only remaining difference between the two doors' answers, so it is worth naming instead of leaving inside a larger divergence.
Found while implementing #9446 (PR #9584). Filed unassigned — not fixed there: closing it needs a decision about a shared resolver's contract, which is wider than that card's ruling.
What is missing
When a flow that ran and failed is reported by the trigger door, the ADR-0112 envelope carries two things beside the message:
error.details.errorMessage— the flow AUTHOR's own failure text (flow.errorMessage). This is the single place the console reads it from (objectuiflowResponse.ts, PR ci(release): 解堵 17.0.0-rc.2,并让空 changeset 不再吃掉一整轮发布 (#4898) #4899), and bug(service-automation):execute()never carries the flow author'ssuccessMessage/errorMessage— onlyresume()does, so a triggered run's friendly text is silently dropped #9414 put it there deliberately because the envelope has nodata.error.details.summary— the run's per-node accounting, i.e. WHICH node failed.The same flow, dispatched through
POST /api/v1/actions/:object/:actionas atype: 'flow'action, answers neither. After #9446 both doors agree on the status and the code (400 FLOW_FAILED), so the divergence that is left is exactly the payload beside them.Why, mechanically
The two doors reach the envelope differently:
detailsstraight todeps.error(message, status, details);/actionsTHROWS (dispatchFlowAction,packages/runtime/src/action-execution.ts) and the route serves the throw throughHttpDispatcher.errorFromThrown, which delegates toresolveThrownHttpError(packages/types/src/thrown-http-error.ts).resolveThrownHttpErrordoes not read a thrown.details. It BUILDS one, from a closed list:So
errorMessageandsummarystamped on the thrown error would be dropped silently. Stamping them anyway is the shape that reads as fixed and is not.Why it matters
A metadata app that dispatches its flow through an ACTION (the documented way to expose one on a record page) shows the caller the raw engine error instead of the message its author wrote for exactly this case. The author's text is declared, and at this door it is declared-but-never-delivered — the
declared ≠ enforcedshape, in the payload rather than in the schema.The decision this needs
resolveThrownHttpErroris shared by two doors (the dispatcher andpackages/rest's direct-mount registrar), and #8016 moved the rule there precisely so one door cannot hold its own copy. So widening what a throw may declare is a change to that shared contract, not a local fix:details(an allow-list of keys, or verbatim) inresolveThrownHttpError. One place, both doors, but it widens what any thrower anywhere can put on a wire — and thecodelimb right above it was deliberately narrowed in [Decision] The dispatcher'serror.codehas a limb authored by TENANTS at runtime — registration cannot close it, and ADR-0112 does not say what should happen there #9106 for that reason.dispatchFlowActiona typed refusal carrier the/actionsroute recognises before the generic catch, leaving the shared resolver untouched. Keeps the widening out of the shared rule, at the cost of a second path into the envelope at one door.No option is obviously right, which is why this is filed rather than decided: the same reasoning that made #9446 a ruling ("is this a property of the contract or of one route?") applies here to the payload rather than to the status.
Not caused by #9446
This gap predates it —
/actionshas never carried these fields. What #9446 changes is that it is now the only remaining difference between the two doors' answers, so it is worth naming instead of leaving inside a larger divergence.Generated by Claude Code