@ai-matrx/coding-sessions 0.1.166 → 0.3.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/engine/index.cjs +273 -59
  3. package/dist/engine/index.cjs.map +1 -1
  4. package/dist/engine/index.d.cts +1 -1
  5. package/dist/engine/index.d.ts +1 -1
  6. package/dist/engine/index.js +269 -58
  7. package/dist/engine/index.js.map +1 -1
  8. package/dist/{index-CDeDKQHe.d.cts → index-Bbe11HoB.d.cts} +108 -3
  9. package/dist/{index-CDeDKQHe.d.ts → index-Bbe11HoB.d.ts} +108 -3
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +30 -1
  12. package/dist/index.d.ts +30 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/react/index.cjs +47 -6
  15. package/dist/react/index.cjs.map +1 -1
  16. package/dist/react/index.d.cts +37 -0
  17. package/dist/react/index.d.ts +37 -0
  18. package/dist/react/index.js +47 -6
  19. package/dist/react/index.js.map +1 -1
  20. package/dist/server/index.cjs +241 -57
  21. package/dist/server/index.cjs.map +1 -1
  22. package/dist/server/index.d.cts +2 -2
  23. package/dist/server/index.d.ts +2 -2
  24. package/dist/server/index.js +241 -57
  25. package/dist/server/index.js.map +1 -1
  26. package/dist/sessions/index.cjs +360 -0
  27. package/dist/sessions/index.cjs.map +1 -0
  28. package/dist/sessions/index.d.cts +71 -0
  29. package/dist/sessions/index.d.ts +71 -0
  30. package/dist/sessions/index.js +337 -0
  31. package/dist/sessions/index.js.map +1 -0
  32. package/dist/titles-DjxUb0ji.d.cts +48 -0
  33. package/dist/titles-DjxUb0ji.d.ts +48 -0
  34. package/dist/ui/assets/index-BqzTnRFe.css +2 -0
  35. package/dist/ui/assets/{index-7eK8XQtL.js → index-U98AY8r-.js} +17 -17
  36. package/dist/ui/index.html +2 -2
  37. package/package.json +13 -3
  38. package/dist/ui/assets/index-BqpVx_My.css +0 -2
@@ -1,5 +1,5 @@
1
- import { C as CodingSessionsMethod, a as CodingSessionsApi, b as CodingEvent, c as CodingSessionsEngine, d as CodingSessionsEngineOptions } from '../index-CDeDKQHe.cjs';
2
- export { e as CodingSessionsAgentsOptions } from '../index-CDeDKQHe.cjs';
1
+ import { C as CodingSessionsMethod, a as CodingSessionsApi, b as CodingEvent, c as CodingSessionsEngine, d as CodingSessionsEngineOptions } from '../index-Bbe11HoB.cjs';
2
+ export { e as CodingSessionsAgentsOptions } from '../index-Bbe11HoB.cjs';
3
3
  import '@ai-matrx/agents/portable';
4
4
  import '@ai-matrx/data';
5
5
  import '@ai-matrx/agents/portable/mcp';
@@ -1,5 +1,5 @@
1
- import { C as CodingSessionsMethod, a as CodingSessionsApi, b as CodingEvent, c as CodingSessionsEngine, d as CodingSessionsEngineOptions } from '../index-CDeDKQHe.js';
2
- export { e as CodingSessionsAgentsOptions } from '../index-CDeDKQHe.js';
1
+ import { C as CodingSessionsMethod, a as CodingSessionsApi, b as CodingEvent, c as CodingSessionsEngine, d as CodingSessionsEngineOptions } from '../index-Bbe11HoB.js';
2
+ export { e as CodingSessionsAgentsOptions } from '../index-Bbe11HoB.js';
3
3
  import '@ai-matrx/agents/portable';
4
4
  import '@ai-matrx/data';
5
5
  import '@ai-matrx/agents/portable/mcp';
@@ -1105,6 +1105,9 @@ var PROVIDER_LABEL = {
1105
1105
  claude: "Claude Code",
1106
1106
  codex: "Codex"
1107
1107
  };
1108
+ function chatKey(provider, providerSessionId, sessionId) {
1109
+ return providerSessionId ? `${provider}:${providerSessionId}` : `session:${sessionId ?? ""}`;
1110
+ }
1108
1111
  var TERMINAL_RUN_STATUSES = ["completed", "stopped", "failed"];
1109
1112
  function isTerminalRunStatus(status) {
1110
1113
  return TERMINAL_RUN_STATUSES.includes(status);
@@ -1419,6 +1422,78 @@ import fs4 from "fs/promises";
1419
1422
  import os3 from "os";
1420
1423
  import path5 from "path";
1421
1424
 
1425
+ // src/sessions/titles.ts
1426
+ var TITLE_MAX = 300;
1427
+ var BRIDGE_PLACEHOLDER = /^AI Matrx · [a-z-]+$/;
1428
+ var TITLE_PRECEDENCE = [
1429
+ /** The person's rename inside Matrx (Coding Sessions' preference). */
1430
+ "override",
1431
+ /** What the coding app's own sidebar shows: Claude's record `title` (renames included); Codex's thread name; Cursor's name. */
1432
+ "app",
1433
+ /** The title the app last showed, remembered by the claude-accounts engine after the record went away. */
1434
+ "app-remembered",
1435
+ /** Claude Code transcript entries (the last one of each kind that is not the bridge placeholder). */
1436
+ "custom-title",
1437
+ "ai-title",
1438
+ "agent-name",
1439
+ "summary",
1440
+ /** The person's first typed message (Codex: the thread's title / first user message). */
1441
+ "first-prompt",
1442
+ "last-prompt"
1443
+ ];
1444
+ var STAMPABLE = /* @__PURE__ */ new Set(["custom-title", "ai-title", "agent-name"]);
1445
+ function isBridgePlaceholder(value) {
1446
+ return typeof value === "string" && BRIDGE_PLACEHOLDER.test(value.trim());
1447
+ }
1448
+ function cleanTitle(value) {
1449
+ if (typeof value !== "string") return null;
1450
+ const line = value.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0);
1451
+ if (!line) return null;
1452
+ return line.length > TITLE_MAX ? `${line.slice(0, TITLE_MAX - 1)}\u2026` : line;
1453
+ }
1454
+ function resolveSessionTitle(sources) {
1455
+ for (const source of TITLE_PRECEDENCE) {
1456
+ const raw = sources[source];
1457
+ if (STAMPABLE.has(source) && isBridgePlaceholder(raw)) continue;
1458
+ const title = cleanTitle(raw);
1459
+ if (title) return { title, source };
1460
+ }
1461
+ return null;
1462
+ }
1463
+ function claudeTranscriptTitleSources(head, tail = []) {
1464
+ const out = {};
1465
+ const take = (source, value) => {
1466
+ if (typeof value !== "string" || !value.trim()) return;
1467
+ if (STAMPABLE.has(source) && isBridgePlaceholder(value)) return;
1468
+ out[source] = value;
1469
+ };
1470
+ const normalizer = createClaudeNormalizer();
1471
+ let firstPrompt = null;
1472
+ const scan = (entries, findPrompt) => {
1473
+ for (const entry of entries) {
1474
+ if (typeof entry !== "object" || entry === null) continue;
1475
+ const e = entry;
1476
+ if (e.type === "custom-title") take("custom-title", e.customTitle);
1477
+ else if (e.type === "ai-title") take("ai-title", e.aiTitle);
1478
+ else if (e.type === "agent-name") take("agent-name", e.agentName);
1479
+ else if (e.type === "summary") take("summary", e.summary);
1480
+ else if (e.type === "last-prompt") take("last-prompt", e.lastPrompt);
1481
+ else if (findPrompt && firstPrompt === null) {
1482
+ for (const item of normalizer.push(entry)) {
1483
+ if (item.kind === "user" && item.text.trim()) {
1484
+ firstPrompt = item.text;
1485
+ break;
1486
+ }
1487
+ }
1488
+ }
1489
+ }
1490
+ };
1491
+ scan(head, true);
1492
+ scan(tail, false);
1493
+ if (firstPrompt !== null) out["first-prompt"] = firstPrompt;
1494
+ return out;
1495
+ }
1496
+
1422
1497
  // src/engine/activity.ts
1423
1498
  import { execFile as execFile2 } from "child_process";
1424
1499
  var RECENT_WRITE_MS = 2 * 60 * 1e3;
@@ -1484,11 +1559,18 @@ function describeActivity(input) {
1484
1559
  }
1485
1560
 
1486
1561
  // src/engine/files.ts
1562
+ function codexHome(dir) {
1563
+ return {
1564
+ sessions: path5.join(dir, "sessions"),
1565
+ archived: path5.join(dir, "archived_sessions"),
1566
+ index: path5.join(dir, "session_index.jsonl")
1567
+ };
1568
+ }
1487
1569
  function defaultProviderHomes(home = os3.homedir()) {
1488
1570
  return {
1489
1571
  claudeProjects: path5.join(home, ".claude", "projects"),
1490
- codexSessions: path5.join(home, ".codex", "sessions"),
1491
- codexIndex: path5.join(home, ".codex", "session_index.jsonl")
1572
+ claudeVault: path5.join(home, ".claude", "claude-app-vault"),
1573
+ codex: [codexHome(path5.join(home, ".codex"))]
1492
1574
  };
1493
1575
  }
1494
1576
  var UUIDISH = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
@@ -1522,7 +1604,7 @@ async function claudeCandidates(root) {
1522
1604
  }
1523
1605
  return out;
1524
1606
  }
1525
- async function codexCandidates(root) {
1607
+ async function codexTree(root) {
1526
1608
  const out = [];
1527
1609
  for (const year of await safeReaddir(root)) {
1528
1610
  if (!year.isDirectory()) continue;
@@ -1530,17 +1612,23 @@ async function codexCandidates(root) {
1530
1612
  if (!month.isDirectory()) continue;
1531
1613
  for (const day of await safeReaddir(path5.join(root, year.name, month.name))) {
1532
1614
  if (!day.isDirectory()) continue;
1533
- const dir = path5.join(root, year.name, month.name, day.name);
1534
- const entries = await safeReaddir(dir);
1535
- const stats = await Promise.all(
1536
- entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex", path5.join(dir, e.name)))
1537
- );
1538
- for (const s of stats) if (s) out.push(s);
1615
+ out.push(...await codexFlat(path5.join(root, year.name, month.name, day.name), false));
1539
1616
  }
1540
1617
  }
1541
1618
  }
1542
1619
  return out;
1543
1620
  }
1621
+ async function codexFlat(dir, archived) {
1622
+ const entries = await safeReaddir(dir);
1623
+ const stats = await Promise.all(
1624
+ entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex", path5.join(dir, e.name)))
1625
+ );
1626
+ return stats.filter((s) => s !== null).map((s) => archived ? { ...s, archived: true } : s);
1627
+ }
1628
+ async function codexCandidates(homes) {
1629
+ const lists = await Promise.all(homes.flatMap((h) => [codexTree(h.sessions), codexFlat(h.archived, true)]));
1630
+ return lists.flat();
1631
+ }
1544
1632
  async function readSlice(file, start, length) {
1545
1633
  const handle = await fs4.open(file, "r");
1546
1634
  try {
@@ -1562,56 +1650,38 @@ function completeLines(text, droppedFirst) {
1562
1650
  }
1563
1651
  var HEAD_BYTES = 256 * 1024;
1564
1652
  var TAIL_BYTES = 128 * 1024;
1565
- function oneLine(text, max = 140) {
1566
- const flat = text.replace(/\s+/g, " ").trim();
1567
- return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
1568
- }
1569
1653
  async function describeClaude(candidate) {
1570
1654
  const head = parseJsonl(completeLines(await readSlice(candidate.file, 0, HEAD_BYTES), false));
1571
1655
  const tailStart = Math.max(0, candidate.size - TAIL_BYTES);
1572
1656
  const tail = tailStart > 0 ? parseJsonl(completeLines(await readSlice(candidate.file, tailStart, TAIL_BYTES), true)) : head;
1573
1657
  let cwd = null;
1574
- let firstPrompt = null;
1575
1658
  let entrypoint = null;
1576
- const normalizer = createClaudeNormalizer();
1577
1659
  for (const entry of head) {
1578
1660
  if (typeof entry !== "object" || entry === null) continue;
1579
1661
  const e = entry;
1580
1662
  if (!cwd && typeof e.cwd === "string") cwd = e.cwd;
1581
1663
  if (!entrypoint && typeof e.entrypoint === "string") entrypoint = e.entrypoint;
1582
- if (firstPrompt === null) {
1583
- for (const item of normalizer.push(entry)) {
1584
- if (item.kind === "user") {
1585
- firstPrompt = item.text;
1586
- break;
1587
- }
1588
- }
1589
- }
1590
- if (cwd && firstPrompt !== null) break;
1664
+ if (cwd && entrypoint) break;
1591
1665
  }
1592
- const titles = { custom: null, ai: null, agent: null, summary: null, last: null };
1593
- for (const entry of [...head, ...tail]) {
1594
- if (typeof entry !== "object" || entry === null) continue;
1595
- const e = entry;
1596
- if (e.type === "custom-title" && typeof e.customTitle === "string") titles.custom = e.customTitle;
1597
- if (e.type === "ai-title" && typeof e.aiTitle === "string") titles.ai = e.aiTitle;
1598
- if (e.type === "agent-name" && typeof e.agentName === "string") titles.agent = e.agentName;
1599
- if (e.type === "summary" && typeof e.summary === "string") titles.summary = e.summary;
1600
- if (e.type === "last-prompt" && typeof e.lastPrompt === "string") titles.last = e.lastPrompt;
1601
- }
1602
- const title = titles.custom ?? titles.ai ?? titles.agent ?? titles.summary ?? firstPrompt ?? titles.last;
1603
- if (!title) return null;
1666
+ const resolved = resolveSessionTitle(claudeTranscriptTitleSources(head, tailStart > 0 ? tail : []));
1667
+ if (!resolved) return null;
1604
1668
  return {
1605
1669
  provider: "claude",
1606
1670
  providerSessionId: candidate.id,
1607
1671
  cwd,
1608
- title: oneLine(title),
1672
+ title: resolved.title,
1673
+ titleSource: resolved.source,
1609
1674
  updatedAt: new Date(candidate.mtimeMs).toISOString(),
1610
1675
  source: entrypoint,
1611
1676
  activeElsewhere: null
1612
1677
  };
1613
1678
  }
1614
- async function readCodexIndex(file) {
1679
+ async function readCodexIndex(files) {
1680
+ const names = /* @__PURE__ */ new Map();
1681
+ for (const file of files) for (const [id, name] of await readCodexIndexFile(file)) if (!names.has(id)) names.set(id, name);
1682
+ return names;
1683
+ }
1684
+ async function readCodexIndexFile(file) {
1615
1685
  const names = /* @__PURE__ */ new Map();
1616
1686
  let text;
1617
1687
  try {
@@ -1657,18 +1727,26 @@ async function describeCodex(candidate, names) {
1657
1727
  }
1658
1728
  }
1659
1729
  if (isSubAgent) return null;
1660
- const title = names.get(candidate.id) ?? firstPrompt;
1661
- if (!title) return null;
1730
+ const resolved = resolveSessionTitle({ app: names.get(candidate.id), "first-prompt": firstPrompt });
1731
+ if (!resolved) return null;
1662
1732
  return {
1663
1733
  provider: "codex",
1664
1734
  providerSessionId: candidate.id,
1665
1735
  cwd,
1666
- title: oneLine(title),
1736
+ title: resolved.title,
1737
+ titleSource: resolved.source,
1738
+ ...candidate.archived ? { archived: true } : {},
1667
1739
  updatedAt: new Date(candidate.mtimeMs).toISOString(),
1668
1740
  source,
1669
1741
  activeElsewhere: null
1670
1742
  };
1671
1743
  }
1744
+ async function codexNames(homes, appTitles) {
1745
+ if (!appTitles) return readCodexIndex(homes.codex.map((h) => h.index));
1746
+ const out = /* @__PURE__ */ new Map();
1747
+ for (const [key, title] of appTitles) if (key.startsWith("codex:")) out.set(key.slice(6), title);
1748
+ return out;
1749
+ }
1672
1750
  async function readTail(file, size) {
1673
1751
  const start = Math.max(0, size - TAIL_BYTES);
1674
1752
  return parseJsonl(completeLines(await readSlice(file, start, Math.min(size, TAIL_BYTES)), start > 0));
@@ -1708,8 +1786,8 @@ async function discoverLocalSessions(options = {}) {
1708
1786
  const homes = options.homes ?? defaultProviderHomes();
1709
1787
  const [claude, codex, names] = await Promise.all([
1710
1788
  claudeCandidates(homes.claudeProjects),
1711
- codexCandidates(homes.codexSessions),
1712
- readCodexIndex(homes.codexIndex)
1789
+ codexCandidates(homes.codex),
1790
+ codexNames(homes, options.appTitles)
1713
1791
  ]);
1714
1792
  const candidates = [...claude, ...codex].sort((a, b) => b.mtimeMs - a.mtimeMs);
1715
1793
  const processes = await listProcesses();
@@ -1729,9 +1807,14 @@ async function discoverLocalSessions(options = {}) {
1729
1807
  }
1730
1808
  async function findSessionFile(provider, providerSessionId, homes = defaultProviderHomes()) {
1731
1809
  if (!/^[0-9a-f-]{36}$/i.test(providerSessionId)) return null;
1732
- const candidates = provider === "claude" ? await claudeCandidates(homes.claudeProjects) : await codexCandidates(homes.codexSessions);
1810
+ const candidates = provider === "claude" ? await claudeCandidates(homes.claudeProjects) : await codexCandidates(homes.codex);
1733
1811
  const hits = candidates.filter((c) => c.id === providerSessionId).sort((a, b) => b.mtimeMs - a.mtimeMs);
1734
- return hits[0]?.file ?? null;
1812
+ if (hits[0]) return hits[0].file;
1813
+ if (provider === "claude" && homes.claudeVault) {
1814
+ const copy = path5.join(homes.claudeVault, providerSessionId, "transcript.jsonl");
1815
+ if ((await fs4.stat(copy).catch(() => null))?.isFile()) return copy;
1816
+ }
1817
+ return null;
1735
1818
  }
1736
1819
  var HISTORY_BUDGET = 24 * 1024 * 1024;
1737
1820
  var MAX_HISTORY_BUDGET = 256 * 1024 * 1024;
@@ -1781,12 +1864,12 @@ async function readSessionFile(provider, file, endOffset, maxBytes = HISTORY_BUD
1781
1864
  async function listSessionFiles(homes = defaultProviderHomes()) {
1782
1865
  const [claude, codex] = await Promise.all([
1783
1866
  claudeCandidates(homes.claudeProjects),
1784
- codexCandidates(homes.codexSessions)
1867
+ codexCandidates(homes.codex)
1785
1868
  ]);
1786
1869
  return [...claude, ...codex].sort((a, b) => b.mtimeMs - a.mtimeMs);
1787
1870
  }
1788
- async function describeSessionFile(candidate, codexNames) {
1789
- return (candidate.provider === "claude" ? describeClaude(candidate) : describeCodex(candidate, codexNames)).catch(
1871
+ async function describeSessionFile(candidate, codexNames2) {
1872
+ return (candidate.provider === "claude" ? describeClaude(candidate) : describeCodex(candidate, codexNames2)).catch(
1790
1873
  () => null
1791
1874
  );
1792
1875
  }
@@ -1824,7 +1907,7 @@ var PreferenceStore = class {
1824
1907
  if (!key || key.length > 200) throw new Error("That chat can't be found.");
1825
1908
  const next = { ...this.data[key] };
1826
1909
  if ("title" in patch) {
1827
- const title = typeof patch.title === "string" ? patch.title.replace(/\s+/g, " ").trim().slice(0, 200) : "";
1910
+ const title = typeof patch.title === "string" ? patch.title.replace(/\s+/g, " ").trim().slice(0, TITLE_MAX) : "";
1828
1911
  if (title) next.title = title;
1829
1912
  else delete next.title;
1830
1913
  }
@@ -1842,6 +1925,18 @@ var PreferenceStore = class {
1842
1925
  this.persist();
1843
1926
  return this.all();
1844
1927
  }
1928
+ /**
1929
+ * Move a preference to the chat's lasting key (`session:<id>` → `<provider>:<id>` once the
1930
+ * provider's id is known), so a rename made early is the one rename everywhere. A preference
1931
+ * already under the lasting key wins field by field.
1932
+ */
1933
+ rekey(from, to) {
1934
+ if (from === to || !this.data[from]) return false;
1935
+ this.data[to] = { ...this.data[from], ...this.data[to] };
1936
+ delete this.data[from];
1937
+ this.persist();
1938
+ return true;
1939
+ }
1845
1940
  persist() {
1846
1941
  const snapshot = JSON.stringify(this.data, null, 1);
1847
1942
  this.writing = this.writing.then(async () => {
@@ -1969,12 +2064,12 @@ async function searchLocalChats(query, options = {}) {
1969
2064
  const raw = JSON.stringify(needle).slice(1, -1);
1970
2065
  const limit = Math.max(1, Math.min(options.limit ?? 30, 100));
1971
2066
  const deadline = Date.now() + (options.budgetMs ?? 3500);
1972
- const names = await readCodexIndex(homes.codexIndex);
2067
+ const names = await codexNames(homes, options.appTitles);
1973
2068
  const hits = [];
1974
2069
  let i = cursor;
1975
2070
  for (; i < files.length && hits.length < limit && Date.now() < deadline; i += 1) {
1976
2071
  const candidate = files[i];
1977
- const named = candidate.provider === "codex" ? names.get(candidate.id) : void 0;
2072
+ const named = options.appTitles?.get(`${candidate.provider}:${candidate.id}`) ?? (candidate.provider === "codex" ? names.get(candidate.id) : void 0);
1978
2073
  const nameHit = named !== void 0 && named.toLowerCase().includes(needle);
1979
2074
  const scanned = nameHit ? null : await scanFile(candidate.provider, candidate.file, needle, raw, deadline);
1980
2075
  if (scanned === TIMED_OUT) {
@@ -2004,6 +2099,55 @@ async function searchLocalChats(query, options = {}) {
2004
2099
  };
2005
2100
  }
2006
2101
 
2102
+ // src/sessions/hints.ts
2103
+ function titleHintKey(provider, sessionId) {
2104
+ return `${provider}:${sessionId}`;
2105
+ }
2106
+ function appTitlesOf(hints) {
2107
+ const out = /* @__PURE__ */ new Map();
2108
+ for (const [key, hint] of hints) out.set(key, hint.title);
2109
+ return out;
2110
+ }
2111
+
2112
+ // src/engine/title-hints.ts
2113
+ function overlayHint(chat, hints, expected) {
2114
+ if (!expected) return chat;
2115
+ if (hints === null) return { ...chat, titlePending: true };
2116
+ const hint = hints.get(titleHintKey(chat.provider, chat.providerSessionId));
2117
+ if (!hint) return chat;
2118
+ return {
2119
+ ...chat,
2120
+ title: hint.title,
2121
+ titleSource: hint.source,
2122
+ ...hint.archived || chat.archived ? { archived: true } : {},
2123
+ ...hint.scheduled ? { scheduled: true } : {}
2124
+ };
2125
+ }
2126
+ function unavailableFromHints(hints, covered) {
2127
+ if (!hints) return [];
2128
+ const out = [];
2129
+ for (const [key, hint] of hints) {
2130
+ if (!hint.unavailable || covered.has(key)) continue;
2131
+ const at = key.indexOf(":");
2132
+ const provider = key.slice(0, at);
2133
+ if (provider !== "claude" && provider !== "codex") continue;
2134
+ out.push({
2135
+ provider,
2136
+ providerSessionId: key.slice(at + 1),
2137
+ cwd: null,
2138
+ title: hint.title,
2139
+ titleSource: hint.source,
2140
+ updatedAt: new Date(hint.lastActiveMs ?? 0).toISOString(),
2141
+ source: null,
2142
+ activeElsewhere: null,
2143
+ unavailable: hint.unavailable,
2144
+ ...hint.archived ? { archived: true } : {},
2145
+ ...hint.scheduled ? { scheduled: true } : {}
2146
+ });
2147
+ }
2148
+ return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
2149
+ }
2150
+
2007
2151
  // src/engine/store.ts
2008
2152
  import { randomUUID as randomUUID3 } from "crypto";
2009
2153
  import fs6 from "fs/promises";
@@ -2274,7 +2418,7 @@ function describeAuthStatus(provider, probe) {
2274
2418
  async function createCodingSessionsEngine(options = {}) {
2275
2419
  const dataDir = options.dataDir ?? path9.join(os5.homedir(), ".matrx", "coding-sessions");
2276
2420
  const stateDir = path9.join(dataDir, "paperclip-state");
2277
- const homes = options.homes ?? defaultProviderHomes();
2421
+ let homes = options.homes ?? defaultProviderHomes();
2278
2422
  const index = new RunIndex(dataDir);
2279
2423
  await index.load();
2280
2424
  const preferences = new PreferenceStore(dataDir);
@@ -2354,6 +2498,18 @@ async function createCodingSessionsEngine(options = {}) {
2354
2498
  }
2355
2499
  }
2356
2500
  }
2501
+ const titleHints = options.titleHints;
2502
+ const hintsNow = () => titleHints?.current() ?? null;
2503
+ const appTitles = () => {
2504
+ if (!titleHints) return void 0;
2505
+ const hints = hintsNow();
2506
+ return hints ? appTitlesOf(hints) : /* @__PURE__ */ new Map();
2507
+ };
2508
+ const unsubscribeHints = titleHints?.subscribe(() => emit({ type: "titles" })) ?? null;
2509
+ const withHintTitle = (session) => {
2510
+ const hint = session.providerSessionId ? hintsNow()?.get(titleHintKey(session.provider, session.providerSessionId)) : void 0;
2511
+ return hint ? { ...session, title: hint.title } : session;
2512
+ };
2357
2513
  function saveSession(session) {
2358
2514
  index.putSession(session);
2359
2515
  emit({ type: "session", session });
@@ -2510,7 +2666,12 @@ async function createCodingSessionsEngine(options = {}) {
2510
2666
  if (sid && sid !== knownProviderSession) {
2511
2667
  knownProviderSession = sid;
2512
2668
  const current = index.session(session.id);
2513
- if (current) saveSession({ ...current, providerSessionId: sid });
2669
+ if (current) {
2670
+ saveSession({ ...current, providerSessionId: sid });
2671
+ if (preferences.rekey(chatKey(current.provider, null, current.id), chatKey(current.provider, sid))) {
2672
+ emit({ type: "preferences", preferences: preferences.all() });
2673
+ }
2674
+ }
2514
2675
  }
2515
2676
  for (const item of items) emitItem(liveRun, item);
2516
2677
  }
@@ -2818,7 +2979,9 @@ async function createCodingSessionsEngine(options = {}) {
2818
2979
  );
2819
2980
  },
2820
2981
  async discoverSessions(opts) {
2821
- const found = await discoverLocalSessions({ limit: opts?.limit, homes, excludePids: ourPids() });
2982
+ const read = await discoverLocalSessions({ limit: opts?.limit, homes, excludePids: ourPids(), appTitles: appTitles() });
2983
+ const hints = hintsNow();
2984
+ const found = read.map((chat) => overlayHint(chat, hints, titleHints !== void 0));
2822
2985
  if (backups) {
2823
2986
  for (const chat of found) {
2824
2987
  const key = `${chat.provider}:${chat.providerSessionId}`;
@@ -2829,7 +2992,12 @@ async function createCodingSessionsEngine(options = {}) {
2829
2992
  }
2830
2993
  }
2831
2994
  }
2832
- return found;
2995
+ const limit = opts?.limit ?? 60;
2996
+ const oldest = found.length >= limit ? found[found.length - 1]?.updatedAt ?? "" : "";
2997
+ const gone = unavailableFromHints(hints, new Set(found.map((c) => titleHintKey(c.provider, c.providerSessionId)))).filter(
2998
+ (c) => c.updatedAt >= oldest
2999
+ );
3000
+ return gone.length ? [...found, ...gone].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, limit) : found;
2833
3001
  },
2834
3002
  async recentFolders() {
2835
3003
  const seen = /* @__PURE__ */ new Map();
@@ -2849,8 +3017,19 @@ async function createCodingSessionsEngine(options = {}) {
2849
3017
  }))
2850
3018
  );
2851
3019
  },
3020
+ async titleStatus() {
3021
+ if (!titleHints) return { owner: false, pending: false, problem: null };
3022
+ const problem = titleHints.problem?.() ?? null;
3023
+ return { owner: true, pending: hintsNow() === null && problem === null, problem };
3024
+ },
3025
+ async retryTitles() {
3026
+ titleHints?.retry?.();
3027
+ },
3028
+ setHomes(next) {
3029
+ homes = next;
3030
+ },
2852
3031
  async listSessions() {
2853
- return index.sessions();
3032
+ return index.sessions().map(withHintTitle);
2854
3033
  },
2855
3034
  async listRuns(sessionId) {
2856
3035
  const session = index.session(sessionId);
@@ -3025,7 +3204,9 @@ async function createCodingSessionsEngine(options = {}) {
3025
3204
  return all;
3026
3205
  },
3027
3206
  async searchChats(query, opts) {
3028
- return searchLocalChats(String(query ?? ""), { homes, cursor: opts?.cursor, limit: opts?.limit });
3207
+ const result = await searchLocalChats(String(query ?? ""), { homes, cursor: opts?.cursor, limit: opts?.limit, appTitles: appTitles() });
3208
+ const hints = hintsNow();
3209
+ return { ...result, hits: result.hits.map((hit) => ({ ...hit, title: hints?.get(titleHintKey(hit.provider, hit.providerSessionId))?.title ?? hit.title })) };
3029
3210
  },
3030
3211
  async openFolder(raw, target) {
3031
3212
  const folder = await assertFolder(String(raw ?? ""));
@@ -3055,6 +3236,7 @@ async function createCodingSessionsEngine(options = {}) {
3055
3236
  return agents.handleMcp(secret, body);
3056
3237
  },
3057
3238
  async dispose() {
3239
+ unsubscribeHints?.();
3058
3240
  for (const liveRun of live.values()) {
3059
3241
  liveRun.stopRequested = true;
3060
3242
  if (liveRun.pgid !== null) killGroup(liveRun);
@@ -3106,6 +3288,8 @@ function openFolderIn(folder, target) {
3106
3288
  var BRIDGE_METHODS = [
3107
3289
  "providers",
3108
3290
  "discoverSessions",
3291
+ "titleStatus",
3292
+ "retryTitles",
3109
3293
  "recentFolders",
3110
3294
  "listSessions",
3111
3295
  "listRuns",