Skip to content

fix(plugin-approvals): a revise-window recall on a batch item resumes the batch - #28228

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-9vip6n
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-9vip6n

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

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

Fixes #28183

Clause-②: no (a batch run that nothing could wake after the recall now continues, in the shape the cancelled-item ruling already set; no schema, accept set, error code or result field changes)
Metadata-protocol: none

维护者速读

  • 改了什么:批量签批(map 逐项审批)里,提交人在修改窗口(revise window)撤回某一项后,撤回现在会顺手恢复停在这一项上的批次 run;map 按 fix(service-automation)!: a map whose item run was cancelled records the item as cancelled on a parent-id resume #28176 的形状把该项记成 { $status: 'cancelled', $itemRunId },count 不算它,然后开下一项或走完。
  • 为什么改:实测 main(3a2f4c00):撤回只对该项 cancelRun,批次 run 一直停在 map:<该项 run>,第 2 项的审批永远不开,除非运维手动用批次 run id 恢复。按分诊与裁决 A(6523833308)的形状修,不新增标记或错误码。
  • 风险与回滚:只在“修改窗口内撤回、cancelRun 确实取消了、且有 run 停在内置 map 节点上等这一项”时多做一次恢复;pending 撤回、无批次的撤回、全部决定完的批次不变(对照钉子)。回滚即 revert 本 PR。
  • 先看:packages/plugins/plugin-approvals/src/approval-service.ts 的 recall 修改窗口分支与新私有方法 resumeBatchOfRecalledItem。
  • 两分钟验证:pnpm --filter @objectstack/plugin-approvals exec vitest run src/batch-item-recall-resumes-batch.test.ts,预期 5 passed。
  • 需要你做的:无,席位在 CI 绿后按落地授权落地。

What changes

  • packages/plugins/plugin-approvals/src/approval-service.ts. In recall's revise-window branch, when cancelRun answers true (it really cancelled the item's run), the new private resumeBatchOfRecalledItem(itemRunId, requestId) looks for the run parked on map:<itemRunId> (listSuspendedRunsDurable, a member the surface already declares), checks through getFlow that the node it is parked at is the built-in map, and resumes that run with serviceResume(batchRunId, {}). The engine's existing parent-id resume (PR fix(service-automation)!: a map whose item run was cancelled records the item as cancelled on a parent-id resume #28176) does the rest: it reads the item's cancelled history, records the marker in the item's slot, leaves it out of count, and the map opens its next item or runs on. No service-automation change.
  • Failures are told as the recall tells its own. The withdrawal stands whatever happens to the batch. A refused batch resume (for example 409 RESUME_CHILD_PENDING when the item's history cannot be read) or a lookup that throws fills resumeError and logs at error; the engine's stranded exit, and a strand bubbled to a subflow parent above the batch, also fill resumeFailure with RESUME_FAILED, the one code this package's ledger row admits. resumed still describes the recalled request's own run, which was cancelled, so it stays false.
  • Nothing is woken without proof. No listSuspendedRunsDurable or getFlow on the attached engine, no run parked on that item, or a parked node that is not the built-in map: the recall answers exactly as before. A home-grown node that parks on a map: link is never re-run, because the engine records the cancelled item only for the built-in map, and re-running another node would advance it past the item with nothing recorded.
  • packages/plugins/plugin-approvals/src/batch-item-recall-resumes-batch.test.ts (new). The real AutomationEngine with the built-in nodes and the approval node, a release_signoff batch (map over task_signoff, an approval with a revise window), the showcase's showcase_release_signoff → showcase_one_task_signoff shape.
  • scripts/engine-double-contract.pinned.json. Two generated rows (delete, update) for the new test's engine double, written by check-engine-double-contract.mjs --write.
  • .changeset/28183-batch-item-recall-resumes-batch.md. @objectstack/plugin-approvals patch.

Why

Before (measured at origin/main 3a2f4c00)

Two tasks; item 1 sent back by the approver, then recalled by the submitter from the revise window:

Before After
item 1's run cancelled cancelled
batch run parked on signoffs, correlation map:<item 1's run>, history paused parked on signoffs, correlation map:<item 2's run>
item 2's request none pending
recall answer resumed: false, no resumeError resumed: false, no resumeError
after item 2 is approved (never reached) batch completed; signoffResults = [{ $status: 'cancelled', $itemRunId }, { decision: 'approved' }], count 1

Why this path

Triage 6527285884 routed the card to ruling A (6523833308): a cancelled item records cancelled and the map moves on, and the recall follows the same shape by resuming the parent with the cancelled marker. PR #28176 already built that resume behind the batch run's own id; the only thing missing was a caller. So the fix stays in the recall path and reuses that resume as is: no new marker shape, no new code, no packages/spec edit.

Tests

batch-item-recall-resumes-batch.test.ts, 5 cells:

  1. the recall resumes the batch: item 1's slot is the marker, count 1, item 2's request opens, and the batch completes after item 2 is approved (red on main);
  2. CONTROL: a revise-window recall on an approval with no batch above it is unchanged, and a batch parked beside it is untouched;
  3. CONTROL: a recall while pending resumes the item down reject, and the map records { decision: 'rejected' };
  4. CONTROL: a batch whose every item is decided (one through a send-back and resubmit round) records both outputs, count 2;
  5. CONTROL: a run parked on a map: link at a node other than the built-in map is not woken.

Ablations through scripts/ablation-replace.mjs, each restored to the HEAD blob with git diff HEAD empty:

Ablation Red
A1: drop the batch resume after cancelRun cell 1
A2: match any map: parent, not this item's cell 2
A3b: a pending recall takes the cancel path cell 3
A4: the decide door always resumes down approve cell 4
A5: drop the built-in map node check cell 5

(A3, the batch resume also issued on the pending path, stayed green: by then the item has completed and the map has moved to item 2, so no run is parked on item 1 and the lookup finds nothing. Harmless, so not a guard.)

Verification

Head acae6743, after a whole-tree turbo run build and fetching the teardown self-test's pinned commit.

  • pnpm --filter @objectstack/plugin-approvals exec vitest run: 106 files, 1357 passed. pnpm --filter @objectstack/plugin-approvals run typecheck: exit 0, test layer held at its ledger.
  • dispatch-gates --commands (re-derived after the second commit): 71 families, all exit 0. --ran: 71 derived, 71 run, 0 NOT-MEASURED, 0 UNRUN. The first sweep caught the test double (check:engine-double-contract, check:objectql-double-limit); the second commit routes it through the dispatch asserts and adds its two generated ledger rows.
  • pnpm lint: exit 0. check-commit-card-trailers --range origin/main..HEAD: exit 0.

Out of scope

  • listSuspendedRunsDurable reads a durable store's deployment-wide page (ObjectStoreSuspendedRunStore.list(), 1000 rows). Past 1000 parked runs the batch run may not be on that page, and the recall then answers as it did before this PR (the batch stays parked, nothing is told). A precise lookup needs an engine member (the item run's $parentRunId), which is outside this card's surface.
  • content/docs/automation/flows.mdx's cancelled-item paragraph and approvals.mdx's revise-window paragraph do not yet say that a recall wakes the batch. Docs were outside the claimed surface.

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4d13k5TVyAhqhiKKgN3

… the batch

A revise-window recall cancels the item's run with cancelRun, which never
touches the parent, so a batch sign-off (a map over approval items) stayed
parked on the recalled item with nothing to wake it. The recall now finds
the run parked at the built-in map node on that item's run and resumes it;
the engine records the item's slot as the cancelled marker, outside count,
and the map goes on. A run parked on such a link at any other node is left
alone. A refused or stranded batch resume is told on resumeError and, for
the engine's stranded exit, resumeFailure, as the recall's own resume is.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4d13k5TVyAhqhiKKgN3
…hrough the dispatch asserts

The double's update/delete now open with assertEngineUpdateDispatch /
assertEngineDeleteDispatch and its find holds the caller's numeric
offset/limit, as check:engine-double-contract and
check:objectql-double-limit require; the pinned ledger learns the two
new rows (check-engine-double-contract --write).

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4d13k5TVyAhqhiKKgN3
@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 1 package(s): @objectstack/plugin-approvals, touching 3 documentable anchor(s).

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

  • content/docs/automation/flows.mdx (via ApprovalService (symbol, a top-level class), RESUME_FAILED (literal, a string literal in resumeBatchOfRecalledItem))

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

  • content/docs/releases/v17/17-6.mdx (via ApprovalService (symbol, a top-level class))
  • content/docs/releases/v18/18-1.mdx (via ApprovalService (symbol, a top-level class))

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 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 6 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 7c3cc889f7c64d465486bb94ded8c8ea558d6945 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from ee4a23998c8b9b2640e76d83ac056a3e3c94eeb9 — the merge of head 4007cab0a1d634b87db5a3db7a91b2b81a9a70ca into base 7c3cc889f7c64d465486bb94ded8c8ea558d6945, 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 ee4a23998c8b9b2640e76d83ac056a3e3c94eeb9 && git checkout ee4a23998c8b9b2640e76d83ac056a3e3c94eeb9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7c3cc889f7c64d465486bb94ded8c8ea558d6945 4007cab0a1d634b87db5a3db7a91b2b81a9a70ca && git checkout -B drift-repro 7c3cc889f7c64d465486bb94ded8c8ea558d6945 && git merge --no-ff 4007cab0a1d634b87db5a3db7a91b2b81a9a70ca

node scripts/docs-audit/affected-docs.mjs --json 7c3cc889f7c64d465486bb94ded8c8ea558d6945

⚠️ 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 7c3cc889f7c64d465486bb94ded8c8ea558d6945 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

维护者速读

  • 改了什么:批量签批(map 逐项审批)里,提交人在修改窗口内撤回某一项后,撤回现在会恢复停在这一项上的批次 run;map 按 fix(service-automation)!: a map whose item run was cancelled records the item as cancelled on a parent-id resume #28176 的形状把该项记成 cancelled 标记,count 不算它,然后开下一项或走完。flows.mdx、approvals.mdx 各补一句说明。
  • 为什么改:实测 main(3a2f4c00):撤回只取消该项 run,批次一直停着,后续项的审批永远不开,除非运维手动恢复。
  • 风险与回滚:Clause-②: no;只在“窗口内撤回且确实取消、且有 run 停在 map 节点等这一项”时多做一次恢复;其余撤回与已决定完的批次不变(对照钉子)。不新增标记或错误码。回滚即 revert 本 PR。
  • 先看:packages/plugins/plugin-approvals/src/approval-service.ts 的 recall 窗口分支与 resumeBatchOfRecalledItem。
  • 两分钟验证:pnpm --filter @objectstack/plugin-approvals exec vitest run src/batch-item-recall-resumes-batch.test.ts,5 passed。
  • 需要你做的:无(非受管;不欠契约复审;席位按本任落地授权落地)。

席位意见:ACCEPT。

席位核对

Head-sha: 4007cab0a1d634b87db5a3db7a91b2b81a9a70ca

  • Shape: 6 files, +430 / −3. The fix is the recall path in approval-service.ts. The rest is a new test, a patch changeset for @objectstack/plugin-approvals, one sentence each in flows.mdx and approvals.mdx, and two generated rows in scripts/engine-double-contract.pinned.json for the new test's engine double. run-resume.ts and packages/spec are untouched. check-governed-merges --pr 28228: NOT governed (0 of 6 paths).
  • Premise measured: on main (3a2f4c00, with PR fix(service-automation)!: a map whose item run was cancelled records the item as cancelled on a parent-id resume #28176 in), a recall left the batch parked at map:<item run>. It now resumes with the item's slot as fix(service-automation)!: a map whose item run was cancelled records the item as cancelled on a parent-id resume #28176's marker. 5 pins: each ablation turns its pins red, and each was restored. Package 1357 passed, typecheck 0. dispatch-gates --ran on acae6743: 71 derived, 71 run, 0 NOT-MEASURED. After the docs commit the derivation grew to 101 families; the dev reports them all exit 0 so far, and CI on 4007cab0 ran its own families green. pnpm lint: 0.
  • Gate check-runs on the head: 50 runs, 38 success, 12 skipped, 0 failure/pending; mergeable_state clean. The reds on acae6743 belong to that superseded head, whose shards the docs push cancelled.
  • Contract review: not owed. The paths decide no face, and the changeset's Clause-②: no puts it on the declaration face at TIER_DEFAULT. check-owed-reviews does not list this PR. Ruling A (6523833308) already fixed the marker shape, and nothing here re-rules it.
  • Corrections to the PR body (written on acae6743):
  • Landing: under the maintainer's landing grant for this seat term (seat 4 thread, 2026-10-29 22:04Z): pr_ready + automerge_enable through the relay.

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

Development

Successfully merging this pull request may close these issues.

plugin-approvals: a revise-window recall on a batch sign-off cancels the map item's run and never resumes the parent, so the batch stays parked

1 participant