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/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.js
2
- import { mkdir as mkdir4, readFile as readFile2, 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";
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 renderSessionMarkdown(meta, events, options = {}) {
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
- for (const ev of list) {
164
- if (!ev || typeof ev !== "object") continue;
165
- const data = ev.data && typeof ev.data === "object" ? ev.data : {};
166
- const type = ev.type;
167
- if (type === "turn/start") {
168
- const next = Number.isInteger(data.turn) ? data.turn : null;
169
- if (next !== null && next !== turn) {
170
- turn = next;
171
- out.push("", `## \u7B2C ${turn} \u8F6E`);
172
- }
173
- continue;
174
- }
175
- if (type === "user/message") {
176
- const blocks = blocksOf(data.content);
177
- const text = textFromBlocks(blocks);
178
- const images = imageCountOf(blocks);
179
- if (!text && images === 0) continue;
180
- out.push("", "### \u7528\u6237", "");
181
- if (text) out.push(text);
182
- for (let i = 0; i < images; i++) out.push("", `![\u56FE\u7247 ${i + 1}](attachment)`);
183
- continue;
184
- }
185
- if (type === "assistant/message") {
186
- const message = data.message && typeof data.message === "object" ? data.message : {};
187
- const blocks = blocksOf(message.content);
188
- const text = textFromBlocks(blocks);
189
- const reasoning = includeReasoning ? reasoningFromBlocks(blocks) : "";
190
- if (!text && !reasoning) continue;
191
- out.push("", "### \u52A9\u624B", "");
192
- if (reasoning) out.push("> \u601D\u8003\uFF1A" + reasoning.split("\n").join("\n> "), "");
193
- if (text) out.push(text);
194
- continue;
195
- }
196
- if (type === "tool/call") {
197
- const name2 = typeof data.name === "string" && data.name ? data.name : "tool";
198
- const summary = summarizeToolArguments(name2, data.arguments);
199
- out.push("", `### \u5DE5\u5177\u8C03\u7528\uFF1A\`${name2}\``, "");
200
- out.push(summary ? "```\n" + summary + "\n```" : "\uFF08\u65E0\u53C2\u6570\uFF09");
201
- continue;
202
- }
203
- if (type === "tool/result" && includeToolResults) {
204
- const message = data.message && typeof data.message === "object" ? data.message : {};
205
- const blocks = blocksOf(message.content);
206
- let text = "";
207
- for (const block of blocks) {
208
- if (block.type === "tool-result") text = textFromBlocks(blocksOf(block.content));
209
- }
210
- if (text) out.push("", "<details><summary>\u5DE5\u5177\u7ED3\u679C</summary>", "", "```\n" + text.slice(0, 2e3) + "\n```", "", "</details>");
211
- }
212
- }
213
- out.push("");
214
- 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
+ };
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
- function fingerprintOf(stat2) {
452
- if (!stat2 || typeof stat2 !== "object") return null;
453
- const mtimeMs = stat2.mtimeMs;
454
- 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;
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 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) {
460
486
  if (!entry) return false;
461
- const fp = fingerprintOf(stat2);
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 stat2 = statsById && statsById.get(String(id));
473
- 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) {
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, stat2) {
516
+ get(id, stat3) {
490
517
  const key = String(id);
491
518
  const entry = map.get(key);
492
- if (isFresh(entry, stat2, Date.now(), ttlMs)) {
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, stat2, meta) {
528
+ set(id, stat3, meta) {
502
529
  if (!meta) return null;
503
- const fp = fingerprintOf(stat2);
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 || !title && !cwd) return null;
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 readSession(id, offset = 0) {
670
- if (typeof service.readFrom === "function") {
671
- const result = await service.readFrom(id, offset);
672
- return {
673
- meta: result && result.meta ? result.meta : null,
674
- inheritedEventCount: result && Number.isSafeInteger(result.inheritedEventCount) ? result.inheritedEventCount : 0,
675
- events: result && Array.isArray(result.events) ? result.events : []
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
- try {
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
- const events = await handle.read(offset);
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: handle.header || handle.meta || null,
691
- inheritedEventCount: Number.isSafeInteger(handle.inheritedEventCount) ? handle.inheritedEventCount : 0,
692
- events: Array.isArray(events) ? 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
- const kind = typeof service.open === "function" ? "session-handle" : "legacy";
703
- return { kind, listEntries, readSession, locate };
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 || join4(homedir3(), ".dsh", "sessions-manager-trash");
741
- 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");
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: join4(TRASH_DIR, "title-index.json") });
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 stat2 = statsById.get(id);
1241
+ const stat3 = statsById.get(id);
840
1242
  const entry = store && store[id];
841
- if (!stat2 || !entry) continue;
842
- const fp = fingerprintOf(stat2);
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
- 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;
940
1350
  } else if (typeof sq.readTitleSnapshot === "function") {
941
1351
  try {
942
- 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;
943
1356
  } catch (e) {
944
1357
  }
945
1358
  }
946
- if (!meta.title || !meta.cwd) {
1359
+ const projectionAvailable = typeof sq.readTitleSnapshot === "function" || typeof sq.readTitleSnapshots === "function";
1360
+ if (!meta.cwd || !meta.title && !projectionAvailable) {
947
1361
  try {
948
- const r = await persistence.readSession(id, 0);
949
- if (r.meta) {
950
- if (!meta.cwd) meta.cwd = r.meta.cwd || null;
951
- 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;
952
1371
  }
953
- if (!meta.title && Array.isArray(r.events)) meta.title = foldTitle(r.events);
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(preloadedHeaders) {
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(preloadedHeaders)) entries = preloadedHeaders;
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 stat(loc.path);
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) 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
+ }
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 mkdir4(TRASH_DIR, { recursive: true });
1022
- const tmp = join4(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
1023
- await writeFile4(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
1024
- 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);
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
- const r = await persistence.readSession(sid, 0);
1068
- if (r && r.meta) {
1069
- if (!cwd) cwd = r.meta.cwd;
1070
- 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) {
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
- 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");
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
- const located = current && persistence.locate(current.header);
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 = target && (basename(dirname2(target)) === sid || basename(target).includes(sid));
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 unlink(target);
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 = join4(homedir3(), p.slice(2));
1234
- if (!isAbsolute(p)) p = join4(homedir3(), 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 mkdir4(p, { recursive: true });
1779
+ await mkdir5(p, { recursive: true });
1243
1780
  canonical = await realpath(p);
1244
1781
  }
1245
- return { canonical, entity: await w.create(canonical, basename(canonical) || "workspace") };
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 mkdir4(dirname2(newPath), { recursive: true });
1820
+ await mkdir5(dirname2(newPath), { recursive: true });
1284
1821
  try {
1285
- await stat(newPath);
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 rename4(oldPath, backupPath);
1291
- const original = await readFile2(backupPath);
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 writeFile4(stagedPath, rewritten, { mode: 384 });
1301
- await rename4(stagedPath, newPath);
1837
+ await writeFile5(stagedPath, rewritten, { mode: 384 });
1838
+ await rename5(stagedPath, newPath);
1302
1839
  destinationInstalled = true;
1303
- await unlink(backupPath);
1840
+ await unlink2(backupPath);
1304
1841
  } catch (e) {
1305
1842
  try {
1306
- await unlink(stagedPath);
1843
+ await unlink2(stagedPath);
1307
1844
  } catch (_) {
1308
1845
  }
1309
1846
  if (destinationInstalled) {
1310
1847
  try {
1311
- await unlink(newPath);
1848
+ await unlink2(newPath);
1312
1849
  } catch (_) {
1313
1850
  }
1314
1851
  }
1315
1852
  try {
1316
- await rename4(backupPath, oldPath);
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 (isOpen) {
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 rename4(oldPath, backupPath);
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 rename4(backupPath, oldPath);
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 unlink(backupPath);
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 allSessionItems(opts = {}) {
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
- hiddenIds = /* @__PURE__ */ new Set([...store.items.map((t) => String(t.sessionId)), ...store.purgedSessionIds.map(String)]);
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) => [id, { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }]));
1517
- const { cached, missing } = metaCache.partition(visibleIds, statsById);
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) => resolveOne(id, usage, {
1529
- exposeUsage: !!(opts && opts.usage),
1530
- preloaded: snapshotById.has(id) ? snapshotById.get(id) : void 0,
1531
- collectDecoded
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 allSessionItems({ usage: true });
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) => [id, { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(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: store.purgedSessionIds.map(String)
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 events = [];
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
- for (const ev of events) {
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]) => stat(p).then(() => true).catch(() => false)));
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 hidden = /* @__PURE__ */ new Set([...trashStore.items.map((item) => String(item.sessionId)), ...trashStore.purgedSessionIds.map(String)]);
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 stat(item.originalPath).then(() => true).catch(() => false);
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 r = await persistence.readSession(sid, 0);
2038
- if (!r || !r.meta) {
2039
- const error = new Error("\u65E0\u6CD5\u8BFB\u53D6\u8BE5\u4F1A\u8BDD\u7684\u65E5\u5FD7");
2040
- error.status = 404;
2041
- throw error;
2042
- }
2043
- const md = renderSessionMarkdown({ ...r.meta, id: sid }, r.events || []);
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
- json(res, await buildDetails(sid));
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
  }