@zhushanwen/session-core 0.12.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 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`,末行半行记 `lastLinePartial`) |
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
@@ -15,12 +15,13 @@
15
15
  * 不私自保留未知字段,保证「读到的就是模型声明的」。
16
16
  */
17
17
  /**
18
- * pi message entry 的 role 域(窄联合:解析时值守卫收窄,非法 role 计坏行)。
18
+ * pi message entry 的 role 域(窄联合:解析时值检查收窄,词表外 role 整文件解析
19
+ * 抛 UnknownMessageRoleError——fail-fast,不静默丢 message)。
19
20
  *
20
21
  * 四值闭集与 pi 1.0 AgentMessage 的 role 全集对齐(pi-ai dist/types.d.ts SystemMessage/
21
- * UserMessage/AssistantMessage/ToolResultMessage)。1.0 起 system 消息落盘
22
- * (工具集变更高频追加),三值时代 role=system 的行在守卫处丢弃 message 字段
23
- * (内容静默丢失);四值放行后 expand/详情可读。
22
+ * UserMessage/AssistantMessage/ToolResultMessage)。词表外 role 意味着写入方 pi 版本
23
+ * 较新:显式失败优于静默丢内容(用户裁决 2026-10-30),pi 新增 role 时扩展本词表
24
+ *(编译期穷尽断言强制清单同步)并补行为测试。
24
25
  */
25
26
  type SessionMessageRole = 'user' | 'assistant' | 'toolResult' | 'system';
26
27
  interface Entry {
@@ -48,8 +49,6 @@ interface ParseResult {
48
49
  /** JSON 解析失败(含缺必填结构字段)的行数 */
49
50
  skippedLines: number;
50
51
  totalBytes: number;
51
- /** 最后一行疑似半行(活跃 session 写入中),区别于中间坏行 */
52
- lastLinePartial: boolean;
53
52
  }
54
53
  /**
55
54
  * session header(首行 type='session' entry 的模型)。
@@ -117,15 +116,30 @@ interface NormalizedSession {
117
116
  *
118
117
  * 坏行语义(严格模型,设计 D1):逐行 JSON.parse,语法错误或缺必填结构字段计入
119
118
  * skippedLines 并丢弃,不占位、不中断整体解析。宽松「坏行保留占位」形态是
120
- * 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),扩展词表(升级)后即恢复可读。
121
125
  */
122
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
+ }
123
138
  /**
124
139
  * 解析 session JSONL 文本为 entries。
125
140
  *
126
- * 末尾换行产生的空行忽略(不计 skipped、不计 partial);中间空行容错跳过
127
- * (正常 JSONL 无空行,防御文件损坏)。最后一行 parse 失败时 lastLinePartial=true
128
- * (活跃 session 写到一半的半行),区别于中间坏行。
141
+ * 末尾换行产生的空行忽略(不计 skipped);中间空行容错跳过
142
+ * (正常 JSONL 无空行,防御文件损坏)。
129
143
  */
130
144
  declare function parseSessionContent(content: string): ParseResult;
131
145
  /** 读取 session 文件并解析。文件不存在按 Node fs 原生错误抛出(ENOENT)。 */
@@ -188,4 +202,4 @@ declare function sessionIdFromFileName(fileName: string): string | undefined;
188
202
  declare const ZCODE_ROW_ID_WIDTH = 8;
189
203
  declare function normalizeZcodeRowId(rowId: number): string;
190
204
 
191
- 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
@@ -10,13 +10,27 @@ var SESSION_MESSAGE_ROLES = [
10
10
  ];
11
11
 
12
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
+ };
13
27
  function isMessageRole(v) {
14
28
  return SESSION_MESSAGE_ROLES.includes(v);
15
29
  }
16
- function parseEntryMessage(raw) {
30
+ function parseEntryMessage(raw, line) {
17
31
  if (raw === null || typeof raw !== "object") return void 0;
18
32
  const m = raw;
19
- if (!isMessageRole(m.role)) return void 0;
33
+ if (!isMessageRole(m.role)) throw new UnknownMessageRoleError(line, m.role);
20
34
  const message = { role: m.role, content: m.content };
21
35
  if (Array.isArray(m.toolCalls)) message.toolCalls = m.toolCalls;
22
36
  if (typeof m.toolName === "string") message.toolName = m.toolName;
@@ -30,7 +44,7 @@ function assignOptionalFields(obj, entry) {
30
44
  if (typeof obj.cwd === "string") entry.cwd = obj.cwd;
31
45
  if (obj.summary !== void 0) entry.summary = obj.summary;
32
46
  }
33
- function toEntry(raw) {
47
+ function toEntry(raw, line) {
34
48
  if (typeof raw !== "object" || raw === null) return void 0;
35
49
  const obj = raw;
36
50
  if (typeof obj.type !== "string") return void 0;
@@ -42,7 +56,7 @@ function toEntry(raw) {
42
56
  parentId: typeof obj.parentId === "string" ? obj.parentId : null
43
57
  };
44
58
  if (typeof obj.timestamp === "string") entry.timestamp = obj.timestamp;
45
- const message = parseEntryMessage(obj.message);
59
+ const message = parseEntryMessage(obj.message, line);
46
60
  if (message !== void 0) entry.message = message;
47
61
  assignOptionalFields(obj, entry);
48
62
  return entry;
@@ -50,27 +64,23 @@ function toEntry(raw) {
50
64
  function parseSessionContent(content) {
51
65
  const entries = [];
52
66
  let skippedLines = 0;
53
- let lastLinePartial = false;
54
67
  const lines = content.split("\n");
55
68
  while (lines.length > 0 && lines[lines.length - 1] === "") {
56
69
  lines.pop();
57
70
  }
58
71
  for (let i = 0; i < lines.length; i++) {
59
72
  const line = lines[i];
60
- const isLast = i === lines.length - 1;
61
73
  if (line.trim() === "") continue;
62
74
  let raw;
63
75
  try {
64
76
  raw = JSON.parse(line);
65
77
  } catch {
66
78
  skippedLines++;
67
- if (isLast) lastLinePartial = true;
68
79
  continue;
69
80
  }
70
- const entry = toEntry(raw);
81
+ const entry = toEntry(raw, i + 1);
71
82
  if (entry === void 0) {
72
83
  skippedLines++;
73
- if (isLast) lastLinePartial = true;
74
84
  continue;
75
85
  }
76
86
  entries.push(entry);
@@ -78,8 +88,7 @@ function parseSessionContent(content) {
78
88
  return {
79
89
  entries,
80
90
  skippedLines,
81
- totalBytes: Buffer.byteLength(content, "utf8"),
82
- lastLinePartial
91
+ totalBytes: Buffer.byteLength(content, "utf8")
83
92
  };
84
93
  }
85
94
  async function parseSessionFile(filePath) {
@@ -180,6 +189,7 @@ function normalizeZcodeRowId(rowId) {
180
189
  return rowId.toString(HEX_RADIX).padStart(ZCODE_ROW_ID_WIDTH, "0");
181
190
  }
182
191
  export {
192
+ UnknownMessageRoleError,
183
193
  ZCODE_ROW_ID_WIDTH,
184
194
  normalizeZcodeRowId,
185
195
  parseSessionContent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/session-core",
3
- "version": "0.12.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
  }