@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
@@ -0,0 +1,1647 @@
1
+ // [H3/R4] RunOrchestration 聚合(域 #6/#7/#12/#14/#15:model 解析 + run 域执行入口 +
2
+ // await 入口 + 引擎编排(含 Continuation 协作面)+ pool/worktree 资源)——自
3
+ // SubagentService 上帝类 strangler 抽取的核心编排聚合(设计
4
+ // docs/design/subagent-service-decomposition.md §2.1 / §3.3 D2 抽取序末位;成员归属以
5
+ // r0-inventory.md 清单① + 域分区为准)。
6
+ //
7
+ // [G1 超限预授权拆分 / 偏差 D-R4-1] 主 agent 派发预授权:R4 域段体量大(派发估算
8
+ // 1200+ 物理行),按内聚边界拆两个文件——本文件(核心编排)+ workflow-dispatch.ts
9
+ //(workflow 族独立)。**实测与派发估算不符**:R0 重排后域段实测 1662 物理行 > 两文件
10
+ // 容量上限(2×700),本文件物理超限不可避免(偏差登记待主 agent 追认;备选 = 第三
11
+ // 文件再拆 Continuation 协作面,但拆三后仍余 ~850 物理段)。两文件零互调零 import
12
+ //(G2 / R1 打样模式 3):workflow-dispatch 的跨文件协作(acquirePoolOrFinalize /
13
+ // settleOneShotOutcome / outcomeToAgentResult / releaseRoundResources /
14
+ // resolveChatEnginePort / assertIdleTimeoutMsSafe)经壳 deps 回调指回本聚合实例方法。
15
+ //
16
+ // 单一职责:run 域执行编排——execute/executeAndAwait 入口(路由 → identity → record
17
+ // 创建 → worktree → detached 引擎 run)、引擎死亡分诊(adopt)、终态收口
18
+ //(settleOneShotOutcome 含 D7 workflow origin 分支)、Continuation 协作面
19
+ //(continuations 队列 + chat 轮次主干 kickOffChatRound + 投递/升级 gate)、
20
+ // pool/worktree 资源装配与回收。
21
+ //
22
+ // [R1 打样模式——R4 落地](模式权威定义见 session-baselines.ts 文件头)
23
+ // 1. 依赖注入形态:deps 全晚绑定闭包(构造期零求值)——pi/会话基线运行时可变态
24
+ // (execNesting/sessionRootId/streamSink/uiObservability)经壳 getter 现读;
25
+ // #1 留壳共享依赖(store/manifestStore/modelService/notifyHost/pool/worktreeManager/
26
+ // collectCoordinator)getter 现读同一实例——深绑测试的 FR 替换语义保持。
27
+ // 2. 转发壳写法:壳保留同名方法单行转发(execute/executeAndAwait/resolveModel/
28
+ // canUpgradeToConversation/deliverChatMessage 对外面 + executeWorkflowAgent →
29
+ // WorkflowDispatch);聚合内部互调(executeViaEngine/kickOffEngineRun/runEngineTask/
30
+ // settleOneShotOutcome 族/kickOffChatRound 等)不经壳。
31
+ // 3. 跨聚合边收敛(r0-inventory 清单① C-4/C-5 + B-6):
32
+ // - C-4(壳 dispose 直调 continuations.clear):字段所有权随域迁入本聚合,壳经
33
+ // clearContinuations() 显式接口(R4 兑现)。
34
+ // - C-5(onRecordFinalizedCleanup 跨域汇聚点 + abortContinuationQueue 队列清空):
35
+ // 本体迁入本聚合;RecordLifecycle deps 回调(R3 装配时指壳方法)改指本聚合
36
+ // 显式接口(R4 兑现)。
37
+ // - B-6(roundSupervisor 归属争议):留壳——boot 分区(initSession)与 dispose 时序
38
+ // 消费在壳、装配闭包 finalizeClosed 经壳转发 late-bound(C-6 天然兼容);本聚合
39
+ // 经 deps.getRoundSupervisor() 现读(noteRun*/adoptOnProcessDeath)。
40
+ // 4. 只搬不改:方法体除依赖通道替换(this.X → this.deps.getY())外逐字节保留
41
+ // (审计 /tmp/r4-move-audit.py);r0-inventory 清单② A 通道直写 14 处(#12 域
42
+ // executeAndAwait 1 处 + #14 域 13 处——任务口径 12 处按「#14 域 adopt 三行并 1」
43
+ // 计)原样随迁,H4 收口。
44
+ // 5. 模块常量 SSOT:PRIORITY_BACKGROUND / MS_PER_SECOND / SECONDS_PER_MINUTE 已
45
+ // [R6/D-R4-4] 归一常量叶子文件 service-constants.ts(原两聚合重复声明消除,
46
+ // 改 import 消费);STALE_CHILD_EXIT_WAIT_MS + delay(唯一消费
47
+ // killStaleChildBeforeDispatch)留聚合本文件(单一消费主体)。
48
+
49
+ import { getLogger } from "../../core/logger.ts";
50
+ import { toErrorMessage } from "../../core/error-message.ts";
51
+ import { MAX_TIMER_DELAY_MS } from "../../shared/timer-delay.ts";
52
+
53
+ import type { AgentResult as WorkflowAgentResult, AgentCallOpts } from "../../orchestration/models/types.ts";
54
+ import { mapToWorkflowAgentResult } from "../agent-result-mapper.ts";
55
+ import { bestEffort } from "../best-effort.ts";
56
+ import type { CollectCoordinator } from "../collect-coordinator.ts";
57
+ import type { ConcurrencyPool } from "../concurrency-pool.ts";
58
+ import {
59
+ ConversationContinuation,
60
+ type ContinuationDispatchInput,
61
+ type ContinuationRoundHandlers,
62
+ } from "../conversation-continuation.ts";
63
+ import { project, tryTransition, updateFromEvent } from "../execution-record.ts";
64
+ import { doFinalizeRoundToIdle, type RoundSettlementOutcome } from "../finalize-record.ts";
65
+ import { assertTaskShapeSupported } from "../engine/common/capability-gate.ts";
66
+ import { JOURNAL_INITIAL_POOL_KEY, wireEventJournal } from "../engine/common/journal-wiring.ts";
67
+ import type { ExecutionNestingContext } from "../engine/common/nesting-guard.ts";
68
+ import { PI_POOL_KEY, resolveHostPiEnginePort } from "../engine/host/pi-host-binding.ts";
69
+ import {
70
+ killRecordChildWithEscalation,
71
+ registerSpawnedChildForRecord,
72
+ } from "../engine/host/spawned-children.ts";
73
+ import type { EnginePort, RunContext } from "../engine/port.ts";
74
+ import { splitEngineModelRef } from "../engine/model-validation.ts";
75
+ import { executeOptionsToEngineTaskSpec } from "../engine/host-task-spec.ts";
76
+ import { DEFAULT_ENGINE_ID, getEngine } from "../engine/registry.ts";
77
+ import { type EngineRouteResult, routeEngineForHost } from "../engine/routing.ts";
78
+ import type { AgentOutcome } from "../engine/types.ts";
79
+ import { hasLiveProcessHandle } from "../lifecycle-predicates.ts";
80
+ // [V2 决策 3] lifecycle-manager:[T4②] DEFAULT_IDLE_TIMEOUT_MS 是 assertIdleTimeoutMsSafe
81
+ // 错误文案的缺省时长基准([R4] 唯一消费主体随域迁入本聚合)。
82
+ import { DEFAULT_IDLE_TIMEOUT_MS } from "../lifecycle-manager.ts";
83
+ import type { ModelConfigService } from "../model-config-service.ts";
84
+ import type { AgentConfig, ModelInfo, ResolvedModel } from "../model-resolver.ts";
85
+ import type { NotifyHost, PiLike } from "../notify-host.ts";
86
+ // [H1 U2] notify 门(notifier.ts)——one-shot 轮末回注双闸消费。
87
+ import { notifyGateAllowsDelivery } from "../notifier.ts";
88
+ import type { RecordStore } from "../record-store.ts";
89
+ // [R3] ResolvedIdentity 接口本体在 record-access.ts(生产者 resolveIdentity 所属聚合),
90
+ // 本聚合单向 type import(D-R3-2 同款非环形态)。
91
+ import type { ResolvedIdentity } from "./record-access.ts";
92
+ import type { RoundSupervisor } from "../round-supervisor/index.ts";
93
+ import {
94
+ armMidRoundNoProgress,
95
+ disarmRoundFromProtocol,
96
+ refreshFromProtocolEvent,
97
+ type SettledWatchdogFireInfo,
98
+ } from "../settled-watchdog.ts";
99
+ import { MAX_FORK_DEPTH } from "../session-context-resolver.ts";
100
+ import { createBackgroundStream, type StreamSink, type SubagentStream } from "../stream-sink.ts";
101
+ import { writeRecordBinding } from "../state-marker.ts";
102
+ import { EngineSdkError } from "@zhushanwen/subagent-engine-sdk";
103
+ import type { ResumeAnchor } from "@zhushanwen/subagent-engine-sdk";
104
+ import type { UiRequestObservability } from "../ui-request-observability.ts";
105
+ import type { WorktreeManager } from "../worktree-manager.ts";
106
+ import type {
107
+ AgentEvent,
108
+ AgentResult,
109
+ ClosedReason,
110
+ WorktreeHandle,
111
+ ExecuteOptions,
112
+ ExecutionHandle,
113
+ ExecutionMode,
114
+ ExecutionRecord,
115
+ } from "../types.ts";
116
+ import { DEFAULT_AGENT_NAME, ForkDepthExceededError } from "../types.ts";
117
+ // [R6/D-R4-4] 跨两聚合消费的值语义纯量归一常量叶子文件(聚合→支撑文件方向合法)。
118
+ import { PRIORITY_BACKGROUND, MS_PER_SECOND, SECONDS_PER_MINUTE } from "./service-constants.ts";
119
+
120
+ const logger = getLogger("subagents");
121
+
122
+ /**
123
+ * [H1 U2 / 红线②] stale-child 兜底的退出等待窗(ms):镜像在途子进程活项时,协议
124
+ * cancel(引擎侧 SIGTERM → pi trap flush → 退出)的有界收敛窗。pi 对裸 SIGTERM 做
125
+ * graceful shutdown(窗口几十~几百 ms,见 disposedUiRequestStub 注释实测口径),
126
+ * 300ms 覆盖常见退出路径;残余双写窗与宿主重启窗口同属红线③经验性登记(量级 =
127
+ * 引擎存活期状态错配频次 × 窗内未退出概率,罕见)。
128
+ * [R4] 随唯一消费主体(killStaleChildBeforeDispatch)自壳文件迁入。 */
129
+ const STALE_CHILD_EXIT_WAIT_MS = 300;
130
+
131
+ /** 有界 delay(stale-child 退出窗消费;fire-and-forget 场景不引入 timer 依赖)。
132
+ * [R4] 随唯一消费主体(killStaleChildBeforeDispatch)自壳文件迁入。 */
133
+ function delay(ms: number): Promise<void> {
134
+ return new Promise((resolve) => {
135
+ setTimeout(resolve, ms);
136
+ });
137
+ }
138
+
139
+ /** one-shot CAS 收口的 closedReason 派生:aborted → cancelled;成功 → user-close;
140
+ * 其余 → gc(settleOneShotOutcome 唯一消费点)。 */
141
+ function deriveOneShotClosedReason(aborted: boolean, success: boolean): ClosedReason {
142
+ return aborted ? "cancelled" : success ? "user-close" : "gc";
143
+ }
144
+
145
+ /**
146
+ * [R1 打样模式 1] 聚合协作 deps——**全部晚绑定闭包,构造期零求值**。
147
+ *
148
+ * 窄结构类型只声明聚合真实消费的通道(不整实例注入)。四类成员:
149
+ * - 断言面(assertReady):execute/executeAndAwait/deliverChatMessage 入口就绪门
150
+ * (本体在 SessionBaselines,壳转发)。
151
+ * - #1 留壳共享依赖 getter(getStore/getModelService/getNotifyHost/
152
+ * getPool/getWorktreeManager/getCwd/getPi/getRoundSupervisor/getCollectCoordinator):
153
+ * getter 现读同一实例(B-6 roundSupervisor 留壳、C-6 装配闭包经壳 late-bound)。
154
+ * - 会话基线 getter(getExecNesting/getSessionRootId/getStreamSink/getUiObservability):
155
+ * initSession 注入的运行时可变态现读(SessionBaselines 经壳 getter 透传)。
156
+ * - R3 聚合显式接口(resolveIdentity/resolveIdentityForEngine/createRecordForMode/
157
+ * buildEarlyFailedHandle/finalizeRecord/finalizeFailed/finalizeAborted/closeChatIdle):
158
+ * 身份解析/record 创建/终态迁移的跨聚合协作(壳装配指 RecordAccess/
159
+ * RecordLifecycle 实例方法,聚合间零私有互调——G2)。
160
+ */
161
+ export interface RunOrchestrationDeps {
162
+ /** [D4 下沉] assertReady 断言(本体在 SessionBaselines,壳转发)。 */
163
+ readonly assertReady: () => void;
164
+ /** RecordStore(#1 留壳共享依赖;运行中句柄回填 reportRecordTransition/Continuation
165
+ * revive register 面)。 */
166
+ readonly getStore: () => RecordStore;
167
+ /** ModelConfigService(resolveModel 代理 + 路由全局缺省 + FinalizeDeps)。 */
168
+ readonly getModelService: () => ModelConfigService;
169
+ /** 进程 cwd(worktree create 锚点)。 */
170
+ readonly getCwd: () => string;
171
+ /** WorktreeManager(worktree 创建/清理 + FinalizeDeps)。 */
172
+ readonly getWorktreeManager: () => WorktreeManager;
173
+ /** NotifyHost(pending emit + one-shot watchdog 失败通知 + FinalizeDeps 注销面)。 */
174
+ readonly getNotifyHost: () => NotifyHost;
175
+ /** ConcurrencyPool(DefaultConcurrencyPool 共享池——execute/executeAndAwait/
176
+ * chat 轮次/引擎任务的并发槽)。 */
177
+ readonly getPool: () => ConcurrencyPool;
178
+ /** pi 句柄(FinalizeDeps manifest 写失败 appendEntry;initSession 晚绑定)。 */
179
+ readonly getPi: () => PiLike | null;
180
+ /** 根 session id(relay 归属键 SESSION_ID 权威源;runCtx 注入)。 */
181
+ readonly getSessionRootId: () => string | null;
182
+ /** UI streaming sink(createBackgroundStream 的 widget 通道)。 */
183
+ readonly getStreamSink: () => StreamSink | null;
184
+ /** UI observability(stream 通道形态判据 getMode)。 */
185
+ readonly getUiObservability: () => UiRequestObservability;
186
+ /** 嵌套身份基线(BC-12 嵌套护栏深度检查)。 */
187
+ readonly getExecNesting: () => ExecutionNestingContext;
188
+ /** [B-6 留壳] 轮次活性监督器(在途记账/死亡分诊 adoptOnProcessDeath)。 */
189
+ readonly getRoundSupervisor: () => RoundSupervisor;
190
+ /** [R2 SyncCollect 显式接口] collectCoordinator 公共投影(bg 完成回注 route 投递)。 */
191
+ readonly getCollectCoordinator: () => CollectCoordinator;
192
+ /** [R3 RecordAccess 显式接口] 步骤 1 身份解析(三层:override → agentConfig → 主
193
+ * agent model;含 pi 未命中跨引擎候选文案)。 */
194
+ readonly resolveIdentity: (
195
+ opts: ExecuteOptions,
196
+ pre?: { agent: string; agentConfig: AgentConfig | undefined },
197
+ ) => Promise<ResolvedIdentity>;
198
+ /** [R3 RecordAccess 显式接口] 非 pi 引擎的 identity 解析。 */
199
+ readonly resolveIdentityForEngine: (
200
+ engine: EnginePort,
201
+ engineModel: string | undefined,
202
+ agent: string,
203
+ agentConfig: AgentConfig | undefined,
204
+ opts: ExecuteOptions,
205
+ ) => ResolvedIdentity;
206
+ /** [R3 RecordAccess 显式接口] 按 mode 生成 id + controller,创建 record 并注册。 */
207
+ readonly createRecordForMode: (
208
+ identity: ResolvedIdentity,
209
+ opts: ExecuteOptions,
210
+ mode: ExecutionMode,
211
+ originFields?: { origin: "workflow"; parentRunId: string },
212
+ ) => ExecutionRecord;
213
+ /** [R3 RecordAccess 显式接口] worktree 前置失败的 early-return handle。 */
214
+ readonly buildEarlyFailedHandle: (record: ExecutionRecord) => ExecutionHandle;
215
+ /** [R3 RecordLifecycle 显式接口] D-017 时序收尾(doFinalizeRecord 委托)。 */
216
+ readonly finalizeRecord: (
217
+ record: ExecutionRecord,
218
+ result: AgentResult,
219
+ status: "closed",
220
+ closedReason?: ClosedReason,
221
+ ) => Promise<void>;
222
+ /** [R3 RecordLifecycle 显式接口] run() 创建期异常的收尾。 */
223
+ readonly finalizeFailed: (record: ExecutionRecord, err: unknown) => Promise<AgentResult>;
224
+ /** [R3 RecordLifecycle 显式接口] 排队中被 abort 的 cancelled 终态收尾。 */
225
+ readonly finalizeAborted: (record: ExecutionRecord) => Promise<AgentResult>;
226
+ /** [R3 RecordLifecycle 显式接口] 无在跑轮 record 的手动终态化(Continuation
227
+ * closeNow 回调面)。 */
228
+ readonly closeChatIdle: (record: ExecutionRecord) => Promise<void>;
229
+ }
230
+
231
+ /**
232
+ * 域 #6/#7/#12/#14/#15 聚合:run 域执行编排(R4 自 SubagentService 抽取)。
233
+ *
234
+ * 字段所有权(r0-inventory 清单①):#31 continuations(Continuation 实例表)——
235
+ * 本聚合唯一写者;壳 dispose 经 clearContinuations() 显式接口触达(C-4 兑现),
236
+ * 字段壳零感知。
237
+ */
238
+ export class RunOrchestration {
239
+ private readonly deps: RunOrchestrationDeps;
240
+
241
+ constructor(deps: RunOrchestrationDeps) {
242
+ this.deps = deps;
243
+ }
244
+
245
+ // [R4 等价形态复刻] sessionRootId 经 getter 转发 deps(属性访问形态保留——TS 对
246
+ // getter 可做 null 收窄,runCtx 条件 spread 的类型推导与壳内原形态一致;函数调用
247
+ // 形态 this.deps.getSessionRootId() 不参与收窄)。
248
+ private get sessionRootId(): string | null {
249
+ return this.deps.getSessionRootId();
250
+ }
251
+
252
+ /**
253
+ * 预解析 model(renderCall 标题行用,同步)。代理 modelService.resolveModel。
254
+ * 仅解析 override/agentConfig 路径;ctxModel 缺失时拋错,调用方 catch 降级。
255
+ */
256
+ resolveModel(
257
+ agent: string,
258
+ override?: { model?: string; thinkingLevel?: string },
259
+ ctxModel?: ModelInfo,
260
+ agentConfig?: AgentConfig,
261
+ ): ResolvedModel {
262
+ return this.deps.getModelService().resolveModel(agent, override, ctxModel, agentConfig);
263
+ }
264
+
265
+ /**
266
+ * 统一执行入口。mode 固定 background(sync 已删除)。
267
+ * 内部完成:模型解析 → 执行 → 收尾。
268
+ *
269
+ * @param opts.ctxModel 主 agent 当前模型(模型解析第三层兼底)。undefined 时仅依赖 override/agentConfig。
270
+ */
271
+ async execute(opts: ExecuteOptions): Promise<ExecutionHandle> {
272
+ this.deps.assertReady();
273
+ // [T4② / PS-4] idleTimeoutMs 配置错误在首个副作用前同步 fail-fast(错误含合法范围)。
274
+ this.assertIdleTimeoutMsSafe(opts);
275
+
276
+ // 通用嵌套深度护栏(D-033):嵌套上下文([D3-⑤] 公共层 ExecutionNestingContext)
277
+ // 记录所有 subagent 嵌套层级(fork + 非 fork),每层 +1。MAX_FORK_DEPTH 同时限
278
+ // fork 链与通用嵌套——非 fork 递归虽不累积 session 体积,但耗资源且 LLM 易陷入
279
+ // 「委派→再委派」死循环。在所有副作用之前拦截,错误直达调用方。
280
+ // 计数基准:顶层 nestingDepth=0,nestingDepth>MAX 被拒。与 fork 体积护栏(parentForkDepth 检查)
281
+ // 互补:本护栏更严(计所有嵌套),混合链下先生效;两者共享 MAX_FORK_DEPTH 上限不漂移。
282
+ // [ALS 断裂修复] current() 内含基线兜底(pi 事件回调模型下 enterWith 不贯穿)。
283
+ const parentNesting = this.deps.getExecNesting().current();
284
+ const nestingDepth = parentNesting ? parentNesting.depth + 1 : 0;
285
+ if (nestingDepth > MAX_FORK_DEPTH) {
286
+ throw new ForkDepthExceededError(
287
+ `subagent nesting depth ${nestingDepth} > ${MAX_FORK_DEPTH} (max recursion), refusing to spawn deeper`,
288
+ );
289
+ }
290
+
291
+ // mode 固定 background(sync 模式已删除)
292
+ const mode: ExecutionMode = "background";
293
+
294
+ // ── 1. IDENTITY 前置解析:agentConfig(agent .md 加载)保持在最前 ──
295
+ // [u-h2 D2-1] 路由先行:model 解析从「路由之前」移到「路由之后、按目标引擎分支」
296
+ // (修 F2-A/B 时序根因——曾 :850 先解析 model 再 :867 路由)。agentConfig 是路由
297
+ // 第二层输入(frontmatter engine)必须先解析;显式 agent ref 校验语义不变。
298
+ const agent = opts.agent ?? DEFAULT_AGENT_NAME;
299
+ const agentConfig = opts.agent
300
+ ? this.deps.getModelService().getRequiredAgentConfig(opts.agent)
301
+ : undefined;
302
+
303
+ // ── 1.5 引擎路由(D2 单轨 + D3-② 路由单点:统一经 routeEngineForHost)──
304
+ // 唯一实现在 engine/routing.ts(pi 同步短路 + registry 注入 + 兜底回本地 pi 实例
305
+ // 收敛于此);本调用点只装配三层输入与注入件。时机:路由(含 probe)在 record
306
+ // 创建前完成——兜底时 record 按 pi 语义创建 + engineFallback 留痕(D5 字节级守护
307
+ // 只约束「无 fallback 的纯缺省路径」);守卫命中/strict 时在此 throw,不产生孤儿
308
+ // record。pi 请求路径同步短路(routed 非 Promise,零微任务——首个 await 前完成
309
+ // 路由决策;执行经进程边界,「run 内首个 await 前已触达 executeAndAwait」的旧
310
+ // 时序契约由引擎协议化设计 §3.5.3 作废放宽)。
311
+ // [u-h2 D2-1] 路由先行于 pi 链 model 解析:agentConfig 是路由第二层输入(frontmatter
312
+ // engine)已前置解析;pi 的 resolveModel 移到路由之后、按目标引擎分支执行——非 pi
313
+ // 请求不被 pi registry 解析错误拦截(F2-A/B 时序根因),model 校验归目标引擎(D2-2)。
314
+ const routingInput = {
315
+ callEngine: opts.engine,
316
+ agentEngine: agentConfig?.engine,
317
+ globalDefaultEngine: this.deps.getModelService().getGlobalConfig().defaultEngine,
318
+ };
319
+ const routed = routeEngineForHost({
320
+ routing: routingInput,
321
+ // 守卫 c 判据只看调用方显式指定的 model(resolved model 含 ctxModel 兼底,
322
+ // 恒非空会把一切兜底误判为 model 绑定命中)
323
+ taskModel: opts.model,
324
+ strict: this.deps.getModelService().getGlobalConfig().engineRouting?.strict === true,
325
+ probe: (engineId) => getEngine(engineId).probe(),
326
+ // [W3] chat 域 pi 路由 = registry cli 形态 port(协议客户端)——inproc DI 实例
327
+ // 随 inproc pi 引擎目录 删除消亡,chat 与 run 域同路(G1 单一 CLI 形态)。
328
+ piEngine: this.resolveChatEnginePort(),
329
+ });
330
+ const route: EngineRouteResult = routed instanceof Promise ? await routed : routed;
331
+ return this.executeViaEngine(opts, { agent, agentConfig }, route, mode);
332
+ }
333
+
334
+ /**
335
+ * workflow 编排层专用:sync-await 接口,内部走 background 管道但返回 Promise<AgentResult>。
336
+ *
337
+ * 与 execute() 的区别(D-A1):
338
+ * 1. 返回 workflow AgentResult(content 字段),非 ExecutionHandle
339
+ * 2. 不经 chat 轮次 kick-off(detached 回注)→ 不注入 followUp 完成通知(BC-11,结果直接返回 workflow)
340
+ * 3. T2 删 sync 时 executeAndAwait 不受牵连(独立方法)
341
+ *
342
+ * 共享:runSpawn + ConcurrencyPool + record + pending emit(D-A4)。
343
+ */
344
+ async executeAndAwait(
345
+ opts: ExecuteOptions,
346
+ signal?: AbortSignal,
347
+ onEvent?: (event: AgentEvent) => void,
348
+ stream?: SubagentStream,
349
+ ): Promise<WorkflowAgentResult> {
350
+ this.deps.assertReady();
351
+ // [T4② / PS-4] 与 execute() 同款入口校验(两入口共享 runAndFinalize → armIdleTimer 链)。
352
+ this.assertIdleTimeoutMsSafe(opts);
353
+
354
+ // ── BC-12 嵌套护栏:复用 execute() 的嵌套上下文深度检查 ──
355
+ // [ALS 断裂修复] current() 内含基线兜底(与 execute 同)。
356
+ const parentNesting = this.deps.getExecNesting().current();
357
+ const nestingDepth = parentNesting ? parentNesting.depth + 1 : 0;
358
+ if (nestingDepth > MAX_FORK_DEPTH) {
359
+ throw new ForkDepthExceededError(
360
+ `subagent nesting depth ${nestingDepth} > ${MAX_FORK_DEPTH} (max recursion), refusing to spawn deeper`,
361
+ );
362
+ }
363
+
364
+ // ── 步骤 1: IDENTITY 解析 ──
365
+ const identity = await this.deps.resolveIdentity(opts);
366
+
367
+ // ── 步骤 2: RECORD 创建(mode="background" 进池)──
368
+ const record = this.deps.createRecordForMode(identity, opts, "background");
369
+ this.deps.getNotifyHost().emitPendingRegister(record.id, record.agent);
370
+
371
+ // ── 步骤 2.5: worktree creation (only worktree===true; handle injection is execute()'s path) ──
372
+ // Workflow path receives boolean only (AgentCallOpts.worktree: boolean) — WorktreeHandle is a
373
+ // main-thread non-serializable object that cannot cross worker postMessage, so no object branch
374
+ // here (unlike execute() :445-447 which serves the subagent-tool path).
375
+ // On create failure, finalizeFailed cleans up the record, then
376
+ // throw lets SAR.run() convert it to an AgentResult.error (not return-handle like execute()).
377
+ let worktreeHandle: WorktreeHandle | undefined;
378
+ if (opts.worktree === true) {
379
+ // [create-await 竞态守卫] 与 execute 同款(见其 worktree 分支注释)——差异仅在
380
+ // 失败语义:executeAndAwait 对齐「失败 throw」,SAR.run 的 catch 会转成 AgentResult.error
381
+ // (cancelled 呈现对齐 cancel 抢先路径)。赋值→终态检查→runAndFinalize 同一同步段。
382
+ // 检查结果经标志位带出 try(守卫 throw 不能落在 try 内——会被下方 catch 当作
383
+ // create 失败再走 finalizeFailed,对已 closed 的 record 语义未定义)。
384
+ let cancelledDuringCreate = false;
385
+ try {
386
+ worktreeHandle = await this.deps.getWorktreeManager().create(this.deps.getCwd(), record.id);
387
+ record.worktreeHandle = worktreeHandle;
388
+ if (record.status === "closed") {
389
+ cancelledDuringCreate = true;
390
+ }
391
+ } catch (err) {
392
+ // finalizeFailed: CAS→finalizeRecord→emitUnregister (record already registered above).
393
+ // throw (not return-handle): executeAndAwait's caller SAR.run() catches and wraps into
394
+ // AgentResult.error. Diverges from execute() which returns buildEarlyFailedHandle
395
+ // because the two methods have different return types.
396
+ await this.deps.finalizeFailed(record, err);
397
+ throw err;
398
+ }
399
+ if (cancelledDuringCreate) {
400
+ await this.deps.getWorktreeManager().cleanup(worktreeHandle);
401
+ throw new Error(`subagent ${record.id} cancelled during worktree creation`);
402
+ }
403
+ }
404
+
405
+ // ── 步骤 3/4 合并(W3):signal 决议(SessionRunnerContext 已随 inproc 链路删除)──
406
+ const effectiveSignal = signal ?? record.controller?.signal;
407
+
408
+ // 步骤 5: runAndFinalize(await,不 detached)。onEvent 独立传,stream 透传。
409
+ const result = await this.runAndFinalize(
410
+ record,
411
+ { ...opts, worktree: worktreeHandle },
412
+ identity,
413
+ effectiveSignal,
414
+ PRIORITY_BACKGROUND,
415
+ onEvent,
416
+ stream,
417
+ );
418
+
419
+ // ── 步骤 6: D-A10 AgentResult 映射 ──
420
+ // [MF-2] 不在此 emit pending:unregister——runAndFinalize 内部已覆盖所有路径:
421
+ // - CAS 成功(runAndFinalize L629)→ finalizeRecord 末尾 emit(L797)
422
+ // - CAS 失败(cancel/finalizeFailed/dispose 抢先转终态)→ 那些路径各自已 emit
423
+ // (cancelBackground L709 / finalizeFailed→finalizeRecord / dispose L240)
424
+ // 旧实现无条件 emit 一次 → CAS 成功分支重复 emit(双注销)。
425
+ const wfResult = mapToWorkflowAgentResult(result);
426
+ // W2 改动 7:注入 worktreePath(worktree 隔离激活时来自 step 2.5 的 worktreeHandle)。
427
+ // mapToWorkflowAgentResult 不感知 worktree(它只做 subagents AgentResult → workflow AgentResult
428
+ // 的 DTO 映射),故在 caller 侧 mutate 刚新建的产物对象(无共享引用,安全)。
429
+ //
430
+ // ⚠️ worktreePath is diagnostic only, may not exist — see AgentResult.worktreePath JSDoc
431
+ // (orchestration/models/types.ts)。下方诊断标识符语义(not cwd)说明同源。
432
+ //
433
+ // 诊断标识符语义(not cwd):
434
+ // - runAndFinalize 内的 finalizeRecord 在 return 前已 cleanup(git worktree remove --force),
435
+ // worktreePath 指向的目录已被删除,不保证存在。
436
+ // - worktreePath 仅供日志/trace 关联(如定位某条 session jsonl 的 worktree 来源),无运行时语义。
437
+ // - **不可作为后续 agent 的 cwd**——目录已删,复用会 ENOENT。
438
+ // - wave 内 worktree 复用(spec-w §2 "wave 内 8 action 共享 worktree")在 pi 当前架构下
439
+ // 不可行:worktree 绑定单次 agent() record,每次 executeAndAwait 结束 finalizeRecord
440
+ // 无条件 cleanup,worktree 无法跨 action 存活。wave 改用主 cwd。
441
+ wfResult.worktreePath = record.worktreeHandle?.path;
442
+ return wfResult;
443
+ }
444
+
445
+ // [H1 U6] resumesInFlight(record 级在途 resume 守卫,[review MF1])随 resumeColdRound
446
+ // 退役删除——Continuation 的 activeRunId 同步占位单飞(dispatchRoundGuarded)构造性
447
+ // 承接防双写者语义。chatRoundRoutes(recordId 键反向通道路由表)随 interact 面退役删除。
448
+
449
+ /**
450
+ * [H1 U2] ConversationContinuation 实例表(recordId 键):chatMode record 的续聊
451
+ * 编排承载(§3.4)。创建点 = chatMode 首轮派发前 / message 到达(SP-5 升级后);
452
+ * 清理点 = record 终态化路径(onRecordFinalizedCleanup)。跨重启冷查(cold-lookup)
453
+ * 重建会创建新 record 对象——continuationFor 对缓存实例做绑定一致性检查,换新即重建。
454
+ */
455
+ private readonly continuations = new Map<string, ConversationContinuation>();
456
+
457
+ // [H1 U6] 旧 chat 域投递/续轮链已整体退役:deliverToRunning 消费链(V2 决策 3)、
458
+ // resumeColdRound(守卫链 + 执行态信号清除 + resume 锚点组装——迁 Continuation
459
+ // dispatchRoundGuarded / dispatchRoundAsync)、onHotPathSettledWatchdogTimeout
460
+ //(热路径 watchdog 载体——迁 Continuation.onWatchdogFire → onRunSettled 失败分支
461
+ // 统一收口,D7)、EPIPE/steer 协议知识(随 interact 面消亡)。
462
+
463
+ /**
464
+ * [V2 决策 3 → H1 U2 改写 / U6 定形] chatMode 统一投递入口(message action 的
465
+ * Service 面)——经 ConversationContinuation.onMessage(§3.4 / D4 状态迁移表 /
466
+ * D2 打断语义):
467
+ *
468
+ * - running(轮间 idle)→ 新轮派发(新 run + resume 锚点,record.sessionFile 续写);
469
+ * - running(有在途轮)→ D2 打断:abort 在途轮 signal + 消息入队,abort 收敛后
470
+ * drain(打断即杀,宽限语义随长驻消亡放弃);
471
+ * - 终态 → guard 分流(closed 硬拒 / 可重连 revive + 非 chatMode 升级格 + D5 gate)。
472
+ *
473
+ * @param record 目标 record(messageHandler 已做归属校验 + 升级 gate)
474
+ * @param text 消息正文
475
+ */
476
+ async deliverChatMessage(record: ExecutionRecord, text: string): Promise<void> {
477
+ this.deps.assertReady();
478
+ this.continuationFor(record).onMessage(text);
479
+ }
480
+
481
+ /**
482
+ * [UF-1] record 绑定 sidecar 写(sessionFile 回填点统一入口)。
483
+ *
484
+ * engine-CLI 化后子 session 文件不含身份 entry(旧 PI_SUBAGENT_SELF_RECORD_ID
485
+ * 注入链消失),跨重启后 coldLookupForAction(findLightById + collectRecords)
486
+ * 失去 id→file 映射,message 一律「not found or not owned」(U4 基线 S6 ❌)。
487
+ * 本方法在 record.sessionFile 被回填的代码点落 `<sessionFile>.record-binding`
488
+ * (id→file + rootSessionId 等身份域),record-store 扫描侧据它重建身份。
489
+ *
490
+ * [D3a v8 时机① / U2b] 本方法是 sessionFile 回填族(run 应答回填 ×3 站点)的
491
+ * 单一收口点——锚点确立即 acquireWriteLease 声明跨进程写权(fresh spawn 全程
492
+ * 声明,缺口 1 闭合;「宿主开始往 session 文件写即声明写权」)。acquire 失败
493
+ * 原样上抛(双写风险敞口必须响亮,D3c——禁止 best-effort 吞错续跑),由各回填
494
+ * 站点所在的 run 收敛 catch 按 run 失败收口;绑定写维持 best-effort(记账面)。
495
+ * sessionFile 未回填(undefined)时静默跳过(绑定无从谈起、亦无写权可声明)。
496
+ */
497
+ writeBindingForRecord(record: ExecutionRecord): void {
498
+ const sessionFile = record.sessionFile;
499
+ if (!sessionFile) return;
500
+ this.deps.getStore().acquireWriteLease(sessionFile, record.id);
501
+ writeRecordBinding(sessionFile, {
502
+ v: 1,
503
+ recordId: record.id,
504
+ rootSessionId: record.rootSessionId,
505
+ parentRecordId: record.parentRecordId,
506
+ depth: record.depth,
507
+ agent: record.agent,
508
+ task: record.task,
509
+ slug: record.slug,
510
+ mode: record.mode,
511
+ startedAt: record.startedAt,
512
+ chatMode: record.chatMode === true,
513
+ round: record.round,
514
+ model: record.model,
515
+ thinkingLevel: record.thinkingLevel,
516
+ worktree: record.worktreeHandle !== undefined || record.hadWorktree === true,
517
+ // [H2 S3] 来源身份随绑定落盘:引擎子文件身份面(binding sidecar)是磁盘重建
518
+ // origin 的唯一现行载体,漏写则归档/重启后 workflow record 逃过 D1 投影过滤。
519
+ origin: record.origin,
520
+ parentRunId: record.parentRunId,
521
+ });
522
+ }
523
+
524
+ /**
525
+ * chat 域统一执行入口(D2 单轨:全引擎——含 pi——经此进入 EnginePort)。路由
526
+ *(routeEngineForHost:三层 + pi 同步短路 + probe/守卫)已由 execute 完成——这里
527
+ * 只剩 unsupported 预检 → identity(pi 链解析 / 非 pi 按目标引擎校验,
528
+ * [u-h2 D2-1/D2-2])→ record 创建+盖章 → worktree → detached 引擎 run。
529
+ * 全部同步拒绝发生在 record 创建前(不产生孤儿 record)。
530
+ */
531
+ async executeViaEngine(
532
+ opts: ExecuteOptions,
533
+ preIdentity: { agent: string; agentConfig: AgentConfig | undefined },
534
+ route: EngineRouteResult,
535
+ mode: ExecutionMode,
536
+ ): Promise<ExecutionHandle> {
537
+ const engine = route.engine;
538
+ // [D3-④ 预检 capabilities 化] 唯一实现 = common/capability-gate(capabilities
539
+ // 驱动,含 maxTurns 扩位)。检查点钉死:execute/executeViaEngine 同步段、record
540
+ // 创建前(engine.capabilities() 同步可得)——承接「全部同步拒绝发生在 record
541
+ // 创建前、不产生孤儿 record」不变量(其后的 kickOffEngineRun 是 fire-and-forget,
542
+ // 检查若只落在 engine.run 内则拒绝异步化为「派发成功 + 静默失败 record」)。
543
+ // [W3 契约变更③补注(协议化能力位方向判定)] 同步拒只覆盖「manifest 少声明」
544
+ // 方向;**manifest 多声明**(声明支持而引擎实际不支持)由首个 run 的协议握手
545
+ // `initialize` 发现 → engine_capability_mismatch 该 run 失败 + record 标 failed,
546
+ // 并**清理 run 前已建的前置副作用**(worktree 经 finalizeFailed → finalizeRecord
547
+ // Step 3b cleanupWorktreeIfBound 清理)。非 gate 位不一致(无论强弱)一律
548
+ // warn 留痕不阻断(诊断面归 EngineClient,设计 §3.3 能力位段)。
549
+ assertTaskShapeSupported(engine.id, engine.capabilities(), opts);
550
+
551
+ // identity 按路由结果分支构造([u-h2 D2-1] 路由先行):
552
+ // - pi:pi 链三层解析在路由后执行(现状三层解析链行为零变化;含 resolveModel
553
+ // 失败的跨引擎候选提示,D2-4);
554
+ // - 非 pi:跳过 pi registry,model 按目标引擎校验(同步期 throw,record 创建前
555
+ // ——场景 2 错误;ctxModel 不透传,缺省语义归引擎,D2-1③)。
556
+ const isPiRoute = route.engineId === DEFAULT_ENGINE_ID;
557
+ // [u-h2 D2-1③] 非 pi 的 model 源 = 调用参数 > agent .md frontmatter(作者声明不
558
+ // 忽略)——frontmatter 声明须真正透传给引擎(taskSpec.model 消费 opts.model),
559
+ // 不能只进 record 留痕;无显式 model 时引擎落自身缺省(validateModel(undefined) 裁决)。
560
+ const engineModel = isPiRoute ? undefined : (opts.model ?? preIdentity.agentConfig?.model);
561
+ const identity = isPiRoute
562
+ ? await this.deps.resolveIdentity(opts, preIdentity)
563
+ : this.deps.resolveIdentityForEngine(engine, engineModel, preIdentity.agent, preIdentity.agentConfig, opts);
564
+
565
+ // record 盖章路由结果(D5 字节级守护的执行侧落点,pi/非 pi 两分支形态见
566
+ // buildPiRecordOptions / buildNonPiRecordOptions):
567
+ // - pi 纯缺省/显式 pi:不盖 engine 键(pi record entry 序列化产物不得新增 engine
568
+ // 键,undefined 经 JSON 省略)——与旧 pi 主路径 piOpts 剥离语义逐字节一致;
569
+ // - pi 兜底:engine='pi' + engineFallback 留痕(engine = 实际执行引擎,from=请求
570
+ // 引擎留痕);
571
+ // - 非 pi:engine=route.engineId 显式留痕(+engineFallback 如有)+ model 覆写
572
+ // (frontmatter 声明透传,u-h2 D2-1③)。
573
+ const recordOpts: ExecuteOptions = isPiRoute
574
+ ? this.buildPiRecordOptions(opts, route)
575
+ : this.buildNonPiRecordOptions(opts, route, engineModel);
576
+ const record = this.deps.createRecordForMode(identity, recordOpts, mode);
577
+ this.deps.getNotifyHost().emitPendingRegister(record.id, record.agent);
578
+
579
+ // ── worktree 创建(仅 worktree===true 或已传入 handle 时)──
580
+ // record 先创建,worktree 失败时可 finalizeFailed(record 已在 store 中)。
581
+ // worktree 必须显式开启:worktree===true 创建新 worktree;worktree===undefined/false 不创建。
582
+ // fork 不隐含 worktree(UC-1 fork 可独立使用,fork 仅继承上下文,在 parent cwd 跑)。
583
+ // 非 pi 引擎带 worktree 已被上方预检同步拒绝(caps.sandbox='none'),此段实际仅
584
+ // sandbox 能力引擎(pi:caps.sandbox='emulated')可达。
585
+ let worktreeHandle: WorktreeHandle | undefined;
586
+ if (typeof opts.worktree === "object") {
587
+ // 传入的是已创建的 WorktreeHandle
588
+ worktreeHandle = opts.worktree;
589
+ } else if (opts.worktree === true) {
590
+ // worktree===true(显式要求)——创建新 worktree。与 fork 正交(worktree 文件隔离不依赖 fork 上下文继承)。
591
+ const created = await this.createNewWorktreeForEngineRun(record);
592
+ if (created.earlyHandle !== undefined) return created.earlyHandle;
593
+ worktreeHandle = created.handle;
594
+ }
595
+
596
+ this.dispatchRoutedEngineRun(record, recordOpts, identity, isPiRoute, engine, worktreeHandle);
597
+ return { mode: "background", subagentId: record.id, sessionFile: record.sessionFile, details: project(record) };
598
+ }
599
+
600
+ /** [executeViaEngine 阶段拆分] pi 路由 record 盖章:纯缺省/显式 pi 不盖 engine 键;
601
+ * 兜底盖 'pi' + engineFallback 留痕(D5 字节级守护)。 */
602
+ private buildPiRecordOptions(opts: ExecuteOptions, route: EngineRouteResult): ExecuteOptions {
603
+ if (route.engineFallback !== undefined) {
604
+ return { ...opts, engine: DEFAULT_ENGINE_ID, engineFallback: route.engineFallback };
605
+ }
606
+ return opts.engine === undefined ? opts : { ...opts, engine: undefined };
607
+ }
608
+
609
+ /** [executeViaEngine 阶段拆分] 非 pi 路由 record 盖章:engine=route.engineId 显式
610
+ * 留痕(+engineFallback 如有)+ model 覆写(frontmatter 声明透传,u-h2 D2-1③)。 */
611
+ private buildNonPiRecordOptions(
612
+ opts: ExecuteOptions,
613
+ route: EngineRouteResult,
614
+ engineModel: string | undefined,
615
+ ): ExecuteOptions {
616
+ return {
617
+ ...opts,
618
+ ...(engineModel !== undefined ? { model: engineModel } : {}),
619
+ engine: route.engineId,
620
+ ...(route.engineFallback !== undefined ? { engineFallback: route.engineFallback } : {}),
621
+ };
622
+ }
623
+
624
+ /** [executeViaEngine 阶段拆分] worktree===true 分支体:创建新 worktree + [create-await
625
+ * 竞态守卫]。返回 earlyHandle = 抢先取消/创建失败已收口(调用点直接 early-failed
626
+ * 返回,不进轮次 kick-off);正常返回 handle 供调用点传入轮次。 */
627
+ private async createNewWorktreeForEngineRun(
628
+ record: ExecutionRecord,
629
+ ): Promise<{ handle: WorktreeHandle | undefined; earlyHandle: ExecutionHandle | undefined }> {
630
+ try {
631
+ const worktreeHandle = await this.deps.getWorktreeManager().create(this.deps.getCwd(), record.id);
632
+ record.worktreeHandle = worktreeHandle;
633
+ // [create-await 竞态守卫] create 的 await 窗口内 cancel/dispose 可 CAS 把 record
634
+ // 转成 closed 终态——cancelBackground 当时读到的 worktreeHandle 可能仍是 undefined
635
+ // (cleanup 被跳过)。赋值后同同步段检查终态:closed 则主动 cleanup(幂等,抢先的
636
+ // fire-and-forget 清理无害)+ early-failed 返回,不进轮次 kick-off(避免子进程白跑)。
637
+ // 实现约束:赋值 → 终态检查 → kick-off 必须在同一同步段,中间禁止插入 await。
638
+ if (record.status === "closed") {
639
+ await this.deps.getWorktreeManager().cleanup(worktreeHandle);
640
+ return { handle: worktreeHandle, earlyHandle: this.deps.buildEarlyFailedHandle(record) };
641
+ }
642
+ return { handle: worktreeHandle, earlyHandle: undefined };
643
+ } catch (err) {
644
+ // create 失败→不进入 run,finalizeFailed 统一收尾(含 emitPendingUnregister failed)
645
+ await this.deps.finalizeFailed(record, err);
646
+ return { handle: undefined, earlyHandle: this.deps.buildEarlyFailedHandle(record) };
647
+ }
648
+ }
649
+
650
+ /** [executeViaEngine 阶段拆分] 按路由结果分派引擎执行(record 已创建 + worktree 已
651
+ * 装配):非 pi 引擎 engine.run 自足执行;pi chatMode 首轮走 Continuation;pi
652
+ * one-shot 走 background 派发主干。 */
653
+ private dispatchRoutedEngineRun(
654
+ record: ExecutionRecord,
655
+ recordOpts: ExecuteOptions,
656
+ identity: ResolvedIdentity,
657
+ isPiRoute: boolean,
658
+ engine: EnginePort,
659
+ worktreeHandle: WorktreeHandle | undefined,
660
+ ): void {
661
+ if (!isPiRoute) {
662
+ // 非 pi 引擎:engine.run 自足执行(handle+outcome),编排侧 journal 接线 + 终态迁移
663
+ this.kickOffEngineRun(record, recordOpts, engine);
664
+ return;
665
+ }
666
+ if (record.chatMode) {
667
+ // [H1 U2] chat 首轮经 ConversationContinuation(§3.5 终态数据流:轮末分流
668
+ // chatMode → Continuation onRunSettled;one-shot → settleOneShotOutcome 照旧)。
669
+ // 首轮 task = dispatchRound([task])(无 resume——新 session,锚点由 run 应答回填)。
670
+ this.continuationFor(record).startFirstRound(recordOpts.task);
671
+ return;
672
+ }
673
+ // one-shot background 派发主干(kickOffChatRound 共享部分,D6 保留泛化)。
674
+ this.kickOffChatRound(
675
+ record,
676
+ { ...recordOpts, worktree: worktreeHandle },
677
+ identity,
678
+ record.controller!.signal,
679
+ PRIORITY_BACKGROUND,
680
+ );
681
+ }
682
+
683
+ /**
684
+ * 非 pi 引擎的 detached 执行编排(与 pi 轮次 kick-off 同构的 background 语义):
685
+ * pool 并发槽(maxConcurrent 对非 pi 引擎同样生效)→ journal 接线(D6 第②级:
686
+ * taskId=record.id,初始池 key 占位 'shared',onPoolResolved retarget 到引擎实际
687
+ * 池 key——路径与 paths.ts 同源推导)→ engine.run(signal 接 record controller,
688
+ * kill-chain 两级生效)→ engineHandle 回填(终态迁移落 entry 前)→ 终态迁移 →
689
+ * bg notify(chat 域宿主职责,与 pi 完成通知同语义)。
690
+ */
691
+ kickOffEngineRun(record: ExecutionRecord, opts: ExecuteOptions, engine: EnginePort): void {
692
+ const signal = record.controller?.signal;
693
+ // [W4] 在途记账(监督器「该等」判据源):run 发起即记账,finally 收口重评估。
694
+ this.deps.getRoundSupervisor().noteRunStarted(record.id);
695
+ void (async () => {
696
+ try {
697
+ await this.deps.getPool().acquire(PRIORITY_BACKGROUND, this.effectiveMaxConcurrentFor(record), signal);
698
+ } catch {
699
+ // S1: 排队中被 abort(signal.aborted)走 cancelled,与已运行被 abort 一致(runAndFinalize 同款)
700
+ if (signal?.aborted) {
701
+ await this.deps.finalizeAborted(record);
702
+ } else {
703
+ await this.deps.finalizeFailed(record, new Error("aborted"));
704
+ }
705
+ return;
706
+ }
707
+ // [review MF1] acquire 成功后必须 finally release:不 release 则
708
+ // DefaultConcurrencyPool._active 永不递减——每次引擎任务泄漏一个并发槽,累计
709
+ // maxConcurrent 次后全部 background subagent(pi 与引擎共用同一池)在 acquire 队列永久挂起
710
+ try {
711
+ // [W4] adopted = 走了表 3 行 1 接管分支(record 保持 resumable 交监督器)——
712
+ // 跳过 bg 完成回注(合并单条通知由监督器 sendMergedFailureNotice 承担,
713
+ // route 会再发一条 toNotifyRecord 投影通知 = 双通知,正是 R3 要消除的时序窗口)。
714
+ const adopted = await this.runEngineTask(record, opts, engine, signal);
715
+ // cancel 抢先(closedReason='cancelled')时 cancelBackground 自己 notify,跳过
716
+ if (!adopted && record.closedReason !== "cancelled") {
717
+ this.deps.getCollectCoordinator().route(record);
718
+ }
719
+ } finally {
720
+ this.deps.getPool().release();
721
+ this.deps.getRoundSupervisor().noteRunEnded(record.id);
722
+ }
723
+ })();
724
+ }
725
+
726
+ /**
727
+ * kickOffEngineRun 的 acquire 后主体:journal 接线(D6 第②级:taskId=record.id,
728
+ * 初始池 key 占位 'shared',onPoolResolved retarget 到引擎实际池 key)→ engine.run
729
+ * (signal 接 record controller,kill-chain 两级生效)→ engineHandle 回填(终态迁移
730
+ * 落 entry 前)→ 终态迁移。bg notify 归编排侧(与 pi 轮次收尾通知归编排对称)。
731
+ */
732
+ async runEngineTask(
733
+ record: ExecutionRecord,
734
+ opts: ExecuteOptions,
735
+ engine: EnginePort,
736
+ signal: AbortSignal | undefined,
737
+ ): Promise<boolean> {
738
+ // [D3-③ journal 接线合一] writer + retarget + 路径权威收敛 common/journal-wiring
739
+ //(与 SAR 同一实现)。chat 域无下游 onEvent 消费者——journal 是事件唯一出口,
740
+ // 不传 forwardEvents。
741
+ const journal = wireEventJournal({ engineId: engine.id, taskId: record.id });
742
+ // [R4 §3.4 不变量 3] 运行中句柄回填:create 应答后(远早于 run resolve)回填
743
+ // record.engineHandle 并经 reportRecordTransition 落 entry——运行中的 GUI 经 entry
744
+ // 重建 record 即拿到 ①②级读取钥匙(sessionRef/dbPath/poolKey/journalPath),
745
+ // 不再等终态回填(详情页中途打开可见当时进度快照)。journalPath 此时已是
746
+ // retarget 后的最终路径(onPoolResolved 在 prepare 期先行触发,writer 是路径权威)。
747
+ // 仅 chat 域接线——workflow 域 SAR 无运行中 record 读取方,刻意不做同类回填(防误扩展)。
748
+ // [F1 修复] 幂等语义 = 按字段补缺,不是整条丢弃:已有值一律不被迟到值覆盖,缺失
749
+ // 字段照常补上。「sessionId 先落、迟到 handleReady 只补 sessionFile」是本 replay
750
+ // 批次新打通且更有价值的形态(close 期 LC-4 后缀反查会在 sessionId 已知后补发
751
+ // sessionFile)——旧守卫「有 sessionId 即整条 return」会把该
752
+ // 回填永久吞掉,令冷续 resume 锚点(anchor.sessionRef.sessionFile)与引擎 interact
753
+ // 定位拿不到 sessionFile([H1 U6] chatHandleFor 已随 interact 面退役)。poolKey / journalPath 不参与补缺:
754
+ // journalPath 权威归 journal writer(本闭包写入即定稿),poolKey 由 onPoolResolved
755
+ // retarget 与首次回填定稿,迟到值不得重置。仅在确有字段落位时才落 entry——无新字段
756
+ // 不写噪(迟到重复回调即零副作用,GUI 无额外投影)。
757
+ const backfillEngineHandle = (partial: { sessionRef: Record<string, string>; poolKey: string }): void => {
758
+ const current = record.engineHandle;
759
+ if (current === undefined) {
760
+ record.engineHandle = {
761
+ sessionRef: { ...partial.sessionRef },
762
+ poolKey: partial.poolKey,
763
+ journalPath: journal.path,
764
+ };
765
+ this.deps.getStore().reportRecordTransition(record);
766
+ return;
767
+ }
768
+ const merged: Record<string, string> = { ...current.sessionRef };
769
+ let filled = false;
770
+ for (const [key, value] of Object.entries(partial.sessionRef)) {
771
+ if (value === undefined || value === "") continue; // 空值不是可落位的字段值
772
+ const existing = merged[key];
773
+ if (existing === undefined || existing === "") {
774
+ merged[key] = value;
775
+ filled = true;
776
+ }
777
+ }
778
+ if (!filled) return;
779
+ record.engineHandle = { ...current, sessionRef: merged };
780
+ this.deps.getStore().reportRecordTransition(record);
781
+ };
782
+ // 对齐点③:journal 路径权威 = 引擎声明的池 key(writer 初始用占位,retarget 后
783
+ // 与 handle.poolKey 同源)。
784
+ // [H2 Gate B 修复] live reducer 喂入恢复(runWorkflowEngineTask observedEvent 同款):
785
+ // 非 pi 引擎派发此前 onEvent 直连 journal.onEvent——事件只落 ②级 journal,live record
786
+ // 零喂入(turns/totalTokens 恒 0,chat 域 record 与 tool one-shot 经非 pi 引擎同病)。
787
+ // 此处恢复 updateFromEvent 喂入:reducer 与 journal-replay / session-view-service
788
+ // 重放路径同源(C5 守护),live ≡ replay 构造性成立。喂入窗口 = run await 窗口,
789
+ // 终态后残余事件仅写内存 record 不落盘——与 workflow 域修法同一取舍。不接
790
+ // refreshFromProtocolEvent:非 pi 派发无轮次 no-progress 守护(arm 点在轮次域),
791
+ // 恒 no-op 不加。
792
+ const journalOnEvent = journal.onEvent;
793
+ const observedEvent = (event: AgentEvent): void => {
794
+ updateFromEvent(record, event);
795
+ journalOnEvent(event);
796
+ };
797
+ const runCtx: RunContext = {
798
+ taskId: record.id,
799
+ poolKey: JOURNAL_INITIAL_POOL_KEY,
800
+ signal,
801
+ ctxModel: opts.ctxModel,
802
+ onEvent: observedEvent,
803
+ onPoolResolved: journal.onPoolResolved,
804
+ // [R4 §3.4 不变量 3] 运行中句柄回填通道(engine/port.ts RunContext.onHandleReady)
805
+ onHandleReady: backfillEngineHandle,
806
+ // D9①:路由层 fallback 留痕投影进 outcome(zcode 无独立 record 通路)
807
+ ...(record.engineFallback !== undefined ? { engineFallback: record.engineFallback } : {}),
808
+ // [F6] 根 session id 注入(relay 归属键 SESSION_ID 权威源;null/空串不上 wire)
809
+ ...(this.sessionRootId !== null && this.sessionRootId !== ""
810
+ ? { sessionRootId: this.sessionRootId }
811
+ : {}),
812
+ // D10 终止链:engine spawn 的子进程注册进 spawnedChildren 记账
813
+ //(cancelBackground SIGTERM / dispose killAll 收割对非 pi record 生效)
814
+ onChildSpawned: (child) => registerSpawnedChildForRecord(record.id, child),
815
+ };
816
+ try {
817
+ const { handle, outcome } = await engine.run(executeOptionsToEngineTaskSpec(opts), runCtx);
818
+ // engineHandle 终态回填(U2:终态迁移落 entry 前;R4 起为兜底面——运行中回填
819
+ // 已由 onHandleReady 提前落 entry,此处覆写终态权威值)。sessionRef 整体透传——
820
+ // 失败终态 sessionId 缺失时也回填已有部分(dbPath/poolKey),读侧①级降②级
821
+ // 的防御形态;journalPath 取 retarget 后的实际落盘路径(writer 是路径权威)。
822
+ record.engineHandle = {
823
+ sessionRef: handle.data.sessionRef,
824
+ poolKey: handle.data.poolKey,
825
+ journalPath: journal.path,
826
+ };
827
+ await journal.close();
828
+ return await this.finalizeEngineOutcome(record, outcome);
829
+ } catch (err) {
830
+ // engine.run prepare 期 reject(进程创建前)→ failed 终态(与 runAndFinalize catch 同语义);
831
+ // journal 尽力而为收口(②级数据源写失败已由 writer 内部 warn 收敛)
832
+ await journal.close();
833
+ // [W4 表 3 行 1] 引擎进程死亡(engine_crashed——run 帧已受理后进程死亡/stdin
834
+ // 写失败)且宿主存活 → record 保持 resumable 交监督器接管(禁 completed 谎报、
835
+ // 禁直接 closed 终局——resume 锚点在盘,逻辑任务可续)。prepare 期失败
836
+ // (handshake/protocol/model 拒——进程创建前)维持现状 closed(确定性失败,
837
+ // 保持 resumable 无意义)。
838
+ // [H2 W2 / adopt 豁免点一] workflow origin 豁免 adopt 链:引擎死亡即 run 失败
839
+ // 即 record 终态化(失败路径表),adopt 链「唤醒→guidance→2h 看门狗→giveUp」
840
+ // 对无脚本可回的 record 全程无意义——豁免落空即落入下方 finalizeFailed 立即
841
+ // 终态化(防「无监督、永不终态化、pending 永不注销」的 resumable 僵尸)。
842
+ if (
843
+ err instanceof EngineSdkError &&
844
+ err.code === "engine_crashed" &&
845
+ record.chatMode !== true &&
846
+ record.origin !== "workflow" &&
847
+ record.status === "running"
848
+ ) {
849
+ this.adoptResumableAfterEngineDeath(record, toErrorMessage(err));
850
+ return true;
851
+ }
852
+ await this.deps.finalizeFailed(record, err);
853
+ return false;
854
+ }
855
+ }
856
+
857
+ /**
858
+ * [W4 表 3 行 1] 引擎/子进程死亡、宿主存活的 record 处置:run 终态如实记 failed
859
+ * 证据(record.error),record **保持 resumable**(session 文件在盘,逻辑任务可续
860
+ * ——冷路径 resume 可直接续写),交监督器接管(合并单条通知 + 三态判定)。禁止
861
+ * 两个事故方向:completed 谎报(G3)与 closed 直接终局(resume 可能性丢失,等待
862
+ * 无主——2026-09-08 事故环 2/3 的 core 侧形态)。
863
+ *
864
+ * 判据状态源钉死 record 级:写点只动 record 字段(resumable/result/error),
865
+ * 不清镜像不查引擎——引擎进程被动重建(ensureConnected 退避重填镜像)不翻转
866
+ * 本处置(纳管模型:死亡事件纳管、重建不解管)。
867
+ *
868
+ * [U2b 修复轮/D2] 三写簿记(error/result/resumable + 迁移上报)归口
869
+ * store.adoptEngineDeath(U1 原语簿记为直接三写的超集,多出 notifyChange 刷新);
870
+ * 监督器编排(adoptOnProcessDeath)留调用方。record 不在 store 内存的形态 =
871
+ * false 旁路 debug 留痕(两调用点的 record 均为创建即注册的在册对象)。
872
+ */
873
+ adoptResumableAfterEngineDeath(record: ExecutionRecord, errMsg: string): void {
874
+ this.deps.getStore().adoptEngineDeath(record.id, { error: errMsg });
875
+ this.deps.getRoundSupervisor().adoptOnProcessDeath(record, errMsg);
876
+ }
877
+
878
+ /**
879
+ * engine.run resolve 的终态迁移:outcome.error → failed(success=false + error 文案);
880
+ * 否则 done(result=content)。CAS 抢锁(tryTransition)防与 cancelBackground 双收尾。
881
+ *
882
+ * [W4 表 3 行 1] 进程死亡分诊:outcome.error 存在且 **exitCode === null**(引擎侧
883
+ * 进程被信号终止/崩溃的合成 outcome 形态——RemoteEngine 运行中失败合成分支恒
884
+ * exitCode:null,引擎如实上报的 turn 失败带数值 exitCode;engine-client 注释同源
885
+ * 「exitCode null = 被信号杀死,杀链判据」)且宿主存活且非 conversation 形态 →
886
+ * record 保持 resumable 交监督器接管(如实 failed 证据 + 合并通知 + 三态判定),
887
+ * 不再 closed 终局。返回 true = 走了接管分支(调用方跳过 bg 完成回注)。
888
+ */
889
+ async finalizeEngineOutcome(record: ExecutionRecord, outcome: AgentOutcome): Promise<boolean> {
890
+ if (outcome.sessionFile !== undefined) {
891
+ record.sessionFile = outcome.sessionFile;
892
+ // [D3a 时机①] 统一入口 writeBindingForRecord 内 acquire 写权声明(U2b)。
893
+ this.writeBindingForRecord(record);
894
+ }
895
+ if (
896
+ outcome.error !== undefined &&
897
+ outcome.exitCode === null &&
898
+ record.chatMode !== true &&
899
+ // [H2 W2 / adopt 豁免点二] workflow origin 豁免(同 runEngineTask catch 分诊点一
900
+ //——豁免落空走下方 tryTransition+finalizeRecord 立即终态化,不交监督器)。
901
+ record.origin !== "workflow" &&
902
+ record.status === "running"
903
+ ) {
904
+ this.adoptResumableAfterEngineDeath(record, outcome.error);
905
+ return true;
906
+ }
907
+ const result = this.outcomeToAgentResult(record, outcome);
908
+ if (tryTransition(record, "closed", "gc")) {
909
+ await this.deps.finalizeRecord(record, result, "closed", "gc");
910
+ }
911
+ return false;
912
+ }
913
+
914
+ /**
915
+ * workflow 域"干活 + 收尾"(sync await;[W3] 执行叶 = 协议 engine.run——原 inproc
916
+ * runSpawn 链随 inproc pi 引擎目录 删除消亡,journal 接线 / record 终态迁移语义保持)。
917
+ * 编排分段保持「装配(池槽/worktree)→ 执行 → 回收(finally)→ 错误收口 → 终态收口」。
918
+ */
919
+ async runAndFinalize(
920
+ record: ExecutionRecord,
921
+ opts: ExecuteOptions,
922
+ identity: ResolvedIdentity,
923
+ signal: AbortSignal | undefined,
924
+ priority: number,
925
+ onEvent?: (event: AgentEvent) => void,
926
+ stream?: SubagentStream,
927
+ ): Promise<AgentResult> {
928
+ const pooled = record.mode === "background";
929
+ let acquired = false;
930
+ if (pooled) {
931
+ const acquireFailure = await this.acquirePoolOrFinalize(record, signal, priority);
932
+ if (acquireFailure !== undefined) return acquireFailure;
933
+ acquired = true;
934
+ }
935
+
936
+ // worktree 句柄经 executeOptionsToEngineTaskSpec(opts).worktree 直传引擎
937
+ //(AgentCallOpts.worktree 接受 WorktreeHandle——原 runSpawn 显式传参的协议形态)。
938
+ const engine = this.resolveChatEnginePort();
939
+ // journal 接线(D6 第②级,与 kickOffEngineRun 同一实现):forwardEvents = onEvent
940
+ //(workflow liveRecord 桥接,D-A8)。
941
+ const journal = wireEventJournal({ engineId: engine.id, taskId: record.id, forwardEvents: onEvent });
942
+
943
+ let result: AgentResult;
944
+ try {
945
+ const runCtx: RunContext = {
946
+ taskId: record.id,
947
+ poolKey: JOURNAL_INITIAL_POOL_KEY,
948
+ signal,
949
+ ctxModel: identity.resolved.model,
950
+ onEvent: journal.onEvent,
951
+ onPoolResolved: journal.onPoolResolved,
952
+ ...(stream !== undefined ? { stream } : {}),
953
+ ...(record.engineFallback !== undefined ? { engineFallback: record.engineFallback } : {}),
954
+ // [F6] 根 session id 注入(relay 归属键 SESSION_ID 权威源;null/空串不上 wire)
955
+ ...(this.sessionRootId !== null && this.sessionRootId !== ""
956
+ ? { sessionRootId: this.sessionRootId }
957
+ : {}),
958
+ // D10 终止链:引擎 spawn 的子进程注册进 spawnedChildren 镜像记账
959
+ onChildSpawned: (child) => registerSpawnedChildForRecord(record.id, child),
960
+ };
961
+ const { handle, outcome } = await engine.run(this.taskSpecWithModel(opts, record.model), runCtx);
962
+ record.engineHandle = {
963
+ sessionRef: handle.data.sessionRef,
964
+ poolKey: handle.data.poolKey,
965
+ journalPath: journal.path,
966
+ };
967
+ await journal.close();
968
+ result = this.outcomeToAgentResult(record, outcome);
969
+ } catch (err) {
970
+ await journal.close();
971
+ // engine.run prepare 期 reject(进程创建前)→ 合成 failed result + 收尾。
972
+ // swallow(不 re-throw):sync 调用方拿到合成 failed result,避免异常逃逸到
973
+ // tool 层 + record 卡 running。([H1 U6] 旧 chatMode 分支 finalizeChatSpawnFailure
974
+ // 已随 resumeColdRound/旧 chat 载体退役——chatMode 轮不经 runAndFinalize,
975
+ // Continuation 轮收口归 onRoundRejected/onRunSettled。)
976
+ return this.deps.finalizeFailed(record, err);
977
+ } finally {
978
+ this.releaseRoundResources(record, pooled && acquired, stream);
979
+ }
980
+
981
+ // v4 B-1: status 恒为 closed。cancelled 折入 closed(closedReason='cancelled')。
982
+ await this.settleOneShotOutcome(record, result, signal?.aborted === true);
983
+ return result;
984
+ }
985
+
986
+ /** engine.run taskSpec 装配单一来源(runAndFinalize 与 kickOffChatRound 共用):
987
+ * executeOptions 协议映射 + model = record 留痕词形(resolved 解析产物,
988
+ * joinEngineModelRef 规范形)覆盖——原 identity.resolved 经 runSpawn --model
989
+ * 兜底的协议等价承载。 */
990
+ taskSpecWithModel(opts: ExecuteOptions, model: string | undefined): AgentCallOpts {
991
+ return { ...executeOptionsToEngineTaskSpec(opts), ...(model !== undefined ? { model } : {}) };
992
+ }
993
+
994
+ /**
995
+ * one-shot(非 chatMode)终态收口(原 settleOneShotOutcome 分支):成功轮消费
996
+ * closeAfterRound 挂起标志;失败/取消一次性销毁。CAS 抢锁失败(cancel/dispose 抢先
997
+ * 终态化)静默跳过。runAndFinalize(workflow 域)与 kickOffChatRound 非 chatMode
998
+ * 分支共用。
999
+ */
1000
+ async settleOneShotOutcome(
1001
+ record: ExecutionRecord,
1002
+ result: AgentResult,
1003
+ aborted: boolean,
1004
+ ): Promise<void> {
1005
+ // [H2 W2 / D7] workflow origin 成功收口 = 立即终态化(closed/"gc")——G1 显式例外:
1006
+ // SP-5 running-resumable 为「父 agent 可 message 续聊」设计,workflow agent 结果
1007
+ // 由脚本返回值承载、无 message 对端,保持 running-idle 会绑架 hasRunning / 恒挂
1008
+ // 30 天 idle-gc / 被误升级为对话容器 / goal defer 恒挂(设计 D7 四面连带)。
1009
+ // 分支置于现有 CAS 之前:防其先把 memory closedReason 写成 "user-close" 与
1010
+ // finalizeRecord 的 "gc" 参数分叉。条件含 !aborted:aborted+success 竞态边缘
1011
+ // 照旧落现有 cancelled 分支,不漂移为 gc。自带 tryTransition 抢锁(承接现状
1012
+ // 「cancel/dispose 抢先 → 静默跳过」守卫语义);失败/取消分支照旧终态化
1013
+ // (下方既有四分支零改动)。
1014
+ if (record.origin === "workflow" && !aborted && result.success) {
1015
+ if (tryTransition(record, "closed", "gc")) {
1016
+ await this.deps.finalizeRecord(record, result, "closed", "gc");
1017
+ }
1018
+ return;
1019
+ }
1020
+ if (!tryTransition(record, "closed", deriveOneShotClosedReason(aborted, result.success))) return;
1021
+ await this.settleOneShotPostTransition(record, result, aborted);
1022
+ }
1023
+
1024
+ /** [settleOneShotOutcome 阶段拆分] CAS 抢锁成功后的四分支收口:成功轮消费
1025
+ * closeAfterRound 挂起标志 / 保持 running;失败轮兑现挂起 close 或一次性销毁。 */
1026
+ private async settleOneShotPostTransition(
1027
+ record: ExecutionRecord,
1028
+ result: AgentResult,
1029
+ aborted: boolean,
1030
+ ): Promise<void> {
1031
+ if (!aborted && result.success && record.closeAfterRound === true) {
1032
+ // [M5] busy 时 close(force:false) 置的标志在本轮完成时消费终态化。
1033
+ await this.consumeCloseAfterRound(record, result, "user-close");
1034
+ } else if (!aborted && result.success) {
1035
+ // [SP-5] one-shot 成功完成 → 保持 running(旧 idle),等待 message 触发 upgrade。
1036
+ // [H1 U2 / D7] 共享调用点恒传 success(行为零变化 G3——成功空文本回退
1037
+ // markRoundIdle 的 one-shot 兜底 前值 ?? "(empty)")。
1038
+ // [U2b] 轮终簿记①-⑨归口 store.markRoundIdle(SP-5 成功分支与 chat 轮末共享轮终
1039
+ // 语义;`.alive` 跨轮保留 [D3a/B5]——release 出口 = 终态原语/idle-GC 归档)。
1040
+ // [W4 发射点② / U5 收口项②] pending 注销已随 store 簿记⑧统一发射(SubagentService
1041
+ // 构造点 setPendingUnregister 接线),本分支不再双轨重复发射。
1042
+ this.deps.getStore().markRoundIdle(record.id, { kind: "success", content: result.text });
1043
+ } else if (!aborted && record.closeAfterRound === true) {
1044
+ // [M5] 优雅关闭挂起的失败轮:轮已完成即兑现 close 意图终态化(含本轮 result)。
1045
+ await this.consumeCloseAfterRound(record, result, "gc");
1046
+ } else {
1047
+ // 非 chatMode 失败/取消 或其他终态:一次性销毁(archive + worktree cleanup)。
1048
+ await this.deps.finalizeRecord(record, result, "closed", aborted ? "cancelled" : "gc");
1049
+ }
1050
+ }
1051
+
1052
+ /** AgentOutcome → execution AgentResult 单一映射源(workflow run 域映射;
1053
+ * finalizeEngineOutcome 终态 result 复用此处构造——exitCode null = 被信号杀死的
1054
+ * 合成终态,error 如实透传)。 */
1055
+ outcomeToAgentResult(record: ExecutionRecord, outcome: AgentOutcome): AgentResult {
1056
+ if (outcome.sessionFile !== undefined) {
1057
+ record.sessionFile = outcome.sessionFile;
1058
+ // [D3a 时机①] 统一入口 writeBindingForRecord 内 acquire 写权声明(U2b)。
1059
+ this.writeBindingForRecord(record);
1060
+ }
1061
+ return {
1062
+ text: outcome.content,
1063
+ turns: outcome.usage?.turns ?? 0,
1064
+ durationMs: outcome.durationMs ?? Date.now() - record.startedAt,
1065
+ success: outcome.error === undefined,
1066
+ ...(outcome.error !== undefined ? { error: outcome.error } : {}),
1067
+ sessionId: outcome.sessionId ?? record.id,
1068
+ toolCalls: [],
1069
+ ...(outcome.parsedOutput !== undefined ? { parsedOutput: outcome.parsedOutput } : {}),
1070
+ };
1071
+ }
1072
+
1073
+ /** [U04 提取·终态收口] closeAfterRound 挂起标志消费(原三处分支的公共收尾序列):
1074
+ * 清标志 + 终态化 closed。reason:成功轮恒 user-close;失败/取消轮用派生值。 */
1075
+ async consumeCloseAfterRound(
1076
+ record: ExecutionRecord,
1077
+ result: AgentResult,
1078
+ reason: ClosedReason,
1079
+ ): Promise<void> {
1080
+ record.closeAfterRound = undefined;
1081
+ await this.deps.finalizeRecord(record, result, "closed", reason);
1082
+ }
1083
+
1084
+ /**
1085
+ * pi 会话形态轮次的 detached 编排([W3 协议形态 → H1 U6 定形]):轮次经协议 run
1086
+ *(会话形态 resume{recordId, resume?})发往 pi-subagent-cli 引擎进程——
1087
+ * - 应答时点 = agent_settled(D7 定案:pi 的 compact/收尾在 agent_end 后执行,
1088
+ * agent_end 即 kill 会截断收尾;one-shot 的 agent_end 即终态语义不归本方法);
1089
+ * - 流式 delta 经 run 作用域反向通道回流(runId 键 ctx.stream);[H1 U6] 轮次
1090
+ * 相位帧反向通道与 recordId 键路由已随 chat 域相位机退役,
1091
+ * 中段刷新 = run 事件通道既有事件、settle 交棒 = run 应答驱动;
1092
+ * - chat 域不接 event journal(pi 子代理 session JSONL 即原生数据源——journal
1093
+ * 接线仅 workflow 域 SAR 与非 pi 引擎路径)。
1094
+ *
1095
+ * [H1 U2 / D6 泛化] 本方法是 pi 引擎 background 派发的共享主干(one-shot 与
1096
+ * Continuation 轮同路)。`continuation` 存在 = Continuation 轮(chat 域统一进
1097
+ * run 域的新编排):
1098
+ * - 应答/reject/acquire 打断三分回调回流 Continuation(轮末分流 D7 单点收口);
1099
+ * - 不终态化 acquire-abort(打断 ≠ cancel——record 保持 running);
1100
+ * - 轮末通知归属 Continuation 双闸(成功 gate→route / 失败独立载荷过门),
1101
+ * 主干尾部回注跳过(防双 route)。
1102
+ * `continuation` 缺省 = one-shot 主干(终态收口 settleOneShotOutcome + 尾部回注)。
1103
+ */
1104
+ kickOffChatRound(
1105
+ record: ExecutionRecord,
1106
+ opts: ExecuteOptions,
1107
+ identity: ResolvedIdentity,
1108
+ signal: AbortSignal | undefined,
1109
+ priority: number,
1110
+ /** 续聊锚点(协议 ResumeAnchor):run.params.resume.resume。undefined = 新 session。 */
1111
+ resume?: ResumeAnchor,
1112
+ /** [H1 U2] Continuation 轮回调面(存在 = 新编排;见方法头注释)。 */
1113
+ continuation?: ContinuationRoundHandlers,
1114
+ ): void {
1115
+ // 创建 streaming 生命周期对象。策略(含 widget 退役步骤 2:GUI + relay 激活时停发
1116
+ // 私货、TUI/未激活原样创建、sink 未注入降级 undefined)集中在 createBackgroundStream。
1117
+ const stream = createBackgroundStream(record.id, this.deps.getStreamSink(), this.deps.getUiObservability().getMode(), process.env);
1118
+
1119
+ // [H1 U6] recordId 键反向通道路由注册段已随 interact 面退役删除——流式 delta
1120
+ // 恒经 run 作用域路由(runId 键 ctx.stream),中段守护刷新由 ctx.onEvent 承担。
1121
+ const engine = this.resolveChatEnginePort();
1122
+
1123
+ // [W4] 会话形态轮的在途记账:死亡纳管 record 被主 agent resume = 决策收敛
1124
+ // (清指引标记与看门狗,回归「该等」)。conversation 形态本就豁免监督域(D8)。
1125
+ this.deps.getRoundSupervisor().noteRunStarted(record.id);
1126
+ void (async () => {
1127
+ try {
1128
+ await this.deps.getPool().acquire(priority, this.effectiveMaxConcurrentFor(record), signal);
1129
+ } catch {
1130
+ // S1: 排队中被 abort(signal.aborted)走 cancelled,与已运行被 abort 一致。
1131
+ // [H1 U2] Continuation 轮例外:abort 来源可能是打断(D2——轮级 signal,
1132
+ // record 保持 running)而非 cancel(record 级,cancelBackground 已终态化)。
1133
+ // 不终态化(abort 不终态化),经 onAbandoned 回流 Continuation(cancel 场景
1134
+ // 该回调内终态守卫 early-return,行为等价)。
1135
+ await this.handleRoundPoolAcquireAbort(record, signal, continuation);
1136
+ return;
1137
+ }
1138
+ try {
1139
+ // [F-2 轮 arm] 轮开跑(pool 槽已到手、run 协议帧即将派发)挂**中段**无进展
1140
+ // 检测——引擎侧 spawn-runner 仅 turn 计数无墙钟,本 arm 是 wedged 轮的唯一熔断
1141
+ //(LC-1 场景①:pi 无事件行输出)。refresh 源:① run 事件通道协议事件行
1142
+ //(ctx.onEvent,含 text_delta,9 种既有事件);② settle 交棒——run 应答驱动
1143
+ //(Continuation onRunSettled 内 noteRoundSettledFromProtocol;[H1 U6] 旧
1144
+ // settled 相位帧消费面已退役)。轮终 disarmRoundFromProtocol 两段一并清。
1145
+ // arm 置于 acquire 之后:排队窗口不计入 no-progress 静默(窗语义 = 轮开跑后)。
1146
+ // fire 处置按轮形态分流:Continuation 轮 → kill + abort 轮 signal,run 收敛后
1147
+ // 经失败分支统一收口(单写者单路);one-shot 轮 → onOneShotSettledWatchdogTimeout
1148
+ //([A1/G3 回归修复] H1 重构曾误删本形态 arm 点——one-shot 轮 run 无协议超时
1149
+ //(engine-client request 不传 timeoutMs = 任务级无超时),无 arm 则楔死 run 永
1150
+ // 卡 running + 池槽泄漏 + 无失败通知,违背 G3「one-shot 行为零变化」)。
1151
+ this.armRoundNoProgressGuard(record, continuation);
1152
+ // 协议 run:record.chatMode = 会话形态(resume.recordId = 关联键;锚点存在 =
1153
+ // 续聊——[H1 U6] 键切换后恒构造 `resume` 键;应答时点 = agent_settled——轮末
1154
+ // 分流归属 Continuation onRunSettled);非 chatMode = 一次性 run(协议面无
1155
+ // resume 键,终态语义对齐 settleOneShotOutcome)。
1156
+ // [H2 Gate B 修复] live reducer 喂入恢复(runWorkflowEngineTask observedEvent
1157
+ // 同款):W3 删 inproc pi 引擎时,原 engines/pi/session-runner.ts agentEvent
1158
+ // 出口的 updateFromEvent(record, event) 一并消失——chat 轮(Continuation)与
1159
+ // tool one-shot 在 live 通路零喂入,record.turns/totalTokens 恒 0(journal-
1160
+ // replay / session-view-service 重放路径反而保真,live ≡ replay 契约被破坏)。
1161
+ // 此处恢复:reducer 与重放路径同源(C5 守护),事件序 = 引擎协议事件序,
1162
+ // message_end(usage) 携带 token 增量。Continuation 轮间共用同一 record 实例
1163
+ //(continuationFor 绑定),跨轮累积天然持续。喂入窗口 = run await 窗口——
1164
+ // cancel/watchdog 抢先终态化后 run 收敛前到达的残余事件仅写内存 record 不落盘
1165
+ //(终态 entry 已写,后续无 reportRecordTransition),且事件流随 kill 枯竭,
1166
+ // 与 workflow 域修法同一取舍(不加状态守卫,维持单一形态)。中段守护刷新源①
1167
+ // 照旧(refreshFromProtocolEvent 在已交棒/已 fire 时幂等 no-op;one-shot 分支
1168
+ // 此前不接 onEvent,本修复顺带补齐其刷新源①)。
1169
+ const { outcome } = await engine.run(
1170
+ // resume 锚点轮引擎侧覆盖 model 解析(taskSpec 装配单一来源见 taskSpecWithModel)。
1171
+ this.taskSpecWithModel(opts, record.model),
1172
+ // 协议 run 上下文(stream/sessionRootId/resume 形态 + live reducer 喂入)
1173
+ // 装配拆 buildChatRoundRunContext。
1174
+ this.buildChatRoundRunContext(record, identity, signal, stream, resume),
1175
+ );
1176
+ await this.settleChatRoundOutcome(record, outcome, continuation, signal);
1177
+ // background 回注:仅当本路径抢到 CAS 才 notify。cancel 抢先时 closedReason=
1178
+ // 'cancelled'(cancelBackground 自己 notify);[T4①/PS-2] parent-new/parent-fork
1179
+ // 是 disposeAllRecords 的编排性关闭(record 已关、告知由 list 的 closedReason
1180
+ // 表达)——迟到的完成回注不注入(可能已切换的)新 session。
1181
+ // [H1 U2] Continuation 轮跳过——通知归属 Continuation 双闸(成功 gate→route /
1182
+ // 失败独立载荷过门),本段回注即双 route。
1183
+ if (continuation === undefined && notifyGateAllowsDelivery(record.closedReason)) {
1184
+ this.deps.getCollectCoordinator().route(record);
1185
+ }
1186
+ } catch (err) {
1187
+ // 轮次 run 失败(prepare 期 reject / 引擎进程死亡 / cancel 后未收敛合成终态):
1188
+ // chatMode MF-6——不销毁对话,回退可恢复(session 文件在盘,续聊 run 接续);
1189
+ // 非 chatMode 终态销毁(finalizeFailed)。cancel 抢先时 record 已终态化
1190
+ //(Continuation 内终态守卫 / tryTransition 失败跳过),此处仅吞错。
1191
+ await this.handleChatRoundRunError(record, err, continuation);
1192
+ } finally {
1193
+ this.deps.getPool().release();
1194
+ // streaming widget 清除(轮终,幂等——续轮 delta 落已 dispose 的 stream 为 no-op)。
1195
+ stream?.dispose();
1196
+ // [W4] 轮收口重评估(死亡纳管 record 的轮终 → 驱动可能又死 → 重新三态判定)。
1197
+ this.deps.getRoundSupervisor().noteRunEnded(record.id);
1198
+ }
1199
+ })();
1200
+ }
1201
+
1202
+ /** [kickOffChatRound 阶段拆分] pool acquire 被打断(catch)的收口:Continuation 轮
1203
+ * 经 onAbandoned 回流(abort 不终态化——打断 ≠ cancel);one-shot 轮按 signal 状态
1204
+ * 终态化(S1 cancelled / failed)。 */
1205
+ private async handleRoundPoolAcquireAbort(
1206
+ record: ExecutionRecord,
1207
+ signal: AbortSignal | undefined,
1208
+ continuation: ContinuationRoundHandlers | undefined,
1209
+ ): Promise<void> {
1210
+ if (continuation !== undefined) {
1211
+ continuation.onAbandoned();
1212
+ return;
1213
+ }
1214
+ if (signal?.aborted) {
1215
+ await this.deps.finalizeAborted(record);
1216
+ } else {
1217
+ await this.deps.finalizeFailed(record, new Error("aborted"));
1218
+ }
1219
+ }
1220
+
1221
+ /** [kickOffChatRound 阶段拆分] 轮开跑 arm 中段无进展检测(F-2):fire 处置按轮形态
1222
+ * 分流——Continuation 轮回调回流 onWatchdogFire(run 收敛后失败分支统一收口);
1223
+ * one-shot 轮在本回调内直接收口(onOneShotSettledWatchdogTimeout)。 */
1224
+ private armRoundNoProgressGuard(
1225
+ record: ExecutionRecord,
1226
+ continuation: ContinuationRoundHandlers | undefined,
1227
+ ): void {
1228
+ if (continuation !== undefined) {
1229
+ armMidRoundNoProgress(record.id, {
1230
+ onMidTimeout: (fire) => continuation.onWatchdogFire(fire),
1231
+ onSettleTimeout: (fire) => continuation.onWatchdogFire(fire),
1232
+ });
1233
+ return;
1234
+ }
1235
+ armMidRoundNoProgress(record.id, {
1236
+ onMidTimeout: (fire) => this.onOneShotSettledWatchdogTimeout(record, fire),
1237
+ onSettleTimeout: (fire) => this.onOneShotSettledWatchdogTimeout(record, fire),
1238
+ });
1239
+ }
1240
+
1241
+ /** [kickOffChatRound 阶段拆分] 协议 run 上下文装配:pi 池键 + streaming 通道 +
1242
+ * [F6] 根 session id 注入 + 会话形态 resume 参数(conversation 形态必传;续聊带
1243
+ * 锚点;一次性 run 不携带);observedEvent 挂 live reducer 喂入。 */
1244
+ private buildChatRoundRunContext(
1245
+ record: ExecutionRecord,
1246
+ identity: ResolvedIdentity,
1247
+ signal: AbortSignal | undefined,
1248
+ stream: SubagentStream | undefined,
1249
+ resume: ResumeAnchor | undefined,
1250
+ ): RunContext {
1251
+ // [H2 Gate B 修复] live reducer 喂入恢复(runWorkflowEngineTask observedEvent
1252
+ // 同款):W3 删 inproc pi 引擎时,原 engines/pi/session-runner.ts agentEvent
1253
+ // 出口的 updateFromEvent(record, event) 一并消失——chat 轮(Continuation)与
1254
+ // tool one-shot 在 live 通路零喂入,record.turns/totalTokens 恒 0(journal-
1255
+ // replay / session-view-service 重放路径反而保真,live ≡ replay 契约被破坏)。
1256
+ // 此处恢复:reducer 与重放路径同源(C5 守护),事件序 = 引擎协议事件序,
1257
+ // message_end(usage) 携带 token 增量。Continuation 轮间共用同一 record 实例
1258
+ //(continuationFor 绑定),跨轮累积天然持续。喂入窗口 = run await 窗口——
1259
+ // cancel/watchdog 抢先终态化后 run 收敛前到达的残余事件仅写内存 record 不落盘
1260
+ //(终态 entry 已写,后续无 reportRecordTransition),且事件流随 kill 枯竭,
1261
+ // 与 workflow 域修法同一取舍(不加状态守卫,维持单一形态)。中段守护刷新源①
1262
+ // 照旧(refreshFromProtocolEvent 在已交棒/已 fire 时幂等 no-op;one-shot 分支
1263
+ // 此前不接 onEvent,本修复顺带补齐其刷新源①)。
1264
+ const observedEvent = (event: AgentEvent): void => {
1265
+ updateFromEvent(record, event);
1266
+ refreshFromProtocolEvent(record.id);
1267
+ };
1268
+ return {
1269
+ taskId: record.id,
1270
+ poolKey: PI_POOL_KEY,
1271
+ signal,
1272
+ ...(stream !== undefined ? { stream } : {}),
1273
+ ctxModel: identity.resolved.model,
1274
+ onEvent: observedEvent,
1275
+ // [F6] 根 session id 注入(relay 归属键 SESSION_ID 权威源;null/空串不上 wire)。
1276
+ // 本方法是 pi 引擎 background 派发的主路径(isPiRoute 恒路由至此,含 workflow
1277
+ // 域一次性 run——非 chatMode 不带 resume 键但同经此处),漏注 = pi child exit 13。
1278
+ ...(this.sessionRootId !== null && this.sessionRootId !== ""
1279
+ ? { sessionRootId: this.sessionRootId }
1280
+ : {}),
1281
+ // 会话形态参数(conversation 形态必传;续聊带锚点);一次性 run 不携带。
1282
+ ...(record.chatMode
1283
+ ? {
1284
+ resume: {
1285
+ recordId: record.id,
1286
+ ...(resume !== undefined ? { resume } : {}),
1287
+ },
1288
+ }
1289
+ : {}),
1290
+ };
1291
+ }
1292
+
1293
+ /** [kickOffChatRound 阶段拆分] run 应答收口:sessionFile 回填 + 轮末分流——
1294
+ * chatMode+Continuation 回流 onRunSettled(D7 单点);one-shot 终态收口
1295
+ * settleOneShotOutcome。 */
1296
+ private async settleChatRoundOutcome(
1297
+ record: ExecutionRecord,
1298
+ outcome: AgentOutcome,
1299
+ continuation: ContinuationRoundHandlers | undefined,
1300
+ signal: AbortSignal | undefined,
1301
+ ): Promise<void> {
1302
+ if (outcome.sessionFile !== undefined) {
1303
+ record.sessionFile = outcome.sessionFile;
1304
+ // [UF-1] 轮应答锚点落盘:跨重启后 coldLookupForAction 据此解析 id→file;
1305
+ // [D3a 时机①] 统一入口内 acquire 写权声明(fresh 首轮与 resume 续轮同经)。
1306
+ this.writeBindingForRecord(record);
1307
+ }
1308
+ if (record.chatMode && continuation !== undefined) {
1309
+ // [H1 U2] 轮末分流:run 应答(= agent_settled)回流 Continuation——round+1 /
1310
+ // 通知 / 交棒 / drain 全在 onRunSettled 单点(D7)。
1311
+ continuation.onSettled(outcome);
1312
+ return;
1313
+ }
1314
+ // 一次性 run 终态收口(对齐原 settleOneShotOutcome:成功轮 SP-5 回退
1315
+ // running-resumable 等待 upgrade;失败/取消一次性销毁)。
1316
+ const result = this.outcomeToAgentResult(record, outcome);
1317
+ await this.settleOneShotOutcome(record, result, signal?.aborted === true);
1318
+ }
1319
+
1320
+ /** [kickOffChatRound 阶段拆分] 轮次 run 失败收口:chatMode MF-6——不销毁对话,
1321
+ * 回退可恢复(onRejected);非 chatMode 终态销毁(finalizeFailed)+ debug 留痕。 */
1322
+ private async handleChatRoundRunError(
1323
+ record: ExecutionRecord,
1324
+ err: unknown,
1325
+ continuation: ContinuationRoundHandlers | undefined,
1326
+ ): Promise<void> {
1327
+ if (record.chatMode && continuation !== undefined) {
1328
+ continuation.onRejected(err);
1329
+ } else {
1330
+ await this.deps.finalizeFailed(record, err);
1331
+ }
1332
+ if (err instanceof Error) {
1333
+ logger.debug(`[subagent] chat round run error (record=${record.id}): ${err.message}`);
1334
+ }
1335
+ }
1336
+
1337
+ /**
1338
+ * [A1 / G3 回归修复] one-shot(非 chatMode)pi background 轮的 settled-watchdog fire
1339
+ * 处置(语义对齐 987346fc5 的 onHotPathSettledWatchdogTimeout 非 chatMode 分支——
1340
+ * H1 重构误删 arm 点的整链恢复,one-shot 行为零变化)。
1341
+ *
1342
+ * 与 Continuation 轮(onWatchdogFire → run 收敛后失败分支统一收口)不同:one-shot
1343
+ * 轮在本回调内直接收口——kill 子进程 + abort 轮 signal(旧 terminateChatSession
1344
+ * cancel 的 H1 后等价杀链)→ CAS(tryTransition closed+gc,防与 cancel/dispose 双
1345
+ * 收尾,抢锁失败即跳过)→ finalizeRecord 终态化 → 失败通知。
1346
+ *
1347
+ * 失败通知按 H1 后失败单发机制接线(Continuation settleRoundFailed 同构):独立构造
1348
+ * BgNotifyRecord 载荷过 notifyGate 门 → notifyHost.notify,不经 route(record)——
1349
+ * 失败正文直接取本回调的失败文案,不以 record.result 旧正文冒充(可达性
1350
+ * [T2-③/LC-1]:失败原因 + 恢复指引必须可达宿主)。
1351
+ *
1352
+ * 回调在 timer 触发的同步上下文执行:同步段只做 kill + abort + CAS(不抛),异步
1353
+ * 收尾 fire-and-forget 且 catch 归 bestEffort——错误逃出回调 = uncaughtException 崩宿主。
1354
+ */
1355
+ onOneShotSettledWatchdogTimeout(record: ExecutionRecord, fire: SettledWatchdogFireInfo): void {
1356
+ const windowDesc =
1357
+ fire.phase === "mid-round"
1358
+ ? `no valid protocol event for ${fire.waitedMs / MS_PER_SECOND / SECONDS_PER_MINUTE} min after prompt (mid-round no-progress)`
1359
+ : `no agent_settled within ${fire.waitedMs / MS_PER_SECOND}s after agent_end (settled phase)`;
1360
+ logger.warn(
1361
+ `[subagents] settled watchdog (${fire.phase}) fired for ${record.id}: ${windowDesc}, ` +
1362
+ `terminating (LC-1 wedge recovery)`,
1363
+ );
1364
+ killRecordChildWithEscalation(record.id, "settled watchdog (one-shot)");
1365
+ // abort 轮 signal(ctx.signal 接 record controller——引擎侧杀链驱动);在途 run 收敛
1366
+ // 后走 kickOffChatRound 既有吞错面(CAS 已终态化,其 finalizeFailed 抢锁失败 no-op)。
1367
+ record.controller?.abort();
1368
+ // fire = 本轮等待窗口终结(timer 回调已自删 entry,此处幂等清防御收尾段残留)。
1369
+ disarmRoundFromProtocol(record.id);
1370
+ if (!tryTransition(record, "closed", "gc")) {
1371
+ return; // 已被 cancel/dispose 抢先终态化——不重复收尾(watchdog disarm 由对方承接)
1372
+ }
1373
+ const failedResult: AgentResult = {
1374
+ text: "",
1375
+ turns: record.turnCount,
1376
+ durationMs: Date.now() - record.startedAt,
1377
+ success: false,
1378
+ error:
1379
+ `subagent did not reach agent_settled (${windowDesc}; settled watchdog); ` +
1380
+ `the process was terminated to bound the wait. ` +
1381
+ `Recovery: check state with subagents action:'list', then re-send your message to continue.`,
1382
+ sessionId: record.id,
1383
+ toolCalls: [],
1384
+ };
1385
+ void this.deps.finalizeRecord(record, failedResult, "closed", "gc")
1386
+ .then(() => {
1387
+ // 失败通知(独立载荷过 notifyGate 门——门拦 cancelled/编排性关闭竞态窗)。
1388
+ if (!notifyGateAllowsDelivery(record.closedReason)) return;
1389
+ // 载荷形态对齐 Continuation settleRoundFailed(closed+failed 文案载体);
1390
+ // sessionFile 不透传(G4:one-shot 通知逐字节——指针行仅 chatMode 语义)。
1391
+ this.deps.getNotifyHost().notify({
1392
+ id: record.id,
1393
+ status: "closed",
1394
+ closedReason: "gc",
1395
+ outcome: "failed",
1396
+ agent: record.agent,
1397
+ ...(record.model !== undefined ? { model: record.model } : {}),
1398
+ error: failedResult.error,
1399
+ startedAt: record.startedAt,
1400
+ endedAt: record.endedAt ?? Date.now(),
1401
+ });
1402
+ })
1403
+ .catch((err: unknown) => bestEffort(err, "settled watchdog one-shot finalize", "error"));
1404
+ }
1405
+
1406
+ // [H1 U6] chat 域相位机整族已随旧协议轮次相位通道退役删除(语义迁移归属):
1407
+ // - handleChatRoundPhase(settled/idle/failed/active 相位分诊)
1408
+ // → settle 交棒 = run 应答驱动(Continuation onRunSettled 内
1409
+ // noteRoundSettledFromProtocol,先于轮终簿记);armMidRoundNoProgress 挂载 =
1410
+ // kickOffChatRound 轮开跑 arm;中段刷新 = run 事件通道 9 种既有事件。
1411
+ // - armChatIdleTimer(idle 相位帧 → idle timer 挂载)→ 随长驻消亡退役
1412
+ // (每轮 = 新 run,轮末进程随 agent_settled 回收,「5min idle 关闭」无对象——
1413
+ // 设计 §2.2#5;30 天 idle-gc 只归档不终态化不变)。
1414
+ // - backfillChatAnchor(idle 帧锚点回填)→ sessionFile 回填改由 run 应答
1415
+ // outcome.sessionFile 承载(+ writeBindingForRecord 落盘,UF-1)。
1416
+ // - onChatRoundFailed(failed 相位分诊)→ Continuation.onRunSettled 失败分支
1417
+ // 统一收口(doFinalizeRoundToIdle outcome=failed + 失败通知,D7)。
1418
+ // - settleChatRoundFromResponse(首轮成功 settle 载体)→ 语义按 D7 迁移清单并入
1419
+ // Continuation.settleRoundSuccess(round+1 / result=content / 门→route;
1420
+ // closeAfterRound 消费 chat 域退役、base 推进退役不迁移)。
1421
+ // - terminateChatSession / chatHandleFor(interact cancel/close 终止意图)→
1422
+ // 随 interact 面退役:真实杀链 = 轮级 abort signal → 协议 cancel 帧(引擎侧
1423
+ // 杀链)+ 镜像置死记账(killRecordChildWithEscalation)+ 引擎退出链收割
1424
+ // (engine-client teardownProcess,红线①)。
1425
+
1426
+ /** [W3] pi 引擎 port 解析(chat 域路由与终止面共用):registry cli 形态 port,
1427
+ * 未注册 = 不可用 stub(engine_not_found,见 pi-host-binding)。 */
1428
+ resolveChatEnginePort(): EnginePort {
1429
+ return resolveHostPiEnginePort(() => null);
1430
+ }
1431
+
1432
+ /**
1433
+ * Continuation 实例解析(ensure 语义)。跨重启 revive(cold-resurrect)会重建新
1434
+ * record 对象并 register——缓存实例的 record 绑定不一致时重建(Continuation 的
1435
+ * 状态写必须落 store 在册对象)。
1436
+ */
1437
+ continuationFor(record: ExecutionRecord): ConversationContinuation {
1438
+ const existing = this.continuations.get(record.id);
1439
+ if (existing !== undefined && existing.boundRecord === record) return existing;
1440
+ const created = new ConversationContinuation(record, {
1441
+ dispatchChatRound: (rec, input) => this.dispatchChatRoundForContinuation(rec, input),
1442
+ finalizeRoundOutcome: (rec, outcome) => this.finalizeRoundToIdle(rec, outcome),
1443
+ routeRecord: (rec) => this.deps.getCollectCoordinator().route(rec),
1444
+ notifyRecord: (n) => this.deps.getNotifyHost().notify(n),
1445
+ killStaleChild: (id) => this.killStaleChildBeforeDispatch(id),
1446
+ killRoundChild: (id, source) => this.killRoundChildForWatchdog(id, source),
1447
+ upgradeGateAllows: (rec) => this.canUpgradeToConversation(rec),
1448
+ reviveClosedRecord: (rec) => {
1449
+ // D4 revive 宿主面:register(跨重启重建后不在内存的形态)+ 迁移上报
1450
+ //(W16 类外状态写点同构——entry 落盘,live/reload 视图同步)。
1451
+ this.deps.getStore().register(rec);
1452
+ this.deps.getStore().reportRecordTransition(rec);
1453
+ },
1454
+ // [U2b 修复轮/D2] 轮始簿记(store.markRoundStarted)——Continuation 的唯一轮始写点。
1455
+ markRoundStarted: (rec) => {
1456
+ this.deps.getStore().markRoundStarted(rec.id);
1457
+ },
1458
+ closeNow: (rec) => this.deps.closeChatIdle(rec),
1459
+ });
1460
+ this.continuations.set(record.id, created);
1461
+ return created;
1462
+ }
1463
+
1464
+ /**
1465
+ * 泛化派发主干的 Continuation 轮入口(§3.4 ②载荷组装——归自 resumeColdRound
1466
+ * 现有实现):model 身份重建(splitEngineModelRef,防多轮模型漂移探针 P-10)、
1467
+ * ExecuteOptions 组装(worktree 句柄 / conversation:true)、detached 交棒
1468
+ * kickOffChatRound(priority = background,轮次在 background 跑)。
1469
+ */
1470
+ dispatchChatRoundForContinuation(record: ExecutionRecord, input: ContinuationDispatchInput): void {
1471
+ const model = splitEngineModelRef(record.model);
1472
+ const identity: ResolvedIdentity = {
1473
+ agent: record.agent,
1474
+ agentConfig: undefined,
1475
+ resolved: {
1476
+ model: { id: model.id, name: model.name, provider: model.provider, reasoning: false },
1477
+ thinkingLevel: record.thinkingLevel,
1478
+ },
1479
+ };
1480
+ const opts: ExecuteOptions = {
1481
+ task: input.task,
1482
+ slug: record.slug,
1483
+ worktree: record.worktreeHandle,
1484
+ conversation: true,
1485
+ };
1486
+ this.kickOffChatRound(record, opts, identity, input.signal, PRIORITY_BACKGROUND, input.resume, {
1487
+ onSettled: input.handlers.onSettled,
1488
+ onRejected: input.handlers.onRejected,
1489
+ onAbandoned: input.handlers.onAbandoned,
1490
+ onWatchdogFire: input.handlers.onWatchdogFire,
1491
+ });
1492
+ }
1493
+
1494
+ /**
1495
+ * [红线②派发前兜底] stale-child:镜像在途子进程活着(引擎存活期的状态错配——
1496
+ * Continuation 无在途 run 但镜像有活项)→ 镜像置死记账 + 协议 cancel(引擎侧杀链)
1497
+ * + 有界退出窗(STALE_CHILD_EXIT_WAIT_MS,pi trap flush 量级)。引擎已死场景的
1498
+ * 孤儿由引擎退出链收割兜底(红线①——engine-client teardownProcess,镜像此刻已
1499
+ * 整体置死,本兜底查不到,两通道正交)。
1500
+ */
1501
+ async killStaleChildBeforeDispatch(recordId: string): Promise<void> {
1502
+ if (!hasLiveProcessHandle(recordId)) return;
1503
+ killRecordChildWithEscalation(recordId, "stale-child guard (dispatch)");
1504
+ await delay(STALE_CHILD_EXIT_WAIT_MS);
1505
+ }
1506
+
1507
+ /** watchdog fire 的 kill 手段(kill + 协议 cancel)——run 收敛由杀链驱动,
1508
+ * 轮末收口统一回流 Continuation onRunSettled/onRoundRejected(单写者单路)。 */
1509
+ killRoundChildForWatchdog(recordId: string, source: string): void {
1510
+ killRecordChildWithEscalation(recordId, source);
1511
+ }
1512
+
1513
+ /**
1514
+ * [D5 双写点 gate 判据] SP-5 升级(one-shot → chatMode)的 conversation 位检查:
1515
+ * record 所属引擎(engine 留痕 ?? 默认引擎)capabilities.conversation 非
1516
+ * 'unsupported' 才放行。引擎未注册 = 无法验证续聊能力,fail-closed 拒绝。
1517
+ * 消费双写点:①进程内热升级(subagent-actions-core messageHandler)②跨重启冷升级
1518
+ *(Continuation D4 revive 格)——zcode 等 unsupported 引擎的 one-shot 收到 message
1519
+ * 不升级(含 parent-shutdown 可重连终态经 message 的升级旁路面)。
1520
+ */
1521
+ canUpgradeToConversation(record: Pick<ExecutionRecord, "engine">): boolean {
1522
+ try {
1523
+ const engine = getEngine(record.engine ?? DEFAULT_ENGINE_ID);
1524
+ return engine.capabilities().conversation !== "unsupported";
1525
+ } catch {
1526
+ return false;
1527
+ }
1528
+ }
1529
+
1530
+ /**
1531
+ * record 终态化路径的宿主侧收口汇聚点([H1 U2] Continuation 实例清理——终态后
1532
+ * 容器不再接收 message,实例滞留即泄漏;[H1 U6] 路由注销面已随 interact 面退役)。
1533
+ */
1534
+ onRecordFinalizedCleanup(recordId: string): void {
1535
+ this.continuations.delete(recordId);
1536
+ }
1537
+
1538
+ /**
1539
+ * 对话模式轮次完成收尾:委托 doFinalizeRoundToIdle(record 进 idle,保留内存 + worktree)。
1540
+ * 与 finalizeRecord 对称的委托方法,deps 同源注入。[H1 U2 / D7] 入参改轮终 outcome
1541
+ * 判别联合(成功 = content / 失败 = reason——result 写入规则归 finalize-record 单点)。
1542
+ * Continuation 轮末分流(success/failed 两分支)消费;one-shot SP-5 共享点已
1543
+ * [U2b] 改直连 store.markRoundIdle(settleOneShotOutcome 成功分支,不再经本方法)。
1544
+ */
1545
+ async finalizeRoundToIdle(
1546
+ record: ExecutionRecord,
1547
+ outcome: RoundSettlementOutcome,
1548
+ ): Promise<void> {
1549
+ await doFinalizeRoundToIdle(
1550
+ {
1551
+ worktreeManager: this.deps.getWorktreeManager(),
1552
+ store: this.deps.getStore(),
1553
+ modelService: this.deps.getModelService(),
1554
+ pi: this.deps.getPi(),
1555
+ emitUnregister: (id, st) => this.deps.getNotifyHost().emitPendingUnregister(id, st),
1556
+ },
1557
+ record,
1558
+ outcome,
1559
+ );
1560
+ }
1561
+
1562
+ /**
1563
+ * 分层并发配额:depth 越深可用配额越少(下限 1)。fork 深度护栏在池维度的投影,
1564
+ * 公式约定以 concurrency-pool.ts 注释为登记处、此处为唯一代码锚点。
1565
+ */
1566
+ effectiveMaxConcurrentFor(record: ExecutionRecord): number {
1567
+ return Math.max(1, this.deps.getPool().maxConcurrent - record.depth);
1568
+ }
1569
+
1570
+ /** [U04 提取·装配] 池槽获取:pooled(background)record 排队 acquire。成功返回 undefined
1571
+ * 继续执行;失败返回终态 result 供调用方 early-return(该路径在 try/finally 之前,
1572
+ * 不触发轮次资源回收——与原控制流逐字节一致)。 */
1573
+ async acquirePoolOrFinalize(
1574
+ record: ExecutionRecord,
1575
+ signal: AbortSignal | undefined,
1576
+ priority: number,
1577
+ ): Promise<AgentResult | undefined> {
1578
+ try {
1579
+ await this.deps.getPool().acquire(priority, this.effectiveMaxConcurrentFor(record), signal);
1580
+ } catch {
1581
+ // S1: 排队中被 abort(signal.aborted)走 cancelled,与已运行被 abort 一致。
1582
+ if (signal?.aborted) return this.deps.finalizeAborted(record);
1583
+ return this.deps.finalizeFailed(record, new Error("aborted"));
1584
+ }
1585
+ return undefined;
1586
+ }
1587
+
1588
+
1589
+ /** [U04 提取·回收] 轮次资源回收(finally 语义,幂等):池槽归还(仅 pooled 且 acquire
1590
+ * 成功)、streaming widget 清除。 */
1591
+ releaseRoundResources(
1592
+ _record: ExecutionRecord,
1593
+ holdSlot: boolean,
1594
+ stream: SubagentStream | undefined,
1595
+ ): void {
1596
+ if (holdSlot) this.deps.getPool().release();
1597
+ // 清除 streaming widget(subagent 终态,幂等)
1598
+ stream?.dispose();
1599
+ }
1600
+
1601
+ /**
1602
+ * [T4② / PS-4] idleTimeoutMs 合法域入口校验(>2^31-1 / 非有限值 fail-fast)。
1603
+ *
1604
+ * 旧链路:非法值穿透到 agent_settled 回调里的 armIdleTimer → assertSafeTimerDelay
1605
+ * throw 被异步 catch 降级——配置错误被吞成静默语义变更(每轮完成通知被 isIdle 放行门
1606
+ * 吞 + 进程无回收 timer)。对齐 shared/timer-delay「不静默 clamp」既有裁决:配置错误
1607
+ * 显式暴露,错误消息含合法范围(0/负数 = 显式禁用是合法语义,不在本校验域;env 非法值
1608
+ * 由 lifecycle-manager 的 warn 回落承接)。execute/executeAndAwait 两入口共用。
1609
+ */
1610
+ assertIdleTimeoutMsSafe(opts: ExecuteOptions): void {
1611
+ const v = opts.idleTimeoutMs;
1612
+ if (v === undefined) return;
1613
+ if (!Number.isFinite(v)) {
1614
+ throw new Error(
1615
+ `subagent idleTimeoutMs = ${v} is not a finite number (NaN/±Infinity). ` +
1616
+ `Valid range: a positive millisecond number up to ${MAX_TIMER_DELAY_MS} (2^31-1, Node setTimeout limit), or 0/negative to disable idle GC. ` +
1617
+ "Recovery: fix the idleTimeoutMs value and retry.",
1618
+ );
1619
+ }
1620
+ if (v > MAX_TIMER_DELAY_MS) {
1621
+ throw new Error(
1622
+ `subagent idleTimeoutMs = ${v} exceeds the Node setTimeout limit ` +
1623
+ `(${MAX_TIMER_DELAY_MS} ms = 2^31-1); larger delays silently collapse to 1ms and would kill the subagent immediately. ` +
1624
+ `Recovery: pass idleTimeoutMs <= ${MAX_TIMER_DELAY_MS}, or omit it for the default (${DEFAULT_IDLE_TIMEOUT_MS}ms), or use 0/negative to disable idle GC.`,
1625
+ );
1626
+ }
1627
+ }
1628
+
1629
+
1630
+ /**
1631
+ * [R4 / C-4 兑现] Continuation 实例全量清理显式接口——原壳 dispose 直调
1632
+ * `this.continuations.clear()` 的跨聚合边(r0-inventory 清单① C-4):字段所有权
1633
+ * 随域 #14 迁入本聚合,壳 dispose 编排改调本接口(聚合间零直写,G2)。
1634
+ */
1635
+ clearContinuations(): void {
1636
+ this.continuations.clear();
1637
+ }
1638
+
1639
+ /**
1640
+ * [R4 / C-5 邻接兑现] Continuation 在途轮打断清队显式接口——原壳装配闭包
1641
+ * `this.continuations.get(id)?.abortAndClearQueue()`(RecordLifecycle deps 的
1642
+ * abortContinuationQueue 回调):R4 抽取后回调改指本聚合接口。
1643
+ */
1644
+ abortContinuationQueue(recordId: string): void {
1645
+ this.continuations.get(recordId)?.abortAndClearQueue();
1646
+ }
1647
+ }