@zhushanwen/pi-session-reader 0.2.0 → 0.2.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 +1 -1
- package/src/__tests__/execution-tree.test.ts +1 -1
- package/src/__tests__/parser.test.ts +2 -1
- package/src/__tests__/real-data.ts +20 -0
- package/src/__tests__/render.test.ts +2 -1
- package/src/__tests__/subagents.test.ts +32 -0
- package/src/__tests__/tool-handler.test.ts +12 -4
- package/src/__tests__/turns.test.ts +2 -1
- package/src/__tests__/workflow.test.ts +74 -3
- package/src/core/workflow.ts +9 -8
- package/src/discovery/roots.ts +3 -2
- package/src/discovery/workflows.ts +4 -2
package/package.json
CHANGED
|
@@ -703,7 +703,7 @@ describe.skipIf(!HAS_REAL)('buildExecutionTree - 真实数据守卫', () => {
|
|
|
703
703
|
// 不抛错(已隐含:到这行说明成功)
|
|
704
704
|
expect(tree.root.type).toBe('main')
|
|
705
705
|
expect(tree.root.sessionId).toBe(FAM)
|
|
706
|
-
})
|
|
706
|
+
}, 60000)
|
|
707
707
|
})
|
|
708
708
|
|
|
709
709
|
// ============================================================
|
|
@@ -44,6 +44,26 @@ export function hasRealSession(sid: string): boolean {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* 同步探测真实数据中任意 session 文件存在(main sessions/ 与 subagents/ 双目录)。
|
|
49
|
+
* 与 hasRealSession 的区别:subagent session 文件位于 subagents/<cwd>/sessions/ 下,
|
|
50
|
+
* hasRealSession 只扫主 sessions/ 目录扫不到。用于对活跃数据目录中具体文件(如 fork
|
|
51
|
+
* 子代/隔代 subagent)存在性的守卫探测。
|
|
52
|
+
*/
|
|
53
|
+
export function hasAnyRealSession(fragment: string): boolean {
|
|
54
|
+
if (!existsSync(REAL_AGENT_DIR)) return false
|
|
55
|
+
try {
|
|
56
|
+
return (
|
|
57
|
+
execSync(
|
|
58
|
+
`find ${REAL_AGENT_DIR}/sessions ${REAL_AGENT_DIR}/subagents -name '*${fragment}*' -name '*.jsonl' ! -name '*.finalized' 2>/dev/null | head -1`,
|
|
59
|
+
{ encoding: 'utf8' },
|
|
60
|
+
).trim().length > 0
|
|
61
|
+
)
|
|
62
|
+
} catch {
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
47
67
|
export const HAS_REAL_AGENT_DIR = existsSync(REAL_AGENT_DIR)
|
|
48
68
|
export const HAS_REAL_SUBAGENTS_DIR = existsSync(join(REAL_AGENT_DIR, 'subagents'))
|
|
49
69
|
export const HAS_REAL_SESSION = existsSync(REAL_SESSION)
|
|
@@ -185,7 +185,8 @@ describe('renderOutline', () => {
|
|
|
185
185
|
// totalEntries 近似(leaf+branch+orphan)不含 session header(segmentTurns 规则1 跳过);
|
|
186
186
|
// 准确值由 M2 工具层用 ParseResult.totalEntries 覆盖。M1 验量级。
|
|
187
187
|
expect(result.stats.totalEntries).toBeGreaterThan(1000)
|
|
188
|
-
|
|
188
|
+
// 5.6MB 全量解析在并发/高负载下可能超 vitest 默认 5s,显式放宽
|
|
189
|
+
}, 60000)
|
|
189
190
|
})
|
|
190
191
|
|
|
191
192
|
describe('renderExpand', () => {
|
|
@@ -32,6 +32,21 @@ function hasRealSession(sid: string): boolean {
|
|
|
32
32
|
return false
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
// 双目录版探测:subagent session 文件在 subagents/ 下,hasRealSession 扫不到。
|
|
37
|
+
// 用于对活跃数据目录中具体文件(fork 子代/隔代 subagent)存在性的守卫。
|
|
38
|
+
function hasAnyRealSession(fragment: string): boolean {
|
|
39
|
+
try {
|
|
40
|
+
return (
|
|
41
|
+
execSync(
|
|
42
|
+
`find ${REAL_AGENT_DIR}/sessions ${REAL_AGENT_DIR}/subagents -name '*${fragment}*' -name '*.jsonl' ! -name '*.finalized' 2>/dev/null | head -1`,
|
|
43
|
+
{ encoding: 'utf8' },
|
|
44
|
+
).trim().length > 0
|
|
45
|
+
)
|
|
46
|
+
} catch {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
35
50
|
const HAS_REAL_ROOT = hasRealSession('019fe620-8ae1-78a7-b76a-43a1ba4cc3c7')
|
|
36
51
|
const HAS_REAL_WF = hasRealSession('019fdcda-75c7-74b7-a160-f67f6bf88384')
|
|
37
52
|
|
|
@@ -440,6 +455,9 @@ describe('buildFamilyFromFs - fixture', () => {
|
|
|
440
455
|
|
|
441
456
|
describe.skipIf(!HAS_REAL_ROOT)('buildFamilyFromFs - 真实数据 ~/.pi/agent', () => {
|
|
442
457
|
it('019fe620 family:fork 019fe632(跨 cwd)+ 隔代 subagent 019fe635(真实 id)', async () => {
|
|
458
|
+
// 数据守卫:019fe632(sessions/)与 019fe635(subagents/)位于活跃数据目录,
|
|
459
|
+
// 被 GC/重命名时跳过而非失败(同 TC14-TC18 守卫模式),避免偶发红。
|
|
460
|
+
if (!hasAnyRealSession('019fe632') || !hasAnyRealSession('019fe635')) return
|
|
443
461
|
const family = await buildFamilyFromFs(
|
|
444
462
|
'019fe620-8ae1-78a7-b76a-43a1ba4cc3c7',
|
|
445
463
|
REAL_AGENT_DIR,
|
|
@@ -458,6 +476,17 @@ describe.skipIf(!HAS_REAL_ROOT)('buildFamilyFromFs - 真实数据 ~/.pi/agent',
|
|
|
458
476
|
|
|
459
477
|
describe.skipIf(!HAS_REAL_WF)('buildFamilyFromFs - 真实 workflow 数据', () => {
|
|
460
478
|
it('019fdcda:workflows 非空,至少一个 workflow calls>=4', async () => {
|
|
479
|
+
// 数据守卫:workflow-state 快照文件可能被清理(wf-state 是会话运行产物),
|
|
480
|
+
// 019fdcda 的 workflow-state-link 指向的快照不存在时跳过而非失败。
|
|
481
|
+
const wfFile = execSync(
|
|
482
|
+
`find ${REAL_AGENT_DIR}/sessions -name '*019fdcda*' -name '*.jsonl' ! -name '*.finalized' 2>/dev/null | head -1`,
|
|
483
|
+
{ encoding: 'utf8' },
|
|
484
|
+
).trim()
|
|
485
|
+
if (!wfFile) return
|
|
486
|
+
const raw = execSync(`grep -c 'workflow-state-link' '${wfFile}' 2>/dev/null || true`, {
|
|
487
|
+
encoding: 'utf8',
|
|
488
|
+
}).trim()
|
|
489
|
+
if (raw === '0' || raw === '') return
|
|
461
490
|
const family = await buildFamilyFromFs(
|
|
462
491
|
'019fdcda-75c7-74b7-a160-f67f6bf88384',
|
|
463
492
|
REAL_AGENT_DIR,
|
|
@@ -682,6 +711,9 @@ const HAS_REAL_SUBAGENTS_DIR = existsSync(join(REAL_AGENT_DIR, 'subagents'))
|
|
|
682
711
|
|
|
683
712
|
describe.skipIf(!HAS_REAL_ROOT || !HAS_REAL_SUBAGENTS_DIR)('U4 真实数据守卫:~/.pi/agent', () => {
|
|
684
713
|
it('TC-u4-real-data-guard: 019fe620 subagents 富字段透传(manifest 主 task 非空率 > 80%)', async () => {
|
|
714
|
+
// 数据守卫:019fe635 是 019fe620 家族已知的隔代 subagent(含 task 富字段的锚点),
|
|
715
|
+
// 被 GC 后跳过富字段验证(manifest 数据不可复现,跳过比失败合理)。
|
|
716
|
+
if (!hasAnyRealSession('019fe635')) return
|
|
685
717
|
const family = await buildFamilyFromFs(
|
|
686
718
|
'019fe620-8ae1-78a7-b76a-43a1ba4cc3c7',
|
|
687
719
|
REAL_AGENT_DIR,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
HAS_E6,
|
|
13
13
|
HAS_REAL,
|
|
14
14
|
HAS_REAL_SUBAGENTS_DIR,
|
|
15
|
+
hasAnyRealSession,
|
|
15
16
|
hasRealSession,
|
|
16
17
|
} from './real-data.js'
|
|
17
18
|
|
|
@@ -27,6 +28,8 @@ import {
|
|
|
27
28
|
* renderExtractItems F9 截断是纯 fixture,无条件跑。
|
|
28
29
|
*/
|
|
29
30
|
|
|
31
|
+
// 真实数据套件 timeout 说明:find 全扫描 + 5.6MB 文件解析在并发/高负载下可能超过
|
|
32
|
+
// vitest 默认 5s(pnpm extensions:test 全量跑时多包集成测试并发 IO),显式放宽到 60s。
|
|
30
33
|
describe.skipIf(!HAS_REAL)('handleSessionRead', () => {
|
|
31
34
|
it('1. find by uuid fragment returns matching session', async () => {
|
|
32
35
|
const r = await handleSessionRead({ action: 'find', query: 'e6c96' }, REAL)
|
|
@@ -54,6 +57,11 @@ describe.skipIf(!HAS_REAL)('handleSessionRead', () => {
|
|
|
54
57
|
})
|
|
55
58
|
|
|
56
59
|
it('4. family lists fork children and隔代 subagents', async () => {
|
|
60
|
+
// 数据守卫:fork 子代 019fe632(sessions/)与隔代 subagent 019fe635(subagents/)
|
|
61
|
+
// 位于活跃数据目录(subagents 目录随 GC/新建持续变化),文件被清理/重命名时跳过
|
|
62
|
+
// 而非失败——与 TC14-TC18 的「数据不存在则 return」守卫模式一致,避免偶发红。
|
|
63
|
+
// 注意必须用 hasAnyRealSession(双目录),hasRealSession 只扫 sessions/ 扫不到 019fe635。
|
|
64
|
+
if (!hasAnyRealSession('019fe632') || !hasAnyRealSession('019fe635')) return
|
|
57
65
|
const r = await handleSessionRead({ action: 'family', session: FAM }, REAL)
|
|
58
66
|
const d = r.details as {
|
|
59
67
|
forks: Array<{ sessionId: string }>
|
|
@@ -114,7 +122,7 @@ describe.skipIf(!HAS_REAL)('handleSessionRead', () => {
|
|
|
114
122
|
const dFrag = rFrag.details as { turns: unknown[] }
|
|
115
123
|
expect(dFrag.turns.length).toBe(dFull.turns.length)
|
|
116
124
|
})
|
|
117
|
-
})
|
|
125
|
+
}, 60000)
|
|
118
126
|
|
|
119
127
|
describe.skipIf(!HAS_E6)('extract (v2 O4)', () => {
|
|
120
128
|
it('user-messages returns 26 user entries with turn + full text', async () => {
|
|
@@ -292,7 +300,7 @@ describe.skipIf(!HAS_E6)('extract (v2 O4)', () => {
|
|
|
292
300
|
expect(msg).not.toContain('用 outline 重看有效范围')
|
|
293
301
|
expect(msg).toContain('该 session extract 共')
|
|
294
302
|
})
|
|
295
|
-
})
|
|
303
|
+
}, 60000)
|
|
296
304
|
|
|
297
305
|
// ---- fixture 工具(tmpdir 造最小 session 文件,供 F2/MF-5 用例)----
|
|
298
306
|
|
|
@@ -924,7 +932,7 @@ describe.skipIf(!HAS_REAL_SUBAGENTS_DIR)('真实数据:subagent sa-id(w2 TC1
|
|
|
924
932
|
handleSessionRead({ action: 'outline', session: fakeId }, REAL),
|
|
925
933
|
).rejects.toThrow('无匹配 record')
|
|
926
934
|
})
|
|
927
|
-
})
|
|
935
|
+
}, 60000)
|
|
928
936
|
|
|
929
937
|
// ============================================================
|
|
930
938
|
// w6: doWorkflow action(TC-w6-single-run/multi-run/runid-filter/runid-not-found/no-runs/snapshot-skip/call-jump)
|
|
@@ -1297,7 +1305,7 @@ describe.skipIf(!HAS_REAL_WF_SESSION)('doWorkflow - 真实数据守卫', () => {
|
|
|
1297
1305
|
expect(r.content[0].text).toContain('run:')
|
|
1298
1306
|
expect(r.content[0].text).toContain('budget:')
|
|
1299
1307
|
}, 30000)
|
|
1300
|
-
})
|
|
1308
|
+
}, 60000)
|
|
1301
1309
|
|
|
1302
1310
|
// ============================================================
|
|
1303
1311
|
// m3b:doFamily recursive false/true(TC-m3b-dofamily-recursive-false/true)
|
|
@@ -138,5 +138,6 @@ describe('segmentTurns', () => {
|
|
|
138
138
|
expect(turns.filter((t) => t.isCompaction).length).toBe(5)
|
|
139
139
|
// 其中有 26 个 user turn
|
|
140
140
|
expect(turns.filter((t) => t.userEntry !== undefined).length).toBe(26)
|
|
141
|
-
|
|
141
|
+
// 5.6MB 全量解析在并发/高负载下可能超 vitest 默认 5s,显式放宽
|
|
142
|
+
}, 60000)
|
|
142
143
|
})
|
|
@@ -5,7 +5,7 @@ import { mkdtemp, writeFile, rm } from 'node:fs/promises'
|
|
|
5
5
|
import { join } from 'node:path'
|
|
6
6
|
|
|
7
7
|
import { parseRunSnapshot, renderWorkflowOverview } from '../core/workflow.js'
|
|
8
|
-
import { readRunSnapshot } from '../discovery/workflows.js'
|
|
8
|
+
import { extractCallSessionFiles, readRunSnapshot } from '../discovery/workflows.js'
|
|
9
9
|
import { REAL_AGENT_DIR } from './real-data.js'
|
|
10
10
|
|
|
11
11
|
// ============================================================
|
|
@@ -48,6 +48,42 @@ const NEW_SNAPSHOT_FIXTURE = {
|
|
|
48
48
|
meta: { startedAt: '2026-08-03T13:05:50.111Z', completedAt: '2026-08-03T13:05:55.384Z' },
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* v2 格式 fixture(pi-subagent-workflow 8.x 一次性生命周期写入的 wf-run-v2 快照)。
|
|
53
|
+
* 读取面形状与 v1 一致(state.calls[].sessionFile/result 保留),差异仅版本字面量与
|
|
54
|
+
* status 两态(running/done)、meta 无 pausedAt——v2 不该因版本字面量被挡在读之外。
|
|
55
|
+
*/
|
|
56
|
+
const V2_SNAPSHOT_FIXTURE = {
|
|
57
|
+
v: 'wf-run-v2',
|
|
58
|
+
runId: 'wf-ignore',
|
|
59
|
+
spec: { scriptName: 'one-shot-flow', name: 'Flow Name', scriptSource: '// ...' },
|
|
60
|
+
state: {
|
|
61
|
+
status: 'done',
|
|
62
|
+
reason: 'completed',
|
|
63
|
+
budget: { usedTokens: 22000, usedCost: 0, totalCallCount: 2, maxTokens: 200000 },
|
|
64
|
+
calls: [
|
|
65
|
+
{
|
|
66
|
+
id: 0,
|
|
67
|
+
opts: { prompt: 'task 0', model: 'default', thinkingLevel: 'high', description: 'v2-step-0' },
|
|
68
|
+
status: 'done',
|
|
69
|
+
attempts: 1,
|
|
70
|
+
sessionId: '019v2a',
|
|
71
|
+
sessionFile: '/abs/v2-call0.jsonl',
|
|
72
|
+
traceNode: { stepIndex: 0, agent: 'dev-W1', task: 'task 0', model: 'default', status: 'completed', phase: 'P0' },
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 1,
|
|
76
|
+
opts: { prompt: 'task 1', model: 'default' },
|
|
77
|
+
status: 'done',
|
|
78
|
+
attempts: 1,
|
|
79
|
+
result: { content: 'OK', durationMs: 567, sessionId: '019v2b', sessionFile: '/abs/v2-call1.jsonl' },
|
|
80
|
+
traceNode: { stepIndex: 1, agent: 'dev-W2', task: 'task 1', model: 'default', status: 'completed', phase: 'P1' },
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
meta: { startedAt: '2026-08-10T10:00:00.000Z', completedAt: '2026-08-10T10:05:00.000Z' },
|
|
85
|
+
}
|
|
86
|
+
|
|
51
87
|
/** OLD 格式 fixture(对齐 wf-skip-ok.jsonl:无 v,callCache value 无 sessionFile/result)。 */
|
|
52
88
|
const OLD_SNAPSHOT_FIXTURE = {
|
|
53
89
|
runId: 'wf-old-ignore',
|
|
@@ -98,6 +134,23 @@ describe('parseRunSnapshot', () => {
|
|
|
98
134
|
expect(step.contentPreview).toBe('PROBE-OK')
|
|
99
135
|
})
|
|
100
136
|
|
|
137
|
+
it('TC-w5-parse-new-v2:v2 快照(pi-subagent-workflow 8.x 写入)解析非 null,calls sessionFile 读出', () => {
|
|
138
|
+
const overview = parseRunSnapshot(V2_SNAPSHOT_FIXTURE, 'wf-v2-link-runid', '/abs/wf-v2.jsonl')
|
|
139
|
+
expect(overview).not.toBeNull()
|
|
140
|
+
expect(overview!.version).toBe('wf-run-v2')
|
|
141
|
+
expect(overview!.status).toBe('done')
|
|
142
|
+
expect(overview!.reason).toBe('completed')
|
|
143
|
+
expect(overview!.script).toBe('one-shot-flow')
|
|
144
|
+
// steps:两个 call 的 sessionFile 均读出(顶层优先 / result.sessionFile 回退)
|
|
145
|
+
expect(overview!.steps).toHaveLength(2)
|
|
146
|
+
expect(overview!.steps[0].sessionFile).toBe('/abs/v2-call0.jsonl')
|
|
147
|
+
expect(overview!.steps[0].sessionId).toBe('019v2a')
|
|
148
|
+
expect(overview!.steps[0].description).toBe('v2-step-0')
|
|
149
|
+
expect(overview!.steps[1].sessionFile).toBe('/abs/v2-call1.jsonl')
|
|
150
|
+
expect(overview!.steps[1].sessionId).toBe('019v2b')
|
|
151
|
+
expect(overview!.steps[1].contentPreview).toBe('OK')
|
|
152
|
+
})
|
|
153
|
+
|
|
101
154
|
it('TC-w5-parse-old:OLD 格式 (无 v) 尽力解析为 legacy overview,step status 推测', () => {
|
|
102
155
|
const overview = parseRunSnapshot(OLD_SNAPSHOT_FIXTURE, 'wf-old-link', '/abs/wf-old.jsonl')
|
|
103
156
|
expect(overview).not.toBeNull()
|
|
@@ -123,13 +176,31 @@ describe('parseRunSnapshot', () => {
|
|
|
123
176
|
})
|
|
124
177
|
|
|
125
178
|
it('TC-w5-parse-malformed:既非 NEW 也非 OLD(缺关键字段)返回 null', () => {
|
|
126
|
-
// (a) 有 v 但 v
|
|
127
|
-
|
|
179
|
+
// (a) 有 v 但 v 非 v1/v2 且无 callCache(未来版本 wf-run-v3 及之后——届时须评估
|
|
180
|
+
// 新版本读取面形状再扩判定,v2 因形状兼容被接受)
|
|
181
|
+
expect(parseRunSnapshot({ v: 'wf-run-v3', state: { calls: [] } }, 'r', 's')).toBeNull()
|
|
128
182
|
// (b) 无 v 无 callCache 无 status(异构对象)
|
|
129
183
|
expect(parseRunSnapshot({ foo: 'bar', baz: 1 }, 'r', 's')).toBeNull()
|
|
130
184
|
})
|
|
131
185
|
})
|
|
132
186
|
|
|
187
|
+
// ============================================================
|
|
188
|
+
// extractCallSessionFiles(纯逻辑,family/workflows 腿的 sessionFile 提取入口)
|
|
189
|
+
// ============================================================
|
|
190
|
+
|
|
191
|
+
describe('extractCallSessionFiles', () => {
|
|
192
|
+
it('v2 快照(wf-run-v2)state.calls 的 sessionFile 能被读出,不因版本字面量挡读', () => {
|
|
193
|
+
const files = extractCallSessionFiles(V2_SNAPSHOT_FIXTURE)
|
|
194
|
+
// 顶层 sessionFile + result.sessionFile 回退,两个 call 都读出
|
|
195
|
+
expect(files).toEqual(['/abs/v2-call0.jsonl', '/abs/v2-call1.jsonl'])
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('v1 快照(wf-run-v1)行为不变:state.calls 提取', () => {
|
|
199
|
+
const files = extractCallSessionFiles(NEW_SNAPSHOT_FIXTURE)
|
|
200
|
+
expect(files).toEqual(['/abs/session.jsonl'])
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
133
204
|
// ============================================================
|
|
134
205
|
// renderWorkflowOverview(纯逻辑,TC-w5-render-new/old)
|
|
135
206
|
// ============================================================
|
package/src/core/workflow.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// ============================================================
|
|
4
4
|
//
|
|
5
5
|
// 本文件消费 discovery/workflows.ts 的 readRunSnapshot(返 unknown 原始快照对象),
|
|
6
|
-
// 把 unknown 类型化为 WorkflowOverview(NEW v='wf-run-v1' / OLD 无 v 双格式分支),
|
|
6
|
+
// 把 unknown 类型化为 WorkflowOverview(NEW v='wf-run-v1'/'wf-run-v2' / OLD 无 v 双格式分支),
|
|
7
7
|
// 再渲染为人类可读文本。零 IO:parseRunSnapshot/renderWorkflowOverview 喂 mock 即可单测
|
|
8
8
|
//(w5 TC-wf-core-pure-logic,对齐 session-reader core/* 纯逻辑约定)。
|
|
9
9
|
//
|
|
@@ -78,8 +78,8 @@ export interface WorkflowOverview {
|
|
|
78
78
|
stateFile: string
|
|
79
79
|
/** NEW state.status / OLD 顶层 status */
|
|
80
80
|
status: string
|
|
81
|
-
/**
|
|
82
|
-
version: 'wf-run-v1' | 'legacy'
|
|
81
|
+
/** 格式标记(渲染/调试用)。v2 读取面形状与 v1 一致(pi-subagent-workflow 8.x 一次性生命周期) */
|
|
82
|
+
version: 'wf-run-v1' | 'wf-run-v2' | 'legacy'
|
|
83
83
|
/** NEW spec.scriptName / spec.name / OLD name */
|
|
84
84
|
script?: string
|
|
85
85
|
/** NEW meta.startedAt / OLD startedAt(统一 string) */
|
|
@@ -199,9 +199,9 @@ function mapCacheEntryToStep(entry: unknown, index: number): WorkflowStep {
|
|
|
199
199
|
*
|
|
200
200
|
* 分支(C-parserunsnapshot-dualformat,TC-wf-snapshot-version-union):
|
|
201
201
|
* - 非对象 → null(调用方跳过,ES-wf-snapshot-unparseable)
|
|
202
|
-
* - NEW (snapshot.v === 'wf-run-v1'):state.* / meta.* / spec.*
|
|
202
|
+
* - NEW (snapshot.v === 'wf-run-v1' 或 'wf-run-v2',读取面形状一致):state.* / meta.* / spec.*
|
|
203
203
|
* - OLD (无 v,有 callCache 数组或顶层 status):顶层 status/budget/startedAt + callCache
|
|
204
|
-
* - 既非 NEW 也非 OLD → null
|
|
204
|
+
* - 既非 NEW 也非 OLD → null(未来版本(如 wf-run-v3)/ 异构内容)
|
|
205
205
|
*
|
|
206
206
|
* runId/stateFile 透传参数(不读 snapshot.runId,保证与 family.workflows 一致,避免 OLD 顶层
|
|
207
207
|
* runId 可信度低的不一致)。零 any(全程 typeof/Array.isArray/isRecord 守卫收窄)。
|
|
@@ -213,8 +213,9 @@ export function parseRunSnapshot(
|
|
|
213
213
|
): WorkflowOverview | null {
|
|
214
214
|
if (!isRecord(snapshot)) return null
|
|
215
215
|
|
|
216
|
-
// NEW 格式(v === 'wf-run-v1')
|
|
217
|
-
|
|
216
|
+
// NEW 格式(v === 'wf-run-v1' || 'wf-run-v2',v2 读取面形状兼容 v1)
|
|
217
|
+
const v = snapshot.v
|
|
218
|
+
if (v === 'wf-run-v1' || v === 'wf-run-v2') {
|
|
218
219
|
const state = isRecord(snapshot.state) ? snapshot.state : {}
|
|
219
220
|
const meta = isRecord(snapshot.meta) ? snapshot.meta : {}
|
|
220
221
|
const spec = isRecord(snapshot.spec) ? snapshot.spec : {}
|
|
@@ -223,7 +224,7 @@ export function parseRunSnapshot(
|
|
|
223
224
|
runId,
|
|
224
225
|
stateFile,
|
|
225
226
|
status: typeof state.status === 'string' ? state.status : '',
|
|
226
|
-
version:
|
|
227
|
+
version: v,
|
|
227
228
|
script:
|
|
228
229
|
typeof spec.scriptName === 'string'
|
|
229
230
|
? spec.scriptName
|
package/src/discovery/roots.ts
CHANGED
|
@@ -19,8 +19,9 @@ export interface SessionFileMeta {
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* main sessions 扫描时整体跳过的子目录名。
|
|
22
|
-
* `workflow-state` 目录存放 workflow 运行状态文件(wf-*.jsonl,首行 `{"v":"wf-run-v1"...}
|
|
23
|
-
*
|
|
22
|
+
* `workflow-state` 目录存放 workflow 运行状态文件(wf-*.jsonl,首行 `{"v":"wf-run-v1"|"wf-run-v2"...}`,
|
|
23
|
+
* 版本随 subagent-workflow 快照格式演进,读取侧 v1/v2 兼容),非 session 文件——属 family 腿
|
|
24
|
+
* 独立处理(design §3.3 D-7),扫描 main sessions 时排除,
|
|
24
25
|
* 否则会把 wf 文件误收为 session(且 find.ts 读其首行 header 时会因 type≠session 被丢弃,
|
|
25
26
|
* 在此排除可避免这批无效首行扫描)。
|
|
26
27
|
*/
|
|
@@ -25,14 +25,16 @@ import { extractSessionIdFromFilename } from './subagents.js'
|
|
|
25
25
|
* 从 wf-state 快照对象提取 calls[].sessionFile(绝对路径数组)。
|
|
26
26
|
*
|
|
27
27
|
* 两种格式(探查确认,本机 371 个 wf 文件):
|
|
28
|
-
* - NEW (v="wf-run-v1"):state.calls[],每项顶层
|
|
28
|
+
* - NEW (v="wf-run-v1" 或 "wf-run-v2",读取面形状一致):state.calls[],每项顶层
|
|
29
|
+
* .sessionFile(258 文件 / 1590 sessionFile)。v2 由 pi-subagent-workflow 8.x 一次性
|
|
30
|
+
* 生命周期收敛引入(status 两态、无 pausedAt),calls[].sessionFile/result 保留
|
|
29
31
|
* - OLD (无 v):callCache[]=[{key,value}],value.sessionFile(112 文件 / 0 sessionFile,旧 pi 不持久化)
|
|
30
32
|
*/
|
|
31
33
|
export function extractCallSessionFiles(snap: unknown): string[] {
|
|
32
34
|
const out: string[] = []
|
|
33
35
|
if (typeof snap !== 'object' || snap === null) return out
|
|
34
36
|
const s = snap as Record<string, unknown>
|
|
35
|
-
const isNew = s.v === 'wf-run-v1'
|
|
37
|
+
const isNew = s.v === 'wf-run-v1' || s.v === 'wf-run-v2'
|
|
36
38
|
let callsRaw: unknown
|
|
37
39
|
if (isNew) {
|
|
38
40
|
const state = s.state
|