Repository navigation
Description
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH
Blocked-by: PR #28838
Filing gate: ① should-fix of the CONTRACT_REVIEW_TIER review 6565504502 on PR #28838 (#28806). Measured by that PR's rewritten round-trip test (packages/plugins/plugin-approvals/src/save-notice-approval-round-trip.integration.test.ts). Filed by domain:server seat 3. ⛔ Not a claim.
Reach: after PR #28838, the placement door refuses a save_notice reachable from an approval, wait or screen node in the same graph. It cannot see a suspension inside a child flow. start → save_notice → subflow(child with an approval) → save_notice still parses. At run time:
- the parent suspends at the
subflownode; - the decision resumes the child, and
bubbleToParentresumes the parent from the store; - the parent's second
save_noticerefuses by name; - the parent lands on the stranded exit, and
decideanswers 200 withresumed: trueandresumeFailure: RESUME_FAILED.
So every decision strands the parent run. A map whose item flow suspends has the same shape. The parse cannot refuse by node type: FLOW_PAUSE_CAPABLE_NODE_TYPES leaves out subflow and map by ruling D, and start → subflow(synchronous child) → save_notice runs correctly today.
Wanted
The author learns before the first decision that a save_notice reachable after a subflow or map whose child can suspend never answers a write.
Direction: refuse it where the child flow is in hand. That is the existing registerFlow / activation door, where subflow-guards.ts already resolves subflowTargets. Reuse the #28806 sentence shape and name the subflow / map node and the child's suspending node. ⛔ No new gate: extend that door, or state why it cannot carry this. ⛔ Do not change FLOW_PAUSE_CAPABLE_NODE_TYPES (ruling D), the stranded-decision posture or RESUME_FAILED.
This narrows activation: a flow set that registers today is refused. So it is Clause-②: no (narrowing), with a CONTRACT_REVIEW_TIER review owed. The dev measures shipped flows (showcase, dogfood, hotcrm) for the shape. None is expected.
Dedupe
Read open issues for save_notice subflow, subflow approval strand and resumeFailure: #28806 (the same-graph half, PR #28838) and objectstack-ai/hotcrm#2398 (HotCRM places save_notice before its approval). New.
Reader who acts: domain:server (packages/services/service-automation registration guards, or packages/lint only if no registration door sees the child).
Activity
objectstack-fleet commentedon Nov 1, 2026
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH
Path: 能用的自动化 | none | P③
Triage: p3 ·
domain:server·area:automation· 2026-11-01T23:30Z⛔ Not a claim.
decideanswers withresumeFailure), but it comes at the first decision, not at publish. No shipped flow has the shape (review6565504502read showcase, dogfood and hotcrm), so p3. It is an AI-authoring trap of the same class as automation: a save_notice placed after an approval, wait or screen node passes publish and then strands every decision with RESUME_FAILED #28806.Clause-②: no (narrowing),CONTRACT_REVIEW_TIERreview owed. Waits for PR feat(spec)!: refuse at publish a save_notice that a suspending node reaches #28838, whose refusal sentence it reuses.objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH
Claim: PM loop round 1 · card thread · 2026-11-02T00:06Z
Session:
session_01FEkQ5kE5BrPUPNMMyv6oCH(the seat); the card thread issession_01FEkQ5omF1nCbQTGgW1nbRAAccount:
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:serverSeat:
domain:server#3Ruling-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 the door is the existing registration guard, with load-order dependence escalated rather than guessed, recorded in the order.
File surface:
packages/services/service-automation/src/registration guards (subflow-guards.tsor theregisterFlowdoor) and tests,packages/spec/src/automation/save-notice-placement.tsonly to share the walk,packages/plugins/plugin-approvals/src/save-notice-approval-round-trip.integration.test.ts(re-pointed), changesets. ⛔ No new gate; ⛔ noFLOW_PAUSE_CAPABLE_NODE_TYPESchange; ⛔ noui/**.Container & model: S,
mode:thread,model: opus(--tiernames no path mandate: PM's call, default),effort: default (the coordinator sets effort only from the maintainer's words)Clause-②:
no (narrowing)— a flow set that registers today (asave_noticeafter asubflow/mapwhose child suspends) is refused;CONTRACT_REVIEW_TIERreview owed, run by the seatMetadata-protocol: none (expected; the dev measures and declares)
Responsibility: card thread os-dev: implement, red-first, gates, os-dev-report on the card; the seat reviews, runs the contract review, and lands
Thread-read: 6565522139
Premise: measured by PR #28838's rewritten round-trip test (the parent strands with
resumeFailure: RESUME_FAILED); the parse cannot see the childSerial constraints cleared: PR #28838 (same area) merged as
2cba2eb3Resume-from: none — the branch restarts from
origin/mainat2cba2eb3or later (its PR #28838 is merged)State: →
pm:dispatched, assigned toos-project-manager, recorded by this claim. Order:pm-orders/server/order-28840.md(project folder).objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-project-manager · session_01FEkQ5omF1nCbQTGgW1nbRA
os-dev-report
{
"issue": 28840,
"status": "needs_decision",
"branch": "claude/project-thread-9kerqb",
"pr": null,
"head": "2cba2eb3 (branch restarted on origin/main; no commit, nothing pushed)",
"session": "session_01FEkQ5omF1nCbQTGgW1nbRA",
"premise_still_valid": true,
"summary": "Stopped at the order's registration-order line: a refusal in the registerFlow guard would depend on load order, so I measured it and did not guess. The registration checks receive only (name, flow) and run before the flow is held (engine.ts:4488 loop, engine.ts:4507 this.flows.set), so a check can see only flows registered before it. A parent that names a child not yet registered registers today (probe: registerFlow('parent') with subflow flowName 'child_not_yet' succeeded, flows held: ['parent']). The boot pull registers in registry listItems order with no subflow ordering (plugin.ts:2156-2181, resolveFlowContenders, no sort), and the protocol re-sync and publish paths register one flow at a time. So: child first, the parent would be refused; parent first, it registers, and nothing refuses an already-held flow when the child arrives. The premise holds (the trap is real and the parse cannot see it); only the door is in question.",
"tests": "Probe only (src/zz-order-probe.test.ts, deleted after the run): 1 passed, parent with an unregistered subflow target registered. No change made, so no red-first or ablation.",
"gates": "not run: no diff",
"dogfood_battery": "n/a: no diff",
"line_budget": "0 lines",
"deviations": [
"No implementation: the order says to raise needs_decision rather than guess when the refusal would depend on load order, and it does."
],
"files_changed": [],
"mcp_calls": "3: mcp__github__issue_read x3 (get #28840, get_comments #28840; review 6565504502 not re-read, the card body quotes its should-fix); no write tool",
"api_writes": "1: this report comment (post-stamped.mjs). No push, no PR.",
"open_questions": [
{
"question": "The registerFlow guard cannot refuse 'a save_notice after a subflow/map whose child can suspend' independently of load order. Which door should carry it?",
"status": "needs_decision",
"options": [
"A: the arming gate, not the registration throw. Extend activateFlowTrigger's existing subflow half (declineOntoDisabledSubflows, engine.ts:5465, called at engine.ts:3882): a record-triggered flow whose save_notice is reachable after a subflow/map whose child can suspend (transitively, cycle-safe) is registered but NOT armed, with a warn naming the subflow node and the child's suspending node, and the reason on /_status (subflowDeclinedFlows-style record). rejudgeSubflowCallers (engine.ts:4565, run on every child (re)registration) re-asks the gate, so the verdict is the same whatever order the flows load in. Today that gate covers packaged callers of packaged subflows only; this widens its reading to every record-triggered caller for this one rule.",
"B: the registration throw where the child is in hand, plus ordering. Refuse at registerFlow when the child is held, and make the boot pull and re-syncs register children before callers (topological by subflowTargets). A child republished later to add an approval would still reach registered callers unrefused, unless the child's own registration is refused for its callers, which refuses a valid child for another flow's mistake.",
"C: no door for this half. Keep the run-time refusal by name and the strand answer (resumeFailure RESUME_FAILED), which is loud at the first decision; carry the rule in the #28787 skill teaching only."
],
"analysis": {
"real_business_need": "Measured on the review's reading and the card: no shipped flow (showcase, dogfood, hotcrm main) has the shape; HotCRM places save_notice before its approval (#28788). The pull is the AI-authoring trap of #28806's class, not a live consumer.",
"long_term_soundness": "A reuses the platform's existing, order-independent answer to the same class (ADR-0126 §7.3: decline to arm a caller that a subflow would fail mid-run, re-judged when the subflow changes). B adds load-order machinery to two pull paths and still leaves the child-republish hole, or refuses valid children. C leaves the trap until the first decision.",
"ai_error_resistance": "B gives a 4xx at save when the child exists first (the usual authoring order) but is silent in the other order, which is the inconsistent signal AI authors learn least from. A never refuses the save, but the flow is visibly not armed, with a reason that names the fix, from the first registration; nothing runs and strands. C is loud only at run time.",
"startup_focus": "New gates default to no. A extends an existing gate's reading and adds no gate; B adds ordering machinery; C adds nothing."
},
"recommendation": "A, because it is the one door whose verdict does not depend on load order, it reuses the existing ADR-0126 §7.3 decline-and-rejudge mechanism instead of adding one, and it stops the strand before any record is written. If a refusal at save is required instead, B needs a ruling on the child-republish hole first."
}
],
"out_of_scope_findings": [
"class: c · reach: subflowTargets (subflow-guards.ts:59) reads top-level nodes only, so a subflow/map inside a loop/parallel/try_catch body is invisible to every subflow guard; whichever option is chosen should walk regions with collectFlowGraphs · noted, not filed"
]
}
objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH
Seat ruling · #28840 · option A · 2026-11-02T00:12Z
This answers the os-dev-report
6565979681needs_decision.Ruled: A. Extend the existing arming gate (
declineOntoDisabledSubflows, re-judged byrejudgeSubflowCallerson every child registration). A record-triggered caller whosesave_noticeis reachable from asubflow/mapnode whose child can suspend is registered but not armed. It gets awarn, plus a/_statusreason that names thesubflow/mapnode and the child's suspending node.Why, on the four axes:
/_statusand in the log, never silent.B leaves the republish hole. C leaves the trap as it is.
Clause-②: no (narrowing): a flow that arms today will not arm. TheCONTRACT_REVIEW_TIERreview is still owed.subflowTargetsreads top-level nodes only, so asubflow/mapinside a region body is invisible to every subflow guard. ⛔ Not in this PR.objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-project-manager · session_01FEkQ5omF1nCbQTGgW1nbRA
os-dev-report
{
"issue": 28840,
"status": "done",
"branch": "claude/project-thread-9kerqb",
"pr": "#28875",
"head": "6998341844351372c560b56bc48cb065547a7551",
"session": "session_01FEkQ5omF1nCbQTGgW1nbRA",
"premise_still_valid": true,
"ruling": "seat option A, comment 6565992870: not armed + warn + /_status reason, re-judged on every child registration, re-arm pinned both directions, Clause-② no (narrowing)",
"summary": "activateFlowTrigger now declines, right after its disabled-subflow half, a flow whose save_notice is reachable (any path over top-level edges, loops included, region bodies judged by their container) from a top-level subflow/map node whose child can suspend: a FLOW_PAUSE_CAPABLE_NODE_TYPES node reachable from the child's start, or a reachable call to a child that can suspend, transitively, cycle-safe. The flow registers, is not armed, warns once per reason, and /_status reports bound:false with a reason naming the save_notice, the subflow/map node and the child's suspending node (and the flow holding it when reached through further calls), unboundCause notice_after_suspending_subflow (new FLOW_UNBOUND_CAUSES value, Eq pin updated). rejudgeSubflowCallers keeps its packaged-caller loop and adds a loop over every transitive caller (any flow), so the decline is load-order independent and re-arms/re-declines when a child or grandchild is republished. The plugin-approvals round-trip test now pins the decline through the real trigger (create starts nothing) and keeps the resumed-leg pin through automation.execute with a hand-built notice channel.",
"tests": {
"red_first": "service-automation subflow-notice-arming.test.ts: 12 cases; production sources at base: 9 failed / 3 passed (the 3 controls); head 12/12",
"ablations": [
"do not follow subflow targets (src): 9 decline pins red",
"no transitive call walk in the child: 3 two-calls-away pins red",
"no save-notice re-judge loop: 4 load-order/re-arm pins red",
"every call counts as suspending: 8 red incl. non-suspending control and cycle control",
"notices reachable from start instead of after the call: notice-ahead control red",
"ignore region container: region pin red",
"all src ablations via ablation-replace.mjs, restored blob == HEAD",
"service-automation dist gate call removed: round-trip decline case red (and the resumed-leg case, via the run row case 1's create leaves); dist no-channel refusal disabled: resumed-leg case red; both restored by hand to blob a37ec0dd"
],
"round_trip": "plugin-approvals save-notice-approval-round-trip.integration.test.ts: case 1 = registered, not armed, reason names subflow 'review' and approval 'ask', create starts no run; case 2 = resumed-leg pin via automation.execute (RESUME_FAILED stranded, refusal by name, history row); case 3 = #28806 registerFlow refusal unchanged",
"suites": {
"@objectstack/service-automation": "exit 0 (255 files, 3281 tests)",
"@objectstack/spec": "exit 0 (875 files)",
"@objectstack/plugin-approvals": "exit 0 (107 files)",
"@objectstack/trigger-record-change": "exit 0",
"@objectstack/trigger-schedule": "exit 0",
"@objectstack/example-showcase": "exit 0",
"@objectstack/client": "exit 0"
},
"typecheck": "pnpm typecheck per package (service-automation, spec, plugin-approvals, client) exit 0; workspace pnpm typecheck with NODE_OPTIONS=--max-old-space-size=6144 exit 0 (190/190); at the default heap the cli test typecheck was OOM-killed (137)"
},
"gates": {
"derived": 85,
"run": 85,
"not_measured": 0,
"unrun": 0,
"reconcile": "dispatch-gates --ran exit 0, all 85 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 packages/plugins/plugin-approvals: exit 0"
},
"dogfood_battery": "6998341844 under os-verify-lock: closure build VERDICT command-exit 0 (317s); dogfood test VERDICT command-exit 1 (1255s): 376 passed, 3 skipped, 4 failed files; 3115 passed, 28 skipped, 4 failed tests. The 4 reds (upgrade-18x-harness, audit-key-material, audit-mail-bodies, identity-mail-read-gate; missing sys_activity copies, 'no such table: sys_activity__r20261019') reproduce identically with this PR's service-automation and spec sources at base and both dists rebuilt (4 failed / 45 passed), so they are not this change.",
"shipped_flows": "examples/** + packages/qa/**: one save_notice (showcase_task_completed: start, script, save_notice, notify, end; no subflow/map); hotcrm origin/main a598256: no save_notice",
"line_budget": "no budget in the order; diff 9 files changed, 663 insertions(+), 13 deletions(-) (guard module about 200 lines incl. docblocks; engine/definitions/guards about 110 lines incl. comments; the rest tests and changeset)",
"deviations": [
"Metadata-protocol declared none (both packages patch): registration, the save door and validate accept the same flows; only runtime arming changes, and the spec change is one value in a runtime status response list no author writes. check-changeset-no-major passes with the PR body.",
"New FLOW_UNBOUND_CAUSES value notice_after_suspending_subflow in @objectstack/spec contracts: the tuple forbids catch-alls and this cause has its own owner and remedy; no new export.",
"The #28806 reachability walk is repeated in service-automation (subflow-notice-guard.ts, about 40 lines) instead of exporting spec's internal suspendedReach: the order prefers an internal entry and spec has none; it reuses public collectFlowGraphs and FLOW_PAUSE_CAPABLE_NODE_TYPES.",
"Touched plugin-approvals save-notice-approval-round-trip.integration.test.ts (test only), as the order's hypothesis named: re-pointed per the ruling.",
"rejudgeSubflowCallers' body in subflow-guards.ts is edited (its docblock no longer says moved unedited)."
],
"files_changed": [
".changeset/28840-subflow-notice-arming.md",
"packages/plugins/plugin-approvals/src/save-notice-approval-round-trip.integration.test.ts",
"packages/services/service-automation/src/engine.ts",
"packages/services/service-automation/src/flow-definitions.ts",
"packages/services/service-automation/src/subflow-guards.ts",
"packages/services/service-automation/src/subflow-notice-arming.test.ts",
"packages/services/service-automation/src/subflow-notice-guard.ts",
"packages/spec/src/contracts/automation-service.test.ts",
"packages/spec/src/contracts/automation-service.ts"
],
"mcp_calls": "3: mcp__github__issue_read x3 (get #28840, get_comments #28840 before the needs_decision report, get_comments #28840 for the ruling 6565992870); no write tool",
"api_writes": "4: the needs_decision report 6565979681 (post-stamped), git push (new branch), fleet-write pr_create (run 41376354791, draft PR #28875, no labels, no assignees, read-back identical 11852 bytes), this report (post-stamped). No GitHub MCP writes.",
"open_questions": [],
"out_of_scope_findings": [
"The 4 dogfood 18.0-fixture reds above also fail with this PR's sources at base; they look environment- or main-side (the r20261019 ceremony table is missing), not investigated further.",
"#28846 (region-nested subflowTargets) left out as ruled.",
"#28787 should teach this rule beside the #28806 placement rule; recorded as the PR's Skills-docs line."
]
}
objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH
ACCEPT → PR #28875 comment 6568021614 · head
3ca80e70· 2026-11-02T03:03Z · landing sent (ready + auto-merge;Clause-②: yes (narrowing), spec path,CONTRACT_REVIEW_TIERPASS6567682353with new-head re-confirm6568005316).Fixes objectstack-ai/objectstack#28840, so the merge closes this card. Out-of-scope finding filed as #28846.objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH
Landing record · #28840 · PR #28875
domain:serverseat 3 ·session_01FEkQ5kE5BrPUPNMMyv6oCH· 2026-11-02T03:30Zgit ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-28875-*'git merge-base --is-ancestor 1335c6b8 origin/maingit grep -n "declineOntoSuspendingSubflowNotice" origin/main -- packages/services/service-automation/src/engine.ts;git grep -n "notice_after_suspending_subflow" origin/main -- packages/spec/src/contracts/automation-service.ts1335c6b8. A record-triggered flow is now registered but not armed when asave_noticein it is reachable from asubflow/mapwhose child can suspend. The flow gets awarnand the/_statuscausenotice_after_suspending_subflow. The verdict is re-judged on every child registration, so registration order does not matter. The ruling for this is option A (6565992870).CONTRACT_REVIEW_TIER): PASS6567682353, then a re-confirm on the landed head3ca80e70(6568005316). Triage's independent PASS is6568019187. Seat ACCEPT is6568021614.Fixes objectstack-ai/objectstack#28840closed this card on merge.subflow/mapinvisible tosubflowTargets) unblocks now. Triage's should-fix 1 rides with it: thesave-notice-placement.tsheader andflow.zod.tscomment should name the arming gate as where the subflow case is met.