@zhushanwen/pi-session-reader 0.7.0 → 0.8.1
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.
- package/package.json +4 -4
- package/src/__tests__/entry-anchor.test.ts +148 -4
- package/src/__tests__/execution-tree.test.ts +6 -12
- package/src/__tests__/find.test.ts +9 -49
- package/src/__tests__/fixtures.ts +15 -0
- package/src/__tests__/parser.test.ts +36 -11
- package/src/__tests__/render.test.ts +22 -18
- package/src/__tests__/roots.test.ts +32 -34
- package/src/__tests__/subagents.test.ts +131 -108
- package/src/__tests__/tool-handler.test.ts +496 -147
- package/src/__tests__/turns.test.ts +21 -19
- package/src/__tests__/workflow.test.ts +319 -23
- package/src/core/workflow.ts +196 -6
- package/src/discovery/entry-anchor.ts +36 -5
- package/src/discovery/roots.ts +1 -2
- package/src/discovery/workflows.ts +232 -22
- package/src/tool-handler.ts +82 -22
- package/src/zcode-anchor-classify.ts +23 -9
- package/src/__tests__/real-data.ts +0 -96
|
@@ -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 {
|
|
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
|
-
//
|
|
420
|
+
// family workflows 富 run(多 call)契约(fixture)
|
|
453
421
|
// ============================================================
|
|
454
422
|
|
|
455
|
-
describe
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
//
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
}
|
|
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
|
-
//
|
|
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)。
|