Skip to content

fix(service-automation)!: a parent-id resume of a map whose item run is still suspended is refused, naming the item run - #27951

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-n14t8u
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-n14t8u

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

domain:server#4 · os-elon-musk · session_016WV4cxEzAw9N6UiWHC6Zp2

Fixes #27877

Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no authorable metadata changes; a resume the engine accepted is refused

维护者速读

  • 改了什么:批量流程停在 map 节点、某一项的子运行还挂起时,用父运行 id 续跑现在被拒绝(带错误码),提示改续跑那一项的子运行 id,什么都不推进。
  • 为什么改:以前这样续跑会让 map 直接跳过那一项:父运行记为完成,那一项根本没跑,它的挂起记录被遗弃。分诊裁定这是静默跳过,必须拒绝。
  • 风险与回滚:收窄(BREAKING):以前答成功的父 id 续跑现在被拒。续跑子运行、subflow 委派、服务专属节点的 403 都不变。拒绝码暂用 RESUME_IN_PROGRESS 占位,是否换新码等分诊答复(卡评论 6514753022)。回滚即还原这一个 commit。
  • 先看:src/run-resume.ts(refuseGatedResume 与新的 refuseMapItemSkip)、src/engine.ts(resolveSuspensionChain 与 LINKED_RUN_PREFIXES 文档块)、src/map-parent-resume-refusal.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/service-automation exec vitest run src/map-parent-resume-refusal.test.ts src/resume-authority-gate.test.ts,预期 27 个测试全绿。
  • 需要你做的:无,席位落地(先等分诊定拒绝码)。

Measured at the in-process door (engine.resume)

A run paused at a map node (correlation map:CHILD), resumed by the parent run id, with the item child parked on a node declaring resumeAuthority: 'any':

Before (origin/main e41642d) After (7edf37b)
answer success: true, no code success: false, code RESUME_IN_PROGRESS (see Open question), message names the item run id: "Resume run 'ITEM' instead"
parent run row completed no terminal row; parent still suspended
map progress moved past the item unchanged (started: 1, results: [])
item child still in listSuspendedRuns(), orphaned still suspended and resumable

Measured before the fix with a scratch test (not committed): {"res":{"success":true},"after":["one_item:child"],"parentRow":["completed"]}.

What changes

  • The resume gate (refuseGatedResume, run-resume.ts) walks the linked-run chain once (resolveSuspensionChain, which replaces resolveEffectiveSuspension and returns every suspension it loaded, the caller's first). Authority is judged first, so an item only its owning service may resume keeps its PERMISSION_DENIED (403). Otherwise refuseMapItemSkip refuses when the chain holds a map: link whose item child is still suspended, naming the item run of the DEEPEST such link, before any state is touched. It refuses whatever the signal carries: a skip is not a question of authority.
  • The engine's own continuations are untouched. The item's up-bubble goes through resumeInternal, never resume, so resuming the item run continues the map exactly as before. A subflow link is not refused: its signal is delegated to the child.
  • A map: link whose item row is gone (cancelled, or never persisted) is not refused; there is nothing parked to skip. That shape is left as it was.
  • MAP_ITEM_PENDING_REFUSAL_CODE in run-resume.ts (module export, not on the package entry) is the one spelling of the code; the refusal and its pins read it.
  • Docblocks: LINKED_RUN_PREFIXES, resume, refuseGatedResume and the chain walk now state the refusal. Two comments in degraded-suspended-run-load-log-cause.test.ts follow the rename.
  • Changeset: @objectstack/service-automation patch, BREAKING accept-set narrowing, ADR-0087 not-required (no-migration-prescription).

Open question (asked on the card, not decided here)

No existing code fits a coded 409. AutomationResult.code is a closed packages/spec union, and the runtime door's RESUME_REFUSAL_ROWS maps exactly one resume code to 409: RESUME_IN_PROGRESS, documented as a benign concurrent duplicate. The order forbids a packages/spec edit, so the code is a placeholder in one constant while the seat's route question (card comment 6514753022, option B: a new code across packages/spec + packages/runtime) is open. If it is ruled B, the constant, the changeset's Now. line and this table's code cell change; nothing else does.

Tests

src/map-parent-resume-refusal.test.ts (new, 5):

  1. Parent-id resume refused with the code and the item run id; the parent, the map state and the item are unchanged, nothing ran, no completed row.
  2. Refused even with the owning-service stamp on the signal.
  3. Resuming the named item run works and the map continues to completion (two items, parent completed, nothing left suspended).
  4. A subflow parent over a parked map: refused at the top id, naming the map's item run.
  5. A subflow parent-id resume is unchanged (delegated, completes).

src/resume-authority-gate.test.ts (changed, 3): "leaves a map over ungated items resumable" pinned the narrowed behaviour and now asserts the refusal. The two map-handoff forgery cases cancel the item first, so the parent's map: link has nothing behind it and the INVALID_SIGNAL guard is still the answer they pin.

Ablation through scripts/ablation-replace.mjs (fix committed first)
ABLATE run-resume.ts  anchor "    for (let i = chain.length - 2; i >= 0; i--) {"  x1 -> x0, blob 067d1647 -> 8cef8f91
× refuses with a code naming the item run, and nothing advances
× is refused whatever the signal carries — a skip is not a question of authority
× follows a subflow link down to a map parked on its item, and names that item
× refuses the MAP PARENT over an ungated item too — the item is the door (#27877)
Tests  4 failed | 23 passed (27)
ok restored: blob == HEAD (067d1647d140) and `git diff HEAD` is empty

The item-door and subflow-unchanged pins stay green under the ablation, as preservation pins should.

Verification

  • @objectstack/service-automation on 7edf37b: vitest 219 files / 2808 tests green; tsc --noEmit and check:test-typecheck green.
  • dispatch-gates.mjs --commands on 7edf37b: 62 derived; 61 exit 0 (check-plugin-teardown-shape --self-test after fetching its pinned commit), 1 NOT MEASURED (check:dual-build-cjs-loads, exit 3, needs every package's dist); --ran reconcile: 62 accounted, 0 unrun. pnpm lint exit 0.
  • Full dogfood battery: owed on the final head (the narrowing arm); reported on the card once it has run.

Acceptance notes

  • The refusal message names the run id only in error text; AutomationResult has no structured member for it, and adding one is a spec change.

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cxEzAw9N6UiWHC6Zp2

…em run is still suspended

A run parked on a `map` node, resumed by its own id, re-ran the map and
advanced past the in-flight item: success, the parent completed, and the
item child was left suspended. The resume gate now walks the linked-run
chain once and, after the authority check, refuses a resume that reaches a
`map:` link whose item child is still suspended, naming that child run id.
Nothing is consumed. The engine's own up-bubble goes through
resumeInternal and is unchanged, as is subflow delegation.

The refusal code lives in one constant, MAP_ITEM_PENDING_REFUSAL_CODE.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxEzAw9N6UiWHC6Zp2
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 30, 2026
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/runtime, @objectstack/service-automation, @objectstack/spec, touching 27 documentable anchor(s).

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via AutomationResult (symbol, a top-level interface), ERROR_CODE_LEDGER (symbol, a top-level const object), FLOW_DISABLED (literal, a string literal in AutomationResult), FLOW_INPUT_SCHEMA_INVALID (literal, a string literal in AutomationResult), FLOW_NO_START_NODE (literal, a string literal in AutomationResult))
  • content/docs/api/declarative-endpoints.mdx (via FLOW_DISABLED (literal, a string literal in AutomationResult), FLOW_INPUT_SCHEMA_INVALID (literal, a string literal in AutomationResult), FLOW_NO_START_NODE (literal, a string literal in AutomationResult), /api/v1/automation/:name/trigger (route, bridged from symbol TriggerFlowResponseSchema — its route source's handler names it))
  • content/docs/api/error-catalog.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/api/error-handling-server.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/api/plugin-endpoints.mdx (via FLOW_DISABLED (literal, a string literal in AutomationResult), FLOW_INPUT_SCHEMA_INVALID (literal, a string literal in AutomationResult), FLOW_NO_START_NODE (literal, a string literal in AutomationResult))
  • content/docs/automation/flows.mdx (via AutomationResult (symbol, a top-level interface), FLOW_DISABLED (literal, a string literal in AutomationResult), FLOW_INPUT_SCHEMA_INVALID (literal, a string literal in AutomationResult), FLOW_NO_START_NODE (literal, a string literal in AutomationResult), INVALID_SCREEN_INPUT (literal, a string literal in AutomationResult), /api/v1/automation/:name/trigger (route, bridged from symbol TriggerFlowResponseSchema — its route source's handler names it))
  • content/docs/kernel/contracts/data-engine.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/permissions/system-context.mdx (via handleAutomationRequest (symbol, a top-level function))
  • content/docs/protocol/kernel/http-protocol.mdx (via FLOW_DISABLED (literal, a string literal in AutomationResult), FLOW_INPUT_SCHEMA_INVALID (literal, a string literal in AutomationResult), FLOW_NO_START_NODE (literal, a string literal in AutomationResult), /api/v1/automation/:name/trigger (route, bridged from symbol TriggerFlowResponseSchema — its route source's handler names it))
  • content/docs/ui/actions.mdx (via FLOW_DISABLED (literal, a string literal in AutomationResult), FLOW_INPUT_SCHEMA_INVALID (literal, a string literal in AutomationResult), FLOW_NO_START_NODE (literal, a string literal in AutomationResult))

⛔ 8 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via AutomationEngine (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via AutomationEngine (symbol, a top-level class), ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/releases/v17/17-1.mdx (via AutomationResult (symbol, a top-level interface), ERROR_CODE_LEDGER (symbol, a top-level const object), FLOW_DISABLED (literal, a string literal in AutomationResult), FLOW_NO_START_NODE (literal, a string literal in AutomationResult))
  • content/docs/releases/v17/17-3.mdx (via AutomationResult (symbol, a top-level interface), FLOW_INPUT_SCHEMA_INVALID (literal, a string literal in AutomationResult))
  • content/docs/releases/v17/17-4.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/releases/v17/17-6.mdx (via AutomationEngine (symbol, a top-level class), AutomationResult (symbol, a top-level interface), FLOW_DISABLED (literal, a string literal in AutomationResult))
  • content/docs/releases/v17/17-7.mdx (via INVALID_SCREEN_INPUT (literal, a string literal in AutomationResult), INVALID_SIGNAL (literal, a string literal in AutomationResult), RESUME_IN_PROGRESS (literal, a string literal in AutomationResult; a string literal in a comment in AutomationResult), RUN_NOT_FOUND (literal, a string literal in AutomationResult), STORE_UNAVAILABLE (literal, a string literal in AutomationResult))
  • content/docs/releases/v18/18-2.mdx (via INVALID_SCREEN_INPUT (literal, a string literal in AutomationResult))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: PERMISSION_DENIED (literal, 50 pages)
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 173 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bacb3e6a0ff6e8bfdeade49d6830e34fb5522589 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5f485a6e345ca070986119353bd098132324ac1d — the merge of head 082f766bf69f702411796717955551f9db58aeb3 into base bacb3e6a0ff6e8bfdeade49d6830e34fb5522589, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5f485a6e345ca070986119353bd098132324ac1d && git checkout 5f485a6e345ca070986119353bd098132324ac1d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bacb3e6a0ff6e8bfdeade49d6830e34fb5522589 082f766bf69f702411796717955551f9db58aeb3 && git checkout -B drift-repro bacb3e6a0ff6e8bfdeade49d6830e34fb5522589 && git merge --no-ff 082f766bf69f702411796717955551f9db58aeb3

node scripts/docs-audit/affected-docs.mjs --json bacb3e6a0ff6e8bfdeade49d6830e34fb5522589

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs bacb3e6a0ff6e8bfdeade49d6830e34fb5522589 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…ode of the map parent-id resume refusal

The refusal answered the RESUME_IN_PROGRESS placeholder, which tells a
caller the work is already being done. It now answers its own code:
a new member of the AutomationResult.code union, the
TriggerFlowResponseSchema code enum and the ADR-0112 ledger row under
service-automation, served 409 by the runtime resume door's
RESUME_REFUSAL_ROWS on REST and MCP alike.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxEzAw9N6UiWHC6Zp2
…orm the dispatcher vocabulary guard resolves

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxEzAw9N6UiWHC6Zp2
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract-review-claim: session_016WV4d1Np3hU7RQnA13YZcL · head 082f766 · 2026-10-30T05:29Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 082f766bf69f702411796717955551f9db58aeb3
Local-runs: none
Scope: packages/spec/src/api/automation-api.zod.ts, packages/spec/src/api/error-code-ledger.zod.ts, packages/spec/src/contracts/automation-service.ts, packages/runtime/src/domains/automation.ts, packages/services/service-automation/src/engine.ts, packages/services/service-automation/src/run-resume.ts + packages/services/service-automation/src/builtin/map-node.ts, packages/services/service-automation/src/builtin/wait-node.ts, packages/runtime/src/domains/mcp.ts, packages/plugins/plugin-approvals/src/approval-service.ts, packages/spec/src/contracts/approval-service.ts, scripts/check-dispatcher-error-vocabulary.mjs, scripts/check-changeset-no-major.mjs, .changeset/27877-map-parent-resume-refusal.md, packages/services/service-automation/src/map-parent-resume-refusal.test.ts, packages/services/service-automation/src/resume-authority-gate.test.ts, packages/runtime/src/domains/automation-resume-refusal-codes.test.ts, packages/runtime/src/mcp-resume-run.test.ts
Thread-read: 6515324504
Reviewed at: 2026-10-30T05:40Z · card #27877 (triage 6513867475, route question 6514753022, answer 6515324504 = B, claim 6514635285, dev report 6515519800), PR #27951 body + file list + net diff against main at this head, and the head's check-runs. Read-only: nothing built, run or re-run; the record template was rendered with record-recognisers.mjs --template --paths.

Check-runs on this head at posting: 28 success, 9 skipped, 0 failure; 11 still in progress — Test Core (1/8–8/8), Type Check · workspace, Type Check UI, Build & E2E UI. Build Core, Live E2E UI and every Lint job (ESLint, repo & tooling gates incl. check:dispatcher-error-vocabulary and check:adr-0087-registration, docs & code guards, CI/release/engine gates, PM dispatch-gates, scoped lanes), Check Changeset, PR-body guards, Spec property liveness, Governed Surface Queue Guard, Dogfood Regression Gate, Check PR Size, Check Documentation Links are success. Pending is pending: this PASS stands on the tree; the seat lands only on all-green.

① Derived judgments

Accept-set and public-surface changes the diff implies, each judged:

  1. AutomationResult.code gains 'RESUME_CHILD_PENDING' (contracts/automation-service.ts, union + bullet) — right. Triage 6515324504 ruled B and named this spelling (name is the maintainer's at approval). The bullet states the 409 mapping, that nothing moves, that error names the child run id, and that it is deliberately distinct from RESUME_IN_PROGRESS (benign vs. "resume a different id") — matches the ruling's reason for B.
  2. TriggerFlowResponseSchema.data.code enum gains the same member (automation-api.zod.ts) — right; the file's own docblock requires the mirror, and automation-api.zod.test.ts binds TriggerFlowResponse['data'] to AutomationResult at compile time, so the pair cannot drift.
  3. ERROR_CODE_LEDGER['@objectstack/service-automation'] gains one row, alphabetically before RESUME_IN_PROGRESS — right. The emitter is run-resume.ts (this package), so the row is identity, not provenance; the row comment follows its neighbours' [#NNNN] convention (ledger comments are not runtime strings).
  4. RESUME_REFUSAL_ROWS gains ['RESUME_CHILD_PENDING', 409] (runtime/domains/automation.ts) — right. ONE table serves both the REST resume arm and the MCP resume_run tool via classifyResumeResult, so both doors answer 409 with details.code promoted verbatim; the arm's status comment and the "seven coded rows" count are updated; automation-resume-refusal-codes.test.ts floor pins the seventh key and its parse as an ErrorCode; mcp-resume-run.test.ts adds the row. Correctly NOT added: RESTORE_REFUSAL_STATUS (the restore door; its SuspensionRestoreRefusal union is unchanged) and RETRYABLE_RESUME_REFUSAL_CODES (see ④).
  5. AutomationEngine.resume(runId) accept set narrowed: a resume addressed to a run whose linked-run chain holds a map: link with a still-suspended item child answers { success:false, code:'RESUME_CHILD_PENDING', error: "...Resume run 'ITEM' instead..." } before any state is touched — right, this is the triage ruling verbatim (refused, coded 409, names the child, nothing advances, orphaned suspension no longer created).
  6. Private rename resolveEffectiveSuspension → resolveSuspensionChain (returns the loaded chain, caller first) — no public surface: RunResumeHost and MAP_ITEM_PENDING_REFUSAL_CODE are module exports of run-resume.ts that src/index.ts does not re-export, so nothing on the package entry or check:api-surface moves.
  7. LINKED_RUN_PREFIXES docblock now states the refusal instead of describing the advance as accepted — right; triage asked for exactly this update.
  8. content/docs/references/api/*.mdx — generated output (+353 enum members), not reviewed; consistent with the enum/ledger change.

② Semver level

Changeset .changeset/27877-map-parent-resume-refusal.md: @objectstack/service-automation, @objectstack/spec, @objectstack/runtime all patch; Clause-②: yes (narrowing); Metadata-protocol: none; BREAKING banner; one ADR-0087 marker not-required (no-migration-prescription) with its why.

  • Level — right. AGENTS.md step 3 / ruling A (check-changeset-no-major.mjs): the level follows the Metadata-protocol kind alone; a new member of a RESPONSE code enum is nothing a metadata author writes, so none is the true kind and none takes patch on every introduced entry. The spec "widening" of the wire vocabulary does not lift the level under ruling A, and check:changeset-no-major's Check Changeset job is green on this head.
  • Clause-②: yes — right, per triage 6515324504 (「协议新增定 p1」; the seat's earlier no (narrowing) does not fit a new enum value).
  • Arm (narrowing) — right. Both directions occur (a previously accepted call is now refused; a response enum gains a member), the closed pair allows at most one arm, and the one that carries the BREAKING signal the ADR-0087 gate and the queue guard read is (narrowing). none beside (narrowing) is the "non-protocol narrowing ships patch and keeps its BREAKING banner" lane the gate names explicitly.
  • ADR-0087 disposition — right. No authorable key added, removed, renamed or re-shaped; a refusal before any state is touched leaves no stored shape to migrate; no-migration-prescription is a member of CATEGORIES; check:adr-0087-registration (Lint · CI, release & engine gates) is green on this head.
  • Migration prose — present: Before / Now / Unchanged / New code / Who sees it / The remedy (resume the named item run).

③ Boundary flags

Dev report 6515519800 (written at 7edf37b) and the PR body (same vintage) — every flag answered or escalated:

  1. open_questions[0] (which code) — ANSWERED by triage 6515324504: B. The tree implements B in full (union, enum, ledger, runtime row, constant MAP_ITEM_PENDING_REFUSAL_CODE = 'RESUME_CHILD_PENDING', changeset Now. line). Closed.
  2. Deviation "placeholder RESUME_IN_PROGRESS" — superseded by B; no placeholder remains in the tree (git grep RESUME_CHILD_PENDING finds every stamp, pin, row and doc; no RESUME_IN_PROGRESS is constructed for this refusal).
  3. Deviation "scratch test src/tmp-measure-27877.test.ts deleted before commit" — confirmed absent from the file list and the tree.
  4. Deviation "line_budget +327/-35 across 6 files" — the head is +361/-40 across 15 files after B (spec ×3, runtime ×3, docs ×3, changeset); within reason for a protocol addition the ruling ordered.
  5. out_of_scope_findings[0]: a map: link whose item run is GONE (cancelled via cancelRun, which does not touch the parent) still lets a parent-id resume advance past that item — "noted, not filed". ESCALATED to the lane seat as a card to file: it is reproducible and measured (the two forgery pins in resume-authority-gate.test.ts now construct exactly that state), and PD chore: version packages #10 files a reproducible defect rather than noting it. Not this PR's: triage's ruling covers the still-suspended item only, and refusing the gone-item shape would need its own ruling (the map re-entry pushes $mapItemOutput ?? null for it). Non-blocking.
  6. Acceptance note: the child run id is carried only in error text; AutomationResult has no structured member — ACCEPTED as declared. The ruling says the 409 "names the child run id", and the message does; classifyResumeResult serves details.code only. A structured details.childRunId would be a spec addition under its own card (the dev did not sneak it in — right).
  7. Dogfood battery — reported green on 7edf37b (357 files / 2875 tests), not on this head; the head differs by the spec/runtime/docs commits and the constant's spelling. The head's Dogfood Regression Gate check is success. Whether the lane owes a battery re-run on the final head is the seat's call per the claim's own Container & model line; not a contract finding.
  8. PR body stale lines (written before triage's answer; the relay cannot re-edit) — the seat's ACCEPT must carry these corrections, judged against the tree:
    • Clause-②: no (narrowing) → Clause-②: yes (narrowing) (the changeset already says yes; ruling A grades on Metadata-protocol: none, so yes + patch passes Check Changeset — see ②).
    • 维护者速读 · 风险与回滚: 「拒绝码暂用 RESUME_IN_PROGRESS 占位,是否换新码等分诊答复」→ the code is RESUME_CHILD_PENDING, ruled B in 6515324504; 「需要你做的」: the maintainer's one act is to confirm the code's NAME at approval.
    • Measured table, After column: code RESUME_IN_PROGRESS (see Open question) → RESUME_CHILD_PENDING.
    • "## Open question" section → resolved (B); the surface widened to packages/spec (union, enum, ledger) and packages/runtime/src/domains/automation.ts (one 409 row), granted in 6515324504.
    • "What changes · Changeset": @objectstack/service-automation patch → three packages patch (spec, runtime, service-automation), Clause-②: yes (narrowing).
    • "Verification" is on 7edf37b; the head is 082f766 (spec/runtime/docs + the constant spelled as an annotated const so check:dispatcher-error-vocabulary's declaredInitializers resolves it — the satisfies form is not a reducible initializer).
    • "Measured" table and "Tests" sections are otherwise accurate against the tree.

④ Design judgment

Face files, each held against the code it cites and the scenarios the card is accepted against:

packages/spec/src/contracts/automation-service.ts — the new bullet's claims each hold in the tree: 409 (runtime row 4 above); "directly, or through the run a subflow node waits on" (the chain walk follows subflow: then map:; test 4 pins it); "error names the child run id" (refuseMapItemSkip message); "the map continues when that child completes" (up-bubble via resumeInternal; test 3). Union order places it beside RESUME_IN_PROGRESS. Holds.

packages/spec/src/api/automation-api.zod.ts — mirror of the union; compile-time pin exists. Holds.

packages/spec/src/api/error-code-ledger.zod.ts — row under the emitting package, alphabetical, 409 noted. check:error-code-provenance / check:dispatcher-error-vocabulary: the stamp site is code: MAP_ITEM_PENDING_REFUSAL_CODE (the objlitconst shape), resolved through resolveConstant → declaredInitializers, whose regex is const NAME(: Type)? = ...; — the head's annotated-const spelling is exactly that form, and the Lint · repo & tooling gates job is green. Holds.

packages/runtime/src/domains/automation.ts — one row, one shared table, two doors agree; fallback text carries no tracker number (check:doc-authoring). MCP reachability note: a map parent's suspension row carries no screen (the map suspend arm sets none), so resumeActionRun step 5 refuses 409 before resume is called; the shared row still makes the doors agree if a surfaced screen ever reaches it. Holds.

packages/services/service-automation/src/engine.ts — resolveSuspensionChain returns the loaded chain; a gone child row ends the chain at the map: link (so scenario 4 is NOT refused — a finished/failed/cancelled item leaves nothing behind the link and refuseMapItemSkip requires a successor); the depth-bound exit pushes the last loaded run. Docblocks on resume, refuseGatedResume, LINKED_RUN_PREFIXES state the ruling. Holds — with one should-fix below.

packages/services/service-automation/src/run-resume.ts — the gate's new order: resolve chain → judge AUTHORITY on the deepest run first (a service-owned item still answers 403 at every id, so a caller is never sent to a door that also refuses; the service stamp is still the recorded decision's tail) → otherwise refuseMapItemSkip, which refuses on the DEEPEST map: link with a successor, whatever the signal carries. Break attempts, each against the tree:

  1. Parent-id resume of a map whose item is still suspended → chain [parent(map:item), item], item authority any → refuseMapItemSkip i=0 → RESUME_CHILD_PENDING naming item.runId; returned from refuseGatedResume before resumeInternal, so no forgetSuspendedRun, $mapState stays {started:1, results:[]}, item stays in listSuspendedRuns(), no completed row. Test 1 pins every one of those; test 2 pins the service-stamped variant. Holds.
  2. Resuming the child id → chain [item], no map: link → gate null → resumeInternal(item) → up-bubble resumeInternal(parent) re-runs the map (not gated) → parks on item 2 → second child resume completes the parent. Test 3. Holds.
  3. subflow: parent-id resume → chain [parent(subflow:child), child], no map: link → delegated as before. Test 5. A subflow over a parked map → chain [outer, batch(map:item), item] → refused at the outer id naming item, message says "waiting on run 'batch'". Test 4. Holds.
  4. Map whose item already finished/failed/cancelled → child row gone → chain [parent] → no successor → null → resumeInternal. The two forgery pins in resume-authority-gate.test.ts cancel the item first and still reach INVALID_SIGNAL. Holds (and is the shape of ③.5).
  5. Consistency and consumers — union, enum, ledger, runtime row (409), MCP (shared table) agree. Deliberately NOT in RETRYABLE_RESUME_REFUSAL_CODES: that set is what resumeInternal answers to a subflow parent delegating down (run-resume.ts ~L1218) and the delegation calls resumeInternal, which never runs the gate, so the code can never arrive there — adding it would be dead. Approvals: serviceResume resumes the approval's OWN run id (a leaf; its correlation is never map:), so the refusal is unreachable from approvals; if it ever arrived, resumeCodeOf(err) === 'RESUME_IN_PROGRESS' is the only tolerated arm and everything else falls through to the loud [approvals] resume failed log with repairable: false — safe fall-through, not a silent skip. Wait-node timer wake / boot re-arm resume the wait run's own id (a leaf; the waitUntil variable is keyed on the parked node, so a map parent never qualifies) — unreachable; its STORE_UNAVAILABLE-only keep-armed arm is unaffected. ResumeFailureReport.code (approvals contract) is ErrorCode, which the ledger row makes the new code a member of. Holds.
  6. Runtime strings carry no tracker number (the #27877 references are in comments and the ledger row comment only). Holds.

Findings:

  • should-fix (non-blocking) — engine.ts resolveSuspensionChain: seen is seeded with child ids only, never the root runId, so a corrupt correlation cycle A→B→A yields the chain [A, B, A] and refuseMapItemSkip refuses naming the caller's own run id as "the run to resume instead". Pre-existing shape of the walk (the old form also re-loaded A), and refusing is the safe direction for corrupt data, but seen.add(runId) before the loop makes the chain honest. Fix in this PR or the lane's next touch of the file.
  • should-fix (outside the face; lane seat's) — the intact-suspension code lists in packages/client/src/index.ts (automation.resume docblock, ~L5947) and packages/services/service-automation/README.md (~L310) still enumerate INVALID_SIGNAL / INVALID_SCREEN_INPUT (400), RESUME_IN_PROGRESS (409), STORE_UNAVAILABLE (503) without the new 409; and docs/qa/platform-checklist/areas/automation.json (acceptance clause "every RESUME refusal answers its own status and code … keep the run paused", ~L1693, and the arm list ~L1730) has no row for 409 RESUME_CHILD_PENDING — the claim's own "automation.json rows if its gate asks" item. No gate on this head asked (Dogfood Regression Gate is green), so these are prose/QA completeness, not contract: a docs-only follow-up or the seat's next QA sweep.
  • No must-fix.

Implemented-by: claude/project-thread-n14t8u
Reviewed-by: session_016WV4d1Np3hU7RQnA13YZcL

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

维护者速读

  • 改了什么:父运行 id 恢复一个 map,而其条目子运行仍在挂起时,现在拒绝为 409 RESUME_CHILD_PENDING,错误里点名该去恢复的条目运行;什么都不推进。新码进入 AutomationResult.code 联合、TriggerFlowResponseSchema 枚举、ADR-0112 账本,REST 与 MCP 两个恢复门共用一行 409。
  • 为什么改:此前这种恢复回答成功、把父运行记为完成、条目子运行留在挂起列表里(service-automation: resuming a run paused at a map node by the PARENT run id completes the parent and leaves the parked item child suspended — accepted shape or a skipped item? #27877,分诊 6515324504 裁 B)。
  • 风险与回滚:Clause-②: yes (narrowing),三个包 patch(spec、runtime、service-automation);只拒绝原本会静默跳过条目的恢复,恢复子运行 id、subflow 委派、上冒泡都不变。回滚即还原本 PR。
  • 先看:run-resume.ts 的 refuseGatedResume / refuseMapItemSkip,engine.ts 的 resolveSuspensionChain。
  • 两分钟验证:pnpm --filter @objectstack/service-automation test -t "map parent resume"(map-parent-resume-refusal.test.ts 5 例);消融后 4 例拒绝断言变红。
  • 需要你做的:确认新码的名字 RESUME_CHILD_PENDING(分诊写明名字由维护者在批准时定)。确认前席位不入队。

席位意见:ACCEPT。

席位核对

Head-sha: 082f766bf69f702411796717955551f9db58aeb3

  • PR body corrections (the relay cannot re-edit the body; these lines supersede it):
    • Clause-②: no (narrowing) → Clause-②: yes (narrowing) (triage 6515324504; the changeset already says yes). Metadata-protocol: none unchanged.
    • 维护者速读 · 风险与回滚 and the Measured table's After cell: the code is RESUME_CHILD_PENDING (409 on both doors), not the RESUME_IN_PROGRESS placeholder.
    • ## Open question: resolved, B; the surface widened to packages/spec (union, enum, ledger) and packages/runtime/src/domains/automation.ts (one 409 row), as granted.
    • What changes · Changeset: three packages patch (@objectstack/service-automation, @objectstack/spec, @objectstack/runtime).
    • Verification: superseded by the final dev report 6516848723 on this head (the body's runs were on 7edf37b).
  • Shape: see the contract review's derived judgments ①1–8; nothing moves on a package entry beyond the union/enum member and the ledger row. check-governed-merges --pr 27951: NOT governed, 401 changed lines (3 generated reference pages).
  • Premise measured: on base e41642d a parent-id resume of a map parked on an open item answered success, recorded the parent completed and left the item child suspended. On this head: build exit 0; service-automation 2808 tests; runtime 7840 passed / 19 skipped; dogfood battery 2875 passed / 20 skipped, 0 failures; dispatch-gates 110 derived, 110 run, 0 not measured, 0 unrun; pnpm lint exit 0 (dev report 6516848723).
  • Gate check-runs on the head: 50 runs, 41 success, 9 skipped, 0 failure/pending; mergeable_state clean.
  • Contract review: PASS, record 6515887943 on this head, no must-fix. Should-fix 1 (resolveSuspensionChain never seeds seen with the root run id, so a corrupt A→B→A correlation cycle refuses naming the caller's own run) is not on this head: pre-existing walk shape, refuses in the safe direction; it rides service-automation: a parent-id resume of a map whose parked item run was cancelled still advances past that item #27971, which touches the same walk. Should-fix 2 (client/README code lists and the QA checklist row for the new 409) rides service-automation: a parent-id resume of a map whose parked item run was cancelled still advances past that item #27971 as filed.
  • Accepted deviations: the surface widened to packages/spec + one runtime row by triage grant and claim amendment before the edit; the code constant is a type-annotated const so check:dispatcher-error-vocabulary resolves it.
  • Landing: Tier S under the maintainer's landing grant for this seat term (seat 4 thread, 2026-10-29 22:04Z), held until the maintainer confirms the code name; then pr_ready + automerge_enable through the relay.

This was referenced Oct 30, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 11:22
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 11:22
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 4793aa4 Oct 30, 2026
51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-n14t8u branch October 30, 2026 11:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant