@zhushanwen/subagent-core 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/dist/{chunk-OL5BK4VN.js → chunk-LZJOEDGQ.js} +455 -203
  2. package/dist/engine-discovery-scan-Da4Dkzsi.d.cts +818 -0
  3. package/dist/engine-discovery-scan-Da4Dkzsi.d.ts +818 -0
  4. package/dist/execution/engine/engine-discovery-scan.cjs +216 -155
  5. package/dist/execution/engine/engine-discovery-scan.d.cts +1 -2
  6. package/dist/execution/engine/engine-discovery-scan.d.ts +1 -2
  7. package/dist/execution/engine/engine-discovery-scan.js +1 -1
  8. package/dist/execution/relay-env.js +5 -4
  9. package/dist/index.cjs +7686 -5894
  10. package/dist/index.d.cts +1876 -1360
  11. package/dist/index.d.ts +1876 -1360
  12. package/dist/index.js +7048 -5493
  13. package/dist.bundle/index.cjs +7747 -5957
  14. package/package.json +4 -4
  15. package/src/__tests__/manifest-store.test.ts +16 -15
  16. package/src/__tests__/robustness-medium-batch1.test.ts +10 -14
  17. package/src/__tests__/robustness-medium-batch2.test.ts +7 -2
  18. package/src/execution/__tests__/alive-store.test.ts +117 -1
  19. package/src/execution/__tests__/batch-finalized.test.ts +206 -0
  20. package/src/execution/__tests__/chat-engine-routing.test.ts +117 -1
  21. package/src/execution/__tests__/{cold-resurrect.test.ts → cold-lookup.test.ts} +133 -33
  22. package/src/execution/__tests__/collect-coordinator-service.test.ts +7 -5
  23. package/src/execution/__tests__/conversation-continuation.test.ts +1452 -0
  24. package/src/execution/__tests__/delivery-methods.test.ts +90 -176
  25. package/src/execution/__tests__/dialog-queue.test.ts +70 -3
  26. package/src/execution/__tests__/dispose-manifest-recovery.test.ts +475 -0
  27. package/src/execution/__tests__/execute-and-await-worktree.test.ts +3 -3
  28. package/src/execution/__tests__/execute-nesting.test.ts +8 -5
  29. package/src/execution/__tests__/execution-record.test.ts +8 -148
  30. package/src/execution/__tests__/finalize-record.test.ts +295 -129
  31. package/src/execution/__tests__/get-record-for-action-restart.test.ts +21 -29
  32. package/src/execution/__tests__/helpers/fake-engine-port.ts +13 -68
  33. package/src/execution/__tests__/helpers/legacy-sidecar.ts +33 -0
  34. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +9 -5
  35. package/src/execution/__tests__/idle-gc.test.ts +60 -0
  36. package/src/execution/__tests__/lifecycle-manager.test.ts +3 -254
  37. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +43 -22
  38. package/src/execution/__tests__/model-config-service.test.ts +111 -0
  39. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +10 -6
  40. package/src/execution/__tests__/notify-ledger.test.ts +42 -1
  41. package/src/execution/__tests__/rebuild-indexes.test.ts +329 -0
  42. package/src/execution/__tests__/record-binding.test.ts +613 -0
  43. package/src/execution/__tests__/record-origin.test.ts +318 -0
  44. package/src/execution/__tests__/record-store-intent-api.test.ts +600 -0
  45. package/src/execution/__tests__/record-store-orphan-revive.test.ts +71 -3
  46. package/src/execution/__tests__/record-store.test.ts +72 -54
  47. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +8 -5
  48. package/src/execution/__tests__/round-supervisor-workflow-origin.test.ts +205 -0
  49. package/src/execution/__tests__/run-orchestration-write-lease.test.ts +293 -0
  50. package/src/execution/__tests__/run-orchestration.test.ts +199 -0
  51. package/src/execution/__tests__/session-file-gc.test.ts +35 -4
  52. package/src/execution/__tests__/session-reconstructor.test.ts +78 -0
  53. package/src/execution/__tests__/state-marker.test.ts +288 -0
  54. package/src/execution/__tests__/stream-sink-retirement.test.ts +8 -5
  55. package/src/execution/__tests__/subagent-actions-core.test.ts +67 -16
  56. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +0 -1
  57. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +27 -11
  58. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +17 -11
  59. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +130 -53
  60. package/src/execution/__tests__/subagent-service.test.ts +17 -215
  61. package/src/execution/__tests__/subprocess-agent-runner-no-progress-killall.test.ts +138 -0
  62. package/src/execution/__tests__/subprocess-agent-runner.test.ts +105 -660
  63. package/src/execution/__tests__/sync-collect-recovery.test.ts +23 -16
  64. package/src/execution/__tests__/terminal-write-latency.test.ts +79 -0
  65. package/src/execution/__tests__/workflow-agent-dispatch.test.ts +782 -0
  66. package/src/execution/alive-store.ts +37 -28
  67. package/src/execution/{cold-resurrect.ts → cold-lookup.ts} +48 -45
  68. package/src/execution/config.ts +1 -1
  69. package/src/execution/conversation-continuation.ts +541 -0
  70. package/src/execution/dialog-queue.ts +60 -12
  71. package/src/execution/engine/__tests__/common/capability-gate.test.ts +1 -1
  72. package/src/execution/engine/__tests__/common/journal-wiring.test.ts +16 -1
  73. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +17 -51
  74. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +0 -5
  75. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +1 -1
  76. package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +5 -9
  77. package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +0 -3
  78. package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -3
  79. package/src/execution/engine/__tests__/model-prompt.test.ts +0 -3
  80. package/src/execution/engine/__tests__/registry.test.ts +0 -1
  81. package/src/execution/engine/__tests__/routing.test.ts +0 -1
  82. package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +0 -3
  83. package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +0 -3
  84. package/src/execution/engine/client/__tests__/engine-client-reap.test.ts +212 -0
  85. package/src/execution/engine/client/__tests__/mirror.test.ts +73 -1
  86. package/src/execution/engine/client/__tests__/protocol-closure.test.ts +2 -9
  87. package/src/execution/engine/client/__tests__/remote-engine.test.ts +46 -12
  88. package/src/execution/engine/client/engine-client.ts +81 -25
  89. package/src/execution/engine/client/mirror.ts +9 -0
  90. package/src/execution/engine/client/remote-engine.ts +52 -34
  91. package/src/execution/engine/client/reverse-router.ts +6 -27
  92. package/src/execution/engine/common/__tests__/run-signals.test.ts +79 -0
  93. package/src/execution/engine/common/capability-gate.ts +27 -0
  94. package/src/execution/engine/common/data-dir.ts +3 -2
  95. package/src/execution/engine/common/errors.ts +1 -1
  96. package/src/execution/engine/common/journal-wiring.ts +18 -9
  97. package/src/execution/engine/common/run-signals.ts +83 -0
  98. package/src/execution/engine/d8-compat.ts +1 -1
  99. package/src/execution/engine/host/__tests__/host-bridge.test.ts +3 -18
  100. package/src/execution/engine/host/host-bridge.ts +19 -41
  101. package/src/execution/engine/host/pi-host-binding.ts +5 -5
  102. package/src/execution/engine/port.ts +34 -54
  103. package/src/execution/engine/types.ts +11 -14
  104. package/src/execution/execution-record.ts +15 -47
  105. package/src/execution/finalize-record.ts +94 -197
  106. package/src/execution/idle-gc.ts +9 -3
  107. package/src/execution/lifecycle-manager.ts +25 -361
  108. package/src/execution/lifecycle-predicates.ts +22 -19
  109. package/src/execution/manifest-store.ts +40 -42
  110. package/src/execution/model-config-service.ts +1 -1
  111. package/src/execution/notifier.ts +39 -1
  112. package/src/execution/notify-host.ts +13 -4
  113. package/src/execution/record-entry.ts +20 -2
  114. package/src/execution/record-store.ts +925 -160
  115. package/src/execution/relay-env.ts +8 -3
  116. package/src/execution/round-supervisor/domain.ts +17 -5
  117. package/src/execution/round-supervisor/service-binding.test.ts +89 -53
  118. package/src/execution/round-supervisor/service-binding.ts +71 -21
  119. package/src/execution/round-supervisor/supervisor.ts +23 -9
  120. package/src/execution/service/record-access.ts +547 -0
  121. package/src/execution/service/record-lifecycle.ts +542 -0
  122. package/src/execution/service/run-orchestration.ts +1647 -0
  123. package/src/execution/service/service-bootstrap.ts +111 -0
  124. package/src/execution/service/service-constants.ts +27 -0
  125. package/src/execution/service/session-baselines.ts +401 -0
  126. package/src/execution/service/sync-collect-domain.ts +442 -0
  127. package/src/execution/service/workflow-dispatch.ts +567 -0
  128. package/src/execution/session-file-gc.ts +11 -8
  129. package/src/execution/session-reconstructor.ts +44 -1
  130. package/src/execution/sessions-index.ts +20 -3
  131. package/src/execution/settled-watchdog.ts +86 -43
  132. package/src/execution/state-marker.ts +516 -0
  133. package/src/execution/stream-sink.ts +1 -1
  134. package/src/execution/subagent-actions-core.ts +53 -31
  135. package/src/execution/subagent-service.ts +643 -2980
  136. package/src/execution/subprocess-agent-runner.ts +46 -323
  137. package/src/execution/types.ts +60 -43
  138. package/src/execution/ui-request-handler-factory.ts +5 -0
  139. package/src/execution/worktree-manager.ts +1 -1
  140. package/src/execution/worktree-registry.ts +1 -1
  141. package/src/index.ts +6 -5
  142. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -3
  143. package/src/orchestration/__tests__/agent-call-stream.test.ts +24 -49
  144. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +4 -4
  145. package/src/orchestration/__tests__/file-run-store.test.ts +5 -7
  146. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +2 -3
  147. package/src/orchestration/__tests__/run-snapshot.test.ts +27 -43
  148. package/src/orchestration/__tests__/worker-message-pump-workflow-dispatch.test.ts +274 -0
  149. package/src/orchestration/execute-agent-call.ts +1 -1
  150. package/src/orchestration/file-run-store.ts +1 -1
  151. package/src/orchestration/models/ports.ts +20 -13
  152. package/src/orchestration/models/types.ts +11 -13
  153. package/src/orchestration/run-snapshot.ts +18 -22
  154. package/src/orchestration/worker-message-pump.ts +33 -57
  155. package/src/shared/atomic-write.ts +10 -8
  156. package/dist/chunk-T2SYBW3I.js +0 -24
  157. package/dist/engine-discovery-scan-BXLA8y-z.d.cts +0 -1624
  158. package/dist/engine-discovery-scan-BXLA8y-z.d.ts +0 -1624
  159. package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +0 -255
  160. package/src/execution/__tests__/finalized-marker.test.ts +0 -104
  161. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
  162. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -488
  163. package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +0 -75
  164. package/src/execution/__tests__/tombstone-store.test.ts +0 -73
  165. package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +0 -230
  166. package/src/execution/finalized-marker.ts +0 -70
  167. package/src/execution/tombstone-store.ts +0 -72
@@ -1,9 +1,14 @@
1
1
  // src/execution/relay-env.ts
2
2
  //
3
- // relay 通道 env 名与协议常量的 re-export shim——单源在
3
+ // relay 通道 env 名与协议常量的 **semver 子入口载体**——单源在
4
4
  // @zhushanwen/subagent-engine-sdk(round1-reuse R9 跨进程契约副本收编;core → SDK
5
- // 边界正向合法)。本文件保持路径不变以保住 package.json exports 的 ./relay-env
6
- // 子入口(semver 面:runtime/extension 消费方 import specifier 不变)。
5
+ // 边界正向合法)。
6
+ //
7
+ // [L5 收窄] 本文件不再是 core 内部或引擎包的消费路径:core 内消费方(stream-sink /
8
+ // d8-compat)与 pi-subagent-cli 已直连 SDK,pi 包的 relay-env shim 已删除。保留本
9
+ // 文件的唯一理由 = package.json exports 的 ./relay-env 子入口(semver 面:runtime 与
10
+ // subagent-workflow 经它消费,两包当前不依赖 SDK——删本文件须同时给两包加 SDK 依赖
11
+ // 并改 import specifier,属独立的依赖面变更,未随 L5 执行)。
7
12
  // 代理脚本 relay.mjs 仍为内嵌镜像(零依赖脚本不能 import workspace 包),
8
13
  // 镜像一致性由 conformance relay 变体断言锁定。
9
14
 
@@ -27,16 +27,28 @@ export type SupervisorDomain =
27
27
  /** run 域一次性任务(非 chatMode record)——监督域主体。 */
28
28
  | "run"
29
29
  /** conversation/chat 形态(record.chatMode)——豁免,现状机制管辖。 */
30
- | "conversation";
30
+ | "conversation"
31
+ /**
32
+ * [H2 W2] workflow 脚本 agent() record(origin="workflow")——adopt 链豁免域:
33
+ * 引擎死亡即 run 失败即 record 终态化(service 分诊两处豁免),无脚本可回的
34
+ * resumable 等待无意义(adopt 链「唤醒→guidance→2h 看门狗→giveUp」全程死路,
35
+ * 还制造 2h 挂账)。豁免只覆盖 adopt 接管入口(adoptOnProcessDeath / boot 分区
36
+ * 重认领)——运行期记账(noteRunStarted/noteRunEnded)与 reconcile-sweep 对账
37
+ * 对 workflow record 照旧(H1 D8「非 chatMode 全量纳管」不因本豁免收窄)。
38
+ */
39
+ | "workflow";
31
40
 
32
41
  /**
33
42
  * 域分类(裁决表 conversation 行「任何触发不入监督域」的判定锚)。
43
+ * record.origin === "workflow" → workflow(adopt 链豁免域,[H2 W2]);否则
34
44
  * record.chatMode === true → conversation(豁免);否则 → run 域。
35
- * (workflow resumable run 的活性收口 = kill-9 恢复链 + idle-gc startedAt 锚
36
- * 归档 + sweep 对账——workflow run 不在 RecordStore,其注册对账按 type=workflow
45
+ * (workflow run 不在 RecordStore,其注册对账按 type=workflow
37
46
  * 保守跳过,见 reconcile-sweep.ts。)
38
47
  */
39
- export function classifySupervisorDomain(record: Pick<ExecutionRecord, "chatMode">): SupervisorDomain {
48
+ export function classifySupervisorDomain(
49
+ record: Pick<ExecutionRecord, "chatMode" | "origin">,
50
+ ): SupervisorDomain {
51
+ if (record.origin === "workflow") return "workflow";
40
52
  return record.chatMode === true ? "conversation" : "run";
41
53
  }
42
54
 
@@ -66,7 +78,7 @@ export function isAwakeWarrantedShape(
66
78
  /**
67
79
  * boot 分区「already-resumable-idle」重认领谓词(与裁决表同源——设计 D4 连带面 2
68
80
  * ②「启动扫描 resumable 且无进程驱动且非 conversation 形态」的单一权威实现;
69
- * cold-resurrect.ts 的 message 冷查链不承载启动扫描,本谓词即其「cold-resurrect
81
+ * cold-lookup.ts 的 message 冷查链不承载启动扫描,本谓词即其「cold-resurrect
70
82
  * 扩展」落点,record-store 孤儿恢复的保留分支与 supervisor.bootPartition 共同消费
71
83
  * 同一判据形态)。
72
84
  *
@@ -10,11 +10,12 @@
10
10
  // 分支,替身 getMutable 受控注入驱动契约);
11
11
  // - finalizeClosed 异常 best-effort(不向上抛、终止通知仍发);
12
12
  // - superseded:errorText 带替代 id、不发终止通知;
13
- // - 磁盘态(boot 重认领后看门狗到期):writeFinalized sidecar 落盘 +
14
- // reportSubagentRecord 终态 entry(closed/gc/endedAt/error)、无 pi 通知;
15
- // - 磁盘态无 sessionFile / writeFinalized 抛错(sidecar catch)/ 双 miss 收尾
16
- // 竞争 / reportSubagentRecord 抛错(entry catch,须先经 boot 重认领武装
17
- // 看门狗——无候选则 giveUp 不执行,用例空转)。
13
+ // - 磁盘态(boot 重认领后看门狗到期):[U2b] store.markFinalized 归口——
14
+ // `.state` sidecar + subagent-record 终态 entry(closed/gc/endedAt/error)+
15
+ // `.alive` release 一体落齐、无 pi 通知;
16
+ // - 磁盘态无 sessionFile / writeFinalizedState 返回 false(§3.4 零持久化
17
+ // 副作用 + 响亮 entry)/ 双 miss 收尾竞争 / entry 面抛错(appendEntry catch,
18
+ // 须先经 boot 重认领武装看门狗——无候选则 giveUp 不执行,用例空转)。
18
19
  // 2. runPendingReconcileSweepForService 的 lookupRecordState 闭包 subagent 判据
19
20
  // (workflow 判据回归钉在 workflow-state-root.test.ts,此处补 subagent 两方向:
20
21
  // 活跃 record 不被 sweep 注销 + 终态/missing 差集补发注销)。
@@ -32,11 +33,11 @@ import * as path from "node:path";
32
33
 
33
34
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
34
35
 
35
- import { COLD_LOOKUP_SCAN_LIMIT } from "../cold-resurrect.ts";
36
- import * as finalizedMarker from "../finalized-marker.ts";
36
+ import { COLD_LOOKUP_SCAN_LIMIT } from "../cold-lookup.ts";
37
+ import * as stateMarker from "../state-marker.ts";
37
38
  import type { PiLike } from "../notify-host.ts";
38
- import type { RecordStore } from "../record-store.ts";
39
- import type { ExecutionRecord, SubagentRecord } from "../types.ts";
39
+ import { RecordStore } from "../record-store.ts";
40
+ import type { ClosedReason, ExecutionRecord, SubagentRecord } from "../types.ts";
40
41
  import { RoundSupervisor, ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS, type SupervisorCandidateRecord } from "./index.ts";
41
42
  import {
42
43
  createRoundSupervisorForService,
@@ -44,13 +45,13 @@ import {
44
45
  type RoundSupervisorBinding,
45
46
  } from "./service-binding.ts";
46
47
 
47
- // writeFinalized 实装自吞 IO 错(内部 try/catch,finalized-marker.ts),give-up
48
- // sidecarcatch(service-binding.ts:164)是防御分支——真实 ENOENT 无法驱动,
49
- // 仅本文件的 sidecar 抛错用例经模块替身注入抛错;默认委托真实实现,相邻用例行为
50
- // 不变(vi.spyOn 对跨模块具名导入绑定不可拦截,必须走 vi.mock)。
51
- vi.mock("../finalized-marker.ts", async (importOriginal) => {
52
- const actual = await importOriginal<typeof import("../finalized-marker.ts")>();
53
- return { ...actual, writeFinalized: vi.fn(actual.writeFinalized) };
48
+ // writeFinalizedState 实装(U1 后)为响亮重试 + 失败返回 false(不抛——state-marker
49
+ // 内部 3 次退避重试),give-up §3.4 false 分支真实磁盘故障无法稳定驱动,仅本文件
50
+ // 的「重试耗尽」用例经模块替身注入 false 返回;默认委托真实实现,相邻用例行为不变
51
+ //(vi.spyOn 对跨模块具名导入绑定不可拦截,必须走 vi.mock)。
52
+ vi.mock("../state-marker.ts", async (importOriginal) => {
53
+ const actual = await importOriginal<typeof import("../state-marker.ts")>();
54
+ return { ...actual, writeFinalizedState: vi.fn(actual.writeFinalizedState) };
54
55
  });
55
56
 
56
57
  // ============================================================
@@ -92,6 +93,8 @@ function makePi(): TestPi {
92
93
 
93
94
  interface StoreHarness {
94
95
  store: RecordStore;
96
+ /** 真实 RecordStore(磁盘态放弃分支 markFinalized 的委托目标——终态四面真写)。 */
97
+ realStore: RecordStore;
95
98
  memory: Map<string, ExecutionRecord>;
96
99
  disk: Map<string, SubagentRecord>;
97
100
  /** collectRecords 返回的候选(替身投影;默认从 memory 生成)。 */
@@ -100,14 +103,17 @@ interface StoreHarness {
100
103
  reported: SubagentRecord[];
101
104
  }
102
105
 
103
- /** duck-typed RecordStore 替身(service-binding 只消费 getMutable / findLightById /
104
- * collectRecords / reportSubagentRecord 四个读写点)。 */
106
+ /** duck-typed RecordStore 替身(service-binding 消费 getMutable / findLightById /
107
+ * collectRecords / reportSubagentRecord / markFinalized 五个读写点——markFinalized
108
+ * [U2b] 委托真实 store(`.state`/entry/manifest/marker release 真写),其余读点走
109
+ * 替身 map(真实磁盘发现需 session 文件扫描,与本文件断言面无关)。 */
105
110
  function makeStore(): StoreHarness {
106
111
  const memory = new Map<string, ExecutionRecord>();
107
112
  const disk = new Map<string, SubagentRecord>();
108
113
  const collectResult: SupervisorCandidateRecord[] = [];
109
114
  const collectCalls: StoreHarness["collectCalls"] = [];
110
115
  const reported: SubagentRecord[] = [];
116
+ const realStore = new RecordStore(path.join(tmpDir, "sessions"));
111
117
  const store = {
112
118
  getMutable: (id: string) => memory.get(id),
113
119
  findLightById: (id: string) => disk.get(id),
@@ -118,8 +124,10 @@ function makeStore(): StoreHarness {
118
124
  reportSubagentRecord: (record: SubagentRecord) => {
119
125
  reported.push(record);
120
126
  },
127
+ markFinalized: (record: ExecutionRecord, closedReason?: ClosedReason) =>
128
+ realStore.markFinalized(record, closedReason),
121
129
  };
122
- return { store: store as unknown as RecordStore, memory, disk, collectResult, collectCalls, reported };
130
+ return { store: store as unknown as RecordStore, realStore, memory, disk, collectResult, collectCalls, reported };
123
131
  }
124
132
 
125
133
  interface BindingHarness {
@@ -329,6 +337,9 @@ describe("supervisorGiveUp 内存态(watchdog-expired / superseded / CAS)",
329
337
  describe("supervisorGiveUp 磁盘态(boot 重认领后看门狗到期)", () => {
330
338
  function makeDiskBinding(diskRecord: SubagentRecord): BindingHarness {
331
339
  const h = makeBinding({ sessionFile });
340
+ // [U2b] 终态 entry/manifest 面 = markFinalized 经真实 store 落 pi.appendEntry
341
+ //(archive 面),绑定 pi 注入真实 store。
342
+ h.store.realStore.setPi(h.pi ?? null);
332
343
  h.store.disk.set(diskRecord.id, diskRecord);
333
344
  // boot 分区候选(真实 collectRecords 的 running 投影形态)
334
345
  h.store.collectResult.push({
@@ -341,37 +352,54 @@ describe("supervisorGiveUp 磁盘态(boot 重认领后看门狗到期)", ()
341
352
  return h;
342
353
  }
343
354
 
344
- it("writeFinalized sidecar 落盘 + reportSubagentRecord 终态 entry(closed/gc/endedAt/error);磁盘态不发终止通知", async () => {
355
+ /** pi.appended customType=subagent-record 的终态 entry 载荷。 */
356
+ function subagentEntries(pi: ReturnType<typeof makePi> | null): Array<Record<string, unknown>> {
357
+ return (pi?.appended ?? [])
358
+ .filter((a) => a.customType === "subagent-record")
359
+ .map((a) => a.data as Record<string, unknown>);
360
+ }
361
+
362
+ it("markFinalized 归口:.state sidecar + subagent-record 终态 entry(closed/gc/endedAt/error)+ .alive release 一体落齐;磁盘态不发终止通知", async () => {
345
363
  const diskRecord = makeDiskRecord(sessionFile);
346
364
  const h = makeDiskBinding(diskRecord);
365
+ // 持有期声明在位(boot 重认领形态——磁盘 running record 携带写权声明)。
366
+ h.store.realStore.acquireWriteLease(sessionFile, diskRecord.id);
367
+ expect(fs.existsSync(`${sessionFile}.alive`)).toBe(true);
347
368
  const supervisor = createRoundSupervisorForService(h.binding);
348
369
 
349
370
  // 纳管经磁盘兜底视图(内存 Map 空)——boot 分区重认领形态
350
371
  supervisor.bootPartition();
351
372
  await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
352
373
 
353
- // sidecar:sessionFile 旁 .finalized 内容 = "gc";finalizeClosed 不适用(无内存 record)
354
- expect(fs.readFileSync(`${sessionFile}.finalized`, "utf-8")).toBe("gc");
374
+ // sidecar:sessionFile 旁 .state 内容 = {status:finalized, reason:"gc"};finalizeClosed 不适用(无内存 record)
375
+ expect(JSON.parse(fs.readFileSync(`${sessionFile}.state`, "utf-8"))).toEqual({
376
+ status: "finalized",
377
+ reason: "gc",
378
+ });
355
379
  expect(h.finalizeClosed).not.toHaveBeenCalled();
356
380
 
357
- // 终态 entry 落盘(reportSubagentRecord):磁盘 record + 终态语义位
358
- expect(h.store.reported).toHaveLength(1);
359
- const reported = h.store.reported[0]!;
360
- expect(reported).toMatchObject({
381
+ // 终态 entry 落盘(markFinalized → archive → pi.appendEntry):磁盘 record + 终态语义位
382
+ const entries = subagentEntries(h.pi);
383
+ expect(entries).toHaveLength(1);
384
+ expect(entries[0]).toMatchObject({
361
385
  id: "bg-disk",
362
386
  status: "closed",
363
387
  closedReason: "gc",
364
388
  endedAt: expect.any(Number),
365
389
  error: expect.stringContaining("safe to re-dispatch"),
366
390
  });
367
- expect(reported.error).not.toContain("superseded");
391
+ expect(String(entries[0]!["error"])).not.toContain("superseded");
392
+
393
+ // [D3a release 出口①] 终态原语删写权声明——磁盘态放弃同样释放(残留声明会
394
+ // 误拦异宿主接管)。
395
+ expect(fs.existsSync(`${sessionFile}.alive`)).toBe(false);
368
396
 
369
397
  // 磁盘态分支无终止通知(sent 仅 boot 重认领的 decision guidance 一条)
370
398
  expect(h.pi?.sent).toHaveLength(1);
371
399
  expect(h.pi?.sent.every((s) => !s.message.content.includes(TERMINATION_NOTICE_MARK))).toBe(true);
372
400
  });
373
401
 
374
- it("磁盘态无 sessionFile → 跳过 sidecar,仍 reportSubagentRecord 终态化", async () => {
402
+ it("磁盘态无 sessionFile → 跳过 sidecar,仍终态 entry(markFinalized 无锚点降级路径)", async () => {
375
403
  const diskRecord = makeDiskRecord(undefined);
376
404
  const h = makeDiskBinding(diskRecord);
377
405
  const supervisor = createRoundSupervisorForService(h.binding);
@@ -379,19 +407,20 @@ describe("supervisorGiveUp 磁盘态(boot 重认领后看门狗到期)", ()
379
407
  supervisor.bootPartition();
380
408
  await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
381
409
 
382
- expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(false);
383
- expect(h.store.reported).toHaveLength(1);
384
- expect(h.store.reported[0]?.status).toBe("closed");
410
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(false);
411
+ const entries = subagentEntries(h.pi);
412
+ expect(entries).toHaveLength(1);
413
+ expect(entries[0]).toMatchObject({ id: "bg-disk", status: "closed", closedReason: "gc" });
385
414
  });
386
415
 
387
- it("reportSubagentRecord 抛错 best-effort 吞掉不向上抛,sidecar 照常落盘", async () => {
416
+ it("entry 面抛错(pi.appendEntry 炸)→ best-effort 吞掉不向上抛,sidecar 照常落盘", async () => {
388
417
  // 候选入 collectResult → boot 重认领(readopted)武装看门狗——没有这一步
389
418
  // listCandidateRecords 返回空、giveUp 永不执行、entry catch 不可达(MF-R2-2)。
390
419
  const diskRecord = makeDiskRecord(sessionFile);
391
420
  const h = makeDiskBinding(diskRecord);
392
- h.store.store.reportSubagentRecord = () => {
421
+ (h.pi!.appendEntry as ReturnType<typeof vi.fn>).mockImplementation(() => {
393
422
  throw new Error("entry write exploded");
394
- };
423
+ });
395
424
  const supervisor = createRoundSupervisorForService(h.binding);
396
425
 
397
426
  const { readopted } = supervisor.bootPartition();
@@ -400,39 +429,46 @@ describe("supervisorGiveUp 磁盘态(boot 重认领后看门狗到期)", ()
400
429
  await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
401
430
 
402
431
  // sidecar 与终态 entry 是两段独立 best-effort:entry 抛错不回滚已落 sidecar
403
- expect(fs.readFileSync(`${sessionFile}.finalized`, "utf-8")).toBe("gc");
404
- // 终态 entry 写入抛错被 give-up entry catch 吞掉 → reported 空
405
- expect(h.store.reported).toHaveLength(0);
432
+ //(markFinalized 写序 = .state 先、archive/entry 后,D8)。
433
+ expect(JSON.parse(fs.readFileSync(`${sessionFile}.state`, "utf-8"))).toEqual({
434
+ status: "finalized",
435
+ reason: "gc",
436
+ });
437
+ expect(subagentEntries(h.pi)).toHaveLength(0);
406
438
  });
407
439
 
408
- it("writeFinalized 抛错 best-effort 吞掉,终态 entry 照常落盘", async () => {
409
- const diskRecord = makeDiskRecord(path.join(tmpDir, "gone-dir", "s.jsonl"));
440
+ it("writeFinalizedState 返回 false(重试耗尽)→ 零持久化副作用:无终态 entry、无 state-write-failed 之外的写入面、响亮 entry 上报", async () => {
441
+ const diskRecord = makeDiskRecord(sessionFile);
410
442
  const h = makeDiskBinding(diskRecord);
411
443
  const supervisor = createRoundSupervisorForService(h.binding);
412
- // 模块替身注入抛错(见文件头 vi.mock 注释);finally 恢复委托真实实现。
413
- const actual = await vi.importActual<typeof import("../finalized-marker.ts")>(
414
- "../finalized-marker.ts",
444
+ // 模块替身注入失败返回(U1 writeFinalizedState 不抛——重试耗尽返回 false);
445
+ // finally 恢复委托真实实现。
446
+ const actual = await vi.importActual<typeof import("../state-marker.ts")>(
447
+ "../state-marker.ts",
415
448
  );
416
- const writeSpy = vi.mocked(finalizedMarker.writeFinalized);
417
- writeSpy.mockImplementation(() => {
418
- throw new Error("sidecar write exploded");
419
- });
449
+ const writeSpy = vi.mocked(stateMarker.writeFinalizedState);
450
+ writeSpy.mockImplementation(() => false);
420
451
  try {
421
452
  supervisor.bootPartition();
422
453
  await vi.advanceTimersByTimeAsync(ROUND_SUPERVISOR_WATCHDOG_DEFAULT_MS + 1);
423
454
  } finally {
424
- writeSpy.mockImplementation(actual.writeFinalized);
455
+ writeSpy.mockImplementation(actual.writeFinalizedState);
425
456
  }
426
457
 
427
- // 替身确实被 giveUp 消费(否则异常被 writeFinalized 实装自吞,用例假绿)
458
+ // 替身确实被 giveUp 消费(否则用例假绿)
428
459
  expect(writeSpy).toHaveBeenCalled();
429
- expect(fs.existsSync(`${diskRecord.sessionFile}.finalized`)).toBe(false);
430
- expect(h.store.reported).toHaveLength(1);
431
- expect(h.store.reported[0]).toMatchObject({ id: diskRecord.id, status: "closed", closedReason: "gc" });
460
+ // [§3.4] record 留 running:.state 未落 + 无终态 entry(零持久化副作用)
461
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(false);
462
+ expect(subagentEntries(h.pi)).toHaveLength(0);
463
+ // 响亮上报腿:subagent:state-write-failed entry(GUI 通知面,对齐 doFinalizeRecord)
464
+ const failNotices = (h.pi?.appended ?? []).filter((a) => a.customType === "subagent:state-write-failed");
465
+ expect(failNotices).toHaveLength(1);
466
+ expect(failNotices[0]?.data).toMatchObject({ id: diskRecord.id, status: "closed", closedReason: "gc" });
432
467
  });
433
468
 
434
469
  it("giveUp 执行时 record 已被 archive(内存移除 + 磁盘无 light)→ 双 miss 直接 return,零副作用", async () => {
435
470
  const h = makeBinding({ sessionFile });
471
+ h.store.realStore.setPi(h.pi ?? null);
436
472
  const rec = makeRecord();
437
473
  h.store.memory.set("bg-1", rec);
438
474
  // 视图检查(内存 running)之后、giveUp 磁盘兜底之前被 archive 的收尾竞争替身投影
@@ -451,8 +487,8 @@ describe("supervisorGiveUp 磁盘态(boot 重认领后看门狗到期)", ()
451
487
  expect(h.finalizeClosed).not.toHaveBeenCalled();
452
488
  expect(h.pi?.sent).toHaveLength(2); // 纳管期通知照常,终止通知不发(零通知放弃)
453
489
  expect(h.pi?.sent.every((s) => !s.message.content.includes(TERMINATION_NOTICE_MARK))).toBe(true);
454
- expect(h.store.reported).toHaveLength(0);
455
- expect(fs.existsSync(`${sessionFile}.finalized`)).toBe(false);
490
+ expect(subagentEntries(h.pi)).toHaveLength(0);
491
+ expect(fs.existsSync(`${sessionFile}.state`)).toBe(false);
456
492
  });
457
493
  });
458
494
 
@@ -10,10 +10,10 @@
10
10
  // 全部依赖经 RoundSupervisorBinding 惰性闭包注入(session 级状态运行时可变——
11
11
  // 对齐 notify-host.ts createNotifyHost 的 deps 惰性求值先例)。
12
12
 
13
+ import { getLogger } from "../../core/logger.ts";
13
14
  import { bestEffort } from "../best-effort.ts";
14
- import { COLD_LOOKUP_SCAN_LIMIT } from "../cold-resurrect.ts";
15
- import { tryTransition } from "../execution-record.ts";
16
- import { writeFinalized } from "../finalized-marker.ts";
15
+ import { COLD_LOOKUP_SCAN_LIMIT } from "../cold-lookup.ts";
16
+ import { createRecord, tryTransition } from "../execution-record.ts";
17
17
  import { hasLiveProcessHandle } from "../lifecycle-predicates.ts";
18
18
  import { FileRunStore } from "../../orchestration/file-run-store.ts";
19
19
  import { resolvePiWorkflowStateDir } from "../workflow-state-root.ts";
@@ -28,6 +28,8 @@ import {
28
28
  type SupervisorRecordView,
29
29
  } from "./index.ts";
30
30
 
31
+ const logger = getLogger("subagents");
32
+
31
33
  /** 绑定面(SubagentService 供给;全部惰性——见文件头注)。 */
32
34
  export interface RoundSupervisorBinding {
33
35
  getStore(): RecordStore;
@@ -63,6 +65,7 @@ function supervisorRecordView(binding: RoundSupervisorBinding, id: string): Supe
63
65
  resumable: memory.resumable === true,
64
66
  hasResult: memory.result !== undefined,
65
67
  chatMode: memory.chatMode === true,
68
+ origin: memory.origin,
66
69
  rootSessionId: memory.rootSessionId,
67
70
  agent: memory.agent,
68
71
  slug: memory.slug,
@@ -78,6 +81,7 @@ function supervisorRecordView(binding: RoundSupervisorBinding, id: string): Supe
78
81
  resumable: disk.resumable === true,
79
82
  hasResult: disk.result !== undefined,
80
83
  chatMode: disk.chatMode === true,
84
+ origin: disk.origin,
81
85
  rootSessionId: disk.rootSessionId,
82
86
  agent: disk.agent,
83
87
  slug: disk.slug,
@@ -86,7 +90,13 @@ function supervisorRecordView(binding: RoundSupervisorBinding, id: string): Supe
86
90
  };
87
91
  }
88
92
 
89
- /** 本 root session 的 running record 候选(内存 ∪ 磁盘重建投影)。 */
93
+ /** 本 root session 的 running record 候选(内存 ∪ 磁盘重建投影)。
94
+ * [H2 W2] origin 透传——superseded 对账的 workflow 候选豁免判据(supervisor.evaluate
95
+ * 消费面,W1 已把 origin 投影进 SubagentRecord)。【双防注记 I2】本处的 collectRecords
96
+ * 不传 includeWorkflow(缺省过滤 workflow origin),而 supervisor.evaluate 的 superseded
97
+ * 对账另有 workflow 候选豁免判据——后者实为死判据:workflow 候选在本查询已结构性
98
+ * 豁免(缺省过滤),判据永不命中。刻意保留作防御纵深:若未来本查询改传 includeWorkflow
99
+ * 或 store 侧过滤语义变更,evaluate 侧判据立即接管,行为不回退。 */
90
100
  function supervisorCandidates(binding: RoundSupervisorBinding): SupervisorCandidateRecord[] {
91
101
  const rootFilter = binding.getSessionRootId() ?? undefined;
92
102
  return binding
@@ -98,6 +108,7 @@ function supervisorCandidates(binding: RoundSupervisorBinding): SupervisorCandid
98
108
  agent: r.agent,
99
109
  slug: r.slug,
100
110
  startedAt: r.startedAt,
111
+ origin: r.origin,
101
112
  }));
102
113
  }
103
114
 
@@ -109,9 +120,12 @@ function supervisorCandidates(binding: RoundSupervisorBinding): SupervisorCandid
109
120
  * 替代通知已由监督器先行发出。boot 直断不经本函数(孤儿恢复层直断 + in-flight
110
121
  * 的重启中断 error 语义由 finalizeOrphanRecord 落位,见 supervisor.bootPartition 头注)。
111
122
  * - 磁盘态(内存无 + findLightById 命中,boot 重认领后看门狗到期的形态):终态
112
- * entry 落盘(reportSubagentRecord)+ finalized sidecar(best-effort,对齐孤儿
113
- * 恢复形态)——不走 finalizeRecord(无内存 recordarchive/CAS 不适用),其注销
114
- * 由对账 sweep(发射点⑤「record 终态 差集补发」)收口。
123
+ * 写面归口 store.markFinalized([U2b/G1 收口] `.state` writeSync + entry/archive +
124
+ * manifest + `.alive` releaseD8 写序——原 store 外终态 sidecar 直写 + 终态
125
+ * entry 直写两段独立 best-effort 直写点消灭;`.state` 重试耗尽返回
126
+ * false = 零持久化副作用,record 留 running 由 boot 孤儿恢复终态化承接,§3.4)
127
+ * ——不走 finalizeRecord(无内存 record,archive/CAS 不适用),其注销由对账
128
+ * sweep(发射点⑤「record 终态 → 差集补发」)收口。
115
129
  */
116
130
  async function supervisorGiveUp(
117
131
  binding: RoundSupervisorBinding,
@@ -157,21 +171,57 @@ async function supervisorGiveUp(
157
171
  // 磁盘态分支(boot 重认领后的放弃——record 不在内存)。
158
172
  const disk = store.findLightById(recordId);
159
173
  if (disk === undefined) return;
160
- if (disk.sessionFile) {
161
- try {
162
- writeFinalized(disk.sessionFile, "gc");
163
- } catch (err) {
164
- bestEffort(err, `round supervisor give-up sidecar (${recordId})`);
165
- }
166
- }
174
+ // 终态投影 record 由磁盘 light 投影重建(createRecord 先例 = cold-lookup 的
175
+ // resurrectColdRecord:identity 域经 identity 入参、readonly engineHandle 经构造
176
+ // spread、运行态域随后补写——origin/parentRunId 不迁 [workflow origin 豁免 adopt
177
+ // 链,本分支不可达])。markFinalized entry 面 = recordToSubagent 投影,磁盘
178
+ // 投影的 result/turns/totalTokens 等终态快照字段随重建保真。
179
+ const record: ExecutionRecord = {
180
+ ...createRecord(recordId, {
181
+ agent: disk.agent,
182
+ model: disk.model,
183
+ thinkingLevel: disk.thinkingLevel,
184
+ mode: disk.mode,
185
+ task: disk.task,
186
+ slug: disk.slug,
187
+ startedAt: disk.startedAt,
188
+ rootSessionId: disk.rootSessionId,
189
+ parentRecordId: disk.parentRecordId,
190
+ depth: disk.depth,
191
+ chatMode: disk.chatMode === true,
192
+ engine: disk.engine,
193
+ engineFallback: disk.engineFallback,
194
+ collectMode: disk.collectMode,
195
+ }),
196
+ ...(disk.engineHandle !== undefined ? { engineHandle: disk.engineHandle } : {}),
197
+ };
198
+ record.sessionFile = disk.sessionFile;
199
+ record.round = disk.round;
200
+ record.resumable = disk.resumable;
201
+ record.hadWorktree = disk.worktree === true;
202
+ record.result = disk.result;
203
+ record.turnCount = disk.turns;
204
+ record.totalTokens = disk.totalTokens;
205
+ record.error = errorText;
206
+ record.endedAt = Date.now();
207
+ // 防御:createRecord 产物恒 running,CAS 失败不可达(不终态化直接返回)。
208
+ if (!tryTransition(record, "closed", "gc")) return;
167
209
  try {
168
- store.reportSubagentRecord({
169
- ...disk,
170
- status: "closed",
171
- closedReason: "gc",
172
- endedAt: Date.now(),
173
- error: errorText,
174
- });
210
+ const persisted = store.markFinalized(record, "gc");
211
+ if (!persisted) {
212
+ // [§3.4] `.state` 重试耗尽:store 层已 error 级留痕,此处补调用方语境 + GUI
213
+ // 通知面腿(对齐 doFinalizeRecord 同款接线)——record 留 running 形态,boot
214
+ // 孤儿恢复终态化承接。
215
+ logger.error(
216
+ `[subagents] round supervisor give-up terminal write failed (record=${recordId}); ` +
217
+ `record stays running on disk — boot orphan recovery will finalize it`,
218
+ );
219
+ binding.getPi()?.appendEntry?.("subagent:state-write-failed", {
220
+ id: recordId,
221
+ status: "closed",
222
+ closedReason: "gc",
223
+ });
224
+ }
175
225
  } catch (err) {
176
226
  bestEffort(err, `round supervisor give-up entry (${recordId})`, "error");
177
227
  }
@@ -120,6 +120,8 @@ export interface SupervisorRecordView {
120
120
  /** 已有完成产出(record.result !== undefined)——SP-5 upgrade 等待态判据。 */
121
121
  hasResult: boolean;
122
122
  chatMode: boolean;
123
+ /** [H2 W2] 来源身份(adopt 链豁免域判据——classifySupervisorDomain 消费)。 */
124
+ origin?: "tool" | "workflow";
123
125
  rootSessionId: string | undefined;
124
126
  agent: string;
125
127
  slug: string;
@@ -141,6 +143,8 @@ export interface SupervisorCandidateRecord {
141
143
  agent: string;
142
144
  slug: string;
143
145
  startedAt: number;
146
+ /** [H2 W2] 来源身份(superseded 对账的 workflow 候选豁免判据)。 */
147
+ origin?: "tool" | "workflow";
144
148
  }
145
149
 
146
150
  /** RoundSupervisor 的依赖(SubagentService 注入;全部单测替身友好)。 */
@@ -231,9 +235,12 @@ export class RoundSupervisor {
231
235
  for (const candidate of this.deps.listCandidateRecords()) {
232
236
  const view = this.deps.getRecordView(candidate.id);
233
237
  if (view === undefined || view.status !== "running") continue;
234
- if (view.chatMode) continue; // conversation 豁免(现状,轮终 idle 机制管辖)
238
+ // [H2 W2] adopt 链豁免域门(conversation 豁免 + workflow origin 豁免):
239
+ // classifySupervisorDomain 不把 workflow record 归入可 adopt 域("run")——
240
+ // boot 重认领对 workflow 形态不生效(豁免落空面无「重认领后 2h 看门狗挂账」)。
241
+ if (classifySupervisorDomain(view) !== "run") continue;
235
242
  // boot 分区只处理「孤儿恢复保留下来的 resumable 且无完成产出的形态」(W4 死亡
236
- // 纳管态跨重启)——in-flight 已被孤儿恢复直断(见头注时序),不再此处重复
243
+ // 纳管态跨重启)——in-flight 已被孤儿恢复直断(见头注),不再此处重复
237
244
  // 终态化(双收尾防线);resumable 且 result 有值是 SP-5 完成态(已完成挂账,
238
245
  // idle-gc 锚归档收口,无需监督)。谓词与 record-store 孤儿恢复保留分支同源
239
246
  // (isBootReadoptable,domain.ts)。
@@ -314,16 +321,22 @@ export class RoundSupervisor {
314
321
  return;
315
322
  }
316
323
  // 该唤醒 → 通知对账(送指引前查替代)。
324
+ // [H2 W2 / superseded 豁免] workflow origin 候选不参与替代判定——parallel 同
325
+ // slug/同 agent 并行是 workflow 常态而非替代关系(replacedNotice「was already
326
+ // replaced by your new task」会把并行任务误述为替代并错误终态化原任务)。
317
327
  const windowMs = getSupervisorWatchdogMs();
318
328
  const verdict = classifyReplacement(
319
329
  { id: view.id, rootSessionId: view.rootSessionId, agentName: view.agent, slug: view.slug },
320
- this.deps.listCandidateRecords().map((c) => ({
321
- id: c.id,
322
- rootSessionId: c.rootSessionId,
323
- agentName: c.agent,
324
- slug: c.slug,
325
- startedAt: c.startedAt,
326
- })),
330
+ this.deps
331
+ .listCandidateRecords()
332
+ .filter((c) => c.origin !== "workflow")
333
+ .map((c) => ({
334
+ id: c.id,
335
+ rootSessionId: c.rootSessionId,
336
+ agentName: c.agent,
337
+ slug: c.slug,
338
+ startedAt: c.startedAt,
339
+ })),
327
340
  windowMs,
328
341
  this.deps.now(),
329
342
  );
@@ -400,6 +413,7 @@ export class RoundSupervisor {
400
413
  resumable: record.resumable === true,
401
414
  hasResult: record.result !== undefined,
402
415
  chatMode: record.chatMode === true,
416
+ origin: record.origin,
403
417
  rootSessionId: record.rootSessionId,
404
418
  agent: record.agent,
405
419
  slug: record.slug,