@zhushanwen/pi-subagent-workflow 7.4.0 → 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 (148) hide show
  1. package/README.md +15 -0
  2. package/package.json +1 -1
  3. package/skills/workflow-script-format/SKILL.md +1 -1
  4. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  5. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
  6. package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
  7. package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
  8. package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
  9. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  10. package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
  11. package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
  12. package/src/execution/__tests__/crash-recovery.test.ts +8 -2
  13. package/src/execution/__tests__/delivery-methods.test.ts +385 -0
  14. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  15. package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
  16. package/src/execution/__tests__/execute-nesting.test.ts +20 -72
  17. package/src/execution/__tests__/execution-record.test.ts +199 -0
  18. package/src/execution/__tests__/finalize-record.test.ts +170 -14
  19. package/src/execution/__tests__/format.test.ts +131 -7
  20. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  21. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  22. package/src/execution/__tests__/helpers/spawn-mock.ts +25 -7
  23. package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
  24. package/src/execution/__tests__/index-session-start.test.ts +257 -5
  25. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  26. package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
  27. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  28. package/src/execution/__tests__/list-component.test.ts +59 -5
  29. package/src/execution/__tests__/list-fields.test.ts +109 -0
  30. package/src/execution/__tests__/model-resolver.test.ts +38 -1
  31. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  32. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  33. package/src/execution/__tests__/notifier-flush.test.ts +209 -7
  34. package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
  35. package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
  36. package/src/execution/__tests__/record-store.test.ts +158 -52
  37. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
  38. package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
  39. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  40. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  41. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  42. package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
  43. package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
  44. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  45. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
  46. package/src/execution/__tests__/session-pending.test.ts +61 -2
  47. package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
  48. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  49. package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
  50. package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
  51. package/src/execution/__tests__/spawn-args.test.ts +127 -49
  52. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
  53. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  54. package/src/execution/__tests__/status-refactor.test.ts +345 -0
  55. package/src/execution/__tests__/stdin-writer.test.ts +97 -0
  56. package/src/execution/__tests__/subagent-service-message-close.test.ts +598 -0
  57. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  58. package/src/execution/__tests__/subagent-service.test.ts +49 -11
  59. package/src/execution/__tests__/timeout-integration.test.ts +27 -13
  60. package/src/execution/__tests__/tool-action.test.ts +12 -10
  61. package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
  62. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  63. package/src/execution/__tests__/worktree-manager.test.ts +292 -89
  64. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +13 -12
  65. package/src/execution/argv-mirror.ts +21 -2
  66. package/src/execution/execution-record.ts +126 -9
  67. package/src/execution/finalize-record.ts +90 -13
  68. package/src/execution/lifecycle-manager.ts +484 -0
  69. package/src/execution/lifecycle-predicates.ts +65 -0
  70. package/src/execution/manifest-store.ts +61 -16
  71. package/src/execution/model-resolver.ts +26 -5
  72. package/src/execution/notifier.ts +69 -12
  73. package/src/execution/pi-invocation.ts +21 -1
  74. package/src/execution/record-store.ts +554 -107
  75. package/src/execution/session-pending.ts +116 -45
  76. package/src/execution/session-reconstructor.ts +224 -7
  77. package/src/execution/session-runner.ts +270 -68
  78. package/src/execution/sessions-index.ts +304 -0
  79. package/src/execution/stdin-writer.ts +93 -7
  80. package/src/execution/stream-sink.ts +20 -3
  81. package/src/execution/subagent-service.ts +867 -138
  82. package/src/execution/turn-limiter.ts +14 -0
  83. package/src/execution/types.ts +200 -19
  84. package/src/execution/worktree-manager.ts +128 -49
  85. package/src/execution/worktree-registry.ts +13 -2
  86. package/src/index.ts +277 -19
  87. package/src/injectors/subagent-list-injector.ts +26 -8
  88. package/src/injectors/workflow-list-injector.ts +25 -8
  89. package/src/interface/__tests__/subagent-tool-prompt.test.ts +17 -4
  90. package/src/interface/__tests__/tool-render.test.ts +10 -8
  91. package/src/interface/bg-notify-render.ts +32 -8
  92. package/src/interface/command-actions.ts +26 -7
  93. package/src/interface/commands.ts +21 -22
  94. package/src/interface/format.ts +43 -16
  95. package/src/interface/gui-mappers.ts +6 -8
  96. package/src/interface/helpers.ts +170 -10
  97. package/src/interface/list-component.ts +53 -14
  98. package/src/interface/subagent-actions.ts +235 -17
  99. package/src/interface/subagent-tool.ts +81 -16
  100. package/src/interface/subagents.ts +2 -1
  101. package/src/interface/tool-render.ts +11 -24
  102. package/src/interface/tool-workflow.ts +20 -35
  103. package/src/interface/views/WorkflowsView.ts +89 -32
  104. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
  105. package/src/interface/views/format.ts +3 -3
  106. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +325 -0
  107. package/src/orchestration/__tests__/args-validator.test.ts +1 -1
  108. package/src/orchestration/__tests__/config-loader.test.ts +38 -0
  109. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
  110. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
  111. package/src/orchestration/__tests__/execute-agent-call.test.ts +95 -0
  112. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +657 -18
  113. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
  114. package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
  115. package/src/orchestration/__tests__/skill-discovery.test.ts +130 -61
  116. package/src/orchestration/__tests__/test-mocks.ts +191 -0
  117. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +98 -0
  118. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
  119. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  120. package/src/orchestration/__tests__/workflows-e2e.test.ts +1 -1
  121. package/src/orchestration/agent-opts-resolver.ts +4 -1
  122. package/src/orchestration/args-validator.ts +2 -2
  123. package/src/orchestration/config-loader.ts +30 -1
  124. package/src/orchestration/error-recovery.ts +133 -29
  125. package/src/orchestration/execute-agent-call.ts +31 -7
  126. package/src/orchestration/jsonl-run-store.ts +287 -40
  127. package/src/orchestration/launcher.ts +7 -1
  128. package/src/orchestration/lifecycle.ts +135 -132
  129. package/src/orchestration/models/__tests__/trace.test.ts +408 -0
  130. package/src/orchestration/models/budget.ts +1 -1
  131. package/src/orchestration/models/run-runtime.ts +15 -17
  132. package/src/orchestration/models/run-spec.ts +2 -2
  133. package/src/orchestration/models/run-state.ts +3 -3
  134. package/src/orchestration/models/trace.ts +95 -15
  135. package/src/orchestration/models/types.ts +8 -9
  136. package/src/orchestration/models/workflow-run.ts +50 -71
  137. package/src/orchestration/models/workflow-script.ts +32 -1
  138. package/src/orchestration/skill-discovery.ts +30 -0
  139. package/src/orchestration/worker-handle.ts +1 -1
  140. package/src/orchestration/worker-host.ts +1 -1
  141. package/src/orchestration/worker-script-builder.ts +29 -10
  142. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  143. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
  144. package/src/shared/__tests__/resource-discovery.test.ts +55 -0
  145. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  146. package/src/shared/agent-ref.ts +16 -0
  147. package/src/shared/resource-discovery.ts +147 -58
  148. package/src/shared/schema-jsonify.ts +53 -0
@@ -84,6 +84,58 @@ interface MtimeCacheEntry {
84
84
 
85
85
  const mtimeCache = new Map<string, MtimeCacheEntry>();
86
86
 
87
+ // [perf] findWorkspaceRoot 结果按 cwd memo——每次调用最多 3 phase × 20 层 existsSync
88
+ //(30-60 次 stat),调用点(session_start、discoverWorkflows、getWorkflow)在 spawn/
89
+ // tool 热路径上重复付。cwd 不变则祖先目录结构(.bare/.git/.pi)不变;变化场景由
90
+ // clearFileCache 兜底(invalidateCache 语义,测试隔离也走这里)。
91
+ const workspaceRootCache = new Map<string, string>();
92
+
93
+ // [perf] npm/dev 包 package.json manifest 缓存(swf-perf-impl cleanup TC1/IF1)。
94
+ // key = package.json 绝对路径;缓存 parse 后的整个 pi 对象(agents/workflows 两 kind
95
+ // 共享一次 parse,不按 kind 分裂条目)。失效 = mtimeMs 严格相等判定(与 mtimeCache 同构,
96
+ // 『内容变 mtime 未变』漏判局限共享 C3 声明,clearFileCache 兜底)。
97
+ // 失败语义(消歧,三方 TC1/DM1/IF1 同一口径):
98
+ // - stat 失败(文件不存在/不可 stat)→ 驱逐该 path 条目 + undefined(对齐 getCachedFile:
99
+ // 文件已删条目无意义)
100
+ // - read 失败(stat 与 read 间竞态删除/EACCES)或 JSON.parse 失败(坏 JSON)→
101
+ // 不缓存(不写新条目、不驱逐已有好条目)+ undefined,下次调用重试——毒条目永不入缓存,
102
+ // 坏文件被修复且 mtime 变化后正常覆写。
103
+ // pi === undefined 条目仅在『read+parse 成功且 manifest 无 pi 字段』时写入(合法解析结果)。
104
+ interface ManifestCacheEntry {
105
+ mtimeMs: number;
106
+ pi: Record<string, unknown> | undefined;
107
+ }
108
+
109
+ const manifestCache = new Map<string, ManifestCacheEntry>();
110
+
111
+ /** 从缓存的 pi 对象派生 kind manifest(廉价操作,每次调用新数组——语义与旧直读版一致)。 */
112
+ function piToManifest(pi: Record<string, unknown> | undefined, kind: ResourceKind): string[] | undefined {
113
+ if (!pi) return undefined;
114
+ const entries = pi[kind];
115
+ if (!Array.isArray(entries)) return undefined;
116
+ // 过滤非字符串元素
117
+ return entries.filter((p): p is string => typeof p === "string");
118
+ }
119
+
120
+ /**
121
+ * package.json 文本 → pi 字段对象(pi.agents / pi.workflows 的容器)。
122
+ * [review 修复] 结构守卫:JSON.parse 对 "42" / '"str"' / "null" 等合法 JSON 产出
123
+ * 非对象值,旧 `as Record<string, unknown>` 盲断言下 .pi 访问是「碰巧不抛」
124
+ * (primitive 装箱返 undefined / null 抛 TypeError 落入 catch)——显式判非对象,
125
+ * 按「无 manifest」(undefined)处理,后果与原先一致但语义不再依赖巧合。
126
+ * pi 字段本身非对象(如 {"pi":42})或为数组(如 {"pi":[]}——数组不是合法 pi 容器,
127
+ * typeof "object" 守卫会放行,Record 断言对数组是谎言)同样显式归 undefined。
128
+ * JSON SyntaxError 向上抛,由调用方 catch 承担「坏 JSON 不缓存、下次重试」语义。
129
+ */
130
+ function parsePiField(content: string): Record<string, unknown> | undefined {
131
+ const parsed: unknown = JSON.parse(content);
132
+ if (typeof parsed !== "object" || parsed === null) return undefined;
133
+ const pi: unknown = (parsed as Record<string, unknown>).pi;
134
+ return typeof pi === "object" && pi !== null && !Array.isArray(pi)
135
+ ? (pi as Record<string, unknown>)
136
+ : undefined;
137
+ }
138
+
87
139
  /**
88
140
  * stat + content 统一缓存。文件不存在/不可读 → null(并驱逐条目)。
89
141
  * mtime 未变 → 返回缓存 content(不重 read);变 → readFileSync + 缓存。
@@ -120,10 +172,20 @@ export function getCachedFileContent(filePath: string): string | null {
120
172
  /** 清空(invalidateCache 语义——测试隔离 + mtime 漏判场景手动刷新兜底)。 */
121
173
  export function clearFileCache(): void {
122
174
  mtimeCache.clear();
175
+ workspaceRootCache.clear();
176
+ manifestCache.clear();
123
177
  }
124
178
 
125
179
  export function findWorkspaceRoot(cwd?: string): string {
126
180
  const dir = cwd ?? process.cwd();
181
+ const memo = workspaceRootCache.get(dir);
182
+ if (memo !== undefined) return memo;
183
+ const root = computeWorkspaceRoot(dir);
184
+ workspaceRootCache.set(dir, root);
185
+ return root;
186
+ }
187
+
188
+ function computeWorkspaceRoot(dir: string): string {
127
189
  const root = resolve("/");
128
190
 
129
191
  // Phase 1: bare repo 优先——先全路径扫一遍找 .bare
@@ -220,22 +282,31 @@ async function scanDirectory(dirPath: string, kind: ResourceKind): Promise<strin
220
282
 
221
283
  /**
222
284
  * 读取 package.json 的 pi.{kind} manifest(pi.agents / pi.workflows)。
223
- * 返回 undefined 表示无 manifest 声明。
285
+ * 返回 undefined 表示无 manifest 声明(无 pi / pi[kind] 非数组 / 解析失败)。
286
+ * 内部走 manifestCache(与 readPackageManifestSync 共享同一 Map,失败语义见声明处)。
224
287
  */
225
288
  async function readPackageManifest(pkgDir: string, kind: ResourceKind): Promise<string[] | undefined> {
226
289
  const pkgJsonPath = resolve(pkgDir, "package.json");
290
+ let mtimeMs: number;
291
+ try {
292
+ mtimeMs = (await stat(pkgJsonPath)).mtimeMs;
293
+ } catch {
294
+ // stat 失败:文件不存在/不可 stat → 驱逐条目(对齐 getCachedFile 语义)
295
+ manifestCache.delete(pkgJsonPath);
296
+ return undefined;
297
+ }
298
+ const entry = manifestCache.get(pkgJsonPath);
299
+ if (entry && entry.mtimeMs === mtimeMs) return piToManifest(entry.pi, kind);
300
+ let pi: Record<string, unknown> | undefined;
227
301
  try {
228
302
  const content = await readFile(pkgJsonPath, "utf-8");
229
- const pkg = JSON.parse(content) as Record<string, unknown>;
230
- const pi = pkg.pi as Record<string, unknown> | undefined;
231
- if (!pi) return undefined;
232
- const entries = pi[kind];
233
- if (!Array.isArray(entries)) return undefined;
234
- // 过滤非字符串元素
235
- return entries.filter((p): p is string => typeof p === "string");
303
+ pi = parsePiField(content);
236
304
  } catch {
305
+ // read 失败或坏 JSON → 不缓存不驱逐已有好条目,下次调用重试
237
306
  return undefined;
238
307
  }
308
+ manifestCache.set(pkgJsonPath, { mtimeMs, pi });
309
+ return piToManifest(pi, kind);
239
310
  }
240
311
 
241
312
  /**
@@ -305,32 +376,33 @@ async function scanNpmDir(
305
376
  return [];
306
377
  }
307
378
 
308
- const results: DiscoveredResource[] = [];
309
-
310
- for (const entry of entries) {
311
- const entryPath = resolve(nodeModulesDir, entry);
312
-
313
- if (entry.startsWith("@")) {
314
- // scoped 包——迭代子包
315
- let scopedEntries: string[];
316
- try {
317
- scopedEntries = await readdir(entryPath);
318
- } catch {
319
- continue;
320
- }
321
- for (const scopedPkg of scopedEntries) {
322
- const scopedPkgDir = resolve(entryPath, scopedPkg);
323
- const pkgResults = await processPackage(scopedPkgDir, kind);
324
- results.push(...pkgResults);
379
+ // [perf] 包级并行(swf-perf-impl cleanup TC2/IF2):entries.map + Promise.all,
380
+ // perEntry.flat() 按原 readdir 序 concat——输出序与串行逐包 push 等价。
381
+ // 不加新增 catch:每包失败面由 processPackage 内部既有 catch 承担,未捕获异常
382
+ // 传播语义与串行版一致(Promise.all 整体 reject ↔ 串行版向上抛)。
383
+ const perEntry = await Promise.all(
384
+ entries.map(async (entry): Promise<DiscoveredResource[]> => {
385
+ const entryPath = resolve(nodeModulesDir, entry);
386
+
387
+ if (entry.startsWith("@")) {
388
+ // scoped 包——迭代子包(子包级同样并行,flat 保序)
389
+ let scopedEntries: string[];
390
+ try {
391
+ scopedEntries = await readdir(entryPath);
392
+ } catch {
393
+ return [];
394
+ }
395
+ const perScoped = await Promise.all(
396
+ scopedEntries.map((scopedPkg) => processPackage(resolve(entryPath, scopedPkg), kind)),
397
+ );
398
+ return perScoped.flat();
325
399
  }
326
- } else {
327
400
  // unscoped 包
328
- const pkgResults = await processPackage(entryPath, kind);
329
- results.push(...pkgResults);
330
- }
331
- }
401
+ return processPackage(entryPath, kind);
402
+ }),
403
+ );
332
404
 
333
- return results;
405
+ return perEntry.flat();
334
406
  }
335
407
 
336
408
  // ── 扫描源构建 ───────────────────────────────────────────────
@@ -414,35 +486,43 @@ function buildScanTargets(config: ScanConfig): ScanTarget[] {
414
486
  * 按优先级低→高扫描所有源,同名资源靠后覆盖靠前(last-writer-wins)。
415
487
  * npm/dev 包内:有 manifest 只走 manifest(路径不存在则失败),无 manifest 扫约定目录。
416
488
  *
417
- * Never throws. 解析失败/不可读的资源以 available=false 返回。
489
+ * Throws on unrecoverable scan errors——未捕获异常向上抛(Promise.all 首个 reject
490
+ * 即整体拒绝,与串行版 discoverResourcesSync 的传播语义一致,见实现内 [perf] 注释)。
491
+ * 预期失败不抛:目录不存在/不可读返回空列表,manifest 声明路径缺失以 available=false 返回。
418
492
  *
419
493
  * @returns 去重后的资源列表(按优先级合并,高优先级覆盖低优先级同名)
420
494
  */
421
495
  export async function discoverResources(config: ScanConfig): Promise<DiscoveredResource[]> {
422
496
  const targets = buildScanTargets(config);
423
497
 
424
- // 逐源扫描,收集结果(保留 source 标签用于优先级合并)
425
- const allBySource: Array<{ source: ResourceSource; resources: DiscoveredResource[] }> = [];
426
-
427
- for (const target of targets) {
428
- if (target.source === "npm" || target.source === "npm-dev") {
429
- // npm/dev 目录:迭代包,走 manifest 或约定目录
430
- const resources = await scanNpmDir(target.dir, config.kind);
431
- // 覆盖 source 标签(scanNpmDir 内部统一标 "npm",这里修正为实际源)
432
- const tagged = resources.map((r) => ({ ...r, source: target.source }));
433
- allBySource.push({ source: target.source, resources: tagged });
434
- } else if (target.source === "user-extension-paths") {
435
- // XYZ_EXTENSION_PATHS(dev-link):每个 dir 是单个包目录,走 processPackage
436
- const resources = await processPackage(target.dir, config.kind);
437
- const tagged = resources.map((r) => ({ ...r, source: target.source }));
438
- allBySource.push({ source: target.source, resources: tagged });
439
- } else {
498
+ // [perf] 源级并行(swf-perf-impl cleanup TC2/IF2):targets.map + Promise.all,
499
+ // Promise.all map 数组保序——allBySource 顺序 = targets 优先级序(低→高),
500
+ // 与串行逐源 push 完全一致 → 下方合并去重逻辑零改动、输出逐字节等价。
501
+ // 不加新增 catch:每源预期失败路径由内部既有 catch 面承担(scanDirectory access
502
+ // catch / scanNpmDir readdir catch / processPackage 全链 catch),未捕获异常
503
+ // 传播语义与串行版等价(ES2:串行版 target k 抛错中断后续源,并行版全部源已并发
504
+ // 启动、以首个 reject 拒绝——两版对调用方同为 discoverResources 抛出,各源只读无副作用)。
505
+ const allBySource: Array<{ source: ResourceSource; resources: DiscoveredResource[] }> = await Promise.all(
506
+ targets.map(async (target) => {
507
+ if (target.source === "npm" || target.source === "npm-dev") {
508
+ // npm/dev 目录:迭代包,走 manifest 或约定目录
509
+ const resources = await scanNpmDir(target.dir, config.kind);
510
+ // 覆盖 source 标签(scanNpmDir 内部统一标 "npm",这里修正为实际源)
511
+ const tagged = resources.map((r) => ({ ...r, source: target.source }));
512
+ return { source: target.source, resources: tagged };
513
+ }
514
+ if (target.source === "user-extension-paths") {
515
+ // XYZ_EXTENSION_PATHS(dev-link):每个 dir 是单个包目录,走 processPackage
516
+ const resources = await processPackage(target.dir, config.kind);
517
+ const tagged = resources.map((r) => ({ ...r, source: target.source }));
518
+ return { source: target.source, resources: tagged };
519
+ }
440
520
  // 普通目录:直接扫
441
521
  const files = await scanDirectory(target.dir, config.kind);
442
522
  const resources = files.map((f) => ({ path: f, source: target.source, available: true }));
443
- allBySource.push({ source: target.source, resources });
444
- }
445
- }
523
+ return { source: target.source, resources };
524
+ }),
525
+ );
446
526
 
447
527
  // 按优先级合并:targets 数组顺序即优先级(低→高),高优先级后写覆盖
448
528
  // 用文件名 stem 作为去重 key(与旧逻辑一致:同名资源高优先级覆盖)
@@ -492,21 +572,30 @@ export function scanDirectorySync(dirPath: string, kind: ResourceKind): string[]
492
572
 
493
573
  /**
494
574
  * 同步版:读取 package.json 的 pi.{kind} manifest。
495
- * 供 agent-registry 同步路径使用。
575
+ * 供 agent-registry 同步路径使用。与 async 版共享 manifestCache(双读者同一 Map)。
496
576
  */
497
577
  export function readPackageManifestSync(pkgDir: string, kind: ResourceKind): string[] | undefined {
498
578
  const pkgJsonPath = resolve(pkgDir, "package.json");
579
+ let mtimeMs: number;
580
+ try {
581
+ mtimeMs = fsSync.statSync(pkgJsonPath).mtimeMs;
582
+ } catch {
583
+ // stat 失败:文件不存在/不可 stat → 驱逐条目(对齐 getCachedFile 语义)
584
+ manifestCache.delete(pkgJsonPath);
585
+ return undefined;
586
+ }
587
+ const entry = manifestCache.get(pkgJsonPath);
588
+ if (entry && entry.mtimeMs === mtimeMs) return piToManifest(entry.pi, kind);
589
+ let pi: Record<string, unknown> | undefined;
499
590
  try {
500
591
  const content = fsSync.readFileSync(pkgJsonPath, "utf-8");
501
- const pkg = JSON.parse(content) as Record<string, unknown>;
502
- const pi = pkg.pi as Record<string, unknown> | undefined;
503
- if (!pi) return undefined;
504
- const entries = pi[kind];
505
- if (!Array.isArray(entries)) return undefined;
506
- return entries.filter((p): p is string => typeof p === "string");
592
+ pi = parsePiField(content);
507
593
  } catch {
594
+ // read 失败或坏 JSON → 不缓存不驱逐已有好条目,下次调用重试
508
595
  return undefined;
509
596
  }
597
+ manifestCache.set(pkgJsonPath, { mtimeMs, pi });
598
+ return piToManifest(pi, kind);
510
599
  }
511
600
 
512
601
  /**
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Schema JSON 序列化缓存(IF7/#13,TC6/DM4)。
3
+ *
4
+ * 同一 schema 对象引用的重复 JSON.stringify 消除:resolver(compact,instruction
5
+ * 与 schemaEnv 复用同串)与 session-runner formatSchemaInstruction(pretty)在
6
+ * 单次 agent call 内对同一 schema 对象各 stringify 一次;error-recovery 重试路径
7
+ * 再加一次。本 helper 用 WeakMap 按对象引用缓存两种格式,命中返回缓存串。
8
+ *
9
+ * 返回值与直接 JSON.stringify 逐字节一致(compact = JSON.stringify(x)、
10
+ * pretty = JSON.stringify(x, null, 2))。
11
+ *
12
+ * 失效语义:无。WeakMap 键随 schema/opts 对象 GC 自动回收;单 call 生命周期内
13
+ * schema 对象引用稳定(resolver/runner 均只读),引用稳定即值稳定;postMessage
14
+ * 克隆使跨 call 恒为新对象,天然隔离(DM4)。
15
+ *
16
+ * 层归属:Shared(resolver 与 session-runner 共用,无 Pi 依赖)。
17
+ */
18
+
19
+ const cache = new WeakMap<object, { compact?: string; pretty?: string }>();
20
+
21
+ /**
22
+ * JSON.stringify(schema) 的引用级缓存版。
23
+ *
24
+ * @param schema schema 对象(调用方 if 守卫保证非 undefined,helper 不判空)
25
+ * @param mode "compact" = JSON.stringify(x);"pretty" = JSON.stringify(x, null, 2)
26
+ */
27
+ export function stringifySchemaCached(schema: object, mode: "compact" | "pretty"): string {
28
+ let entry = cache.get(schema);
29
+ if (!entry) {
30
+ entry = {};
31
+ cache.set(schema, entry);
32
+ }
33
+ const hit = entry[mode];
34
+ if (hit !== undefined) return hit;
35
+ // [review 修复] TS lib 盲区:lib 对 object 参数声明 JSON.stringify 返回 string,但
36
+ // schema 含 `toJSON: () => undefined` 钩子时运行时返回 undefined——直接违反本函数
37
+ // 声明的 string 返回类型,且 `entry[mode] = undefined` 赋值不生效(缓存永不命中,
38
+ // 重复 stringify 仅是性能损耗)。显式标注 string | undefined 捕获该窗口,fail-loud
39
+ // 抛含恢复指引的错误——不回退 String(value)("[object Object]" 会静默拼进 LLM
40
+ // 指令,比崩溃更难排查)。抛错时 entry[mode] 未被赋值,缓存无毒化。
41
+ const serialized: string | undefined =
42
+ mode === "compact" ? JSON.stringify(schema) : JSON.stringify(schema, null, 2);
43
+ if (serialized === undefined) {
44
+ throw new Error(
45
+ `[subagent-workflow] stringifySchemaCached: JSON.stringify returned undefined (mode=${mode}) — ` +
46
+ `the schema object defines a toJSON hook returning undefined. ` +
47
+ `Recovery: check the schema source (agent definition / workflow script), remove that toJSON ` +
48
+ `or make it return a JSON-serializable value.`,
49
+ );
50
+ }
51
+ entry[mode] = serialized;
52
+ return serialized;
53
+ }