@morlay/session-rdb 0.0.19 → 0.0.21-alpha.0
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 +59 -31
- package/dist/artifact.d.mts +25 -65
- package/dist/artifact.mjs +3 -3
- package/dist/{schema-DPcuEh_a.d.mts → backend-DpdtxYpz.d.mts} +360 -107
- package/dist/branch-5JzX9rUq.mjs +390 -0
- package/dist/deletion.d.mts +7 -0
- package/dist/deletion.mjs +2 -0
- package/dist/dist-vIVO6bA-.mjs +1524 -0
- package/dist/import.d.mts +4 -4
- package/dist/import.mjs +214 -1
- package/dist/index.d.mts +2 -3
- package/dist/index.mjs +4 -1630
- package/dist/{log-DO69NQnn.mjs → log-CnYct2Dv.mjs} +37 -82
- package/dist/{sqlite-DYExtbLo.mjs → sqlite-fpvm5Dzs.mjs} +205 -75
- package/dist/src-CWTWV7vx.mjs +1904 -0
- package/dist/storage.d.mts +14 -5
- package/dist/storage.mjs +2 -2
- package/dist/testing.d.mts +2 -26
- package/dist/testing.mjs +10504 -9614
- package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +14 -0
- package/drizzle/postgres/20260918120000_v3_event_usage/snapshot.json +1309 -0
- package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +14 -0
- package/drizzle/sqlite/20260918120000_v3_event_usage/snapshot.json +1031 -0
- package/package.json +29 -30
- package/src/adapters/to-postgres.ts +1 -3
- package/src/adapters/to-sqlite.ts +0 -2
- package/src/adapters/types.ts +0 -3
- package/src/artifact.ts +0 -2
- package/src/backend.ts +31 -2
- package/src/branch.ts +223 -135
- package/src/deletion.ts +87 -0
- package/src/drizzle/postgres-v2.ts +0 -1
- package/src/drizzle/postgres-v3.ts +0 -1
- package/src/drizzle/sqlite-v2.ts +0 -1
- package/src/drizzle/sqlite-v3.ts +0 -1
- package/src/entities/v2/session-events.ts +0 -1
- package/src/entities/v3/event-usage.ts +25 -0
- package/src/entities/v3/events.ts +1 -3
- package/src/entities/v3/index.ts +4 -0
- package/src/entities/v3/session-events.ts +1 -2
- package/src/entities/v3/session-projcache-rows.ts +0 -8
- package/src/entities/v3/sessions.ts +3 -3
- package/src/entities/v3/storage-units.ts +0 -1
- package/src/entities/v3/workspace-sessions.ts +0 -5
- package/src/entities/v3/workspace-state.ts +0 -6
- package/src/entities/v3/workspaces.ts +0 -5
- package/src/export.ts +103 -0
- package/src/gc.ts +76 -0
- package/src/import-storages.ts +4 -37
- package/src/import.ts +54 -31
- package/src/index.ts +150 -114
- package/src/legacy.ts +4 -42
- package/src/log.ts +63 -106
- package/src/postgres.ts +249 -22
- package/src/schema.ts +6 -6
- package/src/session-query.ts +0 -4
- package/src/sqlite.ts +231 -55
- package/src/storage-takeover/index.ts +2 -28
- package/src/storage-takeover/projection-cache.ts +30 -134
- package/src/storage-takeover/repository.ts +20 -59
- package/src/storage-takeover/storage-backend.ts +3 -33
- package/src/storage-takeover/types.ts +14 -56
- package/src/storage.ts +0 -2
- package/src/testing/contract.ts +19 -50
- package/src/testing/coordinator-contract.ts +10 -25
- package/src/testing.ts +2 -2
- package/src/usage.ts +191 -0
- package/dist/import-Bc2QQa5G.mjs +0 -473
- package/dist/index-CCcK9tia.d.mts +0 -270
- package/dist/magic-string.es-BgJoa-3K.mjs +0 -1017
package/dist/import.d.mts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { u as SessionPersistenceRdb } from "./backend-DpdtxYpz.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";
|
|
5
5
|
//#region src/import.d.ts
|
|
6
|
-
/** 当前世代产物的文件名(读入侧接受任意 canonical 世代名,见 parseImportZip)。 */
|
|
7
6
|
declare const SESSION_LOG_ARTIFACT_FILENAME = "session.jsonl";
|
|
8
7
|
declare const SESSION_IMPORT_PATH = "/api/session.import";
|
|
9
8
|
declare function parseJsonlArtifact(content: string): SessionStorageMetadata & {
|
|
10
9
|
events: SessionEvent[];
|
|
11
10
|
};
|
|
12
|
-
declare function parseImportZip(zip: Uint8Array): SessionStorageMetadata & {
|
|
11
|
+
declare function parseImportZip(zip: Uint8Array): Promise<SessionStorageMetadata & {
|
|
13
12
|
events: SessionEvent[];
|
|
14
|
-
}
|
|
13
|
+
}>;
|
|
15
14
|
declare function persistImport(persistence: SessionPersistenceRdb, branch: {
|
|
16
15
|
rewind(id: SessionId, toBoundary: number): Promise<unknown>;
|
|
16
|
+
resetLiveDerivedState?(session: Session): void;
|
|
17
17
|
} | undefined, imported: SessionStorageMetadata & {
|
|
18
18
|
events: SessionEvent[];
|
|
19
19
|
}, targetId?: SessionId, sessions?: {
|
package/dist/import.mjs
CHANGED
|
@@ -1,2 +1,215 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as replaceLiveSessionLog } from "./branch-5JzX9rUq.mjs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
|
|
4
|
+
import { parseSessionFormatLogFilename } from "@deepseek-ai/dsh-session-format";
|
|
5
|
+
import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
6
|
+
import { balanceRewindPrefix } from "@morlay/session-branch";
|
|
7
|
+
import { unzip } from "fflate";
|
|
8
|
+
//#region src/import.ts
|
|
9
|
+
const SESSION_LOG_ARTIFACT_FILENAME = "session.jsonl";
|
|
10
|
+
const SESSION_IMPORT_PATH = "/api/session.import";
|
|
11
|
+
const MAX_IMPORT_ZIP_BYTES = 67108864;
|
|
12
|
+
function parseJsonlArtifact(content) {
|
|
13
|
+
const lines = content.split("\n");
|
|
14
|
+
if (lines.length === 0 || lines[0] === "") throw new Error("imported session log is empty");
|
|
15
|
+
let header;
|
|
16
|
+
try {
|
|
17
|
+
header = JSON.parse(lines[0]);
|
|
18
|
+
} catch {
|
|
19
|
+
throw new Error("imported session log has an unparsable header line");
|
|
20
|
+
}
|
|
21
|
+
let restore;
|
|
22
|
+
try {
|
|
23
|
+
restore = sessionFormatCatalog.createRestore(header, {
|
|
24
|
+
recovery: "strict",
|
|
25
|
+
validation: "transformed"
|
|
26
|
+
});
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new Error(`imported session log has an invalid header line: ${error instanceof Error ? error.message : String(error)}`);
|
|
29
|
+
}
|
|
30
|
+
for (let i = 1; i < lines.length; i++) {
|
|
31
|
+
const line = lines[i];
|
|
32
|
+
if (line === void 0 || line === "") continue;
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(line);
|
|
36
|
+
} catch {
|
|
37
|
+
throw new Error(`imported session log has an unparsable event line at ${i}`);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
restore.decodeRow(parsed);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new Error(`imported session log has an invalid event line at ${i}: ${error instanceof Error ? error.message : String(error)}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
let artifact;
|
|
46
|
+
try {
|
|
47
|
+
artifact = restore.finish();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new Error(`imported session log is incomplete: ${error instanceof Error ? error.message : String(error)}`);
|
|
50
|
+
}
|
|
51
|
+
const meta = artifact.header;
|
|
52
|
+
const events = artifact.events;
|
|
53
|
+
for (let i = 0; i < events.length; i++) if (events[i].seq !== i) throw new Error(`imported session log seq gap at ${i} (got ${events[i].seq}); import requires a dense log`);
|
|
54
|
+
const inheritedEventCount = Math.min(artifact.inheritedEventCount, events.length);
|
|
55
|
+
return {
|
|
56
|
+
meta: {
|
|
57
|
+
version: SESSION_FORMAT_VERSION,
|
|
58
|
+
id: meta.id,
|
|
59
|
+
createdAt: meta.createdAt,
|
|
60
|
+
...meta.cwd === void 0 ? {} : { cwd: meta.cwd },
|
|
61
|
+
...meta.parentSession === void 0 ? {} : { parentSession: meta.parentSession },
|
|
62
|
+
isSeeded: meta.isSeeded,
|
|
63
|
+
...meta.origin === void 0 ? {} : { origin: meta.origin },
|
|
64
|
+
...meta.delegationDepth === void 0 ? {} : { delegationDepth: meta.delegationDepth },
|
|
65
|
+
...meta.agentPreset === void 0 ? {} : { agentPreset: meta.agentPreset }
|
|
66
|
+
},
|
|
67
|
+
inheritedEventCount: SessionLogOffset(inheritedEventCount),
|
|
68
|
+
events
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* fflate 的 `unzip` 是回调式异步 API(node 侧内部走 worker_threads,浏览器侧走 Web Worker),
|
|
73
|
+
* 这里桥成 Promise。它与 `unzipSync` 的差异只在同步/异步:非法 zip 走的是同一段校验代码,
|
|
74
|
+
* 拒绝的错误对象与同步版一致,因此上层文案无需区分。
|
|
75
|
+
*/
|
|
76
|
+
function unzipAsync(data) {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
unzip(data, (error, entries) => {
|
|
79
|
+
if (error === null) resolve(entries);
|
|
80
|
+
else reject(error);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async function parseImportZip(zip) {
|
|
85
|
+
let entries;
|
|
86
|
+
try {
|
|
87
|
+
entries = await unzipAsync(zip);
|
|
88
|
+
} catch {
|
|
89
|
+
throw new Error("imported zip is not a valid ZIP archive");
|
|
90
|
+
}
|
|
91
|
+
const artifact = Object.entries(entries).find(([name]) => parseSessionFormatLogFilename(name) !== void 0);
|
|
92
|
+
if (artifact === void 0) throw new Error(`imported zip has no session log artifact (expected ${SESSION_LOG_ARTIFACT_FILENAME} or a canonical session.vN.jsonl)`);
|
|
93
|
+
return parseJsonlArtifact(new TextDecoder().decode(artifact[1]));
|
|
94
|
+
}
|
|
95
|
+
async function stopAgentLoop(ctx, sessionId) {
|
|
96
|
+
const agent = ctx.get("agents")?.get(sessionId);
|
|
97
|
+
if (agent === void 0) return;
|
|
98
|
+
agent.cancel?.({ kind: "user" }, { keepInbox: true });
|
|
99
|
+
await agent.whenIdle();
|
|
100
|
+
}
|
|
101
|
+
async function persistImport(persistence, branch, imported, targetId, sessions, stopLoop) {
|
|
102
|
+
const id = targetId ?? `session-${randomUUID()}`;
|
|
103
|
+
const events = balanceRewindPrefix(imported.events, { keepOpenTail: true });
|
|
104
|
+
if (targetId !== void 0) {
|
|
105
|
+
if (branch === void 0) throw new Error("sessionBranch service is unavailable");
|
|
106
|
+
if (stopLoop !== void 0) await stopLoop(targetId);
|
|
107
|
+
await branch.rewind(targetId, -1);
|
|
108
|
+
const liveHandle = persistence.tracker.writerOf(targetId);
|
|
109
|
+
if (liveHandle !== void 0) {
|
|
110
|
+
if (events.length > 0) await liveHandle.append(events);
|
|
111
|
+
} else {
|
|
112
|
+
const handle = await persistence.open(targetId, "write");
|
|
113
|
+
try {
|
|
114
|
+
if (events.length > 0) await handle.append(events);
|
|
115
|
+
} finally {
|
|
116
|
+
await handle.close();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
const handle = await persistence.create({
|
|
121
|
+
...imported.meta,
|
|
122
|
+
id
|
|
123
|
+
}, { inheritedEventCount: SessionLogOffset(Math.min(imported.inheritedEventCount, events.length)) });
|
|
124
|
+
if (events.length > 0) await handle.append(events);
|
|
125
|
+
await handle.close();
|
|
126
|
+
}
|
|
127
|
+
if (targetId !== void 0) {
|
|
128
|
+
const live = sessions?.get(targetId);
|
|
129
|
+
if (live !== void 0) {
|
|
130
|
+
replaceLiveSessionLog(live, events);
|
|
131
|
+
branch?.resetLiveDerivedState?.(live);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return id;
|
|
135
|
+
}
|
|
136
|
+
function registerSessionImport(ctx, persistence) {
|
|
137
|
+
ctx.inject(["webServer", "connection"], (webCtx) => {
|
|
138
|
+
const webServer = webCtx.webServer;
|
|
139
|
+
const connection = webCtx.get("connection");
|
|
140
|
+
return webCtx.effect(() => webServer.register({
|
|
141
|
+
kind: "exact",
|
|
142
|
+
path: SESSION_IMPORT_PATH,
|
|
143
|
+
handler: async (req, res) => {
|
|
144
|
+
const rejection = connection.requestRejection(req);
|
|
145
|
+
if (rejection !== void 0) {
|
|
146
|
+
res.writeHead(rejection);
|
|
147
|
+
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const chunks = [];
|
|
151
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
152
|
+
const body = Buffer.concat(chunks);
|
|
153
|
+
let envelope;
|
|
154
|
+
try {
|
|
155
|
+
envelope = JSON.parse(body.toString("utf8"));
|
|
156
|
+
} catch {
|
|
157
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
158
|
+
res.end(JSON.stringify({ error: "request body is not JSON" }));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (typeof envelope.zip !== "string" || envelope.zip === "") {
|
|
162
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
163
|
+
res.end(JSON.stringify({ error: "missing zip field" }));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (envelope.sessionId !== void 0 && (typeof envelope.sessionId !== "string" || envelope.sessionId === "")) {
|
|
167
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
168
|
+
res.end(JSON.stringify({ error: "sessionId must be a non-empty string" }));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
let zip;
|
|
172
|
+
try {
|
|
173
|
+
zip = Buffer.from(envelope.zip, "base64");
|
|
174
|
+
} catch {
|
|
175
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
176
|
+
res.end(JSON.stringify({ error: "zip field is not valid base64" }));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (zip.byteLength > MAX_IMPORT_ZIP_BYTES) {
|
|
180
|
+
res.writeHead(413, { "content-type": "application/json" });
|
|
181
|
+
res.end(JSON.stringify({ error: "imported zip exceeds the size limit" }));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
let imported;
|
|
185
|
+
try {
|
|
186
|
+
imported = await parseImportZip(zip);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
189
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : "imported zip is invalid" }));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const targetId = typeof envelope.sessionId === "string" ? envelope.sessionId : void 0;
|
|
193
|
+
const branch = webCtx.get("sessionBranch");
|
|
194
|
+
try {
|
|
195
|
+
const sessions = webCtx.get("sessions");
|
|
196
|
+
const id = await persistImport(persistence, branch, imported, targetId, sessions, (sessionId) => stopAgentLoop(webCtx, sessionId));
|
|
197
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
198
|
+
res.end(JSON.stringify({ sessionId: id }));
|
|
199
|
+
} catch (error) {
|
|
200
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
201
|
+
if (targetId !== void 0 && /not found/i.test(message)) {
|
|
202
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
203
|
+
res.end(JSON.stringify({ error: `session "${targetId}" not found` }));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
207
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : "import failed" }));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}), `session-rdb: ${SESSION_IMPORT_PATH} route`);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
//#endregion
|
|
2
215
|
export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, parseImportZip, parseJsonlArtifact, persistImport, registerSessionImport };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import { o as SCHEMA_VERSION } from "./
|
|
2
|
-
|
|
3
|
-
export { Config, ProjectionCacheOptions, SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionPersistenceRdb, SessionPersistenceRdb as default, SessionPersistenceRdbInternals, locateTurnEnd };
|
|
1
|
+
import { c as SessionDeletionError, d as SessionPersistenceRdbInternals, f as SessionBranchRdb, l as SessionDeletionErrorCode, m as locateTurnEnd, o as Config, p as SessionBranchRdbProvider, s as ProjectionCacheOptions, u as SessionPersistenceRdb, x as SCHEMA_VERSION } from "./backend-DpdtxYpz.mjs";
|
|
2
|
+
export { Config, ProjectionCacheOptions, SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionDeletionError, SessionDeletionErrorCode, SessionPersistenceRdb, SessionPersistenceRdb as default, SessionPersistenceRdbInternals, locateTurnEnd };
|