@zhushanwen/pi-session-reader 0.6.1 → 0.8.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 (43) hide show
  1. package/README.md +100 -0
  2. package/package.json +6 -3
  3. package/src/__tests__/entry-anchor.test.ts +370 -0
  4. package/src/__tests__/execution-tree.test.ts +12 -12
  5. package/src/__tests__/family-zcode.test.ts +255 -0
  6. package/src/__tests__/family.test.ts +1 -1
  7. package/src/__tests__/find.test.ts +32 -49
  8. package/src/__tests__/fixtures.ts +15 -0
  9. package/src/__tests__/parser.test.ts +37 -13
  10. package/src/__tests__/render.test.ts +22 -19
  11. package/src/__tests__/result.test.ts +2 -2
  12. package/src/__tests__/roots.test.ts +32 -34
  13. package/src/__tests__/subagents.test.ts +131 -108
  14. package/src/__tests__/tool-handler.test.ts +506 -153
  15. package/src/__tests__/toolcall.test.ts +1 -1
  16. package/src/__tests__/tree.test.ts +1 -1
  17. package/src/__tests__/turns.test.ts +21 -20
  18. package/src/__tests__/workflow.test.ts +319 -23
  19. package/src/__tests__/zcode-manifest.test.ts +361 -0
  20. package/src/__tests__/zcode-routing.test.ts +751 -0
  21. package/src/__tests__/zcode-whitelist.test.ts +119 -0
  22. package/src/core/execution-tree.ts +3 -1
  23. package/src/core/family.ts +80 -1
  24. package/src/core/render.ts +1 -1
  25. package/src/core/toolcall.ts +2 -2
  26. package/src/core/tree.ts +1 -1
  27. package/src/core/turns.ts +1 -1
  28. package/src/core/workflow.ts +196 -6
  29. package/src/discovery/entry-anchor.ts +139 -0
  30. package/src/discovery/roots.ts +1 -2
  31. package/src/discovery/session-header.ts +30 -69
  32. package/src/discovery/subagents.ts +9 -2
  33. package/src/discovery/whitelist.ts +87 -0
  34. package/src/discovery/workflows.ts +232 -22
  35. package/src/discovery/zcode-manifest.ts +277 -0
  36. package/src/index.ts +7 -0
  37. package/src/result-action.ts +14 -5
  38. package/src/search-across.ts +1 -1
  39. package/src/tool-format.ts +455 -0
  40. package/src/tool-handler.ts +400 -344
  41. package/src/zcode-anchor-classify.ts +74 -0
  42. package/src/__tests__/real-data.ts +0 -96
  43. package/src/core/parser.ts +0 -167
@@ -1,8 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
2
  import { tmpdir } from 'node:os'
3
- import { existsSync } from 'node:fs'
4
3
  import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
5
- import { execSync } from 'node:child_process'
6
4
  import { join } from 'node:path'
7
5
  import {
8
6
  buildFamilyFromFs,
@@ -10,44 +8,14 @@ import {
10
8
  extractSessionIdFromFilename,
11
9
  type RecordManifest,
12
10
  } from '../discovery/subagents.js'
13
- import { REAL_AGENT_DIR, REAL_DATA_TIMEOUT_MS } from './real-data.js'
11
+ import { resolveSessionRoots } from '../discovery/roots.js'
12
+ import { parseSessionHeader, readSessionHeaderFirstLine } from '../discovery/session-header.js'
14
13
 
15
14
  // ---- fixture 常量(uuid 特征,满足 extractSessionIdFromFilename + 互不为子串)----
16
15
  const ROOT = '0aaaaaaa-bbbb-7ccc-dddd-000000000001'
17
16
  const FORK = '0aaaaaaa-bbbb-7ccc-dddd-000000000002'
18
17
  const SUB_REAL = '0aaaaaaa-bbbb-7ccc-dddd-000000000003'
19
18
 
20
- // 同步探测真实 session 是否存在(不存在则 skip,避免在无该数据的机器上硬失败)
21
- function hasRealSession(sid: string): boolean {
22
- try {
23
- return (
24
- execSync(
25
- `find ${REAL_AGENT_DIR}/sessions -name '*${sid}*' -name '*.jsonl' ! -name '*.finalized' 2>/dev/null | head -1`,
26
- { encoding: 'utf8' },
27
- ).trim().length > 0
28
- )
29
- } catch {
30
- return false
31
- }
32
- }
33
-
34
- // 双目录版探测:subagent session 文件在 subagents/ 下,hasRealSession 扫不到。
35
- // 用于对活跃数据目录中具体文件(fork 子代/隔代 subagent)存在性的守卫。
36
- function hasAnyRealSession(fragment: string): boolean {
37
- try {
38
- return (
39
- execSync(
40
- `find ${REAL_AGENT_DIR}/sessions ${REAL_AGENT_DIR}/subagents -name '*${fragment}*' -name '*.jsonl' ! -name '*.finalized' 2>/dev/null | head -1`,
41
- { encoding: 'utf8' },
42
- ).trim().length > 0
43
- )
44
- } catch {
45
- return false
46
- }
47
- }
48
- const HAS_REAL_ROOT = hasRealSession('019fe620-8ae1-78a7-b76a-43a1ba4cc3c7')
49
- const HAS_REAL_WF = hasRealSession('019fdcda-75c7-74b7-a160-f67f6bf88384')
50
-
51
19
  // ---- fixture helpers ----
52
20
 
53
21
  async function makeAgentDir(): Promise<string> {
@@ -449,49 +417,55 @@ describe('buildFamilyFromFs - fixture', () => {
449
417
  })
450
418
 
451
419
  // ============================================================
452
- // 真实数据集成测试(~/.pi/agent)
420
+ // family workflows 富 run(多 call)契约(fixture)
453
421
  // ============================================================
454
422
 
455
- describe.skipIf(!HAS_REAL_ROOT)('buildFamilyFromFs - 真实数据 ~/.pi/agent', () => {
456
- it('019fe620 family:fork 019fe632(跨 cwd)+ 隔代 subagent 019fe635(真实 id)', async () => {
457
- // 数据守卫:019fe632(sessions/)与 019fe635(subagents/)位于活跃数据目录,
458
- // 被 GC/重命名时跳过而非失败(同 TC14-TC18 守卫模式),避免偶发红。
459
- if (!hasAnyRealSession('019fe632') || !hasAnyRealSession('019fe635')) return
460
- const family = await buildFamilyFromFs(
461
- '019fe620-8ae1-78a7-b76a-43a1ba4cc3c7',
462
- REAL_AGENT_DIR,
463
- )
464
- // fork 019fe632(cwd feat-optimize-todo-goal,与 root 的 fix-cw-tool-wroktree 不同)
465
- const fork = family.forks.find((f) => f.sessionId.startsWith('019fe632'))
466
- expect(fork).toBeDefined()
467
- // 隔代 subagent:rootSessionId=019fe632(fork 子代),sessionId 真实(019fe635 开头,非 sa-)
468
- const sub = family.subagents.find(
469
- (s) => s.rootSessionId.startsWith('019fe632') && s.sessionId.startsWith('019fe635'),
470
- )
471
- expect(sub).toBeDefined()
472
- expect(sub!.sessionId.startsWith('sa-')).toBe(false)
473
- }, REAL_DATA_TIMEOUT_MS)
474
- })
423
+ describe('buildFamilyFromFs - workflow 富 run(多 call)', () => {
424
+ let dir: string
425
+
426
+ beforeEach(async () => {
427
+ dir = await makeAgentDir()
428
+ })
429
+ afterEach(async () => {
430
+ await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
431
+ })
432
+
433
+ it('workflows 非空,多 call(4 个)workflow 的 fileName/stateFile/runId 契约', async () => {
434
+ await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
435
+ // 1 个存活 subagent(pathToRef 命中 → 完整 ref)+ 3 个 GC 路径(未命中 → 最小 ref,
436
+ // fileName 仍非空——calls 的 sessionFile 路径恒落入 fileName)
437
+ const subPath = await writeSubagentSession(dir, '--root-cwd--', SUB_REAL, {
438
+ rootSessionId: ROOT,
439
+ slug: 'wf-rich',
440
+ })
441
+ const ghost = (n: number): string =>
442
+ join(
443
+ dir,
444
+ 'subagents',
445
+ '--root-cwd--',
446
+ 'sessions',
447
+ `2026-08-0${n}T00-00-00-000Z_019fdd55-1111-1111-1111-11111111111${n}.jsonl`,
448
+ )
449
+ const wfPath = await writeWfState(dir, '--root-cwd--', [
450
+ JSON.stringify({
451
+ v: 'wf-run-v1',
452
+ runId: 'wf-rich-run-1',
453
+ state: {
454
+ status: 'done',
455
+ calls: [
456
+ { id: 0, status: 'done', sessionFile: subPath, sessionId: SUB_REAL },
457
+ { id: 1, status: 'done', sessionFile: ghost(1) },
458
+ { id: 2, status: 'done', sessionFile: ghost(2) },
459
+ { id: 3, status: 'done', sessionFile: ghost(3) },
460
+ ],
461
+ },
462
+ }),
463
+ ])
464
+ await writeWfLink(dir, '--root-cwd--', ROOT, { runId: 'wf-rich-run-1', path: wfPath })
465
+
466
+ const family = await buildFamilyFromFs(ROOT, dir)
475
467
 
476
- describe.skipIf(!HAS_REAL_WF)('buildFamilyFromFs - 真实 workflow 数据', () => {
477
- it('019fdcda:workflows 非空,至少一个 workflow calls>=4', async () => {
478
- // 数据守卫:workflow-state 快照文件可能被清理(wf-state 是会话运行产物),
479
- // 019fdcda 的 workflow-state-link 指向的快照不存在时跳过而非失败。
480
- const wfFile = execSync(
481
- `find ${REAL_AGENT_DIR}/sessions -name '*019fdcda*' -name '*.jsonl' ! -name '*.finalized' 2>/dev/null | head -1`,
482
- { encoding: 'utf8' },
483
- ).trim()
484
- if (!wfFile) return
485
- const raw = execSync(`grep -c 'workflow-state-link' '${wfFile}' 2>/dev/null || true`, {
486
- encoding: 'utf8',
487
- }).trim()
488
- if (raw === '0' || raw === '') return
489
- const family = await buildFamilyFromFs(
490
- '019fdcda-75c7-74b7-a160-f67f6bf88384',
491
- REAL_AGENT_DIR,
492
- )
493
468
  expect(family.workflows.length).toBeGreaterThan(0)
494
- // wf-1786121387659-5voqzc 有 4 个 agent() calls(sessionFile 持久化)
495
469
  const rich = family.workflows.find((w) => w.calls.length >= 4)
496
470
  expect(rich).toBeDefined()
497
471
  // calls 的 sessionFile 路径已落入 fileName(sessionRefFromPath)
@@ -499,7 +473,7 @@ describe.skipIf(!HAS_REAL_WF)('buildFamilyFromFs - 真实 workflow 数据', () =
499
473
  // stateFile 是 wf-state 文件绝对路径
500
474
  expect(rich!.stateFile.endsWith('.jsonl')).toBe(true)
501
475
  expect(rich!.runId.startsWith('wf-')).toBe(true)
502
- }, REAL_DATA_TIMEOUT_MS)
476
+ })
503
477
  })
504
478
 
505
479
  // ============================================================
@@ -679,6 +653,86 @@ describe('U4 buildFamilyFromFs 富化(manifest 主 / P-fallback)', () => {
679
653
  })
680
654
  })
681
655
 
656
+ // ============================================================
657
+ // .events 文件族结构性忽略(W1 D3 落点裁决 + 设计检查点④)
658
+ // ============================================================
659
+ //
660
+ // record 事件文件族(<recordsDir>/<sa-id>.events,无 .jsonl 后缀)与 manifest 同目录
661
+ // 同主名。session-reader 的两层兼容形态在此断言:
662
+ // ① 结构性忽略——subagents 树扫描只收 .jsonl,.events 天然不进候选集(D3「无后缀
663
+ // 的结构性收益」:被忽略或被误读两类风险一次排空);
664
+ // ② 首行头行读者兼容——.events 首行是自描述头行 {"type":"record-journal","id":...}
665
+ // (写侧 record-events.ts toRecordJournalHeader 契约),session-reader 对未知文件
666
+ // 读首行判 header 时命中非 session header 即忽略(检查点④:零成本兼容——不改
667
+ // 扫描器即可与该文件族共存)。
668
+
669
+ describe('.events 文件族结构性忽略(W1 D3 / 检查点④)', () => {
670
+ let dir: string
671
+
672
+ beforeEach(async () => {
673
+ dir = await makeAgentDir()
674
+ })
675
+ afterEach(async () => {
676
+ await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
677
+ })
678
+
679
+ /** 写 record 事件文件(首行头行 + 事件行集,形态对齐写侧 record-events.ts)。 */
680
+ async function writeEventsFile(slug: string, saId: string): Promise<string> {
681
+ const recordsDir = join(dir, 'subagents', slug, 'records')
682
+ await mkdir(recordsDir, { recursive: true })
683
+ const path = join(recordsDir, `${saId}.events`)
684
+ const lines = [
685
+ JSON.stringify({ type: 'record-journal', id: saId }),
686
+ JSON.stringify({ type: 'record-created', seq: 1, ts: 1, agent: 'dev', task: 't', slug: 's', origin: 'tool', rootSessionId: ROOT, depth: 0, mode: 'sync', startedAt: 1 }),
687
+ JSON.stringify({ type: 'record-settled', seq: 2, ts: 2, stopReason: 'end_turn', endedAt: 2, turns: 1, totalTokens: 5 }),
688
+ ]
689
+ await writeFile(path, lines.join('\n') + '\n')
690
+ return path
691
+ }
692
+
693
+ it('subagent 树扫描不收 .events:resolveSessionRoots 的 subagent 根 files 含 .jsonl 不含 .events', async () => {
694
+ await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
695
+ const subPath = await writeSubagentSession(dir, '--root-cwd--', SUB_REAL, {
696
+ rootSessionId: ROOT,
697
+ slug: 'ev-sibling',
698
+ })
699
+ await writeEventsFile('--root-cwd--', 'sa-ev-1')
700
+
701
+ const roots = await resolveSessionRoots({ agentDir: dir })
702
+ const subRoot = roots.find((r) => r.kind === 'subagent')
703
+
704
+ expect(subRoot).toBeDefined()
705
+ expect(subRoot!.files.map((f) => f.path)).toContain(subPath)
706
+ expect(subRoot!.files.some((f) => f.path.endsWith('.events'))).toBe(false)
707
+ })
708
+
709
+ it('首行头行命中非 session header 即忽略:record-journal 头行 → parseSessionHeader 返回 null', async () => {
710
+ const eventsPath = await writeEventsFile('--root-cwd--', 'sa-ev-1')
711
+ // 检查点④形态:session-reader 对未知文件读首行判 header,非 session 行 → null
712
+ //(即使未来通配规则把 .events 收进候选集,该文件也不会被误认成 session)
713
+ const firstLine = await readSessionHeaderFirstLine(eventsPath)
714
+ expect(firstLine).toBeDefined()
715
+ expect(parseSessionHeader(firstLine)).toBeNull()
716
+ })
717
+
718
+ it('家族扫描:.events 与正常 subagent 同目录共存 → 家族正常、不产幻影 subagent 节点', async () => {
719
+ await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
720
+ await writeSubagentSession(dir, '--root-cwd--', SUB_REAL, {
721
+ rootSessionId: ROOT,
722
+ slug: 'ev-coexist',
723
+ })
724
+ await writeEventsFile('--root-cwd--', 'sa-ev-1')
725
+
726
+ const family = await buildFamilyFromFs(ROOT, dir)
727
+
728
+ // 正常 subagent 在场
729
+ expect(family.subagents.some((s) => s.sessionId === SUB_REAL)).toBe(true)
730
+ // .events 的 record id 不进任何集合(无幻影)
731
+ expect(family.subagents.some((s) => s.sessionId === 'sa-ev-1')).toBe(false)
732
+ expect(family.subagents.some((s) => s.sessionId.startsWith('sa-ev'))).toBe(false)
733
+ })
734
+ })
735
+
682
736
  // ============================================================
683
737
  // U4 C4 契约:extractSessionIdFromFilename 导出(仅加 export,不改逻辑,供 w4 复用)
684
738
  // ============================================================
@@ -702,37 +756,6 @@ describe('U4 extractSessionIdFromFilename 导出(C4 契约)', () => {
702
756
  })
703
757
  })
704
758
 
705
- // ============================================================
706
- // U4 真实数据守卫(TC-u4-real-data-guard):~/.pi/agent 富字段从 manifest 正确透传
707
- // ============================================================
708
-
709
- const HAS_REAL_SUBAGENTS_DIR = existsSync(join(REAL_AGENT_DIR, 'subagents'))
710
-
711
- describe.skipIf(!HAS_REAL_ROOT || !HAS_REAL_SUBAGENTS_DIR)('U4 真实数据守卫:~/.pi/agent', () => {
712
- it('TC-u4-real-data-guard: 019fe620 subagents 富字段透传(manifest 主 task 非空率 > 80%)', async () => {
713
- // 数据守卫:019fe635 是 019fe620 家族已知的隔代 subagent(含 task 富字段的锚点),
714
- // 被 GC 后跳过富字段验证(manifest 数据不可复现,跳过比失败合理)。
715
- if (!hasAnyRealSession('019fe635')) return
716
- const family = await buildFamilyFromFs(
717
- '019fe620-8ae1-78a7-b76a-43a1ba4cc3c7',
718
- REAL_AGENT_DIR,
719
- )
720
- // 有 subagent 才验证富字段(019fe620 确有隔代 subagent,见既有真实数据测试)
721
- expect(family.subagents.length).toBeGreaterThan(0)
722
-
723
- // manifest 主的 subagent(model !== undefined 近似判定,探针 manifest 20/20 有 model):
724
- // task 非空率应 > 80%(探针 20/20 manifest 全有 task)
725
- const manifestSourced = family.subagents.filter((s) => s.model !== undefined)
726
- if (manifestSourced.length > 0) {
727
- const withTask = manifestSourced.filter((s) => s.task !== undefined && s.task !== '')
728
- expect(withTask.length / manifestSourced.length).toBeGreaterThan(0.8)
729
- }
730
-
731
- // P-fallback 的 subagent(model === undefined):status 必 undefined(identity 不可回退 status)
732
- const pfallback = family.subagents.filter((s) => s.model === undefined)
733
- expect(pfallback.every((s) => s.status === undefined)).toBe(true)
734
-
735
- // 所有 subagent 的 sessionFile 非空(manifest.sessionFile 或 alive meta.path)
736
- expect(family.subagents.every((s) => typeof s.sessionFile === 'string' && s.sessionFile.length > 0)).toBe(true)
737
- }, REAL_DATA_TIMEOUT_MS)
738
- })
759
+ // U4 富字段透传契约由 fixture 侧逐条覆盖:manifest 主(TC-u4-manifest-enrich,task/slug/
760
+ // model/status 全字段 + sessionFile 非空)、P-fallback(TC-u4-pfallback-identity,status 必
761
+ // undefined)、孤儿(TC-u4-orphan-manifest)、旧 manifest 兼容(TC-u4-recordmanifest-compat)。