dsh-sessions-manager 3.6.1 → 3.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -702,7 +702,7 @@ function createSessionMetaCache(opts = {}) {
702
702
  // src/title-persist-index.js
703
703
  import { mkdir as mkdir4, readFile, rename as rename4, writeFile as writeFile4 } from "node:fs/promises";
704
704
  import { dirname, join as join5 } from "node:path";
705
- var TITLE_INDEX_SCHEMA_VERSION = 1;
705
+ var TITLE_INDEX_SCHEMA_VERSION = 2;
706
706
  var MAX_ENTRIES = 2e4;
707
707
  function normalizeEntry(raw) {
708
708
  if (!raw || typeof raw !== "object") return null;
@@ -711,9 +711,13 @@ function normalizeEntry(raw) {
711
711
  const createdAt = typeof raw.createdAt === "number" ? raw.createdAt : null;
712
712
  const fingerprint = typeof raw.fingerprint === "string" && raw.fingerprint ? raw.fingerprint : null;
713
713
  const updatedAt = typeof raw.updatedAt === "number" ? raw.updatedAt : 0;
714
+ const resolved = raw.resolved === true || raw.resolved === 1;
714
715
  if (!fingerprint || fingerprint.startsWith("rev:")) return null;
715
716
  if (!title && !cwd) return null;
716
- return { title, cwd, createdAt, fingerprint, updatedAt };
717
+ if (!title && !resolved) return null;
718
+ const entry = { title, cwd, createdAt, fingerprint, updatedAt };
719
+ if (resolved) entry.resolved = 1;
720
+ return entry;
717
721
  }
718
722
  function normalizeTitleIndex(raw) {
719
723
  const entries = {};
@@ -1414,7 +1418,7 @@ function classifyLineage(header, sizeBytes) {
1414
1418
  }
1415
1419
 
1416
1420
  // src/index.js
1417
- var BUILD_STAMP = true ? "3.6.1+f0814a9e" : "dev";
1421
+ var BUILD_STAMP = true ? "3.6.2+e36dc3fc" : "dev";
1418
1422
  var name = "dsh-sessions-manager";
1419
1423
  var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
1420
1424
  var MAX_TITLE = 80;
@@ -1499,20 +1503,6 @@ function getActiveSessionId(context) {
1499
1503
  }
1500
1504
  return null;
1501
1505
  }
1502
- function foldTitle(events) {
1503
- let found = null;
1504
- let firstUser = null;
1505
- for (const ev of events) {
1506
- if (ev.type === "session/title" && ev.data && typeof ev.data.title === "string" && ev.data.title.length) {
1507
- found = ev.data.title;
1508
- }
1509
- if (firstUser === null && ev.type === "user/message" && ev.data && Array.isArray(ev.data.content)) {
1510
- const txt = ev.data.content.filter((b) => b && b.type === "text").map((b) => b.text).filter(Boolean).join(" ").trim();
1511
- if (txt) firstUser = txt;
1512
- }
1513
- }
1514
- return found || firstUser || null;
1515
- }
1516
1506
  function apply(ctx) {
1517
1507
  const w = ctx.workspaceRegistry;
1518
1508
  const sp = ctx.sessionPersistence;
@@ -1538,7 +1528,7 @@ function apply(ctx) {
1538
1528
  if (!stat5 || !entry) continue;
1539
1529
  const fp = persistFingerprintOf(stat5);
1540
1530
  if (!fp) continue;
1541
- if (fp && entry.fingerprint === fp) {
1531
+ if (entry.fingerprint === fp && (entry.title || entry.resolved)) {
1542
1532
  hits.set(id, { title: entry.title, cwd: entry.cwd, createdAt: entry.createdAt });
1543
1533
  }
1544
1534
  }
@@ -1548,10 +1538,12 @@ function apply(ctx) {
1548
1538
  if (!decoded || !decoded.size) return;
1549
1539
  const batch = {};
1550
1540
  const now = Date.now();
1551
- for (const [id, meta] of decoded) {
1541
+ for (const [id, pack] of decoded) {
1542
+ if (!pack || !pack.resolved) continue;
1543
+ const meta = pack.meta;
1552
1544
  const fp = persistFingerprintOf(statsById.get(id));
1553
1545
  if (!fp) continue;
1554
- batch[id] = { title: meta.title, cwd: meta.cwd, createdAt: meta.createdAt, fingerprint: fp, updatedAt: now };
1546
+ batch[id] = { title: meta.title, cwd: meta.cwd, createdAt: meta.createdAt, fingerprint: fp, updatedAt: now, resolved: 1 };
1555
1547
  }
1556
1548
  if (!Object.keys(batch).length) return;
1557
1549
  try {
@@ -1634,6 +1626,60 @@ function apply(ctx) {
1634
1626
  const warmQueue = /* @__PURE__ */ new Map();
1635
1627
  let warmRunning = false;
1636
1628
  let warmKickTimer = null;
1629
+ const WARM_MAX_ATTEMPTS = 5;
1630
+ const warmFail = /* @__PURE__ */ new Map();
1631
+ function warmFpOf(stat5) {
1632
+ return fingerprintOf(stat5) || persistFingerprintOf(stat5);
1633
+ }
1634
+ function warmBlocked(id, fp) {
1635
+ const rec = warmFail.get(id);
1636
+ if (!rec) return 0;
1637
+ if (fp && rec.fp && rec.fp !== fp) {
1638
+ warmFail.delete(id);
1639
+ return 0;
1640
+ }
1641
+ const now = Date.now();
1642
+ return rec.nextAt > now ? rec.nextAt - now : 0;
1643
+ }
1644
+ function warmMarkFailure(id, stat5, header) {
1645
+ const rec = warmFail.get(id) || { count: 0, fp: null, nextAt: 0 };
1646
+ rec.count += 1;
1647
+ rec.fp = warmFpOf(stat5) || rec.fp;
1648
+ if (stat5) rec.stat = stat5;
1649
+ if (header) rec.header = header;
1650
+ const backoff = rec.count >= WARM_MAX_ATTEMPTS ? 60 * 60 * 1e3 : Math.min(5 * 60 * 1e3, 15 * 1e3 * Math.pow(2, rec.count));
1651
+ rec.nextAt = Date.now() + backoff;
1652
+ warmFail.set(id, rec);
1653
+ }
1654
+ function warmPendingNow() {
1655
+ if (!warmHasApi) return false;
1656
+ if (warmRunning || warmQueue.size) return true;
1657
+ for (const rec of warmFail.values()) if (rec.nextAt > Date.now()) return true;
1658
+ return false;
1659
+ }
1660
+ let warmRetryTimer = null;
1661
+ function scheduleWarmRetry() {
1662
+ if (warmRetryTimer) return;
1663
+ let soonest = 0;
1664
+ for (const rec of warmFail.values()) {
1665
+ const wait = rec.nextAt - Date.now();
1666
+ if (wait <= 0) {
1667
+ soonest = 1;
1668
+ break;
1669
+ }
1670
+ if (!soonest || wait < soonest) soonest = wait;
1671
+ }
1672
+ if (!soonest) return;
1673
+ warmRetryTimer = setTimeout(() => {
1674
+ warmRetryTimer = null;
1675
+ const now = Date.now();
1676
+ for (const [id, rec] of warmFail) {
1677
+ if (rec.nextAt <= now && !warmQueue.has(id)) warmQueue.set(id, { stat: rec.stat || null, header: rec.header || null });
1678
+ }
1679
+ if (warmQueue.size) scheduleWarm();
1680
+ }, soonest);
1681
+ if (typeof warmRetryTimer.unref === "function") warmRetryTimer.unref();
1682
+ }
1637
1683
  function scheduleWarm() {
1638
1684
  if (warmKickTimer || warmRunning) return;
1639
1685
  warmKickTimer = setTimeout(() => {
@@ -1644,15 +1690,44 @@ function apply(ctx) {
1644
1690
  }
1645
1691
  function enqueueWarm(ids, statsById, entryById) {
1646
1692
  if (!ids || !ids.length) return;
1693
+ if (!warmHasApi) return;
1647
1694
  for (const id of ids) {
1648
1695
  const key = String(id);
1696
+ const stat5 = statsById ? statsById.get(key) || null : null;
1697
+ const fp = warmFpOf(stat5);
1698
+ if (!fp) continue;
1699
+ const blockedMs = warmBlocked(key, fp);
1649
1700
  const entry = entryById ? entryById.get(key) : null;
1701
+ const rec = warmFail.get(key);
1702
+ if (rec) {
1703
+ rec.stat = stat5 || rec.stat;
1704
+ if (entry) rec.header = entry.header || rec.header;
1705
+ }
1706
+ if (blockedMs > 0) continue;
1650
1707
  warmQueue.set(key, {
1651
- stat: statsById ? statsById.get(key) || null : null,
1708
+ stat: stat5,
1652
1709
  header: entry && entry.header || null
1653
1710
  });
1654
1711
  }
1655
- scheduleWarm();
1712
+ if (warmQueue.size) scheduleWarm();
1713
+ scheduleWarmRetry();
1714
+ }
1715
+ async function backfillTrashTitles(decoded) {
1716
+ if (!decoded || !decoded.size) return;
1717
+ try {
1718
+ const items = await readTrash();
1719
+ const worth = items.some((t) => !t.title && decoded.get(String(t.sessionId)) && decoded.get(String(t.sessionId)).meta && decoded.get(String(t.sessionId)).meta.title);
1720
+ if (!worth) return;
1721
+ await mutateTrash((store) => {
1722
+ for (const item of store.items) {
1723
+ if (item.title) continue;
1724
+ const pack = decoded.get(String(item.sessionId));
1725
+ const t = pack && pack.meta && pack.meta.title;
1726
+ if (t) item.title = String(t);
1727
+ }
1728
+ });
1729
+ } catch (e) {
1730
+ }
1656
1731
  }
1657
1732
  async function runWarm() {
1658
1733
  if (warmRunning) return;
@@ -1663,21 +1738,27 @@ function apply(ctx) {
1663
1738
  for (const [id] of batch) warmQueue.delete(id);
1664
1739
  try {
1665
1740
  const ids = batch.map(([id]) => id);
1666
- const snapshotById = await projectTitles(ids);
1741
+ const resultById = await projectTitlesStatus(ids);
1667
1742
  const decoded = /* @__PURE__ */ new Map();
1668
1743
  const statsById = /* @__PURE__ */ new Map();
1669
1744
  for (const [id, desc] of batch) {
1670
- const snapshot = snapshotById.has(id) ? snapshotById.get(id) : null;
1671
- const meta = metaFromSnapshot(snapshot);
1745
+ const r = resultById.get(id);
1746
+ if (!r || !r.resolved) {
1747
+ warmMarkFailure(id, desc.stat, desc.header);
1748
+ continue;
1749
+ }
1750
+ warmFail.delete(id);
1751
+ const meta = metaFromSnapshot(r.snapshot || null);
1672
1752
  if (desc.header) {
1673
1753
  if (typeof desc.header.cwd === "string") meta.cwd = desc.header.cwd;
1674
1754
  if (desc.header.createdAt != null) meta.createdAt = desc.header.createdAt;
1675
1755
  }
1676
1756
  metaCache.set(id, desc.stat, meta);
1677
1757
  statsById.set(id, desc.stat);
1678
- if (desc.stat) decoded.set(id, meta);
1758
+ decoded.set(id, { meta, resolved: true });
1679
1759
  }
1680
1760
  await persistDecoded(decoded, statsById);
1761
+ await backfillTrashTitles(decoded);
1681
1762
  } catch (e) {
1682
1763
  }
1683
1764
  await new Promise((resolve) => setImmediate(resolve));
@@ -1685,6 +1766,7 @@ function apply(ctx) {
1685
1766
  } finally {
1686
1767
  warmRunning = false;
1687
1768
  if (warmQueue.size) scheduleWarm();
1769
+ scheduleWarmRetry();
1688
1770
  }
1689
1771
  }
1690
1772
  async function collectUsage(preloadedEntries) {
@@ -1804,30 +1886,19 @@ function apply(ctx) {
1804
1886
  cwd = header.cwd || null;
1805
1887
  title = header.title || header.meta && header.meta.title || null;
1806
1888
  }
1807
- if (!title) {
1808
- if (typeof sq.readTitleSnapshot === "function") {
1809
- try {
1810
- const snap = unwrapSnapshot(await sq.readTitleSnapshot(sid));
1811
- if (snap && snap.title && snap.title.title) title = String(snap.title.title);
1812
- if (snap && snap.session) {
1813
- if (!cwd) cwd = snap.session.cwd || null;
1814
- }
1815
- } catch (e) {
1816
- }
1817
- }
1818
- }
1819
1889
  if (!title) {
1820
1890
  try {
1821
- let folded = null;
1822
- const summary = await persistence.inspectSession(sid, {
1823
- onEvents: (events) => {
1824
- if (!folded) folded = foldTitle(events);
1825
- }
1826
- });
1827
- if (summary && summary.meta && !cwd) cwd = summary.meta.cwd || null;
1828
- title = folded;
1891
+ const delStat = persistenceEntry && typeof persistenceEntry.revision === "string" && persistenceEntry.revision ? { revision: persistenceEntry.revision, sizeBytes: Number.isFinite(persistenceEntry.sizeBytes) ? Number(persistenceEntry.sizeBytes) : void 0 } : removedPath ? await stat4(removedPath).then((st) => ({ mtimeMs: st.mtimeMs, size: st.size })) : null;
1892
+ if (delStat) {
1893
+ const m = metaCache.get(sid, delStat) || (await hydrateFromPersist([sid], /* @__PURE__ */ new Map([[sid, delStat]]))).get(sid);
1894
+ if (m && m.title) title = String(m.title);
1895
+ }
1829
1896
  } catch (e) {
1830
1897
  }
1898
+ if (!title) {
1899
+ const at = authorityTitleCache.get(sid);
1900
+ if (at) title = String(at);
1901
+ }
1831
1902
  }
1832
1903
  } catch (e) {
1833
1904
  }
@@ -1839,8 +1910,10 @@ function apply(ctx) {
1839
1910
  const archived = await mutateArchived((list) => ({ next: null, value: list.includes(sid) })).catch(() => false);
1840
1911
  await mutateTrash((store) => {
1841
1912
  const entry = {
1913
+ // v3.6.2 #7:不再把 cwd/裸 id 冒充标题永久定格——存 null,预热解出后
1914
+ // backfillTrashTitles 补写;渲染端(t.title || t.sessionId)自然降级。
1842
1915
  sessionId: sid,
1843
- title: title || cwd || sid,
1916
+ title: title || null,
1844
1917
  cwd: cwd || null,
1845
1918
  header: header || null,
1846
1919
  originalPath: removedPath || null,
@@ -2455,19 +2528,50 @@ function apply(ctx) {
2455
2528
  return { next: null, value: { ok: true, archived: true } };
2456
2529
  });
2457
2530
  }
2458
- async function projectTitles(ids) {
2531
+ const warmHasApi = typeof sq.readTitleSnapshots === "function" || typeof sq.readTitleSnapshot === "function";
2532
+ async function projectTitlesStatus(ids) {
2459
2533
  const out = /* @__PURE__ */ new Map();
2534
+ const markAll = (resolved) => {
2535
+ for (const id of ids) out.set(String(id), { resolved, snapshot: null });
2536
+ };
2460
2537
  if (!ids || !ids.length) return out;
2461
- if (typeof sq.readTitleSnapshots !== "function") return out;
2462
- try {
2463
- const results = await sq.readTitleSnapshots(ids);
2464
- if (!Array.isArray(results)) return out;
2465
- results.forEach((result, index) => {
2466
- const id = String(ids[index]);
2467
- out.set(id, unwrapSnapshot(result));
2468
- });
2469
- } catch (e) {
2538
+ if (!warmHasApi) {
2539
+ markAll(false);
2540
+ return out;
2541
+ }
2542
+ if (typeof sq.readTitleSnapshots === "function") {
2543
+ let results = null;
2544
+ try {
2545
+ results = await sq.readTitleSnapshots(ids);
2546
+ } catch (e) {
2547
+ results = null;
2548
+ }
2549
+ if (Array.isArray(results)) {
2550
+ results.forEach((result, index) => {
2551
+ const id = String(ids[index]);
2552
+ if (result && result.status === "fulfilled") out.set(id, { resolved: true, snapshot: unwrapSnapshot(result) });
2553
+ else if (result && result.status === "rejected") out.set(id, { resolved: false, snapshot: null });
2554
+ else if (result) out.set(id, { resolved: true, snapshot: unwrapSnapshot(result) });
2555
+ else out.set(id, { resolved: false, snapshot: null });
2556
+ });
2557
+ for (const id of ids) if (!out.has(String(id))) out.set(String(id), { resolved: false, snapshot: null });
2558
+ return out;
2559
+ }
2560
+ }
2561
+ if (typeof sq.readTitleSnapshot === "function") {
2562
+ for (const raw of ids) {
2563
+ const id = String(raw);
2564
+ try {
2565
+ const r = await sq.readTitleSnapshot(id);
2566
+ if (r && r.status === "rejected") out.set(id, { resolved: false, snapshot: null });
2567
+ else out.set(id, { resolved: true, snapshot: unwrapSnapshot(r) });
2568
+ } catch (e) {
2569
+ out.set(id, { resolved: false, snapshot: null });
2570
+ }
2571
+ }
2572
+ return out;
2470
2573
  }
2574
+ markAll(false);
2471
2575
  return out;
2472
2576
  }
2473
2577
  async function allSessionItemsDetailed(opts = {}) {
@@ -2662,40 +2766,53 @@ function apply(ctx) {
2662
2766
  titles: Object.fromEntries(authorityTitleCache),
2663
2767
  trashedSessionIds: store.items.map((item) => String(item.sessionId)),
2664
2768
  purgedSessionIds: activeTombstones,
2665
- lineage
2769
+ lineage,
2770
+ // v3.6.2:还有预热/退避重试在途 → client 缩短轮询节拍,预热一完成就把
2771
+ // 补齐的标题送回(含面板自动刷新)。无预热 API 时恒 false,绝不假忙。
2772
+ warmPending: warmPendingNow()
2666
2773
  };
2667
2774
  }
2668
2775
  const emptyScanCache = /* @__PURE__ */ new Map();
2776
+ const REFINE_BUDGET = 40;
2777
+ function applyEmptyScan(lineage, id, empty) {
2778
+ if (empty) {
2779
+ const info = lineage[id] || (lineage[id] = { origin: null, parentSession: null, delegationDepth: 0, empty: false });
2780
+ info.empty = true;
2781
+ } else if (lineage[id]) {
2782
+ const info = lineage[id];
2783
+ info.empty = false;
2784
+ if (!info.origin && !info.parentSession) delete lineage[id];
2785
+ }
2786
+ }
2669
2787
  async function refineEmptyLineage(lineage, entries) {
2670
2788
  if (!persistence || typeof persistence.inspectSession !== "function") return;
2789
+ const limiter = createLimiter(4);
2790
+ const todo = [];
2671
2791
  for (const entry of entries) {
2672
2792
  const size = entry && Number.isFinite(entry.sizeBytes) ? entry.sizeBytes : null;
2673
2793
  if (size === null || size > EMPTY_DECODE_LIMIT) continue;
2674
2794
  const id = String(entry.id);
2675
- let scanned = emptyScanCache.get(id);
2676
- if (!scanned || scanned.sizeBytes !== size) {
2677
- let isEmpty = false;
2678
- try {
2679
- const types = [];
2680
- await persistence.inspectSession(id, { onEvents: (batch) => {
2681
- for (const ev of batch || []) if (types.length < 64) types.push(ev && ev.type);
2682
- } });
2683
- isEmpty = isEmptyEventTypes(types);
2684
- } catch (e) {
2685
- isEmpty = false;
2686
- }
2687
- scanned = { sizeBytes: size, empty: isEmpty };
2688
- emptyScanCache.set(id, scanned);
2689
- }
2690
- if (scanned.empty) {
2691
- const info = lineage[id] || (lineage[id] = { origin: null, parentSession: null, delegationDepth: 0, empty: false });
2692
- info.empty = true;
2693
- } else if (lineage[id]) {
2694
- const info = lineage[id];
2695
- info.empty = false;
2696
- if (!info.origin && !info.parentSession) delete lineage[id];
2795
+ const scanned = emptyScanCache.get(id);
2796
+ if (scanned && scanned.sizeBytes === size) {
2797
+ applyEmptyScan(lineage, id, scanned.empty);
2798
+ continue;
2697
2799
  }
2800
+ if (todo.length < REFINE_BUDGET) todo.push({ id, size });
2698
2801
  }
2802
+ await Promise.all(todo.map(({ id, size }) => limiter(async () => {
2803
+ let isEmpty = false;
2804
+ try {
2805
+ const types = [];
2806
+ await persistence.inspectSession(id, { onEvents: (batch) => {
2807
+ for (const ev of batch || []) if (types.length < 64) types.push(ev && ev.type);
2808
+ } });
2809
+ isEmpty = isEmptyEventTypes(types);
2810
+ } catch (e) {
2811
+ isEmpty = false;
2812
+ }
2813
+ emptyScanCache.set(id, { sizeBytes: size, empty: isEmpty });
2814
+ applyEmptyScan(lineage, id, isEmpty);
2815
+ })));
2699
2816
  const live = /* @__PURE__ */ new Set();
2700
2817
  for (const entry of entries) {
2701
2818
  const size = entry && Number.isFinite(entry.sizeBytes) ? entry.sizeBytes : null;
@@ -3094,7 +3211,7 @@ function apply(ctx) {
3094
3211
  path: "/archived-sessions/sessions",
3095
3212
  handler: async (req, res) => {
3096
3213
  try {
3097
- json(res, { items: await allSessionItems() });
3214
+ json(res, { items: await allSessionItems(), warmPending: warmPendingNow() });
3098
3215
  } catch (e) {
3099
3216
  json(res, { error: String(e && e.message || e) }, 500);
3100
3217
  }
@@ -3198,9 +3315,18 @@ function apply(ctx) {
3198
3315
  });
3199
3316
  if (Number.isFinite(sizeBytes)) sizeById.set(id, sizeBytes);
3200
3317
  };
3318
+ const lineageStats = /* @__PURE__ */ new Map();
3319
+ const lineageHeaders = /* @__PURE__ */ new Map();
3201
3320
  try {
3202
3321
  for (const entry of await persistence.listEntries()) {
3203
3322
  addHeader(entry.header, entry.sizeBytes);
3323
+ const eid = entry && entry.id != null ? String(entry.id) : null;
3324
+ if (eid) {
3325
+ lineageHeaders.set(eid, { header: entry && entry.header || null });
3326
+ if (entry && typeof entry.revision === "string" && entry.revision) {
3327
+ lineageStats.set(eid, Number.isFinite(entry.sizeBytes) ? { revision: entry.revision, sizeBytes: Number(entry.sizeBytes) } : { revision: entry.revision });
3328
+ }
3329
+ }
3204
3330
  if (entry && entry.id != null) persistedIds.add(String(entry.id));
3205
3331
  }
3206
3332
  } catch (e) {
@@ -3252,11 +3378,17 @@ function apply(ctx) {
3252
3378
  n.children.forEach(walk);
3253
3379
  };
3254
3380
  nodes.forEach(walk);
3255
- const titles = await projectTitles(allIds);
3381
+ const lineageIds = allIds.map(String);
3382
+ const { cached: lineageCached, missing: lineageMissing } = metaCache.partition(lineageIds, lineageStats);
3383
+ const lineagePersisted = await hydrateFromPersist(lineageMissing, lineageStats);
3384
+ for (const [id, meta] of lineagePersisted) metaCache.set(id, lineageStats.get(id), meta);
3385
+ const lineageRest = lineageMissing.filter((id) => !lineagePersisted.has(id));
3386
+ if (lineageRest.length) enqueueWarm(lineageRest, lineageStats, lineageHeaders);
3387
+ const truncateTitle = (t) => t ? t.length > MAX_TITLE ? t.slice(0, MAX_TITLE) + "\u2026" : t : null;
3256
3388
  const titleOf = (id) => {
3257
- const snap = titles.get(id);
3258
- const m = snap ? metaFromSnapshot(snap) : null;
3259
- return m && m.title || null;
3389
+ const key = String(id);
3390
+ const m = lineageCached.get(key) || lineagePersisted.get(key);
3391
+ return truncateTitle(m && m.title || authorityTitleCache.get(key) || null);
3260
3392
  };
3261
3393
  const fill = (n) => {
3262
3394
  n.title = titleOf(n.sessionId);