@zhushanwen/subagent-core 0.7.1 → 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 (176) hide show
  1. package/dist/{chunk-VURUAGMM.js → chunk-LZJOEDGQ.js} +603 -154
  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 +325 -154
  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 +9799 -7954
  10. package/dist/index.d.cts +1896 -1360
  11. package/dist/index.d.ts +1896 -1360
  12. package/dist/index.js +8952 -7513
  13. package/dist.bundle/index.cjs +7897 -6054
  14. package/package.json +5 -5
  15. package/src/__tests__/manifest-store.test.ts +16 -15
  16. package/src/__tests__/record-store-last-line.test.ts +12 -0
  17. package/src/__tests__/robustness-medium-batch1.test.ts +10 -14
  18. package/src/__tests__/robustness-medium-batch2.test.ts +7 -2
  19. package/src/core/logger.ts +1 -1
  20. package/src/execution/__tests__/alive-store.test.ts +117 -1
  21. package/src/execution/__tests__/batch-finalized.test.ts +206 -0
  22. package/src/execution/__tests__/chat-engine-routing.test.ts +117 -1
  23. package/src/execution/__tests__/{cold-resurrect.test.ts → cold-lookup.test.ts} +133 -33
  24. package/src/execution/__tests__/collect-coordinator-service.test.ts +7 -5
  25. package/src/execution/__tests__/conversation-continuation.test.ts +1452 -0
  26. package/src/execution/__tests__/delivery-methods.test.ts +90 -176
  27. package/src/execution/__tests__/dialog-queue.test.ts +70 -3
  28. package/src/execution/__tests__/dispose-manifest-recovery.test.ts +475 -0
  29. package/src/execution/__tests__/execute-and-await-worktree.test.ts +3 -3
  30. package/src/execution/__tests__/execute-nesting.test.ts +8 -5
  31. package/src/execution/__tests__/execution-record.test.ts +8 -148
  32. package/src/execution/__tests__/finalize-record.test.ts +295 -129
  33. package/src/execution/__tests__/get-record-for-action-restart.test.ts +21 -29
  34. package/src/execution/__tests__/helpers/fake-engine-port.ts +13 -68
  35. package/src/execution/__tests__/helpers/legacy-sidecar.ts +33 -0
  36. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +9 -5
  37. package/src/execution/__tests__/idle-gc.test.ts +60 -0
  38. package/src/execution/__tests__/inflight-production-wiring.test.ts +223 -0
  39. package/src/execution/__tests__/lifecycle-manager.test.ts +3 -254
  40. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +43 -22
  41. package/src/execution/__tests__/model-config-service.test.ts +111 -0
  42. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +10 -6
  43. package/src/execution/__tests__/nested-visibility.test.ts +13 -1
  44. package/src/execution/__tests__/notify-ledger.test.ts +42 -1
  45. package/src/execution/__tests__/rebuild-indexes.test.ts +329 -0
  46. package/src/execution/__tests__/record-binding.test.ts +613 -0
  47. package/src/execution/__tests__/record-origin.test.ts +318 -0
  48. package/src/execution/__tests__/record-store-intent-api.test.ts +600 -0
  49. package/src/execution/__tests__/record-store-orphan-revive.test.ts +71 -3
  50. package/src/execution/__tests__/record-store.test.ts +72 -54
  51. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +8 -5
  52. package/src/execution/__tests__/round-supervisor-workflow-origin.test.ts +205 -0
  53. package/src/execution/__tests__/run-orchestration-write-lease.test.ts +293 -0
  54. package/src/execution/__tests__/run-orchestration.test.ts +199 -0
  55. package/src/execution/__tests__/session-file-gc.test.ts +35 -4
  56. package/src/execution/__tests__/session-reconstructor.test.ts +78 -0
  57. package/src/execution/__tests__/state-marker.test.ts +288 -0
  58. package/src/execution/__tests__/stream-sink-retirement.test.ts +8 -5
  59. package/src/execution/__tests__/subagent-actions-core.test.ts +67 -16
  60. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +0 -1
  61. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +27 -11
  62. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +17 -11
  63. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +130 -53
  64. package/src/execution/__tests__/subagent-service.test.ts +17 -215
  65. package/src/execution/__tests__/subprocess-agent-runner-no-progress-killall.test.ts +138 -0
  66. package/src/execution/__tests__/subprocess-agent-runner.test.ts +105 -660
  67. package/src/execution/__tests__/sync-collect-recovery.test.ts +23 -16
  68. package/src/execution/__tests__/terminal-write-latency.test.ts +79 -0
  69. package/src/execution/__tests__/workflow-agent-dispatch.test.ts +782 -0
  70. package/src/execution/alive-store.ts +37 -28
  71. package/src/execution/{cold-resurrect.ts → cold-lookup.ts} +48 -45
  72. package/src/execution/config.ts +1 -1
  73. package/src/execution/conversation-continuation.ts +541 -0
  74. package/src/execution/dialog-queue.ts +60 -12
  75. package/src/execution/engine/__tests__/common/capability-gate.test.ts +1 -1
  76. package/src/execution/engine/__tests__/common/journal-wiring.test.ts +16 -1
  77. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +17 -51
  78. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +0 -5
  79. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +1 -1
  80. package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +5 -9
  81. package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +55 -53
  82. package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +0 -3
  83. package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -3
  84. package/src/execution/engine/__tests__/inflight-snapshot.test.ts +180 -0
  85. package/src/execution/engine/__tests__/model-prompt.test.ts +0 -3
  86. package/src/execution/engine/__tests__/registry.test.ts +0 -1
  87. package/src/execution/engine/__tests__/routing.test.ts +0 -1
  88. package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +0 -3
  89. package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +0 -3
  90. package/src/execution/engine/client/__tests__/engine-client-reap.test.ts +212 -0
  91. package/src/execution/engine/client/__tests__/mirror.test.ts +73 -1
  92. package/src/execution/engine/client/__tests__/protocol-closure.test.ts +2 -9
  93. package/src/execution/engine/client/__tests__/remote-engine.test.ts +46 -12
  94. package/src/execution/engine/client/engine-client.ts +111 -26
  95. package/src/execution/engine/client/mirror.ts +9 -0
  96. package/src/execution/engine/client/remote-engine.ts +52 -34
  97. package/src/execution/engine/client/reverse-router.ts +6 -27
  98. package/src/execution/engine/common/__tests__/run-signals.test.ts +79 -0
  99. package/src/execution/engine/common/capability-gate.ts +27 -0
  100. package/src/execution/engine/common/data-dir.ts +3 -2
  101. package/src/execution/engine/common/errors.ts +1 -1
  102. package/src/execution/engine/common/journal-wiring.ts +18 -9
  103. package/src/execution/engine/common/run-signals.ts +83 -0
  104. package/src/execution/engine/d8-compat.ts +1 -1
  105. package/src/execution/engine/host/__tests__/host-bridge.test.ts +3 -18
  106. package/src/execution/engine/host/host-bridge.ts +26 -41
  107. package/src/execution/engine/host/pi-host-binding.ts +5 -5
  108. package/src/execution/engine/host/spawned-children.ts +1 -1
  109. package/src/execution/engine/inflight-snapshot.ts +92 -0
  110. package/src/execution/engine/port.ts +52 -54
  111. package/src/execution/engine/types.ts +11 -14
  112. package/src/execution/execution-record.ts +15 -47
  113. package/src/execution/finalize-record.ts +94 -197
  114. package/src/execution/idle-gc.ts +9 -3
  115. package/src/execution/lifecycle-manager.ts +25 -361
  116. package/src/execution/lifecycle-predicates.ts +22 -19
  117. package/src/execution/manifest-store.ts +40 -42
  118. package/src/execution/model-config-service.ts +1 -1
  119. package/src/execution/notifier.ts +39 -1
  120. package/src/execution/notify-host.ts +13 -4
  121. package/src/execution/path-encoding.ts +6 -0
  122. package/src/execution/record-entry.ts +20 -2
  123. package/src/execution/record-store.ts +925 -160
  124. package/src/execution/relay-env.ts +8 -3
  125. package/src/execution/round-supervisor/domain.ts +17 -5
  126. package/src/execution/round-supervisor/service-binding.test.ts +89 -53
  127. package/src/execution/round-supervisor/service-binding.ts +71 -21
  128. package/src/execution/round-supervisor/supervisor.ts +23 -9
  129. package/src/execution/service/record-access.ts +547 -0
  130. package/src/execution/service/record-lifecycle.ts +542 -0
  131. package/src/execution/service/run-orchestration.ts +1647 -0
  132. package/src/execution/service/service-bootstrap.ts +111 -0
  133. package/src/execution/service/service-constants.ts +27 -0
  134. package/src/execution/service/session-baselines.ts +401 -0
  135. package/src/execution/service/sync-collect-domain.ts +442 -0
  136. package/src/execution/service/workflow-dispatch.ts +567 -0
  137. package/src/execution/session-file-gc.ts +11 -8
  138. package/src/execution/session-reconstructor.ts +44 -1
  139. package/src/execution/sessions-index.ts +20 -3
  140. package/src/execution/settled-watchdog.ts +86 -43
  141. package/src/execution/state-marker.ts +516 -0
  142. package/src/execution/stream-sink.ts +1 -1
  143. package/src/execution/subagent-actions-core.ts +53 -31
  144. package/src/execution/subagent-service.ts +643 -2955
  145. package/src/execution/subprocess-agent-runner.ts +46 -323
  146. package/src/execution/types.ts +60 -43
  147. package/src/execution/ui-request-handler-factory.ts +5 -0
  148. package/src/execution/worktree-manager.ts +1 -1
  149. package/src/execution/worktree-registry.ts +12 -8
  150. package/src/index.ts +16 -5
  151. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -3
  152. package/src/orchestration/__tests__/agent-call-stream.test.ts +24 -49
  153. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +4 -4
  154. package/src/orchestration/__tests__/file-run-store.test.ts +5 -7
  155. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +2 -3
  156. package/src/orchestration/__tests__/run-snapshot.test.ts +27 -43
  157. package/src/orchestration/__tests__/worker-message-pump-workflow-dispatch.test.ts +274 -0
  158. package/src/orchestration/execute-agent-call.ts +1 -1
  159. package/src/orchestration/file-run-store.ts +1 -1
  160. package/src/orchestration/models/ports.ts +20 -13
  161. package/src/orchestration/models/types.ts +11 -13
  162. package/src/orchestration/run-snapshot.ts +18 -22
  163. package/src/orchestration/worker-message-pump.ts +33 -57
  164. package/src/shared/atomic-write.ts +10 -8
  165. package/dist/chunk-T2SYBW3I.js +0 -24
  166. package/dist/engine-discovery-scan-ocpM8FmI.d.cts +0 -1608
  167. package/dist/engine-discovery-scan-ocpM8FmI.d.ts +0 -1608
  168. package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +0 -255
  169. package/src/execution/__tests__/finalized-marker.test.ts +0 -104
  170. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
  171. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -488
  172. package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +0 -75
  173. package/src/execution/__tests__/tombstone-store.test.ts +0 -73
  174. package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +0 -230
  175. package/src/execution/finalized-marker.ts +0 -70
  176. package/src/execution/tombstone-store.ts +0 -72
@@ -4,10 +4,11 @@
4
4
  // §3.3.5(EnginePort 与中立类型完整契约)+ §3.3.6(EngineHandle/journal/SessionView 格式)。
5
5
  //
6
6
  // [S4 簇 3 收编] 契约类型单源化(type-only):AgentOutcome / EngineHandleData /
7
- // ReplayedTurn / SessionView / EngineCapabilities / ProbeReport / InteractAction /
8
- // InteractResult 的本地定义已删除,自 @zhushanwen/subagent-engine-sdk re-export
9
- // (SDK protocol/contract-types.ts 是类型闭包 SSOT,core 反向 re-export 保上层消费面
10
- // ——import 方路径零改动;结构等价由 protocol-closure.test.ts 断言族守卫)。
7
+ // ReplayedTurn / SessionView / EngineCapabilities / ProbeReport 的本地定义已删除,
8
+ // @zhushanwen/subagent-engine-sdk re-export(SDK protocol/contract-types.ts 是
9
+ // 类型闭包 SSOT,core 反向 re-export 保上层消费面——import 方路径零改动;结构等价
10
+ // protocol-closure.test.ts 断言族守卫)。[H1 U5/U6] InteractAction / InteractResult
11
+ // 已随 chat 域 interact 面退役删除。
11
12
  // EngineHandle 留守本地:SDK 无此类型(协议面用裸 data 载荷),宿主侧句柄包装接口
12
13
  // 是 core 域概念。
13
14
  //
@@ -20,7 +21,7 @@
20
21
 
21
22
  import type { EngineHandleData } from "@zhushanwen/subagent-engine-sdk";
22
23
 
23
- // AgentEvent 8 种事件:权威定义在 SDK contract-types,经 execution/types.ts re-export
24
+ // AgentEvent 9 种事件:权威定义在 SDK contract-types,经 execution/types.ts re-export
24
25
  // → shared/agent-event.ts 转发层 → 本文件(链条保持,维持「shared/ 是类型共享层」的
25
26
  // 架构约定)。新增粗粒度约束(coarse 引擎至少合成一次 message_end + 一次 turn_end)
26
27
  // 由 conformance 套件断言(P4),不在类型层编码。
@@ -34,26 +35,22 @@ export type {
34
35
  AgentOutcome,
35
36
  EngineCapabilities,
36
37
  EngineHandleData,
37
- InteractAction,
38
- InteractResult,
39
38
  ProbeReport,
40
39
  ReplayedTurn,
41
40
  SessionView,
42
41
  } from "@zhushanwen/subagent-engine-sdk";
43
42
 
44
43
  // ============================================================
45
- // EngineHandle(run/interact/read 三面的连接件,D1/§3.3.6)——core 域类型留宿主侧
44
+ // EngineHandle(run/read 两面的连接件,D1/§3.3.6)——core 域类型留宿主侧
46
45
  // ============================================================
47
46
 
48
47
  /**
49
- * 引擎会话句柄(run 返回、interact/read 入参)。
48
+ * 引擎会话句柄(run 返回、read 入参;[H1 U5/U6] interact 面退役后不再有 interact
49
+ * 消费方)。
50
50
  *
51
51
  * 契约三条(D1):不透明(上层不解构——唯一例外是 record 持久化层序列化 data 字段与
52
- * read 降级链)、可持久化(data 是纯 JSON,主会话 reload 后 read/interact 仍可用)、
53
- * 自描述(data 含 engineId + 引擎 session 定位符 + pool key + adapter 版本)。
54
- *
55
- * 对进程已死的 handle 调 interact 必须返回 engine_session_not_resumable(指向 cold
56
- * resume 路径),而非笼统失败——由各引擎 interact 实现保证。
52
+ * read 降级链)、可持久化(data 是纯 JSON,主会话 reload 后 read 仍可用)、自描述
53
+ * data 含 engineId + 引擎 session 定位符 + pool key + adapter 版本)。
57
54
  */
58
55
  export interface EngineHandle {
59
56
  /** 持久化数据。上层不得解构其内部字段(见契约三条)。 */
@@ -470,6 +470,10 @@ export function updateFromEvent(record: ExecutionRecord, event: AgentEvent): voi
470
470
  case "compaction":
471
471
  return;
472
472
 
473
+ // ── activity:纯活性信号,reducer no-op(协议语义见 SDK contract-types)──
474
+ case "activity":
475
+ return;
476
+
473
477
  default: {
474
478
  // 穷尽性检查:新增 AgentEvent variant 时编译期报错
475
479
  const _exhaustive: never = event;
@@ -612,50 +616,10 @@ export function getFullText(record: ExecutionRecord): string {
612
616
  .join("\n\n");
613
617
  }
614
618
 
615
- /**
616
- * [增量通知] 从 fromTurnIndex 起聚合 turn 文本(getFullText 的增量视图)。
617
- *
618
- * 语义:record.turns.slice(Math.max(0, fromTurnIndex)) text 数组
619
- * filter(text => text.length > 0)(判据与 getFullText 逐字对齐——空白串 " " 按非空
620
- * 处理,不发散)后 join("\n\n")。
621
- *
622
- * 不变式(单测锁死):
623
- * - fromTurnIndex = 0 与 getFullText(record) 逐字节等价(one-shot/首轮回退零成本)
624
- * - fromTurnIndex >= turns.length 返回 ""(不抛错、不回绕)
625
- * - fromTurnIndex < 0 规范化为 0(防御,Math.max 不产生 slice 负偏移语义)
626
- *
627
- * 纯函数只读;getFullText 本体与既有调用方(collectResult 等)零改动。
628
- * 唯一调用点 onRoundSettled 的增量派生(record.roundBaseTurnIndex ?? 0)。
629
- */
630
- export function getFullTextFrom(record: ExecutionRecord, fromTurnIndex: number): string {
631
- return record.turns
632
- .slice(Math.max(0, fromTurnIndex))
633
- .map((t) => t.text)
634
- .filter((text) => text.length > 0)
635
- .join("\n\n");
636
- }
637
-
638
- /**
639
- * [增量通知] 轮次边界推进公式(D1):返回下一轮增量的 turns[] 起始下标。
640
- *
641
- * record.turns.length - (末 turn 存在且 !closed 且 text.length === 0 ? 1 : 0)
642
- *
643
- * - 末 turn 已闭合(pi 现序常态:带 usage 的 message_end 恒先于 turn_end,settle 时
644
- * 全闭合)→ 返回 length,下一轮从新 turn 起。
645
- * - 末 turn 是滞后 message_end 开出的空 turn(防御分支,pi 现序不可达)→ 不计入边界
646
- * (-1),留在下一轮增量内:新轮首个 text_delta 经 currentTurn 复用该空 turn,复用
647
- * 累积被 slice(from) 覆盖;若直用 turns.length 会把这段文本挤出 slice 范围静默丢失(D1)。
648
- * - 末 turn 未闭合且 text 非空(防御形态,pi 现序不可达)→ 计入本轮返回 length(该形态
649
- * 即 onRoundSettled 推进前观测哨 logger.warn 的触发条件)。
650
- *
651
- * 纯函数只读不写 record;唯一调用点 onRoundSettled 第 5 步(notify 之后推进)。
652
- */
653
- export function nextRoundBaseTurnIndex(record: ExecutionRecord): number {
654
- const last = record.turns[record.turns.length - 1];
655
- const trailingOpenEmpty =
656
- last !== undefined && !last.closed && last.text.length === 0 ? 1 : 0;
657
- return record.turns.length - trailingOpenEmpty;
658
- }
619
+ // [H1 U6 / D7 ③] getFullTextFrom / nextRoundBaseTurnIndex(增量通知 base 死记账族)
620
+ // 已退役删除:唯一调用点 onRoundSettled(inproc 时代)消亡后生产零调用,写点
621
+ // settleChatRoundFromResponse 随载体删除;「notify 失败 base 不推进」防丢文本语义由
622
+ // record.result 恒写承接(D7 退役不迁移)。getFullText 本体与既有调用方零改动。
659
623
 
660
624
  /**
661
625
  * 聚合所有 turn 的 toolCalls(扁平化),并 strip InternalToolCall 的内部字段。
@@ -990,8 +954,8 @@ function translateMessageUpdate(raw: JsonlEvent): AgentEvent[] {
990
954
  /**
991
955
  * 把一条 JSONL 事件翻译成 AgentEvent。
992
956
  *
993
- * 返回 undefined 表示该事件不映射到任何 AgentEvent(如 session header、message_start
994
- * tool_execution_update),调用方应跳过。
957
+ * 返回 undefined 表示该事件不映射到任何 AgentEvent(如 session header、message_start),
958
+ * 调用方应跳过。
995
959
  *
996
960
  * 一个 JSONL 事件可能产出**多条** AgentEvent(message_end 的 usage + error 各一条),
997
961
  * 故返回数组。绝大多数情况长度为 0 或 1;message_end 最多 2 条。
@@ -1003,9 +967,13 @@ export function jsonlToAgentEvent(raw: JsonlEvent): AgentEvent[] {
1003
967
  case "session":
1004
968
  case "message_start":
1005
969
  case "turn_start":
1006
- case "tool_execution_update":
1007
970
  return [];
1008
971
 
972
+ // 工具执行期活性信号(与 pi 侧 spawn-event-translator 的 TOOL_ACTIVITY_EVENT 同
973
+ // 语义):不产数据,只驱动宿主无进展守护刷新。
974
+ case "tool_execution_update":
975
+ return [{ type: "activity" }];
976
+
1009
977
  case "tool_execution_start":
1010
978
  return translateToolExecutionStart(raw);
1011
979
 
@@ -2,32 +2,30 @@
2
2
  //
3
3
  // 时序收尾逻辑(从 subagent-service.ts 提取,降低主文件行数 < 1000 上限)。
4
4
  //
5
- // D-017 时序:collectPatch completeRecord archive cleanup(finalized+worktree+
6
- // aliveMarker+pending注销) manifest(最后 best-effort)。
5
+ // [U2a / §3.1 意图 API 迁移] 终态持久化四件套(.state 写 + entry/archive + manifest +
6
+ // .alive 删)归口 store.markFinalized / markCancelled——本文件降级为**副作用编排层**
7
+ // (collectPatch / completeRecord / worktree cleanup / pending 注销① / onFinalized 钩子,
8
+ // §3.1 副作用归属边界)。文件布局知识(写哪个文件、什么顺序)不再散落此处:D8 v7 写序
9
+ //(.state writeSync 先 → manifest writeSync 后 → .alive 删)由 store 内部单点保证。
7
10
  //
8
- // [Critical #1 / PR #85] cleanup 全部在 manifest 写之前执行——manifest 是 sa- id 反查
9
- // 索引(最新已知快照),非正确性依赖,写失败仅记录不阻断。旧实现 Step 2.5 throw 会跳过
10
- // Step 3 cleanup,导致磁盘满/权限错时 worktree 泄漏 + finalized marker 不写 + alive
11
- // marker 残留 + pending 记账错乱。现 manifest 写移到 Step 4(最后),best-effort
12
- //(console.error + appendEntry,不 throw)。
11
+ // [Critical #1 / PR #85 精神保持] manifest 写失败响亮上报不阻断(logger.error +
12
+ // subagent:manifest-write-failed entry,§3.4 manifest 面)与终态原语返回 false 均不
13
+ // 得跳过 worktree cleanup——磁盘满/权限错时 worktree 泄漏比索引缺失严重;终态写失
14
+ // 败时 record running 形态(磁盘无终态位),下次 boot 孤儿恢复终态化承接(§3.4)。
13
15
  //
14
- // B9 兜底:completeRecord/archive 抛错→后续 cleanup/manifest 仍执行。
16
+ // B9 兜底:completeRecord/终态原语抛错→后续 cleanup 仍执行。
15
17
 
16
18
  import * as fs from "node:fs";
17
19
  import * as path from "node:path";
18
20
 
19
21
  import { getLogger } from "../core/logger.ts";
20
22
 
21
- import { removeAliveMarker } from "./alive-store.ts";
22
23
  import { bestEffort } from "./best-effort.ts";
23
24
  import { completeRecord } from "./execution-record.ts";
24
- import { writeFinalized } from "./finalized-marker.ts";
25
- import type { ManifestStore } from "./manifest-store.ts";
26
25
  import type { ModelConfigService } from "./model-config-service.ts";
27
26
  import { getSubagentSessionDir } from "./path-encoding.ts";
28
27
  import type { RecordStore } from "./record-store.ts";
29
28
  import { readIdentityHeader, readIdentityTail } from "./session-reconstructor.ts";
30
- import { writeCancelledTombstone } from "./tombstone-store.ts";
31
29
  import type { AgentResult, ClosedReason, ExecutionRecord } from "./types.ts";
32
30
  import type { WorktreeManager } from "./worktree-manager.ts";
33
31
 
@@ -35,31 +33,30 @@ const logger = getLogger("subagents");
35
33
 
36
34
  /** doFinalizeRecord 的依赖(从 SubagentService 注入,避免 this 绑定 + 解耦可测试)。 */
37
35
  export interface FinalizeDeps {
38
- manifestStore: ManifestStore;
39
36
  worktreeManager: WorktreeManager;
40
37
  store: RecordStore;
41
38
  modelService: ModelConfigService;
42
- /** Pi ExtensionAPI(仅用 appendEntry 记录 manifest 写失败事件)。null 在 dispose 后。 */
39
+ /** Pi ExtensionAPI(终态写失败的响亮 entry 上报)。null 在 dispose 后。 */
43
40
  pi: { appendEntry?: (type: string, data: unknown) => void } | null;
44
41
  /** pending-notifications 终态注销(绑定 pi.events.emit,由调用方闭包提供)。 */
45
42
  emitUnregister(id: string, status: string): void;
46
43
  /**
47
44
  * [F-5 修复] record 终态化的宿主侧收口钩子(recordId;best-effort,抛错由调用方
48
45
  * 在闭包内自行兜底)。单一汇聚点:doFinalizeRecord 是全部 closed 终态的必经路径
49
- * (finalizeRecord / closeChatIdle / closeAfterRoundSettled / finalizeFailed /
50
- * finalizeAborted / consumeCloseAfterRound),chat 轮路由注销(chatRoundRoutes)
51
- * SubagentService 经此钩子统一执行——此前仅 cancelBackground 注销,chat record
52
- * 经 finalize 链终态化后路由闭包(持 record/stream/守护引用)泄漏。finalizeRoundToIdle
53
- * 不经本钩子(回 idle 非终态,续聊仍需路由)。
46
+ *(finalizeRecord / closeChatIdle / finalizeFailed / finalizeAborted /
47
+ * consumeCloseAfterRound)。收口面现 = Continuation 实例清理([H1 U6] 旧 chat
48
+ * 路由注销面已随 interact 面退役)。finalizeRoundToIdle 不经本钩子(回 idle 非终态,
49
+ * 续聊仍需容器)。
54
50
  */
55
51
  onFinalized?: (recordId: string) => void;
56
52
  /**
57
53
  * [T1/PS-9] subagent sessionDir(getSubagentSessionDir(agentDir, rootCwd),调用方注入)。
58
54
  *
59
55
  * record.sessionFile 缺失(RC-1 握手失败 + LC-4 反查也未命中的残余形态)时用于磁盘
60
- * 反查:按 identity(record.id)扫目录找真实 session 文件,作为 tombstone/finalized
61
- * sidecar removeAliveMarker 的依据——消除「sessionFile 缺失 → 终态原因丢失 +
62
- * alive marker 残留」。undefined = 调用方无法提供(反查跳过,行为退回修复前)。
56
+ * 反查:按 identity(record.id)扫目录找真实 session 文件,作为终态原语
57
+ * (store.markFinalized/markCancelled .state/manifest/.alive 面)的锚点依据——
58
+ * 消除「sessionFile 缺失 → 终态原因丢失 + alive marker 残留」。undefined = 调用方
59
+ * 无法提供(反查跳过,行为退回修复前)。
63
60
  */
64
61
  sessionDir?: string;
65
62
  // [review 修复] 已删除 redeliverPending 回调(MF-1 消费确认制补投):pendingMessages
@@ -98,11 +95,10 @@ function findSessionFileByRecordIdentity(
98
95
  /**
99
96
  * [T1/PS-9] Step 序言:sessionFile 缺失时按 record 身份在 sessionDir 反查回填。
100
97
  *
101
- * 放在一切步骤前,让 archive 投影 / Step 3 marker / Step 4 manifest 统一受益。
102
- * RC-1(握手失败)+ LC-4(收尾反查未命中)的残余形态下,磁盘上 session 文件仍可能
103
- * 真实存在(identity 携带 record.id)——反查命中则 tombstone/finalized sidecar 与
104
- * removeAliveMarker 有了依据,终态原因不再丢失、alive marker 不再残留;未命中则
105
- * 保持旧行为(best-effort 跳过)。
98
+ * 放在一切步骤前,让终态原语(archive 投影 / .state / manifest / .alive release)
99
+ * 统一受益。RC-1(握手失败)+ LC-4(收尾反查未命中)的残余形态下,磁盘上 session
100
+ * 文件仍可能真实存在(identity 携带 record.id)——反查命中则终态原语有了锚点,
101
+ * 终态原因不再丢失、alive marker 不再残留;未命中则保持旧行为(best-effort 跳过)。
106
102
  */
107
103
  function resolveMissingSessionFile(deps: FinalizeDeps, record: ExecutionRecord): void {
108
104
  if (record.sessionFile || !deps.sessionDir) return;
@@ -136,34 +132,9 @@ async function collectPatchIfWorktree(deps: FinalizeDeps, record: ExecutionRecor
136
132
  }
137
133
  }
138
134
 
139
- /**
140
- * Step 3a: finalized/tombstone sidecar(best-effort 幂等,仅 sessionFile 存在时执行)。
141
- * MF-1 fix / v4 B-1: cancelled(closedReason='cancelled')写 tombstone 而非 finalized,
142
- * 防重建丢失 cancelled;其余 reason 写 finalized sidecar(真实 reason 进 sidecar 内容,
143
- * 磁盘重建用它还原 closedReason,不再一律硬编码 gc)。
144
- */
145
- function writeFinalizedOrTombstone(record: ExecutionRecord, closedReason: ClosedReason | undefined): void {
146
- if (!record.sessionFile) return;
147
- try {
148
- if (closedReason === "cancelled") {
149
- writeCancelledTombstone(record.sessionFile, {
150
- id: record.id,
151
- status: "cancelled",
152
- agent: record.agent,
153
- startedAt: record.startedAt,
154
- endedAt: record.endedAt ?? Date.now(),
155
- });
156
- } else {
157
- writeFinalized(record.sessionFile, closedReason);
158
- }
159
- } catch (err) {
160
- bestEffort(err, "writeFinalized/tombstone (finalizeRecord Step3)");
161
- }
162
- }
163
-
164
135
  /**
165
136
  * Step 3b: worktree cleanup(best-effort 幂等,仅 worktree 绑定时执行)。
166
- * [Critical] 绝不能因 manifest 写失败而跳过(否则 worktree 泄漏)。
137
+ * [Critical] 绝不能因终态原语失败(含 manifest 写失败)而跳过(否则 worktree 泄漏)。
167
138
  */
168
139
  async function cleanupWorktreeIfBound(deps: FinalizeDeps, record: ExecutionRecord): Promise<void> {
169
140
  if (!record.worktreeHandle) return;
@@ -174,59 +145,17 @@ async function cleanupWorktreeIfBound(deps: FinalizeDeps, record: ExecutionRecor
174
145
  }
175
146
  }
176
147
 
177
- /** Step 3c: 删 .alive marker(best-effort 幂等,仅 sessionFile 存在时执行)。 */
178
- function removeAliveMarkerIfPresent(record: ExecutionRecord): void {
179
- if (!record.sessionFile) return;
180
- try {
181
- removeAliveMarker(record.sessionFile);
182
- } catch (err) {
183
- bestEffort(err, "removeAliveMarker (finalizeRecord Step3)");
184
- }
185
- }
186
-
187
148
  /**
188
- * Step 4 (last): manifest 持久化(best-effort,不阻断、不 throw)。
149
+ * 时序收尾(D-017,[U2a] 终态持久化面归口 store 意图原语后的编排形态)。
189
150
  *
190
- * [Critical #1] manifest sa- id 反查索引(最新已知快照),不是正确性依赖。本步骤
191
- * 写终态快照;sync 批成功成员不经此处——落标出口(subagent-service
192
- * appendBatchFinalizedEntry)已先行补写,message upgrade 到终态时被本步骤原子
193
- * 覆盖。写失败时仅记录(console.error + appendEntry),绝不让 manifest 写失败
194
- * 跳过 Step 3 cleanup 或抛出打断 finalize 链。旧实现 Step 2.5 throw 会
195
- * 跳过 Step 3 cleanup。task/slug/model 从 ExecutionRecord 抓取(配合
196
- * ManifestRecord 补字段),manifestToSubagent 投影时用真实值而非硬编码空串。
197
- */
198
- async function writeManifestBestEffort(deps: FinalizeDeps, record: ExecutionRecord): Promise<void> {
199
- try {
200
- await deps.manifestStore.writeManifest({
201
- id: record.id,
202
- rootSessionId: record.rootSessionId ?? "",
203
- parentRecordId: record.parentRecordId,
204
- agentName: record.agent,
205
- // v4 B-1: manifest status 统一为 closed(cancelled 折入 closed,区分靠 tombstone sidecar)
206
- status: "closed",
207
- createdAt: record.startedAt,
208
- completedAt: record.endedAt ?? Date.now(),
209
- sessionFile: record.sessionFile,
210
- task: record.task,
211
- slug: record.slug,
212
- model: record.model,
213
- });
214
- } catch (err) {
215
- const msg = err instanceof Error ? err.message : String(err);
216
- logger.error(`[subagent] manifest 写入失败 (record=${record.id}): ${msg}`);
217
- deps.pi?.appendEntry?.("subagent:manifest-write-failed", {
218
- id: record.id,
219
- error: msg,
220
- });
221
- }
222
- }
223
-
224
- /**
225
- * 时序收尾(D-017)。步骤 0→4 全部 best-effort 互不阻断(除 manifest 外都幂等)。
151
+ * 步骤:序言 sessionFile 反查 Step 0 collectPatch → Step 1 completeRecord →
152
+ * Step 2 终态原语(store.markFinalized / markCancelled:.state writeSync + entry/
153
+ * archive + manifest + .alive 删,D8 v7 写序)→ Step 3b worktree cleanup →
154
+ * pending 注销① onFinalized 钩子。
226
155
  *
227
- * [Critical #1] Step 3 cleanup 全部在 Step 4 manifest 之前——manifest 写失败仅 console.error +
228
- * appendEntry,不 throw 不跳过 cleanup。task/slug/model ExecutionRecord 抓取(配合 ManifestRecord
229
- * 补字段),manifestToSubagent 投影真实值而非硬编码空串。
156
+ * [Critical #1] 终态原语失败(返回 false = .state 重试耗尽;或意外抛错)不得跳过
157
+ * worktree cleanup——写失败时 record running(磁盘无终态位),副作用清理照常
158
+ * (幂等);§3.4 响亮 entry 上报由本编排层接线(store 层已 logger.error)。
230
159
  */
231
160
  export async function doFinalizeRecord(
232
161
  deps: FinalizeDeps,
@@ -248,132 +177,100 @@ export async function doFinalizeRecord(
248
177
  bestEffort(err, "completeRecord (finalizeRecord B9)", "error");
249
178
  }
250
179
 
251
- // ── Step 2: archive(B9: 抛错→后续仍执行)──
180
+ // ── Step 2: 终态持久化四件套归口(B1 迁移点)──
181
+ // cancelled 走 markCancelled(.state 载荷 status:"cancelled" + 精确 endedAt),其余
182
+ // markFinalized。两原语内部写序 = .state writeSync 先 → binding/archive/manifest →
183
+ // .alive 删(release 出口①,D8 v7)。生产路径恒 manifestDir 接线(subagent-service
184
+ // 构造点);缺省异步分支仅纯内存测试形态。
185
+ let persisted = false;
252
186
  try {
253
- deps.store.archive(record);
187
+ persisted =
188
+ closedReason === "cancelled"
189
+ ? deps.store.markCancelled(record)
190
+ : deps.store.markFinalized(record, closedReason);
254
191
  } catch (err) {
255
- bestEffort(err, "store.archive (finalizeRecord B9)", "error");
192
+ bestEffort(err, "store terminal primitive (finalizeRecord B9)", "error");
193
+ }
194
+ if (!persisted) {
195
+ // [§3.4 / U1 偏差 6 接线] 终态写失败(重试耗尽)响亮 entry 上报——GUI 通知面腿;
196
+ // 日志 error 级腿已由 state-marker 内部完成。record 留 running 形态(磁盘无终态
197
+ // 位、未 archive),下次 boot 孤儿恢复终态化承接。
198
+ const reasonDesc = closedReason ?? record.closedReason ?? "gc";
199
+ logger.error(
200
+ `[subagent] terminal state write failed after retries (record=${record.id}, reason=${reasonDesc}); ` +
201
+ `record stays running on disk — boot orphan recovery will finalize it`,
202
+ );
203
+ deps.pi?.appendEntry?.("subagent:state-write-failed", {
204
+ id: record.id,
205
+ status,
206
+ closedReason: reasonDesc,
207
+ });
256
208
  }
257
209
 
258
- // ── Step 3: finalized + cleanup + aliveMarker(全部先执行,幂等)──
259
- // [Critical] 清理必须在 manifest 写入之前:worktree cleanup / finalized marker / aliveMarker
260
- // 都是幂等且不可跳过的副作用。绝不能因 manifest 写失败而跳过 worktree cleanup
261
- // (否则 worktree 泄漏)。各件独立 try/catch,互不阻断。
262
- writeFinalizedOrTombstone(record, closedReason);
210
+ // ── Step 3b: worktree cleanup(best-effort,不可被终态写失败跳过)──
263
211
  await cleanupWorktreeIfBound(deps, record);
264
- removeAliveMarkerIfPresent(record);
265
212
 
266
213
  // pending-notifications:终态注销(只记 registry 状态,通知由 BgNotifier 发)
267
214
  // [W4 发射点枚举归属①] 注销合法发射点枚举(设计 D2)第 ① 处:subagent record
268
- // 终态化(finalizeRecord 路径,含监督器放弃)。其余合法发射点:② chatMode 轮末
269
- // idle(doFinalizeRoundToIdle,本文件下方);③ workflow run 终态迁移
270
- // (transition("done") 路径);④ 监督器显式放弃(走本路径,终态化+注销同批);
215
+ // 终态化(finalizeRecord 路径,含监督器放弃)。其余合法发射点:② = store
216
+ // .markRoundIdle 簿记⑧(U5 收口后编排层无直发;setPendingUnregister
217
+ // emitPendingUnregister 唯一发射);③ workflow run 终态迁移
218
+ //(transition("done") 路径);④ 监督器显式放弃(走本路径,终态化+注销同批);
271
219
  // ⑤ 注册对账 sweep 补发(round-supervisor/reconcile-sweep.ts)。进程退出本身
272
220
  // 永远不是注销理由(subagent-service disposeAllRecords 的 emit 属①——其同批
273
221
  // completeRecord+archive 终态化)。
274
222
  deps.emitUnregister(record.id, status);
275
223
 
276
224
  // [F-5 修复] 宿主侧终态收口钩子(chat 轮路由注销的单一汇聚点,见 FinalizeDeps
277
- // .onFinalized 注释)。fire-and-forget:钩子失败不阻断 manifest 收尾(闭包内自查)。
225
+ // .onFinalized 注释)。fire-and-forget:钩子失败不阻断收尾(闭包内自查)。
278
226
  try {
279
227
  deps.onFinalized?.(record.id);
280
228
  } catch (err) {
281
229
  bestEffort(err, "onFinalized hook (finalizeRecord)");
282
230
  }
283
-
284
- // ── Step 4 (last): manifest 持久化(best-effort,不阻断、不 throw)──
285
- await writeManifestBestEffort(deps, record);
286
231
  }
287
232
 
233
+ /**
234
+ * [H1 U2 / D7] 轮终处置的 outcome 入参(判别联合)。
235
+ *
236
+ * 命名与协议 AgentOutcome 同名不同物(设计 D7 v6:kind 判别联合消歧)——
237
+ * kind 判别轮终分流:成功轮正文来源 = Continuation 成功分支的 content(协议
238
+ * AgentOutcome.content 映射产物);失败轮 reason = 失败原因原文。
239
+ */
240
+ export type RoundSettlementOutcome =
241
+ | { kind: "success"; content: string }
242
+ | { kind: "failed"; reason: string };
243
+
288
244
  /**
289
245
  * 对话模式轮次完成收尾:record 进 idle 态(非终态化,等待续聊)。
290
246
  *
291
- * doFinalizeRecord 的关键区别(M2-A idle 语义):
247
+ * [U2a/B5] 轮终簿记全集(①-⑨)归口 store.markRoundIdle——本方法瘦身为编排薄壳。
248
+ * 簿记语义(细节与 result 写入规则见 record-store.markRoundIdle 方法头):
292
249
  * - 不调 completeRecord(record 不冻结,保留 turns[] 等运行时状态供续聊累积)
293
250
  * - 不调 store.archive(record 留内存,getMutable 可查、list 可见)
294
251
  * - 不 cleanup worktree(保留对话模式工作目录)
295
- * - 不写 manifest:轮终回 running-resumable 非终态化,本方法无终态快照可写——
296
- * manifest sa- id 反查索引(最新已知快照),sync 批成员的先行快照由落标出口
297
- * 补写(subagent-service appendBatchFinalizedEntry),此处不重复
298
- * - 删 .alive marker(进程已 SIGTERM 回收,不再是活进程)
299
- * - emitUnregister(进程已死,从 pending 活跃后代差集移除;record 留内存不 archive)
300
- *
301
- * MF-2:设 record.result = result.text(否则 notifier idle 回复正文恒为 "(empty)",
302
- * G1/G2 多轮回复送达不成立)。失败轮次(result.success=false,MF-6 回退 idle 路径)
303
- * error 优先于 text:collectResult 恒带 getFullText 全量正文,text 优先会把失败的
304
- * 恢复指引覆盖成轮内旧正文(Gate B S-B-1 实测:settled watchdog kill 后宿主收到
305
- * 成功形态通知,无从得知挂死与恢复方式)。
306
- *
307
- * 状态:record.status = "idle"(覆盖 tryTransition 设的 done/failed),record.round += 1。
308
- * 各步骤 best-effort 互不阻断(参照 doFinalizeRecord 的 bestEffort 用法)。
252
+ * - 不写 manifest(轮终回 running-resumable 非终态化,无终态快照可写)
253
+ * - **[B5/D3a] `.alive` 不再删除**——写权声明跨轮延续(release = 终态原语或
254
+ * idle-GC 归档两出口;轮终 record 仍 resumable、随时续聊 spawn 写同一
255
+ * sessionFile,删则轮后跨进程防御空窗)
256
+ * - [A3] 终态簿记已冻结(endedAt 已设)的调用由 store 内硬断言 fail-fast
257
+ * (复活终态的调用即 bug——S7 防御),throw 先于 store 簿记⑧的注销发射
309
258
  *
310
- * @param deps doFinalizeRecord 同源(从 SubagentService 注入)
311
- * @param result 本轮 AgentResult(MF-2:result.text 写入 record.result 供 notifier idle 回复)
259
+ * @throws Error record 终态簿记已冻结(completeRecord 已跑)仍被调用(store.markRoundIdle
260
+ * 内硬断言——冻结权威判据 = endedAt 已设,写点枚举与两构造性调用面论证见其方法头)。
312
261
  */
313
262
  export async function doFinalizeRoundToIdle(
314
263
  deps: FinalizeDeps,
315
264
  record: ExecutionRecord,
316
- result: AgentResult,
265
+ outcome: RoundSettlementOutcome,
317
266
  ): Promise<void> {
318
- // MF-2:设 record.result notifier idle 回复正文(否则恒 "(empty)",G1/G2 不成立)。
319
- // [T2-③ / LC-1] 失败轮 error 优先:collectResult 恒带 getFullText 全量正文(即使
320
- // success=false),text 优先会把 watchdog 等失败的恢复指引覆盖成轮内正文(半成品 /
321
- // 回补值),宿主无从得知失败与恢复方式(S-B-1 首轮 settled watchdog 实测)。
322
- // [R2-1] 轮终写点恒写非空:one-shot 空文本成功完成(collectResult getFullText 返回 ""、
323
- // success=true、真实可达,本写点被 subagent-service runAndFinalize 的成功分支共用)首轮
324
- // result 前值 undefined,兜底补 "(empty)" 占位——措辞与 notifier buildLlmContent 的
325
- // `record.result ?? "(empty)"` 兜底同款,通知文案逐字节不变([增量 G2] G4 取舍保持)。
326
- // 轮终信号优先:record.result 非 undefined 是 renderer hasRunning 排除「轮终
327
- // running-resumable」的判据(shared SubagentRecord.result 契约),保持 undefined 会让
328
- // 完成注入后末位 turn 永久「工作中」。续轮(前值存在)沿用前值:one-shot 无增量语义,
329
- // record.result = 该 subagent 最终输出。chatMode 空增量轮 → 固定占位
330
- // "(no output this round)"(D5:增量语义下沿用旧 record.result = 上一轮增量,本轮通知
331
- // 正文 = 上一轮内容,父 agent 误读为原样重复回复)。
332
- let nextResult: string | undefined;
333
- if (result.error && (!result.success || !result.text)) {
334
- nextResult = `round did not complete: ${result.error}`;
335
- } else if (result.text) {
336
- nextResult = result.text;
337
- } else if (record.chatMode) {
338
- nextResult = "(no output this round)";
339
- } else {
340
- nextResult = record.result ?? "(empty)";
341
- }
342
- record.result = nextResult;
343
-
344
- // 删 .alive marker(进程已 SIGTERM 回收)。
345
- // sessionFile 窗口期可能 undefined(极少——对话模式轮次完成意味着 session 已跑过),
346
- // 缺失时跳过但仍设内存 idle(重启后磁盘重建会落到 crashed,边界可接受)。
347
- if (record.sessionFile) {
348
- try {
349
- removeAliveMarker(record.sessionFile);
350
- } catch (err) {
351
- bestEffort(err, "removeAliveMarker (doFinalizeRoundToIdle)");
352
- }
353
- }
354
-
355
- // pending-notifications:进程已死,从活跃后代差集移除(record 留内存不 archive,
356
- // v4 B-1:record 现为 running-resumable,但 pending 注册的是进程活跃性,进程死了需注销)。
357
- deps.emitUnregister(record.id, "running");
358
-
359
- // 状态机(v4 B-1):record 保持 running(旧 idle 折入 running,覆盖 tryTransition 设的 closed,
360
- // 可冷路径 resume),轮次计数 +1。idleSince 时间戳独立保留供 GC 判据。
361
- // [S10] closedReason 同步清除:调用方(runAndFinalize catch / MF-6 分支)先 tryTransition
362
- // 设了 closed+closedReason 再回退 running——不清则 "gc"/"cancelled" 残留在 running record 上,
363
- // 泄漏进 list 投影与后续 notify 载荷(toNotifyRecord 透传 record.closedReason),让一个
364
- // 活跃 record 看起来像已被某原因关闭过。
365
- record.status = "running";
366
- record.closedReason = undefined;
367
- record.round = (record.round ?? 0) + 1;
368
- record.idleSince = Date.now();
369
- // 执行态信号(residual-fixes):轮终回 running-resumable = 无活进程驱动(idle timer
370
- // 回收/保活等待续聊),GUI 侧据此判 waiting(非 streaming)。冷路径续轮(进程启动)清除。
371
- record.resumable = true;
372
-
373
- // W16 [D4]:轮终回 running-resumable 是类外状态写点(record 留内存不走 archive),
374
- // 显式上报迁移——entry 携带新 round 与本轮 result,pi 文件的重建源不滞后。
375
- deps.store.reportRecordTransition(record);
267
+ // 簿记①-⑨归口(含 A3 硬断言与⑨ reportRecordTransition entry 上报)。
268
+ // 返回 false = record 不在 store 内存(debug 留痕,无副作用)——两构造性调用面
269
+ //(Continuation 轮末分流 / settleOneShotOutcome SP-5)的 record 均在内存,false 即
270
+ // 调用方 bug,留痕足够。
271
+ deps.store.markRoundIdle(record.id, outcome);
376
272
 
377
- // [review 修复] 已删除残留 pendingMessages redeliverPending 补投段(MF-1 消费
378
- // 确认安全网):三段消费链随 deliverToRunning 一并移除,本段不可达。
273
+ // [W4 发射点②] pending 注销(进程已死,从活跃后代差集移除)已随 U5 收口进
274
+ // markRoundIdle 簿记⑧——SubagentService 构造点经 setPendingUnregister 注入
275
+ // notifyHost.emitPendingUnregister,本编排层不再双轨重复发射。
379
276
  }
@@ -54,8 +54,10 @@ export interface WorkflowRunGcStore {
54
54
  * 启动 idle record GC 定时器,返回 stop 函数(清理 interval;幂等)。
55
55
  * 每个扫描周期:
56
56
  * - record 面:对 store 内全部 active record 中 resumable 的,锚点(idleSince
57
- * 优先,缺失回退 startedAt——[W4 锚扩展])超过 IDLE_TTL_MS 的归档。archive
58
- * 单条失败不阻断其余(bestEffort 留痕)。**只归档不补注销**(见文件头注)。
57
+ * 优先,缺失回退 startedAt——[W4 锚扩展])超过 IDLE_TTL_MS 的归档
58
+ * ([U2b] markIdleArchived:archive 先 + `.alive` release 后——归档 = 放弃持有
59
+ * 即放弃写权声明,D3a release 出口②)。单条失败不阻断其余(bestEffort 留痕)。
60
+ * **只归档不补注销**(见文件头注)。
59
61
  * - workflow 面(注入 workflowRuns 时):running 且 meta.startedAt 超
60
62
  * IDLE_TTL_MS 的 run 终态化归档(transition + save),单 run 失败不阻断。
61
63
  *
@@ -83,7 +85,11 @@ export function startIdleGc(store: RecordStore, workflowRuns?: WorkflowRunGcStor
83
85
  `[subagents] GC: archiving idle record ${record.id} (idle for ${Math.round(age / MS_PER_DAY)}d)`,
84
86
  );
85
87
  try {
86
- store.archive(record);
88
+ // [U2b / D3a release 出口②] 归口 markIdleArchived:store.archive 先、`.alive`
89
+ // release 后(写序在 store 内部——归档 = 放弃持有 = 放弃写权声明,残留声明
90
+ // 会把 idle 归档后的预期通道 fork-from/新 start 拦死至宿主退出,纯成本零防御
91
+ // 收益;归档 record 后续被接管时统一 acquireWriteLease 重新声明)。
92
+ store.markIdleArchived(record);
87
93
  } catch (err) {
88
94
  bestEffort(err, `GC archive record ${record.id}`);
89
95
  }