@zhushanwen/pi-subagent-workflow 8.14.7 → 9.0.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 (58) hide show
  1. package/README.md +6 -5
  2. package/package.json +11 -7
  3. package/relay/relay.mjs +45 -2
  4. package/skills/workflow-script-format/SKILL.md +2 -56
  5. package/src/host/__tests__/inflight-reporter.test.ts +26 -1
  6. package/src/host/__tests__/pi-host.test.ts +418 -4
  7. package/src/host/inflight-reporter.ts +13 -10
  8. package/src/host/pi-host.ts +205 -17
  9. package/src/index.ts +21 -72
  10. package/src/injectors/__tests__/engine-awareness.test.ts +236 -33
  11. package/src/injectors/__tests__/helpers/injector-test-mocks.ts +15 -7
  12. package/src/injectors/__tests__/workflow-list-injector.test.ts +447 -40
  13. package/src/injectors/resource-list-injector.ts +103 -32
  14. package/src/injectors/subagent-list-injector.ts +4 -28
  15. package/src/injectors/workflow-list-injector.ts +8 -4
  16. package/src/interface/__tests__/capture-tool.ts +56 -0
  17. package/src/interface/__tests__/commands-resume.test.ts +103 -0
  18. package/src/interface/__tests__/tool-prompt-contract.test.ts +361 -0
  19. package/src/interface/__tests__/tool-render.test.ts +105 -2
  20. package/src/interface/__tests__/tool-subagents.test.ts +24 -26
  21. package/src/interface/__tests__/tool-workflow-resume.test.ts +227 -0
  22. package/src/interface/__tests__/tool-workflow-script.test.ts +557 -0
  23. package/src/interface/__tests__/tool-workflow.test.ts +884 -0
  24. package/src/interface/bg-notify-render.ts +18 -9
  25. package/src/interface/command-actions.ts +22 -17
  26. package/src/interface/commands.ts +58 -12
  27. package/src/interface/format.ts +50 -16
  28. package/src/interface/id-preview.ts +4 -4
  29. package/src/interface/subagent-actions.ts +18 -50
  30. package/src/interface/subagent-tool.ts +26 -18
  31. package/src/interface/subagents.ts +5 -11
  32. package/src/interface/tool-render.ts +14 -30
  33. package/src/interface/tool-shared.ts +55 -12
  34. package/src/interface/tool-subagents.ts +21 -11
  35. package/src/interface/tool-workflow-script.ts +52 -72
  36. package/src/interface/tool-workflow.ts +345 -139
  37. package/src/interface/views/WorkflowsView.ts +10 -7
  38. package/src/interface/views/__tests__/{WorkflowsView-signature.test.ts → WorkflowsView.test.ts} +155 -11
  39. package/src/interface/views/detail-content.ts +3 -5
  40. package/src/jsonl-run-store.ts +656 -544
  41. package/src/model-events.ts +35 -0
  42. package/src/session-lifecycle.ts +376 -85
  43. package/src/subagents-events.ts +56 -0
  44. package/src/workflow-events.ts +285 -200
  45. package/src/workflow-notify.ts +456 -0
  46. package/src/injectors/__tests__/engine-section-stability.test.ts +0 -353
  47. package/src/injectors/__tests__/model-list-injector.test.ts +0 -136
  48. package/src/injectors/__tests__/subagent-list-injector.test.ts +0 -376
  49. package/src/interface/__tests__/detectors.test.ts +0 -161
  50. package/src/interface/__tests__/subagent-tool-prompt.test.ts +0 -183
  51. package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +0 -219
  52. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +0 -251
  53. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +0 -215
  54. package/src/interface/__tests__/workflow-state-file-exposure.test.ts +0 -38
  55. package/src/interface/__tests__/workflow-tool-prompt.test.ts +0 -129
  56. package/src/interface/helpers.ts +0 -272
  57. package/src/interface/views/__tests__/detail-content-session-file.test.ts +0 -73
  58. package/src/interface/views/__tests__/record-progress.test.ts +0 -235
@@ -9,32 +9,37 @@
9
9
  * reload 存活,factory 重跑拿到同一 domain state,post-reload adoption 据此接管
10
10
  * 在飞 run):
11
11
  * 1. per-factory 域状态(lsRef / notifiedRunIds / workerHost / registry / sessionState)
12
- * 2. log(pi.appendEntry 包装)+ makeLifecycleDeps + makeDeps(LauncherDeps 生产装配)
13
- * 3. 7 个 pi.on handler(session_start / session_compact / model_select /
14
- * session_tree / session_before_fork / session_before_switch / session_shutdown,
15
- * 注册相对顺序原样保留)
16
- * 4. getWorkflowDeps 守卫(单一出口,discriminated union)+ lazyDeps(tool lazy 注入源)
12
+ * 2. log(pi.appendEntry 包装)+ runSettledEffects(onRunDone 三步终局副作用
13
+ * 管线,具名导出可直测)+ makeLifecycleDeps + makeDeps(LauncherDeps 纯装配)
14
+ * 3. 本域 3 个 pi.on handler(session_start / session_tree / session_shutdown)
15
+ * + 4 个跨域事件注册(notify ledger compaction 守卫 / model 缓存刷新 /
16
+ * subagents 父级联关闭×2——setup* 函数住各自域模块,本 seam 在原注册位
17
+ * 置调用;pi.on 全链注册顺序逐位不变,
18
+ * workflow-events.test.ts 锁定)
19
+ * 4. deps 守卫(单一出口,失败 throw)+ lazyDeps(tool lazy 注入源)
17
20
  *
18
- * 组合根消费面:setupWorkflowDomain(pi, { inflightReporter }) 返回
19
- * WorkflowDomainHandle(state / lazyDeps / getWorkflowDeps / isScriptRunning),
20
- * tool + command 注册与 pi.__workflowRun 仍留 index.ts。
21
+ * 组合根消费面:setupWorkflowDomain(pi) 返回
22
+ * WorkflowDomainHandle(state / lazyDeps / isScriptRunning),
23
+ * tool + command 注册仍留 index.ts。
21
24
  *
22
25
  * [skill-reload D3] makeDeps 的三个 volatile 成员(eventBus / log / onRunDone 的
23
- * pi 与 GuiContext)不做闭包快照:pi 从槽上 currentPi 现读(factory 重跑时覆盖
24
- * 登记),ctx 从 sessionState 条目的 state.ctx 现读(adoption rebind 换新后自动
25
- * 跟进)——在飞 pump 持有的旧 deps 对象经属性访问自动路由到新绑定,无需遍历重绑。
26
+ * pi)不做闭包快照:pi 从槽上 currentPi 现读(factory 重跑时覆盖登记)——在飞
27
+ * pump 持有的旧 deps 对象经属性访问自动路由到新绑定,无需遍历重绑。
26
28
  *
27
29
  * 测试入口:既有 index 挂载类测试(index-session-start / process-shutdown-hook /
28
30
  * wave0-package-structure 等)经 factory 间接覆盖;mock 锚点是模块解析路径
29
31
  * (jsonl-run-store / interface/* / subagent-core 深路径),随迁不改写。
32
+ * runSettledEffects 经 fake deps 直测(workflow-events.test.ts,
33
+ * 不挂装配面)。注册顺序经 workflow-events.test.ts 锁定。
30
34
  *
31
35
  * 架构导航见 docs/extensions/subagents/architecture.md §2.1。
32
36
  */
33
37
 
38
+ import { join } from "node:path";
39
+
34
40
  import type {
35
41
  ExtensionAPI,
36
42
  ExtensionContext,
37
- SessionCompactEvent,
38
43
  SessionShutdownEvent,
39
44
  SessionStartEvent,
40
45
  SessionTreeEvent,
@@ -43,24 +48,31 @@ import { getLogger } from "@zhushanwen/pi-extension-logger";
43
48
  import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
44
49
  // ═══ 经 core barrel 消费 workflow 域(引擎与 worker 住 packages/subagent-core) ═══
45
50
  import { bestEffort } from "@zhushanwen/subagent-core";
46
- import { getBoundNotifyLedger } from "@zhushanwen/subagent-core";
47
- import { getModelConfigService } from "@zhushanwen/subagent-core";
48
51
  import { getSubagentService } from "@zhushanwen/subagent-core";
49
52
  import type { LauncherDeps } from "@zhushanwen/subagent-core";
50
- import { executeNestedWorkflow, terminateRunningRuns } from "@zhushanwen/subagent-core";
53
+ import { terminateRunningRuns } from "@zhushanwen/subagent-core";
54
+ import { setInFlightListener } from "@zhushanwen/subagent-core";
51
55
  import {
52
56
  evictDoneRunsBeyondCap,
53
57
  MAX_RETAINED_DONE_RUNS,
58
+ runSummary,
54
59
  scheduleTimeBudget,
60
+ STATE_DIR_NAME,
55
61
  } from "@zhushanwen/subagent-core";
56
62
  import type { WorkflowRun } from "@zhushanwen/subagent-core";
57
63
  import { WorkerHostImpl } from "@zhushanwen/subagent-core";
58
64
  import { WorkflowScriptRegistryImpl } from "@zhushanwen/subagent-core";
59
- // [u7a D5] 在途上报出口类型(实例由组合根创建并接线 setInFlightListener,
60
- // 本模块只在 session_start / session_shutdown 驱动 attach/detach)。
61
- import type { InFlightReporter } from "./host/inflight-reporter.ts";
62
- import { toGuiCtx } from "./interface/gui-mappers.ts";
63
- import { notifyDone, trackNotifiedRunId } from "./interface/helpers.ts";
65
+ // [u7a D5] 在途上报出口:实例由本 seam 创建并接线 setInFlightListener(组合根零
66
+ // 管道),session_start / session_shutdown 驱动 attach/detach;测试可注入 fake。
67
+ import { createInFlightReporter, type InFlightReporter } from "./host/inflight-reporter.ts";
68
+ // [W2/V1 D1 第 8 行] 判活类消费面(isScriptRunning)经 core 投影单点
69
+ //(runSummary.status 二值——内部收拢注册表 ∨ 聚合混合判源)。
70
+ import { type RunSettlementRecord } from "./jsonl-run-store.ts";
71
+ import { notifyDone, trackNotifiedRunId } from "./workflow-notify.ts";
72
+ // ═══ 跨域事件注册(handler 体住各自域模块,本 seam 原位调用保注册顺序) ═══
73
+ import { setupNotifyLedgerCompactionGuard } from "./workflow-notify.ts";
74
+ import { setupModelEvents } from "./model-events.ts";
75
+ import { setupSubagentsCascadeEvents } from "./subagents-events.ts";
64
76
  // ═══ session 生命周期装配 seam(bootstrap seam,设计 §3.1/D1) ═══
65
77
  import {
66
78
  getOrCreateDialogQueue,
@@ -142,52 +154,108 @@ function resolveCurrentPi(): ExtensionAPI {
142
154
  return pi;
143
155
  }
144
156
 
145
- /** 组合根侧生产 deps 工厂:SessionLifecycleDeps 全部成员有生产默认实现(住
146
- * session-lifecycle.ts——createOrReuseServices 单例语义 / WorktreeManager 每次
147
- * 扫描新建 / JsonlRunStore per-session 新建),此处无本地构造可注入;工厂形态
148
- * 保留为组合根侧注入点(测试或后续演进可在此覆盖)。
149
- * 测试注入路径:不挂载 index.ts,直接调 setupSessionLifecycle(pi, ctx, fakeDeps)。
150
- * [skill-reload D4] 唯一本地构造 onAdoptionFailed 依赖 setupWorkflowDomain 闭包
151
- * (makeDeps / sessionState),其定义随迁函数体内部(见 makeDeps 之后)。 */
152
-
153
157
  // ── workflow deps 守卫(单一出口) ─────────────────────────────────────────────
154
158
  //
155
- // [守卫合一] 原 pi.__workflowRun 内联守卫 + getDeps 守卫两份重复(state 缺失 /
156
- // storeHealthy=false),合并为单一 getWorkflowDeps:返回 discriminated union,
157
- // 两个消费点各自决定失败形态——pi.__workflowRun(D-8 API)返回错误对象(不
158
- // throw,保住调用方 Promise 契约),getDeps(3 个 tool 的 lazy deps 源)throw
159
- // (pi tool 框架将其转译为 tool 错误结果)。错误消息逐字保留(crash-recovery
160
- // 测试锁 "store unavailable" / "loadAll failed" 子串)。
161
- export type WorkflowDepsResolution =
162
- | { ok: true; deps: LauncherDeps }
163
- | { ok: false; reason: string };
159
+ // 守卫单一出口(lazyDeps 消费):state 缺失 / storeHealthy=false 时 throw(pi tool
160
+ // 框架将其转译为 tool 错误结果)。错误消息逐字保留(crash-recovery 测试锁
161
+ // "store unavailable" / "loadAll failed" 子串)。
164
162
 
165
163
  // ── lazyDeps 样板收敛 ──────────────────────────────────────────────────────────
166
164
 
167
165
  /**
168
- * lazyDeps 单成员转发原语:属性访问触发 getWorkflowDeps 守卫求值,失败 throw
169
- * (与 __workflowRun 的 return 错误对象对齐——同源同消息),成功转发 deps[key]。
170
- * 收敛原 index.ts 10 个同构 4 行 getter;每属性独立求值语义不变。
166
+ * lazyDeps 单成员转发原语:属性访问触发 deps 守卫求值,失败 throw,成功转发
167
+ * deps[key]。收敛原 index.ts 10 个同构 4 行 getter;每属性独立求值语义不变。
171
168
  */
172
169
  function createLazy<K extends keyof LauncherDeps>(
173
- resolve: () => WorkflowDepsResolution,
170
+ resolve: () => LauncherDeps,
174
171
  key: K,
175
172
  ): LauncherDeps[K] {
176
- const resolved = resolve();
177
- if (!resolved.ok) throw new Error(resolved.reason);
178
- return resolved.deps[key];
173
+ return resolve()[key];
174
+ }
175
+
176
+ // ── run 终局固定顺序副作用(makeDeps onRunDone 管线提级,可直测) ─────────────
177
+
178
+ /** runSettledEffects 的注入面(生产装配点 = makeDeps;直测注入 fake——模块行为
179
+ * 可脱离装配面独立测试)。 */
180
+ export interface RunSettledEffectsEnv {
181
+ /** 完成通知的发送面(现读 volatile pi:生产传 resolveCurrentPi,调用时解析——
182
+ * D3 不快照,reload 后自动路由新 pi)。 */
183
+ resolvePi(): ExtensionAPI;
184
+ /** notifyDone 完成通知去重窗口(domainState.notifiedRunIds,调用方持有)。 */
185
+ notifiedRunIds: Set<string>;
186
+ /** per-session 装配结果(sessionDir/runs 两字段调用时现读——sessionState
187
+ * 条目引用,adoption rebind 换新后自动跟进的 D3 语义保持)。 */
188
+ state: Pick<SessionLifecycleResult, "sessionDir" | "runs">;
189
+ /**
190
+ * [W2/V1 D1 第 7 行] 终局记录查询面(通知载荷源——journal run-settled 帧直取)。
191
+ * 生产装配 = JsonlRunStore.settledRecordOf;返回 undefined = 无终局帧(journal
192
+ * dispatch 失败窗口)——通知跳过 + error 留痕(不回退两态机字段兜底:I2 失效
193
+ * 窗口下兜底 = 恒 completed 假成功,比丢通知更糟)。
194
+ */
195
+ settledRecordOf(runId: string): RunSettlementRecord | undefined;
196
+ /** evict 日志的 session 归属(lsRef 引用,调用时现读 lastSessionId)。 */
197
+ lsRef: { lastSessionId: string };
198
+ }
199
+
200
+ /**
201
+ * run 终局后的固定顺序副作用管线(原 makeDeps 的 onRunDone 闭包体提级具名导出)。
202
+ *
203
+ * onRunDone 是全部 done 路径的单点汇聚(abortRun + error-recovery),顺序固化为
204
+ * notifyDone → trackNotifiedRunId → evictDoneRunsBeyondCap:
205
+ * notifyDone 先发完整聚合通知(淘汰后聚合根仍在参数 run 引用上不受影响),
206
+ * trackNotifiedRunId 有界化去重窗口,最后裁剪已终局 run 内存([W2/V1] 终局判定
207
+ * 经 core isRunSettled 换源——本管线被 onRunDone 触发时注册表已 note,本轮 run
208
+ * 恒可淘汰候选,且其终局时序 = 全局最新,恒在保留端,不被自身触发的裁剪淘汰)。
209
+ *
210
+ * 失败语义(直测锁定,workflow-events.test.ts):管线内部无围栏
211
+ * ——notifyDone 抛错(账本写账失败 / 降级直发非 stale 失败)时后续 track/evict
212
+ * 不执行、异常原样上抛,由调用方 finalizeRun 的 onRunDone 独立 try 围栏接住
213
+ * (core worker-message-pump,OR-4/B-4:真实副作用失败 error 留痕不崩宿主)。
214
+ * notifyDone 幂等早退(notifiedRunIds 已含 runId)不是错误:后两步照常执行。
215
+ *
216
+ * [D7] notifyDone 注入产物目录指针(<sessionDir>/workflow-state——
217
+ * journal/manifest/.state 同目录;state.sessionDir 是 sessionState 条目字段,
218
+ * adoption rebind 换新后自动跟进;目录分量经 core barrel STATE_DIR_NAME 单源)。
219
+ * [W2/V1 D1 第 7 行] notifyDone 尾参注入终局记录(载荷源 = 帧直取);记录缺席
220
+ *(journal dispatch 失败窗口)时通知跳过 + error 留痕——evict 照常
221
+ *(内存有界性独立于通知),track 不标(未发通知不占去重窗口,允许后续语义修正)。
222
+ */
223
+ export function runSettledEffects(env: RunSettledEffectsEnv, run: WorkflowRun): void {
224
+ const settlement = env.settledRecordOf(run.runId);
225
+ if (settlement !== undefined) {
226
+ notifyDone(
227
+ env.resolvePi(),
228
+ run.runId,
229
+ run,
230
+ env.notifiedRunIds,
231
+ join(env.state.sessionDir, STATE_DIR_NAME),
232
+ settlement,
233
+ );
234
+ trackNotifiedRunId(env.notifiedRunIds, run.runId);
235
+ } else {
236
+ logger.error(
237
+ `[workflow] done notify skipped: settlement record unavailable (runId=${run.runId}) — ` +
238
+ "run-settled journal dispatch likely failed; recovery: consult the journal error log above",
239
+ );
240
+ }
241
+ const evicted = evictDoneRunsBeyondCap(env.state.runs, MAX_RETAINED_DONE_RUNS);
242
+ if (evicted > 0) {
243
+ logger.debug("[subagent-workflow] evicted done runs beyond cap", {
244
+ evicted,
245
+ keep: MAX_RETAINED_DONE_RUNS,
246
+ sessionId: env.lsRef.lastSessionId,
247
+ });
248
+ }
179
249
  }
180
250
 
181
251
  // ── 组合根消费面 ───────────────────────────────────────────────────────────────
182
252
 
183
253
  export interface WorkflowDomainHandle {
184
254
  /** workflow 域状态(组合根只读消费:engine-awareness lastEngine 存取器 /
185
- * workflows command runs getter / pi.__workflowRun lastSessionId)。 */
255
+ * workflows command runs getter)。 */
186
256
  state: WorkflowDomainState;
187
- /** 3 个 tool + workflows command 的 lazy deps 注入源。 */
257
+ /** 2 个 tool + workflows command 的 lazy deps 注入源(workflow-script tool 走 state.registry 直供不经 lazyDeps)。 */
188
258
  lazyDeps: LauncherDeps;
189
- /** 守卫单一出口(pi.__workflowRun 消费点;lazyDeps 内部走同一函数)。 */
190
- getWorkflowDeps(sessionId: string): WorkflowDepsResolution;
191
259
  /** workflow-script tool 的重入检查(跨 session 全局视角)。 */
192
260
  isScriptRunning(name: string): boolean;
193
261
  }
@@ -196,18 +264,28 @@ export interface WorkflowDomainHandle {
196
264
  * workflow 域事件族装配单一入口(事件族 seam,与 session-lifecycle.ts 同构)。
197
265
  * index.ts 的 workflow 域退为本调用 + 装配结果消费。
198
266
  *
199
- * 7 个 pi.on handler 的注册相对顺序原样保留(session_start → session_compact →
200
- * model_select → session_tree → session_before_fork → session_before_switch →
201
- * session_shutdown);engine-awareness(before_agent_start 链尾)仍由组合根在
202
- * 本调用之后注册(before_agent_start 链序不变,跨事件通道无注册时序语义)。
267
+ * 本域 3 个 handler(session_start → session_tree → session_shutdown)与 4 个
268
+ * 跨域 setup* 注册(notify ledger compaction 守卫 / model 缓存刷新 / subagents
269
+ * 父级联关闭×2)交错,pi.on 全链注册顺序逐位不变(锁
270
+ * workflow-events.test.ts);engine-awareness
271
+ * (before_agent_start 链尾)仍由组合根在本调用之后注册(before_agent_start
272
+ * 链序不变,跨事件通道无注册时序语义)。
203
273
  */
204
274
  export function setupWorkflowDomain(
205
275
  pi: ExtensionAPI,
206
- wiring: { inflightReporter: InFlightReporter },
276
+ wiring: { inflightReporter?: InFlightReporter } = {},
207
277
  ): WorkflowDomainHandle {
208
- const { inflightReporter } = wiring;
278
+ // [u7a D5] 在途聚合上报接线(自组合根 index.ts 收编):core 状态迁移
279
+ // (spawn/close/arm/disarm)→ 出口回调 → reporter 经 select 通道推绝对计数帧。
280
+ // 出口为进程级单监听(在途记账本身是 pi 进程级模块状态),后注册覆盖先注册
281
+ // (jiti 重载幂等);回调同步 void,不进任何生命周期 await 链(D5 接线约束①)。
282
+ // ctx 由 session_start 注入(factory 阶段无 ui)。wiring.inflightReporter 是测试
283
+ // 注入面(fake reporter 观察 attach/detach 驱动时点);生产路径缺省真实创建——
284
+ // 组合根对实例零知识(纯管道参数已删)。
285
+ const inflightReporter = wiring.inflightReporter ?? createInFlightReporter();
286
+ setInFlightListener(inflightReporter.onInFlightChanged);
209
287
  // [skill-reload D2] handle.state 即槽对象本身(不做解构重包装——否则容器每次
210
- // factory 重跑新建,调用方拿不到「同一 domain state 引用」的接管前提)。
288
+ // factory 重跑新建,调用方拿不到「同一 domain state 引用」的接管前提)。
211
289
  const domainState = getOrCreateWorkflowDomainState();
212
290
  // [skill-reload D3] current pi 登记点:factory 每次重跑(reload 后新 factory 持新
213
291
  // pi)在此覆盖槽上 volatile 绑定——旧 deps 对象的现读成员(见 makeDeps)自动
@@ -233,12 +311,19 @@ export function setupWorkflowDomain(
233
311
  data,
234
312
  });
235
313
  } catch (err) {
236
- void err;
314
+ // appendEntry 失败(stale ctx / session 已关闭等)= workflow:log entry 零痕迹。
315
+ // 独立通道留痕:extension-logger 的写点通道是 `subagents:log`(非
316
+ // `workflow:log`),不重入本函数;其 error() 内部 catch appendEntry 失败并
317
+ // 降级文件日志(TAIJI_AGENT_DEBUG=1 可见),自身不再抛。
318
+ logger.error(
319
+ `[subagent-workflow] workflow:log appendEntry failed (component=${component}): ${message}`,
320
+ { level, reason: toErrorMessage(err) },
321
+ );
237
322
  }
238
323
  }
239
324
 
240
325
  function makeDeps(
241
- state: Pick<SessionLifecycleResult, "store" | "runs" | "sessionDir" | "runner" | "ctx">,
326
+ state: Pick<SessionLifecycleResult, "store" | "runs" | "sessionDir" | "runner">,
242
327
  ) {
243
328
  const deps: LauncherDeps = {
244
329
  store: state.store,
@@ -246,30 +331,29 @@ export function setupWorkflowDomain(
246
331
  runner: state.runner,
247
332
  runs: state.runs,
248
333
  registry,
249
- // [skill-reload D3] 三个 volatile 成员(eventBus / log / onRunDone 的 pi 与
250
- // GuiContext)现读不快照:pi 从槽 currentPi 解析(factory 重跑覆盖登记),ctx
251
- // 从 state.ctx 解析(槽上 sessionState 条目字段,adoption rebind 换新后自动
252
- // 跟进)。在飞 pump 持有的旧 deps 对象经属性访问自动路由到新 pi/ctx。eventBus
334
+ // [skill-reload D3] 三个 volatile 成员(eventBus / log / onRunDone 的 pi)
335
+ // 现读不快照:pi 从槽 currentPi 解析(factory 重跑覆盖登记)。在飞 pump 持有的
336
+ // 旧 deps 对象经属性访问自动路由到新 pi。eventBus
253
337
  // 是值成员必须 getter;log/onRunDone 是函数成员,现读在函数体内达成(函数引用
254
338
  // 稳定,调用方缓存引用也无 stale 面)。
255
339
  //
256
- // onRunDone 是全部 done 路径的单点汇聚(abortRun + error-recovery),顺序固化为
257
- // notify → track → evict:notifyDone 先发完整聚合通知(淘汰后聚合根仍在闭包参数
258
- // run 引用上不受影响),trackNotifiedRunId 有界化去重窗口,最后裁剪 done run 内存。
259
- // 本轮 run 的 completedAt 在 transition("done") 时同步设为当前时刻=全局最新,
260
- // 恒在保留端——结构性保证其不被自身触发的裁剪淘汰,无需 protectRunId。
261
- onRunDone: (run: WorkflowRun) => {
262
- notifyDone(resolveCurrentPi(), run.runId, run, notifiedRunIds, toGuiCtx(state.ctx));
263
- trackNotifiedRunId(notifiedRunIds, run.runId);
264
- const evicted = evictDoneRunsBeyondCap(state.runs, MAX_RETAINED_DONE_RUNS);
265
- if (evicted > 0) {
266
- logger.debug("[subagent-workflow] evicted done runs beyond cap", {
267
- evicted,
268
- keep: MAX_RETAINED_DONE_RUNS,
269
- sessionId: lsRef.lastSessionId,
270
- });
271
- }
272
- },
340
+ // onRunDone = run 终局固定顺序副作用管线(提级为 runSettledEffects 具名
341
+ // 导出,顺序与失败语义的权威注释在该函数;此处纯装配注入依赖)。
342
+ // [W2/V1 D1 第 7 行] settledRecordOf 注入 = store 帧查询(notifyDone 在
343
+ // finalizeRun coda 内于 dispatch 落账之后触发,帧必已落 journal)。
344
+ onRunDone: (run: WorkflowRun) =>
345
+ runSettledEffects(
346
+ {
347
+ resolvePi: resolveCurrentPi,
348
+ notifiedRunIds,
349
+ state,
350
+ // [W2/V1 D1 第 7 行] 可选调用(fake lifecycle result 的 store 缺该面时返回 undefined
351
+ // ——notifyDone 跳过分支承接,见 runSettledEffects 注释)。
352
+ settledRecordOf: (runId) => state.store.settledRecordOf?.(runId),
353
+ lsRef,
354
+ },
355
+ run,
356
+ ),
273
357
  get eventBus() {
274
358
  return resolveCurrentPi().events;
275
359
  },
@@ -284,19 +368,23 @@ export function setupWorkflowDomain(
284
368
  },
285
369
  scheduleTimeBudget: (runId: string, budgetTimeMs: number) =>
286
370
  scheduleTimeBudget(runId, deps, budgetTimeMs),
287
- onWorkflowCall: (name: string, args: Record<string, unknown>, parentRun: WorkflowRun) =>
288
- executeNestedWorkflow(name, args, parentRun, deps),
289
371
  // [H2 W3] workflow agent() 统一派发入口(设计 §3.5):pump 侧 dispatchAgentCall
290
372
  // 经此转调 SubagentService.executeWorkflowAgent——真实 record(origin:"workflow"
291
373
  // + parentRunId)进 store、共享池/守护/journal 归 service 编排;parentRunId 由
292
374
  // pump 补 run.runId。service 单例在 session_start 后必在(run 只能于 session 内
293
375
  // 派发);null 时抛错由 pump 的 dispatchCall catch 兜底回发 failed result。
294
- workflowAgentDispatch: (opts, parentRunId, signal) => {
376
+ workflowAgentDispatch: (opts, parentRunId, signal, stepIndex) => {
295
377
  const service = getSubagentService();
296
378
  if (!service) {
297
- throw new Error("workflow agent dispatch unavailable: subagent service not initialized");
379
+ // [C2] 错误带恢复动作:service 缺席 = session_start 装配链失败(与
380
+ // getWorkflowDeps 的 Session not initialized 同根因),文案同款闭环。
381
+ throw new Error(
382
+ "workflow agent dispatch unavailable: subagent service not initialized " +
383
+ "(session_start assembly failed). Recovery: restart pi or reload this session; " +
384
+ "check the subagents extension logs for the root cause.",
385
+ );
298
386
  }
299
- return service.executeWorkflowAgent(opts, parentRunId, signal);
387
+ return service.executeWorkflowAgent(opts, parentRunId, signal, undefined, undefined, stepIndex);
300
388
  },
301
389
  log,
302
390
  };
@@ -306,26 +394,32 @@ export function setupWorkflowDomain(
306
394
  function isScriptRunning(name: string): boolean {
307
395
  for (const state of sessionState.values()) {
308
396
  for (const run of state.runs.values()) {
309
- if (run.spec.scriptName === name && run.state.status === "running") return true;
397
+ // [W2/V1 D1 第 8 行] 判活换源 runSummary 投影二值(混合判源收拢在 core
398
+ // 投影单点——原两态机 status 读随活体写点删除停更)。
399
+ if (run.spec.scriptName === name && runSummary(run).status === "running") return true;
310
400
  }
311
401
  }
312
402
  return false;
313
403
  }
314
404
 
315
- /** [skill-reload D4] onAdoptionFailed 依赖 setupWorkflowDomain 闭包(makeDeps 的
316
- * LauncherDeps 完整形态——workerHost / onRunDone 通知链经 D3 现读自动路由到新
317
- * pi;sessionState 移除依赖 domain state Map),归 workflow 域、session-lifecycle
318
- * seam 无访问通道,经 SessionLifecycleDeps 注入。 */
405
+ /** 组合根侧生产 deps 工厂:SessionLifecycleDeps 全部成员有生产默认实现(住
406
+ * session-lifecycle.ts——createOrReuseServices 单例语义 / WorktreeManager 每次
407
+ * 扫描新建 / JsonlRunStore per-session 新建),此处无本地构造可注入;工厂形态
408
+ * 保留为组合根侧注入点(测试或后续演进可在此覆盖)。
409
+ * 测试注入路径:不挂载 index.ts,直接调 setupSessionLifecycle(pi, ctx, fakeDeps)。
410
+ * [skill-reload D4] 唯一本地构造 onAdoptionFailed 依赖 setupWorkflowDomain 闭包
411
+ * (makeDeps 的 LauncherDeps 完整形态——workerHost / onRunDone 通知链经 D3
412
+ * 现读自动路由到新 pi;sessionState 移除依赖 domain state Map),归 workflow 域、
413
+ * session-lifecycle seam 无访问通道,经 SessionLifecycleDeps 注入。 */
319
414
  function makeLifecycleDeps(): SessionLifecycleDeps {
320
415
  return {
321
416
  onAdoptionFailed: async (existing, reason) => {
322
- // notifyDone: true(D4/r4)——session 仍在(reload 是同会话原地重建),run
323
- // 终止对用户必须可见(G3 不静默);terminate 前的 rebind-first 已在
324
- // failAdoption 完成,终态 flush 走新 pi 落权威 JSONL。
417
+ // [D11] 统一中断:run 转 interrupted 暂停态(非终局),用户可见性由 v2
418
+ // 中断条目承载(workflow 列表「已中断(可续跑)」);terminate 前的
419
+ // rebind-first 已在 failAdoption 完成,中断条目落新 pi 权威 JSONL。
325
420
  await terminateRunningRuns(
326
421
  makeDeps(existing),
327
422
  `skill reload adoption failed: ${reason}`,
328
- { notifyDone: true },
329
423
  );
330
424
  sessionState.delete(existing.sessionId);
331
425
  },
@@ -341,59 +435,45 @@ export function setupWorkflowDomain(
341
435
  // 开头一致)+ 装配结果写入 per-session sessionState。
342
436
  // ════════════════════════════════════════════════════════════
343
437
  pi.on("session_start", async (event: SessionStartEvent, ctx: ExtensionContext) => {
344
- lsRef.lastSessionId = ctx.sessionManager.getSessionId();
345
- // [u7a D5] 初始上报(count=当下绝对计数):触发时点 = extension 加载完成 / session
346
- // 就绪(factory 无 ctx/ui,session_start 是最早带 ctx 的钩子——plugin-bridge 同款
347
- // 事实)。fire-and-forget 在 await 装配链之前发起,不阻塞也不被阻塞。
348
- inflightReporter.attachSession(ctx);
349
- // [skill-reload D4] adoption 入参接线:reason 是 handler 独占信息(event 参数),
350
- // existing 是 sessionState(domainState 闭包)里的既有条目——两者都是
351
- // session-lifecycle seam 的 adoption 分流判据,经 SessionStartOptions 传入。
352
- // 非 reload 的 session_start 不取 existing(quit 族 session_shutdown 已删条目,
353
- // 此处恒 undefined;显式不传也防误接管)。
354
- const existing =
355
- event.reason === "reload" ? sessionState.get(ctx.sessionManager.getSessionId()) : undefined;
356
- const result = await setupSessionLifecycle(pi, ctx, makeLifecycleDeps(), {
357
- reason: event.reason,
358
- existing,
359
- });
360
- sessionState.set(result.sessionId, result);
361
- });
362
-
363
- // ════════════════════════════════════════════════════════════
364
- // [U2 P-B4 降级] session_compact:compaction 对 custom entry 保留行为实装未
365
- // 验证——检测 ledger/ack entry 被 compaction 清除时按内存态补写(notify-ledger
366
- // compactionCheck;未清除则 no-op)。内存态在 compaction 后仍活着,作为补写源;
367
- // 重启后的权威仍是两列 entry 差集(内存不承担销账职责)。
368
- // ═══════════════════════════════════════════════════════════
369
- pi.on("session_compact", (_event: SessionCompactEvent, _ctx: ExtensionContext) => {
438
+ // 装配链异常不向 pi 事件分发逃逸(pi 0.84.4 extension handler 未捕获的 rejection
439
+ // 直接炸进程——E1 同机制):围栏 error 留痕(含 sessionId)后保持 handler 不抛。
440
+ // lsRef 在 try 内先行赋值:getSessionId 自身抛错时 catch 里拿到的是上一 session
441
+ // 的 id(留痕仍可检索)。
370
442
  try {
371
- const rewritten = getBoundNotifyLedger()?.compactionCheck() ?? 0;
372
- if (rewritten > 0) {
373
- logger.warn(`[subagents] notify ledger entries lost to compaction; rewrote ${rewritten} from memory`);
374
- }
375
- } catch (err) {
376
- logger.warn("[subagents] notify ledger compactionCheck failed", {
377
- reason: toErrorMessage(err),
443
+ lsRef.lastSessionId = ctx.sessionManager.getSessionId();
444
+ // [u7a D5] 初始上报(count=当下绝对计数):触发时点 = extension 加载完成 / session
445
+ // 就绪(factory 无 ctx/ui,session_start 是最早带 ctx 的钩子——plugin-bridge 同款
446
+ // 事实)。fire-and-forget 在 await 装配链之前发起,不阻塞也不被阻塞。
447
+ inflightReporter.attachSession(ctx);
448
+ // [skill-reload D4] adoption 入参接线:reason 是 handler 独占信息(event 参数),
449
+ // existing 是 sessionState(domainState 闭包)里的既有条目——两者都是
450
+ // session-lifecycle seam 的 adoption 分流判据,经 SessionStartOptions 传入。
451
+ // 非 reload 的 session_start 不取 existing(quit 族 session_shutdown 已删条目,
452
+ // 此处恒 undefined;显式不传也防误接管)。
453
+ const existing =
454
+ event.reason === "reload" ? sessionState.get(ctx.sessionManager.getSessionId()) : undefined;
455
+ const result = await setupSessionLifecycle(pi, ctx, makeLifecycleDeps(), {
456
+ reason: event.reason,
457
+ existing,
378
458
  });
459
+ sessionState.set(result.sessionId, result);
460
+ } catch (err) {
461
+ logger.error(
462
+ `[subagent-workflow] session_start handler failed (sessionId=${lsRef.lastSessionId})`,
463
+ { reason: toErrorMessage(err) },
464
+ );
379
465
  }
380
466
  });
381
467
 
382
468
  // ════════════════════════════════════════════════════════════
383
- // model_select:用户切换 model 时刷新缓存
469
+ // [U2 P-B4 降级] notify ledger compaction 补写守卫(notify 域)
384
470
  // ════════════════════════════════════════════════════════════
385
- pi.on("model_select", (event, ctx: ExtensionContext) => {
386
- const service = getModelConfigService();
387
- if (service && typeof service.setCtxModel === "function") {
388
- service.setCtxModel(event.model);
389
- }
390
- // H1: 同步刷新所有 session 的 SAR ctxModel(旧实现只在 session_start 固化)
391
- const sid = ctx.sessionManager.getSessionId();
392
- const state = sessionState.get(sid);
393
- if (state) {
394
- state.runner.updateCtxModel(event.model);
395
- }
396
- });
471
+ setupNotifyLedgerCompactionGuard(pi);
472
+
473
+ // ════════════════════════════════════════════════════════════
474
+ // model 缓存刷新(model 域)
475
+ // ════════════════════════════════════════════════════════════
476
+ setupModelEvents(pi);
397
477
 
398
478
  // ════════════════════════════════════════════════════════════
399
479
  // session_tree:切分支前终止所有 running run(一次性生命周期——切走即作废)
@@ -409,47 +489,17 @@ export function setupWorkflowDomain(
409
489
  try {
410
490
  await terminateRunningRuns(makeDeps(state), "Session switched: run terminated");
411
491
  } catch (err) {
412
- bestEffort(err, "terminateRunningRuns (session_tree handler)");
492
+ // 外层兜底(正常路径 helper 内部已自过滤单 run 失败)——error 级:终态落盘
493
+ // 失败意味着重启后 kill-9 恢复的输入缺失,必须可见。
494
+ bestEffort(err, "terminateRunningRuns (session_tree handler)", "error");
413
495
  }
414
496
  }
415
497
  });
416
498
 
417
499
  // ════════════════════════════════════════════════════════════
418
- // SP-4: session_before_fork(/fork)/ session_before_switch(/new)级联关闭
500
+ // SP-4: 父级联关闭(subagents 域)——/fork 与 /new 的级联 record 清理
419
501
  // ════════════════════════════════════════════════════════════
420
- // 主 session /fork 或 /new 时,清理旧 record(disposeAllRecords:CAS 转终态 +
421
- // archive + worktree 清理)。before 事件在 session 替换前触发,确保旧 session 的
422
- // subagent 在新 session 创建前被清理(随后的 session_shutdown → dispose 收割子进程)。
423
- //
424
- // [M2 修复] 旧实现把 /new 级联挂在 session_before_tree 上——SDK 中该事件只由
425
- // AgentSession.navigateTree()(/tree 同 session 分支切换)触发,/new 走
426
- // session_before_switch(reason:"new") + session_shutdown(reason:"new"),从不触发
427
- // before_tree。后果双向:/new 级联是死代码;普通 /tree 分支导航反而误杀全部活跃
428
- // subagent。现 /new 改挂 session_before_switch(reason==="new"),before_tree handler
429
- // 移除(/tree 是同 session 内导航,record/子进程归属不变,无级联关闭诉求)。
430
- pi.on("session_before_fork", (_event, _ctx) => {
431
- const service = getSubagentService();
432
- if (service) {
433
- const count = service.onParentFork();
434
- if (count > 0) {
435
- logger.warn(`[subagents] /fork 级联关闭 ${count} 个 subagent`);
436
- }
437
- }
438
- });
439
-
440
- pi.on("session_before_switch", (event, _ctx) => {
441
- // /new(reason:"new")创建全新 session → 级联关闭旧 record。
442
- // reason:"resume"(/resume /import 回到已有 session)不级联:record 按 rootSessionId
443
- // 归属隔离,跨 session 读写由 store 过滤守卫,无需销毁。
444
- if (event.reason !== "new") return;
445
- const service = getSubagentService();
446
- if (service) {
447
- const count = service.onParentNew();
448
- if (count > 0) {
449
- logger.warn(`[subagents] /new 级联关闭 ${count} 个 subagent`);
450
- }
451
- }
452
- });
502
+ setupSubagentsCascadeEvents(pi);
453
503
 
454
504
  // ════════════════════════════════════════════════════════════
455
505
  // session_shutdown:dispose subagents + terminate workflows + store 收尾 + cleanup
@@ -463,14 +513,15 @@ export function setupWorkflowDomain(
463
513
  // stale ctx 反复 attempt;新 factory 建新 reporter 并覆盖进程级单监听)。存活的
464
514
  // 在飞 run / store 经 D2 槽由 post-reload session_start(reason=reload) 的
465
515
  // adoption 接管(D4,session-lifecycle.ts)。quit/new/resume/fork = 会话真离开,
466
- // 六动作现状全保持;session_tree / session_before_switch 的 terminate 路径不受
467
- // 影响(各自独立 handler,真语义)。
516
+ // 六动作现状全保持;session_tree 与 /new 父级联(subagents-events.ts)的
517
+ // terminate 路径不受影响(各自独立 handler,真语义)。
468
518
  //
469
519
  // store 收尾:每 session 的 JsonlRunStore 在 terminateRunningRuns 之后 dispose(刷
470
520
  // pending 去抖批 + await in-flight 链,见 W2C5)。R3 声明:SIGTERM/SIGINT 走
471
521
  // 组合根 process handler 不触发本路径,pending 去抖丢失等价崩溃链(重启后 kill-9
472
- // 恢复收编 running 残留——终态/创建均冷路径已落盘,丢的只有 ≤saveDebounceMs 的
473
- // running 尾巴,ES1 已接受);不做 best-effort SIGTERM dispose(需同步 IO 改造,
522
+ // 恢复收编 running 残留——终态/创建均冷路径已落盘,丢的只有最后一次成功物化之后
523
+ // 的 running 尾巴(边沿防抖窗口内有批未物化时同属此列),ES1 已接受);不做
524
+ // best-effort SIGTERM dispose(需同步 IO 改造,
474
525
  // 超出 wave 边界)。
475
526
  // ════════════════════════════════════════════════════════════
476
527
  pi.on("session_shutdown", async (event: SessionShutdownEvent, _ctx: ExtensionContext) => {
@@ -498,7 +549,17 @@ export function setupWorkflowDomain(
498
549
  }
499
550
 
500
551
  // ── subagents 域:dispose SubagentService ──
501
- getSubagentService()?.dispose();
552
+ // dispose 是多步同步链,任一步同步抛错会跳过后续全部清理(terminateRunningRuns /
553
+ // store.dispose / dialogQueue.rejectAll)——围栏与下方 store.dispose 的防御同款:
554
+ // error 留痕(含 sessionId)后继续后续清理,不向 pi 事件分发逃逸。
555
+ try {
556
+ getSubagentService()?.dispose();
557
+ } catch (err) {
558
+ logger.error(
559
+ `[subagent-workflow] session_shutdown SubagentService.dispose failed (sessionId=${lsRef.lastSessionId})`,
560
+ { reason: toErrorMessage(err) },
561
+ );
562
+ }
502
563
 
503
564
  // [u7a D5] 在途上报通道随 session 终结:摘 ctx + 停重试(session 已死,重试直至
504
565
  // 成功的语义只对活 session 成立;进程级出口监听保留——后续 /new 重新 attach)。
@@ -508,7 +569,8 @@ export function setupWorkflowDomain(
508
569
  // H-5: 遍历所有 sessionState 条目清理(而不只 lastSessionId——
509
570
  // 防御 session 切换但 session_tree 未先触发导致 lastSessionId 指向已删除 session 的情况)。
510
571
  for (const [sessionId, state] of sessionState) {
511
- // 编排顺序(W2C5):terminate(await,failed 落盘——重启后 kill-9 恢复不误判)
572
+ // 编排顺序(W2C5):terminate(await,[D11] 中断转移落 record——run 停
573
+ // interrupted 暂停态可再 resume)
512
574
  // → store.dispose(await,刷 pending 去抖批 + await in-flight 链,关「shutdown
513
575
  // 时刻 pending 去抖写丢失」窗口)→ delete。terminate 的 running 过滤在 helper
514
576
  // 内部(单 run 失败不中断其余);外层 try/catch 兜底防单 session 异常中断后续
@@ -516,13 +578,15 @@ export function setupWorkflowDomain(
516
578
  try {
517
579
  await terminateRunningRuns(makeDeps(state), "Session shutdown: run terminated");
518
580
  } catch (err) {
519
- bestEffort(err, "terminateRunningRuns (session_shutdown handler)");
581
+ // 外层兜底(正常路径 helper 内部已自过滤单 run 失败)——error 级:中断转移
582
+ // 落账失败意味着重启后收编链的输入缺失(该 run 仍呈 running 活体投影),必须可见。
583
+ bestEffort(err, "terminateRunningRuns (session_shutdown handler)", "error");
520
584
  }
521
585
  // dispose 自身恒 resolve,catch 兜底防御——handler 内抛错会中断后续 session
522
- // 条目清理。不留静默吞错(错误必须可操作):debug 留痕带 sessionId/sessionDir,
586
+ // 条目清理。不留静默吞错(错误必须可操作):warn 留痕带 sessionId/sessionDir,
523
587
  // 排查「shutdown 后 run 状态不落盘」类问题时有迹可循。
524
588
  await state.store.dispose().catch((err: unknown) => {
525
- logger.debug(
589
+ logger.warn(
526
590
  `[subagent-workflow] session_shutdown store.dispose failed (sessionId=${sessionId}, sessionDir=${state.sessionDir})`,
527
591
  { reason: toErrorMessage(err) },
528
592
  );
@@ -543,27 +607,44 @@ export function setupWorkflowDomain(
543
607
  dialogQueue.rejectAll();
544
608
  });
545
609
 
546
- const getWorkflowDeps = (sessionId: string): WorkflowDepsResolution => {
610
+ /** deps 守卫单一出口:state 缺失 / store 不健康时 throw(错误消息逐字保留——
611
+ * crash-recovery / session-lifecycle 等测试锁定子串),成功返回 LauncherDeps。 */
612
+ const resolveDepsFor = (sessionId: string): LauncherDeps => {
547
613
  const state = sessionState.get(sessionId);
548
614
  if (!state) {
549
- return { ok: false, reason: "Session not initialized" };
615
+ // [C2] 错误带恢复动作(对齐下方 store unavailable 的 MF-1 闭环风格):state
616
+ // 缺席的 root cause 是 session_start 装配链失败(围栏 catch 只留 extension
617
+ // 日志),文案指引 reload + 查日志,接通「现象 → 根因」链路。前缀子串
618
+ // "Session not initialized" 被 session-lifecycle / workflow-events-deps-getter
619
+ // 测试锁定(toThrowError 子串匹配),改写时保留该前缀。
620
+ throw new Error(
621
+ "Session not initialized (session_start assembly failed). " +
622
+ "Recovery: restart pi or reload this session to re-run initialization; " +
623
+ "check the subagents extension logs (session_start failure) for the root cause.",
624
+ );
550
625
  }
551
626
  // MF-1: store 不健康时 fail-fast,避免 store.save 再次失败导致 run 状态不落地。
552
627
  if (!state.storeHealthy) {
553
- return { ok: false, reason: "Workflow store unavailable (loadAll failed in session_start)" };
628
+ // 错误带恢复动作(错误 → 恢复闭环):loadAll 失败的 store 本进程内不恢复,
629
+ // 重启 pi 或重载 session(重建 store + 重跑 kill-9 恢复)是唯一出路。前半段
630
+ // 子串("store unavailable" / "loadAll failed")被 crash-recovery 等测试锁定。
631
+ throw new Error(
632
+ "Workflow store unavailable (loadAll failed in session_start). " +
633
+ "Restart pi or reload this session to re-run crash recovery.",
634
+ );
554
635
  }
555
- return { ok: true, deps: makeDeps(state) };
636
+ return makeDeps(state);
556
637
  };
557
638
 
558
639
  // ════════════════════════════════════════════════════════════
559
- // lazyDeps(3 个 tool + workflows command 的 lazy deps 注入源)
640
+ // lazyDeps(2 个 tool + workflows command 的 lazy deps 注入源;
641
+ // workflow-script tool 走 state.registry 直供不经 lazyDeps)
560
642
  //
561
- // 属性访问触发 getWorkflowDeps 守卫 + makeDeps 求值(每属性独立,createLazy
562
- // 原语转发)。守卫合一后 getWorkflowDeps 返回 discriminated union,getter 内
563
- // 消费时 throw(与 __workflowRun 的 return 错误对象对齐——同源同消息,
564
- // session-lifecycle.test.ts 锁定)。
643
+ // 属性访问触发 deps 守卫 + makeDeps 求值(每属性独立,createLazy 原语转发;
644
+ // 守卫失败 throw——pi tool 框架转译为 tool 错误结果,session-lifecycle.test.ts
645
+ // 锁定消息子串)。
565
646
  // ════════════════════════════════════════════════════════════
566
- const resolveDeps = (): WorkflowDepsResolution => getWorkflowDeps(lsRef.lastSessionId);
647
+ const resolveDeps = (): LauncherDeps => resolveDepsFor(lsRef.lastSessionId);
567
648
  const lazyDeps: LauncherDeps = {
568
649
  get store() { return createLazy(resolveDeps, "store"); },
569
650
  get runs() { return createLazy(resolveDeps, "runs"); },
@@ -572,17 +653,21 @@ export function setupWorkflowDomain(
572
653
  get eventBus() { return createLazy(resolveDeps, "eventBus"); },
573
654
  get workerHost() { return createLazy(resolveDeps, "workerHost"); },
574
655
  get runner() { return createLazy(resolveDeps, "runner"); },
575
- // scheduleTimeBudget / onWorkflowCall / appendEntry 不可缺席(ports.ts D-12
576
- // regression fix 同族):rebuildRuntime 重排 run 级墙钟预算计时器、worker 脚本
577
- // 嵌套 workflow() 调用、finalizeRun 的 pending:unregister 直落都经这三个成员
578
- // 消费——lazyDeps 缺席会让消费点拿到 undefined(可选属性静默放行);appendEntry
579
- // 缺席尤其危险:workflow tool 的 run action 以 lazyDeps 为 deps 启动 run,直落
580
- // 静默跳过 + emit 已删 = 注销 entry 永缺位。转发形态与其余成员一致。
656
+ // [A1 修复循环 R3] workflowAgentDispatch 必须随 lazyDeps 转发:workflow tool 的
657
+ // run action 以 lazyDeps 为 deps 启动 run,漏本成员则 pump dispatchAgentCall 读到
658
+ // undefined 回退 deps.runner(SAR.run 占位 runId)→ record.parentRunId =
659
+ // "sar-unattached" → armed 回执落账键错 → fold 出 created 态 → IllegalTransitionError
660
+ // 让位,run journal 恒缺 armed 帧(真机 wf-1790034646281-w7tp4f 实证链,R2 裁决)。
661
+ get workflowAgentDispatch() { return createLazy(resolveDeps, "workflowAgentDispatch"); },
662
+ // scheduleTimeBudget / appendEntry 不可缺席(ports.ts D-12 regression fix 同族):
663
+ // rebuildRuntime 重排 run 级墙钟预算计时器、finalizeRun 的 pending:unregister
664
+ // 直落都经这两个成员消费——lazyDeps 缺席会让消费点拿到 undefined(可选属性
665
+ // 静默放行);appendEntry 缺席尤其危险:workflow tool 的 run action 以 lazyDeps
666
+ // 为 deps 启动 run,直落静默跳过 + emit 已删 = 注销 entry 永缺位。转发形态与其余成员一致。
581
667
  get scheduleTimeBudget() { return createLazy(resolveDeps, "scheduleTimeBudget"); },
582
- get onWorkflowCall() { return createLazy(resolveDeps, "onWorkflowCall"); },
583
668
  get appendEntry() { return createLazy(resolveDeps, "appendEntry"); },
584
669
  get log() { return createLazy(resolveDeps, "log"); },
585
670
  };
586
671
 
587
- return { state: domainState, lazyDeps, getWorkflowDeps, isScriptRunning };
672
+ return { state: domainState, lazyDeps, isScriptRunning };
588
673
  }