@morlay/session-rdb 0.0.21-alpha.7 → 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 +1 -1
- package/drizzle/postgres/20260923120000_v3_usage_materialized/migration.sql +48 -0
- package/drizzle/sqlite/20260923120000_v3_usage_materialized/migration.sql +48 -0
- package/package.json +20 -20
- 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/index.ts
CHANGED
|
@@ -41,11 +41,13 @@ import { sessionFormatLogFilename } from "@deepseek-ai/dsh-session-format";
|
|
|
41
41
|
import {
|
|
42
42
|
type Backend,
|
|
43
43
|
type BackendTx,
|
|
44
|
-
type EventCountBucket,
|
|
45
44
|
type EventInsert,
|
|
45
|
+
type SessionCountBucket,
|
|
46
|
+
type SessionUsageBucket,
|
|
46
47
|
} from "./backend.ts";
|
|
47
48
|
import { WriteGuard } from "./write-guard.ts";
|
|
48
|
-
import { repairReadView, rowToMeta, scanRows, toJsonlArtifact } from "./log.ts";
|
|
49
|
+
import { repairReadView, rowToMeta, scanRows, toJsonlArtifact, usageRowOf } from "./log.ts";
|
|
50
|
+
import type { EventUsageRow } from "./log.ts";
|
|
49
51
|
import {
|
|
50
52
|
DEFAULT_BUSY_TIMEOUT_MS,
|
|
51
53
|
eventDimensions,
|
|
@@ -61,11 +63,16 @@ import { registerSessionDeletion } from "./deletion.ts";
|
|
|
61
63
|
import { registerSessionExport } from "./export.ts";
|
|
62
64
|
import { registerSessionGc } from "./gc.ts";
|
|
63
65
|
import { registerSessionRows } from "./rows.ts";
|
|
64
|
-
import {
|
|
66
|
+
import {
|
|
67
|
+
COUNTED_EVENT_TYPES,
|
|
68
|
+
addActivityCount,
|
|
69
|
+
localDayKey,
|
|
70
|
+
registerSessionUsage,
|
|
71
|
+
} from "./usage.ts";
|
|
65
72
|
import type { UsageAggregate } from "./usage.ts";
|
|
66
73
|
import { SessionQueryRdb } from "./session-query.ts";
|
|
67
74
|
import { adoptLegacyRows, convertLegacyRows, isLegacyVersion } from "./legacy.ts";
|
|
68
|
-
import {
|
|
75
|
+
import { needsShapeAdoption, sealOwnEvents } from "./log.ts";
|
|
69
76
|
import { installStorageTakeover } from "./storage-takeover/index.ts";
|
|
70
77
|
|
|
71
78
|
/** 一批事件里的最大时间:写路径据此推进会话行的「最后活动时间」。 */
|
|
@@ -754,7 +761,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
754
761
|
this.liveFailures.delete(id);
|
|
755
762
|
this.liveDropWarned.delete(id);
|
|
756
763
|
this.reuseEventIds.delete(id);
|
|
757
|
-
await this.
|
|
764
|
+
await this.dropSessionStats(id);
|
|
758
765
|
}
|
|
759
766
|
|
|
760
767
|
/** GC 通道:回收已无桥接行引用的事件行(孤儿),返回删除行数。 */
|
|
@@ -778,7 +785,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
778
785
|
this.liveFailures.delete(id);
|
|
779
786
|
this.liveDropWarned.delete(id);
|
|
780
787
|
this.reuseEventIds.delete(id);
|
|
781
|
-
await this.
|
|
788
|
+
await this.dropSessionStats(id);
|
|
782
789
|
}
|
|
783
790
|
return deleted;
|
|
784
791
|
}
|
|
@@ -854,10 +861,14 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
854
861
|
await this.ready;
|
|
855
862
|
if (events.length === 0) return false;
|
|
856
863
|
|
|
864
|
+
// 本仓库自造的事件类型落库前补上 `ignorable` 信封:不补的话上游那道校验连写入都拒。
|
|
865
|
+
sealOwnEvents(events);
|
|
857
866
|
validateStoredEvents(meta, [...events]);
|
|
858
867
|
|
|
859
868
|
const reuse = this.reuseEventIds.get(meta.id);
|
|
860
869
|
let confirmedHead = -1;
|
|
870
|
+
// 本批用量行由 `appendEventTail` 折好;事务成功后旁路据此累加 token 汇总表。
|
|
871
|
+
let usageRows: EventUsageRow[] = [];
|
|
861
872
|
await this.backend.transaction(async (tx) => {
|
|
862
873
|
if (tornTruncateTo !== undefined) {
|
|
863
874
|
await tx.deleteBridgeTail(meta.id, tornTruncateTo);
|
|
@@ -872,16 +883,17 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
872
883
|
const head = await tx.getHead(meta.id);
|
|
873
884
|
|
|
874
885
|
this.writeGuard.assertNoConcurrentWriter(meta.id, head.fHeadSequence);
|
|
875
|
-
const
|
|
886
|
+
const appended = await appendEventTail(
|
|
876
887
|
tx,
|
|
877
888
|
meta,
|
|
878
889
|
events,
|
|
879
890
|
{ parentId: head.fHeadEventId, nextSeq: head.fHeadSequence + 1 },
|
|
880
891
|
reuse,
|
|
881
892
|
);
|
|
882
|
-
|
|
893
|
+
usageRows = appended.usageRows;
|
|
894
|
+
await tx.updateHead(meta.id, appended.headEventId, appended.headSequence);
|
|
883
895
|
await tx.bumpRevision(meta.id, maxEventTime(events));
|
|
884
|
-
confirmedHead = headSequence;
|
|
896
|
+
confirmedHead = appended.headSequence;
|
|
885
897
|
});
|
|
886
898
|
// 事务**成功之后**才丢掉复用映射:失败(并发写者校验、唯一键冲突等)时这份映射还没被消费,
|
|
887
899
|
// 重试同一个 append 还要靠它复用父会话的事件行——提前删掉会让前缀事件行被重新插入一遍。
|
|
@@ -889,7 +901,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
889
901
|
|
|
890
902
|
this.writeGuard.confirmHead(meta.id, confirmedHead);
|
|
891
903
|
this.tracker.materialized(meta.id);
|
|
892
|
-
await this.
|
|
904
|
+
await this.recordSessionStats(meta.id, events, usageRows);
|
|
893
905
|
return true;
|
|
894
906
|
}
|
|
895
907
|
|
|
@@ -959,7 +971,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
959
971
|
const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0);
|
|
960
972
|
// 版本号不完全可信:写路径曾把回退视图的结果以当前版本号落库,于是库里存在「v4 标记 + 旧代形状」
|
|
961
973
|
// 的行——在扫干净的边界之内再按内容判一次(撕裂尾部与坏行已经被 `scanRows` 丢掉了)。
|
|
962
|
-
if (
|
|
974
|
+
if (needsShapeAdoption(preserved)) {
|
|
963
975
|
const adopted = adoptLegacyRows(row, eventRows);
|
|
964
976
|
return {
|
|
965
977
|
meta: adopted.meta,
|
|
@@ -985,39 +997,60 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
985
997
|
}
|
|
986
998
|
|
|
987
999
|
/**
|
|
988
|
-
*
|
|
989
|
-
*
|
|
1000
|
+
* 统计衍生表的**旁路累加**:不在写事务里、失败只 warn——两张会话汇总表都是可销毁重建的
|
|
1001
|
+
* 派生表,丢几次累加不影响可用性(需要时用 `rebuildSessionStats` 重算)。
|
|
1002
|
+
* token 走写路径已折好的用量行,活动计数走本批事件类型。
|
|
990
1003
|
*/
|
|
991
|
-
private async
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1004
|
+
private async recordSessionStats(
|
|
1005
|
+
id: SessionId,
|
|
1006
|
+
events: readonly SessionEvent[],
|
|
1007
|
+
usageRows: readonly EventUsageRow[],
|
|
1008
|
+
): Promise<void> {
|
|
1009
|
+
const usageBuckets = sessionUsageBuckets(usageRows);
|
|
1010
|
+
if (usageBuckets.length > 0) {
|
|
1011
|
+
try {
|
|
1012
|
+
await this.backend.incrementSessionUsage(id, usageBuckets);
|
|
1013
|
+
} catch (error: unknown) {
|
|
1014
|
+
this.ctx.logger.warn(
|
|
1015
|
+
`session-rdb: token usage for "${id}" not updated (${describeError(error)})`,
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
const countBuckets = sessionCountBuckets(events);
|
|
1020
|
+
if (countBuckets.length > 0) {
|
|
1021
|
+
try {
|
|
1022
|
+
await this.backend.incrementSessionCounts(id, countBuckets);
|
|
1023
|
+
} catch (error: unknown) {
|
|
1024
|
+
this.ctx.logger.warn(
|
|
1025
|
+
`session-rdb: activity counts for "${id}" not updated (${describeError(error)})`,
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1000
1028
|
}
|
|
1001
1029
|
}
|
|
1002
1030
|
|
|
1003
|
-
/**
|
|
1004
|
-
|
|
1031
|
+
/**
|
|
1032
|
+
* 重算一个会话的统计(rewind / fork 之后;best-effort,同 `recordSessionStats`):
|
|
1033
|
+
* 两张汇总表按会话重算,用量行的引用标记全量重算(引用可能跨会话消失)。
|
|
1034
|
+
*/
|
|
1035
|
+
async rebuildSessionStats(id: SessionId): Promise<void> {
|
|
1005
1036
|
try {
|
|
1006
|
-
await this.backend.
|
|
1037
|
+
await this.backend.rebuildSessionStats(id);
|
|
1038
|
+
await this.backend.refreshEventUsageFlags();
|
|
1007
1039
|
} catch (error: unknown) {
|
|
1008
1040
|
this.ctx.logger.warn(
|
|
1009
|
-
`session-rdb:
|
|
1041
|
+
`session-rdb: session stats for "${id}" not rebuilt (${describeError(error)})`,
|
|
1010
1042
|
);
|
|
1011
1043
|
}
|
|
1012
1044
|
}
|
|
1013
1045
|
|
|
1014
|
-
/**
|
|
1015
|
-
private async
|
|
1046
|
+
/** 会话删除时清掉它的汇总行,并重算用量行的引用标记(被删会话引用的行要立刻退出统计)。 */
|
|
1047
|
+
private async dropSessionStats(id: SessionId): Promise<void> {
|
|
1016
1048
|
try {
|
|
1017
|
-
await this.backend.
|
|
1049
|
+
await this.backend.deleteSessionStats(id);
|
|
1050
|
+
await this.backend.refreshEventUsageFlags();
|
|
1018
1051
|
} catch (error: unknown) {
|
|
1019
1052
|
this.ctx.logger.warn(
|
|
1020
|
-
`session-rdb:
|
|
1053
|
+
`session-rdb: session stats for "${id}" not deleted (${describeError(error)})`,
|
|
1021
1054
|
);
|
|
1022
1055
|
}
|
|
1023
1056
|
}
|
|
@@ -1039,13 +1072,15 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1039
1072
|
{ meta: log.meta, inheritedEventCount: SessionLogOffset(log.inheritedEventCount) },
|
|
1040
1073
|
randomUUID(),
|
|
1041
1074
|
);
|
|
1042
|
-
const
|
|
1075
|
+
const appended = await appendEventTail(tx, log.meta, log.events, {
|
|
1043
1076
|
parentId: "",
|
|
1044
1077
|
nextSeq: 0,
|
|
1045
1078
|
});
|
|
1046
|
-
await tx.updateHead(id, headEventId, headSequence);
|
|
1079
|
+
await tx.updateHead(id, appended.headEventId, appended.headSequence);
|
|
1047
1080
|
await tx.bumpRevision(id, maxEventTime(log.events));
|
|
1048
1081
|
});
|
|
1082
|
+
// 重写换了事件行:被删掉的那批成了孤儿(引用标记要落回 0),本会话汇总按新事件重算。
|
|
1083
|
+
await this.rebuildSessionStats(id);
|
|
1049
1084
|
}
|
|
1050
1085
|
|
|
1051
1086
|
async listSnapshots(
|
|
@@ -1374,15 +1409,41 @@ function seedCoversPrefix(seed: readonly SessionEvent[], prefix: readonly Sessio
|
|
|
1374
1409
|
);
|
|
1375
1410
|
}
|
|
1376
1411
|
|
|
1377
|
-
/**
|
|
1378
|
-
function
|
|
1379
|
-
const counted = new Map<string,
|
|
1412
|
+
/** 一批事件折成活动计数的桶:只数四种类型,按本地日聚合成四项。 */
|
|
1413
|
+
function sessionCountBuckets(events: readonly SessionEvent[]): SessionCountBucket[] {
|
|
1414
|
+
const counted = new Map<string, SessionCountBucket>();
|
|
1380
1415
|
for (const event of events) {
|
|
1381
1416
|
if (!(COUNTED_EVENT_TYPES as readonly string[]).includes(event.type)) continue;
|
|
1382
1417
|
const day = localDayKey(event.time);
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
|
|
1418
|
+
const bucket = counted.get(day) ?? { day, turns: 0, steps: 0, userInputs: 0, toolCalls: 0 };
|
|
1419
|
+
addActivityCount(bucket, event.type, 1);
|
|
1420
|
+
counted.set(day, bucket);
|
|
1421
|
+
}
|
|
1422
|
+
return [...counted.values()];
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/** 一批用量行折成 token 桶:按「本地日 × 模型」聚合(模型未知落空串,与表口径一致)。 */
|
|
1426
|
+
function sessionUsageBuckets(usageRows: readonly EventUsageRow[]): SessionUsageBucket[] {
|
|
1427
|
+
const counted = new Map<string, SessionUsageBucket>();
|
|
1428
|
+
for (const row of usageRows) {
|
|
1429
|
+
const provider = row.fProvider ?? "";
|
|
1430
|
+
const model = row.fModel ?? "";
|
|
1431
|
+
const key = `${row.fDay}#${provider}#${model}`;
|
|
1432
|
+
const bucket = counted.get(key) ?? {
|
|
1433
|
+
day: row.fDay,
|
|
1434
|
+
provider,
|
|
1435
|
+
model,
|
|
1436
|
+
inputTokens: 0,
|
|
1437
|
+
outputTokens: 0,
|
|
1438
|
+
cacheReadTokens: 0,
|
|
1439
|
+
reasoningTokens: 0,
|
|
1440
|
+
totalTokens: 0,
|
|
1441
|
+
};
|
|
1442
|
+
bucket.inputTokens += row.fInputTokens;
|
|
1443
|
+
bucket.outputTokens += row.fOutputTokens;
|
|
1444
|
+
bucket.cacheReadTokens += row.fCacheReadTokens;
|
|
1445
|
+
bucket.reasoningTokens += row.fReasoningTokens;
|
|
1446
|
+
bucket.totalTokens += row.fTotalTokens;
|
|
1386
1447
|
counted.set(key, bucket);
|
|
1387
1448
|
}
|
|
1388
1449
|
return [...counted.values()];
|
|
@@ -1424,11 +1485,12 @@ async function appendEventTail(
|
|
|
1424
1485
|
events: readonly SessionEvent[],
|
|
1425
1486
|
anchor: { parentId: string; nextSeq: number },
|
|
1426
1487
|
reuse?: ReadonlyMap<number, string>,
|
|
1427
|
-
): Promise<{ headEventId: string; headSequence: number }> {
|
|
1488
|
+
): Promise<{ headEventId: string; headSequence: number; usageRows: EventUsageRow[] }> {
|
|
1428
1489
|
let parentId = anchor.parentId;
|
|
1429
1490
|
let nextSeq = anchor.nextSeq;
|
|
1430
1491
|
|
|
1431
1492
|
const eventRows: EventInsert[] = [];
|
|
1493
|
+
const usageRows: EventUsageRow[] = [];
|
|
1432
1494
|
const bridgeRows: Array<{
|
|
1433
1495
|
fSessionId: SessionId;
|
|
1434
1496
|
fEventId: string;
|
|
@@ -1447,7 +1509,7 @@ async function appendEventTail(
|
|
|
1447
1509
|
sourceEventSeqs?: unknown;
|
|
1448
1510
|
};
|
|
1449
1511
|
const { data, surfaceOp: _surfaceOp, sourceEventSeqs: _sourceEventSeqs, ...envelope } = raw;
|
|
1450
|
-
|
|
1512
|
+
const row: EventInsert = {
|
|
1451
1513
|
fEventId: eventId,
|
|
1452
1514
|
fParentId: parentId,
|
|
1453
1515
|
fType: event.type,
|
|
@@ -1458,7 +1520,11 @@ async function appendEventTail(
|
|
|
1458
1520
|
fEncoding: EVENT_ENCODING,
|
|
1459
1521
|
fData: JSON.stringify({ ...envelope, data }),
|
|
1460
1522
|
fCreatedAt: event.time,
|
|
1461
|
-
}
|
|
1523
|
+
};
|
|
1524
|
+
eventRows.push(row);
|
|
1525
|
+
// 用量行在这里一次折好(读侧维度一起物化),后端只负责插入,事务后旁路再据此累加汇总表。
|
|
1526
|
+
const usageRow = usageRowOf(row, meta.origin === "subagent");
|
|
1527
|
+
if (usageRow !== undefined) usageRows.push(usageRow);
|
|
1462
1528
|
}
|
|
1463
1529
|
const surfaceOp =
|
|
1464
1530
|
(event as SessionEvent<SurfaceEventType>).surfaceOp === undefined
|
|
@@ -1473,13 +1539,13 @@ async function appendEventTail(
|
|
|
1473
1539
|
parentId = eventId;
|
|
1474
1540
|
nextSeq++;
|
|
1475
1541
|
}
|
|
1476
|
-
if (eventRows.length > 0) await tx.insertEvents(eventRows);
|
|
1542
|
+
if (eventRows.length > 0) await tx.insertEvents(eventRows, usageRows);
|
|
1477
1543
|
await tx.insertBridges(bridgeRows);
|
|
1478
1544
|
|
|
1479
1545
|
if (events.some((event) => (event.type as string) === "session/title")) {
|
|
1480
1546
|
await tx.refreshTitle(meta.id);
|
|
1481
1547
|
}
|
|
1482
|
-
return { headEventId: parentId, headSequence: nextSeq - 1 };
|
|
1548
|
+
return { headEventId: parentId, headSequence: nextSeq - 1, usageRows };
|
|
1483
1549
|
}
|
|
1484
1550
|
|
|
1485
1551
|
export default SessionPersistenceRdb;
|
package/src/log.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { SessionFormatEvent, SessionFormatHeader } from "@deepseek-ai/dsh-s
|
|
|
4
4
|
import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
5
5
|
import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
|
|
6
6
|
import type { EventRow, SessionRow } from "./backend.ts";
|
|
7
|
+
import { localDayKey } from "./usage.ts";
|
|
7
8
|
|
|
8
9
|
function normalizeSurfaceOp(value: unknown): SurfaceOp {
|
|
9
10
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
@@ -469,15 +470,15 @@ export function renameLegacyPtcEvents(events: SessionEvent[]): void {
|
|
|
469
470
|
* (v4 起 system 消息只认这个 kind);
|
|
470
471
|
* - `tool/result` 的消息:v3 是 user 角色、结果块包在 `content[0]`;v4 是 `role: 'tool'` + 顶层
|
|
471
472
|
* `toolCallId` + `content` 就是结果块;
|
|
472
|
-
* -
|
|
473
|
-
*
|
|
473
|
+
* - **本仓库自造、上游不认识的事件类型**(见 `OWN_EVENT_TYPES`:历史遗留的与我们当前在写的)标
|
|
474
|
+
* `ignorable: true`——v4 的校验对未知类型只接受可忽略信封。
|
|
474
475
|
*
|
|
475
476
|
* 只认这张白名单:**其它未知类型保持 fail loud**(那是「数据来自更新的 harness」的信号,不能吞掉)。
|
|
476
477
|
*/
|
|
477
478
|
export function normalizeToCurrentShape(events: SessionEvent[]): void {
|
|
478
479
|
for (const event of events) {
|
|
479
480
|
const type = (event as { type: string }).type;
|
|
480
|
-
if (
|
|
481
|
+
if (OWN_EVENT_TYPES.has(type)) {
|
|
481
482
|
(event as { ignorable?: true }).ignorable = true;
|
|
482
483
|
continue;
|
|
483
484
|
}
|
|
@@ -520,17 +521,45 @@ function liftToolResultMessage(message: unknown): void {
|
|
|
520
521
|
}
|
|
521
522
|
|
|
522
523
|
/**
|
|
523
|
-
*
|
|
524
|
-
*
|
|
524
|
+
* 落库前给**本仓库自造的事件类型**补上 `ignorable: true` 信封。
|
|
525
|
+
*
|
|
526
|
+
* 上游的持久化校验(`validateStoredEvents`)只放行「已知类型」或「带可忽略信封的未知类型」,我们的类型
|
|
527
|
+
* 按构造就在已知表之外——不补的话连**写入**都会被拒(`appendBatch` 里那道校验)。上游 `Session.append`
|
|
528
|
+
* 没有承载这个信封的口子(第三个参数只服务 surface 事件),所以这一笔由持久化层补:canonical log 里的
|
|
529
|
+
* 形状不变,落库的那一份带上信封。
|
|
530
|
+
* @param events - 即将落库的批次(`materializeAppendBatch` 的 JSON 快照副本,可写)。
|
|
531
|
+
*/
|
|
532
|
+
export function sealOwnEvents(events: readonly SessionEvent[]): void {
|
|
533
|
+
for (const event of events) {
|
|
534
|
+
if (OWN_EVENT_TYPES.has((event as { type: string }).type)) {
|
|
535
|
+
(event as { ignorable?: true }).ignorable = true;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* 本仓库自造、上游不认识的事件类型:读的时候标 `ignorable: true`,v4 的持久化校验才肯跳过它们
|
|
542
|
+
* (`validateStoredEvents` 只放行「已知类型」或「带可忽略信封的未知类型」)。
|
|
543
|
+
*
|
|
544
|
+
* 两类都在这张表里:
|
|
545
|
+
* - **历史遗留**:写过、后来连同机制一起删除的(`session-branch/version`);
|
|
546
|
+
* - **当前在写**:本仓库的插件自己声明的事件类型(`session-mode/selected`)——上游的
|
|
547
|
+
* `KNOWN_SESSION_EVENT_TYPES` 由上游仓库的声明生成,外部插件的事件按构造就在它之外。
|
|
548
|
+
*
|
|
549
|
+
* 新增自造事件类型时**必须**登记到这里,否则读路径 fail loud(真回归:`session-mode/selected` 漏登记,
|
|
550
|
+
* 会话打不开)。**不要**用「所有未知类型」代替(那会掩盖更新版本的未知事件——那种必须拒读)。
|
|
525
551
|
*/
|
|
526
|
-
export const
|
|
552
|
+
export const OWN_EVENT_TYPES: ReadonlySet<string> = new Set([
|
|
553
|
+
"session-branch/version",
|
|
554
|
+
"session-mode/selected",
|
|
555
|
+
]);
|
|
527
556
|
|
|
528
|
-
/**
|
|
529
|
-
*
|
|
530
|
-
export function
|
|
557
|
+
/** 这批事件是否需要按当前形状归一(旧代消息形状,或尚未标 `ignorable` 的自造事件类型)——写路径曾把回退
|
|
558
|
+
* 视图的结果以当前版本号落库,所以**版本号不可信**,读之前要按内容判一次。 */
|
|
559
|
+
export function needsShapeAdoption(events: readonly SessionEvent[]): boolean {
|
|
531
560
|
for (const event of events) {
|
|
532
561
|
const type = (event as { type: string }).type;
|
|
533
|
-
if (
|
|
562
|
+
if (OWN_EVENT_TYPES.has(type)) return true;
|
|
534
563
|
if (type === "system/message") {
|
|
535
564
|
const message = (event.data as unknown as Record<string, unknown>)["message"] as
|
|
536
565
|
| Record<string, unknown>
|
|
@@ -716,12 +745,15 @@ export function titleOfEventData(data: string): string | undefined {
|
|
|
716
745
|
}
|
|
717
746
|
}
|
|
718
747
|
|
|
719
|
-
/** 用量行的列(`t_event_usage
|
|
748
|
+
/** 用量行的列(`t_event_usage`):读侧维度(本地日与两个归属标记)在写入时一起物化。 */
|
|
720
749
|
export interface EventUsageRow {
|
|
721
750
|
fEventId: string;
|
|
722
751
|
fCreatedAt: number;
|
|
752
|
+
fDay: string;
|
|
723
753
|
fProvider: string | null;
|
|
724
754
|
fModel: string | null;
|
|
755
|
+
fReferenced: number;
|
|
756
|
+
fSubagent: number;
|
|
725
757
|
fInputTokens: number;
|
|
726
758
|
fOutputTokens: number;
|
|
727
759
|
fCacheReadTokens: number;
|
|
@@ -740,15 +772,21 @@ function textField(value: unknown): string | null {
|
|
|
740
772
|
/**
|
|
741
773
|
* 一条事件行的用量(只有带 `usage` 的 `assistant/message` 才有):写路径据此记录
|
|
742
774
|
* `t_event_usage`,统计因此不必逐行解析 JSON。两种 `f_data` 结构(带信封 / 老格式)都认。
|
|
775
|
+
* 读侧维度一起物化:本地日由事件时间算,`f_referenced` 直接 1(这一批事件行与桥接行同事务,
|
|
776
|
+
* 提交即被引用),`f_subagent` 取写入它的会话是否子代理。
|
|
743
777
|
* @param event - 待写入的事件行。
|
|
778
|
+
* @param subagent - 写入它的会话是否 `origin === 'subagent'`。
|
|
744
779
|
* @returns 用量行,或该事件没有用量时的 undefined。
|
|
745
780
|
*/
|
|
746
|
-
export function usageRowOf(
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
781
|
+
export function usageRowOf(
|
|
782
|
+
event: {
|
|
783
|
+
fEventId: string;
|
|
784
|
+
fCreatedAt: number;
|
|
785
|
+
fType: string;
|
|
786
|
+
fData: string;
|
|
787
|
+
},
|
|
788
|
+
subagent: boolean,
|
|
789
|
+
): EventUsageRow | undefined {
|
|
752
790
|
if (event.fType !== "assistant/message") return undefined;
|
|
753
791
|
let parsed: Record<string, unknown>;
|
|
754
792
|
try {
|
|
@@ -763,8 +801,11 @@ export function usageRowOf(event: {
|
|
|
763
801
|
const row: EventUsageRow = {
|
|
764
802
|
fEventId: event.fEventId,
|
|
765
803
|
fCreatedAt: event.fCreatedAt,
|
|
804
|
+
fDay: localDayKey(event.fCreatedAt),
|
|
766
805
|
fProvider: textField(source?.["provider"]),
|
|
767
806
|
fModel: textField(source?.["model"]),
|
|
807
|
+
fReferenced: 1,
|
|
808
|
+
fSubagent: subagent ? 1 : 0,
|
|
768
809
|
fInputTokens: numericField((usage as Record<string, unknown>)["inputTokens"]),
|
|
769
810
|
fOutputTokens: numericField((usage as Record<string, unknown>)["outputTokens"]),
|
|
770
811
|
fCacheReadTokens: numericField((usage as Record<string, unknown>)["cacheReadTokens"]),
|