dsh-sessions-manager 3.4.1 → 3.5.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,6 +1,6 @@
1
1
  // src/index.js
2
- import { mkdir as mkdir3, realpath, rename as rename3, stat, unlink, writeFile as writeFile3 } from "node:fs/promises";
3
- import { basename, dirname, isAbsolute, join as join3 } from "node:path";
2
+ import { mkdir as mkdir4, realpath, rename as rename4, stat, unlink, writeFile as writeFile4 } from "node:fs/promises";
3
+ import { basename, dirname as dirname2, isAbsolute, join as join4 } from "node:path";
4
4
  import { readFileSync as readFileSync4 } from "node:fs";
5
5
  import { homedir as homedir3 } from "node:os";
6
6
 
@@ -500,12 +500,211 @@ function createSessionMetaCache(opts = {}) {
500
500
  };
501
501
  }
502
502
 
503
+ // src/title-persist-index.js
504
+ import { mkdir as mkdir3, readFile, rename as rename3, writeFile as writeFile3 } from "node:fs/promises";
505
+ import { dirname, join as join3 } from "node:path";
506
+ var TITLE_INDEX_SCHEMA_VERSION = 1;
507
+ var MAX_ENTRIES = 2e4;
508
+ function normalizeEntry(raw) {
509
+ if (!raw || typeof raw !== "object") return null;
510
+ const title = typeof raw.title === "string" ? raw.title : null;
511
+ const cwd = typeof raw.cwd === "string" ? raw.cwd : null;
512
+ const createdAt = typeof raw.createdAt === "number" ? raw.createdAt : null;
513
+ const fingerprint = typeof raw.fingerprint === "string" && raw.fingerprint ? raw.fingerprint : null;
514
+ const updatedAt = typeof raw.updatedAt === "number" ? raw.updatedAt : 0;
515
+ if (!fingerprint || !title && !cwd) return null;
516
+ return { title, cwd, createdAt, fingerprint, updatedAt };
517
+ }
518
+ function normalizeTitleIndex(raw) {
519
+ const entries = {};
520
+ if (raw && typeof raw === "object" && raw.entries && typeof raw.entries === "object") {
521
+ for (const [id, entry] of Object.entries(raw.entries)) {
522
+ if (typeof id !== "string" || !id || id.length > 200) continue;
523
+ const normalized = normalizeEntry(entry);
524
+ if (normalized) entries[id] = normalized;
525
+ }
526
+ }
527
+ return { schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries };
528
+ }
529
+ function mergeEntries(left, right) {
530
+ const merged = { ...left };
531
+ for (const [id, entry] of Object.entries(right)) merged[id] = entry;
532
+ const ids = Object.keys(merged);
533
+ if (ids.length > MAX_ENTRIES) {
534
+ ids.sort((a, b) => (merged[a].updatedAt || 0) - (merged[b].updatedAt || 0));
535
+ for (const id of ids.slice(0, ids.length - MAX_ENTRIES)) delete merged[id];
536
+ }
537
+ return merged;
538
+ }
539
+ function createTitleIndexStore({ dir, file }) {
540
+ let cache = null;
541
+ let chain = Promise.resolve();
542
+ const path = file || join3(dir, "title-index.json");
543
+ async function readRaw() {
544
+ try {
545
+ return normalizeTitleIndex(JSON.parse(await readFile(path, "utf8")));
546
+ } catch (e) {
547
+ return normalizeTitleIndex(null);
548
+ }
549
+ }
550
+ function enqueue(mutator) {
551
+ const operation = chain.then(async () => {
552
+ const store = cache || (cache = (await readRaw()).entries);
553
+ await mutator(store);
554
+ return store;
555
+ });
556
+ chain = operation.catch(() => {
557
+ });
558
+ return operation;
559
+ }
560
+ return {
561
+ // 只读:内存优先,未加载过才落盘一次。绝不抛错。
562
+ async entries() {
563
+ if (cache) return cache;
564
+ cache = (await readRaw()).entries;
565
+ return cache;
566
+ },
567
+ // 批量合并写入(原子替换)。失败静默:索引只是加速器,坏了下次重解码。
568
+ async merge(batch) {
569
+ const right = {};
570
+ for (const [id, entry] of Object.entries(batch || {})) {
571
+ const normalized = normalizeEntry(entry);
572
+ if (normalized) right[String(id)] = normalized;
573
+ }
574
+ if (!Object.keys(right).length) return false;
575
+ await enqueue(async (store) => {
576
+ const next = mergeEntries(store, right);
577
+ await mkdir3(dirname(path), { recursive: true });
578
+ const tmp = join3(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
579
+ await writeFile3(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: next }), { encoding: "utf8", mode: 384 });
580
+ await rename3(tmp, path);
581
+ cache = next;
582
+ });
583
+ return true;
584
+ },
585
+ async remove(ids) {
586
+ const wanted = new Set((ids || []).map(String));
587
+ if (!wanted.size) return false;
588
+ await enqueue(async (store) => {
589
+ let changed = false;
590
+ for (const id of wanted) {
591
+ if (id in store) {
592
+ delete store[id];
593
+ changed = true;
594
+ }
595
+ }
596
+ if (!changed) return;
597
+ await mkdir3(dirname(path), { recursive: true });
598
+ const tmp = join3(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
599
+ await writeFile3(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: store }), { encoding: "utf8", mode: 384 });
600
+ await rename3(tmp, path);
601
+ });
602
+ return true;
603
+ }
604
+ };
605
+ }
606
+
607
+ // src/compat/persistence.js
608
+ function asHeader(value) {
609
+ if (!value || typeof value !== "object") return null;
610
+ const candidate = value.header && typeof value.header === "object" ? value.header : value;
611
+ return candidate.id == null ? null : candidate;
612
+ }
613
+ function normalizePersistenceEntry(value) {
614
+ const header = asHeader(value);
615
+ if (!header) return null;
616
+ const snapshot = value && value.header === header ? value : null;
617
+ return {
618
+ header,
619
+ snapshot,
620
+ id: String(header.id),
621
+ sizeBytes: snapshot && Number.isFinite(snapshot.sizeBytes) ? Number(snapshot.sizeBytes) : null,
622
+ eventCount: snapshot && Number.isSafeInteger(snapshot.eventCount) ? snapshot.eventCount : null,
623
+ revision: snapshot ? snapshot.revision : null
624
+ };
625
+ }
626
+ function normalizePersistenceList(values) {
627
+ if (!Array.isArray(values)) return [];
628
+ return values.map(normalizePersistenceEntry).filter(Boolean);
629
+ }
630
+ function createPersistenceAdapter(service) {
631
+ if (!service || typeof service.list !== "function") throw new TypeError("sessionPersistence.list is required");
632
+ async function listEntries(options) {
633
+ return normalizePersistenceList(await service.list(options));
634
+ }
635
+ async function readSession(id, offset = 0) {
636
+ if (typeof service.readFrom === "function") {
637
+ const result = await service.readFrom(id, offset);
638
+ return {
639
+ meta: result && result.meta ? result.meta : null,
640
+ inheritedEventCount: result && Number.isSafeInteger(result.inheritedEventCount) ? result.inheritedEventCount : 0,
641
+ events: result && Array.isArray(result.events) ? result.events : []
642
+ };
643
+ }
644
+ if (typeof service.open !== "function") throw new Error("\u5F53\u524D DSH \u6301\u4E45\u5316\u670D\u52A1\u4E0D\u652F\u6301\u8BFB\u53D6\u4F1A\u8BDD");
645
+ const handle = await service.open(id, "read");
646
+ if (!handle || typeof handle.read !== "function" || typeof handle.close !== "function") {
647
+ try {
648
+ if (handle && typeof handle.close === "function") await handle.close();
649
+ } catch {
650
+ }
651
+ throw new Error("DSH \u8FD4\u56DE\u4E86\u65E0\u6548\u7684 SessionHandle");
652
+ }
653
+ try {
654
+ const events = await handle.read(offset);
655
+ return {
656
+ meta: handle.header || handle.meta || null,
657
+ inheritedEventCount: Number.isSafeInteger(handle.inheritedEventCount) ? handle.inheritedEventCount : 0,
658
+ events: Array.isArray(events) ? events : [...events || []]
659
+ };
660
+ } finally {
661
+ await handle.close();
662
+ }
663
+ }
664
+ function locate(header) {
665
+ if (typeof service.locate === "function") return service.locate(header);
666
+ return null;
667
+ }
668
+ const kind = typeof service.open === "function" ? "session-handle" : "legacy";
669
+ return { kind, listEntries, readSession, locate };
670
+ }
671
+
672
+ // src/compat/capabilities.js
673
+ function action(available, reason = null) {
674
+ return { available: !!available, reason: available ? null : reason };
675
+ }
676
+ function detectCapabilities({ persistence, workspaceRegistry }) {
677
+ const handleApi = !!(persistence && typeof persistence.open === "function");
678
+ const legacyRead = !!(persistence && typeof persistence.readFrom === "function");
679
+ const legacyLocate = !!(persistence && (typeof persistence.locate === "function" || persistence.backend && typeof persistence.backend.locate === "function"));
680
+ const workspaceInternals = !!(workspaceRegistry && workspaceRegistry.headers && workspaceRegistry.sessionPaths && typeof workspaceRegistry.replaceHeaderIndex === "function");
681
+ return {
682
+ persistence: handleApi ? "session-handle" : "legacy",
683
+ actions: {
684
+ read: action(legacyRead || handleApi, "\u5F53\u524D DSH \u672A\u63D0\u4F9B\u53EF\u8BC6\u522B\u7684\u4F1A\u8BDD\u8BFB\u53D6\u63A5\u53E3"),
685
+ archive: action(!!(workspaceRegistry && typeof workspaceRegistry.archiveSession === "function"), "\u5F53\u524D DSH \u672A\u63D0\u4F9B\u5F52\u6863\u63A5\u53E3"),
686
+ trash: action(legacyRead || handleApi, "\u5F53\u524D DSH \u65E0\u6CD5\u8BFB\u53D6\u4F1A\u8BDD\uFF0C\u4E0D\u80FD\u5B89\u5168\u79FB\u5165\u56DE\u6536\u7AD9"),
687
+ restoreTrash: action(true),
688
+ purge: action(!handleApi && legacyLocate, "\u5F53\u524D DSH \u7248\u672C\u5C1A\u672A\u63D0\u4F9B\u7ECF\u8FC7\u9A8C\u8BC1\u7684\u5B89\u5168\u6C38\u4E45\u5220\u9664\u80FD\u529B"),
689
+ move: action(!handleApi && legacyRead && legacyLocate && workspaceInternals, "\u5F53\u524D DSH \u7248\u672C\u5C1A\u672A\u63D0\u4F9B\u7ECF\u8FC7\u9A8C\u8BC1\u7684\u8DE8\u5DE5\u4F5C\u533A\u8FC1\u79FB\u80FD\u529B")
690
+ }
691
+ };
692
+ }
693
+ function requireCapability(capabilities, name2) {
694
+ const value = capabilities && capabilities.actions && capabilities.actions[name2];
695
+ if (value && value.available) return;
696
+ const error = new Error(value && value.reason || `\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301 ${name2}`);
697
+ error.status = 409;
698
+ error.code = "DSM_CAPABILITY_UNAVAILABLE";
699
+ throw error;
700
+ }
701
+
503
702
  // src/index.js
504
703
  var name = "dsh-sessions-manager";
505
704
  var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
506
705
  var MAX_TITLE = 80;
507
- var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR || join3(homedir3(), ".dsh", "sessions-manager-trash");
508
- var TRASH_INDEX = join3(TRASH_DIR, "index.json");
706
+ var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR || join4(homedir3(), ".dsh", "sessions-manager-trash");
707
+ var TRASH_INDEX = join4(TRASH_DIR, "index.json");
509
708
  var TRASH_SCHEMA_VERSION = 2;
510
709
  var DEFAULT_TRASH_SETTINGS = Object.freeze({ retentionDays: 0 });
511
710
  var FETCH_TOOL_RE = /search|fetch|download|browse/i;
@@ -586,10 +785,46 @@ function foldTitle(events) {
586
785
  function apply(ctx) {
587
786
  const w = ctx.workspaceRegistry;
588
787
  const sp = ctx.sessionPersistence;
788
+ const persistence = createPersistenceAdapter(sp);
789
+ const capabilities = detectCapabilities({ persistence: sp, workspaceRegistry: w });
589
790
  const sq = ctx.sessionQuery;
590
791
  const dom = () => ctx.storageDomain.get("workspace");
591
792
  const authorityTitleCache = /* @__PURE__ */ new Map();
592
793
  const metaCache = createSessionMetaCache();
794
+ const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file: join4(TRASH_DIR, "title-index.json") });
795
+ async function hydrateFromPersist(ids, statsById) {
796
+ const hits = /* @__PURE__ */ new Map();
797
+ if (!ids || !ids.length) return hits;
798
+ let store;
799
+ try {
800
+ store = await titleIndex.entries();
801
+ } catch (e) {
802
+ return hits;
803
+ }
804
+ for (const id of ids) {
805
+ const stat2 = statsById.get(id);
806
+ const entry = store && store[id];
807
+ if (!stat2 || !entry) continue;
808
+ const fp = fingerprintOf(stat2);
809
+ if (fp && entry.fingerprint === fp) {
810
+ hits.set(id, { title: entry.title, cwd: entry.cwd, createdAt: entry.createdAt });
811
+ }
812
+ }
813
+ return hits;
814
+ }
815
+ function persistDecoded(decoded, statsById) {
816
+ if (!decoded || !decoded.size) return;
817
+ const batch = {};
818
+ const now = Date.now();
819
+ for (const [id, meta] of decoded) {
820
+ const fp = fingerprintOf(statsById.get(id));
821
+ if (!fp) continue;
822
+ batch[id] = { title: meta.title, cwd: meta.cwd, createdAt: meta.createdAt, fingerprint: fp, updatedAt: now };
823
+ }
824
+ if (!Object.keys(batch).length) return;
825
+ titleIndex.merge(batch).catch(() => {
826
+ });
827
+ }
593
828
  function metaFromSnapshot(o) {
594
829
  let title = null, createdAt = null, cwd = null;
595
830
  if (o) {
@@ -676,7 +911,7 @@ function apply(ctx) {
676
911
  }
677
912
  if (!meta.title || !meta.cwd) {
678
913
  try {
679
- const r = await sp.readFrom(id, 0);
914
+ const r = await persistence.readSession(id, 0);
680
915
  if (r.meta) {
681
916
  if (!meta.cwd) meta.cwd = r.meta.cwd || null;
682
917
  if (!meta.createdAt) meta.createdAt = r.meta.createdAt || null;
@@ -686,28 +921,31 @@ function apply(ctx) {
686
921
  }
687
922
  }
688
923
  metaCache.set(key, statInfo, meta);
924
+ if (opts.collectDecoded && statInfo) opts.collectDecoded(key, meta);
689
925
  return buildItem(key, meta, usage, opts.exposeUsage);
690
926
  }
691
927
  async function collectUsage(preloadedHeaders) {
692
928
  const sizeById = /* @__PURE__ */ new Map();
693
929
  const mtimeById = /* @__PURE__ */ new Map();
694
- let headers = null;
695
- if (Array.isArray(preloadedHeaders)) headers = preloadedHeaders;
930
+ let entries = null;
931
+ if (Array.isArray(preloadedHeaders)) entries = preloadedHeaders;
696
932
  else {
697
933
  try {
698
- headers = await sp.list();
934
+ entries = await persistence.listEntries();
699
935
  } catch (e) {
700
- headers = [];
936
+ entries = [];
701
937
  }
702
938
  }
703
- if (!Array.isArray(headers)) headers = [];
939
+ if (!Array.isArray(entries)) entries = [];
704
940
  const CHUNK = 8;
705
- for (let i = 0; i < headers.length; i += CHUNK) {
706
- await Promise.all(headers.slice(i, i + CHUNK).map(async (header) => {
707
- const id = header && header.id != null ? String(header.id) : null;
941
+ for (let i = 0; i < entries.length; i += CHUNK) {
942
+ await Promise.all(entries.slice(i, i + CHUNK).map(async (entry) => {
943
+ const header = entry && entry.header ? entry.header : entry;
944
+ const id = entry && entry.id != null ? String(entry.id) : header && header.id != null ? String(header.id) : null;
708
945
  if (!id) return;
946
+ if (entry && Number.isFinite(entry.sizeBytes)) sizeById.set(id, Number(entry.sizeBytes));
709
947
  try {
710
- const loc = sp.locate(header);
948
+ const loc = persistence.locate(header);
711
949
  if (!loc || typeof loc.path !== "string" || !loc.path) return;
712
950
  const st = await stat(loc.path);
713
951
  if (!st) return;
@@ -746,10 +984,10 @@ function apply(ctx) {
746
984
  return (await readTrashStore()).items;
747
985
  }
748
986
  async function writeTrashStore(store) {
749
- await mkdir3(TRASH_DIR, { recursive: true });
750
- const tmp = join3(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
751
- await writeFile3(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
752
- await rename3(tmp, TRASH_INDEX);
987
+ await mkdir4(TRASH_DIR, { recursive: true });
988
+ const tmp = join4(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
989
+ await writeFile4(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
990
+ await rename4(tmp, TRASH_INDEX);
753
991
  }
754
992
  function mutateTrash(mutator) {
755
993
  const operation = trashMutation.then(async () => {
@@ -779,17 +1017,20 @@ function apply(ctx) {
779
1017
  let cwd = null;
780
1018
  let title = null;
781
1019
  let removedPath = null;
1020
+ let persistenceEntry = null;
782
1021
  try {
783
- const headers = await sp.list();
784
- header = headers.find((h) => String(h.id) === sid) || null;
1022
+ const entries = await persistence.listEntries();
1023
+ const found = entries.find((entry) => entry.id === sid) || null;
1024
+ persistenceEntry = found;
1025
+ header = found ? found.header : null;
785
1026
  if (header) {
786
- const loc = sp.locate(header);
1027
+ const loc = persistence.locate(header);
787
1028
  if (loc && typeof loc.path === "string") removedPath = loc.path;
788
1029
  cwd = header.cwd || null;
789
1030
  title = header.title || header.meta && header.meta.title || null;
790
1031
  }
791
1032
  if (!title) {
792
- const r = await sp.readFrom(sid, 0);
1033
+ const r = await persistence.readSession(sid, 0);
793
1034
  if (r && r.meta) {
794
1035
  if (!cwd) cwd = r.meta.cwd;
795
1036
  title = foldTitle(r.events);
@@ -810,7 +1051,7 @@ function apply(ctx) {
810
1051
  cwd: cwd || null,
811
1052
  header: header || null,
812
1053
  originalPath: removedPath || null,
813
- sizeBytes: header && typeof header.size === "number" ? header.size : null,
1054
+ sizeBytes: persistenceEntry && Number.isFinite(persistenceEntry.sizeBytes) ? persistenceEntry.sizeBytes : null,
814
1055
  wasArchived: archived,
815
1056
  deletedAt: Date.now()
816
1057
  };
@@ -838,6 +1079,7 @@ function apply(ctx) {
838
1079
  }
839
1080
  async function purgeFromTrash(sid) {
840
1081
  requireSessionId(sid);
1082
+ requireCapability(capabilities, "purge");
841
1083
  let purged = false;
842
1084
  await mutateTrash(async (store) => {
843
1085
  const entry = store.items.find((t) => String(t.sessionId) === sid);
@@ -848,14 +1090,19 @@ function apply(ctx) {
848
1090
  }
849
1091
  let target = null;
850
1092
  try {
851
- const headers = await sp.list();
852
- const current = headers.find((h) => String(h.id) === sid);
853
- const located = current && sp.locate(current);
1093
+ const entries = await persistence.listEntries();
1094
+ const current = entries.find((entry2) => entry2.id === sid);
1095
+ const located = current && persistence.locate(current.header);
854
1096
  if (located && typeof located.path === "string") target = located.path;
855
1097
  } catch (e) {
856
1098
  }
857
1099
  if (!target && typeof entry.originalPath === "string") target = entry.originalPath;
858
- const targetOwnsSession = target && (basename(dirname(target)) === sid || basename(target).includes(sid));
1100
+ if (!target) {
1101
+ const error = new Error("\u65E0\u6CD5\u786E\u8BA4\u8BE5\u4F1A\u8BDD\u7684\u7269\u7406\u65E5\u5FD7\u4F4D\u7F6E\uFF0C\u5DF2\u505C\u6B62\u6C38\u4E45\u5220\u9664");
1102
+ error.status = 409;
1103
+ throw error;
1104
+ }
1105
+ const targetOwnsSession = target && (basename(dirname2(target)) === sid || basename(target).includes(sid));
859
1106
  if (target && !targetOwnsSession) {
860
1107
  const error = new Error("\u65E5\u5FD7\u8DEF\u5F84\u4E0E\u4F1A\u8BDD ID \u4E0D\u5339\u914D\uFF0C\u5DF2\u505C\u6B62\u6C38\u4E45\u5220\u9664");
861
1108
  error.status = 409;
@@ -930,6 +1177,7 @@ function apply(ctx) {
930
1177
  return (await readTrashStore()).settings;
931
1178
  }
932
1179
  async function cleanupExpiredTrash() {
1180
+ if (!capabilities.actions.purge.available) return 0;
933
1181
  const store = await readTrashStore();
934
1182
  const days = store.settings.retentionDays;
935
1183
  if (!days) return 0;
@@ -948,8 +1196,8 @@ function apply(ctx) {
948
1196
  async function moveTargetWorkspace(rawPath) {
949
1197
  if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
950
1198
  let p = String(rawPath).trim();
951
- if (p.startsWith("~/")) p = join3(homedir3(), p.slice(2));
952
- if (!isAbsolute(p)) p = join3(homedir3(), p);
1199
+ if (p.startsWith("~/")) p = join4(homedir3(), p.slice(2));
1200
+ if (!isAbsolute(p)) p = join4(homedir3(), p);
953
1201
  let canonical = null;
954
1202
  try {
955
1203
  canonical = await realpath(p);
@@ -957,17 +1205,18 @@ function apply(ctx) {
957
1205
  canonical = null;
958
1206
  }
959
1207
  if (canonical === null) {
960
- await mkdir3(p, { recursive: true });
1208
+ await mkdir4(p, { recursive: true });
961
1209
  canonical = await realpath(p);
962
1210
  }
963
1211
  return { canonical, entity: await w.create(canonical, basename(canonical) || "workspace") };
964
1212
  }
965
1213
  async function moveOne(sid, targetPath) {
1214
+ requireCapability(capabilities, "move");
966
1215
  const activeId = getActiveSessionId(ctx);
967
1216
  if (activeId != null && String(activeId) === String(sid)) {
968
1217
  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");
969
1218
  }
970
- const r = await sp.readFrom(sid, 0);
1219
+ const r = await persistence.readSession(sid, 0);
971
1220
  if (!r || !r.meta) throw new Error("\u65E0\u6CD5\u8BFB\u53D6\u8BE5\u4F1A\u8BDD\u7684\u65E5\u5FD7");
972
1221
  const meta = r.meta;
973
1222
  const events = r.events;
@@ -995,13 +1244,13 @@ function apply(ctx) {
995
1244
  if (!oldPath || !newPath || oldPath === newPath) return false;
996
1245
  const backupPath = `${oldPath}.move-backup-${Date.now()}`;
997
1246
  try {
998
- await mkdir3(dirname(newPath), { recursive: true });
999
- await rename3(oldPath, backupPath);
1247
+ await mkdir4(dirname2(newPath), { recursive: true });
1248
+ await rename4(oldPath, backupPath);
1000
1249
  await rewriteFrame0Cwd(backupPath, canonical);
1001
- await rename3(backupPath, newPath);
1250
+ await rename4(backupPath, newPath);
1002
1251
  } catch (e) {
1003
1252
  try {
1004
- await rename3(backupPath, oldPath);
1253
+ await rename4(backupPath, oldPath);
1005
1254
  } catch (_) {
1006
1255
  }
1007
1256
  if (e && e.code !== "ENOENT") throw e;
@@ -1026,7 +1275,7 @@ function apply(ctx) {
1026
1275
  return null;
1027
1276
  };
1028
1277
  if (isOpen) {
1029
- await relocateLog(meta, newHeader);
1278
+ if (!await relocateLog(meta, newHeader)) throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u786E\u8BA4\u4F1A\u8BDD\u65E5\u5FD7\u5DF2\u8FC1\u79FB\u5230\u76EE\u6807\u5DE5\u4F5C\u533A");
1030
1279
  try {
1031
1280
  const st = sp.states && sp.states.get && sp.states.get(sid);
1032
1281
  if (st && st.meta) st.meta = Object.assign({}, st.meta, { cwd: canonical });
@@ -1042,12 +1291,12 @@ function apply(ctx) {
1042
1291
  oldPath = null;
1043
1292
  }
1044
1293
  if (typeof sp.create !== "function" || typeof sp.append !== "function") {
1045
- await relocateLog(meta, newHeader);
1294
+ if (!await relocateLog(meta, newHeader)) throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u786E\u8BA4\u4F1A\u8BDD\u65E5\u5FD7\u5DF2\u8FC1\u79FB\u5230\u76EE\u6807\u5DE5\u4F5C\u533A");
1046
1295
  } else {
1047
1296
  const backupPath = oldPath ? `${oldPath}.move-backup-${Date.now()}` : null;
1048
1297
  if (backupPath) {
1049
1298
  try {
1050
- await rename3(oldPath, backupPath);
1299
+ await rename4(oldPath, backupPath);
1051
1300
  } catch (e) {
1052
1301
  if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
1053
1302
  }
@@ -1055,7 +1304,7 @@ function apply(ctx) {
1055
1304
  const restore = async () => {
1056
1305
  if (backupPath) {
1057
1306
  try {
1058
- await rename3(backupPath, oldPath);
1307
+ await rename4(backupPath, oldPath);
1059
1308
  } catch (_) {
1060
1309
  }
1061
1310
  }
@@ -1063,7 +1312,7 @@ function apply(ctx) {
1063
1312
  try {
1064
1313
  await sp.create(newHeader);
1065
1314
  await sp.append(sid, events);
1066
- const check = await sp.readFrom(sid, 0);
1315
+ const check = await persistence.readSession(sid, 0);
1067
1316
  if (!check || !check.meta || check.meta.cwd !== canonical) {
1068
1317
  throw new Error("\u79FB\u52A8\u540E\u6821\u9A8C\u5931\u8D25\uFF1A\u4F1A\u8BDD\u5DE5\u4F5C\u76EE\u5F55\u672A\u6B63\u786E\u66F4\u65B0");
1069
1318
  }
@@ -1076,7 +1325,7 @@ function apply(ctx) {
1076
1325
  } catch (e) {
1077
1326
  if (ALREADY_EXISTS_RE.test(String(e && e.message || e))) {
1078
1327
  await restore();
1079
- await relocateLog(meta, newHeader);
1328
+ if (!await relocateLog(meta, newHeader)) throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u786E\u8BA4\u4F1A\u8BDD\u65E5\u5FD7\u5DF2\u8FC1\u79FB\u5230\u76EE\u6807\u5DE5\u4F5C\u533A");
1080
1329
  } else {
1081
1330
  await restore();
1082
1331
  throw new Error("\u79FB\u52A8\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A" + String(e && e.message || e));
@@ -1122,14 +1371,14 @@ function apply(ctx) {
1122
1371
  async function reindexRegistry() {
1123
1372
  const reg = w;
1124
1373
  if (!reg || typeof reg.replaceHeaderIndex !== "function") return false;
1125
- let headers = null;
1374
+ let entries = null;
1126
1375
  try {
1127
- headers = await sp.list();
1376
+ entries = await persistence.listEntries();
1128
1377
  } catch (e) {
1129
- headers = null;
1378
+ entries = null;
1130
1379
  }
1131
- if (!headers || !Array.isArray(headers)) return false;
1132
- await reg.replaceHeaderIndex(headers);
1380
+ if (!entries || !Array.isArray(entries)) return false;
1381
+ await reg.replaceHeaderIndex(entries.map((entry) => entry.header));
1133
1382
  if (typeof reg.rebuildEntities === "function") reg.rebuildEntities();
1134
1383
  return true;
1135
1384
  }
@@ -1165,17 +1414,17 @@ function apply(ctx) {
1165
1414
  return out;
1166
1415
  }
1167
1416
  async function allSessionItems(opts = {}) {
1168
- let headers = [];
1417
+ let entries = [];
1169
1418
  let headersOk = false;
1170
1419
  try {
1171
- headers = await sp.list();
1172
- headersOk = Array.isArray(headers);
1173
- if (!headersOk) headers = [];
1420
+ entries = await persistence.listEntries();
1421
+ headersOk = Array.isArray(entries);
1422
+ if (!headersOk) entries = [];
1174
1423
  } catch (e) {
1175
- headers = [];
1424
+ entries = [];
1176
1425
  }
1177
1426
  let live = ctx.get("sessions");
1178
- const ids = headers.map((h) => String(h.id));
1427
+ const ids = entries.map((entry) => entry.id);
1179
1428
  if (live) {
1180
1429
  try {
1181
1430
  live.list().forEach((s) => {
@@ -1200,18 +1449,27 @@ function apply(ctx) {
1200
1449
  }
1201
1450
  const currentArchived = new Set((await archivedState().catch(() => ({ archivedSessionIds: [] }))).archivedSessionIds || []);
1202
1451
  const items = [];
1203
- const usage = await collectUsage(headers);
1452
+ const usage = await collectUsage(entries);
1204
1453
  const statsById = new Map(visibleIds.map((id) => [id, { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }]));
1205
1454
  const { cached, missing } = metaCache.partition(visibleIds, statsById);
1206
- const snapshotById = await projectTitles(missing);
1455
+ const persisted = await hydrateFromPersist(missing, statsById);
1456
+ for (const [id, meta] of persisted) metaCache.set(id, statsById.get(id), meta);
1457
+ const stillMissing = missing.filter((id) => !persisted.has(id));
1458
+ const snapshotById = await projectTitles(stillMissing);
1459
+ const decoded = /* @__PURE__ */ new Map();
1460
+ const collectDecoded = (id, meta) => {
1461
+ decoded.set(id, meta);
1462
+ };
1207
1463
  const CHUNK = 6;
1208
1464
  for (let i = 0; i < visibleIds.length; i += CHUNK) {
1209
1465
  const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map((id) => resolveOne(id, usage, {
1210
1466
  exposeUsage: !!(opts && opts.usage),
1211
- preloaded: snapshotById.has(id) ? snapshotById.get(id) : void 0
1467
+ preloaded: snapshotById.has(id) ? snapshotById.get(id) : void 0,
1468
+ collectDecoded
1212
1469
  })));
1213
1470
  for (const it of res2) items.push({ ...it, archived: currentArchived.has(it.sessionId) });
1214
1471
  }
1472
+ persistDecoded(decoded, statsById);
1215
1473
  let starredSet = /* @__PURE__ */ new Set();
1216
1474
  try {
1217
1475
  starredSet = new Set((await stars.read()).starredSessionIds);
@@ -1257,14 +1515,14 @@ function apply(ctx) {
1257
1515
  }
1258
1516
  async function sidebarAuthority() {
1259
1517
  const ids = [];
1260
- let headers = [];
1518
+ let entries = [];
1261
1519
  try {
1262
- headers = await sp.list();
1520
+ entries = await persistence.listEntries();
1263
1521
  } catch (e) {
1264
- headers = [];
1522
+ entries = [];
1265
1523
  }
1266
- if (!Array.isArray(headers)) headers = [];
1267
- for (const header of headers) ids.push(String(header.id));
1524
+ if (!Array.isArray(entries)) entries = [];
1525
+ for (const entry of entries) ids.push(entry.id);
1268
1526
  const sessions = ctx.get("sessions");
1269
1527
  try {
1270
1528
  if (sessions) sessions.list().forEach((session) => {
@@ -1275,20 +1533,29 @@ function apply(ctx) {
1275
1533
  }
1276
1534
  const store = await readTrashStore();
1277
1535
  if (ids.length) {
1278
- const usage = await collectUsage(headers);
1536
+ const usage = await collectUsage(entries);
1279
1537
  const statsById = new Map(ids.map((id) => [id, { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }]));
1280
1538
  const { cached, missing } = metaCache.partition(ids, statsById);
1281
- const snapshotById = await projectTitles(missing);
1539
+ const persisted = await hydrateFromPersist(missing, statsById);
1540
+ for (const [id, meta] of persisted) metaCache.set(id, statsById.get(id), meta);
1541
+ const rest = missing.filter((id) => !persisted.has(id));
1542
+ const snapshotById = await projectTitles(rest);
1543
+ const decoded = /* @__PURE__ */ new Map();
1544
+ const collectDecoded = (id, meta) => {
1545
+ decoded.set(id, meta);
1546
+ };
1282
1547
  for (const id of ids) {
1283
- let meta = cached.get(id) || null;
1548
+ let meta = cached.get(id) || persisted.get(id) || null;
1284
1549
  if (!meta) {
1285
1550
  const snapshot = snapshotById.has(id) ? snapshotById.get(id) : typeof sq.readTitleSnapshot === "function" ? await sq.readTitleSnapshot(id).catch(() => null) : null;
1286
1551
  const next = metaFromSnapshot(snapshot);
1287
1552
  metaCache.set(id, statsById.get(id), next);
1553
+ if (statsById.get(id)) collectDecoded(id, next);
1288
1554
  meta = next;
1289
1555
  }
1290
1556
  if (meta && meta.title) authorityTitleCache.set(id, String(meta.title));
1291
1557
  }
1558
+ persistDecoded(decoded, statsById);
1292
1559
  }
1293
1560
  return {
1294
1561
  titles: Object.fromEntries(authorityTitleCache),
@@ -1309,14 +1576,14 @@ function apply(ctx) {
1309
1576
  events = [];
1310
1577
  }
1311
1578
  } else {
1312
- const r = await sp.readFrom(sid, 0);
1579
+ const r = await persistence.readSession(sid, 0);
1313
1580
  if (!r || !r.meta) throw new Error("\u627E\u4E0D\u5230\u8BE5\u4F1A\u8BDD\u7684\u8BB0\u5F55\uFF08\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF09");
1314
1581
  meta = r.meta;
1315
1582
  events = Array.isArray(r.events) ? r.events : [];
1316
1583
  }
1317
1584
  let sizeBytes = null;
1318
1585
  try {
1319
- const loc = sp.locate(meta);
1586
+ const loc = persistence.locate(meta);
1320
1587
  if (loc && typeof loc.path === "string" && loc.path) {
1321
1588
  const st = await stat(loc.path);
1322
1589
  if (st && typeof st.size === "number") sizeBytes = st.size;
@@ -1401,7 +1668,8 @@ function apply(ctx) {
1401
1668
  const subagentSet = /* @__PURE__ */ new Set();
1402
1669
  try {
1403
1670
  if (typeof sp.list === "function") {
1404
- for (const h of await sp.list()) {
1671
+ for (const entry of await persistence.listEntries()) {
1672
+ const h = entry.header;
1405
1673
  if (String(h.parentSession) !== String(sid)) continue;
1406
1674
  if (h.origin === "subagent") subagentSet.add(h.id);
1407
1675
  else childrenSet.add(h.id);
@@ -1438,6 +1706,11 @@ function apply(ctx) {
1438
1706
  authorityTitleCache.set(String(session.id), event.data.title);
1439
1707
  }
1440
1708
  }));
1709
+ disposers.push(ctx.webServer.register({
1710
+ kind: "exact",
1711
+ path: "/archived-sessions/capabilities",
1712
+ handler: async (req, res) => json(res, capabilities)
1713
+ }));
1441
1714
  disposers.push(ctx.webServer.register({
1442
1715
  kind: "exact",
1443
1716
  path: "/archived-sessions/list",
@@ -1448,8 +1721,8 @@ function apply(ctx) {
1448
1721
  let materialized = /* @__PURE__ */ new Set();
1449
1722
  let live = ctx.get("sessions");
1450
1723
  try {
1451
- const headers = await sp.list();
1452
- materialized = new Set(headers.map((h) => String(h.id)));
1724
+ const entries = await persistence.listEntries();
1725
+ materialized = new Set(entries.map((entry) => entry.id));
1453
1726
  } catch (e) {
1454
1727
  }
1455
1728
  const trashStore = await readTrashStore();
@@ -1500,12 +1773,12 @@ function apply(ctx) {
1500
1773
  try {
1501
1774
  results.push({ sessionId: sid, ok: true, ...await restoreOne(sid) });
1502
1775
  } catch (e) {
1503
- results.push({ sessionId: sid, ok: false, error: String(e && e.message || e) });
1776
+ results.push({ sessionId: sid, ok: false, code: e && e.code, error: String(e && e.message || e) });
1504
1777
  }
1505
1778
  }
1506
1779
  json(res, { ok: true, restored: results.filter((r) => r.ok).length, results });
1507
1780
  } catch (e) {
1508
- json(res, { ok: false, error: String(e && e.message || e) }, 500);
1781
+ json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
1509
1782
  }
1510
1783
  }
1511
1784
  }));
@@ -1544,7 +1817,7 @@ function apply(ctx) {
1544
1817
  }
1545
1818
  json(res, { ok: true, deleted: results.filter((r) => r.ok).length, results });
1546
1819
  } catch (e) {
1547
- json(res, { ok: false, error: String(e && e.message || e) }, 500);
1820
+ json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
1548
1821
  }
1549
1822
  }
1550
1823
  }));
@@ -1583,11 +1856,19 @@ function apply(ctx) {
1583
1856
  try {
1584
1857
  const items = await readTrash();
1585
1858
  const results = await Promise.all(items.map(async (item) => {
1586
- let exists = false;
1587
- if (typeof item.originalPath === "string") exists = await stat(item.originalPath).then(() => true).catch(() => false);
1588
- return { sessionId: item.sessionId, status: exists ? "ok" : "missing", originalPath: item.originalPath || null };
1859
+ if (typeof item.originalPath !== "string" || !item.originalPath) {
1860
+ return { sessionId: item.sessionId, status: "unverified", originalPath: null };
1861
+ }
1862
+ const exists = await stat(item.originalPath).then(() => true).catch(() => false);
1863
+ return { sessionId: item.sessionId, status: exists ? "ok" : "missing", originalPath: item.originalPath };
1589
1864
  }));
1590
- json(res, { ok: true, healthy: results.filter((r) => r.status === "ok").length, missing: results.filter((r) => r.status === "missing").length, results });
1865
+ json(res, {
1866
+ ok: true,
1867
+ healthy: results.filter((r) => r.status === "ok").length,
1868
+ missing: results.filter((r) => r.status === "missing").length,
1869
+ unverified: results.filter((r) => r.status === "unverified").length,
1870
+ results
1871
+ });
1591
1872
  } catch (e) {
1592
1873
  json(res, { ok: false, error: String(e && e.message || e) }, errorStatus(e));
1593
1874
  }
@@ -1605,7 +1886,7 @@ function apply(ctx) {
1605
1886
  metaCache.invalidate(sid);
1606
1887
  json(res, out);
1607
1888
  } catch (e) {
1608
- json(res, { ok: false, error: String(e && e.message || e) }, 500);
1889
+ json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
1609
1890
  }
1610
1891
  }
1611
1892
  }));
@@ -1619,9 +1900,11 @@ function apply(ctx) {
1619
1900
  if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
1620
1901
  const out = await purgeFromTrash(sid);
1621
1902
  metaCache.invalidate(sid);
1903
+ titleIndex.remove([sid]).catch(() => {
1904
+ });
1622
1905
  json(res, out);
1623
1906
  } catch (e) {
1624
- json(res, { ok: false, error: String(e && e.message || e) }, 500);
1907
+ json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
1625
1908
  }
1626
1909
  }
1627
1910
  }));
@@ -1638,13 +1921,15 @@ function apply(ctx) {
1638
1921
  try {
1639
1922
  results.push({ sessionId: sid, ok: true, ...await purgeFromTrash(sid) });
1640
1923
  metaCache.invalidate(sid);
1924
+ titleIndex.remove([sid]).catch(() => {
1925
+ });
1641
1926
  } catch (e) {
1642
1927
  results.push({ sessionId: sid, ok: false, error: String(e && e.message || e) });
1643
1928
  }
1644
1929
  }
1645
1930
  json(res, { ok: true, purged: results.filter((r) => r.ok).length, results });
1646
1931
  } catch (e) {
1647
- json(res, { ok: false, error: String(e && e.message || e) }, 500);
1932
+ json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
1648
1933
  }
1649
1934
  }
1650
1935
  }));
@@ -1686,7 +1971,7 @@ function apply(ctx) {
1686
1971
  const url = new URL(req.url, "http://localhost");
1687
1972
  const sid = url.searchParams.get("sessionId");
1688
1973
  requireSessionId(sid);
1689
- const r = await sp.readFrom(sid, 0);
1974
+ const r = await persistence.readSession(sid, 0);
1690
1975
  if (!r || !r.meta) {
1691
1976
  const error = new Error("\u65E0\u6CD5\u8BFB\u53D6\u8BE5\u4F1A\u8BDD\u7684\u65E5\u5FD7");
1692
1977
  error.status = 404;
@@ -1744,7 +2029,7 @@ function apply(ctx) {
1744
2029
  }
1745
2030
  json(res, { sessionId: sid, ...moved });
1746
2031
  } catch (e) {
1747
- json(res, { ok: false, error: String(e && e.message || e) }, 500);
2032
+ json(res, { ok: false, code: e && e.code, error: String(e && e.message || e) }, errorStatus(e));
1748
2033
  }
1749
2034
  }
1750
2035
  }));
@@ -1819,8 +2104,16 @@ function apply(ctx) {
1819
2104
  const patch = {};
1820
2105
  if (body && Object.prototype.hasOwnProperty.call(body, "inactiveDays")) patch.inactiveDays = body.inactiveDays;
1821
2106
  if (body && Object.prototype.hasOwnProperty.call(body, "skipStarred")) patch.skipStarred = body.skipStarred;
1822
- const settings = Object.keys(patch).length ? await autoArchive.update(patch) : (await autoArchive.read()).settings;
1823
- const sweep = await autoArchiveSweep();
2107
+ const isPatch = Object.keys(patch).length > 0;
2108
+ const settings = isPatch ? await autoArchive.update(patch) : (await autoArchive.read()).settings;
2109
+ let sweep;
2110
+ if (isPatch) {
2111
+ sweep = await autoArchiveSweep();
2112
+ } else {
2113
+ void autoArchiveSweep().catch(() => {
2114
+ });
2115
+ sweep = { triggered: true };
2116
+ }
1824
2117
  const store = await autoArchive.read();
1825
2118
  json(res, {
1826
2119
  ok: true,