@morlay/ui-conversation-message-actions 0.0.19-alpha.3 → 0.0.19-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 +4 -3
- package/dist/client.cjs +39 -179
- package/dist/index.d.cts +3 -6
- package/dist/index.d.mts +3 -6
- package/dist/index.mjs +8 -86
- package/dist/{plan-CT_6Y1fD.d.cts → plan-BKgGPZdD.d.mts} +2 -3
- package/dist/{plan-Cm9hCord.d.mts → plan-BUTplrpz.d.cts} +2 -3
- package/dist/plan.d.cts +1 -1
- package/dist/plan.d.mts +1 -1
- package/dist/plan.mjs +2 -49
- package/dist/shared-BBbOTwy4.mjs +4 -0
- package/dist/testing.d.cts +2 -2
- package/dist/testing.d.mts +2 -2
- package/dist/testing.mjs +582 -199
- package/package.json +6 -6
- package/src/client/chat-node/MessageItem.tsx +6 -3
- package/src/client/controller.ts +58 -221
- package/src/client/index.ts +0 -4
- package/src/index.ts +5 -143
- package/src/plan.ts +2 -54
- package/src/shared.ts +1 -96
- package/src/testing.ts +0 -2
- package/src/types.ts +1 -3
- package/dist/shared-LK3nImWW.mjs +0 -55
package/dist/testing.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { t as SESSION_EDITOR_PATH } from "./shared-
|
|
1
|
+
import { t as SESSION_EDITOR_PATH } from "./shared-BBbOTwy4.mjs";
|
|
2
2
|
import { SessionEditor } from "./index.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { Context, Service } from "@deepseek-ai/cordis";
|
|
5
5
|
import { KNOWN_SESSION_EVENT_TYPES, SESSION_FORMAT_VERSION, Session, Session as Session$1, SessionId, SessionId as SessionIdBrand, SessionLogOffset, SessionSeq, SessionSeq as SessionSeq$1, SessionStore, foldSurface, interruptedTurnClosers, isSurfaceEvent, snapshotSessionEvent } from "@deepseek-ai/dsh-session";
|
|
6
|
-
import { SessionBranch, SessionBranchError, balanceRewindPrefix,
|
|
6
|
+
import { SessionBranch, SessionBranchError, balanceRewindPrefix, rewindKeepLength } from "@morlay/session-branch";
|
|
7
7
|
import crypto, { createHash, randomUUID } from "node:crypto";
|
|
8
8
|
import { TokenMeter } from "@deepseek-ai/dsh-token-meter";
|
|
9
9
|
import SessionProjectionRegistry from "@deepseek-ai/dsh-session-projection";
|
|
@@ -18966,7 +18966,36 @@ const SURFACE_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
|
18966
18966
|
"tool/result"
|
|
18967
18967
|
]);
|
|
18968
18968
|
const METERING_EVENT_TYPES = /* @__PURE__ */ new Set(["compaction/summary", "compaction/prune"]);
|
|
18969
|
+
/**
|
|
18970
|
+
* 升序的 surface 事件 seq 索引:读视图修复里的溯源(replace 的 `sourceEventSeqs` 与
|
|
18971
|
+
* metering 的 `shadowedSeqs`)共用这一份,避免每个 replace 各扫一遍全量事件。
|
|
18972
|
+
* 前提是输入按 seq 有序(`readLog` 与 live snapshot 都如此)。
|
|
18973
|
+
*/
|
|
18974
|
+
function surfaceSeqIndex(events) {
|
|
18975
|
+
const seqs = [];
|
|
18976
|
+
for (const event of events) if (SURFACE_EVENT_TYPES.has(event.type)) seqs.push(event.seq);
|
|
18977
|
+
return seqs;
|
|
18978
|
+
}
|
|
18979
|
+
/** `[startSeq, endSeq]` 内的 surface seq(含端点),二分定位后切片。 */
|
|
18980
|
+
function surfaceSeqsInRange(seqs, startSeq, endSeq) {
|
|
18981
|
+
let low = 0;
|
|
18982
|
+
let high = seqs.length;
|
|
18983
|
+
while (low < high) {
|
|
18984
|
+
const mid = low + high >> 1;
|
|
18985
|
+
if (seqs[mid] < startSeq) low = mid + 1;
|
|
18986
|
+
else high = mid;
|
|
18987
|
+
}
|
|
18988
|
+
const from = low;
|
|
18989
|
+
high = seqs.length;
|
|
18990
|
+
while (low < high) {
|
|
18991
|
+
const mid = low + high >> 1;
|
|
18992
|
+
if (seqs[mid] <= endSeq) low = mid + 1;
|
|
18993
|
+
else high = mid;
|
|
18994
|
+
}
|
|
18995
|
+
return seqs.slice(from, low);
|
|
18996
|
+
}
|
|
18969
18997
|
function recomputeReplaceProvenance(events) {
|
|
18998
|
+
const seqs = surfaceSeqIndex(events);
|
|
18970
18999
|
for (let i = 0; i < events.length; i++) {
|
|
18971
19000
|
const raw = events[i];
|
|
18972
19001
|
const op = raw.surfaceOp;
|
|
@@ -18978,9 +19007,7 @@ function recomputeReplaceProvenance(events) {
|
|
|
18978
19007
|
raw.sourceEventSeqs = meteringData.shadowedSeqs;
|
|
18979
19008
|
continue;
|
|
18980
19009
|
}
|
|
18981
|
-
|
|
18982
|
-
for (const candidate of events) if (candidate.seq >= startSeq && candidate.seq <= endSeq && SURFACE_EVENT_TYPES.has(candidate.type)) refs.push(candidate.seq);
|
|
18983
|
-
raw.sourceEventSeqs = refs;
|
|
19010
|
+
raw.sourceEventSeqs = surfaceSeqsInRange(seqs, startSeq, endSeq);
|
|
18984
19011
|
}
|
|
18985
19012
|
}
|
|
18986
19013
|
function isEventSeqLike(value) {
|
|
@@ -19133,6 +19160,7 @@ function repairRequestHeaders(events) {
|
|
|
19133
19160
|
}
|
|
19134
19161
|
}
|
|
19135
19162
|
function syncMeteringRanges(events) {
|
|
19163
|
+
const seqs = surfaceSeqIndex(events);
|
|
19136
19164
|
for (let i = 1; i < events.length; i++) {
|
|
19137
19165
|
const metering = events[i - 1];
|
|
19138
19166
|
if (!METERING_EVENT_TYPES.has(metering.type)) continue;
|
|
@@ -19145,7 +19173,7 @@ function syncMeteringRanges(events) {
|
|
|
19145
19173
|
start: startSeq,
|
|
19146
19174
|
end: endSeq
|
|
19147
19175
|
};
|
|
19148
|
-
data.shadowedSeqs =
|
|
19176
|
+
data.shadowedSeqs = surfaceSeqsInRange(seqs, startSeq, endSeq);
|
|
19149
19177
|
}
|
|
19150
19178
|
}
|
|
19151
19179
|
const PTC_EVENT_RENAMES = {
|
|
@@ -23310,7 +23338,7 @@ const eventUsage = {
|
|
|
23310
23338
|
}
|
|
23311
23339
|
},
|
|
23312
23340
|
f_created_at: {
|
|
23313
|
-
type: "
|
|
23341
|
+
type: "bigint",
|
|
23314
23342
|
notNull: true
|
|
23315
23343
|
},
|
|
23316
23344
|
f_provider: { type: "text" },
|
|
@@ -23676,6 +23704,152 @@ function migrate$1(db, config) {
|
|
|
23676
23704
|
return migrateSync(readMigrationFiles(config), db.session, config);
|
|
23677
23705
|
}
|
|
23678
23706
|
//#endregion
|
|
23707
|
+
//#region ../session-rdb/src/usage.ts
|
|
23708
|
+
const SESSION_USAGE_PATH = "/api/session.usage";
|
|
23709
|
+
const DAY_MS = 864e5;
|
|
23710
|
+
const ROLLING_DAYS = {
|
|
23711
|
+
"7d": 7,
|
|
23712
|
+
"30d": 30,
|
|
23713
|
+
"90d": 90
|
|
23714
|
+
};
|
|
23715
|
+
function localDayStart(now) {
|
|
23716
|
+
const start = new Date(now);
|
|
23717
|
+
start.setHours(0, 0, 0, 0);
|
|
23718
|
+
return start;
|
|
23719
|
+
}
|
|
23720
|
+
/**
|
|
23721
|
+
* 范围起点(含),不限时为 undefined。自然日 / 自然周按 host 的本地时区算,周边界是周一 00:00。
|
|
23722
|
+
* @param range - 语义键。
|
|
23723
|
+
* @param now - 当前时刻。
|
|
23724
|
+
* @returns 起点毫秒时间戳,或 undefined。
|
|
23725
|
+
*/
|
|
23726
|
+
function resolveUsageSince(range, now) {
|
|
23727
|
+
switch (range) {
|
|
23728
|
+
case "all": return;
|
|
23729
|
+
case "day": return localDayStart(now).getTime();
|
|
23730
|
+
case "week": {
|
|
23731
|
+
const start = localDayStart(now);
|
|
23732
|
+
start.setDate(start.getDate() - (start.getDay() + 6) % 7);
|
|
23733
|
+
return start.getTime();
|
|
23734
|
+
}
|
|
23735
|
+
default: return now - ROLLING_DAYS[range] * DAY_MS;
|
|
23736
|
+
}
|
|
23737
|
+
}
|
|
23738
|
+
function parseUsageRange(value) {
|
|
23739
|
+
return value === "day" || value === "week" || value === "7d" || value === "30d" || value === "90d" ? value : "all";
|
|
23740
|
+
}
|
|
23741
|
+
/** 计入活动计数的事件类型(轮次 / 步骤 / 用户输入 / 工具调用)。 */
|
|
23742
|
+
const COUNTED_EVENT_TYPES = [
|
|
23743
|
+
"turn/start",
|
|
23744
|
+
"step/start",
|
|
23745
|
+
"user/message",
|
|
23746
|
+
"tool/call"
|
|
23747
|
+
];
|
|
23748
|
+
/** 按事件类型把计数累加到活动指标上(表里按类型存,读的时候折成四项)。 */
|
|
23749
|
+
function addActivityCount(target, type, count) {
|
|
23750
|
+
switch (type) {
|
|
23751
|
+
case "turn/start":
|
|
23752
|
+
target.turns += count;
|
|
23753
|
+
return target;
|
|
23754
|
+
case "step/start":
|
|
23755
|
+
target.steps += count;
|
|
23756
|
+
return target;
|
|
23757
|
+
case "user/message":
|
|
23758
|
+
target.userInputs += count;
|
|
23759
|
+
return target;
|
|
23760
|
+
case "tool/call":
|
|
23761
|
+
target.toolCalls += count;
|
|
23762
|
+
return target;
|
|
23763
|
+
default: return target;
|
|
23764
|
+
}
|
|
23765
|
+
}
|
|
23766
|
+
/** 毫秒时间戳 → host 本地日(`YYYY-MM-DD`,与 SQL 的 localtime 口径一致)。 */
|
|
23767
|
+
function localDayKey(ms) {
|
|
23768
|
+
const date = new Date(ms);
|
|
23769
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
23770
|
+
return `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
23771
|
+
}
|
|
23772
|
+
function emptyTotals() {
|
|
23773
|
+
return {
|
|
23774
|
+
turns: 0,
|
|
23775
|
+
steps: 0,
|
|
23776
|
+
userInputs: 0,
|
|
23777
|
+
toolCalls: 0,
|
|
23778
|
+
inputTokens: 0,
|
|
23779
|
+
outputTokens: 0,
|
|
23780
|
+
cacheReadTokens: 0,
|
|
23781
|
+
reasoningTokens: 0,
|
|
23782
|
+
totalTokens: 0
|
|
23783
|
+
};
|
|
23784
|
+
}
|
|
23785
|
+
function addTokenTotals(target, row) {
|
|
23786
|
+
target.inputTokens += row.inputTokens;
|
|
23787
|
+
target.outputTokens += row.outputTokens;
|
|
23788
|
+
target.cacheReadTokens += row.cacheReadTokens;
|
|
23789
|
+
target.reasoningTokens += row.reasoningTokens;
|
|
23790
|
+
target.totalTokens += row.totalTokens;
|
|
23791
|
+
return target;
|
|
23792
|
+
}
|
|
23793
|
+
function addActivityTotals(target, row) {
|
|
23794
|
+
target.turns += row.turns;
|
|
23795
|
+
target.steps += row.steps;
|
|
23796
|
+
target.userInputs += row.userInputs;
|
|
23797
|
+
target.toolCalls += row.toolCalls;
|
|
23798
|
+
return target;
|
|
23799
|
+
}
|
|
23800
|
+
/** 把一行的整套指标累加进目标(后端合并「是否子代理」两组时用)。 */
|
|
23801
|
+
function addTotals(target, row) {
|
|
23802
|
+
addActivityTotals(target, row);
|
|
23803
|
+
addTokenTotals(target, row);
|
|
23804
|
+
return target;
|
|
23805
|
+
}
|
|
23806
|
+
/**
|
|
23807
|
+
* 用量统计通道:一次请求回报总览(含 subagent 拆分)、按天 × 模型的桶与按会话的行。
|
|
23808
|
+
* 聚合只算被会话引用的事件行——fork 共享行因此只计一次,已删会话留下的孤儿行不计。
|
|
23809
|
+
*/
|
|
23810
|
+
function registerSessionUsage(ctx, persistence) {
|
|
23811
|
+
ctx.inject(["webServer", "connection"], (webCtx) => {
|
|
23812
|
+
const webServer = webCtx.webServer;
|
|
23813
|
+
const connection = webCtx.get("connection");
|
|
23814
|
+
return webCtx.effect(() => webServer.register({
|
|
23815
|
+
kind: "exact",
|
|
23816
|
+
path: SESSION_USAGE_PATH,
|
|
23817
|
+
handler: async (req, res) => {
|
|
23818
|
+
const rejection = connection.requestRejection(req);
|
|
23819
|
+
if (rejection !== void 0) {
|
|
23820
|
+
res.writeHead(rejection);
|
|
23821
|
+
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
23822
|
+
return;
|
|
23823
|
+
}
|
|
23824
|
+
if (req.method !== "POST") {
|
|
23825
|
+
res.writeHead(405, { "content-type": "application/json" });
|
|
23826
|
+
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
23827
|
+
return;
|
|
23828
|
+
}
|
|
23829
|
+
const chunks = [];
|
|
23830
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
23831
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
23832
|
+
let envelope = {};
|
|
23833
|
+
if (raw !== "") try {
|
|
23834
|
+
envelope = JSON.parse(raw);
|
|
23835
|
+
} catch {
|
|
23836
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
23837
|
+
res.end(JSON.stringify({ error: "request body is not JSON" }));
|
|
23838
|
+
return;
|
|
23839
|
+
}
|
|
23840
|
+
try {
|
|
23841
|
+
const report = await persistence.usageReport(resolveUsageSince(parseUsageRange(envelope.range), Date.now()));
|
|
23842
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
23843
|
+
res.end(JSON.stringify(report));
|
|
23844
|
+
} catch (error) {
|
|
23845
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
23846
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : "usage report failed" }));
|
|
23847
|
+
}
|
|
23848
|
+
}
|
|
23849
|
+
}), `session-rdb: ${SESSION_USAGE_PATH} route`);
|
|
23850
|
+
});
|
|
23851
|
+
}
|
|
23852
|
+
//#endregion
|
|
23679
23853
|
//#region ../session-rdb/src/storage-takeover/repository.ts
|
|
23680
23854
|
async function allRows(query) {
|
|
23681
23855
|
const q = query;
|
|
@@ -23926,6 +24100,32 @@ function createStorageRepository(host) {
|
|
|
23926
24100
|
}
|
|
23927
24101
|
//#endregion
|
|
23928
24102
|
//#region ../session-rdb/src/sqlite.ts
|
|
24103
|
+
/** 活动计数的派生表:可销毁重建(不在 drizzle schema / 迁移里,建表幂等)。 */
|
|
24104
|
+
function ensureEventCountsTable(db) {
|
|
24105
|
+
db.exec(`CREATE TABLE IF NOT EXISTS t_event_counts (
|
|
24106
|
+
f_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
24107
|
+
f_session_id TEXT NOT NULL,
|
|
24108
|
+
f_day TEXT NOT NULL,
|
|
24109
|
+
f_type TEXT NOT NULL,
|
|
24110
|
+
f_count INTEGER NOT NULL DEFAULT 0
|
|
24111
|
+
);
|
|
24112
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_event_counts_session_day_type
|
|
24113
|
+
ON t_event_counts(f_session_id, f_day, f_type);
|
|
24114
|
+
CREATE INDEX IF NOT EXISTS idx_event_counts_day ON t_event_counts(f_day);`);
|
|
24115
|
+
}
|
|
24116
|
+
/** 一次性回填活动计数(表为空时从事件表重算;幂等)。 */
|
|
24117
|
+
function backfillEventCounts(db) {
|
|
24118
|
+
if (db.prepare("SELECT count(*) AS n FROM t_event_counts").get().n > 0) return;
|
|
24119
|
+
db.exec(`INSERT OR IGNORE INTO t_event_counts (f_session_id, f_day, f_type, f_count)
|
|
24120
|
+
SELECT b.f_session_id,
|
|
24121
|
+
date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
|
|
24122
|
+
e.f_type,
|
|
24123
|
+
count(*)
|
|
24124
|
+
FROM t_session_events b
|
|
24125
|
+
JOIN t_events e ON e.f_event_id = b.f_event_id
|
|
24126
|
+
WHERE e.f_type IN (${COUNTED_EVENT_TYPE_SQL$1})
|
|
24127
|
+
GROUP BY 1, 2, 3`);
|
|
24128
|
+
}
|
|
23929
24129
|
/**
|
|
23930
24130
|
* 一次性回填:旧库的用量从未写过 `t_event_usage`,建表后按事件行补一次(幂等:表非空即跳过)。
|
|
23931
24131
|
*/
|
|
@@ -23955,9 +24155,8 @@ function backfillEventUsage(db) {
|
|
|
23955
24155
|
OR json_extract(e.f_data, '$.usage') IS NOT NULL)`);
|
|
23956
24156
|
}
|
|
23957
24157
|
/** SQL 的缺失求和是 NULL:没有 usage 字段的行使该字段计 0。 */
|
|
23958
|
-
function
|
|
24158
|
+
function tokenTotalsOf$1(row) {
|
|
23959
24159
|
return {
|
|
23960
|
-
events: row.events,
|
|
23961
24160
|
inputTokens: row.input_tokens ?? 0,
|
|
23962
24161
|
outputTokens: row.output_tokens ?? 0,
|
|
23963
24162
|
cacheReadTokens: row.cache_read_tokens ?? 0,
|
|
@@ -23965,6 +24164,26 @@ function totalsOf(row) {
|
|
|
23965
24164
|
totalTokens: row.total_tokens ?? 0
|
|
23966
24165
|
};
|
|
23967
24166
|
}
|
|
24167
|
+
/** 一行的整套指标:token 用量 + 活动计数(轮次 / 步骤 / 用户输入 / 工具调用)。 */
|
|
24168
|
+
function emptyActivity$1() {
|
|
24169
|
+
return {
|
|
24170
|
+
turns: 0,
|
|
24171
|
+
steps: 0,
|
|
24172
|
+
userInputs: 0,
|
|
24173
|
+
toolCalls: 0
|
|
24174
|
+
};
|
|
24175
|
+
}
|
|
24176
|
+
/** 有 token 用量的会话:活动计数与列表都限定在这一批会话里,口径一致。 */
|
|
24177
|
+
function hasUsageSession(column) {
|
|
24178
|
+
return `EXISTS (SELECT 1 FROM t_session_events ub
|
|
24179
|
+
JOIN t_event_usage uu ON uu.f_event_id = ub.f_event_id
|
|
24180
|
+
WHERE ub.f_session_id = ${column})`;
|
|
24181
|
+
}
|
|
24182
|
+
const SUBAGENT_OF_EVENT = `EXISTS (SELECT 1 FROM t_session_events sb
|
|
24183
|
+
JOIN t_sessions ss ON ss.f_session_id = sb.f_session_id
|
|
24184
|
+
WHERE sb.f_event_id = %EVENT% AND ss.f_origin = 'subagent')`;
|
|
24185
|
+
/** `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。 */
|
|
24186
|
+
const COUNTED_EVENT_TYPE_SQL$1 = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
|
|
23968
24187
|
const sqliteMigrationsDir = fileURLToPath(new URL("../drizzle/sqlite/", import.meta.url));
|
|
23969
24188
|
const sqliteTxQueues = /* @__PURE__ */ new Map();
|
|
23970
24189
|
function enqueueSqliteTx(path, fn) {
|
|
@@ -23987,6 +24206,8 @@ async function openDatabase(path, journalMode, busyTimeout = DEFAULT_BUSY_TIMEOU
|
|
|
23987
24206
|
const { user_version: onDisk } = db.prepare("PRAGMA user_version").get();
|
|
23988
24207
|
if (onDisk === 2 && !hasMigrationsTable(db)) await baselineV2(db);
|
|
23989
24208
|
migrate$1(dbx, { migrationsFolder: sqliteMigrationsDir });
|
|
24209
|
+
ensureEventCountsTable(db);
|
|
24210
|
+
backfillEventCounts(db);
|
|
23990
24211
|
backfillEventUsage(db);
|
|
23991
24212
|
dbx.insert(tPersistenceState).values({
|
|
23992
24213
|
fSingleton: 1,
|
|
@@ -24127,10 +24348,13 @@ var SqliteBackend = class SqliteBackend {
|
|
|
24127
24348
|
return (fromSequence === void 0 ? this.eventRows().where(eq(tSessionEvents.fSessionId, id)) : this.eventRows().where(and(eq(tSessionEvents.fSessionId, id), gte(tSessionEvents.fSequence, fromSequence)))).orderBy(tSessionEvents.fSequence).all();
|
|
24128
24349
|
}
|
|
24129
24350
|
async getEventTypeAt(id, sequence) {
|
|
24130
|
-
return this.
|
|
24351
|
+
return this.db.select({ fType: tEvents.fType }).from(tSessionEvents).innerJoin(tEvents, eq(tSessionEvents.fEventId, tEvents.fEventId)).where(and(eq(tSessionEvents.fSessionId, id), eq(tSessionEvents.fSequence, sequence))).get()?.fType;
|
|
24131
24352
|
}
|
|
24132
24353
|
async getEventTypesBefore(id, beforeSequence, limit) {
|
|
24133
|
-
return this.
|
|
24354
|
+
return this.db.select({
|
|
24355
|
+
fSequence: tSessionEvents.fSequence,
|
|
24356
|
+
fType: tEvents.fType
|
|
24357
|
+
}).from(tSessionEvents).innerJoin(tEvents, eq(tSessionEvents.fEventId, tEvents.fEventId)).where(and(eq(tSessionEvents.fSessionId, id), lt(tSessionEvents.fSequence, beforeSequence))).orderBy(desc(tSessionEvents.fSequence)).limit(limit).all();
|
|
24134
24358
|
}
|
|
24135
24359
|
async listSessions() {
|
|
24136
24360
|
return this.db.select().from(tSessions).all();
|
|
@@ -24258,30 +24482,49 @@ var SqliteBackend = class SqliteBackend {
|
|
|
24258
24482
|
const info = this.db.delete(tSessions).where(inArray(tSessions.fSessionId, ids)).run();
|
|
24259
24483
|
return Number(info.changes);
|
|
24260
24484
|
}
|
|
24261
|
-
/**
|
|
24485
|
+
/**
|
|
24486
|
+
* 用量聚合:token 用量沿 `t_event_usage` 的用量行,活动计数(轮次 / 步骤 / 用户输入 / 工具调用)
|
|
24487
|
+
* 沿 `t_events` 的事件类型数——两者限定在**有 token 用量的会话**里,时间范围各自按自己的
|
|
24488
|
+
* `f_created_at`(同一时刻写入,口径一致)。
|
|
24489
|
+
*/
|
|
24262
24490
|
async usageReport(sinceMs) {
|
|
24263
|
-
const
|
|
24264
|
-
const
|
|
24491
|
+
const usageSince = sinceMs === void 0 ? "" : " AND u.f_created_at >= ?";
|
|
24492
|
+
const params = sinceMs === void 0 ? [] : [sinceMs];
|
|
24493
|
+
const daySince = sinceMs === void 0 ? "" : " AND c.f_day >= ?";
|
|
24494
|
+
const dayParams = sinceMs === void 0 ? [] : [localDayKey(sinceMs)];
|
|
24495
|
+
const subagentOfUsage = SUBAGENT_OF_EVENT.replace("%EVENT%", "u.f_event_id");
|
|
24265
24496
|
const buckets = this.db.$client.prepare(`SELECT date(u.f_created_at / 1000, 'unixepoch', 'localtime') AS day,
|
|
24266
24497
|
u.f_provider AS provider,
|
|
24267
24498
|
u.f_model AS model,
|
|
24268
|
-
|
|
24269
|
-
|
|
24270
|
-
|
|
24271
|
-
|
|
24499
|
+
${subagentOfUsage} AS subagent,
|
|
24500
|
+
sum(u.f_input_tokens) AS input_tokens,
|
|
24501
|
+
sum(u.f_output_tokens) AS output_tokens,
|
|
24502
|
+
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
24503
|
+
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
24504
|
+
sum(u.f_total_tokens) AS total_tokens
|
|
24505
|
+
FROM t_event_usage u
|
|
24506
|
+
WHERE EXISTS (SELECT 1 FROM t_session_events rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
|
|
24507
|
+
GROUP BY day, provider, model, subagent`).all(...params);
|
|
24508
|
+
const tokenScope = this.db.$client.prepare(`SELECT ${subagentOfUsage} AS subagent,
|
|
24272
24509
|
sum(u.f_input_tokens) AS input_tokens,
|
|
24273
24510
|
sum(u.f_output_tokens) AS output_tokens,
|
|
24274
24511
|
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
24275
24512
|
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
24276
24513
|
sum(u.f_total_tokens) AS total_tokens
|
|
24277
24514
|
FROM t_event_usage u
|
|
24278
|
-
WHERE EXISTS (SELECT 1 FROM t_session_events rb WHERE rb.f_event_id = u.f_event_id)${
|
|
24279
|
-
GROUP BY
|
|
24280
|
-
const
|
|
24515
|
+
WHERE EXISTS (SELECT 1 FROM t_session_events rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
|
|
24516
|
+
GROUP BY subagent`).all(...params);
|
|
24517
|
+
const activityScope = this.db.$client.prepare(`SELECT (s.f_origin = 'subagent') AS subagent,
|
|
24518
|
+
c.f_type AS f_type,
|
|
24519
|
+
sum(c.f_count) AS n
|
|
24520
|
+
FROM t_event_counts c
|
|
24521
|
+
JOIN t_sessions s ON s.f_session_id = c.f_session_id
|
|
24522
|
+
WHERE ${hasUsageSession("c.f_session_id")}${daySince}
|
|
24523
|
+
GROUP BY subagent, c.f_type`).all(...dayParams);
|
|
24524
|
+
const tokenSessions = this.db.$client.prepare(`SELECT b.f_session_id AS session_id,
|
|
24281
24525
|
s.f_title AS title,
|
|
24282
24526
|
(s.f_origin = 'subagent') AS subagent,
|
|
24283
24527
|
(s.f_archived_at IS NOT NULL) AS archived,
|
|
24284
|
-
count(*) AS events,
|
|
24285
24528
|
sum(u.f_input_tokens) AS input_tokens,
|
|
24286
24529
|
sum(u.f_output_tokens) AS output_tokens,
|
|
24287
24530
|
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
@@ -24290,25 +24533,69 @@ var SqliteBackend = class SqliteBackend {
|
|
|
24290
24533
|
FROM t_session_events b
|
|
24291
24534
|
JOIN t_event_usage u ON u.f_event_id = b.f_event_id
|
|
24292
24535
|
JOIN t_sessions s ON s.f_session_id = b.f_session_id
|
|
24293
|
-
WHERE 1 = 1${
|
|
24294
|
-
GROUP BY b.f_session_id, s.f_title, s.f_origin, s.f_archived_at`).all(...
|
|
24536
|
+
WHERE 1 = 1${usageSince}
|
|
24537
|
+
GROUP BY b.f_session_id, s.f_title, s.f_origin, s.f_archived_at`).all(...params);
|
|
24538
|
+
const activitySessions = this.db.$client.prepare(`SELECT c.f_session_id AS session_id,
|
|
24539
|
+
c.f_type AS f_type,
|
|
24540
|
+
sum(c.f_count) AS n
|
|
24541
|
+
FROM t_event_counts c
|
|
24542
|
+
WHERE ${hasUsageSession("c.f_session_id")}${daySince}
|
|
24543
|
+
GROUP BY c.f_session_id, c.f_type`).all(...dayParams);
|
|
24544
|
+
const human = emptyTotals();
|
|
24545
|
+
const subagent = emptyTotals();
|
|
24546
|
+
for (const row of tokenScope) addTokenTotals(row.subagent === 1 ? subagent : human, tokenTotalsOf$1(row));
|
|
24547
|
+
for (const row of activityScope) addActivityCount(row.subagent === 1 ? subagent : human, row.f_type, row.n ?? 0);
|
|
24548
|
+
const totals = addTotals(addTotals(emptyTotals(), human), subagent);
|
|
24549
|
+
const activityBySession = /* @__PURE__ */ new Map();
|
|
24550
|
+
for (const row of activitySessions) {
|
|
24551
|
+
const target = activityBySession.get(row.session_id) ?? emptyActivity$1();
|
|
24552
|
+
addActivityCount(target, row.f_type, row.n ?? 0);
|
|
24553
|
+
activityBySession.set(row.session_id, target);
|
|
24554
|
+
}
|
|
24295
24555
|
return {
|
|
24556
|
+
totals,
|
|
24557
|
+
subagent,
|
|
24558
|
+
human,
|
|
24296
24559
|
buckets: buckets.map((row) => ({
|
|
24297
24560
|
day: row.day,
|
|
24298
24561
|
provider: row.provider,
|
|
24299
24562
|
model: row.model,
|
|
24300
24563
|
subagent: row.subagent === 1,
|
|
24301
|
-
...
|
|
24564
|
+
...tokenTotalsOf$1(row)
|
|
24302
24565
|
})),
|
|
24303
|
-
sessions:
|
|
24566
|
+
sessions: tokenSessions.map((row) => ({
|
|
24304
24567
|
sessionId: row.session_id,
|
|
24305
24568
|
title: row.title,
|
|
24306
24569
|
subagent: row.subagent === 1,
|
|
24307
24570
|
archived: row.archived === 1,
|
|
24308
|
-
...
|
|
24571
|
+
...tokenTotalsOf$1(row),
|
|
24572
|
+
...activityBySession.get(row.session_id) ?? emptyActivity$1()
|
|
24309
24573
|
}))
|
|
24310
24574
|
};
|
|
24311
24575
|
}
|
|
24576
|
+
/** 活动计数旁路累加:派生表 `t_event_counts`,不在写事务里(失败可丢,表可销毁重建)。 */
|
|
24577
|
+
async incrementEventCounts(id, buckets) {
|
|
24578
|
+
if (buckets.length === 0) return;
|
|
24579
|
+
const statement = this.db.$client.prepare(`INSERT INTO t_event_counts (f_session_id, f_day, f_type, f_count) VALUES (?, ?, ?, ?)
|
|
24580
|
+
ON CONFLICT(f_session_id, f_day, f_type) DO UPDATE SET f_count = f_count + excluded.f_count`);
|
|
24581
|
+
for (const bucket of buckets) statement.run(id, bucket.day, bucket.type, bucket.count);
|
|
24582
|
+
}
|
|
24583
|
+
/** 按会话重算活动计数(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
|
|
24584
|
+
async rebuildEventCounts(id) {
|
|
24585
|
+
this.db.$client.prepare("DELETE FROM t_event_counts WHERE f_session_id = ?").run(id);
|
|
24586
|
+
this.db.$client.prepare(`INSERT INTO t_event_counts (f_session_id, f_day, f_type, f_count)
|
|
24587
|
+
SELECT b.f_session_id,
|
|
24588
|
+
date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
|
|
24589
|
+
e.f_type,
|
|
24590
|
+
count(*)
|
|
24591
|
+
FROM t_session_events b
|
|
24592
|
+
JOIN t_events e ON e.f_event_id = b.f_event_id
|
|
24593
|
+
WHERE b.f_session_id = ? AND e.f_type IN (${COUNTED_EVENT_TYPE_SQL$1})
|
|
24594
|
+
GROUP BY 1, 2, 3`).run(id);
|
|
24595
|
+
}
|
|
24596
|
+
async deleteEventCounts(id) {
|
|
24597
|
+
this.db.$client.prepare("DELETE FROM t_event_counts WHERE f_session_id = ?").run(id);
|
|
24598
|
+
}
|
|
24312
24599
|
async vacuum() {
|
|
24313
24600
|
await enqueueSqliteTx(this.dbPath, async () => {
|
|
24314
24601
|
this.db.$client.exec("VACUUM");
|
|
@@ -24337,6 +24624,13 @@ async function migrate(db, config) {
|
|
|
24337
24624
|
//#endregion
|
|
24338
24625
|
//#region ../session-rdb/src/postgres.ts
|
|
24339
24626
|
const postgresMigrationsDir = fileURLToPath(new URL("../drizzle/postgres/", import.meta.url));
|
|
24627
|
+
const pgWriteQueues = /* @__PURE__ */ new Map();
|
|
24628
|
+
/** 按介质(连接串 + schema)串行化写事务,与 SQLite 侧的 `enqueueSqliteTx` 同形。 */
|
|
24629
|
+
function enqueuePgWrite(key, fn) {
|
|
24630
|
+
const run = (pgWriteQueues.get(key) ?? Promise.resolve()).then(fn);
|
|
24631
|
+
pgWriteQueues.set(key, run.then(() => void 0, () => void 0));
|
|
24632
|
+
return run;
|
|
24633
|
+
}
|
|
24340
24634
|
/** pg 的 text 列窄化:非字符串(含 null)都不当作文本。 */
|
|
24341
24635
|
function text(value) {
|
|
24342
24636
|
return typeof value === "string" ? value : null;
|
|
@@ -24347,9 +24641,8 @@ function numeric(value) {
|
|
|
24347
24641
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
24348
24642
|
}
|
|
24349
24643
|
/** SQL 的缺失求和是 NULL:没有 usage 字段的行使该字段计 0。 */
|
|
24350
|
-
function
|
|
24644
|
+
function tokenTotalsOf(row) {
|
|
24351
24645
|
return {
|
|
24352
|
-
events: numeric(row["events"]),
|
|
24353
24646
|
inputTokens: numeric(row["input_tokens"]),
|
|
24354
24647
|
outputTokens: numeric(row["output_tokens"]),
|
|
24355
24648
|
cacheReadTokens: numeric(row["cache_read_tokens"]),
|
|
@@ -24357,6 +24650,16 @@ function totalsOfRow(row) {
|
|
|
24357
24650
|
totalTokens: numeric(row["total_tokens"])
|
|
24358
24651
|
};
|
|
24359
24652
|
}
|
|
24653
|
+
function emptyActivity() {
|
|
24654
|
+
return {
|
|
24655
|
+
turns: 0,
|
|
24656
|
+
steps: 0,
|
|
24657
|
+
userInputs: 0,
|
|
24658
|
+
toolCalls: 0
|
|
24659
|
+
};
|
|
24660
|
+
}
|
|
24661
|
+
/** `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。 */
|
|
24662
|
+
const COUNTED_EVENT_TYPE_SQL = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
|
|
24360
24663
|
var PostgresBackend = class PostgresBackend {
|
|
24361
24664
|
db;
|
|
24362
24665
|
options;
|
|
@@ -24377,9 +24680,10 @@ var PostgresBackend = class PostgresBackend {
|
|
|
24377
24680
|
this.rejectOpened = reject;
|
|
24378
24681
|
});
|
|
24379
24682
|
this.opened.catch(() => {});
|
|
24683
|
+
const writeQueueKey = options.identityBase;
|
|
24380
24684
|
this.storage = createStorageRepository({
|
|
24381
24685
|
db: () => this.opened.then(() => this.txOverride ?? this.db),
|
|
24382
|
-
writeAtomically: (fn) => this.db.transaction(async (tx) => {
|
|
24686
|
+
writeAtomically: (fn) => enqueuePgWrite(writeQueueKey, () => this.db.transaction(async (tx) => {
|
|
24383
24687
|
const previous = this.txOverride;
|
|
24384
24688
|
this.txOverride = tx;
|
|
24385
24689
|
try {
|
|
@@ -24387,7 +24691,7 @@ var PostgresBackend = class PostgresBackend {
|
|
|
24387
24691
|
} finally {
|
|
24388
24692
|
this.txOverride = previous;
|
|
24389
24693
|
}
|
|
24390
|
-
}, { isolationLevel: "serializable" }),
|
|
24694
|
+
}, { isolationLevel: "serializable" })),
|
|
24391
24695
|
tables: this.tables
|
|
24392
24696
|
});
|
|
24393
24697
|
}
|
|
@@ -24407,6 +24711,8 @@ var PostgresBackend = class PostgresBackend {
|
|
|
24407
24711
|
if (await this.readMeta(this.db, "schema_version") === "2") await this.baselineV2();
|
|
24408
24712
|
}
|
|
24409
24713
|
await migrate(this.db, { migrationsFolder: postgresMigrationsDir });
|
|
24714
|
+
await this.ensureEventCountsTable();
|
|
24715
|
+
await this.backfillEventCounts();
|
|
24410
24716
|
await this.backfillEventUsage();
|
|
24411
24717
|
const storeId = await this.db.transaction(async (tx) => {
|
|
24412
24718
|
await tx.insert(this.tables["t_persistence_state"]).values({
|
|
@@ -24447,11 +24753,14 @@ var PostgresBackend = class PostgresBackend {
|
|
|
24447
24753
|
}
|
|
24448
24754
|
async getEventTypeAt(id, sequence) {
|
|
24449
24755
|
const table = this.tables["t_session_events"];
|
|
24450
|
-
return (await this.
|
|
24756
|
+
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;
|
|
24451
24757
|
}
|
|
24452
24758
|
async getEventTypesBefore(id, beforeSequence, limit) {
|
|
24453
24759
|
const table = this.tables["t_session_events"];
|
|
24454
|
-
return await this.
|
|
24760
|
+
return await this.db.select({
|
|
24761
|
+
fSequence: table.fSequence,
|
|
24762
|
+
fType: this.tables["t_events"].fType
|
|
24763
|
+
}).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();
|
|
24455
24764
|
}
|
|
24456
24765
|
async listSessions() {
|
|
24457
24766
|
return this.db.select().from(this.tables["t_sessions"]).execute();
|
|
@@ -24609,61 +24918,176 @@ var PostgresBackend = class PostgresBackend {
|
|
|
24609
24918
|
}
|
|
24610
24919
|
/** 用量聚合:与 SQLite 侧同形,读 `t_event_usage`,数值列回来是字符串。 */
|
|
24611
24920
|
async usageReport(sinceMs) {
|
|
24612
|
-
const
|
|
24921
|
+
const usageSince = sinceMs === void 0 ? sql`` : sql` AND u.f_created_at >= ${sinceMs}`;
|
|
24922
|
+
const daySince = sinceMs === void 0 ? sql`` : sql` AND c.f_day >= ${localDayKey(sinceMs)}`;
|
|
24613
24923
|
const tEventUsage = this.tables["t_event_usage"];
|
|
24614
24924
|
const tSessionEvents = this.tables["t_session_events"];
|
|
24615
24925
|
const tSessions = this.tables["t_sessions"];
|
|
24926
|
+
const eventCountsTable = this.qualifiedTable("t_event_counts");
|
|
24927
|
+
const subagentOfUsage = sql`EXISTS (SELECT 1 FROM ${tSessionEvents} sb
|
|
24928
|
+
JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
|
|
24929
|
+
WHERE sb.f_event_id = u.f_event_id AND ss.f_origin = 'subagent')`;
|
|
24930
|
+
const subagentOfCounts = sql`EXISTS (SELECT 1 FROM ${tSessionEvents} sb
|
|
24931
|
+
JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
|
|
24932
|
+
WHERE sb.f_session_id = c.f_session_id AND ss.f_origin = 'subagent')`;
|
|
24933
|
+
const hasUsageSession = (column) => sql`EXISTS (SELECT 1 FROM ${tSessionEvents} ub
|
|
24934
|
+
JOIN ${tEventUsage} uu ON uu.f_event_id = ub.f_event_id
|
|
24935
|
+
WHERE ub.f_session_id = ${sql.raw(column)})`;
|
|
24936
|
+
const tokenColumns = sql.raw(`sum(u.f_input_tokens) AS input_tokens,
|
|
24937
|
+
sum(u.f_output_tokens) AS output_tokens,
|
|
24938
|
+
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
24939
|
+
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
24940
|
+
sum(u.f_total_tokens) AS total_tokens`);
|
|
24616
24941
|
const buckets = await this.db.execute(sql`
|
|
24617
24942
|
SELECT to_char(to_timestamp(u.f_created_at / 1000.0), 'YYYY-MM-DD') AS day,
|
|
24618
24943
|
u.f_provider AS provider,
|
|
24619
24944
|
u.f_model AS model,
|
|
24620
|
-
|
|
24621
|
-
|
|
24622
|
-
WHERE sb.f_event_id = u.f_event_id AND ss.f_origin = 'subagent') AS subagent,
|
|
24623
|
-
count(*) AS events,
|
|
24624
|
-
sum(u.f_input_tokens) AS input_tokens,
|
|
24625
|
-
sum(u.f_output_tokens) AS output_tokens,
|
|
24626
|
-
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
24627
|
-
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
24628
|
-
sum(u.f_total_tokens) AS total_tokens
|
|
24945
|
+
${subagentOfUsage} AS subagent,
|
|
24946
|
+
${tokenColumns}
|
|
24629
24947
|
FROM ${tEventUsage} u
|
|
24630
|
-
WHERE EXISTS (SELECT 1 FROM ${tSessionEvents} rb WHERE rb.f_event_id = u.f_event_id)${
|
|
24948
|
+
WHERE EXISTS (SELECT 1 FROM ${tSessionEvents} rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
|
|
24631
24949
|
GROUP BY 1, 2, 3, 4
|
|
24632
24950
|
`);
|
|
24633
|
-
const
|
|
24951
|
+
const tokenScope = await this.db.execute(sql`
|
|
24952
|
+
SELECT ${subagentOfUsage} AS subagent,
|
|
24953
|
+
${tokenColumns}
|
|
24954
|
+
FROM ${tEventUsage} u
|
|
24955
|
+
WHERE EXISTS (SELECT 1 FROM ${tSessionEvents} rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
|
|
24956
|
+
GROUP BY 1
|
|
24957
|
+
`);
|
|
24958
|
+
const activityScope = await this.db.execute(sql`
|
|
24959
|
+
SELECT ${subagentOfCounts} AS subagent,
|
|
24960
|
+
c.f_type AS f_type,
|
|
24961
|
+
sum(c.f_count) AS n
|
|
24962
|
+
FROM ${sql.raw(eventCountsTable)} c
|
|
24963
|
+
JOIN ${tSessions} s ON s.f_session_id = c.f_session_id
|
|
24964
|
+
WHERE ${hasUsageSession("c.f_session_id")}${daySince}
|
|
24965
|
+
GROUP BY 1, 2
|
|
24966
|
+
`);
|
|
24967
|
+
const tokenSessions = await this.db.execute(sql`
|
|
24634
24968
|
SELECT b.f_session_id AS session_id,
|
|
24635
24969
|
s.f_title AS title,
|
|
24636
24970
|
(s.f_origin = 'subagent') AS subagent,
|
|
24637
24971
|
(s.f_archived_at IS NOT NULL) AS archived,
|
|
24638
|
-
|
|
24639
|
-
sum(u.f_input_tokens) AS input_tokens,
|
|
24640
|
-
sum(u.f_output_tokens) AS output_tokens,
|
|
24641
|
-
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
24642
|
-
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
24643
|
-
sum(u.f_total_tokens) AS total_tokens
|
|
24972
|
+
${tokenColumns}
|
|
24644
24973
|
FROM ${tSessionEvents} b
|
|
24645
24974
|
JOIN ${tEventUsage} u ON u.f_event_id = b.f_event_id
|
|
24646
24975
|
JOIN ${tSessions} s ON s.f_session_id = b.f_session_id
|
|
24647
|
-
WHERE 1 = 1${
|
|
24976
|
+
WHERE 1 = 1${usageSince}
|
|
24648
24977
|
GROUP BY b.f_session_id, s.f_title, s.f_origin, s.f_archived_at
|
|
24649
24978
|
`);
|
|
24979
|
+
const activitySessions = await this.db.execute(sql`
|
|
24980
|
+
SELECT c.f_session_id AS session_id,
|
|
24981
|
+
c.f_type AS f_type,
|
|
24982
|
+
sum(c.f_count) AS n
|
|
24983
|
+
FROM ${sql.raw(eventCountsTable)} c
|
|
24984
|
+
WHERE ${hasUsageSession("c.f_session_id")}${daySince}
|
|
24985
|
+
GROUP BY c.f_session_id, c.f_type
|
|
24986
|
+
`);
|
|
24987
|
+
const human = emptyTotals();
|
|
24988
|
+
const subagent = emptyTotals();
|
|
24989
|
+
for (const row of tokenScope.rows) addTokenTotals(row["subagent"] === true ? subagent : human, tokenTotalsOf(row));
|
|
24990
|
+
for (const row of activityScope.rows) addActivityCount(row["subagent"] === true ? subagent : human, String(row["f_type"]), numeric(row["n"]));
|
|
24991
|
+
const totals = addTotals(addTotals(emptyTotals(), human), subagent);
|
|
24992
|
+
const activityBySession = /* @__PURE__ */ new Map();
|
|
24993
|
+
for (const row of activitySessions.rows) {
|
|
24994
|
+
const key = String(row["session_id"]);
|
|
24995
|
+
const target = activityBySession.get(key) ?? emptyActivity();
|
|
24996
|
+
addActivityCount(target, String(row["f_type"]), numeric(row["n"]));
|
|
24997
|
+
activityBySession.set(key, target);
|
|
24998
|
+
}
|
|
24650
24999
|
return {
|
|
25000
|
+
totals,
|
|
25001
|
+
subagent,
|
|
25002
|
+
human,
|
|
24651
25003
|
buckets: buckets.rows.map((row) => ({
|
|
24652
25004
|
day: String(row["day"]),
|
|
24653
25005
|
provider: text(row["provider"]),
|
|
24654
25006
|
model: text(row["model"]),
|
|
24655
25007
|
subagent: row["subagent"] === true,
|
|
24656
|
-
...
|
|
25008
|
+
...tokenTotalsOf(row)
|
|
24657
25009
|
})),
|
|
24658
|
-
sessions:
|
|
25010
|
+
sessions: tokenSessions.rows.map((row) => ({
|
|
24659
25011
|
sessionId: String(row["session_id"]),
|
|
24660
25012
|
title: text(row["title"]),
|
|
24661
25013
|
subagent: row["subagent"] === true,
|
|
24662
25014
|
archived: row["archived"] === true,
|
|
24663
|
-
...
|
|
25015
|
+
...tokenTotalsOf(row),
|
|
25016
|
+
...activityBySession.get(String(row["session_id"])) ?? emptyActivity()
|
|
24664
25017
|
}))
|
|
24665
25018
|
};
|
|
24666
25019
|
}
|
|
25020
|
+
/** 表名限定到配置的 schema(派生表不进 drizzle schema / 迁移,建表幂等)。 */
|
|
25021
|
+
qualifiedTable(name) {
|
|
25022
|
+
const schema = this.options.schema ?? "public";
|
|
25023
|
+
return schema === "public" ? name : `"${schema}".${name}`;
|
|
25024
|
+
}
|
|
25025
|
+
async ensureEventCountsTable() {
|
|
25026
|
+
const table = this.qualifiedTable("t_event_counts");
|
|
25027
|
+
await this.db.execute(sql.raw(`CREATE TABLE IF NOT EXISTS ${table} (
|
|
25028
|
+
f_id serial PRIMARY KEY,
|
|
25029
|
+
f_session_id text NOT NULL,
|
|
25030
|
+
f_day text NOT NULL,
|
|
25031
|
+
f_type text NOT NULL,
|
|
25032
|
+
f_count integer NOT NULL DEFAULT 0
|
|
25033
|
+
)`));
|
|
25034
|
+
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)`));
|
|
25035
|
+
await this.db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS idx_event_counts_day ON ${table}(f_day)`));
|
|
25036
|
+
}
|
|
25037
|
+
/** 一次性回填活动计数(表为空时从事件表重算;幂等)。 */
|
|
25038
|
+
async backfillEventCounts() {
|
|
25039
|
+
const table = this.qualifiedTable("t_event_counts");
|
|
25040
|
+
const existing = await this.db.execute(sql.raw(`SELECT count(*) AS n FROM ${table}`));
|
|
25041
|
+
if (Number(existing.rows[0]?.["n"] ?? 0) > 0) return;
|
|
25042
|
+
const tSessionEvents = this.tables["t_session_events"];
|
|
25043
|
+
const tEvents = this.tables["t_events"];
|
|
25044
|
+
await this.db.execute(sql`
|
|
25045
|
+
INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
|
|
25046
|
+
SELECT ${tSessionEvents}.f_session_id,
|
|
25047
|
+
to_char(to_timestamp(${tEvents}.f_created_at / 1000.0), 'YYYY-MM-DD'),
|
|
25048
|
+
${tEvents}.f_type,
|
|
25049
|
+
count(*)
|
|
25050
|
+
FROM ${tSessionEvents}
|
|
25051
|
+
JOIN ${tEvents} ON ${tEvents}.f_event_id = ${tSessionEvents}.f_event_id
|
|
25052
|
+
WHERE ${tEvents}.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
|
|
25053
|
+
GROUP BY 1, 2, 3
|
|
25054
|
+
ON CONFLICT DO NOTHING
|
|
25055
|
+
`);
|
|
25056
|
+
}
|
|
25057
|
+
/** 活动计数旁路累加:派生表,不在写事务里(失败可丢,表可销毁重建)。 */
|
|
25058
|
+
async incrementEventCounts(id, buckets) {
|
|
25059
|
+
if (buckets.length === 0) return;
|
|
25060
|
+
const table = this.qualifiedTable("t_event_counts");
|
|
25061
|
+
for (const bucket of buckets) await this.db.execute(sql`
|
|
25062
|
+
INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
|
|
25063
|
+
VALUES (${id}, ${bucket.day}, ${bucket.type}, ${bucket.count})
|
|
25064
|
+
ON CONFLICT (f_session_id, f_day, f_type)
|
|
25065
|
+
DO UPDATE SET f_count = ${sql.raw(`${table}.f_count`)} + EXCLUDED.f_count
|
|
25066
|
+
`);
|
|
25067
|
+
}
|
|
25068
|
+
/** 按会话重算活动计数(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
|
|
25069
|
+
async rebuildEventCounts(id) {
|
|
25070
|
+
const table = this.qualifiedTable("t_event_counts");
|
|
25071
|
+
const tSessionEvents = this.tables["t_session_events"];
|
|
25072
|
+
const tEvents = this.tables["t_events"];
|
|
25073
|
+
await this.db.execute(sql`DELETE FROM ${sql.raw(table)} WHERE f_session_id = ${id}`);
|
|
25074
|
+
await this.db.execute(sql`
|
|
25075
|
+
INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
|
|
25076
|
+
SELECT ${tSessionEvents}.f_session_id,
|
|
25077
|
+
to_char(to_timestamp(${tEvents}.f_created_at / 1000.0), 'YYYY-MM-DD'),
|
|
25078
|
+
${tEvents}.f_type,
|
|
25079
|
+
count(*)
|
|
25080
|
+
FROM ${tSessionEvents}
|
|
25081
|
+
JOIN ${tEvents} ON ${tEvents}.f_event_id = ${tSessionEvents}.f_event_id
|
|
25082
|
+
WHERE ${tSessionEvents}.f_session_id = ${id}
|
|
25083
|
+
AND ${tEvents}.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
|
|
25084
|
+
GROUP BY 1, 2, 3
|
|
25085
|
+
`);
|
|
25086
|
+
}
|
|
25087
|
+
async deleteEventCounts(id) {
|
|
25088
|
+
const table = this.qualifiedTable("t_event_counts");
|
|
25089
|
+
await this.db.execute(sql`DELETE FROM ${sql.raw(table)} WHERE f_session_id = ${id}`);
|
|
25090
|
+
}
|
|
24667
25091
|
async getPrevBridge(exec, id, sequence) {
|
|
24668
25092
|
return (await exec.select({
|
|
24669
25093
|
fEventId: this.tables["t_session_events"].fEventId,
|
|
@@ -24884,6 +25308,7 @@ var SessionBranchRdbProvider = class {
|
|
|
24884
25308
|
} finally {
|
|
24885
25309
|
await handle.close();
|
|
24886
25310
|
}
|
|
25311
|
+
await this.persistence.rebuildEventCounts(childId);
|
|
24887
25312
|
return childId;
|
|
24888
25313
|
}
|
|
24889
25314
|
async rewind(id, toBoundary, signal) {
|
|
@@ -24956,6 +25381,7 @@ var SessionBranchRdbProvider = class {
|
|
|
24956
25381
|
headSeq: keepLength - 1,
|
|
24957
25382
|
snapshotEvents: () => []
|
|
24958
25383
|
});
|
|
25384
|
+
await this.persistence.rebuildEventCounts(id);
|
|
24959
25385
|
return {
|
|
24960
25386
|
header: rowToMeta(row),
|
|
24961
25387
|
revision: await internals.readStoredRevision(id)
|
|
@@ -25058,16 +25484,6 @@ var SessionBranchRdb = class extends SessionBranch {
|
|
|
25058
25484
|
rewind(id, toBoundary, signal) {
|
|
25059
25485
|
return this.provider.rewind(id, toBoundary, signal);
|
|
25060
25486
|
}
|
|
25061
|
-
async timeline(sessionId, signal) {
|
|
25062
|
-
const persistence = this.ctx.sessionPersistence;
|
|
25063
|
-
const snapshots = await persistence.listSnapshots(signal);
|
|
25064
|
-
const readOwnEvents = async (id, fromSeq, s) => {
|
|
25065
|
-
const live = this.ctx.sessions.get(id);
|
|
25066
|
-
if (live !== void 0) return live.snapshotEvents().slice(fromSeq);
|
|
25067
|
-
return (await persistence.internals().readFrom(id, fromSeq, s)).events;
|
|
25068
|
-
};
|
|
25069
|
-
return buildTimeline(snapshots, readOwnEvents, sessionId, signal);
|
|
25070
|
-
}
|
|
25071
25487
|
};
|
|
25072
25488
|
//#endregion
|
|
25073
25489
|
//#region ../../../node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/index.mjs
|
|
@@ -26632,118 +27048,6 @@ function registerSessionGc(ctx, persistence) {
|
|
|
26632
27048
|
});
|
|
26633
27049
|
}
|
|
26634
27050
|
//#endregion
|
|
26635
|
-
//#region ../session-rdb/src/usage.ts
|
|
26636
|
-
const SESSION_USAGE_PATH = "/api/session.usage";
|
|
26637
|
-
const DAY_MS = 864e5;
|
|
26638
|
-
const ROLLING_DAYS = {
|
|
26639
|
-
"7d": 7,
|
|
26640
|
-
"30d": 30,
|
|
26641
|
-
"90d": 90
|
|
26642
|
-
};
|
|
26643
|
-
function localDayStart(now) {
|
|
26644
|
-
const start = new Date(now);
|
|
26645
|
-
start.setHours(0, 0, 0, 0);
|
|
26646
|
-
return start;
|
|
26647
|
-
}
|
|
26648
|
-
/**
|
|
26649
|
-
* 范围起点(含),不限时为 undefined。自然日 / 自然周按 host 的本地时区算,周边界是周一 00:00。
|
|
26650
|
-
* @param range - 语义键。
|
|
26651
|
-
* @param now - 当前时刻。
|
|
26652
|
-
* @returns 起点毫秒时间戳,或 undefined。
|
|
26653
|
-
*/
|
|
26654
|
-
function resolveUsageSince(range, now) {
|
|
26655
|
-
switch (range) {
|
|
26656
|
-
case "all": return;
|
|
26657
|
-
case "day": return localDayStart(now).getTime();
|
|
26658
|
-
case "week": {
|
|
26659
|
-
const start = localDayStart(now);
|
|
26660
|
-
start.setDate(start.getDate() - (start.getDay() + 6) % 7);
|
|
26661
|
-
return start.getTime();
|
|
26662
|
-
}
|
|
26663
|
-
default: return now - ROLLING_DAYS[range] * DAY_MS;
|
|
26664
|
-
}
|
|
26665
|
-
}
|
|
26666
|
-
function parseUsageRange(value) {
|
|
26667
|
-
return value === "day" || value === "week" || value === "7d" || value === "30d" || value === "90d" ? value : "all";
|
|
26668
|
-
}
|
|
26669
|
-
function emptyTotals() {
|
|
26670
|
-
return {
|
|
26671
|
-
events: 0,
|
|
26672
|
-
inputTokens: 0,
|
|
26673
|
-
outputTokens: 0,
|
|
26674
|
-
cacheReadTokens: 0,
|
|
26675
|
-
reasoningTokens: 0,
|
|
26676
|
-
totalTokens: 0
|
|
26677
|
-
};
|
|
26678
|
-
}
|
|
26679
|
-
/** 折叠一组行(桶或会话行)的用量。 */
|
|
26680
|
-
function sumUsage(rows) {
|
|
26681
|
-
const totals = emptyTotals();
|
|
26682
|
-
for (const row of rows) {
|
|
26683
|
-
totals.events += row.events;
|
|
26684
|
-
totals.inputTokens += row.inputTokens;
|
|
26685
|
-
totals.outputTokens += row.outputTokens;
|
|
26686
|
-
totals.cacheReadTokens += row.cacheReadTokens;
|
|
26687
|
-
totals.reasoningTokens += row.reasoningTokens;
|
|
26688
|
-
totals.totalTokens += row.totalTokens;
|
|
26689
|
-
}
|
|
26690
|
-
return totals;
|
|
26691
|
-
}
|
|
26692
|
-
/**
|
|
26693
|
-
* 用量统计通道:一次请求回报总览(含 subagent 拆分)、按天 × 模型的桶与按会话的行。
|
|
26694
|
-
* 聚合只算被会话引用的事件行——fork 共享行因此只计一次,已删会话留下的孤儿行不计。
|
|
26695
|
-
*/
|
|
26696
|
-
function registerSessionUsage(ctx, persistence) {
|
|
26697
|
-
ctx.inject(["webServer", "connection"], (webCtx) => {
|
|
26698
|
-
const webServer = webCtx.webServer;
|
|
26699
|
-
const connection = webCtx.get("connection");
|
|
26700
|
-
return webCtx.effect(() => webServer.register({
|
|
26701
|
-
kind: "exact",
|
|
26702
|
-
path: SESSION_USAGE_PATH,
|
|
26703
|
-
handler: async (req, res) => {
|
|
26704
|
-
const rejection = connection.requestRejection(req);
|
|
26705
|
-
if (rejection !== void 0) {
|
|
26706
|
-
res.writeHead(rejection);
|
|
26707
|
-
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
26708
|
-
return;
|
|
26709
|
-
}
|
|
26710
|
-
if (req.method !== "POST") {
|
|
26711
|
-
res.writeHead(405, { "content-type": "application/json" });
|
|
26712
|
-
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
26713
|
-
return;
|
|
26714
|
-
}
|
|
26715
|
-
const chunks = [];
|
|
26716
|
-
for await (const chunk of req) chunks.push(chunk);
|
|
26717
|
-
const raw = Buffer.concat(chunks).toString("utf8");
|
|
26718
|
-
let envelope = {};
|
|
26719
|
-
if (raw !== "") try {
|
|
26720
|
-
envelope = JSON.parse(raw);
|
|
26721
|
-
} catch {
|
|
26722
|
-
res.writeHead(400, { "content-type": "application/json" });
|
|
26723
|
-
res.end(JSON.stringify({ error: "request body is not JSON" }));
|
|
26724
|
-
return;
|
|
26725
|
-
}
|
|
26726
|
-
try {
|
|
26727
|
-
const aggregate = await persistence.usageReport(resolveUsageSince(parseUsageRange(envelope.range), Date.now()));
|
|
26728
|
-
const totals = sumUsage(aggregate.buckets);
|
|
26729
|
-
const subagent = sumUsage(aggregate.buckets.filter((bucket) => bucket.subagent));
|
|
26730
|
-
const human = sumUsage(aggregate.buckets.filter((bucket) => !bucket.subagent));
|
|
26731
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
26732
|
-
res.end(JSON.stringify({
|
|
26733
|
-
totals,
|
|
26734
|
-
subagent,
|
|
26735
|
-
human,
|
|
26736
|
-
...aggregate
|
|
26737
|
-
}));
|
|
26738
|
-
} catch (error) {
|
|
26739
|
-
res.writeHead(500, { "content-type": "application/json" });
|
|
26740
|
-
res.end(JSON.stringify({ error: error instanceof Error ? error.message : "usage report failed" }));
|
|
26741
|
-
}
|
|
26742
|
-
}
|
|
26743
|
-
}), `session-rdb: ${SESSION_USAGE_PATH} route`);
|
|
26744
|
-
});
|
|
26745
|
-
}
|
|
26746
|
-
//#endregion
|
|
26747
27051
|
//#region ../../../vendor/deepseek-harness/packages/session/session-format-catalog/lib/types/message-projections.js
|
|
26748
27052
|
/** Pure first-party message interpreters for detached current-format replay, including browser readers. */
|
|
26749
27053
|
/** Installed interpretation definitions; recovery listeners are mounted separately by their owning plugins. */
|
|
@@ -34512,7 +34816,10 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
34512
34816
|
});
|
|
34513
34817
|
this.liveBuffers.delete(id);
|
|
34514
34818
|
this.liveReady.delete(id);
|
|
34819
|
+
this.liveFailures.delete(id);
|
|
34820
|
+
this.liveDropWarned.delete(id);
|
|
34515
34821
|
this.reuseEventIds.delete(id);
|
|
34822
|
+
await this.deleteEventCounts(id);
|
|
34516
34823
|
}
|
|
34517
34824
|
/** GC 通道:回收已无桥接行引用的事件行(孤儿),返回删除行数。 */
|
|
34518
34825
|
async collectOrphans() {
|
|
@@ -34528,7 +34835,10 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
34528
34835
|
for (const id of deletable) {
|
|
34529
34836
|
this.liveBuffers.delete(id);
|
|
34530
34837
|
this.liveReady.delete(id);
|
|
34838
|
+
this.liveFailures.delete(id);
|
|
34839
|
+
this.liveDropWarned.delete(id);
|
|
34531
34840
|
this.reuseEventIds.delete(id);
|
|
34841
|
+
await this.deleteEventCounts(id);
|
|
34532
34842
|
}
|
|
34533
34843
|
return deleted;
|
|
34534
34844
|
}
|
|
@@ -34606,6 +34916,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
34606
34916
|
});
|
|
34607
34917
|
this.writeGuard.confirmHead(meta.id, confirmedHead);
|
|
34608
34918
|
this.tracker.materialized(meta.id);
|
|
34919
|
+
await this.recordEventCounts(meta.id, events);
|
|
34609
34920
|
return true;
|
|
34610
34921
|
}
|
|
34611
34922
|
async readLog(id, options = {}, signal) {
|
|
@@ -34654,6 +34965,35 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
34654
34965
|
...tornFrom !== void 0 ? { tornFrom } : {}
|
|
34655
34966
|
};
|
|
34656
34967
|
}
|
|
34968
|
+
/**
|
|
34969
|
+
* 活动计数**旁路累加**:不在写事务里、失败只 warn——`t_event_counts` 是可销毁重建的派生表,
|
|
34970
|
+
* 丢几次累加不影响可用性(需要时用 `rebuildEventCounts` 重算)。
|
|
34971
|
+
*/
|
|
34972
|
+
async recordEventCounts(id, events) {
|
|
34973
|
+
const buckets = eventCountBuckets(events);
|
|
34974
|
+
if (buckets.length === 0) return;
|
|
34975
|
+
try {
|
|
34976
|
+
await this.backend.incrementEventCounts(id, buckets);
|
|
34977
|
+
} catch (error) {
|
|
34978
|
+
this.ctx.logger.warn(`session-rdb: activity counts for "${id}" not updated (${describeError(error)})`);
|
|
34979
|
+
}
|
|
34980
|
+
}
|
|
34981
|
+
/** 重算一个会话的活动计数(rewind / fork 之后;best-effort,同 `recordEventCounts`)。 */
|
|
34982
|
+
async rebuildEventCounts(id) {
|
|
34983
|
+
try {
|
|
34984
|
+
await this.backend.rebuildEventCounts(id);
|
|
34985
|
+
} catch (error) {
|
|
34986
|
+
this.ctx.logger.warn(`session-rdb: activity counts for "${id}" not rebuilt (${describeError(error)})`);
|
|
34987
|
+
}
|
|
34988
|
+
}
|
|
34989
|
+
/** 会话删除时清掉它的活动计数行(best-effort)。 */
|
|
34990
|
+
async deleteEventCounts(id) {
|
|
34991
|
+
try {
|
|
34992
|
+
await this.backend.deleteEventCounts(id);
|
|
34993
|
+
} catch (error) {
|
|
34994
|
+
this.ctx.logger.warn(`session-rdb: activity counts for "${id}" not deleted (${describeError(error)})`);
|
|
34995
|
+
}
|
|
34996
|
+
}
|
|
34657
34997
|
async rewriteMigratedLog(id, log) {
|
|
34658
34998
|
await this.backend.transaction(async (tx) => {
|
|
34659
34999
|
await tx.deleteBridgeTail(id, 0);
|
|
@@ -34712,24 +35052,26 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
34712
35052
|
}
|
|
34713
35053
|
}
|
|
34714
35054
|
async load(id, signal) {
|
|
34715
|
-
|
|
34716
|
-
|
|
34717
|
-
|
|
34718
|
-
|
|
34719
|
-
|
|
34720
|
-
|
|
34721
|
-
|
|
34722
|
-
|
|
34723
|
-
|
|
34724
|
-
|
|
34725
|
-
return {
|
|
34726
|
-
meta: rowToMeta(row),
|
|
34727
|
-
inheritedEventCount: SessionLogOffset(row.fSeedLength ?? 0),
|
|
34728
|
-
events
|
|
35055
|
+
signal?.throwIfAborted();
|
|
35056
|
+
await this.ready;
|
|
35057
|
+
signal?.throwIfAborted();
|
|
35058
|
+
const log = await this.readLog(id, {}, signal);
|
|
35059
|
+
if (log === void 0) {
|
|
35060
|
+
const pending = this.tracker.pendingOf(id);
|
|
35061
|
+
if (pending !== void 0) return {
|
|
35062
|
+
meta: pending.header,
|
|
35063
|
+
inheritedEventCount: SessionLogOffset(pending.inheritedEventCount),
|
|
35064
|
+
events: []
|
|
34729
35065
|
};
|
|
34730
|
-
|
|
34731
|
-
await handle.close();
|
|
35066
|
+
throw new SessionPersistenceNotFoundError(id);
|
|
34732
35067
|
}
|
|
35068
|
+
repairReadView(log.events);
|
|
35069
|
+
validateStoredEvents(log.meta, log.events);
|
|
35070
|
+
return {
|
|
35071
|
+
meta: log.meta,
|
|
35072
|
+
inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
|
|
35073
|
+
events: log.events
|
|
35074
|
+
};
|
|
34733
35075
|
}
|
|
34734
35076
|
async append(id, events) {
|
|
34735
35077
|
const handle = await this.open(id, "write");
|
|
@@ -34772,22 +35114,28 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
34772
35114
|
rowRevision(row) {
|
|
34773
35115
|
return SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`);
|
|
34774
35116
|
}
|
|
35117
|
+
liveFailures = /* @__PURE__ */ new Map();
|
|
35118
|
+
liveDropWarned = /* @__PURE__ */ new Set();
|
|
35119
|
+
watchLiveReady(ctx, session, ready) {
|
|
35120
|
+
ready.catch((error) => {
|
|
35121
|
+
const failure = error instanceof Error ? error : new Error(typeof error === "string" ? error : `live session "${session.id}" persistence failed`);
|
|
35122
|
+
this.liveFailures.set(session.id, failure);
|
|
35123
|
+
this.liveBuffers.delete(session.id);
|
|
35124
|
+
ctx.logger.error(`session-rdb: live session "${session.id}" persistence unavailable (${failure.message}); its events will not be persisted`);
|
|
35125
|
+
});
|
|
35126
|
+
}
|
|
34775
35127
|
installLiveRouting(ctx) {
|
|
34776
35128
|
ctx.on("session/created", (session) => {
|
|
34777
35129
|
this.liveBuffers.set(session.id, []);
|
|
34778
35130
|
const ready = this.ensureLiveHandle(session);
|
|
34779
35131
|
this.liveReady.set(session.id, ready);
|
|
34780
|
-
|
|
34781
|
-
ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
|
|
34782
|
-
});
|
|
35132
|
+
this.watchLiveReady(ctx, session, ready);
|
|
34783
35133
|
});
|
|
34784
35134
|
for (const session of ctx.sessions.list()) {
|
|
34785
35135
|
this.liveBuffers.set(session.id, []);
|
|
34786
35136
|
const ready = this.ensureLiveHandle(session);
|
|
34787
35137
|
this.liveReady.set(session.id, ready);
|
|
34788
|
-
|
|
34789
|
-
ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
|
|
34790
|
-
});
|
|
35138
|
+
this.watchLiveReady(ctx, session, ready);
|
|
34791
35139
|
}
|
|
34792
35140
|
ctx.on("session/event", (session, event) => {
|
|
34793
35141
|
const handle = this.tracker.writerOf(session.id);
|
|
@@ -34797,9 +35145,18 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
34797
35145
|
});
|
|
34798
35146
|
return;
|
|
34799
35147
|
}
|
|
35148
|
+
if (this.liveFailures.has(session.id)) {
|
|
35149
|
+
if (!this.liveDropWarned.has(session.id)) {
|
|
35150
|
+
this.liveDropWarned.add(session.id);
|
|
35151
|
+
ctx.logger.warn(`session-rdb: dropping events for live session "${session.id}" because persistence failed earlier`);
|
|
35152
|
+
}
|
|
35153
|
+
return;
|
|
35154
|
+
}
|
|
34800
35155
|
this.liveBuffers.get(session.id)?.push(structuredClone(event));
|
|
34801
35156
|
});
|
|
34802
35157
|
ctx.on("session/flush", (session) => {
|
|
35158
|
+
const failure = this.liveFailures.get(session.id);
|
|
35159
|
+
if (failure !== void 0) return Promise.reject(failure);
|
|
34803
35160
|
const handle = this.tracker.writerOf(session.id);
|
|
34804
35161
|
if (handle === void 0) {
|
|
34805
35162
|
const ready = this.liveReady.get(session.id);
|
|
@@ -34816,6 +35173,8 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
34816
35173
|
const ready = this.liveReady.get(session.id);
|
|
34817
35174
|
this.liveBuffers.delete(session.id);
|
|
34818
35175
|
this.liveReady.delete(session.id);
|
|
35176
|
+
this.liveFailures.delete(session.id);
|
|
35177
|
+
this.liveDropWarned.delete(session.id);
|
|
34819
35178
|
const closeHandle = () => {
|
|
34820
35179
|
const handle = this.tracker.writerOf(session.id);
|
|
34821
35180
|
if (handle === void 0) return;
|
|
@@ -34831,6 +35190,10 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
34831
35190
|
});
|
|
34832
35191
|
ctx.effect(() => async () => {
|
|
34833
35192
|
await Promise.allSettled(this.liveReady.values());
|
|
35193
|
+
this.liveBuffers.clear();
|
|
35194
|
+
this.liveReady.clear();
|
|
35195
|
+
this.liveFailures.clear();
|
|
35196
|
+
this.liveDropWarned.clear();
|
|
34834
35197
|
await this.tracker.closeAll();
|
|
34835
35198
|
await this.close();
|
|
34836
35199
|
}, `${this.name} open handles`);
|
|
@@ -34869,6 +35232,26 @@ function seedCoversPrefix(seed, prefix) {
|
|
|
34869
35232
|
return seedEvent !== void 0 && JSON.stringify(seedEvent) === JSON.stringify(event);
|
|
34870
35233
|
});
|
|
34871
35234
|
}
|
|
35235
|
+
/** 一批事件折成活动计数的桶:只数四种类型,按本地日聚合。 */
|
|
35236
|
+
function eventCountBuckets(events) {
|
|
35237
|
+
const counted = /* @__PURE__ */ new Map();
|
|
35238
|
+
for (const event of events) {
|
|
35239
|
+
if (!COUNTED_EVENT_TYPES.includes(event.type)) continue;
|
|
35240
|
+
const day = localDayKey(event.time);
|
|
35241
|
+
const key = `${day}#${event.type}`;
|
|
35242
|
+
const bucket = counted.get(key) ?? {
|
|
35243
|
+
day,
|
|
35244
|
+
type: event.type,
|
|
35245
|
+
count: 0
|
|
35246
|
+
};
|
|
35247
|
+
bucket.count += 1;
|
|
35248
|
+
counted.set(key, bucket);
|
|
35249
|
+
}
|
|
35250
|
+
return [...counted.values()];
|
|
35251
|
+
}
|
|
35252
|
+
function describeError(error) {
|
|
35253
|
+
return error instanceof Error ? error.message : "unknown error";
|
|
35254
|
+
}
|
|
34872
35255
|
function createBackend(config) {
|
|
34873
35256
|
if (config.type === "sqlite") return new SqliteBackend({
|
|
34874
35257
|
path: config.path,
|