@zhushanwen/subagent-core 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/dist/{chunk-OL5BK4VN.js → chunk-LZJOEDGQ.js} +455 -203
  2. package/dist/engine-discovery-scan-Da4Dkzsi.d.cts +818 -0
  3. package/dist/engine-discovery-scan-Da4Dkzsi.d.ts +818 -0
  4. package/dist/execution/engine/engine-discovery-scan.cjs +216 -155
  5. package/dist/execution/engine/engine-discovery-scan.d.cts +1 -2
  6. package/dist/execution/engine/engine-discovery-scan.d.ts +1 -2
  7. package/dist/execution/engine/engine-discovery-scan.js +1 -1
  8. package/dist/execution/relay-env.js +5 -4
  9. package/dist/index.cjs +7686 -5894
  10. package/dist/index.d.cts +1876 -1360
  11. package/dist/index.d.ts +1876 -1360
  12. package/dist/index.js +7048 -5493
  13. package/dist.bundle/index.cjs +7747 -5957
  14. package/package.json +4 -4
  15. package/src/__tests__/manifest-store.test.ts +16 -15
  16. package/src/__tests__/robustness-medium-batch1.test.ts +10 -14
  17. package/src/__tests__/robustness-medium-batch2.test.ts +7 -2
  18. package/src/execution/__tests__/alive-store.test.ts +117 -1
  19. package/src/execution/__tests__/batch-finalized.test.ts +206 -0
  20. package/src/execution/__tests__/chat-engine-routing.test.ts +117 -1
  21. package/src/execution/__tests__/{cold-resurrect.test.ts → cold-lookup.test.ts} +133 -33
  22. package/src/execution/__tests__/collect-coordinator-service.test.ts +7 -5
  23. package/src/execution/__tests__/conversation-continuation.test.ts +1452 -0
  24. package/src/execution/__tests__/delivery-methods.test.ts +90 -176
  25. package/src/execution/__tests__/dialog-queue.test.ts +70 -3
  26. package/src/execution/__tests__/dispose-manifest-recovery.test.ts +475 -0
  27. package/src/execution/__tests__/execute-and-await-worktree.test.ts +3 -3
  28. package/src/execution/__tests__/execute-nesting.test.ts +8 -5
  29. package/src/execution/__tests__/execution-record.test.ts +8 -148
  30. package/src/execution/__tests__/finalize-record.test.ts +295 -129
  31. package/src/execution/__tests__/get-record-for-action-restart.test.ts +21 -29
  32. package/src/execution/__tests__/helpers/fake-engine-port.ts +13 -68
  33. package/src/execution/__tests__/helpers/legacy-sidecar.ts +33 -0
  34. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +9 -5
  35. package/src/execution/__tests__/idle-gc.test.ts +60 -0
  36. package/src/execution/__tests__/lifecycle-manager.test.ts +3 -254
  37. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +43 -22
  38. package/src/execution/__tests__/model-config-service.test.ts +111 -0
  39. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +10 -6
  40. package/src/execution/__tests__/notify-ledger.test.ts +42 -1
  41. package/src/execution/__tests__/rebuild-indexes.test.ts +329 -0
  42. package/src/execution/__tests__/record-binding.test.ts +613 -0
  43. package/src/execution/__tests__/record-origin.test.ts +318 -0
  44. package/src/execution/__tests__/record-store-intent-api.test.ts +600 -0
  45. package/src/execution/__tests__/record-store-orphan-revive.test.ts +71 -3
  46. package/src/execution/__tests__/record-store.test.ts +72 -54
  47. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +8 -5
  48. package/src/execution/__tests__/round-supervisor-workflow-origin.test.ts +205 -0
  49. package/src/execution/__tests__/run-orchestration-write-lease.test.ts +293 -0
  50. package/src/execution/__tests__/run-orchestration.test.ts +199 -0
  51. package/src/execution/__tests__/session-file-gc.test.ts +35 -4
  52. package/src/execution/__tests__/session-reconstructor.test.ts +78 -0
  53. package/src/execution/__tests__/state-marker.test.ts +288 -0
  54. package/src/execution/__tests__/stream-sink-retirement.test.ts +8 -5
  55. package/src/execution/__tests__/subagent-actions-core.test.ts +67 -16
  56. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +0 -1
  57. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +27 -11
  58. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +17 -11
  59. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +130 -53
  60. package/src/execution/__tests__/subagent-service.test.ts +17 -215
  61. package/src/execution/__tests__/subprocess-agent-runner-no-progress-killall.test.ts +138 -0
  62. package/src/execution/__tests__/subprocess-agent-runner.test.ts +105 -660
  63. package/src/execution/__tests__/sync-collect-recovery.test.ts +23 -16
  64. package/src/execution/__tests__/terminal-write-latency.test.ts +79 -0
  65. package/src/execution/__tests__/workflow-agent-dispatch.test.ts +782 -0
  66. package/src/execution/alive-store.ts +37 -28
  67. package/src/execution/{cold-resurrect.ts → cold-lookup.ts} +48 -45
  68. package/src/execution/config.ts +1 -1
  69. package/src/execution/conversation-continuation.ts +541 -0
  70. package/src/execution/dialog-queue.ts +60 -12
  71. package/src/execution/engine/__tests__/common/capability-gate.test.ts +1 -1
  72. package/src/execution/engine/__tests__/common/journal-wiring.test.ts +16 -1
  73. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +17 -51
  74. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +0 -5
  75. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +1 -1
  76. package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +5 -9
  77. package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +0 -3
  78. package/src/execution/engine/__tests__/engine-discovery.test.ts +0 -3
  79. package/src/execution/engine/__tests__/model-prompt.test.ts +0 -3
  80. package/src/execution/engine/__tests__/registry.test.ts +0 -1
  81. package/src/execution/engine/__tests__/routing.test.ts +0 -1
  82. package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +0 -3
  83. package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +0 -3
  84. package/src/execution/engine/client/__tests__/engine-client-reap.test.ts +212 -0
  85. package/src/execution/engine/client/__tests__/mirror.test.ts +73 -1
  86. package/src/execution/engine/client/__tests__/protocol-closure.test.ts +2 -9
  87. package/src/execution/engine/client/__tests__/remote-engine.test.ts +46 -12
  88. package/src/execution/engine/client/engine-client.ts +81 -25
  89. package/src/execution/engine/client/mirror.ts +9 -0
  90. package/src/execution/engine/client/remote-engine.ts +52 -34
  91. package/src/execution/engine/client/reverse-router.ts +6 -27
  92. package/src/execution/engine/common/__tests__/run-signals.test.ts +79 -0
  93. package/src/execution/engine/common/capability-gate.ts +27 -0
  94. package/src/execution/engine/common/data-dir.ts +3 -2
  95. package/src/execution/engine/common/errors.ts +1 -1
  96. package/src/execution/engine/common/journal-wiring.ts +18 -9
  97. package/src/execution/engine/common/run-signals.ts +83 -0
  98. package/src/execution/engine/d8-compat.ts +1 -1
  99. package/src/execution/engine/host/__tests__/host-bridge.test.ts +3 -18
  100. package/src/execution/engine/host/host-bridge.ts +19 -41
  101. package/src/execution/engine/host/pi-host-binding.ts +5 -5
  102. package/src/execution/engine/port.ts +34 -54
  103. package/src/execution/engine/types.ts +11 -14
  104. package/src/execution/execution-record.ts +15 -47
  105. package/src/execution/finalize-record.ts +94 -197
  106. package/src/execution/idle-gc.ts +9 -3
  107. package/src/execution/lifecycle-manager.ts +25 -361
  108. package/src/execution/lifecycle-predicates.ts +22 -19
  109. package/src/execution/manifest-store.ts +40 -42
  110. package/src/execution/model-config-service.ts +1 -1
  111. package/src/execution/notifier.ts +39 -1
  112. package/src/execution/notify-host.ts +13 -4
  113. package/src/execution/record-entry.ts +20 -2
  114. package/src/execution/record-store.ts +925 -160
  115. package/src/execution/relay-env.ts +8 -3
  116. package/src/execution/round-supervisor/domain.ts +17 -5
  117. package/src/execution/round-supervisor/service-binding.test.ts +89 -53
  118. package/src/execution/round-supervisor/service-binding.ts +71 -21
  119. package/src/execution/round-supervisor/supervisor.ts +23 -9
  120. package/src/execution/service/record-access.ts +547 -0
  121. package/src/execution/service/record-lifecycle.ts +542 -0
  122. package/src/execution/service/run-orchestration.ts +1647 -0
  123. package/src/execution/service/service-bootstrap.ts +111 -0
  124. package/src/execution/service/service-constants.ts +27 -0
  125. package/src/execution/service/session-baselines.ts +401 -0
  126. package/src/execution/service/sync-collect-domain.ts +442 -0
  127. package/src/execution/service/workflow-dispatch.ts +567 -0
  128. package/src/execution/session-file-gc.ts +11 -8
  129. package/src/execution/session-reconstructor.ts +44 -1
  130. package/src/execution/sessions-index.ts +20 -3
  131. package/src/execution/settled-watchdog.ts +86 -43
  132. package/src/execution/state-marker.ts +516 -0
  133. package/src/execution/stream-sink.ts +1 -1
  134. package/src/execution/subagent-actions-core.ts +53 -31
  135. package/src/execution/subagent-service.ts +643 -2980
  136. package/src/execution/subprocess-agent-runner.ts +46 -323
  137. package/src/execution/types.ts +60 -43
  138. package/src/execution/ui-request-handler-factory.ts +5 -0
  139. package/src/execution/worktree-manager.ts +1 -1
  140. package/src/execution/worktree-registry.ts +1 -1
  141. package/src/index.ts +6 -5
  142. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -3
  143. package/src/orchestration/__tests__/agent-call-stream.test.ts +24 -49
  144. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +4 -4
  145. package/src/orchestration/__tests__/file-run-store.test.ts +5 -7
  146. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +2 -3
  147. package/src/orchestration/__tests__/run-snapshot.test.ts +27 -43
  148. package/src/orchestration/__tests__/worker-message-pump-workflow-dispatch.test.ts +274 -0
  149. package/src/orchestration/execute-agent-call.ts +1 -1
  150. package/src/orchestration/file-run-store.ts +1 -1
  151. package/src/orchestration/models/ports.ts +20 -13
  152. package/src/orchestration/models/types.ts +11 -13
  153. package/src/orchestration/run-snapshot.ts +18 -22
  154. package/src/orchestration/worker-message-pump.ts +33 -57
  155. package/src/shared/atomic-write.ts +10 -8
  156. package/dist/chunk-T2SYBW3I.js +0 -24
  157. package/dist/engine-discovery-scan-BXLA8y-z.d.cts +0 -1624
  158. package/dist/engine-discovery-scan-BXLA8y-z.d.ts +0 -1624
  159. package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +0 -255
  160. package/src/execution/__tests__/finalized-marker.test.ts +0 -104
  161. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +0 -211
  162. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +0 -488
  163. package/src/execution/__tests__/subprocess-agent-runner-timeout.test.ts +0 -75
  164. package/src/execution/__tests__/tombstone-store.test.ts +0 -73
  165. package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +0 -230
  166. package/src/execution/finalized-marker.ts +0 -70
  167. package/src/execution/tombstone-store.ts +0 -72
@@ -25,6 +25,7 @@ import type {
25
25
  ExecutionMode,
26
26
  ExecutionStatus,
27
27
  InternalToolCall,
28
+ RecordOrigin,
28
29
  Turn,
29
30
  } from "./types.ts";
30
31
  import { extractLabelFromArgs } from "./execution-record.ts";
@@ -111,6 +112,14 @@ export interface SubagentIdentityData {
111
112
  worktree?: boolean;
112
113
  /** @deprecated 兼容旧文件:旧 identity entry 写的是 parentSessionId,读取时 fallback 到 rootSessionId。 */
113
114
  parentSessionId?: string;
115
+ /**
116
+ * 来源身份(H2 S3 修复)。旧文件缺失 → undefined(= "tool" 语义,缺省零迁移)。
117
+ * standalone 形态(identity entry 链)的 workflow record 经本字段在重建面保留
118
+ * origin 过滤语义;读取侧经 normalizeReconOrigin 守卫归一(非法值 → undefined)。
119
+ */
120
+ origin?: RecordOrigin;
121
+ /** origin="workflow" 时所属 workflow run id。旧文件缺失 → undefined。 */
122
+ parentRunId?: string;
114
123
  }
115
124
 
116
125
  /** SDK jsonl entry(getEntries() 返回,header 已排除)。 */
@@ -157,6 +166,14 @@ export interface ReconstructedRecord {
157
166
  chatMode?: boolean;
158
167
  /** [review round2] worktree 隔离标志(见 SubagentIdentityData.worktree)。 */
159
168
  worktree?: boolean;
169
+ /**
170
+ * 来源身份(H2 S3 修复,来自 identity custom entry 经守卫归一;旧文件 undefined =
171
+ * "tool" 语义)。漏本字段则全量重建面(getFullRecord → reconstructFromFile)丢
172
+ * origin,workflow record 逃过 D1 投影过滤。
173
+ */
174
+ origin: RecordOrigin | undefined;
175
+ /** origin="workflow" 时所属 workflow run id(守卫归一后;缺省 undefined)。 */
176
+ parentRunId: string | undefined;
160
177
  /**
161
178
  * 对话轮次计数(非 identity entry 字段,reconstructFromFile 不填)。
162
179
  * V2 idle record 的 round 只在内存维护(doFinalizeRoundToIdle 递增),磁盘重建不恢复。
@@ -259,6 +276,21 @@ function isIdentityData(data: unknown): data is SubagentIdentityData {
259
276
  typeof d.task === "string" &&
260
277
  typeof d.startedAt === "number"
261
278
  );
279
+ }
280
+
281
+ /**
282
+ * 来源域守卫归一(H2 S3 修复):origin 非法值/缺省 → undefined(= "tool" 语义)。
283
+ * 字面量守卫与 record-store.readEntryOriginFields(主 entry 重建侧)逐字对齐——
284
+ * identity entry 的 data 经 JSON.parse 后 origin 运行时值不可信,TS 类型只是断言,
285
+ * 消费投影点必须守卫。
286
+ */
287
+ function normalizeReconOrigin(v: unknown): RecordOrigin | undefined {
288
+ return v === "workflow" || v === "tool" ? v : undefined;
289
+ }
290
+
291
+ /** parentRunId 安全读取(非 string → undefined),与 normalizeReconOrigin 同消费点。 */
292
+ function normalizeReconParentRunId(v: unknown): string | undefined {
293
+ return typeof v === "string" ? v : undefined;
262
294
  }/**
263
295
  * 待匹配的 toolCall(assistant 发起,等 toolResult 回填)。
264
296
  * 记录在 assistant Turn 上,toolResult 到达时按 toolCallId 找到并填充。
@@ -485,6 +517,9 @@ function buildReconstructedRecord(
485
517
  const rootSessionId = identity.rootSessionId ?? identity.parentSessionId;
486
518
  // slug 兜底:旧 identity entry 无 slug 字段 → 空串(与 SubagentRecord.slug: string 契约一致)。
487
519
  const slug = identity.slug ?? "";
520
+ // 来源域归一(H2 S3):非法值/缺省 → undefined = "tool" 语义(守卫见 normalizeReconOrigin)。
521
+ const origin = normalizeReconOrigin(identity.origin);
522
+ const parentRunId = normalizeReconParentRunId(identity.parentRunId);
488
523
  return {
489
524
  ...identity,
490
525
  slug,
@@ -492,6 +527,8 @@ function buildReconstructedRecord(
492
527
  parentRecordId: identity.parentRecordId,
493
528
  depth: identity.depth ?? 0,
494
529
  forkDepth: identity.forkDepth,
530
+ origin,
531
+ parentRunId,
495
532
  sessionFile,
496
533
  status,
497
534
  closedReason,
@@ -587,6 +624,10 @@ export interface IdentityHeaderRecon {
587
624
  chatMode?: boolean;
588
625
  /** [review round2] worktree 隔离标志(见 SubagentIdentityData.worktree)。 */
589
626
  worktree?: boolean;
627
+ /** 来源身份(H2 S3 修复,守卫归一后;缺省 undefined = "tool" 语义)。light 列表投影用。 */
628
+ origin: RecordOrigin | undefined;
629
+ /** origin="workflow" 时所属 workflow run id(守卫归一后;缺省 undefined)。 */
630
+ parentRunId: string | undefined;
590
631
  model: string;
591
632
  thinkingLevel: string | undefined;
592
633
  sessionFile: string;
@@ -743,7 +784,7 @@ function absorbScannedEntry(state: LightIdentityScan, entry: JsonlEntry): boolea
743
784
  return false;
744
785
  }
745
786
 
746
- /** 扫描态 → IdentityHeaderRecon(归一化与全量 recon 同源:rootSessionId fallback 旧字段、slug 兜底空串)。 */
787
+ /** 扫描态 → IdentityHeaderRecon(归一化与全量 recon 同源:rootSessionId fallback 旧字段、slug 兜底空串、origin/parentRunId 守卫归一)。 */
747
788
  function toIdentityRecon(
748
789
  state: LightIdentityScan,
749
790
  identity: SubagentIdentityData,
@@ -763,6 +804,8 @@ function toIdentityRecon(
763
804
  forkDepth: identity.forkDepth,
764
805
  chatMode: identity.chatMode,
765
806
  worktree: identity.worktree,
807
+ origin: normalizeReconOrigin(identity.origin),
808
+ parentRunId: normalizeReconParentRunId(identity.parentRunId),
766
809
  model: state.model,
767
810
  thinkingLevel: state.thinkingLevel,
768
811
  sessionFile,
@@ -26,7 +26,7 @@ import * as path from "node:path";
26
26
  import { getLogger } from "../core/logger.ts";
27
27
  import { writeAtomicFile } from "../shared/atomic-write.ts";
28
28
 
29
- import type { ExecutionMode } from "./types.ts";
29
+ import type { ExecutionMode, RecordOrigin } from "./types.ts";
30
30
 
31
31
  const logger = getLogger("subagents");
32
32
 
@@ -74,6 +74,14 @@ export interface SessionsIndexEntry {
74
74
  /** 空串合法:尾部探测(readIdentityTail)拿不到 model 时的合法结果,不当损坏。 */
75
75
  model: string;
76
76
  thinkingLevel: string | undefined;
77
+ /**
78
+ * 来源身份(H2 S3 修复:light→索引→重建往返闭合)。undefined 表示缺失
79
+ * (旧索引文件/origin 缺省 = "tool" 语义),可选字段不升 INDEX_VERSION——
80
+ * 对齐 rootSessionId/parentRecordId 先例(宽容守卫 + 序列化自然缺省)。
81
+ */
82
+ origin?: RecordOrigin;
83
+ /** origin="workflow" 时所属 workflow run id(undefined = 缺失/tool 来源)。 */
84
+ parentRunId?: string;
77
85
  }
78
86
 
79
87
  /**
@@ -133,11 +141,19 @@ function hasSessionDescFields(v: Record<string, unknown>): boolean {
133
141
  return typeof v.task === "string" && typeof v.slug === "string" && typeof v.startedAt === "number";
134
142
  }
135
143
 
136
- /** 可选 string 字段组(rootSessionId/parentRecordId:undefined 表示缺失,合法)。 */
144
+ /** 可选 string 字段组(rootSessionId/parentRecordId/parentRunId:undefined 表示缺失,合法)。 */
137
145
  function hasOptionalStringFields(v: Record<string, unknown>): boolean {
138
146
  return (
139
147
  (v.rootSessionId === undefined || typeof v.rootSessionId === "string") &&
140
- (v.parentRecordId === undefined || typeof v.parentRecordId === "string")
148
+ (v.parentRecordId === undefined || typeof v.parentRecordId === "string") &&
149
+ (v.parentRunId === undefined || typeof v.parentRunId === "string")
150
+ );
151
+ }
152
+
153
+ /** 来源域字段组(origin 守卫宽容:undefined 缺省合法,字面量白名单对齐 readEntryOriginFields)。 */
154
+ function hasOriginFields(v: Record<string, unknown>): boolean {
155
+ return (
156
+ v.origin === undefined || v.origin === "workflow" || v.origin === "tool"
141
157
  );
142
158
  }
143
159
 
@@ -158,6 +174,7 @@ function isPositiveIndexEntry(raw: unknown): raw is SessionsIndexEntry {
158
174
  hasStampAndIdentityFields(v) &&
159
175
  hasSessionDescFields(v) &&
160
176
  hasOptionalStringFields(v) &&
177
+ hasOriginFields(v) &&
161
178
  hasModelFields(v)
162
179
  );
163
180
  }
@@ -8,7 +8,7 @@
8
8
  // 中段(prompt → agent_end):工作段,输出即进展——无进展检测
9
9
  // (armMidRoundNoProgress,锚点:prompt 发出时 arm;有效协议事件行刷新;
10
10
  // 连续静默 SETTLED_MID_ROUND_NO_PROGRESS_MS=30min → kill + 该轮失败终态化)。
11
- // 对齐 keep-alive 无进展检测先例(KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS 同构:
11
+ // 对齐既有 keep-alive 无进展检测先例的 30min 量级(同构思路:
12
12
  // 「仍在推进」由 stdout 有效事件定义而非总时长,修复旧 10min 全程固定窗对
13
13
  // >10min 合法 chatMode 单轮的误杀)。
14
14
  //
@@ -31,14 +31,22 @@
31
31
  // spawn watchdog 默认关」的三无窗口,warn 明示后果),未设/非法 = 默认 600s。
32
32
  // 中段阈值 v1 不开 env(减法:与 keep-alive 先例同为纯常量)。
33
33
  //
34
- // 双挂载点共用同一原语——同一组常量 + 同一组挂载/交棒/清除 helper,仅两个 prompt
35
- // 发出点(设计 T2-③ 明示架构,两处各写一套恰是被否的「散布姿势」微缩复发):
36
- // - 首轮:session-runner.ts runSpawn(prompt 发出后 arm 中段)
37
- // - 后续轮次热路径:subagent-service.ts deliverMessage(发出新一轮 prompt arm 中段)
38
- // 事件侧接线(两路径共用同一 stdout pump 闭包,均在 session-runner.ts):
39
- // - 有效协议事件行 → refreshMidRoundNoProgress(中段刷新)
40
- // - agent_end(chatMode,!willRetry)→ handoverMidRoundToSettled(交棒收尾段)
41
- // - agent_settled / close / 收尾 → disarmSettledWatchdog(两段一并清)
34
+ // [M3 耦合登记] 本原语自 M3 起被 workflow 域复用([H2 W2/W4] 起挂载点 = subagent-service.ts
35
+ // armWorkflowNoProgressWatchdog(runWorkflowEngineTask arm;原 SAR.run per-call 守护
36
+ // W4 掏空归位),armMidRoundNoProgress 同一入口)——故 env ≤0 的「关闭两段」不再是
37
+ // chat 域局部行为:workflow G1 熔断(静默楔死 run 的 30min 无进展回收)一并失效。
38
+ // warn 文案已明示该连带后果(env 开关语义本身不动:设计明文「中段阈值 v1 不开 env」,
39
+ // 有界兜底的处置必须可解释,规则 19)。
40
+ //
41
+ // 挂载点(同一原语——同一组常量 + 同一组挂载/交棒/清除 helper;[H1 U6] 收敛为单挂载):
42
+ // - 会话形态轮:subagent-service.ts kickOffChatRound(轮开跑 arm 中段;[H1 U6] 旧
43
+ // 热路径 deliverChatMessage 第二挂载点随 interact 面退役)
44
+ // - workflow 域:subagent-service.ts armWorkflowNoProgressWatchdog([H2 W2];原
45
+ // SAR.run per-call 守护随 [H2 W4] 掏空归位,M3 起复用)
46
+ // 事件侧接线([H1 U6] 刷新源 = run 事件通道既有事件;交棒 = run 应答驱动):
47
+ // - 有效协议事件行 → refreshMidRoundNoProgress / refreshFromProtocolEvent(中段刷新)
48
+ // - run 应答收敛 → noteRoundSettledFromProtocol(交棒收尾段,Continuation onRunSettled)
49
+ // - 轮终簿记 / close / 终态化 → disarmRoundFromProtocol / disarmSettledWatchdog(两段一并清)
42
50
  //
43
51
  // 与 lifecycle-manager 的 idle timer 互补:idle timer 管 settled 已到达后的空闲
44
52
  // 回收,本原语管 settled 永不到达的 wedged——两条正交通道不互相替代。
@@ -53,11 +61,11 @@ import { assertSafeTimerDelay } from "../shared/timer-delay.ts";
53
61
 
54
62
  const logger = getLogger("subagents");
55
63
 
56
- /** 时间单位换算常量(命名后供 watchdog 常量组合,对齐 session-runner 同名先例,消除裸乘法字面量)。 */
64
+ /** 时间单位换算常量(命名后供 watchdog 常量组合,消除裸乘法字面量)。 */
57
65
  const MS_PER_SECOND = 1000;
58
66
  const SECONDS_PER_MINUTE = 60;
59
67
 
60
- /** 中段无进展阈值(分钟):对齐 KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS 30min 量级。 */
68
+ /** 中段无进展阈值(分钟):对齐既有 keep-alive 无进展检测先例的 30min 量级。 */
61
69
  const MID_ROUND_MINUTES = 30;
62
70
 
63
71
  /**
@@ -73,7 +81,7 @@ export const SETTLED_WATCHDOG_TIMEOUT_MS = 600_000;
73
81
  /**
74
82
  * 中段(prompt → agent_end)无进展检测的连续静默阈值(30min)。
75
83
  *
76
- * 对齐 KEEP_ALIVE_NO_PROGRESS_TIMEOUT_MS 先例(30min 无进展检测,量级同源)。
84
+ * 对齐既有 keep-alive 无进展检测先例(30min 无进展检测,量级同源)。
77
85
  * 刷新面严格限定 stdout pump 解析出的**有效协议事件行**(message_* 与 tool_* 与 turn_end
78
86
  * 等 SdkEvent)——LC-9 的 invalid 行(非法 JSON / 缺 type 字段的调试噪音)不刷新,
79
87
  * 防调试输出续命。v1 不开 env(设计 §6-D9 定案:与 keep-alive 同为纯常量)。
@@ -81,14 +89,41 @@ export const SETTLED_WATCHDOG_TIMEOUT_MS = 600_000;
81
89
  */
82
90
  export const SETTLED_MID_ROUND_NO_PROGRESS_MS = MID_ROUND_MINUTES * SECONDS_PER_MINUTE * MS_PER_SECOND;
83
91
 
92
+ /**
93
+ * [M6,仅测试可达] 中段窗长覆盖值(秒级窗真跑全链测试的注入口)。
94
+ *
95
+ * 生产恒 undefined → 生效值 = SETTLED_MID_ROUND_NO_PROGRESS_MS(30min,默认值由单测
96
+ * 断言守护)。形态对齐既有测试钩子先例 `_resetSettledWatchdogsForTest`(模块级状态 +
97
+ * `_` 前缀导出),不新造第二套模式;`_resetSettledWatchdogsForTest` 一并复位,防用例间
98
+ * 污染。刻意不做 env:设计明文「中段阈值 v1 不开 env」(每个 knob 都是误配置通道,
99
+ * 用户调短会误杀正常长任务)。
100
+ */
101
+ let midRoundWindowOverrideMs: number | undefined;
102
+
103
+ /** 中段生效窗长:测试注入优先,否则常量默认(生产恒 30min)。arm 与 refresh 共用单一读取点。 */
104
+ export function getMidRoundNoProgressWindowMs(): number {
105
+ return midRoundWindowOverrideMs ?? SETTLED_MID_ROUND_NO_PROGRESS_MS;
106
+ }
107
+
108
+ /**
109
+ * [仅测试] 覆盖中段无进展窗长(传 undefined 复位)。生产路径零调用——生产生效值恒为
110
+ * SETTLED_MID_ROUND_NO_PROGRESS_MS。
111
+ */
112
+ export function _setMidRoundNoProgressWindowMsForTest(ms: number | undefined): void {
113
+ midRoundWindowOverrideMs = ms;
114
+ }
115
+
84
116
  /**
85
117
  * 收尾段定值的用户覆盖 env(规则 19:用户显式指定才生效)。
86
118
  *
87
119
  * >0 覆盖收尾段(SETTLED_WATCHDOG_TIMEOUT_MS 默认);≤0 关闭两段(回到三无窗口,
88
- * warn 明示后果);未设 = 默认 600s;非数字 = 非法回落默认 + warn 留痕(对齐
89
- * XYZ_SUBAGENT_IDLE_TIMEOUT_MS LC-7 教训:非法回落必须可见)。前缀用
90
- * XYZ_SUBAGENT_*(ENV_WHITELIST_PREFIXES 白名单,PI_ 前缀在桌面 spawn 链被静默
91
- * 丢弃——同 XYZ_SUBAGENT_IDLE_TIMEOUT_MS 改名教训)。
120
+ * warn 明示后果——[M3 耦合登记] M3 起该 no-op 同时关掉 workflow 域 G1 熔断:
121
+ * subagent-service.ts armWorkflowNoProgressWatchdog(原 SAR.run per-call 守护,[H2 W4]
122
+ * 掏空归位)的 no-progress 守护经 armMidRoundNoProgress 同一入口挂载,watchdog 不 arm
123
+ * workflow 域静默楔死同样无独立回收计时);未设 = 默认 600s;非数字 = 非法回落
124
+ * 默认 + warn 留痕(对齐 XYZ_SUBAGENT_IDLE_TIMEOUT_MS 的 LC-7 教训:非法回落必须
125
+ * 可见)。前缀用 XYZ_SUBAGENT_*(ENV_WHITELIST_PREFIXES 白名单,PI_ 前缀在桌面
126
+ * spawn 链被静默丢弃——同 XYZ_SUBAGENT_IDLE_TIMEOUT_MS 改名教训)。
92
127
  */
93
128
  export const SETTLED_WATCHDOG_ENV = "XYZ_SUBAGENT_SETTLED_WATCHDOG_MS";
94
129
 
@@ -134,9 +169,13 @@ function resolveSettledWatchdogEnv(): { disabled: boolean; overrideMs?: number }
134
169
  if (parsed <= 0) {
135
170
  logger.warn(
136
171
  `[settled-watchdog] ${SETTLED_WATCHDOG_ENV}=${parsed} disables BOTH watchdog phases (mid-round ` +
137
- `no-progress + settled phase limit). Consequence: a wedged chatMode round (no agent_end, or ` +
138
- `agent_settled never arriving) has NO independent recovery timer — the process leaks until the ` +
139
- `host exits (the "three-no-window" shape). Recovery: unset the env or set a positive ms value.`,
172
+ `no-progress + settled phase limit) for ALL domains. Consequences: (1) a wedged chatMode round ` +
173
+ `(no agent_end, or agent_settled never arriving) has NO independent recovery timer — the process ` +
174
+ `leaks until the host exits (the "three-no-window" shape); (2) the workflow-domain no-progress ` +
175
+ `fuse (M3: workflow dispatch arms through this same primitive — ` +
176
+ `SubagentService.runWorkflowEngineTask, see [H2 W4]) is silently disabled too — a wedged workflow ` +
177
+ `run then stalls with no terminal notification even though the chat-domain docs only describe (1). ` +
178
+ `Recovery: unset the env or set a positive ms value.`,
140
179
  );
141
180
  envCache.disabled = true;
142
181
  return envCache;
@@ -180,7 +219,8 @@ function clearEntry(recordId: string): void {
180
219
  * - 到期行为由调用方注入(kill 进程 + 该轮失败终态化)——本模块不持有
181
220
  * ChildProcess / record,与 lifecycle-manager 同构(副作用能力由调用方注入,
182
221
  * 本模块只管 timer 生命周期,可独立编译 + 单测)。
183
- * - env 显式关闭(≤0)时 no-op(解析时已 warn 明示后果)。
222
+ * - env 显式关闭(≤0)时 no-op(解析时已 warn 明示后果——含 workflow 域熔断连带的
223
+ * 明示文案;M3 复用本入口,故本 no-op 不是 chat 域局部行为)。
184
224
  */
185
225
  export function armMidRoundNoProgress(
186
226
  recordId: string,
@@ -191,15 +231,17 @@ export function armMidRoundNoProgress(
191
231
  ): void {
192
232
  if (isSettledWatchdogDisabled()) return;
193
233
  clearEntry(recordId);
194
- // 常量恒在安全域内;入口统一校验防未来值改可配置时静默引入 1ms 溢出语义反转。
195
- assertSafeTimerDelay(SETTLED_MID_ROUND_NO_PROGRESS_MS, "settled watchdog (mid-round)");
234
+ // 生产恒为常量(安全域内);测试注入的秒级窗同样过校验——入口统一校验防未来值改
235
+ // 可配置时静默引入 1ms 溢出语义反转。
236
+ const windowMs = getMidRoundNoProgressWindowMs();
237
+ assertSafeTimerDelay(windowMs, "settled watchdog (mid-round)");
196
238
  const timer = setTimeout(() => {
197
239
  armedEntries.delete(recordId);
198
240
  logger.debug(
199
- `[settled-watchdog] mid-round fired for ${recordId} after ${SETTLED_MID_ROUND_NO_PROGRESS_MS}ms without a valid protocol event`,
241
+ `[settled-watchdog] mid-round fired for ${recordId} after ${windowMs}ms without a valid protocol event`,
200
242
  );
201
- handlers.onMidTimeout({ phase: "mid-round", waitedMs: SETTLED_MID_ROUND_NO_PROGRESS_MS });
202
- }, SETTLED_MID_ROUND_NO_PROGRESS_MS);
243
+ handlers.onMidTimeout({ phase: "mid-round", waitedMs: windowMs });
244
+ }, windowMs);
203
245
  timer.unref();
204
246
  armedEntries.set(recordId, {
205
247
  timer,
@@ -263,15 +305,16 @@ export function refreshMidRoundNoProgress(recordId: string): void {
263
305
  const entry = armedEntries.get(recordId);
264
306
  if (!entry || entry.phase !== "mid-round") return;
265
307
  clearTimeout(entry.timer);
266
- assertSafeTimerDelay(SETTLED_MID_ROUND_NO_PROGRESS_MS, "settled watchdog (mid-round refresh)");
308
+ const windowMs = getMidRoundNoProgressWindowMs();
309
+ assertSafeTimerDelay(windowMs, "settled watchdog (mid-round refresh)");
267
310
  const onMidTimeout = entry.onMidTimeout;
268
311
  const timer = setTimeout(() => {
269
312
  armedEntries.delete(recordId);
270
313
  logger.debug(
271
- `[settled-watchdog] mid-round fired for ${recordId} after ${SETTLED_MID_ROUND_NO_PROGRESS_MS}ms without a valid protocol event`,
314
+ `[settled-watchdog] mid-round fired for ${recordId} after ${windowMs}ms without a valid protocol event`,
272
315
  );
273
- onMidTimeout?.({ phase: "mid-round", waitedMs: SETTLED_MID_ROUND_NO_PROGRESS_MS });
274
- }, SETTLED_MID_ROUND_NO_PROGRESS_MS);
316
+ onMidTimeout?.({ phase: "mid-round", waitedMs: windowMs });
317
+ }, windowMs);
275
318
  timer.unref();
276
319
  entry.timer = timer;
277
320
  }
@@ -299,8 +342,9 @@ export function disarmSettledWatchdog(recordId: string): void {
299
342
  }
300
343
 
301
344
  /**
302
- * 清空全部 armed entry + env 解析缓存(测试隔离用,beforeEach 调;命名对齐
303
- * _resetLifecycleState 先例。清缓存使测试内 vi.stubEnv 改 env 后重新解析生效)。
345
+ * 清空全部 armed entry + env 解析缓存 + 中段窗测试覆盖(测试隔离用,beforeEach/afterEach
346
+ * 调;命名对齐 _resetLifecycleState 先例。清缓存使测试内 vi.stubEnv 改 env 后重新解析
347
+ * 生效;复位 midRoundWindowOverrideMs 防 M6 秒级窗污染其他用例)。
304
348
  */
305
349
  export function _resetSettledWatchdogsForTest(): void {
306
350
  for (const entry of armedEntries.values()) {
@@ -308,42 +352,41 @@ export function _resetSettledWatchdogsForTest(): void {
308
352
  }
309
353
  armedEntries.clear();
310
354
  envCache = undefined;
355
+ midRoundWindowOverrideMs = undefined;
311
356
  }
312
357
 
313
- // ── [W4] 协议事件面接线 API(W3 删件重接的 core 半边)──────────────────
358
+ // ── [W4] 协议事件面接线 API ────────────────────────────────────────────
314
359
  //
315
360
  // 设计权威源:docs/design/chat-domain-v1x-liveness-governance.md §3.2 D2 前置 1 +
316
361
  // D5「settled-watchdog 生产接线重接」:两段守护的 refresh 源随 chat 域 cli 化改挂
317
- // 协议事件流(arm 点 = 轮开始;refresh = host/streamDelta + host/roundLifecycle
318
- // 事件;kill/终态 = 既有杀链)。现状 refresh/handover/disarm 的驱动源在待删的
319
- // session-runner.ts stdout pump(inproc 过渡形态,W3 删件前不得删除——本 API 面即
320
- // 其替换目标:W3 删旧接线时改调下面三个命名入口,语义逐一同构)。
362
+ // 协议事件流(arm 点 = 轮开跑;中段刷新源 = host/streamDelta run 事件通道
363
+ // 事件;kill/终态 = 既有杀链)。旧 inproc stdout-pump 接线(session-runner.ts)已随
364
+ // W3 删件移除,下面三个命名入口是协议事件面的唯一驱动入口。
321
365
  //
322
- // 命名入口与既有原语的映射(刻意薄委托、零新语义——本单元「把新接线立起来」,
323
- // W3 负责删旧,两套接线并存期由原语幂等吸收重复调用):
366
+ // 命名入口与既有原语的映射(刻意薄委托、零新语义):
324
367
  // refreshFromProtocolEvent ↔ refreshMidRoundNoProgress(协议事件行到达)
325
- // noteRoundSettledFromProtocol ↔ handoverMidRoundToSettled(roundLifecycle
368
+ // noteRoundSettledFromProtocol ↔ handoverMidRoundToSettled(轮次收敛交棒
326
369
  // settled 相位 = 轮收敛,中段让位收尾段)
327
370
  // disarmRoundFromProtocol ↔ disarmSettledWatchdog(idle 相位 / close / 终态)
328
371
  //
329
372
  // 监督器域 resume 轮的 refresh 覆盖:resume 轮的 arm 点在 subagent-service
330
373
  // deliverChatMessage(interact 返回点,冷热路径同点),其轮内协议事件(streamDelta /
331
- // roundLifecycle)到达时经 refreshFromProtocolEvent 刷新——W3 接线后两域轮(chat 轮
374
+ // 协议事件行)到达时经 refreshFromProtocolEvent 刷新——W3 接线后两域轮(chat 轮
332
375
  // 与监督器域 resume 轮)共用同一事件面。
333
376
 
334
- /** [W4 协议事件面] 协议事件行(host/streamDelta / host/roundLifecycle)到达:刷新
377
+ /** [W4 协议事件面] 协议事件行(host/streamDelta / run 事件通道)到达:刷新
335
378
  * 中段无进展计时。未挂载 / 已交棒(收尾段不刷新)/ 已 fire 时幂等 no-op。 */
336
379
  export function refreshFromProtocolEvent(recordId: string): void {
337
380
  refreshMidRoundNoProgress(recordId);
338
381
  }
339
382
 
340
- /** [W4 协议事件面] roundLifecycle settled 相位(轮收敛):中段让位收尾段
383
+ /** [W4] 轮次收敛(run 应答 settle,[H1 U6] run 应答驱动交棒):中段让位收尾段
341
384
  * (两段独立计时,交棒语义见 handoverMidRoundToSettled)。未挂载时幂等 no-op。 */
342
385
  export function noteRoundSettledFromProtocol(recordId: string): void {
343
386
  handoverMidRoundToSettled(recordId);
344
387
  }
345
388
 
346
- /** [W4 协议事件面] roundLifecycle idle 相位 / close / 终态化处置:两段一并清。
389
+ /** [W4] 轮终簿记 / close / 终态化处置:两段一并清。
347
390
  * 不存在 armed entry 时幂等 no-op。 */
348
391
  export function disarmRoundFromProtocol(recordId: string): void {
349
392
  disarmSettledWatchdog(recordId);