Repository navigation
fix(plugin-approvals): re-issuing a restored approval continuation proves the pause past the first 1000 parked runs - #28314
Conversation
…pause past the first 1000 parked runs GUARD 3 of continueRestoredRun read where the run is parked from listSuspendedRunsDurable(), one 1000-row page of a DB-backed store plus what this process parked itself. A run another process parked behind more than 1000 others was on no page, so every re-issue was refused with "parked at no node this engine can see". It now asks the engine for that one run: the new AutomationEngine.suspensionOf(runId) is one keyed, store-authoritative read, declared on ApprovalResumeSurface as an optional, fail-closed member. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4czGci1wfPnGZhkvrcF
📓 Docs Drift CheckThis PR changes 2 package(s): ⛔ 3 release-owned page(s) 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 9bd5e9ea46cd317f3712d9f6abdcd7d252a59abb && git checkout 9bd5e9ea46cd317f3712d9f6abdcd7d252a59abb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 57f8ca2f0afbc0b08df9b02543ce32691e0ede34 c4e208a4554b6c9a1075ef6f50b4c9b77eb0e33b && git checkout -B drift-repro 57f8ca2f0afbc0b08df9b02543ce32691e0ede34 && git merge --no-ff c4e208a4554b6c9a1075ef6f50b4c9b77eb0e33b
node scripts/docs-audit/affected-docs.mjs --json 57f8ca2f0afbc0b08df9b02543ce32691e0ede34
|
|
domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia Seat ACCEPT · PR #28314 · head
|
Requested by Elon · project thread
domain:server#3 · os-elon-musk · session_016WV4czGci1wfPnGZhkvrcF
Fixes #28271
Clause-②: no (a re-issue on a store with more than 1000 parked runs now proves the pause as it already does below that; no schema, accept set, error code or result field changes)
Metadata-protocol: none
维护者速读
continueRestoredRun) 的 GUARD 3 改为按 run id 键读这个 run 停在哪个节点(新增引擎成员AutomationEngine.suspensionOf(runId)),不再在listSuspendedRunsDurable()的 1000 行首页里找。69ce01d7):两个副本共用一个ObjectStoreSuspendedRunStore,前面已挂起 1000 条别的 run,副本a搁浅并恢复一个驳回决定,副本b重发:每次都以INVALID_STATE"parked at no node this engine can see" 拒绝,连试 3 次一样,决定永远落不了地;998 条时正常。hasSuspendedRun同一个严格读法,存储读不出时抛错。回滚即 revert 本 PR。packages/services/service-automation/src/engine.ts的suspensionOf,与approval-service.ts里continueRestoredRun的 GUARD 3。@objectstack/service-automation,再pnpm --filter @objectstack/plugin-approvals exec vitest run src/restored-approval-continuation.test.ts(预期 20 passed)与pnpm --filter @objectstack/service-automation exec vitest run src/suspension-of.test.ts(预期 4 passed)。What changes
packages/services/service-automation/src/engine.ts: one new public read,suspensionOf(runId). It loads the run's suspension by id throughloadSuspendedRunStrict, the store-authoritative loaderhasSuspendedRunuses, and answers{ runId, flowName, nodeId, correlation }, orundefinedwhen the run is not suspended. One keyed read, never a listing. It rejects when the store cannot be read, ashasSuspendedRundoes. It sits besidesuspendedMapParentOf(fix(plugin-approvals): a revise-window recall on a batch item finds its batch run past the first 1000 parked runs #28268). No other engine hunk.packages/plugins/plugin-approvals/src/approval-service.ts.ApprovalResumeSurfacedeclaressuspensionOf?(optional, fail-closed: an engine without it is refused, as an engine without the listing was). GUARD 3 ofcontinueRestoredRunreadssuspensionOf(runId)?.nodeIdinstead of the run's entry inlistSuspendedRunsDurable(). The refusal texts are unchanged except the missing-reader one, which now namessuspensionOf.listSuspendedRunsDurable?stays declared (the interface is exported); its docblock now says this service no longer reads it.suspension-of.test.ts(new, service-automation) pins the engine member.restored-approval-continuation.test.ts(plugin-approvals) gains a#28271block over two replicas and oneObjectStoreSuspendedRunStore, reusing the file's existing engine double, soscripts/engine-double-contract.pinned.jsonis untouched..changeset/28271-restored-continuation-past-first-page.md.@objectstack/plugin-approvalsand@objectstack/service-automation, patch.Why
Before (measured at
origin/main69ce01d7)Replicas
aandbshare oneObjectStoreSuspendedRunStoreand one table set. Npausedrows of another flow are stored first. Ona, adeal_approvalrun parks, itsrejectdecision strands (the reject branch throws), andrestoreConsumedSuspensionre-arms the pause. Onb,continueRestoredRunre-issues it.b.hasSuspendedRun(run)truetruetrueb.listSuspendedRunsDurable()continueRestoredRun, 3 triesINVALID_STATE: run '…' is parked at no node this engine can see, …resumed: trueresumed: true, the reject branch runsThe listing reads one 1000-row page of
pausedrows in driver order, and a process adds only what it parked itself. So the run is missed exactly when another process (a replica, or this one before a restart) parked it behind more than 1000 runs, and a retry reads the same page.Why this path
Triage 6535230031: a keyed read by run id, the same kind as
suspendedMapParentOf, still fail-closed when the run is not parked. The strict loader is the one GUARD 2 (hasSuspendedRun) already reads, so GUARD 2 and GUARD 3 now ask the same store the same question; the listing's own merge also let the durable row win (#14332), so below the page nothing changes. On an outage GUARD 2 already throws before GUARD 3 is reached.Other
listSuspendedRunsDurable()callersGrepped (
git grep -n "listSuspendedRunsDurable\|readSuspendedRuns", non-test). GUARD 3 was the only caller that looks up a single run. The rest:engine.tslistSuspendedRunsDurable()itself — the operability listing; a page is its contract.subflow-guards.tsparkedRunsOf→readSuspendedRuns('throw', flowNames)— per-flow, read to its end withlistByFlow(service-automation: ADR-0126 §7.3's packaged-subflow guard holds on the toggle door only — creating, republishing or deleting a flow can still arm a packaged caller onto a disabled packaged subflow #20725); not a page.runtime/src/domains/automation.ts:1628reads the synchronous in-memorylistSuspendedRuns(), not the durable listing, for a listing, not a single-run lookup.Tests
suspension-of.test.ts(4 cells): a parked run answers its node, also on a second engine over the same store with nothing in memory; the answer follows the run to its next pause on another engine and isundefinedonce that engine completes it (no stale memory answer); an unknown id answersundefined; a store whoseloadthrows rejects.restored-approval-continuation.test.ts,#28271block (5 cells):are-armed is proved onband the decision is re-issued; the reject branch runs (red onmain: refused "parked at no node this engine can see");wait_revision(red onmain: it named no node);hasSuspendedRun;suspensionOfis refused withINVALID_STATE, and the pause is left in place.Ablations through
scripts/ablation-replace.mjs, each restored to the HEAD blob withgit diff HEADempty:#28271cells 1 and 3#28271cells 3 and 4#28271cell 5suspensionOfanswers from this process's memory, not the store#28271cells 1, 2 and 3Verification
Head
c4e208a4, after a whole-treeturbo run build,pnpm install --frozen-lockfileand an unshallow fetch;@objectstack/service-automationrebuilt after the engine edit (plugin-approvals tests import its dist).pnpm --filter @objectstack/service-automation run test: 236 files, 2973 passed.pnpm --filter @objectstack/plugin-approvals run test: 106 files, 1366 passed. Both packages'typecheck(includingcheck:test-typecheck): exit 0.dispatch-gates --commands: 64 families, all exit 0.--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.Skills-docs: none.
git grep -n "listSuspendedRunsDurable\|parked at no node\|continueRestoredRun\|suspendedMapParentOf" -- skills content docshas no hit;git grep -il "restoreConsumedSuspension\|re-issue the continuation" -- skills contenthits onlycontent/docs/automation/flows.mdx, whose restore paragraph does not describe the listing or a page cap.Acceptance notes
run-history.ts:463-465(readSuspendedRuns' degrade-path comment) still says "plugin-approvals' restored-continuation proof refuses on an absent entry, so a short list costs it a retry". After this PR that proof no longer reads the listing, so the sentence is stale; the comment's conclusion (keep the level atwarn) still holds. Outside this card's file surface; not edited.Human Minimum
packages/specholds no runtime logic. (Not touched.)pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed. (Not touched.)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4czGci1wfPnGZhkvrcF