@zhushanwen/pi-subagent-workflow 7.3.4 → 8.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 (166) hide show
  1. package/README.md +39 -12
  2. package/agents/analyst.md +61 -0
  3. package/agents/coder.md +70 -0
  4. package/agents/debugger.md +67 -0
  5. package/agents/doc-reviewer.md +3 -3
  6. package/agents/explorer.md +50 -18
  7. package/agents/general-purpose.md +19 -8
  8. package/agents/orchestrator.md +37 -32
  9. package/agents/planner.md +45 -11
  10. package/agents/researcher.md +53 -11
  11. package/agents/reviewer.md +74 -0
  12. package/package.json +1 -1
  13. package/skills/workflow-script-format/SKILL.md +1 -1
  14. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  15. package/src/execution/__tests__/agent-registry.test.ts +13 -11
  16. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
  17. package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
  18. package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
  19. package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
  20. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  21. package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
  22. package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
  23. package/src/execution/__tests__/crash-recovery.test.ts +8 -2
  24. package/src/execution/__tests__/delivery-methods.test.ts +385 -0
  25. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  26. package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
  27. package/src/execution/__tests__/execute-nesting.test.ts +20 -72
  28. package/src/execution/__tests__/execution-record.test.ts +199 -0
  29. package/src/execution/__tests__/finalize-record.test.ts +170 -14
  30. package/src/execution/__tests__/format.test.ts +131 -7
  31. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  32. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  33. package/src/execution/__tests__/helpers/spawn-mock.ts +25 -7
  34. package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
  35. package/src/execution/__tests__/index-session-start.test.ts +257 -5
  36. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  37. package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
  38. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  39. package/src/execution/__tests__/list-component.test.ts +59 -5
  40. package/src/execution/__tests__/list-fields.test.ts +109 -0
  41. package/src/execution/__tests__/model-resolver.test.ts +38 -1
  42. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  43. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  44. package/src/execution/__tests__/notifier-flush.test.ts +209 -7
  45. package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
  46. package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
  47. package/src/execution/__tests__/record-store.test.ts +158 -52
  48. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
  49. package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
  50. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  51. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  52. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  53. package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
  54. package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
  55. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  56. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
  57. package/src/execution/__tests__/session-pending.test.ts +61 -2
  58. package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
  59. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  60. package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
  61. package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
  62. package/src/execution/__tests__/spawn-args.test.ts +127 -49
  63. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
  64. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  65. package/src/execution/__tests__/status-refactor.test.ts +345 -0
  66. package/src/execution/__tests__/stdin-writer.test.ts +97 -0
  67. package/src/execution/__tests__/subagent-service-message-close.test.ts +598 -0
  68. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  69. package/src/execution/__tests__/subagent-service.test.ts +49 -11
  70. package/src/execution/__tests__/timeout-integration.test.ts +27 -13
  71. package/src/execution/__tests__/tool-action.test.ts +12 -10
  72. package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
  73. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  74. package/src/execution/__tests__/worktree-manager.test.ts +292 -89
  75. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +13 -12
  76. package/src/execution/argv-mirror.ts +21 -2
  77. package/src/execution/execution-record.ts +126 -9
  78. package/src/execution/finalize-record.ts +90 -13
  79. package/src/execution/host-mode.ts +1 -1
  80. package/src/execution/lifecycle-manager.ts +484 -0
  81. package/src/execution/lifecycle-predicates.ts +65 -0
  82. package/src/execution/manifest-store.ts +61 -16
  83. package/src/execution/model-resolver.ts +26 -5
  84. package/src/execution/notifier.ts +69 -12
  85. package/src/execution/pi-invocation.ts +21 -1
  86. package/src/execution/record-store.ts +554 -107
  87. package/src/execution/session-pending.ts +116 -45
  88. package/src/execution/session-reconstructor.ts +224 -7
  89. package/src/execution/session-runner.ts +290 -75
  90. package/src/execution/sessions-index.ts +304 -0
  91. package/src/execution/stdin-writer.ts +93 -7
  92. package/src/execution/stream-sink.ts +20 -3
  93. package/src/execution/subagent-service.ts +867 -138
  94. package/src/execution/turn-limiter.ts +14 -0
  95. package/src/execution/types.ts +204 -22
  96. package/src/execution/worktree-manager.ts +128 -49
  97. package/src/execution/worktree-registry.ts +13 -2
  98. package/src/index.ts +277 -19
  99. package/src/injectors/subagent-list-injector.ts +26 -8
  100. package/src/injectors/workflow-list-injector.ts +25 -8
  101. package/src/interface/__tests__/subagent-tool-prompt.test.ts +18 -5
  102. package/src/interface/__tests__/tool-render.test.ts +15 -13
  103. package/src/interface/bg-notify-render.ts +32 -8
  104. package/src/interface/command-actions.ts +26 -7
  105. package/src/interface/commands.ts +21 -22
  106. package/src/interface/format.ts +44 -17
  107. package/src/interface/gui-mappers.ts +6 -8
  108. package/src/interface/helpers.ts +170 -10
  109. package/src/interface/list-component.ts +53 -14
  110. package/src/interface/subagent-actions.ts +235 -17
  111. package/src/interface/subagent-tool.ts +82 -17
  112. package/src/interface/subagents.ts +2 -1
  113. package/src/interface/tool-render.ts +11 -24
  114. package/src/interface/tool-workflow.ts +20 -35
  115. package/src/interface/views/WorkflowsView.ts +89 -32
  116. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
  117. package/src/interface/views/__tests__/detail-content-session-file.test.ts +1 -1
  118. package/src/interface/views/format.ts +3 -3
  119. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +325 -0
  120. package/src/orchestration/__tests__/args-validator.test.ts +1 -1
  121. package/src/orchestration/__tests__/config-loader.test.ts +38 -0
  122. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
  123. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
  124. package/src/orchestration/__tests__/execute-agent-call.test.ts +95 -0
  125. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +657 -18
  126. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
  127. package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
  128. package/src/orchestration/__tests__/skill-discovery.test.ts +157 -0
  129. package/src/orchestration/__tests__/test-mocks.ts +191 -0
  130. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +98 -0
  131. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
  132. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  133. package/src/orchestration/__tests__/workflows-e2e.test.ts +1 -1
  134. package/src/orchestration/agent-opts-resolver.ts +4 -1
  135. package/src/orchestration/args-validator.ts +2 -2
  136. package/src/orchestration/config-loader.ts +30 -1
  137. package/src/orchestration/error-recovery.ts +133 -29
  138. package/src/orchestration/execute-agent-call.ts +31 -7
  139. package/src/orchestration/jsonl-run-store.ts +287 -40
  140. package/src/orchestration/launcher.ts +7 -1
  141. package/src/orchestration/lifecycle.ts +135 -132
  142. package/src/orchestration/models/__tests__/trace.test.ts +408 -0
  143. package/src/orchestration/models/budget.ts +1 -1
  144. package/src/orchestration/models/run-runtime.ts +15 -17
  145. package/src/orchestration/models/run-spec.ts +2 -2
  146. package/src/orchestration/models/run-state.ts +3 -3
  147. package/src/orchestration/models/trace.ts +95 -15
  148. package/src/orchestration/models/types.ts +8 -9
  149. package/src/orchestration/models/workflow-run.ts +50 -71
  150. package/src/orchestration/models/workflow-script.ts +32 -1
  151. package/src/orchestration/skill-discovery.ts +30 -0
  152. package/src/orchestration/worker-handle.ts +1 -1
  153. package/src/orchestration/worker-host.ts +1 -1
  154. package/src/orchestration/worker-script-builder.ts +29 -10
  155. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  156. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
  157. package/src/shared/__tests__/resource-discovery.test.ts +55 -0
  158. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  159. package/src/shared/agent-ref.ts +16 -0
  160. package/src/shared/resource-discovery.ts +147 -58
  161. package/src/shared/schema-jsonify.ts +53 -0
  162. package/workflows/README.md +4 -4
  163. package/agents/code-reviewer.md +0 -47
  164. package/agents/context-builder.md +0 -21
  165. package/agents/oracle.md +0 -34
  166. package/agents/worker.md +0 -20
@@ -14,7 +14,51 @@
14
14
  *
15
15
  * 参考:domain-models.md §6(字段/不变式)。
16
16
  */
17
- import type { ExecutionTraceNode, TracePatch } from "./types.ts";
17
+ import type { AgentResult, ExecutionTraceNode, TracePatch } from "./types.ts";
18
+
19
+ // ── result.content 裁剪 ────────────────────────────────────────
20
+
21
+ /**
22
+ * trace 节点 result.content 裁剪上限(严格大于才裁)。
23
+ *
24
+ * export 供测试边界构造(对齐 budget.ts export 常量先例)。
25
+ * 只裁 trace 节点侧——AgentCall.result 不裁(markDone 存全量在前,
26
+ * worker cached replay 数据源保真)。
27
+ */
28
+ export const TRACE_RESULT_MAX_CHARS = 8000;
29
+
30
+ /**
31
+ * 裁剪保留的头/尾长度(各半)。
32
+ *
33
+ * 耦合声明:TRIM_HEAD_CHARS + TRIM_TAIL_CHARS === TRACE_RESULT_MAX_CHARS
34
+ * (各半相等)是 doc 示例「8001 → 裁后 8000+标记 ≈ 8057」的前提
35
+ * (rt-w1-design.json W1C2 / trimTraceResult 注释同款表述)——改 MAX 时
36
+ * 需同步检查 head/tail 是否仍各半,及上述注释/文档示例数字。
37
+ * 比例钉死在 trace.test.ts 断言中;变更需复查 detail-content.ts
38
+ * 尾 5 行消费点(TUI 尾部预览依赖 tail 段)。
39
+ */
40
+ const TRIM_HEAD_CHARS = 4000;
41
+ const TRIM_TAIL_CHARS = 4000;
42
+
43
+ /**
44
+ * 裁剪 trace result 的 content(Trace 私有纯函数)。
45
+ *
46
+ * - undefined 透传;未超限原引用透传(零拷贝,不超限路径行为与无裁剪时一致)。
47
+ * - 超限返回新对象:head + 含原始长度的标记 + tail(只裁 content,
48
+ * result 其他字段浅拷贝保留)。节点持裁剪副本,与传入对象脱钩。
49
+ * - 边界附近(如 8001 字符)裁后总长 8000 + 标记长度 > 原始长度属
50
+ * 既定语义(head/tail 固定比例下净增长有界),禁止自行缩短 head/tail。
51
+ */
52
+ function trimTraceResult(result: AgentResult | undefined): AgentResult | undefined {
53
+ if (result === undefined) return result;
54
+ const { content } = result;
55
+ if (content.length <= TRACE_RESULT_MAX_CHARS) return result;
56
+ const trimmed =
57
+ content.slice(0, TRIM_HEAD_CHARS) +
58
+ `\n…[trace result truncated, original ${content.length} chars]…\n` +
59
+ content.slice(-TRIM_TAIL_CHARS);
60
+ return { ...result, content: trimmed };
61
+ }
18
62
 
19
63
  /**
20
64
  * Trace 值对象(事件流,唯一来源 D-10)。
@@ -22,28 +66,47 @@ import type { ExecutionTraceNode, TracePatch } from "./types.ts";
22
66
  * 不变式:
23
67
  * - nodes 只增不改索引顺序(append-only)
24
68
  * - update 只改单个 node 的 status/result/error/completedAt/sessionId
69
+ * - byIndex 与 nodes 恒一致(每个 append/remove 同步维护,无惰性重建);
70
+ * Map 值与数组元素引用共享(非拷贝),nodes 仍是持久化与 TUI 投影 SSOT
25
71
  * - 不含 verifyStrategy(G-020 删除,不迁移)
26
72
  */
27
73
  export class Trace {
28
74
  private readonly nodes: ExecutionTraceNode[] = [];
75
+ /** stepIndex 倒排索引(查询加速 O(1);值与 nodes 元素引用共享)。 */
76
+ private readonly byIndex = new Map<number, ExecutionTraceNode>();
29
77
 
30
78
  /**
31
79
  * 从已有节点数组重建 Trace(用于 RunStore 反序列化重水合)。
32
80
  *
33
81
  * 防御性拷贝——传入数组不被持有,外部 mutation 不影响 Trace。
34
82
  * 不验证节点顺序/唯一性(调用方保证快照来源可信)。
83
+ * 不做裁剪——落盘快照已是 write 路径裁剪后形态,旧版本未裁剪长
84
+ * content 重水合保持原样(read 路径无二次信息损失)。
85
+ * 重水合后 call.traceNode(来自快照 calls[].traceNode,
86
+ * jsonl-run-store.ts:156 直接传入)与 Trace.nodes 副本非同引用——
87
+ * D-10 引用共享仅 live append 路径成立。
35
88
  */
36
89
  static fromArray(nodes: readonly ExecutionTraceNode[]): Trace {
37
90
  const trace = new Trace();
38
91
  for (const node of nodes) {
39
- trace.nodes.push({ ...node });
92
+ const copy = { ...node };
93
+ trace.nodes.push(copy);
94
+ trace.byIndex.set(copy.stepIndex, copy); // 重复 stepIndex last-wins
40
95
  }
41
96
  return trace;
42
97
  }
43
98
 
44
- /** Append a trace node(append-only,不改已有节点)。 */
99
+ /**
100
+ * Append a trace node(append-only,不改已有节点)。
101
+ *
102
+ * 入口裁剪:超长 result.content 先 mutate 入参节点的 result 字段,
103
+ * 再 push 原节点引用(禁止 push 副本——保持 AgentCall.traceNode 与
104
+ * Trace.nodes 共享同一引用的 D-10 不变式)。
105
+ */
45
106
  append(node: ExecutionTraceNode): void {
107
+ node.result = trimTraceResult(node.result);
46
108
  this.nodes.push(node);
109
+ this.byIndex.set(node.stepIndex, node);
47
110
  }
48
111
 
49
112
  /**
@@ -57,19 +120,29 @@ export class Trace {
57
120
  if (!node) return; // no-op: 不存在不抛错(D-10 防御性)
58
121
 
59
122
  if (patch.status !== undefined) node.status = patch.status;
60
- if (patch.result !== undefined) node.result = patch.result;
123
+ // 入口裁剪:节点持裁剪副本,与 patch.result 脱钩为两个对象
124
+ // (call.result 保留全量——replay 数据源保真)。patch.result 未提供不动。
125
+ if (patch.result !== undefined) node.result = trimTraceResult(patch.result);
61
126
  if (patch.error !== undefined) node.error = patch.error;
62
127
  if (patch.completedAt !== undefined) node.completedAt = patch.completedAt;
63
128
  if (patch.sessionId !== undefined) node.sessionId = patch.sessionId;
64
129
  if (patch.sessionFile !== undefined) node.sessionFile = patch.sessionFile;
65
130
  }
66
131
 
67
- /** 查找指定 stepIndex 的节点(首个匹配,trace 中 stepIndex 应唯一)。 */
132
+ /**
133
+ * 查找指定 stepIndex 的节点(byIndex O(1),trace 中 stepIndex 应唯一)。
134
+ *
135
+ * 语义差异声明(旧线性扫 first-match → Map last-wins):仅在破坏
136
+ * stepIndex 唯一性的违规使用下可见,两组场景——
137
+ * 1. 重复 append 同 stepIndex 且未 remove:返回最后一个节点(last-wins;
138
+ * 旧线性扫 first-match 会返回第一个)。
139
+ * 2. 重复 append 后 removeByStepIndex:remove 的 findIndex 命中首个旧节点
140
+ * splice,而 byIndex.delete 把整个 stepIndex 键删掉——nodes 残留第二个
141
+ * 节点成为孤儿(find/update 不可达,length/toArray 仍可见)。
142
+ * 合法路径无差异:唯一性由 discard 先 remove 再 append 保证(W1TC12 锚定)。
143
+ */
68
144
  private findByStepIndex(stepIndex: number): ExecutionTraceNode | undefined {
69
- for (const node of this.nodes) {
70
- if (node.stepIndex === stepIndex) return node;
71
- }
72
- return undefined;
145
+ return this.byIndex.get(stepIndex);
73
146
  }
74
147
 
75
148
  /** 按节点引用删除(仅用于测试或 run 重建场景;正常运行不调用)。 */
@@ -78,20 +151,27 @@ export class Trace {
78
151
  }
79
152
 
80
153
  /**
81
- * 按 stepIndex 移除节点(仅 pause 清理在飞 call 用)。
154
+ * 按 stepIndex 移除节点(崩溃重建清理在飞 call 用)。
82
155
  *
83
- * 正常运行不调用(append-only 不变式)。仅 lifecycle.pauseRun 清理被 abort 的
84
- * 在飞 call 时用——移除其 trace 节点,让 resume 重发 agent-call append 全新
85
- * 节点走全新执行路径(避免 stale "running" 节点残留 + trace.update 命中旧节点
86
- * 导致新节点 orphan)。stepIndex 不存在时 no-op(防御性)。
156
+ * 正常运行不调用(append-only 不变式)。仅 error-recovery discardInFlightCalls
157
+ * (rebuildRuntime 内,F2)清理被旧 runtime abort 的在飞 call 时用——移除其 trace
158
+ * 节点,让重跑重发 agent-call append 全新节点走全新执行路径(避免 stale
159
+ * "running" 节点残留 + trace.update 命中旧节点导致新节点 orphan)。
160
+ * stepIndex 不存在时 no-op(防御性)。
87
161
  */
88
162
  removeByStepIndex(stepIndex: number): void {
163
+ // 先 findIndex 判存在再删,避免 byIndex 与 nodes 漂移
89
164
  const idx = this.nodes.findIndex((n) => n.stepIndex === stepIndex);
90
165
  if (idx === -1) return;
91
166
  this.nodes.splice(idx, 1);
167
+ this.byIndex.delete(stepIndex);
92
168
  }
93
169
 
94
- /** readonly 视图——外部不应直接 mutate(不变式保护)。 */
170
+ /**
171
+ * readonly 视图——返回内部 nodes 数组引用(仅类型级 readonly,运行时无
172
+ * 防御)。消费方禁止结构化 mutate(push/splice/重排/覆盖元素):byIndex
173
+ * 引入后外部结构化 mutate 会使 nodes 与倒排索引 desync。字段级变更走 update()。
174
+ */
95
175
  toArray(): readonly ExecutionTraceNode[] {
96
176
  return this.nodes;
97
177
  }
@@ -5,7 +5,7 @@
5
5
  * 可独立编译测试(D-12 三层架构,AC-1)。
6
6
  *
7
7
  * 核心内容:
8
- * - 状态机:RunStatus = "running" | "paused" | "done"(3 态,FR-3)
8
+ * - 状态机:RunStatus = "running" | "done"(2 态,一次性生命周期,FR-3)
9
9
  * + DoneReason(completed/failed/aborted/budget_limited/time_limited)
10
10
  * - AgentCallOpts / AgentResult / AgentUsage(单次 agent 调用的输入/输出)
11
11
  * - ExecutionTraceNode / TracePatch / ToolCallEntry / WorkerLogEntry(trace 数据)
@@ -18,13 +18,13 @@ import type { ExecutionRecord } from "../../execution/types.ts";
18
18
  // ── 状态机 ────────────────────────────────────────────────────
19
19
 
20
20
  /**
21
- * 状态机:3 态(D-12 / FR-3)。
21
+ * 状态机:2 态(D-12 / FR-3,一次性生命周期——run 不可挂起)。
22
22
  *
23
- * running ↔ paused → done
23
+ * running → done
24
24
  *
25
25
  * `done` 是唯一终态,具体原因由 DoneReason 区分。
26
26
  */
27
- export type RunStatus = "running" | "paused" | "done";
27
+ export type RunStatus = "running" | "done";
28
28
 
29
29
  /** 终态原因。done 时必有(WorkflowRun 不变式)。 */
30
30
  export type DoneReason =
@@ -38,12 +38,11 @@ export type DoneReason =
38
38
 
39
39
  /** 合法的状态转换。空数组 = 无出边(done 终态)。 */
40
40
  export const VALID_RUN_TRANSITIONS: Record<RunStatus, readonly RunStatus[]> = {
41
- running: ["paused", "done"] as const,
42
- paused: ["running", "done"] as const,
41
+ running: ["done"] as const,
43
42
  done: [] as const,
44
43
  };
45
44
 
46
- export const ALL_RUN_STATUSES: readonly RunStatus[] = ["running", "paused", "done"] as const;
45
+ export const ALL_RUN_STATUSES: readonly RunStatus[] = ["running", "done"] as const;
47
46
 
48
47
  export const ALL_DONE_REASONS: readonly DoneReason[] = [
49
48
  "completed",
@@ -247,7 +246,7 @@ export interface ExecutionTraceNode {
247
246
  sessionId?: string;
248
247
  /**
249
248
  * Session JSONL 绝对路径。finalizeCall 从 result.sessionFile 透传。
250
- * 持久化到快照(serializeRun),pause/resume + 跨 session 重水合后保留。
249
+ * 持久化到快照(serializeRun),跨 session 重水合后保留。
251
250
  */
252
251
  sessionFile?: string;
253
252
  /**
@@ -255,7 +254,7 @@ export interface ExecutionTraceNode {
255
254
  *
256
255
  * 挂在 node 上(D-10 单源延伸:AgentCall.traceNode 与 Trace.nodes 共享同一引用)。
257
256
  * TUI 通过 trace.toArray() 读 node.live,派生 getEventLog/getCurrentActivity 实时展示。
258
- * 不持久化(pause/resume 时为 undefined,重跑时重建)。
257
+ * 不持久化(序列化时 strip;重跑时由 dispatchAgentCall 重建)。
259
258
  */
260
259
  live?: ExecutionRecord;
261
260
  }
@@ -11,22 +11,19 @@
11
11
  * I1: state.status === "running" ⟺ runtime !== undefined
12
12
  * I2: state.status === "done" ⟹ state.reason !== undefined
13
13
  *
14
- * 状态机(FR-3,3 态):
15
- * paused ──assignRuntime──→ running
16
- * running ──transition("paused")──→ paused (releaseRuntime, G3-001)
14
+ * 状态机(一次性生命周期,2 态):
15
+ * 构造(status="running",I1 构造期跳过——runtime 由 assignRuntime 注入)
17
16
  * running ──transition("done", reason)──→ done (releaseRuntime + completedAt)
18
- * paused ──transition("done", reason)──→ done (completedAt)
19
17
  * done ──(no out edges, zombie)
20
18
  *
21
- * pause/resume 生命周期(G3-001):
22
- * - transition("paused") releaseRuntime,整个 RunRuntime 被丢弃
23
- * (runtime=undefined)。AbortController 一次性无法复用。
24
- * - resume assignRuntime(new RunRuntime(...)),重建 worker/gate/controller。
19
+ * 「创建即 running」与 I1 的协调(F4):构造瞬间 running 而 runtime 尚未注入,
20
+ * I1 在构造期跳过(仅查 I2),完整校验由 assignRuntime/transition/replaceRuntime
21
+ * 末尾的 validateInvariants 维持;构造到 assignRuntime 的 I1 窗口由调用方
22
+ * (lifecycle.runWorkflow assignRuntime 之后才 runs.set)保证对外不可见。
25
23
  *
26
24
  * worker-error-retry(G5-001 + G6-001):
27
25
  * - replaceRuntime(newRt): 前置 status==="running"(G6-001),原子释放前一个 runtime
28
26
  * + 绑定新 runtime,全程保持不变式 I1(中间不经过 runtime===undefined 的可见状态)。
29
- * - paused 状态下 retry 被拒(要 retry 先 resume)。
30
27
  *
31
28
  * 参考:domain-models.md §1(聚合根定义)、clarification.md G3-001/G5-001/G6-001。
32
29
  */
@@ -50,8 +47,6 @@ export interface WorkflowRunMeta {
50
47
  startedAt: string;
51
48
  /** ISO 时间戳,transition("done") 时设置。 */
52
49
  completedAt?: string;
53
- /** ISO 时间戳,transition("paused") 时设置(最近一次 pause)。 */
54
- pausedAt?: string;
55
50
  /** Worker 线程错误计数(C.5:跨 runtime 存活,重试计数载体)。 */
56
51
  workerErrorCount?: number;
57
52
  /** 脚本错误计数(C.5:跨 runtime 存活)。 */
@@ -68,52 +63,43 @@ export class WorkflowRun {
68
63
  meta: WorkflowRunMeta;
69
64
 
70
65
  /**
71
- * 创建聚合根。初始状态通常为 "paused"(runtime=undefined,符合不变式 I1),
72
- * 随后 assignRuntime 进入 "running"。也可传入 done 状态用于 reconstruct
66
+ * 创建聚合根。初始状态 "running"(一次性生命周期:run 从创建起即在执行,
67
+ * runtime 由紧随其后的 assignRuntime 注入)。也可传入 done 状态用于重水合
73
68
  * 已完成的 run(loadAll 后的只读聚合)。
74
69
  *
75
- * 不变式 I1 由构造函数校验——**不可用于 reconstruct 持久化的 running 快照**
76
- *(持久化的 running run 没有 worker,违反 I1;进程被杀后 worker 不可能还活着)。
77
- * 重水合用 `WorkflowRun.reconstruct`,它跳过 I1 校验(快照是可信状态)。
78
- *
79
- * @param reconstructMode 内部用——true 时跳过 I1 校验(仅校验 I2)。
80
- * 调用方用 `WorkflowRun.reconstruct` 静态工厂,不直接传此 flag。
70
+ * 不变式 I1 构造期跳过——「创建即 running」要求构造瞬间 runtime===undefined
71
+ * 合法(runtime 必须由 assignRuntime 注入,构造函数无从持有);重水合的
72
+ * running 快照同样无 worker。I1 的运行时校验在 assignRuntime/transition/
73
+ * replaceRuntime 末尾的 validateInvariants 处生效。
81
74
  */
82
75
  constructor(
83
76
  runId: string,
84
77
  spec: RunSpec,
85
78
  state: RunState,
86
79
  meta: WorkflowRunMeta,
87
- reconstructMode = false,
88
80
  ) {
89
81
  this.runId = runId;
90
82
  this.spec = spec;
91
83
  this.state = state;
92
84
  this.meta = meta;
93
- // runtime 在构造时始终为 undefined——run 创建时无活 worker,resume/loadAll
94
- // 时也不重水合 runtime(worker 必须由 lifecycle 重新 start)。
85
+ // runtime 在构造时始终为 undefined——run 创建时无活 worker,loadAll 重水合
86
+ // 时也不恢复 runtime(worker 必须由 lifecycle 重新 start)。
95
87
  this.runtime = undefined;
96
- if (reconstructMode) {
97
- // 重水合:仅校验 I2(done ⟹ reason)。I1 跳过——持久化的 running 状态没有
98
- // worker,违反 I1;调用方(D-4 kill-9 恢复)负责恢复 I1。
99
- this.validateInvariantI2();
100
- } else {
101
- this.validateInvariants();
102
- }
88
+ // 构造期仅校验 I2(I1 跳过,见方法 doc);I1 由 assignRuntime 末尾
89
+ // validateInvariants 恢复。
90
+ this.validateInvariantI2();
103
91
  }
104
92
 
105
93
  /**
106
- * 从持久化快照重水合聚合根。跳过 I1 校验——持久化的 running 状态没有 worker
107
- * (进程被杀后 worker 不可能还活着),违反 I1。调用方(D-4 kill-9 恢复)负责
108
- * session_start 时把残留 running 转 done,failed,恢复 I1。
109
- *
110
- * 与 `new WorkflowRun(...)` 的区别:constructor 校验 I1(适合 live 创建),
111
- * reconstruct 跳过(适合可信快照重水合)。
94
+ * 从持久化快照重水合聚合根。与构造函数同语义(构造期跳过 I1——持久化的
95
+ * running 状态没有 worker,进程被杀后 worker 不可能还活着)。保留独立工厂
96
+ * 标注重水合意图;调用方(D-4 kill-9 恢复)负责在 session_start 时把残留
97
+ * running 转 done,failed,恢复 I1。
112
98
  *
113
99
  * @throws I2 违反(done 快照缺 reason 仍是 bug,不可跳过)
114
100
  */
115
101
  static reconstruct(runId: string, spec: RunSpec, state: RunState, meta: WorkflowRunMeta): WorkflowRun {
116
- return new WorkflowRun(runId, spec, state, meta, true);
102
+ return new WorkflowRun(runId, spec, state, meta);
117
103
  }
118
104
 
119
105
  // ── 不变式校验 ─────────────────────────────────────────────
@@ -138,8 +124,8 @@ export class WorkflowRun {
138
124
  }
139
125
 
140
126
  /**
141
- * 仅校验不变式 I2(done ⟹ reason)。reconstruct 时用——持久化的 running 快照
142
- * 违反 I1(无 worker),但 I2 必须保证(done 快照缺 reason 是真 bug)。
127
+ * 仅校验不变式 I2(done ⟹ reason)。构造期用——「创建即 running」与重水合的
128
+ * running 快照都无 runtime(I1 构造期跳过),但 I2 必须保证(done reason 是真 bug)。
143
129
  */
144
130
  private validateInvariantI2(): void {
145
131
  if (this.state.status === "done" && this.state.reason === undefined) {
@@ -152,17 +138,16 @@ export class WorkflowRun {
152
138
  // ── 状态机转换 ─────────────────────────────────────────────
153
139
 
154
140
  /**
155
- * 状态机转换。合法转换:running→{paused,done}, paused→done
141
+ * 状态机转换。合法转换:running→done。
156
142
  *
157
- * paused→running 不走 transition——用 assignRuntime(需注入 runtime)。
158
- * 调用 transition("running") 抛错,引导调用方用 assignRuntime。
143
+ * running 的进入不走 transition——构造即 running,replaceRuntime 保持 running。
144
+ * 调用 transition("running") 抛错,防止绕过 runtime 注入直接改状态。
159
145
  *
160
146
  * 副作用:
161
- * - →paused: releaseRuntime(G3-001 丢弃 runtime)+ 设 meta.pausedAt
162
147
  * - →done: releaseRuntime + 设 state.reason + meta.completedAt
163
148
  *
164
- * @param target 目标状态(不允许 "running"——用 assignRuntime)
165
- * @param reason →done 时必填(done ⟹ reason,不变式 I2);→paused 时忽略
149
+ * @param target 目标状态(不允许 "running"——runtime 注入只走 assignRuntime/replaceRuntime
150
+ * @param reason →done 时必填(done ⟹ reason,不变式 I2
166
151
  * @throws 非法转换 / done 缺 reason / target==="running"
167
152
  */
168
153
  transition(target: RunStatus, reason?: DoneReason): void {
@@ -187,18 +172,11 @@ export class WorkflowRun {
187
172
  }
188
173
 
189
174
  // 副作用:先清理 runtime(releaseRuntime 守不变式 I1),再改 status
190
- if (target === "paused" || target === "done") {
191
- this.releaseRuntime();
192
- }
193
-
175
+ // (canRunTransition 已排除 target==="running",此处 target 恒为 "done"
176
+ this.releaseRuntime();
194
177
  this.state.status = target;
195
- if (target === "paused") {
196
- this.meta.pausedAt = new Date().toISOString();
197
- }
198
- if (target === "done") {
199
- this.state.reason = reason;
200
- this.meta.completedAt = new Date().toISOString();
201
- }
178
+ this.state.reason = reason;
179
+ this.meta.completedAt = new Date().toISOString();
202
180
 
203
181
  this.validateInvariants();
204
182
  }
@@ -206,12 +184,14 @@ export class WorkflowRun {
206
184
  // ── Runtime 生命周期 ───────────────────────────────────────
207
185
 
208
186
  /**
209
- * 绑定 runtime 并进入 running 状态。
187
+ * 绑定 runtime(run 创建后注入执行资源)。
210
188
  *
211
- * 前置:status==="paused" && runtime===undefined(首次启动或 resume)。
212
- * 原子地:设 runtime + status="running",保持不变式 I1 全程不违反。
189
+ * 前置:status==="running" && runtime===undefined(runWorkflow 创建路径——
190
+ * 构造即 running runtime 延迟到此处注入)。
191
+ * 原子地:设 runtime 后末尾 validateInvariants,恢复构造期跳过的 I1
192
+ * (running ⟺ runtime!==undefined)。
213
193
  *
214
- * @throws runtime 已定义 / status 不是 "paused"
194
+ * @throws runtime 已定义 / status 不是 "running"(done 僵尸不可复活)
215
195
  */
216
196
  assignRuntime(rt: RunRuntime): void {
217
197
  if (this.runtime !== undefined) {
@@ -219,28 +199,27 @@ export class WorkflowRun {
219
199
  `WorkflowRun.assignRuntime: runtime already defined (runId=${this.runId})`,
220
200
  );
221
201
  }
222
- if (this.state.status !== "paused") {
202
+ if (this.state.status !== "running") {
223
203
  throw new Error(
224
- `WorkflowRun.assignRuntime: requires status==="paused" (current: ${this.state.status}, runId=${this.runId})`,
204
+ `WorkflowRun.assignRuntime: requires status==="running" (current: ${this.state.status}, runId=${this.runId})`,
225
205
  );
226
206
  }
227
- // 原子绑定:先设 runtime(I1 暂时违反:status!=="running" runtime!==undefined),
228
- // 紧接着设 status="running",末尾 validateInvariants 通过。
229
- // 两条赋值间无 await/外部观察点,外部不可见中间状态。
207
+ // 原子绑定:构造期 I1 处于跳过窗口(running runtime undefined),设 runtime
208
+ // 后末尾 validateInvariants 恢复 I1。调用方在 assignRuntime 后才对外注册
209
+ // (lifecycle.runWorkflow 的 runs.set 后移),窗口外部不可见。
230
210
  this.runtime = rt;
231
- this.state.status = "running";
232
211
  this.validateInvariants();
233
212
  }
234
213
 
235
214
  /**
236
- * 解绑 runtime(pause/done 时由 transition 调用,也可独立调用)。
215
+ * 解绑 runtime(done 时由 transition 调用,也可独立调用)。
237
216
  *
238
217
  * 前置:无(runtime===undefined 时 no-op,幂等)。
239
- * 副作用:调 runtime.release("pause") 释放 worker/controller,置 runtime=undefined。
218
+ * 副作用:调 runtime.release("terminal") 释放 worker/controller,置 runtime=undefined。
240
219
  */
241
220
  releaseRuntime(): void {
242
221
  if (this.runtime === undefined) return;
243
- this.runtime.release("pause");
222
+ this.runtime.release("terminal");
244
223
  this.runtime = undefined;
245
224
  // 不改 status——调用方(transition)负责。独立调用时调用方需自行确保
246
225
  // status 一致(如 worker-error-retry 用 replaceRuntime 而非 release+assign)。
@@ -249,12 +228,12 @@ export class WorkflowRun {
249
228
  /**
250
229
  * 原地替换 runtime(G5-001:worker-error-retry)。
251
230
  *
252
- * 前置:status==="running"(G6-001:paused 下拒绝,要 retry 先 resume)。
231
+ * 前置:status==="running"(G6-001:终态 run 拒绝重建)。
253
232
  * 原子地:释放旧 runtime(worker.terminate + abort)+ 绑定新 runtime,
254
233
  * 全程 status 保持 "running",不变式 I1 不违反(中间无 runtime===undefined 可见态)。
255
234
  *
256
- * 与 release+assign 的区别:replaceRuntime 不改 status(避免经过 paused 中间态),
257
- * 中间同步完成,外部观察不到违反不变式的瞬间。
235
+ * 与 release+assign 的区别:replaceRuntime 不改 status,中间同步完成,
236
+ * 外部观察不到违反不变式的瞬间。
258
237
  *
259
238
  * @throws status!=="running"
260
239
  */
@@ -23,6 +23,28 @@ import { type LintResult,lintScript } from "../script-lint.ts";
23
23
  import type { WorkflowMeta } from "../../shared/resource-meta.ts";
24
24
  export type { WorkflowMeta };
25
25
 
26
+ // ── validate lint memo(IF9/#15,TC9/DM2)─────────────────────
27
+
28
+ /**
29
+ * 模块级 lint 结果缓存:key = path,命中条件 = sourceCode 与缓存 srcRef 相等
30
+ *(`===`——JS 字符串比较是**值相等**,设计原文的「引用相等」前提在 JS 不可实现,
31
+ * 实测等值异字面量同样命中;lintScript 是 source 的纯函数,值相等 ⟹ 结果必然
32
+ * 相同,故值键 memo 语义严格正确,且是引用键意图的超集:registry 重建实例传等值
33
+ * 内容也命中)。
34
+ *
35
+ * 重复点:registry.getPath/get 每次 new WorkflowScript(workflow-script-registry-impl),
36
+ * launcher.ts runAndWait / executeNestedWorkflow(每 nested call)各 validate 一次,
37
+ * 同脚本 N 次嵌套 = N 次全量正则 lint。失效语义:文件变更 → 内容值不等 → miss →
38
+ * 重 lint 并覆写条目。等长前缀不同的内容比较在 V8 走指针/长度快路径 + memcmp,
39
+ * 成本远低于正则 lint(TC9 alternatives 中「值键成本≈重 lint」的量级判断不成立)。
40
+ */
41
+ const lintMemo = new Map<string, { srcRef: string; result: LintResult }>();
42
+
43
+ /** 清空 validate lint 缓存(config-loader.invalidateCache 追加调用,测试隔离用)。 */
44
+ export function clearLintMemo(): void {
45
+ lintMemo.clear();
46
+ }
47
+
26
48
  /** 脚本来源:saved(.pi/workflows/ 固定)或 tmp(.pi/workflows/.tmp/ 临时)。 */
27
49
  export type WorkflowSource = "saved" | "tmp";
28
50
 
@@ -68,9 +90,18 @@ export class WorkflowScript {
68
90
  * - agent 选项 outputSchema → schema
69
91
  * - result.output/parsedOutput/content 不存在
70
92
  * - 文件传状态警告
93
+ *
94
+ * IF9(#15):同 path 且 sourceCode 引用相等 → 返回缓存 lint 结果(launcher 嵌套
95
+ * 场景下 registry 重建实例的重复全量 lint 消除);否则 lint + 覆写条目。
71
96
  */
72
97
  validate(): LintResult {
73
- return lintScript(this.sourceCode);
98
+ const memoized = lintMemo.get(this.path);
99
+ if (memoized && memoized.srcRef === this.sourceCode) {
100
+ return memoized.result;
101
+ }
102
+ const result = lintScript(this.sourceCode);
103
+ lintMemo.set(this.path, { srcRef: this.sourceCode, result });
104
+ return result;
74
105
  }
75
106
 
76
107
  /**
@@ -37,8 +37,35 @@ function getNpmSkillCandidates(npmSkillsDir: string): string[] {
37
37
  * PI_CODING_AGENT_DIR 场景读隔离目录,不碰 ~/.pi/agent)
38
38
  * 3. npm packages: <agentDir>/npm/node_modules/<pkg>/skills/<name>/
39
39
  * Returns the directory path if found, undefined otherwise.
40
+ *
41
+ * IF8(#14):结果按 skillName 缓存(含未命中 undefined 也缓存,DM3)——调用点
42
+ * agent-opts-resolver 每次 agent({skill}) call 一次,同 skill 名重复的逐候选
43
+ * existsSync 全部消重。缓存生命周期 = session:index.ts 在 session_start 调
44
+ * clearSkillPathCache 失效两级缓存(对齐同包 subagent-list-injector 的 session
45
+ * 生命周期缓存模式)。不做进程级缓存的原因:pi 同一进程可能有多个 session
46
+ * (TUI /new、/fork 同进程换 session),运行中安装的 skill(写入 project/user/npm
47
+ * 任一源)需要对新 session 可见——进程级缓存会让曾 miss 的 skill 名(含缓存了
48
+ * undefined 的未命中条目)在同进程后续 session 中永久不可见。session 内复用是
49
+ * IF8/DM3 的主要收益(同一次 run 内同 skill 名重复调用消重),保持不变。
50
+ */
51
+ const skillMemo = new Map<string, string | undefined>();
52
+
53
+ /**
54
+ * 清空 resolveSkillPath 两级缓存(skillMemo 结果缓存 + npm 候选列表缓存)。
55
+ * 两个消费点:测试隔离(beforeEach)与 index.ts session_start(session 生命周期
56
+ * 失效——npm 源新装包产生的候选目录也要重新可见)。
40
57
  */
58
+ export function clearSkillPathCache(): void {
59
+ skillMemo.clear();
60
+ skillCandidatesCache.clear();
61
+ }
62
+
41
63
  export function resolveSkillPath(skillName: string): string | undefined {
64
+ // has 先行区分「缓存了未命中(undefined)」与「无条目」——未命中也缓存(DM3)
65
+ if (skillMemo.has(skillName)) {
66
+ return skillMemo.get(skillName);
67
+ }
68
+
42
69
  const candidates = [
43
70
  // Project-level
44
71
  path.resolve(process.cwd(), ".agents/skills", skillName),
@@ -54,9 +81,12 @@ export function resolveSkillPath(skillName: string): string | undefined {
54
81
 
55
82
  for (const dir of candidates) {
56
83
  if (fs.existsSync(dir)) {
84
+ skillMemo.set(skillName, dir);
57
85
  return dir;
58
86
  }
59
87
  }
60
88
 
89
+ // 未命中也缓存:防止不存在的 skill 名反复全候选 existsSync(DM3)
90
+ skillMemo.set(skillName, undefined);
61
91
  return undefined;
62
92
  }
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * 核心职责:竞态防护(G-025)。
8
8
  *
9
- * 背景:一个 run 可经历多个 WorkerHandle(pause/resume/retry 各换一个)。
9
+ * 背景:一个 run 可经历多个 WorkerHandle(终止/重试各换一个)。
10
10
  * 需防止「terminate(old) → start(new) → old exit fires」竞态——
11
11
  * WorkerHandle 把守卫内化:terminate 后 isCurrent=false,
12
12
  * 已终止 handle 的 onMessage/onError/onExit 回调自动 no-op(无需调用方比对引用)。
@@ -40,7 +40,7 @@ export class WorkerHostImpl implements WorkerHost {
40
40
  * handle.isCurrent 做竞态防护,G-025)
41
41
  *
42
42
  * 返回的 WorkerHandle 由调用方(lifecycle)保存到 RunRuntime.worker。
43
- * 终止/pause/resume 时由 RunRuntime.release 接管。
43
+ * 终止/崩溃重建(rebuild)时由 RunRuntime.release 接管。
44
44
  */
45
45
  start(
46
46
  spec: RunSpec,