dsh-sessions-manager 3.5.4 → 3.6.1

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,8 +1,8 @@
1
1
  // src/index.js
2
- import { mkdir as mkdir5, readFile as readFile3, realpath, rename as rename5, stat as stat2, unlink as unlink2, writeFile as writeFile5 } from "node:fs/promises";
3
- import { basename as basename2, dirname as dirname2, isAbsolute, join as join6 } from "node:path";
4
- import { readFileSync as readFileSync3 } from "node:fs";
5
- import { homedir as homedir3 } from "node:os";
2
+ import { mkdir as mkdir6, readFile as readFile3, readdir as readdir4, realpath, rename as rename6, rm as rm3, stat as stat4, unlink, writeFile as writeFile6 } from "node:fs/promises";
3
+ import { basename as basename3, dirname as dirname3, isAbsolute, join as join8 } from "node:path";
4
+ import { readFileSync as readFileSync4 } from "node:fs";
5
+ import { homedir as homedir4 } from "node:os";
6
6
 
7
7
  // src/zstd-frame.js
8
8
  import zlib from "node:zlib";
@@ -303,6 +303,138 @@ function createStarIndex(options = {}) {
303
303
  return { read, write, mutate, setStarred, removeIds, indexPath, dir };
304
304
  }
305
305
 
306
+ // src/pending-moves.js
307
+ import { readFileSync as readFileSync2 } from "node:fs";
308
+ import { mkdir as mkdir2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
309
+ import { homedir as homedir2 } from "node:os";
310
+ import { join as join2 } from "node:path";
311
+ var SCHEMA_VERSION = 1;
312
+ var DEFAULT_DIR = process.env.DSH_SESSIONS_MANAGER_PENDING_DIR || process.env.DSH_SESSIONS_MANAGER_STAR_DIR || join2(homedir2(), ".dsh", "sessions-manager");
313
+ var MAX_ITEMS = 50;
314
+ var MAX_ATTEMPTS = 5;
315
+ function isSafeId(value) {
316
+ return typeof value === "string" && value.length > 0 && value.length <= 200;
317
+ }
318
+ function normalizeStore(raw) {
319
+ const items = [];
320
+ const seen = /* @__PURE__ */ new Set();
321
+ const source = raw && Array.isArray(raw.items) ? raw.items : [];
322
+ for (const item of source) {
323
+ if (!item || !isSafeId(item.sessionId) || typeof item.targetPath !== "string" || item.targetPath.length === 0) continue;
324
+ if (seen.has(item.sessionId)) continue;
325
+ seen.add(item.sessionId);
326
+ items.push({
327
+ sessionId: String(item.sessionId),
328
+ targetPath: String(item.targetPath),
329
+ queuedAt: Number.isFinite(item.queuedAt) ? Number(item.queuedAt) : Date.now(),
330
+ attempts: Number.isSafeInteger(item.attempts) && item.attempts >= 0 ? Number(item.attempts) : 0
331
+ });
332
+ if (items.length >= MAX_ITEMS) break;
333
+ }
334
+ return { schemaVersion: SCHEMA_VERSION, items };
335
+ }
336
+ function createPendingMoveStore(options = {}) {
337
+ const dir = options.dir || DEFAULT_DIR;
338
+ const indexPath = options.indexPath || join2(dir, "pending-moves.json");
339
+ let mutation = Promise.resolve();
340
+ async function read() {
341
+ try {
342
+ return normalizeStore(JSON.parse(readFileSync2(indexPath, "utf8")));
343
+ } catch (e) {
344
+ return normalizeStore(null);
345
+ }
346
+ }
347
+ async function write(store) {
348
+ await mkdir2(dir, { recursive: true });
349
+ const tmp = join2(dir, `.pending-moves-${process.pid}-${Date.now()}.tmp`);
350
+ await writeFile2(tmp, JSON.stringify(normalizeStore(store), null, 2), { encoding: "utf8", mode: 384 });
351
+ await rename2(tmp, indexPath);
352
+ }
353
+ function mutate(mutator) {
354
+ const operation = mutation.then(async () => {
355
+ const store = await read();
356
+ const result = await mutator(store);
357
+ await write(store);
358
+ return result;
359
+ });
360
+ mutation = operation.catch(() => {
361
+ });
362
+ return operation;
363
+ }
364
+ return {
365
+ indexPath,
366
+ dir,
367
+ list: async () => (await read()).items,
368
+ has: async (sessionId) => (await read()).items.some((item) => item.sessionId === String(sessionId)),
369
+ queue: (sessionId, targetPath) => mutate((store) => {
370
+ if (!isSafeId(sessionId) || typeof targetPath !== "string" || targetPath.length === 0) return null;
371
+ const existing = store.items.find((item2) => item2.sessionId === String(sessionId));
372
+ if (existing) {
373
+ existing.targetPath = String(targetPath);
374
+ existing.attempts = 0;
375
+ return existing;
376
+ }
377
+ const item = { sessionId: String(sessionId), targetPath: String(targetPath), queuedAt: Date.now(), attempts: 0 };
378
+ store.items.push(item);
379
+ if (store.items.length > MAX_ITEMS) store.items.splice(0, store.items.length - MAX_ITEMS);
380
+ return item;
381
+ }),
382
+ remove: (sessionIds) => mutate((store) => {
383
+ const drop = new Set((Array.isArray(sessionIds) ? sessionIds : [sessionIds]).filter(isSafeId).map(String));
384
+ const before = store.items.length;
385
+ store.items = store.items.filter((item) => !drop.has(item.sessionId));
386
+ return before - store.items.length;
387
+ }),
388
+ bumpAttempts: (sessionId) => mutate((store) => {
389
+ const item = store.items.find((entry) => entry.sessionId === String(sessionId));
390
+ if (!item) return null;
391
+ item.attempts += 1;
392
+ if (item.attempts >= MAX_ATTEMPTS) {
393
+ store.items = store.items.filter((entry) => entry.sessionId !== item.sessionId);
394
+ return { dropped: true, attempts: item.attempts };
395
+ }
396
+ return { dropped: false, attempts: item.attempts };
397
+ })
398
+ };
399
+ }
400
+
401
+ // src/state-temp-sweep.js
402
+ import { readdir, rm, stat } from "node:fs/promises";
403
+ import { join as join3 } from "node:path";
404
+ var TEMP_NAME_RE = /^\..+-\d+-\d+\.tmp$/;
405
+ async function sweepStaleStateTemps(dirs, options = {}) {
406
+ const maxAgeMs = Number.isSafeInteger(options.maxAgeMs) && options.maxAgeMs >= 0 ? options.maxAgeMs : 36e5;
407
+ const now = Number.isFinite(options.now) ? options.now : Date.now();
408
+ let removed = 0;
409
+ for (const dir of Array.isArray(dirs) ? dirs : []) {
410
+ if (typeof dir !== "string" || dir.length === 0) continue;
411
+ let entries;
412
+ try {
413
+ entries = await readdir(dir);
414
+ } catch (e) {
415
+ continue;
416
+ }
417
+ for (const name2 of entries) {
418
+ if (!TEMP_NAME_RE.test(name2)) continue;
419
+ const path = join3(dir, name2);
420
+ let info;
421
+ try {
422
+ info = await stat(path);
423
+ } catch (e) {
424
+ continue;
425
+ }
426
+ if (!info.isFile()) continue;
427
+ if (now - info.mtimeMs < maxAgeMs) continue;
428
+ try {
429
+ await rm(path, { force: true });
430
+ removed++;
431
+ } catch (e) {
432
+ }
433
+ }
434
+ }
435
+ return removed;
436
+ }
437
+
306
438
  // src/storage-stats.js
307
439
  var UNGROUPED_KEY = "__ungrouped__";
308
440
  function isFiniteSize(value) {
@@ -355,15 +487,15 @@ function aggregateStorage(items, options = {}) {
355
487
  }
356
488
 
357
489
  // src/auto-archive.js
358
- import { mkdir as mkdir2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
359
- import { readFileSync as readFileSync2 } from "node:fs";
360
- import { homedir as homedir2 } from "node:os";
361
- import { join as join2 } from "node:path";
490
+ import { mkdir as mkdir3, rename as rename3, writeFile as writeFile3 } from "node:fs/promises";
491
+ import { readFileSync as readFileSync3 } from "node:fs";
492
+ import { homedir as homedir3 } from "node:os";
493
+ import { join as join4 } from "node:path";
362
494
  var AUTO_ARCHIVE_SCHEMA_VERSION = 4;
363
495
  var INACTIVE_DAY_OPTIONS = Object.freeze([0, 30, 60, 90]);
364
496
  var DAY_MS = 864e5;
365
497
  var RUN_INTERVAL_MS = DAY_MS;
366
- var DEFAULT_DIR = join2(homedir2(), ".dsh", "sessions-manager");
498
+ var DEFAULT_DIR2 = join4(homedir3(), ".dsh", "sessions-manager");
367
499
  function normalizeAutoArchiveStore(raw) {
368
500
  const source = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
369
501
  const settings = source.settings && typeof source.settings === "object" ? source.settings : {};
@@ -407,21 +539,21 @@ function pickInactiveCandidates(items, options = {}) {
407
539
  return out;
408
540
  }
409
541
  function createAutoArchiveStore(options = {}) {
410
- const dir = options.dir || process.env.DSH_SESSIONS_MANAGER_AUTO_ARCHIVE_DIR || DEFAULT_DIR;
411
- const indexPath = options.indexPath || join2(dir, "auto-archive.json");
542
+ const dir = options.dir || process.env.DSH_SESSIONS_MANAGER_AUTO_ARCHIVE_DIR || DEFAULT_DIR2;
543
+ const indexPath = options.indexPath || join4(dir, "auto-archive.json");
412
544
  let mutation = Promise.resolve();
413
545
  async function read() {
414
546
  try {
415
- return normalizeAutoArchiveStore(JSON.parse(readFileSync2(indexPath, "utf8")));
547
+ return normalizeAutoArchiveStore(JSON.parse(readFileSync3(indexPath, "utf8")));
416
548
  } catch {
417
549
  return normalizeAutoArchiveStore(null);
418
550
  }
419
551
  }
420
552
  async function write(store) {
421
- await mkdir2(dir, { recursive: true });
422
- const tmp = join2(dir, `.auto-archive-${process.pid}-${Date.now()}.tmp`);
423
- await writeFile2(tmp, JSON.stringify(normalizeAutoArchiveStore(store), null, 2), { encoding: "utf8", mode: 384 });
424
- await rename2(tmp, indexPath);
553
+ await mkdir3(dir, { recursive: true });
554
+ const tmp = join4(dir, `.auto-archive-${process.pid}-${Date.now()}.tmp`);
555
+ await writeFile3(tmp, JSON.stringify(normalizeAutoArchiveStore(store), null, 2), { encoding: "utf8", mode: 384 });
556
+ await rename3(tmp, indexPath);
425
557
  }
426
558
  function mutate(mutator) {
427
559
  const operation = mutation.then(async () => {
@@ -468,13 +600,13 @@ function createAutoArchiveStore(options = {}) {
468
600
  var DEFAULT_TTL_MS = 5 * 60 * 1e3;
469
601
  var DEFAULT_MAX = 4e3;
470
602
  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;
603
+ function fingerprintOf(stat5) {
604
+ if (!stat5 || typeof stat5 !== "object") return null;
605
+ if (typeof stat5.revision === "string" && stat5.revision.length > 0) {
606
+ return REVISION_PREFIX + stat5.revision;
475
607
  }
476
- const mtimeMs = stat3.mtimeMs;
477
- const size = stat3.size;
608
+ const mtimeMs = stat5.mtimeMs;
609
+ const size = stat5.size;
478
610
  if (typeof mtimeMs !== "number" || !Number.isFinite(mtimeMs) || mtimeMs <= 0) return null;
479
611
  if (typeof size !== "number" || !Number.isFinite(size) || size < 0) return null;
480
612
  return `${Math.floor(mtimeMs)}:${size}`;
@@ -482,9 +614,15 @@ function fingerprintOf(stat3) {
482
614
  function isPersistableFingerprint(fingerprint) {
483
615
  return typeof fingerprint === "string" && fingerprint !== "" && !fingerprint.startsWith(REVISION_PREFIX);
484
616
  }
485
- function isFresh(entry, stat3, now, ttlMs = DEFAULT_TTL_MS) {
617
+ function persistFingerprintOf(stat5) {
618
+ const fp = fingerprintOf(stat5);
619
+ if (fp && isPersistableFingerprint(fp)) return fp;
620
+ if (stat5 && Number.isFinite(stat5.sizeBytes) && stat5.sizeBytes >= 0) return `sz:${stat5.sizeBytes}`;
621
+ return null;
622
+ }
623
+ function isFresh(entry, stat5, now, ttlMs = DEFAULT_TTL_MS) {
486
624
  if (!entry) return false;
487
- const fp = fingerprintOf(stat3);
625
+ const fp = fingerprintOf(stat5);
488
626
  if (!fp) return false;
489
627
  if (entry.fingerprint !== fp) return false;
490
628
  if (typeof entry.at !== "number") return false;
@@ -496,8 +634,8 @@ function partitionByCache(ids, statsById, cache, now = Date.now(), ttlMs = DEFAU
496
634
  const missing = [];
497
635
  for (const id of ids) {
498
636
  const entry = cache && cache.get(String(id));
499
- const stat3 = statsById && statsById.get(String(id));
500
- if (isFresh(entry, stat3, now, ttlMs) && entry && entry.meta) {
637
+ const stat5 = statsById && statsById.get(String(id));
638
+ if (isFresh(entry, stat5, now, ttlMs) && entry && entry.meta) {
501
639
  cached.set(String(id), entry.meta);
502
640
  } else {
503
641
  missing.push(String(id));
@@ -513,10 +651,10 @@ function createSessionMetaCache(opts = {}) {
513
651
  let misses = 0;
514
652
  return {
515
653
  // 命中返回 meta,未命中/无法校验返回 null。
516
- get(id, stat3) {
654
+ get(id, stat5) {
517
655
  const key = String(id);
518
656
  const entry = map.get(key);
519
- if (isFresh(entry, stat3, Date.now(), ttlMs)) {
657
+ if (isFresh(entry, stat5, Date.now(), ttlMs)) {
520
658
  hits++;
521
659
  map.delete(key);
522
660
  map.set(key, entry);
@@ -525,9 +663,9 @@ function createSessionMetaCache(opts = {}) {
525
663
  misses++;
526
664
  return null;
527
665
  },
528
- set(id, stat3, meta) {
666
+ set(id, stat5, meta) {
529
667
  if (!meta) return null;
530
- const fp = fingerprintOf(stat3);
668
+ const fp = fingerprintOf(stat5);
531
669
  if (!fp) return null;
532
670
  const key = String(id);
533
671
  map.delete(key);
@@ -562,8 +700,8 @@ function createSessionMetaCache(opts = {}) {
562
700
  }
563
701
 
564
702
  // src/title-persist-index.js
565
- import { mkdir as mkdir3, readFile, rename as rename3, writeFile as writeFile3 } from "node:fs/promises";
566
- import { dirname, join as join3 } from "node:path";
703
+ import { mkdir as mkdir4, readFile, rename as rename4, writeFile as writeFile4 } from "node:fs/promises";
704
+ import { dirname, join as join5 } from "node:path";
567
705
  var TITLE_INDEX_SCHEMA_VERSION = 1;
568
706
  var MAX_ENTRIES = 2e4;
569
707
  function normalizeEntry(raw) {
@@ -601,7 +739,7 @@ function mergeEntries(left, right) {
601
739
  function createTitleIndexStore({ dir, file }) {
602
740
  let cache = null;
603
741
  let chain = Promise.resolve();
604
- const path = file || join3(dir, "title-index.json");
742
+ const path = file || join5(dir, "title-index.json");
605
743
  async function readRaw() {
606
744
  try {
607
745
  return normalizeTitleIndex(JSON.parse(await readFile(path, "utf8")));
@@ -636,10 +774,10 @@ function createTitleIndexStore({ dir, file }) {
636
774
  if (!Object.keys(right).length) return false;
637
775
  await enqueue(async (store) => {
638
776
  const next = mergeEntries(store, right);
639
- await mkdir3(dirname(path), { recursive: true });
640
- const tmp = join3(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
641
- await writeFile3(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: next }), { encoding: "utf8", mode: 384 });
642
- await rename3(tmp, path);
777
+ await mkdir4(dirname(path), { recursive: true });
778
+ const tmp = join5(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
779
+ await writeFile4(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: next }), { encoding: "utf8", mode: 384 });
780
+ await rename4(tmp, path);
643
781
  cache = next;
644
782
  });
645
783
  return true;
@@ -656,10 +794,10 @@ function createTitleIndexStore({ dir, file }) {
656
794
  }
657
795
  }
658
796
  if (!changed) return;
659
- await mkdir3(dirname(path), { recursive: true });
660
- const tmp = join3(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
661
- await writeFile3(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: store }), { encoding: "utf8", mode: 384 });
662
- await rename3(tmp, path);
797
+ await mkdir4(dirname(path), { recursive: true });
798
+ const tmp = join5(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
799
+ await writeFile4(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: store }), { encoding: "utf8", mode: 384 });
800
+ await rename4(tmp, path);
663
801
  });
664
802
  return true;
665
803
  }
@@ -667,8 +805,8 @@ function createTitleIndexStore({ dir, file }) {
667
805
  }
668
806
 
669
807
  // src/handle-era-paths.js
670
- import { readdir, stat } from "node:fs/promises";
671
- import { basename, join as join4 } from "node:path";
808
+ import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
809
+ import { basename, join as join6 } from "node:path";
672
810
 
673
811
  // src/path-guard.js
674
812
  function splitSegments(target) {
@@ -700,7 +838,7 @@ function pathOwnsSession(target, sid) {
700
838
  }
701
839
 
702
840
  // src/handle-era-paths.js
703
- var GENERATION_LOG_RE = /^session\.v\d+\.jsonl(\.zst(d)?)?$/;
841
+ var GENERATION_LOG_RE = /^session(\.v\d+)?\.jsonl(\.zst(d)?)?$/;
704
842
  function isSafeChar(ch) {
705
843
  return ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch);
706
844
  }
@@ -741,8 +879,11 @@ function resolveSessionRoot(sp) {
741
879
  return typeof root === "string" && root.length > 0 ? root : null;
742
880
  }
743
881
  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));
882
+ const project = cwd === void 0 || cwd === null || cwd === "" ? join6(root, "_no-cwd") : join6(root, projectKeyFor(cwd));
883
+ return join6(project, encodeSegmentFor(id));
884
+ }
885
+ function generationVersionOf(name2) {
886
+ return Number((String(name2).match(/^session\.v(\d+)\./) || [])[1] || 0);
746
887
  }
747
888
  async function locateSessionArtifacts(sp, header) {
748
889
  const root = resolveSessionRoot(sp);
@@ -757,27 +898,26 @@ async function locateSessionArtifacts(sp, header) {
757
898
  if (basename(sessionDir) !== encodeSegmentFor(sid)) return null;
758
899
  let entries;
759
900
  try {
760
- const st = await stat(sessionDir);
901
+ const st = await stat2(sessionDir);
761
902
  if (!st.isDirectory()) return null;
762
- entries = await readdir(sessionDir);
903
+ entries = await readdir2(sessionDir);
763
904
  } catch (e) {
764
905
  return null;
765
906
  }
766
907
  const generationFiles = entries.filter((name2) => GENERATION_LOG_RE.test(name2));
767
908
  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]);
909
+ generationFiles.sort((a, b) => generationVersionOf(b) - generationVersionOf(a));
910
+ const logPath = join6(sessionDir, generationFiles[0]);
774
911
  if (!pathOwnsSession(logPath, sid)) return null;
775
912
  return {
776
913
  root,
777
- projectDir: join4(root, header.cwd === void 0 || header.cwd === null || header.cwd === "" ? "_no-cwd" : projectKeyFor(header.cwd)),
914
+ projectDir: join6(root, header.cwd === void 0 || header.cwd === null || header.cwd === "" ? "_no-cwd" : projectKeyFor(header.cwd)),
778
915
  sessionDir,
779
916
  logPath,
780
- generationFiles
917
+ generationFiles,
918
+ // 最高代版本号:0 = legacy `session.jsonl[.zstd]`,≥1 = `session.vN.jsonl[.zstd]`。
919
+ // 上层用它判断「另一个目录里的同 id 副本是不是被取代的旧代」。
920
+ generationVersion: generationVersionOf(generationFiles[0])
781
921
  };
782
922
  }
783
923
 
@@ -785,9 +925,15 @@ async function locateSessionArtifacts(sp, header) {
785
925
  var DEFAULT_CHUNK = 400;
786
926
  var MAX_CHUNKS = 2e4;
787
927
  function normalizeReadResult(events) {
928
+ if (events === void 0 || events === null) return [];
788
929
  if (Array.isArray(events)) return events;
789
- if (events && typeof events[Symbol.iterator] === "function") return [...events];
790
- return [];
930
+ if (typeof events === "object" && Array.isArray(events.events)) return events.events;
931
+ if (typeof events[Symbol.iterator] === "function") return [...events];
932
+ const error = new Error(
933
+ "DSH \u4F1A\u8BDD\u8BFB\u53D6\u8FD4\u56DE\u4E86\u65E0\u6CD5\u8BC6\u522B\u7684\u7ED3\u6784\uFF08runtime \u63A5\u53E3\u53EF\u80FD\u5DF2\u53D8\u66F4\uFF09\uFF0C\u5DF2\u505C\u6B62\u89E3\u6790\u4EE5\u514D\u8BEF\u5224\u4E3A\u7A7A\uFF1A" + Object.prototype.toString.call(events)
934
+ );
935
+ error.code = "DSM_READ_SHAPE_UNKNOWN";
936
+ throw error;
791
937
  }
792
938
  async function closeQuietly(handle) {
793
939
  try {
@@ -975,9 +1121,80 @@ function requireCapability(capabilities, name2) {
975
1121
  }
976
1122
 
977
1123
  // 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";
1124
+ import { mkdir as mkdir5, readFile as readFile2, readdir as readdir3, rename as rename5, rm as rm2, stat as stat3, writeFile as writeFile5 } from "node:fs/promises";
1125
+ import { basename as basename2, dirname as dirname2, join as join7 } from "node:path";
980
1126
  var MOVE_BATCH = 400;
1127
+ var REAL_LOG_RE = /^session.*\.jsonl(\.zst(d)?)?$/;
1128
+ var MOVE_TEMP_RE = /\.move-(backup|stage)-/;
1129
+ var SESSION_LOCAL_JUNK_RE = /^(\.?session\.lock.*|\.lock.*|.*\.lock|.*\.tmp|.*\.stage|.*\.bak|\.DS_Store)$/;
1130
+ async function cleanSiblingCopies(root, sid, keepDir, keepVersion) {
1131
+ let projects;
1132
+ try {
1133
+ projects = await readdir3(root, { withFileTypes: true });
1134
+ } catch (e) {
1135
+ return { removed: 0, superseded: [], real: [] };
1136
+ }
1137
+ let segment;
1138
+ try {
1139
+ segment = encodeSegmentFor(sid);
1140
+ } catch (e) {
1141
+ return { removed: 0, superseded: [], real: [] };
1142
+ }
1143
+ let removed = 0;
1144
+ const superseded = [];
1145
+ const real = [];
1146
+ for (const ent of projects) {
1147
+ if (!ent.isDirectory()) continue;
1148
+ const candidate = join7(root, ent.name, segment);
1149
+ if (keepDir && candidate === keepDir) continue;
1150
+ let entries;
1151
+ try {
1152
+ const st = await stat3(candidate);
1153
+ if (!st.isDirectory()) continue;
1154
+ entries = await readdir3(candidate);
1155
+ } catch (e) {
1156
+ continue;
1157
+ }
1158
+ const logs = entries.filter((name2) => REAL_LOG_RE.test(name2) && !MOVE_TEMP_RE.test(name2));
1159
+ if (logs.length === 0) {
1160
+ try {
1161
+ await rm2(candidate, { recursive: true, force: true });
1162
+ removed++;
1163
+ } catch (e) {
1164
+ }
1165
+ continue;
1166
+ }
1167
+ const highest = Math.max(...logs.map(generationVersionOf));
1168
+ const onlyLogsAndLocalJunk = entries.every((name2) => REAL_LOG_RE.test(name2) && !MOVE_TEMP_RE.test(name2) || SESSION_LOCAL_JUNK_RE.test(name2));
1169
+ if (Number.isSafeInteger(keepVersion) && highest < keepVersion && onlyLogsAndLocalJunk) {
1170
+ try {
1171
+ await rm2(candidate, { recursive: true, force: true });
1172
+ superseded.push(candidate);
1173
+ } catch (e) {
1174
+ real.push(candidate);
1175
+ }
1176
+ continue;
1177
+ }
1178
+ real.push(candidate);
1179
+ }
1180
+ return { removed, superseded, real };
1181
+ }
1182
+ async function removeSourceDir(sessionDir) {
1183
+ let entries;
1184
+ try {
1185
+ entries = await readdir3(sessionDir);
1186
+ } catch (e) {
1187
+ return { cleaned: true, leftover: [] };
1188
+ }
1189
+ try {
1190
+ await rm2(sessionDir, { recursive: true, force: true });
1191
+ } catch (e) {
1192
+ return { cleaned: false, leftover: entries };
1193
+ }
1194
+ return { cleaned: true, leftover: [] };
1195
+ }
1196
+ var BUSY_MOVE_MESSAGE = "\u4F1A\u8BDD\u6B63\u88AB DSH \u6253\u5F00\uFF0C\u6682\u65F6\u65E0\u6CD5\u79FB\u52A8\uFF1B\u8BF7\u91CD\u542F DSH \u540E\u518D\u8BD5\u3002";
1197
+ var BUSY_PURGE_MESSAGE = "\u4F1A\u8BDD\u6B63\u88AB DSH \u6253\u5F00\uFF0C\u6682\u65F6\u65E0\u6CD5\u5F7B\u5E95\u5220\u9664\uFF1B\u8BF7\u91CD\u542F DSH \u540E\u518D\u8BD5\u3002";
981
1198
  function conflictError(message) {
982
1199
  const error = new Error(message);
983
1200
  error.status = 409;
@@ -1004,7 +1221,7 @@ async function ensureNoActiveWriter(sp, sid) {
1004
1221
  try {
1005
1222
  handle = await sp.open(sid, "write");
1006
1223
  } 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");
1224
+ if (isAlreadyOwned(e)) throw conflictError(BUSY_MOVE_MESSAGE);
1008
1225
  throw e;
1009
1226
  }
1010
1227
  await closeQuietly2(handle);
@@ -1020,13 +1237,13 @@ async function purgeSessionArtifacts(sp, sid, header) {
1020
1237
  try {
1021
1238
  writer = await sp.open(sid, "write");
1022
1239
  } 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");
1240
+ if (isAlreadyOwned(e)) throw conflictError(BUSY_PURGE_MESSAGE);
1024
1241
  throw e;
1025
1242
  }
1026
1243
  await closeQuietly2(writer);
1027
1244
  writer = null;
1028
1245
  try {
1029
- await rm(artifacts.sessionDir, { recursive: true, force: true });
1246
+ await rm2(artifacts.sessionDir, { recursive: true, force: true });
1030
1247
  } catch (e) {
1031
1248
  const error = new Error("\u5220\u9664\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A" + String(e && e.message || e));
1032
1249
  error.status = 500;
@@ -1042,8 +1259,8 @@ async function purgeSessionArtifacts(sp, sid, header) {
1042
1259
  }
1043
1260
  return artifacts;
1044
1261
  }
1045
- async function relocateRewrittenBackup({ sid, canonical, backupPath, artifacts }) {
1046
- const original = await readFile2(backupPath);
1262
+ async function relocateRewrittenBackup({ sid, canonical, backupLogPath, artifacts }) {
1263
+ const original = await readFile2(backupLogPath);
1047
1264
  const frames = scanZstdFrames(original).frames;
1048
1265
  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
1266
  const rewritten = rewriteFrame0CwdInMemory(original, canonical);
@@ -1053,26 +1270,66 @@ async function relocateRewrittenBackup({ sid, canonical, backupPath, artifacts }
1053
1270
  throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u4E8B\u4EF6\u5185\u5BB9\u53D1\u751F\u53D8\u5316");
1054
1271
  }
1055
1272
  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]));
1273
+ await mkdir5(targetDir, { recursive: true });
1274
+ const staged = join7(targetDir, `.move-stage-${process.pid}-${Date.now()}`);
1275
+ await writeFile5(staged, rewritten, { mode: 384 });
1276
+ await rename5(staged, join7(targetDir, artifacts.generationFiles[0]));
1060
1277
  return targetDir;
1061
1278
  }
1062
1279
  async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inheritedEventCount = 0 }) {
1280
+ let probeError = null;
1281
+ try {
1282
+ await ensureNoActiveWriter(sp, sid);
1283
+ } catch (e) {
1284
+ if (e && e.status === 409) throw e;
1285
+ probeError = e;
1286
+ }
1063
1287
  const artifacts = await locateSessionArtifacts(sp, header);
1064
1288
  if (!artifacts) {
1065
1289
  const error = new Error("\u65E0\u6CD5\u5B9A\u4F4D\u8BE5\u4F1A\u8BDD\u7684\u7269\u7406\u65E5\u5FD7\uFF0C\u5DF2\u505C\u6B62\u79FB\u52A8");
1066
1290
  error.status = 409;
1067
1291
  throw error;
1068
1292
  }
1069
- await ensureNoActiveWriter(sp, sid);
1293
+ if (probeError) {
1294
+ const error = new Error("\u65E0\u6CD5\u786E\u8BA4\u8BE5\u4F1A\u8BDD\u662F\u5426\u6B63\u88AB\u5199\u5165\uFF0C\u5DF2\u505C\u6B62\u79FB\u52A8\uFF1A" + String(probeError && probeError.message || probeError));
1295
+ error.status = 409;
1296
+ throw error;
1297
+ }
1298
+ const HEADER_ONLY_MAX = 4096;
1299
+ if (events.length === 0) {
1300
+ let sourceBytes = null;
1301
+ try {
1302
+ sourceBytes = (await stat3(artifacts.logPath)).size;
1303
+ } catch (e) {
1304
+ sourceBytes = null;
1305
+ }
1306
+ if (sourceBytes !== null && sourceBytes > HEADER_ONLY_MAX) {
1307
+ const error = new Error(
1308
+ `\u79FB\u52A8\u524D\u6821\u9A8C\u5931\u8D25\uFF1A\u6E90\u65E5\u5FD7\u6709 ${sourceBytes} \u5B57\u8282\u4F46\u8BFB\u5230 0 \u6761\u4E8B\u4EF6\uFF08\u8BFB\u53D6\u94FE\u8DEF\u5F02\u5E38\uFF09\uFF0C\u5DF2\u505C\u6B62\u79FB\u52A8\u4EE5\u514D\u4E22\u5931\u4F1A\u8BDD\u5185\u5BB9\u3002`
1309
+ );
1310
+ error.status = 409;
1311
+ error.code = "DSM_MOVE_EMPTY_READ";
1312
+ throw error;
1313
+ }
1314
+ }
1315
+ const siblings = await cleanSiblingCopies(artifacts.root, sid, artifacts.sessionDir, artifacts.generationVersion);
1316
+ if (siblings.real.length > 0) {
1317
+ const error = new Error(
1318
+ "\u65E0\u6CD5\u79FB\u52A8\uFF1A\u5728\u5176\u4ED6\u5DE5\u4F5C\u533A\u76EE\u5F55\u53D1\u73B0\u540C\u4E00\u4F1A\u8BDD\u7684\u65E5\u5FD7\u526F\u672C\uFF0C\u8BF7\u5148\u786E\u8BA4\u4FDD\u7559\u54EA\u4E00\u4EFD\uFF1A\n" + siblings.real.map((p) => "\xB7 " + p).join("\n")
1319
+ );
1320
+ error.status = 409;
1321
+ error.code = "DSM_SESSION_DUP_LOG";
1322
+ throw error;
1323
+ }
1070
1324
  const newHeader = Object.assign({}, header, { cwd: canonical });
1071
1325
  const firstSeq = events.length ? Number(events[0].seq) : 0;
1072
1326
  const replay = firstSeq !== 0 ? events.map((event, index) => ({ ...event, seq: index })) : events;
1073
1327
  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);
1328
+ const backupRoot = join7(dirname2(artifacts.root), "sessions-manager-move-backup");
1329
+ const backupDir = join7(backupRoot, `${encodeSegmentFor(sid)}-${process.pid}-${Date.now()}`);
1330
+ await mkdir5(backupRoot, { recursive: true });
1331
+ const backupLogPath = join7(backupDir, basename2(artifacts.logPath));
1332
+ await rename5(artifacts.sessionDir, backupDir);
1076
1333
  let writer = null;
1077
1334
  try {
1078
1335
  try {
@@ -1085,7 +1342,7 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
1085
1342
  writer = null;
1086
1343
  } catch (e) {
1087
1344
  if (isAlreadyExists(e)) {
1088
- await relocateRewrittenBackup({ sid, canonical, backupPath, artifacts });
1345
+ await relocateRewrittenBackup({ sid, canonical, backupLogPath, artifacts });
1089
1346
  } else {
1090
1347
  throw e;
1091
1348
  }
@@ -1101,11 +1358,15 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
1101
1358
  } catch (e) {
1102
1359
  await closeQuietly2(writer);
1103
1360
  try {
1104
- await rm(deriveSessionDir(artifacts.root, canonical, sid), { recursive: true, force: true });
1361
+ await rm2(deriveSessionDir(artifacts.root, canonical, sid), { recursive: true, force: true });
1362
+ } catch (_) {
1363
+ }
1364
+ try {
1365
+ await mkdir5(dirname2(artifacts.sessionDir), { recursive: true });
1105
1366
  } catch (_) {
1106
1367
  }
1107
1368
  try {
1108
- await rename4(backupPath, artifacts.logPath);
1369
+ await rename5(backupDir, artifacts.sessionDir);
1109
1370
  } catch (_) {
1110
1371
  }
1111
1372
  if (e && e.status) throw e;
@@ -1113,19 +1374,53 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
1113
1374
  error.status = 500;
1114
1375
  throw error;
1115
1376
  }
1116
- try {
1117
- await unlink(backupPath);
1118
- } catch (e) {
1119
- }
1120
- return { sessionDir: deriveSessionDir(artifacts.root, canonical, sid) };
1377
+ const sourceCleanup = await removeSourceDir(backupDir);
1378
+ return {
1379
+ sessionDir: deriveSessionDir(artifacts.root, canonical, sid),
1380
+ sourceCleanup,
1381
+ reclaimedSiblings: siblings.superseded
1382
+ };
1383
+ }
1384
+
1385
+ // src/lineage.js
1386
+ var EMPTY_BASE = 190;
1387
+ function isEmptyLogSize(sizeBytes, cwd) {
1388
+ if (!Number.isFinite(sizeBytes) || sizeBytes < 0) return false;
1389
+ const cwdLen = typeof cwd === "string" ? cwd.length : 0;
1390
+ return sizeBytes <= EMPTY_BASE + cwdLen;
1391
+ }
1392
+ var EMPTY_DECODE_LIMIT = 8192;
1393
+ var LIFECYCLE_EVENT_TYPES = /* @__PURE__ */ new Set([
1394
+ "session",
1395
+ "permission/preset",
1396
+ "sandbox/mode",
1397
+ "approval/policy"
1398
+ ]);
1399
+ function isEmptyEventTypes(types) {
1400
+ if (!Array.isArray(types)) return false;
1401
+ for (const t of types) {
1402
+ if (typeof t !== "string" || !LIFECYCLE_EVENT_TYPES.has(t)) return false;
1403
+ }
1404
+ return true;
1405
+ }
1406
+ function classifyLineage(header, sizeBytes) {
1407
+ if (!header || typeof header !== "object") return null;
1408
+ const origin = header.origin === "subagent" ? "subagent" : null;
1409
+ const parentSession = typeof header.parentSession === "string" && header.parentSession ? header.parentSession : null;
1410
+ const delegationDepth = Number.isSafeInteger(header.delegationDepth) && header.delegationDepth > 0 ? header.delegationDepth : 0;
1411
+ const empty = isEmptyLogSize(sizeBytes, header.cwd);
1412
+ if (!origin && !parentSession && !empty) return null;
1413
+ return { origin, parentSession, delegationDepth, empty };
1121
1414
  }
1122
1415
 
1123
1416
  // src/index.js
1417
+ var BUILD_STAMP = true ? "3.6.1+f0814a9e" : "dev";
1124
1418
  var name = "dsh-sessions-manager";
1125
1419
  var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
1126
1420
  var MAX_TITLE = 80;
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");
1421
+ var STATE_DIR = process.env.DSH_SESSIONS_MANAGER_STAR_DIR || join8(homedir4(), ".dsh", "sessions-manager");
1422
+ var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR || join8(homedir4(), ".dsh", "sessions-manager-trash");
1423
+ var TRASH_INDEX = join8(TRASH_DIR, "index.json");
1129
1424
  var TRASH_SCHEMA_VERSION = 2;
1130
1425
  var DEFAULT_TRASH_SETTINGS = Object.freeze({ retentionDays: 0 });
1131
1426
  var FETCH_TOOL_RE = /search|fetch|download|browse/i;
@@ -1227,7 +1522,7 @@ function apply(ctx) {
1227
1522
  const dom = () => ctx.storageDomain.get("workspace");
1228
1523
  const authorityTitleCache = /* @__PURE__ */ new Map();
1229
1524
  const metaCache = createSessionMetaCache();
1230
- const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file: join6(TRASH_DIR, "title-index.json") });
1525
+ const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file: join8(TRASH_DIR, "title-index.json") });
1231
1526
  async function hydrateFromPersist(ids, statsById) {
1232
1527
  const hits = /* @__PURE__ */ new Map();
1233
1528
  if (!ids || !ids.length) return hits;
@@ -1238,29 +1533,31 @@ function apply(ctx) {
1238
1533
  return hits;
1239
1534
  }
1240
1535
  for (const id of ids) {
1241
- const stat3 = statsById.get(id);
1536
+ const stat5 = statsById.get(id);
1242
1537
  const entry = store && store[id];
1243
- if (!stat3 || !entry) continue;
1244
- const fp = fingerprintOf(stat3);
1245
- if (!isPersistableFingerprint(fp)) continue;
1538
+ if (!stat5 || !entry) continue;
1539
+ const fp = persistFingerprintOf(stat5);
1540
+ if (!fp) continue;
1246
1541
  if (fp && entry.fingerprint === fp) {
1247
1542
  hits.set(id, { title: entry.title, cwd: entry.cwd, createdAt: entry.createdAt });
1248
1543
  }
1249
1544
  }
1250
1545
  return hits;
1251
1546
  }
1252
- function persistDecoded(decoded, statsById) {
1547
+ async function persistDecoded(decoded, statsById) {
1253
1548
  if (!decoded || !decoded.size) return;
1254
1549
  const batch = {};
1255
1550
  const now = Date.now();
1256
1551
  for (const [id, meta] of decoded) {
1257
- const fp = fingerprintOf(statsById.get(id));
1258
- if (!isPersistableFingerprint(fp)) continue;
1552
+ const fp = persistFingerprintOf(statsById.get(id));
1553
+ if (!fp) continue;
1259
1554
  batch[id] = { title: meta.title, cwd: meta.cwd, createdAt: meta.createdAt, fingerprint: fp, updatedAt: now };
1260
1555
  }
1261
1556
  if (!Object.keys(batch).length) return;
1262
- titleIndex.merge(batch).catch(() => {
1263
- });
1557
+ try {
1558
+ await titleIndex.merge(batch);
1559
+ } catch (e) {
1560
+ }
1264
1561
  }
1265
1562
  function metaFromSnapshot(o) {
1266
1563
  let title = null, createdAt = null, cwd = null;
@@ -1332,50 +1629,63 @@ function apply(ctx) {
1332
1629
  }
1333
1630
  return base;
1334
1631
  }
1335
- async function resolveOne(id, usage, opts = {}) {
1336
- const key = String(id);
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;
1338
- const cached = metaCache.get(key, statInfo);
1339
- if (cached) return buildItem(key, cached, usage, opts.exposeUsage);
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
- }
1345
- if (opts.preloaded !== void 0) {
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;
1350
- } else if (typeof sq.readTitleSnapshot === "function") {
1351
- try {
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;
1356
- } catch (e) {
1357
- }
1632
+ const EMPTY_META = { title: null, cwd: null, createdAt: null };
1633
+ const WARM_CHUNK = 4;
1634
+ const warmQueue = /* @__PURE__ */ new Map();
1635
+ let warmRunning = false;
1636
+ let warmKickTimer = null;
1637
+ function scheduleWarm() {
1638
+ if (warmKickTimer || warmRunning) return;
1639
+ warmKickTimer = setTimeout(() => {
1640
+ warmKickTimer = null;
1641
+ runWarm();
1642
+ }, 25);
1643
+ if (typeof warmKickTimer.unref === "function") warmKickTimer.unref();
1644
+ }
1645
+ function enqueueWarm(ids, statsById, entryById) {
1646
+ if (!ids || !ids.length) return;
1647
+ for (const id of ids) {
1648
+ const key = String(id);
1649
+ const entry = entryById ? entryById.get(key) : null;
1650
+ warmQueue.set(key, {
1651
+ stat: statsById ? statsById.get(key) || null : null,
1652
+ header: entry && entry.header || null
1653
+ });
1358
1654
  }
1359
- const projectionAvailable = typeof sq.readTitleSnapshot === "function" || typeof sq.readTitleSnapshots === "function";
1360
- if (!meta.cwd || !meta.title && !projectionAvailable) {
1361
- try {
1362
- let foldedTitle = null;
1363
- const summary = await persistence.inspectSession(key, {
1364
- onEvents: (events) => {
1365
- if (!foldedTitle) foldedTitle = foldTitle(events);
1655
+ scheduleWarm();
1656
+ }
1657
+ async function runWarm() {
1658
+ if (warmRunning) return;
1659
+ warmRunning = true;
1660
+ try {
1661
+ while (warmQueue.size) {
1662
+ const batch = [...warmQueue.entries()].slice(0, WARM_CHUNK);
1663
+ for (const [id] of batch) warmQueue.delete(id);
1664
+ try {
1665
+ const ids = batch.map(([id]) => id);
1666
+ const snapshotById = await projectTitles(ids);
1667
+ const decoded = /* @__PURE__ */ new Map();
1668
+ const statsById = /* @__PURE__ */ new Map();
1669
+ for (const [id, desc] of batch) {
1670
+ const snapshot = snapshotById.has(id) ? snapshotById.get(id) : null;
1671
+ const meta = metaFromSnapshot(snapshot);
1672
+ if (desc.header) {
1673
+ if (typeof desc.header.cwd === "string") meta.cwd = desc.header.cwd;
1674
+ if (desc.header.createdAt != null) meta.createdAt = desc.header.createdAt;
1675
+ }
1676
+ metaCache.set(id, desc.stat, meta);
1677
+ statsById.set(id, desc.stat);
1678
+ if (desc.stat) decoded.set(id, meta);
1366
1679
  }
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;
1680
+ await persistDecoded(decoded, statsById);
1681
+ } catch (e) {
1371
1682
  }
1372
- if (!meta.title && foldedTitle) meta.title = foldedTitle;
1373
- } catch (e2) {
1683
+ await new Promise((resolve) => setImmediate(resolve));
1374
1684
  }
1685
+ } finally {
1686
+ warmRunning = false;
1687
+ if (warmQueue.size) scheduleWarm();
1375
1688
  }
1376
- metaCache.set(key, statInfo, meta);
1377
- if (opts.collectDecoded && statInfo) opts.collectDecoded(key, meta);
1378
- return buildItem(key, meta, usage, opts.exposeUsage);
1379
1689
  }
1380
1690
  async function collectUsage(preloadedEntries) {
1381
1691
  const sizeById = /* @__PURE__ */ new Map();
@@ -1399,14 +1709,14 @@ function apply(ctx) {
1399
1709
  if (!id) return;
1400
1710
  if (entry && typeof entry.revision === "string" && entry.revision) {
1401
1711
  if (Number.isFinite(entry.sizeBytes)) sizeById.set(id, Number(entry.sizeBytes));
1402
- statsById.set(id, { revision: entry.revision });
1712
+ statsById.set(id, Number.isFinite(entry.sizeBytes) ? { revision: entry.revision, sizeBytes: Number(entry.sizeBytes) } : { revision: entry.revision });
1403
1713
  return;
1404
1714
  }
1405
1715
  if (entry && Number.isFinite(entry.sizeBytes)) sizeById.set(id, Number(entry.sizeBytes));
1406
1716
  try {
1407
1717
  const loc = persistence.locate(header);
1408
1718
  if (!loc || typeof loc.path !== "string" || !loc.path) return;
1409
- const st = await stat2(loc.path);
1719
+ const st = await stat4(loc.path);
1410
1720
  if (!st) return;
1411
1721
  if (typeof st.size === "number") sizeById.set(id, st.size);
1412
1722
  if (typeof st.mtimeMs === "number" && st.mtimeMs > 0) {
@@ -1437,7 +1747,7 @@ function apply(ctx) {
1437
1747
  }
1438
1748
  async function readTrashStore() {
1439
1749
  try {
1440
- return normalizeTrashStore(JSON.parse(readFileSync3(TRASH_INDEX, "utf8")));
1750
+ return normalizeTrashStore(JSON.parse(readFileSync4(TRASH_INDEX, "utf8")));
1441
1751
  } catch (e) {
1442
1752
  return normalizeTrashStore(null);
1443
1753
  }
@@ -1446,10 +1756,10 @@ function apply(ctx) {
1446
1756
  return (await readTrashStore()).items;
1447
1757
  }
1448
1758
  async function writeTrashStore(store) {
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);
1759
+ await mkdir6(TRASH_DIR, { recursive: true });
1760
+ const tmp = join8(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
1761
+ await writeFile6(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
1762
+ await rename6(tmp, TRASH_INDEX);
1453
1763
  }
1454
1764
  function mutateTrash(mutator) {
1455
1765
  const operation = trashMutation.then(async () => {
@@ -1463,7 +1773,10 @@ function apply(ctx) {
1463
1773
  return operation;
1464
1774
  }
1465
1775
  const stars = createStarIndex();
1776
+ const pendingMoves = createPendingMoveStore();
1466
1777
  const autoArchive = createAutoArchiveStore();
1778
+ sweepStaleStateTemps([STATE_DIR, TRASH_DIR], {}).catch(() => {
1779
+ });
1467
1780
  async function gcStars(validIds) {
1468
1781
  try {
1469
1782
  const store = await stars.read();
@@ -1550,10 +1863,10 @@ function apply(ctx) {
1550
1863
  let exists = false;
1551
1864
  let header = null;
1552
1865
  try {
1553
- const stat3 = await persistence.statSession(sid);
1554
- if (stat3) {
1866
+ const stat5 = await persistence.statSession(sid);
1867
+ if (stat5) {
1555
1868
  exists = true;
1556
- header = stat3.header;
1869
+ header = stat5.header;
1557
1870
  }
1558
1871
  } catch (e) {
1559
1872
  }
@@ -1584,7 +1897,7 @@ function apply(ctx) {
1584
1897
  if (loc && typeof loc.path === "string") originalPath = loc.path;
1585
1898
  }
1586
1899
  if (originalPath) {
1587
- const existsOnDisk = await stat2(originalPath).then(() => true).catch(() => false);
1900
+ const existsOnDisk = await stat4(originalPath).then(() => true).catch(() => false);
1588
1901
  if (!existsOnDisk) {
1589
1902
  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
1903
  }
@@ -1697,7 +2010,7 @@ function apply(ctx) {
1697
2010
  await purgeSessionArtifacts(sp, sid, locatedHeader);
1698
2011
  } else if (target) {
1699
2012
  try {
1700
- await unlink2(target);
2013
+ await unlink(target);
1701
2014
  } catch (e) {
1702
2015
  if (e && e.code !== "ENOENT") throw new Error("\u5220\u9664\u6587\u4EF6\u5931\u8D25\uFF1A" + String(e && e.message || e));
1703
2016
  }
@@ -1730,8 +2043,10 @@ function apply(ctx) {
1730
2043
  purged = true;
1731
2044
  });
1732
2045
  if (!purged) throw new Error("\u5F7B\u5E95\u5220\u9664\u5931\u8D25");
1733
- stars.removeIds([sid]).catch(() => {
1734
- });
2046
+ try {
2047
+ await stars.removeIds([sid]);
2048
+ } catch (e) {
2049
+ }
1735
2050
  return { ok: true, purged: true };
1736
2051
  }
1737
2052
  async function trashSettings(next) {
@@ -1767,8 +2082,8 @@ function apply(ctx) {
1767
2082
  async function moveTargetWorkspace(rawPath) {
1768
2083
  if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
1769
2084
  let p = String(rawPath).trim();
1770
- if (p.startsWith("~/")) p = join6(homedir3(), p.slice(2));
1771
- if (!isAbsolute(p)) p = join6(homedir3(), p);
2085
+ if (p.startsWith("~/")) p = join8(homedir4(), p.slice(2));
2086
+ if (!isAbsolute(p)) p = join8(homedir4(), p);
1772
2087
  let canonical = null;
1773
2088
  try {
1774
2089
  canonical = await realpath(p);
@@ -1776,16 +2091,19 @@ function apply(ctx) {
1776
2091
  canonical = null;
1777
2092
  }
1778
2093
  if (canonical === null) {
1779
- await mkdir5(p, { recursive: true });
2094
+ await mkdir6(p, { recursive: true });
1780
2095
  canonical = await realpath(p);
1781
2096
  }
1782
- return { canonical, entity: await w.create(canonical, basename2(canonical) || "workspace") };
2097
+ return { canonical, entity: await w.create(canonical, basename3(canonical) || "workspace") };
1783
2098
  }
1784
2099
  async function moveOne(sid, targetPath) {
1785
2100
  requireCapability(capabilities, "move");
1786
2101
  const activeId = getActiveSessionId(ctx);
1787
2102
  if (activeId != null && String(activeId) === String(sid)) {
1788
- throw new Error("\u8BE5\u4F1A\u8BDD\u5F53\u524D\u5904\u4E8E\u6253\u5F00\u72B6\u6001\uFF0C\u8BF7\u5148\u5207\u6362\u5230\u522B\u7684\u4F1A\u8BDD\u518D\u79FB\u52A8\u3002");
2103
+ const error = new Error("\u4F1A\u8BDD\u6B63\u88AB DSH \u6253\u5F00\uFF0C\u6682\u65F6\u65E0\u6CD5\u79FB\u52A8\uFF1B\u8BF7\u91CD\u542F DSH \u540E\u518D\u8BD5\u3002");
2104
+ error.status = 409;
2105
+ error.code = "DSM_SESSION_BUSY";
2106
+ throw error;
1789
2107
  }
1790
2108
  const r = await persistence.readSession(sid, 0);
1791
2109
  if (!r || !r.meta) throw new Error("\u65E0\u6CD5\u8BFB\u53D6\u8BE5\u4F1A\u8BDD\u7684\u65E5\u5FD7");
@@ -1805,6 +2123,7 @@ function apply(ctx) {
1805
2123
  }
1806
2124
  }
1807
2125
  const newHeader = Object.assign({}, meta, { cwd: canonical });
2126
+ const moveNotes = [];
1808
2127
  const live = ctx.get("sessions");
1809
2128
  const liveObj = live && live.get && live.get(sid);
1810
2129
  const isOpen = !!liveObj;
@@ -1817,14 +2136,14 @@ function apply(ctx) {
1817
2136
  const stagedPath = `${newPath}.move-stage-${process.pid}-${Date.now()}`;
1818
2137
  let destinationInstalled = false;
1819
2138
  try {
1820
- await mkdir5(dirname2(newPath), { recursive: true });
2139
+ await mkdir6(dirname3(newPath), { recursive: true });
1821
2140
  try {
1822
- await stat2(newPath);
2141
+ await stat4(newPath);
1823
2142
  throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u76EE\u6807\u4F4D\u7F6E\u5DF2\u5B58\u5728\u540C\u540D\u4F1A\u8BDD\u65E5\u5FD7");
1824
2143
  } catch (e) {
1825
2144
  if (e && e.code !== "ENOENT") throw e;
1826
2145
  }
1827
- await rename5(oldPath, backupPath);
2146
+ await rename6(oldPath, backupPath);
1828
2147
  const original = await readFile3(backupPath);
1829
2148
  const originalFrames = scanZstdFrames(original).frames;
1830
2149
  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");
@@ -1834,23 +2153,25 @@ function apply(ctx) {
1834
2153
  const originalTail = original.subarray(originalFrames[0].end);
1835
2154
  const rewrittenTail = rewritten.subarray(rewrittenFrames[0].end);
1836
2155
  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");
1837
- await writeFile5(stagedPath, rewritten, { mode: 384 });
1838
- await rename5(stagedPath, newPath);
2156
+ await writeFile6(stagedPath, rewritten, { mode: 384 });
2157
+ await rename6(stagedPath, newPath);
1839
2158
  destinationInstalled = true;
1840
- await unlink2(backupPath);
2159
+ await unlink(backupPath);
2160
+ const keptInSource = await cleanupMovedSourceDir(dirname3(oldPath));
2161
+ if (keptInSource.length > 0) moveNotes.push(`\u6E90\u76EE\u5F55\u672A\u80FD\u6E05\u7406\u5E72\u51C0\uFF08\u6B8B\u7559 ${keptInSource.join("\u3001")}\uFF09\uFF0C\u82E5\u540E\u7EED\u79FB\u52A8\u62A5\u201Cduplicate\u201D\u8BF7\u624B\u52A8\u6E05\u7A7A\u8BE5\u76EE\u5F55\u3002`);
1841
2162
  } catch (e) {
1842
2163
  try {
1843
- await unlink2(stagedPath);
2164
+ await unlink(stagedPath);
1844
2165
  } catch (_) {
1845
2166
  }
1846
2167
  if (destinationInstalled) {
1847
2168
  try {
1848
- await unlink2(newPath);
2169
+ await unlink(newPath);
1849
2170
  } catch (_) {
1850
2171
  }
1851
2172
  }
1852
2173
  try {
1853
- await rename5(backupPath, oldPath);
2174
+ await rename6(backupPath, oldPath);
1854
2175
  } catch (_) {
1855
2176
  }
1856
2177
  if (e && e.code !== "ENOENT") throw e;
@@ -1874,6 +2195,20 @@ function apply(ctx) {
1874
2195
  }
1875
2196
  return null;
1876
2197
  };
2198
+ const cleanupMovedSourceDir = async (dir) => {
2199
+ let entries = null;
2200
+ try {
2201
+ entries = await readdir4(dir);
2202
+ } catch (e) {
2203
+ return [];
2204
+ }
2205
+ try {
2206
+ await rm3(dir, { recursive: true, force: true });
2207
+ } catch (e) {
2208
+ return entries;
2209
+ }
2210
+ return [];
2211
+ };
1877
2212
  if (persistence.kind === "session-handle") {
1878
2213
  const stat1 = await persistence.statSession(sid);
1879
2214
  if (stat1 && stat1.revision) {
@@ -1883,7 +2218,13 @@ function apply(ctx) {
1883
2218
  }
1884
2219
  }
1885
2220
  try {
1886
- await moveSessionToCwd({ sp, sid, header: meta, canonical, events, inheritedEventCount: r.inheritedEventCount });
2221
+ const movedHandle = await moveSessionToCwd({ sp, sid, header: meta, canonical, events, inheritedEventCount: r.inheritedEventCount });
2222
+ if (movedHandle && Array.isArray(movedHandle.reclaimedSiblings) && movedHandle.reclaimedSiblings.length > 0) {
2223
+ moveNotes.push(`\u5DF2\u987A\u5E26\u56DE\u6536 ${movedHandle.reclaimedSiblings.length} \u5904\u88AB\u53D6\u4EE3\u7684\u65E7\u4EE3\u65E5\u5FD7\u526F\u672C\uFF08\u5386\u53F2\u79FB\u52A8\u6B8B\u7559\uFF09\uFF0C\u907F\u514D\u540C\u4E00\u4F1A\u8BDD\u5728\u591A\u76EE\u5F55\u91CD\u590D\u3002`);
2224
+ }
2225
+ if (movedHandle && movedHandle.sourceCleanup && movedHandle.sourceCleanup.cleaned === false) {
2226
+ moveNotes.push(`\u6E90\u76EE\u5F55\u672A\u80FD\u6E05\u7406\u5E72\u51C0\uFF08\u6B8B\u7559 ${movedHandle.sourceCleanup.leftover.join("\u3001")}\uFF09\uFF0C\u82E5\u540E\u7EED\u79FB\u52A8\u62A5\u201Cduplicate\u201D\u8BF7\u624B\u52A8\u6E05\u7A7A\u8BE5\u76EE\u5F55\u3002`);
2227
+ }
1887
2228
  } catch (e) {
1888
2229
  if (e && e.status) throw e;
1889
2230
  throw new Error("\u79FB\u52A8\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A" + String(e && e.message || e));
@@ -1910,7 +2251,7 @@ function apply(ctx) {
1910
2251
  const backupPath = oldPath ? `${oldPath}.move-backup-${Date.now()}` : null;
1911
2252
  if (backupPath) {
1912
2253
  try {
1913
- await rename5(oldPath, backupPath);
2254
+ await rename6(oldPath, backupPath);
1914
2255
  } catch (e) {
1915
2256
  if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
1916
2257
  }
@@ -1918,7 +2259,7 @@ function apply(ctx) {
1918
2259
  const restore = async () => {
1919
2260
  if (backupPath) {
1920
2261
  try {
1921
- await rename5(backupPath, oldPath);
2262
+ await rename6(backupPath, oldPath);
1922
2263
  } catch (_) {
1923
2264
  }
1924
2265
  }
@@ -1932,10 +2273,14 @@ function apply(ctx) {
1932
2273
  }
1933
2274
  if (backupPath) {
1934
2275
  try {
1935
- await unlink2(backupPath);
2276
+ await unlink(backupPath);
1936
2277
  } catch (e) {
1937
2278
  }
1938
2279
  }
2280
+ if (oldPath) {
2281
+ const keptInSource = await cleanupMovedSourceDir(dirname3(oldPath));
2282
+ if (keptInSource.length > 0) moveNotes.push(`\u6E90\u76EE\u5F55\u672A\u80FD\u6E05\u7406\u5E72\u51C0\uFF08\u6B8B\u7559 ${keptInSource.join("\u3001")}\uFF09\uFF0C\u82E5\u540E\u7EED\u79FB\u52A8\u62A5\u201Cduplicate\u201D\u8BF7\u624B\u52A8\u6E05\u7A7A\u8BE5\u76EE\u5F55\u3002`);
2283
+ }
1939
2284
  } catch (e) {
1940
2285
  if (ALREADY_EXISTS_RE.test(String(e && e.message || e))) {
1941
2286
  await restore();
@@ -1979,9 +2324,107 @@ function apply(ctx) {
1979
2324
  moved: true,
1980
2325
  workspaceId: target.id,
1981
2326
  workspaceTitle: target.title,
1982
- workspacePath: canonical
2327
+ workspacePath: canonical,
2328
+ ...moveNotes.length > 0 ? { notes: moveNotes } : {}
1983
2329
  };
1984
2330
  }
2331
+ const QUEUE_NOTE = "\u5DF2\u6392\u961F\uFF1A\u8BE5\u4F1A\u8BDD\u6B63\u88AB DSH \u6253\u5F00\uFF08\u5199\u6240\u6709\u6743\u53EA\u5728 DSH \u9000\u51FA\u65F6\u91CA\u653E\uFF09\u3002\u91CD\u542F DSH \u65F6\u4F1A\u81EA\u52A8\u5B8C\u6210\uFF0C\u8BF7\u5148\u522B\u6253\u5F00\u5B83\u3002";
2332
+ function isBusyError(e) {
2333
+ if (!e) return false;
2334
+ if (e.code === "DSM_SESSION_BUSY") return true;
2335
+ const text = String(e && e.message || e);
2336
+ return /正被 DSH 打开|already owned/i.test(text);
2337
+ }
2338
+ async function moveOrQueue(sid, targetPath) {
2339
+ try {
2340
+ return await moveOne(sid, targetPath);
2341
+ } catch (e) {
2342
+ if (!isBusyError(e)) throw e;
2343
+ await pendingMoves.queue(sid, targetPath);
2344
+ return { ok: true, moved: false, queued: true, notes: [QUEUE_NOTE] };
2345
+ }
2346
+ }
2347
+ const BOOT_WARMUP_MS = 3e4;
2348
+ const bootedAt = Date.now();
2349
+ let queueRunning = false;
2350
+ async function runPendingMoves(reason) {
2351
+ if (queueRunning) return { ran: false, moved: 0, kept: 0 };
2352
+ queueRunning = true;
2353
+ let moved = 0;
2354
+ const notes = [];
2355
+ try {
2356
+ const items = await pendingMoves.list();
2357
+ for (const item of items) {
2358
+ try {
2359
+ await moveOne(item.sessionId, item.targetPath);
2360
+ await pendingMoves.remove([item.sessionId]);
2361
+ metaCache.invalidate(item.sessionId);
2362
+ moved++;
2363
+ notes.push(`\u6392\u961F\u4E2D\u7684\u79FB\u52A8\u5DF2\u5B8C\u6210\uFF1A${String(item.sessionId).slice(0, 18)}\u2026`);
2364
+ } catch (e) {
2365
+ if (isBusyError(e)) continue;
2366
+ if (Date.now() - bootedAt < BOOT_WARMUP_MS) continue;
2367
+ const bumped = await pendingMoves.bumpAttempts(item.sessionId);
2368
+ if (bumped && bumped.dropped) notes.push(`\u6392\u961F\u4E2D\u7684\u79FB\u52A8\u591A\u6B21\u5931\u8D25\u5DF2\u653E\u5F03\uFF1A${String(item.sessionId).slice(0, 18)}\u2026\uFF08${String(e && e.message || e)}\uFF09`);
2369
+ }
2370
+ }
2371
+ if (moved > 0) {
2372
+ try {
2373
+ await reindexRegistry();
2374
+ } catch (e) {
2375
+ }
2376
+ }
2377
+ } finally {
2378
+ queueRunning = false;
2379
+ }
2380
+ if (notes.length > 0) {
2381
+ try {
2382
+ for (const n of notes) console.warn("[dsh-sessions-manager] " + n);
2383
+ } catch (e) {
2384
+ }
2385
+ }
2386
+ void reason;
2387
+ return { ran: true, moved, kept: (await pendingMoves.list()).length };
2388
+ }
2389
+ const queueEffect = typeof ctx.effect === "function" ? ctx.effect.bind(ctx) : ((fn) => {
2390
+ fn();
2391
+ });
2392
+ if (typeof ctx.on === "function") {
2393
+ queueEffect(() => ctx.on("session/disposed", (session) => {
2394
+ const id = session && session.id != null ? String(session.id) : null;
2395
+ if (!id) return;
2396
+ pendingMoves.has(id).then((queued) => {
2397
+ if (!queued) return;
2398
+ const t = setTimeout(() => {
2399
+ runPendingMoves("session-disposed").catch(() => {
2400
+ });
2401
+ }, 500);
2402
+ if (t && typeof t.unref === "function") t.unref();
2403
+ }).catch(() => {
2404
+ });
2405
+ }));
2406
+ }
2407
+ queueEffect(() => {
2408
+ const timers = [0, 1e3, 3e3, 6e3, 12e3, 3e4].map((ms) => {
2409
+ const t = setTimeout(() => {
2410
+ runPendingMoves(ms === 0 ? "startup" : `startup+${ms}`).catch(() => {
2411
+ });
2412
+ }, ms);
2413
+ if (t && typeof t.unref === "function") t.unref();
2414
+ return t;
2415
+ });
2416
+ const tickTimer = setInterval(() => {
2417
+ pendingMoves.list().then((items) => {
2418
+ if (items.length > 0) return runPendingMoves("tick");
2419
+ }).catch(() => {
2420
+ });
2421
+ }, 12e4);
2422
+ if (tickTimer && typeof tickTimer.unref === "function") tickTimer.unref();
2423
+ return () => {
2424
+ for (const t of timers) clearTimeout(t);
2425
+ clearInterval(tickTimer);
2426
+ };
2427
+ });
1985
2428
  async function reindexRegistry() {
1986
2429
  const reg = w;
1987
2430
  if (!reg || typeof reg.replaceHeaderIndex !== "function") return false;
@@ -2067,35 +2510,48 @@ function apply(ctx) {
2067
2510
  wsByPath = {};
2068
2511
  }
2069
2512
  const currentArchived = new Set((await archivedState().catch(() => ({ archivedSessionIds: [] }))).archivedSessionIds || []);
2070
- const items = [];
2071
2513
  const usage = await collectUsage(entries);
2072
2514
  const statsById = new Map(visibleIds.map((id) => [
2073
2515
  id,
2074
2516
  usage.statsById && usage.statsById.get(id) || { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }
2075
2517
  ]));
2076
- const { missing } = metaCache.partition(visibleIds, statsById);
2518
+ const { cached, missing } = metaCache.partition(visibleIds, statsById);
2077
2519
  const persisted = await hydrateFromPersist(missing, statsById);
2078
- for (const [id, meta] of persisted) metaCache.set(id, statsById.get(id), meta);
2520
+ for (const [id, meta] of persisted) {
2521
+ const entry = entryById.get(id);
2522
+ const header = entry && entry.header ? entry.header : null;
2523
+ metaCache.set(id, statsById.get(id), {
2524
+ title: meta.title,
2525
+ cwd: header && typeof header.cwd === "string" && header.cwd ? header.cwd : meta.cwd,
2526
+ createdAt: header && header.createdAt != null ? header.createdAt : meta.createdAt
2527
+ });
2528
+ }
2079
2529
  const stillMissing = missing.filter((id) => !persisted.has(id));
2080
- const snapshotById = await projectTitles(stillMissing);
2081
- const decoded = /* @__PURE__ */ new Map();
2082
- const collectDecoded = (id, meta) => {
2083
- decoded.set(id, meta);
2084
- };
2085
- const CHUNK = 6;
2086
- for (let i = 0; i < visibleIds.length; i += CHUNK) {
2087
- const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map((id) => {
2530
+ if (stillMissing.length) enqueueWarm(stillMissing, statsById, entryById);
2531
+ const items = [];
2532
+ for (const id of visibleIds) {
2533
+ let meta = metaCache.get(id, statsById.get(id)) || persisted.get(id) || null;
2534
+ if (!meta) {
2535
+ meta = { title: null, cwd: null, createdAt: null };
2088
2536
  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
- }));
2096
- for (const it of res2) items.push({ ...it, archived: currentArchived.has(it.sessionId) });
2537
+ const header = entry && entry.header ? entry.header : null;
2538
+ if (header) {
2539
+ if (typeof header.cwd === "string") meta.cwd = header.cwd;
2540
+ if (header.createdAt != null) meta.createdAt = header.createdAt;
2541
+ }
2542
+ }
2543
+ const it = buildItem(id, meta, usage, !!(opts && opts.usage));
2544
+ items.push({ ...it, archived: currentArchived.has(it.sessionId) });
2545
+ }
2546
+ if (opts && opts.onlyArchived) {
2547
+ const archivedItems = [];
2548
+ for (const it of items) {
2549
+ if (!it.archived) continue;
2550
+ const { archived: _drop, ...rest } = it;
2551
+ archivedItems.push(rest);
2552
+ }
2553
+ return { items: archivedItems, usage };
2097
2554
  }
2098
- persistDecoded(decoded, statsById);
2099
2555
  let starredSet = /* @__PURE__ */ new Set();
2100
2556
  try {
2101
2557
  starredSet = new Set((await stars.read()).starredSessionIds);
@@ -2173,43 +2629,82 @@ function apply(ctx) {
2173
2629
  const activeTombstones = store.purgedSessionIds.map(String).filter((id) => !present.has(id));
2174
2630
  if (ids.length) {
2175
2631
  const usage = await collectUsage(entries);
2632
+ const entryById = new Map(entries.map((entry) => [entry.id, entry]));
2176
2633
  const statsById = new Map(ids.map((id) => [
2177
2634
  id,
2178
2635
  usage.statsById && usage.statsById.get(id) || { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }
2179
2636
  ]));
2180
2637
  const { cached, missing } = metaCache.partition(ids, statsById);
2181
2638
  const persisted = await hydrateFromPersist(missing, statsById);
2182
- for (const [id, meta] of persisted) metaCache.set(id, statsById.get(id), meta);
2639
+ for (const [id, meta] of persisted) {
2640
+ const entry = entryById.get(id);
2641
+ const header = entry && entry.header ? entry.header : null;
2642
+ metaCache.set(id, statsById.get(id), {
2643
+ title: meta.title,
2644
+ cwd: header && typeof header.cwd === "string" && header.cwd ? header.cwd : meta.cwd,
2645
+ createdAt: header && header.createdAt != null ? header.createdAt : meta.createdAt
2646
+ });
2647
+ }
2183
2648
  const rest = missing.filter((id) => !persisted.has(id));
2184
- const snapshotById = await projectTitles(rest);
2185
- const decoded = /* @__PURE__ */ new Map();
2186
- const collectDecoded = (id, meta) => {
2187
- decoded.set(id, meta);
2188
- };
2649
+ if (rest.length) enqueueWarm(rest, statsById, entryById);
2189
2650
  for (const id of ids) {
2190
- let meta = cached.get(id) || persisted.get(id) || null;
2191
- if (!meta) {
2192
- const entry = entries.find((e) => e.id === id);
2193
- const snapshot = snapshotById.has(id) ? snapshotById.get(id) : typeof sq.readTitleSnapshot === "function" ? await sq.readTitleSnapshot(id).catch(() => null) : null;
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
- }
2199
- metaCache.set(id, statsById.get(id), next);
2200
- if (statsById.get(id)) collectDecoded(id, next);
2201
- meta = next;
2202
- }
2651
+ const meta = metaCache.get(id, statsById.get(id)) || persisted.get(id) || null;
2203
2652
  if (meta && meta.title) authorityTitleCache.set(id, String(meta.title));
2204
2653
  }
2205
- persistDecoded(decoded, statsById);
2206
2654
  }
2655
+ const lineage = {};
2656
+ for (const entry of entries) {
2657
+ const info = classifyLineage(entry.header, entry.sizeBytes);
2658
+ if (info) lineage[entry.id] = info;
2659
+ }
2660
+ await refineEmptyLineage(lineage, entries);
2207
2661
  return {
2208
2662
  titles: Object.fromEntries(authorityTitleCache),
2209
2663
  trashedSessionIds: store.items.map((item) => String(item.sessionId)),
2210
- purgedSessionIds: activeTombstones
2664
+ purgedSessionIds: activeTombstones,
2665
+ lineage
2211
2666
  };
2212
2667
  }
2668
+ const emptyScanCache = /* @__PURE__ */ new Map();
2669
+ async function refineEmptyLineage(lineage, entries) {
2670
+ if (!persistence || typeof persistence.inspectSession !== "function") return;
2671
+ for (const entry of entries) {
2672
+ const size = entry && Number.isFinite(entry.sizeBytes) ? entry.sizeBytes : null;
2673
+ if (size === null || size > EMPTY_DECODE_LIMIT) continue;
2674
+ const id = String(entry.id);
2675
+ let scanned = emptyScanCache.get(id);
2676
+ if (!scanned || scanned.sizeBytes !== size) {
2677
+ let isEmpty = false;
2678
+ try {
2679
+ const types = [];
2680
+ await persistence.inspectSession(id, { onEvents: (batch) => {
2681
+ for (const ev of batch || []) if (types.length < 64) types.push(ev && ev.type);
2682
+ } });
2683
+ isEmpty = isEmptyEventTypes(types);
2684
+ } catch (e) {
2685
+ isEmpty = false;
2686
+ }
2687
+ scanned = { sizeBytes: size, empty: isEmpty };
2688
+ emptyScanCache.set(id, scanned);
2689
+ }
2690
+ if (scanned.empty) {
2691
+ const info = lineage[id] || (lineage[id] = { origin: null, parentSession: null, delegationDepth: 0, empty: false });
2692
+ info.empty = true;
2693
+ } else if (lineage[id]) {
2694
+ const info = lineage[id];
2695
+ info.empty = false;
2696
+ if (!info.origin && !info.parentSession) delete lineage[id];
2697
+ }
2698
+ }
2699
+ const live = /* @__PURE__ */ new Set();
2700
+ for (const entry of entries) {
2701
+ const size = entry && Number.isFinite(entry.sizeBytes) ? entry.sizeBytes : null;
2702
+ if (size !== null && size <= EMPTY_DECODE_LIMIT) live.add(String(entry.id));
2703
+ }
2704
+ for (const key of emptyScanCache.keys()) {
2705
+ if (!live.has(key)) emptyScanCache.delete(key);
2706
+ }
2707
+ }
2213
2708
  async function buildDetails(sid, signal) {
2214
2709
  const sessions = ctx.get("sessions");
2215
2710
  const live = sessions && sessions.get(sid);
@@ -2294,8 +2789,8 @@ function apply(ctx) {
2294
2789
  let sizeBytes = null;
2295
2790
  if (live === void 0) {
2296
2791
  try {
2297
- const stat3 = await persistence.statSession(sid);
2298
- if (stat3 && Number.isFinite(stat3.sizeBytes)) sizeBytes = stat3.sizeBytes;
2792
+ const stat5 = await persistence.statSession(sid);
2793
+ if (stat5 && Number.isFinite(stat5.sizeBytes)) sizeBytes = stat5.sizeBytes;
2299
2794
  } catch (e) {
2300
2795
  }
2301
2796
  }
@@ -2303,7 +2798,7 @@ function apply(ctx) {
2303
2798
  try {
2304
2799
  const loc = persistence.locate(meta);
2305
2800
  if (loc && typeof loc.path === "string" && loc.path) {
2306
- const st = await stat2(loc.path);
2801
+ const st = await stat4(loc.path);
2307
2802
  if (st && typeof st.size === "number") sizeBytes = st.size;
2308
2803
  }
2309
2804
  } catch (e) {
@@ -2312,7 +2807,7 @@ function apply(ctx) {
2312
2807
  }
2313
2808
  if (stats.fetches.length > MAX_FETCHES) stats.fetches = stats.fetches.slice(0, MAX_FETCHES);
2314
2809
  const fileEntries = [...fileSet.entries()].slice(0, MAX_FILES * 2);
2315
- const exists = await Promise.all(fileEntries.map(([p]) => stat2(p).then(() => true).catch(() => false)));
2810
+ const exists = await Promise.all(fileEntries.map(([p]) => stat4(p).then(() => true).catch(() => false)));
2316
2811
  const files = fileEntries.filter((_, i) => exists[i]).map(([path, tool]) => ({ path, tool })).slice(0, MAX_FILES);
2317
2812
  const lineage = {
2318
2813
  parentSessionId: meta && typeof meta.parentSession === "string" ? meta.parentSession : null,
@@ -2364,7 +2859,7 @@ function apply(ctx) {
2364
2859
  disposers.push(ctx.webServer.register({
2365
2860
  kind: "exact",
2366
2861
  path: "/archived-sessions/capabilities",
2367
- handler: async (req, res) => json(res, capabilities)
2862
+ handler: async (req, res) => json(res, { ...capabilities, buildStamp: BUILD_STAMP })
2368
2863
  }));
2369
2864
  disposers.push(ctx.webServer.register({
2370
2865
  kind: "exact",
@@ -2395,12 +2890,10 @@ function apply(ctx) {
2395
2890
  } catch (e) {
2396
2891
  wsByPath = {};
2397
2892
  }
2398
- const items = [];
2399
- const CHUNK = 6;
2400
- for (let i = 0; i < idStrs.length; i += CHUNK) {
2401
- const res2 = await Promise.all(idStrs.slice(i, i + CHUNK).map((id) => resolveOne(id)));
2402
- items.push.apply(items, res2);
2403
- }
2893
+ const wanted = new Set(idStrs);
2894
+ const detailed = await allSessionItemsDetailed();
2895
+ const items = detailed.items.filter((it) => wanted.has(String(it.sessionId)) && it.archived);
2896
+ for (const it of items) delete it.archived;
2404
2897
  json(res, { items });
2405
2898
  } catch (e) {
2406
2899
  json(res, { error: String(e && e.message || e) }, 500);
@@ -2520,7 +3013,7 @@ function apply(ctx) {
2520
3013
  if (typeof item.originalPath !== "string" || !item.originalPath) {
2521
3014
  return { sessionId: item.sessionId, status: "unverified", originalPath: null };
2522
3015
  }
2523
- const exists = await stat2(item.originalPath).then(() => true).catch(() => false);
3016
+ const exists = await stat4(item.originalPath).then(() => true).catch(() => false);
2524
3017
  return { sessionId: item.sessionId, status: exists ? "ok" : "missing", originalPath: item.originalPath };
2525
3018
  }));
2526
3019
  json(res, {
@@ -2561,8 +3054,10 @@ function apply(ctx) {
2561
3054
  if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
2562
3055
  const out = await purgeFromTrash(sid);
2563
3056
  metaCache.invalidate(sid);
2564
- titleIndex.remove([sid]).catch(() => {
2565
- });
3057
+ try {
3058
+ await titleIndex.remove([sid]);
3059
+ } catch (e) {
3060
+ }
2566
3061
  json(res, out);
2567
3062
  } catch (e) {
2568
3063
  json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
@@ -2582,7 +3077,7 @@ function apply(ctx) {
2582
3077
  try {
2583
3078
  results.push({ sessionId: sid, ok: true, ...await purgeFromTrash(sid) });
2584
3079
  metaCache.invalidate(sid);
2585
- titleIndex.remove([sid]).catch(() => {
3080
+ await titleIndex.remove([sid]).catch(() => {
2586
3081
  });
2587
3082
  } catch (e) {
2588
3083
  results.push({ sessionId: sid, ok: false, error: String(e && e.message || e) });
@@ -2672,6 +3167,108 @@ function apply(ctx) {
2672
3167
  }
2673
3168
  }
2674
3169
  }));
3170
+ disposers.push(ctx.webServer.register({
3171
+ kind: "exact",
3172
+ path: "/archived-sessions/lineage-tree",
3173
+ handler: async (req, res) => {
3174
+ try {
3175
+ const body = await readJsonBody(req);
3176
+ const rootId = body && typeof body.sessionId === "string" ? body.sessionId : "";
3177
+ if (!rootId || !isSafeSessionId2(rootId)) return json(res, { error: "missing sessionId" }, 400);
3178
+ const headerById = /* @__PURE__ */ new Map();
3179
+ const sizeById = /* @__PURE__ */ new Map();
3180
+ const trashedIds = /* @__PURE__ */ new Set();
3181
+ let purgedIds = [];
3182
+ const persistedIds = /* @__PURE__ */ new Set();
3183
+ try {
3184
+ const store = await readTrashStore();
3185
+ for (const item of store.items) trashedIds.add(String(item.sessionId));
3186
+ purgedIds = store.purgedSessionIds.map(String);
3187
+ } catch (e) {
3188
+ }
3189
+ const addHeader = (h, sizeBytes) => {
3190
+ if (!h || h.id == null) return;
3191
+ const id = String(h.id);
3192
+ if (headerById.has(id) || trashedIds.has(id)) return;
3193
+ headerById.set(id, {
3194
+ parentSession: h.parentSession != null ? String(h.parentSession) : null,
3195
+ subagent: h.origin === "subagent",
3196
+ delegationDepth: Number.isFinite(h.delegationDepth) ? h.delegationDepth : null,
3197
+ createdAt: typeof h.createdAt === "number" ? h.createdAt : null
3198
+ });
3199
+ if (Number.isFinite(sizeBytes)) sizeById.set(id, sizeBytes);
3200
+ };
3201
+ try {
3202
+ for (const entry of await persistence.listEntries()) {
3203
+ addHeader(entry.header, entry.sizeBytes);
3204
+ if (entry && entry.id != null) persistedIds.add(String(entry.id));
3205
+ }
3206
+ } catch (e) {
3207
+ }
3208
+ const liveIds = /* @__PURE__ */ new Set();
3209
+ const live = ctx.get("sessions");
3210
+ try {
3211
+ if (live && typeof live.list === "function") {
3212
+ live.list().forEach((s) => {
3213
+ liveIds.add(String(s.id));
3214
+ addHeader(s.header, void 0);
3215
+ });
3216
+ }
3217
+ } catch (e) {
3218
+ }
3219
+ for (const id of purgedIds) {
3220
+ if (!persistedIds.has(id)) headerById.delete(id);
3221
+ }
3222
+ const kidsOf = /* @__PURE__ */ new Map();
3223
+ for (const [id, h] of headerById) {
3224
+ if (!h.subagent || !h.parentSession) continue;
3225
+ if (!kidsOf.has(h.parentSession)) kidsOf.set(h.parentSession, []);
3226
+ kidsOf.get(h.parentSession).push(id);
3227
+ }
3228
+ const MAX_NODES = 500;
3229
+ let nodeCount = 0;
3230
+ const build = (id, depth, visited) => {
3231
+ nodeCount++;
3232
+ const h = headerById.get(id) || {};
3233
+ const childVisited = new Set(visited);
3234
+ childVisited.add(id);
3235
+ const kidIds = nodeCount >= MAX_NODES ? [] : (kidsOf.get(id) || []).filter((k) => !childVisited.has(k));
3236
+ return {
3237
+ sessionId: id,
3238
+ parentSession: h.parentSession || null,
3239
+ delegationDepth: h.delegationDepth,
3240
+ depth,
3241
+ createdAt: h.createdAt || null,
3242
+ sizeBytes: sizeById.has(id) ? sizeById.get(id) : null,
3243
+ live: liveIds.has(id),
3244
+ title: null,
3245
+ children: kidIds.map((k) => build(k, depth + 1, childVisited))
3246
+ };
3247
+ };
3248
+ const nodes = (kidsOf.get(rootId) || []).map((k) => build(k, 1, /* @__PURE__ */ new Set([rootId])));
3249
+ const allIds = [rootId];
3250
+ const walk = (n) => {
3251
+ allIds.push(n.sessionId);
3252
+ n.children.forEach(walk);
3253
+ };
3254
+ nodes.forEach(walk);
3255
+ const titles = await projectTitles(allIds);
3256
+ const titleOf = (id) => {
3257
+ const snap = titles.get(id);
3258
+ const m = snap ? metaFromSnapshot(snap) : null;
3259
+ return m && m.title || null;
3260
+ };
3261
+ const fill = (n) => {
3262
+ n.title = titleOf(n.sessionId);
3263
+ n.children.forEach(fill);
3264
+ };
3265
+ nodes.forEach(fill);
3266
+ json(res, { parent: { sessionId: rootId, title: titleOf(rootId) }, nodes });
3267
+ } catch (e) {
3268
+ json(res, { error: String(e && e.message || e) }, errorStatus(e));
3269
+ }
3270
+ }
3271
+ }));
2675
3272
  disposers.push(ctx.webServer.register({
2676
3273
  kind: "exact",
2677
3274
  path: "/archived-sessions/workspaces",
@@ -2693,7 +3290,8 @@ function apply(ctx) {
2693
3290
  const target = body && typeof body.targetPath === "string" ? body.targetPath : null;
2694
3291
  if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
2695
3292
  if (!target) return json(res, { ok: false, error: "missing targetPath" }, 400);
2696
- const moved = await moveOne(sid, target);
3293
+ const moved = await moveOrQueue(sid, target);
3294
+ if (moved.queued) return json(res, { sessionId: sid, ...moved });
2697
3295
  metaCache.invalidate(sid);
2698
3296
  try {
2699
3297
  await reindexRegistry();
@@ -2705,6 +3303,74 @@ function apply(ctx) {
2705
3303
  }
2706
3304
  }
2707
3305
  }));
3306
+ disposers.push(ctx.webServer.register({
3307
+ kind: "exact",
3308
+ path: "/archived-sessions/move-many",
3309
+ handler: async (req, res) => {
3310
+ try {
3311
+ const body = await readJsonBody(req);
3312
+ const ids = Array.isArray(body && body.sessionIds) ? [...new Set(body.sessionIds.filter((x) => typeof x === "string" && x))] : [];
3313
+ const target = body && typeof body.targetPath === "string" ? body.targetPath : null;
3314
+ if (!ids.length) return json(res, { ok: false, error: "missing sessionIds" }, 400);
3315
+ if (!target) return json(res, { ok: false, error: "missing targetPath" }, 400);
3316
+ let moved = 0;
3317
+ const failed = [];
3318
+ const queued = [];
3319
+ for (const sid of ids) {
3320
+ try {
3321
+ const r = await moveOrQueue(sid, target);
3322
+ if (r && r.queued) {
3323
+ queued.push(sid);
3324
+ continue;
3325
+ }
3326
+ metaCache.invalidate(sid);
3327
+ moved++;
3328
+ } catch (e) {
3329
+ const err = e && e.code ? { sessionId: sid, error: String(e && e.message || e), code: e.code } : { sessionId: sid, error: String(e && e.message || e) };
3330
+ failed.push(err);
3331
+ }
3332
+ }
3333
+ try {
3334
+ await reindexRegistry();
3335
+ } catch (e) {
3336
+ }
3337
+ json(res, {
3338
+ moved,
3339
+ failed,
3340
+ ...queued.length > 0 ? { queued, notes: [`${queued.length} \u4E2A\u4F1A\u8BDD\u5DF2\u6392\u961F\uFF08\u6B63\u88AB DSH \u6253\u5F00\uFF09\uFF0C\u91CD\u542F DSH \u540E\u4F1A\u81EA\u52A8\u5B8C\u6210\uFF1B\u91CD\u542F\u540E\u8BF7\u5148\u522B\u6253\u5F00\u5B83\u4EEC\u3002`] } : {}
3341
+ });
3342
+ } catch (e) {
3343
+ json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
3344
+ }
3345
+ }
3346
+ }));
3347
+ disposers.push(ctx.webServer.register({
3348
+ kind: "exact",
3349
+ path: "/archived-sessions/pending-moves",
3350
+ handler: async (req, res) => {
3351
+ try {
3352
+ const items = await pendingMoves.list();
3353
+ json(res, { items: items.map((i) => ({ sessionId: i.sessionId, targetPath: i.targetPath, queuedAt: i.queuedAt, attempts: i.attempts })) });
3354
+ } catch (e) {
3355
+ json(res, { error: String(e && e.message || e) }, 500);
3356
+ }
3357
+ }
3358
+ }));
3359
+ disposers.push(ctx.webServer.register({
3360
+ kind: "exact",
3361
+ path: "/archived-sessions/pending-moves/cancel",
3362
+ handler: async (req, res) => {
3363
+ try {
3364
+ const body = await readJsonBody(req);
3365
+ const ids = parseIds(body);
3366
+ if (!ids || ids.length === 0) return json(res, { ok: false, error: "missing sessionIds" }, 400);
3367
+ const removed = await pendingMoves.remove(ids);
3368
+ json(res, { ok: true, removed });
3369
+ } catch (e) {
3370
+ json(res, { ok: false, error: String(e && e.message || e) }, errorStatus(e));
3371
+ }
3372
+ }
3373
+ }));
2708
3374
  disposers.push(ctx.webServer.register({
2709
3375
  kind: "exact",
2710
3376
  path: "/archived-sessions/archive",