Repository navigation
fix(service-automation)!: a parent-id resume of a map whose item run is still suspended is refused, naming the item run - #27951
Conversation
…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
📓 Docs Drift CheckThis PR changes 3 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 8 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 173 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
…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
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
|
domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL Contract-review-claim: session_016WV4d1Np3hU7RQnA13YZcL · head 082f766 · 2026-10-30T05:29Z |
|
domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL Contract reviewServed-tier: 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. ① Derived judgmentsAccept-set and public-surface changes the diff implies, each judged:
② Semver levelChangeset
③ Boundary flagsDev report 6515519800 (written at 7edf37b) and the PR body (same vintage) — every flag answered or escalated:
④ Design judgmentFace files, each held against the code it cites and the scenarios the card is accepted against:
Findings:
Implemented-by: VERDICT: PASS |
|
domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL 维护者速读
席位意见:ACCEPT。 席位核对Head-sha:
|
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直接跳过那一项:父运行记为完成,那一项根本没跑,它的挂起记录被遗弃。分诊裁定这是静默跳过,必须拒绝。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
mapnode (correlationmap:CHILD), resumed by the parent run id, with the item child parked on a node declaringresumeAuthority: 'any':origin/maine41642d)success: true, no codesuccess: false, codeRESUME_IN_PROGRESS(see Open question), message names the item run id: "Resume run 'ITEM' instead"completedstarted: 1, results: [])listSuspendedRuns(), orphanedMeasured before the fix with a scratch test (not committed):
{"res":{"success":true},"after":["one_item:child"],"parentRow":["completed"]}.What changes
refuseGatedResume,run-resume.ts) walks the linked-run chain once (resolveSuspensionChain, which replacesresolveEffectiveSuspensionand returns every suspension it loaded, the caller's first). Authority is judged first, so an item only its owning service may resume keeps itsPERMISSION_DENIED(403). OtherwiserefuseMapItemSkiprefuses when the chain holds amap: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.resumeInternal, neverresume, so resuming the item run continues the map exactly as before. Asubflowlink is not refused: its signal is delegated to the child.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_CODEinrun-resume.ts(module export, not on the package entry) is the one spelling of the code; the refusal and its pins read it.LINKED_RUN_PREFIXES,resume,refuseGatedResumeand the chain walk now state the refusal. Two comments indegraded-suspended-run-load-log-cause.test.tsfollow the rename.@objectstack/service-automationpatch, BREAKING accept-set narrowing, ADR-0087not-required (no-migration-prescription).Open question (asked on the card, not decided here)
No existing code fits a coded 409.
AutomationResult.codeis a closedpackages/specunion, and the runtime door'sRESUME_REFUSAL_ROWSmaps exactly one resume code to 409:RESUME_IN_PROGRESS, documented as a benign concurrent duplicate. The order forbids apackages/specedit, so the code is a placeholder in one constant while the seat's route question (card comment 6514753022, option B: a new code acrosspackages/spec+packages/runtime) is open. If it is ruled B, the constant, the changeset'sNow.line and this table's code cell change; nothing else does.Tests
src/map-parent-resume-refusal.test.ts(new, 5):completedrow.completed, nothing left suspended).subflowparent over a parkedmap: refused at the top id, naming the map's item run.subflowparent-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'smap:link has nothing behind it and theINVALID_SIGNALguard is still the answer they pin.Ablation through scripts/ablation-replace.mjs (fix committed first)
The item-door and
subflow-unchanged pins stay green under the ablation, as preservation pins should.Verification
@objectstack/service-automationon 7edf37b: vitest 219 files / 2808 tests green;tsc --noEmitandcheck:test-typecheckgreen.dispatch-gates.mjs --commandson 7edf37b: 62 derived; 61 exit 0 (check-plugin-teardown-shape --self-testafter fetching its pinned commit), 1 NOT MEASURED (check:dual-build-cjs-loads, exit 3, needs every package's dist);--ranreconcile: 62 accounted, 0 unrun.pnpm lintexit 0.Acceptance notes
errortext;AutomationResulthas no structured member for it, and adding one is a spec change.🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4cxEzAw9N6UiWHC6Zp2