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/README.en.md +4 -4
- package/README.md +4 -4
- package/lib/client.js +100 -28
- package/lib/client.js.map +2 -2
- package/lib/index.js +323 -169
- package/lib/index.js.map +2 -2
- package/package.json +1 -1
- package/src/client/index.jsx +104 -28
- package/src/client/logic.js +29 -0
- package/src/index.js +409 -190
- package/src/session-meta-cache.js +18 -0
- package/src/title-persist-index.js +13 -3
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 =
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
1534
|
-
if (!
|
|
1535
|
-
if (fp && entry.
|
|
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,
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
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
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
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
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
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
|
-
|
|
1660
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
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 ||
|
|
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
|
-
|
|
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 (
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
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)
|
|
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
|
-
|
|
2505
|
-
const
|
|
2506
|
-
const
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
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
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2652
|
-
if (
|
|
2653
|
-
|
|
2654
|
-
|
|
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
|
|
2870
|
-
const
|
|
2871
|
-
|
|
2872
|
-
|
|
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
|
|
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
|
|
3236
|
-
const m =
|
|
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);
|