Repository navigation
fix(service-automation): a try_catch with no catch region and a subflow child that fails on resume carry the refusal to the caller - #27872
Conversation
…ow child that fails on resume carry the refusal to the caller A try_catch with no catch region re-worded its try region's failure into a new returned failure, and the delegated resume leg (a caller resuming a parent paused at a subflow) re-worded the child's failure the same way; both dropped innerRefusal / userMessage. Both now spread the existing carriage (readCarriedRefusal of the last failed attempt; carriedRefusalOfResult of the child's result). The up-bubble leg, the one a map item takes, already carried it and is now pinned. The inner-refusal header names the carriers. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxM6XZhZWEMvCNT26B
📓 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 4483d99cef6a0f5e2dac42058a86b77c404af77e && git checkout 4483d99cef6a0f5e2dac42058a86b77c404af77e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c4cb5ec0c3936dd2c24ae2aa9a349449d8e33bef 4a8756c86c5a483f3b8dd5e36bf331e5488bca81 && git checkout -B drift-repro c4cb5ec0c3936dd2c24ae2aa9a349449d8e33bef && git merge --no-ff 4a8756c86c5a483f3b8dd5e36bf331e5488bca81
node scripts/docs-audit/affected-docs.mjs --json c4cb5ec0c3936dd2c24ae2aa9a349449d8e33bef |
|
domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL 维护者速读
席位意见:ACCEPT。 席位核对Head-sha:
|
Requested by Elon · project thread
domain:server#4 · os-elon-musk · session_016WV4cxM6XZhZWEMvCNT26B
Fixes #27743
Clause-②: no
Metadata-protocol: none
Skills-docs: none — no authorable metadata changes; this is runtime carriage of members the spec already declares
维护者速读
innerRefusal与userMessage)带到流程调用方:没有 catch 区的try_catch,以及先暂停、恢复后才遇到拒绝的subflow子流程(调用方恢复的是父运行 id 时)。map/subflow的同步失败分支,这是它剩下的两条。builtin/try-catch-node.ts(无 catch 区的返回)、src/run-resume.ts(委派恢复腿的子流程失败返回)、src/inner-refusal.ts(头注释列出全部承载者)。pnpm --filter @objectstack/service-automation exec vitest run src/flow-inner-refusal.test.ts,预期 26 个测试全绿,其中新增 8 个(修复前 4 个红)。Measured at the public flow doors (
engine.execute/engine.resume)The harness is the one the existing inner-refusal pins use: a real
AutomationEngine, the real CRUD / try_catch / map / subflow executors, and a data engine throwing the platform's real error classes.try_catch, try region refused, no catch regionexecuteinnerRefusal/userMessageabsent, text onlyexecutePERMISSION_DENIED)subflowchild pauses, then its write is refused; caller resumes the PARENT's run id (delegated leg,run-resume.ts)resume(parent)subflow→subflowtwo frames deep, resumed at the top idresume(top)resume(child)stranded, child's own result)mapitem pauses, then refused; resumed at the child's id (the bubble leg a map item takes)resume(child)So the mechanism assumption held for path 1. For path 2 it held only for the delegated
subflowleg; the bubble leg (bothsubflowandmap) already reached the caller, because the caller of that leg holds the child's own terminal result, whichresumeInternal's stranded exit already brands.What changes
try-catch-node.ts: each failed try attempt recordsreadCarriedRefusal(err)(replaced per attempt, so the last attempt wins, the ruleretryExecutionuses). The no-catch return spreads it beside the unchangederror. The engine's existingattachCarriedRefusalthen brands this node's throw like any data node's.run-resume.ts: the delegated leg's!childRes.successreturn spreadscarriedRefusalOfResult(childRes), exactly as the synchronoussubflowfailure arm does since fix(automation): a map item or subflow child carries its refusal to the flow's caller #27740.inner-refusal.ts: header updated: it now lists both new carriers and the bubble leg, and names the one remaining non-carrier (atry_catchwhose catch region itself fails). Doc comment ofcarriedRefusalOfResultnames its new reader.packages/specedit.Tests (
src/flow-inner-refusal.test.ts, +8)try_catch: the region's refusal reaches the run, with code and userMessage; theerrorchain is pinned byte for byte.try_catch+ retry: carries the last attempt's refusal, never an earlier one's.subflowdelegated leg (resume at the parent id): code, userMessage, and theerrortext.subflow→subflowdelegated twice: the refusal crosses both frames.subflowup-bubble leg (resume at the child id): plain pin, already carried.mapitem bubble leg (resume at the child id): plain pin, already carried.Red before the fix (tests first, fix absent)
Ablation through scripts/ablation-replace.mjs (fix committed first)
Gates
@objectstack/service-automation: full vitest 218 files / 2803 tests green;tsc --noEmitandcheck:test-typecheckgreen.dispatch-gates.mjs --commandsfamilies pluspnpm lint: 61 derived; 60 exit 0, 1 NOT MEASURED (check:dual-build-cjs-loadsexit 3, needs a whole-tree build);--ranreconcile: 0 unrun.pnpm lintexit 0.Out of scope (reported, not changed)
try_catchwhose CATCH region itself fails returns a failure built from the handler's error and carries no member, so a refusal met by a node inside the catch region is dropped too. Same file, outside the claimed surface (the no-catch-region failure); the header names it as the remaining non-carrier.mapnode at the PARENT's run id (rather than the item child's) re-runs the map, advances past the parked item, and records the parentcompletedwhile the item's child stays suspended. No refusal is involved, so nothing to carry; it is a resume-semantics question for the seat.🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4cxM6XZhZWEMvCNT26B