offsite 0.0.64 → 0.0.66

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.
@@ -763,7 +763,7 @@ function toItems$1(turnId, item, parent, time) {
763
763
  case "userMessage": return [{
764
764
  ...base,
765
765
  kind: "user",
766
- text: item.content.flatMap(userText$1).join("\n").trim(),
766
+ text: item.content.flatMap(userText$2).join("\n").trim(),
767
767
  images: item.content.flatMap(userImages),
768
768
  format: "text"
769
769
  }];
@@ -861,7 +861,8 @@ function toItems$1(turnId, item, parent, time) {
861
861
  }];
862
862
  case "contextCompaction": return [{
863
863
  ...base,
864
- kind: "compaction"
864
+ kind: "compaction",
865
+ summary: null
865
866
  }];
866
867
  case "hookPrompt":
867
868
  case "functionCallOutput":
@@ -872,7 +873,7 @@ function toItems$1(turnId, item, parent, time) {
872
873
  case "exitedReviewMode": return [];
873
874
  }
874
875
  }
875
- function userText$1(input) {
876
+ function userText$2(input) {
876
877
  switch (input.type) {
877
878
  case "text": return [input.text.replaceAll(/\[Image: source: [^\]]+\]/g, "")];
878
879
  case "skill":
@@ -1048,7 +1049,8 @@ function configInfo() {
1048
1049
  defaults: defaults$1,
1049
1050
  platform: platform(),
1050
1051
  defaultShell: defaultShell(),
1051
- defaultClaudeBaseDir: path.join(os.homedir(), ".claude")
1052
+ defaultClaudeBaseDir: path.join(os.homedir(), ".claude"),
1053
+ homeDir: os.homedir()
1052
1054
  };
1053
1055
  }
1054
1056
  function readConfig() {
@@ -1947,7 +1949,8 @@ function messageItems(message, parent) {
1947
1949
  case "running": return [];
1948
1950
  case "completed": return [{
1949
1951
  ...base,
1950
- kind: "compaction"
1952
+ kind: "compaction",
1953
+ summary: null
1951
1954
  }];
1952
1955
  case "failed": return [{
1953
1956
  ...errorItem(message.id, message.error.message),
@@ -4819,7 +4822,8 @@ function toItems(messages) {
4819
4822
  items.push({
4820
4823
  ...base,
4821
4824
  id: message.uuid,
4822
- kind: "compaction"
4825
+ kind: "compaction",
4826
+ summary: message.summary
4823
4827
  });
4824
4828
  continue;
4825
4829
  }
@@ -4926,7 +4930,7 @@ function toItems(messages) {
4926
4930
  });
4927
4931
  continue;
4928
4932
  }
4929
- texts.push(userText(block.text));
4933
+ texts.push(userText$1(block.text));
4930
4934
  }
4931
4935
  if (texts.length === 0 && images.length === 0) continue;
4932
4936
  items.push({
@@ -5177,7 +5181,7 @@ function applyResult(item, result) {
5177
5181
  default: return item;
5178
5182
  }
5179
5183
  }
5180
- function userText(value) {
5184
+ function userText$1(value) {
5181
5185
  const command = /<command-name>(.*?)<\/command-name>/s.exec(value);
5182
5186
  if (command !== null) {
5183
5187
  const args = /<command-args>(.*?)<\/command-args>/s.exec(value);
@@ -5285,12 +5289,19 @@ function parseLine(line) {
5285
5289
  function mainMessages(disk, entries) {
5286
5290
  const times = messageTimes(entries);
5287
5291
  const outputs = toolOutputs(entries);
5292
+ const summaries = new Set(entries.flatMap((entry) => entry.isCompactSummary === true && typeof entry.uuid === "string" ? [entry.uuid] : []));
5288
5293
  return disk.flatMap((item) => {
5289
5294
  if (item.type === "system" || item.parent_tool_use_id !== null) return [];
5290
5295
  const message = toMessage(item.type, {
5291
5296
  ...item,
5292
5297
  tool_use_result: outputs.get(item.uuid)
5293
5298
  }, times.get(item.uuid) ?? null);
5299
+ if (summaries.has(item.uuid)) return [{
5300
+ type: "compaction",
5301
+ uuid: item.uuid,
5302
+ time: message.time,
5303
+ summary: message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n").replace(/<system-reminder>[\s\S]*?<\/system-reminder>\s*/g, "")
5304
+ }];
5294
5305
  return [{
5295
5306
  ...message,
5296
5307
  content: message.content.map((block) => block.type === "image" ? {
@@ -5348,11 +5359,7 @@ async function earlier(sessionId, before) {
5348
5359
  const index = session.pages.findIndex((page) => page.before === before);
5349
5360
  if (index < 0) throw new Error(`no messages before entry ${before}`);
5350
5361
  return {
5351
- items: toItems([...mainMessages(session.pages[index].disk, session.entries), {
5352
- type: "compaction",
5353
- uuid: `compacted:${before}`,
5354
- time: null
5355
- }]),
5362
+ items: toItems(mainMessages(session.pages[index].disk, session.entries)),
5356
5363
  before: session.pages[index + 1]?.before ?? null
5357
5364
  };
5358
5365
  }
@@ -5636,7 +5643,8 @@ var Run = class {
5636
5643
  this.add({
5637
5644
  type: "compaction",
5638
5645
  uuid: message.uuid,
5639
- time: Date.now()
5646
+ time: Date.now(),
5647
+ summary: null
5640
5648
  });
5641
5649
  return;
5642
5650
  }
@@ -5850,141 +5858,6 @@ async function isLocalHost(hostname) {
5850
5858
  if (isIP(hostname) !== 0) return isPrivate(hostname);
5851
5859
  return (await dns.promises.lookup(hostname, { all: true })).some((item) => isPrivate(item.address));
5852
5860
  }
5853
- // src/services/message-search.ts
5854
- const READ_LIMIT = 30;
5855
- const CODEX_READ_LIMIT = 10;
5856
- const RG_TIME_LIMIT = 5e3;
5857
- 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)/;
5858
- const run$2 = promisify(execFile);
5859
- async function searchMessages(query) {
5860
- if (!hasSearchableWord(query)) return {
5861
- matches: [],
5862
- failed: []
5863
- };
5864
- const paths = new Map(projects().map((project) => [project.path, project.id]));
5865
- const folders = new Map(projects().map((project) => [project.id, project.path]));
5866
- const results = await Promise.all([
5867
- "claude",
5868
- "codex",
5869
- "opencode"
5870
- ].filter((provider) => readConfig().enabledProviders[provider]).map(async (provider) => {
5871
- try {
5872
- return {
5873
- provider,
5874
- matches: await timeLimit(search(provider, query, paths, folders), `${providerName(provider)} did not search in time`),
5875
- failed: false
5876
- };
5877
- } catch (error) {
5878
- console.warn(`could not search ${providerName(provider)} messages: ${error}`);
5879
- return {
5880
- provider,
5881
- matches: [],
5882
- failed: true
5883
- };
5884
- }
5885
- }));
5886
- return {
5887
- matches: results.flatMap((result) => result.matches),
5888
- failed: results.filter((result) => result.failed).map((result) => result.provider)
5889
- };
5890
- }
5891
- async function search(provider, query, paths, folders) {
5892
- const found = await find(provider, paths);
5893
- if (found === null) return [];
5894
- const sessions = newestFirst(found);
5895
- switch (provider) {
5896
- case "claude": return searchClaude(query, sessions, folders);
5897
- case "codex": return searchCodex(query, sessions);
5898
- case "opencode": return readAndMatch(query, sessions.slice(0, READ_LIMIT), openCodeMessages);
5899
- }
5900
- }
5901
- function newestFirst(sessions) {
5902
- return [...sessions].sort((a, b) => b.updatedAt - a.updatedAt);
5903
- }
5904
- async function searchClaude(query, sessions, folders) {
5905
- const candidates = await claudeCandidates(query);
5906
- const matching = candidates === null ? sessions : sessions.filter((session) => candidates.has(session.id));
5907
- const projectOf = new Map(sessions.map((session) => [session.id, session.projectId]));
5908
- return readAndMatch(query, matching.slice(0, READ_LIMIT), (sessionId) => claudeMessages(sessionId, folders.get(projectOf.get(sessionId) ?? "")));
5909
- }
5910
- async function claudeMessages(sessionId, dir) {
5911
- return (await getSessionMessages(sessionId, { dir })).flatMap((item) => {
5912
- if (item.type === "system" || item.parent_tool_use_id !== null) return [];
5913
- const text = toMessage(item.type, item, null).content.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n");
5914
- return generatedText.test(text) ? [] : [{
5915
- author: item.type,
5916
- text
5917
- }];
5918
- });
5919
- }
5920
- async function claudeCandidates(query) {
5921
- const phrase = exactPhrase(query);
5922
- const words = phrase !== null ? [JSON.stringify(phrase).slice(1, -1)] : searchWords(query).filter((word) => word.length >= 3 && !/["\\]/.test(word));
5923
- if (words.length === 0) return null;
5924
- try {
5925
- const lists = await Promise.all(words.map((word) => sessionsWith(word)));
5926
- return new Set(lists[0].filter((id) => lists.every((list) => list.includes(id))));
5927
- } catch (error) {
5928
- console.warn(`could not narrow Claude sessions with rg: ${error}`);
5929
- return null;
5930
- }
5931
- }
5932
- async function sessionsWith(word) {
5933
- return (await run$2("rg", [
5934
- "--files-with-matches",
5935
- "--fixed-strings",
5936
- "--ignore-case",
5937
- "--no-ignore",
5938
- "--hidden",
5939
- "--max-depth",
5940
- "2",
5941
- "--glob",
5942
- "*.jsonl",
5943
- "--",
5944
- word,
5945
- path.join(claudeBaseDir(), "projects")
5946
- ], {
5947
- maxBuffer: 16777216,
5948
- timeout: RG_TIME_LIMIT
5949
- }).catch((error) => {
5950
- if (error.code === 1) return { stdout: "" };
5951
- throw error;
5952
- })).stdout.split("\n").filter(Boolean).map((file) => path.basename(file, ".jsonl"));
5953
- }
5954
- async function searchCodex(query, sessions) {
5955
- const longest = exactPhrase(query) ?? [...searchWords(query)].sort((a, b) => b.length - a.length)[0];
5956
- const known = new Map(sessions.map((session) => [session.id, session]));
5957
- const found = (await codexSearch(longest)).filter((result) => known.has(result.thread.id)).sort((a, b) => b.thread.updatedAt - a.thread.updatedAt);
5958
- const verified = await readAndMatch(query, found.slice(0, CODEX_READ_LIMIT).map((result) => ({ id: result.thread.id })), codexMessages);
5959
- const unverified = found.slice(CODEX_READ_LIMIT).flatMap((result) => {
5960
- const match = matchMessage(query, result.snippet);
5961
- return match === null ? [] : [{
5962
- sessionId: result.thread.id,
5963
- author: "assistant",
5964
- quality: match.quality,
5965
- snippet: messageSnippet(result.snippet, match.offset)
5966
- }];
5967
- });
5968
- return [...verified, ...unverified];
5969
- }
5970
- async function readAndMatch(query, sessions, read) {
5971
- return (await Promise.all(sessions.map(async (session) => bestMatch(query, session.id, await read(session.id))))).filter((match) => match !== null);
5972
- }
5973
- function bestMatch(query, sessionId, messages) {
5974
- let best = null;
5975
- for (const message of messages) {
5976
- const match = matchMessage(query, message.text);
5977
- if (match === null) continue;
5978
- const candidate = {
5979
- sessionId,
5980
- author: message.author,
5981
- quality: match.quality,
5982
- snippet: messageSnippet(message.text, match.offset)
5983
- };
5984
- if (best === null || compareMessages(candidate, best) < 0) best = candidate;
5985
- }
5986
- return best;
5987
- }
5988
5861
  // src/services/runs.ts
5989
5862
  const locks = /* @__PURE__ */ new Map();
5990
5863
  function stopAll() {
@@ -6112,7 +5985,7 @@ async function runFor(sessionId, cwd, settings) {
6112
5985
  sessionId,
6113
5986
  cwd,
6114
5987
  resume: true,
6115
- resumeAt: null,
5988
+ resumeAt: run?.provider === "claude" && run.messages.length === 0 ? run.resumeAt : null,
6116
5989
  settings
6117
5990
  });
6118
5991
  case "codex": return CodexRun.resume(sessionId, cwd, settings);
@@ -6137,6 +6010,257 @@ async function changedElsewhere(run, cwd) {
6137
6010
  const last = (await getSessionMessages(run.sessionId, { dir: cwd })).at(-1)?.uuid;
6138
6011
  return last !== void 0 && !run.messages.some((item) => item.uuid === last);
6139
6012
  }
6013
+ // src/services/export.ts
6014
+ async function exportSession(sessionId, projectId) {
6015
+ const [session, event] = await Promise.all([findSession(sessionId), sessionHistory(sessionId, projectPath(projectId))]);
6016
+ if (session === null) throw new Error(`session ${sessionId} not found`);
6017
+ if (event.type !== "history") throw new Error(`unexpected ${event.type} event for ${sessionId}`);
6018
+ let items = event.items;
6019
+ let before = event.before;
6020
+ while (before !== null) {
6021
+ const page = await earlier(sessionId, before);
6022
+ items = [...page.items, ...items];
6023
+ before = page.before;
6024
+ }
6025
+ return {
6026
+ messages: markdown(session, items, 0),
6027
+ steps: markdown(session, items, 1),
6028
+ details: markdown(session, items, 2),
6029
+ full: markdown(session, items, 3)
6030
+ };
6031
+ }
6032
+ function markdown(session, items, level) {
6033
+ const sections = [];
6034
+ let assistant = [];
6035
+ for (const item of items) {
6036
+ if (item.parent !== null) continue;
6037
+ if (item.kind === "user") {
6038
+ if (assistant.length > 0) {
6039
+ sections.push(`## Assistant\n\n${assistant.join("\n\n")}`);
6040
+ assistant = [];
6041
+ }
6042
+ sections.push(`## User\n\n${userText(item)}`);
6043
+ continue;
6044
+ }
6045
+ const text = itemText(item, level);
6046
+ if (text !== null) assistant.push(text);
6047
+ }
6048
+ if (assistant.length > 0) sections.push(`## Assistant\n\n${assistant.join("\n\n")}`);
6049
+ return `${[[
6050
+ `# ${session.title}`,
6051
+ "",
6052
+ `**Session ID:** ${session.id}`,
6053
+ `**Provider:** ${providerName(session.provider)}`,
6054
+ ...session.branch === null ? [] : [`**Branch:** ${session.branch}`],
6055
+ `**Updated:** ${new Date(session.updatedAt).toLocaleString()}`
6056
+ ].join("\n"), ...sections].join("\n\n---\n\n")}\n`;
6057
+ }
6058
+ function userText(item) {
6059
+ const images = item.images.length === 0 ? "" : `\n\n_[${item.images.length} image${item.images.length === 1 ? "" : "s"} attached]_`;
6060
+ switch (item.format) {
6061
+ case "text": return `${item.text}${images}`;
6062
+ case "command":
6063
+ case "output": return `${fenced("", item.text)}${images}`;
6064
+ }
6065
+ }
6066
+ function itemText(item, level) {
6067
+ switch (item.kind) {
6068
+ case "assistant": return item.text;
6069
+ case "plan": return `**Plan:**\n\n${item.plan}`;
6070
+ case "question": return item.questions.map((question) => `**Question:** ${question.question}\n\n**Answer:** ${item.answers?.[question.question] ?? "_No answer_"}`).join("\n\n");
6071
+ case "error": return `**Error:** ${item.text}`;
6072
+ case "reasoning": return level < 2 ? null : `_Thinking:_\n\n${item.text}`;
6073
+ case "command":
6074
+ if (level < 1) return null;
6075
+ if (level < 2) return `**Shell:** ${item.description === "" ? `\`${item.command.split("\n")[0]}\`` : item.description}${statusText(item.status)}`;
6076
+ return `**Shell**${item.description === "" ? "" : `: ${item.description}`}${statusText(item.status)}\n\n${fenced("sh", `$ ${item.command}${level < 3 || item.output === "" ? "" : `\n${item.output}`}`)}`;
6077
+ case "file_change": {
6078
+ if (level < 1) return null;
6079
+ const title = `**Edit:** \`${item.path}\`${statusText(item.status)}`;
6080
+ if (level < 2) return title;
6081
+ return `${title}\n\n${fenced("diff", item.diffs.map((diff) => [...lines(diff.before).map((line) => `-${line}`), ...lines(diff.after).map((line) => `+${line}`)].join("\n")).join("\n"))}${level < 3 || item.output === "" ? "" : `\n\n${fenced("", item.output)}`}`;
6082
+ }
6083
+ case "web_search": {
6084
+ if (level < 1) return null;
6085
+ const title = `**Web search:** ${item.query ?? item.url ?? ""}${statusText(item.status)}`;
6086
+ if (level < 2) return title;
6087
+ return [title, ...item.results.map((result) => `- [${result.title}](${result.url})`)].join("\n");
6088
+ }
6089
+ case "skill": {
6090
+ if (level < 1) return null;
6091
+ const title = `**Skill:** ${item.skill}${statusText(item.status)}`;
6092
+ return level < 3 || item.output === "" ? title : `${title}\n\n${fenced("", item.output)}`;
6093
+ }
6094
+ case "tool": {
6095
+ if (level < 1) return null;
6096
+ const title = `**Tool:** ${item.name}${statusText(item.status)}`;
6097
+ if (level < 2) return title;
6098
+ return `${title}\n\n**Input:**\n\n${fenced("json", JSON.stringify(item.input, null, 2))}${level < 3 || item.output === "" ? "" : `\n\n**Output:**\n\n${fenced("", item.output)}`}`;
6099
+ }
6100
+ case "subagent": return level < 1 ? null : `**Subagent (${item.agent}):** ${item.description}${statusText(item.status)}`;
6101
+ case "agent_message": return level < 1 ? null : `**Message${item.target === null ? "" : ` to ${item.target}`}:** ${item.summary}${statusText(item.status)}`;
6102
+ case "notice": return level < 1 ? null : `_${item.text}_`;
6103
+ case "compaction":
6104
+ if (level < 1) return null;
6105
+ return level < 2 || item.summary === null ? "_Conversation compacted_" : `_Conversation compacted_\n\n${item.summary}`;
6106
+ case "todo_list": return level < 2 ? null : [
6107
+ "**Todos:**",
6108
+ "",
6109
+ ...item.steps.map((step) => `- [${step.status === "completed" ? "x" : " "}] ${step.text}`)
6110
+ ].join("\n");
6111
+ }
6112
+ }
6113
+ function statusText(status) {
6114
+ switch (status) {
6115
+ case "completed": return "";
6116
+ case "running": return " _(running)_";
6117
+ case "failed": return " _(failed)_";
6118
+ case "stopped": return " _(stopped)_";
6119
+ }
6120
+ }
6121
+ function lines(text) {
6122
+ return text === "" ? [] : text.split("\n");
6123
+ }
6124
+ function fenced(language, text) {
6125
+ const longest = Math.max(0, ...(text.match(/`+/g) ?? []).map((run) => run.length));
6126
+ const fence = "`".repeat(Math.max(3, longest + 1));
6127
+ return `${fence}${language}\n${text}\n${fence}`;
6128
+ }
6129
+ // src/services/message-search.ts
6130
+ const READ_LIMIT = 30;
6131
+ const CODEX_READ_LIMIT = 10;
6132
+ const RG_TIME_LIMIT = 5e3;
6133
+ 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)/;
6134
+ const run$2 = promisify(execFile);
6135
+ async function searchMessages(query) {
6136
+ if (!hasSearchableWord(query)) return {
6137
+ matches: [],
6138
+ failed: []
6139
+ };
6140
+ const paths = new Map(projects().map((project) => [project.path, project.id]));
6141
+ const folders = new Map(projects().map((project) => [project.id, project.path]));
6142
+ const results = await Promise.all([
6143
+ "claude",
6144
+ "codex",
6145
+ "opencode"
6146
+ ].filter((provider) => readConfig().enabledProviders[provider]).map(async (provider) => {
6147
+ try {
6148
+ return {
6149
+ provider,
6150
+ matches: await timeLimit(search(provider, query, paths, folders), `${providerName(provider)} did not search in time`),
6151
+ failed: false
6152
+ };
6153
+ } catch (error) {
6154
+ console.warn(`could not search ${providerName(provider)} messages: ${error}`);
6155
+ return {
6156
+ provider,
6157
+ matches: [],
6158
+ failed: true
6159
+ };
6160
+ }
6161
+ }));
6162
+ return {
6163
+ matches: results.flatMap((result) => result.matches),
6164
+ failed: results.filter((result) => result.failed).map((result) => result.provider)
6165
+ };
6166
+ }
6167
+ async function search(provider, query, paths, folders) {
6168
+ const found = await find(provider, paths);
6169
+ if (found === null) return [];
6170
+ const sessions = newestFirst(found);
6171
+ switch (provider) {
6172
+ case "claude": return searchClaude(query, sessions, folders);
6173
+ case "codex": return searchCodex(query, sessions);
6174
+ case "opencode": return readAndMatch(query, sessions.slice(0, READ_LIMIT), openCodeMessages);
6175
+ }
6176
+ }
6177
+ function newestFirst(sessions) {
6178
+ return [...sessions].sort((a, b) => b.updatedAt - a.updatedAt);
6179
+ }
6180
+ async function searchClaude(query, sessions, folders) {
6181
+ const candidates = await claudeCandidates(query);
6182
+ const matching = candidates === null ? sessions : sessions.filter((session) => candidates.has(session.id));
6183
+ const projectOf = new Map(sessions.map((session) => [session.id, session.projectId]));
6184
+ return readAndMatch(query, matching.slice(0, READ_LIMIT), (sessionId) => claudeMessages(sessionId, folders.get(projectOf.get(sessionId) ?? "")));
6185
+ }
6186
+ async function claudeMessages(sessionId, dir) {
6187
+ return (await getSessionMessages(sessionId, { dir })).flatMap((item) => {
6188
+ if (item.type === "system" || item.parent_tool_use_id !== null) return [];
6189
+ const text = toMessage(item.type, item, null).content.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n");
6190
+ return generatedText.test(text) ? [] : [{
6191
+ author: item.type,
6192
+ text
6193
+ }];
6194
+ });
6195
+ }
6196
+ async function claudeCandidates(query) {
6197
+ const phrase = exactPhrase(query);
6198
+ const words = phrase !== null ? [JSON.stringify(phrase).slice(1, -1)] : searchWords(query).filter((word) => word.length >= 3 && !/["\\]/.test(word));
6199
+ if (words.length === 0) return null;
6200
+ try {
6201
+ const lists = await Promise.all(words.map((word) => sessionsWith(word)));
6202
+ return new Set(lists[0].filter((id) => lists.every((list) => list.includes(id))));
6203
+ } catch (error) {
6204
+ console.warn(`could not narrow Claude sessions with rg: ${error}`);
6205
+ return null;
6206
+ }
6207
+ }
6208
+ async function sessionsWith(word) {
6209
+ return (await run$2("rg", [
6210
+ "--files-with-matches",
6211
+ "--fixed-strings",
6212
+ "--ignore-case",
6213
+ "--no-ignore",
6214
+ "--hidden",
6215
+ "--max-depth",
6216
+ "2",
6217
+ "--glob",
6218
+ "*.jsonl",
6219
+ "--",
6220
+ word,
6221
+ path.join(claudeBaseDir(), "projects")
6222
+ ], {
6223
+ maxBuffer: 16777216,
6224
+ timeout: RG_TIME_LIMIT
6225
+ }).catch((error) => {
6226
+ if (error.code === 1) return { stdout: "" };
6227
+ throw error;
6228
+ })).stdout.split("\n").filter(Boolean).map((file) => path.basename(file, ".jsonl"));
6229
+ }
6230
+ async function searchCodex(query, sessions) {
6231
+ const longest = exactPhrase(query) ?? [...searchWords(query)].sort((a, b) => b.length - a.length)[0];
6232
+ const known = new Map(sessions.map((session) => [session.id, session]));
6233
+ const found = (await codexSearch(longest)).filter((result) => known.has(result.thread.id)).sort((a, b) => b.thread.updatedAt - a.thread.updatedAt);
6234
+ const verified = await readAndMatch(query, found.slice(0, CODEX_READ_LIMIT).map((result) => ({ id: result.thread.id })), codexMessages);
6235
+ const unverified = found.slice(CODEX_READ_LIMIT).flatMap((result) => {
6236
+ const match = matchMessage(query, result.snippet);
6237
+ return match === null ? [] : [{
6238
+ sessionId: result.thread.id,
6239
+ author: "assistant",
6240
+ quality: match.quality,
6241
+ snippet: messageSnippet(result.snippet, match.offset)
6242
+ }];
6243
+ });
6244
+ return [...verified, ...unverified];
6245
+ }
6246
+ async function readAndMatch(query, sessions, read) {
6247
+ return (await Promise.all(sessions.map(async (session) => bestMatch(query, session.id, await read(session.id))))).filter((match) => match !== null);
6248
+ }
6249
+ function bestMatch(query, sessionId, messages) {
6250
+ let best = null;
6251
+ for (const message of messages) {
6252
+ const match = matchMessage(query, message.text);
6253
+ if (match === null) continue;
6254
+ const candidate = {
6255
+ sessionId,
6256
+ author: message.author,
6257
+ quality: match.quality,
6258
+ snippet: messageSnippet(message.text, match.offset)
6259
+ };
6260
+ if (best === null || compareMessages(candidate, best) < 0) best = candidate;
6261
+ }
6262
+ return best;
6263
+ }
6140
6264
  // src/services/session-title.ts
6141
6265
  const CONTEXT_LIMIT = 8e3;
6142
6266
  const MESSAGE_LIMIT = 2e3;
@@ -7532,6 +7656,7 @@ const handlers = {
7532
7656
  "sessions.searchable": () => searchableSessions(),
7533
7657
  "sessions.searchMessages": (params) => searchMessages(params.query),
7534
7658
  "sessions.earlier": (params) => earlier(params.sessionId, params.before),
7659
+ "sessions.export": (params) => exportSession(params.sessionId, params.projectId),
7535
7660
  "sessions.image": (params) => image(params.sessionId, params.itemId, params.index),
7536
7661
  "sessions.agent": (params) => {
7537
7662
  switch (providerOf(params.sessionId)) {