@zhushanwen/session-core 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.
- package/README.md +3 -3
- package/dist/index.d.ts +30 -9
- package/dist/index.js +32 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,13 +6,13 @@ Coding-agent session 的 canonical 模型与纯 JSONL 原语基座。**零运行
|
|
|
6
6
|
|
|
7
7
|
「把某个 coding-agent 的 session 读成统一结构」的唯一落点:session-reader(pi 会话读取)、runtime 导入、未来的 coding-agent source 包共用本基座,取代各自为政的字节层副本(首行读取、entry 解析、id 归一化)。
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## 导出清单
|
|
10
10
|
|
|
11
11
|
| 导出 | 说明 |
|
|
12
12
|
|------|------|
|
|
13
13
|
| `Entry` / `ParseResult` / `SessionHeader` / `NormalizedSession` | canonical 模型(D1):严格 Entry 树,坏行丢弃不占位 |
|
|
14
|
-
| `parseSessionContent(content)` | JSONL 文本 → `ParseResult`(坏行计 `skippedLines
|
|
15
|
-
| `parseSessionFile(path)` | `parseSessionContent` 的 async 文件版(ENOENT 等按 fs
|
|
14
|
+
| `parseSessionContent(content)` | JSONL 文本 → `ParseResult`(坏行计 `skippedLines`;message 存在但 role 不在词表时整文件抛 `UnknownMessageRoleError`——fail-fast,不静默丢内容) |
|
|
15
|
+
| `parseSessionFile(path)` | `parseSessionContent` 的 async 文件版(ENOENT 等按 fs 原生错误抛出;非法 role 同样抛 `UnknownMessageRoleError`) |
|
|
16
16
|
| `serializeSession(entries)` | `Entry[]` → JSONL,与 pi 产物逐字节兼容(`JSON.stringify(entry) + '\n'`) |
|
|
17
17
|
| `readFirstJsonlLine(path)` / `readFirstJsonlLineSync(path)` | 首行字节原语(async/sync 双形态):只读首行、处理 CRLF/LF、跨块多字节安全;空文件/纯空白 → `undefined`;IO 错误上抛由消费方分流 |
|
|
18
18
|
| `sessionIdFromFileName(fileName)` | 文件名不变量:剥 `.jsonl` 后最后一个 `_` 之后的尾段;提取不出 → `undefined` |
|
package/dist/index.d.ts
CHANGED
|
@@ -14,8 +14,16 @@
|
|
|
14
14
|
* 冻结接口未列的 per-type 附加字段不在此暴露;消费方需要时先扩展本接口——解析层
|
|
15
15
|
* 不私自保留未知字段,保证「读到的就是模型声明的」。
|
|
16
16
|
*/
|
|
17
|
-
/**
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* pi message entry 的 role 域(窄联合:解析时值检查收窄,词表外 role 整文件解析
|
|
19
|
+
* 抛 UnknownMessageRoleError——fail-fast,不静默丢 message)。
|
|
20
|
+
*
|
|
21
|
+
* 四值闭集与 pi 1.0 AgentMessage 的 role 全集对齐(pi-ai dist/types.d.ts SystemMessage/
|
|
22
|
+
* UserMessage/AssistantMessage/ToolResultMessage)。词表外 role 意味着写入方 pi 版本
|
|
23
|
+
* 较新:显式失败优于静默丢内容(用户裁决 2026-10-30),pi 新增 role 时扩展本词表
|
|
24
|
+
*(编译期穷尽断言强制清单同步)并补行为测试。
|
|
25
|
+
*/
|
|
26
|
+
type SessionMessageRole = 'user' | 'assistant' | 'toolResult' | 'system';
|
|
19
27
|
interface Entry {
|
|
20
28
|
type: string;
|
|
21
29
|
id: string;
|
|
@@ -41,8 +49,6 @@ interface ParseResult {
|
|
|
41
49
|
/** JSON 解析失败(含缺必填结构字段)的行数 */
|
|
42
50
|
skippedLines: number;
|
|
43
51
|
totalBytes: number;
|
|
44
|
-
/** 最后一行疑似半行(活跃 session 写入中),区别于中间坏行 */
|
|
45
|
-
lastLinePartial: boolean;
|
|
46
52
|
}
|
|
47
53
|
/**
|
|
48
54
|
* session header(首行 type='session' entry 的模型)。
|
|
@@ -110,15 +116,30 @@ interface NormalizedSession {
|
|
|
110
116
|
*
|
|
111
117
|
* 坏行语义(严格模型,设计 D1):逐行 JSON.parse,语法错误或缺必填结构字段计入
|
|
112
118
|
* skippedLines 并丢弃,不占位、不中断整体解析。宽松「坏行保留占位」形态是
|
|
113
|
-
* core/session-trace 的私有语义(设计 D1
|
|
119
|
+
* core/session-trace 的私有语义(设计 D1 不采用项),不进本基座。
|
|
120
|
+
*
|
|
121
|
+
* 非法 message role 不走坏行容错:message 存在但 role 不在 SESSION_MESSAGE_ROLES
|
|
122
|
+
* 词表时整文件解析抛 UnknownMessageRoleError(fail-fast)。词表外 role 意味着写入方
|
|
123
|
+
* pi 版本较本包词表更新,静默丢 message 会造成会话内容不可见地损失——显式失败优于
|
|
124
|
+
* 静默丢内容(用户裁决 2026-10-30),扩展词表(升级)后即恢复可读。
|
|
114
125
|
*/
|
|
115
126
|
|
|
127
|
+
/**
|
|
128
|
+
* message.role 不在合法词表时的解析错误(fail-fast 通道):整文件解析抛出,
|
|
129
|
+
* 不折叠进 skippedLines、不产出丢弃 message 的空壳 entry。
|
|
130
|
+
*/
|
|
131
|
+
declare class UnknownMessageRoleError extends Error {
|
|
132
|
+
/** 非法行所在行号(1-based,与编辑器行号一致) */
|
|
133
|
+
readonly line: number;
|
|
134
|
+
/** 该行 message.role 的原始值(缺失时为 undefined) */
|
|
135
|
+
readonly role: unknown;
|
|
136
|
+
constructor(line: number, role: unknown);
|
|
137
|
+
}
|
|
116
138
|
/**
|
|
117
139
|
* 解析 session JSONL 文本为 entries。
|
|
118
140
|
*
|
|
119
|
-
* 末尾换行产生的空行忽略(不计 skipped
|
|
120
|
-
* (正常 JSONL
|
|
121
|
-
* (活跃 session 写到一半的半行),区别于中间坏行。
|
|
141
|
+
* 末尾换行产生的空行忽略(不计 skipped);中间空行容错跳过
|
|
142
|
+
* (正常 JSONL 无空行,防御文件损坏)。
|
|
122
143
|
*/
|
|
123
144
|
declare function parseSessionContent(content: string): ParseResult;
|
|
124
145
|
/** 读取 session 文件并解析。文件不存在按 Node fs 原生错误抛出(ENOENT)。 */
|
|
@@ -181,4 +202,4 @@ declare function sessionIdFromFileName(fileName: string): string | undefined;
|
|
|
181
202
|
declare const ZCODE_ROW_ID_WIDTH = 8;
|
|
182
203
|
declare function normalizeZcodeRowId(rowId: number): string;
|
|
183
204
|
|
|
184
|
-
export { type Entry, type ImportDegradation, type NormalizedSession, type ParseResult, type SessionHeader, type SessionMessageRole, ZCODE_ROW_ID_WIDTH, normalizeZcodeRowId, parseSessionContent, parseSessionFile, readFirstJsonlLine, readFirstJsonlLineSync, serializeSession, sessionIdFromFileName };
|
|
205
|
+
export { type Entry, type ImportDegradation, type NormalizedSession, type ParseResult, type SessionHeader, type SessionMessageRole, UnknownMessageRoleError, ZCODE_ROW_ID_WIDTH, normalizeZcodeRowId, parseSessionContent, parseSessionFile, readFirstJsonlLine, readFirstJsonlLineSync, serializeSession, sessionIdFromFileName };
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,36 @@
|
|
|
1
1
|
// src/parse.ts
|
|
2
2
|
import { readFile } from "fs/promises";
|
|
3
|
+
|
|
4
|
+
// src/types.ts
|
|
5
|
+
var SESSION_MESSAGE_ROLES = [
|
|
6
|
+
"user",
|
|
7
|
+
"assistant",
|
|
8
|
+
"toolResult",
|
|
9
|
+
"system"
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
// src/parse.ts
|
|
13
|
+
var UnknownMessageRoleError = class extends Error {
|
|
14
|
+
/** 非法行所在行号(1-based,与编辑器行号一致) */
|
|
15
|
+
line;
|
|
16
|
+
/** 该行 message.role 的原始值(缺失时为 undefined) */
|
|
17
|
+
role;
|
|
18
|
+
constructor(line, role) {
|
|
19
|
+
super(
|
|
20
|
+
`session JSONL \u7B2C ${line} \u884C message.role \u975E\u6CD5\uFF1A${JSON.stringify(role) ?? String(role)}\uFF08\u5408\u6CD5\u503C\uFF1A${SESSION_MESSAGE_ROLES.join("/")}\uFF09\u3002\u{1F449} \u8BE5\u884C\u5185\u5BB9\u4E0D\u4F1A\u88AB\u9759\u9ED8\u4E22\u5F03\uFF1A\u5199\u5165\u65B9 pi \u7248\u672C\u53EF\u80FD\u8F83\u65B0\uFF08\u65B0\u589E role \u800C\u672C\u5305\u8BCD\u8868\u672A\u6536\u5F55\uFF09\uFF0C\u5347\u7EA7 taiji \u540E\u91CD\u8BD5\uFF0C\u6216\u4EBA\u5DE5\u4FEE\u590D\u8BE5\u884C\u7684 role \u5B57\u6BB5\u3002`
|
|
21
|
+
);
|
|
22
|
+
this.name = "UnknownMessageRoleError";
|
|
23
|
+
this.line = line;
|
|
24
|
+
this.role = role;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
3
27
|
function isMessageRole(v) {
|
|
4
|
-
return v
|
|
28
|
+
return SESSION_MESSAGE_ROLES.includes(v);
|
|
5
29
|
}
|
|
6
|
-
function parseEntryMessage(raw) {
|
|
30
|
+
function parseEntryMessage(raw, line) {
|
|
7
31
|
if (raw === null || typeof raw !== "object") return void 0;
|
|
8
32
|
const m = raw;
|
|
9
|
-
if (!isMessageRole(m.role))
|
|
33
|
+
if (!isMessageRole(m.role)) throw new UnknownMessageRoleError(line, m.role);
|
|
10
34
|
const message = { role: m.role, content: m.content };
|
|
11
35
|
if (Array.isArray(m.toolCalls)) message.toolCalls = m.toolCalls;
|
|
12
36
|
if (typeof m.toolName === "string") message.toolName = m.toolName;
|
|
@@ -20,7 +44,7 @@ function assignOptionalFields(obj, entry) {
|
|
|
20
44
|
if (typeof obj.cwd === "string") entry.cwd = obj.cwd;
|
|
21
45
|
if (obj.summary !== void 0) entry.summary = obj.summary;
|
|
22
46
|
}
|
|
23
|
-
function toEntry(raw) {
|
|
47
|
+
function toEntry(raw, line) {
|
|
24
48
|
if (typeof raw !== "object" || raw === null) return void 0;
|
|
25
49
|
const obj = raw;
|
|
26
50
|
if (typeof obj.type !== "string") return void 0;
|
|
@@ -32,7 +56,7 @@ function toEntry(raw) {
|
|
|
32
56
|
parentId: typeof obj.parentId === "string" ? obj.parentId : null
|
|
33
57
|
};
|
|
34
58
|
if (typeof obj.timestamp === "string") entry.timestamp = obj.timestamp;
|
|
35
|
-
const message = parseEntryMessage(obj.message);
|
|
59
|
+
const message = parseEntryMessage(obj.message, line);
|
|
36
60
|
if (message !== void 0) entry.message = message;
|
|
37
61
|
assignOptionalFields(obj, entry);
|
|
38
62
|
return entry;
|
|
@@ -40,27 +64,23 @@ function toEntry(raw) {
|
|
|
40
64
|
function parseSessionContent(content) {
|
|
41
65
|
const entries = [];
|
|
42
66
|
let skippedLines = 0;
|
|
43
|
-
let lastLinePartial = false;
|
|
44
67
|
const lines = content.split("\n");
|
|
45
68
|
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
46
69
|
lines.pop();
|
|
47
70
|
}
|
|
48
71
|
for (let i = 0; i < lines.length; i++) {
|
|
49
72
|
const line = lines[i];
|
|
50
|
-
const isLast = i === lines.length - 1;
|
|
51
73
|
if (line.trim() === "") continue;
|
|
52
74
|
let raw;
|
|
53
75
|
try {
|
|
54
76
|
raw = JSON.parse(line);
|
|
55
77
|
} catch {
|
|
56
78
|
skippedLines++;
|
|
57
|
-
if (isLast) lastLinePartial = true;
|
|
58
79
|
continue;
|
|
59
80
|
}
|
|
60
|
-
const entry = toEntry(raw);
|
|
81
|
+
const entry = toEntry(raw, i + 1);
|
|
61
82
|
if (entry === void 0) {
|
|
62
83
|
skippedLines++;
|
|
63
|
-
if (isLast) lastLinePartial = true;
|
|
64
84
|
continue;
|
|
65
85
|
}
|
|
66
86
|
entries.push(entry);
|
|
@@ -68,8 +88,7 @@ function parseSessionContent(content) {
|
|
|
68
88
|
return {
|
|
69
89
|
entries,
|
|
70
90
|
skippedLines,
|
|
71
|
-
totalBytes: Buffer.byteLength(content, "utf8")
|
|
72
|
-
lastLinePartial
|
|
91
|
+
totalBytes: Buffer.byteLength(content, "utf8")
|
|
73
92
|
};
|
|
74
93
|
}
|
|
75
94
|
async function parseSessionFile(filePath) {
|
|
@@ -170,6 +189,7 @@ function normalizeZcodeRowId(rowId) {
|
|
|
170
189
|
return rowId.toString(HEX_RADIX).padStart(ZCODE_ROW_ID_WIDTH, "0");
|
|
171
190
|
}
|
|
172
191
|
export {
|
|
192
|
+
UnknownMessageRoleError,
|
|
173
193
|
ZCODE_ROW_ID_WIDTH,
|
|
174
194
|
normalizeZcodeRowId,
|
|
175
195
|
parseSessionContent,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/session-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Canonical session model and pure JSONL primitives shared by session readers and importers: Entry/ParseResult/SessionHeader/NormalizedSession, parse/serialize, first-line byte primitive, session id invariants. Zero runtime dependencies.",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -26,6 +26,6 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "tsup",
|
|
28
28
|
"typecheck": "tsc --noEmit",
|
|
29
|
-
"test": "vitest run"
|
|
29
|
+
"test": "vitest run --pool=threads"
|
|
30
30
|
}
|
|
31
31
|
}
|