@zhushanwen/pi-subagent-workflow 8.4.0 → 8.6.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 (171) hide show
  1. package/package.json +22 -7
  2. package/relay/relay.mjs +390 -0
  3. package/skills/subagent-ext-config/SKILL.md +80 -0
  4. package/src/execution/__tests__/agent-registry.test.ts +110 -0
  5. package/src/execution/__tests__/bg-notify-render.test.ts +73 -0
  6. package/src/execution/__tests__/chat-engine-routing.test.ts +601 -0
  7. package/src/execution/__tests__/delivery-methods.test.ts +38 -1
  8. package/src/execution/__tests__/execute-options-mapper.test.ts +11 -0
  9. package/src/execution/__tests__/execution-record.test.ts +237 -1
  10. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
  11. package/src/execution/__tests__/format-schema-instruction.test.ts +63 -32
  12. package/src/execution/__tests__/helpers/spawn-mock.ts +4 -0
  13. package/src/execution/__tests__/index-session-start.test.ts +86 -7
  14. package/src/execution/__tests__/lifecycle-manager.test.ts +46 -0
  15. package/src/execution/__tests__/list-fields.test.ts +45 -14
  16. package/src/execution/__tests__/model-resolver.test.ts +57 -5
  17. package/src/execution/__tests__/notifier-flush.test.ts +64 -26
  18. package/src/execution/__tests__/notify-ledger.test.ts +826 -0
  19. package/src/execution/__tests__/output-collector.test.ts +299 -2
  20. package/src/execution/__tests__/pi-invocation.test.ts +62 -1
  21. package/src/execution/__tests__/relay-agent.test.ts +448 -0
  22. package/src/execution/__tests__/relay-env.test.ts +42 -0
  23. package/src/execution/__tests__/rpc-mode.test.ts +1 -1
  24. package/src/execution/__tests__/run-spawn-edges.test.ts +44 -1
  25. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
  26. package/src/execution/__tests__/session-runner-schema-env.test.ts +39 -0
  27. package/src/execution/__tests__/spawn-args.test.ts +37 -26
  28. package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
  29. package/src/execution/__tests__/startup-config-declaration.test.ts +35 -0
  30. package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
  31. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
  32. package/src/execution/__tests__/subprocess-agent-runner.test.ts +147 -6
  33. package/src/execution/__tests__/timeout-integration.test.ts +220 -2
  34. package/src/execution/__tests__/tool-action.test.ts +92 -1
  35. package/src/execution/agent-registry.ts +16 -0
  36. package/src/execution/argv-mirror.ts +5 -1
  37. package/src/execution/concurrency-pool.ts +1 -1
  38. package/src/execution/config.ts +25 -2
  39. package/src/execution/engine/__tests__/common/data-dir.test.ts +53 -0
  40. package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
  41. package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
  42. package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
  43. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
  44. package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
  45. package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
  46. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
  47. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
  48. package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
  49. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
  50. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
  51. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
  52. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
  53. package/src/execution/engine/__tests__/conformance/contract.relay.test.ts +342 -0
  54. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
  55. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
  56. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
  57. package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
  58. package/src/execution/engine/__tests__/engines-declaration.test.ts +36 -0
  59. package/src/execution/engine/__tests__/model-prompt.test.ts +85 -0
  60. package/src/execution/engine/__tests__/paths.test.ts +39 -0
  61. package/src/execution/engine/__tests__/registry.test.ts +120 -0
  62. package/src/execution/engine/__tests__/routing.test.ts +231 -0
  63. package/src/execution/engine/common/data-dir.ts +62 -0
  64. package/src/execution/engine/common/errors.ts +183 -0
  65. package/src/execution/engine/common/event-journal.ts +254 -0
  66. package/src/execution/engine/common/journal-replay.ts +62 -0
  67. package/src/execution/engine/common/kill-chain.ts +221 -0
  68. package/src/execution/engine/common/nesting-guard.ts +50 -0
  69. package/src/execution/engine/common/persona-router.ts +108 -0
  70. package/src/execution/engine/common/pool-manager.ts +226 -0
  71. package/src/execution/engine/common/schema-emulation.ts +189 -0
  72. package/src/execution/engine/common/session-view-projection.ts +51 -0
  73. package/src/execution/engine/engine-discovery.ts +65 -0
  74. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
  75. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
  76. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
  77. package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
  78. package/src/execution/engine/engines/pi/reader.ts +48 -0
  79. package/src/execution/engine/engines/pi/registration.ts +35 -0
  80. package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
  81. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
  82. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
  83. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
  84. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
  85. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
  86. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +64 -0
  87. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
  88. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +580 -0
  89. package/src/execution/engine/engines/zcode/constants.ts +43 -0
  90. package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
  91. package/src/execution/engine/engines/zcode/launcher.ts +161 -0
  92. package/src/execution/engine/engines/zcode/parser.ts +436 -0
  93. package/src/execution/engine/engines/zcode/preparer.ts +363 -0
  94. package/src/execution/engine/engines/zcode/reader.ts +381 -0
  95. package/src/execution/engine/engines/zcode/registration.ts +37 -0
  96. package/src/execution/engine/engines/zcode/zcode-engine.ts +658 -0
  97. package/src/execution/engine/host-task-spec.ts +47 -0
  98. package/src/execution/engine/model-prompt.ts +59 -0
  99. package/src/execution/engine/paths.ts +42 -0
  100. package/src/execution/engine/port.ts +153 -0
  101. package/src/execution/engine/registry.ts +123 -0
  102. package/src/execution/engine/routing.ts +218 -0
  103. package/src/execution/engine/types.ts +309 -0
  104. package/src/execution/execute-options-mapper.ts +13 -8
  105. package/src/execution/execution-record.ts +66 -1
  106. package/src/execution/lifecycle-manager.ts +23 -1
  107. package/src/execution/model-config-service.ts +16 -1
  108. package/src/execution/model-resolver.ts +37 -59
  109. package/src/execution/notifier.ts +105 -35
  110. package/src/execution/notify-ledger.ts +580 -0
  111. package/src/execution/output-collector.ts +143 -3
  112. package/src/execution/pi-invocation.ts +32 -2
  113. package/src/execution/record-entry.ts +14 -0
  114. package/src/execution/record-store.ts +34 -0
  115. package/src/execution/relay-env.ts +37 -0
  116. package/src/execution/session-runner.ts +328 -71
  117. package/src/execution/stream-sink.ts +26 -0
  118. package/src/execution/subagent-service.ts +273 -13
  119. package/src/execution/subprocess-agent-runner.ts +210 -14
  120. package/src/execution/types.ts +124 -5
  121. package/src/execution/ui-request-queue.ts +14 -4
  122. package/src/index.ts +99 -1
  123. package/src/interface/__tests__/subagent-tool-path-guard.test.ts +157 -0
  124. package/src/interface/__tests__/subagent-tool-prompt.test.ts +12 -0
  125. package/src/interface/bg-notify-render.ts +33 -12
  126. package/src/interface/helpers.ts +2 -2
  127. package/src/interface/subagent-actions.ts +29 -9
  128. package/src/interface/subagent-tool-schema.ts +156 -0
  129. package/src/interface/subagent-tool.ts +56 -119
  130. package/src/interface/subagents.ts +2 -2
  131. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +19 -3
  132. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +0 -6
  133. package/src/orchestration/__tests__/agent-call-stream.test.ts +0 -5
  134. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +89 -4
  135. package/src/orchestration/__tests__/execute-agent-call.test.ts +137 -0
  136. package/src/orchestration/__tests__/jsonl-run-store-corrupt-entry.test.ts +150 -0
  137. package/src/orchestration/__tests__/jsonl-run-store-retention.test.ts +202 -0
  138. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +326 -3
  139. package/src/orchestration/__tests__/lifecycle.test.ts +41 -7
  140. package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
  141. package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +57 -3
  142. package/src/orchestration/__tests__/skill-discovery.test.ts +44 -0
  143. package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
  144. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +43 -0
  145. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +22 -3
  146. package/src/orchestration/agent-opts-resolver.ts +104 -23
  147. package/src/orchestration/error-recovery.ts +189 -33
  148. package/src/orchestration/execute-agent-call.ts +39 -0
  149. package/src/orchestration/jsonl-run-store.ts +121 -7
  150. package/src/orchestration/launcher.ts +60 -15
  151. package/src/orchestration/lifecycle.ts +10 -7
  152. package/src/orchestration/models/__tests__/budget.test.ts +1 -61
  153. package/src/orchestration/models/budget.ts +5 -35
  154. package/src/orchestration/models/run-runtime.ts +24 -9
  155. package/src/orchestration/models/types.ts +16 -0
  156. package/src/orchestration/script-lint.ts +1 -1
  157. package/src/orchestration/skill-discovery.ts +31 -8
  158. package/src/orchestration/worker-script-builder.ts +19 -3
  159. package/src/shared/__tests__/model-ref.test.ts +306 -0
  160. package/src/shared/__tests__/schema-jsonify.test.ts +1 -1
  161. package/src/shared/__tests__/timer-delay.test.ts +61 -0
  162. package/src/shared/meta-parser.ts +5 -1
  163. package/src/shared/model-ref.ts +286 -0
  164. package/src/shared/resource-meta.ts +5 -0
  165. package/src/shared/schema-env.ts +44 -0
  166. package/src/shared/schema-jsonify.ts +6 -4
  167. package/src/shared/timer-delay.ts +54 -0
  168. package/workflows/review-fix-loop-utils.cjs +9 -7
  169. package/workflows/review-fix-loop.js +20 -12
  170. package/src/orchestration/__tests__/concurrency-gate.test.ts +0 -125
  171. package/src/orchestration/concurrency-gate.ts +0 -69
@@ -17,7 +17,7 @@
17
17
  * 3. script.toExecutable → 可执行源
18
18
  * 4. 构建 RunSpec + runWorkflow(spec, deps, signal)
19
19
  * 5. 轮询至 done(间隔 STATUS_POLL_INTERVAL_MS)
20
- * 6. timeout → abortRun + transition done,time_limited
20
+ * 6. 显式 timeoutMs 到期 → abortRun + transition done,time_limited(未传不限时)
21
21
  * 7. signal.aborted → abortRun + reason=aborted
22
22
  *
23
23
  * 层归属:Engine。依赖 registry + runWorkflow/abortRun + LifecycleDeps。
@@ -32,15 +32,35 @@ import type { RunSpec } from "./models/run-spec.ts";
32
32
  import type { DoneReason } from "./models/types.ts";
33
33
  import type { WorkflowRun } from "./models/workflow-run.ts";
34
34
  import type { WorkflowScriptRegistry } from "./models/workflow-script-registry.ts";
35
+ import { assertSafeTimerDelay } from "../shared/timer-delay.ts";
35
36
 
36
37
  // ── 常量 ─────────────────────────────────────────────────────
37
38
 
38
- /** 默认 runAndWait 超时(10 分钟)。 */
39
- const DEFAULT_RUNANDWAIT_TIMEOUT_MS = 600_000;
40
-
41
39
  /** 轮询间隔(500ms)。 */
42
40
  const STATUS_POLL_INTERVAL_MS = 500;
43
41
 
42
+ /**
43
+ * [U7] XYZ_SUBAGENT_RUN_WATCHDOG_MS:无显式限时 run 的轮询绝对时限兜底 env。
44
+ *
45
+ * 与 session-runner 的 XYZ_SUBAGENT_SPAWN_WATCHDOG_MS(spawn watchdog:maxTurns 无
46
+ * 估算依据时的 hang 兜底)对称:未设置 = 无兜底(不限,watchdog 默认关的用户裁决不变);
47
+ * 设置 = pollRunToResult 的 wall-clock 绝对时限——无显式限时的 run(顶层 runAndWait
48
+ * 未传 timeoutMs / 嵌套父 run 未设 budgetTimeMs)若 workflow worker hang 将永不回收,
49
+ * 本 env 提供显式 opt-in 的兜底回收。非法值(非有限数/<=0)视为未设。
50
+ * 前缀用 XYZ_SUBAGENT_*:同 SPAWN_WATCHDOG_ENV 的桌面 safe-env 白名单原因
51
+ * (ENV_WHITELIST_PREFIXES 只放行 XYZ_ 等,PI_ 前缀被静默丢弃)。
52
+ */
53
+ export const RUN_WATCHDOG_ENV = "XYZ_SUBAGENT_RUN_WATCHDOG_MS";
54
+
55
+ /** 解析 run watchdog 毫秒数;env 未设/非法返回 undefined(无兜底,不限)。 */
56
+ function getEnvRunWatchdogMs(): number | undefined {
57
+ const raw = process.env[RUN_WATCHDOG_ENV];
58
+ if (!raw) return undefined;
59
+ const parsed = Number(raw);
60
+ if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
61
+ return parsed;
62
+ }
63
+
44
64
  // ── 类型 ─────────────────────────────────────────────────────
45
65
 
46
66
  /**
@@ -121,10 +141,26 @@ async function pollRunToResult(
121
141
  runId: string,
122
142
  deps: LauncherDeps,
123
143
  signal: AbortSignal | undefined,
124
- timeoutMs: number,
144
+ timeoutMs: number | undefined,
125
145
  abortReason: string,
126
146
  ): Promise<WorkflowRunResult> {
127
- const deadline = Date.now() + timeoutMs;
147
+ // [预算语义对齐 + U2] timeoutMs undefined 或 <=0 → 无 wall-clock deadline(不限):
148
+ // 与 lifecycle.runWorkflow 的 budgetTimeMs 判定(>0 才挂 scheduleTimeBudget)同语义。
149
+ // 旧实现 0 → deadline=now 立即超时("timed out after 0ms")、负数 → "timed out
150
+ // after -5000ms" 类错误串;非正值与 undefined 统一为不限。
151
+ // [U7] 显式不限时由 XYZ_SUBAGENT_RUN_WATCHDOG_MS 提供绝对时限兜底(未设 = 不限)。
152
+ // env 值与显式值同域:越界(>2^31-1)fail-fast——虽 deadline 是算术比较不经
153
+ // setTimeout、无 1ms 陷阱,但如此量级的配置几乎必然是手误,与 spawn watchdog env
154
+ // 的 fail-fast 策略对称。
155
+ const explicitTimeoutMs =
156
+ timeoutMs !== undefined && timeoutMs > 0 ? timeoutMs : getEnvRunWatchdogMs();
157
+ if (explicitTimeoutMs !== undefined) {
158
+ assertSafeTimerDelay(explicitTimeoutMs, `timeoutMs / ${RUN_WATCHDOG_ENV}`);
159
+ }
160
+ // Infinity 哨兵统一 while 条件,避免循环内双重判空;Infinity 永不小于自身,循环只在
161
+ // 有限 deadline 到期时退出。
162
+ const deadline =
163
+ explicitTimeoutMs === undefined ? Number.POSITIVE_INFINITY : Date.now() + explicitTimeoutMs;
128
164
  while (Date.now() < deadline) {
129
165
  if (signal?.aborted) {
130
166
  const runBeforeAbort = deps.runs.get(runId);
@@ -142,14 +178,15 @@ async function pollRunToResult(
142
178
  }
143
179
  const runBeforeTimeout = deps.runs.get(runId);
144
180
  if (runBeforeTimeout?.state.status === "done") return toResult(runBeforeTimeout);
145
- await safeAbort(runId, deps, `Workflow timed out after ${timeoutMs}ms`, "time_limited");
181
+ // 循环退出 ⇒ deadline 有限 ⇒ explicitTimeoutMs 必已定义(undefined/<=0 走 Infinity 不进此分支)
182
+ await safeAbort(runId, deps, `Workflow timed out after ${explicitTimeoutMs}ms`, "time_limited");
146
183
  const finalRun = deps.runs.get(runId);
147
184
  return finalRun
148
185
  ? toResult(finalRun)
149
186
  : {
150
187
  status: "done",
151
188
  reason: "time_limited",
152
- error: `Workflow timed out after ${timeoutMs}ms`,
189
+ error: `Workflow timed out after ${explicitTimeoutMs}ms`,
153
190
  runId,
154
191
  };
155
192
  }
@@ -174,7 +211,9 @@ async function pollRunToResult(
174
211
  * @param args 调用参数(worker 内 $ARGS 访问)
175
212
  * @param deps LauncherDeps(LifecycleDeps + registry)
176
213
  * @param signal 外部 abort signal(可选)
177
- * @param timeoutMs 超时上限(默认 10 分钟)
214
+ * @param timeoutMs 超时上限(可选)。[预算语义对齐 + U2] 未传或 <=0 = 不限(轮询至 done /
215
+ * abort 为止,不限时由 XYZ_SUBAGENT_RUN_WATCHDOG_MS 兜底)——旧实现默认 10 分钟会误杀长任务,
216
+ * 且 0/负值会落成立即超时;仅显式正数才限时。
178
217
  * @returns WorkflowRunResult(status 恒 "done")
179
218
  */
180
219
  export async function runAndWait(
@@ -182,7 +221,7 @@ export async function runAndWait(
182
221
  args: Record<string, unknown>,
183
222
  deps: LauncherDeps,
184
223
  signal?: AbortSignal,
185
- timeoutMs: number = DEFAULT_RUNANDWAIT_TIMEOUT_MS,
224
+ timeoutMs?: number,
186
225
  ): Promise<WorkflowRunResult> {
187
226
  // 1. registry 查找脚本(workflowRef = 绝对路径,S2 路径统一)
188
227
  const script = await deps.registry.getPath(name);
@@ -361,13 +400,19 @@ export async function executeNestedWorkflow(
361
400
  const runId = await runWorkflow(spec, deps, childController.signal);
362
401
 
363
402
  // Step 5: 轮询至 done(复用 runAndWait 的轮询逻辑)
364
- // [H-1] 嵌套 workflow timeout 从父 run 继承:父 spec.budgetTimeMs 存在时取
365
- // min(父 budget, DEFAULT),让子 run 不超出父 run 的剩余时间预算;否则用 DEFAULT。
403
+ // [H-1] 嵌套 workflow timeout 从父 run 完整传导:父 spec.budgetTimeMs 显式设定时
404
+ // 原样作为子 run 轮询 deadline(不 min(DEFAULT) 封顶——旧实现把父 time:2h 截断到
405
+ // 10min,违背「显式传参完整生效」语义);父未设 → undefined,无 deadline(不限)。
366
406
  // budgetRef(共享 Budget)已在 Step 4 透传给子 run 处理 token/cost 预算,
367
407
  // 此处的 budgetTimeMs 只服务 pollRunToResult 的轮询 deadline(wall-clock 兜底)。
368
- const nestedTimeoutMs = parentRun.spec.budgetTimeMs
369
- ? Math.min(parentRun.spec.budgetTimeMs, DEFAULT_RUNANDWAIT_TIMEOUT_MS)
370
- : DEFAULT_RUNANDWAIT_TIMEOUT_MS;
408
+ // [U2] 预算语义统一:父 budgetTimeMs <=0(含 0/负)与 undefined 同义 = 不限——
409
+ // 与 lifecycle.runWorkflow 的 budgetTimeMs 判定(>0 才挂 scheduleTimeBudget)对齐。
410
+ // 旧实现 0 传导给子 run 后 deadline=now 立即超时("timed out after 0ms"),与
411
+ // lifecycle 的 0=不限 语义分裂。pollRunToResult 内亦对非正值兜底归一(双写防漂移),
412
+ // 此处显式归一是传导语义的文档化表达。
413
+ const rawNestedTimeoutMs = parentRun.spec.budgetTimeMs;
414
+ const nestedTimeoutMs =
415
+ rawNestedTimeoutMs !== undefined && rawNestedTimeoutMs > 0 ? rawNestedTimeoutMs : undefined;
371
416
 
372
417
  const result = await pollRunToResult(
373
418
  runId,
@@ -24,10 +24,10 @@
24
24
  * 只有两类重建——rebuildRuntime(error-recovery,崩溃重试路径,run 保持 running、
25
25
  * replaceRuntime 原子换新)与 abort/terminate 的终态释放(transition("done") 内
26
26
  * releaseRuntime,run 不再恢复)。
27
+ * (旧并发门闩 gate 抽象已删——no-op 无生产语义,实际并发由 SubagentService
28
+ * ConcurrencyPool 管理;原 D-13 maxConcurrency=4 无消费方。)
27
29
  *
28
- * **D-13**:maxConcurrency=4(ConcurrencyGate 默认值)。
29
- *
30
- * 层归属:Engine。依赖 LifecycleDeps + ConcurrencyGate + WorkerHost via port +
30
+ * 层归属:Engine。依赖 LifecycleDeps + WorkerHost via port +
31
31
  * WorkflowRun + handleWorker* 函数。
32
32
  *
33
33
  * 参考:domain-models.md §1(聚合根状态机)。
@@ -35,8 +35,8 @@
35
35
 
36
36
  import { getLogger } from "@zhushanwen/pi-extension-logger";
37
37
 
38
+ import { assertSafeTimerDelay } from "../shared/timer-delay.ts";
38
39
  import { validateRunArgs } from "./args-validator.ts";
39
- import { ConcurrencyGate, DEFAULT_CONCURRENCY } from "./concurrency-gate.ts";
40
40
  import {
41
41
  handleWorkerError,
42
42
  handleWorkerExit,
@@ -121,12 +121,16 @@ function makeHandlers(run: WorkflowRun, deps: LifecycleDeps): WorkerHandlers {
121
121
  * 自动清理,避免孤儿触发。worker/script 错误重试经 rebuildRuntime 重排新计时器。
122
122
  *
123
123
  * @returns 计时器句柄(未设预算时 undefined)
124
+ * @throws budgetTimeMs 超出 Node setTimeout 上限(2^31-1)——溢出值会被 Node 置 1ms
125
+ * 立即触发(「不限时预算」变「立即超时」),fail-fast 不静默 clamp(U1)。
124
126
  */
125
127
  export function scheduleTimeBudget(
126
128
  runId: string,
127
129
  deps: LifecycleDeps,
128
130
  budgetTimeMs: number,
129
131
  ): ReturnType<typeof setTimeout> {
132
+ // [U1] arm 入口:值流入 setTimeout 前校验安全域(>2^31-1 会变 1ms 立即触发)。
133
+ assertSafeTimerDelay(budgetTimeMs, "budgetTimeMs");
130
134
  const timer = setTimeout(() => {
131
135
  void abortRun(runId, deps, "Time budget exceeded", "time_limited").catch(
132
136
  (err: unknown) => {
@@ -213,17 +217,16 @@ export async function runWorkflow(
213
217
  );
214
218
  }
215
219
 
216
- // 构造 handlers + runtime(worker + gate + controller)
220
+ // 构造 handlers + runtime(worker + controller)
217
221
  const handlers = makeHandlers(run, deps);
218
222
  const controller = new AbortController();
219
- const gate = new ConcurrencyGate({ maxConcurrency: DEFAULT_CONCURRENCY });
220
223
  const worker = deps.workerHost.start(spec, spec.args, handlers);
221
224
  // C.7:run 级时间预算计时器(spec.budgetTimeMs > 0 时启用,到期 abortRun time_limited)。
222
225
  const timeBudgetTimer =
223
226
  spec.budgetTimeMs && spec.budgetTimeMs > 0
224
227
  ? scheduleTimeBudget(runId, deps, spec.budgetTimeMs)
225
228
  : undefined;
226
- const runtime = new RunRuntime(worker, gate, controller, timeBudgetTimer);
229
+ const runtime = new RunRuntime(worker, controller, timeBudgetTimer);
227
230
 
228
231
  // assignRuntime(注入 runtime,恢复 I1:running ⟺ runtime!==undefined)
229
232
  run.assignRuntime(runtime);
@@ -9,7 +9,6 @@ import {
9
9
  CACHE_WRITE_WEIGHT,
10
10
  INPUT_WEIGHT,
11
11
  OUTPUT_WEIGHT,
12
- SOFT_MAX_AGENTS_WARNING,
13
12
  } from "../budget.js";
14
13
  import type { AgentUsage } from "../types.js";
15
14
 
@@ -106,7 +105,7 @@ describe("Budget.consume 加权公式", () => {
106
105
  // ── consume:NaN 守卫(TDD,预期主 agent 同步补源码守卫)──────
107
106
  //
108
107
  // 源码 consume() 当前无守卫:undefined/NaN/Infinity 进入加权公式会产出
109
- // NaN/Infinity 污染 usedTokens,导致后续 isExceeded/isThresholdReached 永远命中。
108
+ // NaN/Infinity 污染 usedTokens,导致后续 isExceeded 永远命中。
110
109
  // 以下测试断言「脏字段当 0 处理」——守卫补上前会失败,属预期临时状态。
111
110
 
112
111
  describe("Budget.consume NaN 守卫(undefined/NaN/Infinity 当 0 处理)", () => {
@@ -276,65 +275,6 @@ describe("Budget.remaining", () => {
276
275
  });
277
276
  });
278
277
 
279
- // ── isThresholdReached ───────────────────────────────────────
280
-
281
- describe("Budget.isThresholdReached", () => {
282
- it("达到 90% 阈值(usedTokens >= maxTokens × 0.9)", () => {
283
- const b = new Budget({ maxTokens: 1000 });
284
- b.consume(usage({ input: 900 }));
285
- expect(b.isThresholdReached(0.9)).toBe(true);
286
- });
287
-
288
- it("未达 90% 阈值", () => {
289
- const b = new Budget({ maxTokens: 1000 });
290
- b.consume(usage({ input: 899 }));
291
- expect(b.isThresholdReached(0.9)).toBe(false);
292
- });
293
-
294
- it("边界:恰好等于阈值 → true(>= 语义)", () => {
295
- const b = new Budget({ maxTokens: 1000 });
296
- b.consume(usage({ input: 950 }));
297
- expect(b.isThresholdReached(0.95)).toBe(true);
298
- });
299
-
300
- it("maxTokens=0 → false(守卫)", () => {
301
- const b = new Budget({ maxTokens: 0 });
302
- b.consume(usage({ input: 999999 }));
303
- expect(b.isThresholdReached(0.9)).toBe(false);
304
- });
305
-
306
- it("maxTokens undefined → false", () => {
307
- const b = new Budget();
308
- b.consume(usage({ input: 999999 }));
309
- expect(b.isThresholdReached(0.9)).toBe(false);
310
- });
311
-
312
- it("纯查询无状态——重复查询结果一致", () => {
313
- const b = new Budget({ maxTokens: 1000 });
314
- b.consume(usage({ input: 950 }));
315
- expect(b.isThresholdReached(0.9)).toBe(true);
316
- expect(b.isThresholdReached(0.9)).toBe(true);
317
- });
318
- });
319
-
320
- // ── isSoftLimitReached ───────────────────────────────────────
321
-
322
- describe("Budget.isSoftLimitReached", () => {
323
- it(`totalCallCount > ${SOFT_MAX_AGENTS_WARNING} 触发(> 严格语义)`, () => {
324
- const b = new Budget();
325
- for (let i = 0; i < SOFT_MAX_AGENTS_WARNING; i++) b.incrementCallCount();
326
- expect(b.isSoftLimitReached()).toBe(false);
327
- b.incrementCallCount(); // 501
328
- expect(b.isSoftLimitReached()).toBe(true);
329
- });
330
-
331
- it("无状态——可重复查询(非一次性 flag)", () => {
332
- const b = new Budget({ totalCallCount: SOFT_MAX_AGENTS_WARNING + 1 });
333
- expect(b.isSoftLimitReached()).toBe(true);
334
- expect(b.isSoftLimitReached()).toBe(true);
335
- });
336
- });
337
-
338
278
  // ── 构造 ─────────────────────────────────────────────────────
339
279
 
340
280
  describe("Budget 构造", () => {
@@ -3,11 +3,9 @@
3
3
  *
4
4
  * Token / cost 预算值对象(D-12)。纯数据 + 不变式守卫,无副作用。
5
5
  *
6
- * 设计:
7
- * - 无 onConsume 回调(值对象不应持可变回调)。
8
- * - soft limit 通知由 lifecycle 层 consume 后查 isSoftLimitReached 发出(职责分离)。
9
- * - 90% 预警用查询式 isThresholdReached(无状态,可重复查)。
10
- * - maxTokens===0 视为不限制(守卫,避免首个 agent 完成误判 budget_limited)。
6
+ * maxTokens===0 视为不限制(守卫,避免首个 agent 完成误判 budget_limited)。
7
+ * (预算语义对齐 2026-08:soft-limit 常量(500 调用数预警)与 90% 阈值预警方法
8
+ * 已删——全库无生产消费方,仅测试锁定。)
11
9
  *
12
10
  * 层归属:Engine。
13
11
  *
@@ -15,9 +13,6 @@
15
13
  */
16
14
  import type { AgentUsage } from "./types.ts";
17
15
 
18
- /** Soft limit:总调用数超此值发预警(FR-7,从 ConcurrencyGate 迁入)。 */
19
- export const SOFT_MAX_AGENTS_WARNING = 500;
20
-
21
16
  /**
22
17
  * Budget 加权系数(token 口径)。
23
18
  *
@@ -51,7 +46,7 @@ export class Budget {
51
46
  readonly maxTimeMs?: number;
52
47
  usedTokens = 0;
53
48
  usedCost = 0;
54
- /** 总调用计数(soft limit 用,从 ConcurrencyGate.totalCallCount 迁入)。 */
49
+ /** 总调用计数(持久化/诊断用;execute-agent-call 每次 dispatch 后 increment)。 */
55
50
  totalCallCount = 0;
56
51
 
57
52
  constructor(opts: {
@@ -89,7 +84,7 @@ export class Budget {
89
84
  this.usedCost += numOrZero(usage.cost);
90
85
  }
91
86
 
92
- /** 累加调用计数(每次 agent dispatch 后调用)。 */
87
+ /** 累加调用计数(每次 agent dispatch 后调用;持久化快照同步)。 */
93
88
  incrementCallCount(): void {
94
89
  this.totalCallCount += 1;
95
90
  }
@@ -110,17 +105,6 @@ export class Budget {
110
105
  return this.maxCost !== undefined && this.maxCost > 0 && this.usedCost >= this.maxCost;
111
106
  }
112
107
 
113
- /**
114
- * 是否达到 soft limit(FR-7)。
115
- *
116
- * totalCallCount > SOFT_MAX_AGENTS_WARNING(500)。
117
- * 调用方(lifecycle)在 consume/incrementCallCount 后查询,
118
- * 命中时发通知(无状态——可重复查询)。
119
- */
120
- isSoftLimitReached(): boolean {
121
- return this.totalCallCount > SOFT_MAX_AGENTS_WARNING;
122
- }
123
-
124
108
  /**
125
109
  * 剩余 token 预算。maxTokens 未设或 ≤0 时返回 undefined(视为不限制)。
126
110
  *
@@ -131,18 +115,4 @@ export class Budget {
131
115
  if (this.maxTokens === undefined || this.maxTokens <= 0) return undefined;
132
116
  return Math.max(0, this.maxTokens - this.usedTokens);
133
117
  }
134
-
135
- /**
136
- * 是否达到 token 预算的给定比例阈值(如 0.9 = 90% 预警)。
137
- *
138
- * 纯查询,无状态——调用方负责去重(旧 _budgetWarningSent 语义由 lifecycle 层用
139
- * 外部 Set 或 once-listener 实现)。maxTokens 未设或为 0 时返回 false。
140
- */
141
- isThresholdReached(ratio: number): boolean {
142
- return (
143
- this.maxTokens !== undefined &&
144
- this.maxTokens > 0 &&
145
- this.usedTokens >= this.maxTokens * ratio
146
- );
147
- }
148
118
  }
@@ -2,20 +2,20 @@
2
2
  * Workflow Extension — Run Runtime
3
3
  *
4
4
  * 聚合内运行时资源(仅 status==="running" 时存在)。技术资源聚合,
5
- * Engine 层类型,持 WorkerHandle / ConcurrencyGate 具体类(D-12 不造 interface)。
5
+ * Engine 层类型,持 WorkerHandle 具体类(D-12 不造 interface)。
6
6
  *
7
- * 职责:封装一次 running-segment 的所有技术资源(worker 线程 + 并发信号量 +
7
+ * 职责:封装一次 running-segment 的所有技术资源(worker 线程 +
8
8
  * abort controller),统一 release 入口(AC-2:单 release 替代多 boolean flag)。
9
+ * (旧并发门闩 gate 抽象已删——no-op,实际并发由 SubagentService ConcurrencyPool 管理;
10
+ * 原 withSlot 的 pre-abort 检查内联到 error-recovery dispatchAgentCall。)
9
11
  *
10
12
  * 一次性生命周期(G3-001):runtime 释放后不再复用——AbortController 一次性
11
- * 语义决定 controller 无法跨释放复用,gate 队列也在 worker 重跑脚本 +
12
- * callCache replay 时清空无影响,所以整个 RunRuntime 重建。唯一注入路径:
13
+ * 语义决定 controller 无法跨释放复用,所以整个 RunRuntime 重建。唯一注入路径:
13
14
  * assignRuntime(runWorkflow 创建)与 replaceRuntime(error-recovery 崩溃重试)。
14
15
  *
15
16
  * 参考:domain-models.md §10、clarification.md G3-001。
16
17
  */
17
18
 
18
- import { ConcurrencyGate } from "../concurrency-gate.ts";
19
19
  import { WorkerHandle } from "../worker-handle.ts";
20
20
 
21
21
  /**
@@ -32,25 +32,40 @@ export type ReleaseMode = "terminal";
32
32
  export class RunRuntime {
33
33
  /** Worker 线程句柄。 */
34
34
  readonly worker: WorkerHandle;
35
- /** 并发信号量。 */
36
- readonly gate: ConcurrencyGate;
37
35
  /** per-running-segment AbortController(一次性,无法复用——G3-001)。 */
38
36
  readonly controller: AbortController;
39
37
  /** Run 级墙钟时间预算计时器(spec.budgetTimeMs > 0 时由 lifecycle 调度,
40
38
  * 到期 abortRun time_limited)。release 时清理,避免 abort/replaceRuntime
41
39
  * 后孤儿计时器仍触发(rebuildRuntime 会重排一个全新的计时器,旧的不应残留)。 */
42
40
  readonly timeBudgetTimer?: ReturnType<typeof setTimeout>;
41
+ /**
42
+ * 本 runtime 代际是否已收到 worker 的终态消息(return / error)。
43
+ *
44
+ * [F1] worker exit(0) 且本标记为 false = worker 静默退出、未交付任何终态——最常见根因
45
+ * 是 execute() 返回值不可克隆,worker 侧 _safePost 吞掉 DataCloneError 后 return 消息
46
+ * 根本没发出。旧实现 handleWorkerExit 对 code===0 no-op → run 永久 running、runAndWait
47
+ * 悬挂。handleWorkerExit 据此判定转 done,failed。
48
+ *
49
+ * 按代际归零:字段挂在 RunRuntime(每代际 new 一个实例)而非 run.meta——script-error
50
+ * 重试退避窗口内(error 消息已收到、run 仍 running、旧 worker exit(0))必须 no-op 等
51
+ * rebuild;若挂 meta 则 rebuild 后新 worker 再静默退出时会被旧标记误放行,重新悬挂。
52
+ *
53
+ * 写点:① handleWorkerMessage 的 return/error 分支(WorkerHandle.isCurrent 守卫保证
54
+ * 消息必来自当前代际);② handleWorkerError 进入处理前([R4-F1] 同代际幂等守卫——
55
+ * worker 崩溃时 error + exit(1) 双事件各派发一次 handleWorkerError,第一个事件标记
56
+ * 本代际已处理,第二个事件命中标志跳过,消除单次崩溃计数 +2 / 双 rebuild 交错)。
57
+ * rebuildRuntime 构造新 RunRuntime 自然重置。
58
+ */
59
+ receivedTerminalMessage = false;
43
60
  /** 防止 release 重复执行(幂等)。 */
44
61
  private released = false;
45
62
 
46
63
  constructor(
47
64
  worker: WorkerHandle,
48
- gate: ConcurrencyGate,
49
65
  controller: AbortController,
50
66
  timeBudgetTimer?: ReturnType<typeof setTimeout>,
51
67
  ) {
52
68
  this.worker = worker;
53
- this.gate = gate;
54
69
  this.controller = controller;
55
70
  this.timeBudgetTimer = timeBudgetTimer;
56
71
  }
@@ -101,6 +101,15 @@ export interface AgentCallOpts {
101
101
  */
102
102
  timeoutMs?: number;
103
103
  /**
104
+ * Turn 上限(turn limiter 用)。
105
+ *
106
+ * [预算语义对齐] 未传或 <=0 = 不限 turn;此时也不按 turns 估算 spawn watchdog——
107
+ * 仅当 env XYZ_SUBAGENT_SPAWN_WATCHDOG_MS 设置时才按绝对时限挂 watchdog(见
108
+ * session-runner.resolveSpawnWatchdogMs)。mapToExecuteOptions 原样透传到
109
+ * ExecuteOptions.maxTurns → 引擎 task-spec → runSpawn。
110
+ */
111
+ maxTurns?: number;
112
+ /**
104
113
  * Skill name to load (e.g. "code-review"). Resolved to SKILL.md path
105
114
  * and injected via --skill flag in the subprocess.
106
115
  */
@@ -143,6 +152,13 @@ export interface AgentCallOpts {
143
152
  cwd?: string;
144
153
  /** Inherit parent session context (fork mode). Independent of worktree (file isolation). */
145
154
  fork?: boolean;
155
+ /**
156
+ * 执行引擎 id(P4 D9 三层优先级的第一层:调用参数级,workflow step 显式指定)。
157
+ * 仅限「必须某引擎独有能力」的场景使用并注释原因(D9③ workflow 脚本不写死
158
+ * engine——环境差异由 frontmatter/全局默认承载);透传链 worker-script-builder
159
+ * agent() → execute-agent-call → SAR 路由层。
160
+ */
161
+ engine?: string;
146
162
  /** Filesystem isolation: when true, creates a new git worktree for the agent. Independent of fork. */
147
163
  worktree?: boolean;
148
164
  /** When true, agent() resolves {value, sessionFile, worktreePath, error} instead of a bare value.
@@ -739,7 +739,7 @@ export function lintScript(source: string): LintResult {
739
739
  // 教训来源:daily-news-impact.js 用 (async function main(){...})();() 包裹整个脚本,
740
740
  // worker 外层 IIFE 不等内层 IIFE 就 postMessage("return"),主线程 transition done
741
741
  // → release runtime → controller.abort() → spawn 后 2ms SIGKILL 子进程。
742
- // 诊断耗时 4 轮:先后误判为 model 故障 / 工具缺失 / turn-signal abort / ConcurrencyGate 异常,
742
+ // 诊断耗时 4 轮:先后误判为 model 故障 / 工具缺失 / turn-signal abort / 并发门闩 gate 异常,
743
743
  // 最终靠 worker-host → handleReturn → release → abort 的调用栈定位。
744
744
  findings.push(...checkBareAsyncIIFE(source));
745
745
 
@@ -60,23 +60,46 @@ export function clearSkillPathCache(): void {
60
60
  skillCandidatesCache.clear();
61
61
  }
62
62
 
63
+ /**
64
+ * 把 skillName 解析到 rootDir 内的候选路径;越界(穿越)返回 undefined。
65
+ *
66
+ * 为什么需要守卫:path.resolve/path.join 会吸收 `..`("../../x" 落到 skills 根
67
+ * 之外),随后的 existsSync 命中会把 skills 树外的目录当 skill 目录返回——skill 名
68
+ * 是名字不是路径,不该具备树外寻址能力。resolve 后与规范化 skills 根做前缀比较
69
+ * (startsWith(root + sep);恰好等于根也拒绝——"." 会把根自身当 skill 目录),
70
+ * 越界 = 该候选不存在:全部候选越界时 resolveSkillPath 返回 undefined,调用方
71
+ * (agent-opts-resolver)收到 not found(与 workflow name 拒绝的反馈风格一致)。
72
+ *
73
+ * 根内归一化仍允许("a/../b" resolve 后在根内——守卫只拒逃逸,不拒归一化)。
74
+ */
75
+ function resolveWithinRoot(rootDir: string, skillName: string): string | undefined {
76
+ const root = path.resolve(rootDir);
77
+ const resolved = path.resolve(root, skillName);
78
+ if (resolved !== root && !resolved.startsWith(root + path.sep)) {
79
+ return undefined;
80
+ }
81
+ return resolved;
82
+ }
83
+
63
84
  export function resolveSkillPath(skillName: string): string | undefined {
64
85
  // has 先行区分「缓存了未命中(undefined)」与「无条目」——未命中也缓存(DM3)
65
86
  if (skillMemo.has(skillName)) {
66
87
  return skillMemo.get(skillName);
67
88
  }
68
89
 
69
- const candidates = [
70
- // Project-level
71
- path.resolve(process.cwd(), ".agents/skills", skillName),
72
- // Global user skills
73
- path.join(getAgentDir(), "skills", skillName),
74
- ];
90
+ const candidates: string[] = [];
91
+ const pushCandidate = (dir: string | undefined): void => {
92
+ if (dir !== undefined) candidates.push(dir);
93
+ };
94
+ // Project-level
95
+ pushCandidate(resolveWithinRoot(path.resolve(process.cwd(), ".agents/skills"), skillName));
96
+ // Global user skills
97
+ pushCandidate(resolveWithinRoot(path.join(getAgentDir(), "skills"), skillName));
75
98
 
76
- // npm package skills (cached)
99
+ // npm package skills (cached)
77
100
  const npmSkillsDir = path.join(getAgentDir(), "npm/node_modules");
78
101
  for (const pkgSkillsBase of getNpmSkillCandidates(npmSkillsDir)) {
79
- candidates.push(path.join(pkgSkillsBase, skillName));
102
+ pushCandidate(resolveWithinRoot(pkgSkillsBase, skillName));
80
103
  }
81
104
 
82
105
  for (const dir of candidates) {
@@ -66,7 +66,7 @@ const WORKER_TEMPLATE_PRE = [
66
66
  'const _workerLogs = [];',
67
67
  '// IF6(#12): known agent() fields — hoisted to module scope, built once per worker',
68
68
  '// (was rebuilt inside agent() on every call; field set is call-invariant).',
69
- 'const _KNOWN_FIELDS = new Set(["prompt", "description", "schema", "model", "scene", "label", "task", "agent", "phase", "skill", "timeoutMs", "cwd", "fork", "worktree", "returnMeta", "thinkingLevel"]);',
69
+ 'const _KNOWN_FIELDS = new Set(["prompt", "description", "schema", "model", "scene", "label", "task", "agent", "phase", "skill", "timeoutMs", "maxTurns", "cwd", "fork", "worktree", "returnMeta", "thinkingLevel", "engine"]);',
70
70
  'function _pushWorkerLog(level, args) {',
71
71
  ' try { _workerLogs.push({ level, message: args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ") }); } catch (e) { /* swallow */ }',
72
72
  '}',
@@ -152,6 +152,8 @@ const WORKER_TEMPLATE_PRE = [
152
152
  ' // 让 parallel() 下的脚本容错循环(parseResult → null → skip)自然接管。',
153
153
  ' // 错误原因已由主线程 executeAgentCall → trace.update(result.error) 保留在 trace/TUI,',
154
154
  ' // 不丢失。失败 resolve 为空字符串是既定容错策略。',
155
+ ' // [MF-4] schema 模式下失败时 agent() 仍 resolve(content 回退、不 throw)——',
156
+ ' // 需要检查错误时请用 returnMeta:true(resolve 值含 error 字段)。',
155
157
  ' // parsedOutput: validated data object from structured-output execute().',
156
158
  ' // Fallback to content (raw text) when no schema was requested or on error.',
157
159
  ' // W2 改动 9(b):returnMeta===true 时 resolve {value,sessionFile,worktreePath,error,usage,durationMs,sessionId}',
@@ -210,6 +212,11 @@ const WORKER_TEMPLATE_PRE = [
210
212
  ' scene: (secondArg && typeof secondArg === "object" && secondArg.scene) || undefined,\n' +
211
213
  ' phase: (secondArg && typeof secondArg === "object" && secondArg.phase) || undefined,',
212
214
  ' thinkingLevel: (secondArg && typeof secondArg === "object" && secondArg.thinkingLevel) || $THINKING_LEVEL,',
215
+ ' // step 级 turn 上限(turn limiter;显式 0/负 = 显式不限,压过 spawn watchdog env 兑底,SP-6)\n' +
216
+ ' // ?? 语义保真:仅 null/undefined 归 undefined(走 env 兑底),显式 0 保留(U5 参数 > env)',
217
+ ' maxTurns: (secondArg && typeof secondArg === "object" ? secondArg.maxTurns : undefined) ?? undefined,',
218
+ ' // P4 D9③:step 级 engine 显式指定(仅限必须某引擎独有能力的场景)',
219
+ ' engine: (secondArg && typeof secondArg === "object" && secondArg.engine) || undefined,',
213
220
  ' };',
214
221
  ' } else if (typeof firstArg === "object" && firstArg !== null) {',
215
222
  ' if (firstArg.prompt) {',
@@ -226,11 +233,13 @@ const WORKER_TEMPLATE_PRE = [
226
233
  ' scene: firstArg.scene,',
227
234
  ' skill: firstArg.skill,',
228
235
  ' timeoutMs: firstArg.timeoutMs,',
236
+ ' maxTurns: firstArg.maxTurns,',
229
237
  ' cwd: firstArg.cwd,',
230
238
  ' fork: firstArg.fork,',
231
239
  ' worktree: firstArg.worktree,',
232
240
  ' returnMeta: firstArg.returnMeta,',
233
241
  ' thinkingLevel: firstArg.thinkingLevel || $THINKING_LEVEL,',
242
+ ' engine: firstArg.engine,',
234
243
  ' };',
235
244
  ' } else {',
236
245
  ' opts = firstArg;',
@@ -247,7 +256,7 @@ const WORKER_TEMPLATE_PRE = [
247
256
  ' // Validate known agent() fields to catch API misuse early (_KNOWN_FIELDS at module scope)',
248
257
  ' const _unknownFields = Object.keys(opts).filter((k) => !_KNOWN_FIELDS.has(k));',
249
258
  ' if (_unknownFields.length > 0) {',
250
- ' _pushWorkerLog("warn", ["[workflow] agent() received unknown fields: " + _unknownFields.join(", ") + ". Known fields: prompt, description, schema, model, scene, label, task, agent, phase, skill, timeoutMs, cwd, fork, worktree, returnMeta, thinkingLevel"]);',
259
+ ' _pushWorkerLog("warn", ["[workflow] agent() received unknown fields: " + _unknownFields.join(", ") + ". Known fields: prompt, description, schema, model, scene, label, task, agent, phase, skill, timeoutMs, maxTurns, cwd, fork, worktree, returnMeta, thinkingLevel, engine"]);',
251
260
  ' }',
252
261
  '',
253
262
  ' const callId = _callIdCounter;',
@@ -385,7 +394,14 @@ const WORKER_TEMPLATE_POST = [
385
394
  ' }',
386
395
  '})().then((result) => {',
387
396
  ' const runId = (_workerData.args && typeof _workerData.args === "object" && _workerData.args._runId) || "";',
388
- ' _safePost({ type: "return", runId, result, workerLogs: _workerLogs }, "return");',
397
+ ' if (!_safePost({ type: "return", runId, result, workerLogs: _workerLogs }, "return")) {',
398
+ ' // [F1] return 值不可克隆(含 function/Symbol/循环引用 → DataCloneError)时 _safePost',
399
+ ' // 只能记日志返回 false——若不补救,worker 将静默 exit(0),主线程收不到任何终态消息,',
400
+ ' // run 永久 running、runAndWait 悬挂。回发可克隆的 error 消息(DataCloneError 详情',
401
+ ' // 已由 _safePost 记入 _workerLogs 随消息带回),让主线程 handleScriptError 接管,',
402
+ ' // run 经既有重试矩阵收敛到终态 failed。',
403
+ ' _safePost({ type: "error", runId, error: "Workflow return value could not be delivered (structured-clone failed) — see workerLogs for the postMessage error", workerLogs: _workerLogs }, "error");',
404
+ ' }',
389
405
  '}).catch((err) => {',
390
406
  ' const runId = (_workerData.args && typeof _workerData.args === "object" && _workerData.args._runId) || "";',
391
407
  ' _safePost({ type: "error", runId, error: err.message || String(err), workerLogs: _workerLogs }, "error");',