@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
@@ -9,6 +9,11 @@ import {
9
9
  type SessionReadParams,
10
10
  type SessionReadSignals,
11
11
  } from '../tool-handler.js'
12
+ import {
13
+ resolveSessionStoreRoot,
14
+ resolveStoreSubagentSessionsDir,
15
+ resolveStoreSubagentRecordsDir,
16
+ } from '../discovery/roots.js'
12
17
  import { setPiHandle } from '@zhushanwen/pi-extension-logger'
13
18
  import { type DoctorDetails } from '../doctor.js'
14
19
  import { levenshtein } from '../no-match.js'
@@ -41,7 +46,8 @@ vi.mock('node:os', async (importOriginal) => {
41
46
  */
42
47
 
43
48
  describe('handleSessionRead', () => {
44
- let dir: string
49
+ let dataDir: string
50
+ let dir: string // agentDir = <dataDir>/agent(生产拓扑复刻)
45
51
  const SLUG = '--demo-cwd--'
46
52
  // 家族 fixture 组:根 FAM → fork 子代 FORK(跨 cwd,parentSession 指 FAM 文件)
47
53
  // → 隔代 subagent SUB2(rootSessionId=FORK)。前缀互异,断言按 id 精确区分层级。
@@ -106,11 +112,12 @@ describe('handleSessionRead', () => {
106
112
  }
107
113
 
108
114
  beforeEach(async () => {
109
- dir = await mkdtemp(join(tmpdir(), 'tool-handler-main-'))
115
+ dataDir = await mkdtemp(join(tmpdir(), 'tool-handler-main-'))
116
+ dir = join(dataDir, 'agent')
110
117
  await writeMainFixtures()
111
118
  })
112
119
  afterEach(async () => {
113
- await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
120
+ await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
114
121
  })
115
122
 
116
123
  it('1. find by uuid fragment returns matching session', async () => {
@@ -158,21 +165,22 @@ describe('handleSessionRead', () => {
158
165
  expect(typeof d.hits[0].matchSnippet).toBe('string')
159
166
  })
160
167
 
161
- it('6. export outline materializes a .md file', async () => {
162
- // export 写目标 = <agentDir>/tmp/(mkdtemp 自建自删);读侧经 liveSessionDir=[live]
163
- // 根扫本 describe 的 fixture sessions 目录。
164
- const exportDir = await mkdtemp(join(tmpdir(), 'tool-handler-export-'))
168
+ it('6. export outline materializes a .md file under the session store tree', async () => {
169
+ // export 写目标 = <dataDir>/ext-sessions/<sid>/session-view.md(session-store-layout-
170
+ // inversion 域 6):agentDir 参数 = <dataDir>/agent(mkdtemp tmp 子目录,导出树随 tmp
171
+ // 整体清理);读侧经 liveSessionDir=[live] 根扫本 describe 的 fixture sessions 目录。
172
+ const dataDir = await mkdtemp(join(tmpdir(), 'tool-handler-export-'))
165
173
  try {
166
174
  const r = await handleSessionRead(
167
175
  { action: 'export', session: FIX_E6, format: 'outline' },
168
- { agentDir: exportDir, liveSessionDir: join(dir, 'sessions') },
176
+ { agentDir: join(dataDir, 'agent'), liveSessionDir: join(dir, 'sessions') },
169
177
  )
170
178
  const d = r.details as { path: string; sizeBytes: number }
171
- expect(d.path).toMatch(/\.md$/)
179
+ expect(d.path).toBe(join(dataDir, 'ext-sessions', FIX_E6, 'session-view.md'))
172
180
  expect(existsSync(d.path)).toBe(true)
173
181
  expect(d.sizeBytes).toBeGreaterThan(0)
174
182
  } finally {
175
- await rm(exportDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
183
+ await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
176
184
  }
177
185
  })
178
186
 
@@ -213,6 +221,7 @@ describe('handleSessionRead', () => {
213
221
 
214
222
  describe('extract (v2 O4)', () => {
215
223
  let dir: string
224
+ let dataDir: string
216
225
  const SLUG = '--demo-cwd--'
217
226
 
218
227
  /**
@@ -308,11 +317,12 @@ describe('extract (v2 O4)', () => {
308
317
  }
309
318
 
310
319
  beforeEach(async () => {
311
- dir = await mkdtemp(join(tmpdir(), 'tool-handler-extract-'))
320
+ dataDir = await mkdtemp(join(tmpdir(), 'tool-handler-extract-'))
321
+ dir = join(dataDir, 'agent')
312
322
  await writeExtractFixture()
313
323
  })
314
324
  afterEach(async () => {
315
- await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
325
+ await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
316
326
  })
317
327
 
318
328
  it('user-messages returns all user entries with turn + full text', async () => {
@@ -514,13 +524,15 @@ async function makeFixtureSession(
514
524
  }
515
525
 
516
526
  describe('F2 多匹配消歧(fixture,MF-9)', () => {
517
- let dir: string
527
+ let dataDir: string
528
+ let dir: string // agentDir = <dataDir>/agent(生产拓扑复刻,[session-store-layout-inversion])
518
529
 
519
530
  beforeEach(async () => {
520
- dir = await mkdtemp(join(tmpdir(), 'tool-handler-f2-'))
531
+ dataDir = await mkdtemp(join(tmpdir(), 'tool-handler-f2-'))
532
+ dir = join(dataDir, 'agent')
521
533
  })
522
534
  afterEach(async () => {
523
- await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
535
+ await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
524
536
  })
525
537
 
526
538
  it('共享 uuid 片段 → 不抛错,content 含两候选 + 👉,details.ambiguous=true', async () => {
@@ -564,13 +576,15 @@ describe('F2 多匹配消歧(fixture,MF-9)', () => {
564
576
 
565
577
  describe('outline skippedLines 报告(fixture,D8d 有检测必有报告)', () => {
566
578
  let dir: string
579
+ let dataDir: string
567
580
  const SID = '019e6c96-bbbb-cccc-dddd-00000000000b'
568
581
 
569
582
  beforeEach(async () => {
570
- dir = await mkdtemp(join(tmpdir(), 'tool-handler-skipped-'))
583
+ dataDir = await mkdtemp(join(tmpdir(), 'tool-handler-skipped-'))
584
+ dir = join(dataDir, 'agent')
571
585
  })
572
586
  afterEach(async () => {
573
- await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
587
+ await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
574
588
  })
575
589
 
576
590
  it('坏行计入 stats.skippedLines 且文本尾部可见(不静默跳过)', async () => {
@@ -612,6 +626,7 @@ describe('outline skippedLines 报告(fixture,D8d 有检测必有报告)',
612
626
  // ============================================================
613
627
  describe('E7 行渲染统一(outline 与 export outline 同一渲染行)', () => {
614
628
  let dir: string
629
+ let dataDir: string
615
630
  const SID = '019e6c96-bbbb-cccc-dddd-00000000000e'
616
631
 
617
632
  /** 写含 fork 旁支的 session:主链 R→m1(user)→m2(assistant),b1/b2 挂 m1 下成
@@ -637,10 +652,11 @@ describe('E7 行渲染统一(outline 与 export outline 同一渲染行)', (
637
652
  }
638
653
 
639
654
  beforeEach(async () => {
640
- dir = await mkdtemp(join(tmpdir(), 'tool-handler-e7-'))
655
+ dataDir = await mkdtemp(join(tmpdir(), 'tool-handler-e7-'))
656
+ dir = join(dataDir, 'agent')
641
657
  })
642
658
  afterEach(async () => {
643
- await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
659
+ await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
644
660
  })
645
661
 
646
662
  it('① allBranches:true → 旁支行标记 `[旁支 N entries]`(N=forkPoint 旁支子树 entry 数)+ 尾段同改前语义', async () => {
@@ -677,14 +693,16 @@ describe('E7 行渲染统一(outline 与 export outline 同一渲染行)', (
677
693
 
678
694
  describe('search 灾难性正则降级 + abort(fixture,MF-5 回归)', () => {
679
695
  let dir: string
696
+ let dataDir: string
680
697
  const SID = '019e6c96-bbbb-cccc-dddd-00000000000a'
681
698
 
682
699
  beforeEach(async () => {
683
- dir = await mkdtemp(join(tmpdir(), 'tool-handler-search-'))
700
+ dataDir = await mkdtemp(join(tmpdir(), 'tool-handler-search-'))
701
+ dir = join(dataDir, 'agent')
684
702
  await makeFixtureSession(dir, SID, 'aaa plugin 内容')
685
703
  })
686
704
  afterEach(async () => {
687
- await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
705
+ await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
688
706
  })
689
707
 
690
708
  it('嵌套量词 pattern (a+)+ → 降级字面子串(不挂死,零命中)', async () => {
@@ -786,13 +804,15 @@ describe('search 灾难性正则降级 + abort(fixture,MF-5 回归)', () =
786
804
 
787
805
  describe('extract commits 双路径(fixture:git-cmd 主路径 + commit-context 次路径 + 去重)', () => {
788
806
  let dir: string
807
+ let dataDir: string
789
808
  const SID = '019e6c96-bbbb-cccc-dddd-00000000000c'
790
809
 
791
810
  beforeEach(async () => {
792
- dir = await mkdtemp(join(tmpdir(), 'tool-handler-commits-'))
811
+ dataDir = await mkdtemp(join(tmpdir(), 'tool-handler-commits-'))
812
+ dir = join(dataDir, 'agent')
793
813
  })
794
814
  afterEach(async () => {
795
- await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
815
+ await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
796
816
  })
797
817
 
798
818
  /** 写单个 session JSONL(commits 用例行内联传入)。 */
@@ -975,7 +995,7 @@ describe('renderExtractItems F9 截断(S3 首项超大 + S4 文案)', () =>
975
995
 
976
996
  // ============================================================
977
997
  // w2 新增:resolveSessionId 三形态(① 绝对路径 / ② sa-id / ③ findSessions 透传 source)
978
- // + ES1/ES2 错误契约 + source 透传 + 真实数据守卫(design TC2-TC18)
998
+ // + ES1/ES2 错误契约 + source 透传 + 真实数据检查(design TC2-TC18)
979
999
  // ============================================================
980
1000
 
981
1001
  /**
@@ -996,10 +1016,12 @@ async function makeFixtureSubagent(
996
1016
  slug?: string
997
1017
  },
998
1018
  ): Promise<string> {
999
- const slug = opts.slug ?? '--demo-cwd--'
1000
- const sessionFile = join(dir, 'subagents', slug, 'sessions', `${opts.realSessionId}.jsonl`)
1019
+ // [session-store-layout-inversion] 新布局存储树:分区键 = rootSessionId(无 encodeCwd
1020
+ // 目录层,opts.slug 已无布局语义,保留入参兼容旧调用点但不参与路径推导)
1021
+ const rootSid = opts.rootSessionId ?? 'root-session-1'
1022
+ const sessionFile = join(resolveStoreSubagentSessionsDir(dir, rootSid), `${opts.realSessionId}.jsonl`)
1001
1023
  if (opts.sessionFileExists !== false) {
1002
- await mkdir(join(dir, 'subagents', slug, 'sessions'), { recursive: true })
1024
+ await mkdir(resolveStoreSubagentSessionsDir(dir, rootSid), { recursive: true })
1003
1025
  const lines = [
1004
1026
  JSON.stringify({ type: 'session', id: opts.realSessionId, cwd: '/demo' }),
1005
1027
  JSON.stringify({
@@ -1014,13 +1036,13 @@ async function makeFixtureSubagent(
1014
1036
  ]
1015
1037
  await writeFile(sessionFile, lines.join('\n') + '\n')
1016
1038
  }
1017
- const recordsDir = join(dir, 'subagents', slug, 'records')
1039
+ const recordsDir = resolveStoreSubagentRecordsDir(dir, rootSid)
1018
1040
  await mkdir(recordsDir, { recursive: true })
1019
1041
  await writeFile(
1020
1042
  join(recordsDir, `${saId}.json`),
1021
1043
  JSON.stringify({
1022
1044
  id: saId,
1023
- rootSessionId: opts.rootSessionId ?? 'root-session-1',
1045
+ rootSessionId: rootSid,
1024
1046
  agentName: opts.agentName ?? 'explorer',
1025
1047
  sessionFile,
1026
1048
  }),
@@ -1029,13 +1051,15 @@ async function makeFixtureSubagent(
1029
1051
  }
1030
1052
 
1031
1053
  describe('resolveSessionId ① 绝对路径形态(w2 TC2-TC6)', () => {
1032
- let dir: string
1054
+ let dataDir: string
1055
+ let dir: string // agentDir = <dataDir>/agent(生产拓扑复刻,[session-store-layout-inversion])
1033
1056
 
1034
1057
  beforeEach(async () => {
1035
- dir = await mkdtemp(join(tmpdir(), 'tool-handler-path-'))
1058
+ dataDir = await mkdtemp(join(tmpdir(), 'tool-handler-path-'))
1059
+ dir = join(dataDir, 'agent')
1036
1060
  })
1037
1061
  afterEach(async () => {
1038
- await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
1062
+ await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
1039
1063
  })
1040
1064
 
1041
1065
  it('TC2: 绝对路径 outline → sessionId=header 真实 id(非文件名)', async () => {
@@ -1138,13 +1162,15 @@ describe('resolveSessionId ① 绝对路径形态(w2 TC2-TC6)', () => {
1138
1162
  })
1139
1163
 
1140
1164
  describe('resolveSessionId ② sa-id 形态(w2 TC7-TC10 + CQ3)', () => {
1141
- let dir: string
1165
+ let dataDir: string
1166
+ let dir: string // agentDir = <dataDir>/agent(生产拓扑复刻,[session-store-layout-inversion])
1142
1167
 
1143
1168
  beforeEach(async () => {
1144
- dir = await mkdtemp(join(tmpdir(), 'tool-handler-said-'))
1169
+ dataDir = await mkdtemp(join(tmpdir(), 'tool-handler-said-'))
1170
+ dir = join(dataDir, 'agent')
1145
1171
  })
1146
1172
  afterEach(async () => {
1147
- await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
1173
+ await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
1148
1174
  })
1149
1175
 
1150
1176
  it('TC7: sa-id 恰 1 命中 + sessionFile 存在 → 成功,sessionId=header 真实 id(非 sa-)', async () => {
@@ -1183,7 +1209,7 @@ describe('resolveSessionId ② sa-id 形态(w2 TC7-TC10 + CQ3)', () => {
1183
1209
  }
1184
1210
  })
1185
1211
 
1186
- it('TC9: sa-id 0 命中(可能 running)→ zcode_record_not_found(U9:sa-id 语境统一错误面,含 family 指引 + 👉)', async () => {
1212
+ it('TC9: sa-id 0 命中(可能 running)→ zcode_record_not_found(U9:sa-id 语境统一错误范围,含 family 指引 + 👉)', async () => {
1187
1213
  await expect(
1188
1214
  handleSessionRead({ action: 'outline', session: 'sa-nonexist-9999' }, { agentDir: dir }),
1189
1215
  ).rejects.toThrow('zcode_record_not_found')
@@ -1197,7 +1223,7 @@ describe('resolveSessionId ② sa-id 形态(w2 TC7-TC10 + CQ3)', () => {
1197
1223
  }
1198
1224
  })
1199
1225
 
1200
- it('TC10: sa-id 片段输入(精确相等不命中)→ zcode_record_not_found + 完整 sa- id 重试指引(U9 统一错误面)', async () => {
1226
+ it('TC10: sa-id 片段输入(精确相等不命中)→ zcode_record_not_found + 完整 sa- id 重试指引(U9 统一错误范围)', async () => {
1201
1227
  await makeFixtureSubagent(dir, 'sa-c8c8dfa8', {
1202
1228
  realSessionId: '019e6c96-dddd-eeee-ffff-0000000010a3',
1203
1229
  })
@@ -1212,24 +1238,26 @@ describe('resolveSessionId ② sa-id 形态(w2 TC7-TC10 + CQ3)', () => {
1212
1238
  })
1213
1239
 
1214
1240
  it('sa-id 多 manifest 命中(数据异常)→ ES2 ambiguous(C4)', async () => {
1215
- // 同 sa-id 的两个 manifest(不同 cwd slug 目录,模拟数据异常)
1216
- for (const [slug, realId] of [
1217
- ['--demo-cwd--', '019e6c96-dddd-eeee-ffff-0000000004a4'],
1218
- ['--other-cwd--', '019e6c96-dddd-eeee-ffff-0000000004a5'],
1241
+ // 同 sa-id 的两个 manifest(不同 rootSessionId 分区,模拟数据异常——新布局分区键 =
1242
+ // rootSessionId,同分区同名 manifest 会互覆,双 manifest 只能跨分区构造)
1243
+ for (const [rootSid, realId] of [
1244
+ ['0aaaaaaa-bbbb-7ccc-dddd-0000000004r1', '019e6c96-dddd-eeee-ffff-0000000004a4'],
1245
+ ['0aaaaaaa-bbbb-7ccc-dddd-0000000004r2', '019e6c96-dddd-eeee-ffff-0000000004a5'],
1219
1246
  ] as const) {
1220
- const sessionFile = join(dir, 'subagents', slug, 'sessions', `${realId}.jsonl`)
1221
- await mkdir(join(dir, 'subagents', slug, 'sessions'), { recursive: true })
1247
+ const sessionsDir = resolveStoreSubagentSessionsDir(dir, rootSid)
1248
+ const sessionFile = join(sessionsDir, `${realId}.jsonl`)
1249
+ await mkdir(sessionsDir, { recursive: true })
1222
1250
  await writeFile(
1223
1251
  sessionFile,
1224
1252
  JSON.stringify({ type: 'session', id: realId, cwd: '/demo' }) + '\n',
1225
1253
  )
1226
- const recordsDir = join(dir, 'subagents', slug, 'records')
1254
+ const recordsDir = resolveStoreSubagentRecordsDir(dir, rootSid)
1227
1255
  await mkdir(recordsDir, { recursive: true })
1228
1256
  await writeFile(
1229
1257
  join(recordsDir, 'sa-dup.json'),
1230
1258
  JSON.stringify({
1231
1259
  id: 'sa-dup',
1232
- rootSessionId: 'r',
1260
+ rootSessionId: rootSid,
1233
1261
  agentName: 'a',
1234
1262
  sessionFile,
1235
1263
  }),
@@ -1242,11 +1270,11 @@ describe('resolveSessionId ② sa-id 形态(w2 TC7-TC10 + CQ3)', () => {
1242
1270
 
1243
1271
  it('CQ3: sa-id 命中但 sessionFile header 读不出 → F6 风格(不降级 record.id 当 sessionId)', async () => {
1244
1272
  // sessionFile 存在但首行非 session header → readSessionHeaderIdSync 返 undefined
1245
- const slug = '--demo-cwd--'
1246
- const sessionFile = join(dir, 'subagents', slug, 'sessions', 'bad.jsonl')
1247
- await mkdir(join(dir, 'subagents', slug, 'sessions'), { recursive: true })
1273
+ const rootSid = 'r' // 合法 pi sid 形态(单字符分区)
1274
+ const sessionFile = join(resolveStoreSubagentSessionsDir(dir, rootSid), 'bad.jsonl')
1275
+ await mkdir(dirname(sessionFile), { recursive: true })
1248
1276
  await writeFile(sessionFile, JSON.stringify({ type: 'custom', customType: 'x' }) + '\n')
1249
- const recordsDir = join(dir, 'subagents', slug, 'records')
1277
+ const recordsDir = resolveStoreSubagentRecordsDir(dir, rootSid)
1250
1278
  await mkdir(recordsDir, { recursive: true })
1251
1279
  await writeFile(
1252
1280
  join(recordsDir, 'sa-bad.json'),
@@ -1264,7 +1292,7 @@ describe('resolveSessionId ② sa-id 形态(w2 TC7-TC10 + CQ3)', () => {
1264
1292
  })
1265
1293
  })
1266
1294
 
1267
- describe('sa-id entry 兜底 v2 终态条锚定(W1 D1 版本门补齐,第三层兜底消费面)', () => {
1295
+ describe('sa-id entry 兜底 v2 终态条锚定(W1 D1 版本门补齐,第三层兜底消费方)', () => {
1268
1296
  // fixture 骨架对齐 zcode-routing.test.ts:root/agent = agentDir、root/live-sessions =
1269
1297
  // liveSessionDir(entry 兜底候选根)、root/engines/zcode/session-db/db.sqlite = 隔离库
1270
1298
  // 白名单形态路径(不落文件——正向用例锚命中后进 zcode 读链,在白名单存在性检查处
@@ -1361,7 +1389,7 @@ describe('sa-id entry 兜底 v2 终态条锚定(W1 D1 版本门补齐,第三
1361
1389
  await expect(
1362
1390
  handleSessionRead({ action: 'outline', session: 'sa-v2-1' }, { agentDir, liveSessionDir: liveDir }),
1363
1391
  ).rejects.toThrow('zcode_anchor_missing')
1364
- // 归因进结构化日志不进 LLM 可见面(§3.4):firstIncompleteAnchorReason 的 v2 分支
1392
+ // 归因进结构化日志不进 LLM 可见部分(§3.4):firstIncompleteAnchorReason 的 v2 分支
1365
1393
  //(tool-handler 唯一消费点)产出 missing-dbPath
1366
1394
  expect(logCallsWithReason('missing-dbPath')).toBeGreaterThanOrEqual(1)
1367
1395
  })
@@ -1390,13 +1418,15 @@ describe('sa-id entry 兜底 v2 终态条锚定(W1 D1 版本门补齐,第三
1390
1418
  })
1391
1419
 
1392
1420
  describe('source 透传(w2 TC12-TC13,依赖 w1 findSessions opts.source)', () => {
1393
- let dir: string
1421
+ let dataDir: string
1422
+ let dir: string // agentDir = <dataDir>/agent(生产拓扑复刻,[session-store-layout-inversion])
1394
1423
 
1395
1424
  beforeEach(async () => {
1396
- dir = await mkdtemp(join(tmpdir(), 'tool-handler-src-'))
1425
+ dataDir = await mkdtemp(join(tmpdir(), 'tool-handler-src-'))
1426
+ dir = join(dataDir, 'agent')
1397
1427
  })
1398
1428
  afterEach(async () => {
1399
- await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
1429
+ await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
1400
1430
  })
1401
1431
 
1402
1432
  it('TC12: find source 过滤——subagent 只返回 subagent 候选,main 只返回 main', async () => {
@@ -1466,7 +1496,8 @@ describe('source 透传(w2 TC12-TC13,依赖 w1 findSessions opts.source)',
1466
1496
  })
1467
1497
 
1468
1498
  describe('subagent sa-id(w2 TC14-TC17,fixture)', () => {
1469
- let dir: string
1499
+ let dataDir: string
1500
+ let dir: string // agentDir = <dataDir>/agent(生产拓扑复刻)
1470
1501
  const TC14_ID = '0aaaaaaa-0000-7000-8000-0000000014a1'
1471
1502
  const TC15_ID = '0aaaaaaa-0000-7000-8000-0000000015a2'
1472
1503
 
@@ -1478,7 +1509,8 @@ describe('subagent sa-id(w2 TC14-TC17,fixture)', () => {
1478
1509
  * - TC17 的 manifest sessionFile 指向不存在路径(模拟 .jsonl 被 GC)
1479
1510
  */
1480
1511
  beforeEach(async () => {
1481
- dir = await mkdtemp(join(tmpdir(), 'tool-handler-tc14-'))
1512
+ dataDir = await mkdtemp(join(tmpdir(), 'tool-handler-tc14-'))
1513
+ dir = join(dataDir, 'agent')
1482
1514
  await makeFixtureSubagent(dir, 'sa-tc14', {
1483
1515
  realSessionId: TC14_ID,
1484
1516
  firstUserText: 'subagent task',
@@ -1494,7 +1526,7 @@ describe('subagent sa-id(w2 TC14-TC17,fixture)', () => {
1494
1526
  })
1495
1527
  })
1496
1528
  afterEach(async () => {
1497
- await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
1529
+ await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
1498
1530
  })
1499
1531
 
1500
1532
  it('TC14: completed subagent sa-id outline 成功(场景 1,sessionId=header 真实 id)', async () => {
@@ -1511,7 +1543,10 @@ describe('subagent sa-id(w2 TC14-TC17,fixture)', () => {
1511
1543
  it('TC16: 嵌套后代直接 outline(场景 1c,绝对路径形态读任意节点)', async () => {
1512
1544
  // 绝对路径形态直接读(M0 入口不依赖 findSessions)
1513
1545
  const r = await handleSessionRead(
1514
- { action: 'outline', session: join(dir, 'subagents', '--demo-cwd--', 'sessions', `${TC14_ID}.jsonl`) },
1546
+ {
1547
+ action: 'outline',
1548
+ session: join(resolveStoreSubagentSessionsDir(dir, 'root-session-1'), `${TC14_ID}.jsonl`),
1549
+ },
1515
1550
  { agentDir: dir },
1516
1551
  )
1517
1552
  expect(((r.details as { turns: unknown[] }).turns).length).toBeGreaterThan(0)
@@ -1571,13 +1606,15 @@ async function wfStateFile(
1571
1606
  }
1572
1607
 
1573
1608
  describe('doWorkflow(w6,fixture)', () => {
1574
- let dir: string
1609
+ let dataDir: string
1610
+ let dir: string // agentDir = <dataDir>/agent(生产拓扑复刻,[session-store-layout-inversion])
1575
1611
 
1576
1612
  beforeEach(async () => {
1577
- dir = await mkdtemp(join(tmpdir(), 'tool-handler-wf-'))
1613
+ dataDir = await mkdtemp(join(tmpdir(), 'tool-handler-wf-'))
1614
+ dir = join(dataDir, 'agent')
1578
1615
  })
1579
1616
  afterEach(async () => {
1580
- await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
1617
+ await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
1581
1618
  })
1582
1619
 
1583
1620
  // ---- [D16③] v2 record 流档概览(recordPath 锚点 = record 流路径)----
@@ -1603,7 +1640,7 @@ describe('doWorkflow(w6,fixture)', () => {
1603
1640
  }
1604
1641
 
1605
1642
  /** 构造 v2 record 流文件(run-created + agent-settled[+终局] 帧提 calls)并写注册条目——
1606
- * [ADR-0095] v1 读面删除后的单档 fixture 形态(替代 wf-state 快照 + link 指针)。 */
1643
+ * [ADR-0095] v1 读侧删除后的单档 fixture 形态(替代 wf-state 快照 + link 指针)。 */
1607
1644
  async function wfV2Run(
1608
1645
  dir: string,
1609
1646
  slug: string,
@@ -1798,7 +1835,7 @@ describe('doWorkflow(w6,fixture)', () => {
1798
1835
  expect(text).toContain('family')
1799
1836
  })
1800
1837
 
1801
- // [ADR-0095] TC-w6-snapshot-skip(快照不可读 → skippedRuns)随 v1 读面删除:
1838
+ // [ADR-0095] TC-w6-snapshot-skip(快照不可读 → skippedRuns)随 v1 读侧删除:
1802
1839
  // 发现链收敛 v2 后 run 一律 record 流直读(读失败 → running 兜底不 skipped),
1803
1840
  // skippedRuns 快照链对 v2 档不可达。
1804
1841
 
@@ -1831,7 +1868,7 @@ describe('doWorkflow(w6,fixture)', () => {
1831
1868
  const slug = '--wf-subagent--'
1832
1869
  // subagent session 放 subagents/ 下(不在 sessions/——buildFamilyFromFs 的 main byId
1833
1870
  // 索引外,旧实现 resolveFamily 找不到会抛「session not found in family index」)
1834
- const subDir = join(dir, 'subagents', slug, 'sessions')
1871
+ const subDir = resolveStoreSubagentSessionsDir(dir, SUB_WF_ROOT)
1835
1872
  await mkdir(subDir, { recursive: true })
1836
1873
  const subPath = join(subDir, `${SUB_WF_ROOT}.jsonl`)
1837
1874
  await writeFile(
@@ -1873,12 +1910,14 @@ describe('doWorkflow(w6,fixture)', () => {
1873
1910
  // ============================================================
1874
1911
 
1875
1912
  describe('doFamily recursive(m3b U8 接入)', () => {
1876
- let dir: string
1913
+ let dataDir: string
1914
+ let dir: string // agentDir = <dataDir>/agent(生产拓扑复刻)
1877
1915
  const MAIN = '019e6c96-cccc-dddd-eeee-000000000001'
1878
1916
  const SUB = '019e6c96-cccc-dddd-eeee-000000000002'
1879
1917
 
1880
1918
  beforeEach(async () => {
1881
- dir = await mkdtemp(join(tmpdir(), 'tool-handler-recursive-'))
1919
+ dataDir = await mkdtemp(join(tmpdir(), 'tool-handler-recursive-'))
1920
+ dir = join(dataDir, 'agent')
1882
1921
  await makeFixtureSession(dir, MAIN, 'main session 内容')
1883
1922
  // 顶层 subagent(rootSessionId=MAIN,无 parentRecordId → flat 回退)
1884
1923
  await makeFixtureSubagent(dir, `sa-${SUB.slice(0, 8)}`, {
@@ -1889,7 +1928,7 @@ describe('doFamily recursive(m3b U8 接入)', () => {
1889
1928
  })
1890
1929
  })
1891
1930
  afterEach(async () => {
1892
- await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
1931
+ await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
1893
1932
  })
1894
1933
 
1895
1934
  it('TC-m3b-dofamily-recursive-false:不传 recursive → flat family(m0-m2 零回归)', async () => {
@@ -1939,20 +1978,22 @@ describe('doFamily recursive(m3b U8 接入)', () => {
1939
1978
 
1940
1979
  // ===========================================================================
1941
1980
  // ext-simplify-04 U4/E2(D2②):formatFamilyText subagents 行富字段展示
1942
- // status 终态短标签 + agent 名 + task 截 60 摘要;孤儿只标 [已清理];P4 输出量级守卫。
1981
+ // status 终态短标签 + agent 名 + task 截 60 摘要;孤儿只标 [已清理];P4 输出量级检查。
1943
1982
  // ===========================================================================
1944
1983
 
1945
1984
  describe('formatFamilyText subagents 行富字段(ext-simplify-04 U4 / E2)', () => {
1946
1985
  const MAIN = '0aaaaaaa-bbbb-7ccc-dddd-0000000000f1'
1947
1986
  const SUB = '0aaaaaaa-bbbb-7ccc-dddd-0000000000f2'
1948
- let dir: string
1987
+ let dataDir: string
1988
+ let dir: string // agentDir = <dataDir>/agent(生产拓扑复刻)
1949
1989
 
1950
1990
  beforeEach(async () => {
1951
- dir = await mkdtemp(join(tmpdir(), 'tool-handler-family-rich-'))
1991
+ dataDir = await mkdtemp(join(tmpdir(), 'tool-handler-family-rich-'))
1992
+ dir = join(dataDir, 'agent')
1952
1993
  await makeFixtureSession(dir, MAIN, 'main session')
1953
1994
  })
1954
1995
  afterEach(async () => {
1955
- await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
1996
+ await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
1956
1997
  })
1957
1998
 
1958
1999
  /** alive subagent:session 文件(header + 可选 identity 尾行)+ 可选 manifest 富字段 */
@@ -1964,7 +2005,7 @@ describe('formatFamilyText subagents 行富字段(ext-simplify-04 U4 / E2)',
1964
2005
  identityAgent?: string
1965
2006
  } = {},
1966
2007
  ): Promise<void> {
1967
- const sessionDir = join(dir, 'subagents', '--demo-cwd--', 'sessions')
2008
+ const sessionDir = resolveStoreSubagentSessionsDir(dir, MAIN)
1968
2009
  await mkdir(sessionDir, { recursive: true })
1969
2010
  const subPath = join(sessionDir, `${realId}.jsonl`)
1970
2011
  const lines = [JSON.stringify({ type: 'session', id: realId, cwd: '/demo' })]
@@ -1985,7 +2026,7 @@ describe('formatFamilyText subagents 行富字段(ext-simplify-04 U4 / E2)',
1985
2026
  }
1986
2027
  await writeFile(subPath, lines.join('\n') + '\n')
1987
2028
  if (opts.manifest) {
1988
- const recordsDir = join(dir, 'subagents', '--demo-cwd--', 'records')
2029
+ const recordsDir = resolveStoreSubagentRecordsDir(dir, MAIN)
1989
2030
  await mkdir(recordsDir, { recursive: true })
1990
2031
  await writeFile(
1991
2032
  join(recordsDir, `sa-${realId}.json`),
@@ -2004,7 +2045,7 @@ describe('formatFamilyText subagents 行富字段(ext-simplify-04 U4 / E2)',
2004
2045
  saId: string,
2005
2046
  fields: { agentName?: string; task?: string; slug?: string; status?: string } = {},
2006
2047
  ): Promise<void> {
2007
- const recordsDir = join(dir, 'subagents', '--demo-cwd--', 'records')
2048
+ const recordsDir = resolveStoreSubagentRecordsDir(dir, MAIN)
2008
2049
  await mkdir(recordsDir, { recursive: true })
2009
2050
  await writeFile(
2010
2051
  join(recordsDir, `${saId}.json`),
@@ -2107,7 +2148,10 @@ describe('doctor action(u8:环境判定 + 根表 + 告警 + 残留 glob)',
2107
2148
  await writeJsonl(join(agentDir, 'sessions', SLUG, 'a.jsonl'), 'aaaaaaaa')
2108
2149
  await writeJsonl(join(agentDir, 'sessions', SLUG, 'b.jsonl'), 'bbbbbbbb')
2109
2150
  await writeJsonl(join(dataDir, 'sessions', 'old.jsonl'), 'cccccccc') // legacy 非空
2110
- await writeJsonl(join(agentDir, 'subagents', SLUG, 'sessions', 's.jsonl'), 'dddddddd')
2151
+ await writeJsonl(
2152
+ join(resolveStoreSubagentSessionsDir(agentDir, '0aaaaaaa-bbbb-7ccc-dddd-000000000d1'), 's.jsonl'),
2153
+ 'dddddddd',
2154
+ )
2111
2155
  return {
2112
2156
  dataDir,
2113
2157
  agentDir,
@@ -2219,7 +2263,7 @@ describe('doctor action(u8:环境判定 + 根表 + 告警 + 残留 glob)',
2219
2263
  it('无缓存形态(ext-simplify-04 U3):重复调用输出一致(每次实扫),新落盘文件立即可见', async () => {
2220
2264
  // 原三个用例(缓存命中不重扫 / TTL 到期失效 / find 不读 doctor 缓存 PS-14)随
2221
2265
  // doctor 缓存机删除而合并为本用例——无缓存层后「find/doctor 读到旧计数」的形态
2222
- // 在结构上不存在,守卫点收敛为:输出无「缓存命中」行 + 每次调用实扫(新增文件
2266
+ // 在结构上不存在,检查点收敛为:输出无「缓存命中」行 + 每次调用实扫(新增文件
2223
2267
  // 下一次调用即见,反而更实时)
2224
2268
  const agentDir = join(tmp, 'agent')
2225
2269
  await writeJsonl(join(agentDir, 'sessions', SLUG, 'a.jsonl'))
@@ -2286,8 +2330,9 @@ describe('u9 F1 重写 + uuid 归一化(fixture,§5.2 / §6.7 / §11.5)',
2286
2330
  id: string,
2287
2331
  opts?: { subagent?: boolean; firstUserText?: string },
2288
2332
  ): Promise<void> {
2333
+ // [session-store-layout-inversion] subagent 会话在新布局存储树(分区 sid 固定合法形态)
2289
2334
  const dir = opts?.subagent
2290
- ? join(tmp, 'agent', 'subagents', SLUG, 'sessions')
2335
+ ? resolveStoreSubagentSessionsDir(join(tmp, 'agent'), '0aaaaaaa-bbbb-7ccc-dddd-0000000000s1')
2291
2336
  : join(tmp, 'agent', 'sessions', SLUG)
2292
2337
  await mkdir(dir, { recursive: true })
2293
2338
  const lines = [JSON.stringify({ type: 'session', id, cwd: '/demo' })]
@@ -2436,7 +2481,7 @@ describe('u9 F1 重写 + uuid 归一化(fixture,§5.2 / §6.7 / §11.5)',
2436
2481
  )
2437
2482
  const text = r.content[0].text
2438
2483
  expect(text).toContain(`${join(agentDir, 'sessions')}:2 文件`)
2439
- expect(text).toContain(`${join(agentDir, 'subagents')}:1 文件`)
2484
+ expect(text).toContain(`${resolveSessionStoreRoot(agentDir)}:1 文件`)
2440
2485
  expect(text).toContain('候选集非空(共 3 文件)')
2441
2486
 
2442
2487
  // 完整信号包(live 与 default 同路径)→ 去重根注记,不重复计数
@@ -2560,8 +2605,9 @@ describe('u10 find 分组输出(fixture,§6.7 子决策 2/3 + §8.2 回归
2560
2605
  id: string,
2561
2606
  opts?: { subagent?: boolean; firstUserText?: string },
2562
2607
  ): Promise<void> {
2608
+ // [session-store-layout-inversion] subagent 会话在新布局存储树(分区 sid 固定合法形态)
2563
2609
  const dir = opts?.subagent
2564
- ? join(tmp, 'agent', 'subagents', SLUG, 'sessions')
2610
+ ? resolveStoreSubagentSessionsDir(join(tmp, 'agent'), '0aaaaaaa-bbbb-7ccc-dddd-0000000000s1')
2565
2611
  : join(tmp, 'agent', 'sessions', SLUG)
2566
2612
  await mkdir(dir, { recursive: true })
2567
2613
  const lines = [JSON.stringify({ type: 'session', id, cwd: '/demo' })]
@@ -2582,7 +2628,10 @@ describe('u10 find 分组输出(fixture,§6.7 子决策 2/3 + §8.2 回归
2582
2628
  * 供 result action 批量调用(回归基线:批量头行 8 字符短 id 不受 u10 影响)。
2583
2629
  */
2584
2630
  async function writeSubagentWithResult(saId: string, id: string, body: string): Promise<void> {
2585
- const sessionFile = join(tmp, 'agent', 'subagents', SLUG, 'sessions', `${id}.jsonl`)
2631
+ const sessionFile = join(
2632
+ resolveStoreSubagentSessionsDir(join(tmp, 'agent'), 'root-u10'),
2633
+ `${id}.jsonl`,
2634
+ )
2586
2635
  await mkdir(dirname(sessionFile), { recursive: true })
2587
2636
  const lines = [
2588
2637
  JSON.stringify({ type: 'session', id, cwd: '/demo' }),
@@ -2598,7 +2647,7 @@ describe('u10 find 分组输出(fixture,§6.7 子决策 2/3 + §8.2 回归
2598
2647
  }),
2599
2648
  ]
2600
2649
  await writeFile(sessionFile, lines.join('\n') + '\n')
2601
- const recordsDir = join(tmp, 'agent', 'subagents', SLUG, 'records')
2650
+ const recordsDir = resolveStoreSubagentRecordsDir(join(tmp, 'agent'), 'root-u10')
2602
2651
  await mkdir(recordsDir, { recursive: true })
2603
2652
  await writeFile(
2604
2653
  join(recordsDir, `${saId}.json`),
@@ -2804,8 +2853,9 @@ describe('E1 find 单次根解析(探针 P1:resolveSessionRoots 恰 1 次)
2804
2853
 
2805
2854
  /** 写 session 文件(subagent:true 写入 subagent 根),形态同 u10 段 writeSession。 */
2806
2855
  async function writeSession(id: string, opts?: { subagent?: boolean }): Promise<void> {
2856
+ // [session-store-layout-inversion] subagent 会话在新布局存储树(分区 sid 固定合法形态)
2807
2857
  const dir = opts?.subagent
2808
- ? join(tmp, 'agent', 'subagents', SLUG, 'sessions')
2858
+ ? resolveStoreSubagentSessionsDir(join(tmp, 'agent'), '0aaaaaaa-bbbb-7ccc-dddd-0000000000s1')
2809
2859
  : join(tmp, 'agent', 'sessions', SLUG)
2810
2860
  await mkdir(dir, { recursive: true })
2811
2861
  await writeFile(
@@ -3038,7 +3088,7 @@ describe('u12 跨会话内容检索(fixture,V8)', () => {
3038
3088
  opts?: { subagent?: boolean },
3039
3089
  ): Promise<void> {
3040
3090
  const dir = opts?.subagent
3041
- ? join(tmp, 'agent', 'subagents', SLUG, 'sessions')
3091
+ ? resolveStoreSubagentSessionsDir(join(tmp, 'agent'), '0aaaaaaa-bbbb-7ccc-dddd-0000000000s2')
3042
3092
  : join(tmp, 'agent', 'sessions')
3043
3093
  await mkdir(dir, { recursive: true })
3044
3094
  const lines = [JSON.stringify({ type: 'session', id, cwd: '/demo' })]
@@ -57,7 +57,7 @@ const NEW_SNAPSHOT_FIXTURE = {
57
57
 
58
58
  /**
59
59
  * v2 格式 fixture(pi-subagent-workflow 8.x 一次性生命周期写入的 wf-run-v2 快照)。
60
- * 读取面形状与 v1 一致(state.calls[].sessionFile/result 保留),差异仅版本字面量与
60
+ * 读侧形状与 v1 一致(state.calls[].sessionFile/result 保留),差异仅版本字面量与
61
61
  * status 两态(running/done)、meta 无 pausedAt——v2 不该因版本字面量被挡在读之外。
62
62
  */
63
63
  const V2_SNAPSHOT_FIXTURE = {
@@ -206,7 +206,7 @@ describe('parseRunSnapshot', () => {
206
206
 
207
207
  it('TC-w5-parse-malformed:既非 NEW 也非 OLD(缺关键字段)返回 null', () => {
208
208
  // (a) 有 v 但 v 非 v1/v2 且无 callCache(未来版本 wf-run-v3 及之后——届时须评估
209
- // 新版本读取面形状再扩判定,v2 因形状兼容被接受)
209
+ // 新版本读侧形状再扩判定,v2 因形状兼容被接受)
210
210
  expect(parseRunSnapshot({ v: 'wf-run-v3', state: { calls: [] } }, 'r', 's')).toBeNull()
211
211
  // (b) 无 v 无 callCache 无 status(异构对象)
212
212
  expect(parseRunSnapshot({ foo: 'bar', baz: 1 }, 'r', 's')).toBeNull()
@@ -327,7 +327,7 @@ describe('readRunSnapshot', () => {
327
327
  })
328
328
 
329
329
  // ============================================================
330
- // resolveWorkflows 发现链([ADR-0095] v1 读面删除后的单档形态:v2 注册条目
330
+ // resolveWorkflows 发现链([ADR-0095] v1 读侧删除后的单档形态:v2 注册条目
331
331
  // recordPath 主源;历史格式条目——v1 快照 / workflow-state-link 指针——静默消失)
332
332
  // ============================================================
333
333
 
@@ -549,8 +549,8 @@ describe('resolveWorkflows(v2 注册条目单档发现链)', () => {
549
549
  expect(workflows).toEqual([])
550
550
  })
551
551
 
552
- it('跨包契约守卫:发现链判别的本地字面量 === subagent-core 写侧单源([D1] record 后缀)', () => {
553
- // 源码本地持有磁盘协议字符串与版本/尾段(生产依赖面不引 subagent-core),锚定两侧同值
552
+ it('跨包契约检查:发现链判别的本地字面量 === subagent-core 写侧单源([D1] record 后缀)', () => {
553
+ // 源码本地持有磁盘协议字符串与版本/尾段(生产依赖范围不引 subagent-core),锚定两侧同值
554
554
  expect(WORKFLOW_RECORD_CUSTOM_TYPE).toBe('workflow-record')
555
555
  expect(WORKFLOW_RECORD_ENTRY_VERSION).toBe(2)
556
556
  expect(RUN_RECORD_STREAM_SUFFIX).toBe('.record.jsonl')