Repository navigation
Commit 898e439
fix(plugin-approvals): a revise-window recall on a batch item finds its batch run past the first 1000 parked runs (#28268)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DQALeiHN1aR44XyBgGozrec)_
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 行首页。
- 为什么改:实测 main(f40b777d):两个副本共用一个 `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)。
- 需要你做的:无,席位在 CI 绿后按落地授权落地。
## 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 loader
`hasSuspendedRun` uses), reads the `$parentRunId` / `$parentMapNode` the
`map` node launched it with, loads the parent's suspension by that id,
and answers `{ runId, flowName, nodeId, correlation }` only while the
parent is parked on `map:<itemRunId>` at the node the item names.
Otherwise `undefined`. Two keyed reads, never a listing. It rejects when
the store cannot be read, as `hasSuspendedRun` does. No other engine
hunk; `run-resume.ts` and the store are untouched.
- **`packages/plugins/plugin-approvals/src/approval-service.ts`.**
`ApprovalResumeSurface` declares the member (optional). In `recall`'s
revise-window branch the new private `batchAboveItem(runId)` reads it
BEFORE `cancelRun`, because the cancel removes the item's suspension and
the link with it. It never throws; a failed read is carried to
`resumeBatchOfRecalledItem`, which now takes the parent it read instead
of scanning `listSuspendedRunsDurable()`, and tells a failed lookup in
`resumeError` after the cancel, as before. The built-in `map` check
through `getFlow` and everything after it are unchanged.
- **Tests.** `suspended-map-parent-of.test.ts` (new, service-automation)
pins the engine member; `batch-item-recall-resumes-batch.test.ts`
(plugin-approvals) gains a `#28230` block over two replicas and one
`ObjectStoreSuspendedRunStore`. Its existing wiring moved into a
`wireReplica` helper; #28228's five cells are unchanged and green. No
new engine double: the block reuses the file's already-pinned one, so
`scripts/engine-double-contract.pinned.json` is untouched.
- **`.changeset/28230-batch-recall-past-first-page.md`.**
`@objectstack/plugin-approvals` and `@objectstack/service-automation`,
patch.
## Why
### Before (measured at `origin/main` `f40b777d`)
Replicas `a` and `b` share one `ObjectStoreSuspendedRunStore` and one
table set. N `paused` rows of another flow are stored first. The batch
(`map` over two `task_signoff` items) is launched on `a`, item 1 is sent
back on `a`, and the submitter recalls item 1 on `b`.
| | N = 1000, before | N = 998, before | N = 1000, after |
|:---|:---|:---|:---|
| batch on `b.listSuspendedRunsDurable()` | no | yes | no (no longer
read) |
| item 1's run | `cancelled` | `cancelled` | `cancelled` |
| item 2's request | none: the batch stays parked on item 1 | `pending`
| `pending` |
| recall answer | `resumed: false`, no `resumeError` | `resumed: false`,
no `resumeError` | `resumed: false`, no `resumeError` |
| after item 2 is approved | (never reached) | marker in slot 1, `count`
1 | marker in slot 1, `count` 1, batch `completed` |
The listing reads one 1000-row page of `paused` rows 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.ts` launches every item with `$parentRunId` and
`$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: a `subflow` inside a `map` item inherits the item's
`$parentMapNode` through the spread context, so its `$parentRunId` names
the item run, which is parked at a `subflow` node, not the map (pinned).
## Tests
`suspended-map-parent-of.test.ts` (5 cells): a `map` item answers its
batch, also from a second engine over the same store with nothing in
memory; CONTROL a `subflow` child, a `subflow` nested in a `map` item,
an item whose batch has moved on, a top-level run and an unknown id
answer `undefined`.
`batch-item-recall-resumes-batch.test.ts`, `#28230` block (4 cells):
1. the batch past the first page resumes, item 1's slot holds #28176's
`cancelled` marker, `count` 1, the batch completes (red on `main`: no
request opens for item 2);
2. CONTROL: a batch within the first page behaves as before;
3. CONTROL: a recall with no batch above it, past the first page,
behaves as before, and the batch beside it stays parked on its item;
4. CONTROL: a parent the engine answers (a home-grown fan-out that
writes the same linkage) but parked at a node other than the built-in
`map` is left alone.
Ablations through `scripts/ablation-replace.mjs`, each restored to the
HEAD blob with `git diff HEAD` empty:
| Ablation | Red |
|:---|:---|
| A1: `batchAboveItem` answers no parent | #28183 cell 1, #28230 cells 1
and 2 |
| A2: the batch is read after `cancelRun` | #28183 cell 1, #28230 cells
1 and 2 |
| A3: drop the built-in `map` node check | #28230 cell 4 |
| A4: engine member drops the parked-on-this-item check | engine cells
2, 3, 4 |
(A5, dropping the `typeof parentRunId` check, stayed green: a missing id
loads no suspension, so it answers `undefined` either way. It is the
type narrowing the next read needs, not a guard.)
## Verification
Head `776907f9`, after a whole-tree `turbo run build`, `pnpm install
--frozen-lockfile` and 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-loads` and `check:i18n` exited 3 before the
whole-tree build and 0 after it). `--ran`: 64 derived, 64 run, 0
NOT-MEASURED, 0 UNRUN. `check:engine-double-contract` and
`check:objectql-double-limit` are green with no ledger edit.
- `pnpm lint`: exit 0. `check-commit-card-trailers --range
origin/main..HEAD`: exit 0.
## Out of scope
- None filed. Probed while pinning: a `subflow` inside a `map` item
inherits the item's `$parentMapNode`, but on `main` the 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.
- `listSuspendedRunsDurable` still reads one 1000-row page for its other
caller, the restored-continuation proof in `approval-service.ts`, which
refuses on an absent entry (a retry, not a silent strand). Not this
card's.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016WV4d1FQJt7HxgWZZ5cYz9
Co-authored-by: Claude <noreply@anthropic.com>1 parent ef777b8 commit 898e439
5 files changed
Lines changed: 452 additions & 31 deletions
File tree
- .changeset
- packages
- plugins/plugin-approvals/src
- services/service-automation/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
288 | 288 | | |
289 | 289 | | |
290 | 290 | | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
291 | 305 | | |
292 | 306 | | |
293 | 307 | | |
| |||
352 | 366 | | |
353 | 367 | | |
354 | 368 | | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
355 | 375 | | |
356 | 376 | | |
357 | 377 | | |
| |||
5277 | 5297 | | |
5278 | 5298 | | |
5279 | 5299 | | |
| 5300 | + | |
| 5301 | + | |
| 5302 | + | |
5280 | 5303 | | |
5281 | 5304 | | |
5282 | 5305 | | |
| |||
5288 | 5311 | | |
5289 | 5312 | | |
5290 | 5313 | | |
5291 | | - | |
| 5314 | + | |
5292 | 5315 | | |
5293 | 5316 | | |
5294 | 5317 | | |
| |||
5370 | 5393 | | |
5371 | 5394 | | |
5372 | 5395 | | |
| 5396 | + | |
| 5397 | + | |
| 5398 | + | |
| 5399 | + | |
| 5400 | + | |
| 5401 | + | |
| 5402 | + | |
| 5403 | + | |
| 5404 | + | |
| 5405 | + | |
| 5406 | + | |
| 5407 | + | |
| 5408 | + | |
| 5409 | + | |
| 5410 | + | |
| 5411 | + | |
| 5412 | + | |
| 5413 | + | |
| 5414 | + | |
| 5415 | + | |
| 5416 | + | |
| 5417 | + | |
| 5418 | + | |
5373 | 5419 | | |
5374 | 5420 | | |
5375 | 5421 | | |
| |||
5386 | 5432 | | |
5387 | 5433 | | |
5388 | 5434 | | |
5389 | | - | |
| 5435 | + | |
5390 | 5436 | | |
5391 | | - | |
| 5437 | + | |
| 5438 | + | |
5392 | 5439 | | |
5393 | 5440 | | |
5394 | 5441 | | |
5395 | 5442 | | |
| 5443 | + | |
5396 | 5444 | | |
5397 | | - | |
5398 | | - | |
5399 | 5445 | | |
5400 | 5446 | | |
5401 | | - | |
5402 | | - | |
| 5447 | + | |
| 5448 | + | |
5403 | 5449 | | |
| 5450 | + | |
5404 | 5451 | | |
5405 | 5452 | | |
5406 | 5453 | | |
| |||
0 commit comments