@deepwhale/core 1.0.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.
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Session Compaction — Sprint 1c-revive-2-D-5-1
3
+ *
4
+ * 当 LLM context token 数逼近 window 上限时, 把前面的对话
5
+ * 总结成 1 条 summary, 替换 messages 中间段, 避免 OOM/超限.
6
+ *
7
+ * 拍板来源 (research/03_reasonix.md compact.go + research/04_pi.md session_before_compact):
8
+ * - 触发条件: promptTokens >= window * compactRatio (默认 0.8, 拍板 source: Reasonix)
9
+ * - Tail 边界: 保留最近 N 条消息 (D-5-1 = message count, D-5-3 升级到 token budget)
10
+ * - Death-loop 防护: 连续 2 次失败 → latch (D-5-2 拍板)
11
+ * - Compaction = 唯一 cache-reset point (改 system prompt 拍板要同步 review)
12
+ *
13
+ * D-5-1 范围 (本 commit):
14
+ * - 基础 trigger (shouldCompact / estimateTokens)
15
+ * - replace (compact 函数 + 'compaction' SessionEvent)
16
+ * - summary 注入: 走 LLM 拍板, 这里只接 summaryFn callback
17
+ * - 测: shouldCompact 边界 / compact 替换 / event kind
18
+ *
19
+ * D-5-2 拍板: stuck latch (连续 2 次失败 → 暂停 + 拍板)
20
+ * D-5-3 拍板: tail token budget 替代 message count
21
+ *
22
+ * @module @deepwhale/core/session/compaction
23
+ */
24
+ export const COMPACTION_DEFAULTS = {
25
+ compactRatio: 0.8,
26
+ tailMode: 'token_budget', // D-5-3 拍板: 默认走 token budget
27
+ tailKeepMessages: 4,
28
+ tailKeepTokens: 500,
29
+ pauseAfterFailures: 2,
30
+ };
31
+ /**
32
+ * 估算 messages 的 token 数.
33
+ *
34
+ * Sprint 1c.5 拍板: 不引入 tiktoken 等依赖, 用 char/4 粗估.
35
+ * 准确度对 compaction 触发点足够 (0.8 阈值留 20% buffer).
36
+ *
37
+ * 估算口径:
38
+ * - role + content 全算
39
+ * - tool_calls 走 JSON.stringify 后 char/4
40
+ * - 工具调用 id/name 算 ~10 token 额外
41
+ */
42
+ export function estimateTokens(messages) {
43
+ let chars = 0;
44
+ for (const m of messages) {
45
+ chars += m.role.length + 1; // "role:"
46
+ chars += m.content.length;
47
+ if (m.tool_calls) {
48
+ for (const tc of m.tool_calls) {
49
+ chars += tc.id.length + tc.name.length + JSON.stringify(tc.args).length + 10;
50
+ }
51
+ }
52
+ if (m.tool_call_id)
53
+ chars += m.tool_call_id.length;
54
+ if (m.name)
55
+ chars += m.name.length;
56
+ chars += 4; // 消息边界 + role token 开销
57
+ }
58
+ return Math.ceil(chars / 4);
59
+ }
60
+ /**
61
+ * 解析 tail 边界 (Sprint 1c-revive-2-D-5-3):
62
+ * 拍板 'message_count' 走 tailKeepMessages, 'token_budget' 走 tailKeepTokens.
63
+ *
64
+ * 返 { tailStart, head }:
65
+ * - tailStart: tail 段的起始 index (head = messages[0..tailStart))
66
+ * - tail: messages[tailStart..] (>= 1 条, 拍板不变量)
67
+ *
68
+ * 不变量: messages.length > 0 时 tail 至少 1 条
69
+ * messages.length == 0 时 tailStart = 0, tail = []
70
+ *
71
+ * Reasonix compact.go:271-289 拍板 source: tailStart 算到 token >= tailKeepTokens,
72
+ * 拍板让 tail 大小跟 message 数量解耦.
73
+ */
74
+ export function resolveTail(messages, config) {
75
+ const tailMode = config.tailMode ?? COMPACTION_DEFAULTS.tailMode;
76
+ if (messages.length === 0) {
77
+ return { tailStart: 0, head: [], tail: [] };
78
+ }
79
+ if (tailMode === 'message_count') {
80
+ const n = config.tailKeepMessages ?? COMPACTION_DEFAULTS.tailKeepMessages;
81
+ const tailStart = Math.max(0, messages.length - n);
82
+ return {
83
+ tailStart,
84
+ head: messages.slice(0, tailStart),
85
+ tail: messages.slice(tailStart),
86
+ };
87
+ }
88
+ // tailMode === 'token_budget' (D-5-3 默认): 从末尾往前累 token, 达到 budget 停
89
+ const budget = config.tailKeepTokens ?? COMPACTION_DEFAULTS.tailKeepTokens;
90
+ let accTokens = 0;
91
+ let tailStart = messages.length; // 默认全 tail
92
+ for (let i = messages.length - 1; i >= 0; i--) {
93
+ const m = messages[i];
94
+ const mTokens = estimateTokens([m]);
95
+ if (accTokens + mTokens > budget && i < messages.length - 1) {
96
+ // 已超 budget 且还有 1 条保底 (i < messages.length - 1 保证 tail 至少 1)
97
+ break;
98
+ }
99
+ accTokens += mTokens;
100
+ tailStart = i;
101
+ }
102
+ return {
103
+ tailStart,
104
+ head: messages.slice(0, tailStart),
105
+ tail: messages.slice(tailStart),
106
+ };
107
+ }
108
+ /** 合并 config + defaults, 算出有效触发阈值 + tail */
109
+ export function resolveCompactionConfig(config) {
110
+ const compactRatio = config.compactRatio ?? COMPACTION_DEFAULTS.compactRatio;
111
+ const tailMode = config.tailMode ?? COMPACTION_DEFAULTS.tailMode;
112
+ const tailKeepMessages = config.tailKeepMessages ?? COMPACTION_DEFAULTS.tailKeepMessages;
113
+ const tailKeepTokens = config.tailKeepTokens ?? COMPACTION_DEFAULTS.tailKeepTokens;
114
+ const pauseAfterFailures = config.pauseAfterFailures ?? COMPACTION_DEFAULTS.pauseAfterFailures;
115
+ return {
116
+ contextWindow: config.contextWindow,
117
+ compactRatio,
118
+ tailMode,
119
+ tailKeepMessages,
120
+ tailKeepTokens,
121
+ pauseAfterFailures,
122
+ threshold: Math.floor(config.contextWindow * compactRatio),
123
+ };
124
+ }
125
+ /**
126
+ * Compaction 状态机 (Sprint 1c-revive-2-D-5-2):
127
+ * 跟踪连续失败次数, 达到阈值 → latch → 暂停 + 写 paused event.
128
+ *
129
+ * 不变量:
130
+ * - consecutiveFailures 永不为负
131
+ * - paused === true ⇒ consecutiveFailures >= pauseThreshold (且未重置)
132
+ * - 1 次成功 → consecutiveFailures = 0 (无论之前几次失败)
133
+ * - 1 次失败 → consecutiveFailures++; 若 >= pauseThreshold → paused = true
134
+ *
135
+ * 重置 latch:
136
+ * - new CompactionState() 重新初始化
137
+ * - caller 决定何时调用 (e.g. 用户改 summaryFn / 改配置)
138
+ */
139
+ export class CompactionState {
140
+ pauseThreshold;
141
+ consecutiveFailures = 0;
142
+ paused = false;
143
+ lastError = null;
144
+ constructor(pauseThreshold) {
145
+ this.pauseThreshold = pauseThreshold;
146
+ if (pauseThreshold < 0) {
147
+ throw new Error(`CompactionState: pauseThreshold must be >= 0, got ${pauseThreshold}`);
148
+ }
149
+ }
150
+ /** 1 次成功 → reset 失败计数 + unpause */
151
+ recordSuccess() {
152
+ this.consecutiveFailures = 0;
153
+ this.paused = false;
154
+ this.lastError = null;
155
+ }
156
+ /**
157
+ * 1 次失败 → 计数 +1, 达到阈值 → latch.
158
+ * 返 true 表示本次失败触发了 latch (caller 该写 paused event).
159
+ */
160
+ recordFailure(error) {
161
+ this.consecutiveFailures += 1;
162
+ this.lastError = error.message;
163
+ if (this.consecutiveFailures >= this.pauseThreshold && !this.paused) {
164
+ this.paused = true;
165
+ return true;
166
+ }
167
+ return false;
168
+ }
169
+ /** 该不该尝试 compact (paused → false) */
170
+ shouldAttempt() {
171
+ return !this.paused;
172
+ }
173
+ /** Caller 主动重置 latch (e.g. 用户改配置后) */
174
+ reset() {
175
+ this.consecutiveFailures = 0;
176
+ this.paused = false;
177
+ this.lastError = null;
178
+ }
179
+ }
180
+ export async function runCompactionWithLatch(messages, config, summaryFn, state, options = {}) {
181
+ // 拍板 1: latch paused → 直接返 null (不调 summaryFn)
182
+ if (!state.shouldAttempt()) {
183
+ return null;
184
+ }
185
+ // 拍板 2: 不该 compact → 返 null
186
+ if (!shouldCompact(messages, config)) {
187
+ return null;
188
+ }
189
+ const now = options.now ?? (() => Date.now());
190
+ try {
191
+ const result = await compact(messages, config, summaryFn, { now });
192
+ state.recordSuccess();
193
+ return { kind: 'ok', result, event: result.event };
194
+ }
195
+ catch (err) {
196
+ const error = err instanceof Error ? err : new Error(String(err));
197
+ const latched = state.recordFailure(error);
198
+ if (latched) {
199
+ const pauseThreshold = state.consecutiveFailures;
200
+ const pausedEvent = {
201
+ kind: 'compaction_paused',
202
+ ts: now(),
203
+ consecutive_failures: pauseThreshold,
204
+ reason: `compaction failed ${pauseThreshold} times consecutively; auto-paused to prevent death loop`,
205
+ last_error: error.message,
206
+ meta: {
207
+ context_window: config.contextWindow,
208
+ messages_count: messages.length,
209
+ },
210
+ };
211
+ return { kind: 'latched', error, pausedEvent, consecutiveFailures: pauseThreshold };
212
+ }
213
+ // 未触发 latch: 抛给 caller, caller 决定怎么处理 (e.g. retry / 改 summaryFn)
214
+ throw err;
215
+ }
216
+ }
217
+ /**
218
+ * 拍板: 当前 messages 是否该 compact.
219
+ *
220
+ * 规则:
221
+ * - contextWindow = 0 → 永远不 (拍板关闭)
222
+ * - messages 数 == 0 → false (没东西可 compact)
223
+ * - resolveTail 后 head 为空 → false (tail 已占满, 总结不掉什么)
224
+ * - estimated tokens >= threshold → true
225
+ */
226
+ export function shouldCompact(messages, config) {
227
+ if (config.contextWindow <= 0)
228
+ return false;
229
+ if (messages.length === 0)
230
+ return false;
231
+ const resolved = resolveCompactionConfig(config);
232
+ if (estimateTokens(messages) < resolved.threshold)
233
+ return false;
234
+ // D-5-3: 走 resolveTail 看 head 是否为空, 避免"全 tail 没东西总结"白调 summary
235
+ const { head } = resolveTail(messages, config);
236
+ return head.length > 0;
237
+ }
238
+ /**
239
+ * 执行 compaction.
240
+ *
241
+ * 流程:
242
+ * 1. 拍定 [head, tail) 中间段要被总结
243
+ * 2. 调 summaryFn 生成 summary text
244
+ * 3. 拼成新 messages: [{ role: 'system', content: summary }, ...tail]
245
+ * **删** head (旧实现 [...head, summary, ...tail] 把 head 留下来了 → 上下文反而
246
+ * 继续膨胀, 反复总结同一批旧 messages. 拍板 2026-06-04 review: summary 必须
247
+ * **替代** head, 不并存.)
248
+ * 4. 拍 'compaction' event: { summary, replaced_range: [0, head.length) }
249
+ * 不变量: replaced_range 拍的是"基于入参 messages 的 index", caller 调 compact()
250
+ * 时传什么 messages, replaced_range 就拍什么 index. 拍板 session reload 时
251
+ * replay 此 event 拍一致.
252
+ * 5. 返回 CompactionResult (不写盘, 由 caller append event)
253
+ */
254
+ export async function compact(messages, config, summaryFn, options = {}) {
255
+ const resolved = resolveCompactionConfig(config);
256
+ const now = options.now ?? (() => Date.now());
257
+ // D-5-3: 走 resolveTail 拍 tail 边界 (token budget 或 message count)
258
+ const { tailStart, head, tail } = resolveTail(messages, config);
259
+ if (head.length === 0) {
260
+ throw new Error(`compaction: resolveTail produced empty head (messages=${messages.length}, tailMode=${resolved.tailMode}), nothing to compact`);
261
+ }
262
+ // replacedRange 拍的是"原始 messages 哪些被 head 占据" — 跟 resolveTail 拍板一致.
263
+ // tailStart 就是 head 的结束 index (head 拍 messages[0..tailStart)).
264
+ const replacedRange = [0, tailStart];
265
+ const summaryText = await summaryFn(head);
266
+ // 拍新 messages: 1 条 system summary 替代 head, tail 拍原样保留.
267
+ // 关键: head 拍被删, 不再保留. 旧实现 [...head, summary, ...tail] 是 P1 bug.
268
+ const summaryMessage = {
269
+ role: 'system',
270
+ content: `[Session compaction summary]\n${summaryText}`,
271
+ };
272
+ const newMessages = [summaryMessage, ...tail];
273
+ const event = {
274
+ kind: 'compaction',
275
+ ts: now(),
276
+ summary: summaryText,
277
+ replaced_range: replacedRange,
278
+ meta: {
279
+ before_tokens: estimateTokens(messages),
280
+ after_tokens: estimateTokens(newMessages),
281
+ before_messages: messages.length,
282
+ after_messages: newMessages.length,
283
+ tail_mode: resolved.tailMode,
284
+ tail_keep_messages: resolved.tailMode === 'message_count' ? resolved.tailKeepMessages : undefined,
285
+ tail_keep_tokens: resolved.tailMode === 'token_budget' ? resolved.tailKeepTokens : undefined,
286
+ tail_start: tailStart,
287
+ },
288
+ };
289
+ return {
290
+ messages: newMessages,
291
+ event,
292
+ stats: {
293
+ beforeTokens: estimateTokens(messages),
294
+ afterTokens: estimateTokens(newMessages),
295
+ beforeMessages: messages.length,
296
+ afterMessages: newMessages.length,
297
+ replacedRange,
298
+ },
299
+ };
300
+ }
301
+ //# sourceMappingURL=compaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compaction.js","sourceRoot":"","sources":["../../src/session/compaction.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Session JSONL — append-only 持久化 + crash recovery
3
+ *
4
+ * 协议(pi 借鉴):
5
+ * - 每条消息 = 1 行 JSON
6
+ * - 行分隔符 = '\n'(不带 \r)
7
+ * - 写入:append + fsync(保证不丢消息)
8
+ * - 读取:行扫描,**自动截断不完整行**(crash recovery 关键)
9
+ *
10
+ * Sprint 0.2 范围:
11
+ * - SessionWriter(append + fsync + flush)
12
+ * - SessionReader(line-by-line + partial line truncation)
13
+ * - SessionEvent 联合类型(4 种核心:user/assistant/tool/system)
14
+ * - v2.0 升级到 Session DAG(DAG 与 Planner 同链路,arch §3.5)
15
+ *
16
+ * Sprint 1c-revive-2-D-5 已落地(产线代码 packages/core/src/session/compaction.ts):
17
+ * - D-5-1 基础 compaction(shouldCompact + compact + 'compaction' event)
18
+ * 触发:promptTokens >= window * compactRatio (默认 0.8, Reasonix compact.go 拍板)
19
+ * - D-5-2 stuck latch(CompactionState + runCompactionWithLatch + 'compaction_paused' event)
20
+ * 连续 N 次失败 (默认 2) → latch 暂停, 防 death loop
21
+ * - D-5-3 tail token budget(resolveTail, tailMode='token_budget' 默认)
22
+ * 拍板 source: Reasonix compact.go:271-289 (tail 按 token budget 而非 message count)
23
+ *
24
+ * SessionEvent union 当前 7 kind:
25
+ * user / assistant / tool / system / compaction / compaction_paused / verification
26
+ * SessionReader 读 'compaction' / 'compaction_paused' / 'verification' 不重放进 LLM context
27
+ * (compaction.ts 拍板: 这三种是 runtime/metadata, 不进 context).
28
+ *
29
+ * 'verification' event (Sprint 1c-revive-2-D-11-3, 2026-06-04): `deepwhale --verify`
30
+ * 或 REPL `/verify` 跑完生成的 VerificationReport 摘要写到 session JSONL.
31
+ * - 跟 'compaction'/'compaction_paused' 同语义: metadata, 不重放进 LLM context.
32
+ * 用户 reload session 看不到 verification event 拼成 message (跟 paused event 一致).
33
+ * - 字段: report (VerificationReport 形态) + status (passed / failed) — 给 viewer
34
+ * / audit log 留口.
35
+ * - Sprint 1c-revive-2-D-11-3 拍板: 旧 session 文件 (没有 verification event)
36
+ * reload 不崩, 因为 SessionReader 走 kind discriminator union type,
37
+ * 旧 kind 解析流程不变. 新 kind 在旧 reader 读不到 (kind 不在 union),
38
+ * 但新 reader 读旧 JSONL 也不会试图 parse 缺失字段 — 严格 union 兜底.
39
+ * - 拍板 (D-11, 2026-06-04): 不**不**新增 session event 子表 / 不**不**新建 verification.jsonl,
40
+ * 跟 user/assistant/tool 同 append-only 1 JSONL 走, 简单且对旧 loader 透明.
41
+ *
42
+ * Sprint 1+ 仍待落地(待拍板):
43
+ * - 索引(按 messageId 加速查询)
44
+ * - 分片(>100MB 自动切文件)
45
+ * - 加密(at-rest AES-256-GCM)
46
+ * - 压缩(gzip content > N byte)
47
+ */
48
+ /** Session 事件 v1.0 = Linear(arch §2.3 红线:v1.0 = Linear,不做 DAG) */
49
+ export type SessionEvent = {
50
+ kind: 'user';
51
+ ts: number;
52
+ content: string;
53
+ meta?: Record<string, unknown>;
54
+ } | {
55
+ kind: 'assistant';
56
+ ts: number;
57
+ content: string;
58
+ tool_calls?: ReadonlyArray<{
59
+ id: string;
60
+ name: string;
61
+ args: Record<string, unknown>;
62
+ }>;
63
+ meta?: Record<string, unknown>;
64
+ } | {
65
+ kind: 'tool';
66
+ ts: number;
67
+ tool_call_id: string;
68
+ name: string;
69
+ result: {
70
+ success: boolean;
71
+ content: string;
72
+ error?: string;
73
+ };
74
+ duration_ms: number;
75
+ meta?: Record<string, unknown>;
76
+ } | {
77
+ kind: 'system';
78
+ ts: number;
79
+ content: string;
80
+ meta?: Record<string, unknown>;
81
+ } | {
82
+ /**
83
+ * Compaction event (Sprint 1c-revive-2-D-5-1):
84
+ * LLM context 超 window×0.8 触发, 总结前 N 条 message 写 1 条 summary event.
85
+ * SessionReader 读到 kind='compaction' 时不重放进 LLM context (给 caller 拍板).
86
+ *
87
+ * 字段:
88
+ * - summary: 总结文本 (caller 用 LLM 生成)
89
+ * - replaced_range: [start, end) 索引, 拍板原 messages 哪段被替代
90
+ * - meta: 统计 (before/after token, message count) 供调试
91
+ *
92
+ * 不变量: replaced_range[1] - replaced_range[0] >= 1 (有东西被总结)
93
+ */
94
+ kind: 'compaction';
95
+ ts: number;
96
+ summary: string;
97
+ replaced_range: readonly [number, number];
98
+ meta?: Record<string, unknown>;
99
+ } | {
100
+ /**
101
+ * Compaction paused event (Sprint 1c-revive-2-D-5-2):
102
+ * 连续 N 次 compaction 失败 (默认 2) → latch 自动暂停, 写 1 条 paused event.
103
+ * 防止 death loop (每次 LLM context 涨 → 触发 compact → 失败 → 再涨 → 再触发...).
104
+ *
105
+ * 字段:
106
+ * - consecutive_failures: 失败次数 (触发 latch 时 = 阈值)
107
+ * - reason: 拍板暂停原因 (给上层 UI/log 用)
108
+ * - last_error: 最后一次失败 error.message
109
+ *
110
+ * 不变量: SessionReader 读到 paused event 不重放进 LLM context
111
+ * (caller 该决定是否重置 latch / 改配置 / 改 summaryFn 拍板).
112
+ *
113
+ * Reasonix compact.go:88-93 拍板 source: consecutiveCompacts >= 2 → latch compactStuck,
114
+ * 自动暂停 + 拍板"say why, once".
115
+ */
116
+ kind: 'compaction_paused';
117
+ ts: number;
118
+ consecutive_failures: number;
119
+ reason: string;
120
+ last_error: string;
121
+ meta?: Record<string, unknown>;
122
+ } | {
123
+ /**
124
+ * Verification event (Sprint 1c-revive-2-D-11-3, 2026-06-04):
125
+ * `deepwhale --verify` 或 REPL `/verify` 跑完生成的 VerificationReport 摘要
126
+ * 写到 session JSONL. 跟 compaction_paused 同语义: metadata, 不重放进 LLM context.
127
+ *
128
+ * 字段:
129
+ * - status: 'passed' / 'failed' (整体结果, 跟 VerificationReport.overallStatus 一致)
130
+ * - durationMs: 整体耗时 (跟 VerificationReport.durationMs 一致)
131
+ * - command_count: 跑的 step 数 (e.g. 4 = build/lint/typecheck/test)
132
+ * - failed_count: 失败 step 数
133
+ * - summary: 人类可读 summary (跟 VerificationReport.summary 一致)
134
+ * - meta: 给 viewer / audit 留的可选扩展字段 (e.g. log file path, git sha)
135
+ *
136
+ * 不变量:
137
+ * - SessionReader 读 'verification' 不重放进 LLM context
138
+ * (跟 compaction_paused 一致, 跟 tool/user/assistant 不同).
139
+ * - 旧 session reload 不崩: 旧 event 没 'verification' kind, 新 reader union
140
+ * 不会试图 parse 缺失字段; 新 reader 读旧 event 完全不感知.
141
+ * - stdout/stderrTail 不在 event 里 (cap 4KB 内嵌到 event 也会撑爆 JSONL);
142
+ * 要看详细就 reload 时读 `meta.logFilePath` (后续 sprint 加).
143
+ */
144
+ kind: 'verification';
145
+ ts: number;
146
+ status: 'passed' | 'failed';
147
+ durationMs: number;
148
+ command_count: number;
149
+ failed_count: number;
150
+ summary: string;
151
+ meta?: Record<string, unknown>;
152
+ } | {
153
+ /**
154
+ * Policy decision event (Sprint 1c-revive-3-D-13, 2026-06-05).
155
+ * tool 实际 execute 之前, policy layer (src/policy/) 的决策落盘.
156
+ * 拍板 (用户 2026-06-05): 'allow' 不写 (避免 JSONL 被读工具刷爆), 只有
157
+ * 'deny' / 'require_confirmation' / 用户确认结果 ('user_approved' / 'user_denied')
158
+ * 写. 跟 'compaction' / 'compaction_paused' / 'verification' 同语义:
159
+ * metadata, sessionEventsToMessages 跳过, 不进 LLM context.
160
+ *
161
+ * 字段拍板:
162
+ * - tool_call_id: 跟后续 'tool' event 配对 (reload 时 audit trace 完整)
163
+ * - decision: 'deny' | 'require_confirmation' | 'user_approved' | 'user_denied'
164
+ * (拍板: 不写 'allow' — 噪音)
165
+ * - argsDigest: sha256:<12hex>, 不存原始 args (拍板: 防 secret leak)
166
+ * - reason: 自然语言, 已经过 sanitize (长度 / 换行 / NUL)
167
+ */
168
+ kind: 'policy_decision';
169
+ ts: number;
170
+ tool_call_id: string;
171
+ name: string;
172
+ decision: 'deny' | 'require_confirmation' | 'user_approved' | 'user_denied';
173
+ argsDigest: string;
174
+ reason?: string;
175
+ meta?: Record<string, unknown>;
176
+ };
177
+ /**
178
+ * JSONL Writer — append + fsync。
179
+ *
180
+ * 使用方式:
181
+ * const w = new SessionWriter('/path/to/session.jsonl');
182
+ * await w.open();
183
+ * await w.append({ kind: 'user', ts: Date.now(), content: 'hello' });
184
+ * await w.close();
185
+ *
186
+ * 关键:每次 append 都 fsync(v1.0 = 单人本地,可接受开销;v2.0 引入 batch fsync)
187
+ */
188
+ export declare class SessionWriter {
189
+ private readonly path;
190
+ private handle;
191
+ private writeQueue;
192
+ constructor(path: string);
193
+ open(): Promise<void>;
194
+ /** 追加一条事件(fsync 后返回) */
195
+ append(event: SessionEvent): Promise<void>;
196
+ private doAppend;
197
+ close(): Promise<void>;
198
+ }
199
+ /**
200
+ * JSONL Reader — line-by-line + 截断 partial line。
201
+ *
202
+ * Crash recovery 关键点:
203
+ * - 写入中途 crash → 最后一行可能是 partial JSON
204
+ * - read() 检测到不完整行 → 截断 + 警告 + 返回前面的完整行
205
+ * - 不抛错(让 agent 启动不卡死)
206
+ *
207
+ * 截断策略:把最后一行(无论完整与否)从文件删除
208
+ */
209
+ export declare class SessionReader {
210
+ private readonly path;
211
+ constructor(path: string);
212
+ /** 读取所有完整事件(自动 truncate partial last line) */
213
+ readAll(): Promise<ReadonlyArray<SessionEvent>>;
214
+ private parseLines;
215
+ private lastIncompleteLineIndex;
216
+ /** 截断文件到最后一个完整事件(crash recovery) */
217
+ truncate(): Promise<{
218
+ truncated: number;
219
+ }>;
220
+ }
221
+ /**
222
+ * 便捷工厂 — 组合 open + read + truncate + close
223
+ */
224
+ export declare function readSessionEvents(path: string): Promise<ReadonlyArray<SessionEvent>>;
225
+ //# sourceMappingURL=jsonl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonl.d.ts","sourceRoot":"","sources":["../../src/session/jsonl.ts"],"names":[],"mappings":"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"}