@morlay/session-rdb 0.0.9

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 morlay
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # @morlay/session-rdb
2
+
3
+ RDB(SQLite / PostgreSQL)持久会话后端(`ctx.sessionPersistence`):通过 drizzle
4
+ 实现 `PersistenceBackend<number>`,复用上游 `PersistenceCoordinator` 与契约测试套件,
5
+ 支持配置选择 SQLite 或 PostgreSQL 后端。设计细节(表结构、delta 过滤、并发写、
6
+ 方言差异、仓库结构)见 [docs/design.md](docs/design.md)。
7
+
8
+ ## 安装
9
+
10
+ 包发布在 GitHub Packages,按包名安装到 dsh profile(示例:web;`dsh plugin
11
+ add` 把参数原样转发给 pnpm,版本号按发布版本调整)。registry 必须限定在
12
+ `@morlay` scope(`--config.@morlay:registry=…`),不要用全局 `--registry`,
13
+ 否则依赖(`@deepseek-ai/*` 等)也会被指到 GitHub Packages 而解析失败:
14
+
15
+ ```sh
16
+ dsh plugin --profile=web add "@morlay/session-rdb"
17
+ ```
18
+
19
+ GitHub Packages 的 registry 要求认证(公开包也一样):profile 的 `.npmrc`
20
+ 需为 `npm.pkg.github.com` 配置访问 token(如
21
+ `//npm.pkg.github.com/:_authToken=<token>`),详见
22
+ [GitHub 文档](https://docs.github.com/packages/working-with-a-github-packages-registry/working-with-the-npm-registry)。
23
+
24
+ ## 配置
25
+
26
+ 配置写在 `${DSH_HOME}/settings.yaml`,settings namespace 为插件短名
27
+ `session-rdb`(与 cordis 插件 `name` 一致):
28
+
29
+ ```yaml
30
+ session-rdb:
31
+ type: sqlite
32
+ # path 省略时回落 cordis.patch.yml 的默认($DSH_HOME/sessions/sessions.sqlite,
33
+ # 由 bundle patch 的 !!js 表达式求值);自定义路径请用绝对路径字符串。
34
+ path: /absolute/path/to/sessions.sqlite
35
+ journalMode: wal
36
+ busyTimeout: 5000
37
+ ```
38
+
39
+ > settings.yaml 是纯 YAML(settings-local 用 `yaml` 库解析),**不支持 `!!js`
40
+ > JS 表达式**——`!!js dshHomePath(...)` 会被当作字面字符串。`!!js` 只在
41
+ > `cordis.patch.yml`(bundle patch 层,loader 求值)有效。
42
+
43
+ 字段即 Config 判别联合(见下);未写出的字段回落到 bundle patch / cordis.yml 的
44
+ config 默认值。PostgreSQL:
45
+
46
+ ```yaml
47
+ session-rdb:
48
+ type: postgres
49
+ connectionString: postgres://user:pass@localhost:5432/sessions
50
+ ```
51
+
52
+ Config 类型:
53
+
54
+ ```ts
55
+ type Config =
56
+ | {
57
+ type: "sqlite";
58
+ /** SQLite 数据库文件路径;`:memory:` 用于测试。 */
59
+ path: string;
60
+ /** journal_mode:`wal`(默认)/ `delete` / `truncate` / `persist`。 */
61
+ journalMode?: "wal" | "delete" | "truncate" | "persist";
62
+ /** 写锁竞争等待毫秒数(默认 5000)。 */
63
+ busyTimeout?: number;
64
+ }
65
+ | {
66
+ type: "postgres";
67
+ /** node-postgres 连接串;首次打开自动建表并写入 store 身份。 */
68
+ connectionString: string;
69
+ };
70
+ ```
71
+
72
+ ## 分支能力(session-branch 闭环)
73
+
74
+ 除 `ctx.sessionPersistence` 外,本包还实现 `@morlay/session-branch` 的
75
+ provider 抽象并**随插件自动注册 `ctx.sessionBranch`**(`SessionBranchRdb`),
76
+ 在不修改上游代码的前提下提供 `rewind / retry / fork` 的持久化闭环:
77
+
78
+ - **`forkFrom`**:走标准 coordinator 路径(`create` + `append`)从闭合边界
79
+ 派生新会话(纯 append,`parentSession` / `seedLength` lineage);
80
+ - **`rewind`**:直接操作后端事务截断到闭合边界(DELETE 尾部 + head 回退 +
81
+ revision bump),随后重新 `load` 同步 coordinator 状态,并更新并发写
82
+ 检测 head;**支持 live 会话**(`ctx.sessions` 有 owner 时同样就地工作:
83
+ 先 flush write-behind 缓冲 → 截断 RDB → 截断 live 内存 log 并复位
84
+ surface/header/context/derived 派生缓存 → 重置 agent 请求头标记 →
85
+ 同步 coordinator cursor;不调用 `load`——live 时 load 会先 flush 撤销
86
+ 截断);
87
+ - **`timeline`**:`parentSession` + `seedLength` 版本树投影(live 会话含
88
+ 版本效果;cold 会话因版本事件携带 `ignorable` 不进 canonical log 而只有
89
+ lineage 骨架)。
90
+
91
+ 上层编排(edit / reroll / retry / rewind / fork 完整功能)由
92
+ `@morlay/ui-conversation-message-actions` 提供,或直接在 `ctx.sessionBranch` /
93
+ `ctx.sessionEditor` 之上编程。
@@ -0,0 +1,13 @@
1
+ # @morlay/session-rdb bundle patch.
2
+ #
3
+ # 在 profile 的 dsh.profile.bundles 中列出本包后,本 patch 在 base 层之后
4
+ # 生效,插入 RDB 持久化后端插件行(默认 SQLite,库文件放
5
+ # $DSH_HOME/sessions/sessions.sqlite)。用户可在 profile 的 cordis.patch.yml 中
6
+ # 按 id 覆盖 config(例如切到 PostgreSQL)。
7
+
8
+ - insert:
9
+ - id: session-rdb
10
+ name: "@morlay/session-rdb"
11
+ config:
12
+ type: sqlite
13
+ path: !!js dshHomePath('sessions', 'sessions.sqlite')
@@ -0,0 +1,528 @@
1
+ import { Context } from "@deepseek-ai/cordis";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { PersistenceBackend, SessionLocation, SessionPersistence, SessionPersistenceRevision, SessionPersistenceSnapshot, StoredPrefix, StoredSuffix } from "@deepseek-ai/dsh-session-persistence";
4
+ import { Session, SessionEvent, SessionHeader, SessionId } from "@deepseek-ai/dsh-session";
5
+ import { BranchAnchorMode, BranchBoundary, ForkFromOptions, SessionBranch, SessionBranchProvider } from "@morlay/session-branch";
6
+ //#region src/backend.d.ts
7
+ /**
8
+ * 与方言无关的 `t_sessions` 行投影。SQLite / PostgreSQL 的 drizzle
9
+ * `InferSelectModel` 均结构兼容(各自多出的自增主键列不影响赋值)。
10
+ */
11
+ interface SessionRow {
12
+ fSessionId: string;
13
+ /** Empty string means no head (fresh session or rewound to empty). */
14
+ fHeadEventId: string;
15
+ fHeadSequence: number;
16
+ fVersion: number;
17
+ fCreatedAt: number;
18
+ fCwd: string | null;
19
+ fParentSession: string | null;
20
+ fSeedLength: number | null;
21
+ fOrigin: string | null;
22
+ fDelegationDepth: number | null;
23
+ /** Stable identity assigned when this log is materialized. */
24
+ fIncarnation: string;
25
+ /** Monotonic log-change token incremented in each mutating transaction. */
26
+ fRevision: number;
27
+ }
28
+ /**
29
+ * 一次事件插入的完整列值(`t_events` 一行),由存储层按事件构造、后端落库。
30
+ */
31
+ interface EventInsert {
32
+ fEventId: string;
33
+ fParentId: string;
34
+ fKind: string;
35
+ fRole: string;
36
+ fName: string;
37
+ fActionId: string;
38
+ fEncoding: string;
39
+ fData: string;
40
+ fCreatedAt: number;
41
+ fOriginalSeq: number;
42
+ fSourceEventSeqs: string | null;
43
+ fSurfaceOp: string | null;
44
+ }
45
+ /**
46
+ * 一个 joined `t_session_events` + `t_events` 行:按 session 本地 `f_sequence`
47
+ * 寻址的持久化事件(`f_data` 为 JSON 文本,surface 列为 JSON 文本或 null)。
48
+ */
49
+ interface EventRow {
50
+ /** `t_session_events.f_sequence` — the dense persisted seq. */
51
+ fSequence: number;
52
+ /** `t_events.f_original_seq` — the upstream seq before delta filtering. */
53
+ fOriginalSeq: number;
54
+ /** `t_events.f_kind` — the upstream `SessionEvent.type`. */
55
+ fKind: string;
56
+ /** `t_events.f_created_at` — the upstream `SessionEvent.time`. */
57
+ fCreatedAt: number;
58
+ /** `t_events.f_data` — JSON-encoded event data. */
59
+ fData: string;
60
+ /** JSON-encoded `number[]` — the event's sourceEventSeqs (upstream seqs), or null. */
61
+ fSourceEventSeqs: string | null;
62
+ /** JSON-encoded `SurfaceOp` — how the event entered the surface, or null. */
63
+ fSurfaceOp: string | null;
64
+ }
65
+ /**
66
+ * 事务内可用的数据访问原语。后端保证这些调用落在同一个数据库事务里
67
+ * (SQLite 单连接隐式满足;PostgreSQL 绑定到 drizzle 的事务句柄)。
68
+ */
69
+ interface BackendTx {
70
+ /** Insert-or-replace the session's metadata row (initial head cursor). */
71
+ upsertSession(meta: SessionHeader, incarnation: string): Promise<void>;
72
+ /** Fetch the head cursor; the caller materialized the row first. */
73
+ getHead(id: SessionId): Promise<Pick<SessionRow, "fHeadEventId" | "fHeadSequence">>;
74
+ /**
75
+ * Insert event rows in ONE multi-row INSERT. Callers pass non-empty arrays;
76
+ * the implementation may no-op on an empty input.
77
+ */
78
+ insertEvents(events: EventInsert[]): Promise<void>;
79
+ /**
80
+ * Insert session↔event bridge rows in ONE multi-row INSERT. Callers pass
81
+ * non-empty arrays; the implementation may no-op on an empty input.
82
+ */
83
+ insertBridges(rows: Array<{
84
+ fSessionId: SessionId;
85
+ fEventId: string;
86
+ fSequence: number;
87
+ }>): Promise<void>;
88
+ /** Move the head cursor forward. */
89
+ updateHead(id: SessionId, headEventId: string, headSequence: number): Promise<void>;
90
+ /** Increment the session's revision by one. */
91
+ bumpRevision(id: SessionId): Promise<void>;
92
+ /** Delete bridge rows with `f_sequence >= fromSequence` (torn-tail truncate). */
93
+ deleteBridgeTail(id: SessionId, fromSequence: number): Promise<void>;
94
+ /** The bridge row just below `sequence` (the surviving head anchor), if any. */
95
+ getPrevBridge(id: SessionId, sequence: number): Promise<{
96
+ fEventId: string;
97
+ fSequence: number;
98
+ } | undefined>;
99
+ /** The highest bridge row (the physical tail anchor), if any. */
100
+ getLastBridge(id: SessionId): Promise<{
101
+ fEventId: string;
102
+ fSequence: number;
103
+ } | undefined>;
104
+ /**
105
+ * Update an event's mutable columns in place (used by the per-session
106
+ * provenance cleanse, which rewrites surface/provenance JSON into the dense
107
+ * seq space). Undefined fields are left untouched; null clears the column.
108
+ */
109
+ updateEventFields(id: SessionId, sequence: number, fields: {
110
+ fSourceEventSeqs?: string | null;
111
+ fSurfaceOp?: string | null;
112
+ fData?: string;
113
+ }): Promise<void>;
114
+ }
115
+ /**
116
+ * 存储后端:连接生命周期 + store 身份 + 事务外读取。`storeIdentity` 仅在
117
+ * {@link open} 完成后有效。
118
+ */
119
+ interface Backend {
120
+ readonly kind: "sqlite" | "postgres";
121
+ /** Source-qualified store identity (revision 前缀),open 后可用。 */
122
+ readonly storeIdentity: string;
123
+ /** 连接 + 建表 + 版本/身份校验 + 读取 store id;失败时抛错(不迁移)。 */
124
+ open(): Promise<void>;
125
+ /** Fetch a session's row, or undefined if absent. */
126
+ getSession(id: SessionId): Promise<SessionRow | undefined>;
127
+ /** Lightweight two-column upstream→persisted seq map source. */
128
+ getSeqMapRows(id: SessionId): Promise<Array<{
129
+ fSequence: number;
130
+ fOriginalSeq: number;
131
+ fKind: string;
132
+ }>>;
133
+ /** Joined event rows for one session, dense seq ascending (optionally from a seq). */
134
+ getEventRows(id: SessionId, fromSequence?: number): Promise<EventRow[]>;
135
+ /** All materialized sessions' rows. */
136
+ listSessions(): Promise<SessionRow[]>;
137
+ /** Run `fn` inside one durable transaction. */
138
+ transaction<T>(fn: (tx: BackendTx) => Promise<T>): Promise<T>;
139
+ /** Close the connection (awaited by the coordinator's dispose, post-drain). */
140
+ close(): Promise<void>;
141
+ }
142
+ //#endregion
143
+ //#region src/write-guard.d.ts
144
+ /**
145
+ * The write-authority state machine for one backend instance. Not part of the
146
+ * {@link Backend} seam: it guards the orchestration layer's own invariants and
147
+ * lives entirely in memory.
148
+ */
149
+ declare class WriteGuard {
150
+ /**
151
+ * Last CONFIRMED dense head per session — the head this instance itself
152
+ * wrote or observed via `loadStored`. `-1` records a confirmed absence (no
153
+ * row). `undefined` (absent from the map) means this instance never read or
154
+ * wrote the session.
155
+ */
156
+ private readonly headSeqs;
157
+ /**
158
+ * Upstream seqs of delta events dropped per session. Mirrors `headSeqs` in
159
+ * shape: the concurrent-writer guarantee limits each session to one writer,
160
+ * so this instance is the only authority for its dropped seqs.
161
+ */
162
+ private readonly filteredSeqs;
163
+ /**
164
+ * Record a head this instance actually observed or wrote.
165
+ * @param id - the session id.
166
+ * @param head - the confirmed dense head, or `-1` for a confirmed absence
167
+ * (a fresh session this instance has read about — a later append to a
168
+ * session that meanwhile got a row must reject).
169
+ */
170
+ confirmHead(id: SessionId, head: number): void;
171
+ /**
172
+ * Fail loud when the on-disk head no longer matches this instance's last
173
+ * confirmed head for the session. `undefined` (never read/written here) is
174
+ * only acceptable for a session with NO row: a row written by someone else
175
+ * means this instance's coordinator cursor is not the log's authority.
176
+ * @param id - the session id.
177
+ * @param storedHead - the on-disk head cursor, read inside the append
178
+ * transaction before any re-numbering happens.
179
+ */
180
+ assertNoConcurrentWriter(id: SessionId, storedHead: number): void;
181
+ /**
182
+ * Record the upstream seqs of events dropped for a session (delta events and
183
+ * ignorable events), so a later batch's `assistant/message` can prune
184
+ * `sourceEventSeqs` references to events that never got a persisted row.
185
+ * @param id - the session id.
186
+ * @param seqs - the dropped events' upstream seqs (pure-delta batches included).
187
+ */
188
+ noteDropped(id: SessionId, seqs: Iterable<number>): void;
189
+ /**
190
+ * Prune `sourceEventSeqs` references that hit this session's dropped-delta
191
+ * seq set. `undefined`-like state (no dropped seqs recorded for the session)
192
+ * leaves the list untouched, matching the write path's "no known drops →
193
+ * keep verbatim" semantics (repair closers, which never carry provenance,
194
+ * call through the identity path).
195
+ *
196
+ * The predicate is THIS INSTANCE's view (see
197
+ * {@link pruneSourceEventSeqs}): only seqs it knows were dropped are
198
+ * pruned — references to rows persisted by another instance (e.g. a resume
199
+ * seed segment) must survive, so the disk-wide view used by the one-shot
200
+ * repair script is not applicable here.
201
+ * @param id - the session id.
202
+ * @param refs - the event's `sourceEventSeqs` (upstream seqs).
203
+ * @returns the pruned list; identical content when nothing was dropped.
204
+ */
205
+ pruneRefs(id: SessionId, refs: readonly number[]): number[];
206
+ }
207
+ //#endregion
208
+ //#region src/schema.d.ts
209
+ /**
210
+ * The on-disk schema version. Bumped only on a breaking change to the table
211
+ * layout; orthogonal to a session's own `version` (which versions the EVENT
212
+ * vocabulary, stored per session in the `t_sessions` row).
213
+ */
214
+ declare const SCHEMA_VERSION = 1;
215
+ /**
216
+ * Event types whose CONTENT is not persisted: the backend drops these rows
217
+ * entirely and re-numbers the surviving events to a dense persisted seq.
218
+ * Mirrors the persistence proposal's "ephemeral events never enter the
219
+ * canonical log" split.
220
+ */
221
+ declare const EPHEMERAL_EVENT_TYPES: readonly ["assistant/chunk"];
222
+ /**
223
+ * Journal modes the backend will run under. `wal` is the default and the
224
+ * durability model the persistence ADR records; the rollback-journal modes
225
+ * (`delete`/`truncate`/`persist`) exist for filesystems where WAL's
226
+ * shared-memory files do not work (network mounts). `memory`/`off` are
227
+ * excluded: dropping journal durability silently contradicts what this
228
+ * backend promises.
229
+ */
230
+ type JournalMode = "wal" | "delete" | "truncate" | "persist";
231
+ //#endregion
232
+ //#region src/branch.d.ts
233
+ /**
234
+ * 定位 `atSeq` 锚定的闭合 `turn/end` 边界 seq。见
235
+ * {@link SessionBranchProvider.readBranchPrefix} 的锚定语义说明。
236
+ * @param events - 完整(或前缀)事件列表,seq 连续。
237
+ * @param atSeq - 锚定 seq(inclusive);省略取最后闭合轮次。
238
+ * @param mode - `"after"`(默认)或 `"before"`。
239
+ * @returns 边界事件 seq;`"before"` 模式下 atSeq 之前无闭合轮次时返回 -1
240
+ * (空前缀)。
241
+ */
242
+ declare function locateTurnEnd(events: readonly SessionEvent[], atSeq?: number, mode?: BranchAnchorMode): number;
243
+ /**
244
+ * live rewind 所需的「live 会话/agent」最小访问面。上游 `Session` 是
245
+ * append-only、缓存增量式对象,无公开截断方法;`PersistenceCoordinator`
246
+ * 的 `states` 是私有 Map。这些是**运行时最小侵入**(不改上游源码,仅在
247
+ * 编排层操作编译后对象字段),由 `SessionBranchRdb` 注入。
248
+ */
249
+ interface LiveSessionHooks {
250
+ /** 查当前 live 会话(`ctx.sessions.get`)。 */
251
+ getSession(id: SessionId): Session | undefined;
252
+ /** 查当前 live agent(`ctx.agents?.get`,agents 服务可选)。 */
253
+ getAgent(id: SessionId): LiveAgentLike | undefined;
254
+ /** 立即落盘 live 会话的 write-behind 缓冲(`ctx.sessions.flush`)。 */
255
+ flush(session: Session): Promise<boolean>;
256
+ /**
257
+ * 截断后同步 coordinator 的会话内存状态:把 `states` 条目的 `cursor`
258
+ * 对齐到新的尾部(= boundary + 1),使下一次 append 的 seq 连续性校验
259
+ * 通过。**不能**删除 `states` 条目——live 会话的下一次 append 若走
260
+ * `adopt` 会经 `SessionStore.prepare` 构造 detached session,与 store 中
261
+ * 的 live entry 冲突。
262
+ */
263
+ setCoordinatorCursor(id: SessionId, cursor: number): void;
264
+ }
265
+ /** agent 的最小 live 形态:持有同一会话 + 可重置请求头日志标记。 */
266
+ interface LiveAgentLike {
267
+ session: Session;
268
+ /** 编译后字段:是否已 append 过首个 `request/header`(截断后需重置)。 */
269
+ requestHeaderLogged?: boolean;
270
+ }
271
+ /**
272
+ * RDB 分支数据层实现(`SessionBranchProvider`)。与 `SessionPersistenceRdb`
273
+ * 共享同一数据库连接(`Backend`)与 coordinator 写路径。
274
+ */
275
+ declare class SessionBranchRdbProvider implements SessionBranchProvider {
276
+ private readonly persistence;
277
+ /** live 会话/agent 访问面(rewind 支持 live session 时使用)。 */
278
+ private readonly live;
279
+ readonly name = "session-rdb";
280
+ constructor(persistence: SessionPersistenceRdb,
281
+ /** live 会话/agent 访问面(rewind 支持 live session 时使用)。 */
282
+ live?: LiveSessionHooks);
283
+ readBranchPrefix(id: SessionId, atSeq?: number, mode?: BranchAnchorMode, signal?: AbortSignal): Promise<BranchBoundary>;
284
+ forkFrom(sourceId: SessionId, options?: ForkFromOptions, signal?: AbortSignal): Promise<SessionId>;
285
+ rewind(id: SessionId, toBoundary: number, signal?: AbortSignal): Promise<SessionPersistenceSnapshot>;
286
+ }
287
+ /**
288
+ * RDB 的 `ctx.sessionBranch` 服务:组合 {@link SessionBranchRdbProvider} 的
289
+ * 数据层原语与共享版本树投影。插件把本类注册为 `sessionBranch` 服务后,
290
+ * 编排层即可通过统一服务面完成 rewind / retry / fork。
291
+ */
292
+ declare class SessionBranchRdb extends SessionBranch {
293
+ static inject: string[];
294
+ constructor(ctx: import("@deepseek-ai/cordis").Context);
295
+ private readonly provider;
296
+ readBranchPrefix(id: SessionId, atSeq?: number, mode?: BranchAnchorMode, signal?: AbortSignal): Promise<BranchBoundary>;
297
+ forkFrom(sourceId: SessionId, options?: ForkFromOptions, signal?: AbortSignal): Promise<SessionId>;
298
+ rewind(id: SessionId, toBoundary: number, signal?: AbortSignal): Promise<SessionPersistenceSnapshot>;
299
+ /**
300
+ * 清洗一个 session 的 surface/provenance 坐标到稠密空间(持久化层透传)。
301
+ * 旧数据(rewind 前写入)的 sourceEventSeqs / surfaceOp / shadowedRange
302
+ * 是上游坐标,读取时每次都要对齐;清洗一次性写回稠密坐标,之后读取无需
303
+ * 对齐。返回实际变更的事件数。
304
+ */
305
+ cleanseSession(sessionId: SessionId, signal?: AbortSignal): Promise<{
306
+ changed: number;
307
+ }>;
308
+ /**
309
+ * 同步 live 会话的 coordinator 内存 cursor,跳过 ignorable 占位事件。
310
+ * 编排层在 rewind 后把 ignorable 版本效果 push 进 live log(不发布、不
311
+ * 进缓冲)——它占用一个上游 seq,但 coordinator 的 cursor(rewind 设到
312
+ * 截断后长度)看不见它;不跳过的话,flush 的 `appendLiveBatch` 会以
313
+ * `e.seq >= cursor` 过滤掉 cursor 之前的事件(manualTurn 永远不落盘),
314
+ * 或 `appendCore` 的 seq 连续性校验错位。这里把 cursor 从当前值起跳过
315
+ * 连续的 ignorable 事件,对齐到下一个待持久化事件的 seq。
316
+ */
317
+ syncLiveCursor(sessionId: SessionId): void;
318
+ timeline(sessionId: SessionId, signal?: AbortSignal): Promise<import("@morlay/session-branch").BranchTimeline>;
319
+ }
320
+ //#endregion
321
+ //#region src/index.d.ts
322
+ /**
323
+ * 同包分支 provider(rewind / forkFrom)访问 `SessionPersistenceRdb` 内部
324
+ * 能力的窄接口。避免把 backend / writeGuard 暴露成公开 API,同时让
325
+ * {@link SessionBranchRdbProvider} 与持久化后端共享同一连接与写路径。
326
+ * @internal
327
+ */
328
+ interface SessionPersistenceRdbInternals {
329
+ readonly backend: Backend;
330
+ readonly writeGuard: WriteGuard;
331
+ create(meta: SessionHeader): Promise<void>;
332
+ append(id: SessionId, events: readonly SessionEvent[]): Promise<void>;
333
+ load(id: SessionId): Promise<import("@deepseek-ai/dsh-session-persistence").SessionInspection>;
334
+ inspect(id: SessionId, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-session-persistence").SessionInspection>;
335
+ readFrom(id: SessionId, fromSeq: number, signal?: AbortSignal): Promise<{
336
+ meta: SessionHeader;
337
+ events: SessionEvent[];
338
+ }>;
339
+ listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]>;
340
+ readStoredRevision(id: SessionId, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-session-persistence").SessionPersistenceRevision | undefined>;
341
+ }
342
+ /**
343
+ * Plugin configuration — a discriminated union on `type`. The SQLite arm keeps
344
+ * the file path plus the SQLite-specific pragmas; the PostgreSQL arm takes a
345
+ * `node-postgres` connection string.
346
+ */
347
+ type Config = {
348
+ type: "sqlite";
349
+ /**
350
+ * Filesystem path to the SQLite database file. The special value `:memory:`
351
+ * opens an in-process database (tests). On filesystems with POSIX modes,
352
+ * missing directories and databases are created owner-only; existing path
353
+ * modes are preserved.
354
+ */
355
+ path: string;
356
+ /**
357
+ * SQLite `journal_mode` pragma. `wal` (the default) is the recorded
358
+ * durability model; pick a rollback-journal mode (`delete`/`truncate`/
359
+ * `persist`) on filesystems where WAL's shared-memory files do not work
360
+ * (network mounts). See {@link JournalMode}.
361
+ */
362
+ journalMode?: JournalMode;
363
+ /**
364
+ * Milliseconds to wait for a contended write lock before failing. SQLite
365
+ * fails immediately by default, so a second process sharing this database
366
+ * would lose every append that meets an in-flight commit; a nonzero wait
367
+ * turns the contention window into a queue. `0` restores fail-fast.
368
+ */
369
+ busyTimeout?: number;
370
+ } | {
371
+ type: "postgres";
372
+ /**
373
+ * `node-postgres` connection string (e.g.
374
+ * `postgres://user:pass@host:5432/db`). The database must be reachable;
375
+ * the backend creates its tables and identity on first open.
376
+ */
377
+ connectionString: string;
378
+ };
379
+ /**
380
+ * The persistence backend. Load as a plugin; it registers as
381
+ * `ctx.sessionPersistence` and (via the coordinator) installs the write-path
382
+ * listeners. Its torn-tail marker is the persisted seq to delete from.
383
+ *
384
+ * Configuration resolution: `$DSH_HOME/settings.yaml` 的
385
+ * `session-rdb` namespace(settings 服务)覆盖 cordis 层 entry
386
+ * config,见 {@link installSettingsSection}。
387
+ */
388
+ declare class SessionPersistenceRdb extends SessionPersistence implements PersistenceBackend<number> {
389
+ config: Config;
390
+ static inject: string[];
391
+ static Config: z<Config>;
392
+ /** settings namespace:`$DSH_HOME/settings.yaml` 的 `session-rdb` section。 */
393
+ static readonly settingsNs: import("@deepseek-ai/dsh-settings").SettingsNamespace;
394
+ /**
395
+ * Backend label for the coordinator's dispose diagnostics. Intentionally
396
+ * shadows cordis `Service.name` (set to `'sessionPersistence'` by the base);
397
+ * see the JSONL backend for why this does not affect service resolution.
398
+ */
399
+ readonly name = "session-rdb";
400
+ /** One RDB database holds every session; there is no per-session raw artifact. */
401
+ readonly supportsRawArtifacts = false;
402
+ private readonly backend;
403
+ private storeIdentity;
404
+ private readonly ready;
405
+ private readonly coordinator;
406
+ /**
407
+ * Write-authority state: the confirmed dense head per session (concurrent-
408
+ * writer detection) and the dropped delta seqs per session (provenance
409
+ * pruning). See {@link WriteGuard} for the timing contract.
410
+ */
411
+ private readonly writeGuard;
412
+ constructor(ctx: Context, config: Config,
413
+ /**
414
+ * @internal Test injection: use a pre-built backend (e.g. a drizzle PG
415
+ * instance over an in-memory pglite) instead of {@link createBackend}.
416
+ */
417
+ injectedBackend?: Backend);
418
+ private init;
419
+ /** The backend has one database, not an independent local artifact per session. */
420
+ locate(_meta: SessionHeader): SessionLocation | undefined;
421
+ create(meta: SessionHeader): Promise<void>;
422
+ append(id: SessionId, events: readonly SessionEvent[]): Promise<void>;
423
+ load(id: SessionId): Promise<{
424
+ meta: SessionHeader;
425
+ events: readonly SessionEvent[];
426
+ }>;
427
+ inspect(id: SessionId, signal?: AbortSignal): Promise<{
428
+ meta: SessionHeader;
429
+ events: readonly SessionEvent[];
430
+ }>;
431
+ readFrom(id: SessionId, fromSeq: number, signal?: AbortSignal): Promise<{
432
+ meta: SessionHeader;
433
+ events: SessionEvent[];
434
+ }>;
435
+ /** Read a stored prefix by id (ids are globally unique — no scope to scan). */
436
+ loadStored(id: SessionId, signal?: AbortSignal): Promise<StoredPrefix<number> | undefined>;
437
+ /**
438
+ * Seek-capable suffix read: the backend selects `f_sequence >= fromSeq`
439
+ * directly, so the read scales with the suffix, not the log. Provenance
440
+ * remapping still needs every row's upstream seq, so a lightweight
441
+ * two-column map is read alongside. Torn rows past the preserved region are
442
+ * dropped, never repaired (non-mutating read).
443
+ */
444
+ loadStoredFrom(id: SessionId, fromSeq: number, signal?: AbortSignal): Promise<StoredSuffix | undefined>;
445
+ /**
446
+ * Read a session's row + ordered events into a {@link StoredPrefix}. The
447
+ * torn-tail marker is the persisted seq from which a never-committed tail
448
+ * must be deleted (`scanRows` already returns it as `number | undefined`).
449
+ * Records the confirmed dense head (or confirmed absence) so a later
450
+ * `appendBatch` can detect a second writer that advanced the log.
451
+ */
452
+ private readPrefix;
453
+ /**
454
+ * Read the current source-qualified revision for one stored session without
455
+ * loading its event log. Returns `undefined` when the identity is absent.
456
+ * The representation matches {@link loadStored}'s `revision` and
457
+ * {@link listSnapshots} — the coordinator compares them with `===`.
458
+ */
459
+ readStoredRevision(id: SessionId, signal?: AbortSignal): Promise<SessionPersistenceRevision | undefined>;
460
+ /**
461
+ * Shared read pipeline: session row → meta, event rows → preserved prefix.
462
+ * A whole-log read (`fromSeq` absent) builds the seq map from the same rows;
463
+ * a suffix read keeps the backend's lightweight two-column seq-map source so
464
+ * the query still scales with the suffix, not the log.
465
+ */
466
+ private readLog;
467
+ /**
468
+ * Durably append a batch in ONE transaction: materialize the sessions row (if
469
+ * lazy) and INSERT every persisted event (plus its bridge row), or roll back
470
+ * entirely. Delta events and events the writer marked `ignorable` are dropped
471
+ * and the surviving events are re-numbered densely from the session's head
472
+ * cursor; a batch that contains only dropped events is a no-op (no row
473
+ * materialization, no revision bump). Dropped events' upstream seqs are
474
+ * recorded per session so a later batch's surface provenance can prune
475
+ * references to them (see {@link surfaceBindings}).
476
+ * The transaction is the atomicity + durability boundary, so a mid-batch
477
+ * failure (a UNIQUE violation on a duplicated seq) leaves the stored log
478
+ * untouched.
479
+ *
480
+ * SQLite acquires the write lock up front (`BEGIN IMMEDIATE`, queued behind
481
+ * `busy_timeout`); PostgreSQL relies on the transaction's row locks and the
482
+ * `UNIQUE (f_session_id, f_sequence)` constraint to reject a colliding batch.
483
+ * Either way {@link assertNoConcurrentWriter} rejects a second writer before
484
+ * re-numbering — a session has exactly one writer per log, and a second
485
+ * writer fails loud instead of corrupting the log.
486
+ *
487
+ * The row upsert runs UNCONDITIONALLY, not only when `!isMaterialized`: a
488
+ * delta-only batch leaves the coordinator's materialized flag true while no
489
+ * row exists, so the flag cannot be trusted as the row's existence signal.
490
+ * The upsert keeps an existing row's head cursor (only header columns are
491
+ * refreshed on conflict), so a fresh row still starts at the initial head.
492
+ */
493
+ appendBatch(meta: SessionHeader, events: readonly SessionEvent[], _isMaterialized: boolean): Promise<void>;
494
+ /**
495
+ * Make a crash repair durable in ONE transaction: DELETE the torn tail (from
496
+ * `tornMarker`), rewind the head cursor to the last surviving event, INSERT
497
+ * the synthetic `closers`, and bump the revision once. After COMMIT the
498
+ * stored rows == the balanced log.
499
+ */
500
+ commitRepair(meta: SessionHeader, tornMarker: number | undefined, closers: readonly SessionEvent[]): Promise<void>;
501
+ /**
502
+ * 一次性清洗一个 session 的 surface/provenance 坐标到稠密空间并写回。
503
+ *
504
+ * 旧数据(rewind 前的代码写入)的 `sourceEventSeqs`、`surfaceOp` replace
505
+ * range 与 compaction `shadowedRange` 是上游坐标,读取时每次都要做坐标
506
+ * 解析对齐;清洗用与读取完全相同的解析(稠密优先 + 上游映射 + 剪枝 +
507
+ * replace provenance 补全)把它们重写为稠密坐标,之后读取无需再对齐
508
+ * (对已清洗数据解析恒为恒等)。torn tail 片段不参与(读取路径也不会
509
+ * 保留它们)。返回实际变更的事件数。
510
+ */
511
+ cleanseSession(id: SessionId, signal?: AbortSignal): Promise<{
512
+ changed: number;
513
+ }>;
514
+ /** List all materialized sessions' metadata (every row is a materialized session). */
515
+ list(signal?: AbortSignal): Promise<SessionHeader[]>;
516
+ /** List metadata with a source-qualified monotonic revision per session. */
517
+ listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]>;
518
+ /** Close the database connection (awaited by the coordinator's dispose, post-drain). */
519
+ close(): Promise<void>;
520
+ /**
521
+ * 同包分支 provider 的内部访问面(rewind / forkFrom 共享后端与写路径)。
522
+ * @internal 仅供 `SessionBranchRdbProvider` 使用;不是公开 API。
523
+ */
524
+ internals(): SessionPersistenceRdbInternals;
525
+ }
526
+ //#endregion
527
+ export { Config, EPHEMERAL_EVENT_TYPES, SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionPersistenceRdb, SessionPersistenceRdb as default, SessionPersistenceRdbInternals, locateTurnEnd };
528
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/backend.ts","../src/write-guard.ts","../src/schema.ts","../src/branch.ts","../src/index.ts"],"mappings":"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"}