dsh-session-manager 0.4.4 → 0.4.7
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/CHANGELOG.md +131 -0
- package/README.md +106 -112
- package/README.zh.md +27 -35
- package/lib/client.js +23 -18
- package/lib/compat/dsh-adapter.js +200 -0
- package/lib/compat/zstd-frames.js +93 -0
- package/lib/index.js +1151 -1231
- package/package.json +4 -2
- package/scripts/heal-v2-sessions.ps1 +79 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/** Compatibility boundary for DSH services used by dsh-session-manager.
|
|
2
|
+
*
|
|
3
|
+
* DSH service names are the supported seam. Concrete implementations
|
|
4
|
+
* expose optional surfaces that changed during the 0.1.x line; keep all
|
|
5
|
+
* probes here so route code can fail closed and diagnostics may
|
|
6
|
+
* explain what this build actually provides.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-session-manager/compat/dsh-adapter
|
|
9
|
+
*/
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
|
|
16
|
+
/** Pull one service out of the Cordis ctx, tolerating direct property fallbacks. */
|
|
17
|
+
function getService(ctx, name) {
|
|
18
|
+
try {
|
|
19
|
+
const value = typeof ctx?.get === "function" ? ctx.get(name) : undefined;
|
|
20
|
+
if (value !== undefined) return value;
|
|
21
|
+
} catch { /* fall through to the legacy direct property */ }
|
|
22
|
+
return ctx && ctx[name];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Read one package's version from its package.json if resolvable from this plugin. */
|
|
26
|
+
function packageVersion(name) {
|
|
27
|
+
try {
|
|
28
|
+
const manifest = require(name + "/package.json");
|
|
29
|
+
return typeof manifest?.version === "string" ? manifest.version : undefined;
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Locate the dsh-cli installation by scanning process.argv for an
|
|
37
|
+
* `apps/cli/` segment and reading its package.json (or the workspace root).
|
|
38
|
+
* Falls back silently when the host is not the dsh CLI (e.g. a test harness).
|
|
39
|
+
*/
|
|
40
|
+
function versionFromArgv() {
|
|
41
|
+
// process.argv on Windows mixes single backslashes, escaped double backslashes,
|
|
42
|
+
// and forward slashes depending on how the host launched the binary. Match
|
|
43
|
+
// "/apps/cli/" by normalizing any of those separators to a single "/" first,
|
|
44
|
+
// then fall back to checking the parent of bin.js (covers dist bundles that
|
|
45
|
+
// relocate the bin path under lib/bin.js).
|
|
46
|
+
for (const arg of process.argv) {
|
|
47
|
+
const normalized = String(arg).replaceAll("\\\\", "/").replaceAll("\\", "/");
|
|
48
|
+
let at = normalized.indexOf("/apps/cli/");
|
|
49
|
+
if (at < 0) {
|
|
50
|
+
// dist/ build: bin.js sits under apps/cli/lib/ rather than directly
|
|
51
|
+
// under apps/cli/, so anchor on the bin file basename instead.
|
|
52
|
+
const binIdx = normalized.lastIndexOf("/bin.");
|
|
53
|
+
if (binIdx >= 0) at = normalized.indexOf("/apps/cli/", binIdx);
|
|
54
|
+
}
|
|
55
|
+
if (at < 0) continue;
|
|
56
|
+
const root = normalized.slice(0, at);
|
|
57
|
+
for (const file of [join(root, "apps", "cli", "package.json"), join(root, "package.json")]) {
|
|
58
|
+
try {
|
|
59
|
+
const manifest = JSON.parse(readFileSync(file, "utf8"));
|
|
60
|
+
if (typeof manifest.version === "string") return manifest.version;
|
|
61
|
+
} catch { /* try the next candidate */ }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Normalize a persistence list item from both snapshot (header field) and raw header shapes. **/
|
|
68
|
+
export function normalizeSessionHeader(value) {
|
|
69
|
+
if (value === undefined || value === null) return undefined;
|
|
70
|
+
const header = value?.header && typeof value.header === "object" ? value.header : value;
|
|
71
|
+
return header && typeof header.id === "string" ? header : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
export function normalizeSessionHeaders(values) {
|
|
76
|
+
if (!Array.isArray(values)) return [];
|
|
77
|
+
const out = [];
|
|
78
|
+
for (const value of values) {
|
|
79
|
+
const header = normalizeSessionHeader(value);
|
|
80
|
+
if (header !== undefined) out.push(header);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build the adapter. Exposes the public DSH surfaces this plugin needs and a
|
|
87
|
+
* `capabilities()` snapshot for diagnostics.
|
|
88
|
+
*/
|
|
89
|
+
export function createDshAdapter(ctx) {
|
|
90
|
+
const adapter = {
|
|
91
|
+
get workspaceRegistry() { return getService(ctx, "workspaceRegistry"); },
|
|
92
|
+
get sessions() { return getService(ctx, "sessions"); },
|
|
93
|
+
get agents() { return getService(ctx, "agents"); },
|
|
94
|
+
get persistence() { return getService(ctx, "sessionPersistence"); },
|
|
95
|
+
get agentPresets() { return getService(ctx, "agentPresets"); },
|
|
96
|
+
get projectionCache() { return getService(ctx, "sessionProjectionCache"); },
|
|
97
|
+
get homePath() { return getService(ctx, "dshHomePath"); },
|
|
98
|
+
|
|
99
|
+
getSession(id) {
|
|
100
|
+
try { return adapter.sessions && adapter.sessions.get && adapter.sessions.get(id); } catch { return undefined; }
|
|
101
|
+
},
|
|
102
|
+
getAgent(id) {
|
|
103
|
+
try { return adapter.agents && adapter.agents.get && adapter.agents.get(id); } catch { return undefined; }
|
|
104
|
+
},
|
|
105
|
+
getSessionEvents(session) {
|
|
106
|
+
if (Array.isArray(session && session.events)) return session.events;
|
|
107
|
+
return [];
|
|
108
|
+
},
|
|
109
|
+
normalizeSessionHeader,
|
|
110
|
+
normalizeSessionHeaders,
|
|
111
|
+
async listSessionHeaders() {
|
|
112
|
+
const persistence = adapter.persistence;
|
|
113
|
+
if (!persistence || typeof persistence.list !== "function") return [];
|
|
114
|
+
return normalizeSessionHeaders(await persistence.list());
|
|
115
|
+
},
|
|
116
|
+
capabilities() {
|
|
117
|
+
const persistence = adapter.persistence;
|
|
118
|
+
const registry = adapter.workspaceRegistry;
|
|
119
|
+
const sessions = adapter.sessions;
|
|
120
|
+
let workspaces = [];
|
|
121
|
+
try { workspaces = registry && typeof registry.list === "function" ? registry.list() : []; } catch { /* diagnostic only */ }
|
|
122
|
+
let sampleSession;
|
|
123
|
+
try { sampleSession = sessions && sessions.list && sessions.list() && sessions.list()[0]; } catch { /* diagnostic only */ }
|
|
124
|
+
return {
|
|
125
|
+
sessionList: !!(persistence && typeof persistence.list === "function"),
|
|
126
|
+
sessionStat: !!(persistence && typeof persistence.stat === "function"),
|
|
127
|
+
sessionHandleOpen: !!(persistence && typeof persistence.open === "function"),
|
|
128
|
+
persistenceLocate: !!(persistence && typeof persistence.locate === "function"),
|
|
129
|
+
persistenceResolveLog: !!(persistence && typeof persistence.resolveLog === "function"),
|
|
130
|
+
workspaceList: !!(registry && typeof registry.list === "function"),
|
|
131
|
+
workspaceAttach: !!(workspaces[0] && typeof workspaces[0].attachSession === "function"),
|
|
132
|
+
workspaceDetach: !!(workspaces[0] && typeof workspaces[0].detachSession === "function"),
|
|
133
|
+
sessionStore: !!(sessions && typeof sessions.get === "function"),
|
|
134
|
+
liveSessionEvents: !!(sampleSession && Array.isArray(sampleSession.events)),
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
dshVersion() {
|
|
138
|
+
return process.env.DSH_VERSION
|
|
139
|
+
|| packageVersion("@deepseek-ai/dsh")
|
|
140
|
+
|| packageVersion("@deepseek-ai/dsh-cli")
|
|
141
|
+
|| versionFromArgv()
|
|
142
|
+
|| "unknown";
|
|
143
|
+
}, async openSessionRead(id) {
|
|
144
|
+
const persistence = adapter.persistence;
|
|
145
|
+
if (!persistence || typeof persistence.open !== "function") {
|
|
146
|
+
throw new Error("dsh-session-manager: SessionPersistence.open is unavailable; the read path will fall back to disk");
|
|
147
|
+
}
|
|
148
|
+
return await persistence.open(id, "read");
|
|
149
|
+
},
|
|
150
|
+
async statSession(id) {
|
|
151
|
+
const persistence = adapter.persistence;
|
|
152
|
+
if (!persistence || typeof persistence.stat !== "function") return undefined;
|
|
153
|
+
return await persistence.stat(id);
|
|
154
|
+
},
|
|
155
|
+
async resolveSessionPath(id) {
|
|
156
|
+
const persistence = adapter.persistence;
|
|
157
|
+
if (!persistence) return undefined;
|
|
158
|
+
if (typeof persistence.resolveLog === "function") {
|
|
159
|
+
try { return await persistence.resolveLog(id); } catch { return undefined; }
|
|
160
|
+
}
|
|
161
|
+
if (typeof persistence.locate === "function") {
|
|
162
|
+
const header = { id: id };
|
|
163
|
+
try {
|
|
164
|
+
const located = persistence.locate(header);
|
|
165
|
+
return located && located.path;
|
|
166
|
+
} catch { return undefined; }
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
},
|
|
170
|
+
async listSessionSnapshots() {
|
|
171
|
+
const persistence = adapter.persistence;
|
|
172
|
+
if (!persistence || typeof persistence.list !== "function") return [];
|
|
173
|
+
try { return await persistence.list(); } catch { return []; }
|
|
174
|
+
},
|
|
175
|
+
workspaceEntityFor(workspaceId) {
|
|
176
|
+
const registry = adapter.workspaceRegistry;
|
|
177
|
+
if (!registry || typeof registry.get !== "function") return undefined;
|
|
178
|
+
try { return registry.get(workspaceId); } catch { return undefined; }
|
|
179
|
+
},
|
|
180
|
+
workspaceList() {
|
|
181
|
+
const registry = adapter.workspaceRegistry;
|
|
182
|
+
if (!registry || typeof registry.list !== "function") return [];
|
|
183
|
+
try { return registry.list(); } catch { return []; }
|
|
184
|
+
},
|
|
185
|
+
async readSessionEvents(id, max) {
|
|
186
|
+
const handle = await adapter.openSessionRead(id);
|
|
187
|
+
try {
|
|
188
|
+
return await handle.read(0, typeof max === "number" ? max : 1024);
|
|
189
|
+
} finally {
|
|
190
|
+
if (typeof handle.close === "function") {
|
|
191
|
+
try { await handle.close(); } catch { /* best-effort */ }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
pluginVersion() {
|
|
196
|
+
return packageVersion("dsh-session-manager") || "unknown";
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
return adapter;
|
|
200
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Strict, generation-safe Zstandard concatenated-frame helpers.
|
|
2
|
+
*
|
|
3
|
+
* DSH session artifacts are concatenated checksummed Zstandard frames (one
|
|
4
|
+
* frame per durable batch), so the multi-frame boundary must be computed
|
|
5
|
+
* from the frame header itself, not from a magic-byte scan. This is a
|
|
6
|
+
* direct port of DSH's frame scanner and shares its layout contract:
|
|
7
|
+
*
|
|
8
|
+
* - Each frame begins with the 4-byte LE magic 0xfd2fb528.
|
|
9
|
+
* - Reserved bits in the frame-header descriptor byte must be zero.
|
|
10
|
+
* - Each block carries a 3-byte LE header; reserved block type 0x3 fails.
|
|
11
|
+
* - Optional 4-byte frame checksum is appended after the last block.
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-session-manager/compat/zstd-frames
|
|
14
|
+
*/
|
|
15
|
+
import { zstdDecompress } from "node:zlib";
|
|
16
|
+
import { promisify } from "node:util";
|
|
17
|
+
|
|
18
|
+
const zstdDecompressAsync = promisify(zstdDecompress);
|
|
19
|
+
const ZSTD_MAGIC = 0xfd2fb528;
|
|
20
|
+
|
|
21
|
+
/** Locate complete Zstandard frames; an incomplete final frame is returned
|
|
22
|
+
* separately as `tornStart` and can be ignored by read-only paths.
|
|
23
|
+
*/
|
|
24
|
+
export function scanZstdFrames(buffer, maxFrames = Number.POSITIVE_INFINITY) {
|
|
25
|
+
const frames = [];
|
|
26
|
+
let offset = 0;
|
|
27
|
+
while (offset < buffer.length) {
|
|
28
|
+
const start = offset;
|
|
29
|
+
if (buffer.length - offset < 4) return { frames, tornStart: start };
|
|
30
|
+
if (buffer.readUint32LE(offset) != ZSTD_MAGIC) {
|
|
31
|
+
throw new Error(`corrupt Zstandard session log: invalid frame magic at byte ${offset}`);
|
|
32
|
+
}
|
|
33
|
+
offset += 4;
|
|
34
|
+
if (offset === buffer.length) return { frames, tornStart: start };
|
|
35
|
+
const descriptor = buffer.readUint8(offset++);
|
|
36
|
+
if ((descriptor & 0x18) !== 0) {
|
|
37
|
+
throw new Error(`corrupt Zstandard session log: reserved frame-header bit at byte ${offset - 1}`);
|
|
38
|
+
}
|
|
39
|
+
const contentSizeFlag = descriptor >>>6;
|
|
40
|
+
const singleSegment = (descriptor & 0x20) !== 0;
|
|
41
|
+
const checksum = (descriptor & 0x04) !== 0;
|
|
42
|
+
const dictionaryFlag = descriptor & 0x03;
|
|
43
|
+
const dictionaryBytes = dictionaryFlag === 3 ? 4 : dictionaryFlag;
|
|
44
|
+
const contentSizeBytes = contentSizeFlag === 0
|
|
45
|
+
? (singleSegment ? 1 : 0)
|
|
46
|
+
: 1 << contentSizeFlag;
|
|
47
|
+
const remainingHeaderBytes = (singleSegment ? 0 : 1) + dictionaryBytes + contentSizeBytes;
|
|
48
|
+
if (buffer.length - offset < remainingHeaderBytes) return { frames, tornStart: start };
|
|
49
|
+
offset += remainingHeaderBytes;
|
|
50
|
+
for(;;) {
|
|
51
|
+
if (buffer.length - offset < 3) return { frames, tornStart: start };
|
|
52
|
+
const blockHeader = buffer.readUIntLE(offset, 3);
|
|
53
|
+
offset += 3;
|
|
54
|
+
const lastBlock = (blockHeader & 1) !== 0;
|
|
55
|
+
const blockType = (blockHeader >>> 1) & 0x03;
|
|
56
|
+
const blockSize = blockHeader >>> 3;
|
|
57
|
+
if (blockType === 0x03) {
|
|
58
|
+
throw new Error(`corrupt Zstandard session log: reserved block type at byte ${offset - 3}`);
|
|
59
|
+
}
|
|
60
|
+
const payloadBytes = blockType === 0x01 ? 1 : blockSize;
|
|
61
|
+
if (buffer.length - offset < payloadBytes) return { frames, tornStart: start };
|
|
62
|
+
offset += payloadBytes;
|
|
63
|
+
if (lastBlock) break;
|
|
64
|
+
}
|
|
65
|
+
if (checksum) {
|
|
66
|
+
if (buffer.length - offset < 4) return { frames, tornStart: start };
|
|
67
|
+
offset += 4;
|
|
68
|
+
}
|
|
69
|
+
frames.push({ start, end: offset });
|
|
70
|
+
if (frames.length === maxFrames) return { frames };
|
|
71
|
+
}
|
|
72
|
+
return { frames };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function decompressZstdFrame(frame) {
|
|
76
|
+
return zstdDecompressAsync(frame);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function decompressAllZstdFrames(buffer) {
|
|
80
|
+
const scan = scanZstdFrames(buffer);
|
|
81
|
+
if (scan.frames.length === 0) {
|
|
82
|
+
throw new Error("empty or header-less Zstandard session log");
|
|
83
|
+
}
|
|
84
|
+
const decoded = [];
|
|
85
|
+
for (const frame of scan.frames) {
|
|
86
|
+
decoded.push(await decompressZstdFrame(buffer.subarray(frame.start, frame.end)));
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
content: Buffer.concat(decoded),
|
|
90
|
+
frameCount: decoded.length,
|
|
91
|
+
torn: scan.tornStart !== undefined,
|
|
92
|
+
};
|
|
93
|
+
}
|