@morlay/session-rdb 0.0.21-alpha.6 → 0.0.21-alpha.8
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 +17 -10
- package/dist/artifact.d.mts +2 -101
- package/dist/artifact.mjs +2 -2
- package/dist/{branch-BjUR6wfe.mjs → branch-7eOh_tbO.mjs} +3 -3
- 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/{backend-E0bmTXpz.d.mts → log-BbleqD1M.d.mts} +171 -24
- package/dist/{log-qCNkDjh5.mjs → log-DTcvsMz7.mjs} +189 -12
- package/dist/{sqlite-zcXVyMxr.mjs → sqlite-ChM_sHAG.mjs} +320 -259
- package/dist/{src-B9X8BKyh.mjs → src-BWqePRA7.mjs} +290 -162
- package/dist/storage.d.mts +20 -7
- package/dist/storage.mjs +2 -2
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +11 -11
- package/drizzle/postgres/20260923120000_v3_usage_materialized/migration.sql +48 -0
- package/drizzle/sqlite/20260923120000_v3_usage_materialized/migration.sql +48 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +25 -23
- package/src/backend.ts +41 -17
- package/src/branch.ts +4 -4
- package/src/drizzle/postgres-v3.ts +2 -0
- package/src/drizzle/sqlite-v3.ts +2 -0
- package/src/entities/v3/event-usage.ts +8 -1
- package/src/entities/v3/index.ts +8 -0
- package/src/entities/v3/session-counts.ts +23 -0
- package/src/entities/v3/session-usage.ts +29 -0
- package/src/index.ts +109 -43
- package/src/log.ts +58 -17
- package/src/postgres.ts +248 -149
- package/src/rows.ts +2 -1
- package/src/schema.ts +4 -0
- package/src/sqlite.ts +263 -124
- package/src/storage-takeover/repository.ts +1 -4
- package/src/usage.ts +10 -6
package/src/postgres.ts
CHANGED
|
@@ -10,21 +10,23 @@ import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistenc
|
|
|
10
10
|
import {
|
|
11
11
|
type Backend,
|
|
12
12
|
type BackendTx,
|
|
13
|
-
type EventCountBucket,
|
|
14
13
|
type EventInsert,
|
|
15
14
|
type EventRow,
|
|
15
|
+
type SessionCountBucket,
|
|
16
16
|
type SessionListRowsPage,
|
|
17
17
|
type SessionListRowsQuery,
|
|
18
18
|
type SessionRow,
|
|
19
|
+
type SessionUsageBucket,
|
|
19
20
|
} from "./backend.ts";
|
|
20
21
|
import { toPostgresSchema } from "./adapters/index.ts";
|
|
21
22
|
import { postgresTableDefs } from "./entities/index.ts";
|
|
22
|
-
import { sessionConflictRow, sessionInsertRow, titleOfEventData
|
|
23
|
+
import { sessionConflictRow, sessionInsertRow, titleOfEventData } from "./log.ts";
|
|
24
|
+
import type { EventUsageRow } from "./log.ts";
|
|
23
25
|
import { createStorageRepository } from "./storage-takeover/repository.ts";
|
|
24
26
|
import type { StorageRepository } from "./storage-takeover/types.ts";
|
|
25
27
|
import {
|
|
26
28
|
COUNTED_EVENT_TYPES,
|
|
27
|
-
|
|
29
|
+
addActivityTotals,
|
|
28
30
|
addTokenTotals,
|
|
29
31
|
addTotals,
|
|
30
32
|
emptyTotals,
|
|
@@ -76,6 +78,16 @@ function emptyActivity(): UsageActivityTotals {
|
|
|
76
78
|
return { turns: 0, steps: 0, userInputs: 0, toolCalls: 0 };
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
/** `t_session_counts` 的四项计数折成活动计数(pg 的 sum 回落成字符串)。 */
|
|
82
|
+
function activityTotalsOf(row: Record<string, unknown>): UsageActivityTotals {
|
|
83
|
+
return {
|
|
84
|
+
turns: numeric(row["turns"]),
|
|
85
|
+
steps: numeric(row["steps"]),
|
|
86
|
+
userInputs: numeric(row["user_inputs"]),
|
|
87
|
+
toolCalls: numeric(row["tool_calls"]),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
79
91
|
/** `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。 */
|
|
80
92
|
const COUNTED_EVENT_TYPE_SQL = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
|
|
81
93
|
|
|
@@ -164,9 +176,7 @@ export class PostgresBackend implements Backend {
|
|
|
164
176
|
}
|
|
165
177
|
}
|
|
166
178
|
await migrate(this.db, { migrationsFolder: postgresMigrationsDir });
|
|
167
|
-
await this.
|
|
168
|
-
await this.backfillEventCounts();
|
|
169
|
-
await this.backfillEventUsage();
|
|
179
|
+
await this.backfillUsageTables();
|
|
170
180
|
const storeId = await this.db.transaction(async (tx) => {
|
|
171
181
|
await tx
|
|
172
182
|
.insert(this.tables["t_persistence_state"])
|
|
@@ -281,7 +291,7 @@ export class PostgresBackend implements Backend {
|
|
|
281
291
|
getHead: (id) => this.getHead(tx, id),
|
|
282
292
|
getSeedLength: (id) => this.getSeedLength(tx, id),
|
|
283
293
|
updateSeedLength: (id, seedLength) => this.updateSeedLength(tx, id, seedLength),
|
|
284
|
-
insertEvents: (events) => this.insertEvents(tx, events),
|
|
294
|
+
insertEvents: (events, usageRows) => this.insertEvents(tx, events, usageRows),
|
|
285
295
|
insertBridges: (rows) => this.insertBridges(tx, rows),
|
|
286
296
|
updateHead: (id, headEventId, headSequence) =>
|
|
287
297
|
this.updateHead(tx, id, headEventId, headSequence),
|
|
@@ -373,6 +383,7 @@ export class PostgresBackend implements Backend {
|
|
|
373
383
|
private async insertEvents(
|
|
374
384
|
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
375
385
|
events: EventInsert[],
|
|
386
|
+
usageRows: readonly EventUsageRow[],
|
|
376
387
|
): Promise<void> {
|
|
377
388
|
if (events.length === 0) return;
|
|
378
389
|
for (let i = 0; i < events.length; i += PostgresBackend.INSERT_BATCH_ROWS) {
|
|
@@ -383,13 +394,18 @@ export class PostgresBackend implements Backend {
|
|
|
383
394
|
)
|
|
384
395
|
.execute();
|
|
385
396
|
}
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
397
|
+
// 用量行顺带落 t_event_usage(本地日与引用标记已由写路径物化):统计不再解析事件 JSON、
|
|
398
|
+
// 也不回连事件表。
|
|
399
|
+
if (usageRows.length > 0) {
|
|
400
|
+
for (let i = 0; i < usageRows.length; i += PostgresBackend.INSERT_BATCH_ROWS) {
|
|
401
|
+
await exec
|
|
402
|
+
.insert(this.tables["t_event_usage"])
|
|
403
|
+
.values(
|
|
404
|
+
usageRows.slice(i, i + PostgresBackend.INSERT_BATCH_ROWS).map((row) => ({ ...row })),
|
|
405
|
+
)
|
|
406
|
+
.onConflictDoNothing()
|
|
407
|
+
.execute();
|
|
408
|
+
}
|
|
393
409
|
}
|
|
394
410
|
}
|
|
395
411
|
|
|
@@ -571,23 +587,46 @@ export class PostgresBackend implements Backend {
|
|
|
571
587
|
await this.db.execute(sql`VACUUM ANALYZE`);
|
|
572
588
|
}
|
|
573
589
|
|
|
574
|
-
/**
|
|
590
|
+
/** 派生统计表的回填:任一表为空即按事件表全量重算(幂等,迁移删表重建后自愈)。 */
|
|
591
|
+
private async backfillUsageTables(): Promise<void> {
|
|
592
|
+
// 顺序有依赖:用量行(带物化列)先落,两张会话汇总表再从它 / 事件表重算。
|
|
593
|
+
await this.backfillEventUsage();
|
|
594
|
+
await this.backfillSessionUsage();
|
|
595
|
+
await this.backfillSessionCounts();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
private async tableIsEmpty(table: string): Promise<boolean> {
|
|
599
|
+
const existing = (await this.db.execute(
|
|
600
|
+
sql.raw(`SELECT count(*) AS n FROM ${table}`),
|
|
601
|
+
)) as unknown as { rows: Array<Record<string, unknown>> };
|
|
602
|
+
return Number(existing.rows[0]?.["n"] ?? 0) === 0;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* 用量行:一条 `assistant/message` 事件行一行,物化本地日与两个归属标记。
|
|
607
|
+
* 本地日按数据库会话时区渲染(部署前提是它与 host 时区一致,`localDayKey` 同口径)。
|
|
608
|
+
*/
|
|
575
609
|
private async backfillEventUsage(): Promise<void> {
|
|
610
|
+
if (!(await this.tableIsEmpty(this.qualifiedTable("t_event_usage")))) return;
|
|
576
611
|
const tEventUsage = this.tables["t_event_usage"];
|
|
577
612
|
const tEvents = this.tables["t_events"];
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
)) as unknown as { rows: Array<{ n: string }> };
|
|
581
|
-
if (Number(existing.rows[0]?.["n"] ?? 0) > 0) return;
|
|
613
|
+
const tSessionEvents = this.tables["t_session_events"];
|
|
614
|
+
const tSessions = this.tables["t_sessions"];
|
|
582
615
|
await this.db.execute(sql`
|
|
583
616
|
INSERT INTO ${tEventUsage} (
|
|
584
|
-
f_event_id, f_created_at, f_provider, f_model,
|
|
617
|
+
f_event_id, f_created_at, f_day, f_provider, f_model, f_referenced, f_subagent,
|
|
585
618
|
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
586
619
|
SELECT e.f_event_id, e.f_created_at,
|
|
620
|
+
to_char(to_timestamp(e.f_created_at / 1000.0), 'YYYY-MM-DD'),
|
|
587
621
|
coalesce(e.f_data::json #>> '{data,message,source,provider}',
|
|
588
622
|
e.f_data::json #>> '{message,source,provider}'),
|
|
589
623
|
coalesce(e.f_data::json #>> '{data,message,source,model}',
|
|
590
624
|
e.f_data::json #>> '{message,source,model}'),
|
|
625
|
+
(EXISTS (SELECT 1 FROM ${tSessionEvents} rb
|
|
626
|
+
WHERE rb.f_event_id = e.f_event_id))::int,
|
|
627
|
+
(EXISTS (SELECT 1 FROM ${tSessionEvents} sb
|
|
628
|
+
JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
|
|
629
|
+
WHERE sb.f_event_id = e.f_event_id AND ss.f_origin = 'subagent'))::int,
|
|
591
630
|
coalesce((e.f_data::json #>> '{data,usage,inputTokens}')::integer,
|
|
592
631
|
(e.f_data::json #>> '{usage,inputTokens}')::integer, 0),
|
|
593
632
|
coalesce((e.f_data::json #>> '{data,usage,outputTokens}')::integer,
|
|
@@ -606,6 +645,49 @@ export class PostgresBackend implements Backend {
|
|
|
606
645
|
`);
|
|
607
646
|
}
|
|
608
647
|
|
|
648
|
+
/** 会话 × 本地日 × 模型的 token 汇总:从桥接行 × 用量行重算(含 fork 继承前缀)。 */
|
|
649
|
+
private async backfillSessionUsage(): Promise<void> {
|
|
650
|
+
if (!(await this.tableIsEmpty(this.qualifiedTable("t_session_usage")))) return;
|
|
651
|
+
const tSessionUsage = this.tables["t_session_usage"];
|
|
652
|
+
const tSessionEvents = this.tables["t_session_events"];
|
|
653
|
+
const tEventUsage = this.tables["t_event_usage"];
|
|
654
|
+
await this.db.execute(sql`
|
|
655
|
+
INSERT INTO ${tSessionUsage} (
|
|
656
|
+
f_session_id, f_day, f_provider, f_model,
|
|
657
|
+
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
658
|
+
SELECT b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, ''),
|
|
659
|
+
sum(u.f_input_tokens), sum(u.f_output_tokens), sum(u.f_cache_read_tokens),
|
|
660
|
+
sum(u.f_reasoning_tokens), sum(u.f_total_tokens)
|
|
661
|
+
FROM ${tSessionEvents} b
|
|
662
|
+
JOIN ${tEventUsage} u ON u.f_event_id = b.f_event_id
|
|
663
|
+
GROUP BY 1, 2, 3, 4
|
|
664
|
+
ON CONFLICT DO NOTHING
|
|
665
|
+
`);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** 会话 × 本地日的活动计数:从桥接行 × 事件类型重算。 */
|
|
669
|
+
private async backfillSessionCounts(): Promise<void> {
|
|
670
|
+
if (!(await this.tableIsEmpty(this.qualifiedTable("t_session_counts")))) return;
|
|
671
|
+
const tSessionCounts = this.tables["t_session_counts"];
|
|
672
|
+
const tSessionEvents = this.tables["t_session_events"];
|
|
673
|
+
const tEvents = this.tables["t_events"];
|
|
674
|
+
await this.db.execute(sql`
|
|
675
|
+
INSERT INTO ${tSessionCounts} (
|
|
676
|
+
f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
|
|
677
|
+
SELECT b.f_session_id,
|
|
678
|
+
to_char(to_timestamp(e.f_created_at / 1000.0), 'YYYY-MM-DD'),
|
|
679
|
+
sum(CASE WHEN e.f_type = 'turn/start' THEN 1 ELSE 0 END),
|
|
680
|
+
sum(CASE WHEN e.f_type = 'step/start' THEN 1 ELSE 0 END),
|
|
681
|
+
sum(CASE WHEN e.f_type = 'user/message' THEN 1 ELSE 0 END),
|
|
682
|
+
sum(CASE WHEN e.f_type = 'tool/call' THEN 1 ELSE 0 END)
|
|
683
|
+
FROM ${tSessionEvents} b
|
|
684
|
+
JOIN ${tEvents} e ON e.f_event_id = b.f_event_id
|
|
685
|
+
WHERE e.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
|
|
686
|
+
GROUP BY 1, 2
|
|
687
|
+
ON CONFLICT DO NOTHING
|
|
688
|
+
`);
|
|
689
|
+
}
|
|
690
|
+
|
|
609
691
|
/** 用量聚合:与 SQLite 侧同形,读 `t_event_usage`,数值列回来是字符串。 */
|
|
610
692
|
async listSessionRows(query: SessionListRowsQuery = {}): Promise<SessionListRowsPage> {
|
|
611
693
|
const needle = query.query?.trim().toLowerCase() ?? "";
|
|
@@ -644,7 +726,8 @@ export class PostgresBackend implements Backend {
|
|
|
644
726
|
`)) as unknown as { rows: Array<Record<string, unknown>> };
|
|
645
727
|
return {
|
|
646
728
|
items: rows.rows.map((row) => ({
|
|
647
|
-
sessionId:
|
|
729
|
+
sessionId:
|
|
730
|
+
typeof row["session_id"] === "string" ? row["session_id"] : String(row["session_id"]),
|
|
648
731
|
title: typeof row["title"] === "string" ? row["title"] : null,
|
|
649
732
|
origin: typeof row["origin"] === "string" ? row["origin"] : null,
|
|
650
733
|
cwd: typeof row["cwd"] === "string" ? row["cwd"] : null,
|
|
@@ -658,77 +741,73 @@ export class PostgresBackend implements Backend {
|
|
|
658
741
|
};
|
|
659
742
|
}
|
|
660
743
|
|
|
744
|
+
/**
|
|
745
|
+
* 用量聚合:与 SQLite 侧同形——只读三张派生统计表(用量行给去重口径的总量与「天 × 模型」桶,
|
|
746
|
+
* 两张会话汇总表给按会话的行),数值列回来是字符串。物化列让这里不做 `EXISTS`、不回连事件表。
|
|
747
|
+
*/
|
|
661
748
|
async usageReport(sinceMs?: number): Promise<UsageAggregate> {
|
|
662
|
-
const usageSince = sinceMs === undefined ? sql`` : sql` AND
|
|
663
|
-
|
|
664
|
-
|
|
749
|
+
const usageSince = sinceMs === undefined ? sql`` : sql` AND f_created_at >= ${sinceMs}`;
|
|
750
|
+
const dayFilter = (column: string): ReturnType<typeof sql> =>
|
|
751
|
+
sinceMs === undefined ? sql`` : sql` AND ${sql.raw(column)} >= ${localDayKey(sinceMs)}`;
|
|
665
752
|
const tEventUsage = this.tables["t_event_usage"];
|
|
666
|
-
const
|
|
753
|
+
const tSessionUsage = this.tables["t_session_usage"];
|
|
754
|
+
const tSessionCounts = this.tables["t_session_counts"];
|
|
667
755
|
const tSessions = this.tables["t_sessions"];
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
WHERE sb.f_session_id = c.f_session_id AND ss.f_origin = 'subagent')`;
|
|
675
|
-
// 有 token 用量的会话:活动计数与列表都限定在这一批会话里,口径一致。
|
|
676
|
-
const hasUsageSession = (
|
|
677
|
-
column: string,
|
|
678
|
-
): ReturnType<typeof sql> => sql`EXISTS (SELECT 1 FROM ${tSessionEvents} ub
|
|
679
|
-
JOIN ${tEventUsage} uu ON uu.f_event_id = ub.f_event_id
|
|
680
|
-
WHERE ub.f_session_id = ${sql.raw(column)})`;
|
|
681
|
-
const tokenColumns = sql.raw(`sum(u.f_input_tokens) AS input_tokens,
|
|
682
|
-
sum(u.f_output_tokens) AS output_tokens,
|
|
683
|
-
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
684
|
-
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
685
|
-
sum(u.f_total_tokens) AS total_tokens`);
|
|
756
|
+
const tokenColumns = (alias: string): ReturnType<typeof sql> =>
|
|
757
|
+
sql.raw(`sum(${alias}.f_input_tokens) AS input_tokens,
|
|
758
|
+
sum(${alias}.f_output_tokens) AS output_tokens,
|
|
759
|
+
sum(${alias}.f_cache_read_tokens) AS cache_read_tokens,
|
|
760
|
+
sum(${alias}.f_reasoning_tokens) AS reasoning_tokens,
|
|
761
|
+
sum(${alias}.f_total_tokens) AS total_tokens`);
|
|
686
762
|
|
|
687
763
|
const buckets = (await this.db.execute(sql`
|
|
688
|
-
SELECT
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
${tokenColumns}
|
|
693
|
-
FROM ${tEventUsage}
|
|
694
|
-
WHERE
|
|
764
|
+
SELECT a.f_day AS day,
|
|
765
|
+
a.f_provider AS provider,
|
|
766
|
+
a.f_model AS model,
|
|
767
|
+
(a.f_subagent = 1) AS subagent,
|
|
768
|
+
${tokenColumns("a")}
|
|
769
|
+
FROM ${tEventUsage} a
|
|
770
|
+
WHERE a.f_referenced = 1${usageSince}
|
|
695
771
|
GROUP BY 1, 2, 3, 4
|
|
696
772
|
`)) as unknown as { rows: Array<Record<string, unknown>> };
|
|
697
773
|
const tokenScope = (await this.db.execute(sql`
|
|
698
|
-
SELECT
|
|
699
|
-
${tokenColumns}
|
|
700
|
-
FROM ${tEventUsage}
|
|
701
|
-
WHERE
|
|
774
|
+
SELECT (a.f_subagent = 1) AS subagent,
|
|
775
|
+
${tokenColumns("a")}
|
|
776
|
+
FROM ${tEventUsage} a
|
|
777
|
+
WHERE a.f_referenced = 1${usageSince}
|
|
702
778
|
GROUP BY 1
|
|
703
779
|
`)) as unknown as { rows: Array<Record<string, unknown>> };
|
|
704
780
|
const activityScope = (await this.db.execute(sql`
|
|
705
|
-
SELECT
|
|
706
|
-
c.
|
|
707
|
-
sum(c.
|
|
708
|
-
|
|
781
|
+
SELECT (s.f_origin = 'subagent') AS subagent,
|
|
782
|
+
sum(c.f_turns) AS turns,
|
|
783
|
+
sum(c.f_steps) AS steps,
|
|
784
|
+
sum(c.f_user_inputs) AS user_inputs,
|
|
785
|
+
sum(c.f_tool_calls) AS tool_calls
|
|
786
|
+
FROM ${tSessionCounts} c
|
|
709
787
|
JOIN ${tSessions} s ON s.f_session_id = c.f_session_id
|
|
710
|
-
WHERE ${
|
|
711
|
-
GROUP BY 1
|
|
788
|
+
WHERE EXISTS (SELECT 1 FROM ${tSessionUsage} ub WHERE ub.f_session_id = c.f_session_id)${dayFilter("c.f_day")}
|
|
789
|
+
GROUP BY 1
|
|
712
790
|
`)) as unknown as { rows: Array<Record<string, unknown>> };
|
|
713
791
|
const tokenSessions = (await this.db.execute(sql`
|
|
714
|
-
SELECT
|
|
792
|
+
SELECT u.f_session_id AS session_id,
|
|
715
793
|
s.f_title AS title,
|
|
716
794
|
(s.f_origin = 'subagent') AS subagent,
|
|
717
795
|
(s.f_archived_at IS NOT NULL) AS archived,
|
|
718
|
-
${tokenColumns}
|
|
719
|
-
FROM ${
|
|
720
|
-
JOIN ${
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
GROUP BY b.f_session_id, s.f_title, s.f_origin, s.f_archived_at
|
|
796
|
+
${tokenColumns("u")}
|
|
797
|
+
FROM ${tSessionUsage} u
|
|
798
|
+
JOIN ${tSessions} s ON s.f_session_id = u.f_session_id
|
|
799
|
+
WHERE 1 = 1${dayFilter("u.f_day")}
|
|
800
|
+
GROUP BY u.f_session_id, s.f_title, s.f_origin, s.f_archived_at
|
|
724
801
|
`)) as unknown as { rows: Array<Record<string, unknown>> };
|
|
725
802
|
const activitySessions = (await this.db.execute(sql`
|
|
726
803
|
SELECT c.f_session_id AS session_id,
|
|
727
|
-
c.
|
|
728
|
-
sum(c.
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
804
|
+
sum(c.f_turns) AS turns,
|
|
805
|
+
sum(c.f_steps) AS steps,
|
|
806
|
+
sum(c.f_user_inputs) AS user_inputs,
|
|
807
|
+
sum(c.f_tool_calls) AS tool_calls
|
|
808
|
+
FROM ${tSessionCounts} c
|
|
809
|
+
WHERE 1 = 1${dayFilter("c.f_day")}
|
|
810
|
+
GROUP BY c.f_session_id
|
|
732
811
|
`)) as unknown as { rows: Array<Record<string, unknown>> };
|
|
733
812
|
const human = emptyTotals();
|
|
734
813
|
const subagent = emptyTotals();
|
|
@@ -736,20 +815,13 @@ export class PostgresBackend implements Backend {
|
|
|
736
815
|
addTokenTotals(row["subagent"] === true ? subagent : human, tokenTotalsOf(row));
|
|
737
816
|
}
|
|
738
817
|
for (const row of activityScope.rows) {
|
|
739
|
-
|
|
740
|
-
row["subagent"] === true ? subagent : human,
|
|
741
|
-
String(row["f_type"]),
|
|
742
|
-
numeric(row["n"]),
|
|
743
|
-
);
|
|
818
|
+
addActivityTotals(row["subagent"] === true ? subagent : human, activityTotalsOf(row));
|
|
744
819
|
}
|
|
745
820
|
const totals = addTotals(addTotals(emptyTotals(), human), subagent);
|
|
746
821
|
|
|
747
822
|
const activityBySession = new Map<string, UsageActivityTotals>();
|
|
748
823
|
for (const row of activitySessions.rows) {
|
|
749
|
-
|
|
750
|
-
const target = activityBySession.get(key) ?? emptyActivity();
|
|
751
|
-
addActivityCount(target, String(row["f_type"]), numeric(row["n"]));
|
|
752
|
-
activityBySession.set(key, target);
|
|
824
|
+
activityBySession.set(String(row["session_id"]), activityTotalsOf(row));
|
|
753
825
|
}
|
|
754
826
|
return {
|
|
755
827
|
totals,
|
|
@@ -763,7 +835,8 @@ export class PostgresBackend implements Backend {
|
|
|
763
835
|
...tokenTotalsOf(row),
|
|
764
836
|
})),
|
|
765
837
|
sessions: tokenSessions.rows.map((row) => ({
|
|
766
|
-
sessionId:
|
|
838
|
+
sessionId:
|
|
839
|
+
typeof row["session_id"] === "string" ? row["session_id"] : String(row["session_id"]),
|
|
767
840
|
title: text(row["title"]),
|
|
768
841
|
subagent: row["subagent"] === true,
|
|
769
842
|
archived: row["archived"] === true,
|
|
@@ -773,93 +846,119 @@ export class PostgresBackend implements Backend {
|
|
|
773
846
|
};
|
|
774
847
|
}
|
|
775
848
|
|
|
776
|
-
/** 表名限定到配置的 schema
|
|
849
|
+
/** 表名限定到配置的 schema。 */
|
|
777
850
|
private qualifiedTable(name: string): string {
|
|
778
851
|
const schema = this.options.schema ?? "public";
|
|
779
852
|
return schema === "public" ? name : `"${schema}".${name}`;
|
|
780
853
|
}
|
|
781
854
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
const table = this.qualifiedTable("t_event_counts");
|
|
806
|
-
const existing = (await this.db.execute(
|
|
807
|
-
sql.raw(`SELECT count(*) AS n FROM ${table}`),
|
|
808
|
-
)) as unknown as { rows: Array<Record<string, unknown>> };
|
|
809
|
-
if (Number(existing.rows[0]?.["n"] ?? 0) > 0) return;
|
|
810
|
-
const tSessionEvents = this.tables["t_session_events"];
|
|
811
|
-
const tEvents = this.tables["t_events"];
|
|
812
|
-
await this.db.execute(sql`
|
|
813
|
-
INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
|
|
814
|
-
SELECT ${tSessionEvents}.f_session_id,
|
|
815
|
-
to_char(to_timestamp(${tEvents}.f_created_at / 1000.0), 'YYYY-MM-DD'),
|
|
816
|
-
${tEvents}.f_type,
|
|
817
|
-
count(*)
|
|
818
|
-
FROM ${tSessionEvents}
|
|
819
|
-
JOIN ${tEvents} ON ${tEvents}.f_event_id = ${tSessionEvents}.f_event_id
|
|
820
|
-
WHERE ${tEvents}.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
|
|
821
|
-
GROUP BY 1, 2, 3
|
|
822
|
-
ON CONFLICT DO NOTHING
|
|
823
|
-
`);
|
|
855
|
+
/** token 用量旁路累加:派生表 `t_session_usage`,不在写事务里(失败可丢,表可销毁重建)。 */
|
|
856
|
+
async incrementSessionUsage(
|
|
857
|
+
id: SessionId,
|
|
858
|
+
buckets: readonly SessionUsageBucket[],
|
|
859
|
+
): Promise<void> {
|
|
860
|
+
if (buckets.length === 0) return;
|
|
861
|
+
const tSessionUsage = this.tables["t_session_usage"];
|
|
862
|
+
for (const bucket of buckets) {
|
|
863
|
+
await this.db.execute(sql`
|
|
864
|
+
INSERT INTO ${tSessionUsage} (
|
|
865
|
+
f_session_id, f_day, f_provider, f_model,
|
|
866
|
+
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
867
|
+
VALUES (${id}, ${bucket.day}, ${bucket.provider ?? ""}, ${bucket.model ?? ""},
|
|
868
|
+
${bucket.inputTokens}, ${bucket.outputTokens}, ${bucket.cacheReadTokens},
|
|
869
|
+
${bucket.reasoningTokens}, ${bucket.totalTokens})
|
|
870
|
+
ON CONFLICT (f_session_id, f_day, f_provider, f_model) DO UPDATE SET
|
|
871
|
+
f_input_tokens = ${tSessionUsage}.f_input_tokens + EXCLUDED.f_input_tokens,
|
|
872
|
+
f_output_tokens = ${tSessionUsage}.f_output_tokens + EXCLUDED.f_output_tokens,
|
|
873
|
+
f_cache_read_tokens = ${tSessionUsage}.f_cache_read_tokens + EXCLUDED.f_cache_read_tokens,
|
|
874
|
+
f_reasoning_tokens = ${tSessionUsage}.f_reasoning_tokens + EXCLUDED.f_reasoning_tokens,
|
|
875
|
+
f_total_tokens = ${tSessionUsage}.f_total_tokens + EXCLUDED.f_total_tokens
|
|
876
|
+
`);
|
|
877
|
+
}
|
|
824
878
|
}
|
|
825
879
|
|
|
826
|
-
/**
|
|
827
|
-
async
|
|
880
|
+
/** 活动计数旁路累加:派生表 `t_session_counts`,同上(四项计数一次落)。 */
|
|
881
|
+
async incrementSessionCounts(
|
|
882
|
+
id: SessionId,
|
|
883
|
+
buckets: readonly SessionCountBucket[],
|
|
884
|
+
): Promise<void> {
|
|
828
885
|
if (buckets.length === 0) return;
|
|
829
|
-
const
|
|
886
|
+
const tSessionCounts = this.tables["t_session_counts"];
|
|
830
887
|
for (const bucket of buckets) {
|
|
831
888
|
await this.db.execute(sql`
|
|
832
|
-
INSERT INTO ${
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
889
|
+
INSERT INTO ${tSessionCounts} (
|
|
890
|
+
f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
|
|
891
|
+
VALUES (${id}, ${bucket.day}, ${bucket.turns}, ${bucket.steps},
|
|
892
|
+
${bucket.userInputs}, ${bucket.toolCalls})
|
|
893
|
+
ON CONFLICT (f_session_id, f_day) DO UPDATE SET
|
|
894
|
+
f_turns = ${tSessionCounts}.f_turns + EXCLUDED.f_turns,
|
|
895
|
+
f_steps = ${tSessionCounts}.f_steps + EXCLUDED.f_steps,
|
|
896
|
+
f_user_inputs = ${tSessionCounts}.f_user_inputs + EXCLUDED.f_user_inputs,
|
|
897
|
+
f_tool_calls = ${tSessionCounts}.f_tool_calls + EXCLUDED.f_tool_calls
|
|
836
898
|
`);
|
|
837
899
|
}
|
|
838
900
|
}
|
|
839
901
|
|
|
840
|
-
/**
|
|
841
|
-
async
|
|
842
|
-
const
|
|
902
|
+
/** 按会话重算两张汇总表(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
|
|
903
|
+
async rebuildSessionStats(id: SessionId): Promise<void> {
|
|
904
|
+
const tSessionUsage = this.tables["t_session_usage"];
|
|
905
|
+
const tSessionCounts = this.tables["t_session_counts"];
|
|
843
906
|
const tSessionEvents = this.tables["t_session_events"];
|
|
907
|
+
const tEventUsage = this.tables["t_event_usage"];
|
|
844
908
|
const tEvents = this.tables["t_events"];
|
|
845
|
-
await this.db.execute(sql`DELETE FROM ${
|
|
909
|
+
await this.db.execute(sql`DELETE FROM ${tSessionUsage} WHERE f_session_id = ${id}`);
|
|
910
|
+
await this.db.execute(sql`
|
|
911
|
+
INSERT INTO ${tSessionUsage} (
|
|
912
|
+
f_session_id, f_day, f_provider, f_model,
|
|
913
|
+
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
914
|
+
SELECT b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, ''),
|
|
915
|
+
sum(u.f_input_tokens), sum(u.f_output_tokens), sum(u.f_cache_read_tokens),
|
|
916
|
+
sum(u.f_reasoning_tokens), sum(u.f_total_tokens)
|
|
917
|
+
FROM ${tSessionEvents} b
|
|
918
|
+
JOIN ${tEventUsage} u ON u.f_event_id = b.f_event_id
|
|
919
|
+
WHERE b.f_session_id = ${id}
|
|
920
|
+
GROUP BY 1, 2, 3, 4
|
|
921
|
+
`);
|
|
922
|
+
await this.db.execute(sql`DELETE FROM ${tSessionCounts} WHERE f_session_id = ${id}`);
|
|
923
|
+
await this.db.execute(sql`
|
|
924
|
+
INSERT INTO ${tSessionCounts} (
|
|
925
|
+
f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
|
|
926
|
+
SELECT b.f_session_id,
|
|
927
|
+
to_char(to_timestamp(e.f_created_at / 1000.0), 'YYYY-MM-DD'),
|
|
928
|
+
sum(CASE WHEN e.f_type = 'turn/start' THEN 1 ELSE 0 END),
|
|
929
|
+
sum(CASE WHEN e.f_type = 'step/start' THEN 1 ELSE 0 END),
|
|
930
|
+
sum(CASE WHEN e.f_type = 'user/message' THEN 1 ELSE 0 END),
|
|
931
|
+
sum(CASE WHEN e.f_type = 'tool/call' THEN 1 ELSE 0 END)
|
|
932
|
+
FROM ${tSessionEvents} b
|
|
933
|
+
JOIN ${tEvents} e ON e.f_event_id = b.f_event_id
|
|
934
|
+
WHERE b.f_session_id = ${id} AND e.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
|
|
935
|
+
GROUP BY 1, 2
|
|
936
|
+
`);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/** 全量重算用量行的引用标记(rewind / fork / 会话删除之后)。 */
|
|
940
|
+
async refreshEventUsageFlags(): Promise<void> {
|
|
941
|
+
const tEventUsage = this.tables["t_event_usage"];
|
|
942
|
+
const tSessionEvents = this.tables["t_session_events"];
|
|
943
|
+
const tSessions = this.tables["t_sessions"];
|
|
846
944
|
await this.db.execute(sql`
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
WHERE ${tSessionEvents}.f_session_id = ${id}
|
|
855
|
-
AND ${tEvents}.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
|
|
856
|
-
GROUP BY 1, 2, 3
|
|
945
|
+
UPDATE ${tEventUsage} SET
|
|
946
|
+
f_referenced = (EXISTS (SELECT 1 FROM ${tSessionEvents} rb
|
|
947
|
+
WHERE rb.f_event_id = ${tEventUsage}.f_event_id))::int,
|
|
948
|
+
f_subagent = (EXISTS (SELECT 1 FROM ${tSessionEvents} sb
|
|
949
|
+
JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
|
|
950
|
+
WHERE sb.f_event_id = ${tEventUsage}.f_event_id
|
|
951
|
+
AND ss.f_origin = 'subagent'))::int
|
|
857
952
|
`);
|
|
858
953
|
}
|
|
859
954
|
|
|
860
|
-
async
|
|
861
|
-
|
|
862
|
-
|
|
955
|
+
async deleteSessionStats(id: SessionId): Promise<void> {
|
|
956
|
+
await this.db.execute(
|
|
957
|
+
sql`DELETE FROM ${this.tables["t_session_usage"]} WHERE f_session_id = ${id}`,
|
|
958
|
+
);
|
|
959
|
+
await this.db.execute(
|
|
960
|
+
sql`DELETE FROM ${this.tables["t_session_counts"]} WHERE f_session_id = ${id}`,
|
|
961
|
+
);
|
|
863
962
|
}
|
|
864
963
|
|
|
865
964
|
private async getPrevBridge(
|
package/src/rows.ts
CHANGED
|
@@ -92,7 +92,8 @@ export function registerSessionRows(ctx: Context, backend: Backend): void {
|
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
-
const page =
|
|
95
|
+
const page =
|
|
96
|
+
Number.isSafeInteger(envelope.page) && (envelope.page ?? 0) > 0 ? envelope.page! : 1;
|
|
96
97
|
const requested = envelope.pageSize;
|
|
97
98
|
const pageSize =
|
|
98
99
|
Number.isSafeInteger(requested) && (requested ?? 0) > 0
|
package/src/schema.ts
CHANGED
|
@@ -32,6 +32,10 @@ export const tSessionProjcacheRows = sqliteTables["t_session_projcache_row"]!;
|
|
|
32
32
|
|
|
33
33
|
export const tEventUsage = sqliteTables["t_event_usage"]!;
|
|
34
34
|
|
|
35
|
+
export const tSessionUsage = sqliteTables["t_session_usage"]!;
|
|
36
|
+
|
|
37
|
+
export const tSessionCounts = sqliteTables["t_session_counts"]!;
|
|
38
|
+
|
|
35
39
|
export type { SessionRow } from "./backend.ts";
|
|
36
40
|
|
|
37
41
|
export type { EventRow } from "./backend.ts";
|