Skip to content

Commit 27c4984

Browse files
committed
chore(spec): changeset, summary fixture, and the #9585 ceiling docblock relaxed by ruling B'
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
1 parent 88c25b4 commit 27c4984

3 files changed

Lines changed: 25 additions & 3 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): a failed flow run can carry the refusal its write node met — `AutomationResult.userMessage` / `AutomationResult.innerRefusal`, and one `FlowFailureDetailsSchema` for every door's `400 FLOW_FAILED` details
6+
7+
Clause-②: yes (widening)
8+
9+
The maintainer's 「同意」 on ruling B′ of #24914 (decision batch #379 item 5): the inner refusal of a refused flow write node reaches the client structurally, the way a record save's refusal does, and its code takes the wire spelling. This release is the contract half. The engine and the four doors start emitting the new members when the services and runtime half (#24914) lands; until then nothing on the wire changes.
10+
11+
- **`AutomationResult` (`@objectstack/spec/contracts`) gains two optional members.** `userMessage` has `ApiErrorSchema.userMessage`'s semantics: it is producer-marked end-user text, present only when the inner refusal's producer opted in at throw time, and it never replaces `error`. `innerRefusal` is `{ code, message }`. Its `code` is the wire spelling a record save of the same write answers in `error.code` (`UNIQUE_VIOLATION` for a unique refusal), parsed against the same closed `ErrorCode` set `ApiErrorSchema.code` uses. `TriggerFlowResponseSchema.data` mirrors both, so the `TriggerFlowResponse['data']` = `AutomationResult` parity pin holds.
12+
- **New `FlowInnerRefusalSchema` and `FlowFailureDetailsSchema` (`@objectstack/spec/api`).** The second one is the `error.details` of the `400 FLOW_FAILED` arm of the trigger, resume, `/actions` and endpoint doors, declared once: `errorMessage?`, `summary?` and `innerRefusal?`. On the resume door, `ResumeFailureDetailsSchema`'s `runId` / `status` / `repairable` sit beside them in the same object, and each schema parses its own members.
13+
- **The #9585 `/actions` ceiling, relaxed by the ruling.** That ceiling was exactly two keys in `error.details` (`errorMessage`, `summary`) and no new schema key. Ruling B′ adds one key and names it, `innerRefusal`, so the ceiling is now the three keys `FlowFailureDetailsSchema` declares, at every door.
14+
- **Unchanged.** `error.message` keeps each door's wording byte for byte, `error.code` stays `FLOW_FAILED`, and `AutomationResult.code` stays the closed union the #9378 ruling settled. The inner code is nested in `innerRefusal`, so no new ledger code is added. Every new member is optional: an envelope without them parses exactly as before.
15+
16+
**For consumers.** A flow runner can show a failed run's refusal the way a record form shows a save's refusal. Read the envelope's top-level `userMessage` when it is present. Otherwise read `details.innerRefusal` (`code` and `message`), and fall back to `message` last. A client running behind the server ignores the new members and keeps its current behaviour.

‎packages/runtime/src/action-execution.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,8 +1046,13 @@ const FLOW_ACTION_REFUSAL_BRAND = Symbol.for('objectstack.runtime.flowActionRefu
10461046
* The run artefacts a failed flow dispatch carries beside its status and code
10471047
* — EXACTLY the two fields the trigger door ships in `error.details` on its
10481048
* own `400 FLOW_FAILED` arm (`domains/automation.ts`), same names, same
1049-
* source (`AutomationResult.errorMessage` / `.summary`). A third key here is
1050-
* a contract widening the #9585 ruling does not cover.
1049+
* source (`AutomationResult.errorMessage` / `.summary`). The #9585 ruling
1050+
* capped this at those two keys; ruling B′ on #24914 (decision batch #379
1051+
* item 5) relaxes the cap by exactly one, `innerRefusal`, and the spec now
1052+
* declares the three-key set once for every flow door as
1053+
* `FlowFailureDetailsSchema` (`@objectstack/spec/api`). Carrying that third
1054+
* key is #24914's emission half and is not done here yet; any key beyond the
1055+
* three is a contract widening neither ruling covers.
10511056
*/
10521057
export interface FlowActionRunDetails {
10531058
/** The flow AUTHOR's own failure text (`flow.errorMessage`). */

‎packages/spec/src/api/automation-api.zod.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,8 @@ describe('FlowFailureDetailsSchema — one inner-refusal shape across the four F
556556
const USER_MESSAGE = 'An account with this name already exists.';
557557
const SUMMARY = {
558558
selected: 0, acted: 0, skipped: 0, unmeasured: 0,
559-
nodes: [{ nodeId: 'create_account', nodeType: 'create_record', status: 'failure', runs: 1, failures: 1 }],
559+
nodes: [{ nodeId: 'create_account', nodeType: 'create_record', status: 'failure', runs: 1, failures: 1, skipped: 0 }],
560+
gates: [],
560561
};
561562
// The four doors, each with its OWN message wording (unchanged by the
562563
// ruling) and, on the resume door alone, the engine's verdict beside the

0 commit comments

Comments
 (0)