@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @ai-matrx/coding-sessions
2
2
 
3
+ ## 0.2.0
4
+
5
+ - **`./sessions` (new, pure):** the one title rule for a coding chat — `resolveSessionTitle` (override >
6
+ app record > remembered app title > custom-title > ai-title > agent-name > summary > first prompt >
7
+ last prompt), `cleanTitle` (first non-empty line, 300 chars), `isBridgePlaceholder` (the bridge's old
8
+ `AI Matrx · <source>` stamp is never a title), `claudeTranscriptTitleSources`, and the `TitleHints`
9
+ port through which the owner of the apps' index (Matrx 2's core) supplies the apps' own titles,
10
+ archived / scheduled flags and chats whose file is gone. Vectors: `fixtures/title-precedence.json`.
11
+ - Discovery and search titles come from that rule (no more 140-char flattening); Codex archived
12
+ rollouts (`archived_sessions/`) are read and marked `archived`; every Codex home in `homes.codex`.
13
+ - Engine option `titleHints`: discovery overlays the apps' titles without waiting (`titlePending`
14
+ meanwhile, a `titles` event when they land), lists the app's chats whose file is gone as
15
+ `unavailable`, and the engine's own sessions show the app's title once known.
16
+ - Screen: "titles updating" beside Your other chats; archived chats and scheduled runs hidden by
17
+ default behind Show hidden chats (a pin shows them); "file missing" rows. A rename made before the
18
+ provider id is known moves to the chat's lasting key; renames are cut at 300 (was 200).
19
+ - **Consumer action:** `ProviderHomes` is now `{ claudeProjects, codex: CodexHome[] }` (was
20
+ `codexSessions` / `codexIndex`); build one with `codexHome(dir)`. Hosts that own an apps index pass
21
+ `titleHints`.
22
+
3
23
  ## 0.1.166
4
24
 
5
25
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -38,9 +38,11 @@ __export(engine_exports, {
38
38
  buildCleanEnv: () => buildCleanEnv,
39
39
  claudeDefaults: () => claudeDefaults,
40
40
  codexDefaults: () => codexDefaults,
41
+ codexHome: () => codexHome,
41
42
  createAiMatrxRawBackupSink: () => createAiMatrxRawBackupSink,
42
43
  createCodingSessionsEngine: () => createCodingSessionsEngine,
43
44
  createRawBackupScheduler: () => createRawBackupScheduler,
45
+ createTitleHintsStore: () => createTitleHintsStore,
44
46
  defaultProviderHomes: () => defaultProviderHomes,
45
47
  describeActivity: () => describeActivity,
46
48
  describeProviderStatus: () => describeProviderStatus,
@@ -53,7 +55,8 @@ __export(engine_exports, {
53
55
  readSessionFile: () => readSessionFile,
54
56
  searchLocalChats: () => searchLocalChats,
55
57
  sessionActivity: () => sessionActivity,
56
- snippetAround: () => snippetAround
58
+ snippetAround: () => snippetAround,
59
+ titleHintKey: () => titleHintKey
57
60
  });
58
61
  module.exports = __toCommonJS(engine_exports);
59
62
 
@@ -1196,6 +1199,9 @@ var PROVIDER_LABEL = {
1196
1199
  claude: "Claude Code",
1197
1200
  codex: "Codex"
1198
1201
  };
1202
+ function chatKey(provider, providerSessionId, sessionId) {
1203
+ return providerSessionId ? `${provider}:${providerSessionId}` : `session:${sessionId ?? ""}`;
1204
+ }
1199
1205
  var TERMINAL_RUN_STATUSES = ["completed", "stopped", "failed"];
1200
1206
  function isTerminalRunStatus(status) {
1201
1207
  return TERMINAL_RUN_STATUSES.includes(status);
@@ -1510,6 +1516,78 @@ var import_promises4 = __toESM(require("fs/promises"), 1);
1510
1516
  var import_node_os3 = __toESM(require("os"), 1);
1511
1517
  var import_node_path5 = __toESM(require("path"), 1);
1512
1518
 
1519
+ // src/sessions/titles.ts
1520
+ var TITLE_MAX = 300;
1521
+ var BRIDGE_PLACEHOLDER = /^AI Matrx · [a-z-]+$/;
1522
+ var TITLE_PRECEDENCE = [
1523
+ /** The person's rename inside Matrx (Coding Sessions' preference). */
1524
+ "override",
1525
+ /** What the coding app's own sidebar shows: Claude's record `title` (renames included); Codex's thread name; Cursor's name. */
1526
+ "app",
1527
+ /** The title the app last showed, remembered by the claude-accounts engine after the record went away. */
1528
+ "app-remembered",
1529
+ /** Claude Code transcript entries (the last one of each kind that is not the bridge placeholder). */
1530
+ "custom-title",
1531
+ "ai-title",
1532
+ "agent-name",
1533
+ "summary",
1534
+ /** The person's first typed message (Codex: the thread's title / first user message). */
1535
+ "first-prompt",
1536
+ "last-prompt"
1537
+ ];
1538
+ var STAMPABLE = /* @__PURE__ */ new Set(["custom-title", "ai-title", "agent-name"]);
1539
+ function isBridgePlaceholder(value) {
1540
+ return typeof value === "string" && BRIDGE_PLACEHOLDER.test(value.trim());
1541
+ }
1542
+ function cleanTitle(value) {
1543
+ if (typeof value !== "string") return null;
1544
+ const line = value.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0);
1545
+ if (!line) return null;
1546
+ return line.length > TITLE_MAX ? `${line.slice(0, TITLE_MAX - 1)}\u2026` : line;
1547
+ }
1548
+ function resolveSessionTitle(sources) {
1549
+ for (const source of TITLE_PRECEDENCE) {
1550
+ const raw = sources[source];
1551
+ if (STAMPABLE.has(source) && isBridgePlaceholder(raw)) continue;
1552
+ const title = cleanTitle(raw);
1553
+ if (title) return { title, source };
1554
+ }
1555
+ return null;
1556
+ }
1557
+ function claudeTranscriptTitleSources(head, tail = []) {
1558
+ const out = {};
1559
+ const take = (source, value) => {
1560
+ if (typeof value !== "string" || !value.trim()) return;
1561
+ if (STAMPABLE.has(source) && isBridgePlaceholder(value)) return;
1562
+ out[source] = value;
1563
+ };
1564
+ const normalizer = createClaudeNormalizer();
1565
+ let firstPrompt = null;
1566
+ const scan = (entries, findPrompt) => {
1567
+ for (const entry of entries) {
1568
+ if (typeof entry !== "object" || entry === null) continue;
1569
+ const e = entry;
1570
+ if (e.type === "custom-title") take("custom-title", e.customTitle);
1571
+ else if (e.type === "ai-title") take("ai-title", e.aiTitle);
1572
+ else if (e.type === "agent-name") take("agent-name", e.agentName);
1573
+ else if (e.type === "summary") take("summary", e.summary);
1574
+ else if (e.type === "last-prompt") take("last-prompt", e.lastPrompt);
1575
+ else if (findPrompt && firstPrompt === null) {
1576
+ for (const item of normalizer.push(entry)) {
1577
+ if (item.kind === "user" && item.text.trim()) {
1578
+ firstPrompt = item.text;
1579
+ break;
1580
+ }
1581
+ }
1582
+ }
1583
+ }
1584
+ };
1585
+ scan(head, true);
1586
+ scan(tail, false);
1587
+ if (firstPrompt !== null) out["first-prompt"] = firstPrompt;
1588
+ return out;
1589
+ }
1590
+
1513
1591
  // src/engine/activity.ts
1514
1592
  var import_node_child_process2 = require("child_process");
1515
1593
  var RECENT_WRITE_MS = 2 * 60 * 1e3;
@@ -1575,11 +1653,17 @@ function describeActivity(input) {
1575
1653
  }
1576
1654
 
1577
1655
  // src/engine/files.ts
1656
+ function codexHome(dir) {
1657
+ return {
1658
+ sessions: import_node_path5.default.join(dir, "sessions"),
1659
+ archived: import_node_path5.default.join(dir, "archived_sessions"),
1660
+ index: import_node_path5.default.join(dir, "session_index.jsonl")
1661
+ };
1662
+ }
1578
1663
  function defaultProviderHomes(home = import_node_os3.default.homedir()) {
1579
1664
  return {
1580
1665
  claudeProjects: import_node_path5.default.join(home, ".claude", "projects"),
1581
- codexSessions: import_node_path5.default.join(home, ".codex", "sessions"),
1582
- codexIndex: import_node_path5.default.join(home, ".codex", "session_index.jsonl")
1666
+ codex: [codexHome(import_node_path5.default.join(home, ".codex"))]
1583
1667
  };
1584
1668
  }
1585
1669
  var UUIDISH = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
@@ -1613,7 +1697,7 @@ async function claudeCandidates(root) {
1613
1697
  }
1614
1698
  return out;
1615
1699
  }
1616
- async function codexCandidates(root) {
1700
+ async function codexTree(root) {
1617
1701
  const out = [];
1618
1702
  for (const year of await safeReaddir(root)) {
1619
1703
  if (!year.isDirectory()) continue;
@@ -1621,17 +1705,23 @@ async function codexCandidates(root) {
1621
1705
  if (!month.isDirectory()) continue;
1622
1706
  for (const day of await safeReaddir(import_node_path5.default.join(root, year.name, month.name))) {
1623
1707
  if (!day.isDirectory()) continue;
1624
- const dir = import_node_path5.default.join(root, year.name, month.name, day.name);
1625
- const entries = await safeReaddir(dir);
1626
- const stats = await Promise.all(
1627
- entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex", import_node_path5.default.join(dir, e.name)))
1628
- );
1629
- for (const s of stats) if (s) out.push(s);
1708
+ out.push(...await codexFlat(import_node_path5.default.join(root, year.name, month.name, day.name), false));
1630
1709
  }
1631
1710
  }
1632
1711
  }
1633
1712
  return out;
1634
1713
  }
1714
+ async function codexFlat(dir, archived) {
1715
+ const entries = await safeReaddir(dir);
1716
+ const stats = await Promise.all(
1717
+ entries.filter((e) => e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")).map((e) => statCandidate("codex", import_node_path5.default.join(dir, e.name)))
1718
+ );
1719
+ return stats.filter((s) => s !== null).map((s) => archived ? { ...s, archived: true } : s);
1720
+ }
1721
+ async function codexCandidates(homes) {
1722
+ const lists = await Promise.all(homes.flatMap((h) => [codexTree(h.sessions), codexFlat(h.archived, true)]));
1723
+ return lists.flat();
1724
+ }
1635
1725
  async function readSlice(file, start, length) {
1636
1726
  const handle = await import_promises4.default.open(file, "r");
1637
1727
  try {
@@ -1653,56 +1743,38 @@ function completeLines(text, droppedFirst) {
1653
1743
  }
1654
1744
  var HEAD_BYTES = 256 * 1024;
1655
1745
  var TAIL_BYTES = 128 * 1024;
1656
- function oneLine(text, max = 140) {
1657
- const flat = text.replace(/\s+/g, " ").trim();
1658
- return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
1659
- }
1660
1746
  async function describeClaude(candidate) {
1661
1747
  const head = parseJsonl(completeLines(await readSlice(candidate.file, 0, HEAD_BYTES), false));
1662
1748
  const tailStart = Math.max(0, candidate.size - TAIL_BYTES);
1663
1749
  const tail = tailStart > 0 ? parseJsonl(completeLines(await readSlice(candidate.file, tailStart, TAIL_BYTES), true)) : head;
1664
1750
  let cwd = null;
1665
- let firstPrompt = null;
1666
1751
  let entrypoint = null;
1667
- const normalizer = createClaudeNormalizer();
1668
1752
  for (const entry of head) {
1669
1753
  if (typeof entry !== "object" || entry === null) continue;
1670
1754
  const e = entry;
1671
1755
  if (!cwd && typeof e.cwd === "string") cwd = e.cwd;
1672
1756
  if (!entrypoint && typeof e.entrypoint === "string") entrypoint = e.entrypoint;
1673
- if (firstPrompt === null) {
1674
- for (const item of normalizer.push(entry)) {
1675
- if (item.kind === "user") {
1676
- firstPrompt = item.text;
1677
- break;
1678
- }
1679
- }
1680
- }
1681
- if (cwd && firstPrompt !== null) break;
1757
+ if (cwd && entrypoint) break;
1682
1758
  }
1683
- const titles = { custom: null, ai: null, agent: null, summary: null, last: null };
1684
- for (const entry of [...head, ...tail]) {
1685
- if (typeof entry !== "object" || entry === null) continue;
1686
- const e = entry;
1687
- if (e.type === "custom-title" && typeof e.customTitle === "string") titles.custom = e.customTitle;
1688
- if (e.type === "ai-title" && typeof e.aiTitle === "string") titles.ai = e.aiTitle;
1689
- if (e.type === "agent-name" && typeof e.agentName === "string") titles.agent = e.agentName;
1690
- if (e.type === "summary" && typeof e.summary === "string") titles.summary = e.summary;
1691
- if (e.type === "last-prompt" && typeof e.lastPrompt === "string") titles.last = e.lastPrompt;
1692
- }
1693
- const title = titles.custom ?? titles.ai ?? titles.agent ?? titles.summary ?? firstPrompt ?? titles.last;
1694
- if (!title) return null;
1759
+ const resolved = resolveSessionTitle(claudeTranscriptTitleSources(head, tailStart > 0 ? tail : []));
1760
+ if (!resolved) return null;
1695
1761
  return {
1696
1762
  provider: "claude",
1697
1763
  providerSessionId: candidate.id,
1698
1764
  cwd,
1699
- title: oneLine(title),
1765
+ title: resolved.title,
1766
+ titleSource: resolved.source,
1700
1767
  updatedAt: new Date(candidate.mtimeMs).toISOString(),
1701
1768
  source: entrypoint,
1702
1769
  activeElsewhere: null
1703
1770
  };
1704
1771
  }
1705
- async function readCodexIndex(file) {
1772
+ async function readCodexIndex(files) {
1773
+ const names = /* @__PURE__ */ new Map();
1774
+ for (const file of files) for (const [id, name] of await readCodexIndexFile(file)) if (!names.has(id)) names.set(id, name);
1775
+ return names;
1776
+ }
1777
+ async function readCodexIndexFile(file) {
1706
1778
  const names = /* @__PURE__ */ new Map();
1707
1779
  let text;
1708
1780
  try {
@@ -1748,13 +1820,15 @@ async function describeCodex(candidate, names) {
1748
1820
  }
1749
1821
  }
1750
1822
  if (isSubAgent) return null;
1751
- const title = names.get(candidate.id) ?? firstPrompt;
1752
- if (!title) return null;
1823
+ const resolved = resolveSessionTitle({ app: names.get(candidate.id), "first-prompt": firstPrompt });
1824
+ if (!resolved) return null;
1753
1825
  return {
1754
1826
  provider: "codex",
1755
1827
  providerSessionId: candidate.id,
1756
1828
  cwd,
1757
- title: oneLine(title),
1829
+ title: resolved.title,
1830
+ titleSource: resolved.source,
1831
+ ...candidate.archived ? { archived: true } : {},
1758
1832
  updatedAt: new Date(candidate.mtimeMs).toISOString(),
1759
1833
  source,
1760
1834
  activeElsewhere: null
@@ -1799,8 +1873,8 @@ async function discoverLocalSessions(options = {}) {
1799
1873
  const homes = options.homes ?? defaultProviderHomes();
1800
1874
  const [claude, codex, names] = await Promise.all([
1801
1875
  claudeCandidates(homes.claudeProjects),
1802
- codexCandidates(homes.codexSessions),
1803
- readCodexIndex(homes.codexIndex)
1876
+ codexCandidates(homes.codex),
1877
+ readCodexIndex(homes.codex.map((h) => h.index))
1804
1878
  ]);
1805
1879
  const candidates = [...claude, ...codex].sort((a, b) => b.mtimeMs - a.mtimeMs);
1806
1880
  const processes = await listProcesses();
@@ -1820,7 +1894,7 @@ async function discoverLocalSessions(options = {}) {
1820
1894
  }
1821
1895
  async function findSessionFile(provider, providerSessionId, homes = defaultProviderHomes()) {
1822
1896
  if (!/^[0-9a-f-]{36}$/i.test(providerSessionId)) return null;
1823
- const candidates = provider === "claude" ? await claudeCandidates(homes.claudeProjects) : await codexCandidates(homes.codexSessions);
1897
+ const candidates = provider === "claude" ? await claudeCandidates(homes.claudeProjects) : await codexCandidates(homes.codex);
1824
1898
  const hits = candidates.filter((c) => c.id === providerSessionId).sort((a, b) => b.mtimeMs - a.mtimeMs);
1825
1899
  return hits[0]?.file ?? null;
1826
1900
  }
@@ -1872,7 +1946,7 @@ async function readSessionFile(provider, file, endOffset, maxBytes = HISTORY_BUD
1872
1946
  async function listSessionFiles(homes = defaultProviderHomes()) {
1873
1947
  const [claude, codex] = await Promise.all([
1874
1948
  claudeCandidates(homes.claudeProjects),
1875
- codexCandidates(homes.codexSessions)
1949
+ codexCandidates(homes.codex)
1876
1950
  ]);
1877
1951
  return [...claude, ...codex].sort((a, b) => b.mtimeMs - a.mtimeMs);
1878
1952
  }
@@ -1915,7 +1989,7 @@ var PreferenceStore = class {
1915
1989
  if (!key || key.length > 200) throw new Error("That chat can't be found.");
1916
1990
  const next = { ...this.data[key] };
1917
1991
  if ("title" in patch) {
1918
- const title = typeof patch.title === "string" ? patch.title.replace(/\s+/g, " ").trim().slice(0, 200) : "";
1992
+ const title = typeof patch.title === "string" ? patch.title.replace(/\s+/g, " ").trim().slice(0, TITLE_MAX) : "";
1919
1993
  if (title) next.title = title;
1920
1994
  else delete next.title;
1921
1995
  }
@@ -1933,6 +2007,18 @@ var PreferenceStore = class {
1933
2007
  this.persist();
1934
2008
  return this.all();
1935
2009
  }
2010
+ /**
2011
+ * Move a preference to the chat's lasting key (`session:<id>` → `<provider>:<id>` once the
2012
+ * provider's id is known), so a rename made early is the one rename everywhere. A preference
2013
+ * already under the lasting key wins field by field.
2014
+ */
2015
+ rekey(from, to) {
2016
+ if (from === to || !this.data[from]) return false;
2017
+ this.data[to] = { ...this.data[from], ...this.data[to] };
2018
+ delete this.data[from];
2019
+ this.persist();
2020
+ return true;
2021
+ }
1936
2022
  persist() {
1937
2023
  const snapshot = JSON.stringify(this.data, null, 1);
1938
2024
  this.writing = this.writing.then(async () => {
@@ -2060,7 +2146,7 @@ async function searchLocalChats(query, options = {}) {
2060
2146
  const raw = JSON.stringify(needle).slice(1, -1);
2061
2147
  const limit = Math.max(1, Math.min(options.limit ?? 30, 100));
2062
2148
  const deadline = Date.now() + (options.budgetMs ?? 3500);
2063
- const names = await readCodexIndex(homes.codexIndex);
2149
+ const names = await readCodexIndex(homes.codex.map((h) => h.index));
2064
2150
  const hits = [];
2065
2151
  let i = cursor;
2066
2152
  for (; i < files.length && hits.length < limit && Date.now() < deadline; i += 1) {
@@ -2095,6 +2181,65 @@ async function searchLocalChats(query, options = {}) {
2095
2181
  };
2096
2182
  }
2097
2183
 
2184
+ // src/sessions/hints.ts
2185
+ function titleHintKey(provider, sessionId) {
2186
+ return `${provider}:${sessionId}`;
2187
+ }
2188
+ function createTitleHintsStore() {
2189
+ let hints = null;
2190
+ const listeners = /* @__PURE__ */ new Set();
2191
+ return {
2192
+ current: () => hints,
2193
+ subscribe(listener) {
2194
+ listeners.add(listener);
2195
+ return () => listeners.delete(listener);
2196
+ },
2197
+ set(next) {
2198
+ hints = next;
2199
+ for (const l of listeners) l();
2200
+ }
2201
+ };
2202
+ }
2203
+
2204
+ // src/engine/title-hints.ts
2205
+ function overlayHint(chat, hints, expected) {
2206
+ if (!expected) return chat;
2207
+ if (hints === null) return { ...chat, titlePending: true };
2208
+ const hint = hints.get(titleHintKey(chat.provider, chat.providerSessionId));
2209
+ if (!hint) return chat;
2210
+ return {
2211
+ ...chat,
2212
+ title: hint.title,
2213
+ titleSource: hint.source,
2214
+ ...hint.archived || chat.archived ? { archived: true } : {},
2215
+ ...hint.scheduled ? { scheduled: true } : {}
2216
+ };
2217
+ }
2218
+ function unavailableFromHints(hints, covered) {
2219
+ if (!hints) return [];
2220
+ const out = [];
2221
+ for (const [key, hint] of hints) {
2222
+ if (!hint.unavailable || covered.has(key)) continue;
2223
+ const at = key.indexOf(":");
2224
+ const provider = key.slice(0, at);
2225
+ if (provider !== "claude" && provider !== "codex") continue;
2226
+ out.push({
2227
+ provider,
2228
+ providerSessionId: key.slice(at + 1),
2229
+ cwd: null,
2230
+ title: hint.title,
2231
+ titleSource: hint.source,
2232
+ updatedAt: new Date(hint.lastActiveMs ?? 0).toISOString(),
2233
+ source: null,
2234
+ activeElsewhere: null,
2235
+ unavailable: hint.unavailable,
2236
+ ...hint.archived ? { archived: true } : {},
2237
+ ...hint.scheduled ? { scheduled: true } : {}
2238
+ });
2239
+ }
2240
+ return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
2241
+ }
2242
+
2098
2243
  // src/engine/store.ts
2099
2244
  var import_node_crypto4 = require("crypto");
2100
2245
  var import_promises6 = __toESM(require("fs/promises"), 1);
@@ -2445,6 +2590,13 @@ async function createCodingSessionsEngine(options = {}) {
2445
2590
  }
2446
2591
  }
2447
2592
  }
2593
+ const titleHints = options.titleHints;
2594
+ const hintsNow = () => titleHints?.current() ?? null;
2595
+ const unsubscribeHints = titleHints?.subscribe(() => emit({ type: "titles" })) ?? null;
2596
+ const withHintTitle = (session) => {
2597
+ const hint = session.providerSessionId ? hintsNow()?.get(titleHintKey(session.provider, session.providerSessionId)) : void 0;
2598
+ return hint ? { ...session, title: hint.title } : session;
2599
+ };
2448
2600
  function saveSession(session) {
2449
2601
  index.putSession(session);
2450
2602
  emit({ type: "session", session });
@@ -2601,7 +2753,12 @@ async function createCodingSessionsEngine(options = {}) {
2601
2753
  if (sid && sid !== knownProviderSession) {
2602
2754
  knownProviderSession = sid;
2603
2755
  const current = index.session(session.id);
2604
- if (current) saveSession({ ...current, providerSessionId: sid });
2756
+ if (current) {
2757
+ saveSession({ ...current, providerSessionId: sid });
2758
+ if (preferences.rekey(chatKey(current.provider, null, current.id), chatKey(current.provider, sid))) {
2759
+ emit({ type: "preferences", preferences: preferences.all() });
2760
+ }
2761
+ }
2605
2762
  }
2606
2763
  for (const item of items) emitItem(liveRun, item);
2607
2764
  }
@@ -2909,7 +3066,9 @@ async function createCodingSessionsEngine(options = {}) {
2909
3066
  );
2910
3067
  },
2911
3068
  async discoverSessions(opts) {
2912
- const found = await discoverLocalSessions({ limit: opts?.limit, homes, excludePids: ourPids() });
3069
+ const read = await discoverLocalSessions({ limit: opts?.limit, homes, excludePids: ourPids() });
3070
+ const hints = hintsNow();
3071
+ const found = read.map((chat) => overlayHint(chat, hints, titleHints !== void 0));
2913
3072
  if (backups) {
2914
3073
  for (const chat of found) {
2915
3074
  const key = `${chat.provider}:${chat.providerSessionId}`;
@@ -2920,7 +3079,12 @@ async function createCodingSessionsEngine(options = {}) {
2920
3079
  }
2921
3080
  }
2922
3081
  }
2923
- return found;
3082
+ const limit = opts?.limit ?? 60;
3083
+ const oldest = found.length >= limit ? found[found.length - 1]?.updatedAt ?? "" : "";
3084
+ const gone = unavailableFromHints(hints, new Set(found.map((c) => titleHintKey(c.provider, c.providerSessionId)))).filter(
3085
+ (c) => c.updatedAt >= oldest
3086
+ );
3087
+ return gone.length ? [...found, ...gone].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, limit) : found;
2924
3088
  },
2925
3089
  async recentFolders() {
2926
3090
  const seen = /* @__PURE__ */ new Map();
@@ -2941,7 +3105,7 @@ async function createCodingSessionsEngine(options = {}) {
2941
3105
  );
2942
3106
  },
2943
3107
  async listSessions() {
2944
- return index.sessions();
3108
+ return index.sessions().map(withHintTitle);
2945
3109
  },
2946
3110
  async listRuns(sessionId) {
2947
3111
  const session = index.session(sessionId);
@@ -3116,7 +3280,9 @@ async function createCodingSessionsEngine(options = {}) {
3116
3280
  return all;
3117
3281
  },
3118
3282
  async searchChats(query, opts) {
3119
- return searchLocalChats(String(query ?? ""), { homes, cursor: opts?.cursor, limit: opts?.limit });
3283
+ const result = await searchLocalChats(String(query ?? ""), { homes, cursor: opts?.cursor, limit: opts?.limit });
3284
+ const hints = hintsNow();
3285
+ return { ...result, hits: result.hits.map((hit) => ({ ...hit, title: hints?.get(titleHintKey(hit.provider, hit.providerSessionId))?.title ?? hit.title })) };
3120
3286
  },
3121
3287
  async openFolder(raw, target) {
3122
3288
  const folder = await assertFolder(String(raw ?? ""));
@@ -3146,6 +3312,7 @@ async function createCodingSessionsEngine(options = {}) {
3146
3312
  return agents.handleMcp(secret, body);
3147
3313
  },
3148
3314
  async dispose() {
3315
+ unsubscribeHints?.();
3149
3316
  for (const liveRun of live.values()) {
3150
3317
  liveRun.stopRequested = true;
3151
3318
  if (liveRun.pgid !== null) killGroup(liveRun);
@@ -3202,9 +3369,11 @@ function openFolderIn(folder, target) {
3202
3369
  buildCleanEnv,
3203
3370
  claudeDefaults,
3204
3371
  codexDefaults,
3372
+ codexHome,
3205
3373
  createAiMatrxRawBackupSink,
3206
3374
  createCodingSessionsEngine,
3207
3375
  createRawBackupScheduler,
3376
+ createTitleHintsStore,
3208
3377
  defaultProviderHomes,
3209
3378
  describeActivity,
3210
3379
  describeProviderStatus,
@@ -3217,6 +3386,7 @@ function openFolderIn(folder, target) {
3217
3386
  readSessionFile,
3218
3387
  searchLocalChats,
3219
3388
  sessionActivity,
3220
- snippetAround
3389
+ snippetAround,
3390
+ titleHintKey
3221
3391
  });
3222
3392
  //# sourceMappingURL=index.cjs.map