Repository navigation
[finding] service-automation: map rolls a refused child up as an ordinary success too — the same fail-open shape as #18110, second file #18555
Description
Reported by the domain:services dev dispatched on #18110 as an out-of-scope finding, and filed here by the seat — dev agents report findings with dedupe words; they ⛔ do not file.
Mechanism
packages/services/service-automation/src/builtin/map-node.ts has the identical shape as #18110's file: it branches on child.status === 'paused' (:191) and on !child.success (:207), and has no refused arm. Every other child status falls through the same success path, and the child's output is pushed into state.results at :232.
⇒ A refusing end inside a map unit's child flow is rolled up by the parent as an ordinary success — the same fail-open direction #18110 describes for subflow, in a second file.
Seat verification, ⛔ not carried from the report
Re-taken by the domain:services seat on origin/main 79a046f8 with git show origin/main:PATH, ⛔ not a worktree grep:
| reading | result |
|---|---|
child.status === 'paused' |
present, :191 |
!child.success |
present, :207 |
refused anywhere in the file |
0 hits |
control: child in the same file |
31 hits ⇒ the instrument is reading |
⛔ The runtime effect was not driven on map specifically. #18110's dev did reproduce the equivalent on subflow live (parent records completed, fires its own successMessage, downstream nodes run). Whoever takes this card should drive it on map rather than reason by analogy from subflow.
Why this is its own card and ⛔ not part of #18110
- [finding] service-automation: a
subflowchild that endsrefusedis rolled up by the parent as an ordinary success — the refusal reaches nobody #18110 names onlysubflow. Its body, its triage grading and its dispatch order are all scoped tosubflow-node.ts. Ridingmapin would widen a p1 card's verification surface after grading. ⚠️ service-automation:subflowandmaproll a child's contained failures into the parent'sfailedthrough theExecutionStepMetricsfailure slot ruled on #15617 #16314 treatedsubflowandmapas ONE surface — but for the FAILURE direction (rolling a child's contained failures into the parent'sfailed). This card and [finding] service-automation: asubflowchild that endsrefusedis rolled up by the parent as an ordinary success — the refusal reaches nobody #18110 are the refusal direction, which service-automation:subflowandmaproll a child's contained failures into the parent'sfailedthrough theExecutionStepMetricsfailure slot ruled on #15617 #16314 explicitly does not cover. ⛔ Do not close either as a duplicate of service-automation:subflowandmaproll a child's contained failures into the parent'sfailedthrough theExecutionStepMetricsfailure slot ruled on #15617 #16314.
Blocked on the same decision
Blocked-by: #18110
Both files need the same new channel: a node executor currently has no way to terminate its run as refused. NodeExecutionResult (barrel-exported from src/index.ts:9) declares suspend? but no refusal member, and FlowRefusalSignal is thrown from exactly one site (engine.ts:9319), only for node.type === 'end'. #18110 is in the decision box awaiting the maintainer's choice of mechanism; that one decision governs this card too, so ⛔ this should not be dispatched ahead of it.
Dedupe words
map node refused child rollup · map-node.ts child.status paused · refused map unit parent continues · ADR-0037 A2 map refused · #18110 inverse map
Related: #18110 · #16314 · #14945 · #15788
⛔ type and priority are the triage seat's; this card is filed ungraded and unassigned. domain:services applied because the landing site is packages/services/service-automation, this lane's.
Generated by Claude Code
Activity
hotlong commentedon Sep 17, 2026
Pointer — director seat,
session_01Wj1HUjzyeiBQ8atRf1ZhaL· 2026-09-17T12:21ZGoverned by the ruling on objectstack#18110 (batch #145 item 4, letter A, maintainer 「同意,其他也同意」):
refuse?/refusalMessage?joinNodeExecutionResultbesidesuspend?, the engine throwsFlowRefusalSignalwhere the suspend signal is thrown, andmap-node.tsgains the samerefusedarm in the same PR assubflow-node.ts— one channel, two sites. ⛔ Not closed here; the implementing PR carriesPart of #18555and the lane seat closes it at landing.Generated by Claude Code
huangyiirene commentedon Sep 17, 2026
Claim: PM loop round 1 — member of a folded family dispatch; chain head is #18110
Session:
session_01QGMBhvUoyD8t5zY8xHQhnPBranch:
claude/issue-18110-subflow-refused-rollup(⭐ shared, named after the chain-head card — ⛔ this card gets no branch of its own)Worktree:
objectstack-issue-18110Domain:
domain:servicesSeat:
domain:services#1File surface:
packages/services/service-automation/src/—engine.ts,builtin/subflow-node.ts,builtin/map-node.tsand their co-located tests (stop on breach; explain in the report)Container & model:
M,mode:subagent, model: default judgment tier — one dispatch covers both members; the tier derivation is quoted in full on the chain-head claim.Clause-②: yes
Thread-read: 5716379289
Serial constraints cleared:
none— the full serial-constraint check for this family is recorded once, on the chain-head claim at #18110 (issuecomment-5716693954), measured at 2026-09-17T15:11Z: lanepm:dispatcheddisjoint, all 23 open PRs intersected file-by-file with 0service-automationhits, batch siblings disjoint, H17 trigger index 0 intersection.Why this card is folded rather than dispatched on its own
One channel, two sites. The director's ruling on #18110 (batch #145 item 4, letter A, maintainer 「同意,其他也同意」) governs this card by name and states it directly: "
subflow-node.tsandmap-node.tsgain therefusedarm that sets it. One channel, two sites — this ruling governs #18555 too." A node executor has no terminal-refusal channel today; both files need the same new one, so splitting them would mean either building the channel twice or landing one consumer of it and leaving the other dead.⭐ And the triage grading of this card names the concrete cost of NOT folding: a successor seat fixes
subflow, leavesmapin place — the shape recorded in #18559 (one slot fixed for two consumers, a third missed) and #18540. ⛔ That account has already been paid once this shift; it is ⛔ not being paid again knowingly.Delivery shape for this member (from the ruling, ⛔ not this seat's invention)
The implementing PR carries
Part of #18555— ⛔ notFixes. The ruling is explicit that this card is ⛔ not closed by the PR's closing keywords; the lane seat closes it at landing, after verifying themaparm independently of thesubflowarm.Dispatched by the
domain:servicesPM seat (objectstack#6021) at 2026-09-17T15:11Z.needs:contract-reviewhung on this card in the same claim act (dual-carrier rule). ⛔ The dev leaves no second claim — this comment is the identity.Generated by Claude Code
huangyiirene commentedon Sep 17, 2026
Pointer — implementing PR is open: #18706 (draft), branch
claude/issue-18110-subflow-refused-rollup, carryingPart of #18555— ⛔ notFixes; the lane seat closes this card at landing after verifying themaparm independently.The
maparm is driven, ⛔ not reasoned by analogy fromsubflow, exactly as this card asked.src/builtin/map-refused-rollup.test.ts(7 tests) pins it on its own: a parent whosemapunit reaches a refusing child recordsrefused(notcompleted), does not fire its ownsuccessMessage, stops the batch at that item (later items never run), does not walk past the map node, and keeps the batch'sselected/actedrollup. A control with no refusing item is green on both sides.Proven independent of the
subflowarm by ablation: deleting onlymap-node.ts'srefusedarm reds 6 tests in this file whilesubflow-refused-rollup.test.tsstays entirely green, and deleting only thesubflowarm does the reverse. Each ablation leg proved its mutation reached disk (HEAD blob hash vs mutated blob hash) and restored byte-identical fromHEAD.The full structured report is on the chain-head card, #18110.
Generated by Claude Code
huangyiirene commentedon Sep 17, 2026
LANDED — PR #18706 merged. This card's arm (
map) was verified on its own, then closed by this seat.domain:servicesseat (objectstack#6021),session_01QGMBhvUoyD8t5zY8xHQhnP, R1, written at 2026-09-17T17:25Z. Judged onorigin/mainafter the merge, ⛔ never on the PR-closed event.The PR carried
Part of #18555— ⛔ notFixes— so the ruling put this close on the seat, after verifying this card's arm independently of its sibling's. That is the whole reason the two cards were folded into one PR rather than trusted to one test run.This card's arm, measured by itself on
origin/main⭐ And it is pinned separately from the
subflowarm: at review time, revertingmap-node.tsalone reddened 6 map tests while all 6 subflow tests stayed green, and revertingsubflow-node.tsalone reddened 5 subflow tests while all 7 map tests stayed green. ⇒ neither arm is riding the other's coverage.Why this card existed at all
Triage graded it p1 rather than p2 on exactly this reasoning, and it was right: the branch set in
map-node.tswas line-for-line identical tosubflow-node.ts's, missing the same arm. ⭐ Had it been left for later, the successor would have fixedsubflowand leftmapin place — the shape recorded in #18559 and #18540. One channel, two sites, one PR.Full landing record, the channel's own readings and the contract-review pointer:
#18110(issuecomment-5718303504).The synchronous path only. A child that pauses then refuses on its resumed leg is ⛔ not covered on either site — filed as #18714, and named in the changeset's own scope paragraph so the release note does not overclaim.
Generated by Claude Code
fix(service-automation): on the synchronous path, a child run that re…
fix(service-automation): a resumed child's refusal rolls up on both l…