Skip to content

[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

@os-project-manager

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

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

  1. hotlong commented on Sep 17, 2026

    @hotlong
    Contributor

    Pointer — director seat, session_01Wj1HUjzyeiBQ8atRf1ZhaL · 2026-09-17T12:21Z

    Governed by the ruling on objectstack#18110 (batch #145 item 4, letter A, maintainer 「同意,其他也同意」): refuse? / refusalMessage? join NodeExecutionResult beside suspend?, the engine throws FlowRefusalSignal where the suspend signal is thrown, and map-node.ts gains the same refused arm in the same PR as subflow-node.ts — one channel, two sites. ⛔ Not closed here; the implementing PR carries Part of #18555 and the lane seat closes it at landing.


    Generated by Claude Code

  2. self-assigned this
    on Sep 17, 2026
  3. huangyiirene commented on Sep 17, 2026

    @huangyiirene
    Collaborator

    Claim: PM loop round 1 — member of a folded family dispatch; chain head is #18110

    Session: session_01QGMBhvUoyD8t5zY8xHQhnP
    Branch: claude/issue-18110-subflow-refused-rollup (⭐ shared, named after the chain-head card — ⛔ this card gets no branch of its own)
    Worktree: objectstack-issue-18110
    Domain: domain:services
    Seat: domain:services#1
    File surface: packages/services/service-automation/src/ — engine.ts, builtin/subflow-node.ts, builtin/map-node.ts and 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: lane pm:dispatched disjoint, all 23 open PRs intersected file-by-file with 0 service-automation hits, 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.ts and map-node.ts gain the refused arm 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, leaves map in 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 — ⛔ not Fixes. 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 the map arm independently of the subflow arm.

    Dispatched by the domain:services PM seat (objectstack#6021) at 2026-09-17T15:11Z. needs:contract-review hung 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

  4. huangyiirene commented on Sep 17, 2026

    @huangyiirene
    Collaborator

    Pointer — implementing PR is open: #18706 (draft), branch claude/issue-18110-subflow-refused-rollup, carrying Part of #18555 — ⛔ not Fixes; the lane seat closes this card at landing after verifying the map arm independently.

    The map arm is driven, ⛔ not reasoned by analogy from subflow, exactly as this card asked. src/builtin/map-refused-rollup.test.ts (7 tests) pins it on its own: a parent whose map unit reaches a refusing child records refused (not completed), does not fire its own successMessage, stops the batch at that item (later items never run), does not walk past the map node, and keeps the batch's selected / acted rollup. A control with no refusing item is green on both sides.

    Proven independent of the subflow arm by ablation: deleting only map-node.ts's refused arm reds 6 tests in this file while subflow-refused-rollup.test.ts stays entirely green, and deleting only the subflow arm does the reverse. Each ablation leg proved its mutation reached disk (HEAD blob hash vs mutated blob hash) and restored byte-identical from HEAD.

    The full structured report is on the chain-head card, #18110.


    Generated by Claude Code

  5. removed their assignment
    on Sep 17, 2026
  6. huangyiirene commented on Sep 17, 2026

    @huangyiirene
    Collaborator

    LANDED — PR #18706 merged. This card's arm (map) was verified on its own, then closed by this seat.

    domain:services seat (objectstack#6021), session_01QGMBhvUoyD8t5zY8xHQhnP, R1, written at 2026-09-17T17:25Z. Judged on origin/main after the merge, ⛔ never on the PR-closed event.

    The PR carried Part of #18555 — ⛔ not Fixes — 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

    origin/main tip: 5762eaf7e  … (#18706)
    commit window (400):  (#18706) = 1   ·  positive control (#18713) = 1  ·  (#99999) = 0
    
    ARM 2 — this card:
      `refuse` in packages/services/service-automation/src/builtin/map-node.ts   = 6
      builtin/map-refused-rollup.test.ts present on main                          = yes
      negative control in the same file                                           = 0
    

    ⭐ And it is pinned separately from the subflow arm: at review time, reverting map-node.ts alone reddened 6 map tests while all 6 subflow tests stayed green, and reverting subflow-node.ts alone 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.ts was line-for-line identical to subflow-node.ts's, missing the same arm. ⭐ Had it been left for later, the successor would have fixed subflow and left map in 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).

    ⚠️ Scope limit, same as the sibling

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions