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. 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.

  2. 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).

  3. 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.

  4. 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."
    ]
    }

  5. 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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions