@x-otto/persistence 0.0.1-alpha.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,984 @@
1
+ import Database from "better-sqlite3";
2
+
3
+ //#region src/atomic-write.d.ts
4
+ /**
5
+ * 崩溃安全的全量文件写入(RFC-088 D4)。
6
+ *
7
+ * 语义:write-temp → fsync(fd) → close → rename → fsync(dir, best-effort)。
8
+ * rename 在同一文件系统内原子——读者要么看到旧内容、要么看到新内容,绝不看到半写。
9
+ * fsync(fd) 保证 temp 内容在 rename 前已落盘(否则崩溃/断电后 rename 生效但数据丢失);
10
+ * fsync(dir) 保证 rename 这一目录项变更本身落盘(best-effort——部分平台目录 fsync 不支持,忽略其错误)。
11
+ *
12
+ * 用于**全量覆写**路径(snapshot 保存、append-log 的 repair/truncate 重写),非追加热路径
13
+ * (追加路径用 O_DSYNC 打开,见 FileAppendLog)。
14
+ */
15
+ declare function atomicWriteFile(filePath: string, data: string | Uint8Array): Promise<void>;
16
+ //#endregion
17
+ //#region src/types.d.ts
18
+ interface Snapshot<T = unknown> {
19
+ version: number;
20
+ id: string;
21
+ data: T;
22
+ metadata: Metadata;
23
+ }
24
+ interface Metadata {
25
+ createdAt: number;
26
+ updatedAt: number;
27
+ tags: string[];
28
+ owner?: string;
29
+ updatedBy?: string;
30
+ visibility?: 'private' | 'shared';
31
+ [key: string]: unknown;
32
+ }
33
+ interface Codec<T = unknown> {
34
+ encode(snapshot: T): string;
35
+ decode(payload: string): T;
36
+ contentType?: string;
37
+ }
38
+ /**
39
+ * RFC-067 M114-01(A6):持久化的**共享方法契约**单一真相源。snapshot 信封类型 `S` 与分页选项 `P`
40
+ * 由各域固化——`Persistence`(generic `Snapshot<T>` + 通用分页)与 @x-otto/session 的
41
+ * `SessionPersistence`(域信封 `SessionSnapshot<T>` + 域分页 `sortBy=lastActiveAt`)各自 `extends`
42
+ * 之,消除两份等同的五方法声明。
43
+ *
44
+ * **§7.3 现实修正**:RFC A6「单一 `Persistence<T>`、删 SessionPersistence 重复」过断——二者的
45
+ * S/P 域类型真异(envelope: entries[]/leafId/SessionMetadata vs data/Metadata;pagination
46
+ * sortBy: lastActiveAt|createdAt vs createdAt|updatedAt),强行塌成单一接口会**擦除 session 域
47
+ * 类型**。故只抽共享方法形,保留两域接口。`PaginatedResult` 早已单源(session 从本包导入)。
48
+ * (历史 `as unknown as SessionPersistence` cast 来自 SessionPersistenceAdapter 的 sortBy 枚举
49
+ * 桥,已随 M115-01 删除 @x-otto/persistence-client 整包一并消除。)
50
+ */
51
+ interface PersistenceShape<S, P = PaginationOptions> {
52
+ save(snapshot: S): Promise<void>;
53
+ load(id: string): Promise<S | null>;
54
+ delete(id: string): Promise<boolean>;
55
+ list(): Promise<string[]>;
56
+ listPaginated(options: P): Promise<PaginatedResult<string>>;
57
+ }
58
+ /**
59
+ * RFC-108 D2:无分页的实体持久化契约——PanelState 等单资源按 id 存取的场景,
60
+ * 从不翻页/列表查询,不该继承 PersistenceShape 的 list/listPaginated。
61
+ */
62
+ interface EntityPersistenceShape<S> {
63
+ save(snapshot: S): Promise<void>;
64
+ load(id: string): Promise<S | null>;
65
+ delete(id: string): Promise<boolean>;
66
+ }
67
+ interface Persistence<T = unknown> extends PersistenceShape<Snapshot<T>> {}
68
+ interface PaginationOptions {
69
+ /** 0-based(第一页 = 0)。全部 Persistence 实现、SessionStore、StorageHost 统一此语义(F12)。 */
70
+ page: number;
71
+ pageSize?: number;
72
+ sortBy?: 'createdAt' | 'updatedAt';
73
+ order?: 'asc' | 'desc';
74
+ }
75
+ interface PaginatedResult<T> {
76
+ items: T[];
77
+ total: number;
78
+ page: number;
79
+ pageSize: number;
80
+ totalPages: number;
81
+ hasNext: boolean;
82
+ hasPrevious: boolean;
83
+ }
84
+ interface FileStorageOptions<T = unknown> {
85
+ type: 'file';
86
+ baseDir: string;
87
+ extension?: string;
88
+ codec?: Codec<Snapshot<T>>;
89
+ }
90
+ interface MemoryStorageOptions {
91
+ type: 'memory';
92
+ }
93
+ /** sqlite 后端选项(镜像 SqlitePersistenceOptions + type 判别字段,避免与 sqlite-persistence 循环依赖)。 */
94
+ interface SqliteStorageOptions<T = unknown> {
95
+ type: 'sqlite';
96
+ dbPath: string;
97
+ wal?: boolean;
98
+ namespace: string;
99
+ defaultPageSize?: number;
100
+ codec?: Codec<Snapshot<T>>;
101
+ }
102
+ type StorageOptions<T = unknown> = FileStorageOptions<T> | MemoryStorageOptions | SqliteStorageOptions<T>;
103
+ //#endregion
104
+ //#region src/memory-persistence.d.ts
105
+ interface MemoryPersistenceBaseOptions {
106
+ defaultPageSize?: number;
107
+ defaultSortBy?: string;
108
+ }
109
+ /**
110
+ * 抽象内存后端,与 `DiskPersistence<S>` / `RemotePersistence<S>` 对称(都是 `extractId` +
111
+ * 通用 `listPaginated`)。领域包用"薄子类 + extractId/getSortValue 配置"复用,无需各写一套
112
+ * Map+分页。原设计稿 docs/design/storage-kernel.md 已随 RFC-067 M123-03 文档收敛删除,
113
+ * 内容并入 packages/persistence/ARCHITECTURE.md,见 commit 5d6a9f5b。
114
+ */
115
+ declare abstract class MemoryPersistenceBase<S> {
116
+ private readonly store;
117
+ protected readonly defaultPageSize: number;
118
+ protected readonly defaultSortBy: string;
119
+ constructor(options?: MemoryPersistenceBaseOptions);
120
+ protected abstract extractId(record: S): string;
121
+ /** 默认读 `record.metadata[sortBy]`;时间字段命名不同的领域可覆盖。 */
122
+ protected getSortValue(record: S, sortBy: string): number;
123
+ save(record: S): Promise<void>;
124
+ load(id: string): Promise<S | null>;
125
+ delete(id: string): Promise<boolean>;
126
+ list(): Promise<string[]>;
127
+ /**
128
+ * 受保护的只读快照迭代器——供领域子类(如 session 域的 `InMemorySessionPersistence`)
129
+ *实现自己的排序/过滤查询(如 RFC-154 D1 的 `listRecentMeta`),不在通用层硬编码领域
130
+ * 专属语义(如 archived 过滤)。返回浅拷贝数组,调用方不应修改 store 内容。
131
+ */
132
+ protected entries(): ReadonlyArray<readonly [string, S]>;
133
+ listPaginated(options: {
134
+ page: number;
135
+ pageSize?: number;
136
+ sortBy?: string;
137
+ order?: 'asc' | 'desc';
138
+ }): Promise<PaginatedResult<string>>;
139
+ }
140
+ declare class MemoryPersistence<T = unknown> extends MemoryPersistenceBase<Snapshot<T>> implements Persistence<T> {
141
+ protected extractId(snapshot: Snapshot<T>): string;
142
+ }
143
+ //#endregion
144
+ //#region src/save-document.d.ts
145
+ /**
146
+ * 信封构造单一来源(engine-nodes 范式外推)。
147
+ *
148
+ * 内核拥有信封构造:消费者只交 document + 少量选项,内核统一产出
149
+ * `{version, id, data, metadata{createdAt, updatedAt, tags}}` 信封 + 时间戳策略。
150
+ *
151
+ * upsert 默认保留原 createdAt(先 load 再写);append-only 场景传 `preserveCreatedAt: false` 跳过读。
152
+ * 时间戳由调用方注入(`now`),内核不直调 Date.now(确定性/可注入时钟)。
153
+ */
154
+ declare function saveDocument<T>(persistence: Persistence<T>, id: string, data: T, options: {
155
+ now: number;
156
+ version?: number;
157
+ tags?: string[];
158
+ preserveCreatedAt?: boolean;
159
+ owner?: string;
160
+ updatedBy?: string;
161
+ }): Promise<void>;
162
+ //#endregion
163
+ //#region src/disk-persistence.d.ts
164
+ interface DiskPersistenceOptions<S = unknown> {
165
+ baseDir: string;
166
+ extension?: string;
167
+ defaultPageSize?: number;
168
+ codec?: Codec<S>;
169
+ }
170
+ declare abstract class DiskPersistence<S> {
171
+ protected readonly baseDir: string;
172
+ protected readonly extension: string;
173
+ protected readonly defaultPageSize: number;
174
+ protected readonly codec: Codec<S>;
175
+ private initialized;
176
+ constructor(options: DiskPersistenceOptions<S>);
177
+ protected abstract extractId(snapshot: S): string;
178
+ save(snapshot: S): Promise<void>;
179
+ load(id: string): Promise<S | null>;
180
+ delete(id: string): Promise<boolean>;
181
+ list(): Promise<string[]>;
182
+ listPaginated(options: {
183
+ page: number;
184
+ pageSize?: number;
185
+ sortBy?: string;
186
+ order?: 'asc' | 'desc';
187
+ }): Promise<PaginatedResult<string>>;
188
+ private resolvePath;
189
+ private ensureDir;
190
+ }
191
+ //#endregion
192
+ //#region src/panel-state.d.ts
193
+ /** 持久化任务面板条目(内联自 @x-otto/session PersistedSessionConfig.todoList)。 */
194
+ interface PanelStateTodoItem {
195
+ id?: string;
196
+ title?: string;
197
+ status?: 'pending' | 'in_progress' | 'done' | 'skipped' | 'failed';
198
+ turn?: number;
199
+ }
200
+ /** 持久化编辑文件条目(内联自 @x-otto/session PersistedSessionConfig.editedFiles)。 */
201
+ interface PanelStateEditedFile {
202
+ path: string;
203
+ operation: 'edit' | 'write' | 'delete';
204
+ addedLines: number;
205
+ removedLines: number;
206
+ timestamp: number;
207
+ turnId?: string;
208
+ toolCallId?: string;
209
+ }
210
+ /** 持久化子代理条目(内联自 @x-otto/session SubagentEntry)。 */
211
+ interface PanelStateSubagentEntry {
212
+ id: string;
213
+ name: string;
214
+ status: 'running' | 'done' | 'error' | 'killed';
215
+ startedAt: number;
216
+ turnId?: string;
217
+ }
218
+ /** 持久化草稿条目(内联自 @x-otto/session DraftEntry)。 */
219
+ interface PanelStateDraftEntry {
220
+ id: string;
221
+ name: string;
222
+ text: string;
223
+ savedAt: number;
224
+ }
225
+ /**
226
+ * 持久化回合完成摘要条目(内联自 @x-otto/session TurnSummaryEntry)。
227
+ *
228
+ * 用户反馈:TUI 底部"✓ 完成 · N 个工具 · 耗时 · tokens · 模型"这行摘要此前只是
229
+ * `reduceToTui` 在 `prompt.end` 时从瞬时状态(ReducerState 计数器 + 当次事件字段)现算
230
+ * 现造的 `ChatMessage`(`role:'summary'`),从未写入 `session.messages()`(发给模型的
231
+ * 真实历史)也从未写入任何持久化存储——resume 时 `agentMessagesToChatMessages` 只回放
232
+ * user/assistant/tool_result,摘要行永久丢失。
233
+ *
234
+ * 本字段独立于 `todoList`/`editedFiles`——它是**每回合累加**(不像 editedFiles 那样
235
+ * `prompt.start` 清空只留当前回合),因为 resume 后需要在对话流里逐回合重现历史摘要行,
236
+ * 不是只留最后一条。与 messages()/AgentMessage[] 物理隔离存储,天然不会被
237
+ * provider 序列化层读取——满足"持久化但不发给大模型"的约束。
238
+ */
239
+ interface PanelStateTurnSummary {
240
+ turn: number;
241
+ status: 'done' | 'error' | 'interrupted' | 'incomplete';
242
+ toolCount: number;
243
+ elapsedMs: number;
244
+ tokensUsed?: number;
245
+ cacheHitRatio?: number;
246
+ model?: string;
247
+ tasksDone?: number;
248
+ tasksTotal?: number;
249
+ completedAt: number;
250
+ }
251
+ /**
252
+ * RFC-108 D2:PanelState 持久化信封。
253
+ *
254
+ * `sessionId` 既是业务关联键也是持久化主键——PanelState 始终按 sessionId 一对一存取,
255
+ * 从不翻页/列表查询,故契约用 `EntityPersistenceShape` 而非 `PersistenceShape`。
256
+ */
257
+ interface PanelStateSnapshot {
258
+ sessionId: string;
259
+ todoList?: PanelStateTodoItem[];
260
+ editedFiles?: PanelStateEditedFile[];
261
+ subagents?: PanelStateSubagentEntry[];
262
+ drafts?: PanelStateDraftEntry[];
263
+ turnSummaries?: PanelStateTurnSummary[];
264
+ }
265
+ /** RFC-108 D2:PanelState 持久化契约(无分页三方法)。 */
266
+ interface PanelStatePersistence extends EntityPersistenceShape<PanelStateSnapshot> {}
267
+ /**
268
+ * RFC-108 D2:PanelState 本地磁盘持久化实现。
269
+ *
270
+ * 对外仅暴露 `PanelStatePersistence` 三方法(save/load/delete),内部委托
271
+ * `DiskPersistence<PanelStateSnapshot>` 负责实际的磁盘 I/O。
272
+ *
273
+ * 参照 `RemoteSessionPersistence extends RemotePersistence<SessionSnapshot<T>>`
274
+ * 的“领域固化 PersistenceShape”模式——但此处是本地磁盘后端,用委托而非继承,因为
275
+ * `DiskPersistence` 承载了 `list`/`listPaginated` 等 PanelState 不需要的语义,
276
+ * 直接继承会泄漏与 `EntityPersistenceShape` 契约不匹配的方法。
277
+ */
278
+ declare class DiskPanelStatePersistence implements PanelStatePersistence {
279
+ private readonly store;
280
+ constructor(options: {
281
+ baseDir: string;
282
+ });
283
+ save(snapshot: PanelStateSnapshot): Promise<void>;
284
+ load(id: string): Promise<PanelStateSnapshot | null>;
285
+ delete(id: string): Promise<boolean>;
286
+ }
287
+ /**
288
+ * RFC-345:PanelState 内存后端(影子模式)。面板态只活在进程内存,退出即释放。
289
+ */
290
+ declare function createInMemoryPanelStatePersistence(): PanelStatePersistence;
291
+ //#endregion
292
+ //#region src/paste-state.d.ts
293
+ /**
294
+ * 持久化粘贴条目(RFC-338)。
295
+ *
296
+ * **不含时间戳/序号字段**:LRU 顺序由 {@link PasteStateSnapshot.entries} 的
297
+ * **数组顺序**表达(头 = 最久未访问,尾 = 最近访问),与内存侧 `Map` 的插入顺序
298
+ * 语义一一对应(RFC-338 D2)。引入独立 `accessSeq` 会与 Map 顺序构成双真源;
299
+ * 引入 `Date.now()` 则违反引擎核心的确定性重放约束。
300
+ */
301
+ interface PasteStateEntry {
302
+ /** 占位符 `[@paste:#N]` 中的 N。 */
303
+ id: number;
304
+ /** 折叠前的原文。 */
305
+ text: string;
306
+ /** utf-8 字节数(容量闸的另一维度,构造时算好避免重复计算)。 */
307
+ byteLength: number;
308
+ }
309
+ /**
310
+ * RFC-338:paste 缓存独立信封。
311
+ *
312
+ * `sessionId` 既是业务关联键也是持久化主键——按 sessionId 一对一存取,
313
+ * 从不翻页/列表查询,故契约用 `EntityPersistenceShape`(同 PanelState 的判断)。
314
+ */
315
+ interface PasteStateSnapshot {
316
+ sessionId: string;
317
+ /** 有序数组 = LRU 顺序(头 = 最久未访问)。 */
318
+ entries: PasteStateEntry[];
319
+ /**
320
+ * id 分配游标,**单调不回退**(RFC-338 重要事项规则 1,红线)。
321
+ *
322
+ * 违反后果不是「chip 失效」而是「chip 静默展开成另一份内容并发给模型」——
323
+ * 内存态重启后 store 为空、旧 chip 判失效是**安全的失败**;持久化态下若
324
+ * 该游标回退,新粘贴会复用旧 id,历史里的 `[@paste:#7]` 就会指向错误内容。
325
+ */
326
+ nextId: number;
327
+ }
328
+ /** RFC-338:paste 缓存持久化契约(无分页三方法,同 PanelStatePersistence)。 */
329
+ interface PasteStatePersistence extends EntityPersistenceShape<PasteStateSnapshot> {}
330
+ /**
331
+ * RFC-338:paste 缓存本地磁盘持久化实现。
332
+ *
333
+ * **落盘位置是 `panel-state/` 下的独立子目录 `paste/`**,不是与面板态同级的
334
+ * `<sessionId>.paste.json`。原因(实现期实测):`DiskPersistence` 的
335
+ * `list()`/`listPaginated()` 用 `endsWith(extension)` 过滤、用
336
+ * `slice(0, -extension.length)` 反解 id —— 若 paste 用 `.paste.json` 后缀与面板态
337
+ * 同目录并存,面板态那侧以 `.json` 过滤会**连带匹配到 paste 文件**,且把 id
338
+ * 反解成 `<sessionId>.paste`(实测 `'s1.paste.json'.endsWith('.json') === true`)。
339
+ * 独立子目录让两者的文件名空间物理隔离,两侧都用标准 `.json` 后缀,无需依赖
340
+ * 「当前恰好没人调用 list()」这种脆弱前提。
341
+ */
342
+ declare class DiskPasteStatePersistence implements PasteStatePersistence {
343
+ private readonly store;
344
+ constructor(options: {
345
+ baseDir: string;
346
+ });
347
+ save(snapshot: PasteStateSnapshot): Promise<void>;
348
+ load(id: string): Promise<PasteStateSnapshot | null>;
349
+ delete(id: string): Promise<boolean>;
350
+ }
351
+ /**
352
+ * RFC-338 重要事项规则 1(红线):从**不可信**的持久化快照推导安全的 `nextId`。
353
+ *
354
+ * 取 `max(1, 快照声明的 nextId, 全部 entry.id + 1)` —— 三者取最大保证游标既不
355
+ * 回退、也不与任何既存 id 碰撞。
356
+ *
357
+ * **边界(二轮评审 F4,已实测)**:`Math.max(...[])` 返回 `-Infinity`,故绝不能
358
+ * 写成 `Math.max(...ids) + 1` 的裸形式 —— entries 为空时会得到 `-Infinity`,
359
+ * 后续 `nextId++` 全程为 `-Infinity`,占位符变成 `[@paste:#-Infinity]`。
360
+ * 起始种子 `1` 同时兜住了空数组与 `nextId` 缺失两种情况。
361
+ *
362
+ * @param snapshot 可能损坏/缺字段/被手工编辑过的快照
363
+ */
364
+ declare function resolvePasteNextId(snapshot: unknown): number;
365
+ /**
366
+ * RFC-338 §7:把不可信快照规范化为可用状态(fail-open)。
367
+ *
368
+ * 丢弃非法 entry(缺字段/类型错/id 非有限数)而非整份拒绝——用户不该因为一条
369
+ * 坏记录丢掉整个会话的粘贴缓存。`byteLength` 一律按 `text` 重算,不信任存储值
370
+ * (手工编辑或旧版本写入的值可能与实际不符,直接影响容量闸判断)。
371
+ *
372
+ * 无论 entries 如何裁剪,`nextId` 都基于**原始**快照推导——防止「坏 entry 被丢弃
373
+ * → id 空间看似空出 → 新粘贴复用该 id → 历史 chip 错展开」的绕行路径。
374
+ */
375
+ declare function normalizePasteSnapshot(sessionId: string, raw: unknown): PasteStateSnapshot;
376
+ /**
377
+ * RFC-345:paste 缓存内存后端(影子模式)。粘贴态只活在进程内存,退出即释放。
378
+ */
379
+ declare function createInMemoryPasteStatePersistence(): PasteStatePersistence;
380
+ //#endregion
381
+ //#region src/remote-persistence.d.ts
382
+ interface RemotePersistenceOptions<S = unknown> {
383
+ baseUrl: string;
384
+ /**
385
+ * workspace 分区键(default='default');客户端 URL 拼接 wsKey。
386
+ * 支持惰性 getter(`() => string | undefined`)——修复(N3,resume/workspace 关联审计):
387
+ * 消费方(如 App)在构造期就创建持久化实例,而 workspaceRef.key 要到 App.start() 异步
388
+ * resolveLocal 完成后才有真实值,与 RelationalSessionPersistence.workspaceKey 的既有
389
+ * 惰性 getter 模式同源同因(同一类时序错位,见 Bug1 修复记录)。静态字符串仍受支持
390
+ * (组件测试 / 无 workspace 概念场景)。
391
+ */
392
+ wsKey?: string | (() => string | undefined);
393
+ getAuth: () => Promise<{
394
+ token: string;
395
+ }>;
396
+ getHeaders?: () => Promise<Record<string, string>>;
397
+ fetch: typeof globalThis.fetch;
398
+ timeoutMs: number;
399
+ defaultPageSize?: number;
400
+ defaultSortBy?: string;
401
+ codec?: Codec<S>;
402
+ }
403
+ declare abstract class RemotePersistence<S> {
404
+ protected readonly baseUrl: string;
405
+ /** workspace 分区键解析器,URL 路径前缀;缺省 'default'。每次用时惰性求值。 */
406
+ private readonly resolveWsKey;
407
+ protected get wsKey(): string;
408
+ protected readonly getAuth: () => Promise<{
409
+ token: string;
410
+ }>;
411
+ protected readonly getHeaders: () => Promise<Record<string, string>>;
412
+ protected readonly fetch: typeof globalThis.fetch;
413
+ protected readonly timeoutMs: number;
414
+ protected readonly defaultPageSize: number;
415
+ protected readonly defaultSortBy: string;
416
+ protected readonly codec: Codec<S>;
417
+ /**
418
+ * 错误文案用的服务标签;通用基类默认中性名,领域子类覆盖(如 RemoteSessionPersistence → "会话服务")。
419
+ * 修正:基类原硬编码"会话服务器",但它同时被通用 HttpPersistence 继承 → 非会话用途报错误导。
420
+ */
421
+ protected get serviceLabel(): string;
422
+ /** 错误文案用的端点配置项名;领域子类覆盖(如 '--session-url')。 */
423
+ protected get endpointFlag(): string;
424
+ constructor(options: RemotePersistenceOptions<S>);
425
+ /** wsKey 路径拼接:`/{wsKey}/{path}`。 */
426
+ private wsPath;
427
+ /** wsKey 路径 list: `/{wsKey}`。 */
428
+ protected wsListPath(): string;
429
+ protected abstract extractId(snapshot: S): string;
430
+ /**
431
+ * 无条件整份 PUT(兼容旧调用方)。新调用方应改用 `saveWithRevision()` 以获得 CAS 保护。
432
+ */
433
+ save(snapshot: S): Promise<void>;
434
+ /**
435
+ * CAS 写入:带 `If-Match: "<expectedRevision>"` 发起 PUT。
436
+ * - 成功:返回服务端分配的新 revision。
437
+ * - 冲突(409):抛 `RemoteSnapshotConflictError`,`info.revision` 为服务端当前 revision。
438
+ * - 未提供 expectedRevision:等价于无条件写,服务端不比对;返回新 revision。
439
+ *
440
+ * `expectedRevision=0` 显式表示"期望文档尚不存在"(新建场景)。
441
+ */
442
+ saveWithRevision(snapshot: S, expectedRevision?: number): Promise<number>;
443
+ /**
444
+ * 读取快照与其 revision。revision 供下次 `saveWithRevision()` 的 `expectedRevision` 使用。
445
+ * 文档不存在返回 null(revision 也为 null)。
446
+ */
447
+ loadWithRevision(id: string): Promise<{
448
+ snapshot: S;
449
+ revision: number;
450
+ } | null>;
451
+ load(id: string): Promise<S | null>;
452
+ delete(id: string): Promise<boolean>;
453
+ list(): Promise<string[]>;
454
+ listPaginated(options: {
455
+ page: number;
456
+ pageSize?: number;
457
+ sortBy?: string;
458
+ order?: 'asc' | 'desc';
459
+ }): Promise<PaginatedResult<string>>;
460
+ protected request(path: string, init: {
461
+ method: string;
462
+ body?: string;
463
+ }, extraHeaders?: Record<string, string>): Promise<Response>;
464
+ }
465
+ //#endregion
466
+ //#region src/file-persistence.d.ts
467
+ interface FilePersistenceOptions<T = unknown> {
468
+ baseDir: string;
469
+ extension?: string;
470
+ codec?: Codec<Snapshot<T>>;
471
+ }
472
+ declare class FilePersistence<T = unknown> extends DiskPersistence<Snapshot<T>> implements Persistence<T> {
473
+ constructor(options: FilePersistenceOptions<T>);
474
+ protected extractId(snapshot: Snapshot<T>): string;
475
+ }
476
+ //#endregion
477
+ //#region src/errors.d.ts
478
+ declare class PersistenceError extends Error {
479
+ constructor(message: string, options?: {
480
+ cause?: unknown;
481
+ });
482
+ }
483
+ declare class RemotePersistenceError extends PersistenceError {
484
+ readonly statusCode: number;
485
+ constructor(message: string, statusCode: number, options?: {
486
+ cause?: unknown;
487
+ });
488
+ }
489
+ /** 409 conflict response 的可选 payload(If-Match CAS 冲突时,authority 返回当前 revision)。 */
490
+ interface SnapshotConflictInfo {
491
+ /** 服务端当前 revision;可能为 null(极少数并发删除后不存在)。 */
492
+ revision: number | null;
493
+ }
494
+ /** 远端快照写入冲突(CAS 失败)——base class 上特化,调用方可显式识别并做 reload/rebase。 */
495
+ declare class RemoteSnapshotConflictError extends RemotePersistenceError {
496
+ readonly info: SnapshotConflictInfo;
497
+ constructor(message: string, info: SnapshotConflictInfo, options?: {
498
+ cause?: unknown;
499
+ });
500
+ }
501
+ //#endregion
502
+ //#region src/storage-factory.d.ts
503
+ /**
504
+ * 统一存储工厂:按 `options.type` 直构造内置后端(memory/file/http/sqlite)。
505
+ *
506
+ * M91-02(RFC-057):删 StorageBackendRegistry(零外部注册者的 Map 间接层),改内联 switch。
507
+ * M93-01(RFC-057 §3 D7):`StorageOptions` 为穷尽判别联合,switch 按 type 窄化——去工厂边界
508
+ * `as unknown as`,恢复编译期保护(漏 case / 字段类型不符均被 typecheck 拦)。
509
+ */
510
+ declare function createPersistence<T = unknown>(options: StorageOptions<T>): Persistence<T>;
511
+ //#endregion
512
+ //#region src/change-log.d.ts
513
+ /**
514
+ * change_log 表记录:SqlitePersistence 写入,SSE 路由轮询推送。
515
+ */
516
+ interface ChangeLogEntry {
517
+ id: number;
518
+ ns: string;
519
+ doc_id: string;
520
+ op: 'save' | 'delete';
521
+ ts: number;
522
+ }
523
+ //#endregion
524
+ //#region src/append-log.d.ts
525
+ /**
526
+ * append-only 日志原语(取向 B,与 snapshot 取向正交)。
527
+ *
528
+ * 与 `Persistence<Snapshot<T>>`(一份最新状态的 CRUD)不同,`AppendLog<E>` 表达
529
+ * “按序追加 + 范围读 + 实时 tail”的事件溯源语义。`TraceStore` 是它的领域包装
530
+ * (`streamId = sessionId`,`E = TraceEvent`)。原设计稿 docs/design/storage-kernel.md 已随
531
+ * RFC-067 M123-03 文档收敛删除,内容并入 packages/persistence/ARCHITECTURE.md,见 commit 5d6a9f5b。
532
+ *
533
+ * 单写者约束:跨进程并发写**不**保证 seq 全序(引擎进程独占写自己的 trace)。
534
+ */
535
+ interface LogEntry<E> {
536
+ /** 单调递增序号,append 时由后端分配(从 0 起,逐 stream 独立)。等于该 stream 内的行号。 */
537
+ seq: number;
538
+ /** 分流键(如 sessionId)。 */
539
+ streamId: string;
540
+ /** 领域负载(如 TraceEvent)。 */
541
+ entry: E;
542
+ }
543
+ interface AppendRange {
544
+ /** seq 下界(含),默认 0。 */
545
+ from?: number;
546
+ /** seq 上界(不含),默认 +∞。 */
547
+ to?: number;
548
+ }
549
+ interface LogCodec<E> {
550
+ encode(entry: E): string;
551
+ decode(payload: string): E;
552
+ }
553
+ interface AppendLog<E> {
554
+ /** 追加一条,返回含已分配 seq 的记录。 */
555
+ append(streamId: string, entry: E): Promise<LogEntry<E>>;
556
+ /** 按 seq 升序读取(可选范围);stream 不存在 → 空迭代,不抛。 */
557
+ read(streamId: string, range?: AppendRange): AsyncIterable<LogEntry<E>>;
558
+ /**
559
+ * 可选——stream 当前记录数(= 下一个待分配 seq)。宿主用于「只回放最近 N 条」的
560
+ * range 计算(如 inspector backfill 封顶),避免全量 decode 只为拿总数。
561
+ */
562
+ count?(streamId: string): Promise<number>;
563
+ /** 先回放已有,再实时跟随后续 append(实时观测底座)。 */
564
+ tail(streamId: string): AsyncIterable<LogEntry<E>>;
565
+ /**
566
+ * 截断到 toSeq(保留 seq < toSeq,丢弃 seq ≥ toSeq);下次 append 从 toSeq 续号。
567
+ * 回滚/分叉用(trace 截到 checkpoint 的 traceSeq)。toSeq ≤ 0 等价清空该 stream;
568
+ * toSeq ≥ 当前长度为无操作。stream 不存在 → 无操作,不抛。
569
+ *
570
+ * 注意(单写者语义):已在运行的 tail 迭代器持有自身 lastSeq 游标,truncate 后复用的
571
+ * seq 号不会被同一迭代器重新发出(避免乱序)——回滚分支应重新 tail。
572
+ */
573
+ truncate(streamId: string, toSeq: number): Promise<void>;
574
+ /** 删除整个 stream(回滚/分叉用)。 */
575
+ clear(streamId: string): Promise<void>;
576
+ /**
577
+ * 可选——释放底层资源(FileAppendLog/HttpAppendLog 实现关闭文件句柄/刷写队列;
578
+ * MemoryAppendLog 无需)。宿主关停时 `await store.close?.()`,取代旧 `'close' in store` 鸭子探测。
579
+ */
580
+ close?(): Promise<void>;
581
+ }
582
+ type AppendLogOptions<E> = {
583
+ type: 'memory';
584
+ } | {
585
+ type: 'file';
586
+ baseDir: string;
587
+ extension?: string;
588
+ codec?: LogCodec<E>;
589
+ pollIntervalMs?: number;
590
+ };
591
+ declare class MemoryAppendLog<E> implements AppendLog<E> {
592
+ private readonly streams;
593
+ private readonly listeners;
594
+ private readonly maxRecordsPerStream;
595
+ private readonly onEvict?;
596
+ constructor(options?: {
597
+ maxRecordsPerStream?: number; /** 驱逐发生时的诊断回调(可选,如宿主想告警/记日志)。 */
598
+ onEvict?: (streamId: string, evictedSeq: number, totalDropped: number) => void;
599
+ });
600
+ private getOrCreateStream;
601
+ append(streamId: string, entry: E): Promise<LogEntry<E>>;
602
+ read(streamId: string, range?: AppendRange): AsyncIterable<LogEntry<E>>;
603
+ /** stream 记录数上限(= 下一个 seq,与实际保留条数无关——驱逐后二者分道,保持 seq 单调语义)。 */
604
+ count(streamId: string): Promise<number>;
605
+ tail(streamId: string): AsyncIterable<LogEntry<E>>;
606
+ truncate(streamId: string, toSeq: number): Promise<void>;
607
+ clear(streamId: string): Promise<void>;
608
+ }
609
+ declare class FileAppendLog<E> implements AppendLog<E> {
610
+ private readonly baseDir;
611
+ private readonly extension;
612
+ private readonly codec;
613
+ private readonly pollIntervalMs;
614
+ private readonly seqCounters;
615
+ /** 稀疏字节索引缓存(进程内,非持久化——重启后懒重建)。 */
616
+ private readonly byteIndexes;
617
+ /**
618
+ * RFC-145 D1:per-stream 常驻写句柄缓存。追加热路径从每条 open/stat/write/close
619
+ * (~8 syscalls,含 repair 探测)降到 1 次 write——profile 实证长会话进程主线程
620
+ * 非 idle 样本 88% 在 uv_fs 句柄生命周期回调(sample 67571,2026-07-11)。
621
+ * `tailByteOffset` 增量跟踪文件末尾偏移(open 时 stat 一次初始化,每写后累加),
622
+ * 消除 byteIndex 维护的 per-append stat。正确性前提:writeQueue 串行 + O_APPEND +
623
+ * 单写者。失效协议见 invalidateHandle()。
624
+ */
625
+ private readonly handles;
626
+ /**
627
+ * RFC-145 D2:进程内已执行过尾部修复的 stream 集合。单写者约束下半行只可能来自
628
+ * 上次进程崩溃的残留——进程存活期内自己的写不产生半行(writeFile 整行经 writeQueue
629
+ * 串行原子入队),故 repair 探测每 stream 只需执行一次(冷启动语义:新实例首次
630
+ * append 前仍会 repair)。truncate/clear 后不清除——ftruncate 原子、clear 走
631
+ * unlink+重建,均不产生半行。
632
+ */
633
+ private readonly repairedStreams;
634
+ private writeQueue;
635
+ private dirReady;
636
+ private closed;
637
+ constructor(options: {
638
+ baseDir: string;
639
+ extension?: string;
640
+ codec?: LogCodec<E>;
641
+ pollIntervalMs?: number;
642
+ });
643
+ append(streamId: string, entry: E): Promise<LogEntry<E>>;
644
+ private appendSerial;
645
+ /**
646
+ * RFC-145 D1:打开并缓存常驻写句柄。open 时 stat 一次初始化 tailByteOffset;
647
+ * 入缓存前执行上限驱逐(LRU)与 idle 回收(惰性,无常驻 timer)。
648
+ * 仅在 writeQueue 串行域内调用。
649
+ */
650
+ private openResidentHandle;
651
+ /**
652
+ * RFC-145 D1 失效协议:关闭并移除某 stream 的常驻写句柄(连同 tailByteOffset)。
653
+ * 四条路径在操作 stream 文件前必须调用:truncateSerial / clearSerial /
654
+ * repairPartialTail / close()——它们各自 open 独立句柄操作文件,缓存句柄若还活着,
655
+ * 后续 append 会写到被 truncate/unlink 的旧 inode(数据静默丢失)。
656
+ * 均在 writeQueue 串行域内执行,无并发窗口。
657
+ */
658
+ private invalidateHandle;
659
+ /**
660
+ * 索引增量维护:每 INDEX_INTERVAL 行落一个锚点,并推进 tailSeq/tailByteOffset
661
+ * (下次 append 据此判断能否零 IO 继续维护)。只在索引已就位(冷启动懒构建过一次)
662
+ * 时生效——否则每次 append 都要额外 stat 判断是否该建索引,得不偿失。
663
+ *
664
+ * 单调性守卫(`seq < index.tailSeq` 提前返回):append 与 read() 都会推进同一 stream
665
+ * 的索引 tailSeq,二者不互斥(read 不在 writeQueue 内)。appendSerial 在
666
+ * `stat()` 到这里之间有 await 缺口,若同一窗口内一次并发 read() 已把 tailSeq 推进到
667
+ * 更前面,此处必须放弃写入,否则会用旧的 seq/byteOffset 把 tailSeq 拉回退——
668
+ * 破坏 nearestAnchor 依赖的 anchors 严格递增假设。
669
+ */
670
+ private recordIndexPoint;
671
+ /**
672
+ * 冷启动/索引缺口时的懒构建:顺序扫描一遍文件,边扫边记锚点(与 read() 的分块扫描
673
+ * 复用同一遍 IO,零额外开销——调用方(read)在扫描主循环里同步喂入行边界)。
674
+ * 返回构建后的索引(供 read() 立即复用做本次的 seek 判断)。
675
+ *
676
+ * TOCTOU 说明:并发 read() 首次访问同一 streamId 时,Map.get/set 之间无 await 缺口
677
+ * (本方法全同步),不存在两次并发调用互相覆盖对方索引对象的竞态——JS 单线程下
678
+ * 同步代码段天然原子。
679
+ */
680
+ private ensureByteIndex;
681
+ /** 索引中 ≤ from 的最近锚点(无锚点或 from=0 时退化为文件头,即旧行为)。 */
682
+ private nearestAnchor;
683
+ /**
684
+ * 尾部半行修复(崩溃恢复):只检查/裁剪文件尾部,O(尾部) 而非 O(文件)。
685
+ * 旧实现全量 readFile + atomicWriteFile 全量重写——巨型 trace(数百 MB)下不可用。
686
+ * 现:stat + 读最后 1 字节判完整;不完整时从尾部按块回扫找最后一个 '\n',`ftruncate`
687
+ * 原地截断(truncate 掉的只是半行垃圾,无崩溃安全顾虑——重复截断幂等)。
688
+ */
689
+ private repairPartialTail;
690
+ /**
691
+ * 流式逐行读取:分块读 + 增量 decode,只对范围内的行做 JSON decode。
692
+ * 旧实现 loadAll 全量 readFile + 全行 JSON.parse——260MB trace 下 ~1s 主线程阻塞
693
+ * 且峰值内存 >1.4GB;带 range 时(如 inspector backfill 只要最近 N 条)纯浪费。
694
+ *
695
+ * RFC-102 backlog T1:`range.from` 命中已建索引区间时,直接 seek 到最近锚点字节偏移
696
+ * (`handle.read(buf, 0, len, position)` 的 position 参数跳过前缀 IO,不只是跳过 decode)
697
+ * ——替代旧实现「总是从字节 0 顺序扫到 from」。真机验证(260MB/24万行存量 trace,
698
+ * scrubber 高频精细拖动场景):30 次连续 range read 从 13.3s/单次最大阻塞89ms 降至毫秒级。
699
+ * 索引未覆盖 from(冷启动/gap)时透明退化为旧的顺序扫描,同时顺路把扫过的区间记入索引——
700
+ * 首次访问某区域仍是 O(距离),但此后同区域内的精细拖动全部命中索引。
701
+ */
702
+ read(streamId: string, range?: AppendRange): AsyncIterable<LogEntry<E>>;
703
+ /** stream 记录数(= 下一个 seq)。优先用进程内 seq 缓存;否则字节流数行(不 decode)。 */
704
+ count(streamId: string): Promise<number>;
705
+ /**
706
+ * 增量轮询:只读上次字节偏移后新追加的部分、只 decode 新行。
707
+ * 旧实现每 poll `loadAll` 重读整个文件 + 全行 JSON.parse(O(N)/poll)——长会话下吃满 CPU/GC、
708
+ * 阻塞事件循环致输入卡顿(profile 实证 decode/loadAll/string_decoder 占 ~88% CPU)。现降到 O(新增)。
709
+ */
710
+ tail(streamId: string): AsyncIterable<LogEntry<E>>;
711
+ /**
712
+ * 停止所有进行中的 tail() 轮询循环——while(!closed) 在下个轮询点退出,常驻 tail 订阅随之终止。
713
+ * app.stop() 经可选链 store.close?.() 调用本方法(fix: /exit·Ctrl+C 退不掉):缺此方法时,
714
+ * inspector 的常驻 trace tail 永久轮询,是 /exit、Ctrl+C 退不掉的近因。
715
+ */
716
+ close(): Promise<void>;
717
+ truncate(streamId: string, toSeq: number): Promise<void>;
718
+ private truncateSerial;
719
+ clear(streamId: string): Promise<void>;
720
+ private clearSerial;
721
+ /** 字节流数行(数 '\n',不 decode JSON)——nextSeq/count 冷启动用,O(文件) IO 但零 parse/GC。 */
722
+ private countLines;
723
+ private nextSeq;
724
+ private resolvePath;
725
+ private ensureDir;
726
+ }
727
+ /**
728
+ * 统一日志工厂:按 `options.type` 直构造内置后端(memory/file)。
729
+ * `AppendLogOptions` 穷尽判别联合,switch 按 type 窄化——去 `as unknown as`。
730
+ */
731
+ declare function createAppendLog<E>(options: AppendLogOptions<E>): AppendLog<E>;
732
+ /** 内存实时旁路:订阅某 stream 的后续 append(live 观测者用,避免绕磁盘 tail 轮询)。 */
733
+ interface LiveTap<E> {
734
+ /** 订阅 streamId 的后续 append;返回取消订阅函数。无订阅者时 publish 为 no-op。 */
735
+ subscribe(streamId: string, fn: (record: LogEntry<E>) => void): () => void;
736
+ }
737
+ /**
738
+ * 给 AppendLog 套一层内存实时旁路(L3 终局):`append` 在写底层 store 的同时把记录 publish 给内存订阅者。
739
+ * live 观测者(inspector)订阅 `live` 拿实时 append,**不再 `tail` 磁盘轮询**——live 进程内数据不绕磁盘;
740
+ * 底层 store 仍负责持久化与「回放过去会话」的 `read`/`tail`。
741
+ */
742
+ declare function createLiveTappedAppendLog<E>(inner: AppendLog<E>): {
743
+ store: AppendLog<E>;
744
+ live: LiveTap<E>;
745
+ };
746
+ //#endregion
747
+ //#region src/storage-host.d.ts
748
+ interface StorageHostOptions {
749
+ /** 允许的 namespace 白名单(如 ['sessions'];knowledge/archive 已退场)。 */
750
+ namespaces: string[];
751
+ /** 每 namespace 的后端工厂。 */
752
+ backend: (namespace: string) => Persistence<unknown>;
753
+ /** 时间源(信封 createdAt/updatedAt),缺省真实时钟。 */
754
+ now?: () => number;
755
+ }
756
+ declare class StorageHost {
757
+ private readonly namespaces;
758
+ private readonly backend;
759
+ private readonly now;
760
+ private readonly instances;
761
+ /** 每 namespace 的 append-only 日志(惰性创建)。 */
762
+ private readonly appendLogs;
763
+ constructor(options: StorageHostOptions);
764
+ has(namespace: string): boolean;
765
+ /** 返回所有注册的 namespace 列表(health 路由使用)。 */
766
+ listNamespaces(): string[];
767
+ /**
768
+ * workspace 分区——wsKey 前缀到文档 ID,在 namespace 内实现物理分区。
769
+ * wsKey = `ws_<id>`,缺省 `default`(向后兼容旧调用方)。
770
+ */
771
+ private wsId;
772
+ /**
773
+ * 追加一条 entry 到指定 stream 的 append-only 日志。
774
+ *
775
+ * 返回含已分配 seq 的 LogEntry——seq 由 AppendLog 保证单写者递增(0-based per stream)。
776
+ * 默认使用 MemoryAppendLog(进程内原子追加);调用侧(service route)负责
777
+ * 在 append 后通过 EventBus 广播 SSE 实时通知。
778
+ *
779
+ * @param namespace 命名空间(如 'sessions')
780
+ * @param wsKey workspace 分区键(ws_<id>),前缀文档 ID 在 namespace 内物理分区
781
+ * @param streamId 流 ID(通常为文档/会话 ID)
782
+ * @param entry 要追加的条目数据
783
+ * @returns 含 seq、streamId、entry 的 LogEntry
784
+ */
785
+ appendEntry(namespace: string, wsKey: string | undefined, streamId: string, entry: unknown): Promise<LogEntry<unknown>>;
786
+ save(namespace: string, wsKey: string | undefined, id: string, document: unknown, agentId?: string): Promise<void>;
787
+ updateMetadata(namespace: string, wsKey: string | undefined, id: string, patch: Record<string, unknown>, agentId?: string): Promise<void>;
788
+ load(namespace: string, wsKey: string | undefined, id: string): Promise<unknown | null>;
789
+ delete(namespace: string, wsKey: string | undefined, id: string): Promise<boolean>;
790
+ list(namespace: string, wsKey?: string, agentId?: string, scope?: 'owned' | 'visible' | 'all'): Promise<string[]>;
791
+ /**
792
+ * 增量拉取(sync pull 数据源)。
793
+ *
794
+ * 返回自 `since` (unix ms) 以来 updated_at > since 的文档快照列表,
795
+ * 以及最新游标 cursor(所有文档中最大的 updated_at,供下次 pull 使用)。
796
+ *
797
+ * SQLite 后端走索引查询(O(log n));file/memory 后端全量加载后内存过滤。
798
+ */
799
+ getChangesSince(namespace: string, wsKey: string | undefined, since: number, ids?: string[]): Promise<{
800
+ documents: Map<string, unknown | null>;
801
+ cursor: number;
802
+ }>;
803
+ listPaginated(namespace: string, wsKey: string | undefined, options: {
804
+ page: number;
805
+ pageSize: number;
806
+ sortBy: string;
807
+ order: 'asc' | 'desc';
808
+ }): Promise<PaginatedResult<string>>;
809
+ /** 信封 metadata[sortBy] → 文档 metadata?.[sortBy] → 信封 updatedAt 兜底。 */
810
+ private sortValue;
811
+ /**
812
+ * 实时事件流(SSE 推送来源)。
813
+ *
814
+ * 轮询后端 change_log 并 yield 变更事件——仅 SQLite 后端支持;file/memory 后端返回空迭代。
815
+ * 设计决策:用鸭子类型(`getChangeLog` 方法探测)而非直接 import SqlitePersistence,
816
+ * 保持 StorageHost 对具体后端的零耦合。
817
+ *
818
+ * @param signal 取消信号——客户端断连时中止轮询,避免生成器泄漏。
819
+ */
820
+ streamEvents(namespace: string, wsKey: string | undefined, since: number, pollMs?: number, signal?: AbortSignal): AsyncGenerator<{
821
+ id: number;
822
+ doc_id: string;
823
+ op: string;
824
+ ts: number;
825
+ }, void, void>;
826
+ private resolve;
827
+ }
828
+ declare function createFileStorageHost(options: {
829
+ baseDir: string;
830
+ namespaces?: string[];
831
+ now?: () => number;
832
+ }): StorageHost;
833
+ //#endregion
834
+ //#region src/http-append-log.d.ts
835
+ interface HttpAppendLogOptions<E> {
836
+ /** remote-persistence-server base URL (e.g. https://storage.example.com). */
837
+ baseUrl: string;
838
+ /** Auth token provider (same as RemotePersistence). */
839
+ getAuth: () => Promise<{
840
+ token: string;
841
+ }>;
842
+ /** Optional extra headers (e.g. X-Agent-Id). */
843
+ getHeaders?: () => Promise<Record<string, string>>;
844
+ /** fetch impl (defaults to globalThis.fetch). */
845
+ fetch?: typeof globalThis.fetch;
846
+ /** Request timeout ms (default 30s). */
847
+ timeoutMs?: number;
848
+ /** Batch flush interval ms (default 2000). */
849
+ flushIntervalMs?: number;
850
+ /** Max batch size before immediate flush (default 100). */
851
+ flushAtSize?: number;
852
+ /** Poll interval ms for tail (default 500). */
853
+ pollIntervalMs?: number;
854
+ /** Codec for encoding/decoding entries (default JSON). */
855
+ codec?: LogCodec<E>;
856
+ }
857
+ /**
858
+ * HTTP AppendLog 实现 — 将 trace/checkpoint 事件推送到 remote-persistence-server。
859
+ *
860
+ * 内部批处理(定时或满批量 flush),避免每事件一次 HTTP round-trip。
861
+ * append 是 fire-and-forget(调用方只关心事件已入队);flush 在后台异步完成。
862
+ *
863
+ * read/tail 直接拉取服务端已持久化数据;tail 采用轮询模式。
864
+ */
865
+ declare class HttpAppendLog<E> implements AppendLog<E> {
866
+ private readonly baseUrl;
867
+ private readonly getAuth;
868
+ private readonly getHeaders;
869
+ private readonly fetchImpl;
870
+ private readonly timeoutMs;
871
+ private readonly pollIntervalMs;
872
+ private readonly flushAtSize;
873
+ private readonly codec;
874
+ private readonly buffer;
875
+ /** RFC-353: 正在 flush 的 streamId 集合——防止满批量 flush 与定时 flush 并发发同一 batch。 */
876
+ private readonly flushingStreams;
877
+ private flushTimer?;
878
+ private closed;
879
+ constructor(options: HttpAppendLogOptions<E>);
880
+ append(streamId: string, entry: E): Promise<LogEntry<E>>;
881
+ /**
882
+ * RFC-353: per-stream flush 串行化。若该 stream 已在 flush 中(满批量触发 + 定时触发
883
+ * 或两次满批量触发重叠),跳过——正在进行的 flush 完成后,新 buffer 会在下一轮
884
+ * 定时 flush 或下次满批量时被处理。消除并发 flushOne 同一 batch 导致服务端重复 append
885
+ * 与 seq 不连续。
886
+ */
887
+ private tryFlush;
888
+ private flushOne;
889
+ private flushAll;
890
+ read(streamId: string, range?: AppendRange): AsyncIterable<LogEntry<E>>;
891
+ tail(streamId: string): AsyncIterable<LogEntry<E>>;
892
+ /**
893
+ * stream 事件总数(服务端 COUNT(*) 索引扫描 + 本地未 flush 的在途 buffer 计数)。
894
+ * 消费端(inspector backfill)据此只拉最近 N 条,不必先 read() 全量再取 length
895
+ * (RFC-102 backlog T4:此前无 count() 时 `?? 0` 回退触发全量回放)。
896
+ */
897
+ count(streamId: string): Promise<number>;
898
+ truncate(streamId: string, toSeq: number): Promise<void>;
899
+ clear(streamId: string): Promise<void>;
900
+ /** 关闭:停止 flush 定时器,排空 buffer。 */
901
+ close(): Promise<void>;
902
+ private request;
903
+ }
904
+ //#endregion
905
+ //#region src/sqlite-db-cache.d.ts
906
+ /** 单次 schema 迁移。`version` 必须连续从小到大。 */
907
+ interface Migration {
908
+ /** 迁移目标版本号(1-based);`up` 执行后 db 的 user_version 变为该值。 */
909
+ version: number;
910
+ /** 前向迁移 DDL。幂等(建议用 `CREATE TABLE IF NOT EXISTS`)。 */
911
+ up: (db: Database.Database) => void;
912
+ }
913
+ declare function acquireDb(dbPath: string, wal?: boolean, migrationSet?: Migration[]): Database.Database;
914
+ declare function releaseDb(db: Database.Database): void;
915
+ //#endregion
916
+ //#region src/sqlite-persistence.d.ts
917
+ interface SqlitePersistenceOptions<T = unknown> {
918
+ /** SQLite 数据库文件路径。用 ':memory:' 做测试。 */
919
+ dbPath: string;
920
+ /** 是否启用 WAL 模式(默认 true)。 */
921
+ wal?: boolean;
922
+ /** 表内 namespace 标识(用于跨 ns 共享单个 db 文件)。 */
923
+ namespace: string;
924
+ /** 默认每页大小(默认 20)。 */
925
+ defaultPageSize?: number;
926
+ /** snapshots 表的 codec(默认 JSON)。 */
927
+ codec?: Codec<Snapshot<T>>;
928
+ }
929
+ /**
930
+ * SQLite 快照后端(M28-01)。
931
+ *
932
+ * 通过通用 `snapshots` 表实现 `Persistence<T>` 接口——单 db 文件多 namespace 共享,
933
+ * WAL 模式支持 1 写者 + N 读者并发。每次 save 自动写 `change_log` 行供 SSE 轮询。
934
+ *
935
+ * 通过 `sqlite-db-cache` 的单一连接缓存(acquireDb/releaseDb)与其他 consumer 共享同一 db 文件,
936
+ * 避免 WAL 锁冲突。DDL 由 `acquireDb` 的 `applyMigrations` 统一管理(M60-02);本类不再自建表。
937
+ */
938
+ declare class SqlitePersistence<T = unknown> implements Persistence<T> {
939
+ private readonly db;
940
+ private readonly ns;
941
+ private readonly defaultPageSize;
942
+ private readonly codec;
943
+ constructor(options: SqlitePersistenceOptions<T>);
944
+ /** 释放引用;相同 db 路径上所有实例都释放后才关闭物理连接。 */
945
+ close(): void;
946
+ save(snapshot: Snapshot<T>): Promise<void>;
947
+ load(id: string): Promise<Snapshot<T> | null>;
948
+ delete(id: string): Promise<boolean>;
949
+ list(): Promise<string[]>;
950
+ listPaginated(options: {
951
+ page: number;
952
+ pageSize?: number;
953
+ sortBy?: string;
954
+ order?: 'asc' | 'desc';
955
+ }): Promise<PaginatedResult<string>>;
956
+ /**
957
+ * 查询某个 ns 在 since (unix ms) 起的变更日志。
958
+ * 用 `ts >= since`(非 `>`)+ 单调 `id` 次序:同毫秒内后到的变更不会因游标落在该 ms 被跳过
959
+ * (消费方按 change_log.id 去重已产出项)。次序 `ts ASC, id ASC` 保证 id 单调可用作去重游标。
960
+ */
961
+ getChangeLog(ns: string, since: number, limit?: number): ChangeLogEntry[];
962
+ /** 增量拉取自 since 后变更的文档 ID 列表 + 最新游标(M28-05 sync pull 优化路径)。 */
963
+ getChangesSince(ns: string, since: number, ids?: string[]): {
964
+ ids: string[];
965
+ cursor: number;
966
+ };
967
+ /** 查询 since 后被删除的文档 ID 列表(M28-05 sync pull delete 标记)。 */
968
+ getDeletedSince(ns: string, since: number): string[];
969
+ /** 获取某个 ns 的最新 updated_at 时间戳(供 SQL cursor 初始化)。 */
970
+ private getLatestCursor;
971
+ /** 获取某个 ns 的最新变更时间戳(供 cursor 初始化)。 */
972
+ getLatestChangeTs(ns: string): number;
973
+ /** SQLite 完整性诊断信息(M34-02 doctor 使用)。 */
974
+ getInfo(): {
975
+ walEnabled: boolean;
976
+ snapshots: number;
977
+ changeLogEntries: number;
978
+ sessions: number;
979
+ integrity: string;
980
+ };
981
+ }
982
+ //#endregion
983
+ export { type AppendLog, type AppendLogOptions, type AppendRange, type ChangeLogEntry, type Codec, DiskPanelStatePersistence, DiskPasteStatePersistence, DiskPersistence, type DiskPersistenceOptions, type EntityPersistenceShape, FileAppendLog, FilePersistence, type FilePersistenceOptions, HttpAppendLog, type HttpAppendLogOptions, type LiveTap, type LogCodec, type LogEntry, MemoryAppendLog, MemoryPersistence, MemoryPersistenceBase, type MemoryPersistenceBaseOptions, type Metadata, type Migration, type PaginatedResult, type PaginationOptions, type PanelStateDraftEntry, type PanelStateEditedFile, type PanelStatePersistence, type PanelStateSnapshot, type PanelStateSubagentEntry, type PanelStateTodoItem, type PanelStateTurnSummary, type PasteStateEntry, type PasteStatePersistence, type PasteStateSnapshot, type Persistence, PersistenceError, type PersistenceShape, RemotePersistence, RemotePersistenceError, type RemotePersistenceOptions, RemoteSnapshotConflictError, type Snapshot, type SnapshotConflictInfo, SqlitePersistence, type SqlitePersistenceOptions, StorageHost, type StorageHostOptions, type StorageOptions, acquireDb, atomicWriteFile, createAppendLog, createFileStorageHost, createInMemoryPanelStatePersistence, createInMemoryPasteStatePersistence, createLiveTappedAppendLog, createPersistence, normalizePasteSnapshot, releaseDb, resolvePasteNextId, saveDocument };
984
+ //# sourceMappingURL=index.d.ts.map