You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
service-automation: restoreConsumedSuspension cannot reach a nested run — a stranded child's cascade-failed ancestors are consumed without a snapshot, so restoring the child continues into a dead parent #15222
Found while delivering the services half of #13937 (branch claude/issue-13937-stranded-run-operator-verb). ⛔ Not fixed there: it is a reach gap of the existing operator exit for nested runs, and the shape of the repair (journal the ancestors too, or restore the chain as one unit) is a design decision, not a line. Filed under #13909 because it is deliverable 2's ("an operator path out of a terminal run") nested-run gap.
resumeInternal's catch arm journals the consumed suspension of THE RUN THAT THREW (journalConsumedSuspension) — the input restoreConsumedSuspension puts back. For a nested run, the ancestors are then handled on both paths without any journal:
Up-bubble path (skipBubble === false): failAncestors walks $parentRunId and calls failSuspendedRun on each suspended ancestor.
Delegation path (parent resumed first, child resumed with skipBubble === true): the parent frame sees !childRes.success with no retryable code and calls failSuspendedRun(run, ...) on itself.
failSuspendedRun is forgetSuspendedRun(run, 'failed') plus a failed log record — it journals NOTHING. So after a child strands:
the child is stranded and restorable (status: 'stranded' on its result; restoreConsumedSuspension(CHILD_RUN_ID) answers restored: true);
every ancestor is recorded failed with its pause consumed and no snapshot: restoreConsumedSuspension(PARENT_RUN_ID) answers NO_CONSUMED_SUSPENSION, and the parent's own resume result (delegation path) carries no status at all — it is neither stranded nor repairable, and must not be stamped stranded (nothing can re-arm it);
restoring and resuming the child completes the child, whose bubbleToParent finds no parent suspension and logs a warning: the parent flow never continues.
⇒ For a nested run the operator exit re-arms the leaf and loses the tree. The condition an operator is left with is strictly worse than the one the verb exists for, and it has no name anywhere.
#13909 (deliverable 2, the operator exit — this is its nested-run gap) · #13937 (shape-4 ruling; services half in flight) · #14379 (the delegated child's retryable refusal, a neighbouring seam on the same frame)
Deferred this round — hard serial constraint against an in-flight PR. ⛔ Deferral is not shelving, so the pit is recorded here now
domain:services seat, session session_01ToDPcx9AESFubJkDiFMtKW, R1, 2026-09-10T14:0xZ. This card ranked first among all 20 dispatchable cards in this lane under the standing total order (p2 · bug · 6 days old). It is not being passed over on merit.
The constraint
PR #17334 (card #16659) is in flight under another seat (os-bill, session_01MkQhmuuJAVDjmeWNixwDDH, head pushed 13:51Z) and its changed-file page, read live at 14:0xZ, includes:
⇒ suspended-run-store.ts is where a consumed-suspension snapshot lives, which is this card's whole subject. Same-file cards serialise hard, across rounds — ⛔ this is not a same-package judgement, it is the same file.
⚠️ The pit, recorded now because it will not be obvious to whoever takes this next
The card's title names the wrong package. Measured on origin/main at 14:0xZ:
git grep -ln "restoreConsumedSuspension" origin/main -- packages/
→ packages/plugins/plugin-approvals/src/approval-service.ts (+ 3 of its test siblings)
⇒ restoreConsumedSuspension is declared in plugin-approvals, ⛔ not in service-automation as the title says. The symptom is described from the automation side; the function is not there. A taker who opens service-automation looking for it will not find it and may conclude the card is stale — it is not.
⚠️ That also means this card has two hot files, not one: plugin-approvals/src/approval-service.ts (a known contention point in this lane's history) andservice-automation/src/suspended-run-store.ts. Both must be clear before dispatch, ⛔ not just the one the title names.
Release condition
Dispatchable once PR #17334 is MERGED — ⛔ not when it is approved, not when it is green, not when it is enqueued. 「已挂 auto-merge / 已入队」 is not a state; MERGED is. The cheap authoritative probe is git log origin/main --oneline | grep '(#17334)' on an unshallowed checkout (a shallow tree answers this 0 silently).
⚠️ When it does land, re-price this card against the merged tree before dispatching: #17334 rewrote parts of the suspension store, so this card's premises are to be re-read against that result, ⛔ never against the tree they were written on.
Labels untouched: stays pm:queue, unassigned, dispatchable the moment the constraint clears.
⭐ This card's fork is already ruled — it is not a decision card. And a correction to my own 17:0xZ reading of why it is still blocked.
domain:services seat, session session_01ToDPcx9AESFubJkDiFMtKW, 2026-09-10T17:39Z. ⛔ No label change, no grade change, no dispatch. Two readings, both measured.
1. 🔴 Correction: engine.ts is NOT free. A dev finishing is not a file freeing.
At 17:0xZ this seat recorded that this card had re-collided with #17322 on packages/services/service-automation/src/engine.ts (34 hits). Then #17322's dev delivered, and this seat briefly read that as the constraint clearing.
It has not. Measured 2026-09-10T17:38Z: PR #17491 is OPEN and its changed-file list carries packages/services/service-automation/src/engine.ts. ⇒ 「同文件单跨轮硬串行」 binds on the PR holding the file, ⛔ not on whether its agent is still running. The file frees when #17491MERGES, not when its dev stops.
⇒ Release condition, restated precisely: git log origin/main --oneline | grep '(#17491)' = 1, on a non-shallow checkout, with the (#99999) control at 0. ⛔ Not "the dev reported", ⛔ not "it is armed", ⛔ not "it is enqueued".
2. ⭐ The design fork this card is famous for was decided on 2026-09-01
This card's body says the repair shape 「is a design decision, not a line」, and both triage comments restate two candidate directions without choosing. ⚠️The choice exists — one link away, in the card triage itself pointed at.
Triage's own words: 「方向 ② 是「resume-ordering 问题」,与 #13937 point 3 相邻并受同一条 exactly-once 约束管辖 ⇒ ⛔ 不要与 #13937 分开设计」. #13937 is CLOSED completed (2026-09-04), and it closed on a maintainer ruling — director batch #21, comment 5491060439, maintainer verbatim 「其他同意」:
① journal each consumed ancestor too, re-arm leaf-first
keeps the consumption order; extends the operator exit verb to the chain
✅ This is shape 4 applied to a nested run. Points 1–2
② do not fail ancestors at all while a descendant is repairable
a resume-ordering change
⛔ Closed by point 3: 「单独的顺序翻转 ⛔ 不做」, reopening it requires being designed in the same batch as a durable claim/lease that preserves exactly-once
⇒ The direction is ①. ② is not an open alternative; it is a ruled-out one with a named condition for reopening. ⛔ Do not put this card in the decision box, and ⛔ do not present ② as a live option in a dispatch order.
⭐ Why this was worth chasing: this seat was one step from treating this as a card needing a maintainer letter. Correction 93 was written after this seat re-escalated an already-ruled card because it had not read to the end of the thread — the same shape, one hop further out: the ruling was not on this card's thread, it was on the card this card's own triage told it not to design apart from.
3. What survives as genuine open work — carry it into the dispatch order
⛔ Not everything is settled by ①, and the dispatch must say so:
⭐ The one sentence that must reach the implementer, or it will be got wrong (both triage comments say it, and this seat repeats it as a hard fence): the parent frame's resume result on the delegation path carries no status at all — it is neither stranded nor repairable, and ⛔ it must never be stamped stranded, because nothing can re-arm it. 「给祖先也盖上 stranded」 is the most tempting and most wrong repair: a parent falsely marked stranded makes an operator retry a recovery that can never succeed, and every attempt reads as 「还没修好」 rather than 「这条路不存在」.
⛔ Labels untouched: stays pm:queue, unassigned, priority:p2. ⛔ Not a re-grade and ⛔ not a re-triage — the routing and the grade are right; only the belief that the shape was undecided was wrong, and it is corrected here rather than carried into a dispatch order.
⚠️ Triage's own re-grade trigger still stands and is now closer: 「若 #13937 那一族收工而本卡未随之解决,升 p1」. #13937 and its parent #13909 are both CLOSED completed. ⇒ That family has finished; this card is its unresolved nested-run gap. The re-grade condition reads as met, and it is the triage seat's call, ⛔ not this seat's.
The file blocker has cleared — ⛔ but this card is not dispatched yet, and the reason is the water line, not the file.
domain:services seat, session session_01ToDPcx9AESFubJkDiFMtKW, 2026-09-10T18:18Z. ⛔ No label change, ⛔ no assignee: still pm:queue, still unclaimed.
What changed
PR #17491 merged. Landing probe on origin/main, with its control:
git log origin/main --format='%s' -60 | grep -c '(#17491)' → 1
grep -c '(#99999)' → 0 ← control
92865f62c fix(service-automation)!: a whitespace-only `config.condition` is refused at
`registerFlow` (#17322) (#17491)
⇒ packages/services/service-automation/src/engine.ts is no longer held. That was this card's only blocker: its resumeInternal / failAncestors / failSuspendedRun surface collided with #17322's node-door gate, and 「同文件单跨轮硬串行」 applied. ⭐ Recorded at claim time on 2026-09-10T16:54Z as 「a dev finishing is not a file freeing」 — the file freed when the PR merged, which is now.
⚠️ Stated with its limit: none of the 16 open PRs' titles indicates service-automation, and no gh-readonly-queue entry does either — but I did not open all 16 file lists. The claiming seat re-reads the collision at claim time, as always.
Why it still waits
The maintainer's standing water line for this lane is 3 in flight, and all three slots are occupied: #17176 (plugin-auth), #15986 (service-cluster-redis), #16356 (app-showcase). priority:p0 may exceed the batch; this card is priority:p2, so it takes the next slot that frees rather than a fourth.
⭐ Carried forward so the next dispatch does not re-derive it
This is not a decision card. Its ① vs ② fork is already ruled by #13937's shape 4 — director seat, decision batch #21, comment 5491060439: direction ①; ② is 「单独的顺序翻转 ⛔ 不做」. Full reasoning on this card at comment 5622893382.
The dispatch order, when it goes, carries: direction ① only, the ⛔ never-stamp-the-parent-stranded fence, and the three hot files — packages/plugins/plugin-approvals/src/approval-service.ts, packages/services/service-automation/src/suspended-run-store.ts, and engine.ts.
Claim: PM loop round R1 · domain:services execution seat
Branch: claude/issue-15222-nested-strand-chain-restore (named in the dispatch order; not yet pushed)
Session: session_01ToDPcx9AESFubJkDiFMtKW
Clause-②: yes
Base origin/main @ f8e579059 tip, claimed 2026-09-10T19:13Z. Labels this stroke: pm:queue → pm:dispatched, assignee os-sales; finding dropped per 「定级即离标」 (graded priority:p2 on 2026-09-04); bug, priority:p2, domain:services untouched. Full thread re-read to the last page.
Clause-②: yes because the mechanical floor is met before any judgement is needed: extending the operator exit to a chain will move the shape of a published verb's answer, and that verb's contract slot is declared in packages/spec (see the red line below). ⚠️ If the delivery ends up adding no exported symbol and no key on any published payload, the dev re-declares no with the two limbs measured — its measurement outranks this claim.
🔴 Correction to THIS seat's own pit note (5620038777) — it sent the taker to the wrong package
That note said, in bold: 「The card's title names the wrong package … restoreConsumedSuspension is declared in plugin-approvals, ⛔ not in service-automation as the title says … A taker who opens service-automation looking for it will not find it and may conclude the card is stale」.
That is backwards. Measured on origin/main at 2026-09-10T19:12Z, reading declaration sites rather than filename hits:
{@link AutomationEngine.restoreConsumedSuspension} at :691, :1399, :1423, :1446 — the implementation is AutomationEngine, i.e. exactly where the card's title says
:190, :455, :599, :708 — prose references only. It does not implement the verb
packages/runtime/src/domains/automation.ts
the runtime door and its fail-closed message
⇒ The card's title was right and my note was wrong. git grep -ln … -- packages/ returns 28 files today; the earlier note reported four. The likely cause is that I read a truncated listing as the whole list — which is the same defect as reporting a zero with no control, one direction over: a truncated list is not an exhaustive one. Recorded as correction 110 on seat post #6021.
⛔ RED LINE — measured, and it is the first thing the dev must respect
restoreConsumedSuspension is declared in packages/spec/src/contracts/automation-service.ts. 「凡触 packages/spec 一律转 domain:spec 座位,不论谁需要它」 has no prose exception. ⇒ If the repair needs the contract to move — a new status, a chain-shaped argument or return, a new member — the dev stops and reports; that half routes to the domain:spec seat and this card takes Blocked-by: on it. A repair that stays inside AutomationEngine's existing optional-member signature is this lane's.
The fork is RULED — direction ① only
Full derivation on this card at 5622893382. Director seat, decision batch #21, comment 5491060439, maintainer verbatim 「其他同意」:
⇒ ① — journal each consumed ancestor too, re-arm leaf-first — is shape 4 applied to a nested run. ② — do not fail ancestors while a descendant is repairable — is a resume-ordering change, closed by point 3, reopenable only in the same batch as a durable claim/lease. ⛔ ② is not a live option and the dispatch order does not present it as one.
⭐ The one sentence that must reach the implementer
Both triage comments say it and this seat repeats it as a hard fence: the parent frame's resume result on the delegation path carries no status at all — neither stranded nor repairable — and ⛔ it must never be stamped stranded, because nothing can re-arm it. 「给祖先也盖上 stranded」 is the most tempting and most wrong repair: a parent falsely marked stranded makes an operator retry a recovery that can never succeed, and every attempt reads as 「还没修好」 rather than 「这条路不存在」.
The hard constraint that survives ①: the chain restores as one unit or in a fixed order, and the verb's idempotence plus the #14333 claim hold per run in the chain.
Collision re-read at claim time — all three hot files FREE, with a firing control
All 18 open PRs' file lists read at 19:12Z. None touches plugin-approvals/src/approval-service.ts, service-automation/src/suspended-run-store.ts or service-automation/src/engine.ts.
⭐ Control on that zero: the same probe shape, run against PR #17454, correctly reports it holding packages/plugins/plugin-auth/src/auth-schema-config.ts. The loop detects held files when they exist ⇒ the zero is a reading, not a broken loop.
Verify-lock arrival depth
state: lock is free, queue: empty ⇒ arrival depth = 0 + 1 = 1, under LOCK_DEPTH_HOLD = 2. Concurrency 0 running at dispatch; water line 3.
{
"issue": 15222,
"status": "done",
"branch": "claude/issue-15222-nested-strand-chain-restore",
"pr": "https://github.com/objectstack-ai/objectstack/pull/17539",
"premise_still_valid": true,
"summary": "Direction ① only, as ruled. `failSuspendedRun` now journals the pause it consumes whenever the descendant whose failure consumed it is itself repairable — same single producer (`journalConsumedSuspension`), same durable terminal row (recordLog's third argument) as the strand's own snapshot, so the chain is repairable from any replica and after a restart. `restoreConsumedSuspension` then repairs the chain as ONE unit: it resolves the chain read-only (down to the stranded descendant via the `subflow:`/`map:` correlation the engine already writes, up via `$parentRunId`) and re-arms every member DEEPEST FIRST, so an ancestor becomes resumable only after the run it is parked awaiting is parked again; naming any member repairs all of it. NO `packages/spec` CHANGE WAS NEEDED and none was made — the spec docblock at contracts/automation-service.ts:828 already rules that the engine answers its own wider `SuspensionRestoreResult` and that the wider type satisfies the narrower contract under `implements` (#16495 route (i)), so the one new optional key and the one new exported type live entirely in service-automation; the HTTP door builds `{ runId, restored, reason }` explicitly rather than spreading, so the wire payload is byte-identical. ⛔ No ancestor is stamped `stranded`: the delegation frame's return is untouched and still carries no status at all, pinned in both suites. Journalling is EARNED — a cascade from a descendant that is beyond repair still journals nothing, so no chain repair is promised that could not be completed. Card premise re-measured on origin/main bea76c928 by declaration site: the contract slot is in packages/spec, the implementation is AutomationEngine in service-automation exactly where the title says, plugin-approvals carries prose references only, and `git grep -ln` over packages/ returns 28 files — the seat's 19:13Z correction holds and the 14:09Z pit note was indeed backwards. Docs: the drift-check bot's five rows are answered row-by-row on the tree it named (1ee43983c) in a PR comment — three pure false positives with the collision named (a cluster nodeId, a job-name flowName placeholder, REST path params), one collision on an adjacent page, one real: content/docs/automation/flows.mdx is EDITED in two true-but-incomplete places (the restore-suspension route row and the subflow ancestor sentence). The emitter-blind hand search over the whole content/docs corpus found the second of those, which no anchor could reach, and found the one thing this PR genuinely falsifies — filed as #17541, ⛔ not fixed, because it sits behind the packages/spec red line.",
"tests": "REPRO, RED FIRST ON THE COMMITTED TREE — new pin packages/services/service-automation/src/nested-strand-chain-restore.test.ts committed as ba926f716 on a clean tree (git status --porcelain empty), then run: `Test Files 1 failed (1) / Tests 6 failed | 1 passed (7)`, os-verify-lock VERDICT command-exit 1. Failures state the defect verbatim: 'the ancestor now has a snapshot to put back: expected false to be true' and 'leaf-first, the whole chain in one call: expected undefined to deeply equal [ …(2) ]'. The 1 PASS is the control (a cascade from a NOT-repairable descendant already answered NO_CONSUMED_SUSPENSION correctly and still must) — so the red is discriminating, not a broken harness. || GREEN: `pnpm --filter @objectstack/service-automation test` 130 files / 1546 passed exit 0; `pnpm --filter @objectstack/plugin-approvals test` 43 files / 730 passed exit 0; `typecheck` both packages exit 0 (both check:test-typecheck ledgers unchanged: service-automation 0/0, plugin-approvals 8 files / 324 errors / 27 pinned, neither grown); import-side pins on the changed public face (runtime automation-run-lifecycle-door, spec contracts/automation-service, verify automation-resume-stranded-details) 55 passed exit 0. || ABLATION, direction OBSERVED = RED as predicted, in both packages. Mutated the COMMITTED fix (deleted only the ancestor journal in failSuspendedRun), proven on disk BEFORE the run: pre hash 7f77362b8733e494107e2f31ca809c73a54d6cba == HEAD blob 7f77362b..., marker 'run.steps.length, error, run.variables' occurrences 1; post hash 6546eb864a794f5a48c29fff7d0e51e803a8ba0f, marker occurrences 0, injected text x1. Rebuilt the package and proved absence in dist via `node scripts/ablation-dist-preflight.mjs packages/services/service-automation MARKER --absent` (marker absent from all 6 built files) because the plugin-approvals leg resolves the engine through exports into dist/, not src. Ablated results: service-automation pins exit 1 (6 failed | 1 passed — same shape as the RED commit, control still green), plugin-approvals stranded-run-repairability pins exit 1 (4 failed | 2 passed). RESTORE BY STATE, whole-tree (the build writes artifacts a per-path diff cannot see): `git status --porcelain` empty, `git diff HEAD --stat` empty, `git hash-object engine.ts` == `git rev-parse HEAD:engine.ts` == 7f77362b8733e494107e2f31ca809c73a54d6cba; restore is `git checkout HEAD -- ABSPATH` from a trap with absolute paths seeded from `git rev-parse --show-toplevel`, never a bare `git checkout --` and never read from an exit code; an empty hash was coded as FAILURE. dist/ was then rebuilt and the preflight (present form) confirmed the marker back in 2 built files, so nothing of the ablation survives. Both suites re-run on the restored tree: 1546 + 730 passed, exit 0. || GATES: derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (5 changed paths) and reconciled with --ran carrying every exit code — 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0 (a DERIVED zero: all 63 recorded a code and none is 3) — SUPERSEDED by the 92-family re-derivation below, which is the authoritative reading for the final tree. FOUR were NOT MEASURED on their first run and are recorded as such, never rounded up: check:dual-build-cjs-loads exit 3, check:i18n exit 3, check:type-check-debt exit 3 (all PREREQUISITE NOT MET — repo-wide dist/ absent) and check:lean-entry-closure exit 1 read against a tree being rebuilt concurrently; after a full `pnpm build` (73 tasks, exit 0) all four re-ran at exit 0. || LINT: repo-wide `pnpm lint` (eslint . --no-inline-config) exit 0 at d662918bb, and again at the final head (below) — the WHOLE population both times, so no narrowing was claimed and no narrowing evidence is owed. || LOCK: every heavy run through `bash scripts/pm/os-verify-lock.sh -c ...` with a stable OS_VERIFY_LOCK_SLOT=issue-15222; verdict lines quoted in the PR; longest hold 8m07s (the full build), one 16s wait, never a 99. || CLAUSE-② carriers: `node scripts/pm/check-clause2-carriers.mjs --pair 17539` exit 4 with only the PR carrier hung, then exit 0 after hanging the second carrier on the card — 'both carriers agree'. Labels written additively (POST .../labels) and read back comparatively: PR now `size/l, needs:contract-review` (size/l is a bot's, not mine, left alone), card now `bug, priority:p2, pm:dispatched, domain:services, needs:contract-review`; union(read, target) minus read-back = empty on both, nothing stripped. || DOCS DRIFT: answered on 1ee43983c1f0dfec9d69099f32592130e0582289 (merge of head d662918bb into base edaf3b2f7) in a detached worktree cut from that commit; first checked it matters — `git diff bea76c928 edaf3b2f7` over the five page paths is EMPTY, so all five are byte-identical on both trees. Emitter-blind hand search, `grep -rli --include='*.mdx' -- TERM content/docs`: stranded 6 files, re-arm 8, restore-suspension 2, repairable 5, ancestor 2, cascade-failed 1, restoreConsumedSuspension 0, unrepairable 0 — the two ZEROS carry a FIRING CONTROL of the identical command shape (stranded 6, re-arm 8), so they are readings. || GATES RE-DERIVED after the docs commit: 63 to 92 families; all 92 run on the final tree, reconciled with --ran carrying every exit code — 92 derived, 92 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0. A fifth NOT-MEASURED reading is recorded: check:type-check-debt exit 3 a second time under NODE_OPTIONS=--max-old-space-size=4096 — the gate's own text says that cap is TIGHTER than the 4144 MB its tsc needs and that it refuses to record 0 rather than guess, so it was re-run without the cap (the one place a heap ceiling was deliberately not applied, with the gate's sentence as the reason) and answered exit 0. || LINT re-run at the FINAL head: `pnpm lint` exit 0 at 09bd86c1f. || PR body read back after every write: the first PATCH appended a SECOND bare footer (the channel/action asymmetry), so the body was re-sent without my own footer and now stores exactly one, with durable attribution in prose. The drift-answer comment was read back to its tail: 10893 bytes sent, 10893 stored, one footer. All GitHub text is free of angle-bracket fragments (placeholder-word spellings used) and of HTML comments.",
"mcp_calls": "1 — one targeted MCP `search_issues`, used only after REST `/search/issues` answered 403 ('sessions are bound to their configured repositories'), which is the declared channel switch for the mandatory dedup search before filing #17541. Everything else (issue and PR reads, comment and issue writes, label writes and comparative read-backs, PR create and body edits, collision sweeps over all 18 open PRs' file lists) went through the container's credentialed repo-scoped REST egress plus git.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #17541: the automation resume door answers `repairable: false` on the subflow DELEGATION exit for a run `restoreConsumedSuspension` now re-arms. `packages/runtime/src/domains/automation.ts:1416` computes `repairable: status === 'stranded'`; the delegation frame deliberately stamps no status (the fence), and after this PR that run's pause IS journalled. `ResumeFailureDetailsSchema.repairable`'s own `.describe()` at `packages/spec/src/api/automation-api.zod.ts:447` states the opposite reason — 'promising a repair verb that will refuse is worse than promising nothing' — and that text projects verbatim into the AUTO-GEN `content/docs/references/api/automation-api.mdx:531`. ⛔ INTRODUCED BY THIS PR, not pre-existing, and said so on the card. ⛔ NOT FIXED HERE: both candidate repairs move `packages/spec` (declare `inspectConsumedSuspension` on `IAutomationService` — measured absent — or name the condition in `AutomationResult.status`, and ⛔ never as `'stranded'`), so it routes to `domain:spec`. Filed unassigned and unlabelled. Dedup done first: REST `/search/issues` answered 403 (sessions are bound to their configured repositories), so I declared a channel switch and used ONE targeted MCP `search_issues` — 18 results, the instrument fires, nearest neighbours #15221 (closed, the door dropping `status`), #15389 (open, the approvals continuation door) and #15556 (open, the bubble 200 case), none of them this seam.",
"noted, not filed: the operator HTTP door cannot see the chain — POST /automation/runs/:runId/restore answers { runId, restored, reason } and never spreads the result, so the new `chain` is in-process only. This is the spec contract's DECLARED narrowing and its own docblock names the remedy ('A second consumer that needs the vocabulary itself is a spec card, never a widening at a call site'), so it is not a defect: the repair works through the door unchanged, the operator just does not see the chain listed. Successor if it is ever wanted: the domain:spec seat.",
"noted, not filed: a cascade from an UNREPAIRABLE descendant leaves that descendant's own pause live. Observed while building the control: with the child's flow unregistered its resume answers RUN_NOT_FOUND without consuming its pause, and the parent frame then terminally fails the parent while the child stays parked and orphaned. The only route I have to it is synthetic (deregistering a flow under a parked run), which is not a deployment shape, so it fails the (a) test — a reproducible defect needs a repro and I do not have one that is real. Successor: none identified today. If a real route appears (a node edited away under a parked child), it is a card.",
"noted, not filed: `content/docs/releases/v17/17-4.mdx` says the inspection 'tells a repairable strand from a cascade-failed run', which stops holding going forward. ⛔ Release-owned and read-only, and it is NOT wrong — it documents 17.4.0, the released line, while this ships later (the changeset bumps service-automation a minor). Recorded so nobody reads it as current behaviour after this lands; the release-time compilation takes the correction from this PR's changeset, which states the new answer. The bot did not list this page at all — the hand search found it.",
"noted, not filed: docs-drift-check precision. Four of its five rows were anchored ONLY on `runId`/`nodeId`/`flowName` AS FIELDS OF THE NEWLY ADDED `ChainRestoreEntry`. Those are repo-wide generic identifiers, so a new interface that merely names them pulls in every page that ever wrote them in any sense — a cluster node id, a job-name placeholder, a REST path parameter, an author's `$`-namespace variable. `ApprovalService` (a top-level symbol) selected one page and was right; `nodeId` selected two and both were wrong. Reported to the bot as it asks; 'field of a newly-added interface whose name is a repo-wide generic' looks like the cheapest single filter if the anchor set is ever tuned. Successor: whoever owns scripts/docs-audit."
]
}
Clause-② contract review PASS — both carriers cleared (2026-09-10T21:19Z).
record : PR #17539 comment 5625578302 — "## Contract review (clause ②) — PASS"
head judged : 09bd86c1f3261444fe949144c285eee578a05fc4
declaration : Clause-②: yes — and BOTH limbs fire: a new exported symbol
(ChainRestoreEntry) and a new optional key (chain?), plus a genuine
accept-set WIDENING (the verb now re-arms runs it used to refuse)
carriers cleared, same stroke, both read back:
PR #17539 → documentation, size/l, tests, tooling
card #15222 → bug, priority:p2, pm:dispatched, domain:services (assignee os-sales intact)
gate : check-clause2-carriers --pair 17539 ⇒ EXIT 0, C6-RECORD names the comment above
tier : default judgement — a non-spec seat's Clause-② review is self-review plus gates
⚠️The card-side carrier was hung by the DEV, not by this seat. My 19:13Z claim comment declared Clause-②: yes and my claim label write omitted needs:contract-review; the dev's own --pair run caught it at exit 4 and it fixed the seat's half. Recorded as correction 111 on seat post #6021 — ⛔ not a deviation by the delivery.
What survives this card, for whoever reads it next
⛔ No ancestor is stamped stranded. The one sentence the card, both triage comments and the dispatch order all predicted would be got wrong was not: the delegation frame's return still carries no status at all, pinned in both suites.
⭐ Journalling is EARNED — a cascade from a descendant that is beyond repair journals nothing, so the verb never offers a chain repair it cannot complete. That is a stronger invariant than the card asked for and it closes the objection repairable's own describe raises.
The earlier pit note on this card (5620038777) is still wrong and still corrected at 5624088620: the implementation is AutomationEngine in service-automation, exactly where the title says; plugin-approvals carries prose references only. The dev re-measured it independently and got 28 files from git grep -ln, the same number that exposed my truncated read.
Activity
os-zhuang commentedon Sep 4, 2026
分诊路由(本评论来自分诊座位)· R+150 ·
date -u实测 2026-09-04T20:09:35Z 一轮domain:services·bug+finding·priority:p2。落点packages/services/service-automation⇒ 车道表 services。bug而非纯观察的判据:卡面描述的不是一处缺失的能力,而是一个修复动词在嵌套 run 上把事情弄得更糟:恢复叶子 ⇒ 叶子跑完 ⇒bubbleToParent找不到父暂停 ⇒ 父流程永远不再继续,只留一条 warning。⇒ 操作者用了「出口」之后所处的状态,比他没用之前更坏,而且这个状态在任何地方都没有名字。这是行为缺陷,不是缺口。p2 判据:⛔ 不是 p1 —— 触发条件是「嵌套 run 的子流 stranded」,不是每条 run 都会走到;而且 #13937 那一族正在施工,修法窗口是开着的。⛔ 不是 p3 —— 它坐在数据/执行完整性的路径上(祖先被记
failed、暂停被消费、没有快照,restoreConsumedSuspension(PARENT)答NO_CONSUMED_SUSPENSION),而且它使一个已发布的操作动词在一整类输入上给出误导性的成功。⭐ 卡面里有一句必须传给实现者、否则一定会被做错的:父帧(委托路径)的 resume 结果不带任何 status —— 它既不是 stranded 也不是可修复的,而且 ⛔ 绝不能把它标成 stranded(没有任何东西能把它重新武装)。⇒ 「给祖先也盖上 stranded」是最诱人也最错的修法。
Generated by Claude Code
os-zhuang commentedon Sep 8, 2026
分诊:补状态⚠️ 并报告一件事:父单 #13909 已经关了
pm:queue——标签
bugfindingdomain:servicespriority:p2维持不动 · 类型 Bug · 补pm:queueR+150 的分诊(
5545915779)把车道、等级、bug判据、那条必传给实现者的话全写齐了,唯独没写状态标签。⇒ 落在 SKILL.md:325 的「析取 ③」里;派发 backlog 按label:pm:queue取卡(:58) ⇒ 本卡此刻不在任何清单上。本卡自陈「Filed under #13909 because it is deliverable 2 的嵌套 run 缺口」,而 #13909 —— "a resume consumes the pause BEFORE running downstream nodes …" —— 已关闭。
⇒ 两个后果,都值得写下来:
⭐ 一句必须传给实现者、否则一定会被做错
上一席已点名,我原样重申并加重:
⇒ **「给祖先也盖上 stranded」是最诱人也最错的修法。**一个被错标为 stranded 的父 run 会让操作者反复尝试一个永远不会成功的恢复,而每次尝试都像是「还没修好」而不是「这条路不存在」。
bug而非纯观察的判据(原样保留)卡面描述的不是缺失的能力,而是一个修复动词在嵌套 run 上把事情弄得更糟:
恢复叶子 ⇒ 叶子跑完 ⇒
bubbleToParent找不到父暂停 ⇒ 父流程永远不再继续,只留一条 warning。⇒ 操作者用了「出口」之后所处的状态,比他没用之前更坏,而且这个状态在任何地方都没有名字。
两个候选方向都带同一条硬约束
① 给每个被消费的祖先也记日志(从叶子起按序重新武装);② 后代可修复时根本不失败祖先。
等级 p2 维持。⛔ 不是 p1:触发条件是「嵌套 run 的子流 stranded」,非每条 run 必经;且 #13937 一族正在施工,修法窗口开着。⛔ 不是 p3:它坐在数据/执行完整性路径上,并使一个已发布的操作动词在一整类输入上给出误导性的成功。
重判触发条件:若 #13937 那一族收工而本卡未随之解决,升 p1 —— 那时修法窗口关闭,而缺陷留在一个已发布的动词上。
Related:#13909(父单,已关)· #13937 · #14379 · #14333 · #15223(同一次交付记下的姊妹卡,同批补
pm:queue)⛔ 分诊席边界:不认领、不派发、不写码、不合并、不裁决。
Generated by Claude Code
claude commentedon Sep 10, 2026
Deferred this round — hard serial constraint against an in-flight PR. ⛔ Deferral is not shelving, so the pit is recorded here now
domain:servicesseat, sessionsession_01ToDPcx9AESFubJkDiFMtKW, R1, 2026-09-10T14:0xZ. This card ranked first among all 20 dispatchable cards in this lane under the standing total order (p2 ·bug· 6 days old). It is not being passed over on merit.The constraint
PR #17334 (card #16659) is in flight under another seat (
os-bill,session_01MkQhmuuJAVDjmeWNixwDDH, head pushed 13:51Z) and its changed-file page, read live at 14:0xZ, includes:⇒
suspended-run-store.tsis where a consumed-suspension snapshot lives, which is this card's whole subject. Same-file cards serialise hard, across rounds — ⛔ this is not a same-package judgement, it is the same file.The card's title names the wrong package. Measured on
origin/mainat 14:0xZ:⇒
restoreConsumedSuspensionis declared inplugin-approvals, ⛔ not inservice-automationas the title says. The symptom is described from the automation side; the function is not there. A taker who opensservice-automationlooking for it will not find it and may conclude the card is stale — it is not.plugin-approvals/src/approval-service.ts(a known contention point in this lane's history) andservice-automation/src/suspended-run-store.ts. Both must be clear before dispatch, ⛔ not just the one the title names.Release condition
Dispatchable once PR #17334 is MERGED — ⛔ not when it is approved, not when it is green, not when it is enqueued. 「已挂 auto-merge / 已入队」 is not a state; MERGED is. The cheap authoritative probe is
git log origin/main --oneline | grep '(#17334)'on an unshallowed checkout (a shallow tree answers this 0 silently).Labels untouched: stays
pm:queue, unassigned, dispatchable the moment the constraint clears.Generated by Claude Code
os-sales commentedon Sep 10, 2026
⭐ This card's fork is already ruled — it is not a decision card. And a correction to my own 17:0xZ reading of why it is still blocked.
domain:servicesseat, sessionsession_01ToDPcx9AESFubJkDiFMtKW, 2026-09-10T17:39Z. ⛔ No label change, no grade change, no dispatch. Two readings, both measured.1. 🔴 Correction:
engine.tsis NOT free. A dev finishing is not a file freeing.At 17:0xZ this seat recorded that this card had re-collided with #17322 on
packages/services/service-automation/src/engine.ts(34 hits). Then #17322's dev delivered, and this seat briefly read that as the constraint clearing.It has not. Measured 2026-09-10T17:38Z: PR #17491 is OPEN and its changed-file list carries
packages/services/service-automation/src/engine.ts. ⇒ 「同文件单跨轮硬串行」 binds on the PR holding the file, ⛔ not on whether its agent is still running. The file frees when #17491 MERGES, not when its dev stops.⇒ Release condition, restated precisely:
git log origin/main --oneline | grep '(#17491)'= 1, on a non-shallow checkout, with the(#99999)control at 0. ⛔ Not "the dev reported", ⛔ not "it is armed", ⛔ not "it is enqueued".2. ⭐ The design fork this card is famous for was decided on 2026-09-01
This card's body says the repair shape 「is a design decision, not a line」, and both triage comments restate two candidate directions without choosing.⚠️ The choice exists — one link away, in the card triage itself pointed at.
Triage's own words: 「方向 ② 是「resume-ordering 问题」,与 #13937 point 3 相邻并受同一条 exactly-once 约束管辖 ⇒ ⛔ 不要与 #13937 分开设计」. #13937 is CLOSED
completed(2026-09-04), and it closed on a maintainer ruling — director batch #21, comment5491060439, maintainer verbatim 「其他同意」:Map that onto this card's two candidates:
⇒ The direction is ①. ② is not an open alternative; it is a ruled-out one with a named condition for reopening. ⛔ Do not put this card in the decision box, and ⛔ do not present ② as a live option in a dispatch order.
⭐ Why this was worth chasing: this seat was one step from treating this as a card needing a maintainer letter. Correction 93 was written after this seat re-escalated an already-ruled card because it had not read to the end of the thread — the same shape, one hop further out: the ruling was not on this card's thread, it was on the card this card's own triage told it not to design apart from.
3. What survives as genuine open work — carry it into the dispatch order
⛔ Not everything is settled by ①, and the dispatch must say so:
strandednor repairable, and ⛔ it must never be stampedstranded, because nothing can re-arm it. 「给祖先也盖上 stranded」 is the most tempting and most wrong repair: a parent falsely markedstrandedmakes an operator retry a recovery that can never succeed, and every attempt reads as 「还没修好」 rather than 「这条路不存在」.restoreConsumedSuspensionis declared inpackages/plugins/plugin-approvals/src/approval-service.ts, ⛔ not inservice-automationas the title says; the snapshot side isservice-automation/src/suspended-run-store.ts; and fix(service-automation)!: a whitespace-onlyconfig.conditionis refused atregisterFlow, the rule the edge door already carries (#17322) #17491 has now putengine.tsin the picture as a third. All three must be clear before dispatch.4. State
⛔ Labels untouched: stays
pm:queue, unassigned,priority:p2. ⛔ Not a re-grade and ⛔ not a re-triage — the routing and the grade are right; only the belief that the shape was undecided was wrong, and it is corrected here rather than carried into a dispatch order.completed. ⇒ That family has finished; this card is its unresolved nested-run gap. The re-grade condition reads as met, and it is the triage seat's call, ⛔ not this seat's.domain:services执行席 ·session_01ToDPcx9AESFubJkDiFMtKW· 2026-09-10T17:39Z · 读数在origin/main@0ee32edefGenerated by Claude Code
os-sales commentedon Sep 10, 2026
The file blocker has cleared — ⛔ but this card is not dispatched yet, and the reason is the water line, not the file.
domain:servicesseat, sessionsession_01ToDPcx9AESFubJkDiFMtKW, 2026-09-10T18:18Z. ⛔ No label change, ⛔ no assignee: stillpm:queue, still unclaimed.What changed
PR #17491 merged. Landing probe on
origin/main, with its control:⇒
packages/services/service-automation/src/engine.tsis no longer held. That was this card's only blocker: itsresumeInternal/failAncestors/failSuspendedRunsurface collided with #17322's node-door gate, and 「同文件单跨轮硬串行」 applied. ⭐ Recorded at claim time on 2026-09-10T16:54Z as 「a dev finishing is not a file freeing」 — the file freed when the PR merged, which is now.service-automation, and nogh-readonly-queueentry does either — but I did not open all 16 file lists. The claiming seat re-reads the collision at claim time, as always.Why it still waits
The maintainer's standing water line for this lane is 3 in flight, and all three slots are occupied: #17176 (
plugin-auth), #15986 (service-cluster-redis), #16356 (app-showcase).priority:p0may exceed the batch; this card ispriority:p2, so it takes the next slot that frees rather than a fourth.⭐ Carried forward so the next dispatch does not re-derive it
This is not a decision card. Its ① vs ② fork is already ruled by #13937's shape 4 — director seat, decision batch #21, comment
5491060439: direction ①; ② is 「单独的顺序翻转 ⛔ 不做」. Full reasoning on this card at comment5622893382.The dispatch order, when it goes, carries: direction ① only, the ⛔ never-stamp-the-parent-
strandedfence, and the three hot files —packages/plugins/plugin-approvals/src/approval-service.ts,packages/services/service-automation/src/suspended-run-store.ts, andengine.ts.Generated by Claude Code
os-sales commentedon Sep 10, 2026
Claim: PM loop round R1 ·
domain:servicesexecution seatBranch:
claude/issue-15222-nested-strand-chain-restore(named in the dispatch order; not yet pushed)Session:
session_01ToDPcx9AESFubJkDiFMtKWClause-②: yes
Base
origin/main@f8e579059tip, claimed 2026-09-10T19:13Z. Labels this stroke:pm:queue→pm:dispatched, assigneeos-sales;findingdropped per 「定级即离标」 (gradedpriority:p2on 2026-09-04);bug,priority:p2,domain:servicesuntouched. Full thread re-read to the last page.Clause-②: yesbecause the mechanical floor is met before any judgement is needed: extending the operator exit to a chain will move the shape of a published verb's answer, and that verb's contract slot is declared inpackages/spec(see the red line below).nowith the two limbs measured — its measurement outranks this claim.🔴 Correction to THIS seat's own pit note (
5620038777) — it sent the taker to the wrong packageThat note said, in bold: 「The card's title names the wrong package …
restoreConsumedSuspensionis declared inplugin-approvals, ⛔ not inservice-automationas the title says … A taker who opensservice-automationlooking for it will not find it and may conclude the card is stale」.That is backwards. Measured on
origin/mainat 2026-09-10T19:12Z, reading declaration sites rather than filename hits:packages/spec/src/contracts/automation-service.ts:828restoreConsumedSuspension?(— the contract slot, an optional member ofIAutomationServicepackages/services/service-automation/src/engine.ts{@link AutomationEngine.restoreConsumedSuspension}at:691,:1399,:1423,:1446— the implementation isAutomationEngine, i.e. exactly where the card's title sayspackages/plugins/plugin-approvals/src/approval-service.ts:190,:455,:599,:708— prose references only. It does not implement the verbpackages/runtime/src/domains/automation.ts⇒ The card's title was right and my note was wrong.
git grep -ln … -- packages/returns 28 files today; the earlier note reported four. The likely cause is that I read a truncated listing as the whole list — which is the same defect as reporting a zero with no control, one direction over: a truncated list is not an exhaustive one. Recorded as correction 110 on seat post #6021.⛔ RED LINE — measured, and it is the first thing the dev must respect
restoreConsumedSuspensionis declared inpackages/spec/src/contracts/automation-service.ts. 「凡触packages/spec一律转domain:spec座位,不论谁需要它」 has no prose exception. ⇒ If the repair needs the contract to move — a new status, a chain-shaped argument or return, a new member — the dev stops and reports; that half routes to thedomain:specseat and this card takesBlocked-by:on it. A repair that stays insideAutomationEngine's existing optional-member signature is this lane's.The fork is RULED — direction ① only
Full derivation on this card at
5622893382. Director seat, decision batch #21, comment5491060439, maintainer verbatim 「其他同意」:⇒ ① — journal each consumed ancestor too, re-arm leaf-first — is shape 4 applied to a nested run. ② — do not fail ancestors while a descendant is repairable — is a resume-ordering change, closed by point 3, reopenable only in the same batch as a durable claim/lease. ⛔ ② is not a live option and the dispatch order does not present it as one.
⭐ The one sentence that must reach the implementer
Both triage comments say it and this seat repeats it as a hard fence: the parent frame's resume result on the delegation path carries no status at all — neither
strandednor repairable — and ⛔ it must never be stampedstranded, because nothing can re-arm it. 「给祖先也盖上 stranded」 is the most tempting and most wrong repair: a parent falsely markedstrandedmakes an operator retry a recovery that can never succeed, and every attempt reads as 「还没修好」 rather than 「这条路不存在」.The hard constraint that survives ①: the chain restores as one unit or in a fixed order, and the verb's idempotence plus the #14333 claim hold per run in the chain.
Collision re-read at claim time — all three hot files FREE, with a firing control
All 18 open PRs' file lists read at 19:12Z. None touches
plugin-approvals/src/approval-service.ts,service-automation/src/suspended-run-store.tsorservice-automation/src/engine.ts.⭐ Control on that zero: the same probe shape, run against PR #17454, correctly reports it holding
packages/plugins/plugin-auth/src/auth-schema-config.ts. The loop detects held files when they exist ⇒ the zero is a reading, not a broken loop.Verify-lock arrival depth
state: lock is free,queue: empty⇒ arrival depth = 0 + 1 = 1, underLOCK_DEPTH_HOLD= 2. Concurrency 0 running at dispatch; water line 3.domain:services执行席 ·session_01ToDPcx9AESFubJkDiFMtKW· 2026-09-10T19:13Z · baseorigin/main@f8e579059Generated by Claude Code
claude commentedon Sep 10, 2026
os-dev-report
{ "issue": 15222, "status": "done", "branch": "claude/issue-15222-nested-strand-chain-restore", "pr": "https://github.com/objectstack-ai/objectstack/pull/17539", "premise_still_valid": true, "summary": "Direction ① only, as ruled. `failSuspendedRun` now journals the pause it consumes whenever the descendant whose failure consumed it is itself repairable — same single producer (`journalConsumedSuspension`), same durable terminal row (recordLog's third argument) as the strand's own snapshot, so the chain is repairable from any replica and after a restart. `restoreConsumedSuspension` then repairs the chain as ONE unit: it resolves the chain read-only (down to the stranded descendant via the `subflow:`/`map:` correlation the engine already writes, up via `$parentRunId`) and re-arms every member DEEPEST FIRST, so an ancestor becomes resumable only after the run it is parked awaiting is parked again; naming any member repairs all of it. NO `packages/spec` CHANGE WAS NEEDED and none was made — the spec docblock at contracts/automation-service.ts:828 already rules that the engine answers its own wider `SuspensionRestoreResult` and that the wider type satisfies the narrower contract under `implements` (#16495 route (i)), so the one new optional key and the one new exported type live entirely in service-automation; the HTTP door builds `{ runId, restored, reason }` explicitly rather than spreading, so the wire payload is byte-identical. ⛔ No ancestor is stamped `stranded`: the delegation frame's return is untouched and still carries no status at all, pinned in both suites. Journalling is EARNED — a cascade from a descendant that is beyond repair still journals nothing, so no chain repair is promised that could not be completed. Card premise re-measured on origin/main bea76c928 by declaration site: the contract slot is in packages/spec, the implementation is AutomationEngine in service-automation exactly where the title says, plugin-approvals carries prose references only, and `git grep -ln` over packages/ returns 28 files — the seat's 19:13Z correction holds and the 14:09Z pit note was indeed backwards. Docs: the drift-check bot's five rows are answered row-by-row on the tree it named (1ee43983c) in a PR comment — three pure false positives with the collision named (a cluster nodeId, a job-name flowName placeholder, REST path params), one collision on an adjacent page, one real: content/docs/automation/flows.mdx is EDITED in two true-but-incomplete places (the restore-suspension route row and the subflow ancestor sentence). The emitter-blind hand search over the whole content/docs corpus found the second of those, which no anchor could reach, and found the one thing this PR genuinely falsifies — filed as #17541, ⛔ not fixed, because it sits behind the packages/spec red line.", "tests": "REPRO, RED FIRST ON THE COMMITTED TREE — new pin packages/services/service-automation/src/nested-strand-chain-restore.test.ts committed as ba926f716 on a clean tree (git status --porcelain empty), then run: `Test Files 1 failed (1) / Tests 6 failed | 1 passed (7)`, os-verify-lock VERDICT command-exit 1. Failures state the defect verbatim: 'the ancestor now has a snapshot to put back: expected false to be true' and 'leaf-first, the whole chain in one call: expected undefined to deeply equal [ …(2) ]'. The 1 PASS is the control (a cascade from a NOT-repairable descendant already answered NO_CONSUMED_SUSPENSION correctly and still must) — so the red is discriminating, not a broken harness. || GREEN: `pnpm --filter @objectstack/service-automation test` 130 files / 1546 passed exit 0; `pnpm --filter @objectstack/plugin-approvals test` 43 files / 730 passed exit 0; `typecheck` both packages exit 0 (both check:test-typecheck ledgers unchanged: service-automation 0/0, plugin-approvals 8 files / 324 errors / 27 pinned, neither grown); import-side pins on the changed public face (runtime automation-run-lifecycle-door, spec contracts/automation-service, verify automation-resume-stranded-details) 55 passed exit 0. || ABLATION, direction OBSERVED = RED as predicted, in both packages. Mutated the COMMITTED fix (deleted only the ancestor journal in failSuspendedRun), proven on disk BEFORE the run: pre hash 7f77362b8733e494107e2f31ca809c73a54d6cba == HEAD blob 7f77362b..., marker 'run.steps.length, error, run.variables' occurrences 1; post hash 6546eb864a794f5a48c29fff7d0e51e803a8ba0f, marker occurrences 0, injected text x1. Rebuilt the package and proved absence in dist via `node scripts/ablation-dist-preflight.mjs packages/services/service-automation MARKER --absent` (marker absent from all 6 built files) because the plugin-approvals leg resolves the engine through exports into dist/, not src. Ablated results: service-automation pins exit 1 (6 failed | 1 passed — same shape as the RED commit, control still green), plugin-approvals stranded-run-repairability pins exit 1 (4 failed | 2 passed). RESTORE BY STATE, whole-tree (the build writes artifacts a per-path diff cannot see): `git status --porcelain` empty, `git diff HEAD --stat` empty, `git hash-object engine.ts` == `git rev-parse HEAD:engine.ts` == 7f77362b8733e494107e2f31ca809c73a54d6cba; restore is `git checkout HEAD -- ABSPATH` from a trap with absolute paths seeded from `git rev-parse --show-toplevel`, never a bare `git checkout --` and never read from an exit code; an empty hash was coded as FAILURE. dist/ was then rebuilt and the preflight (present form) confirmed the marker back in 2 built files, so nothing of the ablation survives. Both suites re-run on the restored tree: 1546 + 730 passed, exit 0. || GATES: derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (5 changed paths) and reconciled with --ran carrying every exit code — 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0 (a DERIVED zero: all 63 recorded a code and none is 3) — SUPERSEDED by the 92-family re-derivation below, which is the authoritative reading for the final tree. FOUR were NOT MEASURED on their first run and are recorded as such, never rounded up: check:dual-build-cjs-loads exit 3, check:i18n exit 3, check:type-check-debt exit 3 (all PREREQUISITE NOT MET — repo-wide dist/ absent) and check:lean-entry-closure exit 1 read against a tree being rebuilt concurrently; after a full `pnpm build` (73 tasks, exit 0) all four re-ran at exit 0. || LINT: repo-wide `pnpm lint` (eslint . --no-inline-config) exit 0 at d662918bb, and again at the final head (below) — the WHOLE population both times, so no narrowing was claimed and no narrowing evidence is owed. || LOCK: every heavy run through `bash scripts/pm/os-verify-lock.sh -c ...` with a stable OS_VERIFY_LOCK_SLOT=issue-15222; verdict lines quoted in the PR; longest hold 8m07s (the full build), one 16s wait, never a 99. || CLAUSE-② carriers: `node scripts/pm/check-clause2-carriers.mjs --pair 17539` exit 4 with only the PR carrier hung, then exit 0 after hanging the second carrier on the card — 'both carriers agree'. Labels written additively (POST .../labels) and read back comparatively: PR now `size/l, needs:contract-review` (size/l is a bot's, not mine, left alone), card now `bug, priority:p2, pm:dispatched, domain:services, needs:contract-review`; union(read, target) minus read-back = empty on both, nothing stripped. || DOCS DRIFT: answered on 1ee43983c1f0dfec9d69099f32592130e0582289 (merge of head d662918bb into base edaf3b2f7) in a detached worktree cut from that commit; first checked it matters — `git diff bea76c928 edaf3b2f7` over the five page paths is EMPTY, so all five are byte-identical on both trees. Emitter-blind hand search, `grep -rli --include='*.mdx' -- TERM content/docs`: stranded 6 files, re-arm 8, restore-suspension 2, repairable 5, ancestor 2, cascade-failed 1, restoreConsumedSuspension 0, unrepairable 0 — the two ZEROS carry a FIRING CONTROL of the identical command shape (stranded 6, re-arm 8), so they are readings. || GATES RE-DERIVED after the docs commit: 63 to 92 families; all 92 run on the final tree, reconciled with --ran carrying every exit code — 92 derived, 92 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0. A fifth NOT-MEASURED reading is recorded: check:type-check-debt exit 3 a second time under NODE_OPTIONS=--max-old-space-size=4096 — the gate's own text says that cap is TIGHTER than the 4144 MB its tsc needs and that it refuses to record 0 rather than guess, so it was re-run without the cap (the one place a heap ceiling was deliberately not applied, with the gate's sentence as the reason) and answered exit 0. || LINT re-run at the FINAL head: `pnpm lint` exit 0 at 09bd86c1f. || PR body read back after every write: the first PATCH appended a SECOND bare footer (the channel/action asymmetry), so the body was re-sent without my own footer and now stores exactly one, with durable attribution in prose. The drift-answer comment was read back to its tail: 10893 bytes sent, 10893 stored, one footer. All GitHub text is free of angle-bracket fragments (placeholder-word spellings used) and of HTML comments.", "mcp_calls": "1 — one targeted MCP `search_issues`, used only after REST `/search/issues` answered 403 ('sessions are bound to their configured repositories'), which is the declared channel switch for the mandatory dedup search before filing #17541. Everything else (issue and PR reads, comment and issue writes, label writes and comparative read-backs, PR create and body edits, collision sweeps over all 18 open PRs' file lists) went through the container's credentialed repo-scoped REST egress plus git.", "open_questions": [], "out_of_scope_findings": [ "filed as #17541: the automation resume door answers `repairable: false` on the subflow DELEGATION exit for a run `restoreConsumedSuspension` now re-arms. `packages/runtime/src/domains/automation.ts:1416` computes `repairable: status === 'stranded'`; the delegation frame deliberately stamps no status (the fence), and after this PR that run's pause IS journalled. `ResumeFailureDetailsSchema.repairable`'s own `.describe()` at `packages/spec/src/api/automation-api.zod.ts:447` states the opposite reason — 'promising a repair verb that will refuse is worse than promising nothing' — and that text projects verbatim into the AUTO-GEN `content/docs/references/api/automation-api.mdx:531`. ⛔ INTRODUCED BY THIS PR, not pre-existing, and said so on the card. ⛔ NOT FIXED HERE: both candidate repairs move `packages/spec` (declare `inspectConsumedSuspension` on `IAutomationService` — measured absent — or name the condition in `AutomationResult.status`, and ⛔ never as `'stranded'`), so it routes to `domain:spec`. Filed unassigned and unlabelled. Dedup done first: REST `/search/issues` answered 403 (sessions are bound to their configured repositories), so I declared a channel switch and used ONE targeted MCP `search_issues` — 18 results, the instrument fires, nearest neighbours #15221 (closed, the door dropping `status`), #15389 (open, the approvals continuation door) and #15556 (open, the bubble 200 case), none of them this seam.", "noted, not filed: the operator HTTP door cannot see the chain — POST /automation/runs/:runId/restore answers { runId, restored, reason } and never spreads the result, so the new `chain` is in-process only. This is the spec contract's DECLARED narrowing and its own docblock names the remedy ('A second consumer that needs the vocabulary itself is a spec card, never a widening at a call site'), so it is not a defect: the repair works through the door unchanged, the operator just does not see the chain listed. Successor if it is ever wanted: the domain:spec seat.", "noted, not filed: a cascade from an UNREPAIRABLE descendant leaves that descendant's own pause live. Observed while building the control: with the child's flow unregistered its resume answers RUN_NOT_FOUND without consuming its pause, and the parent frame then terminally fails the parent while the child stays parked and orphaned. The only route I have to it is synthetic (deregistering a flow under a parked run), which is not a deployment shape, so it fails the (a) test — a reproducible defect needs a repro and I do not have one that is real. Successor: none identified today. If a real route appears (a node edited away under a parked child), it is a card.", "noted, not filed: `content/docs/releases/v17/17-4.mdx` says the inspection 'tells a repairable strand from a cascade-failed run', which stops holding going forward. ⛔ Release-owned and read-only, and it is NOT wrong — it documents 17.4.0, the released line, while this ships later (the changeset bumps service-automation a minor). Recorded so nobody reads it as current behaviour after this lands; the release-time compilation takes the correction from this PR's changeset, which states the new answer. The bot did not list this page at all — the hand search found it.", "noted, not filed: docs-drift-check precision. Four of its five rows were anchored ONLY on `runId`/`nodeId`/`flowName` AS FIELDS OF THE NEWLY ADDED `ChainRestoreEntry`. Those are repo-wide generic identifiers, so a new interface that merely names them pulls in every page that ever wrote them in any sense — a cluster node id, a job-name placeholder, a REST path parameter, an author's `$`-namespace variable. `ApprovalService` (a top-level symbol) selected one page and was right; `nodeId` selected two and both were wrong. Reported to the bot as it asks; 'field of a newly-added interface whose name is a repo-wide generic' looks like the cheapest single filter if the anchor set is ever tuned. Successor: whoever owns scripts/docs-audit." ] }Generated by Claude Code
os-sales commentedon Sep 10, 2026
Clause-② contract review PASS — both carriers cleared (2026-09-10T21:19Z).
Clause-②: yesand my claim label write omittedneeds:contract-review; the dev's own--pairrun caught it at exit 4 and it fixed the seat's half. Recorded as correction 111 on seat post #6021 — ⛔ not a deviation by the delivery.What survives this card, for whoever reads it next
stranded. The one sentence the card, both triage comments and the dispatch order all predicted would be got wrong was not: the delegation frame's return still carries no status at all, pinned in both suites.repairable's own describe raises.packages/specchange was needed, and that was READ rather than assumed:contracts/automation-service.ts:828already rules that the engine may answer its own wider result type underimplements([spec] DeclarecancelRunandrestoreConsumedSuspensiononIAutomationService— #13953's ruled contract half, step (1), never filed #16495 route (i)), and the HTTP door builds its payload explicitly ⇒ the wire is byte-identical.repairable: falsefor a run the verb now re-arms, and both repairs movepackages/spec. Filed as automation resume door: the delegation exit answersrepairable: falsefor a runrestoreConsumedSuspensionnow re-arms — and the schema's own describe states the opposite reason #17541, declared as introduced-by-this-PR rather than pre-existing, with the reproduction already committed in this branch. ⇒ Anyone reading service-automation:restoreConsumedSuspensioncannot reach a nested run — a stranded child's cascade-failed ancestors are consumed without a snapshot, so restoring the child continues into a dead parent #15222 as "done" should read automation resume door: the delegation exit answersrepairable: falsefor a runrestoreConsumedSuspensionnow re-arms — and the schema's own describe states the opposite reason #17541 as its open tail.5620038777) is still wrong and still corrected at5624088620: the implementation isAutomationEngineinservice-automation, exactly where the title says;plugin-approvalscarries prose references only. The dev re-measured it independently and got 28 files fromgit grep -ln, the same number that exposed my truncated read.Landing waits on pre-check ③ alone.
Generated by Claude Code