dsh-sessions-manager 3.5.0 → 3.5.2
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.en.md +12 -4
- package/README.md +12 -4
- package/lib/client.js +130 -92
- package/lib/client.js.map +2 -2
- package/lib/index.js +904 -241
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
- package/src/client/index.jsx +154 -90
- package/src/client/logic.js +13 -1
- package/src/compat/capabilities.js +64 -8
- package/src/compat/persistence.js +137 -18
- package/src/handle-era-ops.js +185 -0
- package/src/handle-era-paths.js +126 -0
- package/src/index.js +373 -96
- package/src/markdown.js +122 -71
- package/src/path-guard.js +59 -0
- package/src/session-meta-cache.js +40 -13
- package/src/title-persist-index.js +6 -1
- package/src/zstd-frame.js +68 -34
package/lib/index.js
CHANGED
|
@@ -1,32 +1,67 @@
|
|
|
1
1
|
// src/index.js
|
|
2
|
-
import { mkdir as
|
|
3
|
-
import { basename, dirname as dirname2, isAbsolute, join as
|
|
4
|
-
import { readFileSync as
|
|
2
|
+
import { mkdir as mkdir5, readFile as readFile3, realpath, rename as rename5, stat as stat2, unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
|
|
3
|
+
import { basename as basename2, dirname as dirname2, isAbsolute, join as join6 } from "node:path";
|
|
4
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
5
5
|
import { homedir as homedir3 } from "node:os";
|
|
6
6
|
|
|
7
7
|
// src/zstd-frame.js
|
|
8
8
|
import zlib from "node:zlib";
|
|
9
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
10
9
|
var ZSTD_MAGIC = 4247762216;
|
|
11
10
|
var CHECKSUM_OPTS = { params: { [zlib.constants.ZSTD_c_checksumFlag]: 1 } };
|
|
12
|
-
function
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
11
|
+
function scanZstdFrames(buf, maxFrames = Number.POSITIVE_INFINITY) {
|
|
12
|
+
const frames = [];
|
|
13
|
+
let offset = 0;
|
|
14
|
+
while (offset < buf.length) {
|
|
15
|
+
const start = offset;
|
|
16
|
+
if (buf.length - offset < 4) return { frames, tornStart: start };
|
|
17
|
+
if (buf.readUInt32LE(offset) !== ZSTD_MAGIC) {
|
|
18
|
+
throw new Error(`\u4F1A\u8BDD\u65E5\u5FD7\u683C\u5F0F\u5F02\u5E38\uFF08\u5B57\u8282 ${offset} \u7684 zstd magic \u65E0\u6548\uFF09`);
|
|
19
|
+
}
|
|
20
|
+
offset += 4;
|
|
21
|
+
if (offset === buf.length) return { frames, tornStart: start };
|
|
22
|
+
const descriptor = buf.readUInt8(offset++);
|
|
23
|
+
if ((descriptor & 24) !== 0) throw new Error(`\u4F1A\u8BDD\u65E5\u5FD7\u683C\u5F0F\u5F02\u5E38\uFF08\u5B57\u8282 ${offset - 1} \u4F7F\u7528\u4FDD\u7559\u5E27\u5934\u4F4D\uFF09`);
|
|
24
|
+
const contentSizeFlag = descriptor >>> 6;
|
|
25
|
+
const singleSegment = (descriptor & 32) !== 0;
|
|
26
|
+
const checksum = (descriptor & 4) !== 0;
|
|
27
|
+
const dictionaryFlag = descriptor & 3;
|
|
28
|
+
const dictionaryBytes = dictionaryFlag === 3 ? 4 : dictionaryFlag;
|
|
29
|
+
const contentSizeBytes = contentSizeFlag === 0 ? singleSegment ? 1 : 0 : 1 << contentSizeFlag;
|
|
30
|
+
const remainingHeaderBytes = (singleSegment ? 0 : 1) + dictionaryBytes + contentSizeBytes;
|
|
31
|
+
if (buf.length - offset < remainingHeaderBytes) return { frames, tornStart: start };
|
|
32
|
+
offset += remainingHeaderBytes;
|
|
33
|
+
for (; ; ) {
|
|
34
|
+
if (buf.length - offset < 3) return { frames, tornStart: start };
|
|
35
|
+
const blockHeader = buf.readUIntLE(offset, 3);
|
|
36
|
+
offset += 3;
|
|
37
|
+
const lastBlock = (blockHeader & 1) !== 0;
|
|
38
|
+
const blockType = blockHeader >>> 1 & 3;
|
|
39
|
+
const blockSize = blockHeader >>> 3;
|
|
40
|
+
if (blockType === 3) throw new Error(`\u4F1A\u8BDD\u65E5\u5FD7\u683C\u5F0F\u5F02\u5E38\uFF08\u5B57\u8282 ${offset - 3} \u4F7F\u7528\u4FDD\u7559\u5757\u7C7B\u578B\uFF09`);
|
|
41
|
+
const payloadBytes = blockType === 1 ? 1 : blockSize;
|
|
42
|
+
if (buf.length - offset < payloadBytes) return { frames, tornStart: start };
|
|
43
|
+
offset += payloadBytes;
|
|
44
|
+
if (lastBlock) break;
|
|
45
|
+
}
|
|
46
|
+
if (checksum) {
|
|
47
|
+
if (buf.length - offset < 4) return { frames, tornStart: start };
|
|
48
|
+
offset += 4;
|
|
49
|
+
}
|
|
50
|
+
frames.push({ start, end: offset });
|
|
51
|
+
if (frames.length === maxFrames) return { frames };
|
|
52
|
+
}
|
|
53
|
+
return { frames };
|
|
23
54
|
}
|
|
24
|
-
function
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
55
|
+
function firstFrame(buf) {
|
|
56
|
+
const scan = scanZstdFrames(buf, 1);
|
|
57
|
+
const frame = scan.frames[0];
|
|
58
|
+
if (!frame) throw new Error("\u4F1A\u8BDD\u65E5\u5FD7\u683C\u5F0F\u5F02\u5E38\uFF08\u65E0\u5B8C\u6574 zstd \u5E27\uFF09");
|
|
59
|
+
return frame;
|
|
60
|
+
}
|
|
61
|
+
function rewriteFrame0CwdInMemory(buf, newCwd) {
|
|
62
|
+
const frame = firstFrame(buf);
|
|
63
|
+
const end0 = frame.end;
|
|
64
|
+
const frame0 = buf.subarray(frame.start, end0);
|
|
30
65
|
const text = zlib.zstdDecompressSync(frame0).toString("utf8");
|
|
31
66
|
const nl = text.indexOf("\n");
|
|
32
67
|
const line = nl >= 0 ? text.slice(0, nl) : text;
|
|
@@ -34,11 +69,10 @@ function rewriteFrame0Cwd(filePath, newCwd) {
|
|
|
34
69
|
if (obj.type !== "session") {
|
|
35
70
|
throw new Error(`\u4F1A\u8BDD\u65E5\u5FD7\u683C\u5F0F\u5F02\u5E38\uFF08\u5E270 \u4E0D\u662F session header\uFF0C\u5B9E\u9645 type=${obj.type}\uFF09`);
|
|
36
71
|
}
|
|
37
|
-
if (obj.cwd === newCwd) return;
|
|
38
72
|
obj.cwd = newCwd;
|
|
39
73
|
const newFrame0 = zlib.zstdCompressSync(JSON.stringify(obj) + "\n", CHECKSUM_OPTS);
|
|
40
74
|
const rest = buf.subarray(end0);
|
|
41
|
-
|
|
75
|
+
return Buffer.concat([newFrame0, rest]);
|
|
42
76
|
}
|
|
43
77
|
|
|
44
78
|
// src/markdown.js
|
|
@@ -102,87 +136,106 @@ function summarizeToolArguments(name2, rawArguments) {
|
|
|
102
136
|
}
|
|
103
137
|
return JSON.stringify(rest).slice(0, MAX_TOOL_ARG);
|
|
104
138
|
}
|
|
105
|
-
function
|
|
139
|
+
function createMarkdownFold(out, header, options) {
|
|
106
140
|
const includeReasoning = options.includeReasoning === true;
|
|
107
141
|
const includeToolResults = options.includeToolResults === true;
|
|
108
|
-
const header = meta && typeof meta === "object" ? meta : {};
|
|
109
|
-
const list = Array.isArray(events) ? events : [];
|
|
110
142
|
let title = typeof header.title === "string" && header.title.trim() ? header.title.trim() : null;
|
|
111
|
-
for (const ev of list) {
|
|
112
|
-
const data = ev && ev.data;
|
|
113
|
-
if (ev && ev.type === "session/title" && data && typeof data.title === "string" && data.title.trim()) {
|
|
114
|
-
title = data.title.trim();
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
const front = ["---"];
|
|
118
|
-
if (title) front.push(`title: ${yamlString(title)}`);
|
|
119
|
-
if (typeof header.id === "string" && header.id) front.push(`sessionId: ${yamlString(header.id)}`);
|
|
120
|
-
if (typeof header.cwd === "string" && header.cwd) front.push(`cwd: ${yamlString(header.cwd)}`);
|
|
121
|
-
const created = isoTime(header.createdAt);
|
|
122
|
-
if (created) front.push(`createdAt: ${created}`);
|
|
123
|
-
const exported = isoTime(options.exportedAt);
|
|
124
|
-
if (exported) front.push(`exportedAt: ${exported}`);
|
|
125
|
-
front.push("---");
|
|
126
|
-
const out = [front.join("\n")];
|
|
127
|
-
if (title) out.push("", `# ${title}`);
|
|
128
143
|
let turn = null;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
144
|
+
return {
|
|
145
|
+
get title() {
|
|
146
|
+
return title;
|
|
147
|
+
},
|
|
148
|
+
// The last session/title event wins — DSH may retitle a session later on.
|
|
149
|
+
add(events) {
|
|
150
|
+
const list = Array.isArray(events) ? events : [];
|
|
151
|
+
for (const ev of list) {
|
|
152
|
+
if (!ev || typeof ev !== "object") continue;
|
|
153
|
+
const data = ev.data && typeof ev.data === "object" ? ev.data : {};
|
|
154
|
+
const type = ev.type;
|
|
155
|
+
if (type === "session/title" && data && typeof data.title === "string" && data.title.trim()) {
|
|
156
|
+
title = data.title.trim();
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (type === "turn/start") {
|
|
160
|
+
const next = Number.isInteger(data.turn) ? data.turn : null;
|
|
161
|
+
if (next !== null && next !== turn) {
|
|
162
|
+
turn = next;
|
|
163
|
+
out.push("", `## \u7B2C ${turn} \u8F6E`);
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (type === "user/message") {
|
|
168
|
+
const blocks = blocksOf(data.content);
|
|
169
|
+
const text = textFromBlocks(blocks);
|
|
170
|
+
const images = imageCountOf(blocks);
|
|
171
|
+
if (!text && images === 0) continue;
|
|
172
|
+
out.push("", "### \u7528\u6237", "");
|
|
173
|
+
if (text) out.push(text);
|
|
174
|
+
for (let i = 0; i < images; i++) out.push("", ``);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (type === "assistant/message") {
|
|
178
|
+
const message = data.message && typeof data.message === "object" ? data.message : {};
|
|
179
|
+
const blocks = blocksOf(message.content);
|
|
180
|
+
const text = textFromBlocks(blocks);
|
|
181
|
+
const reasoning = includeReasoning ? reasoningFromBlocks(blocks) : "";
|
|
182
|
+
if (!text && !reasoning) continue;
|
|
183
|
+
out.push("", "### \u52A9\u624B", "");
|
|
184
|
+
if (reasoning) out.push("> \u601D\u8003\uFF1A" + reasoning.split("\n").join("\n> "), "");
|
|
185
|
+
if (text) out.push(text);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (type === "tool/call") {
|
|
189
|
+
const name2 = typeof data.name === "string" && data.name ? data.name : "tool";
|
|
190
|
+
const summary = summarizeToolArguments(name2, data.arguments);
|
|
191
|
+
out.push("", `### \u5DE5\u5177\u8C03\u7528\uFF1A\`${name2}\``, "");
|
|
192
|
+
out.push(summary ? "```\n" + summary + "\n```" : "\uFF08\u65E0\u53C2\u6570\uFF09");
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (type === "tool/result" && includeToolResults) {
|
|
196
|
+
const message = data.message && typeof data.message === "object" ? data.message : {};
|
|
197
|
+
const blocks = blocksOf(message.content);
|
|
198
|
+
let text = "";
|
|
199
|
+
for (const block of blocks) {
|
|
200
|
+
if (block.type === "tool-result") text = textFromBlocks(blocksOf(block.content));
|
|
201
|
+
}
|
|
202
|
+
if (text) out.push("", "<details><summary>\u5DE5\u5177\u7ED3\u679C</summary>", "", "```\n" + text.slice(0, 2e3) + "\n```", "", "</details>");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function createSessionMarkdownBuilder(meta, options = {}) {
|
|
209
|
+
const header = meta && typeof meta === "object" ? meta : {};
|
|
210
|
+
const body = [];
|
|
211
|
+
const fold = createMarkdownFold(body, header, options);
|
|
212
|
+
return {
|
|
213
|
+
addEvents(events) {
|
|
214
|
+
fold.add(events);
|
|
215
|
+
},
|
|
216
|
+
finish(metaOverride) {
|
|
217
|
+
const effective = metaOverride && typeof metaOverride === "object" ? metaOverride : header;
|
|
218
|
+
const title = fold.title;
|
|
219
|
+
const front = ["---"];
|
|
220
|
+
if (title) front.push(`title: ${yamlString(title)}`);
|
|
221
|
+
if (typeof effective.id === "string" && effective.id) front.push(`sessionId: ${yamlString(effective.id)}`);
|
|
222
|
+
if (typeof effective.cwd === "string" && effective.cwd) front.push(`cwd: ${yamlString(effective.cwd)}`);
|
|
223
|
+
const created = isoTime(effective.createdAt);
|
|
224
|
+
if (created) front.push(`createdAt: ${created}`);
|
|
225
|
+
const exported = isoTime(options.exportedAt);
|
|
226
|
+
if (exported) front.push(`exportedAt: ${exported}`);
|
|
227
|
+
front.push("---");
|
|
228
|
+
const doc = [front.join("\n")];
|
|
229
|
+
if (title) doc.push("", `# ${title}`);
|
|
230
|
+
doc.push(...body, "");
|
|
231
|
+
return doc.join("\n");
|
|
232
|
+
}
|
|
233
|
+
};
|
|
181
234
|
}
|
|
182
235
|
|
|
183
236
|
// src/star-index.js
|
|
184
237
|
import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
185
|
-
import { readFileSync
|
|
238
|
+
import { readFileSync } from "node:fs";
|
|
186
239
|
import { homedir } from "node:os";
|
|
187
240
|
import { join } from "node:path";
|
|
188
241
|
var STAR_SCHEMA_VERSION = 3;
|
|
@@ -210,7 +263,7 @@ function createStarIndex(options = {}) {
|
|
|
210
263
|
let mutation = Promise.resolve();
|
|
211
264
|
async function read() {
|
|
212
265
|
try {
|
|
213
|
-
return normalizeStarStore(JSON.parse(
|
|
266
|
+
return normalizeStarStore(JSON.parse(readFileSync(indexPath, "utf8")));
|
|
214
267
|
} catch {
|
|
215
268
|
return normalizeStarStore(null);
|
|
216
269
|
}
|
|
@@ -303,7 +356,7 @@ function aggregateStorage(items, options = {}) {
|
|
|
303
356
|
|
|
304
357
|
// src/auto-archive.js
|
|
305
358
|
import { mkdir as mkdir2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
306
|
-
import { readFileSync as
|
|
359
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
307
360
|
import { homedir as homedir2 } from "node:os";
|
|
308
361
|
import { join as join2 } from "node:path";
|
|
309
362
|
var AUTO_ARCHIVE_SCHEMA_VERSION = 4;
|
|
@@ -359,7 +412,7 @@ function createAutoArchiveStore(options = {}) {
|
|
|
359
412
|
let mutation = Promise.resolve();
|
|
360
413
|
async function read() {
|
|
361
414
|
try {
|
|
362
|
-
return normalizeAutoArchiveStore(JSON.parse(
|
|
415
|
+
return normalizeAutoArchiveStore(JSON.parse(readFileSync2(indexPath, "utf8")));
|
|
363
416
|
} catch {
|
|
364
417
|
return normalizeAutoArchiveStore(null);
|
|
365
418
|
}
|
|
@@ -414,20 +467,28 @@ function createAutoArchiveStore(options = {}) {
|
|
|
414
467
|
// src/session-meta-cache.js
|
|
415
468
|
var DEFAULT_TTL_MS = 5 * 60 * 1e3;
|
|
416
469
|
var DEFAULT_MAX = 4e3;
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
470
|
+
var REVISION_PREFIX = "rev:";
|
|
471
|
+
function fingerprintOf(stat3) {
|
|
472
|
+
if (!stat3 || typeof stat3 !== "object") return null;
|
|
473
|
+
if (typeof stat3.revision === "string" && stat3.revision.length > 0) {
|
|
474
|
+
return REVISION_PREFIX + stat3.revision;
|
|
475
|
+
}
|
|
476
|
+
const mtimeMs = stat3.mtimeMs;
|
|
477
|
+
const size = stat3.size;
|
|
421
478
|
if (typeof mtimeMs !== "number" || !Number.isFinite(mtimeMs) || mtimeMs <= 0) return null;
|
|
422
479
|
if (typeof size !== "number" || !Number.isFinite(size) || size < 0) return null;
|
|
423
480
|
return `${Math.floor(mtimeMs)}:${size}`;
|
|
424
481
|
}
|
|
425
|
-
function
|
|
482
|
+
function isPersistableFingerprint(fingerprint) {
|
|
483
|
+
return typeof fingerprint === "string" && fingerprint !== "" && !fingerprint.startsWith(REVISION_PREFIX);
|
|
484
|
+
}
|
|
485
|
+
function isFresh(entry, stat3, now, ttlMs = DEFAULT_TTL_MS) {
|
|
426
486
|
if (!entry) return false;
|
|
427
|
-
const fp = fingerprintOf(
|
|
487
|
+
const fp = fingerprintOf(stat3);
|
|
428
488
|
if (!fp) return false;
|
|
429
489
|
if (entry.fingerprint !== fp) return false;
|
|
430
490
|
if (typeof entry.at !== "number") return false;
|
|
491
|
+
if (fp.startsWith(REVISION_PREFIX)) return true;
|
|
431
492
|
return now - entry.at <= ttlMs;
|
|
432
493
|
}
|
|
433
494
|
function partitionByCache(ids, statsById, cache, now = Date.now(), ttlMs = DEFAULT_TTL_MS) {
|
|
@@ -435,8 +496,8 @@ function partitionByCache(ids, statsById, cache, now = Date.now(), ttlMs = DEFAU
|
|
|
435
496
|
const missing = [];
|
|
436
497
|
for (const id of ids) {
|
|
437
498
|
const entry = cache && cache.get(String(id));
|
|
438
|
-
const
|
|
439
|
-
if (isFresh(entry,
|
|
499
|
+
const stat3 = statsById && statsById.get(String(id));
|
|
500
|
+
if (isFresh(entry, stat3, now, ttlMs) && entry && entry.meta) {
|
|
440
501
|
cached.set(String(id), entry.meta);
|
|
441
502
|
} else {
|
|
442
503
|
missing.push(String(id));
|
|
@@ -452,10 +513,10 @@ function createSessionMetaCache(opts = {}) {
|
|
|
452
513
|
let misses = 0;
|
|
453
514
|
return {
|
|
454
515
|
// 命中返回 meta,未命中/无法校验返回 null。
|
|
455
|
-
get(id,
|
|
516
|
+
get(id, stat3) {
|
|
456
517
|
const key = String(id);
|
|
457
518
|
const entry = map.get(key);
|
|
458
|
-
if (isFresh(entry,
|
|
519
|
+
if (isFresh(entry, stat3, Date.now(), ttlMs)) {
|
|
459
520
|
hits++;
|
|
460
521
|
map.delete(key);
|
|
461
522
|
map.set(key, entry);
|
|
@@ -464,9 +525,9 @@ function createSessionMetaCache(opts = {}) {
|
|
|
464
525
|
misses++;
|
|
465
526
|
return null;
|
|
466
527
|
},
|
|
467
|
-
set(id,
|
|
528
|
+
set(id, stat3, meta) {
|
|
468
529
|
if (!meta) return null;
|
|
469
|
-
const fp = fingerprintOf(
|
|
530
|
+
const fp = fingerprintOf(stat3);
|
|
470
531
|
if (!fp) return null;
|
|
471
532
|
const key = String(id);
|
|
472
533
|
map.delete(key);
|
|
@@ -512,7 +573,8 @@ function normalizeEntry(raw) {
|
|
|
512
573
|
const createdAt = typeof raw.createdAt === "number" ? raw.createdAt : null;
|
|
513
574
|
const fingerprint = typeof raw.fingerprint === "string" && raw.fingerprint ? raw.fingerprint : null;
|
|
514
575
|
const updatedAt = typeof raw.updatedAt === "number" ? raw.updatedAt : 0;
|
|
515
|
-
if (!fingerprint ||
|
|
576
|
+
if (!fingerprint || fingerprint.startsWith("rev:")) return null;
|
|
577
|
+
if (!title && !cwd) return null;
|
|
516
578
|
return { title, cwd, createdAt, fingerprint, updatedAt };
|
|
517
579
|
}
|
|
518
580
|
function normalizeTitleIndex(raw) {
|
|
@@ -605,6 +667,19 @@ function createTitleIndexStore({ dir, file }) {
|
|
|
605
667
|
}
|
|
606
668
|
|
|
607
669
|
// src/compat/persistence.js
|
|
670
|
+
var DEFAULT_CHUNK = 400;
|
|
671
|
+
var MAX_CHUNKS = 2e4;
|
|
672
|
+
function normalizeReadResult(events) {
|
|
673
|
+
if (Array.isArray(events)) return events;
|
|
674
|
+
if (events && typeof events[Symbol.iterator] === "function") return [...events];
|
|
675
|
+
return [];
|
|
676
|
+
}
|
|
677
|
+
async function closeQuietly(handle) {
|
|
678
|
+
try {
|
|
679
|
+
if (handle && typeof handle.close === "function") await handle.close();
|
|
680
|
+
} catch (e) {
|
|
681
|
+
}
|
|
682
|
+
}
|
|
608
683
|
function asHeader(value) {
|
|
609
684
|
if (!value || typeof value !== "object") return null;
|
|
610
685
|
const candidate = value.header && typeof value.header === "object" ? value.header : value;
|
|
@@ -620,7 +695,7 @@ function normalizePersistenceEntry(value) {
|
|
|
620
695
|
id: String(header.id),
|
|
621
696
|
sizeBytes: snapshot && Number.isFinite(snapshot.sizeBytes) ? Number(snapshot.sizeBytes) : null,
|
|
622
697
|
eventCount: snapshot && Number.isSafeInteger(snapshot.eventCount) ? snapshot.eventCount : null,
|
|
623
|
-
revision: snapshot ? snapshot.revision : null
|
|
698
|
+
revision: snapshot && typeof snapshot.revision === "string" && snapshot.revision ? snapshot.revision : null
|
|
624
699
|
};
|
|
625
700
|
}
|
|
626
701
|
function normalizePersistenceList(values) {
|
|
@@ -629,44 +704,112 @@ function normalizePersistenceList(values) {
|
|
|
629
704
|
}
|
|
630
705
|
function createPersistenceAdapter(service) {
|
|
631
706
|
if (!service || typeof service.list !== "function") throw new TypeError("sessionPersistence.list is required");
|
|
707
|
+
const hasStat = typeof service.stat === "function";
|
|
708
|
+
const kind = typeof service.open === "function" ? "session-handle" : "legacy";
|
|
632
709
|
async function listEntries(options) {
|
|
633
710
|
return normalizePersistenceList(await service.list(options));
|
|
634
711
|
}
|
|
635
|
-
async function
|
|
636
|
-
if (
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
712
|
+
async function statSession(id) {
|
|
713
|
+
if (!hasStat) return null;
|
|
714
|
+
const snapshot = await service.stat(id);
|
|
715
|
+
return snapshot ? normalizePersistenceEntry(snapshot) : null;
|
|
716
|
+
}
|
|
717
|
+
async function readChunk(handle, offset, length, signal) {
|
|
718
|
+
if (signal && signal.aborted) {
|
|
719
|
+
const error = new Error("\u4F1A\u8BDD\u8BFB\u53D6\u5DF2\u53D6\u6D88");
|
|
720
|
+
error.code = "DSM_READ_ABORTED";
|
|
721
|
+
throw error;
|
|
643
722
|
}
|
|
723
|
+
const events = await handle.read(offset, length, signal ? { signal } : void 0);
|
|
724
|
+
return normalizeReadResult(events);
|
|
725
|
+
}
|
|
726
|
+
async function readChunks(id, { offset = 0, chunkSize = DEFAULT_CHUNK, signal, onEvents }) {
|
|
644
727
|
if (typeof service.open !== "function") throw new Error("\u5F53\u524D DSH \u6301\u4E45\u5316\u670D\u52A1\u4E0D\u652F\u6301\u8BFB\u53D6\u4F1A\u8BDD");
|
|
645
728
|
const handle = await service.open(id, "read");
|
|
646
729
|
if (!handle || typeof handle.read !== "function" || typeof handle.close !== "function") {
|
|
647
|
-
|
|
648
|
-
if (handle && typeof handle.close === "function") await handle.close();
|
|
649
|
-
} catch {
|
|
650
|
-
}
|
|
730
|
+
await closeQuietly(handle);
|
|
651
731
|
throw new Error("DSH \u8FD4\u56DE\u4E86\u65E0\u6548\u7684 SessionHandle");
|
|
652
732
|
}
|
|
733
|
+
let cursor = Number.isSafeInteger(offset) && offset >= 0 ? offset : 0;
|
|
734
|
+
let total = 0;
|
|
653
735
|
try {
|
|
654
|
-
|
|
736
|
+
for (let round = 0; round < MAX_CHUNKS; round++) {
|
|
737
|
+
const events = await readChunk(handle, cursor, chunkSize, signal);
|
|
738
|
+
if (events.length === 0) break;
|
|
739
|
+
cursor += events.length;
|
|
740
|
+
total += events.length;
|
|
741
|
+
if (onEvents) await onEvents(events, { offset: cursor - events.length, total });
|
|
742
|
+
if (events.length < chunkSize) break;
|
|
743
|
+
}
|
|
744
|
+
} finally {
|
|
745
|
+
await closeQuietly(handle);
|
|
746
|
+
}
|
|
747
|
+
return {
|
|
748
|
+
meta: handle.header || handle.meta || null,
|
|
749
|
+
inheritedEventCount: Number.isSafeInteger(handle.inheritedEventCount) ? handle.inheritedEventCount : 0,
|
|
750
|
+
eventCount: total
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
async function inspectSession(id, opts = {}) {
|
|
754
|
+
const chunkSize = Number.isSafeInteger(opts.chunkSize) && opts.chunkSize > 0 ? opts.chunkSize : DEFAULT_CHUNK;
|
|
755
|
+
if (typeof service.open === "function") {
|
|
756
|
+
if (opts.signal && opts.signal.aborted) {
|
|
757
|
+
const error = new Error("\u4F1A\u8BDD\u8BFB\u53D6\u5DF2\u53D6\u6D88");
|
|
758
|
+
error.code = "DSM_READ_ABORTED";
|
|
759
|
+
throw error;
|
|
760
|
+
}
|
|
761
|
+
return readChunks(id, { offset: opts.offset || 0, chunkSize, signal: opts.signal, onEvents: opts.onEvents });
|
|
762
|
+
}
|
|
763
|
+
if (typeof service.readFrom !== "function") throw new Error("\u5F53\u524D DSH \u6301\u4E45\u5316\u670D\u52A1\u4E0D\u652F\u6301\u8BFB\u53D6\u4F1A\u8BDD");
|
|
764
|
+
if (opts.signal && opts.signal.aborted) {
|
|
765
|
+
const error = new Error("\u4F1A\u8BDD\u8BFB\u53D6\u5DF2\u53D6\u6D88");
|
|
766
|
+
error.code = "DSM_READ_ABORTED";
|
|
767
|
+
throw error;
|
|
768
|
+
}
|
|
769
|
+
const result = await service.readFrom(id, opts.offset || 0);
|
|
770
|
+
const events = normalizeReadResult(result && result.events);
|
|
771
|
+
if (opts.onEvents && events.length) await opts.onEvents(events, { offset: opts.offset || 0, total: events.length });
|
|
772
|
+
return {
|
|
773
|
+
meta: result && result.meta ? result.meta : null,
|
|
774
|
+
inheritedEventCount: result && Number.isSafeInteger(result.inheritedEventCount) ? result.inheritedEventCount : 0,
|
|
775
|
+
eventCount: events.length
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
async function readSession(id, offset = 0) {
|
|
779
|
+
if (typeof service.readFrom === "function") {
|
|
780
|
+
const result = await service.readFrom(id, offset);
|
|
655
781
|
return {
|
|
656
|
-
meta:
|
|
657
|
-
inheritedEventCount: Number.isSafeInteger(
|
|
658
|
-
events: Array.isArray(events) ? events : [
|
|
782
|
+
meta: result && result.meta ? result.meta : null,
|
|
783
|
+
inheritedEventCount: result && Number.isSafeInteger(result.inheritedEventCount) ? result.inheritedEventCount : 0,
|
|
784
|
+
events: result && Array.isArray(result.events) ? result.events : []
|
|
659
785
|
};
|
|
660
|
-
} finally {
|
|
661
|
-
await handle.close();
|
|
662
786
|
}
|
|
787
|
+
const events = [];
|
|
788
|
+
const summary = await readChunks(id, { offset, onEvents: (batch) => {
|
|
789
|
+
events.push(...batch);
|
|
790
|
+
} });
|
|
791
|
+
return {
|
|
792
|
+
meta: summary.meta,
|
|
793
|
+
inheritedEventCount: summary.inheritedEventCount,
|
|
794
|
+
events
|
|
795
|
+
};
|
|
663
796
|
}
|
|
664
797
|
function locate(header) {
|
|
665
798
|
if (typeof service.locate === "function") return service.locate(header);
|
|
666
799
|
return null;
|
|
667
800
|
}
|
|
668
|
-
|
|
669
|
-
|
|
801
|
+
async function locateVerified(header) {
|
|
802
|
+
if (typeof service.locate === "function") {
|
|
803
|
+
try {
|
|
804
|
+
const loc = service.locate(header);
|
|
805
|
+
if (loc && typeof loc.path === "string") return { path: loc.path, sessionDir: null };
|
|
806
|
+
} catch (e) {
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const artifacts = await locateSessionArtifacts(service, header);
|
|
810
|
+
return artifacts ? { path: artifacts.logPath, sessionDir: artifacts.sessionDir } : null;
|
|
811
|
+
}
|
|
812
|
+
return { kind, listEntries, readSession, inspectSession, statSession, locate, locateVerified, hasStat };
|
|
670
813
|
}
|
|
671
814
|
|
|
672
815
|
// src/compat/capabilities.js
|
|
@@ -677,17 +820,34 @@ function detectCapabilities({ persistence, workspaceRegistry }) {
|
|
|
677
820
|
const handleApi = !!(persistence && typeof persistence.open === "function");
|
|
678
821
|
const legacyRead = !!(persistence && typeof persistence.readFrom === "function");
|
|
679
822
|
const legacyLocate = !!(persistence && (typeof persistence.locate === "function" || persistence.backend && typeof persistence.backend.locate === "function"));
|
|
823
|
+
const handleEraRoot = !!(handleApi && persistence && typeof persistence.root === "string" && persistence.root.length > 0);
|
|
824
|
+
const canVerifyExistence = !!(handleApi || legacyRead || persistence && typeof persistence.stat === "function");
|
|
825
|
+
const readOk = legacyRead || handleApi;
|
|
680
826
|
const workspaceInternals = !!(workspaceRegistry && workspaceRegistry.headers && workspaceRegistry.sessionPaths && typeof workspaceRegistry.replaceHeaderIndex === "function");
|
|
827
|
+
const matrix = {
|
|
828
|
+
readInspection: action(readOk, "\u5F53\u524D DSH \u672A\u63D0\u4F9B\u53EF\u8BC6\u522B\u7684\u4F1A\u8BDD\u8BFB\u53D6\u63A5\u53E3"),
|
|
829
|
+
archive: action(!!(workspaceRegistry && typeof workspaceRegistry.archiveSession === "function"), "\u5F53\u524D DSH \u672A\u63D0\u4F9B\u5F52\u6863\u63A5\u53E3"),
|
|
830
|
+
softTrash: action(readOk, "\u5F53\u524D DSH \u65E0\u6CD5\u8BFB\u53D6\u4F1A\u8BDD\uFF0C\u4E0D\u80FD\u5B89\u5168\u79FB\u5165\u56DE\u6536\u7AD9"),
|
|
831
|
+
// 恢复不再无条件宣称可用:必须能校验底层会话仍存在(stat 或 list),
|
|
832
|
+
// 否则恢复只会制造一条指向已消失日志的僵尸条目。
|
|
833
|
+
restoreIndexedSession: action(canVerifyExistence, "\u5F53\u524D DSH \u65E0\u6CD5\u6821\u9A8C\u5E95\u5C42\u4F1A\u8BDD\u662F\u5426\u5B58\u5728\uFF0C\u4E0D\u80FD\u5B89\u5168\u6062\u590D"),
|
|
834
|
+
physicalPurge: action(
|
|
835
|
+
!handleApi && legacyLocate || handleApi && handleEraRoot && canVerifyExistence,
|
|
836
|
+
handleApi && !handleEraRoot ? "\u65E0\u6CD5\u4ECE\u5F53\u524D DSH \u540E\u7AEF\u786E\u8BA4\u4F1A\u8BDD\u5B58\u50A8\u6839\u76EE\u5F55\uFF0C\u5DF2\u505C\u6B62\u7269\u7406\u5220\u9664\u4EE5\u4FDD\u62A4\u6570\u636E\u5B89\u5168" : "\u5F53\u524D DSH \u7248\u672C\u5C1A\u672A\u63D0\u4F9B\u7ECF\u8FC7\u9A8C\u8BC1\u7684\u5B89\u5168\u6C38\u4E45\u5220\u9664\u80FD\u529B\uFF1B\u79FB\u5165\u56DE\u6536\u7AD9\u4E0D\u4F1A\u91CA\u653E\u78C1\u76D8\u7A7A\u95F4"
|
|
837
|
+
),
|
|
838
|
+
relocateSession: action(
|
|
839
|
+
!handleApi && legacyRead && legacyLocate && workspaceInternals || handleApi && handleEraRoot && readOk && workspaceInternals,
|
|
840
|
+
handleApi && !workspaceInternals ? "\u5F53\u524D DSH \u672A\u63D0\u4F9B\u5DE5\u4F5C\u533A\u6CE8\u518C\u8868\u5185\u90E8\u7ED3\u6784\uFF0C\u8DE8\u5DE5\u4F5C\u533A\u79FB\u52A8\u540E\u65E0\u6CD5\u5373\u65F6\u5237\u65B0\u5206\u7EC4" : handleApi && !handleEraRoot ? "\u65E0\u6CD5\u4ECE\u5F53\u524D DSH \u540E\u7AEF\u786E\u8BA4\u4F1A\u8BDD\u5B58\u50A8\u6839\u76EE\u5F55\uFF0C\u5DF2\u505C\u6B62\u79FB\u52A8\u4EE5\u4FDD\u62A4\u6570\u636E\u5B89\u5168" : "\u5F53\u524D DSH \u7248\u672C\u5C1A\u672A\u63D0\u4F9B\u7ECF\u8FC7\u9A8C\u8BC1\u7684\u8DE8\u5DE5\u4F5C\u533A\u8FC1\u79FB\u80FD\u529B"
|
|
841
|
+
)
|
|
842
|
+
};
|
|
843
|
+
matrix.read = matrix.readInspection;
|
|
844
|
+
matrix.trash = matrix.softTrash;
|
|
845
|
+
matrix.restoreTrash = matrix.restoreIndexedSession;
|
|
846
|
+
matrix.purge = matrix.physicalPurge;
|
|
847
|
+
matrix.move = matrix.relocateSession;
|
|
681
848
|
return {
|
|
682
849
|
persistence: handleApi ? "session-handle" : "legacy",
|
|
683
|
-
actions:
|
|
684
|
-
read: action(legacyRead || handleApi, "\u5F53\u524D DSH \u672A\u63D0\u4F9B\u53EF\u8BC6\u522B\u7684\u4F1A\u8BDD\u8BFB\u53D6\u63A5\u53E3"),
|
|
685
|
-
archive: action(!!(workspaceRegistry && typeof workspaceRegistry.archiveSession === "function"), "\u5F53\u524D DSH \u672A\u63D0\u4F9B\u5F52\u6863\u63A5\u53E3"),
|
|
686
|
-
trash: action(legacyRead || handleApi, "\u5F53\u524D DSH \u65E0\u6CD5\u8BFB\u53D6\u4F1A\u8BDD\uFF0C\u4E0D\u80FD\u5B89\u5168\u79FB\u5165\u56DE\u6536\u7AD9"),
|
|
687
|
-
restoreTrash: action(true),
|
|
688
|
-
purge: action(!handleApi && legacyLocate, "\u5F53\u524D DSH \u7248\u672C\u5C1A\u672A\u63D0\u4F9B\u7ECF\u8FC7\u9A8C\u8BC1\u7684\u5B89\u5168\u6C38\u4E45\u5220\u9664\u80FD\u529B"),
|
|
689
|
-
move: action(!handleApi && legacyRead && legacyLocate && workspaceInternals, "\u5F53\u524D DSH \u7248\u672C\u5C1A\u672A\u63D0\u4F9B\u7ECF\u8FC7\u9A8C\u8BC1\u7684\u8DE8\u5DE5\u4F5C\u533A\u8FC1\u79FB\u80FD\u529B")
|
|
690
|
-
}
|
|
850
|
+
actions: matrix
|
|
691
851
|
};
|
|
692
852
|
}
|
|
693
853
|
function requireCapability(capabilities, name2) {
|
|
@@ -699,12 +859,273 @@ function requireCapability(capabilities, name2) {
|
|
|
699
859
|
throw error;
|
|
700
860
|
}
|
|
701
861
|
|
|
862
|
+
// src/path-guard.js
|
|
863
|
+
function splitSegments(target) {
|
|
864
|
+
return String(target).replace(/[\\/]+$/, "").split(/[\\/]/).filter((seg) => seg.length > 0);
|
|
865
|
+
}
|
|
866
|
+
function stripDriveLetter(segments) {
|
|
867
|
+
return segments.length > 0 && /^[A-Za-z]:$/.test(segments[0]) ? segments.slice(1) : segments;
|
|
868
|
+
}
|
|
869
|
+
function pathOwnsSession(target, sid) {
|
|
870
|
+
if (typeof target !== "string" || target.length === 0) return false;
|
|
871
|
+
if (typeof sid !== "string" || sid.length === 0) return false;
|
|
872
|
+
const segments = stripDriveLetter(splitSegments(target));
|
|
873
|
+
if (segments.length === 0) return false;
|
|
874
|
+
const file = segments[segments.length - 1];
|
|
875
|
+
if (segments.length >= 2 && segments[segments.length - 2] === sid) return true;
|
|
876
|
+
if (file.includes(sid)) {
|
|
877
|
+
const ID_CHAR = /[A-Za-z0-9_-]/;
|
|
878
|
+
let from = 0;
|
|
879
|
+
while (true) {
|
|
880
|
+
const at = file.indexOf(sid, from);
|
|
881
|
+
if (at < 0) return false;
|
|
882
|
+
const before = at > 0 ? file[at - 1] : "";
|
|
883
|
+
const after = at + sid.length < file.length ? file[at + sid.length] : "";
|
|
884
|
+
if (!(before && ID_CHAR.test(before)) && !(after && ID_CHAR.test(after))) return true;
|
|
885
|
+
from = at + 1;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// src/handle-era-ops.js
|
|
892
|
+
import { mkdir as mkdir4, readFile as readFile2, rename as rename4, rm, unlink, writeFile as writeFile4 } from "node:fs/promises";
|
|
893
|
+
import { join as join5 } from "node:path";
|
|
894
|
+
|
|
895
|
+
// src/handle-era-paths.js
|
|
896
|
+
import { readdir, stat } from "node:fs/promises";
|
|
897
|
+
import { basename, join as join4 } from "node:path";
|
|
898
|
+
var GENERATION_LOG_RE = /^session\.v\d+\.jsonl(\.zst(d)?)?$/;
|
|
899
|
+
function isSafeChar(ch) {
|
|
900
|
+
return ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch);
|
|
901
|
+
}
|
|
902
|
+
function projectKeyFor(cwd) {
|
|
903
|
+
const s = String(cwd);
|
|
904
|
+
if (s.length === 0) throw new Error("cannot encode an empty project path");
|
|
905
|
+
let readable = "";
|
|
906
|
+
let separatorRun = false;
|
|
907
|
+
for (let i = 0; i < s.length; i++) {
|
|
908
|
+
const ch = s[i];
|
|
909
|
+
if (ch === "/" || ch === "\\" || ch === ":") {
|
|
910
|
+
if (!separatorRun) readable += "-";
|
|
911
|
+
separatorRun = true;
|
|
912
|
+
} else if (isSafeChar(ch)) {
|
|
913
|
+
readable += ch;
|
|
914
|
+
separatorRun = false;
|
|
915
|
+
} else {
|
|
916
|
+
readable += "~" + s.charCodeAt(i).toString(16).toUpperCase().padStart(4, "0");
|
|
917
|
+
separatorRun = false;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return "--" + (readable.replace(/^-+/, "") || "root").slice(0, 251) + "--";
|
|
921
|
+
}
|
|
922
|
+
function encodeSegmentFor(raw) {
|
|
923
|
+
const s = String(raw);
|
|
924
|
+
if (s.length === 0) throw new Error("cannot encode an empty path segment");
|
|
925
|
+
if (s === ".") return "~002E";
|
|
926
|
+
if (s === "..") return "~002E~002E";
|
|
927
|
+
let out = "";
|
|
928
|
+
for (let i = 0; i < s.length; i++) {
|
|
929
|
+
const ch = s[i];
|
|
930
|
+
out += isSafeChar(ch) ? ch : "~" + s.charCodeAt(i).toString(16).toUpperCase().padStart(4, "0");
|
|
931
|
+
}
|
|
932
|
+
return out;
|
|
933
|
+
}
|
|
934
|
+
function resolveSessionRoot(sp) {
|
|
935
|
+
const root = sp && typeof sp === "object" ? sp.root : void 0;
|
|
936
|
+
return typeof root === "string" && root.length > 0 ? root : null;
|
|
937
|
+
}
|
|
938
|
+
function deriveSessionDir(root, cwd, id) {
|
|
939
|
+
const project = cwd === void 0 || cwd === null || cwd === "" ? join4(root, "_no-cwd") : join4(root, projectKeyFor(cwd));
|
|
940
|
+
return join4(project, encodeSegmentFor(id));
|
|
941
|
+
}
|
|
942
|
+
async function locateSessionArtifacts2(sp, header) {
|
|
943
|
+
const root = resolveSessionRoot(sp);
|
|
944
|
+
if (!root || !header || header.id == null) return null;
|
|
945
|
+
const sid = String(header.id);
|
|
946
|
+
let sessionDir;
|
|
947
|
+
try {
|
|
948
|
+
sessionDir = deriveSessionDir(root, header.cwd, sid);
|
|
949
|
+
} catch (e) {
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
if (basename(sessionDir) !== encodeSegmentFor(sid)) return null;
|
|
953
|
+
let entries;
|
|
954
|
+
try {
|
|
955
|
+
const st = await stat(sessionDir);
|
|
956
|
+
if (!st.isDirectory()) return null;
|
|
957
|
+
entries = await readdir(sessionDir);
|
|
958
|
+
} catch (e) {
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
const generationFiles = entries.filter((name2) => GENERATION_LOG_RE.test(name2));
|
|
962
|
+
if (generationFiles.length === 0) return null;
|
|
963
|
+
generationFiles.sort((a, b) => {
|
|
964
|
+
const va = Number((a.match(/^session\.v(\d+)\./) || [])[1] || 0);
|
|
965
|
+
const vb = Number((b.match(/^session\.v(\d+)\./) || [])[1] || 0);
|
|
966
|
+
return vb - va;
|
|
967
|
+
});
|
|
968
|
+
const logPath = join4(sessionDir, generationFiles[0]);
|
|
969
|
+
if (!pathOwnsSession(logPath, sid)) return null;
|
|
970
|
+
return {
|
|
971
|
+
root,
|
|
972
|
+
projectDir: join4(root, header.cwd === void 0 || header.cwd === null || header.cwd === "" ? "_no-cwd" : projectKeyFor(header.cwd)),
|
|
973
|
+
sessionDir,
|
|
974
|
+
logPath,
|
|
975
|
+
generationFiles
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// src/handle-era-ops.js
|
|
980
|
+
var MOVE_BATCH = 400;
|
|
981
|
+
function conflictError(message) {
|
|
982
|
+
const error = new Error(message);
|
|
983
|
+
error.status = 409;
|
|
984
|
+
error.code = "DSM_SESSION_BUSY";
|
|
985
|
+
return error;
|
|
986
|
+
}
|
|
987
|
+
function failureText(e) {
|
|
988
|
+
return `${e && e.name || ""} ${e && e.message || e}`;
|
|
989
|
+
}
|
|
990
|
+
function isAlreadyOwned(e) {
|
|
991
|
+
return /already owned/i.test(failureText(e));
|
|
992
|
+
}
|
|
993
|
+
function isAlreadyExists(e) {
|
|
994
|
+
return /already exists/i.test(failureText(e));
|
|
995
|
+
}
|
|
996
|
+
async function closeQuietly2(handle) {
|
|
997
|
+
try {
|
|
998
|
+
if (handle && typeof handle.close === "function") await handle.close();
|
|
999
|
+
} catch (e) {
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
async function ensureNoActiveWriter(sp, sid) {
|
|
1003
|
+
let handle = null;
|
|
1004
|
+
try {
|
|
1005
|
+
handle = await sp.open(sid, "write");
|
|
1006
|
+
} catch (e) {
|
|
1007
|
+
if (isAlreadyOwned(e)) throw conflictError("\u8BE5\u4F1A\u8BDD\u6B63\u5728\u8FDB\u884C\u4E2D\uFF08\u5B58\u5728\u6D3B\u8DC3\u5199\u5165\uFF09\uFF0C\u8BF7\u5148\u5207\u6362\u5230\u522B\u7684\u4F1A\u8BDD\u518D\u64CD\u4F5C\u3002");
|
|
1008
|
+
throw e;
|
|
1009
|
+
}
|
|
1010
|
+
await closeQuietly2(handle);
|
|
1011
|
+
}
|
|
1012
|
+
async function purgeSessionArtifacts(sp, sid, header) {
|
|
1013
|
+
const artifacts = await locateSessionArtifacts2(sp, header);
|
|
1014
|
+
if (!artifacts) {
|
|
1015
|
+
const error = new Error("\u65E0\u6CD5\u5B9A\u4F4D\u8BE5\u4F1A\u8BDD\u7684\u7269\u7406\u65E5\u5FD7\u76EE\u5F55\uFF0C\u5DF2\u505C\u6B62\u6C38\u4E45\u5220\u9664");
|
|
1016
|
+
error.status = 409;
|
|
1017
|
+
throw error;
|
|
1018
|
+
}
|
|
1019
|
+
let writer = null;
|
|
1020
|
+
try {
|
|
1021
|
+
writer = await sp.open(sid, "write");
|
|
1022
|
+
} catch (e) {
|
|
1023
|
+
if (isAlreadyOwned(e)) throw conflictError("\u8BE5\u4F1A\u8BDD\u6B63\u5728\u8FDB\u884C\u4E2D\uFF08\u5B58\u5728\u6D3B\u8DC3\u5199\u5165\uFF09\uFF0C\u65E0\u6CD5\u5F7B\u5E95\u5220\u9664\u3002");
|
|
1024
|
+
throw e;
|
|
1025
|
+
}
|
|
1026
|
+
await closeQuietly2(writer);
|
|
1027
|
+
writer = null;
|
|
1028
|
+
try {
|
|
1029
|
+
await rm(artifacts.sessionDir, { recursive: true, force: true });
|
|
1030
|
+
} catch (e) {
|
|
1031
|
+
const error = new Error("\u5220\u9664\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A" + String(e && e.message || e));
|
|
1032
|
+
error.status = 500;
|
|
1033
|
+
throw error;
|
|
1034
|
+
}
|
|
1035
|
+
if (typeof sp.stat === "function") {
|
|
1036
|
+
const after = await sp.stat(sid).catch(() => void 0);
|
|
1037
|
+
if (after) {
|
|
1038
|
+
const error = new Error("\u5220\u9664\u540E\u5B98\u65B9 stat \u4ECD\u80FD\u770B\u5230\u8BE5\u4F1A\u8BDD\uFF0C\u5DF2\u4E2D\u6B62\uFF08\u76EE\u5F55\u53EF\u80FD\u88AB\u5E76\u53D1\u91CD\u5EFA\uFF09");
|
|
1039
|
+
error.status = 500;
|
|
1040
|
+
throw error;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return artifacts;
|
|
1044
|
+
}
|
|
1045
|
+
async function relocateRewrittenBackup({ sid, canonical, backupPath, artifacts }) {
|
|
1046
|
+
const original = await readFile2(backupPath);
|
|
1047
|
+
const frames = scanZstdFrames(original).frames;
|
|
1048
|
+
if (frames.length === 0) throw new Error("\u79FB\u52A8\u524D\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u65E5\u5FD7\u6CA1\u6709\u5B8C\u6574 zstd \u5E27");
|
|
1049
|
+
const rewritten = rewriteFrame0CwdInMemory(original, canonical);
|
|
1050
|
+
const rewrittenFrames = scanZstdFrames(rewritten).frames;
|
|
1051
|
+
if (rewrittenFrames.length !== frames.length) throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u65E5\u5FD7\u5E27\u6570\u53D1\u751F\u53D8\u5316");
|
|
1052
|
+
if (!original.subarray(frames[0].end).equals(rewritten.subarray(rewrittenFrames[0].end))) {
|
|
1053
|
+
throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u4E8B\u4EF6\u5185\u5BB9\u53D1\u751F\u53D8\u5316");
|
|
1054
|
+
}
|
|
1055
|
+
const targetDir = deriveSessionDir(artifacts.root, canonical, sid);
|
|
1056
|
+
await mkdir4(targetDir, { recursive: true });
|
|
1057
|
+
const staged = join5(targetDir, `.move-stage-${process.pid}-${Date.now()}`);
|
|
1058
|
+
await writeFile4(staged, rewritten, { mode: 384 });
|
|
1059
|
+
await rename4(staged, join5(targetDir, artifacts.generationFiles[0]));
|
|
1060
|
+
return targetDir;
|
|
1061
|
+
}
|
|
1062
|
+
async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inheritedEventCount = 0 }) {
|
|
1063
|
+
const artifacts = await locateSessionArtifacts2(sp, header);
|
|
1064
|
+
if (!artifacts) {
|
|
1065
|
+
const error = new Error("\u65E0\u6CD5\u5B9A\u4F4D\u8BE5\u4F1A\u8BDD\u7684\u7269\u7406\u65E5\u5FD7\uFF0C\u5DF2\u505C\u6B62\u79FB\u52A8");
|
|
1066
|
+
error.status = 409;
|
|
1067
|
+
throw error;
|
|
1068
|
+
}
|
|
1069
|
+
await ensureNoActiveWriter(sp, sid);
|
|
1070
|
+
const newHeader = Object.assign({}, header, { cwd: canonical });
|
|
1071
|
+
const firstSeq = events.length ? Number(events[0].seq) : 0;
|
|
1072
|
+
const replay = firstSeq !== 0 ? events.map((event, index) => ({ ...event, seq: index })) : events;
|
|
1073
|
+
const createOptions = header.isSeeded && Number.isSafeInteger(inheritedEventCount) && inheritedEventCount > 0 ? { inheritedEventCount } : void 0;
|
|
1074
|
+
const backupPath = `${artifacts.logPath}.move-backup-${process.pid}-${Date.now()}`;
|
|
1075
|
+
await rename4(artifacts.logPath, backupPath);
|
|
1076
|
+
let writer = null;
|
|
1077
|
+
try {
|
|
1078
|
+
try {
|
|
1079
|
+
writer = await sp.create(newHeader, createOptions);
|
|
1080
|
+
for (let i = 0; i < replay.length; i += MOVE_BATCH) {
|
|
1081
|
+
await writer.append(replay.slice(i, i + MOVE_BATCH));
|
|
1082
|
+
}
|
|
1083
|
+
await writer.flush();
|
|
1084
|
+
await writer.close();
|
|
1085
|
+
writer = null;
|
|
1086
|
+
} catch (e) {
|
|
1087
|
+
if (isAlreadyExists(e)) {
|
|
1088
|
+
await relocateRewrittenBackup({ sid, canonical, backupPath, artifacts });
|
|
1089
|
+
} else {
|
|
1090
|
+
throw e;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (typeof sp.stat !== "function") throw new Error("\u79FB\u52A8\u540E\u65E0\u6CD5\u6821\u9A8C\uFF1A\u540E\u7AEF\u672A\u63D0\u4F9B stat");
|
|
1094
|
+
const after = await sp.stat(sid);
|
|
1095
|
+
if (!after || !after.header || after.header.cwd !== canonical) {
|
|
1096
|
+
throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u5DE5\u4F5C\u76EE\u5F55\u672A\u6B63\u786E\u66F4\u65B0");
|
|
1097
|
+
}
|
|
1098
|
+
if (Number.isSafeInteger(after.eventCount) && events.length > 0 && after.eventCount !== events.length) {
|
|
1099
|
+
throw new Error(`\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4E8B\u4EF6\u6570\u4E0D\u4E00\u81F4\uFF08\u6E90 ${events.length}\uFF0C\u526F\u672C ${after.eventCount}\uFF09`);
|
|
1100
|
+
}
|
|
1101
|
+
} catch (e) {
|
|
1102
|
+
await closeQuietly2(writer);
|
|
1103
|
+
try {
|
|
1104
|
+
await rm(deriveSessionDir(artifacts.root, canonical, sid), { recursive: true, force: true });
|
|
1105
|
+
} catch (_) {
|
|
1106
|
+
}
|
|
1107
|
+
try {
|
|
1108
|
+
await rename4(backupPath, artifacts.logPath);
|
|
1109
|
+
} catch (_) {
|
|
1110
|
+
}
|
|
1111
|
+
if (e && e.status) throw e;
|
|
1112
|
+
const error = new Error("\u79FB\u52A8\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A" + String(e && e.message || e));
|
|
1113
|
+
error.status = 500;
|
|
1114
|
+
throw error;
|
|
1115
|
+
}
|
|
1116
|
+
try {
|
|
1117
|
+
await unlink(backupPath);
|
|
1118
|
+
} catch (e) {
|
|
1119
|
+
}
|
|
1120
|
+
return { sessionDir: deriveSessionDir(artifacts.root, canonical, sid) };
|
|
1121
|
+
}
|
|
1122
|
+
|
|
702
1123
|
// src/index.js
|
|
703
1124
|
var name = "dsh-sessions-manager";
|
|
704
1125
|
var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
|
|
705
1126
|
var MAX_TITLE = 80;
|
|
706
|
-
var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR ||
|
|
707
|
-
var TRASH_INDEX =
|
|
1127
|
+
var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR || join6(homedir3(), ".dsh", "sessions-manager-trash");
|
|
1128
|
+
var TRASH_INDEX = join6(TRASH_DIR, "index.json");
|
|
708
1129
|
var TRASH_SCHEMA_VERSION = 2;
|
|
709
1130
|
var DEFAULT_TRASH_SETTINGS = Object.freeze({ retentionDays: 0 });
|
|
710
1131
|
var FETCH_TOOL_RE = /search|fetch|download|browse/i;
|
|
@@ -718,6 +1139,21 @@ function json(res, value, status = 200) {
|
|
|
718
1139
|
function errorStatus(error) {
|
|
719
1140
|
return error && Number.isInteger(error.status) ? error.status : 500;
|
|
720
1141
|
}
|
|
1142
|
+
function createLimiter(max) {
|
|
1143
|
+
let active = 0;
|
|
1144
|
+
const queue = [];
|
|
1145
|
+
return async function run(fn) {
|
|
1146
|
+
if (active >= max) await new Promise((resolve) => queue.push(resolve));
|
|
1147
|
+
active++;
|
|
1148
|
+
try {
|
|
1149
|
+
return await fn();
|
|
1150
|
+
} finally {
|
|
1151
|
+
active--;
|
|
1152
|
+
const next = queue.shift();
|
|
1153
|
+
if (next) next();
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
721
1157
|
async function readJsonBody(req) {
|
|
722
1158
|
const chunks = [];
|
|
723
1159
|
let total = 0;
|
|
@@ -791,7 +1227,7 @@ function apply(ctx) {
|
|
|
791
1227
|
const dom = () => ctx.storageDomain.get("workspace");
|
|
792
1228
|
const authorityTitleCache = /* @__PURE__ */ new Map();
|
|
793
1229
|
const metaCache = createSessionMetaCache();
|
|
794
|
-
const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file:
|
|
1230
|
+
const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file: join6(TRASH_DIR, "title-index.json") });
|
|
795
1231
|
async function hydrateFromPersist(ids, statsById) {
|
|
796
1232
|
const hits = /* @__PURE__ */ new Map();
|
|
797
1233
|
if (!ids || !ids.length) return hits;
|
|
@@ -802,10 +1238,11 @@ function apply(ctx) {
|
|
|
802
1238
|
return hits;
|
|
803
1239
|
}
|
|
804
1240
|
for (const id of ids) {
|
|
805
|
-
const
|
|
1241
|
+
const stat3 = statsById.get(id);
|
|
806
1242
|
const entry = store && store[id];
|
|
807
|
-
if (!
|
|
808
|
-
const fp = fingerprintOf(
|
|
1243
|
+
if (!stat3 || !entry) continue;
|
|
1244
|
+
const fp = fingerprintOf(stat3);
|
|
1245
|
+
if (!isPersistableFingerprint(fp)) continue;
|
|
809
1246
|
if (fp && entry.fingerprint === fp) {
|
|
810
1247
|
hits.set(id, { title: entry.title, cwd: entry.cwd, createdAt: entry.createdAt });
|
|
811
1248
|
}
|
|
@@ -818,7 +1255,7 @@ function apply(ctx) {
|
|
|
818
1255
|
const now = Date.now();
|
|
819
1256
|
for (const [id, meta] of decoded) {
|
|
820
1257
|
const fp = fingerprintOf(statsById.get(id));
|
|
821
|
-
if (!fp) continue;
|
|
1258
|
+
if (!isPersistableFingerprint(fp)) continue;
|
|
822
1259
|
batch[id] = { title: meta.title, cwd: meta.cwd, createdAt: meta.createdAt, fingerprint: fp, updatedAt: now };
|
|
823
1260
|
}
|
|
824
1261
|
if (!Object.keys(batch).length) return;
|
|
@@ -897,26 +1334,42 @@ function apply(ctx) {
|
|
|
897
1334
|
}
|
|
898
1335
|
async function resolveOne(id, usage, opts = {}) {
|
|
899
1336
|
const key = String(id);
|
|
900
|
-
const statInfo = usage ? { mtimeMs: usage.mtimeById.get(key), size: usage.sizeById.get(key) } : null;
|
|
1337
|
+
const statInfo = usage ? (usage.statsById ? usage.statsById.get(key) : null) || { mtimeMs: usage.mtimeById && usage.mtimeById.get(key), size: usage.sizeById && usage.sizeById.get(key) } : null;
|
|
901
1338
|
const cached = metaCache.get(key, statInfo);
|
|
902
1339
|
if (cached) return buildItem(key, cached, usage, opts.exposeUsage);
|
|
903
1340
|
let meta = { title: null, cwd: null, createdAt: null };
|
|
1341
|
+
if (opts.listHeader) {
|
|
1342
|
+
if (typeof opts.listHeader.cwd === "string") meta.cwd = opts.listHeader.cwd;
|
|
1343
|
+
if (opts.listHeader.createdAt != null) meta.createdAt = opts.listHeader.createdAt;
|
|
1344
|
+
}
|
|
904
1345
|
if (opts.preloaded !== void 0) {
|
|
905
|
-
|
|
1346
|
+
const projected = metaFromSnapshot(unwrapSnapshot(opts.preloaded));
|
|
1347
|
+
if (projected.title) meta.title = projected.title;
|
|
1348
|
+
if (!meta.cwd && projected.cwd) meta.cwd = projected.cwd;
|
|
1349
|
+
if (!meta.createdAt && projected.createdAt) meta.createdAt = projected.createdAt;
|
|
906
1350
|
} else if (typeof sq.readTitleSnapshot === "function") {
|
|
907
1351
|
try {
|
|
908
|
-
|
|
1352
|
+
const projected = metaFromSnapshot(await sq.readTitleSnapshot(id));
|
|
1353
|
+
if (projected.title) meta.title = projected.title;
|
|
1354
|
+
if (!meta.cwd && projected.cwd) meta.cwd = projected.cwd;
|
|
1355
|
+
if (!meta.createdAt && projected.createdAt) meta.createdAt = projected.createdAt;
|
|
909
1356
|
} catch (e) {
|
|
910
1357
|
}
|
|
911
1358
|
}
|
|
912
|
-
|
|
1359
|
+
const projectionAvailable = typeof sq.readTitleSnapshot === "function" || typeof sq.readTitleSnapshots === "function";
|
|
1360
|
+
if (!meta.cwd || !meta.title && !projectionAvailable) {
|
|
913
1361
|
try {
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1362
|
+
let foldedTitle = null;
|
|
1363
|
+
const summary = await persistence.inspectSession(key, {
|
|
1364
|
+
onEvents: (events) => {
|
|
1365
|
+
if (!foldedTitle) foldedTitle = foldTitle(events);
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
if (summary && summary.meta) {
|
|
1369
|
+
if (!meta.cwd) meta.cwd = summary.meta.cwd || null;
|
|
1370
|
+
if (!meta.createdAt) meta.createdAt = summary.meta.createdAt || null;
|
|
918
1371
|
}
|
|
919
|
-
if (!meta.title &&
|
|
1372
|
+
if (!meta.title && foldedTitle) meta.title = foldedTitle;
|
|
920
1373
|
} catch (e2) {
|
|
921
1374
|
}
|
|
922
1375
|
}
|
|
@@ -924,11 +1377,12 @@ function apply(ctx) {
|
|
|
924
1377
|
if (opts.collectDecoded && statInfo) opts.collectDecoded(key, meta);
|
|
925
1378
|
return buildItem(key, meta, usage, opts.exposeUsage);
|
|
926
1379
|
}
|
|
927
|
-
async function collectUsage(
|
|
1380
|
+
async function collectUsage(preloadedEntries) {
|
|
928
1381
|
const sizeById = /* @__PURE__ */ new Map();
|
|
929
1382
|
const mtimeById = /* @__PURE__ */ new Map();
|
|
1383
|
+
const statsById = /* @__PURE__ */ new Map();
|
|
930
1384
|
let entries = null;
|
|
931
|
-
if (Array.isArray(
|
|
1385
|
+
if (Array.isArray(preloadedEntries)) entries = preloadedEntries;
|
|
932
1386
|
else {
|
|
933
1387
|
try {
|
|
934
1388
|
entries = await persistence.listEntries();
|
|
@@ -943,19 +1397,27 @@ function apply(ctx) {
|
|
|
943
1397
|
const header = entry && entry.header ? entry.header : entry;
|
|
944
1398
|
const id = entry && entry.id != null ? String(entry.id) : header && header.id != null ? String(header.id) : null;
|
|
945
1399
|
if (!id) return;
|
|
1400
|
+
if (entry && typeof entry.revision === "string" && entry.revision) {
|
|
1401
|
+
if (Number.isFinite(entry.sizeBytes)) sizeById.set(id, Number(entry.sizeBytes));
|
|
1402
|
+
statsById.set(id, { revision: entry.revision });
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
946
1405
|
if (entry && Number.isFinite(entry.sizeBytes)) sizeById.set(id, Number(entry.sizeBytes));
|
|
947
1406
|
try {
|
|
948
1407
|
const loc = persistence.locate(header);
|
|
949
1408
|
if (!loc || typeof loc.path !== "string" || !loc.path) return;
|
|
950
|
-
const st = await
|
|
1409
|
+
const st = await stat2(loc.path);
|
|
951
1410
|
if (!st) return;
|
|
952
1411
|
if (typeof st.size === "number") sizeById.set(id, st.size);
|
|
953
|
-
if (typeof st.mtimeMs === "number" && st.mtimeMs > 0)
|
|
1412
|
+
if (typeof st.mtimeMs === "number" && st.mtimeMs > 0) {
|
|
1413
|
+
mtimeById.set(id, Math.floor(st.mtimeMs));
|
|
1414
|
+
statsById.set(id, { mtimeMs: Math.floor(st.mtimeMs), size: typeof st.size === "number" ? st.size : void 0 });
|
|
1415
|
+
}
|
|
954
1416
|
} catch (e) {
|
|
955
1417
|
}
|
|
956
1418
|
}));
|
|
957
1419
|
}
|
|
958
|
-
return { sizeById, mtimeById };
|
|
1420
|
+
return { sizeById, mtimeById, statsById, hasActivityData: mtimeById.size > 0 };
|
|
959
1421
|
}
|
|
960
1422
|
async function restoreOne(sid) {
|
|
961
1423
|
requireSessionId(sid);
|
|
@@ -975,7 +1437,7 @@ function apply(ctx) {
|
|
|
975
1437
|
}
|
|
976
1438
|
async function readTrashStore() {
|
|
977
1439
|
try {
|
|
978
|
-
return normalizeTrashStore(JSON.parse(
|
|
1440
|
+
return normalizeTrashStore(JSON.parse(readFileSync3(TRASH_INDEX, "utf8")));
|
|
979
1441
|
} catch (e) {
|
|
980
1442
|
return normalizeTrashStore(null);
|
|
981
1443
|
}
|
|
@@ -984,10 +1446,10 @@ function apply(ctx) {
|
|
|
984
1446
|
return (await readTrashStore()).items;
|
|
985
1447
|
}
|
|
986
1448
|
async function writeTrashStore(store) {
|
|
987
|
-
await
|
|
988
|
-
const tmp =
|
|
989
|
-
await
|
|
990
|
-
await
|
|
1449
|
+
await mkdir5(TRASH_DIR, { recursive: true });
|
|
1450
|
+
const tmp = join6(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
|
|
1451
|
+
await writeFile5(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
|
|
1452
|
+
await rename5(tmp, TRASH_INDEX);
|
|
991
1453
|
}
|
|
992
1454
|
function mutateTrash(mutator) {
|
|
993
1455
|
const operation = trashMutation.then(async () => {
|
|
@@ -1030,10 +1492,28 @@ function apply(ctx) {
|
|
|
1030
1492
|
title = header.title || header.meta && header.meta.title || null;
|
|
1031
1493
|
}
|
|
1032
1494
|
if (!title) {
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1495
|
+
if (typeof sq.readTitleSnapshot === "function") {
|
|
1496
|
+
try {
|
|
1497
|
+
const snap = unwrapSnapshot(await sq.readTitleSnapshot(sid));
|
|
1498
|
+
if (snap && snap.title && snap.title.title) title = String(snap.title.title);
|
|
1499
|
+
if (snap && snap.session) {
|
|
1500
|
+
if (!cwd) cwd = snap.session.cwd || null;
|
|
1501
|
+
}
|
|
1502
|
+
} catch (e) {
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
if (!title) {
|
|
1507
|
+
try {
|
|
1508
|
+
let folded = null;
|
|
1509
|
+
const summary = await persistence.inspectSession(sid, {
|
|
1510
|
+
onEvents: (events) => {
|
|
1511
|
+
if (!folded) folded = foldTitle(events);
|
|
1512
|
+
}
|
|
1513
|
+
});
|
|
1514
|
+
if (summary && summary.meta && !cwd) cwd = summary.meta.cwd || null;
|
|
1515
|
+
title = folded;
|
|
1516
|
+
} catch (e) {
|
|
1037
1517
|
}
|
|
1038
1518
|
}
|
|
1039
1519
|
} catch (e) {
|
|
@@ -1062,20 +1542,96 @@ function apply(ctx) {
|
|
|
1062
1542
|
});
|
|
1063
1543
|
return { ok: true, trashed: true };
|
|
1064
1544
|
}
|
|
1545
|
+
async function verifyTrashRestore(sid) {
|
|
1546
|
+
const sessions = ctx.get("sessions");
|
|
1547
|
+
if (sessions && sessions.get && sessions.get(sid)) {
|
|
1548
|
+
return { ok: true, workspaceGone: false, verified: true };
|
|
1549
|
+
}
|
|
1550
|
+
let exists = false;
|
|
1551
|
+
let header = null;
|
|
1552
|
+
try {
|
|
1553
|
+
const stat3 = await persistence.statSession(sid);
|
|
1554
|
+
if (stat3) {
|
|
1555
|
+
exists = true;
|
|
1556
|
+
header = stat3.header;
|
|
1557
|
+
}
|
|
1558
|
+
} catch (e) {
|
|
1559
|
+
}
|
|
1560
|
+
if (!exists) {
|
|
1561
|
+
try {
|
|
1562
|
+
const entries = await persistence.listEntries();
|
|
1563
|
+
const found = entries.find((entry2) => entry2.id === sid);
|
|
1564
|
+
if (found) {
|
|
1565
|
+
exists = true;
|
|
1566
|
+
header = found.header;
|
|
1567
|
+
}
|
|
1568
|
+
} catch (e) {
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
if (!exists) {
|
|
1572
|
+
const store2 = await readTrashStore();
|
|
1573
|
+
if (store2.purgedSessionIds.map(String).includes(sid)) {
|
|
1574
|
+
return { ok: false, status: 410, code: "DSM_SESSION_PURGED", message: "\u8BE5\u4F1A\u8BDD\u5DF2\u5F7B\u5E95\u5220\u9664\uFF0C\u65E0\u6CD5\u4ECE\u56DE\u6536\u7AD9\u6062\u590D" };
|
|
1575
|
+
}
|
|
1576
|
+
return { ok: false, status: 409, code: "DSM_SESSION_MISSING", message: "\u5E95\u5C42\u4F1A\u8BDD\u5DF2\u4E0D\u5B58\u5728\uFF08\u53EF\u80FD\u88AB\u5916\u90E8\u5220\u9664\u6216\u91CD\u5EFA\uFF09\uFF0C\u65E0\u6CD5\u6062\u590D" };
|
|
1577
|
+
}
|
|
1578
|
+
let verified = false;
|
|
1579
|
+
const store = await readTrashStore();
|
|
1580
|
+
const entry = store.items.find((t) => String(t.sessionId) === sid);
|
|
1581
|
+
let originalPath = entry && typeof entry.originalPath === "string" ? entry.originalPath : null;
|
|
1582
|
+
if (!originalPath && header) {
|
|
1583
|
+
const loc = persistence.locate(header);
|
|
1584
|
+
if (loc && typeof loc.path === "string") originalPath = loc.path;
|
|
1585
|
+
}
|
|
1586
|
+
if (originalPath) {
|
|
1587
|
+
const existsOnDisk = await stat2(originalPath).then(() => true).catch(() => false);
|
|
1588
|
+
if (!existsOnDisk) {
|
|
1589
|
+
return { ok: false, status: 409, code: "DSM_SESSION_LOG_MISSING", message: "\u56DE\u6536\u7AD9\u7D22\u5F15\u4ECD\u8BB0\u5F55\u8BE5\u4F1A\u8BDD\uFF0C\u4F46\u5176\u65E5\u5FD7\u6587\u4EF6\u5DF2\u6D88\u5931\uFF08\u53EF\u80FD\u88AB\u5916\u90E8\u79FB\u52A8\u6216\u5220\u9664\uFF09" };
|
|
1590
|
+
}
|
|
1591
|
+
verified = true;
|
|
1592
|
+
}
|
|
1593
|
+
let workspaceGone = false;
|
|
1594
|
+
const cwd = header && header.cwd || entry && entry.cwd || null;
|
|
1595
|
+
if (cwd) {
|
|
1596
|
+
try {
|
|
1597
|
+
workspaceGone = !w.list().some((ent) => ent.path === cwd);
|
|
1598
|
+
} catch (e) {
|
|
1599
|
+
workspaceGone = false;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return { ok: true, workspaceGone, verified };
|
|
1603
|
+
}
|
|
1065
1604
|
async function restoreFromTrash(sid) {
|
|
1066
1605
|
requireSessionId(sid);
|
|
1606
|
+
requireCapability(capabilities, "restoreIndexedSession");
|
|
1607
|
+
let outcome = null;
|
|
1067
1608
|
await mutateTrash(async (store) => {
|
|
1068
1609
|
const entry = store.items.find((t) => String(t.sessionId) === sid);
|
|
1069
1610
|
if (!entry) {
|
|
1070
|
-
|
|
1611
|
+
if (store.purgedSessionIds.map(String).includes(sid)) {
|
|
1612
|
+
const error2 = new Error("\u8BE5\u4F1A\u8BDD\u5DF2\u5F7B\u5E95\u5220\u9664\uFF0C\u65E0\u6CD5\u4ECE\u56DE\u6536\u7AD9\u6062\u590D");
|
|
1613
|
+
error2.status = 410;
|
|
1614
|
+
error2.code = "DSM_SESSION_PURGED";
|
|
1615
|
+
throw error2;
|
|
1616
|
+
}
|
|
1617
|
+
const error = new Error("\u56DE\u6536\u7AD9\u4E2D\u627E\u4E0D\u5230\u8BE5\u4F1A\u8BDD\uFF08\u53EF\u80FD\u5DF2\u6062\u590D\u8FC7\uFF09");
|
|
1071
1618
|
error.status = 404;
|
|
1619
|
+
error.code = "DSM_TRASH_NOT_FOUND";
|
|
1620
|
+
throw error;
|
|
1621
|
+
}
|
|
1622
|
+
const verification = await verifyTrashRestore(sid);
|
|
1623
|
+
if (!verification.ok) {
|
|
1624
|
+
const error = new Error(verification.message);
|
|
1625
|
+
error.status = verification.status;
|
|
1626
|
+
error.code = verification.code;
|
|
1072
1627
|
throw error;
|
|
1073
1628
|
}
|
|
1074
1629
|
if (entry.wasArchived === false) await restoreOne(sid);
|
|
1075
1630
|
store.items = store.items.filter((t) => String(t.sessionId) !== sid);
|
|
1076
1631
|
store.purgedSessionIds = store.purgedSessionIds.filter((id) => id !== sid);
|
|
1632
|
+
outcome = { ok: true, restored: true, workspaceGone: verification.workspaceGone, verified: verification.verified };
|
|
1077
1633
|
});
|
|
1078
|
-
return { ok: true, restored: true };
|
|
1634
|
+
return outcome || { ok: true, restored: true };
|
|
1079
1635
|
}
|
|
1080
1636
|
async function purgeFromTrash(sid) {
|
|
1081
1637
|
requireSessionId(sid);
|
|
@@ -1102,7 +1658,7 @@ function apply(ctx) {
|
|
|
1102
1658
|
error.status = 409;
|
|
1103
1659
|
throw error;
|
|
1104
1660
|
}
|
|
1105
|
-
const targetOwnsSession =
|
|
1661
|
+
const targetOwnsSession = pathOwnsSession(target, sid);
|
|
1106
1662
|
if (target && !targetOwnsSession) {
|
|
1107
1663
|
const error = new Error("\u65E5\u5FD7\u8DEF\u5F84\u4E0E\u4F1A\u8BDD ID \u4E0D\u5339\u914D\uFF0C\u5DF2\u505C\u6B62\u6C38\u4E45\u5220\u9664");
|
|
1108
1664
|
error.status = 409;
|
|
@@ -1124,9 +1680,11 @@ function apply(ctx) {
|
|
|
1124
1680
|
error.status = 409;
|
|
1125
1681
|
throw error;
|
|
1126
1682
|
}
|
|
1127
|
-
if (target) {
|
|
1683
|
+
if (target && persistence.kind === "session-handle" && locatedHeader) {
|
|
1684
|
+
await purgeSessionArtifacts(sp, sid, locatedHeader);
|
|
1685
|
+
} else if (target) {
|
|
1128
1686
|
try {
|
|
1129
|
-
await
|
|
1687
|
+
await unlink2(target);
|
|
1130
1688
|
} catch (e) {
|
|
1131
1689
|
if (e && e.code !== "ENOENT") throw new Error("\u5220\u9664\u6587\u4EF6\u5931\u8D25\uFF1A" + String(e && e.message || e));
|
|
1132
1690
|
}
|
|
@@ -1196,8 +1754,8 @@ function apply(ctx) {
|
|
|
1196
1754
|
async function moveTargetWorkspace(rawPath) {
|
|
1197
1755
|
if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
|
|
1198
1756
|
let p = String(rawPath).trim();
|
|
1199
|
-
if (p.startsWith("~/")) p =
|
|
1200
|
-
if (!isAbsolute(p)) p =
|
|
1757
|
+
if (p.startsWith("~/")) p = join6(homedir3(), p.slice(2));
|
|
1758
|
+
if (!isAbsolute(p)) p = join6(homedir3(), p);
|
|
1201
1759
|
let canonical = null;
|
|
1202
1760
|
try {
|
|
1203
1761
|
canonical = await realpath(p);
|
|
@@ -1205,10 +1763,10 @@ function apply(ctx) {
|
|
|
1205
1763
|
canonical = null;
|
|
1206
1764
|
}
|
|
1207
1765
|
if (canonical === null) {
|
|
1208
|
-
await
|
|
1766
|
+
await mkdir5(p, { recursive: true });
|
|
1209
1767
|
canonical = await realpath(p);
|
|
1210
1768
|
}
|
|
1211
|
-
return { canonical, entity: await w.create(canonical,
|
|
1769
|
+
return { canonical, entity: await w.create(canonical, basename2(canonical) || "workspace") };
|
|
1212
1770
|
}
|
|
1213
1771
|
async function moveOne(sid, targetPath) {
|
|
1214
1772
|
requireCapability(capabilities, "move");
|
|
@@ -1243,14 +1801,43 @@ function apply(ctx) {
|
|
|
1243
1801
|
const newPath = locatePath(newHeaderObj);
|
|
1244
1802
|
if (!oldPath || !newPath || oldPath === newPath) return false;
|
|
1245
1803
|
const backupPath = `${oldPath}.move-backup-${Date.now()}`;
|
|
1804
|
+
const stagedPath = `${newPath}.move-stage-${process.pid}-${Date.now()}`;
|
|
1805
|
+
let destinationInstalled = false;
|
|
1246
1806
|
try {
|
|
1247
|
-
await
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1807
|
+
await mkdir5(dirname2(newPath), { recursive: true });
|
|
1808
|
+
try {
|
|
1809
|
+
await stat2(newPath);
|
|
1810
|
+
throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u76EE\u6807\u4F4D\u7F6E\u5DF2\u5B58\u5728\u540C\u540D\u4F1A\u8BDD\u65E5\u5FD7");
|
|
1811
|
+
} catch (e) {
|
|
1812
|
+
if (e && e.code !== "ENOENT") throw e;
|
|
1813
|
+
}
|
|
1814
|
+
await rename5(oldPath, backupPath);
|
|
1815
|
+
const original = await readFile3(backupPath);
|
|
1816
|
+
const originalFrames = scanZstdFrames(original).frames;
|
|
1817
|
+
if (originalFrames.length === 0) throw new Error("\u79FB\u52A8\u524D\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u65E5\u5FD7\u6CA1\u6709\u5B8C\u6574 zstd \u5E27");
|
|
1818
|
+
const rewritten = rewriteFrame0CwdInMemory(original, canonical);
|
|
1819
|
+
const rewrittenFrames = scanZstdFrames(rewritten).frames;
|
|
1820
|
+
if (rewrittenFrames.length !== originalFrames.length) throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u65E5\u5FD7\u5E27\u6570\u53D1\u751F\u53D8\u5316");
|
|
1821
|
+
const originalTail = original.subarray(originalFrames[0].end);
|
|
1822
|
+
const rewrittenTail = rewritten.subarray(rewrittenFrames[0].end);
|
|
1823
|
+
if (!originalTail.equals(rewrittenTail)) throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u4E8B\u4EF6\u5185\u5BB9\u53D1\u751F\u53D8\u5316");
|
|
1824
|
+
await writeFile5(stagedPath, rewritten, { mode: 384 });
|
|
1825
|
+
await rename5(stagedPath, newPath);
|
|
1826
|
+
destinationInstalled = true;
|
|
1827
|
+
await unlink2(backupPath);
|
|
1251
1828
|
} catch (e) {
|
|
1252
1829
|
try {
|
|
1253
|
-
await
|
|
1830
|
+
await unlink2(stagedPath);
|
|
1831
|
+
} catch (_) {
|
|
1832
|
+
}
|
|
1833
|
+
if (destinationInstalled) {
|
|
1834
|
+
try {
|
|
1835
|
+
await unlink2(newPath);
|
|
1836
|
+
} catch (_) {
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
try {
|
|
1840
|
+
await rename5(backupPath, oldPath);
|
|
1254
1841
|
} catch (_) {
|
|
1255
1842
|
}
|
|
1256
1843
|
if (e && e.code !== "ENOENT") throw e;
|
|
@@ -1274,7 +1861,21 @@ function apply(ctx) {
|
|
|
1274
1861
|
}
|
|
1275
1862
|
return null;
|
|
1276
1863
|
};
|
|
1277
|
-
if (
|
|
1864
|
+
if (persistence.kind === "session-handle") {
|
|
1865
|
+
const stat1 = await persistence.statSession(sid);
|
|
1866
|
+
if (stat1 && stat1.revision) {
|
|
1867
|
+
const stat22 = await persistence.statSession(sid);
|
|
1868
|
+
if (stat22 && stat22.revision !== stat1.revision) {
|
|
1869
|
+
throw new Error("\u8BE5\u4F1A\u8BDD\u5728\u79FB\u52A8\u51C6\u5907\u671F\u95F4\u53D1\u751F\u4E86\u53D8\u5316\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002");
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
try {
|
|
1873
|
+
await moveSessionToCwd({ sp, sid, header: meta, canonical, events, inheritedEventCount: r.inheritedEventCount });
|
|
1874
|
+
} catch (e) {
|
|
1875
|
+
if (e && e.status) throw e;
|
|
1876
|
+
throw new Error("\u79FB\u52A8\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A" + String(e && e.message || e));
|
|
1877
|
+
}
|
|
1878
|
+
} else if (isOpen) {
|
|
1278
1879
|
if (!await relocateLog(meta, newHeader)) throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u786E\u8BA4\u4F1A\u8BDD\u65E5\u5FD7\u5DF2\u8FC1\u79FB\u5230\u76EE\u6807\u5DE5\u4F5C\u533A");
|
|
1279
1880
|
try {
|
|
1280
1881
|
const st = sp.states && sp.states.get && sp.states.get(sid);
|
|
@@ -1296,7 +1897,7 @@ function apply(ctx) {
|
|
|
1296
1897
|
const backupPath = oldPath ? `${oldPath}.move-backup-${Date.now()}` : null;
|
|
1297
1898
|
if (backupPath) {
|
|
1298
1899
|
try {
|
|
1299
|
-
await
|
|
1900
|
+
await rename5(oldPath, backupPath);
|
|
1300
1901
|
} catch (e) {
|
|
1301
1902
|
if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
|
|
1302
1903
|
}
|
|
@@ -1304,7 +1905,7 @@ function apply(ctx) {
|
|
|
1304
1905
|
const restore = async () => {
|
|
1305
1906
|
if (backupPath) {
|
|
1306
1907
|
try {
|
|
1307
|
-
await
|
|
1908
|
+
await rename5(backupPath, oldPath);
|
|
1308
1909
|
} catch (_) {
|
|
1309
1910
|
}
|
|
1310
1911
|
}
|
|
@@ -1318,7 +1919,7 @@ function apply(ctx) {
|
|
|
1318
1919
|
}
|
|
1319
1920
|
if (backupPath) {
|
|
1320
1921
|
try {
|
|
1321
|
-
await
|
|
1922
|
+
await unlink2(backupPath);
|
|
1322
1923
|
} catch (e) {
|
|
1323
1924
|
}
|
|
1324
1925
|
}
|
|
@@ -1413,7 +2014,7 @@ function apply(ctx) {
|
|
|
1413
2014
|
}
|
|
1414
2015
|
return out;
|
|
1415
2016
|
}
|
|
1416
|
-
async function
|
|
2017
|
+
async function allSessionItemsDetailed(opts = {}) {
|
|
1417
2018
|
let entries = [];
|
|
1418
2019
|
let headersOk = false;
|
|
1419
2020
|
try {
|
|
@@ -1423,6 +2024,7 @@ function apply(ctx) {
|
|
|
1423
2024
|
} catch (e) {
|
|
1424
2025
|
entries = [];
|
|
1425
2026
|
}
|
|
2027
|
+
const entryById = new Map(entries.map((entry) => [entry.id, entry]));
|
|
1426
2028
|
let live = ctx.get("sessions");
|
|
1427
2029
|
const ids = entries.map((entry) => entry.id);
|
|
1428
2030
|
if (live) {
|
|
@@ -1437,7 +2039,11 @@ function apply(ctx) {
|
|
|
1437
2039
|
let hiddenIds = /* @__PURE__ */ new Set();
|
|
1438
2040
|
try {
|
|
1439
2041
|
const store = await readTrashStore();
|
|
1440
|
-
|
|
2042
|
+
const present = new Set(ids);
|
|
2043
|
+
hiddenIds = /* @__PURE__ */ new Set([
|
|
2044
|
+
...store.items.map((t) => String(t.sessionId)),
|
|
2045
|
+
...store.purgedSessionIds.map(String).filter((id) => !present.has(id))
|
|
2046
|
+
]);
|
|
1441
2047
|
} catch (e) {
|
|
1442
2048
|
}
|
|
1443
2049
|
const visibleIds = ids.filter((id) => !hiddenIds.has(id));
|
|
@@ -1450,8 +2056,11 @@ function apply(ctx) {
|
|
|
1450
2056
|
const currentArchived = new Set((await archivedState().catch(() => ({ archivedSessionIds: [] }))).archivedSessionIds || []);
|
|
1451
2057
|
const items = [];
|
|
1452
2058
|
const usage = await collectUsage(entries);
|
|
1453
|
-
const statsById = new Map(visibleIds.map((id) => [
|
|
1454
|
-
|
|
2059
|
+
const statsById = new Map(visibleIds.map((id) => [
|
|
2060
|
+
id,
|
|
2061
|
+
usage.statsById && usage.statsById.get(id) || { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }
|
|
2062
|
+
]));
|
|
2063
|
+
const { missing } = metaCache.partition(visibleIds, statsById);
|
|
1455
2064
|
const persisted = await hydrateFromPersist(missing, statsById);
|
|
1456
2065
|
for (const [id, meta] of persisted) metaCache.set(id, statsById.get(id), meta);
|
|
1457
2066
|
const stillMissing = missing.filter((id) => !persisted.has(id));
|
|
@@ -1462,11 +2071,15 @@ function apply(ctx) {
|
|
|
1462
2071
|
};
|
|
1463
2072
|
const CHUNK = 6;
|
|
1464
2073
|
for (let i = 0; i < visibleIds.length; i += CHUNK) {
|
|
1465
|
-
const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map((id) =>
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
2074
|
+
const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map((id) => {
|
|
2075
|
+
const entry = entryById.get(id);
|
|
2076
|
+
return resolveOne(id, usage, {
|
|
2077
|
+
exposeUsage: !!(opts && opts.usage),
|
|
2078
|
+
listHeader: entry ? entry.header : null,
|
|
2079
|
+
preloaded: snapshotById.has(id) ? snapshotById.get(id) : void 0,
|
|
2080
|
+
collectDecoded
|
|
2081
|
+
});
|
|
2082
|
+
}));
|
|
1470
2083
|
for (const it of res2) items.push({ ...it, archived: currentArchived.has(it.sessionId) });
|
|
1471
2084
|
}
|
|
1472
2085
|
persistDecoded(decoded, statsById);
|
|
@@ -1477,7 +2090,10 @@ function apply(ctx) {
|
|
|
1477
2090
|
}
|
|
1478
2091
|
for (const it of items) it.starred = starredSet.has(String(it.sessionId));
|
|
1479
2092
|
if (headersOk) await gcStars(ids);
|
|
1480
|
-
return items;
|
|
2093
|
+
return { items, usage };
|
|
2094
|
+
}
|
|
2095
|
+
async function allSessionItems(opts = {}) {
|
|
2096
|
+
return (await allSessionItemsDetailed(opts)).items;
|
|
1481
2097
|
}
|
|
1482
2098
|
async function buildStorage(opts = {}) {
|
|
1483
2099
|
const items = await allSessionItems({ usage: true });
|
|
@@ -1493,7 +2109,15 @@ function apply(ctx) {
|
|
|
1493
2109
|
if (!(opts && opts.force) && autoArchive.isFresh(store, now)) {
|
|
1494
2110
|
return { ok: true, skipped: "throttled", archived: 0, lastRunAt: store.lastRunAt, lastArchivedCount: store.lastArchivedCount };
|
|
1495
2111
|
}
|
|
1496
|
-
const items = await
|
|
2112
|
+
const { items, usage } = await allSessionItemsDetailed({ usage: true });
|
|
2113
|
+
if (!usage.hasActivityData) {
|
|
2114
|
+
return {
|
|
2115
|
+
ok: true,
|
|
2116
|
+
skipped: "no-activity-data",
|
|
2117
|
+
archived: 0,
|
|
2118
|
+
note: "\u5F53\u524D DSH \u7248\u672C\u672A\u63D0\u4F9B\u53EF\u9760\u7684\u6700\u540E\u6D3B\u8DC3\u65F6\u95F4\uFF0C\u81EA\u52A8\u5F52\u6863\u5DF2\u8DF3\u8FC7\uFF1B\u4E0D\u4F1A\u57FA\u4E8E\u731C\u6D4B\u5F52\u6863\u4EFB\u4F55\u4F1A\u8BDD\u3002"
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
1497
2121
|
const candidates = pickInactiveCandidates(items, {
|
|
1498
2122
|
inactiveDays: days,
|
|
1499
2123
|
skipStarred: store.settings.skipStarred,
|
|
@@ -1532,9 +2156,14 @@ function apply(ctx) {
|
|
|
1532
2156
|
} catch (e) {
|
|
1533
2157
|
}
|
|
1534
2158
|
const store = await readTrashStore();
|
|
2159
|
+
const present = new Set(ids);
|
|
2160
|
+
const activeTombstones = store.purgedSessionIds.map(String).filter((id) => !present.has(id));
|
|
1535
2161
|
if (ids.length) {
|
|
1536
2162
|
const usage = await collectUsage(entries);
|
|
1537
|
-
const statsById = new Map(ids.map((id) => [
|
|
2163
|
+
const statsById = new Map(ids.map((id) => [
|
|
2164
|
+
id,
|
|
2165
|
+
usage.statsById && usage.statsById.get(id) || { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }
|
|
2166
|
+
]));
|
|
1538
2167
|
const { cached, missing } = metaCache.partition(ids, statsById);
|
|
1539
2168
|
const persisted = await hydrateFromPersist(missing, statsById);
|
|
1540
2169
|
for (const [id, meta] of persisted) metaCache.set(id, statsById.get(id), meta);
|
|
@@ -1547,8 +2176,13 @@ function apply(ctx) {
|
|
|
1547
2176
|
for (const id of ids) {
|
|
1548
2177
|
let meta = cached.get(id) || persisted.get(id) || null;
|
|
1549
2178
|
if (!meta) {
|
|
2179
|
+
const entry = entries.find((e) => e.id === id);
|
|
1550
2180
|
const snapshot = snapshotById.has(id) ? snapshotById.get(id) : typeof sq.readTitleSnapshot === "function" ? await sq.readTitleSnapshot(id).catch(() => null) : null;
|
|
1551
2181
|
const next = metaFromSnapshot(snapshot);
|
|
2182
|
+
if (entry && entry.header) {
|
|
2183
|
+
if (!next.cwd && typeof entry.header.cwd === "string") next.cwd = entry.header.cwd;
|
|
2184
|
+
if (!next.createdAt && entry.header.createdAt != null) next.createdAt = entry.header.createdAt;
|
|
2185
|
+
}
|
|
1552
2186
|
metaCache.set(id, statsById.get(id), next);
|
|
1553
2187
|
if (statsById.get(id)) collectDecoded(id, next);
|
|
1554
2188
|
meta = next;
|
|
@@ -1560,38 +2194,14 @@ function apply(ctx) {
|
|
|
1560
2194
|
return {
|
|
1561
2195
|
titles: Object.fromEntries(authorityTitleCache),
|
|
1562
2196
|
trashedSessionIds: store.items.map((item) => String(item.sessionId)),
|
|
1563
|
-
purgedSessionIds:
|
|
2197
|
+
purgedSessionIds: activeTombstones
|
|
1564
2198
|
};
|
|
1565
2199
|
}
|
|
1566
|
-
async function buildDetails(sid) {
|
|
2200
|
+
async function buildDetails(sid, signal) {
|
|
1567
2201
|
const sessions = ctx.get("sessions");
|
|
1568
2202
|
const live = sessions && sessions.get(sid);
|
|
1569
2203
|
let meta = null;
|
|
1570
|
-
let
|
|
1571
|
-
if (live !== void 0) {
|
|
1572
|
-
meta = live && live.header || null;
|
|
1573
|
-
try {
|
|
1574
|
-
events = Array.isArray(live.events) ? [...live.events] : [];
|
|
1575
|
-
} catch (e) {
|
|
1576
|
-
events = [];
|
|
1577
|
-
}
|
|
1578
|
-
} else {
|
|
1579
|
-
const r = await persistence.readSession(sid, 0);
|
|
1580
|
-
if (!r || !r.meta) throw new Error("\u627E\u4E0D\u5230\u8BE5\u4F1A\u8BDD\u7684\u8BB0\u5F55\uFF08\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF09");
|
|
1581
|
-
meta = r.meta;
|
|
1582
|
-
events = Array.isArray(r.events) ? r.events : [];
|
|
1583
|
-
}
|
|
1584
|
-
let sizeBytes = null;
|
|
1585
|
-
try {
|
|
1586
|
-
const loc = persistence.locate(meta);
|
|
1587
|
-
if (loc && typeof loc.path === "string" && loc.path) {
|
|
1588
|
-
const st = await stat(loc.path);
|
|
1589
|
-
if (st && typeof st.size === "number") sizeBytes = st.size;
|
|
1590
|
-
}
|
|
1591
|
-
} catch (e) {
|
|
1592
|
-
sizeBytes = null;
|
|
1593
|
-
}
|
|
1594
|
-
let lastTime = typeof meta.createdAt === "number" ? meta.createdAt : 0;
|
|
2204
|
+
let lastTime = 0;
|
|
1595
2205
|
const fileSet = /* @__PURE__ */ new Map();
|
|
1596
2206
|
const stats = {
|
|
1597
2207
|
turns: 0,
|
|
@@ -1605,7 +2215,7 @@ function apply(ctx) {
|
|
|
1605
2215
|
};
|
|
1606
2216
|
const turnSeen = /* @__PURE__ */ new Set();
|
|
1607
2217
|
const stepSeen = /* @__PURE__ */ new Set();
|
|
1608
|
-
|
|
2218
|
+
const absorb = (ev) => {
|
|
1609
2219
|
if (ev && typeof ev.time === "number" && ev.time > lastTime) lastTime = ev.time;
|
|
1610
2220
|
const d = ev && ev.data && typeof ev.data === "object" ? ev.data : {};
|
|
1611
2221
|
const type = ev && ev.type;
|
|
@@ -1652,12 +2262,44 @@ function apply(ctx) {
|
|
|
1652
2262
|
break;
|
|
1653
2263
|
}
|
|
1654
2264
|
}
|
|
2265
|
+
};
|
|
2266
|
+
if (live !== void 0) {
|
|
2267
|
+
meta = live && live.header || null;
|
|
2268
|
+
try {
|
|
2269
|
+
(Array.isArray(live.events) ? [...live.events] : []).forEach(absorb);
|
|
2270
|
+
} catch (e) {
|
|
2271
|
+
}
|
|
2272
|
+
} else {
|
|
2273
|
+
const summary = await persistence.inspectSession(sid, { signal, onEvents: (batch) => {
|
|
2274
|
+
for (const ev of batch) absorb(ev);
|
|
2275
|
+
} });
|
|
2276
|
+
if (!summary || !summary.meta) throw new Error("\u627E\u4E0D\u5230\u8BE5\u4F1A\u8BDD\u7684\u8BB0\u5F55\uFF08\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF09");
|
|
2277
|
+
meta = summary.meta;
|
|
1655
2278
|
}
|
|
1656
2279
|
stats.turns = turnSeen.size;
|
|
1657
2280
|
stats.steps = stepSeen.size;
|
|
2281
|
+
let sizeBytes = null;
|
|
2282
|
+
if (live === void 0) {
|
|
2283
|
+
try {
|
|
2284
|
+
const stat3 = await persistence.statSession(sid);
|
|
2285
|
+
if (stat3 && Number.isFinite(stat3.sizeBytes)) sizeBytes = stat3.sizeBytes;
|
|
2286
|
+
} catch (e) {
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
if (sizeBytes === null) {
|
|
2290
|
+
try {
|
|
2291
|
+
const loc = persistence.locate(meta);
|
|
2292
|
+
if (loc && typeof loc.path === "string" && loc.path) {
|
|
2293
|
+
const st = await stat2(loc.path);
|
|
2294
|
+
if (st && typeof st.size === "number") sizeBytes = st.size;
|
|
2295
|
+
}
|
|
2296
|
+
} catch (e) {
|
|
2297
|
+
sizeBytes = null;
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
1658
2300
|
if (stats.fetches.length > MAX_FETCHES) stats.fetches = stats.fetches.slice(0, MAX_FETCHES);
|
|
1659
2301
|
const fileEntries = [...fileSet.entries()].slice(0, MAX_FILES * 2);
|
|
1660
|
-
const exists = await Promise.all(fileEntries.map(([p]) =>
|
|
2302
|
+
const exists = await Promise.all(fileEntries.map(([p]) => stat2(p).then(() => true).catch(() => false)));
|
|
1661
2303
|
const files = fileEntries.filter((_, i) => exists[i]).map(([path, tool]) => ({ path, tool })).slice(0, MAX_FILES);
|
|
1662
2304
|
const lineage = {
|
|
1663
2305
|
parentSessionId: meta && typeof meta.parentSession === "string" ? meta.parentSession : null,
|
|
@@ -1693,7 +2335,7 @@ function apply(ctx) {
|
|
|
1693
2335
|
sessionId: sid,
|
|
1694
2336
|
sizeBytes,
|
|
1695
2337
|
createdAt: meta && typeof meta.createdAt === "number" ? meta.createdAt : null,
|
|
1696
|
-
updatedAt: lastTime || null,
|
|
2338
|
+
updatedAt: Math.max(lastTime || 0, meta && typeof meta.createdAt === "number" ? meta.createdAt : 0) || null,
|
|
1697
2339
|
files,
|
|
1698
2340
|
stats,
|
|
1699
2341
|
lineage
|
|
@@ -1726,7 +2368,13 @@ function apply(ctx) {
|
|
|
1726
2368
|
} catch (e) {
|
|
1727
2369
|
}
|
|
1728
2370
|
const trashStore = await readTrashStore();
|
|
1729
|
-
const
|
|
2371
|
+
const present = /* @__PURE__ */ new Set([
|
|
2372
|
+
...materialized,
|
|
2373
|
+
...ids.map(String),
|
|
2374
|
+
...live && typeof live.list === "function" ? live.list().map((s) => String(s.id)) : []
|
|
2375
|
+
]);
|
|
2376
|
+
const tombstones = trashStore.purgedSessionIds.map(String).filter((id) => !present.has(id));
|
|
2377
|
+
const hidden = /* @__PURE__ */ new Set([...trashStore.items.map((item) => String(item.sessionId)), ...tombstones]);
|
|
1730
2378
|
const idStrs = ids.map(String).filter((id) => !hidden.has(id) && (materialized.has(id) || live && live.get(id)));
|
|
1731
2379
|
wsByPath = {};
|
|
1732
2380
|
try {
|
|
@@ -1859,7 +2507,7 @@ function apply(ctx) {
|
|
|
1859
2507
|
if (typeof item.originalPath !== "string" || !item.originalPath) {
|
|
1860
2508
|
return { sessionId: item.sessionId, status: "unverified", originalPath: null };
|
|
1861
2509
|
}
|
|
1862
|
-
const exists = await
|
|
2510
|
+
const exists = await stat2(item.originalPath).then(() => true).catch(() => false);
|
|
1863
2511
|
return { sessionId: item.sessionId, status: exists ? "ok" : "missing", originalPath: item.originalPath };
|
|
1864
2512
|
}));
|
|
1865
2513
|
json(res, {
|
|
@@ -1963,6 +2611,7 @@ function apply(ctx) {
|
|
|
1963
2611
|
}
|
|
1964
2612
|
}
|
|
1965
2613
|
}));
|
|
2614
|
+
const exportLimiter = createLimiter(2);
|
|
1966
2615
|
disposers.push(ctx.webServer.register({
|
|
1967
2616
|
kind: "exact",
|
|
1968
2617
|
path: "/archived-sessions/export-md",
|
|
@@ -1971,13 +2620,23 @@ function apply(ctx) {
|
|
|
1971
2620
|
const url = new URL(req.url, "http://localhost");
|
|
1972
2621
|
const sid = url.searchParams.get("sessionId");
|
|
1973
2622
|
requireSessionId(sid);
|
|
1974
|
-
const
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
2623
|
+
const ac = new AbortController();
|
|
2624
|
+
res.on("close", () => {
|
|
2625
|
+
if (!res.writableEnded) ac.abort();
|
|
2626
|
+
});
|
|
2627
|
+
const md = await exportLimiter(async () => {
|
|
2628
|
+
const builder = createSessionMarkdownBuilder({ id: sid });
|
|
2629
|
+
const summary = await persistence.inspectSession(sid, {
|
|
2630
|
+
signal: ac.signal,
|
|
2631
|
+
onEvents: (batch) => builder.addEvents(batch)
|
|
2632
|
+
});
|
|
2633
|
+
if (!summary || !summary.meta) {
|
|
2634
|
+
const error = new Error("\u65E0\u6CD5\u8BFB\u53D6\u8BE5\u4F1A\u8BDD\u7684\u65E5\u5FD7");
|
|
2635
|
+
error.status = 404;
|
|
2636
|
+
throw error;
|
|
2637
|
+
}
|
|
2638
|
+
return builder.finish({ ...summary.meta, id: sid });
|
|
2639
|
+
});
|
|
1981
2640
|
res.writeHead(200, {
|
|
1982
2641
|
"content-type": "text/markdown; charset=utf-8",
|
|
1983
2642
|
"content-disposition": `attachment; filename="dsh-session-${sid}.md"`,
|
|
@@ -2077,7 +2736,11 @@ function apply(ctx) {
|
|
|
2077
2736
|
const body = await readJsonBody(req);
|
|
2078
2737
|
const sid = body && typeof body.sessionId === "string" ? body.sessionId : null;
|
|
2079
2738
|
if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
|
|
2080
|
-
|
|
2739
|
+
const ac = new AbortController();
|
|
2740
|
+
res.on("close", () => {
|
|
2741
|
+
if (!res.writableEnded) ac.abort();
|
|
2742
|
+
});
|
|
2743
|
+
json(res, await buildDetails(sid, ac.signal));
|
|
2081
2744
|
} catch (e) {
|
|
2082
2745
|
json(res, { error: String(e && e.message || e) }, e && e.status ? e.status : 500);
|
|
2083
2746
|
}
|