dsh-sessions-manager 3.6.0 → 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
@@ -614,6 +614,12 @@ function fingerprintOf(stat5) {
614
614
  function isPersistableFingerprint(fingerprint) {
615
615
  return typeof fingerprint === "string" && fingerprint !== "" && !fingerprint.startsWith(REVISION_PREFIX);
616
616
  }
617
+ function persistFingerprintOf(stat5) {
618
+ const fp = fingerprintOf(stat5);
619
+ if (fp && isPersistableFingerprint(fp)) return fp;
620
+ if (stat5 && Number.isFinite(stat5.sizeBytes) && stat5.sizeBytes >= 0) return `sz:${stat5.sizeBytes}`;
621
+ return null;
622
+ }
617
623
  function isFresh(entry, stat5, now, ttlMs = DEFAULT_TTL_MS) {
618
624
  if (!entry) return false;
619
625
  const fp = fingerprintOf(stat5);
@@ -696,7 +702,7 @@ function createSessionMetaCache(opts = {}) {
696
702
  // src/title-persist-index.js
697
703
  import { mkdir as mkdir4, readFile, rename as rename4, writeFile as writeFile4 } from "node:fs/promises";
698
704
  import { dirname, join as join5 } from "node:path";
699
- var TITLE_INDEX_SCHEMA_VERSION = 1;
705
+ var TITLE_INDEX_SCHEMA_VERSION = 2;
700
706
  var MAX_ENTRIES = 2e4;
701
707
  function normalizeEntry(raw) {
702
708
  if (!raw || typeof raw !== "object") return null;
@@ -705,9 +711,13 @@ function normalizeEntry(raw) {
705
711
  const createdAt = typeof raw.createdAt === "number" ? raw.createdAt : null;
706
712
  const fingerprint = typeof raw.fingerprint === "string" && raw.fingerprint ? raw.fingerprint : null;
707
713
  const updatedAt = typeof raw.updatedAt === "number" ? raw.updatedAt : 0;
714
+ const resolved = raw.resolved === true || raw.resolved === 1;
708
715
  if (!fingerprint || fingerprint.startsWith("rev:")) return null;
709
716
  if (!title && !cwd) return null;
710
- 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;
711
721
  }
712
722
  function normalizeTitleIndex(raw) {
713
723
  const entries = {};
@@ -1408,7 +1418,7 @@ function classifyLineage(header, sizeBytes) {
1408
1418
  }
1409
1419
 
1410
1420
  // src/index.js
1411
- var BUILD_STAMP = true ? "3.6.0 built 2026-09-10T09:47:49.428Z" : "dev";
1421
+ var BUILD_STAMP = true ? "3.6.2+e36dc3fc" : "dev";
1412
1422
  var name = "dsh-sessions-manager";
1413
1423
  var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
1414
1424
  var MAX_TITLE = 80;
@@ -1493,20 +1503,6 @@ function getActiveSessionId(context) {
1493
1503
  }
1494
1504
  return null;
1495
1505
  }
1496
- function foldTitle(events) {
1497
- let found = null;
1498
- let firstUser = null;
1499
- for (const ev of events) {
1500
- if (ev.type === "session/title" && ev.data && typeof ev.data.title === "string" && ev.data.title.length) {
1501
- found = ev.data.title;
1502
- }
1503
- if (firstUser === null && ev.type === "user/message" && ev.data && Array.isArray(ev.data.content)) {
1504
- const txt = ev.data.content.filter((b) => b && b.type === "text").map((b) => b.text).filter(Boolean).join(" ").trim();
1505
- if (txt) firstUser = txt;
1506
- }
1507
- }
1508
- return found || firstUser || null;
1509
- }
1510
1506
  function apply(ctx) {
1511
1507
  const w = ctx.workspaceRegistry;
1512
1508
  const sp = ctx.sessionPersistence;
@@ -1530,9 +1526,9 @@ function apply(ctx) {
1530
1526
  const stat5 = statsById.get(id);
1531
1527
  const entry = store && store[id];
1532
1528
  if (!stat5 || !entry) continue;
1533
- const fp = fingerprintOf(stat5);
1534
- if (!isPersistableFingerprint(fp)) continue;
1535
- if (fp && entry.fingerprint === fp) {
1529
+ const fp = persistFingerprintOf(stat5);
1530
+ if (!fp) continue;
1531
+ if (entry.fingerprint === fp && (entry.title || entry.resolved)) {
1536
1532
  hits.set(id, { title: entry.title, cwd: entry.cwd, createdAt: entry.createdAt });
1537
1533
  }
1538
1534
  }
@@ -1542,10 +1538,12 @@ function apply(ctx) {
1542
1538
  if (!decoded || !decoded.size) return;
1543
1539
  const batch = {};
1544
1540
  const now = Date.now();
1545
- for (const [id, meta] of decoded) {
1546
- const fp = fingerprintOf(statsById.get(id));
1547
- if (!isPersistableFingerprint(fp)) continue;
1548
- batch[id] = { title: meta.title, cwd: meta.cwd, createdAt: meta.createdAt, fingerprint: fp, updatedAt: now };
1541
+ for (const [id, pack] of decoded) {
1542
+ if (!pack || !pack.resolved) continue;
1543
+ const meta = pack.meta;
1544
+ const fp = persistFingerprintOf(statsById.get(id));
1545
+ if (!fp) continue;
1546
+ batch[id] = { title: meta.title, cwd: meta.cwd, createdAt: meta.createdAt, fingerprint: fp, updatedAt: now, resolved: 1 };
1549
1547
  }
1550
1548
  if (!Object.keys(batch).length) return;
1551
1549
  try {
@@ -1623,50 +1621,153 @@ function apply(ctx) {
1623
1621
  }
1624
1622
  return base;
1625
1623
  }
1626
- async function resolveOne(id, usage, opts = {}) {
1627
- const key = String(id);
1628
- const statInfo = usage ? (usage.statsById ? usage.statsById.get(key) : null) || { mtimeMs: usage.mtimeById && usage.mtimeById.get(key), size: usage.sizeById && usage.sizeById.get(key) } : null;
1629
- const cached = metaCache.get(key, statInfo);
1630
- if (cached) return buildItem(key, cached, usage, opts.exposeUsage);
1631
- let meta = { title: null, cwd: null, createdAt: null };
1632
- if (opts.listHeader) {
1633
- if (typeof opts.listHeader.cwd === "string") meta.cwd = opts.listHeader.cwd;
1634
- if (opts.listHeader.createdAt != null) meta.createdAt = opts.listHeader.createdAt;
1635
- }
1636
- if (opts.preloaded !== void 0) {
1637
- const projected = metaFromSnapshot(unwrapSnapshot(opts.preloaded));
1638
- if (projected.title) meta.title = projected.title;
1639
- if (!meta.cwd && projected.cwd) meta.cwd = projected.cwd;
1640
- if (!meta.createdAt && projected.createdAt) meta.createdAt = projected.createdAt;
1641
- } else if (typeof sq.readTitleSnapshot === "function") {
1642
- try {
1643
- const projected = metaFromSnapshot(await sq.readTitleSnapshot(id));
1644
- if (projected.title) meta.title = projected.title;
1645
- if (!meta.cwd && projected.cwd) meta.cwd = projected.cwd;
1646
- if (!meta.createdAt && projected.createdAt) meta.createdAt = projected.createdAt;
1647
- } catch (e) {
1648
- }
1624
+ const EMPTY_META = { title: null, cwd: null, createdAt: null };
1625
+ const WARM_CHUNK = 4;
1626
+ const warmQueue = /* @__PURE__ */ new Map();
1627
+ let warmRunning = false;
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;
1649
1640
  }
1650
- const projectionAvailable = typeof sq.readTitleSnapshot === "function" || typeof sq.readTitleSnapshots === "function";
1651
- if (!meta.cwd || !meta.title && !projectionAvailable) {
1652
- try {
1653
- let foldedTitle = null;
1654
- const summary = await persistence.inspectSession(key, {
1655
- onEvents: (events) => {
1656
- if (!foldedTitle) foldedTitle = foldTitle(events);
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
+ }
1683
+ function scheduleWarm() {
1684
+ if (warmKickTimer || warmRunning) return;
1685
+ warmKickTimer = setTimeout(() => {
1686
+ warmKickTimer = null;
1687
+ runWarm();
1688
+ }, 25);
1689
+ if (typeof warmKickTimer.unref === "function") warmKickTimer.unref();
1690
+ }
1691
+ function enqueueWarm(ids, statsById, entryById) {
1692
+ if (!ids || !ids.length) return;
1693
+ if (!warmHasApi) return;
1694
+ for (const id of ids) {
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);
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;
1707
+ warmQueue.set(key, {
1708
+ stat: stat5,
1709
+ header: entry && entry.header || null
1710
+ });
1711
+ }
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
+ }
1731
+ }
1732
+ async function runWarm() {
1733
+ if (warmRunning) return;
1734
+ warmRunning = true;
1735
+ try {
1736
+ while (warmQueue.size) {
1737
+ const batch = [...warmQueue.entries()].slice(0, WARM_CHUNK);
1738
+ for (const [id] of batch) warmQueue.delete(id);
1739
+ try {
1740
+ const ids = batch.map(([id]) => id);
1741
+ const resultById = await projectTitlesStatus(ids);
1742
+ const decoded = /* @__PURE__ */ new Map();
1743
+ const statsById = /* @__PURE__ */ new Map();
1744
+ for (const [id, desc] of batch) {
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);
1752
+ if (desc.header) {
1753
+ if (typeof desc.header.cwd === "string") meta.cwd = desc.header.cwd;
1754
+ if (desc.header.createdAt != null) meta.createdAt = desc.header.createdAt;
1755
+ }
1756
+ metaCache.set(id, desc.stat, meta);
1757
+ statsById.set(id, desc.stat);
1758
+ decoded.set(id, { meta, resolved: true });
1657
1759
  }
1658
- });
1659
- if (summary && summary.meta) {
1660
- if (!meta.cwd) meta.cwd = summary.meta.cwd || null;
1661
- if (!meta.createdAt) meta.createdAt = summary.meta.createdAt || null;
1760
+ await persistDecoded(decoded, statsById);
1761
+ await backfillTrashTitles(decoded);
1762
+ } catch (e) {
1662
1763
  }
1663
- if (!meta.title && foldedTitle) meta.title = foldedTitle;
1664
- } catch (e2) {
1764
+ await new Promise((resolve) => setImmediate(resolve));
1665
1765
  }
1766
+ } finally {
1767
+ warmRunning = false;
1768
+ if (warmQueue.size) scheduleWarm();
1769
+ scheduleWarmRetry();
1666
1770
  }
1667
- metaCache.set(key, statInfo, meta);
1668
- if (opts.collectDecoded && statInfo) opts.collectDecoded(key, meta);
1669
- return buildItem(key, meta, usage, opts.exposeUsage);
1670
1771
  }
1671
1772
  async function collectUsage(preloadedEntries) {
1672
1773
  const sizeById = /* @__PURE__ */ new Map();
@@ -1690,7 +1791,7 @@ function apply(ctx) {
1690
1791
  if (!id) return;
1691
1792
  if (entry && typeof entry.revision === "string" && entry.revision) {
1692
1793
  if (Number.isFinite(entry.sizeBytes)) sizeById.set(id, Number(entry.sizeBytes));
1693
- statsById.set(id, { revision: entry.revision });
1794
+ statsById.set(id, Number.isFinite(entry.sizeBytes) ? { revision: entry.revision, sizeBytes: Number(entry.sizeBytes) } : { revision: entry.revision });
1694
1795
  return;
1695
1796
  }
1696
1797
  if (entry && Number.isFinite(entry.sizeBytes)) sizeById.set(id, Number(entry.sizeBytes));
@@ -1785,30 +1886,19 @@ function apply(ctx) {
1785
1886
  cwd = header.cwd || null;
1786
1887
  title = header.title || header.meta && header.meta.title || null;
1787
1888
  }
1788
- if (!title) {
1789
- if (typeof sq.readTitleSnapshot === "function") {
1790
- try {
1791
- const snap = unwrapSnapshot(await sq.readTitleSnapshot(sid));
1792
- if (snap && snap.title && snap.title.title) title = String(snap.title.title);
1793
- if (snap && snap.session) {
1794
- if (!cwd) cwd = snap.session.cwd || null;
1795
- }
1796
- } catch (e) {
1797
- }
1798
- }
1799
- }
1800
1889
  if (!title) {
1801
1890
  try {
1802
- let folded = null;
1803
- const summary = await persistence.inspectSession(sid, {
1804
- onEvents: (events) => {
1805
- if (!folded) folded = foldTitle(events);
1806
- }
1807
- });
1808
- if (summary && summary.meta && !cwd) cwd = summary.meta.cwd || null;
1809
- 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
+ }
1810
1896
  } catch (e) {
1811
1897
  }
1898
+ if (!title) {
1899
+ const at = authorityTitleCache.get(sid);
1900
+ if (at) title = String(at);
1901
+ }
1812
1902
  }
1813
1903
  } catch (e) {
1814
1904
  }
@@ -1820,8 +1910,10 @@ function apply(ctx) {
1820
1910
  const archived = await mutateArchived((list) => ({ next: null, value: list.includes(sid) })).catch(() => false);
1821
1911
  await mutateTrash((store) => {
1822
1912
  const entry = {
1913
+ // v3.6.2 #7:不再把 cwd/裸 id 冒充标题永久定格——存 null,预热解出后
1914
+ // backfillTrashTitles 补写;渲染端(t.title || t.sessionId)自然降级。
1823
1915
  sessionId: sid,
1824
- title: title || cwd || sid,
1916
+ title: title || null,
1825
1917
  cwd: cwd || null,
1826
1918
  header: header || null,
1827
1919
  originalPath: removedPath || null,
@@ -2436,19 +2528,50 @@ function apply(ctx) {
2436
2528
  return { next: null, value: { ok: true, archived: true } };
2437
2529
  });
2438
2530
  }
2439
- async function projectTitles(ids) {
2531
+ const warmHasApi = typeof sq.readTitleSnapshots === "function" || typeof sq.readTitleSnapshot === "function";
2532
+ async function projectTitlesStatus(ids) {
2440
2533
  const out = /* @__PURE__ */ new Map();
2534
+ const markAll = (resolved) => {
2535
+ for (const id of ids) out.set(String(id), { resolved, snapshot: null });
2536
+ };
2441
2537
  if (!ids || !ids.length) return out;
2442
- if (typeof sq.readTitleSnapshots !== "function") return out;
2443
- try {
2444
- const results = await sq.readTitleSnapshots(ids);
2445
- if (!Array.isArray(results)) return out;
2446
- results.forEach((result, index) => {
2447
- const id = String(ids[index]);
2448
- out.set(id, unwrapSnapshot(result));
2449
- });
2450
- } 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;
2451
2573
  }
2574
+ markAll(false);
2452
2575
  return out;
2453
2576
  }
2454
2577
  async function allSessionItemsDetailed(opts = {}) {
@@ -2491,35 +2614,48 @@ function apply(ctx) {
2491
2614
  wsByPath = {};
2492
2615
  }
2493
2616
  const currentArchived = new Set((await archivedState().catch(() => ({ archivedSessionIds: [] }))).archivedSessionIds || []);
2494
- const items = [];
2495
2617
  const usage = await collectUsage(entries);
2496
2618
  const statsById = new Map(visibleIds.map((id) => [
2497
2619
  id,
2498
2620
  usage.statsById && usage.statsById.get(id) || { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }
2499
2621
  ]));
2500
- const { missing } = metaCache.partition(visibleIds, statsById);
2622
+ const { cached, missing } = metaCache.partition(visibleIds, statsById);
2501
2623
  const persisted = await hydrateFromPersist(missing, statsById);
2502
- for (const [id, meta] of persisted) metaCache.set(id, statsById.get(id), meta);
2624
+ for (const [id, meta] of persisted) {
2625
+ const entry = entryById.get(id);
2626
+ const header = entry && entry.header ? entry.header : null;
2627
+ metaCache.set(id, statsById.get(id), {
2628
+ title: meta.title,
2629
+ cwd: header && typeof header.cwd === "string" && header.cwd ? header.cwd : meta.cwd,
2630
+ createdAt: header && header.createdAt != null ? header.createdAt : meta.createdAt
2631
+ });
2632
+ }
2503
2633
  const stillMissing = missing.filter((id) => !persisted.has(id));
2504
- const snapshotById = await projectTitles(stillMissing);
2505
- const decoded = /* @__PURE__ */ new Map();
2506
- const collectDecoded = (id, meta) => {
2507
- decoded.set(id, meta);
2508
- };
2509
- const CHUNK = 6;
2510
- for (let i = 0; i < visibleIds.length; i += CHUNK) {
2511
- const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map((id) => {
2634
+ if (stillMissing.length) enqueueWarm(stillMissing, statsById, entryById);
2635
+ const items = [];
2636
+ for (const id of visibleIds) {
2637
+ let meta = metaCache.get(id, statsById.get(id)) || persisted.get(id) || null;
2638
+ if (!meta) {
2639
+ meta = { title: null, cwd: null, createdAt: null };
2512
2640
  const entry = entryById.get(id);
2513
- return resolveOne(id, usage, {
2514
- exposeUsage: !!(opts && opts.usage),
2515
- listHeader: entry ? entry.header : null,
2516
- preloaded: snapshotById.has(id) ? snapshotById.get(id) : void 0,
2517
- collectDecoded
2518
- });
2519
- }));
2520
- for (const it of res2) items.push({ ...it, archived: currentArchived.has(it.sessionId) });
2641
+ const header = entry && entry.header ? entry.header : null;
2642
+ if (header) {
2643
+ if (typeof header.cwd === "string") meta.cwd = header.cwd;
2644
+ if (header.createdAt != null) meta.createdAt = header.createdAt;
2645
+ }
2646
+ }
2647
+ const it = buildItem(id, meta, usage, !!(opts && opts.usage));
2648
+ items.push({ ...it, archived: currentArchived.has(it.sessionId) });
2649
+ }
2650
+ if (opts && opts.onlyArchived) {
2651
+ const archivedItems = [];
2652
+ for (const it of items) {
2653
+ if (!it.archived) continue;
2654
+ const { archived: _drop, ...rest } = it;
2655
+ archivedItems.push(rest);
2656
+ }
2657
+ return { items: archivedItems, usage };
2521
2658
  }
2522
- await persistDecoded(decoded, statsById);
2523
2659
  let starredSet = /* @__PURE__ */ new Set();
2524
2660
  try {
2525
2661
  starredSet = new Set((await stars.read()).starredSessionIds);
@@ -2597,36 +2733,28 @@ function apply(ctx) {
2597
2733
  const activeTombstones = store.purgedSessionIds.map(String).filter((id) => !present.has(id));
2598
2734
  if (ids.length) {
2599
2735
  const usage = await collectUsage(entries);
2736
+ const entryById = new Map(entries.map((entry) => [entry.id, entry]));
2600
2737
  const statsById = new Map(ids.map((id) => [
2601
2738
  id,
2602
2739
  usage.statsById && usage.statsById.get(id) || { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }
2603
2740
  ]));
2604
2741
  const { cached, missing } = metaCache.partition(ids, statsById);
2605
2742
  const persisted = await hydrateFromPersist(missing, statsById);
2606
- for (const [id, meta] of persisted) metaCache.set(id, statsById.get(id), meta);
2743
+ for (const [id, meta] of persisted) {
2744
+ const entry = entryById.get(id);
2745
+ const header = entry && entry.header ? entry.header : null;
2746
+ metaCache.set(id, statsById.get(id), {
2747
+ title: meta.title,
2748
+ cwd: header && typeof header.cwd === "string" && header.cwd ? header.cwd : meta.cwd,
2749
+ createdAt: header && header.createdAt != null ? header.createdAt : meta.createdAt
2750
+ });
2751
+ }
2607
2752
  const rest = missing.filter((id) => !persisted.has(id));
2608
- const snapshotById = await projectTitles(rest);
2609
- const decoded = /* @__PURE__ */ new Map();
2610
- const collectDecoded = (id, meta) => {
2611
- decoded.set(id, meta);
2612
- };
2753
+ if (rest.length) enqueueWarm(rest, statsById, entryById);
2613
2754
  for (const id of ids) {
2614
- let meta = cached.get(id) || persisted.get(id) || null;
2615
- if (!meta) {
2616
- const entry = entries.find((e) => e.id === id);
2617
- const snapshot = snapshotById.has(id) ? snapshotById.get(id) : typeof sq.readTitleSnapshot === "function" ? await sq.readTitleSnapshot(id).catch(() => null) : null;
2618
- const next = metaFromSnapshot(snapshot);
2619
- if (entry && entry.header) {
2620
- if (!next.cwd && typeof entry.header.cwd === "string") next.cwd = entry.header.cwd;
2621
- if (!next.createdAt && entry.header.createdAt != null) next.createdAt = entry.header.createdAt;
2622
- }
2623
- metaCache.set(id, statsById.get(id), next);
2624
- if (statsById.get(id)) collectDecoded(id, next);
2625
- meta = next;
2626
- }
2755
+ const meta = metaCache.get(id, statsById.get(id)) || persisted.get(id) || null;
2627
2756
  if (meta && meta.title) authorityTitleCache.set(id, String(meta.title));
2628
2757
  }
2629
- await persistDecoded(decoded, statsById);
2630
2758
  }
2631
2759
  const lineage = {};
2632
2760
  for (const entry of entries) {
@@ -2638,40 +2766,53 @@ function apply(ctx) {
2638
2766
  titles: Object.fromEntries(authorityTitleCache),
2639
2767
  trashedSessionIds: store.items.map((item) => String(item.sessionId)),
2640
2768
  purgedSessionIds: activeTombstones,
2641
- lineage
2769
+ lineage,
2770
+ // v3.6.2:还有预热/退避重试在途 → client 缩短轮询节拍,预热一完成就把
2771
+ // 补齐的标题送回(含面板自动刷新)。无预热 API 时恒 false,绝不假忙。
2772
+ warmPending: warmPendingNow()
2642
2773
  };
2643
2774
  }
2644
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
+ }
2645
2787
  async function refineEmptyLineage(lineage, entries) {
2646
2788
  if (!persistence || typeof persistence.inspectSession !== "function") return;
2789
+ const limiter = createLimiter(4);
2790
+ const todo = [];
2647
2791
  for (const entry of entries) {
2648
2792
  const size = entry && Number.isFinite(entry.sizeBytes) ? entry.sizeBytes : null;
2649
2793
  if (size === null || size > EMPTY_DECODE_LIMIT) continue;
2650
2794
  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];
2795
+ const scanned = emptyScanCache.get(id);
2796
+ if (scanned && scanned.sizeBytes === size) {
2797
+ applyEmptyScan(lineage, id, scanned.empty);
2798
+ continue;
2673
2799
  }
2800
+ if (todo.length < REFINE_BUDGET) todo.push({ id, size });
2674
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
+ })));
2675
2816
  const live = /* @__PURE__ */ new Set();
2676
2817
  for (const entry of entries) {
2677
2818
  const size = entry && Number.isFinite(entry.sizeBytes) ? entry.sizeBytes : null;
@@ -2866,12 +3007,10 @@ function apply(ctx) {
2866
3007
  } catch (e) {
2867
3008
  wsByPath = {};
2868
3009
  }
2869
- const items = [];
2870
- const CHUNK = 6;
2871
- for (let i = 0; i < idStrs.length; i += CHUNK) {
2872
- const res2 = await Promise.all(idStrs.slice(i, i + CHUNK).map((id) => resolveOne(id)));
2873
- items.push.apply(items, res2);
2874
- }
3010
+ const wanted = new Set(idStrs);
3011
+ const detailed = await allSessionItemsDetailed();
3012
+ const items = detailed.items.filter((it) => wanted.has(String(it.sessionId)) && it.archived);
3013
+ for (const it of items) delete it.archived;
2875
3014
  json(res, { items });
2876
3015
  } catch (e) {
2877
3016
  json(res, { error: String(e && e.message || e) }, 500);
@@ -3072,7 +3211,7 @@ function apply(ctx) {
3072
3211
  path: "/archived-sessions/sessions",
3073
3212
  handler: async (req, res) => {
3074
3213
  try {
3075
- json(res, { items: await allSessionItems() });
3214
+ json(res, { items: await allSessionItems(), warmPending: warmPendingNow() });
3076
3215
  } catch (e) {
3077
3216
  json(res, { error: String(e && e.message || e) }, 500);
3078
3217
  }
@@ -3176,9 +3315,18 @@ function apply(ctx) {
3176
3315
  });
3177
3316
  if (Number.isFinite(sizeBytes)) sizeById.set(id, sizeBytes);
3178
3317
  };
3318
+ const lineageStats = /* @__PURE__ */ new Map();
3319
+ const lineageHeaders = /* @__PURE__ */ new Map();
3179
3320
  try {
3180
3321
  for (const entry of await persistence.listEntries()) {
3181
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
+ }
3182
3330
  if (entry && entry.id != null) persistedIds.add(String(entry.id));
3183
3331
  }
3184
3332
  } catch (e) {
@@ -3230,11 +3378,17 @@ function apply(ctx) {
3230
3378
  n.children.forEach(walk);
3231
3379
  };
3232
3380
  nodes.forEach(walk);
3233
- 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;
3234
3388
  const titleOf = (id) => {
3235
- const snap = titles.get(id);
3236
- const m = snap ? metaFromSnapshot(snap) : null;
3237
- 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);
3238
3392
  };
3239
3393
  const fill = (n) => {
3240
3394
  n.title = titleOf(n.sessionId);