Repository navigation
fix(automation): a map item or subflow child carries its refusal to the flow's caller - #27740
Conversation
…he run Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
…ntainer-carries-refusal
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 7 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 43a9a7bc5b0affdf65c8989608b5885c2e06fc0d && git checkout 43a9a7bc5b0affdf65c8989608b5885c2e06fc0d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 81d00a4ed938eb896859630498fc59d91183cf71 25f0fb7f39ea96999e577e952680a0defbaea099 && git checkout -B drift-repro 81d00a4ed938eb896859630498fc59d91183cf71 && git merge --no-ff 25f0fb7f39ea96999e577e952680a0defbaea099
node scripts/docs-audit/affected-docs.mjs --json 81d00a4ed938eb896859630498fc59d91183cf71 |
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F 维护者速读(席位终稿)
席位意见: ACCEPT。复用现有搬运,无新词汇、无 spec 改动; 席位核对ACCEPT · head
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
Requested by Sales · project thread
Fixes #27601
Clause-②: no
Metadata-protocol: none
Skills-docs: none — no authorable metadata changes; this is runtime carriage of members the spec already declares
维护者速读
map的失败子项和subflow的失败子流程,现在把子运行的结构化拒绝(innerRefusal与userMessage)带到父运行结果上,嵌套多层也能到顶。packages/services/service-automation/src/builtin/map-node.ts(失败分支的展开)、builtin/subflow-node.ts(同一处)、src/inner-refusal.ts(头注释改为两种容器都是承载者)。pnpm --filter @objectstack/service-automation exec vitest run src/flow-inner-refusal.test.ts,预期 18 个测试全绿,其中新增 5 个(map、map 首个失败项、subflow、map→subflow 嵌套、无拒绝对照)。What changes
Both containers build a NEW failure when their child run fails, and that new failure dropped the child run's
innerRefusal/userMessage. Each failure arm now spreadscarriedRefusalOfResult(child)(an existing helper ininner-refusal.ts) onto the result it returns. The engine's existingattachCarriedRefusalcall indag-traversal.tsthen brands the throw exactly as it does for a data node's failure, and the terminal exit reads it withreadCarriedRefusal. So the existing carriage crosses the two containers: no new refusal vocabulary, no new member, no change toattachCarriedRefusal, nopackages/specedit.mapwith several failing items: the batch returns on its first failing item (unchanged behaviour), so it carries THAT item's refusal. That matches how the other carriers choose: one failure, the one that ended the run.carriedRefusalOfResultreturns{}, so nothing is added (unchanged answer, pinned by the control).errortext: byte-identical; the new tests pin the full chain.inner-refusal.tsheader: theCARRIED_REFUSALparagraph now namessubflowandmapas carriers and says what a map carries.try_catchwith no catch region is still named as a non-carrier.Tests
Added to
src/flow-inner-refusal.test.ts(the harness the existing inner-refusal pins use: a realAutomationEngine, the real CRUD / map / subflow executors, a data engine throwing the platform's real error classes), all throughengine.execute:map: a refused write in a map item reaches the run asinnerRefusal{ code: 'RESOURCE_CONFLICT', message }plususerMessage, and theerrorchain is unchanged.map: the batch stops at the first failing item and carries that item's refusal (VALIDATION_FAILED, one write call).subflow: the child's refusal reaches the parent run (code plus userMessage).map→subflownested: aPERMISSION_DENIEDtwo containers deep reaches the top run.errortext as before.Red before the fix (3ef74cf minus the fix lines)
Ablation through scripts/ablation-replace.mjs (fix committed first, at 3ef74cf)
The tests import the executors from
src/by relative path, so nodist/rebuild is in the resolution path.The control stays green under both ablations, as it should: it asserts the absence the ablation produces.
Package gates at 25f0fb7 (after merging origin/main)
Acceptance notes
try_catchwith no catch region (builtin/try-catch-node.ts) still re-words a try-region failure into a new returned failure without the members, so a refusal inside a guarded region that is not caught reaches the caller withoutinnerRefusal. Outside this card's file surface; the header keeps naming it as a non-carrier. Not filed: no public-door measurement in this run.mapitem orsubflowchild that PAUSES and later fails on resume reaches the parent through the bubble path, not through these failure arms. That path is not covered here and was not measured.Verification
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 59 families derived at 25f0fb7; every command run with its exit captured;--ranreconciliation: 59 accounted, 58 run (exit 0), 1 NOT-MEASURED (pnpm check:dual-build-cjs-loads, exit 3 PREREQUISITE NOT MET: needs the full workspacedist/; CI runs it).package.json,scripts/checklist-select.mjsandscripts/pm/check-skill-line-ratchet.mjs(gate definitions, none of them in this diff). CI runs the families from the merge commit.merge-treeagainst.!in the title, no(narrowing)arm).Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F