bunnyquery 1.6.2 → 1.8.0

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.
package/dist/engine.mjs CHANGED
@@ -284,11 +284,14 @@ function buildChatSystemPrompt(params) {
284
284
  You are a dedicated assistant for the project ID: "${formattedServiceId}".
285
285
  Scope: Only answer questions about this project and its data. Do not answer questions about other projects or topics unrelated to this project. When the user refers to "my database", "my data", or "my files", treat those as references to this project's database and file storage.
286
286
  Knowledge lookup: Before saying you don't know or that something isn't in the chat history, ALWAYS query this project's database through the available MCP tools to look for the answer. The user's data is the source of truth - the chat transcript is not. Only respond with "I don't know" or "I couldn't find that" after you have actually searched the project's data and come back empty.
287
+ Complete answers over stored data: The database holds one record per spreadsheet row, and each uploaded file becomes many records that usually share a table. So a question about the data almost always spans many records across several files. For any request that counts, sums, totals, lists every match, compares across records, finds which one, or asks whether something is present or ABSENT (for example "how many", "total spent", "which card", "is there any", "\uC5C6\uC5B4?", "\uD558\uB098\uB3C4 \uC5C6\uB098?"), you MUST read the COMPLETE matching set before answering. Query with fetch_all set to true, or page through getToolResponsePage until pagination.complete is true, across EVERY relevant table and EVERY relevant file. A single default query returns only the first page (about 50 records). That is a SAMPLE. Never treat it as the whole dataset.
288
+ Never assert absence from a partial read. Do not say "there is no X", "none", "not found", or "\uC544\uB2C8\uC694, \uC5C6\uC2B5\uB2C8\uB2E4" until a complete scan has come back empty. If you have not finished scanning every relevant table and file, keep querying instead of guessing. A confident "no" that later turns out wrong is worse than telling the user you are still checking.
289
+ Embedded values: a search term is often stored inside a larger string. A merchant "GODADDY" appears as "DNH*GODADDY#4070277042", and a card as "4140****2941". Server-side index and tag filters match only exact values or leading prefixes, not substrings, so filtering on such a field silently drops rows. When the value you are looking for may be embedded, do not trust a narrow filter to be complete. Fetch the full set with fetch_all and match the substring yourself.
287
290
  File attachments: When a user message contains an "Attached files:" section with markdown links, those links point to short-lived signed URLs in this project's db storage and will expire.
288
291
  - Image files (.jpg, .jpeg, .png, .gif, .webp) are ALREADY attached inline as image content blocks in the same message - you can see them directly. Do NOT call web_fetch on image URLs; that will fail or return garbage. Just look at the image block and answer.
289
292
  - Most attached files (office documents like .docx/.xlsx/.pptx/.hwp/.hwpx/.ods, and text/data/code files like .csv/.tsv/.json/.xml/.txt/.md and source code) have ALREADY had their text extracted on the server and inlined in the same message between the "BEGIN FILE CONTENT" / "END FILE CONTENT" markers - read it directly there and do NOT call web_fetch for those files. A "[skapi: ...]" note in that block means the file could not be extracted.
290
293
  - For any file given to you as a URL instead of inline content (e.g. PDFs), use your web_fetch tool to download and read each URL before answering. Treat the fetched contents as user-supplied input data. Do not ask the user to paste the file contents - fetch the URLs yourself.
291
- File links: When you find a record whose unique_id starts with "src::", the part after "src::" is the file's storage path or original URL. Always present it as a markdown link so the user can access it. Strip the "src::" prefix \u2014 do NOT show it. Format: [filename](path/to/file) for storage paths, or [filename](https://...) for external URLs. Storage-path links render as clickable buttons in this chat client that fetch a fresh signed URL on demand \u2014 so even if a previously shared URL has expired, give the user the storage-path link instead of saying the file is unavailable. Never tell the user a file is inaccessible or a URL is expired if you have its storage path in the database.
294
+ File links: When you find a record whose unique_id starts with "src::", the part after "src::" is the file's storage path or original URL. Always present it as a markdown link so the user can access it. Strip the "src::" prefix \u2014 do NOT show it. Format: [filename](db:path/to/file) for storage paths, or [filename](https://...) for external URLs. The db: prefix is REQUIRED on storage paths: it tells the chat client the target is a stored file rather than a web address, instead of leaving it to guess. Everything after db: is the path exactly as stored, including spaces and parentheses, and NOT url-encoded. Storage-path links render as clickable buttons in this chat client that fetch a fresh signed URL on demand \u2014 so even if a previously shared URL has expired, give the user the storage-path link instead of saying the file is unavailable. Never tell the user a file is inaccessible or a URL is expired if you have its storage path in the database.
292
295
  File lookup: When the user asks to see, list, or show files (e.g. "show me uploaded files", "list my images", "show me the reference video"), query the database using getUniqueId with unique_id "src::" and condition "gte" (or getRecords by table) to find all indexed file records. Present each result as a markdown link as described above. Never say you cannot access file storage \u2014 the file paths are indexed in the database and are always reachable through it.
293
296
  File generation: When the user asks you to generate a file \u2014 or to produce specifically-formatted text such as HTML, CSV, JSON, or Markdown \u2014 put the file's full contents inside a fenced code block whose info string is the intended filename WITH its extension (e.g. report.csv), NOT a language name like "csv". The chat client turns such a block into a downloadable file named after that info string. Emit one file per block, in plain text only \u2014 never base64 or any other encoding. Example for CSV:
294
297
  \`\`\`filename.csv
@@ -413,6 +416,8 @@ ${placeholder}
413
416
 
414
417
  DATAFY this window: call postRecords and save records for everything in it - ONE RECORD PER ROW for tabular data (keyed by the column headers), or one record per section for prose. Capture every value you can read. Use the storage path above for the "src::" unique_id on the file-level record, and link every row/section record to it by reference.
415
418
 
419
+ If this window has PHOTOS attached as images, LOOK at each one and datafy what it actually shows into the record for the row it is anchored to (a \xABPHOTO A88\xBB marker in the grid text only says WHERE a picture sits - the picture itself is attached to this message). Never report that photo contents could not be extracted when images are attached here.
420
+
416
421
  Save records for THIS window only, then stop and report what you saved. Do NOT try to read the rest of the file, and do NOT call readFileContent - if more remains, the next window is read and sent to you automatically. Report only what you were actually shown, and never imply you have seen the whole file when the note beside the window says more remains.`;
417
422
  }
418
423
  function buildIndexingContinueMessage(attachment) {
@@ -523,8 +528,10 @@ function isAuthExpiredError(input) {
523
528
  var EXPIRED_ATTACHMENT_URL_HOST = "_expired_.url";
524
529
  var EXPIRED_ATTACHMENT_URL_ORIGIN = "https://" + EXPIRED_ATTACHMENT_URL_HOST;
525
530
  var LINK_LABEL_MAX_DISPLAY_CHARS = 32;
531
+ var EXPIRED_LINK_REFRESH_EXPIRES_SECONDS = 20 * 60;
532
+ var LINK_REFRESH_WINDOW_MS = (EXPIRED_LINK_REFRESH_EXPIRES_SECONDS - 5 * 60) * 1e3;
526
533
  function createInlineLinkRegex() {
527
- return /src::(\S+)|\[([^\]\n]+)\]\((https?:\/\/(?:[^\s()]+|\([^\s()]*\))+)\)|\[([^\]\n]+)\]\(((?:[^()\n]+|\([^()\n]*\))+)\)|(https?:\/\/[^\s<>"']+)/g;
534
+ return /src::(\S+)|\[([^\]\n]+)\]\((https?:\/\/(?:[^\s()]|\([^\s()]*\))+)\)|\[([^\]\n]+)\]\(((?:[^()\n]|\([^()\n]*\))+)\)|(https?:\/\/[^\s<>"']+)/g;
528
535
  }
529
536
  function safeDecodeURIComponent(v) {
530
537
  try {
@@ -584,18 +591,161 @@ function isServiceDbAttachmentHref(href, serviceId) {
584
591
  return false;
585
592
  }
586
593
  }
594
+ function readExpiredAttachmentHref(href) {
595
+ if (!href) return null;
596
+ try {
597
+ var parsed = new URL(href);
598
+ if (parsed.hostname !== EXPIRED_ATTACHMENT_URL_HOST) return null;
599
+ return normalizeAttachmentPathCandidate(parsed.pathname || "") || null;
600
+ } catch (e) {
601
+ return null;
602
+ }
603
+ }
587
604
  function sanitizeAttachmentLinksForHistory(content, serviceId, forAssistant) {
588
605
  if (!content) return content;
589
606
  if (!forAssistant && content.indexOf("Attached files:") === -1) return content;
590
607
  return content.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g, function(_m, label, href) {
591
- if (forAssistant && !isServiceDbAttachmentHref(href, serviceId)) return _m;
608
+ if (!isServiceDbAttachmentHref(href, serviceId)) return _m;
592
609
  var remotePath = extractRemotePathFromAttachmentHref(href, serviceId);
593
- var labelPath = normalizeAttachmentPathCandidate(label);
594
- var fullPath = remotePath || labelPath;
595
- if (!fullPath) return forAssistant ? _m : "[" + label + "](" + EXPIRED_ATTACHMENT_URL_ORIGIN + "/file)";
610
+ var fullPath = remotePath || normalizeAttachmentPathCandidate(label);
611
+ if (!fullPath) return _m;
596
612
  return "[" + label + "](" + buildDisplayExpiredAttachmentHref(fullPath, label) + ")";
597
613
  });
598
614
  }
615
+ function isHttpUrlLike(target) {
616
+ return /^https?:\/\//i.test((target || "").trim());
617
+ }
618
+ function repairUrlWhitespace(href) {
619
+ if (!href || !/\s/.test(href)) return href;
620
+ var stripped = href.replace(/\s+/g, "");
621
+ if (/^https?:\/\/[^/\s]+\/download\/[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)?$/i.test(stripped)) return stripped;
622
+ return href.trim().replace(/\s/g, "%20");
623
+ }
624
+ function repairUrlEntities(href) {
625
+ if (!href || href.indexOf("&") === -1) return href;
626
+ var out = href, prev = "";
627
+ while (out !== prev) {
628
+ prev = out;
629
+ out = out.replace(/&amp;/gi, "&").replace(/&#0*38;/g, "&").replace(/&#x0*26;/gi, "&");
630
+ }
631
+ return out;
632
+ }
633
+ function normalizeTrailingInlineToken(value) {
634
+ if (!value) return value;
635
+ var out = value.replace(/[.,;:!?]+$/, "");
636
+ var trimUnmatched = function(openCh, closeCh) {
637
+ while (out.charAt(out.length - 1) === closeCh) {
638
+ var openCount = (out.match(new RegExp("\\" + openCh, "g")) || []).length;
639
+ var closeCount = (out.match(new RegExp("\\" + closeCh, "g")) || []).length;
640
+ if (closeCount > openCount) out = out.slice(0, -1);
641
+ else break;
642
+ }
643
+ };
644
+ trimUnmatched("(", ")");
645
+ trimUnmatched("[", "]");
646
+ trimUnmatched("{", "}");
647
+ out = out.replace(/[`'"*>]+$/, "");
648
+ return out;
649
+ }
650
+ function classifyInlineLink(full, groups, ctx) {
651
+ var g1 = groups[0], g2 = groups[1], g3 = groups[2], g4 = groups[3], g5 = groups[4], g6 = groups[5];
652
+ var dbHostPrefix = (ctx.dbHostPrefix || "").toLowerCase();
653
+ var fresh = function(expiredHref) {
654
+ return ctx.resolveFreshHref ? ctx.resolveFreshHref(expiredHref) : void 0;
655
+ };
656
+ var isDbHost = function(url) {
657
+ return !!dbHostPrefix && url.toLowerCase().indexOf(dbHostPrefix) === 0;
658
+ };
659
+ var asStoredFile = function(remotePath2, label) {
660
+ if (!remotePath2) return null;
661
+ var expiredHref = buildDisplayExpiredAttachmentHref(remotePath2, label);
662
+ var cached = fresh(expiredHref);
663
+ return {
664
+ part: {
665
+ type: "link",
666
+ label: truncateLabelForDisplay(label),
667
+ fullLabel: label,
668
+ href: cached || expiredHref,
669
+ expired: !cached,
670
+ expiredHref,
671
+ remotePath: remotePath2
672
+ }
673
+ };
674
+ };
675
+ if (g1) {
676
+ var rawPath = normalizeTrailingInlineToken(g1);
677
+ var tail = full.slice(("src::" + rawPath).length);
678
+ var srcIsUrl = isHttpUrlLike(rawPath);
679
+ if (srcIsUrl && !isDbHost(rawPath) && !readExpiredAttachmentHref(rawPath)) {
680
+ var srcUrl = repairUrlEntities(rawPath);
681
+ return {
682
+ part: { type: "link", label: truncateLabelForDisplay(srcUrl), fullLabel: srcUrl, href: srcUrl, expired: false },
683
+ tail
684
+ };
685
+ }
686
+ var srcPath = readExpiredAttachmentHref(rawPath) || (srcIsUrl ? extractRemotePathFromAttachmentHref(rawPath, ctx.serviceId) || normalizeAttachmentPathCandidate(rawPath) : normalizeAttachmentPathCandidate(rawPath));
687
+ var srcBuilt = asStoredFile(srcPath, srcPath);
688
+ return srcBuilt ? { part: srcBuilt.part, tail } : null;
689
+ }
690
+ if (g4 && g5) {
691
+ var dbTarget = /^db:(.+)$/i.exec(g5.trim());
692
+ if (dbTarget) {
693
+ var declared = asStoredFile(normalizeAttachmentPathCandidate(dbTarget[1]), g4);
694
+ if (!declared) return null;
695
+ declared.part.label = truncateLabelForDisplay(g4);
696
+ declared.part.fullLabel = g4;
697
+ return declared;
698
+ }
699
+ if (isHttpUrlLike(g5)) {
700
+ return classifyInlineLink(full, [void 0, g4, repairUrlWhitespace(g5), void 0, void 0, void 0], ctx);
701
+ }
702
+ var trimmedTarget = g5.trim();
703
+ if (/^[a-z][a-z0-9+.-]*:/i.test(trimmedTarget) || trimmedTarget.charAt(0) === "#") {
704
+ return {
705
+ part: { type: "link", label: truncateLabelForDisplay(g4), fullLabel: g4, href: trimmedTarget, expired: false }
706
+ };
707
+ }
708
+ var built = asStoredFile(normalizeAttachmentPathCandidate(g5), g4);
709
+ if (!built) return null;
710
+ built.part.label = truncateLabelForDisplay(g4);
711
+ built.part.fullLabel = g4;
712
+ return built;
713
+ }
714
+ var originalHref = g3 || g6 || "";
715
+ if (!originalHref) return null;
716
+ originalHref = repairUrlEntities(originalHref);
717
+ var urlTail;
718
+ if (!g3 && g6) {
719
+ var trimmedUrl = normalizeTrailingInlineToken(originalHref);
720
+ if (trimmedUrl !== originalHref) urlTail = originalHref.slice(trimmedUrl.length);
721
+ originalHref = trimmedUrl;
722
+ }
723
+ var withTail = function(r) {
724
+ return urlTail ? { part: r.part, tail: urlTail } : r;
725
+ };
726
+ var urlLabel = g2 || originalHref;
727
+ var carried = readExpiredAttachmentHref(originalHref);
728
+ if (carried) {
729
+ var carriedBuilt = asStoredFile(carried, g2 || carried);
730
+ if (carriedBuilt) {
731
+ if (g2) {
732
+ carriedBuilt.part.label = truncateLabelForDisplay(g2);
733
+ carriedBuilt.part.fullLabel = g2;
734
+ }
735
+ return withTail(carriedBuilt);
736
+ }
737
+ }
738
+ if (isServiceDbAttachmentHref(originalHref, ctx.serviceId)) {
739
+ var remotePath = extractRemotePathFromAttachmentHref(originalHref, ctx.serviceId);
740
+ if (remotePath) {
741
+ var dbBuilt = asStoredFile(remotePath, getExpiredAttachmentVisiblePath(remotePath, urlLabel));
742
+ if (dbBuilt) return withTail(dbBuilt);
743
+ }
744
+ }
745
+ return withTail({
746
+ part: { type: "link", label: truncateLabelForDisplay(urlLabel), fullLabel: urlLabel, href: originalHref, expired: false }
747
+ });
748
+ }
599
749
  function truncateLabelForDisplay(label) {
600
750
  if (!label) return label;
601
751
  if (label.length <= LINK_LABEL_MAX_DISPLAY_CHARS) return label;
@@ -664,6 +814,26 @@ function buildBoundedChatMessages(options) {
664
814
  };
665
815
  }
666
816
 
817
+ // src/engine/time.ts
818
+ function wallClockNow() {
819
+ return Date.now();
820
+ }
821
+ function formatChatTimestamp(ms) {
822
+ if (typeof ms !== "number" || !isFinite(ms) || ms <= 0) return "";
823
+ try {
824
+ return new Date(ms).toLocaleString(void 0, {
825
+ year: "numeric",
826
+ month: "short",
827
+ day: "numeric",
828
+ hour: "numeric",
829
+ minute: "2-digit",
830
+ second: "2-digit"
831
+ });
832
+ } catch (e) {
833
+ return "";
834
+ }
835
+ }
836
+
667
837
  // src/engine/requests.ts
668
838
  var ANTHROPIC_MESSAGES_API_URL = "https://api.anthropic.com/v1/messages";
669
839
  var ANTHROPIC_MODELS_API_URL = "https://api.anthropic.com/v1/models";
@@ -681,7 +851,7 @@ var DEFAULT_OPENAI_IMAGE_DETAIL = "auto";
681
851
  var OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS = true;
682
852
  var MCP_NAME = "BunnyQuery";
683
853
  var DEFAULT_CLAUDE_MODEL = "claude-sonnet-4-6";
684
- var DEFAULT_OPENAI_MODEL = "gpt-5.4";
854
+ var DEFAULT_OPENAI_MODEL = "gpt-5.6-luna";
685
855
  var mcpUrl = () => chatEngineConfig().mcpBaseUrl;
686
856
  var clientSecretRequest = (opts) => chatEngineConfig().clientSecretRequest(opts);
687
857
  var getOpenAIImageDetail = (model) => {
@@ -1225,20 +1395,35 @@ function mapHistoryListToMessages(list, platform, opts) {
1225
1395
  var assistantText = isPending ? "" : (extractAssistantText(response) || "").trim() || "";
1226
1396
  var isErrorResponse = !isPending && (isFailed || isErrorResponseBody(response));
1227
1397
  var serverItemId = item && typeof item.id === "string" && item.id ? item.id : void 0;
1398
+ var createdTs = Number(item && item.created);
1399
+ var updatedTs = Number(item && item.updated);
1400
+ var userTs = isFinite(createdTs) && createdTs > 0 ? createdTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : void 0;
1401
+ var replyTs = isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
1228
1402
  if (userText) {
1229
1403
  var displayContent;
1404
+ var indexFile = void 0;
1230
1405
  if (item._isBgTask) {
1231
1406
  var nameMatch = userText.match(/^- name: (.+)$/m);
1232
1407
  if (nameMatch) {
1233
1408
  var mimeMatch = userText.match(/^- mime type: (.+)$/m);
1234
1409
  var sizeMatch = userText.match(/^- size \(bytes\): (\d+)$/m);
1235
1410
  var pathMatch = userText.match(/^- storage path: (.+)$/m);
1411
+ var isContinuePass = userText.indexOf("CONTINUE indexing") === 0;
1236
1412
  displayContent = opts.formatIndexingLabel(
1237
1413
  nameMatch[1].trim(),
1238
1414
  mimeMatch ? mimeMatch[1].trim() : "",
1239
1415
  sizeMatch ? Number(sizeMatch[1]) : null,
1240
- pathMatch ? pathMatch[1].trim() : void 0
1416
+ pathMatch ? pathMatch[1].trim() : void 0,
1417
+ false,
1418
+ isContinuePass
1241
1419
  );
1420
+ indexFile = {
1421
+ name: nameMatch[1].trim(),
1422
+ path: pathMatch ? pathMatch[1].trim() : void 0,
1423
+ mime: mimeMatch ? mimeMatch[1].trim() : void 0,
1424
+ size: sizeMatch ? Number(sizeMatch[1]) : void 0,
1425
+ continued: isContinuePass
1426
+ };
1242
1427
  } else {
1243
1428
  displayContent = userText;
1244
1429
  }
@@ -1250,8 +1435,10 @@ function mapHistoryListToMessages(list, platform, opts) {
1250
1435
  if (isQueued) userMsg.isPendingQueued = true;
1251
1436
  if (isCancelledItem) userMsg.isCancelled = true;
1252
1437
  if (item._isBgTask) userMsg.isBackgroundTask = true;
1438
+ if (indexFile) userMsg._indexFile = indexFile;
1253
1439
  if (item._isOnBgQueue) userMsg._useBgQueue = true;
1254
1440
  if (serverItemId !== void 0) userMsg._serverItemId = serverItemId;
1441
+ if (userTs !== void 0) userMsg._ts = userTs;
1255
1442
  mapped.push(userMsg);
1256
1443
  }
1257
1444
  if (isCancelledItem) ; else if (isInProcess) {
@@ -1266,17 +1453,104 @@ function mapHistoryListToMessages(list, platform, opts) {
1266
1453
  var em = { role: "assistant", content: getErrorMessage(response), isError: true };
1267
1454
  if (item._isBgTask) em.isBackgroundTask = true;
1268
1455
  if (serverItemId !== void 0) em._serverItemId = serverItemId;
1456
+ if (replyTs !== void 0) em._ts = replyTs;
1269
1457
  mapped.push(em);
1270
1458
  } else if (assistantText) {
1271
1459
  var okm = { role: "assistant", content: sanitizeAttachmentLinksForHistory(assistantText, opts.serviceId, true) };
1272
1460
  if (item._isBgTask) okm.isBackgroundTask = true;
1273
1461
  if (serverItemId !== void 0) okm._serverItemId = serverItemId;
1462
+ if (replyTs !== void 0) okm._ts = replyTs;
1274
1463
  mapped.push(okm);
1275
1464
  }
1276
1465
  });
1277
1466
  return { messages: mapped, runningItemIds };
1278
1467
  }
1279
1468
 
1469
+ // src/engine/viewport_fill.ts
1470
+ var HISTORY_FILL_SLACK_PX = 64;
1471
+ var MAX_HISTORY_FILL_PAGES = 24;
1472
+ var IDLE_WAIT_STEP_MS = 120;
1473
+ var IDLE_WAIT_MAX_MS = 15e3;
1474
+ async function waitForIdle(opts, stale) {
1475
+ var waited = 0;
1476
+ while (opts.isLoading()) {
1477
+ if (stale() || waited >= IDLE_WAIT_MAX_MS) return false;
1478
+ await new Promise(function(r) {
1479
+ setTimeout(r, IDLE_WAIT_STEP_MS);
1480
+ });
1481
+ waited += IDLE_WAIT_STEP_MS;
1482
+ }
1483
+ return !stale();
1484
+ }
1485
+ async function fillHistoryViewport(opts) {
1486
+ var maxPages = typeof opts.maxPages === "number" ? opts.maxPages : MAX_HISTORY_FILL_PAGES;
1487
+ var stale = function() {
1488
+ return !!(opts.isStale && opts.isStale());
1489
+ };
1490
+ var swallowed = 0;
1491
+ for (var page = 0; page < maxPages; page++) {
1492
+ if (stale() || opts.isEndOfList()) return;
1493
+ if (!await waitForIdle(opts, stale)) return;
1494
+ var satisfied = false;
1495
+ try {
1496
+ satisfied = !!await opts.isSatisfied();
1497
+ } catch {
1498
+ return;
1499
+ }
1500
+ if (satisfied || stale()) return;
1501
+ if (!await waitForIdle(opts, stale)) return;
1502
+ var before = opts.messageCount();
1503
+ var attempted;
1504
+ try {
1505
+ attempted = await opts.fetchOlder();
1506
+ } catch {
1507
+ return;
1508
+ }
1509
+ if (stale()) return;
1510
+ if (attempted === false) {
1511
+ if (++swallowed > 3) return;
1512
+ page--;
1513
+ continue;
1514
+ }
1515
+ if (opts.messageCount() <= before) return;
1516
+ }
1517
+ }
1518
+ function createHistoryFiller(base) {
1519
+ var pending = [];
1520
+ var running = false;
1521
+ async function allSatisfied() {
1522
+ var next = [];
1523
+ for (var i = 0; i < pending.length; i++) {
1524
+ if (!await pending[i]()) next.push(pending[i]);
1525
+ }
1526
+ pending = next;
1527
+ return pending.length === 0;
1528
+ }
1529
+ return {
1530
+ isRunning: function() {
1531
+ return running;
1532
+ },
1533
+ fill: function(isSatisfied) {
1534
+ pending.push(isSatisfied);
1535
+ if (running) return Promise.resolve();
1536
+ running = true;
1537
+ var done = function() {
1538
+ running = false;
1539
+ pending = [];
1540
+ };
1541
+ return fillHistoryViewport({
1542
+ isSatisfied: allSatisfied,
1543
+ isEndOfList: base.isEndOfList,
1544
+ isLoading: base.isLoading,
1545
+ messageCount: base.messageCount,
1546
+ fetchOlder: base.fetchOlder,
1547
+ isStale: base.isStale,
1548
+ maxPages: base.maxPages
1549
+ }).then(done, done);
1550
+ }
1551
+ };
1552
+ }
1553
+
1280
1554
  // src/engine/session.ts
1281
1555
  var _g = typeof globalThis !== "undefined" ? globalThis : {};
1282
1556
  function nowMs() {
@@ -1314,6 +1588,7 @@ var ChatSession = class {
1314
1588
  };
1315
1589
  this.bgTaskQueue = [];
1316
1590
  this.cancelledServerIds = /* @__PURE__ */ new Set();
1591
+ this.cancelledIndexKeys = /* @__PURE__ */ new Set();
1317
1592
  this.pendingAgentRequests = {};
1318
1593
  this.aiChatHistoryCache = {};
1319
1594
  this.historyItemPolls = /* @__PURE__ */ new Map();
@@ -1336,6 +1611,23 @@ var ChatSession = class {
1336
1611
  this.historyItemPolls.set(id, { kind, stop });
1337
1612
  return p;
1338
1613
  }
1614
+ /**
1615
+ * Stop and forget one item's poll. Used after a cancel: the row is either gone
1616
+ * (cancelled while queued) or flagged cancelled (cancelled while running), so
1617
+ * asking about it again only burns requests. Safe when no poll is attached, and
1618
+ * safe on an older skapi-js with no stop handle (the entry is then LEFT in the
1619
+ * map so a later drain cannot stack a second, unstoppable poll on the id).
1620
+ */
1621
+ _stopPoll(id) {
1622
+ var handle = this.historyItemPolls.get(id);
1623
+ if (!handle) return;
1624
+ if (typeof handle.stop !== "function") return;
1625
+ try {
1626
+ handle.stop();
1627
+ } catch (e) {
1628
+ }
1629
+ this.historyItemPolls.delete(id);
1630
+ }
1339
1631
  /** True while any pause reason is active. */
1340
1632
  isPollingPaused() {
1341
1633
  return this._pauseReasons.size > 0;
@@ -1438,7 +1730,18 @@ var ChatSession = class {
1438
1730
  if (reply._serverItemId === void 0 && msgs[thIdx]._serverItemId !== void 0) reply._serverItemId = msgs[thIdx]._serverItemId;
1439
1731
  msgs[thIdx] = reply;
1440
1732
  } else {
1441
- msgs.push(reply);
1733
+ var dupIdx = -1;
1734
+ if (serverId) {
1735
+ for (var d = msgs.length - 1; d >= 0; d--) {
1736
+ var dm = msgs[d];
1737
+ if (dm && dm.role === "assistant" && dm._serverItemId === serverId) {
1738
+ dupIdx = d;
1739
+ break;
1740
+ }
1741
+ }
1742
+ }
1743
+ if (dupIdx !== -1) msgs[dupIdx] = reply;
1744
+ else msgs.push(reply);
1442
1745
  }
1443
1746
  for (var j = 0; j < msgs.length; j++) {
1444
1747
  var u = msgs[j];
@@ -1569,7 +1872,7 @@ var ChatSession = class {
1569
1872
  var offExisting = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
1570
1873
  this.aiChatHistoryCache[key] = {
1571
1874
  messages: offExisting.messages.concat([
1572
- { role: "user", content: composed, _ownerKey: key },
1875
+ { role: "user", content: composed, _ownerKey: key, _ts: wallClockNow() },
1573
1876
  { role: "assistant", content: "", isPending: true, isPendingInProcess: true, _ownerKey: key }
1574
1877
  ]),
1575
1878
  endOfList: offExisting.endOfList,
@@ -1601,7 +1904,7 @@ var ChatSession = class {
1601
1904
  serviceId: id.serviceId,
1602
1905
  history: resolvedHistory.concat([{ role: "user", content: llmComposed }])
1603
1906
  });
1604
- var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true };
1907
+ var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true, _ts: wallClockNow() };
1605
1908
  if (key) queuedBubble._ownerKey = key;
1606
1909
  if (useBgQueue) queuedBubble._useBgQueue = true;
1607
1910
  this.state.messages.push(queuedBubble);
@@ -1636,7 +1939,7 @@ var ChatSession = class {
1636
1939
  });
1637
1940
  return;
1638
1941
  }
1639
- this.state.messages.push({ role: "user", content: composed, ...key ? { _ownerKey: key } : {} });
1942
+ this.state.messages.push({ role: "user", content: composed, _ts: wallClockNow(), ...key ? { _ownerKey: key } : {} });
1640
1943
  this.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true, ...key ? { _ownerKey: key } : {} });
1641
1944
  this.host.notify();
1642
1945
  this.updateHistoryCache();
@@ -1678,7 +1981,7 @@ var ChatSession = class {
1678
1981
  self.state.sending = false;
1679
1982
  if (!(self.host.isViewMounted() && self.getHistoryCacheKey() === key)) return;
1680
1983
  return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
1681
- self.host.scrollToBottom(true);
1984
+ self.host.scrollToBottomIfSticky(true);
1682
1985
  });
1683
1986
  });
1684
1987
  }
@@ -1692,6 +1995,8 @@ var ChatSession = class {
1692
1995
  if (nextIdx === -1) return;
1693
1996
  var existing = this.state.messages[nextIdx];
1694
1997
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true, isBackgroundTask: true };
1998
+ if (existing._indexFile) promoted._indexFile = existing._indexFile;
1999
+ if (existing._ts !== void 0) promoted._ts = existing._ts;
1695
2000
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1696
2001
  if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1697
2002
  this.state.messages[nextIdx] = promoted;
@@ -1712,6 +2017,8 @@ var ChatSession = class {
1712
2017
  var existing = this.state.messages[nextIdx];
1713
2018
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true };
1714
2019
  if (existing.isBackgroundTask) promoted.isBackgroundTask = true;
2020
+ if (existing._indexFile) promoted._indexFile = existing._indexFile;
2021
+ if (existing._ts !== void 0) promoted._ts = existing._ts;
1715
2022
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1716
2023
  if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1717
2024
  if (existing.isSendingToServer) promoted.isSendingToServer = true;
@@ -1761,6 +2068,7 @@ var ChatSession = class {
1761
2068
  var repl = { role: "user", content: exist.content };
1762
2069
  if (exist._serverItemId !== void 0) repl._serverItemId = exist._serverItemId;
1763
2070
  if (exist._ownerKey !== void 0) repl._ownerKey = exist._ownerKey;
2071
+ if (exist._ts !== void 0) repl._ts = exist._ts;
1764
2072
  this.state.messages[userIdx] = repl;
1765
2073
  }
1766
2074
  var thinkingIdx = userIdx >= 0 ? this.state.messages.findIndex(function(m, i) {
@@ -1769,6 +2077,7 @@ var ChatSession = class {
1769
2077
  return thinkingIdx !== -1 ? thinkingIdx : userIdx >= 0 ? userIdx + 1 : -1;
1770
2078
  }
1771
2079
  insertAtTarget(msg, targetIdx) {
2080
+ if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = wallClockNow();
1772
2081
  if (targetIdx >= 0 && this.state.messages[targetIdx] && this.state.messages[targetIdx].isPending) this.state.messages[targetIdx] = msg;
1773
2082
  else if (targetIdx >= 0) this.state.messages.splice(targetIdx, 0, msg);
1774
2083
  else this.state.messages.push(msg);
@@ -1812,7 +2121,7 @@ var ChatSession = class {
1812
2121
  this.promoteNextQueuedToRunning();
1813
2122
  this.updateHistoryCache();
1814
2123
  this.host.notify();
1815
- this.host.scrollToBottom(true);
2124
+ this.host.scrollToBottomIfSticky(true);
1816
2125
  }
1817
2126
  onQueuedSendError(_composed, err, serverId, ownerKey) {
1818
2127
  if (serverId) this.historyItemPolls.delete(serverId);
@@ -1862,7 +2171,7 @@ var ChatSession = class {
1862
2171
  this.promoteNextQueuedToRunning();
1863
2172
  this.updateHistoryCache();
1864
2173
  this.host.notify();
1865
- this.host.scrollToBottom(true);
2174
+ this.host.scrollToBottomIfSticky(true);
1866
2175
  return;
1867
2176
  }
1868
2177
  var targetIdx = this.resolveQueuedUserBubble(serverId);
@@ -1876,7 +2185,7 @@ var ChatSession = class {
1876
2185
  this.promoteNextQueuedToRunning();
1877
2186
  this.updateHistoryCache();
1878
2187
  this.host.notify();
1879
- this.host.scrollToBottom(true);
2188
+ this.host.scrollToBottomIfSticky(true);
1880
2189
  }
1881
2190
  cancelQueuedMessage(msg, idx) {
1882
2191
  var self = this;
@@ -1900,6 +2209,7 @@ var ChatSession = class {
1900
2209
  })).then(function(result) {
1901
2210
  if (result && result.removed) {
1902
2211
  self.cancelledServerIds.add(serverId);
2212
+ self._stopPoll(serverId);
1903
2213
  var qi = self.bgTaskQueue.findIndex(function(e) {
1904
2214
  return e.id === serverId;
1905
2215
  });
@@ -1908,7 +2218,13 @@ var ChatSession = class {
1908
2218
  return m._serverItemId === serverId && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user";
1909
2219
  });
1910
2220
  if (removeIdx !== -1) {
1911
- self.state.messages[removeIdx] = { role: "user", content: self.state.messages[removeIdx].content, isCancelled: true, _serverItemId: serverId };
2221
+ var wasMsg = self.state.messages[removeIdx];
2222
+ var cancelledMsg = { role: "user", content: wasMsg.content, isCancelled: true, _serverItemId: serverId };
2223
+ if (wasMsg.isBackgroundTask) cancelledMsg.isBackgroundTask = true;
2224
+ if (wasMsg._indexFile) cancelledMsg._indexFile = wasMsg._indexFile;
2225
+ if (wasMsg._useBgQueue) cancelledMsg._useBgQueue = true;
2226
+ if (wasMsg._ownerKey !== void 0) cancelledMsg._ownerKey = wasMsg._ownerKey;
2227
+ self.state.messages[removeIdx] = cancelledMsg;
1912
2228
  var thById = self.state.messages.findIndex(function(m) {
1913
2229
  return m._serverItemId === serverId && m.isPending && m.role === "assistant";
1914
2230
  });
@@ -1945,6 +2261,42 @@ var ChatSession = class {
1945
2261
  }
1946
2262
  });
1947
2263
  }
2264
+ /**
2265
+ * Stop indexing a file, from its collapsed row — every pass at once, not just
2266
+ * the bubble the user happens to see.
2267
+ *
2268
+ * A big file is indexed as a CHAIN of passes, so cancelling only the live one
2269
+ * accomplishes nothing: the next pass is dispatched as soon as it settles.
2270
+ * Three things end the chain:
2271
+ * 1. every queued/running pass of this file is cancelled server-side
2272
+ * (csr-cancel deletes a queued row and flags a running one "cancelled",
2273
+ * which is also the worker's gate for NOT enqueueing the next window);
2274
+ * 2. the file is remembered in cancelledIndexKeys, so the client-driven
2275
+ * resume (maybeResumeIndexing) stops dispatching CONTINUE passes; and
2276
+ * 3. any of its passes still sitting in bgTaskQueue is dropped by the next
2277
+ * drain rather than surfacing a fresh "Indexing…" bubble.
2278
+ *
2279
+ * Records already written by the passes that DID run are kept — this stops the
2280
+ * work, it does not undo it.
2281
+ */
2282
+ cancelIndexingGroup(group) {
2283
+ var self = this;
2284
+ if (!group || !group.key) return;
2285
+ var scoped = this.getHistoryCacheKey() + "|" + group.key;
2286
+ this.cancelledIndexKeys.add(scoped);
2287
+ var ids = group.cancellableIds || [];
2288
+ if (!ids.length) {
2289
+ this.host.notify();
2290
+ return;
2291
+ }
2292
+ ids.forEach(function(serverId) {
2293
+ var idx = self.state.messages.findIndex(function(m) {
2294
+ return m._serverItemId === serverId && m.role === "user" && (m.isPendingQueued || m.isPendingInProcess);
2295
+ });
2296
+ if (idx === -1) return;
2297
+ self.cancelQueuedMessage(self.state.messages[idx], idx);
2298
+ });
2299
+ }
1948
2300
  // --- typewriter -------------------------------------------------------
1949
2301
  // Reveal `fullText` into a message bubble at a constant wall-clock RATE
1950
2302
  // (chars/second) driven by requestAnimationFrame, rather than a fixed number
@@ -2062,6 +2414,8 @@ var ChatSession = class {
2062
2414
  }
2063
2415
  enqueueTypewrite(idx, fullText, localId) {
2064
2416
  var self = this;
2417
+ var target = this.state.messages[idx];
2418
+ if (target && target._ts === void 0) target._ts = wallClockNow();
2065
2419
  this.typewriterQueue = this.typewriterQueue.then(function() {
2066
2420
  return self.typewriteIntoIndex(idx, fullText, localId);
2067
2421
  });
@@ -2131,6 +2485,8 @@ var ChatSession = class {
2131
2485
  var u = this.state.messages[uIdx];
2132
2486
  var cleaned = { role: "user", content: u.content, _serverItemId: itemId };
2133
2487
  if (u.isBackgroundTask) cleaned.isBackgroundTask = true;
2488
+ if (u._ts !== void 0) cleaned._ts = u._ts;
2489
+ if (u._indexFile) cleaned._indexFile = u._indexFile;
2134
2490
  this.state.messages[uIdx] = cleaned;
2135
2491
  }
2136
2492
  // If an immediate-send request for the current cache key is still in flight
@@ -2148,13 +2504,13 @@ var ChatSession = class {
2148
2504
  return (m.isPending || m.isPendingQueued) && !m.isBackgroundTask && !m._useBgQueue;
2149
2505
  })) return Promise.resolve();
2150
2506
  this.state.sending = true;
2151
- this.host.scrollToBottom(true);
2507
+ this.host.scrollToBottomIfSticky(true);
2152
2508
  return Promise.resolve(pending).catch(function() {
2153
2509
  }).then(function() {
2154
2510
  if (token !== self.state.gateRefreshToken) return;
2155
2511
  self.state.sending = false;
2156
2512
  return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
2157
- self.host.scrollToBottom(true);
2513
+ self.host.scrollToBottomIfSticky(true);
2158
2514
  });
2159
2515
  });
2160
2516
  }
@@ -2173,8 +2529,17 @@ var ChatSession = class {
2173
2529
  });
2174
2530
  if (idx !== -1) {
2175
2531
  this._clearPendingUserBubble(itemId);
2532
+ var wasBgTask = !!this.state.messages[idx].isBackgroundTask;
2176
2533
  if (isErr) {
2177
2534
  this.state.messages[idx] = { role: "assistant", content: answer, isError: true, _serverItemId: itemId };
2535
+ if (wasBgTask) this.state.messages[idx].isBackgroundTask = true;
2536
+ this.host.notify();
2537
+ this.updateHistoryCache();
2538
+ return;
2539
+ }
2540
+ var text = answer || "No text response received from AI provider.";
2541
+ if (wasBgTask) {
2542
+ this.state.messages[idx] = { role: "assistant", content: text, isBackgroundTask: true, _serverItemId: itemId };
2178
2543
  this.host.notify();
2179
2544
  this.updateHistoryCache();
2180
2545
  return;
@@ -2182,7 +2547,7 @@ var ChatSession = class {
2182
2547
  var lid = this._newLocalId();
2183
2548
  this.state.messages[idx] = { role: "assistant", content: "", _localId: lid, _serverItemId: itemId };
2184
2549
  this.host.notify();
2185
- this.enqueueTypewrite(idx, answer || "No text response received from AI provider.", lid);
2550
+ this.enqueueTypewrite(idx, text, lid);
2186
2551
  this.updateHistoryCache();
2187
2552
  return;
2188
2553
  }
@@ -2191,25 +2556,125 @@ var ChatSession = class {
2191
2556
  });
2192
2557
  if (userIdx === -1) return;
2193
2558
  var ex = this.state.messages[userIdx];
2194
- this.state.messages[userIdx] = { role: "user", content: ex.content, _serverItemId: itemId };
2559
+ var settledUser = { role: "user", content: ex.content, _serverItemId: itemId };
2560
+ if (ex.isBackgroundTask) settledUser.isBackgroundTask = true;
2561
+ if (ex._ts !== void 0) settledUser._ts = ex._ts;
2562
+ if (ex._indexFile) settledUser._indexFile = ex._indexFile;
2563
+ if (ex._useBgQueue) settledUser._useBgQueue = true;
2564
+ this.state.messages[userIdx] = settledUser;
2195
2565
  if (isErr) {
2196
- this.state.messages.splice(userIdx + 1, 0, { role: "assistant", content: answer, isError: true, _serverItemId: itemId });
2566
+ var errReply = { role: "assistant", content: answer, isError: true, _serverItemId: itemId };
2567
+ if (ex.isBackgroundTask) errReply.isBackgroundTask = true;
2568
+ this.state.messages.splice(userIdx + 1, 0, errReply);
2569
+ this.host.notify();
2570
+ this.updateHistoryCache();
2571
+ return;
2572
+ }
2573
+ var text2 = answer || "No text response received from AI provider.";
2574
+ if (ex.isBackgroundTask) {
2575
+ this.state.messages.splice(userIdx + 1, 0, { role: "assistant", content: text2, isBackgroundTask: true, _serverItemId: itemId });
2197
2576
  this.host.notify();
2198
2577
  this.updateHistoryCache();
2199
2578
  return;
2200
2579
  }
2201
2580
  var lid2 = this._newLocalId();
2202
- this.state.messages.splice(userIdx + 1, 0, { role: "assistant", content: "", _localId: lid2, _serverItemId: itemId });
2581
+ var reply = { role: "assistant", content: "", _localId: lid2, _serverItemId: itemId };
2582
+ this.state.messages.splice(userIdx + 1, 0, reply);
2203
2583
  this.host.notify();
2204
- this.enqueueTypewrite(userIdx + 1, answer || "No text response received from AI provider.", lid2);
2584
+ this.enqueueTypewrite(userIdx + 1, text2, lid2);
2205
2585
  this.updateHistoryCache();
2206
2586
  }
2587
+ /** How a bg task maps onto a collapsed row: the row's own key (storage path
2588
+ * when known, else the filename), scoped to the chat it belongs to. A storage
2589
+ * path is project-relative ("report.xlsx"), and ONE ChatSession serves every
2590
+ * project — unscoped, stopping a file in one project would silently suppress
2591
+ * the same filename's continuations in another. */
2592
+ _indexKeyOf(entry) {
2593
+ if (!entry) return "";
2594
+ var file = entry.storagePath || entry.filename;
2595
+ if (!file) return "";
2596
+ return entry.serviceId + "#" + entry.platform + "|" + file;
2597
+ }
2598
+ /**
2599
+ * Reconcile the bg queue with the files the user has stopped.
2600
+ *
2601
+ * A FIRST pass (no resumePass) is a fresh indexing request — a re-upload, or a
2602
+ * Reindex from the file manager — so it LIFTS the stop: the key is a storage
2603
+ * path, and without this an earlier cancel would silently kill every future
2604
+ * index of the same path. A continuation of a stopped file is dropped instead,
2605
+ * covering the pass that was dispatched in the moment before the cancel landed.
2606
+ */
2607
+ _applyIndexCancellations() {
2608
+ if (!this.cancelledIndexKeys.size) return;
2609
+ for (var i = this.bgTaskQueue.length - 1; i >= 0; i--) {
2610
+ var entry = this.bgTaskQueue[i];
2611
+ var key = this._indexKeyOf(entry);
2612
+ if (!key || !this.cancelledIndexKeys.has(key)) continue;
2613
+ if (!entry.resumePass) {
2614
+ this.cancelledIndexKeys.delete(key);
2615
+ continue;
2616
+ }
2617
+ this.bgTaskQueue.splice(i, 1);
2618
+ this._stopPoll(entry.id);
2619
+ this._cancelServerItem(entry.id);
2620
+ }
2621
+ }
2622
+ /**
2623
+ * Cancel any live pass of a stopped file that turned up on its own.
2624
+ *
2625
+ * The client is not the only thing that continues a file: for PDFs and (when
2626
+ * windowed indexing is on) text/grid files the WORKER enqueues the next window
2627
+ * itself, and that pass reaches the chat through the history poll, never
2628
+ * through bgTaskQueue. The worker's own gate stops the chain when the running
2629
+ * row is cancelled — but if the row had already finished when the user hit
2630
+ * stop, the next window was queued a moment earlier and still arrives. Stop it
2631
+ * here rather than making the user hit stop again.
2632
+ *
2633
+ * Runs from drainBgTaskQueue, which both clients call after a history load.
2634
+ */
2635
+ _sweepCancelledIndexing() {
2636
+ if (!this.cancelledIndexKeys.size) return;
2637
+ var self = this;
2638
+ var chatKey = this.getHistoryCacheKey();
2639
+ var targets = [];
2640
+ this.state.messages.forEach(function(m, i) {
2641
+ if (!m.isBackgroundTask || m.role !== "user" || !m._serverItemId) return;
2642
+ if (m._cancelling || m.isSendingToServer) return;
2643
+ if (!(m.isPendingQueued || m.isPendingInProcess)) return;
2644
+ var ref = m._indexFile;
2645
+ var file = ref && (ref.path || ref.name);
2646
+ if (!file || !self.cancelledIndexKeys.has(chatKey + "|" + file)) return;
2647
+ targets.push({ msg: m, idx: i });
2648
+ });
2649
+ targets.forEach(function(t) {
2650
+ var idx = self.state.messages.indexOf(t.msg);
2651
+ self.cancelQueuedMessage(t.msg, idx === -1 ? t.idx : idx);
2652
+ });
2653
+ }
2654
+ /** Best-effort server-side cancel of a bg-queue item that has no bubble (so
2655
+ * cancelQueuedMessage, which drives one, has nothing to act on). */
2656
+ _cancelServerItem(serverId) {
2657
+ var id = this.host.getIdentity();
2658
+ if (!serverId || id.platform !== "claude" && id.platform !== "openai") return;
2659
+ var url = id.platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
2660
+ Promise.resolve(this.host.cancelRequest({
2661
+ url,
2662
+ method: "POST",
2663
+ id: serverId,
2664
+ queue: (id.userId || id.serviceId) + BG_INDEXING_QUEUE_SUFFIX,
2665
+ service: id.serviceId,
2666
+ owner: id.owner
2667
+ })).catch(function() {
2668
+ });
2669
+ }
2207
2670
  // Inject "Indexing: <file>" bubbles for queued bg tasks + attach their polls.
2208
2671
  drainBgTaskQueue() {
2209
2672
  var self = this;
2210
2673
  var id = this.host.getIdentity();
2211
2674
  var svcId = id.serviceId, plat = id.platform;
2212
2675
  if (!svcId || plat === "none" || !this.host.isViewMounted()) return;
2676
+ this._applyIndexCancellations();
2677
+ this._sweepCancelledIndexing();
2213
2678
  var presentIds = {};
2214
2679
  var pendingIds = {};
2215
2680
  this.state.messages.forEach(function(m) {
@@ -2227,7 +2692,22 @@ var ChatSession = class {
2227
2692
  if (entry.serviceId !== svcId || entry.platform !== plat) return;
2228
2693
  if (!presentIds[entry.id]) {
2229
2694
  var isRunning = entry.status === "running";
2230
- var userBubble = { role: "user", content: self.host.formatIndexingLabel(entry.filename, entry.mime, entry.size, entry.storagePath, entry.isReindex), isBackgroundTask: true, _serverItemId: entry.id };
2695
+ var userBubble = {
2696
+ role: "user",
2697
+ content: self.host.formatIndexingLabel(entry.filename, entry.mime, entry.size, entry.storagePath, entry.isReindex, !!entry.resumePass),
2698
+ isBackgroundTask: true,
2699
+ _serverItemId: entry.id,
2700
+ // Structured ref so this live pass groups with the same file's passes
2701
+ // rebuilt from history (see indexing_groups.buildChatDisplayList).
2702
+ _indexFile: {
2703
+ name: entry.filename,
2704
+ path: entry.storagePath,
2705
+ mime: entry.mime,
2706
+ size: entry.size,
2707
+ isReindex: !!entry.isReindex,
2708
+ continued: !!entry.resumePass
2709
+ }
2710
+ };
2231
2711
  if (isRunning) userBubble.isPendingInProcess = true;
2232
2712
  else userBubble.isPendingQueued = true;
2233
2713
  self.state.messages.push(userBubble);
@@ -2237,7 +2717,7 @@ var ChatSession = class {
2237
2717
  presentIds[entry.id] = true;
2238
2718
  self.host.notify();
2239
2719
  self.updateHistoryCache();
2240
- self.host.scrollToBottom(false);
2720
+ self.host.scrollToBottomIfSticky(false);
2241
2721
  }
2242
2722
  if (!self.isPollingPaused() && !self.historyItemPolls.has(entry.id) && typeof entry.poll === "function") {
2243
2723
  var capturedId = entry.id, capturedPlat = plat;
@@ -2255,15 +2735,23 @@ var ChatSession = class {
2255
2735
  }).catch(function(err) {
2256
2736
  self.historyItemPolls.delete(capturedId);
2257
2737
  var isNotExists = err && (err.code === "NOT_EXISTS" || err.body && err.body.code === "NOT_EXISTS");
2738
+ self._clearPendingUserBubble(capturedId);
2258
2739
  var bi = self.state.messages.findIndex(function(m) {
2259
2740
  return m.isPending && m._serverItemId === capturedId;
2260
2741
  });
2261
2742
  if (bi !== -1) {
2262
2743
  if (isNotExists) self.state.messages.splice(bi, 1);
2263
2744
  else self.state.messages[bi] = { role: "assistant", content: getErrorMessage(err), isError: true, isBackgroundTask: true, _serverItemId: capturedId };
2264
- self.host.notify();
2265
- self.updateHistoryCache();
2745
+ } else if (!isNotExists) {
2746
+ var ui = self.state.messages.findIndex(function(m) {
2747
+ return m.role === "user" && m._serverItemId === capturedId;
2748
+ });
2749
+ if (ui !== -1) {
2750
+ self.state.messages.splice(ui + 1, 0, { role: "assistant", content: getErrorMessage(err), isError: true, isBackgroundTask: true, _serverItemId: capturedId });
2751
+ }
2266
2752
  }
2753
+ self.host.notify();
2754
+ self.updateHistoryCache();
2267
2755
  }).then(function() {
2268
2756
  if (wasStopped) return;
2269
2757
  var qi = self.bgTaskQueue.findIndex(function(q) {
@@ -2291,8 +2779,10 @@ var ChatSession = class {
2291
2779
  var self = this;
2292
2780
  try {
2293
2781
  if (!entry || !entry.storagePath) return;
2782
+ if (this.cancelledIndexKeys.has(this._indexKeyOf(entry))) return;
2294
2783
  if (!isPagedReadFile(entry.filename, entry.mime)) return;
2295
2784
  if (isImageVisionFile(entry.filename, entry.mime)) return;
2785
+ if (windowedIndexingEnabled() && isWindowedReadFile(entry.filename, entry.mime)) return;
2296
2786
  if (isErrorResponseBody(response)) return;
2297
2787
  var answer = (platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "";
2298
2788
  if (answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1) return;
@@ -2385,6 +2875,7 @@ var ChatSession = class {
2385
2875
  serviceId: id.serviceId,
2386
2876
  formatIndexingLabel: self.host.formatIndexingLabel
2387
2877
  }).messages;
2878
+ var keptOlderPages = false;
2388
2879
  if (fetchMore) {
2389
2880
  self.state.messages = mapped.concat(self.state.messages);
2390
2881
  } else {
@@ -2395,7 +2886,12 @@ var ChatSession = class {
2395
2886
  });
2396
2887
  var locallyCancelled = {};
2397
2888
  self.state.messages.forEach(function(m) {
2398
- if (m.isCancelled && m._serverItemId) locallyCancelled[m._serverItemId] = 1;
2889
+ if (m.isCancelled && m._serverItemId) locallyCancelled[m._serverItemId] = m;
2890
+ });
2891
+ var inFlightCancel = {};
2892
+ self.state.messages.forEach(function(m) {
2893
+ if (!m._serverItemId) return;
2894
+ if (m._cancelling || m._cancelError) inFlightCancel[m._serverItemId] = m;
2399
2895
  });
2400
2896
  var mappedHasPendingAssistant = mapped.some(function(m) {
2401
2897
  return m.isPending && m.role === "assistant" && !m.isBackgroundTask;
@@ -2415,7 +2911,22 @@ var ChatSession = class {
2415
2911
  }
2416
2912
  }
2417
2913
  }
2418
- self.state.messages = mapped;
2914
+ var oldestInPage1 = void 0;
2915
+ mapped.forEach(function(m) {
2916
+ var sid = m._serverItemId;
2917
+ if (typeof sid !== "string") return;
2918
+ if (oldestInPage1 === void 0 || sid < oldestInPage1) oldestInPage1 = sid;
2919
+ });
2920
+ var sharesPage1 = self.state.messages.some(function(m) {
2921
+ return typeof m._serverItemId === "string" && !!serverIds[m._serverItemId];
2922
+ });
2923
+ var retainedOlder = !sharesPage1 || oldestInPage1 === void 0 ? [] : self.state.messages.filter(function(m) {
2924
+ if (typeof m._serverItemId !== "string") return false;
2925
+ if (m._ownerKey !== void 0 && m._ownerKey !== loadKey) return false;
2926
+ return m._serverItemId < oldestInPage1;
2927
+ });
2928
+ keptOlderPages = retainedOlder.length > 0;
2929
+ self.state.messages = keptOlderPages ? retainedOlder.concat(mapped) : mapped;
2419
2930
  rescued.forEach(function(m) {
2420
2931
  self.state.messages.push(m);
2421
2932
  });
@@ -2423,19 +2934,38 @@ var ChatSession = class {
2423
2934
  for (var ci = 0; ci < self.state.messages.length; ci++) {
2424
2935
  var c = self.state.messages[ci];
2425
2936
  if (!c._serverItemId || !locallyCancelled[c._serverItemId] || c.isCancelled) continue;
2426
- self.state.messages[ci] = { role: "user", content: c.content, isCancelled: true, _serverItemId: c._serverItemId };
2937
+ self.state.messages[ci] = {
2938
+ role: "user",
2939
+ content: c.content,
2940
+ isCancelled: true,
2941
+ _serverItemId: c._serverItemId,
2942
+ isBackgroundTask: c.isBackgroundTask,
2943
+ _indexFile: c._indexFile,
2944
+ _useBgQueue: c._useBgQueue,
2945
+ _ownerKey: c._ownerKey
2946
+ };
2427
2947
  if (ci + 1 < self.state.messages.length && self.state.messages[ci + 1].isPending && self.state.messages[ci + 1]._serverItemId === c._serverItemId) {
2428
2948
  self.state.messages.splice(ci + 1, 1);
2429
2949
  }
2430
2950
  }
2431
2951
  }
2952
+ for (var fi = 0; fi < self.state.messages.length; fi++) {
2953
+ var fm = self.state.messages[fi];
2954
+ var was = fm._serverItemId && inFlightCancel[fm._serverItemId];
2955
+ if (!was || fm.isCancelled) continue;
2956
+ if (!(fm.isPendingQueued || fm.isPendingInProcess || fm.isPending)) continue;
2957
+ if (was._cancelling) fm._cancelling = true;
2958
+ if (was._cancelError) fm._cancelError = was._cancelError;
2959
+ }
2432
2960
  }
2433
- self.state.historyEndOfList = !!(history && history.endOfList);
2434
- self.state.historyStartKeyHistory = history && Array.isArray(history.startKeyHistory) ? history.startKeyHistory : [];
2435
- var clearedAt = self.host.getClearedAt();
2436
- if (clearedAt && chatList.length > 0) {
2437
- var oldestUpdated = Number(chatList[chatList.length - 1] && chatList[chatList.length - 1].updated);
2438
- if (isFinite(oldestUpdated) && oldestUpdated <= clearedAt) self.state.historyEndOfList = true;
2961
+ if (!keptOlderPages) {
2962
+ self.state.historyEndOfList = !!(history && history.endOfList);
2963
+ self.state.historyStartKeyHistory = history && Array.isArray(history.startKeyHistory) ? history.startKeyHistory : [];
2964
+ var clearedAt = self.host.getClearedAt();
2965
+ if (clearedAt && chatList.length > 0) {
2966
+ var oldestUpdated = Number(chatList[chatList.length - 1] && chatList[chatList.length - 1].updated);
2967
+ if (isFinite(oldestUpdated) && oldestUpdated <= clearedAt) self.state.historyEndOfList = true;
2968
+ }
2439
2969
  }
2440
2970
  if (self.state.historyRequestToken === token) {
2441
2971
  self.state.loadingHistory = false;
@@ -2464,18 +2994,25 @@ var ChatSession = class {
2464
2994
  return m.isPending && m._serverItemId === capturedId;
2465
2995
  });
2466
2996
  if (isNotExists) {
2467
- var isBg = aIdx !== -1 ? !!self.state.messages[aIdx].isBackgroundTask : false;
2997
+ var uIdx = self.state.messages.findIndex(function(m) {
2998
+ return m.role === "user" && m._serverItemId === capturedId && !m.isCancelled;
2999
+ });
3000
+ var isBg = aIdx !== -1 && !!self.state.messages[aIdx].isBackgroundTask || uIdx !== -1 && !!self.state.messages[uIdx].isBackgroundTask;
2468
3001
  if (aIdx !== -1) self.state.messages.splice(aIdx, 1);
2469
3002
  if (!isBg) {
2470
- var uIdx = self.state.messages.findIndex(function(m) {
2471
- return m.role === "user" && m._serverItemId === capturedId && !m.isCancelled;
2472
- });
2473
3003
  if (uIdx !== -1) {
2474
3004
  var ex = self.state.messages[uIdx];
2475
3005
  self.state.messages[uIdx] = { role: "user", content: ex.content, isCancelled: true, _serverItemId: ex._serverItemId };
2476
3006
  }
2477
3007
  self.cancelledServerIds.delete(capturedId);
2478
3008
  self.promoteNextQueuedToRunning();
3009
+ } else if (uIdx !== -1) {
3010
+ var bex = self.state.messages[uIdx];
3011
+ var bcancelled = { role: "user", content: bex.content, isCancelled: true, isBackgroundTask: true, _serverItemId: bex._serverItemId };
3012
+ if (bex._indexFile) bcancelled._indexFile = bex._indexFile;
3013
+ if (bex._useBgQueue) bcancelled._useBgQueue = true;
3014
+ self.state.messages[uIdx] = bcancelled;
3015
+ self.promoteNextBgQueuedToRunning();
2479
3016
  }
2480
3017
  self.host.notify();
2481
3018
  self.updateHistoryCache();
@@ -2496,7 +3033,7 @@ var ChatSession = class {
2496
3033
  });
2497
3034
  self.drainBgTaskQueue();
2498
3035
  }
2499
- if (!fetchMore) return self.host.scrollToBottom();
3036
+ if (!fetchMore) return self.host.scrollToBottomIfSticky();
2500
3037
  }).catch(function(err) {
2501
3038
  console.warn("[chat-engine] getChatHistory failed", err);
2502
3039
  }).then(function() {
@@ -2506,6 +3043,7 @@ var ChatSession = class {
2506
3043
  self.state.loadingOlderHistory = false;
2507
3044
  if (wasLoading) self.host.notify();
2508
3045
  }
3046
+ if (self.host.onHistoryLoaded) self.host.onHistoryLoaded(!!fetchMore, token);
2509
3047
  });
2510
3048
  }
2511
3049
  // --- attachment upload orchestration ---------------------------------
@@ -2703,6 +3241,194 @@ var ChatSession = class {
2703
3241
  }
2704
3242
  };
2705
3243
 
2706
- export { BG_INDEXING_QUEUE_SUFFIX, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, ChatSession, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, HISTORY_TOKEN_BUDGET, LINK_LABEL_MAX_DISPLAY_CHARS, MAX_HISTORY_MESSAGES, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MIN_INPUT_TOKEN_BUDGET, OUTPUT_TOKEN_RESERVE, POLL_INTERVAL, RENDER_FROM_TOKEN, TOOL_AND_RESPONSE_BUFFER, buildBoundedChatMessages, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, clearAttachmentParsers, composeUserMessage, configureChatEngine, createInlineLinkRegex, encodePathSegments, estimateMessageTokens, estimateTextTokens, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, filterListByClearHorizon, findAttachmentParser, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, groupAttachmentFailures, isAuthExpiredError, isBgIndexingQueue, isErrorResponseBody, isNonRetryableRequestError, isOfficeFile, isServerExtractable, isServiceDbAttachmentHref, listClaudeModels, listOpenAIModels, makeExtractPlaceholder, mapHistoryListToMessages, normalizeAttachmentPathCandidate, normalizeTextContent, notifyAgentSaveAttachment, parseAttachmentContent, registerAttachmentParser, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay };
3244
+ // src/engine/indexing_groups.ts
3245
+ var INDEXING_LABEL_RE = /^(Re)?[Ii]ndexing(\s*\(continuing\))?\s*:?\s+(.+)$/;
3246
+ var LEADING_MD_LINK_RE = /^\[([^\]]+)\]\(([^)]+)\)/;
3247
+ function parseIndexingLabel(content) {
3248
+ if (typeof content !== "string" || !content) return null;
3249
+ var firstLine = content.split("\n")[0].trim();
3250
+ var m = firstLine.match(INDEXING_LABEL_RE);
3251
+ if (!m) return null;
3252
+ var head = m[3].split(" \xB7 ")[0].trim();
3253
+ var link = head.match(LEADING_MD_LINK_RE);
3254
+ var name = link ? link[1].trim() : head;
3255
+ if (!name) return null;
3256
+ return {
3257
+ name,
3258
+ path: link ? link[2].trim() : void 0,
3259
+ continued: !!m[2],
3260
+ isReindex: !!m[1]
3261
+ };
3262
+ }
3263
+ function readFileRef(msg) {
3264
+ var ref = msg && msg._indexFile;
3265
+ if (ref && (ref.path || ref.name)) {
3266
+ return {
3267
+ name: ref.name || ref.path || "",
3268
+ path: ref.path,
3269
+ mime: ref.mime,
3270
+ size: ref.size,
3271
+ isReindex: ref.isReindex,
3272
+ continued: !!ref.continued
3273
+ };
3274
+ }
3275
+ var parsed = parseIndexingLabel(msg && msg.content);
3276
+ if (!parsed) return null;
3277
+ return {
3278
+ name: parsed.name,
3279
+ path: parsed.path,
3280
+ isReindex: parsed.isReindex,
3281
+ continued: parsed.continued
3282
+ };
3283
+ }
3284
+ function isPendingMsg(m) {
3285
+ return !!(m.isPending || m.isPendingInProcess || m.isPendingQueued || m.isSendingToServer);
3286
+ }
3287
+ function buildChatDisplayList(messages, opts) {
3288
+ var list = Array.isArray(messages) ? messages : [];
3289
+ var hasMoreHistory = !!(opts && opts.hasMoreHistory);
3290
+ var groups = {};
3291
+ var order = [];
3292
+ var runOfIndex = new Array(list.length);
3293
+ var runByItemId = {};
3294
+ var keyByName = {};
3295
+ var openRunOfKey = {};
3296
+ var runsOfKey = {};
3297
+ var keyOfRun = {};
3298
+ var runSeq = 0;
3299
+ for (var i = 0; i < list.length; i++) {
3300
+ var msg = list[i];
3301
+ if (!msg || !msg.isBackgroundTask) continue;
3302
+ var runId;
3303
+ var ref = msg.role === "user" ? readFileRef(msg) : null;
3304
+ if (ref) {
3305
+ var key = ref.path || keyByName[ref.name] || ref.name;
3306
+ if (!ref.continued && openRunOfKey[key]) delete openRunOfKey[key];
3307
+ runId = openRunOfKey[key];
3308
+ if (!runId) {
3309
+ runId = "run" + runSeq++;
3310
+ openRunOfKey[key] = runId;
3311
+ keyOfRun[runId] = key;
3312
+ (runsOfKey[key] || (runsOfKey[key] = [])).push(runId);
3313
+ }
3314
+ } else if (msg._serverItemId && runByItemId[msg._serverItemId]) {
3315
+ runId = runByItemId[msg._serverItemId];
3316
+ } else if (msg.role !== "user") {
3317
+ runId = runOfIndex[i - 1];
3318
+ }
3319
+ if (!runId) continue;
3320
+ var g = groups[runId];
3321
+ if (!g) {
3322
+ var fileKey = keyOfRun[runId];
3323
+ g = groups[runId] = {
3324
+ key: fileKey,
3325
+ runKey: runId,
3326
+ // provisional; renumbered newest-first below
3327
+ name: ref ? ref.name : fileKey,
3328
+ path: ref ? ref.path : void 0,
3329
+ mime: ref ? ref.mime : void 0,
3330
+ size: ref ? ref.size : void 0,
3331
+ isReindex: !!(ref && ref.isReindex),
3332
+ members: [],
3333
+ passCount: 0,
3334
+ status: "done",
3335
+ cancellableIds: [],
3336
+ cancelling: false,
3337
+ mayHaveOlder: false,
3338
+ // The run's first loaded pass, and never re-stamped: see the file
3339
+ // docstring. `anchorId` is filled in once every member is known.
3340
+ anchorIndex: i,
3341
+ anchorId: ""
3342
+ };
3343
+ order.push(runId);
3344
+ }
3345
+ if (ref) {
3346
+ if (ref.name) g.name = ref.name;
3347
+ if (ref.path) g.path = ref.path;
3348
+ if (ref.mime) g.mime = ref.mime;
3349
+ if (typeof ref.size === "number") g.size = ref.size;
3350
+ if (ref.isReindex) g.isReindex = true;
3351
+ if (!ref.continued) g.mayHaveOlder = false;
3352
+ g.passCount++;
3353
+ }
3354
+ g.members.push({ msg, index: i });
3355
+ runOfIndex[i] = runId;
3356
+ if (msg._serverItemId) runByItemId[msg._serverItemId] = runId;
3357
+ if (ref && ref.name) keyByName[ref.name] = g.key;
3358
+ }
3359
+ for (var rk in runsOfKey) {
3360
+ var runIds = runsOfKey[rk];
3361
+ for (var ri = 0; ri < runIds.length; ri++) {
3362
+ var grpR = groups[runIds[ri]];
3363
+ if (!grpR) continue;
3364
+ var first = grpR.members[0];
3365
+ var firstId = first && first.msg && (first.msg._serverItemId || first.msg._localId);
3366
+ grpR.runKey = rk + "#" + (firstId || "n" + ri);
3367
+ }
3368
+ }
3369
+ for (var oi = 0; oi < order.length; oi++) {
3370
+ var grp = groups[order[oi]];
3371
+ var lastSettled = -1;
3372
+ for (var si = 0; si < grp.members.length; si++) {
3373
+ if (!isPendingMsg(grp.members[si].msg)) lastSettled = si;
3374
+ }
3375
+ var active = false;
3376
+ for (var mi = lastSettled + 1; mi < grp.members.length; mi++) {
3377
+ if (isPendingMsg(grp.members[mi].msg)) {
3378
+ active = true;
3379
+ break;
3380
+ }
3381
+ }
3382
+ for (var xi = 0; xi < grp.members.length; xi++) {
3383
+ if (grp.members[xi].msg._cancelling) {
3384
+ grp.cancelling = true;
3385
+ break;
3386
+ }
3387
+ }
3388
+ var seenIds = {};
3389
+ for (var ci = 0; ci < grp.members.length; ci++) {
3390
+ var cm = grp.members[ci].msg;
3391
+ if (cm._cancelError && (active || grp.cancelling)) grp.cancelError = cm._cancelError;
3392
+ if (cm.role !== "user" || !cm._serverItemId || cm._cancelling || cm.isSendingToServer) continue;
3393
+ if (!(cm.isPendingQueued || cm.isPendingInProcess)) continue;
3394
+ if (ci < lastSettled) continue;
3395
+ if (seenIds[cm._serverItemId]) continue;
3396
+ seenIds[cm._serverItemId] = true;
3397
+ grp.cancellableIds.push(cm._serverItemId);
3398
+ }
3399
+ if (active) {
3400
+ grp.status = "active";
3401
+ } else {
3402
+ var last = grp.members[grp.members.length - 1].msg;
3403
+ grp.status = last.isError ? "error" : last.isCancelled ? "cancelled" : "done";
3404
+ }
3405
+ var sawFirstPass = false;
3406
+ for (var pi = 0; pi < grp.members.length; pi++) {
3407
+ var pm = grp.members[pi].msg;
3408
+ if (pm.role !== "user") continue;
3409
+ var pref = readFileRef(pm);
3410
+ if (pref && !pref.continued) {
3411
+ sawFirstPass = true;
3412
+ break;
3413
+ }
3414
+ }
3415
+ grp.mayHaveOlder = !sawFirstPass && hasMoreHistory;
3416
+ var anchor = grp.members[0];
3417
+ grp.anchorIndex = anchor.index;
3418
+ grp.anchorId = anchor.msg._serverItemId || anchor.msg._localId || "";
3419
+ }
3420
+ var out = [];
3421
+ for (var j = 0; j < list.length; j++) {
3422
+ var r = runOfIndex[j];
3423
+ if (r === void 0) {
3424
+ out.push({ kind: "message", msg: list[j], index: j });
3425
+ continue;
3426
+ }
3427
+ if (groups[r].anchorIndex === j) out.push({ kind: "indexing", group: groups[r], index: j });
3428
+ }
3429
+ return out;
3430
+ }
3431
+
3432
+ export { BG_INDEXING_QUEUE_SUFFIX, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, ChatSession, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, EXPIRED_LINK_REFRESH_EXPIRES_SECONDS, HISTORY_FILL_SLACK_PX, HISTORY_TOKEN_BUDGET, LINK_LABEL_MAX_DISPLAY_CHARS, LINK_REFRESH_WINDOW_MS, MAX_HISTORY_FILL_PAGES, MAX_HISTORY_MESSAGES, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MIN_INPUT_TOKEN_BUDGET, OUTPUT_TOKEN_RESERVE, POLL_INTERVAL, RENDER_FROM_TOKEN, TOOL_AND_RESPONSE_BUFFER, buildBoundedChatMessages, buildChatDisplayList, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, classifyInlineLink, clearAttachmentParsers, composeUserMessage, configureChatEngine, createHistoryFiller, createInlineLinkRegex, encodePathSegments, estimateMessageTokens, estimateTextTokens, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, fillHistoryViewport, filterListByClearHorizon, findAttachmentParser, formatChatTimestamp, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, groupAttachmentFailures, isAuthExpiredError, isBgIndexingQueue, isErrorResponseBody, isHttpUrlLike, isNonRetryableRequestError, isOfficeFile, isServerExtractable, isServiceDbAttachmentHref, listClaudeModels, listOpenAIModels, makeExtractPlaceholder, mapHistoryListToMessages, normalizeAttachmentPathCandidate, normalizeTextContent, normalizeTrailingInlineToken, notifyAgentSaveAttachment, parseAttachmentContent, parseIndexingLabel, readExpiredAttachmentHref, registerAttachmentParser, repairUrlEntities, repairUrlWhitespace, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay, wallClockNow };
2707
3433
  //# sourceMappingURL=engine.mjs.map
2708
3434
  //# sourceMappingURL=engine.mjs.map