@anuma/sdk 6.26.0 → 6.26.2

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.
@@ -1504,8 +1504,10 @@ __export(index_exports, {
1504
1504
  appFileToStored: () => appFileToStored,
1505
1505
  archiveVaultMemoryOp: () => archiveVaultMemoryOp,
1506
1506
  assembleMemoryContext: () => assembleMemoryContext,
1507
+ attachFileContextToLastUserMessage: () => attachFileContextToLastUserMessage,
1507
1508
  backfillMemoryEntityUserIdsOp: () => backfillMemoryEntityUserIdsOp,
1508
1509
  backfillMemoryTopicsOp: () => backfillMemoryTopicsOp,
1510
+ buildAttachedFilesText: () => buildAttachedFilesText,
1509
1511
  capHopsForDensity: () => capHopsForDensity,
1510
1512
  chatStorageMigrations: () => chatStorageMigrations,
1511
1513
  chatStorageSchema: () => chatStorageSchema,
@@ -1611,6 +1613,7 @@ __export(index_exports, {
1611
1613
  findById: () => findById,
1612
1614
  findMatchingTools: () => findMatchingTools,
1613
1615
  findParentOfId: () => findParentOfId,
1616
+ formatFileProcessingNotes: () => formatFileProcessingNotes,
1614
1617
  generateConversationId: () => generateConversationId,
1615
1618
  generateEmbedding: () => generateEmbedding,
1616
1619
  generateEmbeddings: () => generateEmbeddings,
@@ -1710,6 +1713,7 @@ __export(index_exports, {
1710
1713
  insertAfterId: () => insertAfterId,
1711
1714
  insertChild: () => insertChild,
1712
1715
  isAnumaTag: () => isAnumaTag,
1716
+ isAttachedFilesText: () => isAttachedFilesText,
1713
1717
  isCalendarCallback: () => isCalendarCallback,
1714
1718
  isDegradedTopicSkip: () => isDegradedTopicSkip,
1715
1719
  isDriveCallback: () => isDriveCallback,
@@ -3359,6 +3363,46 @@ async function assembleDecryptedMessage(message, address) {
3359
3363
  // src/lib/db/chat/types.ts
3360
3364
  var import_uuid = require("uuid");
3361
3365
 
3366
+ // src/lib/chat/fileContext.ts
3367
+ var ATTACHED_FILES_OPEN_TAG = "<attached_files>";
3368
+ var ATTACHED_FILES_CLOSE_TAG = "</attached_files>";
3369
+ var ATTACHED_FILES_PREFIX = `${ATTACHED_FILES_OPEN_TAG}
3370
+ 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.`;
3371
+ function isAttachedFilesText(text12) {
3372
+ return typeof text12 === "string" && text12.startsWith(ATTACHED_FILES_PREFIX) && text12.endsWith(ATTACHED_FILES_CLOSE_TAG);
3373
+ }
3374
+ function buildAttachedFilesText(fileContext) {
3375
+ return `${ATTACHED_FILES_PREFIX}
3376
+
3377
+ ${fileContext}
3378
+ ${ATTACHED_FILES_CLOSE_TAG}`;
3379
+ }
3380
+ function attachFileContextToLastUserMessage(messages, fileContext) {
3381
+ if (!fileContext.trim()) return messages;
3382
+ let lastUserIndex = -1;
3383
+ for (let i = messages.length - 1; i >= 0; i--) {
3384
+ if (messages[i].role === "user") {
3385
+ lastUserIndex = i;
3386
+ break;
3387
+ }
3388
+ }
3389
+ if (lastUserIndex === -1) return messages;
3390
+ const message = messages[lastUserIndex];
3391
+ const existing = message.content ?? [];
3392
+ let insertAt = 0;
3393
+ existing.forEach((part, index) => {
3394
+ if (part.type === "text") insertAt = index + 1;
3395
+ });
3396
+ const filePart = {
3397
+ type: "text",
3398
+ text: buildAttachedFilesText(fileContext)
3399
+ };
3400
+ const content = [...existing.slice(0, insertAt), filePart, ...existing.slice(insertAt)];
3401
+ const result = [...messages];
3402
+ result[lastUserIndex] = { ...message, content };
3403
+ return result;
3404
+ }
3405
+
3362
3406
  // src/lib/chat/useChat/strategies/types.ts
3363
3407
  function isChatCompletionResponse(r) {
3364
3408
  if (r.object === "response") return false;
@@ -3480,7 +3524,7 @@ function extractUserMessageFromMessages(messages) {
3480
3524
  const textParts = [];
3481
3525
  const files = [];
3482
3526
  for (const part of lastUserMessage.content) {
3483
- if (part.type === "text" && part.text) {
3527
+ if (part.type === "text" && part.text && !isAttachedFilesText(part.text)) {
3484
3528
  textParts.push(part.text);
3485
3529
  } else if (part.type === "image_url" && part.image_url?.url) {
3486
3530
  files.push({
@@ -7926,7 +7970,7 @@ function extractLastUserText(messages) {
7926
7970
  const content = lastUserMsg.content;
7927
7971
  if (typeof content === "string") return content;
7928
7972
  if (Array.isArray(content)) {
7929
- return content.filter((c) => c?.type === "text").map((c) => c.text ?? "").join(" ");
7973
+ return content.filter((c) => c?.type === "text" && !isAttachedFilesText(c.text)).map((c) => c.text ?? "").join(" ");
7930
7974
  }
7931
7975
  return "";
7932
7976
  }
@@ -21459,14 +21503,37 @@ init_logger();
21459
21503
  if (typeof process !== "undefined" && typeof process.umask !== "function") {
21460
21504
  process.umask = (_mask) => 18;
21461
21505
  }
21506
+ var MAX_ROWS_PER_SHEET = 2e3;
21507
+ function csvField(value) {
21508
+ return /[",\r\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
21509
+ }
21510
+ function uniqueHeaders(raw, columnCount) {
21511
+ const bases = [];
21512
+ for (let col = 1; col <= columnCount; col++) bases.push(raw[col]?.trim() || `Column${col}`);
21513
+ const taken = new Set(bases);
21514
+ const emitted = /* @__PURE__ */ new Set();
21515
+ return bases.map((base) => {
21516
+ if (!emitted.has(base)) {
21517
+ emitted.add(base);
21518
+ return base;
21519
+ }
21520
+ let n = 2;
21521
+ while (taken.has(`${base}_${n}`)) n++;
21522
+ const name = `${base}_${n}`;
21523
+ taken.add(name);
21524
+ emitted.add(name);
21525
+ return name;
21526
+ });
21527
+ }
21462
21528
  var ExcelProcessor = class {
21463
- constructor() {
21529
+ constructor(options = {}) {
21464
21530
  this.name = "excel";
21465
21531
  this.supportedMimeTypes = [
21466
21532
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
21467
21533
  // .xlsx
21468
21534
  ];
21469
21535
  this.supportedExtensions = [".xlsx"];
21536
+ this.maxRowsPerSheet = options.maxRowsPerSheet ?? MAX_ROWS_PER_SHEET;
21470
21537
  }
21471
21538
  async loadExcelJS() {
21472
21539
  const mod = await import("exceljs");
@@ -21481,31 +21548,51 @@ var ExcelProcessor = class {
21481
21548
  if (workbook.worksheets.length === 0) {
21482
21549
  return null;
21483
21550
  }
21484
- const jsonData = {};
21551
+ let truncated = false;
21552
+ const sections = [];
21485
21553
  for (const worksheet of workbook.worksheets) {
21486
- const rows = [];
21487
- const headerRow = worksheet.getRow(1);
21488
- const headers = [];
21489
- headerRow.eachCell({ includeEmpty: true }, (cell, colNumber) => {
21490
- headers[colNumber] = cell.text || `Column${colNumber}`;
21554
+ const rawHeaders = [];
21555
+ let columnCount = 0;
21556
+ worksheet.getRow(1).eachCell({ includeEmpty: true }, (cell, colNumber) => {
21557
+ rawHeaders[colNumber] = cell.text;
21558
+ columnCount = Math.max(columnCount, colNumber);
21491
21559
  });
21560
+ const rows = [];
21561
+ let totalRows = 0;
21492
21562
  worksheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
21493
21563
  if (rowNumber === 1) return;
21494
- const rowData = {};
21564
+ totalRows++;
21565
+ if (rows.length >= this.maxRowsPerSheet) return;
21566
+ const values = [];
21495
21567
  row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
21496
- const key = headers[colNumber] || `Column${colNumber}`;
21497
- rowData[key] = this.resolveCellValue(cell.value);
21568
+ values[colNumber - 1] = String(this.resolveCellValue(cell.value));
21569
+ columnCount = Math.max(columnCount, colNumber);
21498
21570
  });
21499
- rows.push(rowData);
21571
+ rows.push(values);
21500
21572
  });
21501
- jsonData[worksheet.name] = rows;
21573
+ const headers = uniqueHeaders(rawHeaders, columnCount);
21574
+ const lines = [headers, ...rows].map(
21575
+ (values) => headers.map((_, i) => csvField(values[i] ?? "")).join(",")
21576
+ );
21577
+ let section = `## Sheet: ${worksheet.name}
21578
+
21579
+ \`\`\`csv
21580
+ ${lines.join("\n")}
21581
+ \`\`\``;
21582
+ if (totalRows > rows.length) {
21583
+ truncated = true;
21584
+ section += `
21585
+ [truncated: showing the first ${rows.length} of ${totalRows} data rows of sheet "${worksheet.name}"]`;
21586
+ }
21587
+ sections.push(section);
21502
21588
  }
21503
21589
  return {
21504
- extractedText: JSON.stringify(jsonData, null, 2),
21505
- format: "json",
21590
+ extractedText: sections.join("\n\n"),
21591
+ format: "markdown",
21506
21592
  metadata: {
21507
21593
  sheetCount: workbook.worksheets.length,
21508
- sheetNames: workbook.worksheets.map((ws) => ws.name)
21594
+ sheetNames: workbook.worksheets.map((ws) => ws.name),
21595
+ truncated
21509
21596
  }
21510
21597
  };
21511
21598
  } catch (error) {
@@ -21534,6 +21621,9 @@ init_logger();
21534
21621
  // src/lib/pdf.ts
21535
21622
  var pdfjsModule = null;
21536
21623
  var workerConfigured = false;
21624
+ var RENDER_SCALE = 1.5;
21625
+ var MAX_RENDER_SIDE_PX = 1600;
21626
+ var JPEG_QUALITY = 0.8;
21537
21627
  async function getPdfjs() {
21538
21628
  if (!pdfjsModule) {
21539
21629
  pdfjsModule = await import("pdfjs-dist");
@@ -21544,46 +21634,134 @@ async function getPdfjs() {
21544
21634
  }
21545
21635
  return pdfjsModule;
21546
21636
  }
21547
- async function extractTextFromPdf(pdfDataUrl) {
21637
+ async function withPdfDocument(pdfDataUrl, fn) {
21548
21638
  const pdfjs = await getPdfjs();
21549
21639
  const loadingTask = pdfjs.getDocument(pdfDataUrl);
21550
- const pdf = await loadingTask.promise;
21551
- const textParts = [];
21552
- for (let i = 1; i <= pdf.numPages; i++) {
21553
- const page = await pdf.getPage(i);
21554
- const textContent = await page.getTextContent();
21555
- const pageText = textContent.items.map((item) => "str" in item ? item.str : "").join(" ");
21556
- if (pageText.trim()) {
21557
- textParts.push(pageText);
21558
- }
21640
+ let pdf;
21641
+ try {
21642
+ pdf = await loadingTask.promise;
21643
+ } catch (error) {
21644
+ await loadingTask.destroy().catch(() => void 0);
21645
+ throw error;
21646
+ }
21647
+ try {
21648
+ return await fn(pdf);
21649
+ } finally {
21650
+ await pdf.destroy().catch(() => void 0);
21559
21651
  }
21560
- return textParts.join("\n\n");
21561
21652
  }
21562
- async function convertPdfToImages(pdfDataUrl, maxPages) {
21563
- const images = [];
21564
- const pdfjs = await getPdfjs();
21565
- const loadingTask = pdfjs.getDocument(pdfDataUrl);
21566
- const pdf = await loadingTask.promise;
21567
- const pageLimit = maxPages !== void 0 ? Math.min(maxPages, pdf.numPages) : pdf.numPages;
21568
- for (let i = 1; i <= pageLimit; i++) {
21569
- const page = await pdf.getPage(i);
21570
- const viewport = page.getViewport({ scale: 1.5 });
21571
- const canvas = document.createElement("canvas");
21572
- const context = canvas.getContext("2d");
21573
- if (!context) continue;
21574
- canvas.height = viewport.height;
21575
- canvas.width = viewport.width;
21576
- await page.render({
21577
- canvasContext: context,
21578
- viewport
21579
- }).promise;
21580
- images.push(canvas.toDataURL("image/png"));
21581
- }
21582
- return images;
21653
+ function joinPdfTextItems(items) {
21654
+ let text12 = "";
21655
+ for (const item of items) {
21656
+ if (!("str" in item)) continue;
21657
+ const { str, hasEOL } = item;
21658
+ text12 += str + (hasEOL ? "\n" : " ");
21659
+ }
21660
+ return text12.replace(/[ \t]+\n/g, "\n").trim();
21661
+ }
21662
+ function computeRenderScale(width, height) {
21663
+ const longest = Math.max(width, height);
21664
+ if (!(longest > 0)) return RENDER_SCALE;
21665
+ return Math.min(RENDER_SCALE, MAX_RENDER_SIDE_PX / longest);
21666
+ }
21667
+ async function extractPdfPageTexts(pdfDataUrl) {
21668
+ return withPdfDocument(pdfDataUrl, async (pdf) => {
21669
+ const pageTexts = [];
21670
+ for (let i = 1; i <= pdf.numPages; i++) {
21671
+ const page = await pdf.getPage(i);
21672
+ const textContent = await page.getTextContent();
21673
+ pageTexts.push(joinPdfTextItems(textContent.items));
21674
+ }
21675
+ return pageTexts;
21676
+ });
21677
+ }
21678
+ async function extractTextFromPdf(pdfDataUrl) {
21679
+ const pageTexts = await extractPdfPageTexts(pdfDataUrl);
21680
+ return pageTexts.filter((text12) => text12.trim()).join("\n\n");
21681
+ }
21682
+ async function renderPdfPages(pdfDataUrl, maxPages, pageNumbers) {
21683
+ return withPdfDocument(pdfDataUrl, async (pdf) => {
21684
+ const requested = pageNumbers?.filter((n) => n >= 1 && n <= pdf.numPages) ?? Array.from(
21685
+ { length: maxPages !== void 0 ? Math.min(maxPages, pdf.numPages) : pdf.numPages },
21686
+ (_, i) => i + 1
21687
+ );
21688
+ const pages = [];
21689
+ for (const pageNumber of requested) {
21690
+ const page = await pdf.getPage(pageNumber);
21691
+ const base = page.getViewport({ scale: 1 });
21692
+ const viewport = page.getViewport({ scale: computeRenderScale(base.width, base.height) });
21693
+ const canvas = document.createElement("canvas");
21694
+ const context = canvas.getContext("2d");
21695
+ if (!context) continue;
21696
+ canvas.height = Math.floor(viewport.height);
21697
+ canvas.width = Math.floor(viewport.width);
21698
+ context.fillStyle = "#ffffff";
21699
+ context.fillRect(0, 0, canvas.width, canvas.height);
21700
+ await page.render({
21701
+ canvasContext: context,
21702
+ viewport,
21703
+ background: "#ffffff"
21704
+ }).promise;
21705
+ pages.push({ pageNumber, dataUrl: canvas.toDataURL("image/jpeg", JPEG_QUALITY) });
21706
+ canvas.width = 0;
21707
+ canvas.height = 0;
21708
+ }
21709
+ return { pages, pageCount: pdf.numPages };
21710
+ });
21711
+ }
21712
+ async function convertPdfToImages(pdfDataUrl, maxPages, pageNumbers) {
21713
+ const { pages } = await renderPdfPages(pdfDataUrl, maxPages, pageNumbers);
21714
+ return pages.map((p) => p.dataUrl);
21583
21715
  }
21584
21716
 
21585
21717
  // src/lib/processors/PdfProcessor.ts
21586
21718
  var MAX_IMAGE_PAGES = 20;
21719
+ var MIN_PAGE_TEXT_CHARS = 20;
21720
+ function selectPdfImagePages(pageTexts, maxImagePages = MAX_IMAGE_PAGES) {
21721
+ const needImage = [];
21722
+ pageTexts.forEach((text12, index) => {
21723
+ if (text12.trim().length < MIN_PAGE_TEXT_CHARS) needImage.push(index + 1);
21724
+ });
21725
+ const limit = Math.max(0, maxImagePages);
21726
+ return { rendered: needImage.slice(0, limit), omitted: needImage.slice(limit) };
21727
+ }
21728
+ function formatPageList(pages) {
21729
+ const ranges = [];
21730
+ for (let i = 0; i < pages.length; i++) {
21731
+ const start = pages[i];
21732
+ let end = start;
21733
+ while (i + 1 < pages.length && pages[i + 1] === end + 1) end = pages[++i];
21734
+ ranges.push(start === end ? `${start}` : `${start}-${end}`);
21735
+ }
21736
+ return `${pages.length === 1 ? "page" : "pages"} ${ranges.join(", ")}`;
21737
+ }
21738
+ function buildPdfImageNote(fileName, includedPages, omittedPages, pageCount) {
21739
+ const imageOnlyCount = includedPages.length + omittedPages.length;
21740
+ const allPages = [...includedPages, ...omittedPages].sort((a, b) => a - b);
21741
+ 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)})`;
21742
+ if (includedPages.length === 0) {
21743
+ return `[${subject} \u2014 ${formatPageList(omittedPages)} could not be included as images because of the image limit, so their content is not available]`;
21744
+ }
21745
+ const included = `${formatPageList(includedPages)} rendered as images and included in this message for visual analysis`;
21746
+ if (omittedPages.length === 0) return `[${subject} \u2014 ${included}]`;
21747
+ return `[${subject} \u2014 ${included}; ${formatPageList(omittedPages)} not included because of the image limit, so their content is not available]`;
21748
+ }
21749
+ function rewriteImageNote(result, fileName, keptImages) {
21750
+ const images = result.imageDataUrls ?? [];
21751
+ const note = result.metadata?.imageNote;
21752
+ if (!note || keptImages >= images.length) return result.extractedText;
21753
+ const imagePages = result.metadata?.imagePages ?? [];
21754
+ const omittedPages = result.metadata?.omittedImagePages ?? [];
21755
+ const pageCount = result.metadata?.pageCount;
21756
+ const kept = Math.max(0, keptImages);
21757
+ const newNote = buildPdfImageNote(
21758
+ fileName,
21759
+ imagePages.slice(0, kept),
21760
+ [...imagePages.slice(kept), ...omittedPages].sort((a, b) => a - b),
21761
+ pageCount
21762
+ );
21763
+ return result.extractedText.replace(note, () => newNote);
21764
+ }
21587
21765
  var PdfProcessor = class {
21588
21766
  constructor() {
21589
21767
  this.name = "pdf";
@@ -21592,43 +21770,65 @@ var PdfProcessor = class {
21592
21770
  }
21593
21771
  async process(file) {
21594
21772
  const logger = getLogger();
21773
+ let pageTexts = null;
21595
21774
  try {
21596
- const text12 = await extractTextFromPdf(file.dataUrl);
21597
- if (text12 && text12.trim().length > 0) {
21598
- return {
21599
- extractedText: text12,
21600
- format: "plain"
21601
- };
21602
- }
21603
- logger.info(
21604
- `[PdfProcessor] Text extraction returned empty for "${file.name}" \u2014 falling back to image conversion`
21605
- );
21775
+ pageTexts = await extractPdfPageTexts(file.dataUrl);
21606
21776
  } catch (textError) {
21607
21777
  logger.warn(
21608
- `[PdfProcessor] Text extraction failed for "${file.name}" \u2014 falling back to image conversion:`,
21778
+ "[PdfProcessor] Text extraction failed \u2014 falling back to image conversion:",
21609
21779
  textError
21610
21780
  );
21611
21781
  }
21782
+ const text12 = (pageTexts ?? []).filter((t) => t.trim()).join("\n\n");
21783
+ const { rendered, omitted } = pageTexts ? selectPdfImagePages(pageTexts) : { rendered: [], omitted: [] };
21784
+ if (pageTexts && rendered.length === 0 && omitted.length === 0) {
21785
+ return text12.trim() ? { extractedText: text12, format: "plain" } : null;
21786
+ }
21787
+ let renderedPages;
21788
+ let pageCount = pageTexts?.length;
21612
21789
  try {
21613
- const images = await convertPdfToImages(file.dataUrl, MAX_IMAGE_PAGES);
21614
- if (images.length === 0) {
21615
- logger.warn(`[PdfProcessor] Image conversion also returned empty for "${file.name}"`);
21616
- return null;
21790
+ if (pageTexts) {
21791
+ renderedPages = (await renderPdfPages(file.dataUrl, void 0, rendered)).pages;
21792
+ } else {
21793
+ const result = await renderPdfPages(file.dataUrl, MAX_IMAGE_PAGES);
21794
+ renderedPages = result.pages;
21795
+ pageCount = result.pageCount;
21617
21796
  }
21618
- logger.info(`[PdfProcessor] Converted ${images.length} page(s) of "${file.name}" to images`);
21619
- return {
21620
- // Contextual note for the LLM — keep it self-contained since the
21621
- // images are only injected in the current request and not persisted
21622
- // for follow-up turns.
21623
- extractedText: `[${file.name}: scanned/image-based PDF \u2014 ${images.length} page(s) rendered as images and included in this message for visual analysis]`,
21624
- format: "plain",
21625
- imageDataUrls: images,
21626
- metadata: { pageCount: images.length }
21627
- };
21628
21797
  } catch (imageError) {
21629
- logger.error(`[PdfProcessor] Image conversion also failed for "${file.name}":`, imageError);
21630
- throw imageError;
21798
+ if (!text12.trim()) {
21799
+ logger.error("[PdfProcessor] Image conversion also failed:", imageError);
21800
+ throw imageError;
21801
+ }
21802
+ logger.warn("[PdfProcessor] Image conversion failed \u2014 keeping text only:", imageError);
21803
+ renderedPages = [];
21804
+ }
21805
+ const images = renderedPages.map((p) => p.dataUrl);
21806
+ const imagePages = renderedPages.map((p) => p.pageNumber);
21807
+ const included = new Set(imagePages);
21808
+ const needImage = pageTexts ? [...rendered, ...omitted] : Array.from({ length: pageCount ?? 0 }, (_, i) => i + 1);
21809
+ const omittedPages = needImage.filter((n) => !included.has(n)).sort((a, b) => a - b);
21810
+ if (images.length === 0 && !text12.trim()) {
21811
+ logger.warn("[PdfProcessor] Image conversion also returned empty");
21812
+ return null;
21631
21813
  }
21814
+ logger.info(
21815
+ `[PdfProcessor] Rendered ${images.length} page(s) as images, ${omittedPages.length} image-only page(s) omitted`
21816
+ );
21817
+ const imageNote = buildPdfImageNote(file.name, imagePages, omittedPages, pageCount);
21818
+ return {
21819
+ extractedText: text12.trim() ? `${imageNote}
21820
+
21821
+ ${text12}` : imageNote,
21822
+ format: "plain",
21823
+ imageDataUrls: images.length > 0 ? images : void 0,
21824
+ metadata: {
21825
+ pageCount,
21826
+ imageNote,
21827
+ imagePages,
21828
+ omittedImagePages: omittedPages,
21829
+ truncated: omittedPages.length > 0
21830
+ }
21831
+ };
21632
21832
  }
21633
21833
  };
21634
21834
 
@@ -21670,6 +21870,21 @@ var EXT = {
21670
21870
  CFG: ".cfg",
21671
21871
  CONF: ".conf"
21672
21872
  };
21873
+ function decodeText(bytes) {
21874
+ if (bytes[0] === 255 && bytes[1] === 254) {
21875
+ return new TextDecoder("utf-16le", { fatal: false }).decode(bytes.subarray(2));
21876
+ }
21877
+ if (bytes[0] === 254 && bytes[1] === 255) {
21878
+ const swapped = new Uint8Array(bytes.length - 2);
21879
+ for (let i = 2; i + 1 < bytes.length; i += 2) {
21880
+ swapped[i - 2] = bytes[i + 1];
21881
+ swapped[i - 1] = bytes[i];
21882
+ }
21883
+ return new TextDecoder("utf-16le", { fatal: false }).decode(swapped);
21884
+ }
21885
+ const hasUtf8Bom = bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191;
21886
+ return new TextDecoder("utf-8", { fatal: false }).decode(hasUtf8Bom ? bytes.subarray(3) : bytes);
21887
+ }
21673
21888
  var TextProcessor = class {
21674
21889
  constructor() {
21675
21890
  this.name = "text";
@@ -21679,7 +21894,7 @@ var TextProcessor = class {
21679
21894
  async process(file) {
21680
21895
  try {
21681
21896
  const arrayBuffer = await dataUrlToArrayBuffer(file.dataUrl);
21682
- const text12 = new TextDecoder("utf-8", { fatal: false }).decode(arrayBuffer);
21897
+ const text12 = decodeText(new Uint8Array(arrayBuffer));
21683
21898
  if (!text12.trim()) {
21684
21899
  return null;
21685
21900
  }
@@ -21702,7 +21917,7 @@ var TextProcessor = class {
21702
21917
  * rather than prose).
21703
21918
  */
21704
21919
  resolveFormat(file) {
21705
- const type = file.type?.toLowerCase() ?? "";
21920
+ const type = file.type?.split(";")[0].trim().toLowerCase() ?? "";
21706
21921
  const name = file.name?.toLowerCase() ?? "";
21707
21922
  if (type === MIME.MARKDOWN || type === MIME.X_MARKDOWN || name.endsWith(EXT.MD) || name.endsWith(EXT.MARKDOWN)) {
21708
21923
  return "markdown";
@@ -21760,6 +21975,12 @@ var WordProcessor = class {
21760
21975
  // src/lib/processors/ZipProcessor.ts
21761
21976
  var import_jszip = __toESM(require("jszip"));
21762
21977
  init_logger();
21978
+ var MAX_ZIP_ENTRIES = 1e3;
21979
+ var MAX_TOTAL_UNCOMPRESSED_BYTES = 50 * 1024 * 1024;
21980
+ function declaredUncompressedSize(zipObject) {
21981
+ const size = zipObject._data?.uncompressedSize;
21982
+ return typeof size === "number" ? size : void 0;
21983
+ }
21763
21984
  var ZipProcessor = class {
21764
21985
  constructor(options = {}) {
21765
21986
  this.name = "zip";
@@ -21794,14 +22015,35 @@ var ZipProcessor = class {
21794
22015
  zipObject: zipEntry
21795
22016
  });
21796
22017
  });
21797
- const filteredEntries = this.includeHidden ? entries : entries.filter((entry) => !this.isHidden(entry.path));
21798
- filteredEntries.sort((a, b) => {
22018
+ const visibleEntries = this.includeHidden ? entries : entries.filter((entry) => !this.isHidden(entry.path));
22019
+ visibleEntries.sort((a, b) => {
21799
22020
  if (a.isDirectory !== b.isDirectory) {
21800
22021
  return a.isDirectory ? -1 : 1;
21801
22022
  }
21802
22023
  return a.path.localeCompare(b.path);
21803
22024
  });
21804
- for (const entry of filteredEntries) {
22025
+ const visibleDirs = visibleEntries.filter((e) => e.isDirectory);
22026
+ const visibleFiles = visibleEntries.filter((e) => !e.isDirectory);
22027
+ const listedDirs = visibleDirs.slice(0, MAX_ZIP_ENTRIES);
22028
+ const listedFiles = visibleFiles.slice(0, MAX_ZIP_ENTRIES);
22029
+ const filteredEntries = [...listedDirs, ...listedFiles];
22030
+ const notes = [];
22031
+ if (visibleFiles.length > listedFiles.length) {
22032
+ notes.push(
22033
+ `[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]`
22034
+ );
22035
+ }
22036
+ if (visibleDirs.length > listedDirs.length) {
22037
+ notes.push(
22038
+ `[truncated: listing the first ${listedDirs.length} of ${visibleDirs.length} directories]`
22039
+ );
22040
+ }
22041
+ let totalBytes = 0;
22042
+ let byteBudgetHit = false;
22043
+ let truncated = notes.length > 0;
22044
+ const logger = getLogger();
22045
+ for (let index = 0; index < filteredEntries.length; index++) {
22046
+ const entry = filteredEntries[index];
21805
22047
  if (entry.isDirectory) continue;
21806
22048
  const fileName = entry.path.split("/").pop() || entry.path;
21807
22049
  const extension = this.getFileExtension(fileName);
@@ -21815,9 +22057,20 @@ var ZipProcessor = class {
21815
22057
  };
21816
22058
  const processor = this.registry?.findProcessor(fileMetadata);
21817
22059
  if (!processor || processor.name === "zip") continue;
22060
+ const declared = declaredUncompressedSize(entry.zipObject);
22061
+ if (declared !== void 0 && declared > this.maxFileSize) continue;
22062
+ if (totalBytes + (declared ?? 0) > MAX_TOTAL_UNCOMPRESSED_BYTES) {
22063
+ byteBudgetHit = true;
22064
+ break;
22065
+ }
21818
22066
  try {
21819
22067
  const data = await entry.zipObject.async("uint8array");
22068
+ totalBytes += data.length;
21820
22069
  if (data.length > this.maxFileSize) continue;
22070
+ if (totalBytes > MAX_TOTAL_UNCOMPRESSED_BYTES) {
22071
+ byteBudgetHit = true;
22072
+ break;
22073
+ }
21821
22074
  const base64 = uint8ArrayToBase64(data);
21822
22075
  const dataUrl = `data:${mimeType};base64,${base64}`;
21823
22076
  const fileWithData = {
@@ -21827,23 +22080,36 @@ var ZipProcessor = class {
21827
22080
  };
21828
22081
  const result = await processor.process(fileWithData);
21829
22082
  if (result && result.extractedText.trim()) {
22083
+ if (result.metadata?.truncated || result.imageDataUrls?.length) truncated = true;
21830
22084
  processedContents.push({
21831
22085
  path: entry.path,
21832
22086
  processorName: processor.name,
21833
- result
22087
+ // Page images of nested files are not forwarded, so their note must not claim them.
22088
+ result: { ...result, extractedText: rewriteImageNote(result, fileName, 0) }
21834
22089
  });
21835
22090
  }
21836
- } catch {
22091
+ } catch (error) {
22092
+ logger.warn(
22093
+ `[ZipProcessor] Failed to process archive entry #${index + 1} (${processor.name}, ${mimeType})`,
22094
+ error
22095
+ );
21837
22096
  }
21838
22097
  }
21839
- const output = this.formatOutput(filteredEntries, processedContents);
22098
+ if (byteBudgetHit) {
22099
+ truncated = true;
22100
+ notes.push(
22101
+ `[truncated: stopped reading the archive after ${Math.round(MAX_TOTAL_UNCOMPRESSED_BYTES / 1024 / 1024)} MB of decompressed content; later files were not extracted]`
22102
+ );
22103
+ }
22104
+ const output = [this.formatOutput(filteredEntries, processedContents), ...notes].join("\n");
21840
22105
  return {
21841
22106
  extractedText: output,
21842
22107
  format: "markdown",
21843
22108
  metadata: {
21844
22109
  fileCount: filteredEntries.filter((e) => !e.isDirectory).length,
21845
22110
  directoryCount: filteredEntries.filter((e) => e.isDirectory).length,
21846
- processedFiles: processedContents.length
22111
+ processedFiles: processedContents.length,
22112
+ truncated
21847
22113
  }
21848
22114
  };
21849
22115
  } catch (error) {
@@ -21945,9 +22211,10 @@ var ProcessorRegistry = class {
21945
22211
  * @returns The matching processor, or null if none found
21946
22212
  */
21947
22213
  findProcessor(file) {
21948
- if (file.type) {
22214
+ const mimeType = file.type?.split(";")[0].trim().toLowerCase();
22215
+ if (mimeType) {
21949
22216
  for (const processor of this.processors.values()) {
21950
- if (processor.supportedMimeTypes.includes(file.type)) {
22217
+ if (processor.supportedMimeTypes.includes(mimeType)) {
21951
22218
  return processor;
21952
22219
  }
21953
22220
  }
@@ -22014,9 +22281,55 @@ var ProcessorRegistry = class {
22014
22281
  }
22015
22282
  };
22016
22283
 
22284
+ // src/lib/processors/fileStatusNotes.ts
22285
+ var DEFAULT_MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
22286
+ function formatMegabytes(bytes) {
22287
+ const mb = bytes / (1024 * 1024);
22288
+ return `${Number.isInteger(mb) ? mb : mb.toFixed(1)} MB`;
22289
+ }
22290
+ function formatFileProcessingNotes(statuses, options = {}) {
22291
+ const maxFileSizeBytes = options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
22292
+ const why = {
22293
+ too_large: `the file is larger than ${formatMegabytes(maxFileSizeBytes)}`,
22294
+ unsupported_type: "its file type is not supported for reading",
22295
+ no_data: "its contents were not available to the app",
22296
+ empty: "no text or images could be extracted from it (it may be blank)",
22297
+ timeout: "reading it took too long and was stopped",
22298
+ error: "its text could not be extracted (it may be password-protected or damaged)"
22299
+ };
22300
+ const lines = statuses.filter((s) => s.status === "skipped" || s.status === "failed").map((s) => `[${s.fileName} could not be read: ${why[s.reason ?? "error"]}]`);
22301
+ return lines.length > 0 ? lines.join("\n") : null;
22302
+ }
22303
+
22017
22304
  // src/lib/processors/preprocessor.ts
22018
22305
  init_logger();
22019
22306
  var MAX_TOTAL_IMAGES = 20;
22307
+ var DEFAULT_MAX_EXTRACTED_CHARS_PER_FILE = 1e5;
22308
+ var DEFAULT_MAX_EXTRACTED_CHARS_TOTAL = 2e5;
22309
+ var ProcessingTimeoutError = class extends Error {
22310
+ constructor(timeoutMs) {
22311
+ super(`Timed out processing file after ${timeoutMs}ms`);
22312
+ this.name = "ProcessingTimeoutError";
22313
+ }
22314
+ };
22315
+ function truncationMarker2(kept, total, fileName) {
22316
+ return `
22317
+ [truncated: showing the first ${kept} of ${total} characters of ${fileName}]`;
22318
+ }
22319
+ function truncateText(text12, limit, fileName) {
22320
+ if (text12.length <= limit) return { text: text12, truncated: false };
22321
+ let end = Math.max(0, limit);
22322
+ const code = text12.charCodeAt(end - 1);
22323
+ if (end > 0 && code >= 55296 && code <= 56319) end--;
22324
+ return {
22325
+ text: `${text12.slice(0, end)}${truncationMarker2(end, text12.length, fileName)}`,
22326
+ truncated: true
22327
+ };
22328
+ }
22329
+ var FILE_SEPARATOR = "\n\n---\n\n";
22330
+ function isImageFile(file) {
22331
+ return (file.type ?? "").trim().toLowerCase().startsWith("image/");
22332
+ }
22020
22333
  function createDefaultRegistry() {
22021
22334
  const registry2 = new ProcessorRegistry();
22022
22335
  registry2.register(new PdfProcessor());
@@ -22066,10 +22379,11 @@ async function preprocessFiles(files, options = {}) {
22066
22379
  processors = void 0,
22067
22380
  // undefined means use defaults
22068
22381
  keepOriginalFiles = true,
22069
- maxFileSizeBytes = 10 * 1024 * 1024,
22070
- // 10MB
22382
+ maxFileSizeBytes = DEFAULT_MAX_FILE_SIZE_BYTES,
22071
22383
  timeoutMs = 3e4,
22072
22384
  // 30s per file
22385
+ maxExtractedCharsPerFile = DEFAULT_MAX_EXTRACTED_CHARS_PER_FILE,
22386
+ maxExtractedCharsTotal = DEFAULT_MAX_EXTRACTED_CHARS_TOTAL,
22073
22387
  onProgress,
22074
22388
  onError
22075
22389
  } = options;
@@ -22079,23 +22393,23 @@ async function preprocessFiles(files, options = {}) {
22079
22393
  extractedContent: null,
22080
22394
  originalFiles: files,
22081
22395
  preprocessedFileIds: [],
22396
+ fileStatuses: [],
22082
22397
  metadata: { processedCount: 0, skippedCount: 0, errorCount: 0 }
22083
22398
  };
22084
22399
  }
22085
- if (processors !== void 0 && processors !== null && processors.length === 0) {
22086
- return {
22087
- extractedContent: null,
22088
- originalFiles: files,
22089
- preprocessedFileIds: [],
22090
- metadata: { processedCount: 0, skippedCount: 0, errorCount: 0 }
22091
- };
22092
- }
22093
- if (processors === null) {
22400
+ if (processors === null || processors?.length === 0) {
22401
+ const fileStatuses2 = files.filter((file) => !isImageFile(file)).map((file) => ({
22402
+ fileId: file.id,
22403
+ fileName: file.name,
22404
+ status: "skipped",
22405
+ reason: "unsupported_type"
22406
+ }));
22094
22407
  return {
22095
22408
  extractedContent: null,
22096
22409
  originalFiles: files,
22097
22410
  preprocessedFileIds: [],
22098
- metadata: { processedCount: 0, skippedCount: 0, errorCount: 0 }
22411
+ fileStatuses: fileStatuses2,
22412
+ metadata: { processedCount: 0, skippedCount: fileStatuses2.length, errorCount: 0 }
22099
22413
  };
22100
22414
  }
22101
22415
  let registry2;
@@ -22113,28 +22427,38 @@ async function preprocessFiles(files, options = {}) {
22113
22427
  const extractedTexts = [];
22114
22428
  const allImageUrls = [];
22115
22429
  const preprocessedFileIds = [];
22430
+ const fileStatuses = [];
22116
22431
  let processedCount = 0;
22117
22432
  let skippedCount = 0;
22118
22433
  let errorCount = 0;
22434
+ let totalChars = 0;
22435
+ const overBudgetFiles = [];
22119
22436
  for (let i = 0; i < files.length; i++) {
22120
22437
  const file = files[i];
22438
+ const label = `#${i + 1} (${file.type || "unknown type"}, ${file.size} bytes)`;
22439
+ const skip = (reason) => {
22440
+ skippedCount++;
22441
+ fileStatuses.push({ fileId: file.id, fileName: file.name, status: "skipped", reason });
22442
+ };
22121
22443
  onProgress?.(i + 1, files.length, file.name);
22444
+ const processor = registry2.findProcessor(file);
22445
+ if (!processor && isImageFile(file)) continue;
22122
22446
  if (file.size > maxFileSizeBytes) {
22123
22447
  logger.info(
22124
- `[preprocessFiles] Skipping "${file.name}" \u2014 exceeds ${maxFileSizeBytes} byte limit`
22448
+ `[preprocessFiles] Skipping file ${label} \u2014 exceeds ${maxFileSizeBytes} byte limit`
22125
22449
  );
22126
- skippedCount++;
22450
+ skip("too_large");
22127
22451
  continue;
22128
22452
  }
22129
- const processor = registry2.findProcessor(file);
22130
22453
  if (!processor) {
22131
- skippedCount++;
22454
+ skip("unsupported_type");
22132
22455
  continue;
22133
22456
  }
22457
+ let timer;
22134
22458
  try {
22135
22459
  if (!file.url) {
22136
- logger.info(`[preprocessFiles] Skipping "${file.name}" \u2014 no data URL available`);
22137
- skippedCount++;
22460
+ logger.info(`[preprocessFiles] Skipping file ${label} \u2014 no data URL available`);
22461
+ skip("no_data");
22138
22462
  continue;
22139
22463
  }
22140
22464
  const fileWithData = {
@@ -22143,40 +22467,76 @@ async function preprocessFiles(files, options = {}) {
22143
22467
  };
22144
22468
  const result = await Promise.race([
22145
22469
  processor.process(fileWithData),
22146
- new Promise(
22147
- (_, reject) => setTimeout(() => reject(new Error(`Timed out processing ${file.name}`)), timeoutMs)
22148
- )
22470
+ new Promise((_, reject) => {
22471
+ timer = setTimeout(() => reject(new ProcessingTimeoutError(timeoutMs)), timeoutMs);
22472
+ })
22149
22473
  ]);
22150
22474
  if (result && result.extractedText.trim()) {
22151
- const formattedContent = formatExtractedContent(
22152
- file.name,
22153
- result.extractedText,
22154
- result.format
22155
- );
22156
- extractedTexts.push(formattedContent);
22475
+ const separatorLength = extractedTexts.length > 0 ? FILE_SEPARATOR.length : 0;
22476
+ const remaining = maxExtractedCharsTotal - totalChars - separatorLength - formatExtractedContent(file.name, "", result.format).length;
22157
22477
  preprocessedFileIds.push(file.id);
22158
22478
  processedCount++;
22159
- if (result.imageDataUrls && result.imageDataUrls.length > 0) {
22160
- const remaining = MAX_TOTAL_IMAGES - allImageUrls.length;
22161
- if (remaining > 0) {
22162
- allImageUrls.push(...result.imageDataUrls.slice(0, remaining));
22163
- }
22479
+ const fullLength = result.extractedText.length;
22480
+ if (fullLength > remaining && remaining - truncationMarker2(fullLength, fullLength, file.name).length <= 0) {
22481
+ overBudgetFiles.push(file.name);
22482
+ fileStatuses.push({ fileId: file.id, fileName: file.name, status: "truncated" });
22483
+ continue;
22164
22484
  }
22485
+ const images = result.imageDataUrls ?? [];
22486
+ const keptImages = Math.min(
22487
+ images.length,
22488
+ Math.max(0, MAX_TOTAL_IMAGES - allImageUrls.length)
22489
+ );
22490
+ allImageUrls.push(...images.slice(0, keptImages));
22491
+ const text12 = rewriteImageNote(result, file.name, keptImages);
22492
+ const fitsWhole = text12.length <= Math.min(maxExtractedCharsPerFile, remaining);
22493
+ const limit = fitsWhole ? text12.length : Math.min(
22494
+ maxExtractedCharsPerFile,
22495
+ remaining - truncationMarker2(text12.length, text12.length, file.name).length
22496
+ );
22497
+ const capped = truncateText(text12, Math.max(0, limit), file.name);
22498
+ const formattedContent = formatExtractedContent(file.name, capped.text, result.format);
22499
+ totalChars += separatorLength + formattedContent.length;
22500
+ extractedTexts.push(formattedContent);
22501
+ const truncated = capped.truncated || keptImages < images.length || result.metadata?.truncated === true;
22502
+ fileStatuses.push({
22503
+ fileId: file.id,
22504
+ fileName: file.name,
22505
+ status: truncated ? "truncated" : keptImages > 0 ? "rendered_as_images" : "extracted"
22506
+ });
22165
22507
  } else {
22166
- skippedCount++;
22508
+ skip("empty");
22167
22509
  }
22168
22510
  } catch (error) {
22169
22511
  errorCount++;
22170
- logger.error(`[preprocessFiles] Error processing "${file.name}":`, error);
22512
+ const timedOut = error instanceof ProcessingTimeoutError;
22513
+ fileStatuses.push({
22514
+ fileId: file.id,
22515
+ fileName: file.name,
22516
+ status: "failed",
22517
+ reason: timedOut ? "timeout" : "error"
22518
+ });
22519
+ logger.error(
22520
+ `[preprocessFiles] Error processing file ${label} with ${processor.name}:`,
22521
+ error
22522
+ );
22171
22523
  onError?.(file.name, error instanceof Error ? error : new Error(String(error)));
22524
+ } finally {
22525
+ clearTimeout(timer);
22172
22526
  }
22173
22527
  }
22174
- const extractedContent = extractedTexts.length > 0 ? extractedTexts.join("\n\n---\n\n") : null;
22528
+ if (overBudgetFiles.length > 0) {
22529
+ extractedTexts.push(
22530
+ `[truncated: the attachment text limit (${maxExtractedCharsTotal} characters) was reached; the contents of ${overBudgetFiles.join(", ")} were not included]`
22531
+ );
22532
+ }
22533
+ const extractedContent = extractedTexts.length > 0 ? extractedTexts.join(FILE_SEPARATOR) : null;
22175
22534
  return {
22176
22535
  extractedContent,
22177
22536
  imageContentUrls: allImageUrls.length > 0 ? allImageUrls : void 0,
22178
22537
  originalFiles: keepOriginalFiles ? files : void 0,
22179
22538
  preprocessedFileIds,
22539
+ fileStatuses,
22180
22540
  metadata: { processedCount, skippedCount, errorCount }
22181
22541
  };
22182
22542
  }
@@ -23533,7 +23893,7 @@ async function storedToLlmapiMessage(stored, encryptionKey, resolveMediaByIds) {
23533
23893
  } else if (encryptionKey && isOPFSSupported()) {
23534
23894
  try {
23535
23895
  const result = await readEncryptedFile(file.id, encryptionKey);
23536
- if (result) {
23896
+ if (result && result.blob.type.startsWith("image/")) {
23537
23897
  const dataUri = await blobToDataUri(result.blob);
23538
23898
  imageParts.push({
23539
23899
  type: "image_url",
@@ -23706,6 +24066,7 @@ function useChatStorage(options) {
23706
24066
  minContentLength = DEFAULT_MIN_CONTENT_LENGTH,
23707
24067
  mcpR2Domain = MCP_R2_DOMAIN,
23708
24068
  preProcessors,
24069
+ smoothing,
23709
24070
  piiRedaction,
23710
24071
  onPiiRedacted,
23711
24072
  nerDetector,
@@ -24253,6 +24614,7 @@ function useChatStorage(options) {
24253
24614
  onToolCallArgumentsDelta,
24254
24615
  apiType,
24255
24616
  preProcessors,
24617
+ smoothing,
24256
24618
  piiRedaction: resolvedPiiRedaction,
24257
24619
  onPiiRedacted
24258
24620
  });
@@ -24474,7 +24836,8 @@ function useChatStorage(options) {
24474
24836
  conversationId: explicitConversationId,
24475
24837
  parentMessageId,
24476
24838
  assistantUniqueId,
24477
- piiRedaction: requestPiiRedaction
24839
+ piiRedaction: requestPiiRedaction,
24840
+ onFileProcessingResult
24478
24841
  } = args;
24479
24842
  const resolvePiiForCall = (conversationIdForCall) => {
24480
24843
  const { redactor, forInnerSend } = resolveCallPii(
@@ -24670,9 +25033,12 @@ function useChatStorage(options) {
24670
25033
  const contentForStorage = resolveStoredUserContent(storedUserContent, extracted.content);
24671
25034
  const filesForStorage = files ?? extracted.files;
24672
25035
  let fileContextForRequest;
25036
+ let fileContextIsCurrentTurn = false;
24673
25037
  let preprocessedFileIds = [];
24674
25038
  let imageContentUrls;
25039
+ let fileProcessingNotes = null;
24675
25040
  if (filesForStorage && filesForStorage.length > 0) {
25041
+ let fileStatuses;
24676
25042
  try {
24677
25043
  const preprocessingResult = await preprocessFiles(filesForStorage, {
24678
25044
  processors: fileProcessors,
@@ -24680,16 +25046,27 @@ function useChatStorage(options) {
24680
25046
  });
24681
25047
  if (preprocessingResult.extractedContent) {
24682
25048
  fileContextForRequest = preprocessingResult.extractedContent;
25049
+ fileContextIsCurrentTurn = true;
24683
25050
  preprocessedFileIds = preprocessingResult.preprocessedFileIds;
24684
25051
  }
24685
25052
  if (preprocessingResult.imageContentUrls?.length) {
24686
25053
  imageContentUrls = preprocessingResult.imageContentUrls;
24687
25054
  }
25055
+ fileStatuses = preprocessingResult.fileStatuses;
24688
25056
  } catch (err) {
24689
25057
  getLogger().error(
24690
25058
  "[sendMessage] File preprocessing failed \u2014 continuing without file context:",
24691
25059
  err
24692
25060
  );
25061
+ fileStatuses = filesForStorage.filter((f) => !(f.type ?? "").toLowerCase().startsWith("image/")).map((f) => ({ fileId: f.id, fileName: f.name, status: "failed", reason: "error" }));
25062
+ }
25063
+ fileProcessingNotes = formatFileProcessingNotes(fileStatuses, {
25064
+ maxFileSizeBytes: fileProcessingOptions?.maxFileSizeBytes
25065
+ });
25066
+ try {
25067
+ onFileProcessingResult?.(fileStatuses);
25068
+ } catch (err) {
25069
+ getLogger().warn("[sendMessage] onFileProcessingResult threw \u2014 ignoring:", err);
24693
25070
  }
24694
25071
  }
24695
25072
  let convId;
@@ -24863,7 +25240,11 @@ function useChatStorage(options) {
24863
25240
  if (part.type === "text") return true;
24864
25241
  if (part.type === "input_file" && part.file) {
24865
25242
  const fileId = part.file.file_id;
24866
- return !fileId || !preprocessedFileIds.includes(fileId);
25243
+ if (fileId) return !preprocessedFileIds.includes(fileId);
25244
+ const { file_data: fileData, file_url: fileUrl } = part.file;
25245
+ return !filesForStorage?.some(
25246
+ (f) => preprocessedFileIds.includes(f.id) && !!f.url && (f.url === fileData || f.url === fileUrl)
25247
+ );
24867
25248
  }
24868
25249
  if (part.type === "image_url" && part.image_url?.url) {
24869
25250
  const matchesPreprocessed = filesForStorage?.some(
@@ -24898,6 +25279,13 @@ function useChatStorage(options) {
24898
25279
  };
24899
25280
  }
24900
25281
  }
25282
+ const currentTurnFileText = [
25283
+ fileContextIsCurrentTurn ? fileContextForRequest : void 0,
25284
+ fileProcessingNotes
25285
+ ].filter(Boolean).join("\n\n");
25286
+ if (currentTurnFileText) {
25287
+ messagesToSend = attachFileContextToLastUserMessage(messagesToSend, currentTurnFileText);
25288
+ }
24901
25289
  let userFileIds = [];
24902
25290
  if (filesForStorage && filesForStorage.length > 0 && walletAddress && isEncryptionReady()) {
24903
25291
  userFileIds = await storeUserFilesInOPFS(filesForStorage, walletAddress, convId);
@@ -24908,7 +25296,11 @@ function useChatStorage(options) {
24908
25296
  content: contentForStorage,
24909
25297
  fileIds: userFileIds.length > 0 ? userFileIds : void 0,
24910
25298
  model,
24911
- // Store extracted file content in thinking field for retrieval in follow-up messages
25299
+ // Store the file context — this turn's, or the one recalled from an earlier row — in the
25300
+ // thinking field for retrieval in follow-up messages. Re-storing recalled context is the
25301
+ // carry-forward: recall only scans the last `maxHistoryMessages` rows, so without it a
25302
+ // file's text is lost once the turn that attached it scrolls out of the window. Failure
25303
+ // notes are never stored: only `[Extracted content from …]` text, which recall keys on.
24912
25304
  thinking: fileContextForRequest,
24913
25305
  parentMessageId
24914
25306
  };
@@ -25066,7 +25458,7 @@ function useChatStorage(options) {
25066
25458
  headers,
25067
25459
  memoryContext,
25068
25460
  searchContext,
25069
- fileContext: fileContextForRequest,
25461
+ fileContext: fileContextIsCurrentTurn ? void 0 : fileContextForRequest,
25070
25462
  toolGuidance: computeToolGuidance(
25071
25463
  filteredServerTools,
25072
25464
  filteredClientTools,
@@ -36339,8 +36731,10 @@ function renderAnumaJsx(jsx3) {
36339
36731
  appFileToStored,
36340
36732
  archiveVaultMemoryOp,
36341
36733
  assembleMemoryContext,
36734
+ attachFileContextToLastUserMessage,
36342
36735
  backfillMemoryEntityUserIdsOp,
36343
36736
  backfillMemoryTopicsOp,
36737
+ buildAttachedFilesText,
36344
36738
  capHopsForDensity,
36345
36739
  chatStorageMigrations,
36346
36740
  chatStorageSchema,
@@ -36446,6 +36840,7 @@ function renderAnumaJsx(jsx3) {
36446
36840
  findById,
36447
36841
  findMatchingTools,
36448
36842
  findParentOfId,
36843
+ formatFileProcessingNotes,
36449
36844
  generateConversationId,
36450
36845
  generateEmbedding,
36451
36846
  generateEmbeddings,
@@ -36545,6 +36940,7 @@ function renderAnumaJsx(jsx3) {
36545
36940
  insertAfterId,
36546
36941
  insertChild,
36547
36942
  isAnumaTag,
36943
+ isAttachedFilesText,
36548
36944
  isCalendarCallback,
36549
36945
  isDegradedTopicSkip,
36550
36946
  isDriveCallback,