Skip to content

automation: subflow guards read top-level nodes only, so a subflow or map inside a region body escapes every subflow guard #28846

Closed

Description

@objectstack-fleet

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

Filing gate: ① out-of-scope finding of the #28840 dev (os-dev-report 6565979681). Read in the tree by the seat: subflowTargets in packages/services/service-automation/src/subflow-guards.ts (lines 59–69 on origin/main) loops over flow.nodes only. Not measured on the wire. Filed by domain:server seat 3. ⛔ Not a claim.

Reach: every subflow guard reads a flow's child targets through subflowTargets:

It walks the top-level nodes only. A subflow or map node inside a region body (loop, parallel branch, try_catch slot) is never listed. A flow can therefore call a disabled or under-reaching child from inside a region and arm anyway. The guards' verdicts depend on where the author places the node, not on what the flow calls.

Wanted

  1. Measure first: a caller whose subflow sits in a loop body, aimed at a disabled packaged child. Does it arm today, and does a top-level twin decline?
  2. If the region case escapes, subflowTargets lists targets at every region depth, reusing the existing region walk (collectFlowGraphs in packages/spec/src/automation/control-flow.zod.ts) rather than a new walker. Every guard then sees the same set.
  3. ⛔ No new gate: the guards exist, and this fixes what they read.

This narrows arming: a flow that arms today may decline. So it is Clause-②: no (narrowing), with a CONTRACT_REVIEW_TIER review owed. The dev measures shipped flows (showcase, dogfood, hotcrm) for a region-nested subflow / map.

Dedupe

Read open issues for subflowTargets, subflow region and subflow nested region guard: only #28840, which consumes subflowTargets and is in flight. New.

Reader who acts: domain:server (packages/services/service-automation).

Links back: #28840, #28806, PR #28838

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 2, 2026
  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

    Path: 能用的自动化 | none | P③

    Triage: p3 · domain:server · area:automation · 2026-11-02T00:14Z

    ⛔ Not a claim.

  3. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Nov 2, 2026
  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

    Claim: PM loop round 1 · card thread · 2026-11-02T03:31Z
    Session: session_01FEkQ5kE5BrPUPNMMyv6oCH (the seat); the card thread is session_01FEkQ5omF1nCbQTGgW1nbRA
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-9kerqb (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#3
    Ruling-ref: the maintainer, 2026-11-01T03:46Z: 「server 车道现在就是你负责,任务有一大堆啊,都可以派,包括 p3」, with 2026-10-26T10:29Z 「p1 p2 处理完了,就可以处理 p3」, and 2026-11-01T06:39Z 「server 队列还是很多任务,再开一个席位吧」. Seat 3 rules subflowTargets reuses collectFlowGraphs, and carries triage should-fix 1 (spec prose), recorded in the order.
    File surface: packages/services/service-automation/src/subflow-guards.ts (subflowTargets), subflow-notice-guard.ts if its reachability needs the container rule, their tests, packages/spec/src/automation/save-notice-placement.ts header and flow.zod.ts comment (prose only), changesets. ⛔ No new gate; ⛔ no FLOW_PAUSE_CAPABLE_NODE_TYPES change; ⛔ no ui/**.
    Container & model: S, mode:thread, model: opus (--tier names no path mandate: PM's call, default), effort: default (the coordinator sets effort only from the maintainer's words)
    Clause-②: no (narrowing) — a flow whose region-nested subflow/map reaches a disabled or suspending child, and which arms today, may decline; CONTRACT_REVIEW_TIER review owed, run by the seat
    Metadata-protocol: none (expected; the dev measures and declares)
    Responsibility: card thread os-dev: measure, implement, red-first, gates, os-dev-report on the card; the seat reviews, runs the contract review, and lands
    Thread-read: 6566019553
    Premise: read in the tree: subflowTargets loops over flow.nodes only; not yet measured on the wire, the dev measures first
    Serial constraints cleared: PR #28875 (same files) merged as 1335c6b8
    Resume-from: none — the branch restarts from origin/main at 1335c6b8 or later (its PR #28875 is merged)

    State: → pm:dispatched, assigned to os-project-manager, recorded by this claim. Order: pm-orders/server/order-28846.md (project folder).

  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

    Seat ruling · #28846 half (b) · 2026-11-02T03:38Z

    This answers the dev's measurement of half (b). In all six region cases, a region-nested subflow / map with a suspending child fails at the region boundary ("durable pause inside a structured region") and leaves the child run suspended, whether or not the flow has a save_notice.

    Ruled: the recommendation. #28840's gate (subflow-notice-guard.ts) stays top-level only. This PR does half (a) alone: subflowTargets reads every region depth through collectFlowGraphs. It also carries the prose should-fix. Why:

    • A decline under notice_after_suspending_subflow would name a cause that is false for this shape.
    • The defect hits every run whether or not a notice is present, so it is its own card, not a notice case.
    • It adds no second decline half to this PR.

    The order's container rule for #28840's reachability is withdrawn.

    HotCRM campaign_enrollment still arms, and §7.3 now refuses disabling or removing its children while it is enabled. That is accepted as the guards working.

    The seat files the region-boundary defect as its own card. ⛔ Not in this PR.

  7. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5omF1nCbQTGgW1nbRA
    os-dev-report
    {
    "issue": 28846,
    "status": "done",
    "branch": "claude/project-thread-9kerqb",
    "pr": "#28959",
    "head": "87411da83042879ee3c0d8a72a22a73ac3f99fe0",
    "session": "session_01FEkQ5omF1nCbQTGgW1nbRA",
    "premise_still_valid": "half (a) yes; half (b) no, ruled by the seat in 6568462471 (container rule withdrawn, region-boundary failure filed as #28897)",
    "summary": "subflowTargets (subflow-guards.ts) now reads every graph collectFlowGraphs returns (top level plus every loop body, parallel branch and try_catch slot), so every ADR-0126 7.3 guard that reads it (arming decline via disabledPackagedSubflows, re-judge via packagedSubflowCallers, switch-off/removal refusal, enable refusal, ledger-cycle exemption) sees region-nested subflow/map calls. The #28840 save-notice gate keeps reading top-level calls only, per the seat ruling: a region cannot carry a durable pause, so a region-nested call to a suspending child fails the run at the region boundary before the notice. Triage should-fix 1: the save-notice-placement.ts header and the flow.zod.ts wiring comment now name the arming gate and notice_after_suspending_subflow as where the subflow/map case is met, the run-time refusal as residue (plugin pausing types). The subflow-notice-guard.ts header prose was corrected to match.",
    "measurement": {
    "a_disabled_child": "6/6 region cases (subflow, map x loop, parallel, try_catch) armed with no cause; top-level twins declined disabled_subflow",
    "b_suspending_child_with_notice": "6/6 armed, but execute() failed at the region boundary ('durable pause inside a structured region'), no notice set, child run left suspended; top-level twins declined notice_after_suspending_subflow"
    },
    "tests": {
    "red_first": "service-automation subflow-guard-region-calls.test.ts: 14 cases; base 10 failed / 4 passed (2 controls + 2 notice-gate pins); head 14/14. Refusal cases assert code DELETE_RESTRICTED + status 409 + subflowCallers + sentence",
    "ablations": [
    "top-level only (collectFlowGraphs(flow).slice(0,1)): 10 region pins red; controls, subflow-guard-every-door and subflow-notice-arming green (34 passed)",
    "notice gate follows a region call in the caller: notice pin 1 red",
    "child counts a region call as suspending: notice pin 2 red",
    "all via ablation-replace.mjs, restored blob == HEAD"
    ],
    "suites": {
    "@objectstack/service-automation": "exit 0 (256 files, 3295 tests)",
    "@objectstack/plugin-approvals": "exit 0 (107 files)",
    "@objectstack/trigger-record-change": "exit 0",
    "@objectstack/example-showcase": "exit 0",
    "@objectstack/spec src/automation": "exit 0 (45 files, 1745 tests)"
    },
    "typecheck": "workspace pnpm typecheck (6 GB heap): every task passed except @objectstack/spec, whose test-layer tsc was killed without diagnostics (exit null) on the shared box; @objectstack/spec pnpm typecheck alone with 8 GB heap: exit 0"
    },
    "gates": {
    "derived": 83,
    "run": 83,
    "not_measured": 0,
    "unrun": 0,
    "reconcile": "dispatch-gates --ran exit 0, all 83 exit 0",
    "check_changeset_no_major_with_pr_body": "exit 0 (narrowing, none, patch)",
    "eslint": "--no-inline-config over packages/services/service-automation packages/spec/src/automation: exit 0"
    },
    "dogfood_battery": "87411da830 under os-verify-lock: closure build VERDICT command-exit 0 (338s); dogfood test VERDICT command-exit 0 (1332s): 382 passed, 3 skipped files (385); 3156 passed, 28 skipped tests (3184)",
    "shipped_flows": "examples/** + packages/qa/**: every subflow/map top-level; hotcrm origin/main d4ffe99: campaign_enrollment calls campaign_lead_member_enroll and campaign_contact_member_enroll from loop bodies (both active, no suspending node) and still arms; switching those children off or removing them while it is enabled is now refused (accepted by the seat)",
    "line_budget": "no budget in the order; diff 7 files changed, 296 insertions(+), 15 deletions(-) (subflowTargets about +4 net lines; prose about 25 lines; the rest the new test file and changeset)",
    "deviations": [
    "Half (b) of the order (the container rule for the #28840 gate) not implemented: measured premise false; seat ruled in 6568462471 to keep subflow-notice-guard.ts top-level only and filed #28897. Two pins hold the gate top-level.",
    "subflow-notice-guard.ts header prose edited (no code): it said the gate reads the calls subflowTargets answers, which is no longer true.",
    "No @objectstack/spec changeset: the spec diff is comments only and nothing in packages/spec/dist changes."
    ],
    "files_changed": [
    ".changeset/28846-region-nested-subflow-calls.md",
    "packages/services/service-automation/src/engine.ts",
    "packages/services/service-automation/src/subflow-guard-region-calls.test.ts",
    "packages/services/service-automation/src/subflow-guards.ts",
    "packages/services/service-automation/src/subflow-notice-guard.ts",
    "packages/spec/src/automation/flow.zod.ts",
    "packages/spec/src/automation/save-notice-placement.ts"
    ],
    "mcp_calls": "2: mcp__github__issue_read x2 (get_comments #28846, get_comments PR #28875 for triage review 6568019187); no write tool",
    "api_writes": "3: git push (new branch), fleet-write pr_create (run 41391266753, draft PR #28959, no labels, no assignees, read-back identical 9247 bytes), this report (post-stamped). No GitHub MCP writes.",
    "open_questions": [],
    "out_of_scope_findings": [
    "#28897 (seat-filed): a region-nested subflow/map whose child suspends fails every run at the region boundary and leaves the child run suspended."
    ]
    }

  8. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

    Landing record · #28846 · PR #28959

    domain:server seat 3 · session_01FEkQ5kE5BrPUPNMMyv6oCH · 2026-11-02T05:51Z

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-28959-*' 0 refs (released)
    Ancestry git merge-base --is-ancestor 4748bac81f origin/main yes
    Content git grep -n "collectFlowGraphs" origin/main -- packages/services/service-automation/src/subflow-guards.ts import at line 16, subflowTargets loop at line 65
    • Landed: merged as 4748bac8. The §7.3 subflow guards (arming decline, re-judge, switch-off / removal / enable refusals) now see a subflow / map nested in a loop body, a parallel branch or a try_catch slot, read through the spec's one region walk collectFlowGraphs. The save_notice arming gate stays top-level, as ruled (6568462471).
    • Contract review (CONTRACT_REVIEW_TIER): PASS 6569776675 on the landed head 87411da8. Seat ACCEPT 6569799812.
    • Fixes #28846 closed this card on merge.
    • Follow-ups: automation: a subflow or map in a region body whose child suspends arms, fails every run at the region boundary, and leaves the child run suspended #28897 unblocks now and goes to the same card thread, carrying the review's two should-fixes (an enable-door pin for a region-nested call; the subflow-guards.ts header's "moved unedited" line).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:automationdomain:serverDomain lane — seat card indexed by label:pm:seatfindingRecorded observation — held, not dispatchable until the findings triage round grades itpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions