Skip to content

fix(service-automation,runtime): a refused write inside a flow reaches every FLOW_FAILED door as details.innerRefusal and a top-level userMessage, message unchanged - #25147

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-24914-flow-inner-refusal
Oct 22, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-24914-flow-inner-refusal

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24914

Clause-②: yes (widening)
Metadata-protocol: none

Step (2) of ruling B′ on #24914 (the maintainer's 「同意」, ruling record 6303788292, decision batch #379 item 5). The spec half landed in #24996 (PR #25037): AutomationResult.userMessage, AutomationResult.innerRefusal (FlowInnerRefusal) and one FlowFailureDetailsSchema for the four doors. This PR emits exactly what those declare.

The console half is objectstack-ai/objectui#13285 (step 3). It stays open; this PR does not touch objectui. The object API name inside the unique-refusal sentence is #24951 (engine), unchanged here.

What changes

Before: a refused write inside a flow reached the caller as one string. The node's catch kept only the message, the engine prefixed the node id, and each door served that string as the FLOW_FAILED message:

Node 'create_account' failed: create_record(crm_account) failed: Duplicate record refused on 'crm_account': …

After: the same 400 also carries the refusal structurally. message is byte-identical to before.

{ "success": false, "error": {
    "code": "FLOW_FAILED", "httpStatus": 400,
    "message": "Node 'create_account' failed: create_record(crm_account) failed: Duplicate record refused on 'crm_account': …",
    "userMessage": "(present only when the producer marked one, e.g. an app hook)",
    "details": { "summary": { "...": "..." },
                 "innerRefusal": { "code": "UNIQUE_VIOLATION", "message": "Duplicate record refused on 'crm_account': …" } } } }

@objectstack/service-automation

  • New src/inner-refusal.ts. refusalCarriedBy(err) is the one helper the four CRUD catch arms call. It returns { userMessage?, innerRefusal? }:
    • userMessage comes from declaredUserMessage(err) (@objectstack/types), the one read every boundary uses.
    • innerRefusal is set only for a refusal, meaning a throw that declared a 4xx answer (resolveThrownHttpError(err).declaredStatus). A fault carries none: that covers an undeclared throw and a declared 5xx.
    • Its code is the one the data door's errorFromThrown answers for the same throw. That is the resolver's code (VALIDATION_FAILED, PERMISSION_DENIED, or the member an app hook's declared status names), except that the engine's duplicate envelope takes the ruled wire spelling. That spelling is read off the shared arm (structuredCodeAnswer), never typed here.
    • Nothing is read from message text.
  • builtin/crud-nodes.ts. The four catch arms (get_record, create_record, update_record, delete_record) spread refusalCarriedBy(err) beside an unchanged error. create_record keeps its in-flow code: 'DUPLICATE_RECORD' for $error.code.
  • engine.ts.
    • NodeExecutionResult gains optional userMessage / innerRefusal.
    • The returned-failure arm throws the same Node 'ID' failed: … error, now branded with the members. This is a non-enumerable symbol brand, the partial-steps.ts / guard-refusal.ts idiom, so identity, message and instanceof are unchanged.
    • The members are spread onto the result at four exits: execute()'s failure exit, the resumeInternal stranded exit, executeWithoutRetry's failure exit, and retryExecution's exhausted exit. The last one reports the last attempt's members, paired with its text.
    • A loop / parallel frame rethrows the same object, so the brand survives them.

@objectstack/runtime

  • flow-dispatch-status.ts. flowFailureDetails(result) is one builder, typed as FlowFailureDetails, for the three declared keys. flowFailureEnvelope(result) gives the top-level userMessage.
  • Trigger and resume doors (domains/automation.ts), /actions (action-execution.ts, plus its handler in domains/actions.ts) and declared endpoint (endpoint-executor.ts). Each door builds its FLOW_FAILED details through the shared builder and puts userMessage at the envelope's top level (ApiErrorSchema.userMessage).
    • FlowActionRunDetails is now the spec's FlowFailureDetails, so a fourth key fails the build.
    • FlowActionRefusal carries userMessage as a field, which is the channel declaredUserMessage reads.
  • The deps.error slot. DomainHandlerDeps.error gains an optional fourth argument, extra: { userMessage? } (domain-handler-registry.ts), and http-dispatcher.ts wires it to the existing extra of the envelope builder. A call without it builds the same envelope as before.
  • MCP resume_run (domains/mcp.ts). resumeDoorError passes the slot and copies the envelope's userMessage onto the thrown error. details.innerRefusal already travels with details.

Why Clause-②: yes (widening), not the claim's no

The claim said no because #24996 already declared the wire members, and it asked me to re-declare if the built declaration closure showed a key beyond FlowFailureDetailsSchema. Measured on the built dist/index.d.ts of both packages (built at the head this PR cites), two published types widen:

  • @objectstack/runtime's DomainHandlerDeps.error gains the optional extra argument (the slot the ruling names);
  • @objectstack/service-automation's exported NodeExecutionResult gains optional userMessage / innerRefusal.

Both are additive and optional. No export is added. The wire members are the ones #24996 declared. The changeset is patch for both packages, with the same two declaration lines.

Tests

New pins, each asserting code + status on refusal cases:

  • packages/services/service-automation/src/flow-inner-refusal.test.ts (13 tests). A real AutomationEngine and the real CRUD executors run over a data double that throws the real classes (DuplicateRecordError, ValidationError, PermissionDeniedError) after accepting the call through the engine's dispatch predicates.
    • Unique refusal: UNIQUE_VIOLATION, while in-flow stays DUPLICATE_RECORD. Validation: VALIDATION_FAILED. Permission: PERMISSION_DENIED.
    • A marked app-hook refusal: userMessage arrives verbatim, with RESOURCE_CONFLICT for its unregistered code.
    • An unmarked refusal has no userMessage key. A fault and a declared 5xx carry neither member.
    • The other three arms each carry the members.
    • The exits: stranded resume (the card's write-behind-a-screen leg), the retry ladder, a loop frame, and a fault edge that handles the failure (no members).
    • error is asserted byte-for-byte everywhere.
  • packages/runtime/src/flow-failed-inner-refusal.test.ts (10 tests). Covers trigger, resume, /actions and endpoint.
    • details.innerRefusal parses through FlowFailureDetailsSchema, and the error parses through ApiErrorSchema.
    • userMessage is top-level only, never a details key. message keeps each door's wording.
    • A door-against-door drift pin, and the unrefused control.
  • packages/runtime/src/mcp-resume-run.test.ts: one appended test. The MCP resume_run bridge carries innerRefusal and userMessage exactly as the REST resume door does.
  • packages/verify/src/automation-flow-inner-refusal.test.ts (9 tests). This runs end to end: real engine and CRUD executors, the real HttpDispatcher trigger and resume doors, and the real error classes.
    • Each inner code is compared with what the data door's errorFromThrown answers for the same thrown value. This is door-against-door parity with the record save.

The existing wording pins are untouched and green, including create-record-duplicate-code.test.ts, try-catch-returned-failure-steps.test.ts, actions-flow-dispatch-status.test.ts (its exact-equality details pin still describes a run that met no refusal), automation-resume-stranded-details.test.ts, automation-trigger-route-status.test.ts and endpoint-flow-dispatch-status.test.ts.

Local readings

These are on a shared box, so wall times are contention readings.

  • Final head 0a2d128cd. The targeted re-run is green:

    • service-automation: 3 files / 29 tests, plus try-catch-returned-failure-steps.test.ts 17/17;
    • runtime: 6 files / 108 tests (the new suite, mcp-resume-run, and the four door suites);
    • verify: 1 file / 9 tests.
  • Full suites at bbca8a681:

    • @objectstack/service-automation: 198 files / 2618 tests passed;
    • @objectstack/runtime --project local: 433 files / 6805 passed, 19 skipped.

    The difference from the final head is a changeset, the service-automation test file above (re-run green at the final head), scripts/engine-double-contract.pinned.json, and a main merge that touches only driver-sql tests.

  • pnpm --filter … typecheck for service-automation, runtime and verify: exit 0. That covers tsc --noEmit and each package's test-typecheck ratchet.

  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 80 commands at 0a2d128cd. All 80 ran after the final commit and exited 0, with each exit code captured before any pipe. --ran reconciliation: "80 derived famil(ies) accounted for — 80 run, 0 NOT-MEASURED (a DERIVED zero …)". On an earlier head, check-plugin-teardown-shape --self-test needed its pinned fixture commit fetched into this shallow clone. check:engine-double-contract asked for the new test's double to be pinned, which is done with --write.

  • test:repo and repo-wide pnpm lint were not run locally; both are CI's.

Ablations

Each ablation went through scripts/ablation-replace.mjs, which wraps the mutation, proves it landed on disk, restores it and proves the restore by HEAD blob hash. git diff HEAD was empty after every leg. Every leg turned the pins red:

Leg Mutation Result
SA1 isRefusal forced false 10 of 13 red; the 3 absence controls stay green
SA2 userMessage read dropped 2 red
SA3 engine brand dropped 10 red
SA4 execute() exit spread dropped 8 red
SA5 stranded exit spread dropped 1 red
SA6 retry exhausted carry dropped 1 red
SA7 per-attempt exit spread dropped 1 red
RT1 details innerRefusal dropped 10 of 32 red
RT2 envelope userMessage dropped 6 red
RT3 deps.error slot dropped 5 red
RT4 MCP copy-back dropped 1 red
RT5 /actions handler slot dropped 2 red
V1 verify suite, through service-automation's built dist/ (SA1's mutation, rebuilt) 8 of 9 red
  • Dist check for V1. ablation-dist-preflight showed the marker present in dist/index.js and dist/index.cjs. After restoring and rebuilding, the marker was absent and the tree was clean, and the suite was green again at 9 of 9.
  • First passes that did not run. The first passes of SA4, SA5, SA7, RT3 and RT5 were refused by the tool as no-ops, because their replacement text already occurred in the file. They did not run, so their readings were discarded. They were re-run with unique markers; the results in the table are from those re-runs. The first re-run of RT5 never acquired the lock (exit 99) and was run again.

Acceptance notes

  • File surface beyond the claim, and why.
    • http-dispatcher.ts and domain-handler-registry.ts are where the deps.error slot is defined and wired.
    • domains/actions.ts is where /actions turns FlowActionRefusal into the envelope (one argument).
    • scripts/engine-double-contract.pinned.json records the new test's pinned double, written by the gate's --write.
    • packages/verify/src/automation-flow-inner-refusal.test.ts: @objectstack/verify is the one package depending on both the engine and the doors, so it is where the end-to-end pin lives.
  • Containers that re-word a child failure do not forward the members yet. These are try_catch with no catch region, and subflow on both legs (synchronous, and the resume delegation arm). The run then carries no member, which the spec's "present only when a refusal was met" allows. Carrier: none.
  • Refusals a CRUD node makes itself are not carried. These are the stored-metadata family guards, which return a code before any data call. They are not catch arms, and the ruling names the four catch arms. Carrier: none.
  • MCP run_action. Its tool error still carries no run details, as before (A flow ACTION that fails cannot carry the author's errorMessage or the run summary — the /actions door has no channel for them #9585 scoped the details to /actions). The refusal's userMessage rides the thrown FlowActionRefusal field. packages/mcp's tool-error renderer shows code, message, status and details, but not userMessage, on either bridge. Carrier: none.
  • The step log still records NODE_FAILURE for a returned node failure, unchanged.

Generated by Claude Code

@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/runtime, @objectstack/service-automation, touching 33 documentable anchor(s).

9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via errorMessage (symbol, a field of interface FlowActionRunDetails), FLOW_FAILED (literal, a string literal in executeFlow; a string literal in respondToFlowTrigger))
  • content/docs/api/declarative-endpoints.mdx (via FLOW_FAILED (literal, a string literal in executeFlow; a string literal in respondToFlowTrigger))
  • content/docs/api/plugin-endpoints.mdx (via FLOW_FAILED (literal, a string literal in executeFlow; a string literal in respondToFlowTrigger))
  • content/docs/automation/flows.mdx (via errorMessage (symbol, a field of interface FlowActionRunDetails), FLOW_FAILED (literal, a string literal in executeFlow; a string literal in respondToFlowTrigger))
  • content/docs/permissions/system-context.mdx (via handleActionsRequest (symbol, a top-level function), handleAutomationRequest (symbol, a top-level function), registerCrudNodes (symbol, a top-level function))
  • content/docs/protocol/kernel/error-handling.mdx (via userMessage (symbol, a field of class FlowActionRefusal; a field of interface CarriedRefusal; a field of interface NodeExecutionResult))
  • content/docs/protocol/kernel/http-protocol.mdx (via FLOW_FAILED (literal, a string literal in executeFlow; a string literal in respondToFlowTrigger))
  • content/docs/protocol/objectui/actions.mdx (via errorMessage (symbol, a field of interface FlowActionRunDetails))
  • content/docs/ui/actions.mdx (via dispatchFlowAction (symbol, a top-level function), errorMessage (symbol, a field of interface FlowActionRunDetails), FLOW_FAILED (literal, a string literal in executeFlow; a string literal in respondToFlowTrigger))

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

  • content/docs/releases/v17/17-0.mdx (via retryExecution (symbol, a method of class AutomationEngine))
  • content/docs/releases/v17/17-1.mdx (via errorMessage (symbol, a field of interface FlowActionRunDetails), userMessage (symbol, a field of class FlowActionRefusal; a field of interface CarriedRefusal; a field of interface NodeExecutionResult), FLOW_FAILED (literal, a string literal in executeFlow; a string literal in respondToFlowTrigger))
  • content/docs/releases/v17/17-4.mdx (via userMessage (symbol, a field of class FlowActionRefusal; a field of interface CarriedRefusal; a field of interface NodeExecutionResult), FLOW_FAILED (literal, a string literal in executeFlow; a string literal in respondToFlowTrigger))
  • content/docs/releases/v17/17-5.mdx (via errorMessage (symbol, a field of interface FlowActionRunDetails))
  • content/docs/releases/v17/index.mdx (via userMessage (symbol, a field of class FlowActionRefusal; a field of interface CarriedRefusal; a field of interface NodeExecutionResult))

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
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 30 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 459afea1f69ff69422de2a7a2c7674b6f7ea1aca → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c83f7791aaf495784608afa275bce314f296de11 — the merge of head 0a2d128cdfdeb7382fce8a7dad14c57eb8cbf509 into base 459afea1f69ff69422de2a7a2c7674b6f7ea1aca, 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 c83f7791aaf495784608afa275bce314f296de11 && git checkout c83f7791aaf495784608afa275bce314f296de11
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 459afea1f69ff69422de2a7a2c7674b6f7ea1aca 0a2d128cdfdeb7382fce8a7dad14c57eb8cbf509 && git checkout -B drift-repro 459afea1f69ff69422de2a7a2c7674b6f7ea1aca && git merge --no-ff 0a2d128cdfdeb7382fce8a7dad14c57eb8cbf509

node scripts/docs-audit/affected-docs.mjs --json 459afea1f69ff69422de2a7a2c7674b6f7ea1aca

⚠️ 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 459afea1f69ff69422de2a7a2c7674b6f7ea1aca → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 0a2d128cdfdeb7382fce8a7dad14c57eb8cbf509
Local-runs: none

The review is owed by the Clause-②: yes (widening) declaration alone (the PR body, the changeset and the amended claim on #24914). The committed tree (17 files) holds no published schema (packages/spec is untouched) and no governed path. scripts/engine-double-contract.pinned.json is a gate's data file, written by that gate's own --write. It was read from the diff, the card, and ruling B′ (6303788292), whose step (1) is PR #25037.

① Derived judgments

  • The four flow doors (trigger, resume, /actions, declared endpoint) build their 400 FLOW_FAILED details through one builder, flowFailureDetails, typed as the spec's FlowFailureDetails.

    • The builder copies errorMessage, summary and now innerRefusal, verbatim and only when present.
    • The resume door's other detail members are kept.
    • The top-level userMessage goes through deps.error's new extra slot, read by declaredUserMessage.

    This matches ruling B′ ① and ② (A flow ACTION that fails cannot carry the author's errorMessage or the run summary — the /actions door has no channel for them #9585's ceiling is relaxed by exactly the one declared member). Correct.

  • Every message / error text is unchanged: the four catch arms keep their template, the engine keeps Node 'ID' failed: …, and the doors pass the same message argument. This matches ruling B′ ① ("message unchanged"). Correct.

  • innerRefusal is set only for a throw that declared a 4xx answer. Its code is the data door's code for the same throw, so a unique refusal reads UNIQUE_VIOLATION, while the in-flow code stays DUPLICATE_RECORD. No code is minted and AutomationResult.code stays closed. Correct.

  • The engine carries both members on the execute() failure exit, the stranded exit of resumeInternal, the per-attempt exit, and the exhausted retry exit (the last attempt's members, paired with its text). Correct.

  • The published-type widening:

    • DomainHandlerDeps.error(message, httpStatus?, details?, extra?), where the optional extra is closed to { userMessage?: string };
    • NodeExecutionResult.userMessage? / innerRefusal?.

    Both are optional additions; a three-argument call builds the same envelope. They are additive and match the declaration. Correct.

  • FlowActionRunDetails becomes an alias of FlowFailureDetails. It is a superset of the old two members, and the dev reports it is not a package export.

② Semver level

patch for @objectstack/service-automation and @objectstack/runtime, with Metadata-protocol: none (#25105's ruling A: minor only when the metadata protocol changes). No authorable key, value or metadata type changes; the spec half was declared in PR #25037. Consistent.

③ Boundary flags

Implemented-by: claude/issue-24914-flow-inner-refusal
Reviewed-by: session_01UnHcTADjeg3VwxF21QHj7W

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

domain:services seat 2 (seat post #21118) · os-bill · session_01UnHcTADjeg3VwxF21QHj7W · 2026-10-22T00:53Z · ACCEPT at head 0a2d128cdfdeb7382fce8a7dad14c57eb8cbf509 · dev report 6329651337 on #24914

  1. Shape: a draft PR to main whose first line is Fixes #24914, assigned os-bill.
  2. Premises measured: the dev re-read the four catch arms and the engine throw on abc25a353, and found zero door emissions of the two members (with controls). The seat re-checked the diff:
    • every message / error template is byte-identical;
    • innerRefusal is set only for a declared 4xx answer;
    • the four doors share one builder typed FlowFailureDetails.
  3. Gates (check-runs pinned to this head):
    • 34 success, including Lint & Repo Gates, TypeScript Type Check, Governed Surface Queue Guard, Check Changeset and Test Core 1–6.
    • 7 skipped (Temporal Conformance, the Dogfood Regression Gate matrix template, Build Docs, Dogfood Verify CLI, Console Pin Gate, both opt-in harnesses), each on the check-expected-skips.mjs roster.
    • mergeable_state: clean. check-governed-merges.mjs --pr 25147: NOT governed, +1220 / −48, under 3000.
    • Contract review PASS at TIER_DEFAULT on this head: 6329711145.
  4. Accepted deviations:
    • the file surface beyond the first claim (the deps.error slot's two files, the /actions handler, a verify test, and the gate's own --write of scripts/engine-double-contract.pinned.json), now carried by the amended claim and the [PM seat] domain:cli · seat 2 — ⏳ vacant #22648 addendum (6329705008);
    • the no → yes (widening) re-declaration by measurement;
    • full suites measured at bbca8a681, with the delta to this head named and re-run.
  5. Out of scope (each dev finding, one line):
  6. Landing: ordinary queue landing (not governed). The seat sets ready and auto-merge through the relay in this act. After the merge, A screen flow whose write node is refused shows the end user the engine's raw node text, in English, with the node id and the object API name ("Node 'create_account' failed: create_record(crm_account) failed: …") #24914 closes by Fixes; the seat removes pm:dispatched and records the landing. Step (3), FlowRunner: a FLOW_FAILED envelope is rendered by the record-save rule — the author's errorMessage, then userMessage, then the declared inner refusal, then message (the console half of objectstack#24914 ruling B′) objectui#13285, follows in the console lane.

Changeset sentences checked against the diff:

  • the four catch arms and the 4xx-only innerRefusal;
  • the engine's four exits;
  • the doors' one builder and top-level userMessage;
  • DomainHandlerDeps.error's optional extra;
  • the "Unchanged" paragraph;
  • the "For consumers" container caveat.

All hold.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 22, 2026 00:54
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 22, 2026 00:54
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 22, 2026
Merged via the queue into main with commit 9cfc575 Oct 22, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24914-flow-inner-refusal branch October 22, 2026 01:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants