@morlay/session-rdb 0.0.21-alpha.3 → 0.0.21-alpha.4
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 +1 -1
- package/dist/artifact.d.mts +1 -1
- package/dist/artifact.mjs +1 -1
- package/dist/{backend-DpdtxYpz.d.mts → backend-a0xDrUFG.d.mts} +58 -9
- package/dist/{branch-5JzX9rUq.mjs → branch-Dlmbkfjj.mjs} +4 -12
- package/dist/deletion.d.mts +1 -1
- package/dist/deletion.mjs +1 -1
- package/dist/import.d.mts +1 -1
- package/dist/import.mjs +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +3 -3
- package/dist/{log-CnYct2Dv.mjs → log-C-rbdFwK.mjs} +32 -4
- package/dist/{sqlite-fpvm5Dzs.mjs → sqlite-C3LHLzYW.mjs} +282 -23
- package/dist/{src-CWTWV7vx.mjs → src-Bn0038Sp.mjs} +269 -165
- package/dist/storage.d.mts +11 -2
- package/dist/storage.mjs +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +11 -11
- package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +1 -1
- package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +1 -1
- package/package.json +2 -2
- package/src/backend.ts +26 -1
- package/src/branch.ts +5 -13
- package/src/entities/v3/event-usage.ts +2 -1
- package/src/index.ts +132 -33
- package/src/log.ts +36 -19
- package/src/postgres.ts +232 -38
- package/src/sqlite.ts +204 -25
- package/src/usage.ts +96 -32
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { _ as toJsonlArtifact, d as rowToMeta, f as scanRows, g as titleOfEventData, m as sessionInsertRow, p as sessionConflictRow, s as repairReadView, v as usageRowOf } from "./log-
|
|
3
|
-
import { c as isLegacyVersion, o as adoptLegacyRows, s as convertLegacyRows, t as SessionBranchRdb } from "./branch-
|
|
1
|
+
import { A as WriteGuard, O as postgresTableDefs, a as addActivityCount, c as emptyTotals, d as DEFAULT_BUSY_TIMEOUT_MS, f as EVENT_ENCODING, h as eventDimensions, i as COUNTED_EVENT_TYPES, k as toPostgresSchema, l as localDayKey, o as addTokenTotals, r as createStorageRepository, s as addTotals, t as SqliteBackend, u as registerSessionUsage } from "./sqlite-C3LHLzYW.mjs";
|
|
2
|
+
import { _ as toJsonlArtifact, d as rowToMeta, f as scanRows, g as titleOfEventData, m as sessionInsertRow, p as sessionConflictRow, s as repairReadView, v as usageRowOf } from "./log-C-rbdFwK.mjs";
|
|
3
|
+
import { c as isLegacyVersion, o as adoptLegacyRows, s as convertLegacyRows, t as SessionBranchRdb } from "./branch-Dlmbkfjj.mjs";
|
|
4
4
|
import { registerSessionImport } from "./import.mjs";
|
|
5
5
|
import z from "@deepseek-ai/schemastery";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
@@ -20,6 +20,13 @@ import { StorageError, storageBackendServiceKey } from "@deepseek-ai/dsh-storage
|
|
|
20
20
|
import { Service } from "@deepseek-ai/cordis";
|
|
21
21
|
//#region src/postgres.ts
|
|
22
22
|
const postgresMigrationsDir = fileURLToPath(new URL("../drizzle/postgres/", import.meta.url));
|
|
23
|
+
const pgWriteQueues = /* @__PURE__ */ new Map();
|
|
24
|
+
/** 按介质(连接串 + schema)串行化写事务,与 SQLite 侧的 `enqueueSqliteTx` 同形。 */
|
|
25
|
+
function enqueuePgWrite(key, fn) {
|
|
26
|
+
const run = (pgWriteQueues.get(key) ?? Promise.resolve()).then(fn);
|
|
27
|
+
pgWriteQueues.set(key, run.then(() => void 0, () => void 0));
|
|
28
|
+
return run;
|
|
29
|
+
}
|
|
23
30
|
/** pg 的 text 列窄化:非字符串(含 null)都不当作文本。 */
|
|
24
31
|
function text(value) {
|
|
25
32
|
return typeof value === "string" ? value : null;
|
|
@@ -30,9 +37,8 @@ function numeric(value) {
|
|
|
30
37
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
31
38
|
}
|
|
32
39
|
/** SQL 的缺失求和是 NULL:没有 usage 字段的行使该字段计 0。 */
|
|
33
|
-
function
|
|
40
|
+
function tokenTotalsOf(row) {
|
|
34
41
|
return {
|
|
35
|
-
events: numeric(row["events"]),
|
|
36
42
|
inputTokens: numeric(row["input_tokens"]),
|
|
37
43
|
outputTokens: numeric(row["output_tokens"]),
|
|
38
44
|
cacheReadTokens: numeric(row["cache_read_tokens"]),
|
|
@@ -40,6 +46,16 @@ function totalsOfRow(row) {
|
|
|
40
46
|
totalTokens: numeric(row["total_tokens"])
|
|
41
47
|
};
|
|
42
48
|
}
|
|
49
|
+
function emptyActivity() {
|
|
50
|
+
return {
|
|
51
|
+
turns: 0,
|
|
52
|
+
steps: 0,
|
|
53
|
+
userInputs: 0,
|
|
54
|
+
toolCalls: 0
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。 */
|
|
58
|
+
const COUNTED_EVENT_TYPE_SQL = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
|
|
43
59
|
var PostgresBackend = class PostgresBackend {
|
|
44
60
|
db;
|
|
45
61
|
options;
|
|
@@ -60,9 +76,10 @@ var PostgresBackend = class PostgresBackend {
|
|
|
60
76
|
this.rejectOpened = reject;
|
|
61
77
|
});
|
|
62
78
|
this.opened.catch(() => {});
|
|
79
|
+
const writeQueueKey = options.identityBase;
|
|
63
80
|
this.storage = createStorageRepository({
|
|
64
81
|
db: () => this.opened.then(() => this.txOverride ?? this.db),
|
|
65
|
-
writeAtomically: (fn) => this.db.transaction(async (tx) => {
|
|
82
|
+
writeAtomically: (fn) => enqueuePgWrite(writeQueueKey, () => this.db.transaction(async (tx) => {
|
|
66
83
|
const previous = this.txOverride;
|
|
67
84
|
this.txOverride = tx;
|
|
68
85
|
try {
|
|
@@ -70,7 +87,7 @@ var PostgresBackend = class PostgresBackend {
|
|
|
70
87
|
} finally {
|
|
71
88
|
this.txOverride = previous;
|
|
72
89
|
}
|
|
73
|
-
}, { isolationLevel: "serializable" }),
|
|
90
|
+
}, { isolationLevel: "serializable" })),
|
|
74
91
|
tables: this.tables
|
|
75
92
|
});
|
|
76
93
|
}
|
|
@@ -90,6 +107,8 @@ var PostgresBackend = class PostgresBackend {
|
|
|
90
107
|
if (await this.readMeta(this.db, "schema_version") === "2") await this.baselineV2();
|
|
91
108
|
}
|
|
92
109
|
await migrate(this.db, { migrationsFolder: postgresMigrationsDir });
|
|
110
|
+
await this.ensureEventCountsTable();
|
|
111
|
+
await this.backfillEventCounts();
|
|
93
112
|
await this.backfillEventUsage();
|
|
94
113
|
const storeId = await this.db.transaction(async (tx) => {
|
|
95
114
|
await tx.insert(this.tables["t_persistence_state"]).values({
|
|
@@ -130,11 +149,14 @@ var PostgresBackend = class PostgresBackend {
|
|
|
130
149
|
}
|
|
131
150
|
async getEventTypeAt(id, sequence) {
|
|
132
151
|
const table = this.tables["t_session_events"];
|
|
133
|
-
return (await this.
|
|
152
|
+
return (await this.db.select({ fType: this.tables["t_events"].fType }).from(table).innerJoin(this.tables["t_events"], eq(table.fEventId, this.tables["t_events"].fEventId)).where(and(eq(table.fSessionId, id), eq(table.fSequence, sequence))).limit(1).execute())[0]?.fType;
|
|
134
153
|
}
|
|
135
154
|
async getEventTypesBefore(id, beforeSequence, limit) {
|
|
136
155
|
const table = this.tables["t_session_events"];
|
|
137
|
-
return await this.
|
|
156
|
+
return await this.db.select({
|
|
157
|
+
fSequence: table.fSequence,
|
|
158
|
+
fType: this.tables["t_events"].fType
|
|
159
|
+
}).from(table).innerJoin(this.tables["t_events"], eq(table.fEventId, this.tables["t_events"].fEventId)).where(and(eq(table.fSessionId, id), lt(table.fSequence, beforeSequence))).orderBy(desc(table.fSequence)).limit(limit).execute();
|
|
138
160
|
}
|
|
139
161
|
async listSessions() {
|
|
140
162
|
return this.db.select().from(this.tables["t_sessions"]).execute();
|
|
@@ -292,61 +314,176 @@ var PostgresBackend = class PostgresBackend {
|
|
|
292
314
|
}
|
|
293
315
|
/** 用量聚合:与 SQLite 侧同形,读 `t_event_usage`,数值列回来是字符串。 */
|
|
294
316
|
async usageReport(sinceMs) {
|
|
295
|
-
const
|
|
317
|
+
const usageSince = sinceMs === void 0 ? sql`` : sql` AND u.f_created_at >= ${sinceMs}`;
|
|
318
|
+
const daySince = sinceMs === void 0 ? sql`` : sql` AND c.f_day >= ${localDayKey(sinceMs)}`;
|
|
296
319
|
const tEventUsage = this.tables["t_event_usage"];
|
|
297
320
|
const tSessionEvents = this.tables["t_session_events"];
|
|
298
321
|
const tSessions = this.tables["t_sessions"];
|
|
322
|
+
const eventCountsTable = this.qualifiedTable("t_event_counts");
|
|
323
|
+
const subagentOfUsage = sql`EXISTS (SELECT 1 FROM ${tSessionEvents} sb
|
|
324
|
+
JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
|
|
325
|
+
WHERE sb.f_event_id = u.f_event_id AND ss.f_origin = 'subagent')`;
|
|
326
|
+
const subagentOfCounts = sql`EXISTS (SELECT 1 FROM ${tSessionEvents} sb
|
|
327
|
+
JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
|
|
328
|
+
WHERE sb.f_session_id = c.f_session_id AND ss.f_origin = 'subagent')`;
|
|
329
|
+
const hasUsageSession = (column) => sql`EXISTS (SELECT 1 FROM ${tSessionEvents} ub
|
|
330
|
+
JOIN ${tEventUsage} uu ON uu.f_event_id = ub.f_event_id
|
|
331
|
+
WHERE ub.f_session_id = ${sql.raw(column)})`;
|
|
332
|
+
const tokenColumns = sql.raw(`sum(u.f_input_tokens) AS input_tokens,
|
|
333
|
+
sum(u.f_output_tokens) AS output_tokens,
|
|
334
|
+
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
335
|
+
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
336
|
+
sum(u.f_total_tokens) AS total_tokens`);
|
|
299
337
|
const buckets = await this.db.execute(sql`
|
|
300
338
|
SELECT to_char(to_timestamp(u.f_created_at / 1000.0), 'YYYY-MM-DD') AS day,
|
|
301
339
|
u.f_provider AS provider,
|
|
302
340
|
u.f_model AS model,
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
WHERE sb.f_event_id = u.f_event_id AND ss.f_origin = 'subagent') AS subagent,
|
|
306
|
-
count(*) AS events,
|
|
307
|
-
sum(u.f_input_tokens) AS input_tokens,
|
|
308
|
-
sum(u.f_output_tokens) AS output_tokens,
|
|
309
|
-
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
310
|
-
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
311
|
-
sum(u.f_total_tokens) AS total_tokens
|
|
341
|
+
${subagentOfUsage} AS subagent,
|
|
342
|
+
${tokenColumns}
|
|
312
343
|
FROM ${tEventUsage} u
|
|
313
|
-
WHERE EXISTS (SELECT 1 FROM ${tSessionEvents} rb WHERE rb.f_event_id = u.f_event_id)${
|
|
344
|
+
WHERE EXISTS (SELECT 1 FROM ${tSessionEvents} rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
|
|
314
345
|
GROUP BY 1, 2, 3, 4
|
|
315
346
|
`);
|
|
316
|
-
const
|
|
347
|
+
const tokenScope = await this.db.execute(sql`
|
|
348
|
+
SELECT ${subagentOfUsage} AS subagent,
|
|
349
|
+
${tokenColumns}
|
|
350
|
+
FROM ${tEventUsage} u
|
|
351
|
+
WHERE EXISTS (SELECT 1 FROM ${tSessionEvents} rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
|
|
352
|
+
GROUP BY 1
|
|
353
|
+
`);
|
|
354
|
+
const activityScope = await this.db.execute(sql`
|
|
355
|
+
SELECT ${subagentOfCounts} AS subagent,
|
|
356
|
+
c.f_type AS f_type,
|
|
357
|
+
sum(c.f_count) AS n
|
|
358
|
+
FROM ${sql.raw(eventCountsTable)} c
|
|
359
|
+
JOIN ${tSessions} s ON s.f_session_id = c.f_session_id
|
|
360
|
+
WHERE ${hasUsageSession("c.f_session_id")}${daySince}
|
|
361
|
+
GROUP BY 1, 2
|
|
362
|
+
`);
|
|
363
|
+
const tokenSessions = await this.db.execute(sql`
|
|
317
364
|
SELECT b.f_session_id AS session_id,
|
|
318
365
|
s.f_title AS title,
|
|
319
366
|
(s.f_origin = 'subagent') AS subagent,
|
|
320
367
|
(s.f_archived_at IS NOT NULL) AS archived,
|
|
321
|
-
|
|
322
|
-
sum(u.f_input_tokens) AS input_tokens,
|
|
323
|
-
sum(u.f_output_tokens) AS output_tokens,
|
|
324
|
-
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
325
|
-
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
326
|
-
sum(u.f_total_tokens) AS total_tokens
|
|
368
|
+
${tokenColumns}
|
|
327
369
|
FROM ${tSessionEvents} b
|
|
328
370
|
JOIN ${tEventUsage} u ON u.f_event_id = b.f_event_id
|
|
329
371
|
JOIN ${tSessions} s ON s.f_session_id = b.f_session_id
|
|
330
|
-
WHERE 1 = 1${
|
|
372
|
+
WHERE 1 = 1${usageSince}
|
|
331
373
|
GROUP BY b.f_session_id, s.f_title, s.f_origin, s.f_archived_at
|
|
332
374
|
`);
|
|
375
|
+
const activitySessions = await this.db.execute(sql`
|
|
376
|
+
SELECT c.f_session_id AS session_id,
|
|
377
|
+
c.f_type AS f_type,
|
|
378
|
+
sum(c.f_count) AS n
|
|
379
|
+
FROM ${sql.raw(eventCountsTable)} c
|
|
380
|
+
WHERE ${hasUsageSession("c.f_session_id")}${daySince}
|
|
381
|
+
GROUP BY c.f_session_id, c.f_type
|
|
382
|
+
`);
|
|
383
|
+
const human = emptyTotals();
|
|
384
|
+
const subagent = emptyTotals();
|
|
385
|
+
for (const row of tokenScope.rows) addTokenTotals(row["subagent"] === true ? subagent : human, tokenTotalsOf(row));
|
|
386
|
+
for (const row of activityScope.rows) addActivityCount(row["subagent"] === true ? subagent : human, String(row["f_type"]), numeric(row["n"]));
|
|
387
|
+
const totals = addTotals(addTotals(emptyTotals(), human), subagent);
|
|
388
|
+
const activityBySession = /* @__PURE__ */ new Map();
|
|
389
|
+
for (const row of activitySessions.rows) {
|
|
390
|
+
const key = String(row["session_id"]);
|
|
391
|
+
const target = activityBySession.get(key) ?? emptyActivity();
|
|
392
|
+
addActivityCount(target, String(row["f_type"]), numeric(row["n"]));
|
|
393
|
+
activityBySession.set(key, target);
|
|
394
|
+
}
|
|
333
395
|
return {
|
|
396
|
+
totals,
|
|
397
|
+
subagent,
|
|
398
|
+
human,
|
|
334
399
|
buckets: buckets.rows.map((row) => ({
|
|
335
400
|
day: String(row["day"]),
|
|
336
401
|
provider: text(row["provider"]),
|
|
337
402
|
model: text(row["model"]),
|
|
338
403
|
subagent: row["subagent"] === true,
|
|
339
|
-
...
|
|
404
|
+
...tokenTotalsOf(row)
|
|
340
405
|
})),
|
|
341
|
-
sessions:
|
|
406
|
+
sessions: tokenSessions.rows.map((row) => ({
|
|
342
407
|
sessionId: String(row["session_id"]),
|
|
343
408
|
title: text(row["title"]),
|
|
344
409
|
subagent: row["subagent"] === true,
|
|
345
410
|
archived: row["archived"] === true,
|
|
346
|
-
...
|
|
411
|
+
...tokenTotalsOf(row),
|
|
412
|
+
...activityBySession.get(String(row["session_id"])) ?? emptyActivity()
|
|
347
413
|
}))
|
|
348
414
|
};
|
|
349
415
|
}
|
|
416
|
+
/** 表名限定到配置的 schema(派生表不进 drizzle schema / 迁移,建表幂等)。 */
|
|
417
|
+
qualifiedTable(name) {
|
|
418
|
+
const schema = this.options.schema ?? "public";
|
|
419
|
+
return schema === "public" ? name : `"${schema}".${name}`;
|
|
420
|
+
}
|
|
421
|
+
async ensureEventCountsTable() {
|
|
422
|
+
const table = this.qualifiedTable("t_event_counts");
|
|
423
|
+
await this.db.execute(sql.raw(`CREATE TABLE IF NOT EXISTS ${table} (
|
|
424
|
+
f_id serial PRIMARY KEY,
|
|
425
|
+
f_session_id text NOT NULL,
|
|
426
|
+
f_day text NOT NULL,
|
|
427
|
+
f_type text NOT NULL,
|
|
428
|
+
f_count integer NOT NULL DEFAULT 0
|
|
429
|
+
)`));
|
|
430
|
+
await this.db.execute(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS uq_event_counts_session_day_type ON ${table}(f_session_id, f_day, f_type)`));
|
|
431
|
+
await this.db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS idx_event_counts_day ON ${table}(f_day)`));
|
|
432
|
+
}
|
|
433
|
+
/** 一次性回填活动计数(表为空时从事件表重算;幂等)。 */
|
|
434
|
+
async backfillEventCounts() {
|
|
435
|
+
const table = this.qualifiedTable("t_event_counts");
|
|
436
|
+
const existing = await this.db.execute(sql.raw(`SELECT count(*) AS n FROM ${table}`));
|
|
437
|
+
if (Number(existing.rows[0]?.["n"] ?? 0) > 0) return;
|
|
438
|
+
const tSessionEvents = this.tables["t_session_events"];
|
|
439
|
+
const tEvents = this.tables["t_events"];
|
|
440
|
+
await this.db.execute(sql`
|
|
441
|
+
INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
|
|
442
|
+
SELECT ${tSessionEvents}.f_session_id,
|
|
443
|
+
to_char(to_timestamp(${tEvents}.f_created_at / 1000.0), 'YYYY-MM-DD'),
|
|
444
|
+
${tEvents}.f_type,
|
|
445
|
+
count(*)
|
|
446
|
+
FROM ${tSessionEvents}
|
|
447
|
+
JOIN ${tEvents} ON ${tEvents}.f_event_id = ${tSessionEvents}.f_event_id
|
|
448
|
+
WHERE ${tEvents}.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
|
|
449
|
+
GROUP BY 1, 2, 3
|
|
450
|
+
ON CONFLICT DO NOTHING
|
|
451
|
+
`);
|
|
452
|
+
}
|
|
453
|
+
/** 活动计数旁路累加:派生表,不在写事务里(失败可丢,表可销毁重建)。 */
|
|
454
|
+
async incrementEventCounts(id, buckets) {
|
|
455
|
+
if (buckets.length === 0) return;
|
|
456
|
+
const table = this.qualifiedTable("t_event_counts");
|
|
457
|
+
for (const bucket of buckets) await this.db.execute(sql`
|
|
458
|
+
INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
|
|
459
|
+
VALUES (${id}, ${bucket.day}, ${bucket.type}, ${bucket.count})
|
|
460
|
+
ON CONFLICT (f_session_id, f_day, f_type)
|
|
461
|
+
DO UPDATE SET f_count = ${sql.raw(`${table}.f_count`)} + EXCLUDED.f_count
|
|
462
|
+
`);
|
|
463
|
+
}
|
|
464
|
+
/** 按会话重算活动计数(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
|
|
465
|
+
async rebuildEventCounts(id) {
|
|
466
|
+
const table = this.qualifiedTable("t_event_counts");
|
|
467
|
+
const tSessionEvents = this.tables["t_session_events"];
|
|
468
|
+
const tEvents = this.tables["t_events"];
|
|
469
|
+
await this.db.execute(sql`DELETE FROM ${sql.raw(table)} WHERE f_session_id = ${id}`);
|
|
470
|
+
await this.db.execute(sql`
|
|
471
|
+
INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
|
|
472
|
+
SELECT ${tSessionEvents}.f_session_id,
|
|
473
|
+
to_char(to_timestamp(${tEvents}.f_created_at / 1000.0), 'YYYY-MM-DD'),
|
|
474
|
+
${tEvents}.f_type,
|
|
475
|
+
count(*)
|
|
476
|
+
FROM ${tSessionEvents}
|
|
477
|
+
JOIN ${tEvents} ON ${tEvents}.f_event_id = ${tSessionEvents}.f_event_id
|
|
478
|
+
WHERE ${tSessionEvents}.f_session_id = ${id}
|
|
479
|
+
AND ${tEvents}.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
|
|
480
|
+
GROUP BY 1, 2, 3
|
|
481
|
+
`);
|
|
482
|
+
}
|
|
483
|
+
async deleteEventCounts(id) {
|
|
484
|
+
const table = this.qualifiedTable("t_event_counts");
|
|
485
|
+
await this.db.execute(sql`DELETE FROM ${sql.raw(table)} WHERE f_session_id = ${id}`);
|
|
486
|
+
}
|
|
350
487
|
async getPrevBridge(exec, id, sequence) {
|
|
351
488
|
return (await exec.select({
|
|
352
489
|
fEventId: this.tables["t_session_events"].fEventId,
|
|
@@ -556,118 +693,6 @@ function registerSessionGc(ctx, persistence) {
|
|
|
556
693
|
});
|
|
557
694
|
}
|
|
558
695
|
//#endregion
|
|
559
|
-
//#region src/usage.ts
|
|
560
|
-
const SESSION_USAGE_PATH = "/api/session.usage";
|
|
561
|
-
const DAY_MS = 864e5;
|
|
562
|
-
const ROLLING_DAYS = {
|
|
563
|
-
"7d": 7,
|
|
564
|
-
"30d": 30,
|
|
565
|
-
"90d": 90
|
|
566
|
-
};
|
|
567
|
-
function localDayStart(now) {
|
|
568
|
-
const start = new Date(now);
|
|
569
|
-
start.setHours(0, 0, 0, 0);
|
|
570
|
-
return start;
|
|
571
|
-
}
|
|
572
|
-
/**
|
|
573
|
-
* 范围起点(含),不限时为 undefined。自然日 / 自然周按 host 的本地时区算,周边界是周一 00:00。
|
|
574
|
-
* @param range - 语义键。
|
|
575
|
-
* @param now - 当前时刻。
|
|
576
|
-
* @returns 起点毫秒时间戳,或 undefined。
|
|
577
|
-
*/
|
|
578
|
-
function resolveUsageSince(range, now) {
|
|
579
|
-
switch (range) {
|
|
580
|
-
case "all": return;
|
|
581
|
-
case "day": return localDayStart(now).getTime();
|
|
582
|
-
case "week": {
|
|
583
|
-
const start = localDayStart(now);
|
|
584
|
-
start.setDate(start.getDate() - (start.getDay() + 6) % 7);
|
|
585
|
-
return start.getTime();
|
|
586
|
-
}
|
|
587
|
-
default: return now - ROLLING_DAYS[range] * DAY_MS;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
function parseUsageRange(value) {
|
|
591
|
-
return value === "day" || value === "week" || value === "7d" || value === "30d" || value === "90d" ? value : "all";
|
|
592
|
-
}
|
|
593
|
-
function emptyTotals() {
|
|
594
|
-
return {
|
|
595
|
-
events: 0,
|
|
596
|
-
inputTokens: 0,
|
|
597
|
-
outputTokens: 0,
|
|
598
|
-
cacheReadTokens: 0,
|
|
599
|
-
reasoningTokens: 0,
|
|
600
|
-
totalTokens: 0
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
/** 折叠一组行(桶或会话行)的用量。 */
|
|
604
|
-
function sumUsage(rows) {
|
|
605
|
-
const totals = emptyTotals();
|
|
606
|
-
for (const row of rows) {
|
|
607
|
-
totals.events += row.events;
|
|
608
|
-
totals.inputTokens += row.inputTokens;
|
|
609
|
-
totals.outputTokens += row.outputTokens;
|
|
610
|
-
totals.cacheReadTokens += row.cacheReadTokens;
|
|
611
|
-
totals.reasoningTokens += row.reasoningTokens;
|
|
612
|
-
totals.totalTokens += row.totalTokens;
|
|
613
|
-
}
|
|
614
|
-
return totals;
|
|
615
|
-
}
|
|
616
|
-
/**
|
|
617
|
-
* 用量统计通道:一次请求回报总览(含 subagent 拆分)、按天 × 模型的桶与按会话的行。
|
|
618
|
-
* 聚合只算被会话引用的事件行——fork 共享行因此只计一次,已删会话留下的孤儿行不计。
|
|
619
|
-
*/
|
|
620
|
-
function registerSessionUsage(ctx, persistence) {
|
|
621
|
-
ctx.inject(["webServer", "connection"], (webCtx) => {
|
|
622
|
-
const webServer = webCtx.webServer;
|
|
623
|
-
const connection = webCtx.get("connection");
|
|
624
|
-
return webCtx.effect(() => webServer.register({
|
|
625
|
-
kind: "exact",
|
|
626
|
-
path: SESSION_USAGE_PATH,
|
|
627
|
-
handler: async (req, res) => {
|
|
628
|
-
const rejection = connection.requestRejection(req);
|
|
629
|
-
if (rejection !== void 0) {
|
|
630
|
-
res.writeHead(rejection);
|
|
631
|
-
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
if (req.method !== "POST") {
|
|
635
|
-
res.writeHead(405, { "content-type": "application/json" });
|
|
636
|
-
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
const chunks = [];
|
|
640
|
-
for await (const chunk of req) chunks.push(chunk);
|
|
641
|
-
const raw = Buffer.concat(chunks).toString("utf8");
|
|
642
|
-
let envelope = {};
|
|
643
|
-
if (raw !== "") try {
|
|
644
|
-
envelope = JSON.parse(raw);
|
|
645
|
-
} catch {
|
|
646
|
-
res.writeHead(400, { "content-type": "application/json" });
|
|
647
|
-
res.end(JSON.stringify({ error: "request body is not JSON" }));
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
try {
|
|
651
|
-
const aggregate = await persistence.usageReport(resolveUsageSince(parseUsageRange(envelope.range), Date.now()));
|
|
652
|
-
const totals = sumUsage(aggregate.buckets);
|
|
653
|
-
const subagent = sumUsage(aggregate.buckets.filter((bucket) => bucket.subagent));
|
|
654
|
-
const human = sumUsage(aggregate.buckets.filter((bucket) => !bucket.subagent));
|
|
655
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
656
|
-
res.end(JSON.stringify({
|
|
657
|
-
totals,
|
|
658
|
-
subagent,
|
|
659
|
-
human,
|
|
660
|
-
...aggregate
|
|
661
|
-
}));
|
|
662
|
-
} catch (error) {
|
|
663
|
-
res.writeHead(500, { "content-type": "application/json" });
|
|
664
|
-
res.end(JSON.stringify({ error: error instanceof Error ? error.message : "usage report failed" }));
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
}), `session-rdb: ${SESSION_USAGE_PATH} route`);
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
//#endregion
|
|
671
696
|
//#region src/session-query.ts
|
|
672
697
|
var SessionQueryRdb = class extends SessionQueryEngine {
|
|
673
698
|
constructor(ctx, config = {}) {
|
|
@@ -1478,7 +1503,10 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
1478
1503
|
});
|
|
1479
1504
|
this.liveBuffers.delete(id);
|
|
1480
1505
|
this.liveReady.delete(id);
|
|
1506
|
+
this.liveFailures.delete(id);
|
|
1507
|
+
this.liveDropWarned.delete(id);
|
|
1481
1508
|
this.reuseEventIds.delete(id);
|
|
1509
|
+
await this.deleteEventCounts(id);
|
|
1482
1510
|
}
|
|
1483
1511
|
/** GC 通道:回收已无桥接行引用的事件行(孤儿),返回删除行数。 */
|
|
1484
1512
|
async collectOrphans() {
|
|
@@ -1494,7 +1522,10 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
1494
1522
|
for (const id of deletable) {
|
|
1495
1523
|
this.liveBuffers.delete(id);
|
|
1496
1524
|
this.liveReady.delete(id);
|
|
1525
|
+
this.liveFailures.delete(id);
|
|
1526
|
+
this.liveDropWarned.delete(id);
|
|
1497
1527
|
this.reuseEventIds.delete(id);
|
|
1528
|
+
await this.deleteEventCounts(id);
|
|
1498
1529
|
}
|
|
1499
1530
|
return deleted;
|
|
1500
1531
|
}
|
|
@@ -1572,6 +1603,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
1572
1603
|
});
|
|
1573
1604
|
this.writeGuard.confirmHead(meta.id, confirmedHead);
|
|
1574
1605
|
this.tracker.materialized(meta.id);
|
|
1606
|
+
await this.recordEventCounts(meta.id, events);
|
|
1575
1607
|
return true;
|
|
1576
1608
|
}
|
|
1577
1609
|
async readLog(id, options = {}, signal) {
|
|
@@ -1620,6 +1652,35 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
1620
1652
|
...tornFrom !== void 0 ? { tornFrom } : {}
|
|
1621
1653
|
};
|
|
1622
1654
|
}
|
|
1655
|
+
/**
|
|
1656
|
+
* 活动计数**旁路累加**:不在写事务里、失败只 warn——`t_event_counts` 是可销毁重建的派生表,
|
|
1657
|
+
* 丢几次累加不影响可用性(需要时用 `rebuildEventCounts` 重算)。
|
|
1658
|
+
*/
|
|
1659
|
+
async recordEventCounts(id, events) {
|
|
1660
|
+
const buckets = eventCountBuckets(events);
|
|
1661
|
+
if (buckets.length === 0) return;
|
|
1662
|
+
try {
|
|
1663
|
+
await this.backend.incrementEventCounts(id, buckets);
|
|
1664
|
+
} catch (error) {
|
|
1665
|
+
this.ctx.logger.warn(`session-rdb: activity counts for "${id}" not updated (${describeError(error)})`);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
/** 重算一个会话的活动计数(rewind / fork 之后;best-effort,同 `recordEventCounts`)。 */
|
|
1669
|
+
async rebuildEventCounts(id) {
|
|
1670
|
+
try {
|
|
1671
|
+
await this.backend.rebuildEventCounts(id);
|
|
1672
|
+
} catch (error) {
|
|
1673
|
+
this.ctx.logger.warn(`session-rdb: activity counts for "${id}" not rebuilt (${describeError(error)})`);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
/** 会话删除时清掉它的活动计数行(best-effort)。 */
|
|
1677
|
+
async deleteEventCounts(id) {
|
|
1678
|
+
try {
|
|
1679
|
+
await this.backend.deleteEventCounts(id);
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
this.ctx.logger.warn(`session-rdb: activity counts for "${id}" not deleted (${describeError(error)})`);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1623
1684
|
async rewriteMigratedLog(id, log) {
|
|
1624
1685
|
await this.backend.transaction(async (tx) => {
|
|
1625
1686
|
await tx.deleteBridgeTail(id, 0);
|
|
@@ -1678,24 +1739,26 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
1678
1739
|
}
|
|
1679
1740
|
}
|
|
1680
1741
|
async load(id, signal) {
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
return {
|
|
1692
|
-
meta: rowToMeta(row),
|
|
1693
|
-
inheritedEventCount: SessionLogOffset(row.fSeedLength ?? 0),
|
|
1694
|
-
events
|
|
1742
|
+
signal?.throwIfAborted();
|
|
1743
|
+
await this.ready;
|
|
1744
|
+
signal?.throwIfAborted();
|
|
1745
|
+
const log = await this.readLog(id, {}, signal);
|
|
1746
|
+
if (log === void 0) {
|
|
1747
|
+
const pending = this.tracker.pendingOf(id);
|
|
1748
|
+
if (pending !== void 0) return {
|
|
1749
|
+
meta: pending.header,
|
|
1750
|
+
inheritedEventCount: SessionLogOffset(pending.inheritedEventCount),
|
|
1751
|
+
events: []
|
|
1695
1752
|
};
|
|
1696
|
-
|
|
1697
|
-
await handle.close();
|
|
1753
|
+
throw new SessionPersistenceNotFoundError(id);
|
|
1698
1754
|
}
|
|
1755
|
+
repairReadView(log.events);
|
|
1756
|
+
validateStoredEvents(log.meta, log.events);
|
|
1757
|
+
return {
|
|
1758
|
+
meta: log.meta,
|
|
1759
|
+
inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
|
|
1760
|
+
events: log.events
|
|
1761
|
+
};
|
|
1699
1762
|
}
|
|
1700
1763
|
async append(id, events) {
|
|
1701
1764
|
const handle = await this.open(id, "write");
|
|
@@ -1738,22 +1801,28 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
1738
1801
|
rowRevision(row) {
|
|
1739
1802
|
return SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`);
|
|
1740
1803
|
}
|
|
1804
|
+
liveFailures = /* @__PURE__ */ new Map();
|
|
1805
|
+
liveDropWarned = /* @__PURE__ */ new Set();
|
|
1806
|
+
watchLiveReady(ctx, session, ready) {
|
|
1807
|
+
ready.catch((error) => {
|
|
1808
|
+
const failure = error instanceof Error ? error : new Error(typeof error === "string" ? error : `live session "${session.id}" persistence failed`);
|
|
1809
|
+
this.liveFailures.set(session.id, failure);
|
|
1810
|
+
this.liveBuffers.delete(session.id);
|
|
1811
|
+
ctx.logger.error(`session-rdb: live session "${session.id}" persistence unavailable (${failure.message}); its events will not be persisted`);
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1741
1814
|
installLiveRouting(ctx) {
|
|
1742
1815
|
ctx.on("session/created", (session) => {
|
|
1743
1816
|
this.liveBuffers.set(session.id, []);
|
|
1744
1817
|
const ready = this.ensureLiveHandle(session);
|
|
1745
1818
|
this.liveReady.set(session.id, ready);
|
|
1746
|
-
|
|
1747
|
-
ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
|
|
1748
|
-
});
|
|
1819
|
+
this.watchLiveReady(ctx, session, ready);
|
|
1749
1820
|
});
|
|
1750
1821
|
for (const session of ctx.sessions.list()) {
|
|
1751
1822
|
this.liveBuffers.set(session.id, []);
|
|
1752
1823
|
const ready = this.ensureLiveHandle(session);
|
|
1753
1824
|
this.liveReady.set(session.id, ready);
|
|
1754
|
-
|
|
1755
|
-
ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
|
|
1756
|
-
});
|
|
1825
|
+
this.watchLiveReady(ctx, session, ready);
|
|
1757
1826
|
}
|
|
1758
1827
|
ctx.on("session/event", (session, event) => {
|
|
1759
1828
|
const handle = this.tracker.writerOf(session.id);
|
|
@@ -1763,9 +1832,18 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
1763
1832
|
});
|
|
1764
1833
|
return;
|
|
1765
1834
|
}
|
|
1835
|
+
if (this.liveFailures.has(session.id)) {
|
|
1836
|
+
if (!this.liveDropWarned.has(session.id)) {
|
|
1837
|
+
this.liveDropWarned.add(session.id);
|
|
1838
|
+
ctx.logger.warn(`session-rdb: dropping events for live session "${session.id}" because persistence failed earlier`);
|
|
1839
|
+
}
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1766
1842
|
this.liveBuffers.get(session.id)?.push(structuredClone(event));
|
|
1767
1843
|
});
|
|
1768
1844
|
ctx.on("session/flush", (session) => {
|
|
1845
|
+
const failure = this.liveFailures.get(session.id);
|
|
1846
|
+
if (failure !== void 0) return Promise.reject(failure);
|
|
1769
1847
|
const handle = this.tracker.writerOf(session.id);
|
|
1770
1848
|
if (handle === void 0) {
|
|
1771
1849
|
const ready = this.liveReady.get(session.id);
|
|
@@ -1782,6 +1860,8 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
1782
1860
|
const ready = this.liveReady.get(session.id);
|
|
1783
1861
|
this.liveBuffers.delete(session.id);
|
|
1784
1862
|
this.liveReady.delete(session.id);
|
|
1863
|
+
this.liveFailures.delete(session.id);
|
|
1864
|
+
this.liveDropWarned.delete(session.id);
|
|
1785
1865
|
const closeHandle = () => {
|
|
1786
1866
|
const handle = this.tracker.writerOf(session.id);
|
|
1787
1867
|
if (handle === void 0) return;
|
|
@@ -1797,6 +1877,10 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
1797
1877
|
});
|
|
1798
1878
|
ctx.effect(() => async () => {
|
|
1799
1879
|
await Promise.allSettled(this.liveReady.values());
|
|
1880
|
+
this.liveBuffers.clear();
|
|
1881
|
+
this.liveReady.clear();
|
|
1882
|
+
this.liveFailures.clear();
|
|
1883
|
+
this.liveDropWarned.clear();
|
|
1800
1884
|
await this.tracker.closeAll();
|
|
1801
1885
|
await this.close();
|
|
1802
1886
|
}, `${this.name} open handles`);
|
|
@@ -1835,6 +1919,26 @@ function seedCoversPrefix(seed, prefix) {
|
|
|
1835
1919
|
return seedEvent !== void 0 && JSON.stringify(seedEvent) === JSON.stringify(event);
|
|
1836
1920
|
});
|
|
1837
1921
|
}
|
|
1922
|
+
/** 一批事件折成活动计数的桶:只数四种类型,按本地日聚合。 */
|
|
1923
|
+
function eventCountBuckets(events) {
|
|
1924
|
+
const counted = /* @__PURE__ */ new Map();
|
|
1925
|
+
for (const event of events) {
|
|
1926
|
+
if (!COUNTED_EVENT_TYPES.includes(event.type)) continue;
|
|
1927
|
+
const day = localDayKey(event.time);
|
|
1928
|
+
const key = `${day}#${event.type}`;
|
|
1929
|
+
const bucket = counted.get(key) ?? {
|
|
1930
|
+
day,
|
|
1931
|
+
type: event.type,
|
|
1932
|
+
count: 0
|
|
1933
|
+
};
|
|
1934
|
+
bucket.count += 1;
|
|
1935
|
+
counted.set(key, bucket);
|
|
1936
|
+
}
|
|
1937
|
+
return [...counted.values()];
|
|
1938
|
+
}
|
|
1939
|
+
function describeError(error) {
|
|
1940
|
+
return error instanceof Error ? error.message : "unknown error";
|
|
1941
|
+
}
|
|
1838
1942
|
function createBackend(config) {
|
|
1839
1943
|
if (config.type === "sqlite") return new SqliteBackend({
|
|
1840
1944
|
path: config.path,
|
package/dist/storage.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as tSessionEvents, C as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, D as tEvents, E as tEventUsage, F as tWorkspaceState, I as tWorkspaces, L as WriteGuard, M as tSessions, N as tStorageUnits, O as tPersistenceState, P as tWorkspaceSessions, R as StorageRepository, S as SCHEMA_VERSION, T as eventKind, _ as DEFAULT_BUSY_TIMEOUT_MS, a as SessionRow, b as EventRole, i as EventRow, j as tSessionProjcacheRows, k as tSchemaMeta, n as BackendTx, o as UsageAggregate, r as EventCountBucket, t as Backend, v as EVENT_ENCODING, w as eventDimensions, x as JournalMode, y as EventKind } from "./backend-a0xDrUFG.mjs";
|
|
2
2
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
3
3
|
import { DatabaseSync } from "node:sqlite";
|
|
4
4
|
//#region src/sqlite.d.ts
|
|
@@ -47,8 +47,17 @@ declare class SqliteBackend implements Backend {
|
|
|
47
47
|
/** 父会话被删后留下的 subagent 会话:父已不在表里,或本来就没有父。 */
|
|
48
48
|
listOrphanSubagentSessions(): Promise<SessionId[]>;
|
|
49
49
|
private deleteSessions;
|
|
50
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* 用量聚合:token 用量沿 `t_event_usage` 的用量行,活动计数(轮次 / 步骤 / 用户输入 / 工具调用)
|
|
52
|
+
* 沿 `t_events` 的事件类型数——两者限定在**有 token 用量的会话**里,时间范围各自按自己的
|
|
53
|
+
* `f_created_at`(同一时刻写入,口径一致)。
|
|
54
|
+
*/
|
|
51
55
|
usageReport(sinceMs?: number): Promise<UsageAggregate>;
|
|
56
|
+
/** 活动计数旁路累加:派生表 `t_event_counts`,不在写事务里(失败可丢,表可销毁重建)。 */
|
|
57
|
+
incrementEventCounts(id: SessionId, buckets: readonly EventCountBucket[]): Promise<void>;
|
|
58
|
+
/** 按会话重算活动计数(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
|
|
59
|
+
rebuildEventCounts(id: SessionId): Promise<void>;
|
|
60
|
+
deleteEventCounts(id: SessionId): Promise<void>;
|
|
52
61
|
vacuum(): Promise<void>;
|
|
53
62
|
private eventRows;
|
|
54
63
|
}
|
package/dist/storage.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as WriteGuard, C as tSessions, D as tWorkspaces, E as tWorkspaceState, S as tSessionProjcacheRows, T as tWorkspaceSessions, _ as tEventUsage, b as tSchemaMeta, d as DEFAULT_BUSY_TIMEOUT_MS, f as EVENT_ENCODING, g as eventKind, h as eventDimensions, m as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, n as openDatabase, p as SCHEMA_VERSION, t as SqliteBackend, v as tEvents, w as tStorageUnits, x as tSessionEvents, y as tPersistenceState } from "./sqlite-C3LHLzYW.mjs";
|
|
2
2
|
export { DEFAULT_BUSY_TIMEOUT_MS, EVENT_ENCODING, SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, SqliteBackend, WriteGuard, eventDimensions, eventKind, openDatabase, tEventUsage, tEvents, tPersistenceState, tSchemaMeta, tSessionEvents, tSessionProjcacheRows, tSessions, tStorageUnits, tWorkspaceSessions, tWorkspaceState, tWorkspaces };
|
package/dist/testing.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { g as truncateLiveSession } from "./backend-a0xDrUFG.mjs";
|
|
2
2
|
import { SessionPersistence } from "@deepseek-ai/dsh-session-persistence";
|
|
3
3
|
import { Session, SessionEvent, SessionHeader, SessionId } from "@deepseek-ai/dsh-session";
|
|
4
4
|
import { Context, Fiber } from "@deepseek-ai/cordis";
|