dsh-sessions-manager 3.5.3 → 3.6.0

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,9 @@ 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 isFresh(entry, stat5, now, ttlMs = DEFAULT_TTL_MS) {
486
618
  if (!entry) return false;
487
- const fp = fingerprintOf(stat3);
619
+ const fp = fingerprintOf(stat5);
488
620
  if (!fp) return false;
489
621
  if (entry.fingerprint !== fp) return false;
490
622
  if (typeof entry.at !== "number") return false;
@@ -496,8 +628,8 @@ function partitionByCache(ids, statsById, cache, now = Date.now(), ttlMs = DEFAU
496
628
  const missing = [];
497
629
  for (const id of ids) {
498
630
  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) {
631
+ const stat5 = statsById && statsById.get(String(id));
632
+ if (isFresh(entry, stat5, now, ttlMs) && entry && entry.meta) {
501
633
  cached.set(String(id), entry.meta);
502
634
  } else {
503
635
  missing.push(String(id));
@@ -513,10 +645,10 @@ function createSessionMetaCache(opts = {}) {
513
645
  let misses = 0;
514
646
  return {
515
647
  // 命中返回 meta,未命中/无法校验返回 null。
516
- get(id, stat3) {
648
+ get(id, stat5) {
517
649
  const key = String(id);
518
650
  const entry = map.get(key);
519
- if (isFresh(entry, stat3, Date.now(), ttlMs)) {
651
+ if (isFresh(entry, stat5, Date.now(), ttlMs)) {
520
652
  hits++;
521
653
  map.delete(key);
522
654
  map.set(key, entry);
@@ -525,9 +657,9 @@ function createSessionMetaCache(opts = {}) {
525
657
  misses++;
526
658
  return null;
527
659
  },
528
- set(id, stat3, meta) {
660
+ set(id, stat5, meta) {
529
661
  if (!meta) return null;
530
- const fp = fingerprintOf(stat3);
662
+ const fp = fingerprintOf(stat5);
531
663
  if (!fp) return null;
532
664
  const key = String(id);
533
665
  map.delete(key);
@@ -562,8 +694,8 @@ function createSessionMetaCache(opts = {}) {
562
694
  }
563
695
 
564
696
  // 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";
697
+ import { mkdir as mkdir4, readFile, rename as rename4, writeFile as writeFile4 } from "node:fs/promises";
698
+ import { dirname, join as join5 } from "node:path";
567
699
  var TITLE_INDEX_SCHEMA_VERSION = 1;
568
700
  var MAX_ENTRIES = 2e4;
569
701
  function normalizeEntry(raw) {
@@ -601,7 +733,7 @@ function mergeEntries(left, right) {
601
733
  function createTitleIndexStore({ dir, file }) {
602
734
  let cache = null;
603
735
  let chain = Promise.resolve();
604
- const path = file || join3(dir, "title-index.json");
736
+ const path = file || join5(dir, "title-index.json");
605
737
  async function readRaw() {
606
738
  try {
607
739
  return normalizeTitleIndex(JSON.parse(await readFile(path, "utf8")));
@@ -636,10 +768,10 @@ function createTitleIndexStore({ dir, file }) {
636
768
  if (!Object.keys(right).length) return false;
637
769
  await enqueue(async (store) => {
638
770
  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);
771
+ await mkdir4(dirname(path), { recursive: true });
772
+ const tmp = join5(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
773
+ await writeFile4(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: next }), { encoding: "utf8", mode: 384 });
774
+ await rename4(tmp, path);
643
775
  cache = next;
644
776
  });
645
777
  return true;
@@ -656,10 +788,10 @@ function createTitleIndexStore({ dir, file }) {
656
788
  }
657
789
  }
658
790
  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);
791
+ await mkdir4(dirname(path), { recursive: true });
792
+ const tmp = join5(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
793
+ await writeFile4(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: store }), { encoding: "utf8", mode: 384 });
794
+ await rename4(tmp, path);
663
795
  });
664
796
  return true;
665
797
  }
@@ -667,8 +799,8 @@ function createTitleIndexStore({ dir, file }) {
667
799
  }
668
800
 
669
801
  // src/handle-era-paths.js
670
- import { readdir, stat } from "node:fs/promises";
671
- import { basename, join as join4 } from "node:path";
802
+ import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
803
+ import { basename, join as join6 } from "node:path";
672
804
 
673
805
  // src/path-guard.js
674
806
  function splitSegments(target) {
@@ -700,7 +832,7 @@ function pathOwnsSession(target, sid) {
700
832
  }
701
833
 
702
834
  // src/handle-era-paths.js
703
- var GENERATION_LOG_RE = /^session\.v\d+\.jsonl(\.zst(d)?)?$/;
835
+ var GENERATION_LOG_RE = /^session(\.v\d+)?\.jsonl(\.zst(d)?)?$/;
704
836
  function isSafeChar(ch) {
705
837
  return ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch);
706
838
  }
@@ -741,8 +873,11 @@ function resolveSessionRoot(sp) {
741
873
  return typeof root === "string" && root.length > 0 ? root : null;
742
874
  }
743
875
  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));
876
+ const project = cwd === void 0 || cwd === null || cwd === "" ? join6(root, "_no-cwd") : join6(root, projectKeyFor(cwd));
877
+ return join6(project, encodeSegmentFor(id));
878
+ }
879
+ function generationVersionOf(name2) {
880
+ return Number((String(name2).match(/^session\.v(\d+)\./) || [])[1] || 0);
746
881
  }
747
882
  async function locateSessionArtifacts(sp, header) {
748
883
  const root = resolveSessionRoot(sp);
@@ -757,27 +892,26 @@ async function locateSessionArtifacts(sp, header) {
757
892
  if (basename(sessionDir) !== encodeSegmentFor(sid)) return null;
758
893
  let entries;
759
894
  try {
760
- const st = await stat(sessionDir);
895
+ const st = await stat2(sessionDir);
761
896
  if (!st.isDirectory()) return null;
762
- entries = await readdir(sessionDir);
897
+ entries = await readdir2(sessionDir);
763
898
  } catch (e) {
764
899
  return null;
765
900
  }
766
901
  const generationFiles = entries.filter((name2) => GENERATION_LOG_RE.test(name2));
767
902
  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]);
903
+ generationFiles.sort((a, b) => generationVersionOf(b) - generationVersionOf(a));
904
+ const logPath = join6(sessionDir, generationFiles[0]);
774
905
  if (!pathOwnsSession(logPath, sid)) return null;
775
906
  return {
776
907
  root,
777
- projectDir: join4(root, header.cwd === void 0 || header.cwd === null || header.cwd === "" ? "_no-cwd" : projectKeyFor(header.cwd)),
908
+ projectDir: join6(root, header.cwd === void 0 || header.cwd === null || header.cwd === "" ? "_no-cwd" : projectKeyFor(header.cwd)),
778
909
  sessionDir,
779
910
  logPath,
780
- generationFiles
911
+ generationFiles,
912
+ // 最高代版本号:0 = legacy `session.jsonl[.zstd]`,≥1 = `session.vN.jsonl[.zstd]`。
913
+ // 上层用它判断「另一个目录里的同 id 副本是不是被取代的旧代」。
914
+ generationVersion: generationVersionOf(generationFiles[0])
781
915
  };
782
916
  }
783
917
 
@@ -785,9 +919,15 @@ async function locateSessionArtifacts(sp, header) {
785
919
  var DEFAULT_CHUNK = 400;
786
920
  var MAX_CHUNKS = 2e4;
787
921
  function normalizeReadResult(events) {
922
+ if (events === void 0 || events === null) return [];
788
923
  if (Array.isArray(events)) return events;
789
- if (events && typeof events[Symbol.iterator] === "function") return [...events];
790
- return [];
924
+ if (typeof events === "object" && Array.isArray(events.events)) return events.events;
925
+ if (typeof events[Symbol.iterator] === "function") return [...events];
926
+ const error = new Error(
927
+ "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)
928
+ );
929
+ error.code = "DSM_READ_SHAPE_UNKNOWN";
930
+ throw error;
791
931
  }
792
932
  async function closeQuietly(handle) {
793
933
  try {
@@ -975,9 +1115,80 @@ function requireCapability(capabilities, name2) {
975
1115
  }
976
1116
 
977
1117
  // 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";
1118
+ 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";
1119
+ import { basename as basename2, dirname as dirname2, join as join7 } from "node:path";
980
1120
  var MOVE_BATCH = 400;
1121
+ var REAL_LOG_RE = /^session.*\.jsonl(\.zst(d)?)?$/;
1122
+ var MOVE_TEMP_RE = /\.move-(backup|stage)-/;
1123
+ var SESSION_LOCAL_JUNK_RE = /^(\.?session\.lock.*|\.lock.*|.*\.lock|.*\.tmp|.*\.stage|.*\.bak|\.DS_Store)$/;
1124
+ async function cleanSiblingCopies(root, sid, keepDir, keepVersion) {
1125
+ let projects;
1126
+ try {
1127
+ projects = await readdir3(root, { withFileTypes: true });
1128
+ } catch (e) {
1129
+ return { removed: 0, superseded: [], real: [] };
1130
+ }
1131
+ let segment;
1132
+ try {
1133
+ segment = encodeSegmentFor(sid);
1134
+ } catch (e) {
1135
+ return { removed: 0, superseded: [], real: [] };
1136
+ }
1137
+ let removed = 0;
1138
+ const superseded = [];
1139
+ const real = [];
1140
+ for (const ent of projects) {
1141
+ if (!ent.isDirectory()) continue;
1142
+ const candidate = join7(root, ent.name, segment);
1143
+ if (keepDir && candidate === keepDir) continue;
1144
+ let entries;
1145
+ try {
1146
+ const st = await stat3(candidate);
1147
+ if (!st.isDirectory()) continue;
1148
+ entries = await readdir3(candidate);
1149
+ } catch (e) {
1150
+ continue;
1151
+ }
1152
+ const logs = entries.filter((name2) => REAL_LOG_RE.test(name2) && !MOVE_TEMP_RE.test(name2));
1153
+ if (logs.length === 0) {
1154
+ try {
1155
+ await rm2(candidate, { recursive: true, force: true });
1156
+ removed++;
1157
+ } catch (e) {
1158
+ }
1159
+ continue;
1160
+ }
1161
+ const highest = Math.max(...logs.map(generationVersionOf));
1162
+ const onlyLogsAndLocalJunk = entries.every((name2) => REAL_LOG_RE.test(name2) && !MOVE_TEMP_RE.test(name2) || SESSION_LOCAL_JUNK_RE.test(name2));
1163
+ if (Number.isSafeInteger(keepVersion) && highest < keepVersion && onlyLogsAndLocalJunk) {
1164
+ try {
1165
+ await rm2(candidate, { recursive: true, force: true });
1166
+ superseded.push(candidate);
1167
+ } catch (e) {
1168
+ real.push(candidate);
1169
+ }
1170
+ continue;
1171
+ }
1172
+ real.push(candidate);
1173
+ }
1174
+ return { removed, superseded, real };
1175
+ }
1176
+ async function removeSourceDir(sessionDir) {
1177
+ let entries;
1178
+ try {
1179
+ entries = await readdir3(sessionDir);
1180
+ } catch (e) {
1181
+ return { cleaned: true, leftover: [] };
1182
+ }
1183
+ try {
1184
+ await rm2(sessionDir, { recursive: true, force: true });
1185
+ } catch (e) {
1186
+ return { cleaned: false, leftover: entries };
1187
+ }
1188
+ return { cleaned: true, leftover: [] };
1189
+ }
1190
+ 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";
1191
+ 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
1192
  function conflictError(message) {
982
1193
  const error = new Error(message);
983
1194
  error.status = 409;
@@ -1004,7 +1215,7 @@ async function ensureNoActiveWriter(sp, sid) {
1004
1215
  try {
1005
1216
  handle = await sp.open(sid, "write");
1006
1217
  } 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");
1218
+ if (isAlreadyOwned(e)) throw conflictError(BUSY_MOVE_MESSAGE);
1008
1219
  throw e;
1009
1220
  }
1010
1221
  await closeQuietly2(handle);
@@ -1020,13 +1231,13 @@ async function purgeSessionArtifacts(sp, sid, header) {
1020
1231
  try {
1021
1232
  writer = await sp.open(sid, "write");
1022
1233
  } 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");
1234
+ if (isAlreadyOwned(e)) throw conflictError(BUSY_PURGE_MESSAGE);
1024
1235
  throw e;
1025
1236
  }
1026
1237
  await closeQuietly2(writer);
1027
1238
  writer = null;
1028
1239
  try {
1029
- await rm(artifacts.sessionDir, { recursive: true, force: true });
1240
+ await rm2(artifacts.sessionDir, { recursive: true, force: true });
1030
1241
  } catch (e) {
1031
1242
  const error = new Error("\u5220\u9664\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A" + String(e && e.message || e));
1032
1243
  error.status = 500;
@@ -1042,8 +1253,8 @@ async function purgeSessionArtifacts(sp, sid, header) {
1042
1253
  }
1043
1254
  return artifacts;
1044
1255
  }
1045
- async function relocateRewrittenBackup({ sid, canonical, backupPath, artifacts }) {
1046
- const original = await readFile2(backupPath);
1256
+ async function relocateRewrittenBackup({ sid, canonical, backupLogPath, artifacts }) {
1257
+ const original = await readFile2(backupLogPath);
1047
1258
  const frames = scanZstdFrames(original).frames;
1048
1259
  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
1260
  const rewritten = rewriteFrame0CwdInMemory(original, canonical);
@@ -1053,26 +1264,66 @@ async function relocateRewrittenBackup({ sid, canonical, backupPath, artifacts }
1053
1264
  throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u4E8B\u4EF6\u5185\u5BB9\u53D1\u751F\u53D8\u5316");
1054
1265
  }
1055
1266
  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]));
1267
+ await mkdir5(targetDir, { recursive: true });
1268
+ const staged = join7(targetDir, `.move-stage-${process.pid}-${Date.now()}`);
1269
+ await writeFile5(staged, rewritten, { mode: 384 });
1270
+ await rename5(staged, join7(targetDir, artifacts.generationFiles[0]));
1060
1271
  return targetDir;
1061
1272
  }
1062
1273
  async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inheritedEventCount = 0 }) {
1274
+ let probeError = null;
1275
+ try {
1276
+ await ensureNoActiveWriter(sp, sid);
1277
+ } catch (e) {
1278
+ if (e && e.status === 409) throw e;
1279
+ probeError = e;
1280
+ }
1063
1281
  const artifacts = await locateSessionArtifacts(sp, header);
1064
1282
  if (!artifacts) {
1065
1283
  const error = new Error("\u65E0\u6CD5\u5B9A\u4F4D\u8BE5\u4F1A\u8BDD\u7684\u7269\u7406\u65E5\u5FD7\uFF0C\u5DF2\u505C\u6B62\u79FB\u52A8");
1066
1284
  error.status = 409;
1067
1285
  throw error;
1068
1286
  }
1069
- await ensureNoActiveWriter(sp, sid);
1287
+ if (probeError) {
1288
+ 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));
1289
+ error.status = 409;
1290
+ throw error;
1291
+ }
1292
+ const HEADER_ONLY_MAX = 4096;
1293
+ if (events.length === 0) {
1294
+ let sourceBytes = null;
1295
+ try {
1296
+ sourceBytes = (await stat3(artifacts.logPath)).size;
1297
+ } catch (e) {
1298
+ sourceBytes = null;
1299
+ }
1300
+ if (sourceBytes !== null && sourceBytes > HEADER_ONLY_MAX) {
1301
+ const error = new Error(
1302
+ `\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`
1303
+ );
1304
+ error.status = 409;
1305
+ error.code = "DSM_MOVE_EMPTY_READ";
1306
+ throw error;
1307
+ }
1308
+ }
1309
+ const siblings = await cleanSiblingCopies(artifacts.root, sid, artifacts.sessionDir, artifacts.generationVersion);
1310
+ if (siblings.real.length > 0) {
1311
+ const error = new Error(
1312
+ "\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")
1313
+ );
1314
+ error.status = 409;
1315
+ error.code = "DSM_SESSION_DUP_LOG";
1316
+ throw error;
1317
+ }
1070
1318
  const newHeader = Object.assign({}, header, { cwd: canonical });
1071
1319
  const firstSeq = events.length ? Number(events[0].seq) : 0;
1072
1320
  const replay = firstSeq !== 0 ? events.map((event, index) => ({ ...event, seq: index })) : events;
1073
1321
  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);
1322
+ const backupRoot = join7(dirname2(artifacts.root), "sessions-manager-move-backup");
1323
+ const backupDir = join7(backupRoot, `${encodeSegmentFor(sid)}-${process.pid}-${Date.now()}`);
1324
+ await mkdir5(backupRoot, { recursive: true });
1325
+ const backupLogPath = join7(backupDir, basename2(artifacts.logPath));
1326
+ await rename5(artifacts.sessionDir, backupDir);
1076
1327
  let writer = null;
1077
1328
  try {
1078
1329
  try {
@@ -1085,7 +1336,7 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
1085
1336
  writer = null;
1086
1337
  } catch (e) {
1087
1338
  if (isAlreadyExists(e)) {
1088
- await relocateRewrittenBackup({ sid, canonical, backupPath, artifacts });
1339
+ await relocateRewrittenBackup({ sid, canonical, backupLogPath, artifacts });
1089
1340
  } else {
1090
1341
  throw e;
1091
1342
  }
@@ -1101,11 +1352,15 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
1101
1352
  } catch (e) {
1102
1353
  await closeQuietly2(writer);
1103
1354
  try {
1104
- await rm(deriveSessionDir(artifacts.root, canonical, sid), { recursive: true, force: true });
1355
+ await rm2(deriveSessionDir(artifacts.root, canonical, sid), { recursive: true, force: true });
1105
1356
  } catch (_) {
1106
1357
  }
1107
1358
  try {
1108
- await rename4(backupPath, artifacts.logPath);
1359
+ await mkdir5(dirname2(artifacts.sessionDir), { recursive: true });
1360
+ } catch (_) {
1361
+ }
1362
+ try {
1363
+ await rename5(backupDir, artifacts.sessionDir);
1109
1364
  } catch (_) {
1110
1365
  }
1111
1366
  if (e && e.status) throw e;
@@ -1113,19 +1368,53 @@ async function moveSessionToCwd({ sp, sid, header, canonical, events = [], inher
1113
1368
  error.status = 500;
1114
1369
  throw error;
1115
1370
  }
1116
- try {
1117
- await unlink(backupPath);
1118
- } catch (e) {
1119
- }
1120
- return { sessionDir: deriveSessionDir(artifacts.root, canonical, sid) };
1371
+ const sourceCleanup = await removeSourceDir(backupDir);
1372
+ return {
1373
+ sessionDir: deriveSessionDir(artifacts.root, canonical, sid),
1374
+ sourceCleanup,
1375
+ reclaimedSiblings: siblings.superseded
1376
+ };
1377
+ }
1378
+
1379
+ // src/lineage.js
1380
+ var EMPTY_BASE = 190;
1381
+ function isEmptyLogSize(sizeBytes, cwd) {
1382
+ if (!Number.isFinite(sizeBytes) || sizeBytes < 0) return false;
1383
+ const cwdLen = typeof cwd === "string" ? cwd.length : 0;
1384
+ return sizeBytes <= EMPTY_BASE + cwdLen;
1385
+ }
1386
+ var EMPTY_DECODE_LIMIT = 8192;
1387
+ var LIFECYCLE_EVENT_TYPES = /* @__PURE__ */ new Set([
1388
+ "session",
1389
+ "permission/preset",
1390
+ "sandbox/mode",
1391
+ "approval/policy"
1392
+ ]);
1393
+ function isEmptyEventTypes(types) {
1394
+ if (!Array.isArray(types)) return false;
1395
+ for (const t of types) {
1396
+ if (typeof t !== "string" || !LIFECYCLE_EVENT_TYPES.has(t)) return false;
1397
+ }
1398
+ return true;
1399
+ }
1400
+ function classifyLineage(header, sizeBytes) {
1401
+ if (!header || typeof header !== "object") return null;
1402
+ const origin = header.origin === "subagent" ? "subagent" : null;
1403
+ const parentSession = typeof header.parentSession === "string" && header.parentSession ? header.parentSession : null;
1404
+ const delegationDepth = Number.isSafeInteger(header.delegationDepth) && header.delegationDepth > 0 ? header.delegationDepth : 0;
1405
+ const empty = isEmptyLogSize(sizeBytes, header.cwd);
1406
+ if (!origin && !parentSession && !empty) return null;
1407
+ return { origin, parentSession, delegationDepth, empty };
1121
1408
  }
1122
1409
 
1123
1410
  // src/index.js
1411
+ var BUILD_STAMP = true ? "3.6.0 built 2026-09-10T09:47:49.428Z" : "dev";
1124
1412
  var name = "dsh-sessions-manager";
1125
1413
  var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
1126
1414
  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");
1415
+ var STATE_DIR = process.env.DSH_SESSIONS_MANAGER_STAR_DIR || join8(homedir4(), ".dsh", "sessions-manager");
1416
+ var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR || join8(homedir4(), ".dsh", "sessions-manager-trash");
1417
+ var TRASH_INDEX = join8(TRASH_DIR, "index.json");
1129
1418
  var TRASH_SCHEMA_VERSION = 2;
1130
1419
  var DEFAULT_TRASH_SETTINGS = Object.freeze({ retentionDays: 0 });
1131
1420
  var FETCH_TOOL_RE = /search|fetch|download|browse/i;
@@ -1227,7 +1516,7 @@ function apply(ctx) {
1227
1516
  const dom = () => ctx.storageDomain.get("workspace");
1228
1517
  const authorityTitleCache = /* @__PURE__ */ new Map();
1229
1518
  const metaCache = createSessionMetaCache();
1230
- const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file: join6(TRASH_DIR, "title-index.json") });
1519
+ const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file: join8(TRASH_DIR, "title-index.json") });
1231
1520
  async function hydrateFromPersist(ids, statsById) {
1232
1521
  const hits = /* @__PURE__ */ new Map();
1233
1522
  if (!ids || !ids.length) return hits;
@@ -1238,10 +1527,10 @@ function apply(ctx) {
1238
1527
  return hits;
1239
1528
  }
1240
1529
  for (const id of ids) {
1241
- const stat3 = statsById.get(id);
1530
+ const stat5 = statsById.get(id);
1242
1531
  const entry = store && store[id];
1243
- if (!stat3 || !entry) continue;
1244
- const fp = fingerprintOf(stat3);
1532
+ if (!stat5 || !entry) continue;
1533
+ const fp = fingerprintOf(stat5);
1245
1534
  if (!isPersistableFingerprint(fp)) continue;
1246
1535
  if (fp && entry.fingerprint === fp) {
1247
1536
  hits.set(id, { title: entry.title, cwd: entry.cwd, createdAt: entry.createdAt });
@@ -1249,7 +1538,7 @@ function apply(ctx) {
1249
1538
  }
1250
1539
  return hits;
1251
1540
  }
1252
- function persistDecoded(decoded, statsById) {
1541
+ async function persistDecoded(decoded, statsById) {
1253
1542
  if (!decoded || !decoded.size) return;
1254
1543
  const batch = {};
1255
1544
  const now = Date.now();
@@ -1259,8 +1548,10 @@ function apply(ctx) {
1259
1548
  batch[id] = { title: meta.title, cwd: meta.cwd, createdAt: meta.createdAt, fingerprint: fp, updatedAt: now };
1260
1549
  }
1261
1550
  if (!Object.keys(batch).length) return;
1262
- titleIndex.merge(batch).catch(() => {
1263
- });
1551
+ try {
1552
+ await titleIndex.merge(batch);
1553
+ } catch (e) {
1554
+ }
1264
1555
  }
1265
1556
  function metaFromSnapshot(o) {
1266
1557
  let title = null, createdAt = null, cwd = null;
@@ -1406,7 +1697,7 @@ function apply(ctx) {
1406
1697
  try {
1407
1698
  const loc = persistence.locate(header);
1408
1699
  if (!loc || typeof loc.path !== "string" || !loc.path) return;
1409
- const st = await stat2(loc.path);
1700
+ const st = await stat4(loc.path);
1410
1701
  if (!st) return;
1411
1702
  if (typeof st.size === "number") sizeById.set(id, st.size);
1412
1703
  if (typeof st.mtimeMs === "number" && st.mtimeMs > 0) {
@@ -1437,7 +1728,7 @@ function apply(ctx) {
1437
1728
  }
1438
1729
  async function readTrashStore() {
1439
1730
  try {
1440
- return normalizeTrashStore(JSON.parse(readFileSync3(TRASH_INDEX, "utf8")));
1731
+ return normalizeTrashStore(JSON.parse(readFileSync4(TRASH_INDEX, "utf8")));
1441
1732
  } catch (e) {
1442
1733
  return normalizeTrashStore(null);
1443
1734
  }
@@ -1446,10 +1737,10 @@ function apply(ctx) {
1446
1737
  return (await readTrashStore()).items;
1447
1738
  }
1448
1739
  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);
1740
+ await mkdir6(TRASH_DIR, { recursive: true });
1741
+ const tmp = join8(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
1742
+ await writeFile6(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
1743
+ await rename6(tmp, TRASH_INDEX);
1453
1744
  }
1454
1745
  function mutateTrash(mutator) {
1455
1746
  const operation = trashMutation.then(async () => {
@@ -1463,7 +1754,10 @@ function apply(ctx) {
1463
1754
  return operation;
1464
1755
  }
1465
1756
  const stars = createStarIndex();
1757
+ const pendingMoves = createPendingMoveStore();
1466
1758
  const autoArchive = createAutoArchiveStore();
1759
+ sweepStaleStateTemps([STATE_DIR, TRASH_DIR], {}).catch(() => {
1760
+ });
1467
1761
  async function gcStars(validIds) {
1468
1762
  try {
1469
1763
  const store = await stars.read();
@@ -1550,10 +1844,10 @@ function apply(ctx) {
1550
1844
  let exists = false;
1551
1845
  let header = null;
1552
1846
  try {
1553
- const stat3 = await persistence.statSession(sid);
1554
- if (stat3) {
1847
+ const stat5 = await persistence.statSession(sid);
1848
+ if (stat5) {
1555
1849
  exists = true;
1556
- header = stat3.header;
1850
+ header = stat5.header;
1557
1851
  }
1558
1852
  } catch (e) {
1559
1853
  }
@@ -1580,11 +1874,11 @@ function apply(ctx) {
1580
1874
  const entry = store.items.find((t) => String(t.sessionId) === sid);
1581
1875
  let originalPath = entry && typeof entry.originalPath === "string" ? entry.originalPath : null;
1582
1876
  if (!originalPath && header) {
1583
- const loc = persistence.locate(header);
1877
+ const loc = await persistence.locateVerified(header).catch(() => null);
1584
1878
  if (loc && typeof loc.path === "string") originalPath = loc.path;
1585
1879
  }
1586
1880
  if (originalPath) {
1587
- const existsOnDisk = await stat2(originalPath).then(() => true).catch(() => false);
1881
+ const existsOnDisk = await stat4(originalPath).then(() => true).catch(() => false);
1588
1882
  if (!existsOnDisk) {
1589
1883
  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
1884
  }
@@ -1697,7 +1991,7 @@ function apply(ctx) {
1697
1991
  await purgeSessionArtifacts(sp, sid, locatedHeader);
1698
1992
  } else if (target) {
1699
1993
  try {
1700
- await unlink2(target);
1994
+ await unlink(target);
1701
1995
  } catch (e) {
1702
1996
  if (e && e.code !== "ENOENT") throw new Error("\u5220\u9664\u6587\u4EF6\u5931\u8D25\uFF1A" + String(e && e.message || e));
1703
1997
  }
@@ -1730,8 +2024,10 @@ function apply(ctx) {
1730
2024
  purged = true;
1731
2025
  });
1732
2026
  if (!purged) throw new Error("\u5F7B\u5E95\u5220\u9664\u5931\u8D25");
1733
- stars.removeIds([sid]).catch(() => {
1734
- });
2027
+ try {
2028
+ await stars.removeIds([sid]);
2029
+ } catch (e) {
2030
+ }
1735
2031
  return { ok: true, purged: true };
1736
2032
  }
1737
2033
  async function trashSettings(next) {
@@ -1767,8 +2063,8 @@ function apply(ctx) {
1767
2063
  async function moveTargetWorkspace(rawPath) {
1768
2064
  if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
1769
2065
  let p = String(rawPath).trim();
1770
- if (p.startsWith("~/")) p = join6(homedir3(), p.slice(2));
1771
- if (!isAbsolute(p)) p = join6(homedir3(), p);
2066
+ if (p.startsWith("~/")) p = join8(homedir4(), p.slice(2));
2067
+ if (!isAbsolute(p)) p = join8(homedir4(), p);
1772
2068
  let canonical = null;
1773
2069
  try {
1774
2070
  canonical = await realpath(p);
@@ -1776,16 +2072,19 @@ function apply(ctx) {
1776
2072
  canonical = null;
1777
2073
  }
1778
2074
  if (canonical === null) {
1779
- await mkdir5(p, { recursive: true });
2075
+ await mkdir6(p, { recursive: true });
1780
2076
  canonical = await realpath(p);
1781
2077
  }
1782
- return { canonical, entity: await w.create(canonical, basename2(canonical) || "workspace") };
2078
+ return { canonical, entity: await w.create(canonical, basename3(canonical) || "workspace") };
1783
2079
  }
1784
2080
  async function moveOne(sid, targetPath) {
1785
2081
  requireCapability(capabilities, "move");
1786
2082
  const activeId = getActiveSessionId(ctx);
1787
2083
  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");
2084
+ 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");
2085
+ error.status = 409;
2086
+ error.code = "DSM_SESSION_BUSY";
2087
+ throw error;
1789
2088
  }
1790
2089
  const r = await persistence.readSession(sid, 0);
1791
2090
  if (!r || !r.meta) throw new Error("\u65E0\u6CD5\u8BFB\u53D6\u8BE5\u4F1A\u8BDD\u7684\u65E5\u5FD7");
@@ -1805,6 +2104,7 @@ function apply(ctx) {
1805
2104
  }
1806
2105
  }
1807
2106
  const newHeader = Object.assign({}, meta, { cwd: canonical });
2107
+ const moveNotes = [];
1808
2108
  const live = ctx.get("sessions");
1809
2109
  const liveObj = live && live.get && live.get(sid);
1810
2110
  const isOpen = !!liveObj;
@@ -1817,14 +2117,14 @@ function apply(ctx) {
1817
2117
  const stagedPath = `${newPath}.move-stage-${process.pid}-${Date.now()}`;
1818
2118
  let destinationInstalled = false;
1819
2119
  try {
1820
- await mkdir5(dirname2(newPath), { recursive: true });
2120
+ await mkdir6(dirname3(newPath), { recursive: true });
1821
2121
  try {
1822
- await stat2(newPath);
2122
+ await stat4(newPath);
1823
2123
  throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u76EE\u6807\u4F4D\u7F6E\u5DF2\u5B58\u5728\u540C\u540D\u4F1A\u8BDD\u65E5\u5FD7");
1824
2124
  } catch (e) {
1825
2125
  if (e && e.code !== "ENOENT") throw e;
1826
2126
  }
1827
- await rename5(oldPath, backupPath);
2127
+ await rename6(oldPath, backupPath);
1828
2128
  const original = await readFile3(backupPath);
1829
2129
  const originalFrames = scanZstdFrames(original).frames;
1830
2130
  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 +2134,25 @@ function apply(ctx) {
1834
2134
  const originalTail = original.subarray(originalFrames[0].end);
1835
2135
  const rewrittenTail = rewritten.subarray(rewrittenFrames[0].end);
1836
2136
  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);
2137
+ await writeFile6(stagedPath, rewritten, { mode: 384 });
2138
+ await rename6(stagedPath, newPath);
1839
2139
  destinationInstalled = true;
1840
- await unlink2(backupPath);
2140
+ await unlink(backupPath);
2141
+ const keptInSource = await cleanupMovedSourceDir(dirname3(oldPath));
2142
+ 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
2143
  } catch (e) {
1842
2144
  try {
1843
- await unlink2(stagedPath);
2145
+ await unlink(stagedPath);
1844
2146
  } catch (_) {
1845
2147
  }
1846
2148
  if (destinationInstalled) {
1847
2149
  try {
1848
- await unlink2(newPath);
2150
+ await unlink(newPath);
1849
2151
  } catch (_) {
1850
2152
  }
1851
2153
  }
1852
2154
  try {
1853
- await rename5(backupPath, oldPath);
2155
+ await rename6(backupPath, oldPath);
1854
2156
  } catch (_) {
1855
2157
  }
1856
2158
  if (e && e.code !== "ENOENT") throw e;
@@ -1874,6 +2176,20 @@ function apply(ctx) {
1874
2176
  }
1875
2177
  return null;
1876
2178
  };
2179
+ const cleanupMovedSourceDir = async (dir) => {
2180
+ let entries = null;
2181
+ try {
2182
+ entries = await readdir4(dir);
2183
+ } catch (e) {
2184
+ return [];
2185
+ }
2186
+ try {
2187
+ await rm3(dir, { recursive: true, force: true });
2188
+ } catch (e) {
2189
+ return entries;
2190
+ }
2191
+ return [];
2192
+ };
1877
2193
  if (persistence.kind === "session-handle") {
1878
2194
  const stat1 = await persistence.statSession(sid);
1879
2195
  if (stat1 && stat1.revision) {
@@ -1883,7 +2199,13 @@ function apply(ctx) {
1883
2199
  }
1884
2200
  }
1885
2201
  try {
1886
- await moveSessionToCwd({ sp, sid, header: meta, canonical, events, inheritedEventCount: r.inheritedEventCount });
2202
+ const movedHandle = await moveSessionToCwd({ sp, sid, header: meta, canonical, events, inheritedEventCount: r.inheritedEventCount });
2203
+ if (movedHandle && Array.isArray(movedHandle.reclaimedSiblings) && movedHandle.reclaimedSiblings.length > 0) {
2204
+ 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`);
2205
+ }
2206
+ if (movedHandle && movedHandle.sourceCleanup && movedHandle.sourceCleanup.cleaned === false) {
2207
+ 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`);
2208
+ }
1887
2209
  } catch (e) {
1888
2210
  if (e && e.status) throw e;
1889
2211
  throw new Error("\u79FB\u52A8\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A" + String(e && e.message || e));
@@ -1910,7 +2232,7 @@ function apply(ctx) {
1910
2232
  const backupPath = oldPath ? `${oldPath}.move-backup-${Date.now()}` : null;
1911
2233
  if (backupPath) {
1912
2234
  try {
1913
- await rename5(oldPath, backupPath);
2235
+ await rename6(oldPath, backupPath);
1914
2236
  } catch (e) {
1915
2237
  if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
1916
2238
  }
@@ -1918,7 +2240,7 @@ function apply(ctx) {
1918
2240
  const restore = async () => {
1919
2241
  if (backupPath) {
1920
2242
  try {
1921
- await rename5(backupPath, oldPath);
2243
+ await rename6(backupPath, oldPath);
1922
2244
  } catch (_) {
1923
2245
  }
1924
2246
  }
@@ -1932,10 +2254,14 @@ function apply(ctx) {
1932
2254
  }
1933
2255
  if (backupPath) {
1934
2256
  try {
1935
- await unlink2(backupPath);
2257
+ await unlink(backupPath);
1936
2258
  } catch (e) {
1937
2259
  }
1938
2260
  }
2261
+ if (oldPath) {
2262
+ const keptInSource = await cleanupMovedSourceDir(dirname3(oldPath));
2263
+ 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`);
2264
+ }
1939
2265
  } catch (e) {
1940
2266
  if (ALREADY_EXISTS_RE.test(String(e && e.message || e))) {
1941
2267
  await restore();
@@ -1979,9 +2305,107 @@ function apply(ctx) {
1979
2305
  moved: true,
1980
2306
  workspaceId: target.id,
1981
2307
  workspaceTitle: target.title,
1982
- workspacePath: canonical
2308
+ workspacePath: canonical,
2309
+ ...moveNotes.length > 0 ? { notes: moveNotes } : {}
1983
2310
  };
1984
2311
  }
2312
+ 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";
2313
+ function isBusyError(e) {
2314
+ if (!e) return false;
2315
+ if (e.code === "DSM_SESSION_BUSY") return true;
2316
+ const text = String(e && e.message || e);
2317
+ return /正被 DSH 打开|already owned/i.test(text);
2318
+ }
2319
+ async function moveOrQueue(sid, targetPath) {
2320
+ try {
2321
+ return await moveOne(sid, targetPath);
2322
+ } catch (e) {
2323
+ if (!isBusyError(e)) throw e;
2324
+ await pendingMoves.queue(sid, targetPath);
2325
+ return { ok: true, moved: false, queued: true, notes: [QUEUE_NOTE] };
2326
+ }
2327
+ }
2328
+ const BOOT_WARMUP_MS = 3e4;
2329
+ const bootedAt = Date.now();
2330
+ let queueRunning = false;
2331
+ async function runPendingMoves(reason) {
2332
+ if (queueRunning) return { ran: false, moved: 0, kept: 0 };
2333
+ queueRunning = true;
2334
+ let moved = 0;
2335
+ const notes = [];
2336
+ try {
2337
+ const items = await pendingMoves.list();
2338
+ for (const item of items) {
2339
+ try {
2340
+ await moveOne(item.sessionId, item.targetPath);
2341
+ await pendingMoves.remove([item.sessionId]);
2342
+ metaCache.invalidate(item.sessionId);
2343
+ moved++;
2344
+ notes.push(`\u6392\u961F\u4E2D\u7684\u79FB\u52A8\u5DF2\u5B8C\u6210\uFF1A${String(item.sessionId).slice(0, 18)}\u2026`);
2345
+ } catch (e) {
2346
+ if (isBusyError(e)) continue;
2347
+ if (Date.now() - bootedAt < BOOT_WARMUP_MS) continue;
2348
+ const bumped = await pendingMoves.bumpAttempts(item.sessionId);
2349
+ 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`);
2350
+ }
2351
+ }
2352
+ if (moved > 0) {
2353
+ try {
2354
+ await reindexRegistry();
2355
+ } catch (e) {
2356
+ }
2357
+ }
2358
+ } finally {
2359
+ queueRunning = false;
2360
+ }
2361
+ if (notes.length > 0) {
2362
+ try {
2363
+ for (const n of notes) console.warn("[dsh-sessions-manager] " + n);
2364
+ } catch (e) {
2365
+ }
2366
+ }
2367
+ void reason;
2368
+ return { ran: true, moved, kept: (await pendingMoves.list()).length };
2369
+ }
2370
+ const queueEffect = typeof ctx.effect === "function" ? ctx.effect.bind(ctx) : ((fn) => {
2371
+ fn();
2372
+ });
2373
+ if (typeof ctx.on === "function") {
2374
+ queueEffect(() => ctx.on("session/disposed", (session) => {
2375
+ const id = session && session.id != null ? String(session.id) : null;
2376
+ if (!id) return;
2377
+ pendingMoves.has(id).then((queued) => {
2378
+ if (!queued) return;
2379
+ const t = setTimeout(() => {
2380
+ runPendingMoves("session-disposed").catch(() => {
2381
+ });
2382
+ }, 500);
2383
+ if (t && typeof t.unref === "function") t.unref();
2384
+ }).catch(() => {
2385
+ });
2386
+ }));
2387
+ }
2388
+ queueEffect(() => {
2389
+ const timers = [0, 1e3, 3e3, 6e3, 12e3, 3e4].map((ms) => {
2390
+ const t = setTimeout(() => {
2391
+ runPendingMoves(ms === 0 ? "startup" : `startup+${ms}`).catch(() => {
2392
+ });
2393
+ }, ms);
2394
+ if (t && typeof t.unref === "function") t.unref();
2395
+ return t;
2396
+ });
2397
+ const tickTimer = setInterval(() => {
2398
+ pendingMoves.list().then((items) => {
2399
+ if (items.length > 0) return runPendingMoves("tick");
2400
+ }).catch(() => {
2401
+ });
2402
+ }, 12e4);
2403
+ if (tickTimer && typeof tickTimer.unref === "function") tickTimer.unref();
2404
+ return () => {
2405
+ for (const t of timers) clearTimeout(t);
2406
+ clearInterval(tickTimer);
2407
+ };
2408
+ });
1985
2409
  async function reindexRegistry() {
1986
2410
  const reg = w;
1987
2411
  if (!reg || typeof reg.replaceHeaderIndex !== "function") return false;
@@ -2095,7 +2519,7 @@ function apply(ctx) {
2095
2519
  }));
2096
2520
  for (const it of res2) items.push({ ...it, archived: currentArchived.has(it.sessionId) });
2097
2521
  }
2098
- persistDecoded(decoded, statsById);
2522
+ await persistDecoded(decoded, statsById);
2099
2523
  let starredSet = /* @__PURE__ */ new Set();
2100
2524
  try {
2101
2525
  starredSet = new Set((await stars.read()).starredSessionIds);
@@ -2202,14 +2626,61 @@ function apply(ctx) {
2202
2626
  }
2203
2627
  if (meta && meta.title) authorityTitleCache.set(id, String(meta.title));
2204
2628
  }
2205
- persistDecoded(decoded, statsById);
2629
+ await persistDecoded(decoded, statsById);
2206
2630
  }
2631
+ const lineage = {};
2632
+ for (const entry of entries) {
2633
+ const info = classifyLineage(entry.header, entry.sizeBytes);
2634
+ if (info) lineage[entry.id] = info;
2635
+ }
2636
+ await refineEmptyLineage(lineage, entries);
2207
2637
  return {
2208
2638
  titles: Object.fromEntries(authorityTitleCache),
2209
2639
  trashedSessionIds: store.items.map((item) => String(item.sessionId)),
2210
- purgedSessionIds: activeTombstones
2640
+ purgedSessionIds: activeTombstones,
2641
+ lineage
2211
2642
  };
2212
2643
  }
2644
+ const emptyScanCache = /* @__PURE__ */ new Map();
2645
+ async function refineEmptyLineage(lineage, entries) {
2646
+ if (!persistence || typeof persistence.inspectSession !== "function") return;
2647
+ for (const entry of entries) {
2648
+ const size = entry && Number.isFinite(entry.sizeBytes) ? entry.sizeBytes : null;
2649
+ if (size === null || size > EMPTY_DECODE_LIMIT) continue;
2650
+ const id = String(entry.id);
2651
+ let scanned = emptyScanCache.get(id);
2652
+ if (!scanned || scanned.sizeBytes !== size) {
2653
+ let isEmpty = false;
2654
+ try {
2655
+ const types = [];
2656
+ await persistence.inspectSession(id, { onEvents: (batch) => {
2657
+ for (const ev of batch || []) if (types.length < 64) types.push(ev && ev.type);
2658
+ } });
2659
+ isEmpty = isEmptyEventTypes(types);
2660
+ } catch (e) {
2661
+ isEmpty = false;
2662
+ }
2663
+ scanned = { sizeBytes: size, empty: isEmpty };
2664
+ emptyScanCache.set(id, scanned);
2665
+ }
2666
+ if (scanned.empty) {
2667
+ const info = lineage[id] || (lineage[id] = { origin: null, parentSession: null, delegationDepth: 0, empty: false });
2668
+ info.empty = true;
2669
+ } else if (lineage[id]) {
2670
+ const info = lineage[id];
2671
+ info.empty = false;
2672
+ if (!info.origin && !info.parentSession) delete lineage[id];
2673
+ }
2674
+ }
2675
+ const live = /* @__PURE__ */ new Set();
2676
+ for (const entry of entries) {
2677
+ const size = entry && Number.isFinite(entry.sizeBytes) ? entry.sizeBytes : null;
2678
+ if (size !== null && size <= EMPTY_DECODE_LIMIT) live.add(String(entry.id));
2679
+ }
2680
+ for (const key of emptyScanCache.keys()) {
2681
+ if (!live.has(key)) emptyScanCache.delete(key);
2682
+ }
2683
+ }
2213
2684
  async function buildDetails(sid, signal) {
2214
2685
  const sessions = ctx.get("sessions");
2215
2686
  const live = sessions && sessions.get(sid);
@@ -2294,8 +2765,8 @@ function apply(ctx) {
2294
2765
  let sizeBytes = null;
2295
2766
  if (live === void 0) {
2296
2767
  try {
2297
- const stat3 = await persistence.statSession(sid);
2298
- if (stat3 && Number.isFinite(stat3.sizeBytes)) sizeBytes = stat3.sizeBytes;
2768
+ const stat5 = await persistence.statSession(sid);
2769
+ if (stat5 && Number.isFinite(stat5.sizeBytes)) sizeBytes = stat5.sizeBytes;
2299
2770
  } catch (e) {
2300
2771
  }
2301
2772
  }
@@ -2303,7 +2774,7 @@ function apply(ctx) {
2303
2774
  try {
2304
2775
  const loc = persistence.locate(meta);
2305
2776
  if (loc && typeof loc.path === "string" && loc.path) {
2306
- const st = await stat2(loc.path);
2777
+ const st = await stat4(loc.path);
2307
2778
  if (st && typeof st.size === "number") sizeBytes = st.size;
2308
2779
  }
2309
2780
  } catch (e) {
@@ -2312,7 +2783,7 @@ function apply(ctx) {
2312
2783
  }
2313
2784
  if (stats.fetches.length > MAX_FETCHES) stats.fetches = stats.fetches.slice(0, MAX_FETCHES);
2314
2785
  const fileEntries = [...fileSet.entries()].slice(0, MAX_FILES * 2);
2315
- const exists = await Promise.all(fileEntries.map(([p]) => stat2(p).then(() => true).catch(() => false)));
2786
+ const exists = await Promise.all(fileEntries.map(([p]) => stat4(p).then(() => true).catch(() => false)));
2316
2787
  const files = fileEntries.filter((_, i) => exists[i]).map(([path, tool]) => ({ path, tool })).slice(0, MAX_FILES);
2317
2788
  const lineage = {
2318
2789
  parentSessionId: meta && typeof meta.parentSession === "string" ? meta.parentSession : null,
@@ -2364,7 +2835,7 @@ function apply(ctx) {
2364
2835
  disposers.push(ctx.webServer.register({
2365
2836
  kind: "exact",
2366
2837
  path: "/archived-sessions/capabilities",
2367
- handler: async (req, res) => json(res, capabilities)
2838
+ handler: async (req, res) => json(res, { ...capabilities, buildStamp: BUILD_STAMP })
2368
2839
  }));
2369
2840
  disposers.push(ctx.webServer.register({
2370
2841
  kind: "exact",
@@ -2520,7 +2991,7 @@ function apply(ctx) {
2520
2991
  if (typeof item.originalPath !== "string" || !item.originalPath) {
2521
2992
  return { sessionId: item.sessionId, status: "unverified", originalPath: null };
2522
2993
  }
2523
- const exists = await stat2(item.originalPath).then(() => true).catch(() => false);
2994
+ const exists = await stat4(item.originalPath).then(() => true).catch(() => false);
2524
2995
  return { sessionId: item.sessionId, status: exists ? "ok" : "missing", originalPath: item.originalPath };
2525
2996
  }));
2526
2997
  json(res, {
@@ -2561,8 +3032,10 @@ function apply(ctx) {
2561
3032
  if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
2562
3033
  const out = await purgeFromTrash(sid);
2563
3034
  metaCache.invalidate(sid);
2564
- titleIndex.remove([sid]).catch(() => {
2565
- });
3035
+ try {
3036
+ await titleIndex.remove([sid]);
3037
+ } catch (e) {
3038
+ }
2566
3039
  json(res, out);
2567
3040
  } catch (e) {
2568
3041
  json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
@@ -2582,7 +3055,7 @@ function apply(ctx) {
2582
3055
  try {
2583
3056
  results.push({ sessionId: sid, ok: true, ...await purgeFromTrash(sid) });
2584
3057
  metaCache.invalidate(sid);
2585
- titleIndex.remove([sid]).catch(() => {
3058
+ await titleIndex.remove([sid]).catch(() => {
2586
3059
  });
2587
3060
  } catch (e) {
2588
3061
  results.push({ sessionId: sid, ok: false, error: String(e && e.message || e) });
@@ -2672,6 +3145,108 @@ function apply(ctx) {
2672
3145
  }
2673
3146
  }
2674
3147
  }));
3148
+ disposers.push(ctx.webServer.register({
3149
+ kind: "exact",
3150
+ path: "/archived-sessions/lineage-tree",
3151
+ handler: async (req, res) => {
3152
+ try {
3153
+ const body = await readJsonBody(req);
3154
+ const rootId = body && typeof body.sessionId === "string" ? body.sessionId : "";
3155
+ if (!rootId || !isSafeSessionId2(rootId)) return json(res, { error: "missing sessionId" }, 400);
3156
+ const headerById = /* @__PURE__ */ new Map();
3157
+ const sizeById = /* @__PURE__ */ new Map();
3158
+ const trashedIds = /* @__PURE__ */ new Set();
3159
+ let purgedIds = [];
3160
+ const persistedIds = /* @__PURE__ */ new Set();
3161
+ try {
3162
+ const store = await readTrashStore();
3163
+ for (const item of store.items) trashedIds.add(String(item.sessionId));
3164
+ purgedIds = store.purgedSessionIds.map(String);
3165
+ } catch (e) {
3166
+ }
3167
+ const addHeader = (h, sizeBytes) => {
3168
+ if (!h || h.id == null) return;
3169
+ const id = String(h.id);
3170
+ if (headerById.has(id) || trashedIds.has(id)) return;
3171
+ headerById.set(id, {
3172
+ parentSession: h.parentSession != null ? String(h.parentSession) : null,
3173
+ subagent: h.origin === "subagent",
3174
+ delegationDepth: Number.isFinite(h.delegationDepth) ? h.delegationDepth : null,
3175
+ createdAt: typeof h.createdAt === "number" ? h.createdAt : null
3176
+ });
3177
+ if (Number.isFinite(sizeBytes)) sizeById.set(id, sizeBytes);
3178
+ };
3179
+ try {
3180
+ for (const entry of await persistence.listEntries()) {
3181
+ addHeader(entry.header, entry.sizeBytes);
3182
+ if (entry && entry.id != null) persistedIds.add(String(entry.id));
3183
+ }
3184
+ } catch (e) {
3185
+ }
3186
+ const liveIds = /* @__PURE__ */ new Set();
3187
+ const live = ctx.get("sessions");
3188
+ try {
3189
+ if (live && typeof live.list === "function") {
3190
+ live.list().forEach((s) => {
3191
+ liveIds.add(String(s.id));
3192
+ addHeader(s.header, void 0);
3193
+ });
3194
+ }
3195
+ } catch (e) {
3196
+ }
3197
+ for (const id of purgedIds) {
3198
+ if (!persistedIds.has(id)) headerById.delete(id);
3199
+ }
3200
+ const kidsOf = /* @__PURE__ */ new Map();
3201
+ for (const [id, h] of headerById) {
3202
+ if (!h.subagent || !h.parentSession) continue;
3203
+ if (!kidsOf.has(h.parentSession)) kidsOf.set(h.parentSession, []);
3204
+ kidsOf.get(h.parentSession).push(id);
3205
+ }
3206
+ const MAX_NODES = 500;
3207
+ let nodeCount = 0;
3208
+ const build = (id, depth, visited) => {
3209
+ nodeCount++;
3210
+ const h = headerById.get(id) || {};
3211
+ const childVisited = new Set(visited);
3212
+ childVisited.add(id);
3213
+ const kidIds = nodeCount >= MAX_NODES ? [] : (kidsOf.get(id) || []).filter((k) => !childVisited.has(k));
3214
+ return {
3215
+ sessionId: id,
3216
+ parentSession: h.parentSession || null,
3217
+ delegationDepth: h.delegationDepth,
3218
+ depth,
3219
+ createdAt: h.createdAt || null,
3220
+ sizeBytes: sizeById.has(id) ? sizeById.get(id) : null,
3221
+ live: liveIds.has(id),
3222
+ title: null,
3223
+ children: kidIds.map((k) => build(k, depth + 1, childVisited))
3224
+ };
3225
+ };
3226
+ const nodes = (kidsOf.get(rootId) || []).map((k) => build(k, 1, /* @__PURE__ */ new Set([rootId])));
3227
+ const allIds = [rootId];
3228
+ const walk = (n) => {
3229
+ allIds.push(n.sessionId);
3230
+ n.children.forEach(walk);
3231
+ };
3232
+ nodes.forEach(walk);
3233
+ const titles = await projectTitles(allIds);
3234
+ const titleOf = (id) => {
3235
+ const snap = titles.get(id);
3236
+ const m = snap ? metaFromSnapshot(snap) : null;
3237
+ return m && m.title || null;
3238
+ };
3239
+ const fill = (n) => {
3240
+ n.title = titleOf(n.sessionId);
3241
+ n.children.forEach(fill);
3242
+ };
3243
+ nodes.forEach(fill);
3244
+ json(res, { parent: { sessionId: rootId, title: titleOf(rootId) }, nodes });
3245
+ } catch (e) {
3246
+ json(res, { error: String(e && e.message || e) }, errorStatus(e));
3247
+ }
3248
+ }
3249
+ }));
2675
3250
  disposers.push(ctx.webServer.register({
2676
3251
  kind: "exact",
2677
3252
  path: "/archived-sessions/workspaces",
@@ -2693,7 +3268,8 @@ function apply(ctx) {
2693
3268
  const target = body && typeof body.targetPath === "string" ? body.targetPath : null;
2694
3269
  if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
2695
3270
  if (!target) return json(res, { ok: false, error: "missing targetPath" }, 400);
2696
- const moved = await moveOne(sid, target);
3271
+ const moved = await moveOrQueue(sid, target);
3272
+ if (moved.queued) return json(res, { sessionId: sid, ...moved });
2697
3273
  metaCache.invalidate(sid);
2698
3274
  try {
2699
3275
  await reindexRegistry();
@@ -2705,6 +3281,74 @@ function apply(ctx) {
2705
3281
  }
2706
3282
  }
2707
3283
  }));
3284
+ disposers.push(ctx.webServer.register({
3285
+ kind: "exact",
3286
+ path: "/archived-sessions/move-many",
3287
+ handler: async (req, res) => {
3288
+ try {
3289
+ const body = await readJsonBody(req);
3290
+ const ids = Array.isArray(body && body.sessionIds) ? [...new Set(body.sessionIds.filter((x) => typeof x === "string" && x))] : [];
3291
+ const target = body && typeof body.targetPath === "string" ? body.targetPath : null;
3292
+ if (!ids.length) return json(res, { ok: false, error: "missing sessionIds" }, 400);
3293
+ if (!target) return json(res, { ok: false, error: "missing targetPath" }, 400);
3294
+ let moved = 0;
3295
+ const failed = [];
3296
+ const queued = [];
3297
+ for (const sid of ids) {
3298
+ try {
3299
+ const r = await moveOrQueue(sid, target);
3300
+ if (r && r.queued) {
3301
+ queued.push(sid);
3302
+ continue;
3303
+ }
3304
+ metaCache.invalidate(sid);
3305
+ moved++;
3306
+ } catch (e) {
3307
+ const err = e && e.code ? { sessionId: sid, error: String(e && e.message || e), code: e.code } : { sessionId: sid, error: String(e && e.message || e) };
3308
+ failed.push(err);
3309
+ }
3310
+ }
3311
+ try {
3312
+ await reindexRegistry();
3313
+ } catch (e) {
3314
+ }
3315
+ json(res, {
3316
+ moved,
3317
+ failed,
3318
+ ...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`] } : {}
3319
+ });
3320
+ } catch (e) {
3321
+ json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
3322
+ }
3323
+ }
3324
+ }));
3325
+ disposers.push(ctx.webServer.register({
3326
+ kind: "exact",
3327
+ path: "/archived-sessions/pending-moves",
3328
+ handler: async (req, res) => {
3329
+ try {
3330
+ const items = await pendingMoves.list();
3331
+ json(res, { items: items.map((i) => ({ sessionId: i.sessionId, targetPath: i.targetPath, queuedAt: i.queuedAt, attempts: i.attempts })) });
3332
+ } catch (e) {
3333
+ json(res, { error: String(e && e.message || e) }, 500);
3334
+ }
3335
+ }
3336
+ }));
3337
+ disposers.push(ctx.webServer.register({
3338
+ kind: "exact",
3339
+ path: "/archived-sessions/pending-moves/cancel",
3340
+ handler: async (req, res) => {
3341
+ try {
3342
+ const body = await readJsonBody(req);
3343
+ const ids = parseIds(body);
3344
+ if (!ids || ids.length === 0) return json(res, { ok: false, error: "missing sessionIds" }, 400);
3345
+ const removed = await pendingMoves.remove(ids);
3346
+ json(res, { ok: true, removed });
3347
+ } catch (e) {
3348
+ json(res, { ok: false, error: String(e && e.message || e) }, errorStatus(e));
3349
+ }
3350
+ }
3351
+ }));
2708
3352
  disposers.push(ctx.webServer.register({
2709
3353
  kind: "exact",
2710
3354
  path: "/archived-sessions/archive",