offsite 0.0.55 → 0.0.56

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.
@@ -21,7 +21,7 @@ import { sumBy } from "es-toolkit";
21
21
  import { DatabaseSync } from "node:sqlite";
22
22
  import { drizzle } from "drizzle-orm/node-sqlite";
23
23
  import { migrate } from "drizzle-orm/node-sqlite/migrator";
24
- import { integer, snakeCase, text } from "drizzle-orm/sqlite-core";
24
+ import { integer, real, snakeCase, text } from "drizzle-orm/sqlite-core";
25
25
  import sharp from "sharp";
26
26
  import { promisify } from "node:util";
27
27
  import webPush from "web-push";
@@ -1597,6 +1597,30 @@ async function codexThread(threadId) {
1597
1597
  includeTurns: false
1598
1598
  })).thread;
1599
1599
  }
1600
+ async function codexSearch(term) {
1601
+ return (await appServer(codexBaseDir).request("thread/search", {
1602
+ searchTerm: term,
1603
+ limit: 50,
1604
+ sortKey: "updated_at",
1605
+ sourceKinds: [
1606
+ "cli",
1607
+ "vscode",
1608
+ "appServer"
1609
+ ]
1610
+ })).data;
1611
+ }
1612
+ async function codexMessages(threadId) {
1613
+ return (await readThread(threadId)).turns.flatMap((turn) => turn.items.flatMap((item) => {
1614
+ if (item.type === "userMessage") return [{
1615
+ author: "user",
1616
+ text: item.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("\n")
1617
+ }];
1618
+ return item.type === "agentMessage" ? [{
1619
+ author: "assistant",
1620
+ text: item.text
1621
+ }] : [];
1622
+ }));
1623
+ }
1600
1624
  async function codexHistory(sessionId) {
1601
1625
  const run = codexRun(sessionId);
1602
1626
  const items = turnItems((await readThread(sessionId)).turns, null);
@@ -1694,6 +1718,118 @@ async function readThread(threadId) {
1694
1718
  includeTurns: true
1695
1719
  })).thread;
1696
1720
  }
1721
+ const WORD_START = .8;
1722
+ const MID_WORD = .3;
1723
+ const PHRASE_BONUS = .25;
1724
+ const MID_WORD_LENGTH = 4;
1725
+ const SNIPPET_LEAD = 24;
1726
+ const SNIPPET_LENGTH = 160;
1727
+ const alphanumeric = /[\p{L}\p{N}]/u;
1728
+ const lowercase = /\p{Ll}/u;
1729
+ const uppercase = /\p{Lu}/u;
1730
+ const digit = /\p{N}/u;
1731
+ function fold(text) {
1732
+ return text.replace(/[\u0080-￿]/g, (character) => {
1733
+ const folded = character.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase();
1734
+ return folded.length === 1 ? folded : character;
1735
+ }).toLowerCase();
1736
+ }
1737
+ function searchWords(query) {
1738
+ return fold(query).replace(/^["'`]+|["'`]+$/g, "").split(/\s+/).filter(Boolean);
1739
+ }
1740
+ function hasSearchableWord(query) {
1741
+ return searchWords(query).some((word) => word.length >= 3);
1742
+ }
1743
+ function isWordStart(text, index) {
1744
+ const character = text[index];
1745
+ const previous = text[index - 1] ?? "";
1746
+ const next = text[index + 1] ?? "";
1747
+ return alphanumeric.test(character) && (previous === "" || !alphanumeric.test(previous) || lowercase.test(previous) && uppercase.test(character) || uppercase.test(previous) && uppercase.test(character) && lowercase.test(next) || digit.test(previous) !== digit.test(character));
1748
+ }
1749
+ function isWordEnd(text, end) {
1750
+ return end >= text.length || !alphanumeric.test(text[end]) || isWordStart(text, end);
1751
+ }
1752
+ function findWord(text, folded, word) {
1753
+ let quality = 0;
1754
+ let at = -1;
1755
+ for (let index = folded.indexOf(word); index !== -1; index = folded.indexOf(word, index + 1)) {
1756
+ const found = !isWordStart(text, index) ? MID_WORD : isWordEnd(text, index + word.length) ? 1 : WORD_START;
1757
+ if (found > quality) {
1758
+ quality = found;
1759
+ at = index;
1760
+ }
1761
+ }
1762
+ return at === -1 ? null : {
1763
+ quality,
1764
+ at
1765
+ };
1766
+ }
1767
+ function matchMessage(query, text) {
1768
+ const words = searchWords(query);
1769
+ if (words.length === 0) return null;
1770
+ const folded = fold(text);
1771
+ let quality = 1;
1772
+ let offset = text.length;
1773
+ for (const word of words) {
1774
+ const found = findWord(text, folded, word);
1775
+ if (found === null || found.quality === MID_WORD && word.length < MID_WORD_LENGTH) return null;
1776
+ if (word.length >= 3) quality = Math.min(quality, found.quality);
1777
+ offset = Math.min(offset, found.at);
1778
+ }
1779
+ const phrase = words.length > 1 ? folded.indexOf(words.join(" ")) : -1;
1780
+ return phrase === -1 ? {
1781
+ quality,
1782
+ offset
1783
+ } : {
1784
+ quality: quality + PHRASE_BONUS,
1785
+ offset: phrase
1786
+ };
1787
+ }
1788
+ function messageSnippet(text, offset) {
1789
+ let start = Math.max(0, offset - SNIPPET_LEAD);
1790
+ const space = text.slice(start, offset).search(/\s/);
1791
+ if (start > 0 && space !== -1) start += space + 1;
1792
+ const shown = text.slice(start, start + 320).replace(/\s+/g, " ").trim();
1793
+ const cut = shown.length > SNIPPET_LENGTH;
1794
+ return `${start > 0 ? "…" : ""}${shown.slice(0, SNIPPET_LENGTH)}${cut ? "…" : ""}`;
1795
+ }
1796
+ // ../core/src/search.ts
1797
+ const OPEN_REPEAT = 6e5;
1798
+ const PICK_WINDOW = 18144e5;
1799
+ const PICK_LIMIT = 3;
1800
+ const PICK_LENGTH = 32;
1801
+ const USAGE_HALF_LIFE = 12096e5;
1802
+ function decay(age) {
1803
+ return 2 ** (-Math.max(0, age) / USAGE_HALF_LIFE);
1804
+ }
1805
+ function storedQuery(query) {
1806
+ return searchWords(query).join(" ").slice(0, PICK_LENGTH).trim();
1807
+ }
1808
+ function nextOpens(previous, at, query) {
1809
+ const pick = storedQuery(query ?? "");
1810
+ const earlier = (previous?.picks ?? []).filter((entry) => at - entry.at <= PICK_WINDOW && entry.query !== pick);
1811
+ const kept = (pick === "" ? earlier : [{
1812
+ query: pick,
1813
+ at
1814
+ }, ...earlier]).slice(0, PICK_LIMIT);
1815
+ if (previous === null) return {
1816
+ score: 1,
1817
+ openedAt: at,
1818
+ picks: kept
1819
+ };
1820
+ if (at - previous.openedAt < OPEN_REPEAT) return {
1821
+ ...previous,
1822
+ picks: kept
1823
+ };
1824
+ return {
1825
+ score: previous.score * decay(at - previous.openedAt) + 1,
1826
+ openedAt: at,
1827
+ picks: kept
1828
+ };
1829
+ }
1830
+ function compareMessages(left, right) {
1831
+ return right.quality - left.quality || Number(right.author === "user") - Number(left.author === "user");
1832
+ }
1697
1833
  // src/lib/opencode/items.ts
1698
1834
  const toolStatuses = {
1699
1835
  streaming: "running",
@@ -2100,6 +2236,19 @@ async function openCodeHistory(sessionId, cwd) {
2100
2236
  partial: run?.partial ?? ""
2101
2237
  };
2102
2238
  }
2239
+ async function openCodeMessages(sessionId) {
2240
+ return (await allMessages(await openCode(), sessionId)).flatMap((message) => {
2241
+ if (message.type === "user") return [{
2242
+ author: "user",
2243
+ text: message.text
2244
+ }];
2245
+ if (message.type !== "assistant") return [];
2246
+ return [{
2247
+ author: "assistant",
2248
+ text: message.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("\n")
2249
+ }];
2250
+ });
2251
+ }
2103
2252
  async function openCodeAgent(agentId, parent) {
2104
2253
  return (await allMessages(await openCode(), agentId)).flatMap((message) => messageItems(message, parent));
2105
2254
  }
@@ -2586,6 +2735,12 @@ const sessions$1 = snakeCase.table("sessions", {
2586
2735
  "minimized"
2587
2736
  ] }).notNull()
2588
2737
  });
2738
+ const sessionOpens = snakeCase.table("session_opens", {
2739
+ sessionId: text().primaryKey(),
2740
+ score: real().notNull(),
2741
+ openedAt: integer().notNull(),
2742
+ picks: text({ mode: "json" }).$type().notNull()
2743
+ });
2589
2744
  const pairingCodes = snakeCase.table("pairing_codes", {
2590
2745
  hash: text().primaryKey(),
2591
2746
  name: text(),
@@ -2610,7 +2765,7 @@ const pushSubscriptions = snakeCase.table("push_subscriptions", {
2610
2765
  // src/services/files.ts
2611
2766
  const FILE_LIMIT = 2e4;
2612
2767
  const OUTPUT_LIMIT$1 = 262144;
2613
- const READ_LIMIT = 1048576;
2768
+ const READ_LIMIT$1 = 1048576;
2614
2769
  const MEDIA_LINK_LIFETIME = 36e5;
2615
2770
  const browseToken = randomBytes(32).toString("base64url");
2616
2771
  const mediaLinks = /* @__PURE__ */ new Map();
@@ -2661,10 +2816,10 @@ function readProjectFile(cwd, file) {
2661
2816
  const full = resolveProjectFile(cwd, file);
2662
2817
  const stats = statSync(full);
2663
2818
  if (!stats.isFile()) throw new Error(`${file} is not a file`);
2664
- if (stats.size > READ_LIMIT) return {
2819
+ if (stats.size > READ_LIMIT$1) return {
2665
2820
  kind: "large",
2666
2821
  size: stats.size,
2667
- limit: READ_LIMIT
2822
+ limit: READ_LIMIT$1
2668
2823
  };
2669
2824
  const data = readFileSync(full);
2670
2825
  const mediaType = mediaTypes.get(path.extname(full).toLowerCase());
@@ -2850,7 +3005,7 @@ const UNTRACKED_LIMIT = 100;
2850
3005
  const NEW_FILE_LIMIT = 262144;
2851
3006
  const REPO_DEPTH = 3;
2852
3007
  const FETCH_TIMEOUT = 6e4;
2853
- const run$2 = promisify(execFile);
3008
+ const run$3 = promisify(execFile);
2854
3009
  const PULL_REQUEST_INSTRUCTIONS = `You write GitHub pull requests as Markdown.
2855
3010
 
2856
3011
  ## TITLE RULES
@@ -2937,7 +3092,7 @@ async function sync(cwd) {
2937
3092
  async function fetchProject(cwd) {
2938
3093
  await Promise.all((await projectRepos(cwd)).map(async (repo) => {
2939
3094
  try {
2940
- await run$2("git", ["fetch"], {
3095
+ await run$3("git", ["fetch"], {
2941
3096
  cwd: path.join(cwd, repo),
2942
3097
  env: {
2943
3098
  ...process.env,
@@ -2968,7 +3123,7 @@ async function switchBranch(cwd, branch, create) {
2968
3123
  async function pullRequestBranches(cwd) {
2969
3124
  const head = (await gitStatus(cwd)).branch;
2970
3125
  if (head === null) throw new Error("Not on a branch");
2971
- const base = (await run$2("gh", [
3126
+ const base = (await run$3("gh", [
2972
3127
  "repo",
2973
3128
  "view",
2974
3129
  "--json",
@@ -2993,7 +3148,7 @@ async function preparePullRequest(cwd, base, commitChanges) {
2993
3148
  if (commitChanges) await commit(cwd, "", true);
2994
3149
  const status = await gitStatus(cwd);
2995
3150
  if (status.upstream === null || status.ahead > 0) await push$1(cwd);
2996
- const open = (await run$2("gh", [
3151
+ const open = (await run$3("gh", [
2997
3152
  "pr",
2998
3153
  "list",
2999
3154
  "--head",
@@ -3040,7 +3195,7 @@ async function preparePullRequest(cwd, base, commitChanges) {
3040
3195
  };
3041
3196
  }
3042
3197
  async function createPullRequest(cwd, request) {
3043
- const url = (await run$2("gh", [
3198
+ const url = (await run$3("gh", [
3044
3199
  "pr",
3045
3200
  "create",
3046
3201
  "--title",
@@ -3073,7 +3228,7 @@ async function projectDiff(cwd) {
3073
3228
  return (await Promise.all(repos.map((repo) => repoDiff(path.join(cwd, repo), repo === "" ? "" : `${repo}/`)))).join("");
3074
3229
  }
3075
3230
  function git(cwd, args) {
3076
- return run$2("git", args, {
3231
+ return run$3("git", args, {
3077
3232
  cwd,
3078
3233
  maxBuffer: 67108864
3079
3234
  });
@@ -3367,6 +3522,13 @@ function expandHome(input) {
3367
3522
  return input;
3368
3523
  }
3369
3524
  // src/services/provider-sessions.ts
3525
+ const TIME_LIMIT = 1e4;
3526
+ function timeLimit(work, message) {
3527
+ let timer;
3528
+ return Promise.race([work, new Promise((_, reject) => {
3529
+ timer = setTimeout(() => reject(new Error(message)), TIME_LIMIT);
3530
+ })]).finally(() => clearTimeout(timer));
3531
+ }
3370
3532
  async function info(provider, id, paths) {
3371
3533
  switch (provider) {
3372
3534
  case "claude": return (await Promise.all([...paths].map(async ([cwd, projectId]) => {
@@ -3450,7 +3612,6 @@ async function find(provider, paths) {
3450
3612
  }
3451
3613
  // src/services/sessions.ts
3452
3614
  const day = 864e5;
3453
- const LIST_TIMEOUT = 1e4;
3454
3615
  const REFRESH_DELAY = 1e3;
3455
3616
  const allProviders = [
3456
3617
  "claude",
@@ -3490,13 +3651,34 @@ function sessions() {
3490
3651
  async function inactiveSessions(projectId) {
3491
3652
  return (await everything(projectPaths().filter(([, id]) => projectId === null || id === projectId))).filter((session) => session.status === "inactive").sort((a, b) => b.updatedAt - a.updatedAt);
3492
3653
  }
3493
- async function searchSessions(query) {
3494
- const words = query.toLowerCase().split(/\s+/).filter(Boolean);
3495
- const folders = new Map(projects().map((project) => [project.id, project.path]));
3496
- return (await everything(projectPaths())).filter((session) => {
3497
- const text = `${session.title} ${folders.get(session.projectId) ?? ""}`.toLowerCase();
3498
- return words.every((word) => text.includes(word));
3499
- }).sort((a, b) => b.updatedAt - a.updatedAt).slice(0, 50);
3654
+ async function searchableSessions() {
3655
+ const paths = projectPaths();
3656
+ const lists = await Promise.all(enabledProviders().map(async (provider) => {
3657
+ try {
3658
+ return {
3659
+ provider,
3660
+ items: await timeLimit(find(provider, new Map(paths)), `${providerName(provider)} did not list its sessions in time`) ?? [],
3661
+ failed: false
3662
+ };
3663
+ } catch (error) {
3664
+ console.warn(`could not search ${providerName(provider)}: ${error}`);
3665
+ return {
3666
+ provider,
3667
+ items: [],
3668
+ failed: true
3669
+ };
3670
+ }
3671
+ }));
3672
+ const found = lists.flatMap((list) => list.items);
3673
+ for (const item of found) providers.set(item.id, item.provider);
3674
+ const opens = new Map(db.select().from(sessionOpens).all().map((record) => [record.sessionId, record]));
3675
+ return {
3676
+ sessions: withStatus(found).map((session) => ({
3677
+ ...session,
3678
+ opens: opens.get(session.id) ?? null
3679
+ })),
3680
+ failed: lists.filter((list) => list.failed).map((list) => list.provider)
3681
+ };
3500
3682
  }
3501
3683
  async function findSession(id) {
3502
3684
  const paths = new Map(projectPaths());
@@ -3516,7 +3698,7 @@ async function findSession(id) {
3516
3698
  return null;
3517
3699
  }
3518
3700
  async function everything(paths) {
3519
- const found = (await Promise.all(allProviders.filter((provider) => readConfig().enabledProviders[provider]).map((provider) => find(provider, new Map(paths))))).flatMap((items) => items ?? []);
3701
+ const found = (await Promise.all(enabledProviders().map((provider) => find(provider, new Map(paths))))).flatMap((items) => items ?? []);
3520
3702
  for (const item of found) providers.set(item.id, item.provider);
3521
3703
  return withStatus(found);
3522
3704
  }
@@ -3532,6 +3714,9 @@ function withStatus(items) {
3532
3714
  };
3533
3715
  });
3534
3716
  }
3717
+ function enabledProviders() {
3718
+ return allProviders.filter((provider) => readConfig().enabledProviders[provider]);
3719
+ }
3535
3720
  function projectPaths() {
3536
3721
  return projects().map((project) => [project.path, project.id]);
3537
3722
  }
@@ -3562,6 +3747,16 @@ function recordInteraction(id) {
3562
3747
  }
3563
3748
  }).run();
3564
3749
  }
3750
+ function recordOpen(id, query) {
3751
+ const opens = nextOpens(db.select().from(sessionOpens).where(eq(sessionOpens.sessionId, id)).get() ?? null, Date.now(), query);
3752
+ db.insert(sessionOpens).values({
3753
+ sessionId: id,
3754
+ ...opens
3755
+ }).onConflictDoUpdate({
3756
+ target: sessionOpens.sessionId,
3757
+ set: opens
3758
+ }).run();
3759
+ }
3565
3760
  function publishSessions() {
3566
3761
  broadcast({
3567
3762
  type: "sessions",
@@ -3595,7 +3790,6 @@ async function refresh(provider) {
3595
3790
  async function list(provider) {
3596
3791
  const paths = new Map(projectPaths());
3597
3792
  const before = statuses[provider];
3598
- let timer;
3599
3793
  try {
3600
3794
  if (!readConfig().enabledProviders[provider]) {
3601
3795
  listed[provider] = [];
@@ -3606,9 +3800,7 @@ async function list(provider) {
3606
3800
  };
3607
3801
  return;
3608
3802
  }
3609
- const found = await Promise.race([tracked(provider, paths), new Promise((_, reject) => {
3610
- timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`${providerName(provider)} did not list its sessions within ${LIST_TIMEOUT / 1e3} seconds`)), LIST_TIMEOUT);
3611
- })]);
3803
+ const found = await timeLimit(tracked(provider, paths), `${providerName(provider)} did not list its sessions in time`);
3612
3804
  if (found === null) {
3613
3805
  listed[provider] = [];
3614
3806
  statuses[provider] = {
@@ -3634,7 +3826,6 @@ async function list(provider) {
3634
3826
  error: message
3635
3827
  };
3636
3828
  } finally {
3637
- clearTimeout(timer);
3638
3829
  publishSessions();
3639
3830
  if (JSON.stringify(statuses[provider]) !== JSON.stringify(before)) broadcast({
3640
3831
  type: "providers",
@@ -3722,6 +3913,7 @@ async function removeSession(id) {
3722
3913
  case "opencode": await removeOpenCode(id);
3723
3914
  }
3724
3915
  db.delete(sessions$1).where(eq(sessions$1.sessionId, id)).run();
3916
+ db.delete(sessionOpens).where(eq(sessionOpens.sessionId, id)).run();
3725
3917
  listed[provider] = listed[provider].filter((item) => item.id !== id);
3726
3918
  }
3727
3919
  async function fork(sessionId, projectId, messageId) {
@@ -5354,6 +5546,140 @@ var Run = class {
5354
5546
  });
5355
5547
  }
5356
5548
  };
5549
+ // src/services/message-search.ts
5550
+ const READ_LIMIT = 30;
5551
+ const CODEX_READ_LIMIT = 10;
5552
+ const RG_TIME_LIMIT = 5e3;
5553
+ const generatedText = /^(?:This session is being continued from a previous conversation|<command-|<local-command-|<bash-|<task-notification|<cross-session-message|Another Claude session sent a message:|\[Request interrupted by user)/;
5554
+ const run$2 = promisify(execFile);
5555
+ async function searchMessages(query) {
5556
+ if (!hasSearchableWord(query)) return {
5557
+ matches: [],
5558
+ failed: []
5559
+ };
5560
+ const paths = new Map(projects().map((project) => [project.path, project.id]));
5561
+ const folders = new Map(projects().map((project) => [project.id, project.path]));
5562
+ const results = await Promise.all([
5563
+ "claude",
5564
+ "codex",
5565
+ "opencode"
5566
+ ].filter((provider) => readConfig().enabledProviders[provider]).map(async (provider) => {
5567
+ try {
5568
+ return {
5569
+ provider,
5570
+ matches: await timeLimit(search(provider, query, paths, folders), `${providerName(provider)} did not search in time`),
5571
+ failed: false
5572
+ };
5573
+ } catch (error) {
5574
+ console.warn(`could not search ${providerName(provider)} messages: ${error}`);
5575
+ return {
5576
+ provider,
5577
+ matches: [],
5578
+ failed: true
5579
+ };
5580
+ }
5581
+ }));
5582
+ return {
5583
+ matches: results.flatMap((result) => result.matches),
5584
+ failed: results.filter((result) => result.failed).map((result) => result.provider)
5585
+ };
5586
+ }
5587
+ async function search(provider, query, paths, folders) {
5588
+ const found = await find(provider, paths);
5589
+ if (found === null) return [];
5590
+ const sessions = newestFirst(found);
5591
+ switch (provider) {
5592
+ case "claude": return searchClaude(query, sessions, folders);
5593
+ case "codex": return searchCodex(query, sessions);
5594
+ case "opencode": return readAndMatch(query, sessions.slice(0, READ_LIMIT), openCodeMessages);
5595
+ }
5596
+ }
5597
+ function newestFirst(sessions) {
5598
+ return [...sessions].sort((a, b) => b.updatedAt - a.updatedAt);
5599
+ }
5600
+ async function searchClaude(query, sessions, folders) {
5601
+ const candidates = await claudeCandidates(query);
5602
+ const matching = candidates === null ? sessions : sessions.filter((session) => candidates.has(session.id));
5603
+ const projectOf = new Map(sessions.map((session) => [session.id, session.projectId]));
5604
+ return readAndMatch(query, matching.slice(0, READ_LIMIT), (sessionId) => claudeMessages(sessionId, folders.get(projectOf.get(sessionId) ?? "")));
5605
+ }
5606
+ async function claudeMessages(sessionId, dir) {
5607
+ return (await getSessionMessages(sessionId, { dir })).flatMap((item) => {
5608
+ if (item.type === "system" || item.parent_tool_use_id !== null) return [];
5609
+ const text = toMessage(item.type, item, null).content.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n");
5610
+ return generatedText.test(text) ? [] : [{
5611
+ author: item.type,
5612
+ text
5613
+ }];
5614
+ });
5615
+ }
5616
+ async function claudeCandidates(query) {
5617
+ const words = searchWords(query).filter((word) => word.length >= 3 && !/["\\]/.test(word));
5618
+ if (words.length === 0) return null;
5619
+ try {
5620
+ const lists = await Promise.all(words.map((word) => sessionsWith(word)));
5621
+ return new Set(lists[0].filter((id) => lists.every((list) => list.includes(id))));
5622
+ } catch (error) {
5623
+ console.warn(`could not narrow Claude sessions with rg: ${error}`);
5624
+ return null;
5625
+ }
5626
+ }
5627
+ async function sessionsWith(word) {
5628
+ return (await run$2("rg", [
5629
+ "--files-with-matches",
5630
+ "--fixed-strings",
5631
+ "--ignore-case",
5632
+ "--no-ignore",
5633
+ "--hidden",
5634
+ "--max-depth",
5635
+ "2",
5636
+ "--glob",
5637
+ "*.jsonl",
5638
+ "--",
5639
+ word,
5640
+ path.join(claudeBaseDir(), "projects")
5641
+ ], {
5642
+ maxBuffer: 16777216,
5643
+ timeout: RG_TIME_LIMIT
5644
+ }).catch((error) => {
5645
+ if (error.code === 1) return { stdout: "" };
5646
+ throw error;
5647
+ })).stdout.split("\n").filter(Boolean).map((file) => path.basename(file, ".jsonl"));
5648
+ }
5649
+ async function searchCodex(query, sessions) {
5650
+ const longest = [...searchWords(query)].sort((a, b) => b.length - a.length)[0];
5651
+ const known = new Map(sessions.map((session) => [session.id, session]));
5652
+ const found = (await codexSearch(longest)).filter((result) => known.has(result.thread.id)).sort((a, b) => b.thread.updatedAt - a.thread.updatedAt);
5653
+ const verified = await readAndMatch(query, found.slice(0, CODEX_READ_LIMIT).map((result) => ({ id: result.thread.id })), codexMessages);
5654
+ const unverified = found.slice(CODEX_READ_LIMIT).flatMap((result) => {
5655
+ const match = matchMessage(query, result.snippet);
5656
+ return match === null ? [] : [{
5657
+ sessionId: result.thread.id,
5658
+ author: "assistant",
5659
+ quality: match.quality,
5660
+ snippet: messageSnippet(result.snippet, match.offset)
5661
+ }];
5662
+ });
5663
+ return [...verified, ...unverified];
5664
+ }
5665
+ async function readAndMatch(query, sessions, read) {
5666
+ return (await Promise.all(sessions.map(async (session) => bestMatch(query, session.id, await read(session.id))))).filter((match) => match !== null);
5667
+ }
5668
+ function bestMatch(query, sessionId, messages) {
5669
+ let best = null;
5670
+ for (const message of messages) {
5671
+ const match = matchMessage(query, message.text);
5672
+ if (match === null) continue;
5673
+ const candidate = {
5674
+ sessionId,
5675
+ author: message.author,
5676
+ quality: match.quality,
5677
+ snippet: messageSnippet(message.text, match.offset)
5678
+ };
5679
+ if (best === null || compareMessages(candidate, best) < 0) best = candidate;
5680
+ }
5681
+ return best;
5682
+ }
5357
5683
  // src/services/proxy.ts
5358
5684
  const proxies = /* @__PURE__ */ new Map();
5359
5685
  const pageScriptPath = "/__offsite/browser.js";
@@ -7008,10 +7334,12 @@ const handlers = {
7008
7334
  "sessions.open": async (params, client) => {
7009
7335
  const session = await findSession(params.sessionId);
7010
7336
  await open$1(client, params.sessionId, params.projectId);
7337
+ if (params.opened && session !== null) recordOpen(params.sessionId, params.query);
7011
7338
  return session;
7012
7339
  },
7013
7340
  "sessions.inactive": (params) => inactiveSessions(params.projectId),
7014
- "sessions.search": (params) => searchSessions(params.query),
7341
+ "sessions.searchable": () => searchableSessions(),
7342
+ "sessions.searchMessages": (params) => searchMessages(params.query),
7015
7343
  "sessions.earlier": (params) => earlier(params.sessionId, params.before),
7016
7344
  "sessions.image": (params) => image(params.sessionId, params.itemId, params.index),
7017
7345
  "sessions.agent": (params) => {