@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,4 +1,4 @@
1
- export { e as CodingSessionsAgentsOptions, c as CodingSessionsEngine, d as CodingSessionsEngineOptions, D as DEFAULT_SERVER_URL, f as DEFAULT_SUPABASE_URL, E as EFFORT_LEVELS, P as PreferenceStore, g as ProviderHomes, R as RAW_TRANSCRIPT_MAX_BYTES, h as RawBackupPolicy, i as RawBackupSink, j as RunStatus, k as buildCleanEnv, l as claudeDefaults, m as codexDefaults, n as createAiMatrxRawBackupSink, o as createCodingSessionsEngine, p as createRawBackupScheduler, q as defaultProviderHomes, r as describeActivity, s as describeProviderStatus, t as discoverLocalSessions, u as endsWithOpenToolCall, v as findDrivingProcess, w as findSessionFile, x as isAllowedEnvName, y as listProcesses, z as readSessionFile, A as searchLocalChats, B as sessionActivity, F as snippetAround } from '../index-CDeDKQHe.cjs';
1
+ export { f as CodexHome, e as CodingSessionsAgentsOptions, c as CodingSessionsEngine, d as CodingSessionsEngineOptions, D as DEFAULT_SERVER_URL, g as DEFAULT_SUPABASE_URL, E as EFFORT_LEVELS, P as PreferenceStore, h as ProviderHomes, R as RAW_TRANSCRIPT_MAX_BYTES, i as RawBackupPolicy, j as RawBackupSink, k as RunStatus, T as TitleHint, l as TitleHints, m as buildCleanEnv, n as claudeDefaults, o as codexDefaults, p as codexHome, q as createAiMatrxRawBackupSink, r as createCodingSessionsEngine, s as createRawBackupScheduler, t as createTitleHintsStore, u as defaultProviderHomes, v as describeActivity, w as describeProviderStatus, x as discoverLocalSessions, y as endsWithOpenToolCall, z as findDrivingProcess, A as findSessionFile, B as isAllowedEnvName, F as listProcesses, G as readSessionFile, H as searchLocalChats, I as sessionActivity, J as snippetAround, K as titleHintKey } from '../index-Bbe11HoB.cjs';
2
2
  import '@ai-matrx/agents/portable/mcp';
3
3
  import '@ai-matrx/agents/portable';
4
4
  import '@ai-matrx/data';
@@ -1,4 +1,4 @@
1
- export { e as CodingSessionsAgentsOptions, c as CodingSessionsEngine, d as CodingSessionsEngineOptions, D as DEFAULT_SERVER_URL, f as DEFAULT_SUPABASE_URL, E as EFFORT_LEVELS, P as PreferenceStore, g as ProviderHomes, R as RAW_TRANSCRIPT_MAX_BYTES, h as RawBackupPolicy, i as RawBackupSink, j as RunStatus, k as buildCleanEnv, l as claudeDefaults, m as codexDefaults, n as createAiMatrxRawBackupSink, o as createCodingSessionsEngine, p as createRawBackupScheduler, q as defaultProviderHomes, r as describeActivity, s as describeProviderStatus, t as discoverLocalSessions, u as endsWithOpenToolCall, v as findDrivingProcess, w as findSessionFile, x as isAllowedEnvName, y as listProcesses, z as readSessionFile, A as searchLocalChats, B as sessionActivity, F as snippetAround } from '../index-CDeDKQHe.js';
1
+ export { f as CodexHome, e as CodingSessionsAgentsOptions, c as CodingSessionsEngine, d as CodingSessionsEngineOptions, D as DEFAULT_SERVER_URL, g as DEFAULT_SUPABASE_URL, E as EFFORT_LEVELS, P as PreferenceStore, h as ProviderHomes, R as RAW_TRANSCRIPT_MAX_BYTES, i as RawBackupPolicy, j as RawBackupSink, k as RunStatus, T as TitleHint, l as TitleHints, m as buildCleanEnv, n as claudeDefaults, o as codexDefaults, p as codexHome, q as createAiMatrxRawBackupSink, r as createCodingSessionsEngine, s as createRawBackupScheduler, t as createTitleHintsStore, u as defaultProviderHomes, v as describeActivity, w as describeProviderStatus, x as discoverLocalSessions, y as endsWithOpenToolCall, z as findDrivingProcess, A as findSessionFile, B as isAllowedEnvName, F as listProcesses, G as readSessionFile, H as searchLocalChats, I as sessionActivity, J as snippetAround, K as titleHintKey } from '../index-Bbe11HoB.js';
2
2
  import '@ai-matrx/agents/portable/mcp';
3
3
  import '@ai-matrx/agents/portable';
4
4
  import '@ai-matrx/data';
@@ -1133,6 +1133,9 @@ var PROVIDER_LABEL = {
1133
1133
  claude: "Claude Code",
1134
1134
  codex: "Codex"
1135
1135
  };
1136
+ function chatKey(provider, providerSessionId, sessionId) {
1137
+ return providerSessionId ? `${provider}:${providerSessionId}` : `session:${sessionId ?? ""}`;
1138
+ }
1136
1139
  var TERMINAL_RUN_STATUSES = ["completed", "stopped", "failed"];
1137
1140
  function isTerminalRunStatus(status) {
1138
1141
  return TERMINAL_RUN_STATUSES.includes(status);
@@ -1447,6 +1450,78 @@ import fs4 from "fs/promises";
1447
1450
  import os3 from "os";
1448
1451
  import path5 from "path";
1449
1452
 
1453
+ // src/sessions/titles.ts
1454
+ var TITLE_MAX = 300;
1455
+ var BRIDGE_PLACEHOLDER = /^AI Matrx · [a-z-]+$/;
1456
+ var TITLE_PRECEDENCE = [
1457
+ /** The person's rename inside Matrx (Coding Sessions' preference). */
1458
+ "override",
1459
+ /** What the coding app's own sidebar shows: Claude's record `title` (renames included); Codex's thread name; Cursor's name. */
1460
+ "app",
1461
+ /** The title the app last showed, remembered by the claude-accounts engine after the record went away. */
1462
+ "app-remembered",
1463
+ /** Claude Code transcript entries (the last one of each kind that is not the bridge placeholder). */
1464
+ "custom-title",
1465
+ "ai-title",
1466
+ "agent-name",
1467
+ "summary",
1468
+ /** The person's first typed message (Codex: the thread's title / first user message). */
1469
+ "first-prompt",
1470
+ "last-prompt"
1471
+ ];
1472
+ var STAMPABLE = /* @__PURE__ */ new Set(["custom-title", "ai-title", "agent-name"]);
1473
+ function isBridgePlaceholder(value) {
1474
+ return typeof value === "string" && BRIDGE_PLACEHOLDER.test(value.trim());
1475
+ }
1476
+ function cleanTitle(value) {
1477
+ if (typeof value !== "string") return null;
1478
+ const line = value.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0);
1479
+ if (!line) return null;
1480
+ return line.length > TITLE_MAX ? `${line.slice(0, TITLE_MAX - 1)}\u2026` : line;
1481
+ }
1482
+ function resolveSessionTitle(sources) {
1483
+ for (const source of TITLE_PRECEDENCE) {
1484
+ const raw = sources[source];
1485
+ if (STAMPABLE.has(source) && isBridgePlaceholder(raw)) continue;
1486
+ const title = cleanTitle(raw);
1487
+ if (title) return { title, source };
1488
+ }
1489
+ return null;
1490
+ }
1491
+ function claudeTranscriptTitleSources(head, tail = []) {
1492
+ const out = {};
1493
+ const take = (source, value) => {
1494
+ if (typeof value !== "string" || !value.trim()) return;
1495
+ if (STAMPABLE.has(source) && isBridgePlaceholder(value)) return;
1496
+ out[source] = value;
1497
+ };
1498
+ const normalizer = createClaudeNormalizer();
1499
+ let firstPrompt = null;
1500
+ const scan = (entries, findPrompt) => {
1501
+ for (const entry of entries) {
1502
+ if (typeof entry !== "object" || entry === null) continue;
1503
+ const e = entry;
1504
+ if (e.type === "custom-title") take("custom-title", e.customTitle);
1505
+ else if (e.type === "ai-title") take("ai-title", e.aiTitle);
1506
+ else if (e.type === "agent-name") take("agent-name", e.agentName);
1507
+ else if (e.type === "summary") take("summary", e.summary);
1508
+ else if (e.type === "last-prompt") take("last-prompt", e.lastPrompt);
1509
+ else if (findPrompt && firstPrompt === null) {
1510
+ for (const item of normalizer.push(entry)) {
1511
+ if (item.kind === "user" && item.text.trim()) {
1512
+ firstPrompt = item.text;
1513
+ break;
1514
+ }
1515
+ }
1516
+ }
1517
+ }
1518
+ };
1519
+ scan(head, true);
1520
+ scan(tail, false);
1521
+ if (firstPrompt !== null) out["first-prompt"] = firstPrompt;
1522
+ return out;
1523
+ }
1524
+
1450
1525
  // src/engine/activity.ts
1451
1526
  import { execFile as execFile2 } from "child_process";
1452
1527
  var RECENT_WRITE_MS = 2 * 60 * 1e3;
@@ -1512,11 +1587,18 @@ function describeActivity(input) {
1512
1587
  }
1513
1588
 
1514
1589
  // src/engine/files.ts
1590
+ function codexHome(dir) {
1591
+ return {
1592
+ sessions: path5.join(dir, "sessions"),
1593
+ archived: path5.join(dir, "archived_sessions"),
1594
+ index: path5.join(dir, "session_index.jsonl")
1595
+ };
1596
+ }
1515
1597
  function defaultProviderHomes(home = os3.homedir()) {
1516
1598
  return {
1517
1599
  claudeProjects: path5.join(home, ".claude", "projects"),
1518
- codexSessions: path5.join(home, ".codex", "sessions"),
1519
- codexIndex: path5.join(home, ".codex", "session_index.jsonl")
1600
+ claudeVault: path5.join(home, ".claude", "claude-app-vault"),
1601
+ codex: [codexHome(path5.join(home, ".codex"))]
1520
1602
  };
1521
1603
  }
1522
1604
  var UUIDISH = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
@@ -1550,7 +1632,7 @@ async function claudeCandidates(root) {
1550
1632
  }
1551
1633
  return out;
1552
1634
  }
1553
- async function codexCandidates(root) {
1635
+ async function codexTree(root) {
1554
1636
  const out = [];
1555
1637
  for (const year of await safeReaddir(root)) {
1556
1638
  if (!year.isDirectory()) continue;
@@ -1558,17 +1640,23 @@ async function codexCandidates(root) {
1558
1640
  if (!month.isDirectory()) continue;
1559
1641
  for (const day of await safeReaddir(path5.join(root, year.name, month.name))) {
1560
1642
  if (!day.isDirectory()) continue;
1561
- const dir = path5.join(root, year.name, month.name, day.name);
1562
- const entries = await safeReaddir(dir);
1563
- const stats = await Promise.all(
1564
- entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex", path5.join(dir, e.name)))
1565
- );
1566
- for (const s of stats) if (s) out.push(s);
1643
+ out.push(...await codexFlat(path5.join(root, year.name, month.name, day.name), false));
1567
1644
  }
1568
1645
  }
1569
1646
  }
1570
1647
  return out;
1571
1648
  }
1649
+ async function codexFlat(dir, archived) {
1650
+ const entries = await safeReaddir(dir);
1651
+ const stats = await Promise.all(
1652
+ entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex", path5.join(dir, e.name)))
1653
+ );
1654
+ return stats.filter((s) => s !== null).map((s) => archived ? { ...s, archived: true } : s);
1655
+ }
1656
+ async function codexCandidates(homes) {
1657
+ const lists = await Promise.all(homes.flatMap((h) => [codexTree(h.sessions), codexFlat(h.archived, true)]));
1658
+ return lists.flat();
1659
+ }
1572
1660
  async function readSlice(file, start, length) {
1573
1661
  const handle = await fs4.open(file, "r");
1574
1662
  try {
@@ -1590,56 +1678,38 @@ function completeLines(text, droppedFirst) {
1590
1678
  }
1591
1679
  var HEAD_BYTES = 256 * 1024;
1592
1680
  var TAIL_BYTES = 128 * 1024;
1593
- function oneLine(text, max = 140) {
1594
- const flat = text.replace(/\s+/g, " ").trim();
1595
- return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
1596
- }
1597
1681
  async function describeClaude(candidate) {
1598
1682
  const head = parseJsonl(completeLines(await readSlice(candidate.file, 0, HEAD_BYTES), false));
1599
1683
  const tailStart = Math.max(0, candidate.size - TAIL_BYTES);
1600
1684
  const tail = tailStart > 0 ? parseJsonl(completeLines(await readSlice(candidate.file, tailStart, TAIL_BYTES), true)) : head;
1601
1685
  let cwd = null;
1602
- let firstPrompt = null;
1603
1686
  let entrypoint = null;
1604
- const normalizer = createClaudeNormalizer();
1605
1687
  for (const entry of head) {
1606
1688
  if (typeof entry !== "object" || entry === null) continue;
1607
1689
  const e = entry;
1608
1690
  if (!cwd && typeof e.cwd === "string") cwd = e.cwd;
1609
1691
  if (!entrypoint && typeof e.entrypoint === "string") entrypoint = e.entrypoint;
1610
- if (firstPrompt === null) {
1611
- for (const item of normalizer.push(entry)) {
1612
- if (item.kind === "user") {
1613
- firstPrompt = item.text;
1614
- break;
1615
- }
1616
- }
1617
- }
1618
- if (cwd && firstPrompt !== null) break;
1692
+ if (cwd && entrypoint) break;
1619
1693
  }
1620
- const titles = { custom: null, ai: null, agent: null, summary: null, last: null };
1621
- for (const entry of [...head, ...tail]) {
1622
- if (typeof entry !== "object" || entry === null) continue;
1623
- const e = entry;
1624
- if (e.type === "custom-title" && typeof e.customTitle === "string") titles.custom = e.customTitle;
1625
- if (e.type === "ai-title" && typeof e.aiTitle === "string") titles.ai = e.aiTitle;
1626
- if (e.type === "agent-name" && typeof e.agentName === "string") titles.agent = e.agentName;
1627
- if (e.type === "summary" && typeof e.summary === "string") titles.summary = e.summary;
1628
- if (e.type === "last-prompt" && typeof e.lastPrompt === "string") titles.last = e.lastPrompt;
1629
- }
1630
- const title = titles.custom ?? titles.ai ?? titles.agent ?? titles.summary ?? firstPrompt ?? titles.last;
1631
- if (!title) return null;
1694
+ const resolved = resolveSessionTitle(claudeTranscriptTitleSources(head, tailStart > 0 ? tail : []));
1695
+ if (!resolved) return null;
1632
1696
  return {
1633
1697
  provider: "claude",
1634
1698
  providerSessionId: candidate.id,
1635
1699
  cwd,
1636
- title: oneLine(title),
1700
+ title: resolved.title,
1701
+ titleSource: resolved.source,
1637
1702
  updatedAt: new Date(candidate.mtimeMs).toISOString(),
1638
1703
  source: entrypoint,
1639
1704
  activeElsewhere: null
1640
1705
  };
1641
1706
  }
1642
- async function readCodexIndex(file) {
1707
+ async function readCodexIndex(files) {
1708
+ const names = /* @__PURE__ */ new Map();
1709
+ for (const file of files) for (const [id, name] of await readCodexIndexFile(file)) if (!names.has(id)) names.set(id, name);
1710
+ return names;
1711
+ }
1712
+ async function readCodexIndexFile(file) {
1643
1713
  const names = /* @__PURE__ */ new Map();
1644
1714
  let text;
1645
1715
  try {
@@ -1685,18 +1755,26 @@ async function describeCodex(candidate, names) {
1685
1755
  }
1686
1756
  }
1687
1757
  if (isSubAgent) return null;
1688
- const title = names.get(candidate.id) ?? firstPrompt;
1689
- if (!title) return null;
1758
+ const resolved = resolveSessionTitle({ app: names.get(candidate.id), "first-prompt": firstPrompt });
1759
+ if (!resolved) return null;
1690
1760
  return {
1691
1761
  provider: "codex",
1692
1762
  providerSessionId: candidate.id,
1693
1763
  cwd,
1694
- title: oneLine(title),
1764
+ title: resolved.title,
1765
+ titleSource: resolved.source,
1766
+ ...candidate.archived ? { archived: true } : {},
1695
1767
  updatedAt: new Date(candidate.mtimeMs).toISOString(),
1696
1768
  source,
1697
1769
  activeElsewhere: null
1698
1770
  };
1699
1771
  }
1772
+ async function codexNames(homes, appTitles) {
1773
+ if (!appTitles) return readCodexIndex(homes.codex.map((h) => h.index));
1774
+ const out = /* @__PURE__ */ new Map();
1775
+ for (const [key, title] of appTitles) if (key.startsWith("codex:")) out.set(key.slice(6), title);
1776
+ return out;
1777
+ }
1700
1778
  async function readTail(file, size) {
1701
1779
  const start = Math.max(0, size - TAIL_BYTES);
1702
1780
  return parseJsonl(completeLines(await readSlice(file, start, Math.min(size, TAIL_BYTES)), start > 0));
@@ -1736,8 +1814,8 @@ async function discoverLocalSessions(options = {}) {
1736
1814
  const homes = options.homes ?? defaultProviderHomes();
1737
1815
  const [claude, codex, names] = await Promise.all([
1738
1816
  claudeCandidates(homes.claudeProjects),
1739
- codexCandidates(homes.codexSessions),
1740
- readCodexIndex(homes.codexIndex)
1817
+ codexCandidates(homes.codex),
1818
+ codexNames(homes, options.appTitles)
1741
1819
  ]);
1742
1820
  const candidates = [...claude, ...codex].sort((a, b) => b.mtimeMs - a.mtimeMs);
1743
1821
  const processes = await listProcesses();
@@ -1757,9 +1835,14 @@ async function discoverLocalSessions(options = {}) {
1757
1835
  }
1758
1836
  async function findSessionFile(provider, providerSessionId, homes = defaultProviderHomes()) {
1759
1837
  if (!/^[0-9a-f-]{36}$/i.test(providerSessionId)) return null;
1760
- const candidates = provider === "claude" ? await claudeCandidates(homes.claudeProjects) : await codexCandidates(homes.codexSessions);
1838
+ const candidates = provider === "claude" ? await claudeCandidates(homes.claudeProjects) : await codexCandidates(homes.codex);
1761
1839
  const hits = candidates.filter((c) => c.id === providerSessionId).sort((a, b) => b.mtimeMs - a.mtimeMs);
1762
- return hits[0]?.file ?? null;
1840
+ if (hits[0]) return hits[0].file;
1841
+ if (provider === "claude" && homes.claudeVault) {
1842
+ const copy = path5.join(homes.claudeVault, providerSessionId, "transcript.jsonl");
1843
+ if ((await fs4.stat(copy).catch(() => null))?.isFile()) return copy;
1844
+ }
1845
+ return null;
1763
1846
  }
1764
1847
  var HISTORY_BUDGET = 24 * 1024 * 1024;
1765
1848
  var MAX_HISTORY_BUDGET = 256 * 1024 * 1024;
@@ -1809,12 +1892,12 @@ async function readSessionFile(provider, file, endOffset, maxBytes = HISTORY_BUD
1809
1892
  async function listSessionFiles(homes = defaultProviderHomes()) {
1810
1893
  const [claude, codex] = await Promise.all([
1811
1894
  claudeCandidates(homes.claudeProjects),
1812
- codexCandidates(homes.codexSessions)
1895
+ codexCandidates(homes.codex)
1813
1896
  ]);
1814
1897
  return [...claude, ...codex].sort((a, b) => b.mtimeMs - a.mtimeMs);
1815
1898
  }
1816
- async function describeSessionFile(candidate, codexNames) {
1817
- return (candidate.provider === "claude" ? describeClaude(candidate) : describeCodex(candidate, codexNames)).catch(
1899
+ async function describeSessionFile(candidate, codexNames2) {
1900
+ return (candidate.provider === "claude" ? describeClaude(candidate) : describeCodex(candidate, codexNames2)).catch(
1818
1901
  () => null
1819
1902
  );
1820
1903
  }
@@ -1852,7 +1935,7 @@ var PreferenceStore = class {
1852
1935
  if (!key || key.length > 200) throw new Error("That chat can't be found.");
1853
1936
  const next = { ...this.data[key] };
1854
1937
  if ("title" in patch) {
1855
- const title = typeof patch.title === "string" ? patch.title.replace(/\s+/g, " ").trim().slice(0, 200) : "";
1938
+ const title = typeof patch.title === "string" ? patch.title.replace(/\s+/g, " ").trim().slice(0, TITLE_MAX) : "";
1856
1939
  if (title) next.title = title;
1857
1940
  else delete next.title;
1858
1941
  }
@@ -1870,6 +1953,18 @@ var PreferenceStore = class {
1870
1953
  this.persist();
1871
1954
  return this.all();
1872
1955
  }
1956
+ /**
1957
+ * Move a preference to the chat's lasting key (`session:<id>` → `<provider>:<id>` once the
1958
+ * provider's id is known), so a rename made early is the one rename everywhere. A preference
1959
+ * already under the lasting key wins field by field.
1960
+ */
1961
+ rekey(from, to) {
1962
+ if (from === to || !this.data[from]) return false;
1963
+ this.data[to] = { ...this.data[from], ...this.data[to] };
1964
+ delete this.data[from];
1965
+ this.persist();
1966
+ return true;
1967
+ }
1873
1968
  persist() {
1874
1969
  const snapshot = JSON.stringify(this.data, null, 1);
1875
1970
  this.writing = this.writing.then(async () => {
@@ -1997,12 +2092,12 @@ async function searchLocalChats(query, options = {}) {
1997
2092
  const raw = JSON.stringify(needle).slice(1, -1);
1998
2093
  const limit = Math.max(1, Math.min(options.limit ?? 30, 100));
1999
2094
  const deadline = Date.now() + (options.budgetMs ?? 3500);
2000
- const names = await readCodexIndex(homes.codexIndex);
2095
+ const names = await codexNames(homes, options.appTitles);
2001
2096
  const hits = [];
2002
2097
  let i = cursor;
2003
2098
  for (; i < files.length && hits.length < limit && Date.now() < deadline; i += 1) {
2004
2099
  const candidate = files[i];
2005
- const named = candidate.provider === "codex" ? names.get(candidate.id) : void 0;
2100
+ const named = options.appTitles?.get(`${candidate.provider}:${candidate.id}`) ?? (candidate.provider === "codex" ? names.get(candidate.id) : void 0);
2006
2101
  const nameHit = named !== void 0 && named.toLowerCase().includes(needle);
2007
2102
  const scanned = nameHit ? null : await scanFile(candidate.provider, candidate.file, needle, raw, deadline);
2008
2103
  if (scanned === TIMED_OUT) {
@@ -2032,6 +2127,81 @@ async function searchLocalChats(query, options = {}) {
2032
2127
  };
2033
2128
  }
2034
2129
 
2130
+ // src/sessions/hints.ts
2131
+ function titleHintKey(provider, sessionId) {
2132
+ return `${provider}:${sessionId}`;
2133
+ }
2134
+ function createTitleHintsStore() {
2135
+ let hints = null;
2136
+ let problem = null;
2137
+ const listeners = /* @__PURE__ */ new Set();
2138
+ const tell = () => {
2139
+ for (const l of listeners) l();
2140
+ };
2141
+ return {
2142
+ current: () => hints,
2143
+ problem: () => problem,
2144
+ subscribe(listener) {
2145
+ listeners.add(listener);
2146
+ return () => listeners.delete(listener);
2147
+ },
2148
+ set(next) {
2149
+ hints = next;
2150
+ problem = null;
2151
+ tell();
2152
+ },
2153
+ setProblem(next) {
2154
+ if (next === problem) return;
2155
+ problem = next;
2156
+ tell();
2157
+ }
2158
+ };
2159
+ }
2160
+ function appTitlesOf(hints) {
2161
+ const out = /* @__PURE__ */ new Map();
2162
+ for (const [key, hint] of hints) out.set(key, hint.title);
2163
+ return out;
2164
+ }
2165
+
2166
+ // src/engine/title-hints.ts
2167
+ function overlayHint(chat, hints, expected) {
2168
+ if (!expected) return chat;
2169
+ if (hints === null) return { ...chat, titlePending: true };
2170
+ const hint = hints.get(titleHintKey(chat.provider, chat.providerSessionId));
2171
+ if (!hint) return chat;
2172
+ return {
2173
+ ...chat,
2174
+ title: hint.title,
2175
+ titleSource: hint.source,
2176
+ ...hint.archived || chat.archived ? { archived: true } : {},
2177
+ ...hint.scheduled ? { scheduled: true } : {}
2178
+ };
2179
+ }
2180
+ function unavailableFromHints(hints, covered) {
2181
+ if (!hints) return [];
2182
+ const out = [];
2183
+ for (const [key, hint] of hints) {
2184
+ if (!hint.unavailable || covered.has(key)) continue;
2185
+ const at = key.indexOf(":");
2186
+ const provider = key.slice(0, at);
2187
+ if (provider !== "claude" && provider !== "codex") continue;
2188
+ out.push({
2189
+ provider,
2190
+ providerSessionId: key.slice(at + 1),
2191
+ cwd: null,
2192
+ title: hint.title,
2193
+ titleSource: hint.source,
2194
+ updatedAt: new Date(hint.lastActiveMs ?? 0).toISOString(),
2195
+ source: null,
2196
+ activeElsewhere: null,
2197
+ unavailable: hint.unavailable,
2198
+ ...hint.archived ? { archived: true } : {},
2199
+ ...hint.scheduled ? { scheduled: true } : {}
2200
+ });
2201
+ }
2202
+ return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
2203
+ }
2204
+
2035
2205
  // src/engine/store.ts
2036
2206
  import { randomUUID as randomUUID3 } from "crypto";
2037
2207
  import fs6 from "fs/promises";
@@ -2302,7 +2472,7 @@ function describeAuthStatus(provider, probe) {
2302
2472
  async function createCodingSessionsEngine(options = {}) {
2303
2473
  const dataDir = options.dataDir ?? path9.join(os5.homedir(), ".matrx", "coding-sessions");
2304
2474
  const stateDir = path9.join(dataDir, "paperclip-state");
2305
- const homes = options.homes ?? defaultProviderHomes();
2475
+ let homes = options.homes ?? defaultProviderHomes();
2306
2476
  const index = new RunIndex(dataDir);
2307
2477
  await index.load();
2308
2478
  const preferences = new PreferenceStore(dataDir);
@@ -2382,6 +2552,18 @@ async function createCodingSessionsEngine(options = {}) {
2382
2552
  }
2383
2553
  }
2384
2554
  }
2555
+ const titleHints = options.titleHints;
2556
+ const hintsNow = () => titleHints?.current() ?? null;
2557
+ const appTitles = () => {
2558
+ if (!titleHints) return void 0;
2559
+ const hints = hintsNow();
2560
+ return hints ? appTitlesOf(hints) : /* @__PURE__ */ new Map();
2561
+ };
2562
+ const unsubscribeHints = titleHints?.subscribe(() => emit({ type: "titles" })) ?? null;
2563
+ const withHintTitle = (session) => {
2564
+ const hint = session.providerSessionId ? hintsNow()?.get(titleHintKey(session.provider, session.providerSessionId)) : void 0;
2565
+ return hint ? { ...session, title: hint.title } : session;
2566
+ };
2385
2567
  function saveSession(session) {
2386
2568
  index.putSession(session);
2387
2569
  emit({ type: "session", session });
@@ -2538,7 +2720,12 @@ async function createCodingSessionsEngine(options = {}) {
2538
2720
  if (sid && sid !== knownProviderSession) {
2539
2721
  knownProviderSession = sid;
2540
2722
  const current = index.session(session.id);
2541
- if (current) saveSession({ ...current, providerSessionId: sid });
2723
+ if (current) {
2724
+ saveSession({ ...current, providerSessionId: sid });
2725
+ if (preferences.rekey(chatKey(current.provider, null, current.id), chatKey(current.provider, sid))) {
2726
+ emit({ type: "preferences", preferences: preferences.all() });
2727
+ }
2728
+ }
2542
2729
  }
2543
2730
  for (const item of items) emitItem(liveRun, item);
2544
2731
  }
@@ -2846,7 +3033,9 @@ async function createCodingSessionsEngine(options = {}) {
2846
3033
  );
2847
3034
  },
2848
3035
  async discoverSessions(opts) {
2849
- const found = await discoverLocalSessions({ limit: opts?.limit, homes, excludePids: ourPids() });
3036
+ const read = await discoverLocalSessions({ limit: opts?.limit, homes, excludePids: ourPids(), appTitles: appTitles() });
3037
+ const hints = hintsNow();
3038
+ const found = read.map((chat) => overlayHint(chat, hints, titleHints !== void 0));
2850
3039
  if (backups) {
2851
3040
  for (const chat of found) {
2852
3041
  const key = `${chat.provider}:${chat.providerSessionId}`;
@@ -2857,7 +3046,12 @@ async function createCodingSessionsEngine(options = {}) {
2857
3046
  }
2858
3047
  }
2859
3048
  }
2860
- return found;
3049
+ const limit = opts?.limit ?? 60;
3050
+ const oldest = found.length >= limit ? found[found.length - 1]?.updatedAt ?? "" : "";
3051
+ const gone = unavailableFromHints(hints, new Set(found.map((c) => titleHintKey(c.provider, c.providerSessionId)))).filter(
3052
+ (c) => c.updatedAt >= oldest
3053
+ );
3054
+ return gone.length ? [...found, ...gone].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, limit) : found;
2861
3055
  },
2862
3056
  async recentFolders() {
2863
3057
  const seen = /* @__PURE__ */ new Map();
@@ -2877,8 +3071,19 @@ async function createCodingSessionsEngine(options = {}) {
2877
3071
  }))
2878
3072
  );
2879
3073
  },
3074
+ async titleStatus() {
3075
+ if (!titleHints) return { owner: false, pending: false, problem: null };
3076
+ const problem = titleHints.problem?.() ?? null;
3077
+ return { owner: true, pending: hintsNow() === null && problem === null, problem };
3078
+ },
3079
+ async retryTitles() {
3080
+ titleHints?.retry?.();
3081
+ },
3082
+ setHomes(next) {
3083
+ homes = next;
3084
+ },
2880
3085
  async listSessions() {
2881
- return index.sessions();
3086
+ return index.sessions().map(withHintTitle);
2882
3087
  },
2883
3088
  async listRuns(sessionId) {
2884
3089
  const session = index.session(sessionId);
@@ -3053,7 +3258,9 @@ async function createCodingSessionsEngine(options = {}) {
3053
3258
  return all;
3054
3259
  },
3055
3260
  async searchChats(query, opts) {
3056
- return searchLocalChats(String(query ?? ""), { homes, cursor: opts?.cursor, limit: opts?.limit });
3261
+ const result = await searchLocalChats(String(query ?? ""), { homes, cursor: opts?.cursor, limit: opts?.limit, appTitles: appTitles() });
3262
+ const hints = hintsNow();
3263
+ return { ...result, hits: result.hits.map((hit) => ({ ...hit, title: hints?.get(titleHintKey(hit.provider, hit.providerSessionId))?.title ?? hit.title })) };
3057
3264
  },
3058
3265
  async openFolder(raw, target) {
3059
3266
  const folder = await assertFolder(String(raw ?? ""));
@@ -3083,6 +3290,7 @@ async function createCodingSessionsEngine(options = {}) {
3083
3290
  return agents.handleMcp(secret, body);
3084
3291
  },
3085
3292
  async dispose() {
3293
+ unsubscribeHints?.();
3086
3294
  for (const liveRun of live.values()) {
3087
3295
  liveRun.stopRequested = true;
3088
3296
  if (liveRun.pgid !== null) killGroup(liveRun);
@@ -3138,9 +3346,11 @@ export {
3138
3346
  buildCleanEnv,
3139
3347
  claudeDefaults,
3140
3348
  codexDefaults,
3349
+ codexHome,
3141
3350
  createAiMatrxRawBackupSink,
3142
3351
  createCodingSessionsEngine,
3143
3352
  createRawBackupScheduler,
3353
+ createTitleHintsStore,
3144
3354
  defaultProviderHomes,
3145
3355
  describeActivity,
3146
3356
  describeProviderStatus,
@@ -3153,6 +3363,7 @@ export {
3153
3363
  readSessionFile,
3154
3364
  searchLocalChats,
3155
3365
  sessionActivity,
3156
- snippetAround
3366
+ snippetAround,
3367
+ titleHintKey
3157
3368
  };
3158
3369
  //# sourceMappingURL=index.js.map