@morlay/session-rdb 0.0.16-alpha.2 → 0.0.16
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/dist/artifact.mjs +1 -1
- package/dist/{import-eBSoCrxl.mjs → import-BPEHfHNk.mjs} +39 -4
- package/dist/import.d.mts +1 -1
- package/dist/import.mjs +1 -1
- package/dist/{index-DfS0PzGM.d.mts → index-CgAqCQAb.d.mts} +22 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +4 -3
- package/dist/{sqlite-Dpd17YhI.mjs → sqlite-IH5I48aL.mjs} +2 -1
- package/dist/storage.mjs +1 -1
- package/dist/testing.mjs +4 -4
- package/package.json +17 -12
- package/src/branch.ts +99 -3
- package/src/postgres.ts +2 -1
- package/src/sqlite.ts +2 -1
package/dist/artifact.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { a as repairAssistantSettlement, c as repairRequestHeaders, d as rowToMeta, f as scanRows, g as toJsonlArtifact, h as syncMeteringRanges, i as renameLegacyPtcEvents, l as repairSurfaceOps, m as sessionInsertRow, n as orphanInboxSpliceSeqs, o as repairOrphanInboxSplices, p as sessionConflictRow, r as recomputeReplaceProvenance, s as repairReadView, t as findSurfaceRepairs, u as rowToEvent } from "./log-BIuXv09P.mjs";
|
|
2
|
-
import { a as persistImport, i as parseJsonlArtifact, n as SESSION_LOG_ARTIFACT_FILENAME, o as registerSessionImport, r as parseImportZip, t as SESSION_IMPORT_PATH } from "./import-
|
|
2
|
+
import { a as persistImport, i as parseJsonlArtifact, n as SESSION_LOG_ARTIFACT_FILENAME, o as registerSessionImport, r as parseImportZip, t as SESSION_IMPORT_PATH } from "./import-BPEHfHNk.mjs";
|
|
3
3
|
export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, renameLegacyPtcEvents, repairAssistantSettlement, repairOrphanInboxSplices, repairReadView, repairRequestHeaders, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, syncMeteringRanges, toJsonlArtifact };
|
|
@@ -142,7 +142,8 @@ var SessionBranchRdbProvider = class {
|
|
|
142
142
|
if (!Number.isSafeInteger(toBoundary) || toBoundary < -1) throw new SessionBranchError(`rewind boundary must be a non-negative safe integer, got ${toBoundary}`, "INVALID_BOUNDARY");
|
|
143
143
|
const live = this.live.getSession(id);
|
|
144
144
|
if (live !== void 0) await this.live.flush(live);
|
|
145
|
-
const raw = live === void 0 ? await this.
|
|
145
|
+
const raw = live === void 0 ? await this.persistence.readLog(id, {}, signal) : void 0;
|
|
146
|
+
if (live === void 0 && raw === void 0) throw new SessionBranchError(`session "${id}" not found`, "SESSION_NOT_FOUND");
|
|
146
147
|
const inspection = live === void 0 ? void 0 : await this.persistence.internals().inspect(id, signal);
|
|
147
148
|
const events = live === void 0 ? raw.events : inspection.events;
|
|
148
149
|
const meta = live === void 0 ? raw.meta : inspection.meta;
|
|
@@ -150,7 +151,8 @@ var SessionBranchRdbProvider = class {
|
|
|
150
151
|
if (toBoundary === -1) {} else if (boundaryEvent === void 0) throw new SessionBranchError(`rewind boundary ${toBoundary} does not exist in session "${id}"`, "INVALID_BOUNDARY");
|
|
151
152
|
else if (boundaryEvent.type !== "turn/end" && boundaryEvent.type !== "user/message") throw new SessionBranchError(`rewind boundary ${toBoundary} is not a turn/end or user/message (${boundaryEvent.type})`, "INVALID_BOUNDARY");
|
|
152
153
|
const rawKeepLength = toBoundary === -1 ? 0 : boundaryEvent.type === "turn/end" ? toBoundary + 1 : toBoundary;
|
|
153
|
-
const
|
|
154
|
+
const kept = balanceRewindPrefix(events.slice(0, rawKeepLength));
|
|
155
|
+
const keepLength = kept.length;
|
|
154
156
|
const internals = this.persistence.internals();
|
|
155
157
|
const denseBoundary = keepLength - 1;
|
|
156
158
|
const newSeedLength = await internals.backend.transaction(async (tx) => {
|
|
@@ -175,6 +177,7 @@ var SessionBranchRdbProvider = class {
|
|
|
175
177
|
internals.writeGuard.confirmHead(id, denseBoundary);
|
|
176
178
|
if (live !== void 0) {
|
|
177
179
|
truncateLiveSession(live, keepLength);
|
|
180
|
+
this.live.resetProjections?.(live);
|
|
178
181
|
const agent = this.live.getAgent(id);
|
|
179
182
|
if (agent !== void 0) {
|
|
180
183
|
agent.requestHeaderLogged = false;
|
|
@@ -184,7 +187,15 @@ var SessionBranchRdbProvider = class {
|
|
|
184
187
|
}
|
|
185
188
|
const handle = this.persistence.tracker.writerOf(id);
|
|
186
189
|
if (handle !== void 0) handle.resetAfterRewind(keepLength, newSeedLength === null ? void 0 : newSeedLength);
|
|
187
|
-
|
|
190
|
+
agent?.inbox?.clear();
|
|
191
|
+
await this.live.flush(live);
|
|
192
|
+
await this.refreshProjectionCache(live);
|
|
193
|
+
} else await this.refreshProjectionCache({
|
|
194
|
+
id,
|
|
195
|
+
header: meta,
|
|
196
|
+
inheritedEventCount: SessionLogOffset(raw.inheritedEventCount),
|
|
197
|
+
snapshotEvents: () => kept
|
|
198
|
+
});
|
|
188
199
|
const row = await internals.backend.getSession(id);
|
|
189
200
|
if (row === void 0) return {
|
|
190
201
|
header: {
|
|
@@ -205,6 +216,16 @@ var SessionBranchRdbProvider = class {
|
|
|
205
216
|
revision: await internals.readStoredRevision(id)
|
|
206
217
|
};
|
|
207
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* 让持久化投影检查点跟随截断后的 log(见 {@link LiveSessionHooks.refreshProjectionCache})。
|
|
221
|
+
* 缓存是派生数据:刷新失败只让缓存多滞后一轮,不能让 rewind 失败。
|
|
222
|
+
*/
|
|
223
|
+
async refreshProjectionCache(session) {
|
|
224
|
+
if (this.live.refreshProjectionCache === void 0) return;
|
|
225
|
+
try {
|
|
226
|
+
await this.live.refreshProjectionCache(session);
|
|
227
|
+
} catch {}
|
|
228
|
+
}
|
|
208
229
|
};
|
|
209
230
|
var SessionBranchRdb = class extends SessionBranch {
|
|
210
231
|
static inject = ["sessionPersistence", "sessions"];
|
|
@@ -216,7 +237,21 @@ var SessionBranchRdb = class extends SessionBranch {
|
|
|
216
237
|
getAgent: (id) => {
|
|
217
238
|
return this.ctx.get("agents")?.get(id);
|
|
218
239
|
},
|
|
219
|
-
flush: (session) => this.ctx.sessions.flush(session)
|
|
240
|
+
flush: (session) => this.ctx.sessions.flush(session),
|
|
241
|
+
resetProjections: (session) => {
|
|
242
|
+
const registry = this.ctx.get("sessionProjections");
|
|
243
|
+
if (registry?.registrations === void 0) return;
|
|
244
|
+
for (const registration of registry.registrations.values()) registration.cells.delete(session);
|
|
245
|
+
},
|
|
246
|
+
refreshProjectionCache: async (session) => {
|
|
247
|
+
const cache = this.ctx.get("sessionProjectionCache");
|
|
248
|
+
if (cache === void 0) return;
|
|
249
|
+
try {
|
|
250
|
+
await cache.write(session);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
this.ctx.logger.warn(`session-rdb: projection cache refresh after rewind for "${session.id}" failed (cache stays stale): ${String(error)}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
220
255
|
});
|
|
221
256
|
readBranchPrefix(id, atSeq, mode, signal) {
|
|
222
257
|
return this.provider.readBranchPrefix(id, atSeq, mode, signal);
|
package/dist/import.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as SessionPersistenceRdb } from "./index-
|
|
1
|
+
import { n as SessionPersistenceRdb } from "./index-CgAqCQAb.mjs";
|
|
2
2
|
import { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
|
|
3
3
|
import { Session, SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
|
|
4
4
|
import { Context } from "@deepseek-ai/cordis";
|
package/dist/import.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as persistImport, i as parseJsonlArtifact, n as SESSION_LOG_ARTIFACT_FILENAME, o as registerSessionImport, r as parseImportZip, t as SESSION_IMPORT_PATH } from "./import-
|
|
1
|
+
import { a as persistImport, i as parseJsonlArtifact, n as SESSION_LOG_ARTIFACT_FILENAME, o as registerSessionImport, r as parseImportZip, t as SESSION_IMPORT_PATH } from "./import-BPEHfHNk.mjs";
|
|
2
2
|
export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, parseImportZip, parseJsonlArtifact, persistImport, registerSessionImport };
|
|
@@ -10,10 +10,27 @@ interface LiveSessionHooks {
|
|
|
10
10
|
getSession(id: SessionId): Session | undefined;
|
|
11
11
|
getAgent(id: SessionId): LiveAgentLike | undefined;
|
|
12
12
|
flush(session: Session): Promise<boolean>;
|
|
13
|
+
resetProjections?(session: Session): void;
|
|
14
|
+
refreshProjectionCache?(session: ProjectionCacheSession): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 投影检查点刷新所需的最小会话面:cold rewind 没有 live Session 可复用,
|
|
18
|
+
* 而截断后的前缀未必是合法的独立会话(surface 引用可能悬空),不能走
|
|
19
|
+
* `Session.create` 的校验;缓存服务只读 id / header / inheritedEventCount
|
|
20
|
+
* 与事件前缀,用普通对象承载即可。
|
|
21
|
+
*/
|
|
22
|
+
interface ProjectionCacheSession {
|
|
23
|
+
readonly id: SessionId;
|
|
24
|
+
readonly header: SessionHeader;
|
|
25
|
+
readonly inheritedEventCount: SessionLogOffset;
|
|
26
|
+
snapshotEvents(): readonly SessionEvent[];
|
|
13
27
|
}
|
|
14
28
|
interface LiveAgentLike {
|
|
15
29
|
session: Session;
|
|
16
30
|
requestHeaderLogged?: boolean;
|
|
31
|
+
inbox?: {
|
|
32
|
+
clear(): void;
|
|
33
|
+
};
|
|
17
34
|
}
|
|
18
35
|
declare class SessionBranchRdbProvider implements SessionBranchProvider {
|
|
19
36
|
private readonly persistence;
|
|
@@ -27,6 +44,11 @@ declare class SessionBranchRdbProvider implements SessionBranchProvider {
|
|
|
27
44
|
}>;
|
|
28
45
|
forkFrom(sourceId: SessionId, options?: ForkFromOptions, signal?: AbortSignal): Promise<SessionId>;
|
|
29
46
|
rewind(id: SessionId, toBoundary: number, signal?: AbortSignal): Promise<SessionPersistenceSnapshot>;
|
|
47
|
+
/**
|
|
48
|
+
* 让持久化投影检查点跟随截断后的 log(见 {@link LiveSessionHooks.refreshProjectionCache})。
|
|
49
|
+
* 缓存是派生数据:刷新失败只让缓存多滞后一轮,不能让 rewind 失败。
|
|
50
|
+
*/
|
|
51
|
+
private refreshProjectionCache;
|
|
30
52
|
}
|
|
31
53
|
declare class SessionBranchRdb extends SessionBranch {
|
|
32
54
|
static inject: string[];
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { o as SCHEMA_VERSION } from "./schema-COK7-wRV.mjs";
|
|
2
|
-
import { a as SessionBranchRdbProvider, i as SessionBranchRdb, n as SessionPersistenceRdb, o as locateTurnEnd, r as SessionPersistenceRdbInternals, t as Config } from "./index-
|
|
2
|
+
import { a as SessionBranchRdbProvider, i as SessionBranchRdb, n as SessionPersistenceRdb, o as locateTurnEnd, r as SessionPersistenceRdbInternals, t as Config } from "./index-CgAqCQAb.mjs";
|
|
3
3
|
export { Config, SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionPersistenceRdb, SessionPersistenceRdb as default, SessionPersistenceRdbInternals, locateTurnEnd };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as SCHEMA_VERSION, g as WriteGuard, h as toPostgresSchema, i as EVENT_ENCODING, m as postgresTableDefs, r as DEFAULT_BUSY_TIMEOUT_MS, s as eventDimensions, t as SqliteBackend } from "./sqlite-
|
|
1
|
+
import { a as SCHEMA_VERSION, g as WriteGuard, h as toPostgresSchema, i as EVENT_ENCODING, m as postgresTableDefs, r as DEFAULT_BUSY_TIMEOUT_MS, s as eventDimensions, t as SqliteBackend } from "./sqlite-IH5I48aL.mjs";
|
|
2
2
|
import { c as repairRequestHeaders, d as rowToMeta, f as scanRows, g as toJsonlArtifact, m as sessionInsertRow, p as sessionConflictRow, s as repairReadView } from "./log-BIuXv09P.mjs";
|
|
3
|
-
import { c as SessionBranchRdbProvider, l as locateTurnEnd, o as registerSessionImport, s as SessionBranchRdb } from "./import-
|
|
3
|
+
import { c as SessionBranchRdbProvider, l as locateTurnEnd, o as registerSessionImport, s as SessionBranchRdb } from "./import-BPEHfHNk.mjs";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { Pool } from "pg";
|
|
@@ -10,10 +10,11 @@ import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-sessi
|
|
|
10
10
|
import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
11
11
|
import { and, eq, gte, sql } from "drizzle-orm";
|
|
12
12
|
import { readdirSync } from "node:fs";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
13
14
|
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
|
14
15
|
//#region src/postgres.ts
|
|
15
16
|
/** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
|
|
16
|
-
const postgresMigrationsDir = new URL("../drizzle/postgres/", import.meta.url)
|
|
17
|
+
const postgresMigrationsDir = fileURLToPath(new URL("../drizzle/postgres/", import.meta.url));
|
|
17
18
|
var PostgresBackend = class PostgresBackend {
|
|
18
19
|
db;
|
|
19
20
|
options;
|
|
@@ -6,6 +6,7 @@ import { bigint, check as check$1, index as index$1, integer as integer$1, pgSch
|
|
|
6
6
|
import { readdirSync, statSync } from "node:fs";
|
|
7
7
|
import { mkdir, open } from "node:fs/promises";
|
|
8
8
|
import { dirname, resolve } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
9
10
|
import { DatabaseSync } from "node:sqlite";
|
|
10
11
|
import { drizzle } from "drizzle-orm/node-sqlite";
|
|
11
12
|
import { migrate } from "drizzle-orm/node-sqlite/migrator";
|
|
@@ -482,7 +483,7 @@ function eventDimensions(event) {
|
|
|
482
483
|
//#endregion
|
|
483
484
|
//#region src/sqlite.ts
|
|
484
485
|
/** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
|
|
485
|
-
const sqliteMigrationsDir = new URL("../drizzle/sqlite/", import.meta.url)
|
|
486
|
+
const sqliteMigrationsDir = fileURLToPath(new URL("../drizzle/sqlite/", import.meta.url));
|
|
486
487
|
const sqliteTxQueues = /* @__PURE__ */ new Map();
|
|
487
488
|
function enqueueSqliteTx(path, fn) {
|
|
488
489
|
const run = (sqliteTxQueues.get(path) ?? Promise.resolve()).then(fn);
|
package/dist/storage.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as SCHEMA_VERSION, c as eventKind, d as tSchemaMeta, f as tSessionEvents, g as WriteGuard, i as EVENT_ENCODING, l as tEvents, n as openDatabase, o as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, p as tSessions, r as DEFAULT_BUSY_TIMEOUT_MS, s as eventDimensions, t as SqliteBackend, u as tPersistenceState } from "./sqlite-
|
|
1
|
+
import { a as SCHEMA_VERSION, c as eventKind, d as tSchemaMeta, f as tSessionEvents, g as WriteGuard, i as EVENT_ENCODING, l as tEvents, n as openDatabase, o as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, p as tSessions, r as DEFAULT_BUSY_TIMEOUT_MS, s as eventDimensions, t as SqliteBackend, u as tPersistenceState } from "./sqlite-IH5I48aL.mjs";
|
|
2
2
|
export { DEFAULT_BUSY_TIMEOUT_MS, EVENT_ENCODING, SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, SqliteBackend, WriteGuard, eventDimensions, eventKind, openDatabase, tEvents, tPersistenceState, tSchemaMeta, tSessionEvents, tSessions };
|
package/dist/testing.mjs
CHANGED
|
@@ -5116,7 +5116,7 @@ function manageArtifactAttachment(attachment) {
|
|
|
5116
5116
|
if (attachment.body != null) attachment.bodyEncoding ??= "base64";
|
|
5117
5117
|
}
|
|
5118
5118
|
//#endregion
|
|
5119
|
-
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.
|
|
5119
|
+
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.1_happy-dom@20.14.3_vite@8.2.2__e05875f349154650201ff2ddb1226938/node_modules/vitest/dist/chunks/utils.BX5Fg8C4.js
|
|
5120
5120
|
const NAME_WORKER_STATE = "__vitest_worker__";
|
|
5121
5121
|
function getWorkerState() {
|
|
5122
5122
|
const workerState = globalThis[NAME_WORKER_STATE];
|
|
@@ -10019,10 +10019,10 @@ function offsetToLineNumber(source, offset) {
|
|
|
10019
10019
|
return line + 1;
|
|
10020
10020
|
}
|
|
10021
10021
|
//#endregion
|
|
10022
|
-
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.
|
|
10022
|
+
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.1_happy-dom@20.14.3_vite@8.2.2__e05875f349154650201ff2ddb1226938/node_modules/vitest/dist/chunks/_commonjsHelpers.D26ty3Ew.js
|
|
10023
10023
|
var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
|
|
10024
10024
|
//#endregion
|
|
10025
|
-
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.
|
|
10025
|
+
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.1_happy-dom@20.14.3_vite@8.2.2__e05875f349154650201ff2ddb1226938/node_modules/vitest/dist/chunks/rpc.MzXet3jl.js
|
|
10026
10026
|
const RealDate = Date;
|
|
10027
10027
|
let now = null;
|
|
10028
10028
|
var MockDate = class MockDate extends RealDate {
|
|
@@ -10987,7 +10987,7 @@ function raceWith(promise, other) {
|
|
|
10987
10987
|
}))]);
|
|
10988
10988
|
}
|
|
10989
10989
|
//#endregion
|
|
10990
|
-
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.
|
|
10990
|
+
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.1_happy-dom@20.14.3_vite@8.2.2__e05875f349154650201ff2ddb1226938/node_modules/vitest/dist/chunks/test.DNmyFkvJ.js
|
|
10991
10991
|
var fakeTimersSrc = {};
|
|
10992
10992
|
var global$1;
|
|
10993
10993
|
var hasRequiredGlobal;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/session-rdb",
|
|
3
|
-
"version": "0.0.16
|
|
3
|
+
"version": "0.0.16",
|
|
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",
|
|
@@ -35,27 +35,32 @@
|
|
|
35
35
|
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@morlay/session-branch": "^0.0.4
|
|
38
|
+
"@morlay/session-branch": "^0.0.4",
|
|
39
39
|
"drizzle-orm": "^1.0.0-rc.5-ab785fc",
|
|
40
40
|
"fflate": "^0.8.2",
|
|
41
41
|
"pg": "^8.23.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@deepseek-ai/dsh-session-projection": "^0.1.5-
|
|
45
|
-
"@deepseek-ai/dsh-
|
|
44
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.5-rc.1",
|
|
45
|
+
"@deepseek-ai/dsh-session-projection-cache": "^0.1.5-rc.1",
|
|
46
|
+
"@deepseek-ai/dsh-session-turn-outline": "^0.1.5-rc.1",
|
|
47
|
+
"@deepseek-ai/dsh-storage": "^0.1.5-rc.1",
|
|
48
|
+
"@deepseek-ai/dsh-storage-domain": "^0.1.5-rc.1",
|
|
49
|
+
"@deepseek-ai/dsh-storage-json": "^0.1.5-rc.1",
|
|
50
|
+
"@deepseek-ai/dsh-token-meter": "^0.1.5-rc.1",
|
|
46
51
|
"@types/pg": "^8.23.1",
|
|
47
52
|
"drizzle-kit": "1.0.0-rc.5-ab785fc"
|
|
48
53
|
},
|
|
49
54
|
"peerDependencies": {
|
|
50
55
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
51
|
-
"@deepseek-ai/dsh-invariants": "^0.1.5-
|
|
52
|
-
"@deepseek-ai/dsh-llm": "^0.1.5-
|
|
53
|
-
"@deepseek-ai/dsh-scope": "^0.1.5-
|
|
54
|
-
"@deepseek-ai/dsh-session": "^0.1.5-
|
|
55
|
-
"@deepseek-ai/dsh-session-format": "^0.1.5-
|
|
56
|
-
"@deepseek-ai/dsh-session-format-catalog": "^0.1.5-
|
|
57
|
-
"@deepseek-ai/dsh-session-persistence": "^0.1.5-
|
|
58
|
-
"@deepseek-ai/dsh-settings": "^0.1.5-
|
|
56
|
+
"@deepseek-ai/dsh-invariants": "^0.1.5-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-scope": "^0.1.5-rc.1",
|
|
59
|
+
"@deepseek-ai/dsh-session": "^0.1.5-rc.1",
|
|
60
|
+
"@deepseek-ai/dsh-session-format": "^0.1.5-rc.1",
|
|
61
|
+
"@deepseek-ai/dsh-session-format-catalog": "^0.1.5-rc.1",
|
|
62
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.5-rc.1",
|
|
63
|
+
"@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
|
|
59
64
|
"@deepseek-ai/schemastery": "^3.18.2"
|
|
60
65
|
},
|
|
61
66
|
"dsh": {
|
package/src/branch.ts
CHANGED
|
@@ -66,12 +66,45 @@ export interface LiveSessionHooks {
|
|
|
66
66
|
getAgent(id: SessionId): LiveAgentLike | undefined;
|
|
67
67
|
|
|
68
68
|
flush(session: Session): Promise<boolean>;
|
|
69
|
+
|
|
70
|
+
// 丢弃 live 会话已缓存的投影单元:rewind 直接截断内存 log,投影 registry
|
|
71
|
+
// 的 observedSeq 仍停在截断前,后续 append 的事件(seq 更小)会被 drive()
|
|
72
|
+
// 跳过,重放输入永远进不了 inbox 投影。可选:纯持久化环境没有投影服务。
|
|
73
|
+
resetProjections?(session: Session): void;
|
|
74
|
+
|
|
75
|
+
// 用截断后的 log 重写该会话的持久化投影检查点:rewind 不会产生事件,
|
|
76
|
+
// 缓存行的水位(以及基于它折出的值)仍停在截断前。超前行不会被前端投影
|
|
77
|
+
// store 的 higher-seq-wins 规则覆盖(rewind 后的正确值 seq 更小),表现为
|
|
78
|
+
// 轮次导航残留被删除的旧轮次。可选:纯持久化环境没有投影缓存服务。
|
|
79
|
+
refreshProjectionCache?(session: ProjectionCacheSession): Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 投影检查点刷新所需的最小会话面:cold rewind 没有 live Session 可复用,
|
|
84
|
+
* 而截断后的前缀未必是合法的独立会话(surface 引用可能悬空),不能走
|
|
85
|
+
* `Session.create` 的校验;缓存服务只读 id / header / inheritedEventCount
|
|
86
|
+
* 与事件前缀,用普通对象承载即可。
|
|
87
|
+
*/
|
|
88
|
+
export interface ProjectionCacheSession {
|
|
89
|
+
readonly id: SessionId;
|
|
90
|
+
readonly header: SessionHeader;
|
|
91
|
+
readonly inheritedEventCount: SessionLogOffset;
|
|
92
|
+
snapshotEvents(): readonly SessionEvent[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 投影 registry 的失效面(上游私有结构,duck-type 读取)。
|
|
96
|
+
interface ProjectionRegistryLike {
|
|
97
|
+
registrations?: Map<string, { cells: WeakMap<object, unknown> }>;
|
|
69
98
|
}
|
|
70
99
|
|
|
71
100
|
export interface LiveAgentLike {
|
|
72
101
|
session: Session;
|
|
73
102
|
|
|
74
103
|
requestHeaderLogged?: boolean;
|
|
104
|
+
|
|
105
|
+
// 上游 Agent 的 inbox 契约面(ReactLoopInbox.clear):rewind 后残留的排队
|
|
106
|
+
// 输入必须 durable 取消,否则 agent 会继续处理它们。可选:测试替身可能没有。
|
|
107
|
+
inbox?: { clear(): void };
|
|
75
108
|
}
|
|
76
109
|
|
|
77
110
|
export function truncateLiveSession(session: Session, newLength: number): void {
|
|
@@ -246,8 +279,12 @@ export class SessionBranchRdbProvider implements SessionBranchProvider {
|
|
|
246
279
|
if (live !== undefined) await this.live.flush(live);
|
|
247
280
|
// 边界校验用原始事件(readLog,不补 closers)——inspect 会给未闭合
|
|
248
281
|
// log 补合成 closers,把 user/message 边界掩盖成 turn/end,丢失 exclusive
|
|
249
|
-
// 语义。
|
|
250
|
-
|
|
282
|
+
// 语义。cold 分支同时取回 inheritedEventCount:刷新投影检查点时要用它
|
|
283
|
+
// 重建与存储一致的会话身份。
|
|
284
|
+
const raw = live === undefined ? await this.persistence.readLog(id, {}, signal) : undefined;
|
|
285
|
+
if (live === undefined && raw === undefined) {
|
|
286
|
+
throw new SessionBranchError(`session "${id}" not found`, "SESSION_NOT_FOUND");
|
|
287
|
+
}
|
|
251
288
|
const inspection =
|
|
252
289
|
live === undefined ? undefined : await this.persistence.internals().inspect(id, signal);
|
|
253
290
|
const events = live === undefined ? raw!.events : inspection!.events;
|
|
@@ -270,7 +307,8 @@ export class SessionBranchRdbProvider implements SessionBranchProvider {
|
|
|
270
307
|
// 编辑版替换)。exclusive 截断可能残留孤儿 step/start,经平衡化剔除。
|
|
271
308
|
const rawKeepLength =
|
|
272
309
|
toBoundary === -1 ? 0 : boundaryEvent!.type === "turn/end" ? toBoundary + 1 : toBoundary;
|
|
273
|
-
const
|
|
310
|
+
const kept = balanceRewindPrefix(events.slice(0, rawKeepLength));
|
|
311
|
+
const keepLength = kept.length;
|
|
274
312
|
|
|
275
313
|
const internals = this.persistence.internals();
|
|
276
314
|
// 原样存储(无过滤):live 视图与 RDB head 同空间(上游 seq),
|
|
@@ -314,6 +352,9 @@ export class SessionBranchRdbProvider implements SessionBranchProvider {
|
|
|
314
352
|
// agent 轮次游标。不调用 load——live 时 load 会先 flush 把旧内存写回,
|
|
315
353
|
// 撤销本次截断。
|
|
316
354
|
truncateLiveSession(live, keepLength);
|
|
355
|
+
// 投影缓存同样停在截断前的水位,必须先失效,否则截断后的重放事件
|
|
356
|
+
// (seq 回退)不会进入投影。
|
|
357
|
+
this.live.resetProjections?.(live);
|
|
317
358
|
const agent = this.live.getAgent(id);
|
|
318
359
|
if (agent !== undefined) {
|
|
319
360
|
agent.requestHeaderLogged = false;
|
|
@@ -330,6 +371,24 @@ export class SessionBranchRdbProvider implements SessionBranchProvider {
|
|
|
330
371
|
if (handle !== undefined) {
|
|
331
372
|
handle.resetAfterRewind(keepLength, newSeedLength === null ? undefined : newSeedLength);
|
|
332
373
|
}
|
|
374
|
+
// 截断后强制清空排队输入:保留区里未被截断的 pending(`agent/inbox/spliced`)
|
|
375
|
+
// 仍会被 agent 处理,必须 durable 取消。cursor 已对齐,取消事件从截断点续接。
|
|
376
|
+
agent?.inbox?.clear();
|
|
377
|
+
// 取消事件先经 coordinator 缓冲,落盘后 rewind 才真正 durable(调用方
|
|
378
|
+
// 可能在 rewind 返回后直接读后端)。
|
|
379
|
+
await this.live.flush(live);
|
|
380
|
+
// 持久化投影检查点也要退到截断后的水位:rewind 不产生事件,缓存行
|
|
381
|
+
// 会停在截断前;超前行锁死前端(higher-seq-wins),轮次导航残留旧轮次。
|
|
382
|
+
await this.refreshProjectionCache(live);
|
|
383
|
+
} else {
|
|
384
|
+
// cold:用截断后的前缀 + 存储身份构造最小会话面,让缓存服务按截断后
|
|
385
|
+
// 的 log 重写检查点(没有 live 会话可复用,也不为此 resume 一个 agent)。
|
|
386
|
+
await this.refreshProjectionCache({
|
|
387
|
+
id,
|
|
388
|
+
header: meta,
|
|
389
|
+
inheritedEventCount: SessionLogOffset(raw!.inheritedEventCount),
|
|
390
|
+
snapshotEvents: () => kept,
|
|
391
|
+
});
|
|
333
392
|
}
|
|
334
393
|
|
|
335
394
|
const row = await internals.backend.getSession(id);
|
|
@@ -354,6 +413,19 @@ export class SessionBranchRdbProvider implements SessionBranchProvider {
|
|
|
354
413
|
}
|
|
355
414
|
return { header: rowToMeta(row), revision: (await internals.readStoredRevision(id))! };
|
|
356
415
|
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* 让持久化投影检查点跟随截断后的 log(见 {@link LiveSessionHooks.refreshProjectionCache})。
|
|
419
|
+
* 缓存是派生数据:刷新失败只让缓存多滞后一轮,不能让 rewind 失败。
|
|
420
|
+
*/
|
|
421
|
+
private async refreshProjectionCache(session: ProjectionCacheSession): Promise<void> {
|
|
422
|
+
if (this.live.refreshProjectionCache === undefined) return;
|
|
423
|
+
try {
|
|
424
|
+
await this.live.refreshProjectionCache(session);
|
|
425
|
+
} catch {
|
|
426
|
+
// 实现方(SessionBranchRdb)已记日志;此处只保证 rewind 不因派生数据失败。
|
|
427
|
+
}
|
|
428
|
+
}
|
|
357
429
|
}
|
|
358
430
|
|
|
359
431
|
export class SessionBranchRdb extends SessionBranch {
|
|
@@ -375,6 +447,30 @@ export class SessionBranchRdb extends SessionBranch {
|
|
|
375
447
|
return agents?.get(id);
|
|
376
448
|
},
|
|
377
449
|
flush: (session) => this.ctx.sessions.flush(session),
|
|
450
|
+
resetProjections: (session) => {
|
|
451
|
+
// sessionProjections 是可选服务(纯持久化环境无 agent-loop/投影)。
|
|
452
|
+
const registry = (this.ctx as unknown as { get(name: string): unknown }).get(
|
|
453
|
+
"sessionProjections",
|
|
454
|
+
) as ProjectionRegistryLike | undefined;
|
|
455
|
+
if (registry?.registrations === undefined) return;
|
|
456
|
+
for (const registration of registry.registrations.values()) {
|
|
457
|
+
registration.cells.delete(session);
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
refreshProjectionCache: async (session) => {
|
|
461
|
+
// sessionProjectionCache 是可选服务(纯持久化环境无投影缓存)。
|
|
462
|
+
const cache = this.ctx.get("sessionProjectionCache") as
|
|
463
|
+
| { write(session: ProjectionCacheSession): Promise<void> }
|
|
464
|
+
| undefined;
|
|
465
|
+
if (cache === undefined) return;
|
|
466
|
+
try {
|
|
467
|
+
await cache.write(session);
|
|
468
|
+
} catch (error: unknown) {
|
|
469
|
+
this.ctx.logger.warn(
|
|
470
|
+
`session-rdb: projection cache refresh after rewind for "${session.id}" failed (cache stays stale): ${String(error)}`,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
},
|
|
378
474
|
},
|
|
379
475
|
);
|
|
380
476
|
|
package/src/postgres.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { readdirSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { and, eq, gte, sql } from "drizzle-orm";
|
|
4
5
|
import type { PgAsyncDatabase, PgAsyncTransaction } from "drizzle-orm/pg-core";
|
|
5
6
|
import type { NodePgDatabase, NodePgQueryResultHKT } from "drizzle-orm/node-postgres";
|
|
@@ -18,7 +19,7 @@ import { postgresTableDefs } from "./entities/index.ts";
|
|
|
18
19
|
import { sessionConflictRow, sessionInsertRow } from "./log.ts";
|
|
19
20
|
|
|
20
21
|
/** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
|
|
21
|
-
const postgresMigrationsDir = new URL("../drizzle/postgres/", import.meta.url)
|
|
22
|
+
const postgresMigrationsDir = fileURLToPath(new URL("../drizzle/postgres/", import.meta.url));
|
|
22
23
|
|
|
23
24
|
export interface PostgresBackendOptions {
|
|
24
25
|
identityBase: string;
|
package/src/sqlite.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { readdirSync, statSync } from "node:fs";
|
|
3
3
|
import { mkdir, open } from "node:fs/promises";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { DatabaseSync } from "node:sqlite";
|
|
6
7
|
import { and, eq, gte, sql } from "drizzle-orm";
|
|
7
8
|
import { drizzle, type NodeSQLiteDatabase } from "drizzle-orm/node-sqlite";
|
|
@@ -30,7 +31,7 @@ import {
|
|
|
30
31
|
type SqliteDb = NodeSQLiteDatabase & { $client: DatabaseSync };
|
|
31
32
|
|
|
32
33
|
/** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
|
|
33
|
-
const sqliteMigrationsDir = new URL("../drizzle/sqlite/", import.meta.url)
|
|
34
|
+
const sqliteMigrationsDir = fileURLToPath(new URL("../drizzle/sqlite/", import.meta.url));
|
|
34
35
|
|
|
35
36
|
const sqliteTxQueues = new Map<string, Promise<void>>();
|
|
36
37
|
|