@morlay/session-rdb 0.0.21-alpha.3 → 0.0.21-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/artifact.d.mts +1 -1
- package/dist/artifact.mjs +1 -1
- package/dist/{backend-DpdtxYpz.d.mts → backend-a0xDrUFG.d.mts} +58 -9
- package/dist/{branch-5JzX9rUq.mjs → branch-Dlmbkfjj.mjs} +4 -12
- package/dist/deletion.d.mts +1 -1
- package/dist/deletion.mjs +1 -1
- package/dist/import.d.mts +1 -1
- package/dist/import.mjs +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +3 -3
- package/dist/{log-CnYct2Dv.mjs → log-C-rbdFwK.mjs} +32 -4
- package/dist/{sqlite-fpvm5Dzs.mjs → sqlite-C3LHLzYW.mjs} +282 -23
- package/dist/{src-CWTWV7vx.mjs → src-Bn0038Sp.mjs} +269 -165
- package/dist/storage.d.mts +11 -2
- package/dist/storage.mjs +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +11 -11
- package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +1 -1
- package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +1 -1
- package/package.json +2 -2
- package/src/backend.ts +26 -1
- package/src/branch.ts +5 -13
- package/src/entities/v3/event-usage.ts +2 -1
- package/src/index.ts +132 -33
- package/src/log.ts +36 -19
- package/src/postgres.ts +232 -38
- package/src/sqlite.ts +204 -25
- package/src/usage.ts +96 -32
package/dist/testing.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as truncateLiveSession } from "./branch-
|
|
1
|
+
import { a as truncateLiveSession } from "./branch-Dlmbkfjj.mjs";
|
|
2
2
|
import { SessionAlreadyExistsError, SessionAlreadyOwnedError, SessionFormatUnsupportedError, SessionHandleClosedError, SessionPersistenceNotFoundError, SessionReadOnlyError } from "@deepseek-ai/dsh-session-persistence";
|
|
3
3
|
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
|
|
4
4
|
import { Context } from "@deepseek-ai/cordis";
|
|
@@ -23,7 +23,7 @@ var EmptySettings = class extends SettingsProvider {
|
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
25
|
//#endregion
|
|
26
|
-
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.
|
|
26
|
+
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.2_happy-dom@20.14.5_jsdom@30.1.0_35ad368e4c4a4b8f279ac875fcf2bfae/node_modules/vitest/dist/chunks/tinyrainbow.Ht9iggcq.js
|
|
27
27
|
var b$1 = {
|
|
28
28
|
reset: [0, 0],
|
|
29
29
|
bold: [
|
|
@@ -106,7 +106,7 @@ function C({ force: e } = {}) {
|
|
|
106
106
|
}
|
|
107
107
|
var y$1 = C();
|
|
108
108
|
//#endregion
|
|
109
|
-
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.
|
|
109
|
+
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.2_happy-dom@20.14.5_jsdom@30.1.0_35ad368e4c4a4b8f279ac875fcf2bfae/node_modules/vitest/dist/chunks/index.M2dsQ_UQ.js
|
|
110
110
|
function _mergeNamespaces$1(n, m) {
|
|
111
111
|
m.forEach(function(e) {
|
|
112
112
|
e && typeof e !== "string" && !Array.isArray(e) && Object.keys(e).forEach(function(k) {
|
|
@@ -1005,7 +1005,7 @@ const plugins = {
|
|
|
1005
1005
|
Error: ErrorPlugin
|
|
1006
1006
|
};
|
|
1007
1007
|
//#endregion
|
|
1008
|
-
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.
|
|
1008
|
+
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.2_happy-dom@20.14.5_jsdom@30.1.0_35ad368e4c4a4b8f279ac875fcf2bfae/node_modules/vitest/dist/chunks/display.pkpxlVcY.js
|
|
1009
1009
|
const { AsymmetricMatcher: AsymmetricMatcher$3, DOMCollection: DOMCollection$2, DOMElement: DOMElement$2, Immutable: Immutable$2, ReactElement: ReactElement$2, ReactTestComponent: ReactTestComponent$2 } = plugins;
|
|
1010
1010
|
const PLUGINS$2 = [
|
|
1011
1011
|
ReactTestComponent$2,
|
|
@@ -1466,7 +1466,7 @@ const isAbsolute = function(p) {
|
|
|
1466
1466
|
return _IS_ABSOLUTE_RE.test(p);
|
|
1467
1467
|
};
|
|
1468
1468
|
//#endregion
|
|
1469
|
-
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.
|
|
1469
|
+
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.2_happy-dom@20.14.5_jsdom@30.1.0_35ad368e4c4a4b8f279ac875fcf2bfae/node_modules/vitest/dist/chunks/source-map.BH0bbrs9.js
|
|
1470
1470
|
var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
|
|
1471
1471
|
function getDefaultExportFromCjs(x) {
|
|
1472
1472
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
|
|
@@ -2249,7 +2249,7 @@ function getOriginalPosition(map, needle) {
|
|
|
2249
2249
|
return result;
|
|
2250
2250
|
}
|
|
2251
2251
|
//#endregion
|
|
2252
|
-
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.
|
|
2252
|
+
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.2_happy-dom@20.14.5_jsdom@30.1.0_35ad368e4c4a4b8f279ac875fcf2bfae/node_modules/vitest/dist/task-utils.js
|
|
2253
2253
|
/* @__NO_SIDE_EFFECTS__ */
|
|
2254
2254
|
function getNames(task) {
|
|
2255
2255
|
const names = [task.name];
|
|
@@ -2266,7 +2266,7 @@ function createTaskName(names, separator = " > ") {
|
|
|
2266
2266
|
return names.filter((name) => name !== void 0).join(separator);
|
|
2267
2267
|
}
|
|
2268
2268
|
//#endregion
|
|
2269
|
-
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.
|
|
2269
|
+
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.2_happy-dom@20.14.5_jsdom@30.1.0_35ad368e4c4a4b8f279ac875fcf2bfae/node_modules/vitest/dist/chunks/run.C5UmxDPh.js
|
|
2270
2270
|
/**
|
|
2271
2271
|
* Diff Match and Patch
|
|
2272
2272
|
* Copyright 2018 The diff-match-patch Authors.
|
|
@@ -4917,7 +4917,7 @@ async function finishSendTasksUpdate(runner) {
|
|
|
4917
4917
|
await Promise.all(pendingTasksUpdates);
|
|
4918
4918
|
}
|
|
4919
4919
|
//#endregion
|
|
4920
|
-
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.
|
|
4920
|
+
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.2_happy-dom@20.14.5_jsdom@30.1.0_35ad368e4c4a4b8f279ac875fcf2bfae/node_modules/vitest/dist/chunks/utils.DYj33du9.js
|
|
4921
4921
|
const NAME_WORKER_STATE = "__vitest_worker__";
|
|
4922
4922
|
function getWorkerState() {
|
|
4923
4923
|
const workerState = globalThis[NAME_WORKER_STATE];
|
|
@@ -4943,7 +4943,7 @@ function resetModules(modules, resetMocks = false) {
|
|
|
4943
4943
|
});
|
|
4944
4944
|
}
|
|
4945
4945
|
//#endregion
|
|
4946
|
-
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.
|
|
4946
|
+
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.2_happy-dom@20.14.5_jsdom@30.1.0_35ad368e4c4a4b8f279ac875fcf2bfae/node_modules/vitest/dist/chunks/spy.DQ0ZsPbi.js
|
|
4947
4947
|
function isMockFunction(fn) {
|
|
4948
4948
|
return typeof fn === "function" && "_isMockFunction" in fn && fn._isMockFunction === true;
|
|
4949
4949
|
}
|
|
@@ -8339,7 +8339,7 @@ function use(fn) {
|
|
|
8339
8339
|
}
|
|
8340
8340
|
__name(use, "use");
|
|
8341
8341
|
//#endregion
|
|
8342
|
-
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.
|
|
8342
|
+
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.2_happy-dom@20.14.5_jsdom@30.1.0_35ad368e4c4a4b8f279ac875fcf2bfae/node_modules/vitest/dist/chunks/plugins.Cigb0uSy.js
|
|
8343
8343
|
const serialize$1 = (val, config, indentation, depth, refs, printer) => {
|
|
8344
8344
|
const name = val.getMockName();
|
|
8345
8345
|
const nameString = name === "vi.fn()" ? "" : ` ${name}`;
|
|
@@ -8372,7 +8372,7 @@ function getSerializers() {
|
|
|
8372
8372
|
return PLUGINS;
|
|
8373
8373
|
}
|
|
8374
8374
|
//#endregion
|
|
8375
|
-
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.
|
|
8375
|
+
//#region ../../../node_modules/.pnpm/vitest@5.0.1_@opentelemetry+api@1.9.1_@types+node@26.6.2_happy-dom@20.14.5_jsdom@30.1.0_35ad368e4c4a4b8f279ac875fcf2bfae/node_modules/vitest/dist/chunks/offset.Dy-5Fdfn.js
|
|
8376
8376
|
const lineSplitRE = /\r?\n/;
|
|
8377
8377
|
function positionToOffset(source, lineNumber, columnNumber) {
|
|
8378
8378
|
const lines = source.split(lineSplitRE);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/session-rdb",
|
|
3
|
-
"version": "0.0.21-alpha.
|
|
3
|
+
"version": "0.0.21-alpha.5",
|
|
4
4
|
"description": "RDB durable session backend for DeepSeek Harness: implements both session-persistence and session-branch (rewind / retry / fork) providers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"branch",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@morlay/session-branch": "0.0.10-alpha.
|
|
42
|
+
"@morlay/session-branch": "0.0.10-alpha.5",
|
|
43
43
|
"drizzle-orm": "^1.0.0-rc.5-ab785fc",
|
|
44
44
|
"fflate": "^0.8.2",
|
|
45
45
|
"pg": "^8.23.0"
|
package/src/backend.ts
CHANGED
|
@@ -97,6 +97,13 @@ export interface BackendTx {
|
|
|
97
97
|
deleteSessions(ids: SessionId[]): Promise<number>;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/** 活动计数的一个桶:本地日 + 事件类型 + 计数。 */
|
|
101
|
+
export interface EventCountBucket {
|
|
102
|
+
day: string;
|
|
103
|
+
type: string;
|
|
104
|
+
count: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
100
107
|
export interface Backend {
|
|
101
108
|
readonly kind: "sqlite" | "postgres";
|
|
102
109
|
|
|
@@ -129,11 +136,29 @@ export interface Backend {
|
|
|
129
136
|
listOrphanSubagentSessions(): Promise<SessionId[]>;
|
|
130
137
|
|
|
131
138
|
/**
|
|
132
|
-
*
|
|
139
|
+
* 用量统计的原始聚合:token 用量读 `t_event_usage`、活动计数读派生表 `t_event_counts`,
|
|
140
|
+
* 加按天×模型的桶与按会话的行(事件行去重、排除孤儿)。
|
|
133
141
|
* @param sinceMs - 只算该时刻(含)之后的事件行;省略即全量。
|
|
134
142
|
*/
|
|
135
143
|
usageReport(sinceMs?: number): Promise<UsageAggregate>;
|
|
136
144
|
|
|
145
|
+
/**
|
|
146
|
+
* 活动计数**旁路累加**(派生表 `t_event_counts`,不参与写事务、失败可丢——表可销毁重建):
|
|
147
|
+
* 每个 (会话, 本地日, 事件类型) 记一行计数。
|
|
148
|
+
* @param id - 会话 id。
|
|
149
|
+
* @param buckets - 本批要累加的 (day, type, count)。
|
|
150
|
+
*/
|
|
151
|
+
incrementEventCounts(id: SessionId, buckets: readonly EventCountBucket[]): Promise<void>;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 按会话从事件表重算活动计数(rewind / fork 之后调用):先删该会话的行再重算。
|
|
155
|
+
* @param id - 会话 id。
|
|
156
|
+
*/
|
|
157
|
+
rebuildEventCounts(id: SessionId): Promise<void>;
|
|
158
|
+
|
|
159
|
+
/** 删掉一个会话的活动计数行(会话删除时)。 */
|
|
160
|
+
deleteEventCounts(id: SessionId): Promise<void>;
|
|
161
|
+
|
|
137
162
|
/** 回收空间与统计(含 VACUUM);不得在事务内执行。 */
|
|
138
163
|
vacuum(): Promise<void>;
|
|
139
164
|
|
package/src/branch.ts
CHANGED
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
SessionBranch,
|
|
12
12
|
SessionBranchError,
|
|
13
13
|
balanceRewindPrefix,
|
|
14
|
-
buildTimeline,
|
|
15
14
|
rewindKeepLength,
|
|
16
15
|
type BranchAnchorMode,
|
|
17
16
|
type BranchBoundary,
|
|
@@ -295,6 +294,8 @@ export class SessionBranchRdbProvider implements SessionBranchProvider {
|
|
|
295
294
|
} finally {
|
|
296
295
|
await handle.close();
|
|
297
296
|
}
|
|
297
|
+
// 子会话继承了前缀事件:活动计数按子会话重算(派生表,best-effort)。
|
|
298
|
+
await this.persistence.rebuildEventCounts(childId);
|
|
298
299
|
return childId;
|
|
299
300
|
}
|
|
300
301
|
|
|
@@ -425,6 +426,9 @@ export class SessionBranchRdbProvider implements SessionBranchProvider {
|
|
|
425
426
|
});
|
|
426
427
|
}
|
|
427
428
|
|
|
429
|
+
// 截断后该会话的事件集合变了:活动计数跟着重算(派生表,best-effort)。
|
|
430
|
+
await this.persistence.rebuildEventCounts(id);
|
|
431
|
+
|
|
428
432
|
return { header: rowToMeta(row), revision: (await internals.readStoredRevision(id))! };
|
|
429
433
|
}
|
|
430
434
|
|
|
@@ -596,18 +600,6 @@ export class SessionBranchRdb extends SessionBranch {
|
|
|
596
600
|
): Promise<SessionPersistenceSnapshot> {
|
|
597
601
|
return this.provider.rewind(id, toBoundary, signal);
|
|
598
602
|
}
|
|
599
|
-
|
|
600
|
-
async timeline(sessionId: SessionId, signal?: AbortSignal) {
|
|
601
|
-
const persistence = this.ctx.sessionPersistence as SessionPersistenceRdb;
|
|
602
|
-
const snapshots = await persistence.listSnapshots(signal);
|
|
603
|
-
|
|
604
|
-
const readOwnEvents = async (id: SessionId, fromSeq: number, s?: AbortSignal) => {
|
|
605
|
-
const live = this.ctx.sessions.get(id);
|
|
606
|
-
if (live !== undefined) return live.snapshotEvents().slice(fromSeq);
|
|
607
|
-
return (await persistence.internals().readFrom(id, fromSeq, s)).events;
|
|
608
|
-
};
|
|
609
|
-
return buildTimeline(snapshots, readOwnEvents, sessionId, signal);
|
|
610
|
-
}
|
|
611
603
|
}
|
|
612
604
|
|
|
613
605
|
export default SessionBranchRdb;
|
|
@@ -12,7 +12,8 @@ export const eventUsage: TableDef = {
|
|
|
12
12
|
primaryKey: true,
|
|
13
13
|
references: { table: "t_events", column: "f_event_id", onDelete: "cascade" },
|
|
14
14
|
},
|
|
15
|
-
|
|
15
|
+
// 毫秒时间戳:SQLite 的 integer 是 64 位,PG 的 integer 只有 int4(21 亿上限)——必须是 bigint。
|
|
16
|
+
f_created_at: { type: "bigint", notNull: true },
|
|
16
17
|
f_provider: { type: "text" },
|
|
17
18
|
f_model: { type: "text" },
|
|
18
19
|
f_input_tokens: { type: "integer", notNull: true },
|
package/src/index.ts
CHANGED
|
@@ -39,7 +39,12 @@ import {
|
|
|
39
39
|
type SurfaceEventType,
|
|
40
40
|
} from "@deepseek-ai/dsh-session";
|
|
41
41
|
import { sessionFormatLogFilename } from "@deepseek-ai/dsh-session-format";
|
|
42
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
type Backend,
|
|
44
|
+
type BackendTx,
|
|
45
|
+
type EventCountBucket,
|
|
46
|
+
type EventInsert,
|
|
47
|
+
} from "./backend.ts";
|
|
43
48
|
import { WriteGuard } from "./write-guard.ts";
|
|
44
49
|
import { repairReadView, rowToMeta, scanRows, toJsonlArtifact } from "./log.ts";
|
|
45
50
|
import {
|
|
@@ -56,7 +61,7 @@ import { registerSessionImport } from "./import.ts";
|
|
|
56
61
|
import { registerSessionDeletion } from "./deletion.ts";
|
|
57
62
|
import { registerSessionExport } from "./export.ts";
|
|
58
63
|
import { registerSessionGc } from "./gc.ts";
|
|
59
|
-
import { registerSessionUsage } from "./usage.ts";
|
|
64
|
+
import { COUNTED_EVENT_TYPES, localDayKey, registerSessionUsage } from "./usage.ts";
|
|
60
65
|
import type { UsageAggregate } from "./usage.ts";
|
|
61
66
|
import { SessionQueryRdb } from "./session-query.ts";
|
|
62
67
|
import { adoptLegacyRows, convertLegacyRows, isLegacyVersion } from "./legacy.ts";
|
|
@@ -742,7 +747,10 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
742
747
|
});
|
|
743
748
|
this.liveBuffers.delete(id);
|
|
744
749
|
this.liveReady.delete(id);
|
|
750
|
+
this.liveFailures.delete(id);
|
|
751
|
+
this.liveDropWarned.delete(id);
|
|
745
752
|
this.reuseEventIds.delete(id);
|
|
753
|
+
await this.deleteEventCounts(id);
|
|
746
754
|
}
|
|
747
755
|
|
|
748
756
|
/** GC 通道:回收已无桥接行引用的事件行(孤儿),返回删除行数。 */
|
|
@@ -763,7 +771,10 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
763
771
|
for (const id of deletable) {
|
|
764
772
|
this.liveBuffers.delete(id);
|
|
765
773
|
this.liveReady.delete(id);
|
|
774
|
+
this.liveFailures.delete(id);
|
|
775
|
+
this.liveDropWarned.delete(id);
|
|
766
776
|
this.reuseEventIds.delete(id);
|
|
777
|
+
await this.deleteEventCounts(id);
|
|
767
778
|
}
|
|
768
779
|
return deleted;
|
|
769
780
|
}
|
|
@@ -872,6 +883,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
872
883
|
|
|
873
884
|
this.writeGuard.confirmHead(meta.id, confirmedHead);
|
|
874
885
|
this.tracker.materialized(meta.id);
|
|
886
|
+
await this.recordEventCounts(meta.id, events);
|
|
875
887
|
return true;
|
|
876
888
|
}
|
|
877
889
|
|
|
@@ -951,6 +963,44 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
951
963
|
};
|
|
952
964
|
}
|
|
953
965
|
|
|
966
|
+
/**
|
|
967
|
+
* 活动计数**旁路累加**:不在写事务里、失败只 warn——`t_event_counts` 是可销毁重建的派生表,
|
|
968
|
+
* 丢几次累加不影响可用性(需要时用 `rebuildEventCounts` 重算)。
|
|
969
|
+
*/
|
|
970
|
+
private async recordEventCounts(id: SessionId, events: readonly SessionEvent[]): Promise<void> {
|
|
971
|
+
const buckets = eventCountBuckets(events);
|
|
972
|
+
if (buckets.length === 0) return;
|
|
973
|
+
try {
|
|
974
|
+
await this.backend.incrementEventCounts(id, buckets);
|
|
975
|
+
} catch (error: unknown) {
|
|
976
|
+
this.ctx.logger.warn(
|
|
977
|
+
`session-rdb: activity counts for "${id}" not updated (${describeError(error)})`,
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/** 重算一个会话的活动计数(rewind / fork 之后;best-effort,同 `recordEventCounts`)。 */
|
|
983
|
+
async rebuildEventCounts(id: SessionId): Promise<void> {
|
|
984
|
+
try {
|
|
985
|
+
await this.backend.rebuildEventCounts(id);
|
|
986
|
+
} catch (error: unknown) {
|
|
987
|
+
this.ctx.logger.warn(
|
|
988
|
+
`session-rdb: activity counts for "${id}" not rebuilt (${describeError(error)})`,
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/** 会话删除时清掉它的活动计数行(best-effort)。 */
|
|
994
|
+
private async deleteEventCounts(id: SessionId): Promise<void> {
|
|
995
|
+
try {
|
|
996
|
+
await this.backend.deleteEventCounts(id);
|
|
997
|
+
} catch (error: unknown) {
|
|
998
|
+
this.ctx.logger.warn(
|
|
999
|
+
`session-rdb: activity counts for "${id}" not deleted (${describeError(error)})`,
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
954
1004
|
private async rewriteMigratedLog(
|
|
955
1005
|
id: SessionId,
|
|
956
1006
|
log: { meta: SessionHeader; inheritedEventCount: number; events: SessionEvent[] },
|
|
@@ -1033,31 +1083,31 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1033
1083
|
id: SessionId,
|
|
1034
1084
|
signal?: AbortSignal,
|
|
1035
1085
|
): Promise<import("@deepseek-ai/dsh-session-persistence").SessionInspection> {
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
if (isLegacyVersion(row.fVersion)) {
|
|
1086
|
+
signal?.throwIfAborted();
|
|
1087
|
+
await this.ready;
|
|
1088
|
+
signal?.throwIfAborted();
|
|
1089
|
+
// 一条读取路径:load 的语义就是「读一次完整快照」,不再经 handle 面再读一遍
|
|
1090
|
+
// (原先 open(read) 读一次 + handle.read 又读一次,等于把最贵的读付两遍)。
|
|
1091
|
+
const log = await this.readLog(id, {}, signal);
|
|
1092
|
+
if (log === undefined) {
|
|
1093
|
+
// 未落库的 live 会话没有磁盘视图:与 handle.read 的 detached 口径一致,返回空日志。
|
|
1094
|
+
const pending = this.tracker.pendingOf(id);
|
|
1095
|
+
if (pending !== undefined) {
|
|
1047
1096
|
return {
|
|
1048
|
-
meta:
|
|
1049
|
-
inheritedEventCount:
|
|
1050
|
-
events,
|
|
1097
|
+
meta: pending.header,
|
|
1098
|
+
inheritedEventCount: SessionLogOffset(pending.inheritedEventCount),
|
|
1099
|
+
events: [],
|
|
1051
1100
|
};
|
|
1052
1101
|
}
|
|
1053
|
-
|
|
1054
|
-
meta: rowToMeta(row),
|
|
1055
|
-
inheritedEventCount: SessionLogOffset(row.fSeedLength ?? 0),
|
|
1056
|
-
events,
|
|
1057
|
-
};
|
|
1058
|
-
} finally {
|
|
1059
|
-
await handle.close();
|
|
1102
|
+
throw new SessionPersistenceNotFoundError(id);
|
|
1060
1103
|
}
|
|
1104
|
+
repairReadView(log.events);
|
|
1105
|
+
validateStoredEvents(log.meta, log.events);
|
|
1106
|
+
return {
|
|
1107
|
+
meta: log.meta,
|
|
1108
|
+
inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
|
|
1109
|
+
events: log.events,
|
|
1110
|
+
};
|
|
1061
1111
|
}
|
|
1062
1112
|
|
|
1063
1113
|
async append(id: SessionId, events: readonly SessionEvent[]): Promise<void> {
|
|
@@ -1123,27 +1173,41 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1123
1173
|
);
|
|
1124
1174
|
}
|
|
1125
1175
|
|
|
1176
|
+
private readonly liveFailures = new Map<SessionId, Error>();
|
|
1177
|
+
private readonly liveDropWarned = new Set<SessionId>();
|
|
1178
|
+
|
|
1179
|
+
// live 会话的持久化初始化失败是**终态**(cwd / 继承前缀冲突、id 碰撞、后端打不开都不自愈):
|
|
1180
|
+
// 记账一条 error、丢掉已缓冲的事件,之后该会话的事件不再进内存缓冲(否则表现为
|
|
1181
|
+
// 「聊了一整轮、重启后全丢,内存里还留一份无界副本」),flush 把失败抛回上游。
|
|
1182
|
+
private watchLiveReady(ctx: Context, session: Session, ready: Promise<void>): void {
|
|
1183
|
+
void ready.catch((error: unknown) => {
|
|
1184
|
+
const failure =
|
|
1185
|
+
error instanceof Error
|
|
1186
|
+
? error
|
|
1187
|
+
: new Error(
|
|
1188
|
+
typeof error === "string" ? error : `live session "${session.id}" persistence failed`,
|
|
1189
|
+
);
|
|
1190
|
+
this.liveFailures.set(session.id, failure);
|
|
1191
|
+
this.liveBuffers.delete(session.id);
|
|
1192
|
+
ctx.logger.error(
|
|
1193
|
+
`session-rdb: live session "${session.id}" persistence unavailable (${failure.message}); its events will not be persisted`,
|
|
1194
|
+
);
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1126
1198
|
private installLiveRouting(ctx: Context): void {
|
|
1127
1199
|
ctx.on("session/created", (session: Session) => {
|
|
1128
1200
|
this.liveBuffers.set(session.id, []);
|
|
1129
1201
|
const ready = this.ensureLiveHandle(session);
|
|
1130
1202
|
this.liveReady.set(session.id, ready);
|
|
1131
|
-
|
|
1132
|
-
ctx.logger.warn(
|
|
1133
|
-
`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`,
|
|
1134
|
-
);
|
|
1135
|
-
});
|
|
1203
|
+
this.watchLiveReady(ctx, session, ready);
|
|
1136
1204
|
});
|
|
1137
1205
|
|
|
1138
1206
|
for (const session of ctx.sessions.list()) {
|
|
1139
1207
|
this.liveBuffers.set(session.id, []);
|
|
1140
1208
|
const ready = this.ensureLiveHandle(session);
|
|
1141
1209
|
this.liveReady.set(session.id, ready);
|
|
1142
|
-
|
|
1143
|
-
ctx.logger.warn(
|
|
1144
|
-
`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`,
|
|
1145
|
-
);
|
|
1146
|
-
});
|
|
1210
|
+
this.watchLiveReady(ctx, session, ready);
|
|
1147
1211
|
}
|
|
1148
1212
|
ctx.on("session/event", (session: Session, event: SessionEvent) => {
|
|
1149
1213
|
const handle = this.tracker.writerOf(session.id);
|
|
@@ -1155,9 +1219,20 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1155
1219
|
});
|
|
1156
1220
|
return;
|
|
1157
1221
|
}
|
|
1222
|
+
if (this.liveFailures.has(session.id)) {
|
|
1223
|
+
if (!this.liveDropWarned.has(session.id)) {
|
|
1224
|
+
this.liveDropWarned.add(session.id);
|
|
1225
|
+
ctx.logger.warn(
|
|
1226
|
+
`session-rdb: dropping events for live session "${session.id}" because persistence failed earlier`,
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1158
1231
|
this.liveBuffers.get(session.id)?.push(structuredClone(event));
|
|
1159
1232
|
});
|
|
1160
1233
|
ctx.on("session/flush", (session: Session) => {
|
|
1234
|
+
const failure = this.liveFailures.get(session.id);
|
|
1235
|
+
if (failure !== undefined) return Promise.reject(failure);
|
|
1161
1236
|
const handle = this.tracker.writerOf(session.id);
|
|
1162
1237
|
if (handle === undefined) {
|
|
1163
1238
|
const ready = this.liveReady.get(session.id);
|
|
@@ -1174,6 +1249,8 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1174
1249
|
const ready = this.liveReady.get(session.id);
|
|
1175
1250
|
this.liveBuffers.delete(session.id);
|
|
1176
1251
|
this.liveReady.delete(session.id);
|
|
1252
|
+
this.liveFailures.delete(session.id);
|
|
1253
|
+
this.liveDropWarned.delete(session.id);
|
|
1177
1254
|
const closeHandle = (): void => {
|
|
1178
1255
|
const handle = this.tracker.writerOf(session.id);
|
|
1179
1256
|
if (handle === undefined) return;
|
|
@@ -1193,6 +1270,10 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1193
1270
|
ctx.effect(
|
|
1194
1271
|
() => async () => {
|
|
1195
1272
|
await Promise.allSettled(this.liveReady.values());
|
|
1273
|
+
this.liveBuffers.clear();
|
|
1274
|
+
this.liveReady.clear();
|
|
1275
|
+
this.liveFailures.clear();
|
|
1276
|
+
this.liveDropWarned.clear();
|
|
1196
1277
|
await this.tracker.closeAll();
|
|
1197
1278
|
|
|
1198
1279
|
await this.close();
|
|
@@ -1259,6 +1340,24 @@ function seedCoversPrefix(seed: readonly SessionEvent[], prefix: readonly Sessio
|
|
|
1259
1340
|
);
|
|
1260
1341
|
}
|
|
1261
1342
|
|
|
1343
|
+
/** 一批事件折成活动计数的桶:只数四种类型,按本地日聚合。 */
|
|
1344
|
+
function eventCountBuckets(events: readonly SessionEvent[]): EventCountBucket[] {
|
|
1345
|
+
const counted = new Map<string, EventCountBucket>();
|
|
1346
|
+
for (const event of events) {
|
|
1347
|
+
if (!(COUNTED_EVENT_TYPES as readonly string[]).includes(event.type)) continue;
|
|
1348
|
+
const day = localDayKey(event.time);
|
|
1349
|
+
const key = `${day}#${event.type}`;
|
|
1350
|
+
const bucket = counted.get(key) ?? { day, type: event.type, count: 0 };
|
|
1351
|
+
bucket.count += 1;
|
|
1352
|
+
counted.set(key, bucket);
|
|
1353
|
+
}
|
|
1354
|
+
return [...counted.values()];
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function describeError(error: unknown): string {
|
|
1358
|
+
return error instanceof Error ? error.message : "unknown error";
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1262
1361
|
function createBackend(config: Config): Backend {
|
|
1263
1362
|
if (config.type === "sqlite") {
|
|
1264
1363
|
return new SqliteBackend({
|
package/src/log.ts
CHANGED
|
@@ -127,7 +127,40 @@ const SURFACE_EVENT_TYPES = new Set([
|
|
|
127
127
|
|
|
128
128
|
const METERING_EVENT_TYPES = new Set(["compaction/summary", "compaction/prune"]);
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* 升序的 surface 事件 seq 索引:读视图修复里的溯源(replace 的 `sourceEventSeqs` 与
|
|
132
|
+
* metering 的 `shadowedSeqs`)共用这一份,避免每个 replace 各扫一遍全量事件。
|
|
133
|
+
* 前提是输入按 seq 有序(`readLog` 与 live snapshot 都如此)。
|
|
134
|
+
*/
|
|
135
|
+
function surfaceSeqIndex(events: readonly SessionEvent[]): number[] {
|
|
136
|
+
const seqs: number[] = [];
|
|
137
|
+
for (const event of events) {
|
|
138
|
+
if (SURFACE_EVENT_TYPES.has(event.type)) seqs.push(event.seq);
|
|
139
|
+
}
|
|
140
|
+
return seqs;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** `[startSeq, endSeq]` 内的 surface seq(含端点),二分定位后切片。 */
|
|
144
|
+
function surfaceSeqsInRange(seqs: readonly number[], startSeq: number, endSeq: number): number[] {
|
|
145
|
+
let low = 0;
|
|
146
|
+
let high = seqs.length;
|
|
147
|
+
while (low < high) {
|
|
148
|
+
const mid = (low + high) >> 1;
|
|
149
|
+
if (seqs[mid]! < startSeq) low = mid + 1;
|
|
150
|
+
else high = mid;
|
|
151
|
+
}
|
|
152
|
+
const from = low;
|
|
153
|
+
high = seqs.length;
|
|
154
|
+
while (low < high) {
|
|
155
|
+
const mid = (low + high) >> 1;
|
|
156
|
+
if (seqs[mid]! <= endSeq) low = mid + 1;
|
|
157
|
+
else high = mid;
|
|
158
|
+
}
|
|
159
|
+
return seqs.slice(from, low);
|
|
160
|
+
}
|
|
161
|
+
|
|
130
162
|
export function recomputeReplaceProvenance(events: SessionEvent[]): void {
|
|
163
|
+
const seqs = surfaceSeqIndex(events);
|
|
131
164
|
for (let i = 0; i < events.length; i++) {
|
|
132
165
|
const event = events[i]!;
|
|
133
166
|
const raw = event as unknown as { surfaceOp?: unknown; sourceEventSeqs?: number[] };
|
|
@@ -145,17 +178,7 @@ export function recomputeReplaceProvenance(events: SessionEvent[]): void {
|
|
|
145
178
|
raw.sourceEventSeqs = meteringData.shadowedSeqs;
|
|
146
179
|
continue;
|
|
147
180
|
}
|
|
148
|
-
|
|
149
|
-
for (const candidate of events) {
|
|
150
|
-
if (
|
|
151
|
-
candidate.seq >= startSeq &&
|
|
152
|
-
candidate.seq <= endSeq &&
|
|
153
|
-
SURFACE_EVENT_TYPES.has(candidate.type)
|
|
154
|
-
) {
|
|
155
|
-
refs.push(candidate.seq);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
raw.sourceEventSeqs = refs;
|
|
181
|
+
raw.sourceEventSeqs = surfaceSeqsInRange(seqs, startSeq, endSeq);
|
|
159
182
|
}
|
|
160
183
|
}
|
|
161
184
|
|
|
@@ -359,6 +382,7 @@ export function repairRequestHeaders(events: SessionEvent[]): void {
|
|
|
359
382
|
}
|
|
360
383
|
|
|
361
384
|
export function syncMeteringRanges(events: SessionEvent[]): void {
|
|
385
|
+
const seqs = surfaceSeqIndex(events);
|
|
362
386
|
for (let i = 1; i < events.length; i++) {
|
|
363
387
|
const metering = events[i - 1]!;
|
|
364
388
|
if (!METERING_EVENT_TYPES.has(metering.type)) continue;
|
|
@@ -372,14 +396,7 @@ export function syncMeteringRanges(events: SessionEvent[]): void {
|
|
|
372
396
|
};
|
|
373
397
|
if (data.shadowedRange?.start === startSeq && data.shadowedRange.end === endSeq) continue;
|
|
374
398
|
data.shadowedRange = { start: startSeq, end: endSeq };
|
|
375
|
-
data.shadowedSeqs =
|
|
376
|
-
.filter(
|
|
377
|
-
(candidate) =>
|
|
378
|
-
candidate.seq >= startSeq &&
|
|
379
|
-
candidate.seq <= endSeq &&
|
|
380
|
-
SURFACE_EVENT_TYPES.has(candidate.type),
|
|
381
|
-
)
|
|
382
|
-
.map((candidate) => candidate.seq);
|
|
399
|
+
data.shadowedSeqs = surfaceSeqsInRange(seqs, startSeq, endSeq);
|
|
383
400
|
}
|
|
384
401
|
}
|
|
385
402
|
|