@morlay/session-rdb 0.0.10 → 0.0.12
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 +0 -16
- package/dist/index.d.mts +203 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2030 -0
- package/dist/index.mjs.map +1 -0
- package/dist/invariant.d.mts +8 -0
- package/dist/invariant.d.mts.map +1 -0
- package/dist/invariant.mjs +10 -0
- package/dist/invariant.mjs.map +1 -0
- package/package.json +22 -21
- package/src/adapters/ddl.ts +77 -0
- package/src/adapters/index.ts +4 -0
- package/src/adapters/to-postgres.ts +91 -0
- package/src/adapters/to-sqlite.ts +79 -0
- package/src/backend.ts +112 -0
- package/src/branch.ts +508 -0
- package/src/entities/events.ts +27 -0
- package/src/entities/index.ts +30 -0
- package/src/entities/persistence-state.ts +10 -0
- package/src/entities/schema-meta.ts +9 -0
- package/src/entities/session-events.ts +29 -0
- package/src/entities/sessions.ts +20 -0
- package/src/entities/types.ts +48 -0
- package/src/import.ts +270 -0
- package/src/index.ts +543 -0
- package/src/invariant.ts +15 -0
- package/src/log.ts +370 -0
- package/src/migrate.ts +137 -0
- package/src/postgres.ts +369 -0
- package/src/schema.ts +234 -0
- package/src/sqlite.ts +412 -0
- package/src/write-guard.ts +27 -0
- package/lib/index.d.mts +0 -558
- package/lib/index.d.mts.map +0 -1
- package/lib/index.mjs +0 -2175
- package/lib/index.mjs.map +0 -1
- package/lib/invariant.d.mts +0 -15
- package/lib/invariant.d.mts.map +0 -1
- package/lib/invariant.mjs +0 -21
- package/lib/invariant.mjs.map +0 -1
package/src/postgres.ts
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { and, desc, eq, gte, sql } from "drizzle-orm";
|
|
3
|
+
import type { PgAsyncDatabase, PgAsyncTransaction, PgQueryResultHKT } from "drizzle-orm/pg-core";
|
|
4
|
+
import type { SessionId } from "@deepseek-ai/dsh-session";
|
|
5
|
+
import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
|
|
6
|
+
import {
|
|
7
|
+
type Backend,
|
|
8
|
+
type BackendTx,
|
|
9
|
+
type EventInsert,
|
|
10
|
+
type EventRow,
|
|
11
|
+
type SessionRow,
|
|
12
|
+
} from "./backend.ts";
|
|
13
|
+
import { SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID } from "./schema.ts";
|
|
14
|
+
import { createTablesSql, toPostgresSchema } from "./adapters/index.ts";
|
|
15
|
+
import { postgresTableDefs } from "./entities/index.ts";
|
|
16
|
+
import { sessionConflictRow, sessionInsertRow } from "./log.ts";
|
|
17
|
+
|
|
18
|
+
export interface PostgresBackendOptions {
|
|
19
|
+
identityBase: string;
|
|
20
|
+
|
|
21
|
+
schema?: string;
|
|
22
|
+
|
|
23
|
+
close: () => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> implements Backend {
|
|
27
|
+
readonly kind = "postgres" as const;
|
|
28
|
+
storeIdentity!: string;
|
|
29
|
+
|
|
30
|
+
private readonly tables: Record<string, any>;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
private readonly db: PgAsyncDatabase<THKT>,
|
|
34
|
+
private readonly options: PostgresBackendOptions,
|
|
35
|
+
) {
|
|
36
|
+
this.tables = toPostgresSchema(postgresTableDefs, this.options.schema ?? "public");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async open(): Promise<void> {
|
|
40
|
+
const storeId = await this.db.transaction(async (tx) => {
|
|
41
|
+
const schema = this.options.schema ?? "public";
|
|
42
|
+
// 探测在建表之前:t_schema_meta 存在与否区分全新库与已有库。
|
|
43
|
+
// `to_regclass` 是数据库元数据查询,drizzle 无对应 API;schema 限定
|
|
44
|
+
// 显式引用(不依赖 search_path)。
|
|
45
|
+
const qualifiedMeta = schema === "public" ? "t_schema_meta" : `"${schema}".t_schema_meta`;
|
|
46
|
+
const probe = (await tx.execute(
|
|
47
|
+
sql`SELECT to_regclass(${qualifiedMeta}) IS NOT NULL AS exists`,
|
|
48
|
+
)) as unknown as { rows: { exists: boolean }[] };
|
|
49
|
+
const metaExists = probe.rows[0]?.exists === true;
|
|
50
|
+
// DDL 一次一条(PG 的 extended query protocol 拒绝多语句字符串),
|
|
51
|
+
// 逐条执行保持初始化原子。
|
|
52
|
+
for (const statement of createTablesSql("postgres", postgresTableDefs, schema)) {
|
|
53
|
+
await tx.execute(sql.raw(statement));
|
|
54
|
+
}
|
|
55
|
+
if (!metaExists) {
|
|
56
|
+
await tx
|
|
57
|
+
.insert(this.tables["t_schema_meta"])
|
|
58
|
+
.values([
|
|
59
|
+
{ fKey: "schema_version", fValue: String(SCHEMA_VERSION) },
|
|
60
|
+
{ fKey: "application_id", fValue: String(SESSION_PERSISTENCE_SQLITE_APPLICATION_ID) },
|
|
61
|
+
])
|
|
62
|
+
.execute();
|
|
63
|
+
}
|
|
64
|
+
// 校验:缺版本行 = 有对象但未版本化 → 拒绝,不迁移。
|
|
65
|
+
const version = await this.readMeta(tx, "schema_version");
|
|
66
|
+
const applicationId = await this.readMeta(tx, "application_id");
|
|
67
|
+
if (version === undefined || applicationId === undefined) {
|
|
68
|
+
throw new Error("session database has an unversioned schema or application identity");
|
|
69
|
+
}
|
|
70
|
+
if (Number(version) !== SCHEMA_VERSION) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`session database has schema version ${version}, incompatible with this build (${SCHEMA_VERSION})`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (Number(applicationId) !== SESSION_PERSISTENCE_SQLITE_APPLICATION_ID) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`session database has application id ${applicationId}, expected ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
await tx
|
|
81
|
+
.insert(this.tables["t_persistence_state"])
|
|
82
|
+
.values({ fSingleton: 1, fStoreId: randomUUID() })
|
|
83
|
+
.onConflictDoNothing()
|
|
84
|
+
.execute();
|
|
85
|
+
const store = await tx
|
|
86
|
+
.select({ fStoreId: this.tables["t_persistence_state"].fStoreId })
|
|
87
|
+
.from(this.tables["t_persistence_state"])
|
|
88
|
+
.where(eq(this.tables["t_persistence_state"].fSingleton, 1))
|
|
89
|
+
.execute();
|
|
90
|
+
const storeId = store[0]?.fStoreId;
|
|
91
|
+
if (storeId === undefined || storeId.length === 0) {
|
|
92
|
+
throw new Error("session database has no valid store identity");
|
|
93
|
+
}
|
|
94
|
+
return storeId;
|
|
95
|
+
});
|
|
96
|
+
this.storeIdentity = `${this.options.identityBase}:store:${storeId}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async close(): Promise<void> {
|
|
100
|
+
await this.options.close();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async getSession(id: SessionId): Promise<SessionRow | undefined> {
|
|
104
|
+
return (
|
|
105
|
+
await this.db
|
|
106
|
+
.select()
|
|
107
|
+
.from(this.tables["t_sessions"])
|
|
108
|
+
.where(eq(this.tables["t_sessions"].fSessionId, id))
|
|
109
|
+
.execute()
|
|
110
|
+
)[0] as SessionRow | undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async getSeqMapRows(id: SessionId): Promise<Array<{ fSequence: number; fOriginalSeq: number }>> {
|
|
114
|
+
return this.db
|
|
115
|
+
.select({
|
|
116
|
+
fSequence: this.tables["t_session_events"].fSequence,
|
|
117
|
+
fOriginalSeq: this.tables["t_session_events"].fOriginalSeq,
|
|
118
|
+
})
|
|
119
|
+
.from(this.tables["t_session_events"])
|
|
120
|
+
.where(eq(this.tables["t_session_events"].fSessionId, id))
|
|
121
|
+
.execute();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async getEventRows(id: SessionId, fromSequence?: number): Promise<EventRow[]> {
|
|
125
|
+
const scoped =
|
|
126
|
+
fromSequence === undefined
|
|
127
|
+
? this.eventRows(this.db).where(eq(this.tables["t_session_events"].fSessionId, id))
|
|
128
|
+
: this.eventRows(this.db).where(
|
|
129
|
+
and(
|
|
130
|
+
eq(this.tables["t_session_events"].fSessionId, id),
|
|
131
|
+
gte(this.tables["t_session_events"].fSequence, fromSequence),
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
return scoped
|
|
135
|
+
.orderBy(this.tables["t_session_events"].fSequence)
|
|
136
|
+
.execute() as unknown as EventRow[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async listSessions(): Promise<SessionRow[]> {
|
|
140
|
+
return this.db.select().from(this.tables["t_sessions"]).execute() as unknown as Promise<
|
|
141
|
+
SessionRow[]
|
|
142
|
+
>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async transaction<T>(fn: (tx: BackendTx) => Promise<T>): Promise<T> {
|
|
146
|
+
return this.db.transaction(async (tx) => fn(this.txFor(tx)));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private txFor(tx: PgAsyncTransaction<THKT>): BackendTx {
|
|
150
|
+
return {
|
|
151
|
+
upsertSession: (storage, incarnation) => this.upsertSession(tx, storage, incarnation),
|
|
152
|
+
getHead: (id) => this.getHead(tx, id),
|
|
153
|
+
getSeedLength: (id) => this.getSeedLength(tx, id),
|
|
154
|
+
updateSeedLength: (id, seedLength) => this.updateSeedLength(tx, id, seedLength),
|
|
155
|
+
insertEvents: (events) => this.insertEvents(tx, events),
|
|
156
|
+
insertBridges: (rows) => this.insertBridges(tx, rows),
|
|
157
|
+
updateHead: (id, headEventId, headSequence) =>
|
|
158
|
+
this.updateHead(tx, id, headEventId, headSequence),
|
|
159
|
+
bumpRevision: (id) => this.bumpRevision(tx, id),
|
|
160
|
+
deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(tx, id, fromSequence),
|
|
161
|
+
getPrevBridge: (id, sequence) => this.getPrevBridge(tx, id, sequence),
|
|
162
|
+
getLastBridge: (id) => this.getLastBridge(tx, id),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- meta helpers ---
|
|
167
|
+
|
|
168
|
+
private async readMeta(exec: PgAsyncDatabase<THKT>, key: string): Promise<string | undefined> {
|
|
169
|
+
const rows = await exec
|
|
170
|
+
.select({ fValue: this.tables["t_schema_meta"].fValue })
|
|
171
|
+
.from(this.tables["t_schema_meta"])
|
|
172
|
+
.where(eq(this.tables["t_schema_meta"].fKey, key))
|
|
173
|
+
.execute();
|
|
174
|
+
return rows[0]?.fValue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// --- row primitives (transaction-internal) ---
|
|
178
|
+
|
|
179
|
+
private async upsertSession(
|
|
180
|
+
exec: PgAsyncDatabase<THKT>,
|
|
181
|
+
storage: SessionStorageMetadata,
|
|
182
|
+
incarnation: string,
|
|
183
|
+
): Promise<void> {
|
|
184
|
+
await exec
|
|
185
|
+
.insert(this.tables["t_sessions"])
|
|
186
|
+
.values(sessionInsertRow(storage, incarnation))
|
|
187
|
+
.onConflictDoUpdate({
|
|
188
|
+
target: this.tables["t_sessions"].fSessionId,
|
|
189
|
+
set: sessionConflictRow(storage),
|
|
190
|
+
})
|
|
191
|
+
.execute();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private async getHead(
|
|
195
|
+
exec: PgAsyncDatabase<THKT>,
|
|
196
|
+
id: SessionId,
|
|
197
|
+
): Promise<Pick<SessionRow, "fHeadEventId" | "fHeadSequence">> {
|
|
198
|
+
const head = (
|
|
199
|
+
await exec
|
|
200
|
+
.select({
|
|
201
|
+
fHeadEventId: this.tables["t_sessions"].fHeadEventId,
|
|
202
|
+
fHeadSequence: this.tables["t_sessions"].fHeadSequence,
|
|
203
|
+
})
|
|
204
|
+
.from(this.tables["t_sessions"])
|
|
205
|
+
.where(eq(this.tables["t_sessions"].fSessionId, id))
|
|
206
|
+
.execute()
|
|
207
|
+
)[0] as Pick<SessionRow, "fHeadEventId" | "fHeadSequence"> | undefined;
|
|
208
|
+
/* v8 ignore next -- appendBatch/commitRepair always materialize the row before reading the head */
|
|
209
|
+
if (head === undefined) throw new Error(`session "${id}" has no materialized row`);
|
|
210
|
+
return head;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private async getSeedLength(exec: PgAsyncDatabase<THKT>, id: SessionId): Promise<number | null> {
|
|
214
|
+
const row = (
|
|
215
|
+
await exec
|
|
216
|
+
.select({ fSeedLength: this.tables["t_sessions"].fSeedLength })
|
|
217
|
+
.from(this.tables["t_sessions"])
|
|
218
|
+
.where(eq(this.tables["t_sessions"].fSessionId, id))
|
|
219
|
+
.execute()
|
|
220
|
+
)[0] as { fSeedLength: number | null } | undefined;
|
|
221
|
+
/* v8 ignore next -- rewind always materializes the row before reading the seed length */
|
|
222
|
+
if (row === undefined) throw new Error(`session "${id}" has no materialized row`);
|
|
223
|
+
return row.fSeedLength;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private async updateSeedLength(
|
|
227
|
+
exec: PgAsyncDatabase<THKT>,
|
|
228
|
+
id: SessionId,
|
|
229
|
+
seedLength: number,
|
|
230
|
+
): Promise<void> {
|
|
231
|
+
await exec
|
|
232
|
+
.update(this.tables["t_sessions"])
|
|
233
|
+
.set({ fSeedLength: seedLength })
|
|
234
|
+
.where(eq(this.tables["t_sessions"].fSessionId, id))
|
|
235
|
+
.execute();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private static readonly INSERT_BATCH_ROWS = 1000;
|
|
239
|
+
|
|
240
|
+
private async insertEvents(exec: PgAsyncDatabase<THKT>, events: EventInsert[]): Promise<void> {
|
|
241
|
+
if (events.length === 0) return;
|
|
242
|
+
for (let i = 0; i < events.length; i += PostgresBackend.INSERT_BATCH_ROWS) {
|
|
243
|
+
await exec
|
|
244
|
+
.insert(this.tables["t_events"])
|
|
245
|
+
.values(
|
|
246
|
+
events.slice(i, i + PostgresBackend.INSERT_BATCH_ROWS).map((event) => ({ ...event })),
|
|
247
|
+
)
|
|
248
|
+
.execute();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private async insertBridges(
|
|
253
|
+
exec: PgAsyncDatabase<THKT>,
|
|
254
|
+
rows: Array<{
|
|
255
|
+
fSessionId: SessionId;
|
|
256
|
+
fEventId: string;
|
|
257
|
+
fSequence: number;
|
|
258
|
+
fOriginalSeq: number;
|
|
259
|
+
fSurfaceOp: string | null;
|
|
260
|
+
}>,
|
|
261
|
+
): Promise<void> {
|
|
262
|
+
if (rows.length === 0) return;
|
|
263
|
+
for (let i = 0; i < rows.length; i += PostgresBackend.INSERT_BATCH_ROWS) {
|
|
264
|
+
await exec
|
|
265
|
+
.insert(this.tables["t_session_events"])
|
|
266
|
+
.values(rows.slice(i, i + PostgresBackend.INSERT_BATCH_ROWS).map((row) => ({ ...row })))
|
|
267
|
+
.execute();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private async updateHead(
|
|
272
|
+
exec: PgAsyncDatabase<THKT>,
|
|
273
|
+
id: SessionId,
|
|
274
|
+
headEventId: string,
|
|
275
|
+
headSequence: number,
|
|
276
|
+
): Promise<void> {
|
|
277
|
+
await exec
|
|
278
|
+
.update(this.tables["t_sessions"])
|
|
279
|
+
.set({ fHeadEventId: headEventId, fHeadSequence: headSequence })
|
|
280
|
+
.where(eq(this.tables["t_sessions"].fSessionId, id))
|
|
281
|
+
.execute();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private async bumpRevision(exec: PgAsyncDatabase<THKT>, id: SessionId): Promise<void> {
|
|
285
|
+
await exec
|
|
286
|
+
.update(this.tables["t_sessions"])
|
|
287
|
+
.set({ fRevision: sql`${this.tables["t_sessions"].fRevision} + 1` })
|
|
288
|
+
.where(eq(this.tables["t_sessions"].fSessionId, id))
|
|
289
|
+
.execute();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private async deleteBridgeTail(
|
|
293
|
+
exec: PgAsyncDatabase<THKT>,
|
|
294
|
+
id: SessionId,
|
|
295
|
+
fromSequence: number,
|
|
296
|
+
): Promise<void> {
|
|
297
|
+
await exec
|
|
298
|
+
.delete(this.tables["t_session_events"])
|
|
299
|
+
.where(
|
|
300
|
+
and(
|
|
301
|
+
eq(this.tables["t_session_events"].fSessionId, id),
|
|
302
|
+
gte(this.tables["t_session_events"].fSequence, fromSequence),
|
|
303
|
+
),
|
|
304
|
+
)
|
|
305
|
+
.execute();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private async getPrevBridge(
|
|
309
|
+
exec: PgAsyncDatabase<THKT>,
|
|
310
|
+
id: SessionId,
|
|
311
|
+
sequence: number,
|
|
312
|
+
): Promise<{ fEventId: string; fSequence: number } | undefined> {
|
|
313
|
+
return (
|
|
314
|
+
await exec
|
|
315
|
+
.select({
|
|
316
|
+
fEventId: this.tables["t_session_events"].fEventId,
|
|
317
|
+
fSequence: this.tables["t_session_events"].fSequence,
|
|
318
|
+
})
|
|
319
|
+
.from(this.tables["t_session_events"])
|
|
320
|
+
.where(
|
|
321
|
+
and(
|
|
322
|
+
eq(this.tables["t_session_events"].fSessionId, id),
|
|
323
|
+
eq(this.tables["t_session_events"].fSequence, sequence),
|
|
324
|
+
),
|
|
325
|
+
)
|
|
326
|
+
.execute()
|
|
327
|
+
)[0] as { fEventId: string; fSequence: number } | undefined;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private async getLastBridge(
|
|
331
|
+
exec: PgAsyncDatabase<THKT>,
|
|
332
|
+
id: SessionId,
|
|
333
|
+
): Promise<{ fEventId: string; fSequence: number } | undefined> {
|
|
334
|
+
return (
|
|
335
|
+
await exec
|
|
336
|
+
.select({
|
|
337
|
+
fEventId: this.tables["t_session_events"].fEventId,
|
|
338
|
+
fSequence: this.tables["t_session_events"].fSequence,
|
|
339
|
+
})
|
|
340
|
+
.from(this.tables["t_session_events"])
|
|
341
|
+
.where(eq(this.tables["t_session_events"].fSessionId, id))
|
|
342
|
+
.orderBy(desc(this.tables["t_session_events"].fSequence))
|
|
343
|
+
.limit(1)
|
|
344
|
+
.execute()
|
|
345
|
+
)[0] as { fEventId: string; fSequence: number } | undefined;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private eventRows(exec: PgAsyncDatabase<THKT>) {
|
|
349
|
+
return exec
|
|
350
|
+
.select({
|
|
351
|
+
fEventId: this.tables["t_session_events"].fEventId,
|
|
352
|
+
fSequence: this.tables["t_session_events"].fSequence,
|
|
353
|
+
fOriginalSeq: this.tables["t_session_events"].fOriginalSeq,
|
|
354
|
+
fType: this.tables["t_events"].fType,
|
|
355
|
+
fKind: this.tables["t_events"].fKind,
|
|
356
|
+
fRole: this.tables["t_events"].fRole,
|
|
357
|
+
fName: this.tables["t_events"].fName,
|
|
358
|
+
fActionId: this.tables["t_events"].fActionId,
|
|
359
|
+
fCreatedAt: this.tables["t_events"].fCreatedAt,
|
|
360
|
+
fData: this.tables["t_events"].fData,
|
|
361
|
+
fSurfaceOp: this.tables["t_session_events"].fSurfaceOp,
|
|
362
|
+
})
|
|
363
|
+
.from(this.tables["t_session_events"])
|
|
364
|
+
.innerJoin(
|
|
365
|
+
this.tables["t_events"],
|
|
366
|
+
eq(this.tables["t_session_events"].fEventId, this.tables["t_events"].fEventId),
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { SessionEvent } from "@deepseek-ai/dsh-session";
|
|
2
|
+
import { toSqliteSchema } from "./adapters/index.ts";
|
|
3
|
+
import { sqliteTableDefs } from "./entities/index.ts";
|
|
4
|
+
|
|
5
|
+
export const SCHEMA_VERSION = 2;
|
|
6
|
+
|
|
7
|
+
export const SESSION_PERSISTENCE_SQLITE_APPLICATION_ID = 0x44534850;
|
|
8
|
+
|
|
9
|
+
export const EPHEMERAL_EVENT_TYPES = ["assistant/chunk"] as const;
|
|
10
|
+
|
|
11
|
+
export const EVENT_ENCODING = "json";
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
const sqliteTables: Record<string, any> = toSqliteSchema(sqliteTableDefs);
|
|
15
|
+
|
|
16
|
+
export const tPersistenceState = sqliteTables["t_persistence_state"]!;
|
|
17
|
+
|
|
18
|
+
export const tSessions = sqliteTables["t_sessions"]!;
|
|
19
|
+
|
|
20
|
+
export const tEvents = sqliteTables["t_events"]!;
|
|
21
|
+
|
|
22
|
+
export const tSessionEvents = sqliteTables["t_session_events"]!;
|
|
23
|
+
|
|
24
|
+
export type { SessionRow } from "./backend.ts";
|
|
25
|
+
|
|
26
|
+
export type { EventRow } from "./backend.ts";
|
|
27
|
+
|
|
28
|
+
export type JournalMode = "wal" | "delete" | "truncate" | "persist";
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_BUSY_TIMEOUT_MS = 5000;
|
|
31
|
+
|
|
32
|
+
export function isEphemeralType(type: string): boolean {
|
|
33
|
+
return (EPHEMERAL_EVENT_TYPES as readonly string[]).includes(type);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isPersistedEvent(event: SessionEvent): boolean {
|
|
37
|
+
return (
|
|
38
|
+
!isEphemeralType(event.type) &&
|
|
39
|
+
(event as SessionEvent & { ignorable?: unknown }).ignorable !== true
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type EventKind =
|
|
44
|
+
| "message"
|
|
45
|
+
| "thinking"
|
|
46
|
+
| "turn"
|
|
47
|
+
| "tool"
|
|
48
|
+
| "request"
|
|
49
|
+
| "config"
|
|
50
|
+
| "audit"
|
|
51
|
+
| "lifecycle"
|
|
52
|
+
| "inbox"
|
|
53
|
+
| "compaction"
|
|
54
|
+
| "llm"
|
|
55
|
+
| "subagent"
|
|
56
|
+
| "team"
|
|
57
|
+
| "workflow"
|
|
58
|
+
| "goal"
|
|
59
|
+
| "schedule"
|
|
60
|
+
| "todo"
|
|
61
|
+
| "web";
|
|
62
|
+
|
|
63
|
+
export type EventRole = "user" | "assistant" | "tool";
|
|
64
|
+
|
|
65
|
+
export function eventKind(event: { type: string; data?: unknown }): EventKind | "" {
|
|
66
|
+
switch (event.type) {
|
|
67
|
+
case "user/message":
|
|
68
|
+
return "message";
|
|
69
|
+
case "assistant/message": {
|
|
70
|
+
const content = (event.data as { message?: { content?: unknown[] } }).message?.content;
|
|
71
|
+
if (content?.some((block) => (block as { type?: string })?.type === "reasoning")) {
|
|
72
|
+
return "thinking";
|
|
73
|
+
}
|
|
74
|
+
return "message";
|
|
75
|
+
}
|
|
76
|
+
case "turn/start":
|
|
77
|
+
case "turn/end":
|
|
78
|
+
case "step/start":
|
|
79
|
+
case "step/end":
|
|
80
|
+
case "session/end-seed":
|
|
81
|
+
return "turn";
|
|
82
|
+
case "tool/call":
|
|
83
|
+
case "tool/result":
|
|
84
|
+
case "tool/code-dispatch-start":
|
|
85
|
+
case "tool/code-dispatch":
|
|
86
|
+
return "tool";
|
|
87
|
+
case "request/header":
|
|
88
|
+
case "request/context":
|
|
89
|
+
return "request";
|
|
90
|
+
case "model/selection":
|
|
91
|
+
case "permission/preset":
|
|
92
|
+
case "approval/policy":
|
|
93
|
+
case "sandbox/mode":
|
|
94
|
+
case "plan/mode":
|
|
95
|
+
case "agent-preset/selected":
|
|
96
|
+
return "config";
|
|
97
|
+
case "approval/asked":
|
|
98
|
+
case "approval/decided":
|
|
99
|
+
case "command/run":
|
|
100
|
+
case "command/done":
|
|
101
|
+
case "hook/invoked":
|
|
102
|
+
case "hook/result":
|
|
103
|
+
case "feedback/record":
|
|
104
|
+
return "audit";
|
|
105
|
+
case "session/title":
|
|
106
|
+
case "session/title-llm-request":
|
|
107
|
+
case "session-log-deepseek/delivery-accepted":
|
|
108
|
+
return "lifecycle";
|
|
109
|
+
case "agent/inbox/spliced":
|
|
110
|
+
return "inbox";
|
|
111
|
+
case "compaction/start":
|
|
112
|
+
case "compaction/end":
|
|
113
|
+
case "compaction/summary":
|
|
114
|
+
case "compaction/prune":
|
|
115
|
+
return "compaction";
|
|
116
|
+
case "llm/retry":
|
|
117
|
+
case "llm/retry-started":
|
|
118
|
+
return "llm";
|
|
119
|
+
case "subagent/descriptor":
|
|
120
|
+
case "subagent/model-selection-policy":
|
|
121
|
+
return "subagent";
|
|
122
|
+
case "team/member":
|
|
123
|
+
case "team/task":
|
|
124
|
+
case "team/message/queued":
|
|
125
|
+
case "team/message/delivered":
|
|
126
|
+
return "team";
|
|
127
|
+
case "tool-workflow/run-start":
|
|
128
|
+
case "tool-workflow/run-end":
|
|
129
|
+
case "tool-workflow/agent-start":
|
|
130
|
+
case "tool-workflow/agent-end":
|
|
131
|
+
return "workflow";
|
|
132
|
+
case "goal/change":
|
|
133
|
+
return "goal";
|
|
134
|
+
case "schedule/change":
|
|
135
|
+
return "schedule";
|
|
136
|
+
case "todo/write":
|
|
137
|
+
return "todo";
|
|
138
|
+
case "web/deepseek-search-llm-request":
|
|
139
|
+
return "web";
|
|
140
|
+
default:
|
|
141
|
+
return ""; // unknown plugin-merged type
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function eventDimensions(event: SessionEvent): {
|
|
146
|
+
kind: string;
|
|
147
|
+
role: string;
|
|
148
|
+
name: string;
|
|
149
|
+
actionId: string;
|
|
150
|
+
} {
|
|
151
|
+
const kind = eventKind(event);
|
|
152
|
+
// 插件合并类型不在 core 的判别联合内,type/data 经结构化视图访问。
|
|
153
|
+
const type = event.type as string;
|
|
154
|
+
const data = event.data as Record<string, unknown>;
|
|
155
|
+
switch (type) {
|
|
156
|
+
case "user/message":
|
|
157
|
+
return { kind, role: "user", name: "", actionId: "" };
|
|
158
|
+
case "assistant/message":
|
|
159
|
+
return { kind, role: "assistant", name: "", actionId: "" };
|
|
160
|
+
case "tool/result": {
|
|
161
|
+
// append 可能携带旧形状(无 message);可选链容忍。
|
|
162
|
+
const message = data["message"] as { content?: Array<{ toolCallId?: string }> } | undefined;
|
|
163
|
+
return { kind, role: "tool", name: "", actionId: message?.content?.[0]?.toolCallId ?? "" };
|
|
164
|
+
}
|
|
165
|
+
case "tool/call":
|
|
166
|
+
return {
|
|
167
|
+
kind,
|
|
168
|
+
role: "",
|
|
169
|
+
name: typeof data["name"] === "string" ? data["name"] : "",
|
|
170
|
+
actionId: typeof data["callId"] === "string" ? data["callId"] : "",
|
|
171
|
+
};
|
|
172
|
+
case "tool/code-dispatch-start":
|
|
173
|
+
case "tool/code-dispatch":
|
|
174
|
+
return {
|
|
175
|
+
kind,
|
|
176
|
+
role: "",
|
|
177
|
+
name: "",
|
|
178
|
+
actionId: typeof data["subCallId"] === "string" ? data["subCallId"] : "",
|
|
179
|
+
};
|
|
180
|
+
case "command/run":
|
|
181
|
+
case "command/done":
|
|
182
|
+
return {
|
|
183
|
+
kind,
|
|
184
|
+
role: "",
|
|
185
|
+
name: type === "command/run" && typeof data["name"] === "string" ? data["name"] : "",
|
|
186
|
+
actionId: typeof data["commandId"] === "string" ? data["commandId"] : "",
|
|
187
|
+
};
|
|
188
|
+
case "approval/asked":
|
|
189
|
+
case "approval/decided":
|
|
190
|
+
return {
|
|
191
|
+
kind,
|
|
192
|
+
role: "",
|
|
193
|
+
name: "",
|
|
194
|
+
actionId: typeof data["id"] === "string" ? data["id"] : "",
|
|
195
|
+
};
|
|
196
|
+
case "hook/invoked":
|
|
197
|
+
case "hook/result":
|
|
198
|
+
return {
|
|
199
|
+
kind,
|
|
200
|
+
role: "",
|
|
201
|
+
name: "",
|
|
202
|
+
actionId: typeof data["handlerId"] === "string" ? data["handlerId"] : "",
|
|
203
|
+
};
|
|
204
|
+
case "llm/retry":
|
|
205
|
+
case "llm/retry-started":
|
|
206
|
+
return {
|
|
207
|
+
kind,
|
|
208
|
+
role: "",
|
|
209
|
+
name: "",
|
|
210
|
+
actionId: typeof data["retryId"] === "string" ? data["retryId"] : "",
|
|
211
|
+
};
|
|
212
|
+
case "tool-workflow/run-start":
|
|
213
|
+
case "tool-workflow/run-end":
|
|
214
|
+
case "tool-workflow/agent-start":
|
|
215
|
+
case "tool-workflow/agent-end":
|
|
216
|
+
return {
|
|
217
|
+
kind,
|
|
218
|
+
role: "",
|
|
219
|
+
name: "",
|
|
220
|
+
actionId: typeof data["runId"] === "string" ? data["runId"] : "",
|
|
221
|
+
};
|
|
222
|
+
case "todo/write":
|
|
223
|
+
return { kind, role: "", name: "todos", actionId: "" };
|
|
224
|
+
case "subagent/descriptor":
|
|
225
|
+
return {
|
|
226
|
+
kind,
|
|
227
|
+
role: "",
|
|
228
|
+
name: typeof data["label"] === "string" ? data["label"] : "",
|
|
229
|
+
actionId: "",
|
|
230
|
+
};
|
|
231
|
+
default:
|
|
232
|
+
return { kind, role: "", name: "", actionId: "" };
|
|
233
|
+
}
|
|
234
|
+
}
|