@ai-matrx/coding-sessions 0.1.166 → 0.2.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.
@@ -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-29t7n472.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-29t7n472.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,17 @@ 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
+ codex: [codexHome(path5.join(home, ".codex"))]
1520
1601
  };
1521
1602
  }
1522
1603
  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 +1631,7 @@ async function claudeCandidates(root) {
1550
1631
  }
1551
1632
  return out;
1552
1633
  }
1553
- async function codexCandidates(root) {
1634
+ async function codexTree(root) {
1554
1635
  const out = [];
1555
1636
  for (const year of await safeReaddir(root)) {
1556
1637
  if (!year.isDirectory()) continue;
@@ -1558,17 +1639,23 @@ async function codexCandidates(root) {
1558
1639
  if (!month.isDirectory()) continue;
1559
1640
  for (const day of await safeReaddir(path5.join(root, year.name, month.name))) {
1560
1641
  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);
1642
+ out.push(...await codexFlat(path5.join(root, year.name, month.name, day.name), false));
1567
1643
  }
1568
1644
  }
1569
1645
  }
1570
1646
  return out;
1571
1647
  }
1648
+ async function codexFlat(dir, archived) {
1649
+ const entries = await safeReaddir(dir);
1650
+ const stats = await Promise.all(
1651
+ entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex", path5.join(dir, e.name)))
1652
+ );
1653
+ return stats.filter((s) => s !== null).map((s) => archived ? { ...s, archived: true } : s);
1654
+ }
1655
+ async function codexCandidates(homes) {
1656
+ const lists = await Promise.all(homes.flatMap((h) => [codexTree(h.sessions), codexFlat(h.archived, true)]));
1657
+ return lists.flat();
1658
+ }
1572
1659
  async function readSlice(file, start, length) {
1573
1660
  const handle = await fs4.open(file, "r");
1574
1661
  try {
@@ -1590,56 +1677,38 @@ function completeLines(text, droppedFirst) {
1590
1677
  }
1591
1678
  var HEAD_BYTES = 256 * 1024;
1592
1679
  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
1680
  async function describeClaude(candidate) {
1598
1681
  const head = parseJsonl(completeLines(await readSlice(candidate.file, 0, HEAD_BYTES), false));
1599
1682
  const tailStart = Math.max(0, candidate.size - TAIL_BYTES);
1600
1683
  const tail = tailStart > 0 ? parseJsonl(completeLines(await readSlice(candidate.file, tailStart, TAIL_BYTES), true)) : head;
1601
1684
  let cwd = null;
1602
- let firstPrompt = null;
1603
1685
  let entrypoint = null;
1604
- const normalizer = createClaudeNormalizer();
1605
1686
  for (const entry of head) {
1606
1687
  if (typeof entry !== "object" || entry === null) continue;
1607
1688
  const e = entry;
1608
1689
  if (!cwd && typeof e.cwd === "string") cwd = e.cwd;
1609
1690
  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;
1691
+ if (cwd && entrypoint) break;
1619
1692
  }
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;
1693
+ const resolved = resolveSessionTitle(claudeTranscriptTitleSources(head, tailStart > 0 ? tail : []));
1694
+ if (!resolved) return null;
1632
1695
  return {
1633
1696
  provider: "claude",
1634
1697
  providerSessionId: candidate.id,
1635
1698
  cwd,
1636
- title: oneLine(title),
1699
+ title: resolved.title,
1700
+ titleSource: resolved.source,
1637
1701
  updatedAt: new Date(candidate.mtimeMs).toISOString(),
1638
1702
  source: entrypoint,
1639
1703
  activeElsewhere: null
1640
1704
  };
1641
1705
  }
1642
- async function readCodexIndex(file) {
1706
+ async function readCodexIndex(files) {
1707
+ const names = /* @__PURE__ */ new Map();
1708
+ for (const file of files) for (const [id, name] of await readCodexIndexFile(file)) if (!names.has(id)) names.set(id, name);
1709
+ return names;
1710
+ }
1711
+ async function readCodexIndexFile(file) {
1643
1712
  const names = /* @__PURE__ */ new Map();
1644
1713
  let text;
1645
1714
  try {
@@ -1685,13 +1754,15 @@ async function describeCodex(candidate, names) {
1685
1754
  }
1686
1755
  }
1687
1756
  if (isSubAgent) return null;
1688
- const title = names.get(candidate.id) ?? firstPrompt;
1689
- if (!title) return null;
1757
+ const resolved = resolveSessionTitle({ app: names.get(candidate.id), "first-prompt": firstPrompt });
1758
+ if (!resolved) return null;
1690
1759
  return {
1691
1760
  provider: "codex",
1692
1761
  providerSessionId: candidate.id,
1693
1762
  cwd,
1694
- title: oneLine(title),
1763
+ title: resolved.title,
1764
+ titleSource: resolved.source,
1765
+ ...candidate.archived ? { archived: true } : {},
1695
1766
  updatedAt: new Date(candidate.mtimeMs).toISOString(),
1696
1767
  source,
1697
1768
  activeElsewhere: null
@@ -1736,8 +1807,8 @@ async function discoverLocalSessions(options = {}) {
1736
1807
  const homes = options.homes ?? defaultProviderHomes();
1737
1808
  const [claude, codex, names] = await Promise.all([
1738
1809
  claudeCandidates(homes.claudeProjects),
1739
- codexCandidates(homes.codexSessions),
1740
- readCodexIndex(homes.codexIndex)
1810
+ codexCandidates(homes.codex),
1811
+ readCodexIndex(homes.codex.map((h) => h.index))
1741
1812
  ]);
1742
1813
  const candidates = [...claude, ...codex].sort((a, b) => b.mtimeMs - a.mtimeMs);
1743
1814
  const processes = await listProcesses();
@@ -1757,7 +1828,7 @@ async function discoverLocalSessions(options = {}) {
1757
1828
  }
1758
1829
  async function findSessionFile(provider, providerSessionId, homes = defaultProviderHomes()) {
1759
1830
  if (!/^[0-9a-f-]{36}$/i.test(providerSessionId)) return null;
1760
- const candidates = provider === "claude" ? await claudeCandidates(homes.claudeProjects) : await codexCandidates(homes.codexSessions);
1831
+ const candidates = provider === "claude" ? await claudeCandidates(homes.claudeProjects) : await codexCandidates(homes.codex);
1761
1832
  const hits = candidates.filter((c) => c.id === providerSessionId).sort((a, b) => b.mtimeMs - a.mtimeMs);
1762
1833
  return hits[0]?.file ?? null;
1763
1834
  }
@@ -1809,7 +1880,7 @@ async function readSessionFile(provider, file, endOffset, maxBytes = HISTORY_BUD
1809
1880
  async function listSessionFiles(homes = defaultProviderHomes()) {
1810
1881
  const [claude, codex] = await Promise.all([
1811
1882
  claudeCandidates(homes.claudeProjects),
1812
- codexCandidates(homes.codexSessions)
1883
+ codexCandidates(homes.codex)
1813
1884
  ]);
1814
1885
  return [...claude, ...codex].sort((a, b) => b.mtimeMs - a.mtimeMs);
1815
1886
  }
@@ -1852,7 +1923,7 @@ var PreferenceStore = class {
1852
1923
  if (!key || key.length > 200) throw new Error("That chat can't be found.");
1853
1924
  const next = { ...this.data[key] };
1854
1925
  if ("title" in patch) {
1855
- const title = typeof patch.title === "string" ? patch.title.replace(/\s+/g, " ").trim().slice(0, 200) : "";
1926
+ const title = typeof patch.title === "string" ? patch.title.replace(/\s+/g, " ").trim().slice(0, TITLE_MAX) : "";
1856
1927
  if (title) next.title = title;
1857
1928
  else delete next.title;
1858
1929
  }
@@ -1870,6 +1941,18 @@ var PreferenceStore = class {
1870
1941
  this.persist();
1871
1942
  return this.all();
1872
1943
  }
1944
+ /**
1945
+ * Move a preference to the chat's lasting key (`session:<id>` → `<provider>:<id>` once the
1946
+ * provider's id is known), so a rename made early is the one rename everywhere. A preference
1947
+ * already under the lasting key wins field by field.
1948
+ */
1949
+ rekey(from, to) {
1950
+ if (from === to || !this.data[from]) return false;
1951
+ this.data[to] = { ...this.data[from], ...this.data[to] };
1952
+ delete this.data[from];
1953
+ this.persist();
1954
+ return true;
1955
+ }
1873
1956
  persist() {
1874
1957
  const snapshot = JSON.stringify(this.data, null, 1);
1875
1958
  this.writing = this.writing.then(async () => {
@@ -1997,7 +2080,7 @@ async function searchLocalChats(query, options = {}) {
1997
2080
  const raw = JSON.stringify(needle).slice(1, -1);
1998
2081
  const limit = Math.max(1, Math.min(options.limit ?? 30, 100));
1999
2082
  const deadline = Date.now() + (options.budgetMs ?? 3500);
2000
- const names = await readCodexIndex(homes.codexIndex);
2083
+ const names = await readCodexIndex(homes.codex.map((h) => h.index));
2001
2084
  const hits = [];
2002
2085
  let i = cursor;
2003
2086
  for (; i < files.length && hits.length < limit && Date.now() < deadline; i += 1) {
@@ -2032,6 +2115,65 @@ async function searchLocalChats(query, options = {}) {
2032
2115
  };
2033
2116
  }
2034
2117
 
2118
+ // src/sessions/hints.ts
2119
+ function titleHintKey(provider, sessionId) {
2120
+ return `${provider}:${sessionId}`;
2121
+ }
2122
+ function createTitleHintsStore() {
2123
+ let hints = null;
2124
+ const listeners = /* @__PURE__ */ new Set();
2125
+ return {
2126
+ current: () => hints,
2127
+ subscribe(listener) {
2128
+ listeners.add(listener);
2129
+ return () => listeners.delete(listener);
2130
+ },
2131
+ set(next) {
2132
+ hints = next;
2133
+ for (const l of listeners) l();
2134
+ }
2135
+ };
2136
+ }
2137
+
2138
+ // src/engine/title-hints.ts
2139
+ function overlayHint(chat, hints, expected) {
2140
+ if (!expected) return chat;
2141
+ if (hints === null) return { ...chat, titlePending: true };
2142
+ const hint = hints.get(titleHintKey(chat.provider, chat.providerSessionId));
2143
+ if (!hint) return chat;
2144
+ return {
2145
+ ...chat,
2146
+ title: hint.title,
2147
+ titleSource: hint.source,
2148
+ ...hint.archived || chat.archived ? { archived: true } : {},
2149
+ ...hint.scheduled ? { scheduled: true } : {}
2150
+ };
2151
+ }
2152
+ function unavailableFromHints(hints, covered) {
2153
+ if (!hints) return [];
2154
+ const out = [];
2155
+ for (const [key, hint] of hints) {
2156
+ if (!hint.unavailable || covered.has(key)) continue;
2157
+ const at = key.indexOf(":");
2158
+ const provider = key.slice(0, at);
2159
+ if (provider !== "claude" && provider !== "codex") continue;
2160
+ out.push({
2161
+ provider,
2162
+ providerSessionId: key.slice(at + 1),
2163
+ cwd: null,
2164
+ title: hint.title,
2165
+ titleSource: hint.source,
2166
+ updatedAt: new Date(hint.lastActiveMs ?? 0).toISOString(),
2167
+ source: null,
2168
+ activeElsewhere: null,
2169
+ unavailable: hint.unavailable,
2170
+ ...hint.archived ? { archived: true } : {},
2171
+ ...hint.scheduled ? { scheduled: true } : {}
2172
+ });
2173
+ }
2174
+ return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
2175
+ }
2176
+
2035
2177
  // src/engine/store.ts
2036
2178
  import { randomUUID as randomUUID3 } from "crypto";
2037
2179
  import fs6 from "fs/promises";
@@ -2382,6 +2524,13 @@ async function createCodingSessionsEngine(options = {}) {
2382
2524
  }
2383
2525
  }
2384
2526
  }
2527
+ const titleHints = options.titleHints;
2528
+ const hintsNow = () => titleHints?.current() ?? null;
2529
+ const unsubscribeHints = titleHints?.subscribe(() => emit({ type: "titles" })) ?? null;
2530
+ const withHintTitle = (session) => {
2531
+ const hint = session.providerSessionId ? hintsNow()?.get(titleHintKey(session.provider, session.providerSessionId)) : void 0;
2532
+ return hint ? { ...session, title: hint.title } : session;
2533
+ };
2385
2534
  function saveSession(session) {
2386
2535
  index.putSession(session);
2387
2536
  emit({ type: "session", session });
@@ -2538,7 +2687,12 @@ async function createCodingSessionsEngine(options = {}) {
2538
2687
  if (sid && sid !== knownProviderSession) {
2539
2688
  knownProviderSession = sid;
2540
2689
  const current = index.session(session.id);
2541
- if (current) saveSession({ ...current, providerSessionId: sid });
2690
+ if (current) {
2691
+ saveSession({ ...current, providerSessionId: sid });
2692
+ if (preferences.rekey(chatKey(current.provider, null, current.id), chatKey(current.provider, sid))) {
2693
+ emit({ type: "preferences", preferences: preferences.all() });
2694
+ }
2695
+ }
2542
2696
  }
2543
2697
  for (const item of items) emitItem(liveRun, item);
2544
2698
  }
@@ -2846,7 +3000,9 @@ async function createCodingSessionsEngine(options = {}) {
2846
3000
  );
2847
3001
  },
2848
3002
  async discoverSessions(opts) {
2849
- const found = await discoverLocalSessions({ limit: opts?.limit, homes, excludePids: ourPids() });
3003
+ const read = await discoverLocalSessions({ limit: opts?.limit, homes, excludePids: ourPids() });
3004
+ const hints = hintsNow();
3005
+ const found = read.map((chat) => overlayHint(chat, hints, titleHints !== void 0));
2850
3006
  if (backups) {
2851
3007
  for (const chat of found) {
2852
3008
  const key = `${chat.provider}:${chat.providerSessionId}`;
@@ -2857,7 +3013,12 @@ async function createCodingSessionsEngine(options = {}) {
2857
3013
  }
2858
3014
  }
2859
3015
  }
2860
- return found;
3016
+ const limit = opts?.limit ?? 60;
3017
+ const oldest = found.length >= limit ? found[found.length - 1]?.updatedAt ?? "" : "";
3018
+ const gone = unavailableFromHints(hints, new Set(found.map((c) => titleHintKey(c.provider, c.providerSessionId)))).filter(
3019
+ (c) => c.updatedAt >= oldest
3020
+ );
3021
+ return gone.length ? [...found, ...gone].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, limit) : found;
2861
3022
  },
2862
3023
  async recentFolders() {
2863
3024
  const seen = /* @__PURE__ */ new Map();
@@ -2878,7 +3039,7 @@ async function createCodingSessionsEngine(options = {}) {
2878
3039
  );
2879
3040
  },
2880
3041
  async listSessions() {
2881
- return index.sessions();
3042
+ return index.sessions().map(withHintTitle);
2882
3043
  },
2883
3044
  async listRuns(sessionId) {
2884
3045
  const session = index.session(sessionId);
@@ -3053,7 +3214,9 @@ async function createCodingSessionsEngine(options = {}) {
3053
3214
  return all;
3054
3215
  },
3055
3216
  async searchChats(query, opts) {
3056
- return searchLocalChats(String(query ?? ""), { homes, cursor: opts?.cursor, limit: opts?.limit });
3217
+ const result = await searchLocalChats(String(query ?? ""), { homes, cursor: opts?.cursor, limit: opts?.limit });
3218
+ const hints = hintsNow();
3219
+ return { ...result, hits: result.hits.map((hit) => ({ ...hit, title: hints?.get(titleHintKey(hit.provider, hit.providerSessionId))?.title ?? hit.title })) };
3057
3220
  },
3058
3221
  async openFolder(raw, target) {
3059
3222
  const folder = await assertFolder(String(raw ?? ""));
@@ -3083,6 +3246,7 @@ async function createCodingSessionsEngine(options = {}) {
3083
3246
  return agents.handleMcp(secret, body);
3084
3247
  },
3085
3248
  async dispose() {
3249
+ unsubscribeHints?.();
3086
3250
  for (const liveRun of live.values()) {
3087
3251
  liveRun.stopRequested = true;
3088
3252
  if (liveRun.pgid !== null) killGroup(liveRun);
@@ -3138,9 +3302,11 @@ export {
3138
3302
  buildCleanEnv,
3139
3303
  claudeDefaults,
3140
3304
  codexDefaults,
3305
+ codexHome,
3141
3306
  createAiMatrxRawBackupSink,
3142
3307
  createCodingSessionsEngine,
3143
3308
  createRawBackupScheduler,
3309
+ createTitleHintsStore,
3144
3310
  defaultProviderHomes,
3145
3311
  describeActivity,
3146
3312
  describeProviderStatus,
@@ -3153,6 +3319,7 @@ export {
3153
3319
  readSessionFile,
3154
3320
  searchLocalChats,
3155
3321
  sessionActivity,
3156
- snippetAround
3322
+ snippetAround,
3323
+ titleHintKey
3157
3324
  };
3158
3325
  //# sourceMappingURL=index.js.map