@zhushanwen/pi-session-reader 0.13.3 → 0.14.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.
- package/package.json +6 -6
- package/src/__tests__/parser.test.ts +16 -19
- package/src/discovery/entry-anchor.ts +29 -12
- package/src/tool-handler.ts +12 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-session-reader",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"taiji": {
|
|
@@ -16,16 +16,16 @@
|
|
|
16
16
|
"pi-package"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@zhushanwen/
|
|
20
|
-
"@zhushanwen/pi-ext-guards": "0.5.0",
|
|
19
|
+
"@zhushanwen/pi-ext-guards": "0.5.1",
|
|
21
20
|
"@zhushanwen/pi-extension-logger": "0.6.2",
|
|
22
|
-
"@zhushanwen/
|
|
23
|
-
"@zhushanwen/
|
|
21
|
+
"@zhushanwen/extension-protocol": "0.27.0",
|
|
22
|
+
"@zhushanwen/session-core": "0.13.0",
|
|
23
|
+
"@zhushanwen/zcode-session-source": "0.2.10"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@vitest/coverage-v8": "^4.1.9",
|
|
27
27
|
"vitest": "^4.1.8",
|
|
28
|
-
"@zhushanwen/subagent-core": "2.
|
|
28
|
+
"@zhushanwen/subagent-core": "2.2.0"
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
"index.ts",
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'
|
|
|
2
2
|
import { mkdtemp, writeFile, rm } from 'node:fs/promises'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
|
-
import { parseSessionContent, parseSessionFile, type Entry } from '@zhushanwen/session-core'
|
|
5
|
+
import { parseSessionContent, parseSessionFile, UnknownMessageRoleError, type Entry } from '@zhushanwen/session-core'
|
|
6
6
|
|
|
7
7
|
/** 构造单行 JSONL entry 字符串 */
|
|
8
8
|
function line(obj: Record<string, unknown>): string {
|
|
@@ -22,7 +22,6 @@ describe('parseSessionContent', () => {
|
|
|
22
22
|
const result = parseSessionContent(content)
|
|
23
23
|
|
|
24
24
|
expect(result.skippedLines).toBe(0)
|
|
25
|
-
expect(result.lastLinePartial).toBe(false)
|
|
26
25
|
expect(result.entries).toHaveLength(5)
|
|
27
26
|
|
|
28
27
|
const [s, u, a, c, custom] = result.entries
|
|
@@ -69,29 +68,25 @@ describe('parseSessionContent', () => {
|
|
|
69
68
|
expect(result.entries[0].id).toBe('a')
|
|
70
69
|
expect(result.entries[1].id).toBe('b')
|
|
71
70
|
expect(result.skippedLines).toBe(1)
|
|
72
|
-
// 最后一行解析成功 → 非 partial
|
|
73
|
-
expect(result.lastLinePartial).toBe(false)
|
|
74
71
|
})
|
|
75
72
|
|
|
76
|
-
it('空字符串:entries 为空,skippedLines=0
|
|
73
|
+
it('空字符串:entries 为空,skippedLines=0', () => {
|
|
77
74
|
const result = parseSessionContent('')
|
|
78
75
|
|
|
79
76
|
expect(result.entries).toEqual([])
|
|
80
77
|
expect(result.skippedLines).toBe(0)
|
|
81
|
-
expect(result.lastLinePartial).toBe(false)
|
|
82
78
|
expect(result.totalBytes).toBe(0)
|
|
83
79
|
})
|
|
84
80
|
|
|
85
|
-
it('末尾换行不产生幽灵行(不计 skipped
|
|
81
|
+
it('末尾换行不产生幽灵行(不计 skipped)', () => {
|
|
86
82
|
const content = line({ type: 'message', id: 'a', parentId: null }) + '\n'
|
|
87
83
|
const result = parseSessionContent(content)
|
|
88
84
|
|
|
89
85
|
expect(result.entries).toHaveLength(1)
|
|
90
86
|
expect(result.skippedLines).toBe(0)
|
|
91
|
-
expect(result.lastLinePartial).toBe(false)
|
|
92
87
|
})
|
|
93
88
|
|
|
94
|
-
it('最后一行是半 JSON
|
|
89
|
+
it('最后一行是半 JSON:仍计入 skippedLines', () => {
|
|
95
90
|
const content =
|
|
96
91
|
line({ type: 'message', id: 'a', parentId: null }) + '\n' + '{"type":"message","id":"x"'
|
|
97
92
|
const result = parseSessionContent(content)
|
|
@@ -99,7 +94,6 @@ describe('parseSessionContent', () => {
|
|
|
99
94
|
expect(result.entries).toHaveLength(1)
|
|
100
95
|
expect(result.entries[0].id).toBe('a')
|
|
101
96
|
expect(result.skippedLines).toBe(1)
|
|
102
|
-
expect(result.lastLinePartial).toBe(true)
|
|
103
97
|
})
|
|
104
98
|
|
|
105
99
|
it('JSON 合法但缺必填结构字段(type/id)视为坏行', () => {
|
|
@@ -114,8 +108,6 @@ describe('parseSessionContent', () => {
|
|
|
114
108
|
expect(result.entries).toHaveLength(1)
|
|
115
109
|
expect(result.entries[0].id).toBe('ok')
|
|
116
110
|
expect(result.skippedLines).toBe(2)
|
|
117
|
-
// 最后那条坏行也是最后一行 → partial
|
|
118
|
-
expect(result.lastLinePartial).toBe(true)
|
|
119
111
|
})
|
|
120
112
|
|
|
121
113
|
it('custom entry 无顶层 id → 坏行跳过(W4:data.id fallback 死分支已删,pi appendCustomEntry 恒写顶层 id)', () => {
|
|
@@ -144,23 +136,29 @@ describe('parseSessionContent', () => {
|
|
|
144
136
|
expect(result.entries[0].parentId).toBeNull()
|
|
145
137
|
})
|
|
146
138
|
|
|
147
|
-
it('message
|
|
139
|
+
it('message 非法/缺失 role → 整文件解析抛 UnknownMessageRoleError(fail-fast 不静默丢内容)', () => {
|
|
148
140
|
// role 样本用词表外的值('bot')——'system' 已随 pi 1.0 四值词表放行(session-core D9①)
|
|
149
141
|
const content = [
|
|
150
142
|
line({ type: 'message', id: 'm1', parentId: null, message: { role: 'bot', content: 'x' } }),
|
|
151
|
-
line({ type: 'message', id: 'm2', parentId: null, message: { content: 'x' } }),
|
|
152
143
|
].join('\n')
|
|
153
144
|
|
|
154
|
-
|
|
145
|
+
expect(() => parseSessionContent(content)).toThrow(UnknownMessageRoleError)
|
|
146
|
+
expect(() => parseSessionContent(content)).toThrow(/第 1 行.*"bot"/)
|
|
155
147
|
|
|
156
|
-
|
|
148
|
+
// message 存在但缺失 role 同样 fail-fast(role 不在词表)
|
|
149
|
+
const missingRole = line({ type: 'message', id: 'm2', parentId: null, message: { content: 'x' } })
|
|
150
|
+
expect(() => parseSessionContent(missingRole)).toThrow(UnknownMessageRoleError)
|
|
151
|
+
|
|
152
|
+
// message 键整体缺省照常产出 entry(无 message 结构可校验)
|
|
153
|
+
const noMessage = line({ type: 'message', id: 'm3', parentId: null })
|
|
154
|
+
const result = parseSessionContent(noMessage)
|
|
155
|
+
expect(result.entries).toHaveLength(1)
|
|
157
156
|
expect(result.entries[0].message).toBeUndefined()
|
|
158
|
-
expect(result.entries[1].message).toBeUndefined()
|
|
159
157
|
})
|
|
160
158
|
})
|
|
161
159
|
|
|
162
160
|
describe('parseSessionFile', () => {
|
|
163
|
-
it('磁盘文件全量解析:entries 数 = 行数 / 0 skipped /
|
|
161
|
+
it('磁盘文件全量解析:entries 数 = 行数 / 0 skipped / totalBytes 与内容一致', async () => {
|
|
164
162
|
// mkdtemp 自建自删的 fixture 文件(不读本机真实数据目录):session header +
|
|
165
163
|
// 10 条链式 message 的最小行集,验证「文件读取 → 逐行解析」回路的完整性与计数字段
|
|
166
164
|
const dir = await mkdtemp(join(tmpdir(), 'parser-file-'))
|
|
@@ -187,7 +185,6 @@ describe('parseSessionFile', () => {
|
|
|
187
185
|
|
|
188
186
|
expect(result.entries).toHaveLength(11)
|
|
189
187
|
expect(result.skippedLines).toBe(0)
|
|
190
|
-
expect(result.lastLinePartial).toBe(false)
|
|
191
188
|
expect(result.totalBytes).toBe(Buffer.byteLength(content, 'utf8'))
|
|
192
189
|
} finally {
|
|
193
190
|
await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
@@ -100,31 +100,28 @@ function extractZcodeSessionRefAnchor(engineHandle: unknown): ZcodeAnchor | unde
|
|
|
100
100
|
* 多候选文件:按候选列表顺序逐文件扫,文件内有命中锚(含末条裁决后)即止——
|
|
101
101
|
* 候选顺序 = 调用方声明的优先级(liveSessionDir 优先),不跨文件合并。
|
|
102
102
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
103
|
+
* 行级容错:坏行(非法 JSON/缺 type/id)由基座 parseSessionContent 跳过继续;message
|
|
104
|
+
* 存在但 role 不在词表时基座整文件解析抛 UnknownMessageRoleError(fail-fast,不静默
|
|
105
|
+
* 丢内容)——readParsedEntriesBestEffort 把两类读失败(IO / 解析 fail-fast)统一收敛为
|
|
106
|
+
* undefined,候选跳过。
|
|
105
107
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
+
* 候选文件读失败(不存在/不可读/解析 fail-fast)= 该候选无命中,跳过继续下一候选;
|
|
109
|
+
* 本函数契约是「锚或 not-found」,IO 错误同样收敛到 not-found 信号(调用方映射错误码),
|
|
108
110
|
* 不向上抛。
|
|
109
111
|
*
|
|
110
112
|
* 读取用 node:fs/promises 的 async readFile——本函数跑在 pi 扩展进程内,同步
|
|
111
113
|
* readFileSync 全文读入会阻塞事件循环(同函数族 classifyIncompleteEntryAnchor
|
|
112
114
|
* 同形态;对照 readSessionHeaderIdSync 只在首行 4KB 有界读时才允许同步)。
|
|
113
115
|
*
|
|
114
|
-
* @returns
|
|
116
|
+
* @returns 命中的锚;所有候选均未命中(或全部候选不可读/解析失败)→ undefined(not-found 信号)
|
|
115
117
|
*/
|
|
116
118
|
export async function findZcodeEntryAnchor(
|
|
117
119
|
candidateFiles: readonly string[],
|
|
118
120
|
saId: string,
|
|
119
121
|
): Promise<ZcodeAnchor | undefined> {
|
|
120
122
|
for (const file of candidateFiles) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
content = await readFile(file, 'utf8')
|
|
124
|
-
} catch {
|
|
125
|
-
continue
|
|
126
|
-
}
|
|
127
|
-
const { entries } = parseSessionContent(content)
|
|
123
|
+
const entries = await readParsedEntriesBestEffort(file)
|
|
124
|
+
if (entries === undefined) continue
|
|
128
125
|
let anchor: ZcodeAnchor | undefined
|
|
129
126
|
for (const entry of entries) {
|
|
130
127
|
const hit = zcodeAnchorOfEntry(entry, saId)
|
|
@@ -134,3 +131,23 @@ export async function findZcodeEntryAnchor(
|
|
|
134
131
|
}
|
|
135
132
|
return undefined
|
|
136
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 读 session 文件并解析为 entries 的 best-effort 形态:读取失败(不存在/不可读)或
|
|
137
|
+
* 解析 fail-fast(message 存在但 role 不在词表等)都返回 undefined——候选扫描语境的
|
|
138
|
+
* 容错读(失败候选不贡献结果);需要显式报错的主读链不走本函数(tool-handler
|
|
139
|
+
* safeParse 把解析错误转为带 👉 的工具错误)。
|
|
140
|
+
*/
|
|
141
|
+
export async function readParsedEntriesBestEffort(file: string): Promise<Entry[] | undefined> {
|
|
142
|
+
let content: string
|
|
143
|
+
try {
|
|
144
|
+
content = await readFile(file, 'utf8')
|
|
145
|
+
} catch {
|
|
146
|
+
return undefined
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
return parseSessionContent(content).entries
|
|
150
|
+
} catch {
|
|
151
|
+
return undefined
|
|
152
|
+
}
|
|
153
|
+
}
|
package/src/tool-handler.ts
CHANGED
|
@@ -46,7 +46,11 @@ import {
|
|
|
46
46
|
} from './discovery/archive-match.js'
|
|
47
47
|
import { resolveSessionRoots } from './discovery/roots.js'
|
|
48
48
|
import { readSessionHeaderIdSync } from './discovery/session-header.js'
|
|
49
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
findZcodeEntryAnchor,
|
|
51
|
+
readParsedEntriesBestEffort,
|
|
52
|
+
type ZcodeAnchor,
|
|
53
|
+
} from './discovery/entry-anchor.js'
|
|
50
54
|
import {
|
|
51
55
|
listZcodeManifests,
|
|
52
56
|
readZcodeManifest,
|
|
@@ -59,11 +63,7 @@ import {
|
|
|
59
63
|
type RecordManifest,
|
|
60
64
|
} from './discovery/subagents.js'
|
|
61
65
|
import { readRunSnapshot, resolveWorkflows } from './discovery/workflows.js'
|
|
62
|
-
import {
|
|
63
|
-
parseSessionContent,
|
|
64
|
-
parseSessionFile,
|
|
65
|
-
type ParseResult,
|
|
66
|
-
} from '@zhushanwen/session-core'
|
|
66
|
+
import { parseSessionFile, type ParseResult } from '@zhushanwen/session-core'
|
|
67
67
|
import { parseRunRecordStream, parseRunSnapshot, renderWorkflowOverview, type WorkflowOverview } from './core/workflow.js'
|
|
68
68
|
import { buildTreeView } from './core/tree.js'
|
|
69
69
|
import { segmentTurns } from './core/turns.js'
|
|
@@ -358,13 +358,11 @@ async function classifyIncompleteEntryAnchor(
|
|
|
358
358
|
saId: string,
|
|
359
359
|
): Promise<ZcodeAnchorMissingReason | undefined> {
|
|
360
360
|
for (const file of candidateFiles) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
const reason = firstIncompleteAnchorReason(parseSessionContent(content).entries, saId)
|
|
361
|
+
// 读取/解析失败(含非法 message role 的 fail-fast 抛错)按候选跳过:归因分类是
|
|
362
|
+
// best-effort 附加信息,最终 not-found 错误仍由调用方显式抛出
|
|
363
|
+
const entries = await readParsedEntriesBestEffort(file)
|
|
364
|
+
if (entries === undefined) continue
|
|
365
|
+
const reason = firstIncompleteAnchorReason(entries, saId)
|
|
368
366
|
if (reason !== undefined) return reason
|
|
369
367
|
}
|
|
370
368
|
return undefined
|
|
@@ -566,7 +564,7 @@ async function loadZcodeParsed(anchor: ZcodeAnchor, agentDir: string): Promise<P
|
|
|
566
564
|
}
|
|
567
565
|
// ParseResult 形状对齐:zcode 库读无「文件字节/坏行」概念(strict Entry 树已由
|
|
568
566
|
// converter 保证),totalBytes/skippedLines 恒 0
|
|
569
|
-
return { entries: normalized.entries, totalBytes: 0, skippedLines: 0
|
|
567
|
+
return { entries: normalized.entries, totalBytes: 0, skippedLines: 0 }
|
|
570
568
|
} catch (e) {
|
|
571
569
|
if (e instanceof Error && e.message.startsWith('[zcode_')) throw e // 本函数产出的错误范围原样传播
|
|
572
570
|
throw err(zcodeReadErrorMessage(e, agentDir))
|