Repository navigation
fix(plugin-approvals): a revise-window recall on a batch item finds its batch run past the first 1000 parked runs - #28268
Conversation
…ts batch run past the first 1000 parked runs The recall looked for the batch above the item it cancels in listSuspendedRunsDurable(), which reads one 1000-row page of a database-backed store plus what this process parked itself. A batch another process parked behind more than 1000 runs was missed and stayed parked on the cancelled item. AutomationEngine.suspendedMapParentOf(itemRunId) now answers the batch from the item's own $parentRunId / $parentMapNode with two keyed reads, and only while the parent is parked on map:<itemRunId> at that node. The recall reads it before cancelRun, which removes the item's suspension. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4d1FQJt7HxgWZZ5cYz9
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 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 087d6ed9ac43be29c3109d38424027b1e315234b && git checkout 087d6ed9ac43be29c3109d38424027b1e315234b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 744ad0ad3afbc29fd0bfee9e8a8e3b7ac3958aec 776907f9fc296bcd492f03c87531fc544fedc671 && git checkout -B drift-repro 744ad0ad3afbc29fd0bfee9e8a8e3b7ac3958aec && git merge --no-ff 776907f9fc296bcd492f03c87531fc544fedc671
node scripts/docs-audit/affected-docs.mjs --json 744ad0ad3afbc29fd0bfee9e8a8e3b7ac3958aec
|
|
domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL 维护者速读
席位意见:ACCEPT。 席位核对Head-sha:
|
Requested by Elon · project thread
domain:server#4 · os-elon-musk · session_016WV4d1FQJt7HxgWZZ5cYz9
Fixes #28230
Clause-②: no (a recall on a store with more than 1000 parked runs now wakes its batch, as it already does below that; no schema, accept set, error code or result field changes)
Metadata-protocol: none
维护者速读
cancelRun之前按该项 run 自身的$parentRunId/$parentMapNode键读出它上面的批次 run(新增引擎成员AutomationEngine.suspendedMapParentOf),不再扫listSuspendedRunsDurable()的 1000 行首页。ObjectStoreSuspendedRunStore,批次之前已挂起 1000 条别的 run,在另一副本上撤回:该项被取消,批次 run 一直停在这一项上,第 2 项不开,resumeError为空,什么都不说;998 条时一切正常。map节点的检查、失败的告知方式都没变。新成员只读,两次按 id 读,存储读不出时抛错并照旧写进resumeError。回滚即 revert 本 PR。packages/services/service-automation/src/engine.ts的suspendedMapParentOf,与approval-service.ts里recall修改窗口分支和新私有方法batchAboveItem。pnpm --filter @objectstack/plugin-approvals exec vitest run src/batch-item-recall-resumes-batch.test.ts(预期 9 passed,需先 build@objectstack/service-automation)与pnpm --filter @objectstack/service-automation exec vitest run src/suspended-map-parent-of.test.ts(预期 5 passed)。What changes
packages/services/service-automation/src/engine.ts: one new public read,suspendedMapParentOf(itemRunId). It loads the item run's suspension by id (the strict, store-authoritative loaderhasSuspendedRunuses), reads the$parentRunId/$parentMapNodethemapnode launched it with, loads the parent's suspension by that id, and answers{ runId, flowName, nodeId, correlation }only while the parent is parked onmap:<itemRunId>at the node the item names. Otherwiseundefined. Two keyed reads, never a listing. It rejects when the store cannot be read, ashasSuspendedRundoes. No other engine hunk;run-resume.tsand the store are untouched.packages/plugins/plugin-approvals/src/approval-service.ts.ApprovalResumeSurfacedeclares the member (optional). Inrecall's revise-window branch the new privatebatchAboveItem(runId)reads it BEFOREcancelRun, because the cancel removes the item's suspension and the link with it. It never throws; a failed read is carried toresumeBatchOfRecalledItem, which now takes the parent it read instead of scanninglistSuspendedRunsDurable(), and tells a failed lookup inresumeErrorafter the cancel, as before. The built-inmapcheck throughgetFlowand everything after it are unchanged.suspended-map-parent-of.test.ts(new, service-automation) pins the engine member;batch-item-recall-resumes-batch.test.ts(plugin-approvals) gains a#28230block over two replicas and oneObjectStoreSuspendedRunStore. Its existing wiring moved into awireReplicahelper; fix(plugin-approvals): a revise-window recall on a batch item resumes the batch #28228's five cells are unchanged and green. No new engine double: the block reuses the file's already-pinned one, soscripts/engine-double-contract.pinned.jsonis untouched..changeset/28230-batch-recall-past-first-page.md.@objectstack/plugin-approvalsand@objectstack/service-automation, patch.Why
Before (measured at
origin/mainf40b777d)Replicas
aandbshare oneObjectStoreSuspendedRunStoreand one table set. Npausedrows of another flow are stored first. The batch (mapover twotask_signoffitems) is launched ona, item 1 is sent back ona, and the submitter recalls item 1 onb.b.listSuspendedRunsDurable()cancelledcancelledcancelledpendingpendingresumed: false, noresumeErrorresumed: false, noresumeErrorresumed: false, noresumeErrorcount1count1, batchcompletedThe listing reads one 1000-row page of
pausedrows in driver order, and a process adds only what it parked itself. So the batch is missed exactly when another process (a replica, or this one before a restart) parked it behind more than 1000 runs.Why this path
Triage 6531219559 / 6532374421 ruled 「a correlation-keyed lookup instead of a full listing」. The item run already carries its batch:
map-node.tslaunches every item with$parentRunIdand$parentMapNode. Reading that before the cancel needs no store change, no schema change and no new code. The parked-on-this-item check guards one real shape: asubflowinside amapitem inherits the item's$parentMapNodethrough the spread context, so its$parentRunIdnames the item run, which is parked at asubflownode, not the map (pinned).Tests
suspended-map-parent-of.test.ts(5 cells): amapitem answers its batch, also from a second engine over the same store with nothing in memory; CONTROL asubflowchild, asubflownested in amapitem, an item whose batch has moved on, a top-level run and an unknown id answerundefined.batch-item-recall-resumes-batch.test.ts,#28230block (4 cells):cancelledmarker,count1, the batch completes (red onmain: no request opens for item 2);mapis left alone.Ablations through
scripts/ablation-replace.mjs, each restored to the HEAD blob withgit diff HEADempty:batchAboveItemanswers no parentcancelRunmapnode check(A5, dropping the
typeof parentRunIdcheck, stayed green: a missing id loads no suspension, so it answersundefinedeither way. It is the type narrowing the next read needs, not a guard.)Verification
Head
776907f9, after a whole-treeturbo run build,pnpm install --frozen-lockfileand fetching the teardown self-test's pinned commit.pnpm --filter @objectstack/service-automation run test: 234 files, 2952 passed.pnpm --filter @objectstack/plugin-approvals run test: 106 files, 1361 passed. Both packages'typecheck: exit 0.dispatch-gates --commands: 64 families, all exit 0 (check:dual-build-cjs-loadsandcheck:i18nexited 3 before the whole-tree build and 0 after it).--ran: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN.check:engine-double-contractandcheck:objectql-double-limitare green with no ledger edit.pnpm lint: exit 0.check-commit-card-trailers --range origin/main..HEAD: exit 0.Out of scope
subflowinside amapitem inherits the item's$parentMapNode, but onmainthe nested chain still completes into the item run and the batch moves on, so it is not a defect. The engine member's parked-on-this-item check keeps it from answering that shape.listSuspendedRunsDurablestill reads one 1000-row page for its other caller, the restored-continuation proof inapproval-service.ts, which refuses on an absent entry (a retry, not a silent strand). Not this card's.🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4d1FQJt7HxgWZZ5cYz9