@anuma/sdk 6.25.3 → 6.26.1

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.
@@ -18,7 +18,7 @@ import {
18
18
  taskTypeHeader,
19
19
  validateEndpointOverride,
20
20
  withInternalFlowMarker
21
- } from "./chunk-BPMINZPO.mjs";
21
+ } from "./chunk-LBIKGBXE.mjs";
22
22
  import {
23
23
  BASE_URL,
24
24
  MCP_R2_DOMAIN,
@@ -1512,6 +1512,46 @@ async function assembleDecryptedMessage(message, address) {
1512
1512
  // src/lib/db/chat/types.ts
1513
1513
  import { v7 as uuidv7 } from "uuid";
1514
1514
 
1515
+ // src/lib/chat/fileContext.ts
1516
+ var ATTACHED_FILES_OPEN_TAG = "<attached_files>";
1517
+ var ATTACHED_FILES_CLOSE_TAG = "</attached_files>";
1518
+ var ATTACHED_FILES_PREFIX = `${ATTACHED_FILES_OPEN_TAG}
1519
+ The user attached the following file(s) to this message. Their extracted contents are below; when the user refers to "the attached file", "this file" or "the document", they mean these.`;
1520
+ function isAttachedFilesText(text12) {
1521
+ return typeof text12 === "string" && text12.startsWith(ATTACHED_FILES_PREFIX) && text12.endsWith(ATTACHED_FILES_CLOSE_TAG);
1522
+ }
1523
+ function buildAttachedFilesText(fileContext) {
1524
+ return `${ATTACHED_FILES_PREFIX}
1525
+
1526
+ ${fileContext}
1527
+ ${ATTACHED_FILES_CLOSE_TAG}`;
1528
+ }
1529
+ function attachFileContextToLastUserMessage(messages, fileContext) {
1530
+ if (!fileContext.trim()) return messages;
1531
+ let lastUserIndex = -1;
1532
+ for (let i = messages.length - 1; i >= 0; i--) {
1533
+ if (messages[i].role === "user") {
1534
+ lastUserIndex = i;
1535
+ break;
1536
+ }
1537
+ }
1538
+ if (lastUserIndex === -1) return messages;
1539
+ const message = messages[lastUserIndex];
1540
+ const existing = message.content ?? [];
1541
+ let insertAt = 0;
1542
+ existing.forEach((part, index) => {
1543
+ if (part.type === "text") insertAt = index + 1;
1544
+ });
1545
+ const filePart = {
1546
+ type: "text",
1547
+ text: buildAttachedFilesText(fileContext)
1548
+ };
1549
+ const content = [...existing.slice(0, insertAt), filePart, ...existing.slice(insertAt)];
1550
+ const result = [...messages];
1551
+ result[lastUserIndex] = { ...message, content };
1552
+ return result;
1553
+ }
1554
+
1515
1555
  // src/lib/chat/useChat/strategies/types.ts
1516
1556
  function isChatCompletionResponse(r) {
1517
1557
  if (r.object === "response") return false;
@@ -1633,7 +1673,7 @@ function extractUserMessageFromMessages(messages) {
1633
1673
  const textParts = [];
1634
1674
  const files = [];
1635
1675
  for (const part of lastUserMessage.content) {
1636
- if (part.type === "text" && part.text) {
1676
+ if (part.type === "text" && part.text && !isAttachedFilesText(part.text)) {
1637
1677
  textParts.push(part.text);
1638
1678
  } else if (part.type === "image_url" && part.image_url?.url) {
1639
1679
  files.push({
@@ -6069,7 +6109,7 @@ function extractLastUserText(messages) {
6069
6109
  const content = lastUserMsg.content;
6070
6110
  if (typeof content === "string") return content;
6071
6111
  if (Array.isArray(content)) {
6072
- return content.filter((c) => c?.type === "text").map((c) => c.text ?? "").join(" ");
6112
+ return content.filter((c) => c?.type === "text" && !isAttachedFilesText(c.text)).map((c) => c.text ?? "").join(" ");
6073
6113
  }
6074
6114
  return "";
6075
6115
  }
@@ -14932,7 +14972,7 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
14932
14972
  content: m.content,
14933
14973
  similarity: m.similarity
14934
14974
  }));
14935
- const { consolidateMemory: doConsolidate } = await import("./consolidate-A2X3M47N.mjs");
14975
+ const { consolidateMemory: doConsolidate } = await import("./consolidate-2NLZAEWR.mjs");
14936
14976
  const decision = await doConsolidate(trimmed, candidates, consolidateOptions);
14937
14977
  if (decision.action === "create") return decision.fallbackReason ? null : { create: true };
14938
14978
  if (decision.action === "supersede" && decision.content) {
@@ -19549,14 +19589,37 @@ function createMemoryVaultTool(vaultCtx, options, embeddingOptions, cache) {
19549
19589
  if (typeof process !== "undefined" && typeof process.umask !== "function") {
19550
19590
  process.umask = (_mask) => 18;
19551
19591
  }
19592
+ var MAX_ROWS_PER_SHEET = 2e3;
19593
+ function csvField(value) {
19594
+ return /[",\r\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
19595
+ }
19596
+ function uniqueHeaders(raw, columnCount) {
19597
+ const bases = [];
19598
+ for (let col = 1; col <= columnCount; col++) bases.push(raw[col]?.trim() || `Column${col}`);
19599
+ const taken = new Set(bases);
19600
+ const emitted = /* @__PURE__ */ new Set();
19601
+ return bases.map((base) => {
19602
+ if (!emitted.has(base)) {
19603
+ emitted.add(base);
19604
+ return base;
19605
+ }
19606
+ let n = 2;
19607
+ while (taken.has(`${base}_${n}`)) n++;
19608
+ const name = `${base}_${n}`;
19609
+ taken.add(name);
19610
+ emitted.add(name);
19611
+ return name;
19612
+ });
19613
+ }
19552
19614
  var ExcelProcessor = class {
19553
- constructor() {
19615
+ constructor(options = {}) {
19554
19616
  this.name = "excel";
19555
19617
  this.supportedMimeTypes = [
19556
19618
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
19557
19619
  // .xlsx
19558
19620
  ];
19559
19621
  this.supportedExtensions = [".xlsx"];
19622
+ this.maxRowsPerSheet = options.maxRowsPerSheet ?? MAX_ROWS_PER_SHEET;
19560
19623
  }
19561
19624
  async loadExcelJS() {
19562
19625
  const mod = await import("exceljs");
@@ -19571,31 +19634,51 @@ var ExcelProcessor = class {
19571
19634
  if (workbook.worksheets.length === 0) {
19572
19635
  return null;
19573
19636
  }
19574
- const jsonData = {};
19637
+ let truncated = false;
19638
+ const sections = [];
19575
19639
  for (const worksheet of workbook.worksheets) {
19576
- const rows = [];
19577
- const headerRow = worksheet.getRow(1);
19578
- const headers = [];
19579
- headerRow.eachCell({ includeEmpty: true }, (cell, colNumber) => {
19580
- headers[colNumber] = cell.text || `Column${colNumber}`;
19640
+ const rawHeaders = [];
19641
+ let columnCount = 0;
19642
+ worksheet.getRow(1).eachCell({ includeEmpty: true }, (cell, colNumber) => {
19643
+ rawHeaders[colNumber] = cell.text;
19644
+ columnCount = Math.max(columnCount, colNumber);
19581
19645
  });
19646
+ const rows = [];
19647
+ let totalRows = 0;
19582
19648
  worksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
19583
19649
  if (rowNumber === 1) return;
19584
- const rowData = {};
19650
+ totalRows++;
19651
+ if (rows.length >= this.maxRowsPerSheet) return;
19652
+ const values = [];
19585
19653
  row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
19586
- const key = headers[colNumber] || `Column${colNumber}`;
19587
- rowData[key] = this.resolveCellValue(cell.value);
19654
+ values[colNumber - 1] = String(this.resolveCellValue(cell.value));
19655
+ columnCount = Math.max(columnCount, colNumber);
19588
19656
  });
19589
- rows.push(rowData);
19657
+ rows.push(values);
19590
19658
  });
19591
- jsonData[worksheet.name] = rows;
19659
+ const headers = uniqueHeaders(rawHeaders, columnCount);
19660
+ const lines = [headers, ...rows].map(
19661
+ (values) => headers.map((_, i) => csvField(values[i] ?? "")).join(",")
19662
+ );
19663
+ let section = `## Sheet: ${worksheet.name}
19664
+
19665
+ \`\`\`csv
19666
+ ${lines.join("\n")}
19667
+ \`\`\``;
19668
+ if (totalRows > rows.length) {
19669
+ truncated = true;
19670
+ section += `
19671
+ [truncated: showing the first ${rows.length} of ${totalRows} data rows of sheet "${worksheet.name}"]`;
19672
+ }
19673
+ sections.push(section);
19592
19674
  }
19593
19675
  return {
19594
- extractedText: JSON.stringify(jsonData, null, 2),
19595
- format: "json",
19676
+ extractedText: sections.join("\n\n"),
19677
+ format: "markdown",
19596
19678
  metadata: {
19597
19679
  sheetCount: workbook.worksheets.length,
19598
- sheetNames: workbook.worksheets.map((ws) => ws.name)
19680
+ sheetNames: workbook.worksheets.map((ws) => ws.name),
19681
+ truncated
19599
19682
  }
19600
19683
  };
19601
19684
  } catch (error) {
@@ -19621,6 +19704,9 @@ var ExcelProcessor = class {
19621
19704
  // src/lib/pdf.ts
19622
19705
  var pdfjsModule = null;
19623
19706
  var workerConfigured = false;
19707
+ var RENDER_SCALE = 1.5;
19708
+ var MAX_RENDER_SIDE_PX = 1600;
19709
+ var JPEG_QUALITY = 0.8;
19624
19710
  async function getPdfjs() {
19625
19711
  if (!pdfjsModule) {
19626
19712
  pdfjsModule = await import("pdfjs-dist");
@@ -19631,46 +19717,134 @@ async function getPdfjs() {
19631
19717
  }
19632
19718
  return pdfjsModule;
19633
19719
  }
19634
- async function extractTextFromPdf(pdfDataUrl) {
19720
+ async function withPdfDocument(pdfDataUrl, fn) {
19635
19721
  const pdfjs = await getPdfjs();
19636
19722
  const loadingTask = pdfjs.getDocument(pdfDataUrl);
19637
- const pdf = await loadingTask.promise;
19638
- const textParts = [];
19639
- for (let i = 1; i <= pdf.numPages; i++) {
19640
- const page = await pdf.getPage(i);
19641
- const textContent = await page.getTextContent();
19642
- const pageText = textContent.items.map((item) => "str" in item ? item.str : "").join(" ");
19643
- if (pageText.trim()) {
19644
- textParts.push(pageText);
19645
- }
19723
+ let pdf;
19724
+ try {
19725
+ pdf = await loadingTask.promise;
19726
+ } catch (error) {
19727
+ await loadingTask.destroy().catch(() => void 0);
19728
+ throw error;
19729
+ }
19730
+ try {
19731
+ return await fn(pdf);
19732
+ } finally {
19733
+ await pdf.destroy().catch(() => void 0);
19646
19734
  }
19647
- return textParts.join("\n\n");
19648
19735
  }
19649
- async function convertPdfToImages(pdfDataUrl, maxPages) {
19650
- const images = [];
19651
- const pdfjs = await getPdfjs();
19652
- const loadingTask = pdfjs.getDocument(pdfDataUrl);
19653
- const pdf = await loadingTask.promise;
19654
- const pageLimit = maxPages !== void 0 ? Math.min(maxPages, pdf.numPages) : pdf.numPages;
19655
- for (let i = 1; i <= pageLimit; i++) {
19656
- const page = await pdf.getPage(i);
19657
- const viewport = page.getViewport({ scale: 1.5 });
19658
- const canvas = document.createElement("canvas");
19659
- const context = canvas.getContext("2d");
19660
- if (!context) continue;
19661
- canvas.height = viewport.height;
19662
- canvas.width = viewport.width;
19663
- await page.render({
19664
- canvasContext: context,
19665
- viewport
19666
- }).promise;
19667
- images.push(canvas.toDataURL("image/png"));
19668
- }
19669
- return images;
19736
+ function joinPdfTextItems(items) {
19737
+ let text12 = "";
19738
+ for (const item of items) {
19739
+ if (!("str" in item)) continue;
19740
+ const { str, hasEOL } = item;
19741
+ text12 += str + (hasEOL ? "\n" : " ");
19742
+ }
19743
+ return text12.replace(/[ \t]+\n/g, "\n").trim();
19744
+ }
19745
+ function computeRenderScale(width, height) {
19746
+ const longest = Math.max(width, height);
19747
+ if (!(longest > 0)) return RENDER_SCALE;
19748
+ return Math.min(RENDER_SCALE, MAX_RENDER_SIDE_PX / longest);
19749
+ }
19750
+ async function extractPdfPageTexts(pdfDataUrl) {
19751
+ return withPdfDocument(pdfDataUrl, async (pdf) => {
19752
+ const pageTexts = [];
19753
+ for (let i = 1; i <= pdf.numPages; i++) {
19754
+ const page = await pdf.getPage(i);
19755
+ const textContent = await page.getTextContent();
19756
+ pageTexts.push(joinPdfTextItems(textContent.items));
19757
+ }
19758
+ return pageTexts;
19759
+ });
19760
+ }
19761
+ async function extractTextFromPdf(pdfDataUrl) {
19762
+ const pageTexts = await extractPdfPageTexts(pdfDataUrl);
19763
+ return pageTexts.filter((text12) => text12.trim()).join("\n\n");
19764
+ }
19765
+ async function renderPdfPages(pdfDataUrl, maxPages, pageNumbers) {
19766
+ return withPdfDocument(pdfDataUrl, async (pdf) => {
19767
+ const requested = pageNumbers?.filter((n) => n >= 1 && n <= pdf.numPages) ?? Array.from(
19768
+ { length: maxPages !== void 0 ? Math.min(maxPages, pdf.numPages) : pdf.numPages },
19769
+ (_, i) => i + 1
19770
+ );
19771
+ const pages = [];
19772
+ for (const pageNumber of requested) {
19773
+ const page = await pdf.getPage(pageNumber);
19774
+ const base = page.getViewport({ scale: 1 });
19775
+ const viewport = page.getViewport({ scale: computeRenderScale(base.width, base.height) });
19776
+ const canvas = document.createElement("canvas");
19777
+ const context = canvas.getContext("2d");
19778
+ if (!context) continue;
19779
+ canvas.height = Math.floor(viewport.height);
19780
+ canvas.width = Math.floor(viewport.width);
19781
+ context.fillStyle = "#ffffff";
19782
+ context.fillRect(0, 0, canvas.width, canvas.height);
19783
+ await page.render({
19784
+ canvasContext: context,
19785
+ viewport,
19786
+ background: "#ffffff"
19787
+ }).promise;
19788
+ pages.push({ pageNumber, dataUrl: canvas.toDataURL("image/jpeg", JPEG_QUALITY) });
19789
+ canvas.width = 0;
19790
+ canvas.height = 0;
19791
+ }
19792
+ return { pages, pageCount: pdf.numPages };
19793
+ });
19794
+ }
19795
+ async function convertPdfToImages(pdfDataUrl, maxPages, pageNumbers) {
19796
+ const { pages } = await renderPdfPages(pdfDataUrl, maxPages, pageNumbers);
19797
+ return pages.map((p) => p.dataUrl);
19670
19798
  }
19671
19799
 
19672
19800
  // src/lib/processors/PdfProcessor.ts
19673
19801
  var MAX_IMAGE_PAGES = 20;
19802
+ var MIN_PAGE_TEXT_CHARS = 20;
19803
+ function selectPdfImagePages(pageTexts, maxImagePages = MAX_IMAGE_PAGES) {
19804
+ const needImage = [];
19805
+ pageTexts.forEach((text12, index) => {
19806
+ if (text12.trim().length < MIN_PAGE_TEXT_CHARS) needImage.push(index + 1);
19807
+ });
19808
+ const limit = Math.max(0, maxImagePages);
19809
+ return { rendered: needImage.slice(0, limit), omitted: needImage.slice(limit) };
19810
+ }
19811
+ function formatPageList(pages) {
19812
+ const ranges = [];
19813
+ for (let i = 0; i < pages.length; i++) {
19814
+ const start = pages[i];
19815
+ let end = start;
19816
+ while (i + 1 < pages.length && pages[i + 1] === end + 1) end = pages[++i];
19817
+ ranges.push(start === end ? `${start}` : `${start}-${end}`);
19818
+ }
19819
+ return `${pages.length === 1 ? "page" : "pages"} ${ranges.join(", ")}`;
19820
+ }
19821
+ function buildPdfImageNote(fileName, includedPages, omittedPages, pageCount) {
19822
+ const imageOnlyCount = includedPages.length + omittedPages.length;
19823
+ const allPages = [...includedPages, ...omittedPages].sort((a, b) => a - b);
19824
+ const subject = pageCount === void 0 || imageOnlyCount >= pageCount ? `${fileName}: scanned/image-based PDF (no extractable text)` : `${fileName}: ${imageOnlyCount} of ${pageCount} pages have no extractable text (${formatPageList(allPages)})`;
19825
+ if (includedPages.length === 0) {
19826
+ return `[${subject} \u2014 ${formatPageList(omittedPages)} could not be included as images because of the image limit, so their content is not available]`;
19827
+ }
19828
+ const included = `${formatPageList(includedPages)} rendered as images and included in this message for visual analysis`;
19829
+ if (omittedPages.length === 0) return `[${subject} \u2014 ${included}]`;
19830
+ return `[${subject} \u2014 ${included}; ${formatPageList(omittedPages)} not included because of the image limit, so their content is not available]`;
19831
+ }
19832
+ function rewriteImageNote(result, fileName, keptImages) {
19833
+ const images = result.imageDataUrls ?? [];
19834
+ const note = result.metadata?.imageNote;
19835
+ if (!note || keptImages >= images.length) return result.extractedText;
19836
+ const imagePages = result.metadata?.imagePages ?? [];
19837
+ const omittedPages = result.metadata?.omittedImagePages ?? [];
19838
+ const pageCount = result.metadata?.pageCount;
19839
+ const kept = Math.max(0, keptImages);
19840
+ const newNote = buildPdfImageNote(
19841
+ fileName,
19842
+ imagePages.slice(0, kept),
19843
+ [...imagePages.slice(kept), ...omittedPages].sort((a, b) => a - b),
19844
+ pageCount
19845
+ );
19846
+ return result.extractedText.replace(note, () => newNote);
19847
+ }
19674
19848
  var PdfProcessor = class {
19675
19849
  constructor() {
19676
19850
  this.name = "pdf";
@@ -19679,43 +19853,65 @@ var PdfProcessor = class {
19679
19853
  }
19680
19854
  async process(file) {
19681
19855
  const logger = getLogger();
19856
+ let pageTexts = null;
19682
19857
  try {
19683
- const text12 = await extractTextFromPdf(file.dataUrl);
19684
- if (text12 && text12.trim().length > 0) {
19685
- return {
19686
- extractedText: text12,
19687
- format: "plain"
19688
- };
19689
- }
19690
- logger.info(
19691
- `[PdfProcessor] Text extraction returned empty for "${file.name}" \u2014 falling back to image conversion`
19692
- );
19858
+ pageTexts = await extractPdfPageTexts(file.dataUrl);
19693
19859
  } catch (textError) {
19694
19860
  logger.warn(
19695
- `[PdfProcessor] Text extraction failed for "${file.name}" \u2014 falling back to image conversion:`,
19861
+ "[PdfProcessor] Text extraction failed \u2014 falling back to image conversion:",
19696
19862
  textError
19697
19863
  );
19698
19864
  }
19865
+ const text12 = (pageTexts ?? []).filter((t) => t.trim()).join("\n\n");
19866
+ const { rendered, omitted } = pageTexts ? selectPdfImagePages(pageTexts) : { rendered: [], omitted: [] };
19867
+ if (pageTexts && rendered.length === 0 && omitted.length === 0) {
19868
+ return text12.trim() ? { extractedText: text12, format: "plain" } : null;
19869
+ }
19870
+ let renderedPages;
19871
+ let pageCount = pageTexts?.length;
19699
19872
  try {
19700
- const images = await convertPdfToImages(file.dataUrl, MAX_IMAGE_PAGES);
19701
- if (images.length === 0) {
19702
- logger.warn(`[PdfProcessor] Image conversion also returned empty for "${file.name}"`);
19703
- return null;
19873
+ if (pageTexts) {
19874
+ renderedPages = (await renderPdfPages(file.dataUrl, void 0, rendered)).pages;
19875
+ } else {
19876
+ const result = await renderPdfPages(file.dataUrl, MAX_IMAGE_PAGES);
19877
+ renderedPages = result.pages;
19878
+ pageCount = result.pageCount;
19704
19879
  }
19705
- logger.info(`[PdfProcessor] Converted ${images.length} page(s) of "${file.name}" to images`);
19706
- return {
19707
- // Contextual note for the LLM — keep it self-contained since the
19708
- // images are only injected in the current request and not persisted
19709
- // for follow-up turns.
19710
- extractedText: `[${file.name}: scanned/image-based PDF \u2014 ${images.length} page(s) rendered as images and included in this message for visual analysis]`,
19711
- format: "plain",
19712
- imageDataUrls: images,
19713
- metadata: { pageCount: images.length }
19714
- };
19715
19880
  } catch (imageError) {
19716
- logger.error(`[PdfProcessor] Image conversion also failed for "${file.name}":`, imageError);
19717
- throw imageError;
19881
+ if (!text12.trim()) {
19882
+ logger.error("[PdfProcessor] Image conversion also failed:", imageError);
19883
+ throw imageError;
19884
+ }
19885
+ logger.warn("[PdfProcessor] Image conversion failed \u2014 keeping text only:", imageError);
19886
+ renderedPages = [];
19887
+ }
19888
+ const images = renderedPages.map((p) => p.dataUrl);
19889
+ const imagePages = renderedPages.map((p) => p.pageNumber);
19890
+ const included = new Set(imagePages);
19891
+ const needImage = pageTexts ? [...rendered, ...omitted] : Array.from({ length: pageCount ?? 0 }, (_, i) => i + 1);
19892
+ const omittedPages = needImage.filter((n) => !included.has(n)).sort((a, b) => a - b);
19893
+ if (images.length === 0 && !text12.trim()) {
19894
+ logger.warn("[PdfProcessor] Image conversion also returned empty");
19895
+ return null;
19718
19896
  }
19897
+ logger.info(
19898
+ `[PdfProcessor] Rendered ${images.length} page(s) as images, ${omittedPages.length} image-only page(s) omitted`
19899
+ );
19900
+ const imageNote = buildPdfImageNote(file.name, imagePages, omittedPages, pageCount);
19901
+ return {
19902
+ extractedText: text12.trim() ? `${imageNote}
19903
+
19904
+ ${text12}` : imageNote,
19905
+ format: "plain",
19906
+ imageDataUrls: images.length > 0 ? images : void 0,
19907
+ metadata: {
19908
+ pageCount,
19909
+ imageNote,
19910
+ imagePages,
19911
+ omittedImagePages: omittedPages,
19912
+ truncated: omittedPages.length > 0
19913
+ }
19914
+ };
19719
19915
  }
19720
19916
  };
19721
19917
 
@@ -19756,6 +19952,21 @@ var EXT = {
19756
19952
  CFG: ".cfg",
19757
19953
  CONF: ".conf"
19758
19954
  };
19955
+ function decodeText(bytes) {
19956
+ if (bytes[0] === 255 && bytes[1] === 254) {
19957
+ return new TextDecoder("utf-16le", { fatal: false }).decode(bytes.subarray(2));
19958
+ }
19959
+ if (bytes[0] === 254 && bytes[1] === 255) {
19960
+ const swapped = new Uint8Array(bytes.length - 2);
19961
+ for (let i = 2; i + 1 < bytes.length; i += 2) {
19962
+ swapped[i - 2] = bytes[i + 1];
19963
+ swapped[i - 1] = bytes[i];
19964
+ }
19965
+ return new TextDecoder("utf-16le", { fatal: false }).decode(swapped);
19966
+ }
19967
+ const hasUtf8Bom = bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191;
19968
+ return new TextDecoder("utf-8", { fatal: false }).decode(hasUtf8Bom ? bytes.subarray(3) : bytes);
19969
+ }
19759
19970
  var TextProcessor = class {
19760
19971
  constructor() {
19761
19972
  this.name = "text";
@@ -19765,7 +19976,7 @@ var TextProcessor = class {
19765
19976
  async process(file) {
19766
19977
  try {
19767
19978
  const arrayBuffer = await dataUrlToArrayBuffer(file.dataUrl);
19768
- const text12 = new TextDecoder("utf-8", { fatal: false }).decode(arrayBuffer);
19979
+ const text12 = decodeText(new Uint8Array(arrayBuffer));
19769
19980
  if (!text12.trim()) {
19770
19981
  return null;
19771
19982
  }
@@ -19788,7 +19999,7 @@ var TextProcessor = class {
19788
19999
  * rather than prose).
19789
20000
  */
19790
20001
  resolveFormat(file) {
19791
- const type = file.type?.toLowerCase() ?? "";
20002
+ const type = file.type?.split(";")[0].trim().toLowerCase() ?? "";
19792
20003
  const name = file.name?.toLowerCase() ?? "";
19793
20004
  if (type === MIME.MARKDOWN || type === MIME.X_MARKDOWN || name.endsWith(EXT.MD) || name.endsWith(EXT.MARKDOWN)) {
19794
20005
  return "markdown";
@@ -19844,6 +20055,12 @@ var WordProcessor = class {
19844
20055
 
19845
20056
  // src/lib/processors/ZipProcessor.ts
19846
20057
  import JSZip from "jszip";
20058
+ var MAX_ZIP_ENTRIES = 1e3;
20059
+ var MAX_TOTAL_UNCOMPRESSED_BYTES = 50 * 1024 * 1024;
20060
+ function declaredUncompressedSize(zipObject) {
20061
+ const size = zipObject._data?.uncompressedSize;
20062
+ return typeof size === "number" ? size : void 0;
20063
+ }
19847
20064
  var ZipProcessor = class {
19848
20065
  constructor(options = {}) {
19849
20066
  this.name = "zip";
@@ -19878,14 +20095,35 @@ var ZipProcessor = class {
19878
20095
  zipObject: zipEntry
19879
20096
  });
19880
20097
  });
19881
- const filteredEntries = this.includeHidden ? entries : entries.filter((entry) => !this.isHidden(entry.path));
19882
- filteredEntries.sort((a, b) => {
20098
+ const visibleEntries = this.includeHidden ? entries : entries.filter((entry) => !this.isHidden(entry.path));
20099
+ visibleEntries.sort((a, b) => {
19883
20100
  if (a.isDirectory !== b.isDirectory) {
19884
20101
  return a.isDirectory ? -1 : 1;
19885
20102
  }
19886
20103
  return a.path.localeCompare(b.path);
19887
20104
  });
19888
- for (const entry of filteredEntries) {
20105
+ const visibleDirs = visibleEntries.filter((e) => e.isDirectory);
20106
+ const visibleFiles = visibleEntries.filter((e) => !e.isDirectory);
20107
+ const listedDirs = visibleDirs.slice(0, MAX_ZIP_ENTRIES);
20108
+ const listedFiles = visibleFiles.slice(0, MAX_ZIP_ENTRIES);
20109
+ const filteredEntries = [...listedDirs, ...listedFiles];
20110
+ const notes = [];
20111
+ if (visibleFiles.length > listedFiles.length) {
20112
+ notes.push(
20113
+ `[truncated: the archive has ${visibleFiles.length} files; only the first ${listedFiles.length} were listed and considered for extraction, the other ${visibleFiles.length - listedFiles.length} files were dropped]`
20114
+ );
20115
+ }
20116
+ if (visibleDirs.length > listedDirs.length) {
20117
+ notes.push(
20118
+ `[truncated: listing the first ${listedDirs.length} of ${visibleDirs.length} directories]`
20119
+ );
20120
+ }
20121
+ let totalBytes = 0;
20122
+ let byteBudgetHit = false;
20123
+ let truncated = notes.length > 0;
20124
+ const logger = getLogger();
20125
+ for (let index = 0; index < filteredEntries.length; index++) {
20126
+ const entry = filteredEntries[index];
19889
20127
  if (entry.isDirectory) continue;
19890
20128
  const fileName = entry.path.split("/").pop() || entry.path;
19891
20129
  const extension = this.getFileExtension(fileName);
@@ -19899,9 +20137,20 @@ var ZipProcessor = class {
19899
20137
  };
19900
20138
  const processor = this.registry?.findProcessor(fileMetadata);
19901
20139
  if (!processor || processor.name === "zip") continue;
20140
+ const declared = declaredUncompressedSize(entry.zipObject);
20141
+ if (declared !== void 0 && declared > this.maxFileSize) continue;
20142
+ if (totalBytes + (declared ?? 0) > MAX_TOTAL_UNCOMPRESSED_BYTES) {
20143
+ byteBudgetHit = true;
20144
+ break;
20145
+ }
19902
20146
  try {
19903
20147
  const data = await entry.zipObject.async("uint8array");
20148
+ totalBytes += data.length;
19904
20149
  if (data.length > this.maxFileSize) continue;
20150
+ if (totalBytes > MAX_TOTAL_UNCOMPRESSED_BYTES) {
20151
+ byteBudgetHit = true;
20152
+ break;
20153
+ }
19905
20154
  const base64 = uint8ArrayToBase64(data);
19906
20155
  const dataUrl = `data:${mimeType};base64,${base64}`;
19907
20156
  const fileWithData = {
@@ -19911,23 +20160,36 @@ var ZipProcessor = class {
19911
20160
  };
19912
20161
  const result = await processor.process(fileWithData);
19913
20162
  if (result && result.extractedText.trim()) {
20163
+ if (result.metadata?.truncated || result.imageDataUrls?.length) truncated = true;
19914
20164
  processedContents.push({
19915
20165
  path: entry.path,
19916
20166
  processorName: processor.name,
19917
- result
20167
+ // Page images of nested files are not forwarded, so their note must not claim them.
20168
+ result: { ...result, extractedText: rewriteImageNote(result, fileName, 0) }
19918
20169
  });
19919
20170
  }
19920
- } catch {
20171
+ } catch (error) {
20172
+ logger.warn(
20173
+ `[ZipProcessor] Failed to process archive entry #${index + 1} (${processor.name}, ${mimeType})`,
20174
+ error
20175
+ );
19921
20176
  }
19922
20177
  }
19923
- const output = this.formatOutput(filteredEntries, processedContents);
20178
+ if (byteBudgetHit) {
20179
+ truncated = true;
20180
+ notes.push(
20181
+ `[truncated: stopped reading the archive after ${Math.round(MAX_TOTAL_UNCOMPRESSED_BYTES / 1024 / 1024)} MB of decompressed content; later files were not extracted]`
20182
+ );
20183
+ }
20184
+ const output = [this.formatOutput(filteredEntries, processedContents), ...notes].join("\n");
19924
20185
  return {
19925
20186
  extractedText: output,
19926
20187
  format: "markdown",
19927
20188
  metadata: {
19928
20189
  fileCount: filteredEntries.filter((e) => !e.isDirectory).length,
19929
20190
  directoryCount: filteredEntries.filter((e) => e.isDirectory).length,
19930
- processedFiles: processedContents.length
20191
+ processedFiles: processedContents.length,
20192
+ truncated
19931
20193
  }
19932
20194
  };
19933
20195
  } catch (error) {
@@ -20029,9 +20291,10 @@ var ProcessorRegistry = class {
20029
20291
  * @returns The matching processor, or null if none found
20030
20292
  */
20031
20293
  findProcessor(file) {
20032
- if (file.type) {
20294
+ const mimeType = file.type?.split(";")[0].trim().toLowerCase();
20295
+ if (mimeType) {
20033
20296
  for (const processor of this.processors.values()) {
20034
- if (processor.supportedMimeTypes.includes(file.type)) {
20297
+ if (processor.supportedMimeTypes.includes(mimeType)) {
20035
20298
  return processor;
20036
20299
  }
20037
20300
  }
@@ -20098,8 +20361,54 @@ var ProcessorRegistry = class {
20098
20361
  }
20099
20362
  };
20100
20363
 
20364
+ // src/lib/processors/fileStatusNotes.ts
20365
+ var DEFAULT_MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
20366
+ function formatMegabytes(bytes) {
20367
+ const mb = bytes / (1024 * 1024);
20368
+ return `${Number.isInteger(mb) ? mb : mb.toFixed(1)} MB`;
20369
+ }
20370
+ function formatFileProcessingNotes(statuses, options = {}) {
20371
+ const maxFileSizeBytes = options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
20372
+ const why = {
20373
+ too_large: `the file is larger than ${formatMegabytes(maxFileSizeBytes)}`,
20374
+ unsupported_type: "its file type is not supported for reading",
20375
+ no_data: "its contents were not available to the app",
20376
+ empty: "no text or images could be extracted from it (it may be blank)",
20377
+ timeout: "reading it took too long and was stopped",
20378
+ error: "its text could not be extracted (it may be password-protected or damaged)"
20379
+ };
20380
+ const lines = statuses.filter((s) => s.status === "skipped" || s.status === "failed").map((s) => `[${s.fileName} could not be read: ${why[s.reason ?? "error"]}]`);
20381
+ return lines.length > 0 ? lines.join("\n") : null;
20382
+ }
20383
+
20101
20384
  // src/lib/processors/preprocessor.ts
20102
20385
  var MAX_TOTAL_IMAGES = 20;
20386
+ var DEFAULT_MAX_EXTRACTED_CHARS_PER_FILE = 1e5;
20387
+ var DEFAULT_MAX_EXTRACTED_CHARS_TOTAL = 2e5;
20388
+ var ProcessingTimeoutError = class extends Error {
20389
+ constructor(timeoutMs) {
20390
+ super(`Timed out processing file after ${timeoutMs}ms`);
20391
+ this.name = "ProcessingTimeoutError";
20392
+ }
20393
+ };
20394
+ function truncationMarker2(kept, total, fileName) {
20395
+ return `
20396
+ [truncated: showing the first ${kept} of ${total} characters of ${fileName}]`;
20397
+ }
20398
+ function truncateText(text12, limit, fileName) {
20399
+ if (text12.length <= limit) return { text: text12, truncated: false };
20400
+ let end = Math.max(0, limit);
20401
+ const code = text12.charCodeAt(end - 1);
20402
+ if (end > 0 && code >= 55296 && code <= 56319) end--;
20403
+ return {
20404
+ text: `${text12.slice(0, end)}${truncationMarker2(end, text12.length, fileName)}`,
20405
+ truncated: true
20406
+ };
20407
+ }
20408
+ var FILE_SEPARATOR = "\n\n---\n\n";
20409
+ function isImageFile(file) {
20410
+ return (file.type ?? "").trim().toLowerCase().startsWith("image/");
20411
+ }
20103
20412
  function createDefaultRegistry() {
20104
20413
  const registry2 = new ProcessorRegistry();
20105
20414
  registry2.register(new PdfProcessor());
@@ -20149,10 +20458,11 @@ async function preprocessFiles(files, options = {}) {
20149
20458
  processors = void 0,
20150
20459
  // undefined means use defaults
20151
20460
  keepOriginalFiles = true,
20152
- maxFileSizeBytes = 10 * 1024 * 1024,
20153
- // 10MB
20461
+ maxFileSizeBytes = DEFAULT_MAX_FILE_SIZE_BYTES,
20154
20462
  timeoutMs = 3e4,
20155
20463
  // 30s per file
20464
+ maxExtractedCharsPerFile = DEFAULT_MAX_EXTRACTED_CHARS_PER_FILE,
20465
+ maxExtractedCharsTotal = DEFAULT_MAX_EXTRACTED_CHARS_TOTAL,
20156
20466
  onProgress,
20157
20467
  onError
20158
20468
  } = options;
@@ -20162,23 +20472,23 @@ async function preprocessFiles(files, options = {}) {
20162
20472
  extractedContent: null,
20163
20473
  originalFiles: files,
20164
20474
  preprocessedFileIds: [],
20475
+ fileStatuses: [],
20165
20476
  metadata: { processedCount: 0, skippedCount: 0, errorCount: 0 }
20166
20477
  };
20167
20478
  }
20168
- if (processors !== void 0 && processors !== null && processors.length === 0) {
20169
- return {
20170
- extractedContent: null,
20171
- originalFiles: files,
20172
- preprocessedFileIds: [],
20173
- metadata: { processedCount: 0, skippedCount: 0, errorCount: 0 }
20174
- };
20175
- }
20176
- if (processors === null) {
20479
+ if (processors === null || processors?.length === 0) {
20480
+ const fileStatuses2 = files.filter((file) => !isImageFile(file)).map((file) => ({
20481
+ fileId: file.id,
20482
+ fileName: file.name,
20483
+ status: "skipped",
20484
+ reason: "unsupported_type"
20485
+ }));
20177
20486
  return {
20178
20487
  extractedContent: null,
20179
20488
  originalFiles: files,
20180
20489
  preprocessedFileIds: [],
20181
- metadata: { processedCount: 0, skippedCount: 0, errorCount: 0 }
20490
+ fileStatuses: fileStatuses2,
20491
+ metadata: { processedCount: 0, skippedCount: fileStatuses2.length, errorCount: 0 }
20182
20492
  };
20183
20493
  }
20184
20494
  let registry2;
@@ -20196,28 +20506,38 @@ async function preprocessFiles(files, options = {}) {
20196
20506
  const extractedTexts = [];
20197
20507
  const allImageUrls = [];
20198
20508
  const preprocessedFileIds = [];
20509
+ const fileStatuses = [];
20199
20510
  let processedCount = 0;
20200
20511
  let skippedCount = 0;
20201
20512
  let errorCount = 0;
20513
+ let totalChars = 0;
20514
+ const overBudgetFiles = [];
20202
20515
  for (let i = 0; i < files.length; i++) {
20203
20516
  const file = files[i];
20517
+ const label = `#${i + 1} (${file.type || "unknown type"}, ${file.size} bytes)`;
20518
+ const skip = (reason) => {
20519
+ skippedCount++;
20520
+ fileStatuses.push({ fileId: file.id, fileName: file.name, status: "skipped", reason });
20521
+ };
20204
20522
  onProgress?.(i + 1, files.length, file.name);
20523
+ const processor = registry2.findProcessor(file);
20524
+ if (!processor && isImageFile(file)) continue;
20205
20525
  if (file.size > maxFileSizeBytes) {
20206
20526
  logger.info(
20207
- `[preprocessFiles] Skipping "${file.name}" \u2014 exceeds ${maxFileSizeBytes} byte limit`
20527
+ `[preprocessFiles] Skipping file ${label} \u2014 exceeds ${maxFileSizeBytes} byte limit`
20208
20528
  );
20209
- skippedCount++;
20529
+ skip("too_large");
20210
20530
  continue;
20211
20531
  }
20212
- const processor = registry2.findProcessor(file);
20213
20532
  if (!processor) {
20214
- skippedCount++;
20533
+ skip("unsupported_type");
20215
20534
  continue;
20216
20535
  }
20536
+ let timer;
20217
20537
  try {
20218
20538
  if (!file.url) {
20219
- logger.info(`[preprocessFiles] Skipping "${file.name}" \u2014 no data URL available`);
20220
- skippedCount++;
20539
+ logger.info(`[preprocessFiles] Skipping file ${label} \u2014 no data URL available`);
20540
+ skip("no_data");
20221
20541
  continue;
20222
20542
  }
20223
20543
  const fileWithData = {
@@ -20226,40 +20546,76 @@ async function preprocessFiles(files, options = {}) {
20226
20546
  };
20227
20547
  const result = await Promise.race([
20228
20548
  processor.process(fileWithData),
20229
- new Promise(
20230
- (_, reject) => setTimeout(() => reject(new Error(`Timed out processing ${file.name}`)), timeoutMs)
20231
- )
20549
+ new Promise((_, reject) => {
20550
+ timer = setTimeout(() => reject(new ProcessingTimeoutError(timeoutMs)), timeoutMs);
20551
+ })
20232
20552
  ]);
20233
20553
  if (result && result.extractedText.trim()) {
20234
- const formattedContent = formatExtractedContent(
20235
- file.name,
20236
- result.extractedText,
20237
- result.format
20238
- );
20239
- extractedTexts.push(formattedContent);
20554
+ const separatorLength = extractedTexts.length > 0 ? FILE_SEPARATOR.length : 0;
20555
+ const remaining = maxExtractedCharsTotal - totalChars - separatorLength - formatExtractedContent(file.name, "", result.format).length;
20240
20556
  preprocessedFileIds.push(file.id);
20241
20557
  processedCount++;
20242
- if (result.imageDataUrls && result.imageDataUrls.length > 0) {
20243
- const remaining = MAX_TOTAL_IMAGES - allImageUrls.length;
20244
- if (remaining > 0) {
20245
- allImageUrls.push(...result.imageDataUrls.slice(0, remaining));
20246
- }
20558
+ const fullLength = result.extractedText.length;
20559
+ if (fullLength > remaining && remaining - truncationMarker2(fullLength, fullLength, file.name).length <= 0) {
20560
+ overBudgetFiles.push(file.name);
20561
+ fileStatuses.push({ fileId: file.id, fileName: file.name, status: "truncated" });
20562
+ continue;
20247
20563
  }
20564
+ const images = result.imageDataUrls ?? [];
20565
+ const keptImages = Math.min(
20566
+ images.length,
20567
+ Math.max(0, MAX_TOTAL_IMAGES - allImageUrls.length)
20568
+ );
20569
+ allImageUrls.push(...images.slice(0, keptImages));
20570
+ const text12 = rewriteImageNote(result, file.name, keptImages);
20571
+ const fitsWhole = text12.length <= Math.min(maxExtractedCharsPerFile, remaining);
20572
+ const limit = fitsWhole ? text12.length : Math.min(
20573
+ maxExtractedCharsPerFile,
20574
+ remaining - truncationMarker2(text12.length, text12.length, file.name).length
20575
+ );
20576
+ const capped = truncateText(text12, Math.max(0, limit), file.name);
20577
+ const formattedContent = formatExtractedContent(file.name, capped.text, result.format);
20578
+ totalChars += separatorLength + formattedContent.length;
20579
+ extractedTexts.push(formattedContent);
20580
+ const truncated = capped.truncated || keptImages < images.length || result.metadata?.truncated === true;
20581
+ fileStatuses.push({
20582
+ fileId: file.id,
20583
+ fileName: file.name,
20584
+ status: truncated ? "truncated" : keptImages > 0 ? "rendered_as_images" : "extracted"
20585
+ });
20248
20586
  } else {
20249
- skippedCount++;
20587
+ skip("empty");
20250
20588
  }
20251
20589
  } catch (error) {
20252
20590
  errorCount++;
20253
- logger.error(`[preprocessFiles] Error processing "${file.name}":`, error);
20591
+ const timedOut = error instanceof ProcessingTimeoutError;
20592
+ fileStatuses.push({
20593
+ fileId: file.id,
20594
+ fileName: file.name,
20595
+ status: "failed",
20596
+ reason: timedOut ? "timeout" : "error"
20597
+ });
20598
+ logger.error(
20599
+ `[preprocessFiles] Error processing file ${label} with ${processor.name}:`,
20600
+ error
20601
+ );
20254
20602
  onError?.(file.name, error instanceof Error ? error : new Error(String(error)));
20603
+ } finally {
20604
+ clearTimeout(timer);
20255
20605
  }
20256
20606
  }
20257
- const extractedContent = extractedTexts.length > 0 ? extractedTexts.join("\n\n---\n\n") : null;
20607
+ if (overBudgetFiles.length > 0) {
20608
+ extractedTexts.push(
20609
+ `[truncated: the attachment text limit (${maxExtractedCharsTotal} characters) was reached; the contents of ${overBudgetFiles.join(", ")} were not included]`
20610
+ );
20611
+ }
20612
+ const extractedContent = extractedTexts.length > 0 ? extractedTexts.join(FILE_SEPARATOR) : null;
20258
20613
  return {
20259
20614
  extractedContent,
20260
20615
  imageContentUrls: allImageUrls.length > 0 ? allImageUrls : void 0,
20261
20616
  originalFiles: keepOriginalFiles ? files : void 0,
20262
20617
  preprocessedFileIds,
20618
+ fileStatuses,
20263
20619
  metadata: { processedCount, skippedCount, errorCount }
20264
20620
  };
20265
20621
  }
@@ -21614,7 +21970,7 @@ async function storedToLlmapiMessage(stored, encryptionKey, resolveMediaByIds) {
21614
21970
  } else if (encryptionKey && isOPFSSupported()) {
21615
21971
  try {
21616
21972
  const result = await readEncryptedFile(file.id, encryptionKey);
21617
- if (result) {
21973
+ if (result && result.blob.type.startsWith("image/")) {
21618
21974
  const dataUri = await blobToDataUri(result.blob);
21619
21975
  imageParts.push({
21620
21976
  type: "image_url",
@@ -22555,7 +22911,8 @@ function useChatStorage(options) {
22555
22911
  conversationId: explicitConversationId,
22556
22912
  parentMessageId,
22557
22913
  assistantUniqueId,
22558
- piiRedaction: requestPiiRedaction
22914
+ piiRedaction: requestPiiRedaction,
22915
+ onFileProcessingResult
22559
22916
  } = args;
22560
22917
  const resolvePiiForCall = (conversationIdForCall) => {
22561
22918
  const { redactor, forInnerSend } = resolveCallPii(
@@ -22751,9 +23108,12 @@ function useChatStorage(options) {
22751
23108
  const contentForStorage = resolveStoredUserContent(storedUserContent, extracted.content);
22752
23109
  const filesForStorage = files ?? extracted.files;
22753
23110
  let fileContextForRequest;
23111
+ let fileContextIsCurrentTurn = false;
22754
23112
  let preprocessedFileIds = [];
22755
23113
  let imageContentUrls;
23114
+ let fileProcessingNotes = null;
22756
23115
  if (filesForStorage && filesForStorage.length > 0) {
23116
+ let fileStatuses;
22757
23117
  try {
22758
23118
  const preprocessingResult = await preprocessFiles(filesForStorage, {
22759
23119
  processors: fileProcessors,
@@ -22761,16 +23121,27 @@ function useChatStorage(options) {
22761
23121
  });
22762
23122
  if (preprocessingResult.extractedContent) {
22763
23123
  fileContextForRequest = preprocessingResult.extractedContent;
23124
+ fileContextIsCurrentTurn = true;
22764
23125
  preprocessedFileIds = preprocessingResult.preprocessedFileIds;
22765
23126
  }
22766
23127
  if (preprocessingResult.imageContentUrls?.length) {
22767
23128
  imageContentUrls = preprocessingResult.imageContentUrls;
22768
23129
  }
23130
+ fileStatuses = preprocessingResult.fileStatuses;
22769
23131
  } catch (err) {
22770
23132
  getLogger().error(
22771
23133
  "[sendMessage] File preprocessing failed \u2014 continuing without file context:",
22772
23134
  err
22773
23135
  );
23136
+ fileStatuses = filesForStorage.filter((f) => !(f.type ?? "").toLowerCase().startsWith("image/")).map((f) => ({ fileId: f.id, fileName: f.name, status: "failed", reason: "error" }));
23137
+ }
23138
+ fileProcessingNotes = formatFileProcessingNotes(fileStatuses, {
23139
+ maxFileSizeBytes: fileProcessingOptions?.maxFileSizeBytes
23140
+ });
23141
+ try {
23142
+ onFileProcessingResult?.(fileStatuses);
23143
+ } catch (err) {
23144
+ getLogger().warn("[sendMessage] onFileProcessingResult threw \u2014 ignoring:", err);
22774
23145
  }
22775
23146
  }
22776
23147
  let convId;
@@ -22944,7 +23315,11 @@ function useChatStorage(options) {
22944
23315
  if (part.type === "text") return true;
22945
23316
  if (part.type === "input_file" && part.file) {
22946
23317
  const fileId = part.file.file_id;
22947
- return !fileId || !preprocessedFileIds.includes(fileId);
23318
+ if (fileId) return !preprocessedFileIds.includes(fileId);
23319
+ const { file_data: fileData, file_url: fileUrl } = part.file;
23320
+ return !filesForStorage?.some(
23321
+ (f) => preprocessedFileIds.includes(f.id) && !!f.url && (f.url === fileData || f.url === fileUrl)
23322
+ );
22948
23323
  }
22949
23324
  if (part.type === "image_url" && part.image_url?.url) {
22950
23325
  const matchesPreprocessed = filesForStorage?.some(
@@ -22979,6 +23354,13 @@ function useChatStorage(options) {
22979
23354
  };
22980
23355
  }
22981
23356
  }
23357
+ const currentTurnFileText = [
23358
+ fileContextIsCurrentTurn ? fileContextForRequest : void 0,
23359
+ fileProcessingNotes
23360
+ ].filter(Boolean).join("\n\n");
23361
+ if (currentTurnFileText) {
23362
+ messagesToSend = attachFileContextToLastUserMessage(messagesToSend, currentTurnFileText);
23363
+ }
22982
23364
  let userFileIds = [];
22983
23365
  if (filesForStorage && filesForStorage.length > 0 && walletAddress && isEncryptionReady()) {
22984
23366
  userFileIds = await storeUserFilesInOPFS(filesForStorage, walletAddress, convId);
@@ -22989,7 +23371,11 @@ function useChatStorage(options) {
22989
23371
  content: contentForStorage,
22990
23372
  fileIds: userFileIds.length > 0 ? userFileIds : void 0,
22991
23373
  model,
22992
- // Store extracted file content in thinking field for retrieval in follow-up messages
23374
+ // Store the file context — this turn's, or the one recalled from an earlier row — in the
23375
+ // thinking field for retrieval in follow-up messages. Re-storing recalled context is the
23376
+ // carry-forward: recall only scans the last `maxHistoryMessages` rows, so without it a
23377
+ // file's text is lost once the turn that attached it scrolls out of the window. Failure
23378
+ // notes are never stored: only `[Extracted content from …]` text, which recall keys on.
22993
23379
  thinking: fileContextForRequest,
22994
23380
  parentMessageId
22995
23381
  };
@@ -23147,7 +23533,7 @@ function useChatStorage(options) {
23147
23533
  headers,
23148
23534
  memoryContext,
23149
23535
  searchContext,
23150
- fileContext: fileContextForRequest,
23536
+ fileContext: fileContextIsCurrentTurn ? void 0 : fileContextForRequest,
23151
23537
  toolGuidance: computeToolGuidance(
23152
23538
  filteredServerTools,
23153
23539
  filteredClientTools,
@@ -34428,8 +34814,10 @@ export {
34428
34814
  appFileToStored,
34429
34815
  archiveVaultMemoryOp,
34430
34816
  assembleMemoryContext,
34817
+ attachFileContextToLastUserMessage,
34431
34818
  backfillMemoryEntityUserIdsOp,
34432
34819
  backfillMemoryTopicsOp,
34820
+ buildAttachedFilesText,
34433
34821
  capHopsForDensity,
34434
34822
  chatStorageMigrations,
34435
34823
  chatStorageSchema,
@@ -34535,6 +34923,7 @@ export {
34535
34923
  findById,
34536
34924
  findMatchingTools,
34537
34925
  findParentOfId,
34926
+ formatFileProcessingNotes,
34538
34927
  generateConversationId,
34539
34928
  generateEmbedding,
34540
34929
  generateEmbeddings,
@@ -34634,6 +35023,7 @@ export {
34634
35023
  insertAfterId,
34635
35024
  insertChild,
34636
35025
  isAnumaTag,
35026
+ isAttachedFilesText,
34637
35027
  isCalendarCallback,
34638
35028
  isDegradedTopicSkip,
34639
35029
  isDriveCallback,