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/lib/index.js CHANGED
@@ -1,32 +1,67 @@
1
1
  // src/index.js
2
- import { mkdir as mkdir4, realpath, rename as rename4, stat, unlink, writeFile as writeFile4 } from "node:fs/promises";
3
- import { basename, dirname as dirname2, isAbsolute, join as join4 } from "node:path";
4
- import { readFileSync as readFileSync4 } from "node:fs";
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 findZstdFrameStarts(buf) {
13
- const starts = [];
14
- for (let i = 0; i + 4 <= buf.length; i++) {
15
- if (buf.readUInt32LE(i) !== ZSTD_MAGIC) continue;
16
- try {
17
- const out = zlib.zstdDecompressSync(buf.subarray(i, i + Math.min(buf.length - i, 1e6)));
18
- if (out.length > 0) starts.push(i);
19
- } catch (_) {
20
- }
21
- }
22
- return starts;
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 rewriteFrame0Cwd(filePath, newCwd) {
25
- const buf = readFileSync(filePath);
26
- const starts = findZstdFrameStarts(buf);
27
- if (starts.length === 0) throw new Error("\u4F1A\u8BDD\u65E5\u5FD7\u683C\u5F0F\u5F02\u5E38\uFF08\u65E0 zstd \u5E27\uFF09");
28
- const end0 = starts.length > 1 ? starts[1] : buf.length;
29
- const frame0 = buf.subarray(starts[0], end0);
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
- writeFileSync(filePath, Buffer.concat([newFrame0, rest]));
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 renderSessionMarkdown(meta, events, options = {}) {
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
- for (const ev of list) {
130
- if (!ev || typeof ev !== "object") continue;
131
- const data = ev.data && typeof ev.data === "object" ? ev.data : {};
132
- const type = ev.type;
133
- if (type === "turn/start") {
134
- const next = Number.isInteger(data.turn) ? data.turn : null;
135
- if (next !== null && next !== turn) {
136
- turn = next;
137
- out.push("", `## \u7B2C ${turn} \u8F6E`);
138
- }
139
- continue;
140
- }
141
- if (type === "user/message") {
142
- const blocks = blocksOf(data.content);
143
- const text = textFromBlocks(blocks);
144
- const images = imageCountOf(blocks);
145
- if (!text && images === 0) continue;
146
- out.push("", "### \u7528\u6237", "");
147
- if (text) out.push(text);
148
- for (let i = 0; i < images; i++) out.push("", `![\u56FE\u7247 ${i + 1}](attachment)`);
149
- continue;
150
- }
151
- if (type === "assistant/message") {
152
- const message = data.message && typeof data.message === "object" ? data.message : {};
153
- const blocks = blocksOf(message.content);
154
- const text = textFromBlocks(blocks);
155
- const reasoning = includeReasoning ? reasoningFromBlocks(blocks) : "";
156
- if (!text && !reasoning) continue;
157
- out.push("", "### \u52A9\u624B", "");
158
- if (reasoning) out.push("> \u601D\u8003\uFF1A" + reasoning.split("\n").join("\n> "), "");
159
- if (text) out.push(text);
160
- continue;
161
- }
162
- if (type === "tool/call") {
163
- const name2 = typeof data.name === "string" && data.name ? data.name : "tool";
164
- const summary = summarizeToolArguments(name2, data.arguments);
165
- out.push("", `### \u5DE5\u5177\u8C03\u7528\uFF1A\`${name2}\``, "");
166
- out.push(summary ? "```\n" + summary + "\n```" : "\uFF08\u65E0\u53C2\u6570\uFF09");
167
- continue;
168
- }
169
- if (type === "tool/result" && includeToolResults) {
170
- const message = data.message && typeof data.message === "object" ? data.message : {};
171
- const blocks = blocksOf(message.content);
172
- let text = "";
173
- for (const block of blocks) {
174
- if (block.type === "tool-result") text = textFromBlocks(blocksOf(block.content));
175
- }
176
- if (text) out.push("", "<details><summary>\u5DE5\u5177\u7ED3\u679C</summary>", "", "```\n" + text.slice(0, 2e3) + "\n```", "", "</details>");
177
- }
178
- }
179
- out.push("");
180
- return out.join("\n");
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("", `![\u56FE\u7247 ${i + 1}](attachment)`);
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 as readFileSync2 } from "node:fs";
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(readFileSync2(indexPath, "utf8")));
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 readFileSync3 } from "node:fs";
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(readFileSync3(indexPath, "utf8")));
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
- function fingerprintOf(stat2) {
418
- if (!stat2 || typeof stat2 !== "object") return null;
419
- const mtimeMs = stat2.mtimeMs;
420
- const size = stat2.size;
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 isFresh(entry, stat2, now, ttlMs = DEFAULT_TTL_MS) {
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(stat2);
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 stat2 = statsById && statsById.get(String(id));
439
- if (isFresh(entry, stat2, now, ttlMs) && entry && entry.meta) {
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, stat2) {
516
+ get(id, stat3) {
456
517
  const key = String(id);
457
518
  const entry = map.get(key);
458
- if (isFresh(entry, stat2, Date.now(), ttlMs)) {
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, stat2, meta) {
528
+ set(id, stat3, meta) {
468
529
  if (!meta) return null;
469
- const fp = fingerprintOf(stat2);
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 || !title && !cwd) return null;
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 readSession(id, offset = 0) {
636
- if (typeof service.readFrom === "function") {
637
- const result = await service.readFrom(id, offset);
638
- return {
639
- meta: result && result.meta ? result.meta : null,
640
- inheritedEventCount: result && Number.isSafeInteger(result.inheritedEventCount) ? result.inheritedEventCount : 0,
641
- events: result && Array.isArray(result.events) ? result.events : []
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
- try {
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
- const events = await handle.read(offset);
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: handle.header || handle.meta || null,
657
- inheritedEventCount: Number.isSafeInteger(handle.inheritedEventCount) ? handle.inheritedEventCount : 0,
658
- events: Array.isArray(events) ? 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
- const kind = typeof service.open === "function" ? "session-handle" : "legacy";
669
- return { kind, listEntries, readSession, locate };
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 || join4(homedir3(), ".dsh", "sessions-manager-trash");
707
- var TRASH_INDEX = join4(TRASH_DIR, "index.json");
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: join4(TRASH_DIR, "title-index.json") });
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 stat2 = statsById.get(id);
1241
+ const stat3 = statsById.get(id);
806
1242
  const entry = store && store[id];
807
- if (!stat2 || !entry) continue;
808
- const fp = fingerprintOf(stat2);
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
- meta = metaFromSnapshot(unwrapSnapshot(opts.preloaded));
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
- meta = metaFromSnapshot(await sq.readTitleSnapshot(id));
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
- if (!meta.title || !meta.cwd) {
1359
+ const projectionAvailable = typeof sq.readTitleSnapshot === "function" || typeof sq.readTitleSnapshots === "function";
1360
+ if (!meta.cwd || !meta.title && !projectionAvailable) {
913
1361
  try {
914
- const r = await persistence.readSession(id, 0);
915
- if (r.meta) {
916
- if (!meta.cwd) meta.cwd = r.meta.cwd || null;
917
- if (!meta.createdAt) meta.createdAt = r.meta.createdAt || null;
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 && Array.isArray(r.events)) meta.title = foldTitle(r.events);
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(preloadedHeaders) {
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(preloadedHeaders)) entries = preloadedHeaders;
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 stat(loc.path);
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) mtimeById.set(id, Math.floor(st.mtimeMs));
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(readFileSync4(TRASH_INDEX, "utf8")));
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 mkdir4(TRASH_DIR, { recursive: true });
988
- const tmp = join4(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
989
- await writeFile4(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
990
- await rename4(tmp, TRASH_INDEX);
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
- const r = await persistence.readSession(sid, 0);
1034
- if (r && r.meta) {
1035
- if (!cwd) cwd = r.meta.cwd;
1036
- title = foldTitle(r.events);
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
- const error = new Error("\u56DE\u6536\u7AD9\u4E2D\u627E\u4E0D\u5230\u8BE5\u4F1A\u8BDD");
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 = target && (basename(dirname2(target)) === sid || basename(target).includes(sid));
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 unlink(target);
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 = join4(homedir3(), p.slice(2));
1200
- if (!isAbsolute(p)) p = join4(homedir3(), 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 mkdir4(p, { recursive: true });
1766
+ await mkdir5(p, { recursive: true });
1209
1767
  canonical = await realpath(p);
1210
1768
  }
1211
- return { canonical, entity: await w.create(canonical, basename(canonical) || "workspace") };
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 mkdir4(dirname2(newPath), { recursive: true });
1248
- await rename4(oldPath, backupPath);
1249
- await rewriteFrame0Cwd(backupPath, canonical);
1250
- await rename4(backupPath, newPath);
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 rename4(backupPath, oldPath);
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 (isOpen) {
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 rename4(oldPath, backupPath);
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 rename4(backupPath, oldPath);
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 unlink(backupPath);
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 allSessionItems(opts = {}) {
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
- hiddenIds = /* @__PURE__ */ new Set([...store.items.map((t) => String(t.sessionId)), ...store.purgedSessionIds.map(String)]);
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) => [id, { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }]));
1454
- const { cached, missing } = metaCache.partition(visibleIds, statsById);
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) => resolveOne(id, usage, {
1466
- exposeUsage: !!(opts && opts.usage),
1467
- preloaded: snapshotById.has(id) ? snapshotById.get(id) : void 0,
1468
- collectDecoded
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 allSessionItems({ usage: true });
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) => [id, { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(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: store.purgedSessionIds.map(String)
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 events = [];
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
- for (const ev of events) {
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]) => stat(p).then(() => true).catch(() => false)));
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 hidden = /* @__PURE__ */ new Set([...trashStore.items.map((item) => String(item.sessionId)), ...trashStore.purgedSessionIds.map(String)]);
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 stat(item.originalPath).then(() => true).catch(() => false);
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 r = await persistence.readSession(sid, 0);
1975
- if (!r || !r.meta) {
1976
- const error = new Error("\u65E0\u6CD5\u8BFB\u53D6\u8BE5\u4F1A\u8BDD\u7684\u65E5\u5FD7");
1977
- error.status = 404;
1978
- throw error;
1979
- }
1980
- const md = renderSessionMarkdown({ ...r.meta, id: sid }, r.events || []);
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
- json(res, await buildDetails(sid));
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
  }