Repository navigation
fix(service-automation)!: a map whose item run was cancelled records the item as cancelled on a parent-id resume - #28176
Conversation
…med run no longer names that run as its own child resolveSuspensionChain now seeds its seen-set with the caller's run id, so an A -> B -> A correlation cycle ends the chain at B and the map refusal names the item run, not the run the caller is already resuming. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4d1oZadHHFdEZXANxiG
…her resume refusals The client resume docblock, the service README's code list and the automation checklist's trigger-status item now name the map-item refusal the resume door answers since the parent-id gate landed. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4d1oZadHHFdEZXANxiG
…e-automation Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4d1oZadHHFdEZXANxiG
…the item as cancelled on a parent-id resume
The resume gate reads the item run's history when the map is parked on an
item whose suspension row is gone. A cancelled item lets the map go on with
the item's slot holding { $status: 'cancelled', $itemRunId }, outside count;
any other history (none, unreadable, not cancelled) is refused with
RESUME_CHILD_PENDING before anything moves.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4d1oZadHHFdEZXANxiG
…marker MapConfigSchema.outputVariable's description and the flows guide's batch paragraph say a cancelled item keeps its slot as the closed $-keyed marker, outside count; the generated reference page is regenerated. Changeset for the map fix. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4d1oZadHHFdEZXANxiG
…ed-item handoff The cancelled-item continuation and its refusal apply when the run is parked at a `map` node; another executor that mints a `map:` correlation keeps its resume unchanged. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4d1oZadHHFdEZXANxiG
…item change Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4d1oZadHHFdEZXANxiG
📓 Docs Drift CheckThis PR changes 3 package(s): 7 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 0f967d22d50ced459f455516495a246b94697a21 && git checkout 0f967d22d50ced459f455516495a246b94697a21
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c48cd2c103a9dfa99313a67a3ec622291d793900 8ec36611b9ba9005b191cb11e3378a636a57a53e && git checkout -B drift-repro c48cd2c103a9dfa99313a67a3ec622291d793900 && git merge --no-ff 8ec36611b9ba9005b191cb11e3378a636a57a53e
node scripts/docs-audit/affected-docs.mjs --json c48cd2c103a9dfa99313a67a3ec622291d793900
|
|
domain:server#1 · os-elon-musk · session_016WV4czVnUdQQCVKSW5J2uk Contract reviewServed-tier: Reviewed 2026-10-30T15:38Z · PR #28176 · card #27971 · ruling 6523833308 (letter A) · base Check-runs on the head at this reading — completed: Auto Label, Dogfood Regression Gate (its three matrix shards skipped), Plan Test Core shards, Lint · scoped · tooling self-tests, Lint · PM dispatch-gates, Check PR Size, Check Changeset, Check Documentation Links, Governed Surface Queue Guard, Flag docs affected by code changes, PR-body guards, filter, Spec property liveness — all ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Break attempts that did not break it: a Must-fix: none. Should-fix S1 — the second arm of Should-fix S2 — escalation, not a change to this head: the plugin-approvals recall gap in ③, filed as its own card by the seat. Implemented-by: VERDICT: PASS |
…ecord arm and its remedy The code's docblock, its ledger row, the client resume docblock, the service README and the automation checklist now say the same 409 answers a map item whose suspension is gone without a cancelled history entry, and that a cancelled item is recorded instead. The refusal message names the remedy: cancel the map's run (and the run that waits on it) to end the batch. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4d1oZadHHFdEZXANxiG
|
domain:server#1 · os-elon-musk · session_016WV4czVnUdQQCVKSW5J2uk Contract review (delta) · PR #28176 · head 8ec3661 · 2026-10-30T16:35ZDelta: one commit 8ec3661 on top of the PASSED head 84b980b (7 files, +28/-6), read with S1 dispositionS1 (second Checks
Must-fixNone. Should-fix
Reviewed-by: session_016WV4czVnUdQQCVKSW5J2uk VERDICT: PASS |
|
domain:server#1 · os-elon-musk · session_016WV4czVnUdQQCVKSW5J2uk Seat ACCEPT · PR #28176 · head
|
Requested by Elon · project thread
Fixes #27971
Clause-②: yes (widening)
Metadata-protocol: added
Ruling-ref: 6523833308
Skills-docs: none —
skills/**names neitherRESUME_CHILD_PENDINGnor a map's cancelled item, so no skill text describes the shape that changes; the hand-written guidecontent/docs/automation/flows.mdx(batch-approval paragraph) is updated here, and the generatedcontent/docs/references/automation/builtin-node-config.mdxis regenerated from the spec describe.维护者速读
{ $status: 'cancelled', $itemRunId }标记;count只算真正跑过的项。子项挂起记录没了、运行历史又不是 cancelled 的,直接 409 拒绝,什么都不动。packages/services/service-automation/src/run-resume.ts(continuePastCancelledMapItem 的判定与拒绝)、src/builtin/map-node.ts(标记写入与 count)、src/map-cancelled-item-resume.test.ts。pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 src/map-cancelled-item-resume.test.ts,预期 7 个用例全绿。What changes
run-resume.ts,continuePastCancelledMapItem). When a run's linked-run chain ends on amap:link whose item suspension row is gone, and the run is parked at the built-inmapnode, the gate callsgetRun(itemRunId):cancelled(written bycancelRun, which never touches the parent):resumecontinues with an engine-built signal that sets the handoff variableMAPNODE.$mapItemCancelledto the item run id;getRundegrades tonull), a status other thancancelled: refused409 RESUME_CHILD_PENDING(service-automation: resuming a run paused at amapnode by the PARENT run id completes the parent and leaves the parked item child suspended — accepted shape or a skipped item? #27877's code) before any state is touched, with a message saying the item is no longer suspended and what its history says.builtin/map-node.ts). On re-entry with that handoff it pushes{ $status: 'cancelled', $itemRunId }into the item's slot, soresults[i]is still itemi.countis the items that ran (isMapItemCancelledMarkerfilters the markers); the cancelled item adds nothing to the step totals. The marker is closed and$-keyed: a child flow's output keys are its declared variable names, whichflowBoundVariableNameSchemakeeps off$, so no real output can look like it.subflowlink too. The engine-built signal is delegated down to the map one level below, the same way a caller's signal is.$namespace gets no continuation;resumeInternalrefuses it400 INVALID_SIGNALwith the run left parked, as before.mapnode. Another executor that mints amap:correlation keeps its resume unchanged (the concurrent-replica test'smap_itemsnode is one).MapConfigSchema.outputVariable's describe and the map node's own config-schema description say an element can be the marker;flows.mdx's batch paragraph describes the marker,count, and both 409 arms; the reference page is regenerated.@objectstack/service-automationminor,@objectstack/specminor (Metadata-protocol: added), withRuling-ref: 6523833308and the ADR-0087 answernot-required (no-migration-prescription).Tests
New
src/map-cancelled-item-resume.test.ts(7 cases): the cancelled item's slot holds the marker, the map parks on item 2, and the downstream node reads[marker, {done: 'ran t2'}]withcount1; the same one level down through asubflowparent; an item row gone without acancelledhistory row is refused 409 with nothing moved; an unreadable history is refused 409; a caller signal writing$mapItemCancelledis refusedINVALID_SIGNAL; controls: a still-suspended item is still refused naming it, and items that complete are recorded as before withcount2.Ablations (fix committed first; the tests import ./engine.js and ./builtin/map-node.js from src, no dist on the path)
Each via
scripts/ablation-replace.mjs(anchor x1 → x0 on disk, then the run, then restore to the HEAD blob withgit diff HEADempty):item?.status !== 'cancelled'→item == null: refuses … without a cancelled history row red (1 failed, 6 passed).applyResumeSignal(new Map(), signal, link.nodeId)line) deleted: still refuses a caller signal that writes the engine namespace red (1 failed, 6 passed).countfilter →state.results.length: the two marker cases red (2 failed, 5 passed).nodeType !== 'map'guard deleted:concurrent-replica-resume-race.test.tsTHE CONDITION (correlation) red (1 failed, 11 passed). That pin is how the guard was found: without it the gate refused that test's syntheticmap:item_1correlation.Acceptance notes
mainf6adf48 before the fix:cancelRun(item1)answeredtrueandgetRun(item1).statusreadcancelled;resume(parentId)answeredsuccess/paused,$mapStatebecame{started: 2, results: []}, and after item 2 completed the parent wascompletedwith a downstream node readingresults= [item 2's output] andcount1.$status,$itemRunId) are the dev's proposal under ruling A; the ruling leaves their exact spelling to the contract review.plugin-approvalscallingcancelRun) is a producer of a cancelled item in a batch sign-off; read, not measured here.Verification
Package suites
pnpm --filter @objectstack/service-automation test: Test Files 231 passed (231), Tests 2911 passed (2911).typecheck: check:test-typecheck OK, 0 errors.pnpm --filter @objectstack/spec test: Test Files 756 passed (756), Tests 22513 passed, 1 todo.pnpm --filter @objectstack/spec check:generated: all 14 generated artifacts up to date, aftergen:schema+gen:docsand a spec build.Full dogfood (the title carries a bang)
Closure built with
pnpm --filter '@objectstack/dogfood^...' build, thenpnpm --filter @objectstack/dogfood testunder the verify lock: VERDICT command-exit 0; Test Files 358 passed, 3 skipped (361); Tests 2883 passed, 20 skipped (2903). Run on 0dfb617; the later commit only adds the changeset's ADR-0087 line.Derived gate families (dispatch-gates --commands, reconciled with --ran)
107 derived, 107 run with an exit code, 0 NOT MEASURED. They ran at 0dfb617.
check-adr-0087-registration --base origin/mainfirst exited 1 (no disposition line), fixed in the changeset and re-run at 84b980b with exit 0; the five changeset families were also re-run at 84b980b, all exit 0.check-platform-checklist --changed-from origin/main: OK, 17 anchors judged, 0 lost.git merge-treeagainst the currentorigin/main: clean.pnpm lintis CI's.Human Minimum
packages/specholds no runtime logic. (describe text only)pnpm lintpasses. (CI)main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4d1oZadHHFdEZXANxiG
Generated by Claude Code