Skip to content

service-automation: a durable suspension inside a structured region (loop / parallel branch / try_catch) must fail the run with a named error, not leave progress state and report success — runtime half of #15646 ruling D #18881

Description

@hotlong

Filed by the director seat (objectstack #12708, summon #24, session_01Wj1HUjzyeiBQ8atRf1ZhaL) executing decision batch #153 item 1 — maintainer 「其他同意」 to D on #15646. This is the runtime half; the parse-time half (refuse the unconditionally pausing node types and end inside region bodies) stays on #15646 in packages/spec.

The defect this half closes

#15646's reproduction: loop { try_catch { map(pausing sub-flow) } } — the map's child suspends durably, the region's progress state is left behind, later iterations skip, and the run reports success with summary.failed = 0 and ran = []. Whether a map / subflow child pauses is known only at runtime (it lives in the sub-flow record config.flowName names), so parse-time refusal cannot reach this case without also refusing loop { map(synchronous sub-flow) }, a shape that runs correctly today (#15616's regression suite, 5 tests in 3 packages/services files).

Ruling frame — ⛔ not re-adjudicable by the dev

  1. When a node contained in a structured region body (loop, a parallel branch, try_catch's try or catch, at any depth) durably suspends, the engine fails the run with a named, structured error that carries the region node id, the suspending node id and the sub-flow name — the loud form of [P2] engine ADR: durable pause inside structured regions (unlock topology-level parallel approvals / waits / subflows) #3267's ruling 禁 (structured regions do not carry a durable pause). ⛔ Never success with nothing run; ⛔ never a warning-level log.
  2. Nothing changes for a region whose nodes complete synchronously: loop { map(synchronous) } and service-automation: a map node inside a loop body runs its collection ONCE — iterations 2..n do nothing, report success, and the run completes green #15616's suite keep passing unchanged. ⛔ No parse-time rule is added here (that is service-automation: a PAUSING map inside a contained region leaves its progress state behind — later loop iterations skip items and the exhausted map returns success having run nothing #15646's half).
  3. The error is surfaced where a run's terminal failure already is (run status + summary.failed), so an operator and a client see it the way they see any failed run.

Acceptance

Refs

#15646 (ruling D, batch #153 item 1) · #3267 (禁) · #15616 · #15788 · #16134 (region walk precedent)

Dedup terms: region durable pause runtime refusal, map inside loop suspends, progress state left behind, summary.failed silent success


Generated by Claude Code

Activity

  1. self-assigned this
    on Sep 18, 2026
  2. huangyiirene commented on Sep 18, 2026

    @huangyiirene
    Collaborator

    Claim: PM loop round 1
    Session: session_019hBqDVrwbijUCoK9qsss2E
    Branch: claude/issue-18881-region-durable-suspension-refusal
    Worktree: objectstack-issue-18881
    Domain: domain:services
    Seat: domain:services#1
    File surface: packages/services/service-automation/src/ + .changeset/ (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default judgment tier (opus) — this act's dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/services/service-automation/src/engine.ts returned verbatim 「no path-derived mandate: the surface hits none of the 3 declared glob(s) … floor sonnet · default opus · ceiling fable」 ⇒ ⛔ no path mandate; taken to default on card content: this is a ruling implementation with a live design judgment (where the named error is raised in the region walk), ⛔ not a mechanical edit.
    Clause-②: no
    Thread-read: none (the card carries 0 comments at this moment)
    Serial constraints cleared: board-wide pm:dispatched = 40 at this fire; domain:services = 1 — #16506 (assignee zhuangjianguo, ⛔ not this seat's), declared surface src/email-service.queue-delivery.test.ts ⇒ disjoint. This surface is wholly inside packages/services/service-automation/ (one lane, one package) ⇒ the cross-domain arm does not apply and no other lane's in-flight card declares it. ⭐ Sibling #18714 (p1, pm:queue) shares engine.ts and is deliberately held serial behind this card — see the fold-or-serial ruling posted on it this fire. ⛔ No sibling card pins behaviour this one asserts.


    Premise checks run by this seat before dispatch — ⛔ all three surfaces, ⛔ none inherited from the card

    surface reading (this act, origin/main)
    action git log --oneline -8 origin/main -- packages/services/service-automation/src/ — top entry 78436637, and ⭐ the action this card names is NOT already done (see the work-item row).
    referenced cards — read for their CURRENT state, ⛔ not as the card describes them #15646 open · #18112 closed/completed · #15616 closed/completed · #15788 closed/completed · #3267 open · PR #18688 closed (merged)
    work item the defect shape is still live: no commit in the window closes the region/durable-suspension arm.

    🔴 One reading changes this card's closing line, and it is the reason this block exists

    ⭐ The spec half has LANDED. 78436637 feat(spec)!: a structured region body refuses a pause-capable node and an 'end' node (#18688) is on origin/main now. The card body says, verbatim:

    The PR closes #15646 with Fixes #15646 once the spec half (Part of #15646) is on main; if this half lands first, use Part of and let the spec half's landing close it.

    ⇒ the condition is MET, so the PR carries Fixes #18881 and Fixes #15646, ⛔ not Part of. #15646 is measured still open, so it is this PR's to close.

    ⚠️ This is a PM mechanism assumption, ⛔ not a ruling: verify #18688 really landed the authoring-time half before relying on it, and if the region walk it added already refuses your reproduction at parse time, say so and stop rather than building a runtime arm that cannot be reached.

    Written by this seat at 2026-09-18T21:42Z. Seat post: #6021.


    Generated by Claude Code

  3. huangyiirene commented on Sep 18, 2026

    @huangyiirene
    Collaborator

    os-dev-report

    {
      "issue": 18881,
      "status": "done",
      "branch": "claude/issue-18881-region-durable-suspension-refusal",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/19140",
      "session": "session_019hBqDVrwbijUCoK9qsss2E",
      "premise_still_valid": true,
      "summary": "The card premise held and the defect reproduced exactly as described: loop { try_catch { map(pausing child) } } ran green with result.success true, run completed and summary.failed 0 over 0 of 10 child runs, and even the loop-direct case that DID fail named neither the region nor the sub-flow. runRegion now raises FlowRegionSuspensionRefusalError (new internal module, not exported from src/index.ts) carrying regionNodeId, regionKind, suspendedNodeId and subFlowName as fields as well as message text; try_catch re-throws it from both its try-attempt and catch-region arms and spends no retry attempt on it, parallel re-throws it instead of folding it into a fault-routable returned failure, loop already re-threw. One refusal is one failure: the region node frame records the EXECUTION_ERROR step and publishes $error, enclosing frames record nothing, so summary.failed counts the fault and not the nesting depth. Assignee was huangyiirene (set by the PM) on arrival and was not touched. ⚠️ The PM mechanism assumption that the PR should carry a closing keyword on #15646 is FALSIFIED in its conclusion, not in its premise — see deviations; the body carries Part of #15646 and #15646 remains open.",
      "tests": "ALL FIGURES AT bad6404b0, the final commit. (1) New suite packages/services/service-automation/src/region-durable-suspension-refusal.test.ts — 8 tests, every refusal case paired with a synchronous control; vitest run --maxWorkers=2 on that file: 8 passed (8). Before the fix, same file on origin/main c70581bc with only the test added: 6 failed | 2 passed (8), the card reproduction failing on expect(result.success).toBe(false) received true and expect(summary.failed).toBe(1) received +0. (2) Whole package pnpm --filter @objectstack/service-automation test: 139 files / 1660 tests passed. PR #18688 measured this package at 138 files / 1652 tests on the tree this branch is cut from; 1652 + 8 = 1660, so nothing was lost, re-homed, skipped or quarantined — #15616 five tests, #15788 runtime end test and #16314 rollup suite are untouched in the diff and green. (3) typecheck: pnpm --filter @objectstack/service-automation typecheck exit 0, check:test-typecheck OK, 0 files / 0 errors / 0 pinned signatures. (4) ABLATION, two legs, each mutated through scripts/ablation-replace.mjs (anchor must hit, blob hash must change) and restored against HEAD with git diff HEAD empty; the package tests import their subject through relative specifiers so the mutation is live from source with no dist on the path, which each leg demonstrates by going red. Leg A, the try_catch re-throw arm, anchor if (isRegionSuspensionRefusal(err)) { x1 to x0, blob c4ed5b307865 to c3b44a8af269: 3 failed | 5 passed — exactly the three try_catch containment cases, with both controls and the loop/parallel cases staying green, which also shows that arm is what closes the contained case. Leg B, the one-refusal-one-failure rule, anchor isRegionSuspensionRefusal(execErr) && execErr.regionNodeId !== node.id; x1 to x0: 1 failed | 7 passed, AssertionError expected 2 to be 1 — 2 being exactly the nesting-depth reading the suppression prevents. Both legs restored, blob == HEAD, git status porcelain empty. (5) LINT, a declared and measured narrowing: pnpm lint is eslint . --no-inline-config, a whole-repo scan whose broadest block is files **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} minus NEVER_LINTED, read from eslint.config.mjs itself; run over this diff five lintable paths and counted from the linter own --format json output: 5 files, 0 errors, 0 warnings. The narrowing excludes nothing as a property of the config, not an assumption: eslint.config.mjs never enables type-aware linting for ANY file (no parserOptions.project, no typed typescript-eslint rules), stated verbatim at eslint.config.mjs:326 with its own positive control, so every rule is per-file and syntactic.",
      "gates": "59 derived / 59 run / 0 NOT-MEASURED / 0 UNRUN, all exit 0. Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, NO paths passed; reconciled with --ran over a record of the printed command plus its exit code captured before any pipe: exit 0, verdict line ✓ 59 derived famil(ies) accounted for — 59 run, 0 NOT-MEASURED (a DERIVED zero). Three needed a prerequisite before they measured anything and none of the three is a finding: check:dual-build-cjs-loads and check:type-check-debt both answered PREREQUISITE NOT MET (exit 3) because they read built output the whole tree must supply — re-run after pnpm exec turbo run build --filter=./packages/* --filter=./packages/*/* (72/72 tasks) both exit 0; check-plugin-teardown-shape --self-test answered exit 3 on a fixture pinned to a commit this shallow checkout could not reach — after git fetch --unshallow it passes its 48 cases. The seat lead list was a subset: all seven of check-changeset-no-major, check-ci-filter-parity, check-closing-keyword-parity, check-comment-mask-adoption, check-comment-mask-corpus, check-keyed-text-bounds and check-platform-object-tenancy-census are in the derived 59 and green. Two PR-scoped gates dry-run against the candidate body before the PR was opened: check:partof-closing-keyword exit 0 and check:closing-target-claim exit 0. ⚠️ dispatch-gates reports its derivation taken from a tree behind origin/main with scripts/check-release-spec-changes.mjs and scripts/ts-parse.mjs changed across that range; neither declares a packages/services population (check:spec-changes is spec-release-scoped, ts-parse.mjs is a parser library declaring no family) so the derived list is unchanged at 59, and CI derives it again on the merge base.",
      "line_budget": "n/a — no skills/** path in the diff. The published skill packages are untouched, so no per-file or per-package line ratchet applies and none was paid.",
      "files_changed": [
        ".changeset/18881-region-durable-suspension-refusal.md (new, patch on @objectstack/service-automation)",
        "packages/services/service-automation/src/region-suspension-refusal.ts (new, internal, NOT exported from src/index.ts)",
        "packages/services/service-automation/src/region-durable-suspension-refusal.test.ts (new, 8 tests)",
        "packages/services/service-automation/src/engine.ts (runRegion conversion + the one-refusal-one-failure rule in executeNode catch arm)",
        "packages/services/service-automation/src/builtin/try-catch-node.ts (re-throw from the try-attempt and catch-region arms)",
        "packages/services/service-automation/src/builtin/parallel-node.ts (re-throw instead of a returned branch failure)"
      ],
      "deviations": [
        "PM mechanism assumption 1, CONCLUSION FALSIFIED — the PR carries Part of #15646, NOT the closing keyword the dispatch expected. The premise verified in both halves: 78436637 is an ancestor of origin/main (exit 0) and of this branch base, PR #18688 is merged, and the parse-time refusal does NOT reach the card reproduction (map and subflow are deliberately not judged by type there), so the runtime arm is live and every fixture registers and runs. What blocks the conclusion is a shipped gate: check:closing-target-claim refuses a PR closing a card whose thread carries no Claim: naming this PR head branch. #15646 carries three claims, all naming claude/issue-15646-region-pause-end-refusal, and its newest protocol event is a Release: (comment 5730095126) — so it holds no live claim and none names this branch, and a dispatched executor may not post a claim of its own to clear that. MEASURED, not argued: the same gate run against a body carrying closing keywords on both #18881 and #15646 exits 1 with the governing Claim: on #15646 names claude/issue-15646-region-pause-end-refusal, not claude/issue-18881-..., and its own remedy 3 prescribes Part of. Run against the body actually shipped it exits 0. ⇒ #15646 remains open after this merges; retiring it is the PM act, either deliberately or by re-dispatching with a claim on this branch.",
        "NO new error.code was minted. The ruling asks for a named structured error; the closed ERROR_CODE_LEDGER that governs error.code (ADR-0112) lives in packages/spec, outside this card declared file surface. The refusal is named by its TYPE and by four structured fields, and the step it produces keeps the EXECUTION_ERROR code every thrown node failure has always carried. Flagged so a reviewer can rule otherwise cheaply.",
        "DECLARED CONSEQUENCE of the one-refusal-one-failure rule: an enclosing container the refusal travels out through now records no step of its own, so in loop { try_catch { map } } the loop node is absent from summary.nodes while its body steps still land in the log (the readPartialSteps fold is untouched). Exactly one node carries failures, the region node, pinned by the suite.",
        "ZERO label writes. The dispatch Writes section narrows the budget to the report comment and the draft PR and forbids any other write endpoint, so the default labelling step was not taken and no label read-back was performed. skip-changeset does not apply — this package publishes and a patch changeset is included.",
        "Gate derivation taken at a tree 6 commits behind origin/main; two derivation inputs changed across that range, neither declaring a packages/services population. Argued above under gates; CI re-derives on the merge base."
      ],
      "mcp_calls": "0 — no MCP GitHub tool was called; every GitHub read and write went through the REST proxy with curl.",
      "api_writes": "2 — POST /repos/objectstack-ai/objectstack/pulls (the draft PR, HTTP 201, #19140) and POST /repos/objectstack-ai/objectstack/issues/18881/comments (this report). No other write endpoint was called.",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: runRegion #15788 isRefusalSignal arm still converts a region-contained REFUSAL into a plain Error, so an enclosing try_catch can still contain that one exactly as it contained the pause before this card. Not reachable today — #18688 refuses an end inside a region body at parse, and the other producers are a subflow/map whose CHILD run refused, which is #18112 option B and explicitly not implemented. Deliberately left alone: touching it would move accept/reject behaviour outside this card ruling. 承接者: #18714, queued on this same engine.ts region surface.",
        "noted, not filed: parallel converts a branch throw into a RETURNED failure while loop re-throws, so an ordinary region failure is fault-routable on a parallel node and not on a loop. Pre-existing asymmetry, no reproduction taken and no declared contract quoted, so it is an observation and not a class (a) or (b) finding. 承接者: #18714, same file and same region surface."
      ]
    }

    Generated by Claude Code

  4. huangyiirene commented on Sep 18, 2026

    @huangyiirene
    Collaborator

    ACCEPT — PR #19140, reviewed against GitHub and the tree, ⛔ not against the report's narrative

    domain:services seat (#6021) · session_019hBqDVrwbijUCoK9qsss2E · R1 · written 2026-09-18T23:10Z. Reviewed head bad6404b08. ⛔ Landing is gated on CI convergence — see the last section.

    🔴 First: a PM mechanism assumption this seat wrote into the dispatch was FALSIFIED, and the correction belongs here in public

    The dispatch order told the dev, as a PM mechanism assumption (⛔ explicitly not a ruling, and explicitly marked 「verify this yourself」), that the PR should carry Fixes #15646. The dev verified the premise, found it true in both halves, and then measured that the conclusion does not follow — and it was right.

    ⭐ The premise held: 78436637 (PR #18688, the spec half) is an ancestor of origin/main; the parse-time refusal does not reach this card's reproduction, because map and subflow are deliberately not judged by type there ⇒ the runtime arm is genuinely live and reachable. ⛔ The runtime arm is not dead code, which is the outcome the dispatch told the dev to stop and report if it found.

    🔴 The conclusion was blocked by a shipped gate this seat did not know about. check:closing-target-claim refuses a PR closing a card whose thread carries no live Claim: naming that PR's head branch — and #15646 held three claims, all naming claude/issue-15646-region-pause-end-refusal, with a Release: as its newest protocol event.

    ⇒ This seat's assumption was wrong, it was wrong in a way the dev caught, and the dev shipping Part of was the correct call at its authority level — a dispatched executor may ⛔ not post a claim on another card to clear a gate.

    How it was resolved — the ruling executed literally, ⛔ not re-adjudicated

    Ruling D's execution clause is not re-adjudicable and names the mechanism verbatim: 「the runtime card's PR closes this card with Fixes #15646 once both are on main.」 The gate's own remedy 1 is a PM act, so this seat performed it:

    1. Re-measured the gate myself rather than taking the dev's word — it is a reading this seat then acted on. ⚠️ The first run used wrong flags and answered NOT MEASURED — …wiring, usage or transport failure, NOT a verdict; ⛔ that refusal was not read as a pass. Re-run the way the gate prescribes: exit 1, refusing as reported.
    2. Claimed service-automation: a PAUSING map inside a contained region leaves its progress state behind — later loop iterations skip items and the exhausted map returns success having run nothing #15646 on this branch (issuecomment-5737225992) + assigned this seat. Legitimate because the domain:spec seat had already released the runtime half to this lane in writing 「由 services 车道重新认领」, and because the claim is true — that card's remaining half really is in flight here. ⚠️ The gate exists to stop a second seat duplicating work on an unclaimed card; a truthful claim serves that purpose rather than evading it.
    3. Re-ran the gate: exit 0 — 「PR fix(service-automation): a durable suspension inside a structured region fails the run with a named refusal #19140 closes service-automation: a durable suspension inside a structured region (loop / parallel branch / try_catch) must fail the run with a named error, not leave progress state and report success — runtime half of #15646 ruling D #18881, service-automation: a PAUSING map inside a contained region leaves its progress state behind — later loop iterations skip items and the exhausted map returns success having run nothing #15646, and each carries a Claim: whose Branch: line names claude/issue-18881-region-durable-suspension-refusal」.
    4. Edited the PR body to Fixes #15646, and rewrote its explanatory section to record the round trip rather than delete it. Both PR-scoped gates were run against the candidate body before the edit — closing-target-claim 0 and partof-closing-keyword 0 — because the new section quotes Part of #15646 in prose and a parser could have read that as a contradiction. Read back after the PATCH: line 0 Fixes #18881, line 2 Fixes #15646, still draft, base main, head unchanged, exactly one _Generated by footer (confirming this board's standing reading that a body PATCH re-appends one ⇒ send zero).

    ⛔ #15646 keeps domain:spec — routing is the triage seat's sole production and an execution seat ⛔ never rewrites it, whatever a release comment says. Flagged for triage, ⛔ not acted on.

    Checklist — every line a reading this seat took

    item verdict
    PR shape draft ✓ · base main ✓ · first line Fixes #18881 ✓
    path surface (/pulls/19140/files, ⛔ not the report) 6 files, all under packages/services/service-automation/src/ + .changeset/ — declared surface honoured exactly
    governed surface NONE · ⛔ zero packages/spec — this lane's red line intact
    clause-② path leg NONE (packages/spec/src/** untouched)
    clause-② declaration leg no — ⭐ independently verified, ⛔ not accepted on the declaration: src/index.ts is not in the diff at all, and region-suspension-refusal resolves 0 times in the package entry on the branch. Control on the same instrument: that entry carries 26 export lines ⇒ the zero is a reading, not a dead grep. The new refusal type is genuinely internal.
    changeset present, patch on @objectstack/service-automation, substantive. The card's own acceptance says 「patch or minor per its rules」 ⇒ within what the card permits; ⛔ no finding manufactured.
    mcp_calls 0 ✓ · api_writes 2 (draft PR + report comment), inside the dispatch budget ✓
    report location os-dev-report on the issue ✓

    ⚠️ One instrument returned NOT MEASURED and is reported as such, ⛔ not as clean: check-widening-tells --declaration no exits 0 but prints 「NOT MEASURED is not a clean reading — no tell could have fired on these files whatever they contain: no declared surface covers it (6)」. ⇒ it judged nothing here, and the clause-② evidence above is the entry-point reading, ⛔ not this tool's silence.

    Deviations — reviewed, all four sustained

    • No new error.code minted. Correct: the closed ERROR_CODE_LEDGER (ADR-0112) lives in packages/spec, outside the declared surface and behind this lane's red line. Minting one there would have been a Clause-②: yes act on another lane's card. The refusal is named by type + four structured fields. ⭐ A reviewer can still rule otherwise cheaply, which is why it was flagged rather than silently decided.
    • One-refusal-one-failure suppression means an enclosing container records no step of its own, so loop is absent from summary.nodes while its body steps still land. Declared, pinned, and the right trade: counting nesting depth as failures is the bug, not the fix.
    • Zero label writes. Correct — the dispatch's Writes: budget named exactly two endpoints, and the dev honoured it rather than taking the habitual labelling step. ⭐ Exactly the behaviour a write budget is for.
    • Gate derivation taken 6 commits behind origin/main, two derivation inputs changed in that range, neither declaring a packages/services population ⇒ derived list unchanged at 59, and CI re-derives on the merge base. Sustained.

    Evidence quality — the two ablation legs are what make this an ACCEPT rather than a hopeful one

    Both negative pins were dissolved through scripts/ablation-replace.mjs, so the anchor hit and the on-disk blob change were proven before any result was read, and the restore proven by blob == HEAD + empty git diff HEAD — ⛔ never by an exit code. Leg A (the try_catch re-throw arm) reddened exactly the three containment cases with both controls staying green; leg B (the one-refusal-one-failure rule) reddened one with expected 2 to be 1, 2 being precisely the nesting-depth reading the rule suppresses. ⭐ Two legs reddening disjointly is what makes them two measurements rather than one restated twice. Package total 1652 + 8 = 1660 ⇒ nothing lost, re-homed, skipped or quarantined.

    🛑 Landing — NOT done, and what it waits on

    CI on bad6404b08 at this moment: 32 success · 5 skipped · 3 in progress (Check Changeset, Type Check · workspace, Lint & Repo Gates — re-triggered by this seat's body edit), 0 red. ⛔ Enqueue requires every check green or expected-skip, ⛔ not a required subset. ⇒ this seat flips ready and enqueues on the next fire once they land, and ⛔ not before.

    ⭐ When this merges it closes both #18881 and #15646, which is ruling D's stated end state.


    Generated by Claude Code

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

    @huangyiirene
    Collaborator

    ✅ Landed — PR #19140 MERGED, verified on origin/main with controls, and the half-state it left behind is cleared

    domain:services seat (#6021) · session_019hBqDVrwbijUCoK9qsss2E · R1 · written 2026-09-19T00:10Z. Landing criterion for this repo is MERGED (a merge here does ⛔ not deploy or publish).

    Delivery on origin/main — ⛔ judged on the merged ref, not on the PR-closed event

    probe value
    (#19140) in the commit log 1
    positive control (#19142) 1
    negative control (#99999) 0
    region-suspension-refusal.ts present in the package 1
    FlowRegionSuspensionRefusalError 2 sites — engine.ts and the new module
    negative control on a bogus symbol 0
    ⭐ still NOT exported from the package entry 0 — the Clause-②: no declaration holds after landing, ⛔ not merely as declared

    ⇒ the content is on main, not just the commit subject.

    Both cards closed by this one PR — which is ruling D's stated end state

    #18881 and #15646 both closed completed at 00:09:28Z. ⭐ That is the point of the round trip recorded on #18881: ruling D says 「the runtime card's PR closes this card with Fixes #15646 once both are on main」, a shipped gate refused the literal mechanism, and the gate's own remedy 1 — a truthful Claim: on #15646 naming this branch — cleared it. CI confirmed it independently before the merge: 「The card this PR closes must claim this branch」 = success.

    🔴 Half-state cleared — GitHub closes a card, it does ⛔ not clean the board

    Auto-closing via a closing keyword leaves the pm:* state label and the assignee exactly where they were. The contract is 「关闭即在同一笔摘掉 pm:* 状态标」, so both were cleaned on reading this event rather than left for the patrol:

    card before after
    #18881 pm:dispatched + assignee huangyiirene ⇒ bug, priority:p1, domain:services · 0 assignees
    #15646 pm:blocked + assignee huangyiirene ⇒ bug, priority:p1, domain:spec · 0 assignees

    ⭐ domain:* and the type labels stay — 归属不是状态. ⛔ And domain:spec on #15646 was ⛔ not rewritten to this lane despite the spec seat's release routing its runtime half here: domain:* is the triage seat's sole production, and an execution seat does not rewrite it even when it looks wrong.

    ⚠️ The assignee was cleared with this note rather than silently — a dropped assignee with no record is the H47 shape. ⛔ No Release: line is written: release semantics are 「card leaves your hands, next seat re-claims」, and a card closed completed is going to nobody.

    What landed, in one line

    A node that durably suspends inside a structured region body (loop, a parallel branch, try_catch, at any depth) now fails the run with a named structured error carrying the region node, the suspending node and the sub-flow — instead of leaving progress state behind and reporting success over a sweep that processed nothing.


    Generated by Claude Code

  7. added 2 commits that reference this issue on Sep 28, 2026
    554e928
    97466dd
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