@morlay/session-rdb 0.0.19 → 0.0.21-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.
Files changed (70) hide show
  1. package/README.md +59 -31
  2. package/dist/artifact.d.mts +25 -65
  3. package/dist/artifact.mjs +3 -3
  4. package/dist/{schema-DPcuEh_a.d.mts → backend-DpdtxYpz.d.mts} +360 -107
  5. package/dist/branch-5JzX9rUq.mjs +390 -0
  6. package/dist/deletion.d.mts +7 -0
  7. package/dist/deletion.mjs +2 -0
  8. package/dist/dist-vIVO6bA-.mjs +1524 -0
  9. package/dist/import.d.mts +4 -4
  10. package/dist/import.mjs +214 -1
  11. package/dist/index.d.mts +2 -3
  12. package/dist/index.mjs +4 -1630
  13. package/dist/{log-DO69NQnn.mjs → log-CnYct2Dv.mjs} +37 -82
  14. package/dist/{sqlite-DYExtbLo.mjs → sqlite-fpvm5Dzs.mjs} +205 -75
  15. package/dist/src-CWTWV7vx.mjs +1904 -0
  16. package/dist/storage.d.mts +14 -5
  17. package/dist/storage.mjs +2 -2
  18. package/dist/testing.d.mts +2 -26
  19. package/dist/testing.mjs +10504 -9614
  20. package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +14 -0
  21. package/drizzle/postgres/20260918120000_v3_event_usage/snapshot.json +1309 -0
  22. package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +14 -0
  23. package/drizzle/sqlite/20260918120000_v3_event_usage/snapshot.json +1031 -0
  24. package/package.json +29 -30
  25. package/src/adapters/to-postgres.ts +1 -3
  26. package/src/adapters/to-sqlite.ts +0 -2
  27. package/src/adapters/types.ts +0 -3
  28. package/src/artifact.ts +0 -2
  29. package/src/backend.ts +31 -2
  30. package/src/branch.ts +223 -135
  31. package/src/deletion.ts +87 -0
  32. package/src/drizzle/postgres-v2.ts +0 -1
  33. package/src/drizzle/postgres-v3.ts +0 -1
  34. package/src/drizzle/sqlite-v2.ts +0 -1
  35. package/src/drizzle/sqlite-v3.ts +0 -1
  36. package/src/entities/v2/session-events.ts +0 -1
  37. package/src/entities/v3/event-usage.ts +25 -0
  38. package/src/entities/v3/events.ts +1 -3
  39. package/src/entities/v3/index.ts +4 -0
  40. package/src/entities/v3/session-events.ts +1 -2
  41. package/src/entities/v3/session-projcache-rows.ts +0 -8
  42. package/src/entities/v3/sessions.ts +3 -3
  43. package/src/entities/v3/storage-units.ts +0 -1
  44. package/src/entities/v3/workspace-sessions.ts +0 -5
  45. package/src/entities/v3/workspace-state.ts +0 -6
  46. package/src/entities/v3/workspaces.ts +0 -5
  47. package/src/export.ts +103 -0
  48. package/src/gc.ts +76 -0
  49. package/src/import-storages.ts +4 -37
  50. package/src/import.ts +54 -31
  51. package/src/index.ts +150 -114
  52. package/src/legacy.ts +4 -42
  53. package/src/log.ts +63 -106
  54. package/src/postgres.ts +249 -22
  55. package/src/schema.ts +6 -6
  56. package/src/session-query.ts +0 -4
  57. package/src/sqlite.ts +231 -55
  58. package/src/storage-takeover/index.ts +2 -28
  59. package/src/storage-takeover/projection-cache.ts +30 -134
  60. package/src/storage-takeover/repository.ts +20 -59
  61. package/src/storage-takeover/storage-backend.ts +3 -33
  62. package/src/storage-takeover/types.ts +14 -56
  63. package/src/storage.ts +0 -2
  64. package/src/testing/contract.ts +19 -50
  65. package/src/testing/coordinator-contract.ts +10 -25
  66. package/src/testing.ts +2 -2
  67. package/src/usage.ts +191 -0
  68. package/dist/import-Bc2QQa5G.mjs +0 -473
  69. package/dist/index-CCcK9tia.d.mts +0 -270
  70. package/dist/magic-string.es-BgJoa-3K.mjs +0 -1017
package/src/postgres.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { readdirSync } from "node:fs";
2
+ import { readdir } from "node:fs/promises";
3
3
  import { fileURLToPath } from "node:url";
4
- import { and, desc, eq, gte, sql } from "drizzle-orm";
4
+ import { and, desc, eq, gte, inArray, lt, notInArray, sql } from "drizzle-orm";
5
5
  import type { PgAsyncDatabase, PgAsyncTransaction } from "drizzle-orm/pg-core";
6
6
  import type { NodePgDatabase, NodePgQueryResultHKT } from "drizzle-orm/node-postgres";
7
7
  import { migrate } from "drizzle-orm/node-postgres/migrator";
@@ -16,13 +16,36 @@ import {
16
16
  } from "./backend.ts";
17
17
  import { toPostgresSchema } from "./adapters/index.ts";
18
18
  import { postgresTableDefs } from "./entities/index.ts";
19
- import { sessionConflictRow, sessionInsertRow, titleOfEventData } from "./log.ts";
19
+ import { sessionConflictRow, sessionInsertRow, titleOfEventData, usageRowOf } from "./log.ts";
20
20
  import { createStorageRepository } from "./storage-takeover/repository.ts";
21
21
  import type { StorageRepository } from "./storage-takeover/types.ts";
22
+ import type { UsageAggregate, UsageTotals } from "./usage.ts";
22
23
 
23
- /** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
24
24
  const postgresMigrationsDir = fileURLToPath(new URL("../drizzle/postgres/", import.meta.url));
25
25
 
26
+ /** pg 的 text 列窄化:非字符串(含 null)都不当作文本。 */
27
+ function text(value: unknown): string | null {
28
+ return typeof value === "string" ? value : null;
29
+ }
30
+
31
+ /** pg 的 count/sum 回落成字符串:统一转成有限数。 */
32
+ function numeric(value: unknown): number {
33
+ const parsed = typeof value === "number" ? value : Number(value ?? 0);
34
+ return Number.isFinite(parsed) ? parsed : 0;
35
+ }
36
+
37
+ /** SQL 的缺失求和是 NULL:没有 usage 字段的行使该字段计 0。 */
38
+ function totalsOfRow(row: Record<string, unknown>): UsageTotals {
39
+ return {
40
+ events: numeric(row["events"]),
41
+ inputTokens: numeric(row["input_tokens"]),
42
+ outputTokens: numeric(row["output_tokens"]),
43
+ cacheReadTokens: numeric(row["cache_read_tokens"]),
44
+ reasoningTokens: numeric(row["reasoning_tokens"]),
45
+ totalTokens: numeric(row["total_tokens"]),
46
+ };
47
+ }
48
+
26
49
  export interface PostgresBackendOptions {
27
50
  identityBase: string;
28
51
 
@@ -37,13 +60,12 @@ export class PostgresBackend implements Backend {
37
60
 
38
61
  private readonly tables: Record<string, any>;
39
62
 
40
- /** storages 接管表访问层;句柄在 open()(含迁移)完成后解析。 */
41
63
  readonly storage: StorageRepository;
42
64
 
43
65
  private readonly opened: Promise<void>;
44
66
  private resolveOpened!: () => void;
45
67
  private rejectOpened!: (error: unknown) => void;
46
- /** 事务期间的连接覆盖:storage 写方法经它加入当前事务。 */
68
+
47
69
  private txOverride: unknown;
48
70
 
49
71
  constructor(
@@ -55,14 +77,11 @@ export class PostgresBackend implements Backend {
55
77
  this.resolveOpened = resolve;
56
78
  this.rejectOpened = reject;
57
79
  });
58
- // 每个 storage 原语都会重新 await 同一 promise,失败仍逐一可见;这个
59
- // 守卫只防止 open 失败早于首次使用时的 unhandled rejection。
80
+
60
81
  this.opened.catch(() => {});
61
82
  this.storage = createStorageRepository({
62
83
  db: () => this.opened.then(() => this.txOverride ?? this.db),
63
- // storages 写事务用 serializable:workspace 域是整记录替换,多实例并发写
64
- // 同一记录在 read committed 下是「最后提交者赢」的静默覆盖;提升隔离级别
65
- // 后冲突以序列化失败暴露(与「并发写入 fail loud」一致),调用方重试即可。
84
+
66
85
  writeAtomically: (fn) =>
67
86
  this.db.transaction(
68
87
  async (tx) => {
@@ -92,8 +111,7 @@ export class PostgresBackend implements Backend {
92
111
 
93
112
  private async doOpen(): Promise<void> {
94
113
  const schema = this.options.schema ?? "public";
95
- // drizzle-kit 迁移:v2 baseline 由旧版本建表(首次打开时标记为已应用),
96
- // 本版本只执行 v3 diff(删 f_original_seq)。
114
+
97
115
  const qualifiedMeta = schema === "public" ? "t_schema_meta" : `"${schema}".t_schema_meta`;
98
116
  const probe = (await this.db.execute(
99
117
  sql`SELECT to_regclass(${qualifiedMeta}) IS NOT NULL AS exists`,
@@ -106,6 +124,7 @@ export class PostgresBackend implements Backend {
106
124
  }
107
125
  }
108
126
  await migrate(this.db, { migrationsFolder: postgresMigrationsDir });
127
+ await this.backfillEventUsage();
109
128
  const storeId = await this.db.transaction(async (tx) => {
110
129
  await tx
111
130
  .insert(this.tables["t_persistence_state"])
@@ -126,9 +145,8 @@ export class PostgresBackend implements Backend {
126
145
  this.storeIdentity = `${this.options.identityBase}:store:${storeId}`;
127
146
  }
128
147
 
129
- /** 标记 v2 baseline 已应用(迁移表 v1 结构:id/hash/created_at/name/applied_at)。 */
130
148
  private async baselineV2(): Promise<void> {
131
- const dir = readdirSync(postgresMigrationsDir).find((name) => name.endsWith("_v2_initial"));
149
+ const dir = (await readdir(postgresMigrationsDir)).find((name) => name.endsWith("_v2_initial"));
132
150
  if (dir === undefined) throw new Error("missing v2 baseline migration in drizzle/postgres");
133
151
  await this.db.execute(sql`
134
152
  CREATE SCHEMA IF NOT EXISTS drizzle
@@ -176,6 +194,28 @@ export class PostgresBackend implements Backend {
176
194
  .execute() as unknown as EventRow[];
177
195
  }
178
196
 
197
+ async getEventTypeAt(id: SessionId, sequence: number): Promise<string | undefined> {
198
+ const table = this.tables["t_session_events"];
199
+ const rows = (await this.eventRows(this.db)
200
+ .where(and(eq(table.fSessionId, id), eq(table.fSequence, sequence)))
201
+ .limit(1)
202
+ .execute()) as Array<{ fType?: string }>;
203
+ return rows[0]?.fType;
204
+ }
205
+
206
+ async getEventTypesBefore(
207
+ id: SessionId,
208
+ beforeSequence: number,
209
+ limit: number,
210
+ ): Promise<Array<Pick<EventRow, "fSequence" | "fType">>> {
211
+ const table = this.tables["t_session_events"];
212
+ return (await this.eventRows(this.db)
213
+ .where(and(eq(table.fSessionId, id), lt(table.fSequence, beforeSequence)))
214
+ .orderBy(desc(table.fSequence))
215
+ .limit(limit)
216
+ .execute()) as unknown as Array<Pick<EventRow, "fSequence" | "fType">>;
217
+ }
218
+
179
219
  async listSessions(): Promise<SessionRow[]> {
180
220
  return this.db.select().from(this.tables["t_sessions"]).execute() as unknown as Promise<
181
221
  SessionRow[]
@@ -200,11 +240,11 @@ export class PostgresBackend implements Backend {
200
240
  refreshTitle: (id) => this.refreshTitle(tx, id),
201
241
  deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(tx, id, fromSequence),
202
242
  getPrevBridge: (id, sequence) => this.getPrevBridge(tx, id, sequence),
243
+ deleteSession: (id) => this.deleteSession(tx, id),
244
+ deleteSessions: (ids) => this.deleteSessions(tx, ids),
203
245
  };
204
246
  }
205
247
 
206
- // --- meta helpers ---
207
-
208
248
  private async readMeta(
209
249
  exec: PgAsyncDatabase<NodePgQueryResultHKT>,
210
250
  key: string,
@@ -217,8 +257,6 @@ export class PostgresBackend implements Backend {
217
257
  return rows[0]?.fValue;
218
258
  }
219
259
 
220
- // --- row primitives (transaction-internal) ---
221
-
222
260
  private async upsertSession(
223
261
  exec: PgAsyncDatabase<NodePgQueryResultHKT>,
224
262
  storage: SessionStorageMetadata,
@@ -248,7 +286,7 @@ export class PostgresBackend implements Backend {
248
286
  .where(eq(this.tables["t_sessions"].fSessionId, id))
249
287
  .execute()
250
288
  )[0] as Pick<SessionRow, "fHeadEventId" | "fHeadSequence"> | undefined;
251
- /* v8 ignore next -- appendBatch/commitRepair always materialize the row before reading the head */
289
+
252
290
  if (head === undefined) throw new Error(`session "${id}" has no materialized row`);
253
291
  return head;
254
292
  }
@@ -264,7 +302,7 @@ export class PostgresBackend implements Backend {
264
302
  .where(eq(this.tables["t_sessions"].fSessionId, id))
265
303
  .execute()
266
304
  )[0] as { fSeedLength: number | null } | undefined;
267
- /* v8 ignore next -- rewind always materializes the row before reading the seed length */
305
+
268
306
  if (row === undefined) throw new Error(`session "${id}" has no materialized row`);
269
307
  return row.fSeedLength;
270
308
  }
@@ -296,6 +334,14 @@ export class PostgresBackend implements Backend {
296
334
  )
297
335
  .execute();
298
336
  }
337
+ // 用量顺带落 t_event_usage:统计不再逐行解析事件 JSON。
338
+ const usage = events.flatMap((event) => {
339
+ const row = usageRowOf(event);
340
+ return row === undefined ? [] : [row];
341
+ });
342
+ if (usage.length > 0) {
343
+ await exec.insert(this.tables["t_event_usage"]).values(usage).onConflictDoNothing().execute();
344
+ }
299
345
  }
300
346
 
301
347
  private async insertBridges(
@@ -340,7 +386,6 @@ export class PostgresBackend implements Backend {
340
386
  .execute();
341
387
  }
342
388
 
343
- /** 从事件表重算标题(最后一条 `session/title`),写回会话行的 f_title 列。 */
344
389
  private async refreshTitle(
345
390
  exec: PgAsyncDatabase<NodePgQueryResultHKT>,
346
391
  id: SessionId,
@@ -381,6 +426,188 @@ export class PostgresBackend implements Backend {
381
426
  .execute();
382
427
  }
383
428
 
429
+ private async deleteSession(
430
+ exec: PgAsyncDatabase<NodePgQueryResultHKT>,
431
+ id: SessionId,
432
+ ): Promise<void> {
433
+ await exec
434
+ .delete(this.tables["t_session_events"])
435
+ .where(eq(this.tables["t_session_events"].fSessionId, id))
436
+ .execute();
437
+ await exec
438
+ .delete(this.tables["t_workspace_sessions"])
439
+ .where(eq(this.tables["t_workspace_sessions"].fSessionId, id))
440
+ .execute();
441
+ await exec
442
+ .delete(this.tables["t_session_projcache_row"])
443
+ .where(eq(this.tables["t_session_projcache_row"].fSessionId, id))
444
+ .execute();
445
+ await exec
446
+ .delete(this.tables["t_sessions"])
447
+ .where(eq(this.tables["t_sessions"].fSessionId, id))
448
+ .execute();
449
+ }
450
+
451
+ /** 事件行可能被多个会话共享(fork 派生),所以孤儿只能在全库范围内判定。 */
452
+ async collectOrphans(): Promise<number> {
453
+ const tEvents = this.tables["t_events"];
454
+ const tEventUsage = this.tables["t_event_usage"];
455
+ const tSessionEvents = this.tables["t_session_events"];
456
+ const result = (await this.db
457
+ .delete(tEvents)
458
+ .where(
459
+ notInArray(
460
+ tEvents.fEventId,
461
+ this.db.select({ fEventId: tSessionEvents.fEventId }).from(tSessionEvents),
462
+ ),
463
+ )
464
+ .execute()) as unknown as { rowCount?: number | null };
465
+ // 用量行跟着事件行走:没有事件行的用量不再计入统计。
466
+ await this.db
467
+ .delete(tEventUsage)
468
+ .where(
469
+ notInArray(
470
+ tEventUsage.fEventId,
471
+ this.db.select({ fEventId: tEvents.fEventId }).from(tEvents),
472
+ ),
473
+ )
474
+ .execute();
475
+ return result.rowCount ?? 0;
476
+ }
477
+
478
+ /** 父会话被删后留下的 subagent 会话:父已不在表里,或本来就没有父。 */
479
+ async listOrphanSubagentSessions(): Promise<SessionId[]> {
480
+ const tSessions = this.tables["t_sessions"];
481
+ const result = (await this.db.execute(sql`
482
+ SELECT s.f_session_id AS id FROM ${tSessions} AS s
483
+ WHERE s.f_origin = 'subagent'
484
+ AND (s.f_parent_session IS NULL
485
+ OR s.f_parent_session NOT IN (SELECT p.f_session_id FROM ${tSessions} AS p))
486
+ `)) as unknown as { rows: Array<{ id: string }> };
487
+ return result.rows.map((row) => row.id as SessionId);
488
+ }
489
+
490
+ private async deleteSessions(
491
+ exec: PgAsyncDatabase<NodePgQueryResultHKT>,
492
+ ids: SessionId[],
493
+ ): Promise<number> {
494
+ if (ids.length === 0) return 0;
495
+ await exec
496
+ .delete(this.tables["t_session_events"])
497
+ .where(inArray(this.tables["t_session_events"].fSessionId, ids))
498
+ .execute();
499
+ await exec
500
+ .delete(this.tables["t_workspace_sessions"])
501
+ .where(inArray(this.tables["t_workspace_sessions"].fSessionId, ids))
502
+ .execute();
503
+ await exec
504
+ .delete(this.tables["t_session_projcache_row"])
505
+ .where(inArray(this.tables["t_session_projcache_row"].fSessionId, ids))
506
+ .execute();
507
+ const result = (await exec
508
+ .delete(this.tables["t_sessions"])
509
+ .where(inArray(this.tables["t_sessions"].fSessionId, ids))
510
+ .execute()) as unknown as { rowCount?: number | null };
511
+ return result.rowCount ?? 0;
512
+ }
513
+
514
+ async vacuum(): Promise<void> {
515
+ await this.db.execute(sql`VACUUM ANALYZE`);
516
+ }
517
+
518
+ /** 一次性回填:旧库没写过 `t_event_usage`,建表后按事件行补一次(表非空即跳过)。 */
519
+ private async backfillEventUsage(): Promise<void> {
520
+ const tEventUsage = this.tables["t_event_usage"];
521
+ const tEvents = this.tables["t_events"];
522
+ const existing = (await this.db.execute(
523
+ sql`SELECT count(*) AS n FROM ${tEventUsage}`,
524
+ )) as unknown as { rows: Array<{ n: string }> };
525
+ if (Number(existing.rows[0]?.["n"] ?? 0) > 0) return;
526
+ await this.db.execute(sql`
527
+ INSERT INTO ${tEventUsage} (
528
+ f_event_id, f_created_at, f_provider, f_model,
529
+ f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
530
+ SELECT e.f_event_id, e.f_created_at,
531
+ coalesce(e.f_data::json #>> '{data,message,source,provider}',
532
+ e.f_data::json #>> '{message,source,provider}'),
533
+ coalesce(e.f_data::json #>> '{data,message,source,model}',
534
+ e.f_data::json #>> '{message,source,model}'),
535
+ coalesce((e.f_data::json #>> '{data,usage,inputTokens}')::integer,
536
+ (e.f_data::json #>> '{usage,inputTokens}')::integer, 0),
537
+ coalesce((e.f_data::json #>> '{data,usage,outputTokens}')::integer,
538
+ (e.f_data::json #>> '{usage,outputTokens}')::integer, 0),
539
+ coalesce((e.f_data::json #>> '{data,usage,cacheReadTokens}')::integer,
540
+ (e.f_data::json #>> '{usage,cacheReadTokens}')::integer, 0),
541
+ coalesce((e.f_data::json #>> '{data,usage,reasoningTokens}')::integer,
542
+ (e.f_data::json #>> '{usage,reasoningTokens}')::integer, 0),
543
+ coalesce((e.f_data::json #>> '{data,usage,totalTokens}')::integer,
544
+ (e.f_data::json #>> '{usage,totalTokens}')::integer, 0)
545
+ FROM ${tEvents} e
546
+ WHERE e.f_type = 'assistant/message'
547
+ AND (e.f_data::json #> '{data,usage}' IS NOT NULL
548
+ OR e.f_data::json #> '{usage}' IS NOT NULL)
549
+ ON CONFLICT DO NOTHING
550
+ `);
551
+ }
552
+
553
+ /** 用量聚合:与 SQLite 侧同形,读 `t_event_usage`,数值列回来是字符串。 */
554
+ async usageReport(sinceMs?: number): Promise<UsageAggregate> {
555
+ const sinceClause = sinceMs === undefined ? sql`` : sql` AND u.f_created_at >= ${sinceMs}`;
556
+ const tEventUsage = this.tables["t_event_usage"];
557
+ const tSessionEvents = this.tables["t_session_events"];
558
+ const tSessions = this.tables["t_sessions"];
559
+ const buckets = (await this.db.execute(sql`
560
+ SELECT to_char(to_timestamp(u.f_created_at / 1000.0), 'YYYY-MM-DD') AS day,
561
+ u.f_provider AS provider,
562
+ u.f_model AS model,
563
+ EXISTS (SELECT 1 FROM ${tSessionEvents} sb
564
+ JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
565
+ WHERE sb.f_event_id = u.f_event_id AND ss.f_origin = 'subagent') AS subagent,
566
+ count(*) AS events,
567
+ sum(u.f_input_tokens) AS input_tokens,
568
+ sum(u.f_output_tokens) AS output_tokens,
569
+ sum(u.f_cache_read_tokens) AS cache_read_tokens,
570
+ sum(u.f_reasoning_tokens) AS reasoning_tokens,
571
+ sum(u.f_total_tokens) AS total_tokens
572
+ FROM ${tEventUsage} u
573
+ WHERE EXISTS (SELECT 1 FROM ${tSessionEvents} rb WHERE rb.f_event_id = u.f_event_id)${sinceClause}
574
+ GROUP BY 1, 2, 3, 4
575
+ `)) as unknown as { rows: Array<Record<string, unknown>> };
576
+ const sessions = (await this.db.execute(sql`
577
+ SELECT b.f_session_id AS session_id,
578
+ s.f_title AS title,
579
+ (s.f_origin = 'subagent') AS subagent,
580
+ (s.f_archived_at IS NOT NULL) AS archived,
581
+ count(*) AS events,
582
+ sum(u.f_input_tokens) AS input_tokens,
583
+ sum(u.f_output_tokens) AS output_tokens,
584
+ sum(u.f_cache_read_tokens) AS cache_read_tokens,
585
+ sum(u.f_reasoning_tokens) AS reasoning_tokens,
586
+ sum(u.f_total_tokens) AS total_tokens
587
+ FROM ${tSessionEvents} b
588
+ JOIN ${tEventUsage} u ON u.f_event_id = b.f_event_id
589
+ JOIN ${tSessions} s ON s.f_session_id = b.f_session_id
590
+ WHERE 1 = 1${sinceClause}
591
+ GROUP BY b.f_session_id, s.f_title, s.f_origin, s.f_archived_at
592
+ `)) as unknown as { rows: Array<Record<string, unknown>> };
593
+ return {
594
+ buckets: buckets.rows.map((row) => ({
595
+ day: String(row["day"]),
596
+ provider: text(row["provider"]),
597
+ model: text(row["model"]),
598
+ subagent: row["subagent"] === true,
599
+ ...totalsOfRow(row),
600
+ })),
601
+ sessions: sessions.rows.map((row) => ({
602
+ sessionId: String(row["session_id"]),
603
+ title: text(row["title"]),
604
+ subagent: row["subagent"] === true,
605
+ archived: row["archived"] === true,
606
+ ...totalsOfRow(row),
607
+ })),
608
+ };
609
+ }
610
+
384
611
  private async getPrevBridge(
385
612
  exec: PgAsyncDatabase<NodePgQueryResultHKT>,
386
613
  id: SessionId,
package/src/schema.ts CHANGED
@@ -8,7 +8,6 @@ export const SESSION_PERSISTENCE_SQLITE_APPLICATION_ID = 0x44534850;
8
8
 
9
9
  export const EVENT_ENCODING = "json";
10
10
 
11
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
11
  const sqliteTables: Record<string, any> = toSqliteSchema(sqliteTableDefs);
13
12
 
14
13
  export const tPersistenceState = sqliteTables["t_persistence_state"]!;
@@ -31,6 +30,8 @@ export const tWorkspaceState = sqliteTables["t_workspace_state"]!;
31
30
 
32
31
  export const tSessionProjcacheRows = sqliteTables["t_session_projcache_row"]!;
33
32
 
33
+ export const tEventUsage = sqliteTables["t_event_usage"]!;
34
+
34
35
  export type { SessionRow } from "./backend.ts";
35
36
 
36
37
  export type { EventRow } from "./backend.ts";
@@ -82,7 +83,7 @@ export function eventKind(event: { type: string; data?: unknown }): EventKind |
82
83
  case "tool/result":
83
84
  case "tool/ptc-dispatch-start":
84
85
  case "tool/ptc-dispatch":
85
- case "tool/code-dispatch-start": // v2 旧名(读路径归一为 ptc)
86
+ case "tool/code-dispatch-start":
86
87
  case "tool/code-dispatch":
87
88
  return "tool";
88
89
  case "request/header":
@@ -139,7 +140,7 @@ export function eventKind(event: { type: string; data?: unknown }): EventKind |
139
140
  case "web/deepseek-search-llm-request":
140
141
  return "web";
141
142
  default:
142
- return ""; // unknown plugin-merged type
143
+ return "";
143
144
  }
144
145
  }
145
146
 
@@ -150,7 +151,7 @@ export function eventDimensions(event: SessionEvent): {
150
151
  actionId: string;
151
152
  } {
152
153
  const kind = eventKind(event);
153
- // 插件合并类型不在 core 的判别联合内,type/data 经结构化视图访问。
154
+
154
155
  const type = event.type as string;
155
156
  const data = event.data as Record<string, unknown>;
156
157
  switch (type) {
@@ -159,7 +160,6 @@ export function eventDimensions(event: SessionEvent): {
159
160
  case "assistant/message":
160
161
  return { kind, role: "assistant", name: "", actionId: "" };
161
162
  case "tool/result": {
162
- // append 可能携带旧形状(无 message);可选链容忍。
163
163
  const message = data["message"] as { content?: Array<{ toolCallId?: string }> } | undefined;
164
164
  return { kind, role: "tool", name: "", actionId: message?.content?.[0]?.toolCallId ?? "" };
165
165
  }
@@ -172,7 +172,7 @@ export function eventDimensions(event: SessionEvent): {
172
172
  };
173
173
  case "tool/ptc-dispatch-start":
174
174
  case "tool/ptc-dispatch":
175
- case "tool/code-dispatch-start": // v2 旧名(读路径归一为 ptc)
175
+ case "tool/code-dispatch-start":
176
176
  case "tool/code-dispatch":
177
177
  return {
178
178
  kind,
@@ -1,10 +1,6 @@
1
1
  import type { Context } from "@deepseek-ai/cordis";
2
2
  import { SessionQueryEngine, SessionQueryError, type Config } from "@deepseek-ai/dsh-session-query";
3
3
 
4
- // 会话查询服务的 rdb 实现:精确读 / 过滤 / 血缘全部复用上游基类(数据面走
5
- // ctx.sessions 与我们的 ctx.sessionPersistence),这里只接管服务所有权。
6
- // 全文检索与官方 `openAt: never` 装配保持一致——直接拒绝,不引入 FTS 索引、
7
- // node:sqlite 或派生库,搜索语义留待显式实现(见 ADR 0006)。
8
4
  export class SessionQueryRdb extends SessionQueryEngine {
9
5
  constructor(ctx: Context, config: Config = {}) {
10
6
  super(ctx, config);