@zhushanwen/pi-session-reader 0.11.0 → 0.13.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 (42) hide show
  1. package/package.json +5 -5
  2. package/src/__tests__/archive-ledger.test.ts +447 -0
  3. package/src/__tests__/cross-package-subagent-core.test.ts +34 -25
  4. package/src/__tests__/entry-anchor.test.ts +3 -3
  5. package/src/__tests__/execution-tree.test.ts +85 -86
  6. package/src/__tests__/family-zcode.test.ts +24 -14
  7. package/src/__tests__/family.test.ts +4 -4
  8. package/src/__tests__/find.test.ts +50 -5
  9. package/src/__tests__/index.test.ts +3 -3
  10. package/src/__tests__/rebuild-manifest-dual-write.test.ts +29 -22
  11. package/src/__tests__/render.test.ts +2 -2
  12. package/src/__tests__/result.test.ts +20 -11
  13. package/src/__tests__/roots.test.ts +64 -26
  14. package/src/__tests__/subagents.test.ts +108 -52
  15. package/src/__tests__/tool-handler.test.ts +133 -83
  16. package/src/__tests__/workflow.test.ts +5 -5
  17. package/src/__tests__/zcode-manifest.test.ts +18 -10
  18. package/src/__tests__/zcode-routing.test.ts +21 -19
  19. package/src/__tests__/zcode-whitelist.test.ts +1 -1
  20. package/src/core/family.ts +6 -6
  21. package/src/core/render.ts +2 -2
  22. package/src/core/toolcall.ts +1 -1
  23. package/src/core/workflow.ts +9 -9
  24. package/src/discovery/archive-ledger.ts +221 -0
  25. package/src/discovery/archive-match.ts +84 -0
  26. package/src/discovery/entry-anchor.ts +5 -5
  27. package/src/discovery/find.ts +12 -1
  28. package/src/discovery/roots.ts +149 -14
  29. package/src/discovery/subagents.ts +24 -17
  30. package/src/discovery/whitelist.ts +4 -4
  31. package/src/discovery/workflows.ts +8 -8
  32. package/src/discovery/zcode-manifest.ts +22 -27
  33. package/src/doctor.ts +3 -0
  34. package/src/extract.ts +1 -1
  35. package/src/index.ts +5 -2
  36. package/src/no-match.ts +29 -4
  37. package/src/result-action.ts +7 -1
  38. package/src/search-across.ts +3 -3
  39. package/src/tool-format.ts +24 -13
  40. package/src/tool-handler.ts +68 -29
  41. package/src/tui/session-command.ts +1 -1
  42. package/src/zcode-anchor-classify.ts +2 -2
@@ -3,6 +3,10 @@ import { tmpdir } from 'node:os'
3
3
  import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
4
4
  import { join } from 'node:path'
5
5
  import { buildExecutionTree, formatExecutionTreeText, type ExecutionTreeNode } from '../core/execution-tree.js'
6
+ import {
7
+ resolveStoreSubagentSessionsDir,
8
+ resolveStoreSubagentRecordsDir,
9
+ } from '../discovery/roots.js'
6
10
 
7
11
  /**
8
12
  * M3b U7 buildExecutionTree 单测(design m3b 6 testCases)。
@@ -17,7 +21,7 @@ import { buildExecutionTree, formatExecutionTreeText, type ExecutionTreeNode } f
17
21
  * - TC-m3b-cycle-detection:workflow 指针环(A→B→A),visited Set 防环
18
22
  * - TC-m3b-single-node:单节点树(无后代,ES5)
19
23
  * - TC-m3b-source-priority:parentRecordId 三级数据源优先级(manifest>identity>flat,DM4)
20
- * - TC-m3b-real-data-guard:真实形态数据守卫(合成 fixture):mkdtemp 临时目录仿真真实
24
+ * - TC-m3b-real-data-guard:真实形态数据检查(合成 fixture):mkdtemp 临时目录仿真真实
21
25
  * agentDir 布局(时间戳前缀文件名 / 新旧机制混合 / 孤儿 manifest / 坏 manifest / 外来
22
26
  * 家族),guard 语义断言——解析不抛错 / 树非空 / 结构自洽,不锁 sourceMode 具体值
23
27
  */
@@ -51,11 +55,11 @@ async function writeMainSession(dir: string, slug: string, id: string): Promise<
51
55
  /**
52
56
  * 写 subagent session 文件:首行 header(真实 id)+ 占位 message + 尾行 identity。
53
57
  * identity 尾行含 rootSessionId/slug/agent,可选 parentRecordId(数据源 ② 测试用)。
54
- * fileName 可选:默认 `<realId>.jsonl`;真实形态守卫用例传时间戳前缀名(<ts>_<id>.jsonl)。
58
+ * fileName 可选:默认 `<realId>.jsonl`;真实形态检查用例传时间戳前缀名(<ts>_<id>.jsonl)。
55
59
  */
56
60
  async function writeSubagentSession(
57
61
  dir: string,
58
- slug: string,
62
+ rootSessionId: string,
59
63
  realId: string,
60
64
  identity: {
61
65
  rootSessionId: string
@@ -66,7 +70,7 @@ async function writeSubagentSession(
66
70
  },
67
71
  fileName: string = `${realId}.jsonl`,
68
72
  ): Promise<string> {
69
- const sessionDir = join(dir, 'subagents', slug, 'sessions')
73
+ const sessionDir = resolveStoreSubagentSessionsDir(dir, rootSessionId)
70
74
  await mkdir(sessionDir, { recursive: true })
71
75
  const path = join(sessionDir, fileName)
72
76
  const data: Record<string, unknown> = {
@@ -80,7 +84,7 @@ async function writeSubagentSession(
80
84
  }
81
85
  if (identity.parentRecordId !== undefined) data.parentRecordId = identity.parentRecordId
82
86
  const lines = [
83
- JSON.stringify({ type: 'session', id: realId, cwd: `/proj/${slug}` }),
87
+ JSON.stringify({ type: 'session', id: realId, cwd: `/proj/${rootSessionId}` }),
84
88
  JSON.stringify({
85
89
  type: 'message',
86
90
  id: 'm1',
@@ -96,7 +100,7 @@ async function writeSubagentSession(
96
100
  /** 写 records manifest。fields 含必填 + 可选 parentRecordId(数据源 ①)。 */
97
101
  async function writeRecordManifest(
98
102
  dir: string,
99
- slug: string,
103
+ rootSessionId: string,
100
104
  id: string,
101
105
  fields: {
102
106
  rootSessionId: string
@@ -109,7 +113,7 @@ async function writeRecordManifest(
109
113
  parentRecordId?: string
110
114
  },
111
115
  ): Promise<void> {
112
- const recordsDir = join(dir, 'subagents', slug, 'records')
116
+ const recordsDir = resolveStoreSubagentRecordsDir(dir, rootSessionId)
113
117
  await mkdir(recordsDir, { recursive: true })
114
118
  await writeFile(join(recordsDir, `${id}.json`), JSON.stringify({ id, ...fields }))
115
119
  }
@@ -174,24 +178,26 @@ function findNode(
174
178
  // ============================================================
175
179
 
176
180
  describe('buildExecutionTree - fixture', () => {
177
- let dir: string
181
+ let dataDir: string
182
+ let dir: string // agentDir = <dataDir>/agent(生产拓扑复刻)
178
183
 
179
184
  beforeEach(async () => {
180
- dir = await makeAgentDir()
185
+ dataDir = await makeAgentDir()
186
+ dir = join(dataDir, 'agent')
181
187
  })
182
188
  afterEach(async () => {
183
- await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
189
+ await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
184
190
  })
185
191
 
186
192
  it('TC-m3b-nested-tree:main→subagent A→{workflow-call B, subagent C} 相互嵌套(parentRecordId 精确链)', async () => {
187
193
  await writeMainSession(dir, '--main-cwd--', MAIN)
188
194
  // subagent A(顶层,parentRecordId=undefined),sessionFile 含 workflow-state-link → call session B
189
- const saA = await writeSubagentSession(dir, '--main-cwd--', A_REAL, {
195
+ const saA = await writeSubagentSession(dir, MAIN, A_REAL, {
190
196
  rootSessionId: MAIN,
191
197
  slug: 'sub-a',
192
198
  })
193
199
  const recAId = 'sa-record-A'
194
- await writeRecordManifest(dir, '--main-cwd--', recAId, {
200
+ await writeRecordManifest(dir, MAIN, recAId, {
195
201
  rootSessionId: MAIN,
196
202
  agentName: 'explorer',
197
203
  sessionFile: saA,
@@ -199,11 +205,11 @@ describe('buildExecutionTree - fixture', () => {
199
205
  parentRecordId: undefined,
200
206
  })
201
207
  // subagent C(A 的后代,parentRecordId=recAId,精确链)
202
- const saC = await writeSubagentSession(dir, '--main-cwd--', C_REAL, {
208
+ const saC = await writeSubagentSession(dir, MAIN, C_REAL, {
203
209
  rootSessionId: MAIN,
204
210
  slug: 'sub-c',
205
211
  })
206
- await writeRecordManifest(dir, '--main-cwd--', 'sa-record-C', {
212
+ await writeRecordManifest(dir, MAIN, 'sa-record-C', {
207
213
  rootSessionId: MAIN,
208
214
  agentName: 'worker',
209
215
  sessionFile: saC,
@@ -212,14 +218,11 @@ describe('buildExecutionTree - fixture', () => {
212
218
  })
213
219
  // workflow-call B:A 的 sessionFile 的 v2 注册条目 → record 流 → calls[].sessionFile = SB
214
220
  const sbB = join(
215
- dir,
216
- 'subagents',
217
- '--main-cwd--',
218
- 'sessions',
221
+ resolveStoreSubagentSessionsDir(dir, MAIN),
219
222
  `2026-08-07T16-49-48-393Z_${B_REAL}.jsonl`,
220
223
  )
221
224
  // B call session 文件需存在(resolveWorkflows 读 call sessionFile;这里只需路径,不读内容建树)
222
- await mkdir(join(dir, 'subagents', '--main-cwd--', 'sessions'), { recursive: true })
225
+ await mkdir(resolveStoreSubagentSessionsDir(dir, MAIN), { recursive: true })
223
226
  await writeFile(sbB, JSON.stringify({ type: 'session', id: B_REAL, cwd: '/proj/wf' }) + '\n')
224
227
  await writeWfV2Run(dir, '--main-cwd--', saA, A_REAL, RUN_A, [sbB])
225
228
 
@@ -252,29 +255,29 @@ describe('buildExecutionTree - fixture', () => {
252
255
  it('TC-m3b-flat-fallback:旧机制全无 parentRecordId → 全挂 main(扁平)+ flat-fallback', async () => {
253
256
  await writeMainSession(dir, '--main-cwd--', MAIN)
254
257
  // 3 个顶层 subagent,全无 parentRecordId(旧机制)
255
- const saA = await writeSubagentSession(dir, '--main-cwd--', A_REAL, {
258
+ const saA = await writeSubagentSession(dir, MAIN, A_REAL, {
256
259
  rootSessionId: MAIN,
257
260
  slug: 'a',
258
261
  })
259
- await writeRecordManifest(dir, '--main-cwd--', 'sa-A', {
262
+ await writeRecordManifest(dir, MAIN, 'sa-A', {
260
263
  rootSessionId: MAIN,
261
264
  agentName: 'explorer',
262
265
  sessionFile: saA,
263
266
  })
264
- const saB = await writeSubagentSession(dir, '--main-cwd--', B_REAL, {
267
+ const saB = await writeSubagentSession(dir, MAIN, B_REAL, {
265
268
  rootSessionId: MAIN,
266
269
  slug: 'b',
267
270
  })
268
- await writeRecordManifest(dir, '--main-cwd--', 'sa-B', {
271
+ await writeRecordManifest(dir, MAIN, 'sa-B', {
269
272
  rootSessionId: MAIN,
270
273
  agentName: 'explorer',
271
274
  sessionFile: saB,
272
275
  })
273
- const saC = await writeSubagentSession(dir, '--main-cwd--', C_REAL, {
276
+ const saC = await writeSubagentSession(dir, MAIN, C_REAL, {
274
277
  rootSessionId: MAIN,
275
278
  slug: 'c',
276
279
  })
277
- await writeRecordManifest(dir, '--main-cwd--', 'sa-C', {
280
+ await writeRecordManifest(dir, MAIN, 'sa-C', {
278
281
  rootSessionId: MAIN,
279
282
  agentName: 'explorer',
280
283
  sessionFile: saC,
@@ -294,21 +297,21 @@ describe('buildExecutionTree - fixture', () => {
294
297
  it('TC-m3b-cycle-detection:workflow 指针环 A→B→A,visited Set 防环 + truncated', async () => {
295
298
  await writeMainSession(dir, '--main-cwd--', MAIN)
296
299
  // A(顶层),A 的 workflow call → SB
297
- const saA = await writeSubagentSession(dir, '--main-cwd--', A_REAL, {
300
+ const saA = await writeSubagentSession(dir, MAIN, A_REAL, {
298
301
  rootSessionId: MAIN,
299
302
  slug: 'a',
300
303
  })
301
- await writeRecordManifest(dir, '--main-cwd--', 'sa-A', {
304
+ await writeRecordManifest(dir, MAIN, 'sa-A', {
302
305
  rootSessionId: MAIN,
303
306
  agentName: 'explorer',
304
307
  sessionFile: saA,
305
308
  })
306
309
  // B(顶层),B 的 workflow call → SA(指回 A,成环)
307
- const saB = await writeSubagentSession(dir, '--main-cwd--', B_REAL, {
310
+ const saB = await writeSubagentSession(dir, MAIN, B_REAL, {
308
311
  rootSessionId: MAIN,
309
312
  slug: 'b',
310
313
  })
311
- await writeRecordManifest(dir, '--main-cwd--', 'sa-B', {
314
+ await writeRecordManifest(dir, MAIN, 'sa-B', {
312
315
  rootSessionId: MAIN,
313
316
  agentName: 'explorer',
314
317
  sessionFile: saB,
@@ -346,44 +349,44 @@ describe('buildExecutionTree - fixture', () => {
346
349
  it('TC-m3b-source-priority:parentRecordId 三级数据源 manifest>identity>flat(DM4)', async () => {
347
350
  await writeMainSession(dir, '--main-cwd--', MAIN)
348
351
  // X:顶层 subagent(挂 main)
349
- const saX = await writeSubagentSession(dir, '--main-cwd--', A_REAL, {
352
+ const saX = await writeSubagentSession(dir, MAIN, A_REAL, {
350
353
  rootSessionId: MAIN,
351
354
  slug: 'x',
352
355
  })
353
- await writeRecordManifest(dir, '--main-cwd--', 'sa-X', {
356
+ await writeRecordManifest(dir, MAIN, 'sa-X', {
354
357
  rootSessionId: MAIN,
355
358
  agentName: 'explorer',
356
359
  sessionFile: saX,
357
360
  })
358
361
  // Y:manifest.parentRecordId=sa-X(数据源 ①,精确挂 X)
359
- const saY = await writeSubagentSession(dir, '--main-cwd--', B_REAL, {
362
+ const saY = await writeSubagentSession(dir, MAIN, B_REAL, {
360
363
  rootSessionId: MAIN,
361
364
  slug: 'y',
362
365
  })
363
- await writeRecordManifest(dir, '--main-cwd--', 'sa-Y', {
366
+ await writeRecordManifest(dir, MAIN, 'sa-Y', {
364
367
  rootSessionId: MAIN,
365
368
  agentName: 'worker',
366
369
  sessionFile: saY,
367
370
  parentRecordId: 'sa-X',
368
371
  })
369
372
  // Z:manifest 无 parentRecordId,但 identity.data.parentRecordId=sa-X(数据源 ②,精确挂 X)
370
- const saZ = await writeSubagentSession(dir, '--main-cwd--', C_REAL, {
373
+ const saZ = await writeSubagentSession(dir, MAIN, C_REAL, {
371
374
  rootSessionId: MAIN,
372
375
  slug: 'z',
373
376
  parentRecordId: 'sa-X', // 写入 identity.data.parentRecordId
374
377
  })
375
- await writeRecordManifest(dir, '--main-cwd--', 'sa-Z', {
378
+ await writeRecordManifest(dir, MAIN, 'sa-Z', {
376
379
  rootSessionId: MAIN,
377
380
  agentName: 'worker',
378
381
  sessionFile: saZ,
379
382
  // manifest 无 parentRecordId → 走 ② identity
380
383
  })
381
384
  // W:manifest + identity 都无 parentRecordId(数据源 ③,flat 挂 main)
382
- const saW = await writeSubagentSession(dir, '--main-cwd--', '000000000005', {
385
+ const saW = await writeSubagentSession(dir, MAIN, '000000000005', {
383
386
  rootSessionId: MAIN,
384
387
  slug: 'w',
385
388
  })
386
- await writeRecordManifest(dir, '--main-cwd--', 'sa-W', {
389
+ await writeRecordManifest(dir, MAIN, 'sa-W', {
387
390
  rootSessionId: MAIN,
388
391
  agentName: 'worker',
389
392
  sessionFile: saW,
@@ -417,12 +420,12 @@ describe('buildExecutionTree - fixture', () => {
417
420
  it('TC-m3b-mf1-dedup:call session 有 manifest(parentRecordId 链 + wf 指针双命中)只出现一次(MF-1)', async () => {
418
421
  await writeMainSession(dir, '--main-cwd--', MAIN)
419
422
  // A(顶层,parentRecordId=undefined),sessionFile 含 workflow-state-link → call session B
420
- const saA = await writeSubagentSession(dir, '--main-cwd--', A_REAL, {
423
+ const saA = await writeSubagentSession(dir, MAIN, A_REAL, {
421
424
  rootSessionId: MAIN,
422
425
  slug: 'sub-a',
423
426
  })
424
427
  const recAId = 'sa-record-A'
425
- await writeRecordManifest(dir, '--main-cwd--', recAId, {
428
+ await writeRecordManifest(dir, MAIN, recAId, {
426
429
  rootSessionId: MAIN,
427
430
  agentName: 'explorer',
428
431
  sessionFile: saA,
@@ -431,16 +434,13 @@ describe('buildExecutionTree - fixture', () => {
431
434
  })
432
435
  // B:A 的 subagent 后代(parentRecordId=recA)+ A 的 workflow call 目标(双命中,MF-1 场景)
433
436
  const sbB = join(
434
- dir,
435
- 'subagents',
436
- '--main-cwd--',
437
- 'sessions',
437
+ resolveStoreSubagentSessionsDir(dir, MAIN),
438
438
  `2026-08-07T16-49-48-393Z_${B_REAL}.jsonl`,
439
439
  )
440
- await mkdir(join(dir, 'subagents', '--main-cwd--', 'sessions'), { recursive: true })
440
+ await mkdir(resolveStoreSubagentSessionsDir(dir, MAIN), { recursive: true })
441
441
  await writeFile(sbB, JSON.stringify({ type: 'session', id: B_REAL, cwd: '/proj/wf' }) + '\n')
442
442
  const recBId = 'sa-record-B'
443
- await writeRecordManifest(dir, '--main-cwd--', recBId, {
443
+ await writeRecordManifest(dir, MAIN, recBId, {
444
444
  rootSessionId: MAIN,
445
445
  agentName: 'worker',
446
446
  sessionFile: sbB,
@@ -448,11 +448,11 @@ describe('buildExecutionTree - fixture', () => {
448
448
  parentRecordId: recAId,
449
449
  })
450
450
  // D:B 的 subagent 后代(验证 call session 内派生的嵌套 sub-subagent 仍正确挂到 B 下)
451
- const saD = await writeSubagentSession(dir, '--main-cwd--', D_REAL, {
451
+ const saD = await writeSubagentSession(dir, MAIN, D_REAL, {
452
452
  rootSessionId: MAIN,
453
453
  slug: 'sub-d',
454
454
  })
455
- await writeRecordManifest(dir, '--main-cwd--', 'sa-record-D', {
455
+ await writeRecordManifest(dir, MAIN, 'sa-record-D', {
456
456
  rootSessionId: MAIN,
457
457
  agentName: 'worker',
458
458
  sessionFile: saD,
@@ -490,23 +490,23 @@ describe('buildExecutionTree - fixture', () => {
490
490
  it('TC-m3b-mf2-subtree:buildExecutionTree(subagentId) 切该 subagent 子树(MF-2,§5.5)', async () => {
491
491
  await writeMainSession(dir, '--main-cwd--', MAIN)
492
492
  // A(顶层)+ X(顶层,A 的兄弟,不应进 A 的子树)
493
- const saA = await writeSubagentSession(dir, '--main-cwd--', A_REAL, {
493
+ const saA = await writeSubagentSession(dir, MAIN, A_REAL, {
494
494
  rootSessionId: MAIN,
495
495
  slug: 'sub-a',
496
496
  })
497
497
  const recAId = 'sa-record-A'
498
- await writeRecordManifest(dir, '--main-cwd--', recAId, {
498
+ await writeRecordManifest(dir, MAIN, recAId, {
499
499
  rootSessionId: MAIN,
500
500
  agentName: 'explorer',
501
501
  sessionFile: saA,
502
502
  status: 'completed',
503
503
  parentRecordId: undefined,
504
504
  })
505
- const saX = await writeSubagentSession(dir, '--main-cwd--', X_REAL, {
505
+ const saX = await writeSubagentSession(dir, MAIN, X_REAL, {
506
506
  rootSessionId: MAIN,
507
507
  slug: 'sub-x',
508
508
  })
509
- await writeRecordManifest(dir, '--main-cwd--', 'sa-record-X', {
509
+ await writeRecordManifest(dir, MAIN, 'sa-record-X', {
510
510
  rootSessionId: MAIN,
511
511
  agentName: 'explorer',
512
512
  sessionFile: saX,
@@ -514,23 +514,23 @@ describe('buildExecutionTree - fixture', () => {
514
514
  parentRecordId: undefined,
515
515
  })
516
516
  // B(A 的后代),C(B 的后代)
517
- const saB = await writeSubagentSession(dir, '--main-cwd--', B_REAL, {
517
+ const saB = await writeSubagentSession(dir, MAIN, B_REAL, {
518
518
  rootSessionId: MAIN,
519
519
  slug: 'sub-b',
520
520
  })
521
521
  const recBId = 'sa-record-B'
522
- await writeRecordManifest(dir, '--main-cwd--', recBId, {
522
+ await writeRecordManifest(dir, MAIN, recBId, {
523
523
  rootSessionId: MAIN,
524
524
  agentName: 'worker',
525
525
  sessionFile: saB,
526
526
  status: 'completed',
527
527
  parentRecordId: recAId,
528
528
  })
529
- const saC = await writeSubagentSession(dir, '--main-cwd--', C_REAL, {
529
+ const saC = await writeSubagentSession(dir, MAIN, C_REAL, {
530
530
  rootSessionId: MAIN,
531
531
  slug: 'sub-c',
532
532
  })
533
- await writeRecordManifest(dir, '--main-cwd--', 'sa-record-C', {
533
+ await writeRecordManifest(dir, MAIN, 'sa-record-C', {
534
534
  rootSessionId: MAIN,
535
535
  agentName: 'worker',
536
536
  sessionFile: saC,
@@ -565,13 +565,10 @@ describe('buildExecutionTree - fixture', () => {
565
565
  const mainPath = await writeMainSession(dir, '--main-cwd--', MAIN)
566
566
  // main 的 v2 注册条目 → record 流 → call session B
567
567
  const sbB = join(
568
- dir,
569
- 'subagents',
570
- '--main-cwd--',
571
- 'sessions',
568
+ resolveStoreSubagentSessionsDir(dir, MAIN),
572
569
  `2026-08-07T16-49-48-393Z_${B_REAL}.jsonl`,
573
570
  )
574
- await mkdir(join(dir, 'subagents', '--main-cwd--', 'sessions'), { recursive: true })
571
+ await mkdir(resolveStoreSubagentSessionsDir(dir, MAIN), { recursive: true })
575
572
  await writeFile(sbB, JSON.stringify({ type: 'session', id: B_REAL, cwd: '/proj/wf' }) + '\n')
576
573
  await writeWfV2Run(dir, '--main-cwd--', mainPath, MAIN, RUN_A, [sbB])
577
574
 
@@ -588,7 +585,7 @@ describe('buildExecutionTree - fixture', () => {
588
585
  expect(tree.maxDepth).toBe(1)
589
586
  expect(tree.truncated).toBe(false)
590
587
 
591
- // 差分守卫:不传 mainSessionFile(旧行为)→ main root 无 sessionFile → workflow run 不可达
588
+ // 差分检查:不传 mainSessionFile(旧行为)→ main root 无 sessionFile → workflow run 不可达
592
589
  const tree2 = await buildExecutionTree(MAIN, dir)
593
590
  expect(tree2.root.sessionFile).toBeUndefined()
594
591
  expect(tree2.totalNodes).toBe(1)
@@ -612,11 +609,11 @@ describe('buildExecutionTree - fixture', () => {
612
609
  const recIdOf = (n: number): string => `sa-chain-${String(n).padStart(2, '0')}`
613
610
  let prevRecId: string | undefined
614
611
  for (let i = 1; i <= CHAIN_RECORDS; i++) {
615
- const saPath = await writeSubagentSession(dir, '--main-cwd--', sidOf(i), {
612
+ const saPath = await writeSubagentSession(dir, MAIN, sidOf(i), {
616
613
  rootSessionId: MAIN,
617
614
  slug: `chain-${i}`,
618
615
  })
619
- await writeRecordManifest(dir, '--main-cwd--', recIdOf(i), {
616
+ await writeRecordManifest(dir, MAIN, recIdOf(i), {
620
617
  rootSessionId: MAIN,
621
618
  agentName: 'explorer',
622
619
  sessionFile: saPath,
@@ -644,11 +641,11 @@ describe('buildExecutionTree - fixture', () => {
644
641
  it('TC-m3b-bfs-spread:旧机制嵌套形态 rootSessionId=父 session id,BFS 扩散收集不丢 record(MF-5)', async () => {
645
642
  await writeMainSession(dir, '--main-cwd--', MAIN)
646
643
  // A:顶层(rootSessionId=MAIN,无 parentRecordId——旧机制)
647
- const saA = await writeSubagentSession(dir, '--main-cwd--', A_REAL, {
644
+ const saA = await writeSubagentSession(dir, MAIN, A_REAL, {
648
645
  rootSessionId: MAIN,
649
646
  slug: 'a',
650
647
  })
651
- await writeRecordManifest(dir, '--main-cwd--', 'sa-A', {
648
+ await writeRecordManifest(dir, MAIN, 'sa-A', {
652
649
  rootSessionId: MAIN,
653
650
  agentName: 'explorer',
654
651
  sessionFile: saA,
@@ -658,7 +655,7 @@ describe('buildExecutionTree - fixture', () => {
658
655
  rootSessionId: A_REAL,
659
656
  slug: 'b',
660
657
  })
661
- await writeRecordManifest(dir, '--main-cwd--', 'sa-B', {
658
+ await writeRecordManifest(dir, MAIN, 'sa-B', {
662
659
  rootSessionId: A_REAL,
663
660
  agentName: 'explorer',
664
661
  sessionFile: saB,
@@ -681,18 +678,20 @@ describe('buildExecutionTree - fixture', () => {
681
678
  })
682
679
 
683
680
  // ============================================================
684
- // 真实形态数据守卫(合成 fixture):mkdtemp 临时目录仿真真实 agentDir 布局,
681
+ // 真实形态数据检查(合成 fixture):mkdtemp 临时目录仿真真实 agentDir 布局,
685
682
  // guard 语义断言(解析不抛错 / 树非空 / 结构自洽 / sourceMode 值域),不触碰真实数据目录。
686
683
  // ============================================================
687
684
 
688
685
  describe('buildExecutionTree - 真实形态数据守卫(合成)', () => {
689
- let dir: string
686
+ let dataDir: string
687
+ let dir: string // agentDir = <dataDir>/agent(生产拓扑复刻)
690
688
 
691
689
  beforeEach(async () => {
692
- dir = await makeAgentDir()
690
+ dataDir = await makeAgentDir()
691
+ dir = join(dataDir, 'agent')
693
692
  })
694
693
  afterEach(async () => {
695
- await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
694
+ await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
696
695
  })
697
696
 
698
697
  it('TC-m3b-real-data-guard:真实形态布局(时间戳文件名/新旧机制混合/孤儿 manifest/坏 manifest/外来家族)解析不抛错,树非空且结构自洽', async () => {
@@ -712,12 +711,12 @@ describe('buildExecutionTree - 真实形态数据守卫(合成)', () => {
712
711
  // A:顶层 subagent(identity 尾行走数据源 ② 读路径,无 parentRecordId → 顶层挂 main)
713
712
  const saA = await writeSubagentSession(
714
713
  dir,
715
- CWD,
714
+ MAIN,
716
715
  A_REAL,
717
716
  { rootSessionId: MAIN, slug: 'guard-a' },
718
717
  prefixed(A_REAL),
719
718
  )
720
- await writeRecordManifest(dir, CWD, 'rec-ga', {
719
+ await writeRecordManifest(dir, MAIN, 'rec-ga', {
721
720
  rootSessionId: MAIN,
722
721
  agentName: 'explorer',
723
722
  sessionFile: saA,
@@ -727,12 +726,12 @@ describe('buildExecutionTree - 真实形态数据守卫(合成)', () => {
727
726
  // B:A 的嵌套后代(manifest.parentRecordId 精确链,新机制)
728
727
  const saB = await writeSubagentSession(
729
728
  dir,
730
- CWD,
729
+ MAIN,
731
730
  B_REAL,
732
731
  { rootSessionId: MAIN, slug: 'guard-b' },
733
732
  prefixed(B_REAL),
734
733
  )
735
- await writeRecordManifest(dir, CWD, 'rec-gb', {
734
+ await writeRecordManifest(dir, MAIN, 'rec-gb', {
736
735
  rootSessionId: MAIN,
737
736
  agentName: 'worker',
738
737
  sessionFile: saB,
@@ -743,12 +742,12 @@ describe('buildExecutionTree - 真实形态数据守卫(合成)', () => {
743
742
  // C:旧机制顶层 subagent(manifest/identity 均无 parentRecordId → flat 挂 main)
744
743
  const saC = await writeSubagentSession(
745
744
  dir,
746
- CWD,
745
+ MAIN,
747
746
  C_REAL,
748
747
  { rootSessionId: MAIN, slug: 'guard-c' },
749
748
  prefixed(C_REAL),
750
749
  )
751
- await writeRecordManifest(dir, CWD, 'rec-gc', {
750
+ await writeRecordManifest(dir, MAIN, 'rec-gc', {
752
751
  rootSessionId: MAIN,
753
752
  agentName: 'explorer',
754
753
  sessionFile: saC,
@@ -756,24 +755,24 @@ describe('buildExecutionTree - 真实形态数据守卫(合成)', () => {
756
755
  status: 'completed',
757
756
  })
758
757
  // D:孤儿 manifest(sessionFile 指向已被 GC 的不存在文件——manifest 残留形态)
759
- await writeRecordManifest(dir, CWD, 'rec-gd', {
758
+ await writeRecordManifest(dir, MAIN, 'rec-gd', {
760
759
  rootSessionId: MAIN,
761
760
  agentName: 'worker',
762
- sessionFile: join(dir, 'subagents', CWD, 'sessions', prefixed(D_REAL)),
761
+ sessionFile: join(resolveStoreSubagentSessionsDir(dir, MAIN), prefixed(D_REAL)),
763
762
  status: 'completed',
764
763
  })
765
764
  // 坏 manifest(JSON 损坏,listRecordManifests 容错跳过不中断)
766
- await mkdir(join(dir, 'subagents', CWD, 'records'), { recursive: true })
767
- await writeFile(join(dir, 'subagents', CWD, 'records', 'broken.json'), '{oops')
765
+ await mkdir(resolveStoreSubagentRecordsDir(dir, MAIN), { recursive: true })
766
+ await writeFile(join(resolveStoreSubagentRecordsDir(dir, MAIN), 'broken.json'), '{oops')
768
767
  // 外来家族 X:同 agentDir 内其他 main(OTHER_MAIN)的 subagent,不应泄漏进 MAIN 的树
769
768
  const saX = await writeSubagentSession(
770
769
  dir,
771
- CWD,
770
+ OTHER_MAIN,
772
771
  X_REAL,
773
772
  { rootSessionId: OTHER_MAIN, slug: 'foreign-x' },
774
773
  prefixed(X_REAL),
775
774
  )
776
- await writeRecordManifest(dir, CWD, 'rec-gx', {
775
+ await writeRecordManifest(dir, MAIN, 'rec-gx', {
777
776
  rootSessionId: OTHER_MAIN,
778
777
  agentName: 'explorer',
779
778
  sessionFile: saX,
@@ -786,16 +785,16 @@ describe('buildExecutionTree - 真实形态数据守卫(合成)', () => {
786
785
  // 全无 → flat-fallback),不锁具体值
787
786
  expect(['precise', 'flat-fallback']).toContain(tree.sourceMode)
788
787
  // 家族树非空:main + 顶层 A/C/D + A 的嵌套后代 B = 5;外来 X 不泄漏(合成数据确定性
789
- // 可数,精确计数即非泄漏守卫)
788
+ // 可数,精确计数即非泄漏检查)
790
789
  expect(tree.totalNodes).toBe(5)
791
790
  expect(tree.root.type).toBe('main')
792
791
  expect(tree.root.sessionId).toBe(MAIN)
793
792
  expect(findNode(tree.root, 'subagent', X_REAL)).toBeUndefined()
794
- // 孤儿 D 仍入树(sessionFile 残留路径,不因文件缺失抛错/丢弃 record——原真实数据守卫
795
- // 独有覆盖面,合成化后保留)
793
+ // 孤儿 D 仍入树(sessionFile 残留路径,不因文件缺失抛错/丢弃 record——原真实数据检查
794
+ // 独有覆盖范围,合成化后保留)
796
795
  const nodeD = findNode(tree.root, 'subagent', D_REAL)
797
796
  expect(nodeD).toBeDefined()
798
- expect(nodeD!.sessionFile).toBe(join(dir, 'subagents', CWD, 'sessions', prefixed(D_REAL)))
797
+ expect(nodeD!.sessionFile).toBe(join(resolveStoreSubagentSessionsDir(dir, MAIN), prefixed(D_REAL)))
799
798
  // 树结构自洽(ExecutionTreeNode 契约,与具体数据形态无关):DFS 实际节点数 ===
800
799
  // totalNodes;子节点 depth = 父 depth + 1;全树 rootSessionId 共享同一顶层 main
801
800
  let count = 0
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'
4
- import { join } from 'node:path'
4
+ import { join, dirname } from 'node:path'
5
5
  import { DatabaseSync } from 'node:sqlite'
6
6
 
7
7
  import { buildFamilyIndex, resolveFamily, buildZcodeSubagentsByRoot } from '../core/family.js'
@@ -17,7 +17,7 @@ import { listZcodeManifests } from '../discovery/zcode-manifest.js'
17
17
  // 行被删 → 仍 false)即「列表视图不开库」的分工声明断言。
18
18
  //
19
19
  // 与 pi 判据不同构(D5-1 显式声明):pi = fileStats JSONL 存在性;zcode 节点无
20
- // JSONL 文件,按「库可达」判。pi 侧既有 fixture 行为零变化由 family.test.ts 全绿守卫
20
+ // JSONL 文件,按「库可达」判。pi 侧既有 fixture 行为零变化由 family.test.ts 全绿检查
21
21
  // (本文件零改动 family.test.ts——更强的零涟漪证据)。
22
22
  // ============================================================
23
23
 
@@ -47,13 +47,13 @@ function zcodeNode(opts: Partial<ZcodeFamilyNode> & { id: string }): ZcodeFamily
47
47
  }
48
48
  }
49
49
 
50
- /** 在 <agentDir>/subagents/<slug>/records/ 下写 zcode manifest(F21 磁盘真实形态:无 sessionFile 键) */
50
+ /** 在存储树分区 <root>/<rootSessionId>/subagents/records/ 下写 zcode manifest(F21 磁盘真实形态:无 sessionFile 键;[session-store-layout-inversion D6/单新根] 读侧已换存储树) */
51
51
  function writeZcodeManifest(
52
52
  agentDir: string,
53
53
  saId: string,
54
54
  sessionRef: Record<string, string>,
55
55
  ): void {
56
- const recordsDir = join(agentDir, 'subagents', 'proj-a', 'records')
56
+ const recordsDir = join(dirname(agentDir), 'ext-sessions', ROOT, 'subagents', 'records')
57
57
  mkdirSync(recordsDir, { recursive: true })
58
58
  writeFileSync(
59
59
  join(recordsDir, `${saId}.json`),
@@ -167,11 +167,15 @@ describe('core:buildZcodeSubagentsByRoot / buildFamilyIndex 的 zcode 分支
167
167
 
168
168
  describe('集成:listZcodeManifests → buildFamilyIndex → resolveFamily(场景 2 两态 + 反向断言)', () => {
169
169
  it('库可达态(dbPath 指向真实 fixture 库)→ cleanedUp=false', async () => {
170
+ // agentDir 两层化(<mkdtemp>/agent):存储树推导 = dirname(agentDir)/ext-sessions,
171
+ // 单层直接当 agentDir 会把扫描根泄到 tmpdir 公共层(跨测试污染——先例 reaper 同修)
172
+ const agentDir = join(dir, 'agent')
173
+ mkdirSync(agentDir, { recursive: true })
170
174
  const dbPath = join(dir, 'isolated.sqlite')
171
175
  createFixtureDb(dbPath, 'sess_live')
172
- writeZcodeManifest(dir, 'sa-live', { sessionId: 'sess_live', dbPath })
176
+ writeZcodeManifest(agentDir, 'sa-live', { sessionId: 'sess_live', dbPath })
173
177
 
174
- const nodes = await listZcodeManifests(dir)
178
+ const nodes = await listZcodeManifests(agentDir)
175
179
  expect(nodes).toHaveLength(1)
176
180
  expect(nodes[0]?.dbFileExists).toBe(true)
177
181
 
@@ -181,12 +185,14 @@ describe('集成:listZcodeManifests → buildFamilyIndex → resolveFamily(
181
185
  })
182
186
 
183
187
  it('库不存在态(dbPath 指向不存在路径)→ cleanedUp=true(GC 语义,不报错)', async () => {
184
- writeZcodeManifest(dir, 'sa-gone', {
188
+ const agentDir = join(dir, 'agent')
189
+ mkdirSync(agentDir, { recursive: true })
190
+ writeZcodeManifest(agentDir, 'sa-gone', {
185
191
  sessionId: 'sess_gone',
186
192
  dbPath: join(dir, 'subagents', 'proj-a', 'missing.sqlite'),
187
193
  })
188
194
 
189
- const nodes = await listZcodeManifests(dir)
195
+ const nodes = await listZcodeManifests(agentDir)
190
196
  expect(nodes[0]?.dbFileExists).toBe(false)
191
197
 
192
198
  const family = resolveFamily(ROOT, buildFamilyIndex([header(ROOT)], [], new Map(), nodes))
@@ -196,9 +202,11 @@ describe('集成:listZcodeManifests → buildFamilyIndex → resolveFamily(
196
202
  it('反向断言:库文件在但库内 session 行被删 → 仍 cleanedUp=false(列表视图不开库的分工声明)', async () => {
197
203
  // 分工(D5-1):库内 session 已被 zcode GC 的情形由 read 路径 zcode_session_not_found
198
204
  // 权威承接;family 只 stat 文件存在性。本用例删行留文件,断言 family 不受库内容影响。
205
+ const agentDir = join(dir, 'agent')
206
+ mkdirSync(agentDir, { recursive: true })
199
207
  const dbPath = join(dir, 'gc.sqlite')
200
208
  createFixtureDb(dbPath, 'sess_victim')
201
- writeZcodeManifest(dir, 'sa-gced', { sessionId: 'sess_victim', dbPath })
209
+ writeZcodeManifest(agentDir, 'sa-gced', { sessionId: 'sess_victim', dbPath })
202
210
 
203
211
  // 前置核实:fixture 库里确实有该 session 行(反向断言的非平凡性前提)
204
212
  {
@@ -225,7 +233,7 @@ describe('集成:listZcodeManifests → buildFamilyIndex → resolveFamily(
225
233
  }
226
234
  }
227
235
 
228
- const nodes = await listZcodeManifests(dir)
236
+ const nodes = await listZcodeManifests(agentDir)
229
237
  expect(nodes[0]?.dbFileExists).toBe(true) // 只 stat:文件在即 true,不看内容
230
238
 
231
239
  const family = resolveFamily(ROOT, buildFamilyIndex([header(ROOT)], [], new Map(), nodes))
@@ -235,17 +243,19 @@ describe('集成:listZcodeManifests → buildFamilyIndex → resolveFamily(
235
243
  it('坏 manifest 与缺库节点混排 → 好节点照常输出(单节点不可达不炸整屏)', async () => {
236
244
  const dbPath = join(dir, 'ok.sqlite')
237
245
  createFixtureDb(dbPath, 'sess_ok')
238
- writeZcodeManifest(dir, 'sa-ok', { sessionId: 'sess_ok', dbPath })
246
+ const agentDir = join(dir, 'agent')
247
+ mkdirSync(agentDir, { recursive: true })
248
+ writeZcodeManifest(agentDir, 'sa-ok', { sessionId: 'sess_ok', dbPath })
239
249
  // 缺库节点:合法 zcode 形态(dbFileExists=false,不抛错),不剔除出列表
240
- writeZcodeManifest(dir, 'sa-broken-manifest', { sessionId: 'sess_x', dbPath: '/nonexistent/x.sqlite' })
250
+ writeZcodeManifest(agentDir, 'sa-broken-manifest', { sessionId: 'sess_x', dbPath: '/nonexistent/x.sqlite' })
241
251
  // 坏 JSON manifest:枚举跳过(不中断)
242
252
  writeFileSync(
243
- join(dir, 'subagents', 'proj-a', 'records', 'sa-corrupt.json'),
253
+ join(dirname(agentDir), 'ext-sessions', ROOT, 'subagents', 'records', 'sa-corrupt.json'),
244
254
  '{{{broken',
245
255
  'utf8',
246
256
  )
247
257
 
248
- const nodes = await listZcodeManifests(dir)
258
+ const nodes = await listZcodeManifests(agentDir)
249
259
  expect(nodes.map((n) => n.id).sort()).toEqual(['sa-broken-manifest', 'sa-ok'])
250
260
 
251
261
  const family = resolveFamily(ROOT, buildFamilyIndex([header(ROOT)], [], new Map(), nodes))