Repository navigation
Commit 239e3a8
fix(service-automation): the region refusal cancels its child run even when the suspension store cannot be read (#29329)
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi98Cgk5ia5FFRbkAcQ3MQ1FV)_
Fixes #29166
Item (1) is built here. Item (2) is answered by the seat's ruling (card
comment 6576815635, option B): the run-time failure is the answer,
nothing new is declined at arming, and this PR records that reading in
two docblocks.
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:区域(loop / parallel /
try_catch)内的子流程挂起被拒绝时,若暂停记录存储读不到、挂起链为空,现在仍对子运行调用一次 `cancelRun`,不再只留一条
warn。
- 为什么改:原来空链意味着零次取消,子运行可能一直停在存储里,之后被恢复去替一个早已失败的调用方继续跑。
- 风险与回滚:只在挂起链为空时多调用一次 `cancelRun`;无此挂起时它是空操作。拒绝本身(文字、不可路由)不变。回滚即还原
`dag-traversal.ts` 中这一个分支。
-
先看:`packages/services/service-automation/src/dag-traversal.ts`(`runRegion`
的空链分支),再看 `subflow-region-suspension.test.ts` 末尾新增的两例。
- 两分钟验证:`pnpm --filter @objectstack/service-automation exec vitest run
src/subflow-region-suspension.test.ts`,预期 22 例全过。
- 需要你做的:无,席位落地。(第 2 项已由席位裁定为方案 B:不在布防时拒绝,运行期失败即答案,本 PR 只补注释记录这一读法。)
## What changes
- `runRegion`
(`packages/services/service-automation/src/dag-traversal.ts`): when the
region refusal fires on a `subflow` / `map` node, the child run it
parked is cancelled by walking `resolveSuspensionChain(childRunId)`.
That walk reads through the degrading `loadSuspendedRun`, so a store it
cannot read yields an empty chain, and the walk cancelled nothing. Now
an empty chain calls `cancelRun(childRunId, reason)` once. `cancelRun`
reads through the strict loader: if the store answers by then, the child
is cancelled; if it still cannot be read, `cancelRun` itself logs the
skipped cancellation at `error`, naming the run. If there really is no
suspension under that id, `cancelRun` returns `false` and records
nothing.
- The refusal is unchanged and still thrown after the cancel (code,
message, un-routability).
- Item (2), option B, comments only: `subflow-region-guard.ts` and
`region-suspension-refusal.ts` docblocks now record that the region
arming decline is one level deep by design. A child's region call fails
the CHILD run; its caller meets an ordinary, routable child failure and
stays armed; keeping the refusal un-routable one level up would be a
separate narrowing decision.
- Changeset: `.changeset/29166-region-refusal-empty-chain-cancel.md`,
`@objectstack/service-automation` patch.
## Tests
Two cases added to `src/subflow-region-suspension.test.ts` (no existing
test edited), with a store whose `load` fails for the child flow's runs:
- **unreadable store** (always fails): the refusal text is unchanged,
the child row is still in the store, `cancelRun` is called exactly once
with the child run id, and exactly one `error` record is `cancelRun`'s
own skipped-cancellation line for that id.
- **store that answers again by the cancel** (fails once per run id):
`cancelRun` is called once and the child run ends `cancelled`, with
nothing left parked.
Red first, then ablation (removing the new `cancelRun` call) — readings
below.
## Acceptance notes
- **Item (2): option B, by seat ruling (card comment 6576815635).**
Measured on a scratch run at this branch (not committed): caller
`deal_submit` has a top-level `subflow` to `deal_review`, whose `loop`
body calls `deal_ask`, which suspends at a `wait`. The caller arms.
Without a `fault` edge on its `subflow` node, its run fails loudly
(`Node 'top_call' failed: subflow 'deal_review' failed: durable pause
inside a structured region …`), `deal_review` ends `failed`, `deal_ask`
ends `cancelled`, nothing parked. **With** a `fault` edge on that node,
the caller's run completes with `success: true`: the child's refusal
reaches the caller as an ordinary child failure, which the `subflow`
contract routes. Declining such a caller (option A) would refuse a flow
its author routed and that completes today, so the seat ruled B: the
decline stays one level deep, recorded in the docblocks above. No
behaviour change for item (2).
- Partial chains are out of this PR: if the child is read but a deeper
link's read fails, the chain stops at the last readable link and that
deeper run is not cancelled (the degrading read's `warn` is the only
record). The seat filed that as #29333.
## Verification
<details><summary>Red first (fix absent)</summary>
```
× an unreadable store: … the child run gets exactly one cancelRun, …
× a store that answers again by the cancel: …
AssertionError: expected "cancelRun" to be called 1 times, but got 0 times
Tests 2 failed | 20 passed (22)
```
</details>
<details><summary>Green, then ablation (scripts/ablation-replace.mjs,
on-disk proof)</summary>
```
green: Tests 22 passed (22)
ablation-replace: anchor "await this.cancelRun(childRunId, reason);" x1 -> x0
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 0395cdb -> 16347308d964
× an unreadable store … × a store that answers again …
AssertionError: expected "cancelRun" to be called 1 times, but got 0 times
Tests 2 failed | 20 passed (22)
ablation-replace: ok restored: blob == HEAD (0395cdb) and `git diff HEAD` is empty
```
The test imports `./engine.js` from `src`, so no `dist` leg applies.
</details>
<details><summary>Final head 7aa1787 (comments-only commit on top of
36b3678)</summary>
```
turbo build ./packages/** then, under os-verify-lock (VERDICT command-exit 0):
vitest run src/subflow-region-suspension.test.ts src/subflow-guard-region-calls.test.ts Test Files 2 passed (2) · Tests 37 passed (37)
pnpm --filter @objectstack/service-automation typecheck exit 0 (check:test-typecheck OK, 0 pinned)
dispatch-gates --ran: 61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN (all exit 0)
```
```
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-tenant-audit-census.mjs :: exit 0
node scripts/check-tenant-audit-census.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:durability-log-level :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
```
</details>
<details><summary>Earlier head 36b3678 (after merging main) — full
package run</summary>
```
pnpm --filter @objectstack/service-automation test Test Files 259 passed (259) · Tests 3337 passed (3337)
pnpm --filter @objectstack/service-automation typecheck exit 0
eslint --no-inline-config on the two changed .ts files exit 0
dispatch-gates --ran: 61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN (all exit 0)
```
On 7e91f5a, before the merge, the same 61 were green. Two of them first
answered exit 3: `check-plugin-teardown-shape --self-test` (the fixture
commit was not fetched) and `check:dual-build-cjs-loads` (the packages
were not built). Both were re-run green after `git fetch origin
621a487` and a `./packages/**` build.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADHkmSmWmYVtGXXtTcxTr3)_
<!-- signed: domain:server#3 · os-steve ·
session_01ADHkmSmWmYVtGXXtTcxTr3 -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d8b7ef4 commit 239e3a8
5 files changed
Lines changed: 118 additions & 0 deletions
File tree
- .changeset
- packages/services/service-automation/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
860 | 860 | | |
861 | 861 | | |
862 | 862 | | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
| 871 | + | |
| 872 | + | |
| 873 | + | |
| 874 | + | |
863 | 875 | | |
864 | 876 | | |
865 | 877 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
40 | 46 | | |
41 | 47 | | |
42 | 48 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
24 | 36 | | |
25 | 37 | | |
26 | 38 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
24 | 31 | | |
25 | 32 | | |
26 | 33 | | |
27 | 34 | | |
28 | 35 | | |
29 | 36 | | |
30 | 37 | | |
| 38 | + | |
| 39 | + | |
31 | 40 | | |
32 | 41 | | |
33 | 42 | | |
| |||
283 | 292 | | |
284 | 293 | | |
285 | 294 | | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
0 commit comments