dsh-sessions-manager 3.5.1 → 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,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) {
@@ -639,6 +667,19 @@ function createTitleIndexStore({ dir, file }) {
639
667
  }
640
668
 
641
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
+ }
642
683
  function asHeader(value) {
643
684
  if (!value || typeof value !== "object") return null;
644
685
  const candidate = value.header && typeof value.header === "object" ? value.header : value;
@@ -654,7 +695,7 @@ function normalizePersistenceEntry(value) {
654
695
  id: String(header.id),
655
696
  sizeBytes: snapshot && Number.isFinite(snapshot.sizeBytes) ? Number(snapshot.sizeBytes) : null,
656
697
  eventCount: snapshot && Number.isSafeInteger(snapshot.eventCount) ? snapshot.eventCount : null,
657
- revision: snapshot ? snapshot.revision : null
698
+ revision: snapshot && typeof snapshot.revision === "string" && snapshot.revision ? snapshot.revision : null
658
699
  };
659
700
  }
660
701
  function normalizePersistenceList(values) {
@@ -663,44 +704,112 @@ function normalizePersistenceList(values) {
663
704
  }
664
705
  function createPersistenceAdapter(service) {
665
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";
666
709
  async function listEntries(options) {
667
710
  return normalizePersistenceList(await service.list(options));
668
711
  }
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
- };
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;
677
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 }) {
678
727
  if (typeof service.open !== "function") throw new Error("\u5F53\u524D DSH \u6301\u4E45\u5316\u670D\u52A1\u4E0D\u652F\u6301\u8BFB\u53D6\u4F1A\u8BDD");
679
728
  const handle = await service.open(id, "read");
680
729
  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
- }
730
+ await closeQuietly(handle);
685
731
  throw new Error("DSH \u8FD4\u56DE\u4E86\u65E0\u6548\u7684 SessionHandle");
686
732
  }
733
+ let cursor = Number.isSafeInteger(offset) && offset >= 0 ? offset : 0;
734
+ let total = 0;
687
735
  try {
688
- 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);
689
781
  return {
690
- meta: handle.header || handle.meta || null,
691
- inheritedEventCount: Number.isSafeInteger(handle.inheritedEventCount) ? handle.inheritedEventCount : 0,
692
- 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 : []
693
785
  };
694
- } finally {
695
- await handle.close();
696
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
+ };
697
796
  }
698
797
  function locate(header) {
699
798
  if (typeof service.locate === "function") return service.locate(header);
700
799
  return null;
701
800
  }
702
- const kind = typeof service.open === "function" ? "session-handle" : "legacy";
703
- 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 };
704
813
  }
705
814
 
706
815
  // src/compat/capabilities.js
@@ -711,17 +820,34 @@ function detectCapabilities({ persistence, workspaceRegistry }) {
711
820
  const handleApi = !!(persistence && typeof persistence.open === "function");
712
821
  const legacyRead = !!(persistence && typeof persistence.readFrom === "function");
713
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;
714
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;
715
848
  return {
716
849
  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
- }
850
+ actions: matrix
725
851
  };
726
852
  }
727
853
  function requireCapability(capabilities, name2) {
@@ -733,12 +859,273 @@ function requireCapability(capabilities, name2) {
733
859
  throw error;
734
860
  }
735
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
+
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);
@@ -1136,7 +1658,7 @@ function apply(ctx) {
1136
1658
  error.status = 409;
1137
1659
  throw error;
1138
1660
  }
1139
- const targetOwnsSession = target && (basename(dirname2(target)) === sid || basename(target).includes(sid));
1661
+ const targetOwnsSession = pathOwnsSession(target, sid);
1140
1662
  if (target && !targetOwnsSession) {
1141
1663
  const error = new Error("\u65E5\u5FD7\u8DEF\u5F84\u4E0E\u4F1A\u8BDD ID \u4E0D\u5339\u914D\uFF0C\u5DF2\u505C\u6B62\u6C38\u4E45\u5220\u9664");
1142
1664
  error.status = 409;
@@ -1158,9 +1680,11 @@ function apply(ctx) {
1158
1680
  error.status = 409;
1159
1681
  throw error;
1160
1682
  }
1161
- if (target) {
1683
+ if (target && persistence.kind === "session-handle" && locatedHeader) {
1684
+ await purgeSessionArtifacts(sp, sid, locatedHeader);
1685
+ } else if (target) {
1162
1686
  try {
1163
- await unlink(target);
1687
+ await unlink2(target);
1164
1688
  } catch (e) {
1165
1689
  if (e && e.code !== "ENOENT") throw new Error("\u5220\u9664\u6587\u4EF6\u5931\u8D25\uFF1A" + String(e && e.message || e));
1166
1690
  }
@@ -1230,8 +1754,8 @@ function apply(ctx) {
1230
1754
  async function moveTargetWorkspace(rawPath) {
1231
1755
  if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
1232
1756
  let p = String(rawPath).trim();
1233
- if (p.startsWith("~/")) p = join4(homedir3(), p.slice(2));
1234
- 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);
1235
1759
  let canonical = null;
1236
1760
  try {
1237
1761
  canonical = await realpath(p);
@@ -1239,10 +1763,10 @@ function apply(ctx) {
1239
1763
  canonical = null;
1240
1764
  }
1241
1765
  if (canonical === null) {
1242
- await mkdir4(p, { recursive: true });
1766
+ await mkdir5(p, { recursive: true });
1243
1767
  canonical = await realpath(p);
1244
1768
  }
1245
- return { canonical, entity: await w.create(canonical, basename(canonical) || "workspace") };
1769
+ return { canonical, entity: await w.create(canonical, basename2(canonical) || "workspace") };
1246
1770
  }
1247
1771
  async function moveOne(sid, targetPath) {
1248
1772
  requireCapability(capabilities, "move");
@@ -1280,15 +1804,15 @@ function apply(ctx) {
1280
1804
  const stagedPath = `${newPath}.move-stage-${process.pid}-${Date.now()}`;
1281
1805
  let destinationInstalled = false;
1282
1806
  try {
1283
- await mkdir4(dirname2(newPath), { recursive: true });
1807
+ await mkdir5(dirname2(newPath), { recursive: true });
1284
1808
  try {
1285
- await stat(newPath);
1809
+ await stat2(newPath);
1286
1810
  throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u76EE\u6807\u4F4D\u7F6E\u5DF2\u5B58\u5728\u540C\u540D\u4F1A\u8BDD\u65E5\u5FD7");
1287
1811
  } catch (e) {
1288
1812
  if (e && e.code !== "ENOENT") throw e;
1289
1813
  }
1290
- await rename4(oldPath, backupPath);
1291
- const original = await readFile2(backupPath);
1814
+ await rename5(oldPath, backupPath);
1815
+ const original = await readFile3(backupPath);
1292
1816
  const originalFrames = scanZstdFrames(original).frames;
1293
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");
1294
1818
  const rewritten = rewriteFrame0CwdInMemory(original, canonical);
@@ -1297,23 +1821,23 @@ function apply(ctx) {
1297
1821
  const originalTail = original.subarray(originalFrames[0].end);
1298
1822
  const rewrittenTail = rewritten.subarray(rewrittenFrames[0].end);
1299
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");
1300
- await writeFile4(stagedPath, rewritten, { mode: 384 });
1301
- await rename4(stagedPath, newPath);
1824
+ await writeFile5(stagedPath, rewritten, { mode: 384 });
1825
+ await rename5(stagedPath, newPath);
1302
1826
  destinationInstalled = true;
1303
- await unlink(backupPath);
1827
+ await unlink2(backupPath);
1304
1828
  } catch (e) {
1305
1829
  try {
1306
- await unlink(stagedPath);
1830
+ await unlink2(stagedPath);
1307
1831
  } catch (_) {
1308
1832
  }
1309
1833
  if (destinationInstalled) {
1310
1834
  try {
1311
- await unlink(newPath);
1835
+ await unlink2(newPath);
1312
1836
  } catch (_) {
1313
1837
  }
1314
1838
  }
1315
1839
  try {
1316
- await rename4(backupPath, oldPath);
1840
+ await rename5(backupPath, oldPath);
1317
1841
  } catch (_) {
1318
1842
  }
1319
1843
  if (e && e.code !== "ENOENT") throw e;
@@ -1337,7 +1861,21 @@ function apply(ctx) {
1337
1861
  }
1338
1862
  return null;
1339
1863
  };
1340
- 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) {
1341
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");
1342
1880
  try {
1343
1881
  const st = sp.states && sp.states.get && sp.states.get(sid);
@@ -1359,7 +1897,7 @@ function apply(ctx) {
1359
1897
  const backupPath = oldPath ? `${oldPath}.move-backup-${Date.now()}` : null;
1360
1898
  if (backupPath) {
1361
1899
  try {
1362
- await rename4(oldPath, backupPath);
1900
+ await rename5(oldPath, backupPath);
1363
1901
  } catch (e) {
1364
1902
  if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
1365
1903
  }
@@ -1367,7 +1905,7 @@ function apply(ctx) {
1367
1905
  const restore = async () => {
1368
1906
  if (backupPath) {
1369
1907
  try {
1370
- await rename4(backupPath, oldPath);
1908
+ await rename5(backupPath, oldPath);
1371
1909
  } catch (_) {
1372
1910
  }
1373
1911
  }
@@ -1381,7 +1919,7 @@ function apply(ctx) {
1381
1919
  }
1382
1920
  if (backupPath) {
1383
1921
  try {
1384
- await unlink(backupPath);
1922
+ await unlink2(backupPath);
1385
1923
  } catch (e) {
1386
1924
  }
1387
1925
  }
@@ -1476,7 +2014,7 @@ function apply(ctx) {
1476
2014
  }
1477
2015
  return out;
1478
2016
  }
1479
- async function allSessionItems(opts = {}) {
2017
+ async function allSessionItemsDetailed(opts = {}) {
1480
2018
  let entries = [];
1481
2019
  let headersOk = false;
1482
2020
  try {
@@ -1486,6 +2024,7 @@ function apply(ctx) {
1486
2024
  } catch (e) {
1487
2025
  entries = [];
1488
2026
  }
2027
+ const entryById = new Map(entries.map((entry) => [entry.id, entry]));
1489
2028
  let live = ctx.get("sessions");
1490
2029
  const ids = entries.map((entry) => entry.id);
1491
2030
  if (live) {
@@ -1500,7 +2039,11 @@ function apply(ctx) {
1500
2039
  let hiddenIds = /* @__PURE__ */ new Set();
1501
2040
  try {
1502
2041
  const store = await readTrashStore();
1503
- 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
+ ]);
1504
2047
  } catch (e) {
1505
2048
  }
1506
2049
  const visibleIds = ids.filter((id) => !hiddenIds.has(id));
@@ -1513,8 +2056,11 @@ function apply(ctx) {
1513
2056
  const currentArchived = new Set((await archivedState().catch(() => ({ archivedSessionIds: [] }))).archivedSessionIds || []);
1514
2057
  const items = [];
1515
2058
  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);
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);
1518
2064
  const persisted = await hydrateFromPersist(missing, statsById);
1519
2065
  for (const [id, meta] of persisted) metaCache.set(id, statsById.get(id), meta);
1520
2066
  const stillMissing = missing.filter((id) => !persisted.has(id));
@@ -1525,11 +2071,15 @@ function apply(ctx) {
1525
2071
  };
1526
2072
  const CHUNK = 6;
1527
2073
  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
- })));
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
+ }));
1533
2083
  for (const it of res2) items.push({ ...it, archived: currentArchived.has(it.sessionId) });
1534
2084
  }
1535
2085
  persistDecoded(decoded, statsById);
@@ -1540,7 +2090,10 @@ function apply(ctx) {
1540
2090
  }
1541
2091
  for (const it of items) it.starred = starredSet.has(String(it.sessionId));
1542
2092
  if (headersOk) await gcStars(ids);
1543
- return items;
2093
+ return { items, usage };
2094
+ }
2095
+ async function allSessionItems(opts = {}) {
2096
+ return (await allSessionItemsDetailed(opts)).items;
1544
2097
  }
1545
2098
  async function buildStorage(opts = {}) {
1546
2099
  const items = await allSessionItems({ usage: true });
@@ -1556,7 +2109,15 @@ function apply(ctx) {
1556
2109
  if (!(opts && opts.force) && autoArchive.isFresh(store, now)) {
1557
2110
  return { ok: true, skipped: "throttled", archived: 0, lastRunAt: store.lastRunAt, lastArchivedCount: store.lastArchivedCount };
1558
2111
  }
1559
- 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
+ }
1560
2121
  const candidates = pickInactiveCandidates(items, {
1561
2122
  inactiveDays: days,
1562
2123
  skipStarred: store.settings.skipStarred,
@@ -1595,9 +2156,14 @@ function apply(ctx) {
1595
2156
  } catch (e) {
1596
2157
  }
1597
2158
  const store = await readTrashStore();
2159
+ const present = new Set(ids);
2160
+ const activeTombstones = store.purgedSessionIds.map(String).filter((id) => !present.has(id));
1598
2161
  if (ids.length) {
1599
2162
  const usage = await collectUsage(entries);
1600
- 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
+ ]));
1601
2167
  const { cached, missing } = metaCache.partition(ids, statsById);
1602
2168
  const persisted = await hydrateFromPersist(missing, statsById);
1603
2169
  for (const [id, meta] of persisted) metaCache.set(id, statsById.get(id), meta);
@@ -1610,8 +2176,13 @@ function apply(ctx) {
1610
2176
  for (const id of ids) {
1611
2177
  let meta = cached.get(id) || persisted.get(id) || null;
1612
2178
  if (!meta) {
2179
+ const entry = entries.find((e) => e.id === id);
1613
2180
  const snapshot = snapshotById.has(id) ? snapshotById.get(id) : typeof sq.readTitleSnapshot === "function" ? await sq.readTitleSnapshot(id).catch(() => null) : null;
1614
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
+ }
1615
2186
  metaCache.set(id, statsById.get(id), next);
1616
2187
  if (statsById.get(id)) collectDecoded(id, next);
1617
2188
  meta = next;
@@ -1623,38 +2194,14 @@ function apply(ctx) {
1623
2194
  return {
1624
2195
  titles: Object.fromEntries(authorityTitleCache),
1625
2196
  trashedSessionIds: store.items.map((item) => String(item.sessionId)),
1626
- purgedSessionIds: store.purgedSessionIds.map(String)
2197
+ purgedSessionIds: activeTombstones
1627
2198
  };
1628
2199
  }
1629
- async function buildDetails(sid) {
2200
+ async function buildDetails(sid, signal) {
1630
2201
  const sessions = ctx.get("sessions");
1631
2202
  const live = sessions && sessions.get(sid);
1632
2203
  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;
2204
+ let lastTime = 0;
1658
2205
  const fileSet = /* @__PURE__ */ new Map();
1659
2206
  const stats = {
1660
2207
  turns: 0,
@@ -1668,7 +2215,7 @@ function apply(ctx) {
1668
2215
  };
1669
2216
  const turnSeen = /* @__PURE__ */ new Set();
1670
2217
  const stepSeen = /* @__PURE__ */ new Set();
1671
- for (const ev of events) {
2218
+ const absorb = (ev) => {
1672
2219
  if (ev && typeof ev.time === "number" && ev.time > lastTime) lastTime = ev.time;
1673
2220
  const d = ev && ev.data && typeof ev.data === "object" ? ev.data : {};
1674
2221
  const type = ev && ev.type;
@@ -1715,12 +2262,44 @@ function apply(ctx) {
1715
2262
  break;
1716
2263
  }
1717
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;
1718
2278
  }
1719
2279
  stats.turns = turnSeen.size;
1720
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
+ }
1721
2300
  if (stats.fetches.length > MAX_FETCHES) stats.fetches = stats.fetches.slice(0, MAX_FETCHES);
1722
2301
  const fileEntries = [...fileSet.entries()].slice(0, MAX_FILES * 2);
1723
- 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)));
1724
2303
  const files = fileEntries.filter((_, i) => exists[i]).map(([path, tool]) => ({ path, tool })).slice(0, MAX_FILES);
1725
2304
  const lineage = {
1726
2305
  parentSessionId: meta && typeof meta.parentSession === "string" ? meta.parentSession : null,
@@ -1756,7 +2335,7 @@ function apply(ctx) {
1756
2335
  sessionId: sid,
1757
2336
  sizeBytes,
1758
2337
  createdAt: meta && typeof meta.createdAt === "number" ? meta.createdAt : null,
1759
- updatedAt: lastTime || null,
2338
+ updatedAt: Math.max(lastTime || 0, meta && typeof meta.createdAt === "number" ? meta.createdAt : 0) || null,
1760
2339
  files,
1761
2340
  stats,
1762
2341
  lineage
@@ -1789,7 +2368,13 @@ function apply(ctx) {
1789
2368
  } catch (e) {
1790
2369
  }
1791
2370
  const trashStore = await readTrashStore();
1792
- 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]);
1793
2378
  const idStrs = ids.map(String).filter((id) => !hidden.has(id) && (materialized.has(id) || live && live.get(id)));
1794
2379
  wsByPath = {};
1795
2380
  try {
@@ -1922,7 +2507,7 @@ function apply(ctx) {
1922
2507
  if (typeof item.originalPath !== "string" || !item.originalPath) {
1923
2508
  return { sessionId: item.sessionId, status: "unverified", originalPath: null };
1924
2509
  }
1925
- const exists = await stat(item.originalPath).then(() => true).catch(() => false);
2510
+ const exists = await stat2(item.originalPath).then(() => true).catch(() => false);
1926
2511
  return { sessionId: item.sessionId, status: exists ? "ok" : "missing", originalPath: item.originalPath };
1927
2512
  }));
1928
2513
  json(res, {
@@ -2026,6 +2611,7 @@ function apply(ctx) {
2026
2611
  }
2027
2612
  }
2028
2613
  }));
2614
+ const exportLimiter = createLimiter(2);
2029
2615
  disposers.push(ctx.webServer.register({
2030
2616
  kind: "exact",
2031
2617
  path: "/archived-sessions/export-md",
@@ -2034,13 +2620,23 @@ function apply(ctx) {
2034
2620
  const url = new URL(req.url, "http://localhost");
2035
2621
  const sid = url.searchParams.get("sessionId");
2036
2622
  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 || []);
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
+ });
2044
2640
  res.writeHead(200, {
2045
2641
  "content-type": "text/markdown; charset=utf-8",
2046
2642
  "content-disposition": `attachment; filename="dsh-session-${sid}.md"`,
@@ -2140,7 +2736,11 @@ function apply(ctx) {
2140
2736
  const body = await readJsonBody(req);
2141
2737
  const sid = body && typeof body.sessionId === "string" ? body.sessionId : null;
2142
2738
  if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
2143
- 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));
2144
2744
  } catch (e) {
2145
2745
  json(res, { error: String(e && e.message || e) }, e && e.status ? e.status : 500);
2146
2746
  }