@zhushanwen/subagent-core 0.2.0 → 0.4.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 (195) hide show
  1. package/README.md +45 -2
  2. package/agents/analyst.md +60 -0
  3. package/agents/coder.md +69 -0
  4. package/agents/debugger.md +66 -0
  5. package/agents/doc-reviewer.md +49 -0
  6. package/agents/explorer.md +63 -0
  7. package/agents/general-purpose.md +32 -0
  8. package/agents/orchestrator.md +61 -0
  9. package/agents/planner.md +53 -0
  10. package/agents/researcher.md +67 -0
  11. package/agents/reviewer.md +73 -0
  12. package/dist/chunk-4KN4TTG7.js +240 -0
  13. package/dist/chunk-APZY4IME.js +27 -0
  14. package/dist/chunk-X7SZ5HLQ.js +66 -0
  15. package/dist/execution/engine/engines/zcode/constants.cjs +65 -0
  16. package/dist/execution/engine/engines/zcode/constants.d.cts +93 -1
  17. package/dist/execution/engine/engines/zcode/constants.d.ts +93 -1
  18. package/dist/execution/engine/engines/zcode/constants.js +43 -1
  19. package/dist/execution/engine/engines/zcode/reader.d.cts +1 -1
  20. package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
  21. package/dist/execution/engine/engines/zcode/reader.js +4 -234
  22. package/dist/execution/engine/paths.js +7 -19
  23. package/dist/index.cjs +14709 -100
  24. package/dist/index.d.cts +4053 -200
  25. package/dist/index.d.ts +4053 -200
  26. package/dist/index.js +14358 -102
  27. package/dist/{types-BxyAidGf.d.cts → types-DpUO16pj.d.cts} +464 -1
  28. package/dist/{types-BxyAidGf.d.ts → types-DpUO16pj.d.ts} +464 -1
  29. package/package.json +6 -3
  30. package/src/__tests__/review-fix-loop-script.test.ts +90 -2
  31. package/src/__tests__/review-fix-loop-utils.test.ts +21 -0
  32. package/src/__tests__/smoke.test.ts +9 -2
  33. package/src/execution/__tests__/agent-profile.test.ts +232 -0
  34. package/src/execution/__tests__/agents-assembly.test.ts +219 -0
  35. package/src/execution/__tests__/chat-engine-routing.test.ts +71 -0
  36. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +1 -0
  37. package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
  38. package/src/execution/__tests__/delivery-methods.test.ts +2 -0
  39. package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
  40. package/src/execution/__tests__/dialog-queue.test.ts +199 -1
  41. package/src/execution/__tests__/epipe-fallback.test.ts +2 -0
  42. package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
  43. package/src/execution/__tests__/finalize-record.test.ts +106 -0
  44. package/src/execution/__tests__/gc-timer.test.ts +2 -0
  45. package/src/execution/__tests__/get-record-for-action-restart.test.ts +2 -0
  46. package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
  47. package/src/execution/__tests__/keep-alive-no-progress.test.ts +378 -0
  48. package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
  49. package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
  50. package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
  51. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
  52. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
  53. package/src/execution/__tests__/notify-ledger.test.ts +294 -0
  54. package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
  55. package/src/execution/__tests__/record-store.test.ts +83 -0
  56. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +4 -0
  57. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +1 -0
  58. package/src/execution/__tests__/run-spawn-edges.test.ts +407 -17
  59. package/src/execution/__tests__/run-spawn-integration.test.ts +1 -1
  60. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +12 -4
  61. package/src/execution/__tests__/service-kill-escalation.test.ts +89 -0
  62. package/src/execution/__tests__/session-pending.test.ts +228 -0
  63. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
  64. package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
  65. package/src/execution/__tests__/session-runner-epipe.test.ts +1 -0
  66. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +226 -0
  67. package/src/execution/__tests__/settled-watchdog.test.ts +289 -0
  68. package/src/execution/__tests__/spawned-children.test.ts +153 -10
  69. package/src/execution/__tests__/subagent-actions-core.test.ts +997 -0
  70. package/src/execution/__tests__/subagent-service-message-close.test.ts +71 -10
  71. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
  72. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +266 -0
  73. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +3 -0
  74. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +316 -0
  75. package/src/execution/__tests__/timeout-integration.test.ts +7 -7
  76. package/src/execution/__tests__/ui-request-handler-factory.test.ts +95 -1
  77. package/src/execution/__tests__/ui-request-queue.test.ts +25 -0
  78. package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
  79. package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
  80. package/src/execution/agent-registry.ts +225 -1
  81. package/src/execution/agents-assembly.ts +88 -0
  82. package/src/execution/concurrency-pool.ts +71 -9
  83. package/src/execution/dialog-queue.ts +101 -3
  84. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +49 -2
  85. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +87 -1
  86. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +26 -0
  87. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +83 -1
  88. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +131 -0
  89. package/src/execution/engine/__tests__/registry.test.ts +90 -1
  90. package/src/execution/engine/engine-discovery.ts +12 -16
  91. package/src/execution/engine/engines/pi/task-spec-mapper.ts +3 -3
  92. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +274 -0
  93. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
  94. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +472 -0
  95. package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +387 -0
  96. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +780 -0
  97. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +815 -0
  98. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +462 -0
  99. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +119 -1
  100. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +136 -6
  101. package/src/execution/engine/engines/zcode/appserver-home.ts +442 -0
  102. package/src/execution/engine/engines/zcode/appserver-probe.ts +141 -0
  103. package/src/execution/engine/engines/zcode/connection.ts +585 -0
  104. package/src/execution/engine/engines/zcode/constants.ts +129 -0
  105. package/src/execution/engine/engines/zcode/golden-sample.ts +54 -8
  106. package/src/execution/engine/engines/zcode/preparer.ts +25 -22
  107. package/src/execution/engine/engines/zcode/session-channel.ts +656 -0
  108. package/src/execution/engine/engines/zcode/zcode-engine.ts +909 -41
  109. package/src/execution/engine/host-task-spec.ts +3 -3
  110. package/src/execution/engine/port.ts +41 -1
  111. package/src/execution/engine/registry.ts +53 -0
  112. package/src/execution/finalize-record.ts +63 -5
  113. package/src/execution/get-state-handshake.ts +85 -10
  114. package/src/execution/lifecycle-manager.ts +27 -3
  115. package/src/execution/manifest-store.ts +53 -62
  116. package/src/execution/notifier.ts +10 -10
  117. package/src/execution/notify-ledger.ts +117 -16
  118. package/src/execution/record-entry.ts +8 -2
  119. package/src/execution/record-store.ts +29 -15
  120. package/src/execution/session-pending.ts +213 -62
  121. package/src/execution/session-runner.ts +974 -128
  122. package/src/execution/sessions-index.ts +10 -55
  123. package/src/execution/settled-watchdog.ts +99 -0
  124. package/src/execution/subagent-actions-core.ts +686 -0
  125. package/src/execution/subagent-service.ts +396 -49
  126. package/src/execution/ui-request-handler-factory.ts +27 -4
  127. package/src/execution/worktree-git-ops.ts +397 -0
  128. package/src/execution/worktree-manager.ts +72 -10
  129. package/src/execution/worktree-registry.ts +6 -12
  130. package/src/index.ts +413 -6
  131. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
  132. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -4
  133. package/src/orchestration/__tests__/args-meta.test.ts +358 -0
  134. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +1 -8
  135. package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
  136. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +382 -0
  137. package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
  138. package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
  139. package/src/orchestration/__tests__/file-run-store.test.ts +389 -0
  140. package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
  141. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
  142. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
  143. package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
  144. package/src/orchestration/__tests__/lifecycle.test.ts +77 -0
  145. package/src/orchestration/__tests__/run-snapshot.test.ts +353 -0
  146. package/src/orchestration/__tests__/script-generate.test.ts +314 -0
  147. package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
  148. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
  149. package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
  150. package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
  151. package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
  152. package/src/orchestration/agent-opts-resolver.ts +7 -7
  153. package/src/orchestration/args-meta.ts +198 -0
  154. package/src/orchestration/error-recovery.ts +416 -146
  155. package/src/orchestration/execute-agent-call.ts +9 -9
  156. package/src/orchestration/file-run-store.ts +327 -0
  157. package/src/orchestration/launcher.ts +35 -26
  158. package/src/orchestration/lifecycle.ts +355 -79
  159. package/src/orchestration/models/agent-call.ts +7 -7
  160. package/src/orchestration/models/budget.ts +5 -5
  161. package/src/orchestration/models/run-runtime.ts +13 -13
  162. package/src/orchestration/models/trace.ts +8 -8
  163. package/src/orchestration/models/workflow-run.ts +27 -27
  164. package/src/orchestration/models/workflow-script.ts +4 -4
  165. package/src/orchestration/run-snapshot.ts +266 -0
  166. package/src/orchestration/script-generate.ts +154 -0
  167. package/src/orchestration/script-lint.ts +33 -33
  168. package/src/orchestration/worker-handle.ts +10 -10
  169. package/src/orchestration/worker-host.ts +10 -10
  170. package/src/orchestration/worker-script-builder.ts +401 -346
  171. package/src/orchestration/workflow-files.ts +37 -11
  172. package/src/orchestration/workflow-run-summary.ts +69 -0
  173. package/src/orchestration/workflow-script-registry-impl.ts +31 -9
  174. package/src/shared/__tests__/agent-ref.test.ts +209 -2
  175. package/src/shared/__tests__/atomic-write.test.ts +267 -0
  176. package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
  177. package/src/shared/__tests__/injection-render.test.ts +518 -0
  178. package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
  179. package/src/shared/__tests__/resource-discovery.test.ts +3 -2
  180. package/src/shared/agent-ref.ts +143 -2
  181. package/src/shared/atomic-write.ts +320 -0
  182. package/src/shared/bounded-serialize.ts +154 -0
  183. package/src/shared/injection-render.ts +279 -0
  184. package/src/shared/meta-parser.ts +41 -1
  185. package/src/shared/resource-discovery.ts +104 -37
  186. package/src/shared/resource-meta.ts +14 -0
  187. package/src/shared/xml-injection.ts +9 -9
  188. package/workflows/README.md +9 -9
  189. package/workflows/chain.js +4 -2
  190. package/workflows/map-reduce.js +5 -3
  191. package/workflows/parallel.js +5 -3
  192. package/workflows/review-fix-loop-utils.cjs +14 -3
  193. package/workflows/review-fix-loop.js +18 -7
  194. package/workflows/scatter-gather.js +4 -2
  195. package/dist/chunk-3VOERJPJ.js +0 -22
@@ -20,8 +20,70 @@
20
20
  // 直接调 handler(见 ui-request-handler-factory.ts),不经过本队列。enqueue 内仍防御性兼容
21
21
  // fire-and-forget(万一调用方未判):直接调 handler 返回,不入队串行。调用方不应依赖此防御。
22
22
 
23
+ import { getLogger } from "../core/logger.ts";
24
+ import { MAX_TIMER_DELAY_MS } from "../shared/timer-delay.ts";
25
+
23
26
  import { isDialogMethod } from "./ui-interaction-model.ts";
24
27
 
28
+ const logger = getLogger("subagents");
29
+
30
+ // ── dialog 超时上界(LC-3 / T2⑦,设计 docs/design/subagent-core-unbounded-wait-audit.md)──
31
+
32
+ /** 分钟/秒/毫秒换算常数(与 lifecycle-manager.ts 同款命名)。 */
33
+ const SECONDS_PER_MINUTE = 60;
34
+ const MS_PER_SECOND = 1000;
35
+
36
+ /** dialog 默认超时上界(30 分钟):请求方未传 timeout 时的回收层兜底。
37
+ *
38
+ * 30 分钟是裁决值(产品语义:ask_user 挂起 30 分钟无响应视为放弃),不随 P-T2
39
+ * (keep-alive 分布,等后代)标定——那是无关总体。请求方需要更长等待时在协议
40
+ * request 上显式传 timeout(毫秒,pi ExtensionUIDialogOptions.timeout 同义)覆盖。
41
+ *
42
+ * 「等用户无限久」改为默认有界是有意的行为变更:LC-3 的两种形态(host UI promise
43
+ * 挂死 / 用户永不回答)原都会把 L2 processing 永久占死 → 所有子进程 ask_user 全局
44
+ * 死锁 + 该 child L1 队列 head-of-line 阻塞。 */
45
+ const DEFAULT_DIALOG_TIMEOUT_MINUTES = 30;
46
+ export const DEFAULT_DIALOG_TIMEOUT_MS =
47
+ DEFAULT_DIALOG_TIMEOUT_MINUTES * SECONDS_PER_MINUTE * MS_PER_SECOND;
48
+
49
+ /**
50
+ * 解析一个 dialog 项的队列级上界:请求方传了合法正数 timeout 用之,否则挂默认上界。
51
+ * 非法值(<=0 / NaN / ±Infinity)回落默认——不因脏参数把上界拆掉(拆掉即回到 LC-3 无界)。
52
+ *
53
+ * 这里 clamp 到 MAX_TIMER_DELAY_MS 而非 fail-fast(timer-delay.ts 的 assertSafeTimerDelay
54
+ * 惯例是配置入口 fail-fast):timeout 是 per-request 运行时传值,fail-fast 会把整个
55
+ * dialog 处理路径炸成 cancelled(伤害正常请求);超大 timeout 的调用方意图是「近乎不限时」,
56
+ * clamp 到 2^31-1(约 24.8 天)是该意图在 setTimeout 域内的安全语义近似——不 clamp 会被
57
+ * Node 塌缩为 1ms 立即触发(语义反转:刚入队就超时)。
58
+ */
59
+ function resolveDialogTimeoutMs(timeout: number | undefined): number {
60
+ const resolved = isValidDialogTimeout(timeout) ? timeout : DEFAULT_DIALOG_TIMEOUT_MS;
61
+ return Math.min(resolved, MAX_TIMER_DELAY_MS);
62
+ }
63
+
64
+ /**
65
+ * dialog req.timeout 合法域判定(单一权威源,A2-3):合法正数(finite 且 > 0)才参与
66
+ * 超时语义——队列层据此决定上界取值(非法回落 DEFAULT_DIALOG_TIMEOUT_MS),
67
+ * factory 层据此决定是否透传 SDK 第三参(非法不透传,数组为空,由队列层默认上界兜底)。
68
+ * 两处共用本函数,禁止各写一份判定(此前两处不一致:SDK 层把 0/负数/NaN 原样透传,
69
+ * 队列层判非法回落默认)。
70
+ */
71
+ export function isValidDialogTimeout(timeout: number | undefined): timeout is number {
72
+ return typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0;
73
+ }
74
+
75
+ /** 超时 settle 的日志消息(恢复指引 + 已等待时长,父进程日志是完整错误消息的承载面——
76
+ * pi 协议 RpcExtensionUIResponse 是封闭联合(value/confirmed/cancelled),错误文本
77
+ * 无法进 stdin 响应体,子 agent 侧收到的是 cancelled 语义)。 */
78
+ function dialogTimeoutLogMessage(req: UiRequest, waitedMs: number): string {
79
+ return (
80
+ `[subagents] dialog timed out after ${waitedMs}ms without response ` +
81
+ `(id=${req.id}, method=${req.method}) — settled as cancelled. ` +
82
+ "Recovery: the requesting agent can continue and re-issue the question (重新发起提问); " +
83
+ "to wait longer, pass an explicit timeout (ms) on the request."
84
+ );
85
+ }
86
+
25
87
  // ── 类型定义(本模块是 UiRequest/UiResponse/UiRequestHandler 的规范来源) ──
26
88
  // session-runner.ts 和 ui-channels.ts 复用这些类型(session-runner 再导出供测试 import)。
27
89
 
@@ -118,6 +180,11 @@ interface QueueItem {
118
180
  childPid: number | undefined;
119
181
  /** 是否已 settle(防 handler 完成 / rejectChildDialogs 重复 resolve)。 */
120
182
  settled: boolean;
183
+ /** 队列级超时 timer 句柄(processNext 使 item 成为 current 时挂上)。
184
+ * settleItem 统一 clearTimeout——handler 永挂时 processNext 的 await 永不 resume,
185
+ * 其内联 clearTimeout 不可达,reject/rejectAll 抢先 settle 的路径只能靠这里清理
186
+ * (A2-2:否则 timer armed 到期触发虚假「dialog timed out」warn + 句柄滞留)。 */
187
+ timeoutTimer?: ReturnType<typeof setTimeout>;
121
188
  }
122
189
 
123
190
  /**
@@ -137,6 +204,10 @@ interface QueueItem {
137
204
  * - FIFO 串行:前一个 handler settle 后才处理下一个
138
205
  * - SR-4:rejectChildDialogs(child) 把该 child 的 pending 全部 resolve 为 {cancelled:true}
139
206
  * - handler 抛错兜底:catch → {cancelled:true} → 继续下一个(队列不卡死)
207
+ * - 超时上界(LC-3/T2⑦):每个 dialog 项必有上界——req.timeout 显式传值优先,
208
+ * 未传挂 DEFAULT_DIALOG_TIMEOUT_MS(30min,裁决值);到点 settle {cancelled:true}
209
+ * 并 warn(恢复指引见 dialogTimeoutLogMessage),L2 processing 释放、队列继续推进。
210
+ * 「等用户无限久」改为默认有界是有意的行为变更。
140
211
  * - 调用方约定只对 dialog 类调 enqueue;fire-and-forget 由调用方直接调 handler 不入队
141
212
  *(enqueue 内仍防御性兼容 fire-and-forget,但不保证行为)
142
213
  *
@@ -201,18 +272,26 @@ export class DialogGlobalQueue {
201
272
  }
202
273
 
203
274
  /**
204
- * settle 一个 item(幂等)。handler 完成 / rejectChildDialogs / rejectAll 都通过本方法,
205
- * settled 标志保证只 settle 一次(防竞争)。
275
+ * settle 一个 item(幂等)。handler 完成 / rejectChildDialogs / rejectAll / 超时 timer
276
+ * 都通过本方法,settled 标志保证只 settle 一次(防竞争)。
206
277
  *
207
278
  * #19 单一推进点:本方法 settle Promise + 清状态后,**唯一**调 processNext 推进队列。
208
279
  * processNext 尾部不再调 processNext(旧代码双重推进,虽靠 processing 标志幂等,但语义混乱)。
209
280
  * 为什么推进必须在 settleItem 而非 processNext 尾部:rejectChildDialogs 取消一个永不 settle
210
281
  * 的 current(handler 等用户输入卡死)时,processNext 的 `await item.handler` 永不 resume,
211
282
  * 尾部不会执行;只有 settleItem 里的 processNext 才能打破死锁,推进下一个。
283
+ *
284
+ * [A2-2] 超时 timer 清理也统一收口在这里:任何路径抢先 settle(reject/rejectAll/超时
285
+ * 自身)都必须撤下 armed timer——handler 永挂时 processNext 内联的 clearTimeout 不可达,
286
+ * 不在这里清则 timer 到期触发虚假「dialog timed out」warn 且句柄滞留至超时点。
212
287
  */
213
288
  private settleItem(item: QueueItem, resp: UiResponse): void {
214
289
  if (item.settled) return;
215
290
  item.settled = true;
291
+ if (item.timeoutTimer) {
292
+ clearTimeout(item.timeoutTimer);
293
+ item.timeoutTimer = undefined;
294
+ }
216
295
  item.resolve(resp);
217
296
  // 若是正在处理的项,清空 current/processing 并推进队列(唯一推进点)。
218
297
  // 非当前项(队列中被 reject)只 settle Promise,不影响 current/推进。
@@ -263,6 +342,14 @@ export class DialogGlobalQueue {
263
342
  *
264
343
  * handler 抛错兜底(TC-E4 case 3):catch → settle {cancelled:true} → 继续。
265
344
  * 不能让一个失败卡死队列(processing 永远 true)。
345
+ *
346
+ * LC-3/T2⑦ 超时上界:`await item.handler` 原本无上界——host UI promise 挂死或用户
347
+ * 永不回答时 processing 恒 true(全局 dialog 死锁)。现在每项挂队列级 timer:
348
+ * req.timeout(请求方显式传值)优先,未传/非法挂 DEFAULT_DIALOG_TIMEOUT_MS。
349
+ * 到点 settle {cancelled:true}(完整错误消息落父进程日志,含恢复指引与等待时长),
350
+ * settleItem 的 settled 标志保证与 handler 完成 / rejectChildDialogs 三方竞态下
351
+ * 恰 settle 一次。timer 回调闭包捕获 item(非读 this.current):迟到触发时
352
+ * settled 标志已置位,直接 noop,不误伤后继项。
266
353
  */
267
354
  private async processNext(): Promise<void> {
268
355
  if (this.processing) return;
@@ -270,12 +357,23 @@ export class DialogGlobalQueue {
270
357
  this.processing = true;
271
358
  const item = this.queue.shift()!;
272
359
  this.current = item;
360
+ const timeoutMs = resolveDialogTimeoutMs(item.req.timeout);
361
+ const timer = setTimeout(() => {
362
+ // [A2-2] 已被抢先 settle(正常 settle 时 timer 已被 settleItem 清除,这里是防御性
363
+ // 兜底):静默返回,不发虚假「dialog timed out」warn、不重复 settle。
364
+ if (item.settled) return;
365
+ logger.warn(dialogTimeoutLogMessage(item.req, timeoutMs));
366
+ this.settleItem(item, { cancelled: true });
367
+ }, timeoutMs);
368
+ item.timeoutTimer = timer;
273
369
  try {
274
370
  const resp = await item.handler(item.req);
275
- // handler 完成:settle(若已被 rejectChildDialogs 抢先 settle 则 noop)。
371
+ clearTimeout(timer);
372
+ // handler 完成:settle(若已被超时 timer / rejectChildDialogs 抢先 settle 则 noop)。
276
373
  // settleItem 内会清 current/processing 并推进队列(#19 单一推进点)。
277
374
  this.settleItem(item, resp);
278
375
  } catch {
376
+ clearTimeout(timer);
279
377
  // handler 抛错兜底:回 cancelled,不向上抛(队列不能卡死)
280
378
  this.settleItem(item, { cancelled: true });
281
379
  }
@@ -1,6 +1,12 @@
1
1
  // contract.abort.test.ts —— conformance C4(abort 行为):运行中 cancel → 宿主合成
2
2
  // 终态(exitCode=null)、无悬挂 promise(run 正常 resolve、exited 收口)、错误事件
3
3
  // 先于终态 emit(不变量 5 的事件面)。fake launcher 注入(不依赖真机)。
4
+ //
5
+ // [R6] 双模式口径:spawn 钉扎用例(deps.launch 驱动——launch 是 spawn 路径专属 dep,
6
+ // 钉 XYZ_ZCODE_MODE=spawn 使 fixture 生效,必要性核验成立)+ app-server 常驻用例
7
+ // (fake-appserver.mjs 挂起场景,stop 链优先于杀链——D3)。两形态各自覆盖:
8
+ // capabilities.interrupt 维持 kill-only 声明(D5——改链路先于改声明,stop 链路经
9
+ // conformance 真机验证后再评估升 native)。
4
10
 
5
11
  import { spawn } from "node:child_process";
6
12
  import * as fs from "node:fs";
@@ -8,12 +14,13 @@ import * as os from "node:os";
8
14
  import * as path from "node:path";
9
15
  import { PassThrough } from "node:stream";
10
16
 
11
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
17
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
12
18
 
13
19
  import type { RunContext } from "../../port.ts";
14
20
  import type { AgentEvent, AgentTaskSpec } from "../../types.ts";
15
21
  import { ZcodeEngine, type ZcodeEngineDeps } from "../../engines/zcode/zcode-engine.ts";
16
22
  import type { ZcodeLaunchedProcess } from "../../engines/zcode/launcher.ts";
23
+ import { makeAppserverHarness, sentMethodNames } from "./zcode-appserver-harness.ts";
17
24
 
18
25
  const PROVIDER = "provider-x";
19
26
 
@@ -79,7 +86,7 @@ describe("conformance C4:abort 行为(运行中 cancel → 合成终态、
79
86
  const engine = new ZcodeEngine({
80
87
  engineDataDir: () => dataDir,
81
88
  sources: { v2ConfigPath: v2Path },
82
- processEnv: { PATH: "/usr/bin" },
89
+ processEnv: { PATH: "/usr/bin", XYZ_ZCODE_MODE: "spawn" },
83
90
  launch: fake.launch,
84
91
  });
85
92
 
@@ -106,4 +113,44 @@ describe("conformance C4:abort 行为(运行中 cancel → 合成终态、
106
113
  expect(fake.proc()?.killedByUs()).toBe(true);
107
114
  expect(handle.data.engineId).toBe("zcode");
108
115
  }, 15_000);
116
+
117
+ // [R6] app-server 常驻形态:abort 走 D3 链(stop → grace → killChain)。挂起场景
118
+ // (只推 state.updated,turn 永不落定)+ stopBehavior 缺省 terminal = stop 优雅
119
+ // 生效路径——不杀共享进程,run 正常 resolve(C4 的常驻通道面)。
120
+ it("app-server 模式:abort → stop 帧先发 → 优雅收口(run resolve、exitCode=null、error 事件先于终态、kill-only 声明维持)", async () => {
121
+ const h = makeAppserverHarness({ hangOnly: true });
122
+ try {
123
+ // capabilities.interrupt 维持 kill-only(D5——升级序里 interrupt 不动)
124
+ expect(h.engine.capabilities().interrupt).toBe("kill-only");
125
+
126
+ const controller = new AbortController();
127
+ const events: AgentEvent[] = [];
128
+ const runP = h.engine.run(
129
+ { task: "hang", slug: "abort-appserver", model: "conformance-provider/m1", cwd: h.workspace },
130
+ { taskId: "sa-c4-appserver", poolKey: "", signal: controller.signal, onEvent: (e) => events.push(e) },
131
+ );
132
+ // 推进到 send 已达(create+send 帧落 fake 流水)再 abort——abort 先于 create 会
133
+ // 走 pre-aborted 短路面,覆盖不到 stop 链
134
+ await vi.waitFor(
135
+ () => expect(sentMethodNames(h.stateFile)).toContain("session/send"),
136
+ { timeout: 5_000 },
137
+ );
138
+ controller.abort();
139
+ const { outcome } = await runP; // 必须正常 resolve(stop 优雅收口,不悬挂)
140
+
141
+ expect(outcome.exitCode).toBeNull();
142
+ expect(outcome.error).toContain("session/stop");
143
+ // 错误事件先于终态 emit(不变量 5 的事件面——journal 可重放出失败事实)。abort
144
+ // 终态不合成 turn_end(与 spawn 钉扎用例同语义:只断言 error 事件到达,且其后
145
+ // 无任何非 error 事件流出)
146
+ const errorIdx = events.findIndex((e) => e.type === "error");
147
+ expect(errorIdx).toBeGreaterThanOrEqual(0);
148
+ expect(events.slice(errorIdx + 1).every((e) => e.type === "error")).toBe(true);
149
+ // D3 序:stop 帧先于任何杀链动作(优雅路径下进程未被杀)
150
+ const methods = sentMethodNames(h.stateFile);
151
+ expect(methods.indexOf("session/stop")).toBeGreaterThan(methods.indexOf("session/send"));
152
+ } finally {
153
+ await h.dispose();
154
+ }
155
+ }, 20_000);
109
156
  });
@@ -2,12 +2,23 @@
2
2
  // (A12「套件有牙」:故意破坏一个不变量的样本必须被断言器检出——用「注入坏
3
3
  // parser」的形态:wrap 真实 synthesizeCoarseEvents 抽掉 message_end,断言套件转红;
4
4
  // 若断言器检不出破坏则本元测试失败)。
5
+ //
6
+ // [R6] 双模式口径:zcode 引擎 conformance 覆盖两种通道形态——spawn 钉扎(golden
7
+ // stdout 语料 + coarse 合成,下方原有用例)与 app-server 常驻(fake 常驻引擎全链 +
8
+ // stream 口径,文件末新增 describe;app-server 设计 §3.4 不变量 1:text_delta 拼接 ==
9
+ // read 全文、终态唯一 turn.terminal 权威、message_end.usage 完整、tool 事件不合成
10
+ // ——granularity 声明与实际流出一致)。
5
11
 
6
12
  import { describe, expect, it } from "vitest";
7
13
 
8
14
  import type { AgentEvent } from "../../../types.ts";
9
- import { ZCODE_GOLDEN_STDOUT } from "../../engines/zcode/golden-sample.ts";
15
+ import { ZCODE_APPSERVER_GOLDEN, ZCODE_GOLDEN_STDOUT } from "../../engines/zcode/golden-sample.ts";
16
+ import { ZCODE_APPSERVER_POOL_KEY } from "../../engines/zcode/constants.ts";
10
17
  import { parseZcodeTerminal, synthesizeCoarseEvents } from "../../engines/zcode/parser.ts";
18
+ import {
19
+ goldenReadFullText,
20
+ makeAppserverHarness,
21
+ } from "./zcode-appserver-harness.ts";
11
22
  import {
12
23
  assertAgentEventInvariants,
13
24
  checkAgentEventInvariants,
@@ -99,3 +110,78 @@ describe("conformance 负例自证(A12:套件有牙)", () => {
99
110
  expect(findings.some((f) => f.invariant === "2")).toBe(true);
100
111
  });
101
112
  });
113
+
114
+ // ============================================================
115
+ // [R6] C3 app-server 口径:fake 常驻引擎全链(eventGranularity=stream 形态)
116
+ // —— spawn 钉扎用例见上方 coarse describe,两形态各自覆盖不合并(D2 降级链保留
117
+ // spawn 路径,双通道都是生产可达形态)。
118
+ // ============================================================
119
+
120
+ describe("conformance C3:zcode app-server 常驻通道(stream 口径,fake 常驻全链)", () => {
121
+ it("text_delta 拼接 == read 全文(byte 级)+ 终态唯一 + message_end.usage 完整 + tool 事件不合成", async () => {
122
+ const h = makeAppserverHarness();
123
+ try {
124
+ const events: AgentEvent[] = [];
125
+ const { outcome } = await h.engine.run(
126
+ { task: "做点什么", slug: "c3-appserver", model: "conformance-provider/m1", cwd: h.workspace },
127
+ { taskId: "sa-c3-appserver", poolKey: "", onEvent: (e) => events.push(e) },
128
+ );
129
+
130
+ // granularity 声明与实际流出一致:声明 stream,流出有 text_delta 实时形态
131
+ expect(h.engine.capabilities().eventGranularity).toBe("stream");
132
+ expect(events.some((e) => e.type === "text_delta")).toBe(true);
133
+
134
+ // 事件形态:三条 delta(golden pushStream)→ message_end(usage) → turn_end
135
+ expect(events.map((e) => e.type)).toEqual([
136
+ "text_delta", "text_delta", "text_delta", "message_end", "turn_end",
137
+ ]);
138
+ const messageEnd = events.find((e) => e.type === "message_end") as
139
+ | { usage?: { input: number; output: number; cacheRead: number; cacheWrite: number } }
140
+ | undefined;
141
+ expect(messageEnd?.usage).toEqual({ input: 12599, output: 17, cacheRead: 0, cacheWrite: 0 });
142
+
143
+ // tool 事件不合成:首期无逐工具推送帧,流出面不伪造 tool_start/tool_end
144
+ // (granularity 声明与实际流出一致即合法——app-server 设计 §3.4 不变量 1)
145
+ expect(events.some((e) => e.type === "tool_start" || e.type === "tool_end")).toBe(false);
146
+
147
+ // 不变量五条(stream 口径):3a 的 content 用 read 全文(终态权威来源)
148
+ const readFullText = goldenReadFullText();
149
+ assertAgentEventInvariants(events, { granularity: "stream", content: readFullText });
150
+ expect(outcome.content).toBe(readFullText);
151
+ } finally {
152
+ await h.dispose();
153
+ }
154
+ }, 20_000);
155
+
156
+ it("终态权威 turn.terminal 缺失 → read 兜底收口,事件不变量仍全绿(宽松匹配防洪堤不破坏 C3)", async () => {
157
+ const h = makeAppserverHarness({ dropTurnTerminal: true });
158
+ try {
159
+ const events: AgentEvent[] = [];
160
+ const { outcome } = await h.engine.run(
161
+ { task: "做点什么", slug: "c3-no-terminal", model: "conformance-provider/m1", cwd: h.workspace },
162
+ { taskId: "sa-c3-no-terminal", poolKey: "", onEvent: (e) => events.push(e) },
163
+ );
164
+ expect(outcome.error).toBeUndefined();
165
+ // 缺 turn.terminal 时收尾帧 + read 兜底仍产出完整终态(session-channel 宽松匹配)
166
+ assertAgentEventInvariants(events, { granularity: "stream", content: goldenReadFullText() });
167
+ } finally {
168
+ await h.dispose();
169
+ }
170
+ }, 20_000);
171
+
172
+ it("handle 锚定常驻 HOME(poolKey=home-appserver,①级读取钥匙随 handle 走)", async () => {
173
+ const h = makeAppserverHarness();
174
+ try {
175
+ const { handle, outcome } = await h.engine.run(
176
+ { task: "做点什么", slug: "c3-anchor", model: "conformance-provider/m1", cwd: h.workspace },
177
+ { taskId: "sa-c3-anchor", poolKey: "" },
178
+ );
179
+ expect(handle.data.poolKey).toBe(ZCODE_APPSERVER_POOL_KEY);
180
+ expect(handle.data.sessionRef["sessionId"]).toBe(outcome.sessionId);
181
+ // golden 锚点:sessionId 来自 session.sessionId(projection.sessionId 恒 "unknown" 勿用)
182
+ expect(JSON.parse(ZCODE_APPSERVER_GOLDEN.createResponse).session.sessionId).toBe("sess_golden_r3_01");
183
+ } finally {
184
+ await h.dispose();
185
+ }
186
+ }, 20_000);
187
+ });
@@ -66,6 +66,32 @@ describe.skipIf(!LIVE)("conformance run 层(真实 spawn,手动门)", () =
66
66
  expect(report.ok).toBe(true);
67
67
  expect(report.engineVersion).toMatch(/^0\.\d+\.\d+$/);
68
68
  }, 60_000);
69
+
70
+ // [R6] 常驻通道的 conformance run 层(RA8「C1-C8 适配后全绿」的 live 面):
71
+ // XYZ_ZCODE_MODE=appserver 定向(不探不降)跑最小任务——C2 outcome + C3 stream
72
+ // 不变量(app-server 设计 §3.4 不变量 1)。更深断言(schema/abort/进程锚定)由
73
+ // engines/zcode/__tests__/zcode-engine.live.test.ts 的 [R6] app-server 段承载。
74
+ it("zcode:app-server 常驻通道 run 全链(C2 outcome 无 error + C3 stream 事件不变量)", async (testCtx) => {
75
+ const model = process.env["ZCODE_E2E_MODEL"];
76
+ if (model === undefined || model === "") {
77
+ testCtx.skip("ZCODE_E2E_MODEL 未设置(需真实 provider/model 凭据)——appserver live run 面跳过");
78
+ return;
79
+ }
80
+ const engine = new ZcodeEngine({
81
+ engineDataDir: () => "/tmp/zcode-conformance-live-appserver",
82
+ processEnv: { ...process.env, XYZ_ZCODE_MODE: "appserver" },
83
+ });
84
+ const events: AgentEvent[] = [];
85
+ const { outcome } = await engine.run(
86
+ { task: "Reply with the single word: ok", slug: "live-appserver-c2", model, cwd: "/tmp" },
87
+ { taskId: "sa-live-zcode-appserver", poolKey: "", onEvent: (e) => events.push(e) },
88
+ );
89
+ expect(outcome.error).toBeUndefined();
90
+ expect(outcome.content.trim().length).toBeGreaterThan(0);
91
+ expect(outcome.sessionId).toMatch(/^sess_/);
92
+ assertAgentEventInvariants(events, { granularity: "stream", content: outcome.content });
93
+ await engine.dispose().catch(() => undefined);
94
+ }, 180_000);
69
95
  });
70
96
 
71
97
  // ── relay 变体(E 方案 §2.3:同一契约 × 不同 spawn 通道,手动门内的手动门)──
@@ -3,6 +3,11 @@
3
3
  // zcode-golden-spawn.json 的 stdoutRaw)——parser 对实录样本回归 + coarse 事件合成
4
4
  // 不变量断言 + 双副本 diff 校验(fixture 与 golden-sample.ts 内嵌副本一致,防漂移:
5
5
  // 更新样本必须同步两处,探针的干跑校验消费内嵌副本,两处不一致 = 探针在测旧契约)。
6
+ //
7
+ // [R6] 双语料并存:① spawn stdout(上方,spawn 降级路径 + probe 干跑的回归语料——
8
+ // 生产消费方存在故保留,去留决策见 impl-plan R6 偏差登记)② app-server 帧序列
9
+ // (下方新增,常驻通道主力语料——双副本 diff 自 session-channel 测试接管到 conformance
10
+ // 回放层,帧形态不变量在此断言;A8「golden 帧序列语料 diff 通过」的本仓段)。
6
11
 
7
12
  import { readFileSync } from "node:fs";
8
13
  import { dirname, join } from "node:path";
@@ -10,7 +15,7 @@ import { fileURLToPath } from "node:url";
10
15
 
11
16
  import { describe, expect, it } from "vitest";
12
17
 
13
- import { ZCODE_GOLDEN_STDOUT } from "../../engines/zcode/golden-sample.ts";
18
+ import { ZCODE_APPSERVER_GOLDEN, ZCODE_GOLDEN_STDOUT } from "../../engines/zcode/golden-sample.ts";
14
19
  import {
15
20
  mapZcodeOutcomeUsage,
16
21
  parseZcodeTerminal,
@@ -32,6 +37,14 @@ interface ZcodeGoldenFile {
32
37
 
33
38
  const fixture = JSON.parse(readFileSync(fixturePath, "utf8")) as ZcodeGoldenFile;
34
39
 
40
+ // [R6] app-server 帧序列语料 fixture(采集 SSOT——双副本 diff 的另一副本)
41
+ const appserverFixturePath = join(dirname(fixturePath), "zcode-golden-appserver.json");
42
+ interface AppServerGoldenFile {
43
+ _meta: Record<string, unknown>;
44
+ frames: { createResponse: string; pushStream: string[]; terminal: string[]; readResponse: string };
45
+ }
46
+ const appserverFixture = JSON.parse(readFileSync(appserverFixturePath, "utf8")) as AppServerGoldenFile;
47
+
35
48
  describe("zcode golden 回放(conformance C3,免 LLM/免二进制)", () => {
36
49
  it("双副本 diff:golden-sample.ts 内嵌副本 === fixture stdoutRaw(防漂移,A12)", () => {
37
50
  expect(ZCODE_GOLDEN_STDOUT).toBe(fixture.stdoutRaw);
@@ -77,3 +90,72 @@ describe("zcode golden 回放(conformance C3,免 LLM/免二进制)", () =>
77
90
  });
78
91
  });
79
92
  });
93
+
94
+ // ============================================================
95
+ // [R6] app-server 帧序列语料回放(常驻通道主力语料,conformance 层接管双副本 diff)
96
+ // ============================================================
97
+
98
+ describe("zcode app-server golden 回放(conformance C3 帧形态,免 LLM/免二进制)", () => {
99
+ it("双副本 diff:golden-sample.ts 内嵌副本 === fixture frames(防漂移,A8 本仓段)", () => {
100
+ expect(ZCODE_APPSERVER_GOLDEN.createResponse).toBe(appserverFixture.frames.createResponse);
101
+ expect(ZCODE_APPSERVER_GOLDEN.pushStream).toEqual(appserverFixture.frames.pushStream);
102
+ expect(ZCODE_APPSERVER_GOLDEN.terminal).toEqual(appserverFixture.frames.terminal);
103
+ expect(ZCODE_APPSERVER_GOLDEN.readResponse).toBe(appserverFixture.frames.readResponse);
104
+ });
105
+
106
+ it("create 应答形状:权威 sessionId 在 session.sessionId(projection.sessionId 恒 unknown 勿用)", () => {
107
+ const create = JSON.parse(ZCODE_APPSERVER_GOLDEN.createResponse) as {
108
+ session: { sessionId: string };
109
+ projection: { sessionId: string };
110
+ };
111
+ expect(create.session.sessionId).toMatch(/^sess_/);
112
+ expect(create.projection.sessionId).toBe("unknown");
113
+ });
114
+
115
+ it("推送流帧形态:stream.chunk 无文本(文本在 session/event delta)——文本三源恒等(A8 不变量 1)", () => {
116
+ const pushes = ZCODE_APPSERVER_GOLDEN.pushStream.map((l) => JSON.parse(l) as {
117
+ method: string;
118
+ params?: { kind?: string; payload?: { delta?: string } };
119
+ });
120
+ const chunks = pushes.filter((f) => f.params?.kind === "stream.chunk");
121
+ expect(chunks.length).toBeGreaterThan(0);
122
+ for (const c of chunks) expect(c.params?.payload?.delta).toBeUndefined();
123
+
124
+ const deltaJoined = pushes
125
+ .filter((f) => f.method === "session/event" && typeof f.params?.payload?.delta === "string")
126
+ .map((f) => f.params?.payload?.delta)
127
+ .join("");
128
+ const terminal = ZCODE_APPSERVER_GOLDEN.terminal.map((l) => JSON.parse(l) as {
129
+ params?: { kind?: string; payload?: { response?: string; usage?: { inputTokens: number; outputTokens: number } } };
130
+ });
131
+ const closing = terminal.find((f) => f.params?.payload?.response !== undefined);
132
+ expect(closing?.params?.payload?.response).toBe(deltaJoined);
133
+
134
+ const read = JSON.parse(ZCODE_APPSERVER_GOLDEN.readResponse) as {
135
+ messages: Array<{ info: { role: string }; parts: Array<{ type: string; text?: string }> }>;
136
+ };
137
+ const readText = (read.messages.at(-1)?.parts ?? [])
138
+ .filter((p) => p.type === "text")
139
+ .map((p) => p.text ?? "")
140
+ .join("");
141
+ expect(readText).toBe(deltaJoined); // text_delta 拼接 == 收尾帧 == read 全文
142
+ });
143
+
144
+ it("终态权威帧:turn.terminal 存在且先于收尾帧;usage 与 spawn golden 同值(漂移对照锚点)", () => {
145
+ const terminal = ZCODE_APPSERVER_GOLDEN.terminal.map((l) => JSON.parse(l) as {
146
+ params?: { kind?: string; status?: string; payload?: { usage?: { inputTokens: number; outputTokens: number } } };
147
+ });
148
+ const terminalFrameIdx = terminal.findIndex((f) => f.params?.kind === "turn.terminal");
149
+ const closingFrameIdx = terminal.findIndex((f) => f.params?.payload?.usage !== undefined);
150
+ expect(terminalFrameIdx).toBeGreaterThanOrEqual(0);
151
+ expect(terminal[terminalFrameIdx]?.params?.status).toBe("success");
152
+ expect(terminalFrameIdx).toBeLessThan(closingFrameIdx);
153
+
154
+ const spawnUsage = parseZcodeTerminal(ZCODE_GOLDEN_STDOUT);
155
+ if (!spawnUsage.ok) throw new Error("spawn golden 样本解析失败");
156
+ expect(terminal[closingFrameIdx]?.params?.payload?.usage).toEqual({
157
+ inputTokens: spawnUsage.payload.usage.input,
158
+ outputTokens: spawnUsage.payload.usage.output,
159
+ });
160
+ });
161
+ });
@@ -0,0 +1,131 @@
1
+ // zcode-appserver-harness.ts —— conformance 层的 fake app-server 引擎组装器(R6)。
2
+ //
3
+ // 与 engines/zcode/__tests__/zcode-engine-appserver.test.ts 的 makeEngine 同模式
4
+ // (scenario 文件随 env 固化进 fake 子进程),抽出为 conformance 专用副本而非跨测试
5
+ // 目录 import 测试文件——conformance 套件自包含(A12:套件不依赖被测引擎自己的
6
+ // 单测文件结构)。协议/断言面同源:fake-appserver.mjs + ZCODE_APPSERVER_GOLDEN。
7
+
8
+ import * as fs from "node:fs";
9
+ import * as os from "node:os";
10
+ import * as path from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ import { ZCODE_APPSERVER_GOLDEN } from "../../engines/zcode/golden-sample.ts";
14
+ import { ZcodeEngine } from "../../engines/zcode/zcode-engine.ts";
15
+
16
+ const FAKE_CLI = fileURLToPath(
17
+ new URL("../../engines/zcode/__tests__/__fixtures__/fake-appserver.mjs", import.meta.url),
18
+ );
19
+ const PROVIDER = "conformance-provider";
20
+
21
+ export interface AppserverHarnessOptions {
22
+ /** 覆盖 send 推送帧(缺省 = golden pushStream + 终态两帧;dropTurnTerminal 置真时去 turn.terminal)。 */
23
+ dropTurnTerminal?: boolean;
24
+ /** 覆盖 read 应答(read 是全文权威来源)。 */
25
+ readResult?: unknown;
26
+ /** session/stop 行为(fake scenario.stopBehavior;缺省 'terminal' = stop 优雅生效)。 */
27
+ stopBehavior?: "terminal" | "none";
28
+ /** 只保留 state.updated(挂起场景——turn 永不自然落定,abort 用例用)。 */
29
+ hangOnly?: boolean;
30
+ }
31
+
32
+ export interface AppserverHarness {
33
+ engine: ZcodeEngine;
34
+ /** fake 子进程的事件流水文件(帧序断言数据源)。 */
35
+ stateFile: string;
36
+ workspace: string;
37
+ dataDir: string;
38
+ dispose(): Promise<void>;
39
+ }
40
+
41
+ /** 组装连到 fake-appserver 的常驻引擎(XYZ_ZCODE_MODE=appserver 定向——不探不降)。 */
42
+ export function makeAppserverHarness(opts: AppserverHarnessOptions = {}): AppserverHarness {
43
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "zcode-conformance-appserver-"));
44
+ const dataDir = path.join(tmpRoot, "data");
45
+ const stateFile = path.join(tmpRoot, "state.jsonl");
46
+ const scenarioFile = path.join(tmpRoot, "scenario.json");
47
+ const workspace = path.join(tmpRoot, "ws");
48
+ const v2Path = path.join(tmpRoot, "v2.json");
49
+
50
+ fs.mkdirSync(path.dirname(v2Path), { recursive: true });
51
+ fs.writeFileSync(
52
+ v2Path,
53
+ JSON.stringify({
54
+ provider: { [PROVIDER]: { options: { apiKey: "k", baseURL: "https://t.example" }, models: { m1: {} } } },
55
+ }),
56
+ );
57
+
58
+ const pushes = opts.hangOnly
59
+ ? [ZCODE_APPSERVER_GOLDEN.pushStream[0]].map((l) => JSON.parse(l) as unknown)
60
+ : [
61
+ ...ZCODE_APPSERVER_GOLDEN.pushStream,
62
+ ...(opts.dropTurnTerminal ? [] : [ZCODE_APPSERVER_GOLDEN.terminal[0]]),
63
+ ZCODE_APPSERVER_GOLDEN.terminal[1],
64
+ ].map((l) => JSON.parse(l) as unknown);
65
+ fs.writeFileSync(
66
+ scenarioFile,
67
+ JSON.stringify({
68
+ createResult: JSON.parse(ZCODE_APPSERVER_GOLDEN.createResponse),
69
+ readResult: opts.readResult ?? JSON.parse(ZCODE_APPSERVER_GOLDEN.readResponse),
70
+ sendPushes: pushes,
71
+ ...(opts.stopBehavior !== undefined ? { stopBehavior: opts.stopBehavior } : {}),
72
+ }),
73
+ );
74
+
75
+ const engine = new ZcodeEngine({
76
+ engineDataDir: () => dataDir,
77
+ cliPath: FAKE_CLI,
78
+ sources: { v2ConfigPath: v2Path },
79
+ processEnv: {
80
+ PATH: process.env.PATH ?? "",
81
+ // 定向 appserver(R5 D2①):不探不降——conformance 测常驻路径本体,缺省路径的
82
+ // 探针门控/降级链归 zcode-engine-degrade.test.ts
83
+ XYZ_ZCODE_MODE: "appserver",
84
+ FAKE_STATE_FILE: stateFile,
85
+ FAKE_SESSION_SCENARIO: scenarioFile,
86
+ },
87
+ });
88
+ return {
89
+ engine,
90
+ stateFile,
91
+ workspace,
92
+ dataDir,
93
+ dispose: () =>
94
+ engine.dispose().finally(() => {
95
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
96
+ }),
97
+ };
98
+ }
99
+
100
+ /** 读 fake 流水(帧序断言用)。 */
101
+ export function readFakeState(file: string): Array<Record<string, unknown>> {
102
+ try {
103
+ return fs
104
+ .readFileSync(file, "utf8")
105
+ .trim()
106
+ .split("\n")
107
+ .filter(Boolean)
108
+ .map((l) => JSON.parse(l) as Record<string, unknown>);
109
+ } catch {
110
+ return [];
111
+ }
112
+ }
113
+
114
+ /** fake 收到的客户端帧方法名序列(abort 链「stop 先于杀链」断言面)。 */
115
+ export function sentMethodNames(stateFile: string): string[] {
116
+ return readFakeState(stateFile)
117
+ .map((e) => e["frame"])
118
+ .filter((f): f is Record<string, unknown> => typeof f === "object" && f !== null && typeof f["method"] === "string")
119
+ .map((f) => f["method"] as string);
120
+ }
121
+
122
+ /** golden read 应答里 assistant 消息的 text parts 拼接(read 全文——不变量 3a 的比对基准)。 */
123
+ export function goldenReadFullText(): string {
124
+ const read = JSON.parse(ZCODE_APPSERVER_GOLDEN.readResponse) as {
125
+ messages: Array<{ info: { role: string }; parts: Array<{ type: string; text?: string }> }>;
126
+ };
127
+ return (read.messages.at(-1)?.parts ?? [])
128
+ .filter((p) => p.type === "text")
129
+ .map((p) => p.text ?? "")
130
+ .join("");
131
+ }