offsite 0.0.65 → 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":
@@ -1948,7 +1949,8 @@ function messageItems(message, parent) {
1948
1949
  case "running": return [];
1949
1950
  case "completed": return [{
1950
1951
  ...base,
1951
- kind: "compaction"
1952
+ kind: "compaction",
1953
+ summary: null
1952
1954
  }];
1953
1955
  case "failed": return [{
1954
1956
  ...errorItem(message.id, message.error.message),
@@ -4820,7 +4822,8 @@ function toItems(messages) {
4820
4822
  items.push({
4821
4823
  ...base,
4822
4824
  id: message.uuid,
4823
- kind: "compaction"
4825
+ kind: "compaction",
4826
+ summary: message.summary
4824
4827
  });
4825
4828
  continue;
4826
4829
  }
@@ -4927,7 +4930,7 @@ function toItems(messages) {
4927
4930
  });
4928
4931
  continue;
4929
4932
  }
4930
- texts.push(userText(block.text));
4933
+ texts.push(userText$1(block.text));
4931
4934
  }
4932
4935
  if (texts.length === 0 && images.length === 0) continue;
4933
4936
  items.push({
@@ -5178,7 +5181,7 @@ function applyResult(item, result) {
5178
5181
  default: return item;
5179
5182
  }
5180
5183
  }
5181
- function userText(value) {
5184
+ function userText$1(value) {
5182
5185
  const command = /<command-name>(.*?)<\/command-name>/s.exec(value);
5183
5186
  if (command !== null) {
5184
5187
  const args = /<command-args>(.*?)<\/command-args>/s.exec(value);
@@ -5286,12 +5289,19 @@ function parseLine(line) {
5286
5289
  function mainMessages(disk, entries) {
5287
5290
  const times = messageTimes(entries);
5288
5291
  const outputs = toolOutputs(entries);
5292
+ const summaries = new Set(entries.flatMap((entry) => entry.isCompactSummary === true && typeof entry.uuid === "string" ? [entry.uuid] : []));
5289
5293
  return disk.flatMap((item) => {
5290
5294
  if (item.type === "system" || item.parent_tool_use_id !== null) return [];
5291
5295
  const message = toMessage(item.type, {
5292
5296
  ...item,
5293
5297
  tool_use_result: outputs.get(item.uuid)
5294
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
+ }];
5295
5305
  return [{
5296
5306
  ...message,
5297
5307
  content: message.content.map((block) => block.type === "image" ? {
@@ -5349,11 +5359,7 @@ async function earlier(sessionId, before) {
5349
5359
  const index = session.pages.findIndex((page) => page.before === before);
5350
5360
  if (index < 0) throw new Error(`no messages before entry ${before}`);
5351
5361
  return {
5352
- items: toItems([...mainMessages(session.pages[index].disk, session.entries), {
5353
- type: "compaction",
5354
- uuid: `compacted:${before}`,
5355
- time: null
5356
- }]),
5362
+ items: toItems(mainMessages(session.pages[index].disk, session.entries)),
5357
5363
  before: session.pages[index + 1]?.before ?? null
5358
5364
  };
5359
5365
  }
@@ -5637,7 +5643,8 @@ var Run = class {
5637
5643
  this.add({
5638
5644
  type: "compaction",
5639
5645
  uuid: message.uuid,
5640
- time: Date.now()
5646
+ time: Date.now(),
5647
+ summary: null
5641
5648
  });
5642
5649
  return;
5643
5650
  }
@@ -5851,141 +5858,6 @@ async function isLocalHost(hostname) {
5851
5858
  if (isIP(hostname) !== 0) return isPrivate(hostname);
5852
5859
  return (await dns.promises.lookup(hostname, { all: true })).some((item) => isPrivate(item.address));
5853
5860
  }
5854
- // src/services/message-search.ts
5855
- const READ_LIMIT = 30;
5856
- const CODEX_READ_LIMIT = 10;
5857
- const RG_TIME_LIMIT = 5e3;
5858
- 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)/;
5859
- const run$2 = promisify(execFile);
5860
- async function searchMessages(query) {
5861
- if (!hasSearchableWord(query)) return {
5862
- matches: [],
5863
- failed: []
5864
- };
5865
- const paths = new Map(projects().map((project) => [project.path, project.id]));
5866
- const folders = new Map(projects().map((project) => [project.id, project.path]));
5867
- const results = await Promise.all([
5868
- "claude",
5869
- "codex",
5870
- "opencode"
5871
- ].filter((provider) => readConfig().enabledProviders[provider]).map(async (provider) => {
5872
- try {
5873
- return {
5874
- provider,
5875
- matches: await timeLimit(search(provider, query, paths, folders), `${providerName(provider)} did not search in time`),
5876
- failed: false
5877
- };
5878
- } catch (error) {
5879
- console.warn(`could not search ${providerName(provider)} messages: ${error}`);
5880
- return {
5881
- provider,
5882
- matches: [],
5883
- failed: true
5884
- };
5885
- }
5886
- }));
5887
- return {
5888
- matches: results.flatMap((result) => result.matches),
5889
- failed: results.filter((result) => result.failed).map((result) => result.provider)
5890
- };
5891
- }
5892
- async function search(provider, query, paths, folders) {
5893
- const found = await find(provider, paths);
5894
- if (found === null) return [];
5895
- const sessions = newestFirst(found);
5896
- switch (provider) {
5897
- case "claude": return searchClaude(query, sessions, folders);
5898
- case "codex": return searchCodex(query, sessions);
5899
- case "opencode": return readAndMatch(query, sessions.slice(0, READ_LIMIT), openCodeMessages);
5900
- }
5901
- }
5902
- function newestFirst(sessions) {
5903
- return [...sessions].sort((a, b) => b.updatedAt - a.updatedAt);
5904
- }
5905
- async function searchClaude(query, sessions, folders) {
5906
- const candidates = await claudeCandidates(query);
5907
- const matching = candidates === null ? sessions : sessions.filter((session) => candidates.has(session.id));
5908
- const projectOf = new Map(sessions.map((session) => [session.id, session.projectId]));
5909
- return readAndMatch(query, matching.slice(0, READ_LIMIT), (sessionId) => claudeMessages(sessionId, folders.get(projectOf.get(sessionId) ?? "")));
5910
- }
5911
- async function claudeMessages(sessionId, dir) {
5912
- return (await getSessionMessages(sessionId, { dir })).flatMap((item) => {
5913
- if (item.type === "system" || item.parent_tool_use_id !== null) return [];
5914
- const text = toMessage(item.type, item, null).content.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n");
5915
- return generatedText.test(text) ? [] : [{
5916
- author: item.type,
5917
- text
5918
- }];
5919
- });
5920
- }
5921
- async function claudeCandidates(query) {
5922
- const phrase = exactPhrase(query);
5923
- const words = phrase !== null ? [JSON.stringify(phrase).slice(1, -1)] : searchWords(query).filter((word) => word.length >= 3 && !/["\\]/.test(word));
5924
- if (words.length === 0) return null;
5925
- try {
5926
- const lists = await Promise.all(words.map((word) => sessionsWith(word)));
5927
- return new Set(lists[0].filter((id) => lists.every((list) => list.includes(id))));
5928
- } catch (error) {
5929
- console.warn(`could not narrow Claude sessions with rg: ${error}`);
5930
- return null;
5931
- }
5932
- }
5933
- async function sessionsWith(word) {
5934
- return (await run$2("rg", [
5935
- "--files-with-matches",
5936
- "--fixed-strings",
5937
- "--ignore-case",
5938
- "--no-ignore",
5939
- "--hidden",
5940
- "--max-depth",
5941
- "2",
5942
- "--glob",
5943
- "*.jsonl",
5944
- "--",
5945
- word,
5946
- path.join(claudeBaseDir(), "projects")
5947
- ], {
5948
- maxBuffer: 16777216,
5949
- timeout: RG_TIME_LIMIT
5950
- }).catch((error) => {
5951
- if (error.code === 1) return { stdout: "" };
5952
- throw error;
5953
- })).stdout.split("\n").filter(Boolean).map((file) => path.basename(file, ".jsonl"));
5954
- }
5955
- async function searchCodex(query, sessions) {
5956
- const longest = exactPhrase(query) ?? [...searchWords(query)].sort((a, b) => b.length - a.length)[0];
5957
- const known = new Map(sessions.map((session) => [session.id, session]));
5958
- const found = (await codexSearch(longest)).filter((result) => known.has(result.thread.id)).sort((a, b) => b.thread.updatedAt - a.thread.updatedAt);
5959
- const verified = await readAndMatch(query, found.slice(0, CODEX_READ_LIMIT).map((result) => ({ id: result.thread.id })), codexMessages);
5960
- const unverified = found.slice(CODEX_READ_LIMIT).flatMap((result) => {
5961
- const match = matchMessage(query, result.snippet);
5962
- return match === null ? [] : [{
5963
- sessionId: result.thread.id,
5964
- author: "assistant",
5965
- quality: match.quality,
5966
- snippet: messageSnippet(result.snippet, match.offset)
5967
- }];
5968
- });
5969
- return [...verified, ...unverified];
5970
- }
5971
- async function readAndMatch(query, sessions, read) {
5972
- return (await Promise.all(sessions.map(async (session) => bestMatch(query, session.id, await read(session.id))))).filter((match) => match !== null);
5973
- }
5974
- function bestMatch(query, sessionId, messages) {
5975
- let best = null;
5976
- for (const message of messages) {
5977
- const match = matchMessage(query, message.text);
5978
- if (match === null) continue;
5979
- const candidate = {
5980
- sessionId,
5981
- author: message.author,
5982
- quality: match.quality,
5983
- snippet: messageSnippet(message.text, match.offset)
5984
- };
5985
- if (best === null || compareMessages(candidate, best) < 0) best = candidate;
5986
- }
5987
- return best;
5988
- }
5989
5861
  // src/services/runs.ts
5990
5862
  const locks = /* @__PURE__ */ new Map();
5991
5863
  function stopAll() {
@@ -6113,7 +5985,7 @@ async function runFor(sessionId, cwd, settings) {
6113
5985
  sessionId,
6114
5986
  cwd,
6115
5987
  resume: true,
6116
- resumeAt: null,
5988
+ resumeAt: run?.provider === "claude" && run.messages.length === 0 ? run.resumeAt : null,
6117
5989
  settings
6118
5990
  });
6119
5991
  case "codex": return CodexRun.resume(sessionId, cwd, settings);
@@ -6138,6 +6010,257 @@ async function changedElsewhere(run, cwd) {
6138
6010
  const last = (await getSessionMessages(run.sessionId, { dir: cwd })).at(-1)?.uuid;
6139
6011
  return last !== void 0 && !run.messages.some((item) => item.uuid === last);
6140
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
+ }
6141
6264
  // src/services/session-title.ts
6142
6265
  const CONTEXT_LIMIT = 8e3;
6143
6266
  const MESSAGE_LIMIT = 2e3;
@@ -7533,6 +7656,7 @@ const handlers = {
7533
7656
  "sessions.searchable": () => searchableSessions(),
7534
7657
  "sessions.searchMessages": (params) => searchMessages(params.query),
7535
7658
  "sessions.earlier": (params) => earlier(params.sessionId, params.before),
7659
+ "sessions.export": (params) => exportSession(params.sessionId, params.projectId),
7536
7660
  "sessions.image": (params) => image(params.sessionId, params.itemId, params.index),
7537
7661
  "sessions.agent": (params) => {
7538
7662
  switch (providerOf(params.sessionId)) {