@zhushanwen/pi-subagent-workflow 8.7.0 → 8.8.1

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 (56) hide show
  1. package/README.md +5 -11
  2. package/package.json +8 -11
  3. package/skills/workflow-script-format/SKILL.md +32 -13
  4. package/src/host/__tests__/pi-host.test.ts +23 -2
  5. package/src/host/pi-host.ts +61 -6
  6. package/src/index.ts +155 -528
  7. package/src/injectors/__tests__/engine-awareness.test.ts +5 -3
  8. package/src/injectors/__tests__/engine-section-stability.test.ts +48 -33
  9. package/src/injectors/__tests__/model-list-injector.test.ts +18 -21
  10. package/src/injectors/__tests__/subagent-list-injector.test.ts +54 -14
  11. package/src/injectors/__tests__/workflow-list-injector.test.ts +26 -12
  12. package/src/injectors/engine-awareness.ts +80 -8
  13. package/src/injectors/model-list-injector.ts +15 -58
  14. package/src/injectors/resource-list-injector.ts +209 -0
  15. package/src/injectors/subagent-list-injector.ts +53 -224
  16. package/src/injectors/workflow-list-injector.ts +36 -196
  17. package/src/interface/__tests__/detectors.test.ts +46 -35
  18. package/src/interface/__tests__/subagent-tool-path-guard.test.ts +42 -26
  19. package/src/interface/__tests__/subagent-tool-prompt.test.ts +14 -3
  20. package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +216 -0
  21. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +17 -6
  22. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +38 -2
  23. package/src/interface/bg-notify-render.ts +7 -17
  24. package/src/interface/command-actions.ts +5 -15
  25. package/src/interface/commands.ts +116 -76
  26. package/src/interface/format.ts +354 -245
  27. package/src/interface/gui-mappers.ts +18 -22
  28. package/src/interface/helpers.ts +9 -130
  29. package/src/interface/list-component.ts +164 -117
  30. package/src/interface/list-shared.ts +3 -3
  31. package/src/interface/list-view.ts +130 -73
  32. package/src/interface/subagent-actions.ts +50 -675
  33. package/src/interface/subagent-tool-schema.ts +7 -9
  34. package/src/interface/subagent-tool.ts +5 -4
  35. package/src/interface/subagents.ts +9 -8
  36. package/src/interface/tool-render.ts +5 -15
  37. package/src/interface/tool-workflow-script.ts +34 -75
  38. package/src/interface/tool-workflow.ts +62 -96
  39. package/src/interface/tui-kit.ts +305 -0
  40. package/src/interface/views/WorkflowsView.ts +270 -207
  41. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +3 -3
  42. package/src/interface/views/__tests__/detail-content-session-file.test.ts +7 -4
  43. package/src/interface/views/detail-content.ts +16 -14
  44. package/src/interface/views/view-constants.ts +19 -0
  45. package/src/jsonl-run-store.ts +144 -228
  46. package/agents/analyst.md +0 -61
  47. package/agents/coder.md +0 -70
  48. package/agents/debugger.md +0 -67
  49. package/agents/doc-reviewer.md +0 -50
  50. package/agents/explorer.md +0 -64
  51. package/agents/general-purpose.md +0 -32
  52. package/agents/orchestrator.md +0 -63
  53. package/agents/planner.md +0 -54
  54. package/agents/researcher.md +0 -65
  55. package/agents/reviewer.md +0 -74
  56. package/src/interface/views/format.ts +0 -320
package/src/index.ts CHANGED
@@ -13,54 +13,30 @@
13
13
  * 设计基线:D-004(旧包不动)/ ADR-025(进程内执行)/ D-8(pi.__workflowRun 签名)。
14
14
  */
15
15
 
16
- import * as fs from "node:fs";
17
- import * as path from "node:path";
18
-
19
- import type { BeforeAgentStartEvent, ExtensionAPI, ExtensionContext, SessionCompactEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent } from "@earendil-works/pi-coding-agent";
16
+ import type { ExtensionAPI, ExtensionContext, SessionCompactEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent } from "@earendil-works/pi-coding-agent";
20
17
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
21
18
  import { getLogger, setPiHandle } from "@zhushanwen/pi-extension-logger";
22
19
 
23
20
  // ═══ core 宿主端口接线(subagent-core 包抽离 u0-wire;实现见 src/host/pi-host.ts) ═══
24
- import { configureCore } from "@zhushanwen/subagent-core/core/host-services.ts";
25
- import { configureNotifyDomain } from "@zhushanwen/subagent-core/core/notify-ports.ts";
21
+ import { configureCore } from "@zhushanwen/subagent-core";
22
+ import { configureNotifyDomain } from "@zhushanwen/subagent-core";
26
23
  import { createPiHostServices, createPiNotifyDomainPorts } from "./host/pi-host.ts";
27
24
 
28
- import { bestEffort } from "@zhushanwen/subagent-core/execution/best-effort.ts";
25
+ import { bestEffort } from "@zhushanwen/subagent-core";
29
26
  // ═══ execution/ 层(subagents 核心 + 运行时) ═══
30
- import { getOrCreateChannelRegistry } from "@zhushanwen/subagent-core/execution/channel-registry-access.ts";
31
- import { DialogGlobalQueue } from "@zhushanwen/subagent-core/execution/dialog-queue.ts";
32
- // [engine-awareness U3] 全局 config 三态读取(检测 poll 与 session_start 基线共用)
33
- import { readGlobalConfig } from "@zhushanwen/subagent-core/execution/config.ts";
34
27
  // [U7] 引擎列表状态文件(registry → engines.json,GUI 引擎选择器数据源)
35
- import { syncEnginesFile } from "@zhushanwen/subagent-core/execution/engine/engine-discovery.ts";
36
- // [U7] 引擎模型段注入(defaultEngine 非 pi 时 system prompt 补 <available_<engine>_models>)
37
- // [engine-awareness U3] 补恒在状态段 <current_subagent_engine>(D6,pi 引擎也声明)
38
- import { buildEngineModelsPromptAppend, buildSubagentEngineSection } from "@zhushanwen/subagent-core/execution/engine/model-prompt.ts";
28
+ import { syncEnginesFile } from "@zhushanwen/subagent-core";
39
29
  // [P1 引擎接线] 组合根登记 'pi' 引擎进 registry(引擎获取统一经 getEngine,缺省 id 'pi')
40
- import { registerPiEngine } from "@zhushanwen/subagent-core/execution/engine/engines/pi/registration.ts";
30
+ import { registerPiEngine } from "@zhushanwen/subagent-core";
41
31
  // [P3 引擎接线] 组合根登记 'zcode' 引擎(spawn 单轮模式;engineDataDir 默认走
42
32
  // common/data-dir SSOT,见 engines/zcode/registration.ts)
43
- import { registerZcodeEngine } from "@zhushanwen/subagent-core/execution/engine/engines/zcode/registration.ts";
44
- import { createUiRequestHandlerForMode } from "@zhushanwen/subagent-core/execution/ui-request-handler-factory.ts";
45
- import {
46
- getModelConfigService,
47
- ModelConfigService,
48
- setModelConfigService,
49
- } from "@zhushanwen/subagent-core/execution/model-config-service.ts";
50
- import { bindNotifyLedgerHost, getBoundNotifyLedger, type NotifyLedgerHost } from "@zhushanwen/subagent-core/execution/notify-ledger.ts";
51
- import { IDENTITY_CUSTOM_TYPE, type SubagentIdentityData } from "@zhushanwen/subagent-core/execution/session-reconstructor.ts";
52
- import type { ExecutionMode, SubagentRecord } from "@zhushanwen/subagent-core/execution/types.ts";
53
- import { maybeCleanupExpiredSessionFiles } from "@zhushanwen/subagent-core/execution/session-file-gc.ts";
54
- import {
55
- getSubagentService,
56
- setSubagentService,
57
- SubagentService,
58
- } from "@zhushanwen/subagent-core/execution/subagent-service.ts";
59
- import { killAllSpawnedChildren } from "@zhushanwen/subagent-core/execution/session-runner.ts";
60
- import { SubprocessAgentRunner } from "@zhushanwen/subagent-core/execution/subprocess-agent-runner.ts";
61
- import { WorktreeManager } from "@zhushanwen/subagent-core/execution/worktree-manager.ts";
62
- // [engine-awareness U3] per-turn 引擎检测编排(D1/D1b/D2/D3/D5)
63
- import { normalizeEngineId, runEngineAwarenessTurn } from "./injectors/engine-awareness.ts";
33
+ import { registerZcodeEngine } from "@zhushanwen/subagent-core";
34
+ import { getModelConfigService } from "@zhushanwen/subagent-core";
35
+ import { getBoundNotifyLedger } from "@zhushanwen/subagent-core";
36
+ import { getSubagentService } from "@zhushanwen/subagent-core";
37
+ import { killAllSpawnedChildren } from "@zhushanwen/subagent-core";
38
+ // [engine-awareness U3/D7-④] per-turn 引擎检测编排 + before_agent_start 链尾接线
39
+ import { setupEngineAwarenessInjector } from "./injectors/engine-awareness.ts";
64
40
  import { setupModelListInjector } from "./injectors/model-list-injector.ts";
65
41
  import { setupSubagentListInjector } from "./injectors/subagent-list-injector.ts";
66
42
  import { setupWorkflowListInjector } from "./injectors/workflow-list-injector.ts";
@@ -73,20 +49,26 @@ import { registerSubagentTool } from "./interface/subagent-tool.ts";
73
49
  import { registerSubagentsCommand } from "./interface/subagents.ts";
74
50
  import { registerWorkflowTool } from "./interface/tool-workflow.ts";
75
51
  import { registerWorkflowScriptTool } from "./interface/tool-workflow-script.ts";
76
- import { JsonlRunStore } from "./jsonl-run-store.ts";
77
- import { clearSkillPathCache } from "@zhushanwen/subagent-core/orchestration/skill-discovery.ts";
78
52
  // ═══ orchestration/ 层(workflow engine + infra) ═══
79
- import type { LauncherDeps } from "@zhushanwen/subagent-core/orchestration/launcher.ts";
80
- import { executeNestedWorkflow, runAndWait, type WorkflowRunResult } from "@zhushanwen/subagent-core/orchestration/launcher.ts";
53
+ import type { LauncherDeps } from "@zhushanwen/subagent-core";
54
+ import { executeNestedWorkflow, runAndWait, type WorkflowRunResult } from "@zhushanwen/subagent-core";
81
55
  import {
82
56
  evictDoneRunsBeyondCap,
83
57
  MAX_RETAINED_DONE_RUNS,
84
58
  scheduleTimeBudget,
85
59
  terminateRunningRuns,
86
- } from "@zhushanwen/subagent-core/orchestration/lifecycle.ts";
87
- import type { WorkflowRun } from "@zhushanwen/subagent-core/orchestration/models/workflow-run.ts";
88
- import { WorkerHostImpl } from "@zhushanwen/subagent-core/orchestration/worker-host.ts";
89
- import { WorkflowScriptRegistryImpl } from "@zhushanwen/subagent-core/orchestration/workflow-script-registry-impl.ts";
60
+ } from "@zhushanwen/subagent-core";
61
+ import type { WorkflowRun } from "@zhushanwen/subagent-core";
62
+ import { WorkerHostImpl } from "@zhushanwen/subagent-core";
63
+ import { WorkflowScriptRegistryImpl } from "@zhushanwen/subagent-core";
64
+ // ═══ session 生命周期装配 seam(bootstrap seam,设计 §3.1/D1) ═══
65
+ import {
66
+ getOrCreateDialogQueue,
67
+ setupSessionLifecycle,
68
+ type SessionLifecycleDeps,
69
+ type SessionLifecycleResult,
70
+ } from "./session-lifecycle.ts";
71
+ import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
90
72
 
91
73
  // ── pi.__workflowRun 类型扩展(D-8 签名) ─────────────────
92
74
 
@@ -106,42 +88,6 @@ declare module "@earendil-works/pi-coding-agent" {
106
88
  // 模块级 logger(setPiHandle 注入后自动走 appendEntry)
107
89
  const logger = getLogger("subagents");
108
90
 
109
- // ── subagent 状态快照格式化 ──
110
- //
111
- // [v4 A-6] before_agent_start 注入 hook 已删(活跃 subagent 清单改由 agent 按需调
112
- // action:'list' 拉取,消除每 loop 注入的上下文税与盲点)。本函数保留为纯格式化工具:
113
- // before-agent-start-injection / parent-child-matrix 测试覆盖其正确性,未来 list
114
- // 视图或其他注入点可复用。
115
-
116
- /** 活跃 subagent 数量上限(超过截断显示)。 */
117
- const MAX_STATUS_INJECTION = 10;
118
-
119
- /**
120
- * 将活跃 subagent record 格式化为一行一条的快照文本。
121
- *
122
- * 格式:
123
- * [subagent-status] N active subagents:
124
- * - sa-xxx (slug): running, rounds 0
125
- * - sa-yyy (slug): idle, rounds 3
126
- * +2 more, use action:'list'
127
- *
128
- * @param records 已筛选的活跃 record(running + idle)
129
- */
130
- export function formatSubagentStatusSnapshot(records: SubagentRecord[]): string {
131
- const lines = [`[subagent-status] ${records.length} active subagent${records.length === 1 ? "" : "s"}:`];
132
- const shown = records.slice(0, MAX_STATUS_INJECTION);
133
- for (const r of shown) {
134
- const slug = r.slug || r.agent;
135
- const roundPart = r.round !== undefined && r.round > 0 ? `, rounds ${r.round}` : "";
136
- lines.push(`- ${r.id} (${slug}): ${r.status}${roundPart}`);
137
- }
138
- const remaining = records.length - MAX_STATUS_INJECTION;
139
- if (remaining > 0) {
140
- lines.push(`+${remaining} more, use action:'list'`);
141
- }
142
- return lines.join("\n");
143
- }
144
-
145
91
  // ═══ [V2 决策 7 防线 i] process 级 shutdown hook ═══
146
92
  //
147
93
  // session_shutdown 是 pi 的 async hook,进程被 SIGTERM/SIGINT 强杀或崩溃时来不及
@@ -160,7 +106,7 @@ function reapSpawnedChildrenOnShutdown(): void {
160
106
  // 不静默吞错,对齐「错误必须可操作」。
161
107
  logger.debug(
162
108
  "[subagents] process shutdown reap best-effort failed (killAllSpawnedChildren SIGTERM)",
163
- { reason: err instanceof Error ? err.message : String(err) },
109
+ { reason: toErrorMessage(err) },
164
110
  );
165
111
  }
166
112
  }
@@ -224,36 +170,14 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
224
170
  setupWorkflowListInjector(pi);
225
171
  setupModelListInjector(pi);
226
172
 
227
- // 模块级缓存:主 session 的 sessionFile(fork source 解析用)。
228
- let cachedMainSessionFile: string | undefined;
229
- function getCachedMainSessionFile(): string | undefined {
230
- return cachedMainSessionFile;
231
- }
232
-
233
- /**
234
- * 按 sessionId 解析主 session 文件路径(文件名约定 `<ISO 时间戳>_<sessionId>.jsonl`)。
235
- * [E2E 实测] attach 场景下 ctx.sessionManager.getSessionFile() 会返回前一 session 的
236
- * 文件(session_start(root=01a01bf5) 时仍返回刚新建 session 的路径)——恢复逻辑
237
- * 读错文件会整段漏判。此处按 id 从 sessions 目录解析为准;新 session 文件未 flush
238
- * 时(AGENTS.md 规则 6:首条 assistant 消息前可能不存在)返回 undefined,调用方
239
- * (fork 解析 / 孤儿恢复)对该场景本就无 entry 可读。
240
- */
241
- function resolveMainSessionFileById(sessionId: string): string | undefined {
242
- const sessionsDir = path.join(getAgentDir(), "..", "sessions");
243
- try {
244
- const match = fs.readdirSync(sessionsDir).find((f) => f.endsWith(`_${sessionId}.jsonl`));
245
- return match === undefined ? undefined : path.join(sessionsDir, match);
246
- } catch {
247
- return undefined;
248
- }
249
- }
173
+ // [主 session 文件解析随迁] cachedMainSessionFile 缓存与 resolveMainSessionFileById
174
+ // 随 session_start 装配块整体迁入 session-lifecycle.ts(唯一消费方是随迁块;
175
+ // SubagentService 的 getMainSessionFile getter 由 createOrReuseServices 提供)。
250
176
 
251
- // resources_discover:不再注入额外 skill 目录(ADR-031 废弃 discovery.json)。
252
- // pi 核心 auto-discovery 已覆盖 .agents/skills 等标准目录,子 session 的
253
- // --skill 由 agent({skill}) 调用方显式传入,无需 extension 额外补充。
254
- pi.on("resources_discover", (_event, _ctx: ExtensionContext) => {
255
- return {};
256
- });
177
+ // resources_discover:不再注册 handler(v4 决策:不再注入额外 skill 目录,
178
+ // ADR-031 废弃 discovery.json)。pi 核心 auto-discovery 已覆盖 .agents/skills
179
+ // 等标准目录,子 session 的 --skill 由 agent({skill}) 调用方显式传入,无需
180
+ // extension 额外补充。
257
181
 
258
182
  // ════════════════════════════════════════════════════════════
259
183
  // workflow 域:tools + command + pi.__workflowRun + state
@@ -266,31 +190,13 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
266
190
  const workerHost = new WorkerHostImpl();
267
191
  const registry = new WorkflowScriptRegistryImpl();
268
192
 
269
- // SAR 改为 per-session 构造(需要 ctxModel 填底 D-008 + subagentService 委托目标)
270
- // old: const runner = new SubprocessAgentRunner();
271
- // new: per-session session_start 时创建,见下方 makeDeps 前的 runner 创建
272
-
273
- // per-session 状态(session_start 时重建)
274
- const sessionState = new Map<
275
- string,
276
- {
277
- store: JsonlRunStore;
278
- runs: Map<string, WorkflowRun>;
279
- sessionDir: string;
280
- /** D-008 per-session SAR(需要 ctxModel + subagentService) */
281
- runner: SubprocessAgentRunner;
282
- /** session 上下文(notifyDone 需要 GuiContext) */
283
- ctx?: ExtensionContext;
284
- /** MF-1: store 健康度。session_start 时 store.loadAll 失败则置 false,
285
- * workflow 域启动时 fail-fast,避免后续 store.save 再次失败导致 run 状态不落地。
286
- * subagent 域不依赖 store,不受此标志影响。 */
287
- storeHealthy: boolean;
288
- /** [engine-awareness D1b] 上一次已知默认引擎(session_start 初始化,per-turn 检测
289
- * diff 基准)。undefined = 初始化时 config 读失败——首 turn 检测遇 undefined
290
- * 静默基线化,不算变更、不发通知(防首 turn 伪通知)。 */
291
- lastEngine?: string;
292
- }
293
- >();
193
+ // SAR 改为 per-session 构造(需要 ctxModel 填底 D-008 + subagentService 委托目标),
194
+ // 构造点随 session_start 装配块迁入 session-lifecycle.ts,经 SessionLifecycleResult
195
+ // 传回(见 setupSessionLifecycle)。
196
+
197
+ // per-session 状态(session_start 时重建)。value = SessionLifecycleResult
198
+ // (setupSessionLifecycle 装配结果,ctx 必有)。
199
+ const sessionState = new Map<string, SessionLifecycleResult>();
294
200
 
295
201
  function log(
296
202
  level: "debug" | "info" | "warn" | "error",
@@ -311,21 +217,20 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
311
217
  }
312
218
  }
313
219
 
314
- function resolveSessionDir(): string {
315
- const defaultDir = getAgentDir();
316
- const sessionSlug = `--${process.cwd().replace(/^\//, "").replace(/\//g, "-")}--`;
317
- // F2:根改 getAgentDir() 派生(实例隔离);保留 sessionScopedDir 存在则用之的探测语义
318
- const sessionScopedDir = path.join(getAgentDir(), "sessions", sessionSlug);
319
- return fs.existsSync(sessionScopedDir) ? sessionScopedDir : defaultDir;
220
+ // [workflow state 目录解析随迁] resolveSessionDir 随 session_start 装配块整体迁入
221
+ // session-lifecycle.ts(唯一消费方是随迁块)。
222
+
223
+ /** 组合根侧生产 deps 工厂:SessionLifecycleDeps 全部成员有生产默认实现(住
224
+ * session-lifecycle.ts——createOrReuseServices 单例语义 / WorktreeManager 每次
225
+ * 扫描新建 / JsonlRunStore per-session 新建),此处无本地构造可注入;工厂形态
226
+ * 保留为 index 侧注入点(测试或后续演进可在此覆盖)。
227
+ * 测试注入路径:不挂载 index.ts,直接调 setupSessionLifecycle(pi, ctx, fakeDeps)。 */
228
+ function makeLifecycleDeps(): SessionLifecycleDeps {
229
+ return {};
320
230
  }
321
231
 
322
232
  function makeDeps(
323
- state: {
324
- store: JsonlRunStore;
325
- runs: Map<string, WorkflowRun>;
326
- sessionDir: string;
327
- runner: SubprocessAgentRunner;
328
- },
233
+ state: Pick<SessionLifecycleResult, "store" | "runs" | "sessionDir" | "runner">,
329
234
  sessionCtx?: ExtensionContext,
330
235
  ) {
331
236
  const deps: LauncherDeps = {
@@ -373,284 +278,16 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
373
278
 
374
279
  // ════════════════════════════════════════════════════════════
375
280
  // session_start:初始化 subagents + workflow 两域
281
+ //
282
+ // 六职责编排(identity 重建 / ledger 装配 / 双 Service 装配 / GC+恢复 / kill-9
283
+ // 循环 / SAR+engine 基线)已随迁 session-lifecycle.ts(bootstrap seam,设计
284
+ // §3.1/D1/D2 原样搬移)。此处仅接线:lastSessionId 先行赋值(时序与原 handler
285
+ // 开头一致)+ 装配结果写入 per-session sessionState。
376
286
  // ════════════════════════════════════════════════════════════
377
287
  pi.on("session_start", async (_event: SessionStartEvent, ctx: ExtensionContext) => {
378
- const cwd = ctx.cwd;
379
- const agentDir = getAgentDir();
380
- const sessionId = ctx.sessionManager.getSessionId();
381
- lsRef.lastSessionId = sessionId;
382
-
383
- // [U7] 引擎列表同步 engines.json(幂等零写 + fail-safe;组合根注册已在
384
- // extension 工厂体完成,此处 registry 已含全部引擎)
385
- syncEnginesFile(agentDir);
386
-
387
- // skill 路径两级缓存 session 级失效:pi 同进程可能有多个 session(TUI /new、/fork),
388
- // 运行中安装的 skill 需对新 session 可见(含曾 miss 缓存的 undefined 条目与 npm 新装
389
- // 包的候选目录)。session 内复用收益不变(IF8/DM3 消重发生在同 session 的重复调用)。
390
- clearSkillPathCache();
391
-
392
- // ── [M4] identity 子进程写入(V2 决策 5)──
393
- // 子进程经 env(PI_SUBAGENT_*)接收自己的 identity,在 session_start 用 pi.appendEntry
394
- // 写 subagent-identity custom entry。pi 自动生成 id/parentId → message tree 连续。
395
- // 旧实现父进程 fs.appendFileSync 补写的 custom entry 缺 id/parentId → 污染 _buildIndex
396
- // leafId 指针 → message tree 断成两棵 → 多轮对话丢上下文(bug 根因)。
397
- // 主/子进程判定:PI_SUBAGENT_SELF_RECORD_ID 仅 session-runner spawn 子进程时注入,
398
- // 主进程无此 env → 跳过(identity 只在子进程写一次)。
399
- const selfRecordId = process.env.PI_SUBAGENT_SELF_RECORD_ID;
400
- if (selfRecordId) {
401
- try {
402
- const modeEnv = process.env.PI_SUBAGENT_MODE;
403
- // ExecutionMode 联合窄化:父进程经 env 注入(record.mode 恒为 "background"),
404
- // 运行时校验合法值,非法兜底 background(避免裸 cast,符合 taste/no-unsafe-cast)。
405
- const mode: ExecutionMode = modeEnv === "background" ? modeEnv : "background";
406
- const identity: SubagentIdentityData = {
407
- id: selfRecordId,
408
- agent: process.env.PI_SUBAGENT_AGENT ?? "",
409
- mode,
410
- task: process.env.PI_SUBAGENT_TASK ?? "",
411
- slug: process.env.PI_SUBAGENT_SLUG,
412
- startedAt: Number(process.env.PI_SUBAGENT_STARTED_AT ?? Date.now()),
413
- rootSessionId: process.env.PI_SUBAGENT_ROOT_SESSION_ID,
414
- parentRecordId: process.env.PI_SUBAGENT_PARENT_RECORD_ID,
415
- depth:
416
- process.env.PI_SUBAGENT_DEPTH !== undefined
417
- ? Number(process.env.PI_SUBAGENT_DEPTH)
418
- : undefined,
419
- forkDepth:
420
- process.env.PI_SUBAGENT_FORK_DEPTH !== undefined
421
- ? Number(process.env.PI_SUBAGENT_FORK_DEPTH)
422
- : undefined,
423
- chatMode: process.env.PI_SUBAGENT_CHAT_MODE === "true",
424
- // [review round2] worktree 隔离标志(session-runner 注入):跨重启重建路径据此
425
- // 拒绝续聊(handle 不可序列化,reattach 不可行)。
426
- worktree: process.env.PI_SUBAGENT_WORKTREE === "true",
427
- };
428
- pi.appendEntry(IDENTITY_CUSTOM_TYPE, identity);
429
- } catch (err) {
430
- logger.warn("[subagents] identity appendEntry failed in session_start", {
431
- reason: err instanceof Error ? err.message : String(err),
432
- });
433
- }
434
- }
435
-
436
- // ── [U2] 通知账本装配 + 重启恢复(设计 D4:存在性 / 可达性分离)──
437
- // bind 先于 service.initSession(notifier.revive 在其内——notify() 经
438
- // getBoundNotifyLedger 消费账本)。recoverFromSession 扫 ledger/ack 两列 entry
439
- // 差集:未销账号重放投递(已销账零重发,notifyId 幂等);fork 继承未销账
440
- // pending 属可接受语义(D4 归属规则——扫描域 = 单 session 文件,幂等键作用域
441
- // 随文件域隔离)。compaction 存活情况归 session_compact handler 的条件降级(P-B4
442
- // 探针阶段 5 实测,见 notify-ledger.ts compactionCheck)。
443
- try {
444
- const ledgerHost: NotifyLedgerHost = {
445
- appendLedgerEntry: (customType, data) => {
446
- pi.appendEntry(customType, data);
447
- },
448
- readSessionEntries: () => ctx.sessionManager.getEntries(),
449
- isIdle: () => ctx.isIdle(),
450
- onAgentSettled: (handler) => {
451
- pi.on("agent_settled", handler);
452
- },
453
- sendDelivery: (message) => {
454
- // D5 单通道:唯一发送形态 = sendCustomMessage({triggerTurn:true}),
455
- // courier 已在发送前二次复查 isIdle,多通道投递选项已删(D5)。
456
- pi.sendMessage(message, { triggerTurn: true });
457
- },
458
- };
459
- // U4:重放观测已内聚到 ledger 分桶日志(recoveryReplays 桶经 extensionLogger
460
- // 通道落盘),此处不再重复打日志。
461
- bindNotifyLedgerHost(ledgerHost).recoverFromSession();
462
- } catch (err) {
463
- // 账本装配失败不阻断 session_start(通知退回 notifier 的内核路径)
464
- logger.warn("[subagents] notify ledger bind failed", {
465
- reason: err instanceof Error ? err.message : String(err),
466
- });
467
- }
468
-
469
- // ── subagents 域:双 Service 装配 ──
470
- const existingService = getSubagentService();
471
- const existingModelService = getModelConfigService();
472
- const modelService = existingModelService ?? new ModelConfigService({ agentDir, cwd });
473
- const service = existingService ?? new SubagentService({ cwd, modelService, getMainSessionFile: getCachedMainSessionFile });
474
-
475
- modelService.initModel({
476
- modelRegistry: ctx.modelRegistry,
477
- sessionId: ctx.sessionManager.getSessionId(),
478
- ctxModel: ctx.model ?? undefined,
479
- });
480
-
481
- // ── W3: handler 注入链路接通 ──
482
- // 进程级单例:channel registry + dialog queue 跨 session 复用
483
- //(与 SubagentService 单例模式一致,globalThis Symbol 持有避免 jiti 多实例分裂)。
484
- const channelRegistry = getOrCreateChannelRegistry();
485
- const dialogQueue = getOrCreateDialogQueue();
486
- const uiRequestHandler = createUiRequestHandlerForMode(ctx, channelRegistry, dialogQueue);
487
- // SR-3: 无论 new 还是 existing(/resume /fork 复用),session_start 都必须注入 handler
488
- service.setUiRequestHandler(uiRequestHandler);
489
-
490
- // 主 session 文件:按 sessionId 解析(getSessionFile() 在 attach 场景会返回前一
491
- // session 的文件,E2E 实测),未 flush 的新 session 回退 getSessionFile()。
492
- // 值直传 initSession——jiti 多实例分裂下闭包缓存(cachedMainSessionFile)不跨
493
- // 实例共享,恢复逻辑经缓存读会拿到滞后一个事件的值(E2E 实测 ENOENT 漏判);
494
- // 缓存本身保留给既有 getter 消费者(fork source 解析)。
495
- cachedMainSessionFile =
496
- resolveMainSessionFileById(ctx.sessionManager.getSessionId()) ??
497
- ctx.sessionManager.getSessionFile() ??
498
- undefined;
499
-
500
- service.initSession({
501
- pi,
502
- sessionId: ctx.sessionManager.getSessionId(),
503
- mainSessionFile: cachedMainSessionFile,
504
- // 注入 ctx.ui.setWidget 作为 streaming sink(只绑方法,不持有整个 ctx)。
505
- // background subagent 执行期间,text_delta 经 SubagentStream 合并后由此通道转发。
506
- // [W1 修复] ctx.mode === 'rpc' 守卫:TUI/json/print 下 streamSink = undefined(无 widget 噪音),
507
- // rpc mode(GUI/xyz-agent)下保持原行为(ctx.ui.setWidget → sidecar → chatStore)。
508
- // streamSink API 不变(SubagentStream.onDelta 仍可调,只是 TUI 下 stream 不会被创建)。
509
- streamSink: ctx.mode === "rpc"
510
- ? { setWidget: (key, lines) => ctx.ui.setWidget(key, lines) }
511
- : undefined,
512
- // [#24] uiRequestHandler 单一注入入口:上方 setUiRequestHandler 已注入(SR-3 语义,
513
- // new/existing service 均覆盖)。此处不再重复传 initSession.uiRequestHandler,避免
514
- // 同一 handler 双路径注入造成的语义混淆与“哪一个是 source of truth”歧义。
515
- // mode 仍需 session 级注入(uiObservability.setMode 依赖它,与 handler 无关)。
516
- mode: ctx.mode,
517
- // SR-4:注入 L2 dialog 队列——session-runner child close 时调 rejectChildDialogs
518
- // 清理该 child 在 L2 的 pending dialog,防全局死锁(C1 修复:清理路径接通)。
519
- dialogQueue,
520
- // [竞态修复] 注入 ctx.isIdle:notifier flush 在主 agent busy 时退避,idle 后再
521
- // sendMessage(triggerTurn),规避 agent_end→finishRun 窗口里走 steer 分支丢失通知。
522
- isIdle: () => ctx.isIdle(),
523
- });
524
-
525
- if (!existingService) {
526
- setModelConfigService(modelService);
527
- setSubagentService(service);
528
- }
529
-
530
- // S-2: 启动 idle record GC 定时器(30 天 TTL,每小时检查一次)
531
- service.startGcTimer();
532
-
533
- try {
534
- maybeCleanupExpiredSessionFiles(agentDir, cwd);
535
- } catch (err) {
536
- logger.warn("[subagents] expired session file cleanup failed", {
537
- reason: err instanceof Error ? err.message : String(err),
538
- });
539
- }
540
-
541
- // ADR-035 启动恢复:扫描 manifest tmp 残留(崩溃打断的 writeManifest 留下),
542
- // 每次 session_start 都调(与上方 maybeCleanupExpiredSessionFiles 一致)。
543
- try {
544
- const recovered = await service.recoverManifestTmpFiles();
545
- if (recovered.recovered > 0 || recovered.deleted > 0) {
546
- logger.warn(`[subagents] manifest tmp recovery: ${recovered.recovered} promoted, ${recovered.deleted} deleted`);
547
- }
548
- } catch (err) {
549
- logger.warn("[subagents] manifest tmp recovery failed", {
550
- reason: err instanceof Error ? err.message : String(err),
551
- });
552
- }
553
-
554
- try {
555
- const wtm = new WorktreeManager(agentDir);
556
- await wtm.scan();
557
- } catch (err) {
558
- logger.warn("[subagents] worktree reaper scan failed", {
559
- reason: err instanceof Error ? err.message : String(err),
560
- });
561
- }
562
-
563
- // ── workflow 域:per-session store + runs ──
564
- const sessionDir = resolveSessionDir();
565
- const store = new JsonlRunStore({
566
- sessionDir,
567
- pi,
568
- ctx,
569
- });
570
- const runs = new Map<string, WorkflowRun>();
571
-
572
- // F-4/D-003: agent 发现走 shared/resource-discovery(ADR-031),modelService
573
- // 自持 AgentRegistry(subagents/workflow 两域共用同一发现结果)。
574
- // M2 修正:workflow 域 resolveAgentOpts 不再消费 agentRegistry(agent ref 交
575
- // resolveIdentity),无需经 state 透传——modelService 是唯一 registry 源。
576
-
577
- // MF-1: store 健康度跟踪。loadAll 失败 → storeHealthy=false,workflow 域启动时 fail-fast。
578
- let storeHealthy = true;
579
- try {
580
- const loaded = await store.loadAll();
581
- for (const run of loaded) {
582
- if (run.state.status === "running") {
583
- run.state.error = "Process killed (kill-9 or crash recovery)";
584
- run.transition("done", "failed");
585
- pi.events.emit("pending:unregister", {
586
- id: run.runId,
587
- reason: "failed",
588
- });
589
- // 恢复终态必须落盘:save 走冷路径(done 绕过去抖)同步写 state 文件 +
590
- // append 终态 workflow-record entry——entry_appended 事件驱动 runtime 派生
591
- // 缓存失效重拉(无 triggerTurn 副作用)。不 save 则 entry/state 双双停留
592
- // running,侧栏永久卡 running。失败仅记日志不阻断其余 run 的恢复(下次
593
- // session_start 重开重试,恢复循环天然幂等)。
594
- try {
595
- await store.save(run);
596
- } catch (err) {
597
- logger.error("[subagent-workflow] kill-9 recovery store.save failed", {
598
- runId: run.runId,
599
- reason: err instanceof Error ? err.message : String(err),
600
- });
601
- }
602
- }
603
- runs.set(run.runId, run);
604
- }
605
- // done run 内存有界性:loadAll 全量重水合后立即裁剪到 K。kill-9 恢复(上方
606
- // running → transition("done","failed"))的 run completedAt 为 transition 时刻
607
- // (当前时间=全局最新)参与排序且必在保留端;多条恢复 run 同 ms completedAt →
608
- // tie 稳定排序。淘汰只 delete runs Map 条目——磁盘 state 文件与
609
- // workflow-state-link 指针条目均不动(历史审计保留);下次 session_start loadAll
610
- // 从指针全量重水合后再次裁剪,该循环每次 session 启动重复且可接受:内存峰值只在
611
- // 启动期,常驻 O(K + 活跃 run)。消除启动峰值需指针 compaction,属 append+replay
612
- // 长期方案问题域,非本范围。
613
- const evicted = evictDoneRunsBeyondCap(runs, MAX_RETAINED_DONE_RUNS);
614
- if (evicted > 0) {
615
- logger.debug("[subagent-workflow] evicted done runs beyond cap after loadAll", {
616
- evicted,
617
- keep: MAX_RETAINED_DONE_RUNS,
618
- sessionId,
619
- });
620
- }
621
- } catch (err) {
622
- // QMF-4 fix: store.loadAll 失败是关键路径错误,workflow 域将未初始化
623
- logger.error("[subagent-workflow] store.loadAll failed, workflow domain uninitialized", {
624
- reason: err instanceof Error ? err.message : String(err),
625
- });
626
- storeHealthy = false;
627
- }
628
-
629
- // D-008: per-session SAR(需要 ctxModel 填底 + subagentService 委托目标)。
630
- // old: const runner = new SubprocessAgentRunner()(module-level singleton,无 deps)
631
- // new: per-session session_start 时创建,通过 sessionState 传给 makeDeps。
632
- const runner = new SubprocessAgentRunner({
633
- subagentService: service,
634
- ctxModel: ctx.model ?? undefined,
635
- });
636
-
637
- // [engine-awareness D1b] lastEngine 初始化:构造性同源——单次 reloadGlobalConfig
638
- // 读取同时刷新 Service 路由缓存与 lastEngine 基准,消灭 initModel 与本处两次独立
639
- // 读取间的分叉窗口(两读值不一致时检测走 unchanged 分支不 reload,状态段/路由
640
- // 永停旧值且永不通知)。ok/absent → 归一后的当前引擎;failed → undefined(首 turn
641
- // 检测静默基线化兜底,此时缓存亦保持不动)。/resume、/fork 同样走 session_start
642
- // (SR-3),基线天然覆盖。
643
- const engineRead = modelService.reloadGlobalConfig();
644
- sessionState.set(sessionId, {
645
- store,
646
- runs,
647
- sessionDir,
648
- runner,
649
- ctx,
650
- storeHealthy,
651
- lastEngine:
652
- engineRead.status === "failed" ? undefined : normalizeEngineId(engineRead.config.defaultEngine),
653
- });
288
+ lsRef.lastSessionId = ctx.sessionManager.getSessionId();
289
+ const result = await setupSessionLifecycle(pi, ctx, makeLifecycleDeps());
290
+ sessionState.set(result.sessionId, result);
654
291
  });
655
292
 
656
293
  // ════════════════════════════════════════════════════════════
@@ -667,7 +304,7 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
667
304
  }
668
305
  } catch (err) {
669
306
  logger.warn("[subagents] notify ledger compactionCheck failed", {
670
- reason: err instanceof Error ? err.message : String(err),
307
+ reason: toErrorMessage(err),
671
308
  });
672
309
  }
673
310
  });
@@ -675,43 +312,16 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
675
312
  // ════════════════════════════════════════════════════════════
676
313
  // [U7 + engine-awareness U3] before_agent_start:引擎感知注入(链尾注册,D7——
677
314
  // 段内容变化只断 system prompt 尾部 cache 前缀)。
678
- // ① per-turn 检测编排(§2.3 数据流):三态 poll config → lastEngine diff →
679
- // 变更时读取结果先行提交缓存(D2,applyGlobalConfig 纯赋值,本 turn 路由生效)
680
- // → sendMessage 通知
681
- // (D3,不设 triggerTurn——P1 探针已证此形态消息进入本 turn LLM 上下文,
682
- // 证据:真机 pi rpc payload dump + 0.84.4 dist sendMessage→_appendCustomMessage
683
- // →agent.state.messages.push→createContextSnapshot 调用链)→ 更新 lastEngine。
684
- // ② 恒在状态段 <current_subagent_engine>(D6)+ 引擎清单段 <available_<engine>_models>。
685
- // apply 后 getGlobalConfig() 即新值——通知、状态段、路由三处同 turn 对齐(G2)。
686
- // 段序:状态段在前(文案声明 "listed ... below"),清单段在后;provider models 段
687
- // 由更早注册的 handler 注入、位于上方。fail-safe 任何异常不注入不阻塞 agent loop。
315
+ // D7-④:接线收编于 engine-awareness.ts 的 setupEngineAwarenessInjector(与上方
316
+ // 三个 setup* 同形,注入链序由调用先后表达);per-session lastEngine 经
317
+ // sessionState 存取器注入。编排/渲染/链尾依据的完整注释随迁至该函数。
688
318
  // ════════════════════════════════════════════════════════════
689
- pi.on("before_agent_start", (event: BeforeAgentStartEvent, ctx: ExtensionContext) => {
690
- try {
691
- const service = getModelConfigService();
692
- if (service === null || typeof event.systemPrompt !== "string") return undefined;
693
- const sid = ctx.sessionManager.getSessionId();
694
- runEngineAwarenessTurn({
695
- readConfig: () => readGlobalConfig(getAgentDir()),
696
- applyRead: (read) => service.applyGlobalConfig(read),
697
- sendMessage: (message) => {
698
- // D3:不设 triggerTurn——切换是用户主动行为,无需唤醒 AI 立即行动
699
- pi.sendMessage(message, {});
700
- },
701
- getLastEngine: () => sessionState.get(sid)?.lastEngine,
702
- setLastEngine: (engine) => {
703
- const state = sessionState.get(sid);
704
- if (state) state.lastEngine = engine;
705
- },
706
- });
707
- const defaultEngine = service.getGlobalConfig().defaultEngine;
708
- const append = [buildSubagentEngineSection(defaultEngine), buildEngineModelsPromptAppend(defaultEngine)]
709
- .filter((part) => part !== "")
710
- .join("\n\n");
711
- return { systemPrompt: `${event.systemPrompt}\n\n${append}` };
712
- } catch {
713
- return undefined;
714
- }
319
+ setupEngineAwarenessInjector(pi, {
320
+ getLastEngine: (sid) => sessionState.get(sid)?.lastEngine,
321
+ setLastEngine: (sid, engine) => {
322
+ const state = sessionState.get(sid);
323
+ if (state) state.lastEngine = engine;
324
+ },
715
325
  });
716
326
 
717
327
  // ════════════════════════════════════════════════════════════
@@ -820,7 +430,7 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
820
430
  await state.store.dispose().catch((err: unknown) => {
821
431
  logger.debug(
822
432
  `[subagent-workflow] session_shutdown store.dispose failed (sessionId=${sessionId}, sessionDir=${state.sessionDir})`,
823
- { reason: err instanceof Error ? err.message : String(err) },
433
+ { reason: toErrorMessage(err) },
824
434
  );
825
435
  });
826
436
  sessionState.delete(sessionId);
@@ -878,6 +488,32 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
878
488
  process.on("SIGINT", sigintHandler);
879
489
  process.on("beforeExit", reapSpawnedChildrenOnShutdown);
880
490
 
491
+ // ════════════════════════════════════════════════════════════
492
+ // workflow deps 守卫(单一出口)
493
+ //
494
+ // [守卫合一] 原 pi.__workflowRun 内联守卫 + getDeps 守卫两份重复(state 缺失 /
495
+ // storeHealthy=false),合并为单一 getWorkflowDeps:返回 discriminated union,
496
+ // 两个消费点各自决定失败形态——pi.__workflowRun(D-8 API)返回错误对象(不
497
+ // throw,保住调用方 Promise 契约),getDeps(3 个 tool 的 lazy deps 源)throw
498
+ // (pi tool 框架将其转译为 tool 错误结果)。错误消息逐字保留(crash-recovery
499
+ // 测试锁 "store unavailable" / "loadAll failed" 子串)。
500
+ // ════════════════════════════════════════════════════════════
501
+ type WorkflowDepsResolution =
502
+ | { ok: true; deps: LauncherDeps }
503
+ | { ok: false; reason: string };
504
+
505
+ const getWorkflowDeps = (sessionId: string): WorkflowDepsResolution => {
506
+ const state = sessionState.get(sessionId);
507
+ if (!state) {
508
+ return { ok: false, reason: "Session not initialized" };
509
+ }
510
+ // MF-1: store 不健康时 fail-fast,避免 store.save 再次失败导致 run 状态不落地。
511
+ if (!state.storeHealthy) {
512
+ return { ok: false, reason: "Workflow store unavailable (loadAll failed in session_start)" };
513
+ }
514
+ return { ok: true, deps: makeDeps(state, state.ctx) };
515
+ };
516
+
881
517
  // ════════════════════════════════════════════════════════════
882
518
  // pi.__workflowRun(D-8 签名)
883
519
  // ════════════════════════════════════════════════════════════
@@ -890,28 +526,19 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
890
526
  // 注意:lastSessionId 是单值假设——Pi 当前保证单 session 串行(一次只一个活跃 session)。
891
527
  // 若未来 Pi 支持多 session 并发,此处需改为从 ctx.sessionManager.getSessionId() 显式传入。
892
528
  // M-2 已记录此假设。
893
- const state = sessionState.get(lsRef.lastSessionId);
894
- if (!state) {
529
+ const resolved = getWorkflowDeps(lsRef.lastSessionId);
530
+ if (!resolved.ok) {
895
531
  return {
896
532
  status: "done",
897
533
  reason: "failed",
898
- error: "Session not initialized",
899
- runId: "",
900
- };
901
- }
902
- // MF-1: store 不健康时 fail-fast,避免 store.save 再次失败导致 run 状态不落地。
903
- if (!state.storeHealthy) {
904
- return {
905
- status: "done",
906
- reason: "failed",
907
- error: "Workflow store unavailable (loadAll failed in session_start)",
534
+ error: resolved.reason,
908
535
  runId: "",
909
536
  };
910
537
  }
911
538
  return runAndWait(
912
539
  workflowName,
913
540
  workflowArgs,
914
- makeDeps(state, state.ctx),
541
+ resolved.deps,
915
542
  workflowSignal,
916
543
  workflowTimeoutMs,
917
544
  );
@@ -919,46 +546,65 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
919
546
 
920
547
  // ════════════════════════════════════════════════════════════
921
548
  // Tools(3 个)—— lazy deps 注入
549
+ //
550
+ // lazyDeps:属性访问触发 getWorkflowDeps 守卫 + makeDeps 求值(每属性独立)。
551
+ // 守卫合一后 getWorkflowDeps 返回 discriminated union,getter 内消费时 throw
552
+ // (与 __workflowRun 的 return 错误对象对齐——同源同消息,session-lifecycle.test.ts 锁定)。
922
553
  // ════════════════════════════════════════════════════════════
923
- const getDeps = () => {
924
- // 注意:lastSessionId 是单值假设——Pi 当前保证单 session 串行(一次只一个活跃 session)。
925
- // 若未来 Pi 支持多 session 并发,此处需改为从 ctx.sessionManager.getSessionId() 显式传入。
926
- // M-2 已记录此假设。
927
- const state = sessionState.get(lsRef.lastSessionId);
928
- if (!state) throw new Error("Session not initialized");
929
- // MF-1: store 不健康时 fail-fast,避免 store.save 再次失败导致 run 状态不落地。
930
- if (!state.storeHealthy) {
931
- throw new Error("Workflow store unavailable (loadAll failed in session_start)");
932
- }
933
- return makeDeps(state, state.ctx);
934
- };
935
-
936
554
  const lazyDeps: LauncherDeps = {
937
555
  get store() {
938
- return getDeps().store;
939
- },
940
- workerHost,
941
- get runner() {
942
- return getDeps().runner;
556
+ const resolved = getWorkflowDeps(lsRef.lastSessionId);
557
+ if (!resolved.ok) throw new Error(resolved.reason);
558
+ return resolved.deps.store;
943
559
  },
944
560
  get runs() {
945
- return getDeps().runs;
561
+ const resolved = getWorkflowDeps(lsRef.lastSessionId);
562
+ if (!resolved.ok) throw new Error(resolved.reason);
563
+ return resolved.deps.runs;
564
+ },
565
+ get registry() {
566
+ const resolved = getWorkflowDeps(lsRef.lastSessionId);
567
+ if (!resolved.ok) throw new Error(resolved.reason);
568
+ return resolved.deps.registry;
946
569
  },
947
- registry,
948
570
  get onRunDone() {
949
- return getDeps().onRunDone;
571
+ const resolved = getWorkflowDeps(lsRef.lastSessionId);
572
+ if (!resolved.ok) throw new Error(resolved.reason);
573
+ return resolved.deps.onRunDone;
950
574
  },
951
575
  get eventBus() {
952
- return getDeps().eventBus;
576
+ const resolved = getWorkflowDeps(lsRef.lastSessionId);
577
+ if (!resolved.ok) throw new Error(resolved.reason);
578
+ return resolved.deps.eventBus;
579
+ },
580
+ get workerHost() {
581
+ const resolved = getWorkflowDeps(lsRef.lastSessionId);
582
+ if (!resolved.ok) throw new Error(resolved.reason);
583
+ return resolved.deps.workerHost;
584
+ },
585
+ get runner() {
586
+ const resolved = getWorkflowDeps(lsRef.lastSessionId);
587
+ if (!resolved.ok) throw new Error(resolved.reason);
588
+ return resolved.deps.runner;
953
589
  },
590
+ // scheduleTimeBudget / onWorkflowCall 不可缺席(ports.ts D-12 regression fix):
591
+ // rebuildRuntime 重排 run 级墙钟预算计时器、worker 脚本嵌套 workflow() 调用都经
592
+ // 这两个成员消费——lazyDeps 缺席会让消费点拿到 undefined(可选属性静默放行),
593
+ // 带时间预算的 run 命中一次错误重试后计时器静默失效。转发形态与其余成员一致。
954
594
  get scheduleTimeBudget() {
955
- return getDeps().scheduleTimeBudget;
595
+ const resolved = getWorkflowDeps(lsRef.lastSessionId);
596
+ if (!resolved.ok) throw new Error(resolved.reason);
597
+ return resolved.deps.scheduleTimeBudget;
956
598
  },
957
599
  get onWorkflowCall() {
958
- return getDeps().onWorkflowCall;
600
+ const resolved = getWorkflowDeps(lsRef.lastSessionId);
601
+ if (!resolved.ok) throw new Error(resolved.reason);
602
+ return resolved.deps.onWorkflowCall;
959
603
  },
960
604
  get log() {
961
- return getDeps().log;
605
+ const resolved = getWorkflowDeps(lsRef.lastSessionId);
606
+ if (!resolved.ok) throw new Error(resolved.reason);
607
+ return resolved.deps.log;
962
608
  },
963
609
  };
964
610
 
@@ -983,31 +629,12 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
983
629
  // ============================================================
984
630
 
985
631
  // channel registry 经 channel-registry-access.ts 公开访问(跨扩展 API)。
986
- // dialog queue 仍为本模块私有——无外部消费者。
987
- const DIALOG_QUEUE_KEY = Symbol.for("@zhushanwen/pi-subagents.dialogQueue");
988
-
989
- /** 获取或创建进程级 dialog queue 单例。
990
- * L2 跨子进程串行队列——所有子进程的 dialog 类请求共享同一队列实例。 */
991
- function getOrCreateDialogQueue(): DialogGlobalQueue {
992
- let queue = Reflect.get(globalThis, DIALOG_QUEUE_KEY) as DialogGlobalQueue | undefined;
993
- if (!queue) {
994
- queue = new DialogGlobalQueue();
995
- Reflect.set(globalThis, DIALOG_QUEUE_KEY, queue);
996
- }
997
- return queue;
998
- }
999
-
1000
- // ============================================================
1001
- // Public cross-extension API(channel handler 注册入口)
1002
- // ============================================================
1003
- //
1004
- // 跨扩展消费者(ask-user 等)通过包根 import 注册 channel handler,
1005
- // 让 subagent 子进程的 UI 请求(ask_user 等)透传到主进程渲染。
1006
- // 重新导出 channel-registry-access 的公开 API——稳定 surface,
1007
- // 内部存储实现演进不影响消费者。
1008
-
1009
- export {
1010
- getOrCreateChannelRegistry,
1011
- type UiChannelRegistry,
1012
- type ChannelHandler,
1013
- } from "@zhushanwen/subagent-core/execution/channel-registry-access.ts";
632
+ // dialog queue 单例随 session_start 装配域迁居 session-lifecycle.ts
633
+ // (getOrCreateDialogQueue;消费方 = 该文件 createOrReuseServices + 本文件
634
+ // session_shutdown),Symbol key 单定义点随迁,避免双份定义漂移。
635
+
636
+ // 跨扩展 channel handler 注册入口已收口到 core 深路径
637
+ // `@zhushanwen/subagent-core/execution/channel-registry-access.ts`
638
+ // (getOrCreateChannelRegistry / UiChannelRegistry / ChannelHandler)。
639
+ // 历史上的包根 re-export 已删:ask-user 等跨扩展消费者经 globalThis 握手
640
+ // (DIALOG_QUEUE_KEY 同款进程级单例),不再经包根 import 消费本模块。