Skip to content

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

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-amh4u6
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-amh4u6

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 时)。
  • 为什么改:这两处都把失败改写成一条新的失败,结构化成员被丢掉,调用方只拿到整串节点报错文字,界面无法显示简洁的拒绝提示。fix(automation): a map item or subflow child carries its refusal to the flow's caller #27740 修了 map / subflow 的同步失败分支,这是它剩下的两条。
  • 风险与回滚:很低。两处各加一行展开现有的携带函数,错误文字一字不变;没遇到拒绝时展开的是空对象。回滚即还原这一个 commit。
  • 先看: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.

Path Door Before After
1. try_catch, try region refused, no catch region execute dropped: innerRefusal / userMessage absent, text only carried
1b. same, with a retry ladder (validation, then permission) execute dropped carries the LAST attempt's refusal (PERMISSION_DENIED)
2a. subflow child pauses, then its write is refused; caller resumes the PARENT's run id (delegated leg, run-resume.ts) resume(parent) dropped carried
2b. same, subflow → subflow two frames deep, resumed at the top id resume(top) dropped carried through both frames
2c. same child, caller resumes the CHILD's run id (up-bubble leg) resume(child) already carried (stranded, child's own result) unchanged, now pinned
2d. map item pauses, then refused; resumed at the child's id (the bubble leg a map item takes) resume(child) already carried unchanged, now pinned

So the mechanism assumption held for path 1. For path 2 it held only for the delegated subflow leg; the bubble leg (both subflow and map) already reached the caller, because the caller of that leg holds the child's own terminal result, which resumeInternal's stranded exit already brands.

What changes

  • try-catch-node.ts: each failed try attempt records readCarriedRefusal(err) (replaced per attempt, so the last attempt wins, the rule retryExecution uses). The no-catch return spreads it beside the unchanged error. The engine's existing attachCarriedRefusal then brands this node's throw like any data node's.
  • run-resume.ts: the delegated leg's !childRes.success return spreads carriedRefusalOfResult(childRes), exactly as the synchronous subflow failure 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 (a try_catch whose catch region itself fails). Doc comment of carriedRefusalOfResult names its new reader.
  • No new vocabulary, no new member, no helper change, no packages/spec edit.

Tests (src/flow-inner-refusal.test.ts, +8)

  1. try_catch: the region's refusal reaches the run, with code and userMessage; the error chain is pinned byte for byte.
  2. try_catch + retry: carries the last attempt's refusal, never an earlier one's.
  3. Control: a retry whose last attempt met a fault carries neither member, even after an earlier refusal.
  4. subflow delegated leg (resume at the parent id): code, userMessage, and the error text.
  5. subflow → subflow delegated twice: the refusal crosses both frames.
  6. subflow up-bubble leg (resume at the child id): plain pin, already carried.
  7. map item bubble leg (resume at the child id): plain pin, already carried.
  8. Control: a child that fails on resume with a fault carries neither member on the delegated leg.
Red before the fix (tests first, fix absent)
× the region's refusal reaches the run — code and userMessage — and the text keeps its chain
× a retry ladder carries the LAST attempt's refusal, never an earlier one's
× subflow, resumed at the PARENT's run id (the delegated leg): the child's refusal reaches the caller
× subflow → subflow, resumed at the TOP run id: the refusal crosses both delegated frames
AssertionError: expected undefined to deeply equal { code: 'RESOURCE_CONFLICT', …(1) }
AssertionError: expected undefined to deeply equal { code: 'PERMISSION_DENIED', …(1) }
Tests  4 failed | 22 passed (26)
Ablation through scripts/ablation-replace.mjs (fix committed first)
### ABLATE try-catch-node  (anchor "...lastCarried,"  x1 -> x0, blob 5186063a -> 50139552)
× the region's refusal reaches the run — code and userMessage — and the text keeps its chain
× a retry ladder carries the LAST attempt's refusal, never an earlier one's
Tests  2 failed | 24 passed (26)
ok restored: blob == HEAD (5186063a) and `git diff HEAD` is empty

### ABLATE run-resume  (anchor "...carriedRefusalOfResult(childRes),"  x1 -> x0, blob 51da355c -> a75e1d78)
× subflow, resumed at the PARENT's run id (the delegated leg): the child's refusal reaches the caller
× subflow → subflow, resumed at the TOP run id: the refusal crosses both delegated frames
Tests  2 failed | 24 passed (26)
ok restored: blob == HEAD (51da355c) and `git diff HEAD` is empty

Gates

  • @objectstack/service-automation: full vitest 218 files / 2803 tests green; tsc --noEmit and check:test-typecheck green.
  • dispatch-gates.mjs --commands families plus pnpm lint: 61 derived; 60 exit 0, 1 NOT MEASURED (check:dual-build-cjs-loads exit 3, needs a whole-tree build); --ran reconcile: 0 unrun. pnpm lint exit 0.

Out of scope (reported, not changed)

  • A try_catch whose 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.
  • Measured while probing path 2: resuming a run paused at a map node at the PARENT's run id (rather than the item child's) re-runs the map, advances past the parked item, and records the parent completed while 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

…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
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/services/service-automation/src/inner-refusal.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/services/service-automation/src/inner-refusal.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 58 of 210 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 — 7 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 c4cb5ec0c3936dd2c24ae2aa9a349449d8e33bef → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4483d99cef6a0f5e2dac42058a86b77c404af77e — the merge of head 4a8756c86c5a483f3b8dd5e36bf331e5488bca81 into base c4cb5ec0c3936dd2c24ae2aa9a349449d8e33bef, 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 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

维护者速读

席位意见:ACCEPT。

席位核对

Head-sha: 4a8756c86c5a483f3b8dd5e36bf331e5488bca81

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/m tests tooling

Projects

None yet

1 participant