dsh-sessions-manager 3.5.1 → 3.5.3
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 -2
- package/README.md +12 -2
- package/lib/client.js +121 -90
- package/lib/client.js.map +2 -2
- package/lib/index.js +834 -221
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
- package/src/client/index.jsx +154 -86
- package/src/client/logic.js +5 -1
- package/src/compat/capabilities.js +64 -8
- package/src/compat/persistence.js +139 -18
- package/src/handle-era-ops.js +185 -0
- package/src/handle-era-paths.js +126 -0
- package/src/index.js +367 -92
- 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/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.js
|
|
2
|
-
import { mkdir as
|
|
3
|
-
import { basename, dirname as dirname2, isAbsolute, join 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
4
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
5
5
|
import { homedir as homedir3 } from "node:os";
|
|
6
6
|
|
|
@@ -136,82 +136,101 @@ function summarizeToolArguments(name2, rawArguments) {
|
|
|
136
136
|
}
|
|
137
137
|
return JSON.stringify(rest).slice(0, MAX_TOOL_ARG);
|
|
138
138
|
}
|
|
139
|
-
function
|
|
139
|
+
function createMarkdownFold(out, header, options) {
|
|
140
140
|
const includeReasoning = options.includeReasoning === true;
|
|
141
141
|
const includeToolResults = options.includeToolResults === true;
|
|
142
|
-
const header = meta && typeof meta === "object" ? meta : {};
|
|
143
|
-
const list = Array.isArray(events) ? events : [];
|
|
144
142
|
let title = typeof header.title === "string" && header.title.trim() ? header.title.trim() : null;
|
|
145
|
-
for (const ev of list) {
|
|
146
|
-
const data = ev && ev.data;
|
|
147
|
-
if (ev && ev.type === "session/title" && data && typeof data.title === "string" && data.title.trim()) {
|
|
148
|
-
title = data.title.trim();
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
const front = ["---"];
|
|
152
|
-
if (title) front.push(`title: ${yamlString(title)}`);
|
|
153
|
-
if (typeof header.id === "string" && header.id) front.push(`sessionId: ${yamlString(header.id)}`);
|
|
154
|
-
if (typeof header.cwd === "string" && header.cwd) front.push(`cwd: ${yamlString(header.cwd)}`);
|
|
155
|
-
const created = isoTime(header.createdAt);
|
|
156
|
-
if (created) front.push(`createdAt: ${created}`);
|
|
157
|
-
const exported = isoTime(options.exportedAt);
|
|
158
|
-
if (exported) front.push(`exportedAt: ${exported}`);
|
|
159
|
-
front.push("---");
|
|
160
|
-
const out = [front.join("\n")];
|
|
161
|
-
if (title) out.push("", `# ${title}`);
|
|
162
143
|
let turn = null;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
+
};
|
|
215
234
|
}
|
|
216
235
|
|
|
217
236
|
// src/star-index.js
|
|
@@ -448,20 +467,28 @@ function createAutoArchiveStore(options = {}) {
|
|
|
448
467
|
// src/session-meta-cache.js
|
|
449
468
|
var DEFAULT_TTL_MS = 5 * 60 * 1e3;
|
|
450
469
|
var DEFAULT_MAX = 4e3;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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;
|
|
455
478
|
if (typeof mtimeMs !== "number" || !Number.isFinite(mtimeMs) || mtimeMs <= 0) return null;
|
|
456
479
|
if (typeof size !== "number" || !Number.isFinite(size) || size < 0) return null;
|
|
457
480
|
return `${Math.floor(mtimeMs)}:${size}`;
|
|
458
481
|
}
|
|
459
|
-
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) {
|
|
460
486
|
if (!entry) return false;
|
|
461
|
-
const fp = fingerprintOf(
|
|
487
|
+
const fp = fingerprintOf(stat3);
|
|
462
488
|
if (!fp) return false;
|
|
463
489
|
if (entry.fingerprint !== fp) return false;
|
|
464
490
|
if (typeof entry.at !== "number") return false;
|
|
491
|
+
if (fp.startsWith(REVISION_PREFIX)) return true;
|
|
465
492
|
return now - entry.at <= ttlMs;
|
|
466
493
|
}
|
|
467
494
|
function partitionByCache(ids, statsById, cache, now = Date.now(), ttlMs = DEFAULT_TTL_MS) {
|
|
@@ -469,8 +496,8 @@ function partitionByCache(ids, statsById, cache, now = Date.now(), ttlMs = DEFAU
|
|
|
469
496
|
const missing = [];
|
|
470
497
|
for (const id of ids) {
|
|
471
498
|
const entry = cache && cache.get(String(id));
|
|
472
|
-
const
|
|
473
|
-
if (isFresh(entry,
|
|
499
|
+
const stat3 = statsById && statsById.get(String(id));
|
|
500
|
+
if (isFresh(entry, stat3, now, ttlMs) && entry && entry.meta) {
|
|
474
501
|
cached.set(String(id), entry.meta);
|
|
475
502
|
} else {
|
|
476
503
|
missing.push(String(id));
|
|
@@ -486,10 +513,10 @@ function createSessionMetaCache(opts = {}) {
|
|
|
486
513
|
let misses = 0;
|
|
487
514
|
return {
|
|
488
515
|
// 命中返回 meta,未命中/无法校验返回 null。
|
|
489
|
-
get(id,
|
|
516
|
+
get(id, stat3) {
|
|
490
517
|
const key = String(id);
|
|
491
518
|
const entry = map.get(key);
|
|
492
|
-
if (isFresh(entry,
|
|
519
|
+
if (isFresh(entry, stat3, Date.now(), ttlMs)) {
|
|
493
520
|
hits++;
|
|
494
521
|
map.delete(key);
|
|
495
522
|
map.set(key, entry);
|
|
@@ -498,9 +525,9 @@ function createSessionMetaCache(opts = {}) {
|
|
|
498
525
|
misses++;
|
|
499
526
|
return null;
|
|
500
527
|
},
|
|
501
|
-
set(id,
|
|
528
|
+
set(id, stat3, meta) {
|
|
502
529
|
if (!meta) return null;
|
|
503
|
-
const fp = fingerprintOf(
|
|
530
|
+
const fp = fingerprintOf(stat3);
|
|
504
531
|
if (!fp) return null;
|
|
505
532
|
const key = String(id);
|
|
506
533
|
map.delete(key);
|
|
@@ -546,7 +573,8 @@ function normalizeEntry(raw) {
|
|
|
546
573
|
const createdAt = typeof raw.createdAt === "number" ? raw.createdAt : null;
|
|
547
574
|
const fingerprint = typeof raw.fingerprint === "string" && raw.fingerprint ? raw.fingerprint : null;
|
|
548
575
|
const updatedAt = typeof raw.updatedAt === "number" ? raw.updatedAt : 0;
|
|
549
|
-
if (!fingerprint ||
|
|
576
|
+
if (!fingerprint || fingerprint.startsWith("rev:")) return null;
|
|
577
|
+
if (!title && !cwd) return null;
|
|
550
578
|
return { title, cwd, createdAt, fingerprint, updatedAt };
|
|
551
579
|
}
|
|
552
580
|
function normalizeTitleIndex(raw) {
|
|
@@ -638,7 +666,135 @@ function createTitleIndexStore({ dir, file }) {
|
|
|
638
666
|
};
|
|
639
667
|
}
|
|
640
668
|
|
|
669
|
+
// src/handle-era-paths.js
|
|
670
|
+
import { readdir, stat } from "node:fs/promises";
|
|
671
|
+
import { basename, join as join4 } from "node:path";
|
|
672
|
+
|
|
673
|
+
// src/path-guard.js
|
|
674
|
+
function splitSegments(target) {
|
|
675
|
+
return String(target).replace(/[\\/]+$/, "").split(/[\\/]/).filter((seg) => seg.length > 0);
|
|
676
|
+
}
|
|
677
|
+
function stripDriveLetter(segments) {
|
|
678
|
+
return segments.length > 0 && /^[A-Za-z]:$/.test(segments[0]) ? segments.slice(1) : segments;
|
|
679
|
+
}
|
|
680
|
+
function pathOwnsSession(target, sid) {
|
|
681
|
+
if (typeof target !== "string" || target.length === 0) return false;
|
|
682
|
+
if (typeof sid !== "string" || sid.length === 0) return false;
|
|
683
|
+
const segments = stripDriveLetter(splitSegments(target));
|
|
684
|
+
if (segments.length === 0) return false;
|
|
685
|
+
const file = segments[segments.length - 1];
|
|
686
|
+
if (segments.length >= 2 && segments[segments.length - 2] === sid) return true;
|
|
687
|
+
if (file.includes(sid)) {
|
|
688
|
+
const ID_CHAR = /[A-Za-z0-9_-]/;
|
|
689
|
+
let from = 0;
|
|
690
|
+
while (true) {
|
|
691
|
+
const at = file.indexOf(sid, from);
|
|
692
|
+
if (at < 0) return false;
|
|
693
|
+
const before = at > 0 ? file[at - 1] : "";
|
|
694
|
+
const after = at + sid.length < file.length ? file[at + sid.length] : "";
|
|
695
|
+
if (!(before && ID_CHAR.test(before)) && !(after && ID_CHAR.test(after))) return true;
|
|
696
|
+
from = at + 1;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/handle-era-paths.js
|
|
703
|
+
var GENERATION_LOG_RE = /^session\.v\d+\.jsonl(\.zst(d)?)?$/;
|
|
704
|
+
function isSafeChar(ch) {
|
|
705
|
+
return ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch);
|
|
706
|
+
}
|
|
707
|
+
function projectKeyFor(cwd) {
|
|
708
|
+
const s = String(cwd);
|
|
709
|
+
if (s.length === 0) throw new Error("cannot encode an empty project path");
|
|
710
|
+
let readable = "";
|
|
711
|
+
let separatorRun = false;
|
|
712
|
+
for (let i = 0; i < s.length; i++) {
|
|
713
|
+
const ch = s[i];
|
|
714
|
+
if (ch === "/" || ch === "\\" || ch === ":") {
|
|
715
|
+
if (!separatorRun) readable += "-";
|
|
716
|
+
separatorRun = true;
|
|
717
|
+
} else if (isSafeChar(ch)) {
|
|
718
|
+
readable += ch;
|
|
719
|
+
separatorRun = false;
|
|
720
|
+
} else {
|
|
721
|
+
readable += "~" + s.charCodeAt(i).toString(16).toUpperCase().padStart(4, "0");
|
|
722
|
+
separatorRun = false;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return "--" + (readable.replace(/^-+/, "") || "root").slice(0, 251) + "--";
|
|
726
|
+
}
|
|
727
|
+
function encodeSegmentFor(raw) {
|
|
728
|
+
const s = String(raw);
|
|
729
|
+
if (s.length === 0) throw new Error("cannot encode an empty path segment");
|
|
730
|
+
if (s === ".") return "~002E";
|
|
731
|
+
if (s === "..") return "~002E~002E";
|
|
732
|
+
let out = "";
|
|
733
|
+
for (let i = 0; i < s.length; i++) {
|
|
734
|
+
const ch = s[i];
|
|
735
|
+
out += isSafeChar(ch) ? ch : "~" + s.charCodeAt(i).toString(16).toUpperCase().padStart(4, "0");
|
|
736
|
+
}
|
|
737
|
+
return out;
|
|
738
|
+
}
|
|
739
|
+
function resolveSessionRoot(sp) {
|
|
740
|
+
const root = sp && typeof sp === "object" ? sp.root : void 0;
|
|
741
|
+
return typeof root === "string" && root.length > 0 ? root : null;
|
|
742
|
+
}
|
|
743
|
+
function deriveSessionDir(root, cwd, id) {
|
|
744
|
+
const project = cwd === void 0 || cwd === null || cwd === "" ? join4(root, "_no-cwd") : join4(root, projectKeyFor(cwd));
|
|
745
|
+
return join4(project, encodeSegmentFor(id));
|
|
746
|
+
}
|
|
747
|
+
async function locateSessionArtifacts(sp, header) {
|
|
748
|
+
const root = resolveSessionRoot(sp);
|
|
749
|
+
if (!root || !header || header.id == null) return null;
|
|
750
|
+
const sid = String(header.id);
|
|
751
|
+
let sessionDir;
|
|
752
|
+
try {
|
|
753
|
+
sessionDir = deriveSessionDir(root, header.cwd, sid);
|
|
754
|
+
} catch (e) {
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
if (basename(sessionDir) !== encodeSegmentFor(sid)) return null;
|
|
758
|
+
let entries;
|
|
759
|
+
try {
|
|
760
|
+
const st = await stat(sessionDir);
|
|
761
|
+
if (!st.isDirectory()) return null;
|
|
762
|
+
entries = await readdir(sessionDir);
|
|
763
|
+
} catch (e) {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
const generationFiles = entries.filter((name2) => GENERATION_LOG_RE.test(name2));
|
|
767
|
+
if (generationFiles.length === 0) return null;
|
|
768
|
+
generationFiles.sort((a, b) => {
|
|
769
|
+
const va = Number((a.match(/^session\.v(\d+)\./) || [])[1] || 0);
|
|
770
|
+
const vb = Number((b.match(/^session\.v(\d+)\./) || [])[1] || 0);
|
|
771
|
+
return vb - va;
|
|
772
|
+
});
|
|
773
|
+
const logPath = join4(sessionDir, generationFiles[0]);
|
|
774
|
+
if (!pathOwnsSession(logPath, sid)) return null;
|
|
775
|
+
return {
|
|
776
|
+
root,
|
|
777
|
+
projectDir: join4(root, header.cwd === void 0 || header.cwd === null || header.cwd === "" ? "_no-cwd" : projectKeyFor(header.cwd)),
|
|
778
|
+
sessionDir,
|
|
779
|
+
logPath,
|
|
780
|
+
generationFiles
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
641
784
|
// src/compat/persistence.js
|
|
785
|
+
var DEFAULT_CHUNK = 400;
|
|
786
|
+
var MAX_CHUNKS = 2e4;
|
|
787
|
+
function normalizeReadResult(events) {
|
|
788
|
+
if (Array.isArray(events)) return events;
|
|
789
|
+
if (events && typeof events[Symbol.iterator] === "function") return [...events];
|
|
790
|
+
return [];
|
|
791
|
+
}
|
|
792
|
+
async function closeQuietly(handle) {
|
|
793
|
+
try {
|
|
794
|
+
if (handle && typeof handle.close === "function") await handle.close();
|
|
795
|
+
} catch (e) {
|
|
796
|
+
}
|
|
797
|
+
}
|
|
642
798
|
function asHeader(value) {
|
|
643
799
|
if (!value || typeof value !== "object") return null;
|
|
644
800
|
const candidate = value.header && typeof value.header === "object" ? value.header : value;
|
|
@@ -654,7 +810,7 @@ function normalizePersistenceEntry(value) {
|
|
|
654
810
|
id: String(header.id),
|
|
655
811
|
sizeBytes: snapshot && Number.isFinite(snapshot.sizeBytes) ? Number(snapshot.sizeBytes) : null,
|
|
656
812
|
eventCount: snapshot && Number.isSafeInteger(snapshot.eventCount) ? snapshot.eventCount : null,
|
|
657
|
-
revision: snapshot ? snapshot.revision : null
|
|
813
|
+
revision: snapshot && typeof snapshot.revision === "string" && snapshot.revision ? snapshot.revision : null
|
|
658
814
|
};
|
|
659
815
|
}
|
|
660
816
|
function normalizePersistenceList(values) {
|
|
@@ -663,44 +819,112 @@ function normalizePersistenceList(values) {
|
|
|
663
819
|
}
|
|
664
820
|
function createPersistenceAdapter(service) {
|
|
665
821
|
if (!service || typeof service.list !== "function") throw new TypeError("sessionPersistence.list is required");
|
|
822
|
+
const hasStat = typeof service.stat === "function";
|
|
823
|
+
const kind = typeof service.open === "function" ? "session-handle" : "legacy";
|
|
666
824
|
async function listEntries(options) {
|
|
667
825
|
return normalizePersistenceList(await service.list(options));
|
|
668
826
|
}
|
|
669
|
-
async function
|
|
670
|
-
if (
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
827
|
+
async function statSession(id) {
|
|
828
|
+
if (!hasStat) return null;
|
|
829
|
+
const snapshot = await service.stat(id);
|
|
830
|
+
return snapshot ? normalizePersistenceEntry(snapshot) : null;
|
|
831
|
+
}
|
|
832
|
+
async function readChunk(handle, offset, length, signal) {
|
|
833
|
+
if (signal && signal.aborted) {
|
|
834
|
+
const error = new Error("\u4F1A\u8BDD\u8BFB\u53D6\u5DF2\u53D6\u6D88");
|
|
835
|
+
error.code = "DSM_READ_ABORTED";
|
|
836
|
+
throw error;
|
|
677
837
|
}
|
|
838
|
+
const events = await handle.read(offset, length, signal ? { signal } : void 0);
|
|
839
|
+
return normalizeReadResult(events);
|
|
840
|
+
}
|
|
841
|
+
async function readChunks(id, { offset = 0, chunkSize = DEFAULT_CHUNK, signal, onEvents }) {
|
|
678
842
|
if (typeof service.open !== "function") throw new Error("\u5F53\u524D DSH \u6301\u4E45\u5316\u670D\u52A1\u4E0D\u652F\u6301\u8BFB\u53D6\u4F1A\u8BDD");
|
|
679
843
|
const handle = await service.open(id, "read");
|
|
680
844
|
if (!handle || typeof handle.read !== "function" || typeof handle.close !== "function") {
|
|
681
|
-
|
|
682
|
-
if (handle && typeof handle.close === "function") await handle.close();
|
|
683
|
-
} catch {
|
|
684
|
-
}
|
|
845
|
+
await closeQuietly(handle);
|
|
685
846
|
throw new Error("DSH \u8FD4\u56DE\u4E86\u65E0\u6548\u7684 SessionHandle");
|
|
686
847
|
}
|
|
848
|
+
let cursor = Number.isSafeInteger(offset) && offset >= 0 ? offset : 0;
|
|
849
|
+
let total = 0;
|
|
687
850
|
try {
|
|
688
|
-
|
|
851
|
+
for (let round = 0; round < MAX_CHUNKS; round++) {
|
|
852
|
+
const events = await readChunk(handle, cursor, chunkSize, signal);
|
|
853
|
+
if (events.length === 0) break;
|
|
854
|
+
cursor += events.length;
|
|
855
|
+
total += events.length;
|
|
856
|
+
if (onEvents) await onEvents(events, { offset: cursor - events.length, total });
|
|
857
|
+
if (events.length < chunkSize) break;
|
|
858
|
+
}
|
|
859
|
+
} finally {
|
|
860
|
+
await closeQuietly(handle);
|
|
861
|
+
}
|
|
862
|
+
return {
|
|
863
|
+
meta: handle.header || handle.meta || null,
|
|
864
|
+
inheritedEventCount: Number.isSafeInteger(handle.inheritedEventCount) ? handle.inheritedEventCount : 0,
|
|
865
|
+
eventCount: total
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
async function inspectSession(id, opts = {}) {
|
|
869
|
+
const chunkSize = Number.isSafeInteger(opts.chunkSize) && opts.chunkSize > 0 ? opts.chunkSize : DEFAULT_CHUNK;
|
|
870
|
+
if (typeof service.open === "function") {
|
|
871
|
+
if (opts.signal && opts.signal.aborted) {
|
|
872
|
+
const error = new Error("\u4F1A\u8BDD\u8BFB\u53D6\u5DF2\u53D6\u6D88");
|
|
873
|
+
error.code = "DSM_READ_ABORTED";
|
|
874
|
+
throw error;
|
|
875
|
+
}
|
|
876
|
+
return readChunks(id, { offset: opts.offset || 0, chunkSize, signal: opts.signal, onEvents: opts.onEvents });
|
|
877
|
+
}
|
|
878
|
+
if (typeof service.readFrom !== "function") throw new Error("\u5F53\u524D DSH \u6301\u4E45\u5316\u670D\u52A1\u4E0D\u652F\u6301\u8BFB\u53D6\u4F1A\u8BDD");
|
|
879
|
+
if (opts.signal && opts.signal.aborted) {
|
|
880
|
+
const error = new Error("\u4F1A\u8BDD\u8BFB\u53D6\u5DF2\u53D6\u6D88");
|
|
881
|
+
error.code = "DSM_READ_ABORTED";
|
|
882
|
+
throw error;
|
|
883
|
+
}
|
|
884
|
+
const result = await service.readFrom(id, opts.offset || 0);
|
|
885
|
+
const events = normalizeReadResult(result && result.events);
|
|
886
|
+
if (opts.onEvents && events.length) await opts.onEvents(events, { offset: opts.offset || 0, total: events.length });
|
|
887
|
+
return {
|
|
888
|
+
meta: result && result.meta ? result.meta : null,
|
|
889
|
+
inheritedEventCount: result && Number.isSafeInteger(result.inheritedEventCount) ? result.inheritedEventCount : 0,
|
|
890
|
+
eventCount: events.length
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
async function readSession(id, offset = 0) {
|
|
894
|
+
if (typeof service.readFrom === "function") {
|
|
895
|
+
const result = await service.readFrom(id, offset);
|
|
689
896
|
return {
|
|
690
|
-
meta:
|
|
691
|
-
inheritedEventCount: Number.isSafeInteger(
|
|
692
|
-
events: Array.isArray(events) ? events : [
|
|
897
|
+
meta: result && result.meta ? result.meta : null,
|
|
898
|
+
inheritedEventCount: result && Number.isSafeInteger(result.inheritedEventCount) ? result.inheritedEventCount : 0,
|
|
899
|
+
events: result && Array.isArray(result.events) ? result.events : []
|
|
693
900
|
};
|
|
694
|
-
} finally {
|
|
695
|
-
await handle.close();
|
|
696
901
|
}
|
|
902
|
+
const events = [];
|
|
903
|
+
const summary = await readChunks(id, { offset, onEvents: (batch) => {
|
|
904
|
+
events.push(...batch);
|
|
905
|
+
} });
|
|
906
|
+
return {
|
|
907
|
+
meta: summary.meta,
|
|
908
|
+
inheritedEventCount: summary.inheritedEventCount,
|
|
909
|
+
events
|
|
910
|
+
};
|
|
697
911
|
}
|
|
698
912
|
function locate(header) {
|
|
699
913
|
if (typeof service.locate === "function") return service.locate(header);
|
|
700
914
|
return null;
|
|
701
915
|
}
|
|
702
|
-
|
|
703
|
-
|
|
916
|
+
async function locateVerified(header) {
|
|
917
|
+
if (typeof service.locate === "function") {
|
|
918
|
+
try {
|
|
919
|
+
const loc = service.locate(header);
|
|
920
|
+
if (loc && typeof loc.path === "string") return { path: loc.path, sessionDir: null };
|
|
921
|
+
} catch (e) {
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
const artifacts = await locateSessionArtifacts(service, header);
|
|
925
|
+
return artifacts ? { path: artifacts.logPath, sessionDir: artifacts.sessionDir } : null;
|
|
926
|
+
}
|
|
927
|
+
return { kind, listEntries, readSession, inspectSession, statSession, locate, locateVerified, hasStat };
|
|
704
928
|
}
|
|
705
929
|
|
|
706
930
|
// src/compat/capabilities.js
|
|
@@ -711,17 +935,34 @@ function detectCapabilities({ persistence, workspaceRegistry }) {
|
|
|
711
935
|
const handleApi = !!(persistence && typeof persistence.open === "function");
|
|
712
936
|
const legacyRead = !!(persistence && typeof persistence.readFrom === "function");
|
|
713
937
|
const legacyLocate = !!(persistence && (typeof persistence.locate === "function" || persistence.backend && typeof persistence.backend.locate === "function"));
|
|
938
|
+
const handleEraRoot = !!(handleApi && persistence && typeof persistence.root === "string" && persistence.root.length > 0);
|
|
939
|
+
const canVerifyExistence = !!(handleApi || legacyRead || persistence && typeof persistence.stat === "function");
|
|
940
|
+
const readOk = legacyRead || handleApi;
|
|
714
941
|
const workspaceInternals = !!(workspaceRegistry && workspaceRegistry.headers && workspaceRegistry.sessionPaths && typeof workspaceRegistry.replaceHeaderIndex === "function");
|
|
942
|
+
const matrix = {
|
|
943
|
+
readInspection: action(readOk, "\u5F53\u524D DSH \u672A\u63D0\u4F9B\u53EF\u8BC6\u522B\u7684\u4F1A\u8BDD\u8BFB\u53D6\u63A5\u53E3"),
|
|
944
|
+
archive: action(!!(workspaceRegistry && typeof workspaceRegistry.archiveSession === "function"), "\u5F53\u524D DSH \u672A\u63D0\u4F9B\u5F52\u6863\u63A5\u53E3"),
|
|
945
|
+
softTrash: action(readOk, "\u5F53\u524D DSH \u65E0\u6CD5\u8BFB\u53D6\u4F1A\u8BDD\uFF0C\u4E0D\u80FD\u5B89\u5168\u79FB\u5165\u56DE\u6536\u7AD9"),
|
|
946
|
+
// 恢复不再无条件宣称可用:必须能校验底层会话仍存在(stat 或 list),
|
|
947
|
+
// 否则恢复只会制造一条指向已消失日志的僵尸条目。
|
|
948
|
+
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"),
|
|
949
|
+
physicalPurge: action(
|
|
950
|
+
!handleApi && legacyLocate || handleApi && handleEraRoot && canVerifyExistence,
|
|
951
|
+
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"
|
|
952
|
+
),
|
|
953
|
+
relocateSession: action(
|
|
954
|
+
!handleApi && legacyRead && legacyLocate && workspaceInternals || handleApi && handleEraRoot && readOk && workspaceInternals,
|
|
955
|
+
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"
|
|
956
|
+
)
|
|
957
|
+
};
|
|
958
|
+
matrix.read = matrix.readInspection;
|
|
959
|
+
matrix.trash = matrix.softTrash;
|
|
960
|
+
matrix.restoreTrash = matrix.restoreIndexedSession;
|
|
961
|
+
matrix.purge = matrix.physicalPurge;
|
|
962
|
+
matrix.move = matrix.relocateSession;
|
|
715
963
|
return {
|
|
716
964
|
persistence: handleApi ? "session-handle" : "legacy",
|
|
717
|
-
actions:
|
|
718
|
-
read: action(legacyRead || handleApi, "\u5F53\u524D DSH \u672A\u63D0\u4F9B\u53EF\u8BC6\u522B\u7684\u4F1A\u8BDD\u8BFB\u53D6\u63A5\u53E3"),
|
|
719
|
-
archive: action(!!(workspaceRegistry && typeof workspaceRegistry.archiveSession === "function"), "\u5F53\u524D DSH \u672A\u63D0\u4F9B\u5F52\u6863\u63A5\u53E3"),
|
|
720
|
-
trash: action(legacyRead || handleApi, "\u5F53\u524D DSH \u65E0\u6CD5\u8BFB\u53D6\u4F1A\u8BDD\uFF0C\u4E0D\u80FD\u5B89\u5168\u79FB\u5165\u56DE\u6536\u7AD9"),
|
|
721
|
-
restoreTrash: action(true),
|
|
722
|
-
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"),
|
|
723
|
-
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")
|
|
724
|
-
}
|
|
965
|
+
actions: matrix
|
|
725
966
|
};
|
|
726
967
|
}
|
|
727
968
|
function requireCapability(capabilities, name2) {
|
|
@@ -733,12 +974,158 @@ function requireCapability(capabilities, name2) {
|
|
|
733
974
|
throw error;
|
|
734
975
|
}
|
|
735
976
|
|
|
977
|
+
// src/handle-era-ops.js
|
|
978
|
+
import { mkdir as mkdir4, readFile as readFile2, rename as rename4, rm, unlink, writeFile as writeFile4 } from "node:fs/promises";
|
|
979
|
+
import { join as join5 } from "node:path";
|
|
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 locateSessionArtifacts(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 locateSessionArtifacts(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
|
+
|
|
736
1123
|
// src/index.js
|
|
737
1124
|
var name = "dsh-sessions-manager";
|
|
738
1125
|
var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
|
|
739
1126
|
var MAX_TITLE = 80;
|
|
740
|
-
var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR ||
|
|
741
|
-
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");
|
|
742
1129
|
var TRASH_SCHEMA_VERSION = 2;
|
|
743
1130
|
var DEFAULT_TRASH_SETTINGS = Object.freeze({ retentionDays: 0 });
|
|
744
1131
|
var FETCH_TOOL_RE = /search|fetch|download|browse/i;
|
|
@@ -752,6 +1139,21 @@ function json(res, value, status = 200) {
|
|
|
752
1139
|
function errorStatus(error) {
|
|
753
1140
|
return error && Number.isInteger(error.status) ? error.status : 500;
|
|
754
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
|
+
}
|
|
755
1157
|
async function readJsonBody(req) {
|
|
756
1158
|
const chunks = [];
|
|
757
1159
|
let total = 0;
|
|
@@ -825,7 +1227,7 @@ function apply(ctx) {
|
|
|
825
1227
|
const dom = () => ctx.storageDomain.get("workspace");
|
|
826
1228
|
const authorityTitleCache = /* @__PURE__ */ new Map();
|
|
827
1229
|
const metaCache = createSessionMetaCache();
|
|
828
|
-
const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file:
|
|
1230
|
+
const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file: join6(TRASH_DIR, "title-index.json") });
|
|
829
1231
|
async function hydrateFromPersist(ids, statsById) {
|
|
830
1232
|
const hits = /* @__PURE__ */ new Map();
|
|
831
1233
|
if (!ids || !ids.length) return hits;
|
|
@@ -836,10 +1238,11 @@ function apply(ctx) {
|
|
|
836
1238
|
return hits;
|
|
837
1239
|
}
|
|
838
1240
|
for (const id of ids) {
|
|
839
|
-
const
|
|
1241
|
+
const stat3 = statsById.get(id);
|
|
840
1242
|
const entry = store && store[id];
|
|
841
|
-
if (!
|
|
842
|
-
const fp = fingerprintOf(
|
|
1243
|
+
if (!stat3 || !entry) continue;
|
|
1244
|
+
const fp = fingerprintOf(stat3);
|
|
1245
|
+
if (!isPersistableFingerprint(fp)) continue;
|
|
843
1246
|
if (fp && entry.fingerprint === fp) {
|
|
844
1247
|
hits.set(id, { title: entry.title, cwd: entry.cwd, createdAt: entry.createdAt });
|
|
845
1248
|
}
|
|
@@ -852,7 +1255,7 @@ function apply(ctx) {
|
|
|
852
1255
|
const now = Date.now();
|
|
853
1256
|
for (const [id, meta] of decoded) {
|
|
854
1257
|
const fp = fingerprintOf(statsById.get(id));
|
|
855
|
-
if (!fp) continue;
|
|
1258
|
+
if (!isPersistableFingerprint(fp)) continue;
|
|
856
1259
|
batch[id] = { title: meta.title, cwd: meta.cwd, createdAt: meta.createdAt, fingerprint: fp, updatedAt: now };
|
|
857
1260
|
}
|
|
858
1261
|
if (!Object.keys(batch).length) return;
|
|
@@ -931,26 +1334,42 @@ function apply(ctx) {
|
|
|
931
1334
|
}
|
|
932
1335
|
async function resolveOne(id, usage, opts = {}) {
|
|
933
1336
|
const key = String(id);
|
|
934
|
-
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;
|
|
935
1338
|
const cached = metaCache.get(key, statInfo);
|
|
936
1339
|
if (cached) return buildItem(key, cached, usage, opts.exposeUsage);
|
|
937
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
|
+
}
|
|
938
1345
|
if (opts.preloaded !== void 0) {
|
|
939
|
-
|
|
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;
|
|
940
1350
|
} else if (typeof sq.readTitleSnapshot === "function") {
|
|
941
1351
|
try {
|
|
942
|
-
|
|
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;
|
|
943
1356
|
} catch (e) {
|
|
944
1357
|
}
|
|
945
1358
|
}
|
|
946
|
-
|
|
1359
|
+
const projectionAvailable = typeof sq.readTitleSnapshot === "function" || typeof sq.readTitleSnapshots === "function";
|
|
1360
|
+
if (!meta.cwd || !meta.title && !projectionAvailable) {
|
|
947
1361
|
try {
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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;
|
|
952
1371
|
}
|
|
953
|
-
if (!meta.title &&
|
|
1372
|
+
if (!meta.title && foldedTitle) meta.title = foldedTitle;
|
|
954
1373
|
} catch (e2) {
|
|
955
1374
|
}
|
|
956
1375
|
}
|
|
@@ -958,11 +1377,12 @@ function apply(ctx) {
|
|
|
958
1377
|
if (opts.collectDecoded && statInfo) opts.collectDecoded(key, meta);
|
|
959
1378
|
return buildItem(key, meta, usage, opts.exposeUsage);
|
|
960
1379
|
}
|
|
961
|
-
async function collectUsage(
|
|
1380
|
+
async function collectUsage(preloadedEntries) {
|
|
962
1381
|
const sizeById = /* @__PURE__ */ new Map();
|
|
963
1382
|
const mtimeById = /* @__PURE__ */ new Map();
|
|
1383
|
+
const statsById = /* @__PURE__ */ new Map();
|
|
964
1384
|
let entries = null;
|
|
965
|
-
if (Array.isArray(
|
|
1385
|
+
if (Array.isArray(preloadedEntries)) entries = preloadedEntries;
|
|
966
1386
|
else {
|
|
967
1387
|
try {
|
|
968
1388
|
entries = await persistence.listEntries();
|
|
@@ -977,19 +1397,27 @@ function apply(ctx) {
|
|
|
977
1397
|
const header = entry && entry.header ? entry.header : entry;
|
|
978
1398
|
const id = entry && entry.id != null ? String(entry.id) : header && header.id != null ? String(header.id) : null;
|
|
979
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
|
+
}
|
|
980
1405
|
if (entry && Number.isFinite(entry.sizeBytes)) sizeById.set(id, Number(entry.sizeBytes));
|
|
981
1406
|
try {
|
|
982
1407
|
const loc = persistence.locate(header);
|
|
983
1408
|
if (!loc || typeof loc.path !== "string" || !loc.path) return;
|
|
984
|
-
const st = await
|
|
1409
|
+
const st = await stat2(loc.path);
|
|
985
1410
|
if (!st) return;
|
|
986
1411
|
if (typeof st.size === "number") sizeById.set(id, st.size);
|
|
987
|
-
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
|
+
}
|
|
988
1416
|
} catch (e) {
|
|
989
1417
|
}
|
|
990
1418
|
}));
|
|
991
1419
|
}
|
|
992
|
-
return { sizeById, mtimeById };
|
|
1420
|
+
return { sizeById, mtimeById, statsById, hasActivityData: mtimeById.size > 0 };
|
|
993
1421
|
}
|
|
994
1422
|
async function restoreOne(sid) {
|
|
995
1423
|
requireSessionId(sid);
|
|
@@ -1018,10 +1446,10 @@ function apply(ctx) {
|
|
|
1018
1446
|
return (await readTrashStore()).items;
|
|
1019
1447
|
}
|
|
1020
1448
|
async function writeTrashStore(store) {
|
|
1021
|
-
await
|
|
1022
|
-
const tmp =
|
|
1023
|
-
await
|
|
1024
|
-
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);
|
|
1025
1453
|
}
|
|
1026
1454
|
function mutateTrash(mutator) {
|
|
1027
1455
|
const operation = trashMutation.then(async () => {
|
|
@@ -1064,10 +1492,28 @@ function apply(ctx) {
|
|
|
1064
1492
|
title = header.title || header.meta && header.meta.title || null;
|
|
1065
1493
|
}
|
|
1066
1494
|
if (!title) {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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) {
|
|
1071
1517
|
}
|
|
1072
1518
|
}
|
|
1073
1519
|
} catch (e) {
|
|
@@ -1096,20 +1542,96 @@ function apply(ctx) {
|
|
|
1096
1542
|
});
|
|
1097
1543
|
return { ok: true, trashed: true };
|
|
1098
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
|
+
}
|
|
1099
1604
|
async function restoreFromTrash(sid) {
|
|
1100
1605
|
requireSessionId(sid);
|
|
1606
|
+
requireCapability(capabilities, "restoreIndexedSession");
|
|
1607
|
+
let outcome = null;
|
|
1101
1608
|
await mutateTrash(async (store) => {
|
|
1102
1609
|
const entry = store.items.find((t) => String(t.sessionId) === sid);
|
|
1103
1610
|
if (!entry) {
|
|
1104
|
-
|
|
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");
|
|
1105
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;
|
|
1106
1627
|
throw error;
|
|
1107
1628
|
}
|
|
1108
1629
|
if (entry.wasArchived === false) await restoreOne(sid);
|
|
1109
1630
|
store.items = store.items.filter((t) => String(t.sessionId) !== sid);
|
|
1110
1631
|
store.purgedSessionIds = store.purgedSessionIds.filter((id) => id !== sid);
|
|
1632
|
+
outcome = { ok: true, restored: true, workspaceGone: verification.workspaceGone, verified: verification.verified };
|
|
1111
1633
|
});
|
|
1112
|
-
return { ok: true, restored: true };
|
|
1634
|
+
return outcome || { ok: true, restored: true };
|
|
1113
1635
|
}
|
|
1114
1636
|
async function purgeFromTrash(sid) {
|
|
1115
1637
|
requireSessionId(sid);
|
|
@@ -1123,20 +1645,33 @@ function apply(ctx) {
|
|
|
1123
1645
|
throw error;
|
|
1124
1646
|
}
|
|
1125
1647
|
let target = null;
|
|
1648
|
+
let locatedHeader = null;
|
|
1126
1649
|
try {
|
|
1127
1650
|
const entries = await persistence.listEntries();
|
|
1128
1651
|
const current = entries.find((entry2) => entry2.id === sid);
|
|
1129
|
-
|
|
1652
|
+
locatedHeader = current ? current.header : null;
|
|
1653
|
+
const located = current ? await persistence.locateVerified(current.header) : null;
|
|
1130
1654
|
if (located && typeof located.path === "string") target = located.path;
|
|
1131
1655
|
} catch (e) {
|
|
1132
1656
|
}
|
|
1657
|
+
if (!locatedHeader && typeof persistence.statSession === "function") {
|
|
1658
|
+
try {
|
|
1659
|
+
const snap = await persistence.statSession(sid);
|
|
1660
|
+
if (snap && snap.header) {
|
|
1661
|
+
locatedHeader = snap.header;
|
|
1662
|
+
const located = await persistence.locateVerified(snap.header);
|
|
1663
|
+
if (located && typeof located.path === "string") target = located.path;
|
|
1664
|
+
}
|
|
1665
|
+
} catch (e) {
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1133
1668
|
if (!target && typeof entry.originalPath === "string") target = entry.originalPath;
|
|
1134
1669
|
if (!target) {
|
|
1135
1670
|
const error = new Error("\u65E0\u6CD5\u786E\u8BA4\u8BE5\u4F1A\u8BDD\u7684\u7269\u7406\u65E5\u5FD7\u4F4D\u7F6E\uFF0C\u5DF2\u505C\u6B62\u6C38\u4E45\u5220\u9664");
|
|
1136
1671
|
error.status = 409;
|
|
1137
1672
|
throw error;
|
|
1138
1673
|
}
|
|
1139
|
-
const targetOwnsSession =
|
|
1674
|
+
const targetOwnsSession = pathOwnsSession(target, sid);
|
|
1140
1675
|
if (target && !targetOwnsSession) {
|
|
1141
1676
|
const error = new Error("\u65E5\u5FD7\u8DEF\u5F84\u4E0E\u4F1A\u8BDD ID \u4E0D\u5339\u914D\uFF0C\u5DF2\u505C\u6B62\u6C38\u4E45\u5220\u9664");
|
|
1142
1677
|
error.status = 409;
|
|
@@ -1158,9 +1693,11 @@ function apply(ctx) {
|
|
|
1158
1693
|
error.status = 409;
|
|
1159
1694
|
throw error;
|
|
1160
1695
|
}
|
|
1161
|
-
if (target) {
|
|
1696
|
+
if (target && persistence.kind === "session-handle" && locatedHeader) {
|
|
1697
|
+
await purgeSessionArtifacts(sp, sid, locatedHeader);
|
|
1698
|
+
} else if (target) {
|
|
1162
1699
|
try {
|
|
1163
|
-
await
|
|
1700
|
+
await unlink2(target);
|
|
1164
1701
|
} catch (e) {
|
|
1165
1702
|
if (e && e.code !== "ENOENT") throw new Error("\u5220\u9664\u6587\u4EF6\u5931\u8D25\uFF1A" + String(e && e.message || e));
|
|
1166
1703
|
}
|
|
@@ -1230,8 +1767,8 @@ function apply(ctx) {
|
|
|
1230
1767
|
async function moveTargetWorkspace(rawPath) {
|
|
1231
1768
|
if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
|
|
1232
1769
|
let p = String(rawPath).trim();
|
|
1233
|
-
if (p.startsWith("~/")) p =
|
|
1234
|
-
if (!isAbsolute(p)) p =
|
|
1770
|
+
if (p.startsWith("~/")) p = join6(homedir3(), p.slice(2));
|
|
1771
|
+
if (!isAbsolute(p)) p = join6(homedir3(), p);
|
|
1235
1772
|
let canonical = null;
|
|
1236
1773
|
try {
|
|
1237
1774
|
canonical = await realpath(p);
|
|
@@ -1239,10 +1776,10 @@ function apply(ctx) {
|
|
|
1239
1776
|
canonical = null;
|
|
1240
1777
|
}
|
|
1241
1778
|
if (canonical === null) {
|
|
1242
|
-
await
|
|
1779
|
+
await mkdir5(p, { recursive: true });
|
|
1243
1780
|
canonical = await realpath(p);
|
|
1244
1781
|
}
|
|
1245
|
-
return { canonical, entity: await w.create(canonical,
|
|
1782
|
+
return { canonical, entity: await w.create(canonical, basename2(canonical) || "workspace") };
|
|
1246
1783
|
}
|
|
1247
1784
|
async function moveOne(sid, targetPath) {
|
|
1248
1785
|
requireCapability(capabilities, "move");
|
|
@@ -1280,15 +1817,15 @@ function apply(ctx) {
|
|
|
1280
1817
|
const stagedPath = `${newPath}.move-stage-${process.pid}-${Date.now()}`;
|
|
1281
1818
|
let destinationInstalled = false;
|
|
1282
1819
|
try {
|
|
1283
|
-
await
|
|
1820
|
+
await mkdir5(dirname2(newPath), { recursive: true });
|
|
1284
1821
|
try {
|
|
1285
|
-
await
|
|
1822
|
+
await stat2(newPath);
|
|
1286
1823
|
throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u76EE\u6807\u4F4D\u7F6E\u5DF2\u5B58\u5728\u540C\u540D\u4F1A\u8BDD\u65E5\u5FD7");
|
|
1287
1824
|
} catch (e) {
|
|
1288
1825
|
if (e && e.code !== "ENOENT") throw e;
|
|
1289
1826
|
}
|
|
1290
|
-
await
|
|
1291
|
-
const original = await
|
|
1827
|
+
await rename5(oldPath, backupPath);
|
|
1828
|
+
const original = await readFile3(backupPath);
|
|
1292
1829
|
const originalFrames = scanZstdFrames(original).frames;
|
|
1293
1830
|
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");
|
|
1294
1831
|
const rewritten = rewriteFrame0CwdInMemory(original, canonical);
|
|
@@ -1297,23 +1834,23 @@ function apply(ctx) {
|
|
|
1297
1834
|
const originalTail = original.subarray(originalFrames[0].end);
|
|
1298
1835
|
const rewrittenTail = rewritten.subarray(rewrittenFrames[0].end);
|
|
1299
1836
|
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");
|
|
1300
|
-
await
|
|
1301
|
-
await
|
|
1837
|
+
await writeFile5(stagedPath, rewritten, { mode: 384 });
|
|
1838
|
+
await rename5(stagedPath, newPath);
|
|
1302
1839
|
destinationInstalled = true;
|
|
1303
|
-
await
|
|
1840
|
+
await unlink2(backupPath);
|
|
1304
1841
|
} catch (e) {
|
|
1305
1842
|
try {
|
|
1306
|
-
await
|
|
1843
|
+
await unlink2(stagedPath);
|
|
1307
1844
|
} catch (_) {
|
|
1308
1845
|
}
|
|
1309
1846
|
if (destinationInstalled) {
|
|
1310
1847
|
try {
|
|
1311
|
-
await
|
|
1848
|
+
await unlink2(newPath);
|
|
1312
1849
|
} catch (_) {
|
|
1313
1850
|
}
|
|
1314
1851
|
}
|
|
1315
1852
|
try {
|
|
1316
|
-
await
|
|
1853
|
+
await rename5(backupPath, oldPath);
|
|
1317
1854
|
} catch (_) {
|
|
1318
1855
|
}
|
|
1319
1856
|
if (e && e.code !== "ENOENT") throw e;
|
|
@@ -1337,7 +1874,21 @@ function apply(ctx) {
|
|
|
1337
1874
|
}
|
|
1338
1875
|
return null;
|
|
1339
1876
|
};
|
|
1340
|
-
if (
|
|
1877
|
+
if (persistence.kind === "session-handle") {
|
|
1878
|
+
const stat1 = await persistence.statSession(sid);
|
|
1879
|
+
if (stat1 && stat1.revision) {
|
|
1880
|
+
const stat22 = await persistence.statSession(sid);
|
|
1881
|
+
if (stat22 && stat22.revision !== stat1.revision) {
|
|
1882
|
+
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");
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
try {
|
|
1886
|
+
await moveSessionToCwd({ sp, sid, header: meta, canonical, events, inheritedEventCount: r.inheritedEventCount });
|
|
1887
|
+
} catch (e) {
|
|
1888
|
+
if (e && e.status) throw e;
|
|
1889
|
+
throw new Error("\u79FB\u52A8\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A" + String(e && e.message || e));
|
|
1890
|
+
}
|
|
1891
|
+
} else if (isOpen) {
|
|
1341
1892
|
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");
|
|
1342
1893
|
try {
|
|
1343
1894
|
const st = sp.states && sp.states.get && sp.states.get(sid);
|
|
@@ -1359,7 +1910,7 @@ function apply(ctx) {
|
|
|
1359
1910
|
const backupPath = oldPath ? `${oldPath}.move-backup-${Date.now()}` : null;
|
|
1360
1911
|
if (backupPath) {
|
|
1361
1912
|
try {
|
|
1362
|
-
await
|
|
1913
|
+
await rename5(oldPath, backupPath);
|
|
1363
1914
|
} catch (e) {
|
|
1364
1915
|
if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
|
|
1365
1916
|
}
|
|
@@ -1367,7 +1918,7 @@ function apply(ctx) {
|
|
|
1367
1918
|
const restore = async () => {
|
|
1368
1919
|
if (backupPath) {
|
|
1369
1920
|
try {
|
|
1370
|
-
await
|
|
1921
|
+
await rename5(backupPath, oldPath);
|
|
1371
1922
|
} catch (_) {
|
|
1372
1923
|
}
|
|
1373
1924
|
}
|
|
@@ -1381,7 +1932,7 @@ function apply(ctx) {
|
|
|
1381
1932
|
}
|
|
1382
1933
|
if (backupPath) {
|
|
1383
1934
|
try {
|
|
1384
|
-
await
|
|
1935
|
+
await unlink2(backupPath);
|
|
1385
1936
|
} catch (e) {
|
|
1386
1937
|
}
|
|
1387
1938
|
}
|
|
@@ -1476,7 +2027,7 @@ function apply(ctx) {
|
|
|
1476
2027
|
}
|
|
1477
2028
|
return out;
|
|
1478
2029
|
}
|
|
1479
|
-
async function
|
|
2030
|
+
async function allSessionItemsDetailed(opts = {}) {
|
|
1480
2031
|
let entries = [];
|
|
1481
2032
|
let headersOk = false;
|
|
1482
2033
|
try {
|
|
@@ -1486,6 +2037,7 @@ function apply(ctx) {
|
|
|
1486
2037
|
} catch (e) {
|
|
1487
2038
|
entries = [];
|
|
1488
2039
|
}
|
|
2040
|
+
const entryById = new Map(entries.map((entry) => [entry.id, entry]));
|
|
1489
2041
|
let live = ctx.get("sessions");
|
|
1490
2042
|
const ids = entries.map((entry) => entry.id);
|
|
1491
2043
|
if (live) {
|
|
@@ -1500,7 +2052,11 @@ function apply(ctx) {
|
|
|
1500
2052
|
let hiddenIds = /* @__PURE__ */ new Set();
|
|
1501
2053
|
try {
|
|
1502
2054
|
const store = await readTrashStore();
|
|
1503
|
-
|
|
2055
|
+
const present = new Set(ids);
|
|
2056
|
+
hiddenIds = /* @__PURE__ */ new Set([
|
|
2057
|
+
...store.items.map((t) => String(t.sessionId)),
|
|
2058
|
+
...store.purgedSessionIds.map(String).filter((id) => !present.has(id))
|
|
2059
|
+
]);
|
|
1504
2060
|
} catch (e) {
|
|
1505
2061
|
}
|
|
1506
2062
|
const visibleIds = ids.filter((id) => !hiddenIds.has(id));
|
|
@@ -1513,8 +2069,11 @@ function apply(ctx) {
|
|
|
1513
2069
|
const currentArchived = new Set((await archivedState().catch(() => ({ archivedSessionIds: [] }))).archivedSessionIds || []);
|
|
1514
2070
|
const items = [];
|
|
1515
2071
|
const usage = await collectUsage(entries);
|
|
1516
|
-
const statsById = new Map(visibleIds.map((id) => [
|
|
1517
|
-
|
|
2072
|
+
const statsById = new Map(visibleIds.map((id) => [
|
|
2073
|
+
id,
|
|
2074
|
+
usage.statsById && usage.statsById.get(id) || { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }
|
|
2075
|
+
]));
|
|
2076
|
+
const { missing } = metaCache.partition(visibleIds, statsById);
|
|
1518
2077
|
const persisted = await hydrateFromPersist(missing, statsById);
|
|
1519
2078
|
for (const [id, meta] of persisted) metaCache.set(id, statsById.get(id), meta);
|
|
1520
2079
|
const stillMissing = missing.filter((id) => !persisted.has(id));
|
|
@@ -1525,11 +2084,15 @@ function apply(ctx) {
|
|
|
1525
2084
|
};
|
|
1526
2085
|
const CHUNK = 6;
|
|
1527
2086
|
for (let i = 0; i < visibleIds.length; i += CHUNK) {
|
|
1528
|
-
const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map((id) =>
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
2087
|
+
const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map((id) => {
|
|
2088
|
+
const entry = entryById.get(id);
|
|
2089
|
+
return resolveOne(id, usage, {
|
|
2090
|
+
exposeUsage: !!(opts && opts.usage),
|
|
2091
|
+
listHeader: entry ? entry.header : null,
|
|
2092
|
+
preloaded: snapshotById.has(id) ? snapshotById.get(id) : void 0,
|
|
2093
|
+
collectDecoded
|
|
2094
|
+
});
|
|
2095
|
+
}));
|
|
1533
2096
|
for (const it of res2) items.push({ ...it, archived: currentArchived.has(it.sessionId) });
|
|
1534
2097
|
}
|
|
1535
2098
|
persistDecoded(decoded, statsById);
|
|
@@ -1540,7 +2103,10 @@ function apply(ctx) {
|
|
|
1540
2103
|
}
|
|
1541
2104
|
for (const it of items) it.starred = starredSet.has(String(it.sessionId));
|
|
1542
2105
|
if (headersOk) await gcStars(ids);
|
|
1543
|
-
return items;
|
|
2106
|
+
return { items, usage };
|
|
2107
|
+
}
|
|
2108
|
+
async function allSessionItems(opts = {}) {
|
|
2109
|
+
return (await allSessionItemsDetailed(opts)).items;
|
|
1544
2110
|
}
|
|
1545
2111
|
async function buildStorage(opts = {}) {
|
|
1546
2112
|
const items = await allSessionItems({ usage: true });
|
|
@@ -1556,7 +2122,15 @@ function apply(ctx) {
|
|
|
1556
2122
|
if (!(opts && opts.force) && autoArchive.isFresh(store, now)) {
|
|
1557
2123
|
return { ok: true, skipped: "throttled", archived: 0, lastRunAt: store.lastRunAt, lastArchivedCount: store.lastArchivedCount };
|
|
1558
2124
|
}
|
|
1559
|
-
const items = await
|
|
2125
|
+
const { items, usage } = await allSessionItemsDetailed({ usage: true });
|
|
2126
|
+
if (!usage.hasActivityData) {
|
|
2127
|
+
return {
|
|
2128
|
+
ok: true,
|
|
2129
|
+
skipped: "no-activity-data",
|
|
2130
|
+
archived: 0,
|
|
2131
|
+
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"
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
1560
2134
|
const candidates = pickInactiveCandidates(items, {
|
|
1561
2135
|
inactiveDays: days,
|
|
1562
2136
|
skipStarred: store.settings.skipStarred,
|
|
@@ -1595,9 +2169,14 @@ function apply(ctx) {
|
|
|
1595
2169
|
} catch (e) {
|
|
1596
2170
|
}
|
|
1597
2171
|
const store = await readTrashStore();
|
|
2172
|
+
const present = new Set(ids);
|
|
2173
|
+
const activeTombstones = store.purgedSessionIds.map(String).filter((id) => !present.has(id));
|
|
1598
2174
|
if (ids.length) {
|
|
1599
2175
|
const usage = await collectUsage(entries);
|
|
1600
|
-
const statsById = new Map(ids.map((id) => [
|
|
2176
|
+
const statsById = new Map(ids.map((id) => [
|
|
2177
|
+
id,
|
|
2178
|
+
usage.statsById && usage.statsById.get(id) || { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }
|
|
2179
|
+
]));
|
|
1601
2180
|
const { cached, missing } = metaCache.partition(ids, statsById);
|
|
1602
2181
|
const persisted = await hydrateFromPersist(missing, statsById);
|
|
1603
2182
|
for (const [id, meta] of persisted) metaCache.set(id, statsById.get(id), meta);
|
|
@@ -1610,8 +2189,13 @@ function apply(ctx) {
|
|
|
1610
2189
|
for (const id of ids) {
|
|
1611
2190
|
let meta = cached.get(id) || persisted.get(id) || null;
|
|
1612
2191
|
if (!meta) {
|
|
2192
|
+
const entry = entries.find((e) => e.id === id);
|
|
1613
2193
|
const snapshot = snapshotById.has(id) ? snapshotById.get(id) : typeof sq.readTitleSnapshot === "function" ? await sq.readTitleSnapshot(id).catch(() => null) : null;
|
|
1614
2194
|
const next = metaFromSnapshot(snapshot);
|
|
2195
|
+
if (entry && entry.header) {
|
|
2196
|
+
if (!next.cwd && typeof entry.header.cwd === "string") next.cwd = entry.header.cwd;
|
|
2197
|
+
if (!next.createdAt && entry.header.createdAt != null) next.createdAt = entry.header.createdAt;
|
|
2198
|
+
}
|
|
1615
2199
|
metaCache.set(id, statsById.get(id), next);
|
|
1616
2200
|
if (statsById.get(id)) collectDecoded(id, next);
|
|
1617
2201
|
meta = next;
|
|
@@ -1623,38 +2207,14 @@ function apply(ctx) {
|
|
|
1623
2207
|
return {
|
|
1624
2208
|
titles: Object.fromEntries(authorityTitleCache),
|
|
1625
2209
|
trashedSessionIds: store.items.map((item) => String(item.sessionId)),
|
|
1626
|
-
purgedSessionIds:
|
|
2210
|
+
purgedSessionIds: activeTombstones
|
|
1627
2211
|
};
|
|
1628
2212
|
}
|
|
1629
|
-
async function buildDetails(sid) {
|
|
2213
|
+
async function buildDetails(sid, signal) {
|
|
1630
2214
|
const sessions = ctx.get("sessions");
|
|
1631
2215
|
const live = sessions && sessions.get(sid);
|
|
1632
2216
|
let meta = null;
|
|
1633
|
-
let
|
|
1634
|
-
if (live !== void 0) {
|
|
1635
|
-
meta = live && live.header || null;
|
|
1636
|
-
try {
|
|
1637
|
-
events = Array.isArray(live.events) ? [...live.events] : [];
|
|
1638
|
-
} catch (e) {
|
|
1639
|
-
events = [];
|
|
1640
|
-
}
|
|
1641
|
-
} else {
|
|
1642
|
-
const r = await persistence.readSession(sid, 0);
|
|
1643
|
-
if (!r || !r.meta) throw new Error("\u627E\u4E0D\u5230\u8BE5\u4F1A\u8BDD\u7684\u8BB0\u5F55\uFF08\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF09");
|
|
1644
|
-
meta = r.meta;
|
|
1645
|
-
events = Array.isArray(r.events) ? r.events : [];
|
|
1646
|
-
}
|
|
1647
|
-
let sizeBytes = null;
|
|
1648
|
-
try {
|
|
1649
|
-
const loc = persistence.locate(meta);
|
|
1650
|
-
if (loc && typeof loc.path === "string" && loc.path) {
|
|
1651
|
-
const st = await stat(loc.path);
|
|
1652
|
-
if (st && typeof st.size === "number") sizeBytes = st.size;
|
|
1653
|
-
}
|
|
1654
|
-
} catch (e) {
|
|
1655
|
-
sizeBytes = null;
|
|
1656
|
-
}
|
|
1657
|
-
let lastTime = typeof meta.createdAt === "number" ? meta.createdAt : 0;
|
|
2217
|
+
let lastTime = 0;
|
|
1658
2218
|
const fileSet = /* @__PURE__ */ new Map();
|
|
1659
2219
|
const stats = {
|
|
1660
2220
|
turns: 0,
|
|
@@ -1668,7 +2228,7 @@ function apply(ctx) {
|
|
|
1668
2228
|
};
|
|
1669
2229
|
const turnSeen = /* @__PURE__ */ new Set();
|
|
1670
2230
|
const stepSeen = /* @__PURE__ */ new Set();
|
|
1671
|
-
|
|
2231
|
+
const absorb = (ev) => {
|
|
1672
2232
|
if (ev && typeof ev.time === "number" && ev.time > lastTime) lastTime = ev.time;
|
|
1673
2233
|
const d = ev && ev.data && typeof ev.data === "object" ? ev.data : {};
|
|
1674
2234
|
const type = ev && ev.type;
|
|
@@ -1715,12 +2275,44 @@ function apply(ctx) {
|
|
|
1715
2275
|
break;
|
|
1716
2276
|
}
|
|
1717
2277
|
}
|
|
2278
|
+
};
|
|
2279
|
+
if (live !== void 0) {
|
|
2280
|
+
meta = live && live.header || null;
|
|
2281
|
+
try {
|
|
2282
|
+
(Array.isArray(live.events) ? [...live.events] : []).forEach(absorb);
|
|
2283
|
+
} catch (e) {
|
|
2284
|
+
}
|
|
2285
|
+
} else {
|
|
2286
|
+
const summary = await persistence.inspectSession(sid, { signal, onEvents: (batch) => {
|
|
2287
|
+
for (const ev of batch) absorb(ev);
|
|
2288
|
+
} });
|
|
2289
|
+
if (!summary || !summary.meta) throw new Error("\u627E\u4E0D\u5230\u8BE5\u4F1A\u8BDD\u7684\u8BB0\u5F55\uFF08\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF09");
|
|
2290
|
+
meta = summary.meta;
|
|
1718
2291
|
}
|
|
1719
2292
|
stats.turns = turnSeen.size;
|
|
1720
2293
|
stats.steps = stepSeen.size;
|
|
2294
|
+
let sizeBytes = null;
|
|
2295
|
+
if (live === void 0) {
|
|
2296
|
+
try {
|
|
2297
|
+
const stat3 = await persistence.statSession(sid);
|
|
2298
|
+
if (stat3 && Number.isFinite(stat3.sizeBytes)) sizeBytes = stat3.sizeBytes;
|
|
2299
|
+
} catch (e) {
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
if (sizeBytes === null) {
|
|
2303
|
+
try {
|
|
2304
|
+
const loc = persistence.locate(meta);
|
|
2305
|
+
if (loc && typeof loc.path === "string" && loc.path) {
|
|
2306
|
+
const st = await stat2(loc.path);
|
|
2307
|
+
if (st && typeof st.size === "number") sizeBytes = st.size;
|
|
2308
|
+
}
|
|
2309
|
+
} catch (e) {
|
|
2310
|
+
sizeBytes = null;
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
1721
2313
|
if (stats.fetches.length > MAX_FETCHES) stats.fetches = stats.fetches.slice(0, MAX_FETCHES);
|
|
1722
2314
|
const fileEntries = [...fileSet.entries()].slice(0, MAX_FILES * 2);
|
|
1723
|
-
const exists = await Promise.all(fileEntries.map(([p]) =>
|
|
2315
|
+
const exists = await Promise.all(fileEntries.map(([p]) => stat2(p).then(() => true).catch(() => false)));
|
|
1724
2316
|
const files = fileEntries.filter((_, i) => exists[i]).map(([path, tool]) => ({ path, tool })).slice(0, MAX_FILES);
|
|
1725
2317
|
const lineage = {
|
|
1726
2318
|
parentSessionId: meta && typeof meta.parentSession === "string" ? meta.parentSession : null,
|
|
@@ -1756,7 +2348,7 @@ function apply(ctx) {
|
|
|
1756
2348
|
sessionId: sid,
|
|
1757
2349
|
sizeBytes,
|
|
1758
2350
|
createdAt: meta && typeof meta.createdAt === "number" ? meta.createdAt : null,
|
|
1759
|
-
updatedAt: lastTime || null,
|
|
2351
|
+
updatedAt: Math.max(lastTime || 0, meta && typeof meta.createdAt === "number" ? meta.createdAt : 0) || null,
|
|
1760
2352
|
files,
|
|
1761
2353
|
stats,
|
|
1762
2354
|
lineage
|
|
@@ -1789,7 +2381,13 @@ function apply(ctx) {
|
|
|
1789
2381
|
} catch (e) {
|
|
1790
2382
|
}
|
|
1791
2383
|
const trashStore = await readTrashStore();
|
|
1792
|
-
const
|
|
2384
|
+
const present = /* @__PURE__ */ new Set([
|
|
2385
|
+
...materialized,
|
|
2386
|
+
...ids.map(String),
|
|
2387
|
+
...live && typeof live.list === "function" ? live.list().map((s) => String(s.id)) : []
|
|
2388
|
+
]);
|
|
2389
|
+
const tombstones = trashStore.purgedSessionIds.map(String).filter((id) => !present.has(id));
|
|
2390
|
+
const hidden = /* @__PURE__ */ new Set([...trashStore.items.map((item) => String(item.sessionId)), ...tombstones]);
|
|
1793
2391
|
const idStrs = ids.map(String).filter((id) => !hidden.has(id) && (materialized.has(id) || live && live.get(id)));
|
|
1794
2392
|
wsByPath = {};
|
|
1795
2393
|
try {
|
|
@@ -1922,7 +2520,7 @@ function apply(ctx) {
|
|
|
1922
2520
|
if (typeof item.originalPath !== "string" || !item.originalPath) {
|
|
1923
2521
|
return { sessionId: item.sessionId, status: "unverified", originalPath: null };
|
|
1924
2522
|
}
|
|
1925
|
-
const exists = await
|
|
2523
|
+
const exists = await stat2(item.originalPath).then(() => true).catch(() => false);
|
|
1926
2524
|
return { sessionId: item.sessionId, status: exists ? "ok" : "missing", originalPath: item.originalPath };
|
|
1927
2525
|
}));
|
|
1928
2526
|
json(res, {
|
|
@@ -2026,6 +2624,7 @@ function apply(ctx) {
|
|
|
2026
2624
|
}
|
|
2027
2625
|
}
|
|
2028
2626
|
}));
|
|
2627
|
+
const exportLimiter = createLimiter(2);
|
|
2029
2628
|
disposers.push(ctx.webServer.register({
|
|
2030
2629
|
kind: "exact",
|
|
2031
2630
|
path: "/archived-sessions/export-md",
|
|
@@ -2034,13 +2633,23 @@ function apply(ctx) {
|
|
|
2034
2633
|
const url = new URL(req.url, "http://localhost");
|
|
2035
2634
|
const sid = url.searchParams.get("sessionId");
|
|
2036
2635
|
requireSessionId(sid);
|
|
2037
|
-
const
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2636
|
+
const ac = new AbortController();
|
|
2637
|
+
res.on("close", () => {
|
|
2638
|
+
if (!res.writableEnded) ac.abort();
|
|
2639
|
+
});
|
|
2640
|
+
const md = await exportLimiter(async () => {
|
|
2641
|
+
const builder = createSessionMarkdownBuilder({ id: sid });
|
|
2642
|
+
const summary = await persistence.inspectSession(sid, {
|
|
2643
|
+
signal: ac.signal,
|
|
2644
|
+
onEvents: (batch) => builder.addEvents(batch)
|
|
2645
|
+
});
|
|
2646
|
+
if (!summary || !summary.meta) {
|
|
2647
|
+
const error = new Error("\u65E0\u6CD5\u8BFB\u53D6\u8BE5\u4F1A\u8BDD\u7684\u65E5\u5FD7");
|
|
2648
|
+
error.status = 404;
|
|
2649
|
+
throw error;
|
|
2650
|
+
}
|
|
2651
|
+
return builder.finish({ ...summary.meta, id: sid });
|
|
2652
|
+
});
|
|
2044
2653
|
res.writeHead(200, {
|
|
2045
2654
|
"content-type": "text/markdown; charset=utf-8",
|
|
2046
2655
|
"content-disposition": `attachment; filename="dsh-session-${sid}.md"`,
|
|
@@ -2140,7 +2749,11 @@ function apply(ctx) {
|
|
|
2140
2749
|
const body = await readJsonBody(req);
|
|
2141
2750
|
const sid = body && typeof body.sessionId === "string" ? body.sessionId : null;
|
|
2142
2751
|
if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
|
|
2143
|
-
|
|
2752
|
+
const ac = new AbortController();
|
|
2753
|
+
res.on("close", () => {
|
|
2754
|
+
if (!res.writableEnded) ac.abort();
|
|
2755
|
+
});
|
|
2756
|
+
json(res, await buildDetails(sid, ac.signal));
|
|
2144
2757
|
} catch (e) {
|
|
2145
2758
|
json(res, { error: String(e && e.message || e) }, e && e.status ? e.status : 500);
|
|
2146
2759
|
}
|