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.cjs CHANGED
@@ -286,11 +286,14 @@ function buildChatSystemPrompt(params) {
286
286
  You are a dedicated assistant for the project ID: "${formattedServiceId}".
287
287
  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.
288
288
  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.
289
+ 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.
290
+ 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.
291
+ 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.
289
292
  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.
290
293
  - 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.
291
294
  - 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.
292
295
  - 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.
293
- 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.
296
+ 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.
294
297
  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.
295
298
  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:
296
299
  \`\`\`filename.csv
@@ -415,6 +418,8 @@ ${placeholder}
415
418
 
416
419
  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.
417
420
 
421
+ 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.
422
+
418
423
  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.`;
419
424
  }
420
425
  function buildIndexingContinueMessage(attachment) {
@@ -525,8 +530,10 @@ function isAuthExpiredError(input) {
525
530
  var EXPIRED_ATTACHMENT_URL_HOST = "_expired_.url";
526
531
  var EXPIRED_ATTACHMENT_URL_ORIGIN = "https://" + EXPIRED_ATTACHMENT_URL_HOST;
527
532
  var LINK_LABEL_MAX_DISPLAY_CHARS = 32;
533
+ var EXPIRED_LINK_REFRESH_EXPIRES_SECONDS = 20 * 60;
534
+ var LINK_REFRESH_WINDOW_MS = (EXPIRED_LINK_REFRESH_EXPIRES_SECONDS - 5 * 60) * 1e3;
528
535
  function createInlineLinkRegex() {
529
- return /src::(\S+)|\[([^\]\n]+)\]\((https?:\/\/(?:[^\s()]+|\([^\s()]*\))+)\)|\[([^\]\n]+)\]\(((?:[^()\n]+|\([^()\n]*\))+)\)|(https?:\/\/[^\s<>"']+)/g;
536
+ return /src::(\S+)|\[([^\]\n]+)\]\((https?:\/\/(?:[^\s()]|\([^\s()]*\))+)\)|\[([^\]\n]+)\]\(((?:[^()\n]|\([^()\n]*\))+)\)|(https?:\/\/[^\s<>"']+)/g;
530
537
  }
531
538
  function safeDecodeURIComponent(v) {
532
539
  try {
@@ -586,18 +593,161 @@ function isServiceDbAttachmentHref(href, serviceId) {
586
593
  return false;
587
594
  }
588
595
  }
596
+ function readExpiredAttachmentHref(href) {
597
+ if (!href) return null;
598
+ try {
599
+ var parsed = new URL(href);
600
+ if (parsed.hostname !== EXPIRED_ATTACHMENT_URL_HOST) return null;
601
+ return normalizeAttachmentPathCandidate(parsed.pathname || "") || null;
602
+ } catch (e) {
603
+ return null;
604
+ }
605
+ }
589
606
  function sanitizeAttachmentLinksForHistory(content, serviceId, forAssistant) {
590
607
  if (!content) return content;
591
608
  if (!forAssistant && content.indexOf("Attached files:") === -1) return content;
592
609
  return content.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g, function(_m, label, href) {
593
- if (forAssistant && !isServiceDbAttachmentHref(href, serviceId)) return _m;
610
+ if (!isServiceDbAttachmentHref(href, serviceId)) return _m;
594
611
  var remotePath = extractRemotePathFromAttachmentHref(href, serviceId);
595
- var labelPath = normalizeAttachmentPathCandidate(label);
596
- var fullPath = remotePath || labelPath;
597
- if (!fullPath) return forAssistant ? _m : "[" + label + "](" + EXPIRED_ATTACHMENT_URL_ORIGIN + "/file)";
612
+ var fullPath = remotePath || normalizeAttachmentPathCandidate(label);
613
+ if (!fullPath) return _m;
598
614
  return "[" + label + "](" + buildDisplayExpiredAttachmentHref(fullPath, label) + ")";
599
615
  });
600
616
  }
617
+ function isHttpUrlLike(target) {
618
+ return /^https?:\/\//i.test((target || "").trim());
619
+ }
620
+ function repairUrlWhitespace(href) {
621
+ if (!href || !/\s/.test(href)) return href;
622
+ var stripped = href.replace(/\s+/g, "");
623
+ if (/^https?:\/\/[^/\s]+\/download\/[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)?$/i.test(stripped)) return stripped;
624
+ return href.trim().replace(/\s/g, "%20");
625
+ }
626
+ function repairUrlEntities(href) {
627
+ if (!href || href.indexOf("&") === -1) return href;
628
+ var out = href, prev = "";
629
+ while (out !== prev) {
630
+ prev = out;
631
+ out = out.replace(/&amp;/gi, "&").replace(/&#0*38;/g, "&").replace(/&#x0*26;/gi, "&");
632
+ }
633
+ return out;
634
+ }
635
+ function normalizeTrailingInlineToken(value) {
636
+ if (!value) return value;
637
+ var out = value.replace(/[.,;:!?]+$/, "");
638
+ var trimUnmatched = function(openCh, closeCh) {
639
+ while (out.charAt(out.length - 1) === closeCh) {
640
+ var openCount = (out.match(new RegExp("\\" + openCh, "g")) || []).length;
641
+ var closeCount = (out.match(new RegExp("\\" + closeCh, "g")) || []).length;
642
+ if (closeCount > openCount) out = out.slice(0, -1);
643
+ else break;
644
+ }
645
+ };
646
+ trimUnmatched("(", ")");
647
+ trimUnmatched("[", "]");
648
+ trimUnmatched("{", "}");
649
+ out = out.replace(/[`'"*>]+$/, "");
650
+ return out;
651
+ }
652
+ function classifyInlineLink(full, groups, ctx) {
653
+ var g1 = groups[0], g2 = groups[1], g3 = groups[2], g4 = groups[3], g5 = groups[4], g6 = groups[5];
654
+ var dbHostPrefix = (ctx.dbHostPrefix || "").toLowerCase();
655
+ var fresh = function(expiredHref) {
656
+ return ctx.resolveFreshHref ? ctx.resolveFreshHref(expiredHref) : void 0;
657
+ };
658
+ var isDbHost = function(url) {
659
+ return !!dbHostPrefix && url.toLowerCase().indexOf(dbHostPrefix) === 0;
660
+ };
661
+ var asStoredFile = function(remotePath2, label) {
662
+ if (!remotePath2) return null;
663
+ var expiredHref = buildDisplayExpiredAttachmentHref(remotePath2, label);
664
+ var cached = fresh(expiredHref);
665
+ return {
666
+ part: {
667
+ type: "link",
668
+ label: truncateLabelForDisplay(label),
669
+ fullLabel: label,
670
+ href: cached || expiredHref,
671
+ expired: !cached,
672
+ expiredHref,
673
+ remotePath: remotePath2
674
+ }
675
+ };
676
+ };
677
+ if (g1) {
678
+ var rawPath = normalizeTrailingInlineToken(g1);
679
+ var tail = full.slice(("src::" + rawPath).length);
680
+ var srcIsUrl = isHttpUrlLike(rawPath);
681
+ if (srcIsUrl && !isDbHost(rawPath) && !readExpiredAttachmentHref(rawPath)) {
682
+ var srcUrl = repairUrlEntities(rawPath);
683
+ return {
684
+ part: { type: "link", label: truncateLabelForDisplay(srcUrl), fullLabel: srcUrl, href: srcUrl, expired: false },
685
+ tail
686
+ };
687
+ }
688
+ var srcPath = readExpiredAttachmentHref(rawPath) || (srcIsUrl ? extractRemotePathFromAttachmentHref(rawPath, ctx.serviceId) || normalizeAttachmentPathCandidate(rawPath) : normalizeAttachmentPathCandidate(rawPath));
689
+ var srcBuilt = asStoredFile(srcPath, srcPath);
690
+ return srcBuilt ? { part: srcBuilt.part, tail } : null;
691
+ }
692
+ if (g4 && g5) {
693
+ var dbTarget = /^db:(.+)$/i.exec(g5.trim());
694
+ if (dbTarget) {
695
+ var declared = asStoredFile(normalizeAttachmentPathCandidate(dbTarget[1]), g4);
696
+ if (!declared) return null;
697
+ declared.part.label = truncateLabelForDisplay(g4);
698
+ declared.part.fullLabel = g4;
699
+ return declared;
700
+ }
701
+ if (isHttpUrlLike(g5)) {
702
+ return classifyInlineLink(full, [void 0, g4, repairUrlWhitespace(g5), void 0, void 0, void 0], ctx);
703
+ }
704
+ var trimmedTarget = g5.trim();
705
+ if (/^[a-z][a-z0-9+.-]*:/i.test(trimmedTarget) || trimmedTarget.charAt(0) === "#") {
706
+ return {
707
+ part: { type: "link", label: truncateLabelForDisplay(g4), fullLabel: g4, href: trimmedTarget, expired: false }
708
+ };
709
+ }
710
+ var built = asStoredFile(normalizeAttachmentPathCandidate(g5), g4);
711
+ if (!built) return null;
712
+ built.part.label = truncateLabelForDisplay(g4);
713
+ built.part.fullLabel = g4;
714
+ return built;
715
+ }
716
+ var originalHref = g3 || g6 || "";
717
+ if (!originalHref) return null;
718
+ originalHref = repairUrlEntities(originalHref);
719
+ var urlTail;
720
+ if (!g3 && g6) {
721
+ var trimmedUrl = normalizeTrailingInlineToken(originalHref);
722
+ if (trimmedUrl !== originalHref) urlTail = originalHref.slice(trimmedUrl.length);
723
+ originalHref = trimmedUrl;
724
+ }
725
+ var withTail = function(r) {
726
+ return urlTail ? { part: r.part, tail: urlTail } : r;
727
+ };
728
+ var urlLabel = g2 || originalHref;
729
+ var carried = readExpiredAttachmentHref(originalHref);
730
+ if (carried) {
731
+ var carriedBuilt = asStoredFile(carried, g2 || carried);
732
+ if (carriedBuilt) {
733
+ if (g2) {
734
+ carriedBuilt.part.label = truncateLabelForDisplay(g2);
735
+ carriedBuilt.part.fullLabel = g2;
736
+ }
737
+ return withTail(carriedBuilt);
738
+ }
739
+ }
740
+ if (isServiceDbAttachmentHref(originalHref, ctx.serviceId)) {
741
+ var remotePath = extractRemotePathFromAttachmentHref(originalHref, ctx.serviceId);
742
+ if (remotePath) {
743
+ var dbBuilt = asStoredFile(remotePath, getExpiredAttachmentVisiblePath(remotePath, urlLabel));
744
+ if (dbBuilt) return withTail(dbBuilt);
745
+ }
746
+ }
747
+ return withTail({
748
+ part: { type: "link", label: truncateLabelForDisplay(urlLabel), fullLabel: urlLabel, href: originalHref, expired: false }
749
+ });
750
+ }
601
751
  function truncateLabelForDisplay(label) {
602
752
  if (!label) return label;
603
753
  if (label.length <= LINK_LABEL_MAX_DISPLAY_CHARS) return label;
@@ -666,6 +816,26 @@ function buildBoundedChatMessages(options) {
666
816
  };
667
817
  }
668
818
 
819
+ // src/engine/time.ts
820
+ function wallClockNow() {
821
+ return Date.now();
822
+ }
823
+ function formatChatTimestamp(ms) {
824
+ if (typeof ms !== "number" || !isFinite(ms) || ms <= 0) return "";
825
+ try {
826
+ return new Date(ms).toLocaleString(void 0, {
827
+ year: "numeric",
828
+ month: "short",
829
+ day: "numeric",
830
+ hour: "numeric",
831
+ minute: "2-digit",
832
+ second: "2-digit"
833
+ });
834
+ } catch (e) {
835
+ return "";
836
+ }
837
+ }
838
+
669
839
  // src/engine/requests.ts
670
840
  var ANTHROPIC_MESSAGES_API_URL = "https://api.anthropic.com/v1/messages";
671
841
  var ANTHROPIC_MODELS_API_URL = "https://api.anthropic.com/v1/models";
@@ -683,7 +853,7 @@ var DEFAULT_OPENAI_IMAGE_DETAIL = "auto";
683
853
  var OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS = true;
684
854
  var MCP_NAME = "BunnyQuery";
685
855
  var DEFAULT_CLAUDE_MODEL = "claude-sonnet-4-6";
686
- var DEFAULT_OPENAI_MODEL = "gpt-5.4";
856
+ var DEFAULT_OPENAI_MODEL = "gpt-5.6-luna";
687
857
  var mcpUrl = () => chatEngineConfig().mcpBaseUrl;
688
858
  var clientSecretRequest = (opts) => chatEngineConfig().clientSecretRequest(opts);
689
859
  var getOpenAIImageDetail = (model) => {
@@ -1227,20 +1397,35 @@ function mapHistoryListToMessages(list, platform, opts) {
1227
1397
  var assistantText = isPending ? "" : (extractAssistantText(response) || "").trim() || "";
1228
1398
  var isErrorResponse = !isPending && (isFailed || isErrorResponseBody(response));
1229
1399
  var serverItemId = item && typeof item.id === "string" && item.id ? item.id : void 0;
1400
+ var createdTs = Number(item && item.created);
1401
+ var updatedTs = Number(item && item.updated);
1402
+ var userTs = isFinite(createdTs) && createdTs > 0 ? createdTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : void 0;
1403
+ var replyTs = isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
1230
1404
  if (userText) {
1231
1405
  var displayContent;
1406
+ var indexFile = void 0;
1232
1407
  if (item._isBgTask) {
1233
1408
  var nameMatch = userText.match(/^- name: (.+)$/m);
1234
1409
  if (nameMatch) {
1235
1410
  var mimeMatch = userText.match(/^- mime type: (.+)$/m);
1236
1411
  var sizeMatch = userText.match(/^- size \(bytes\): (\d+)$/m);
1237
1412
  var pathMatch = userText.match(/^- storage path: (.+)$/m);
1413
+ var isContinuePass = userText.indexOf("CONTINUE indexing") === 0;
1238
1414
  displayContent = opts.formatIndexingLabel(
1239
1415
  nameMatch[1].trim(),
1240
1416
  mimeMatch ? mimeMatch[1].trim() : "",
1241
1417
  sizeMatch ? Number(sizeMatch[1]) : null,
1242
- pathMatch ? pathMatch[1].trim() : void 0
1418
+ pathMatch ? pathMatch[1].trim() : void 0,
1419
+ false,
1420
+ isContinuePass
1243
1421
  );
1422
+ indexFile = {
1423
+ name: nameMatch[1].trim(),
1424
+ path: pathMatch ? pathMatch[1].trim() : void 0,
1425
+ mime: mimeMatch ? mimeMatch[1].trim() : void 0,
1426
+ size: sizeMatch ? Number(sizeMatch[1]) : void 0,
1427
+ continued: isContinuePass
1428
+ };
1244
1429
  } else {
1245
1430
  displayContent = userText;
1246
1431
  }
@@ -1252,8 +1437,10 @@ function mapHistoryListToMessages(list, platform, opts) {
1252
1437
  if (isQueued) userMsg.isPendingQueued = true;
1253
1438
  if (isCancelledItem) userMsg.isCancelled = true;
1254
1439
  if (item._isBgTask) userMsg.isBackgroundTask = true;
1440
+ if (indexFile) userMsg._indexFile = indexFile;
1255
1441
  if (item._isOnBgQueue) userMsg._useBgQueue = true;
1256
1442
  if (serverItemId !== void 0) userMsg._serverItemId = serverItemId;
1443
+ if (userTs !== void 0) userMsg._ts = userTs;
1257
1444
  mapped.push(userMsg);
1258
1445
  }
1259
1446
  if (isCancelledItem) ; else if (isInProcess) {
@@ -1268,17 +1455,104 @@ function mapHistoryListToMessages(list, platform, opts) {
1268
1455
  var em = { role: "assistant", content: getErrorMessage(response), isError: true };
1269
1456
  if (item._isBgTask) em.isBackgroundTask = true;
1270
1457
  if (serverItemId !== void 0) em._serverItemId = serverItemId;
1458
+ if (replyTs !== void 0) em._ts = replyTs;
1271
1459
  mapped.push(em);
1272
1460
  } else if (assistantText) {
1273
1461
  var okm = { role: "assistant", content: sanitizeAttachmentLinksForHistory(assistantText, opts.serviceId, true) };
1274
1462
  if (item._isBgTask) okm.isBackgroundTask = true;
1275
1463
  if (serverItemId !== void 0) okm._serverItemId = serverItemId;
1464
+ if (replyTs !== void 0) okm._ts = replyTs;
1276
1465
  mapped.push(okm);
1277
1466
  }
1278
1467
  });
1279
1468
  return { messages: mapped, runningItemIds };
1280
1469
  }
1281
1470
 
1471
+ // src/engine/viewport_fill.ts
1472
+ var HISTORY_FILL_SLACK_PX = 64;
1473
+ var MAX_HISTORY_FILL_PAGES = 24;
1474
+ var IDLE_WAIT_STEP_MS = 120;
1475
+ var IDLE_WAIT_MAX_MS = 15e3;
1476
+ async function waitForIdle(opts, stale) {
1477
+ var waited = 0;
1478
+ while (opts.isLoading()) {
1479
+ if (stale() || waited >= IDLE_WAIT_MAX_MS) return false;
1480
+ await new Promise(function(r) {
1481
+ setTimeout(r, IDLE_WAIT_STEP_MS);
1482
+ });
1483
+ waited += IDLE_WAIT_STEP_MS;
1484
+ }
1485
+ return !stale();
1486
+ }
1487
+ async function fillHistoryViewport(opts) {
1488
+ var maxPages = typeof opts.maxPages === "number" ? opts.maxPages : MAX_HISTORY_FILL_PAGES;
1489
+ var stale = function() {
1490
+ return !!(opts.isStale && opts.isStale());
1491
+ };
1492
+ var swallowed = 0;
1493
+ for (var page = 0; page < maxPages; page++) {
1494
+ if (stale() || opts.isEndOfList()) return;
1495
+ if (!await waitForIdle(opts, stale)) return;
1496
+ var satisfied = false;
1497
+ try {
1498
+ satisfied = !!await opts.isSatisfied();
1499
+ } catch {
1500
+ return;
1501
+ }
1502
+ if (satisfied || stale()) return;
1503
+ if (!await waitForIdle(opts, stale)) return;
1504
+ var before = opts.messageCount();
1505
+ var attempted;
1506
+ try {
1507
+ attempted = await opts.fetchOlder();
1508
+ } catch {
1509
+ return;
1510
+ }
1511
+ if (stale()) return;
1512
+ if (attempted === false) {
1513
+ if (++swallowed > 3) return;
1514
+ page--;
1515
+ continue;
1516
+ }
1517
+ if (opts.messageCount() <= before) return;
1518
+ }
1519
+ }
1520
+ function createHistoryFiller(base) {
1521
+ var pending = [];
1522
+ var running = false;
1523
+ async function allSatisfied() {
1524
+ var next = [];
1525
+ for (var i = 0; i < pending.length; i++) {
1526
+ if (!await pending[i]()) next.push(pending[i]);
1527
+ }
1528
+ pending = next;
1529
+ return pending.length === 0;
1530
+ }
1531
+ return {
1532
+ isRunning: function() {
1533
+ return running;
1534
+ },
1535
+ fill: function(isSatisfied) {
1536
+ pending.push(isSatisfied);
1537
+ if (running) return Promise.resolve();
1538
+ running = true;
1539
+ var done = function() {
1540
+ running = false;
1541
+ pending = [];
1542
+ };
1543
+ return fillHistoryViewport({
1544
+ isSatisfied: allSatisfied,
1545
+ isEndOfList: base.isEndOfList,
1546
+ isLoading: base.isLoading,
1547
+ messageCount: base.messageCount,
1548
+ fetchOlder: base.fetchOlder,
1549
+ isStale: base.isStale,
1550
+ maxPages: base.maxPages
1551
+ }).then(done, done);
1552
+ }
1553
+ };
1554
+ }
1555
+
1282
1556
  // src/engine/session.ts
1283
1557
  var _g = typeof globalThis !== "undefined" ? globalThis : {};
1284
1558
  function nowMs() {
@@ -1316,6 +1590,7 @@ var ChatSession = class {
1316
1590
  };
1317
1591
  this.bgTaskQueue = [];
1318
1592
  this.cancelledServerIds = /* @__PURE__ */ new Set();
1593
+ this.cancelledIndexKeys = /* @__PURE__ */ new Set();
1319
1594
  this.pendingAgentRequests = {};
1320
1595
  this.aiChatHistoryCache = {};
1321
1596
  this.historyItemPolls = /* @__PURE__ */ new Map();
@@ -1338,6 +1613,23 @@ var ChatSession = class {
1338
1613
  this.historyItemPolls.set(id, { kind, stop });
1339
1614
  return p;
1340
1615
  }
1616
+ /**
1617
+ * Stop and forget one item's poll. Used after a cancel: the row is either gone
1618
+ * (cancelled while queued) or flagged cancelled (cancelled while running), so
1619
+ * asking about it again only burns requests. Safe when no poll is attached, and
1620
+ * safe on an older skapi-js with no stop handle (the entry is then LEFT in the
1621
+ * map so a later drain cannot stack a second, unstoppable poll on the id).
1622
+ */
1623
+ _stopPoll(id) {
1624
+ var handle = this.historyItemPolls.get(id);
1625
+ if (!handle) return;
1626
+ if (typeof handle.stop !== "function") return;
1627
+ try {
1628
+ handle.stop();
1629
+ } catch (e) {
1630
+ }
1631
+ this.historyItemPolls.delete(id);
1632
+ }
1341
1633
  /** True while any pause reason is active. */
1342
1634
  isPollingPaused() {
1343
1635
  return this._pauseReasons.size > 0;
@@ -1440,7 +1732,18 @@ var ChatSession = class {
1440
1732
  if (reply._serverItemId === void 0 && msgs[thIdx]._serverItemId !== void 0) reply._serverItemId = msgs[thIdx]._serverItemId;
1441
1733
  msgs[thIdx] = reply;
1442
1734
  } else {
1443
- msgs.push(reply);
1735
+ var dupIdx = -1;
1736
+ if (serverId) {
1737
+ for (var d = msgs.length - 1; d >= 0; d--) {
1738
+ var dm = msgs[d];
1739
+ if (dm && dm.role === "assistant" && dm._serverItemId === serverId) {
1740
+ dupIdx = d;
1741
+ break;
1742
+ }
1743
+ }
1744
+ }
1745
+ if (dupIdx !== -1) msgs[dupIdx] = reply;
1746
+ else msgs.push(reply);
1444
1747
  }
1445
1748
  for (var j = 0; j < msgs.length; j++) {
1446
1749
  var u = msgs[j];
@@ -1571,7 +1874,7 @@ var ChatSession = class {
1571
1874
  var offExisting = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
1572
1875
  this.aiChatHistoryCache[key] = {
1573
1876
  messages: offExisting.messages.concat([
1574
- { role: "user", content: composed, _ownerKey: key },
1877
+ { role: "user", content: composed, _ownerKey: key, _ts: wallClockNow() },
1575
1878
  { role: "assistant", content: "", isPending: true, isPendingInProcess: true, _ownerKey: key }
1576
1879
  ]),
1577
1880
  endOfList: offExisting.endOfList,
@@ -1603,7 +1906,7 @@ var ChatSession = class {
1603
1906
  serviceId: id.serviceId,
1604
1907
  history: resolvedHistory.concat([{ role: "user", content: llmComposed }])
1605
1908
  });
1606
- var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true };
1909
+ var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true, _ts: wallClockNow() };
1607
1910
  if (key) queuedBubble._ownerKey = key;
1608
1911
  if (useBgQueue) queuedBubble._useBgQueue = true;
1609
1912
  this.state.messages.push(queuedBubble);
@@ -1638,7 +1941,7 @@ var ChatSession = class {
1638
1941
  });
1639
1942
  return;
1640
1943
  }
1641
- this.state.messages.push({ role: "user", content: composed, ...key ? { _ownerKey: key } : {} });
1944
+ this.state.messages.push({ role: "user", content: composed, _ts: wallClockNow(), ...key ? { _ownerKey: key } : {} });
1642
1945
  this.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true, ...key ? { _ownerKey: key } : {} });
1643
1946
  this.host.notify();
1644
1947
  this.updateHistoryCache();
@@ -1680,7 +1983,7 @@ var ChatSession = class {
1680
1983
  self.state.sending = false;
1681
1984
  if (!(self.host.isViewMounted() && self.getHistoryCacheKey() === key)) return;
1682
1985
  return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
1683
- self.host.scrollToBottom(true);
1986
+ self.host.scrollToBottomIfSticky(true);
1684
1987
  });
1685
1988
  });
1686
1989
  }
@@ -1694,6 +1997,8 @@ var ChatSession = class {
1694
1997
  if (nextIdx === -1) return;
1695
1998
  var existing = this.state.messages[nextIdx];
1696
1999
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true, isBackgroundTask: true };
2000
+ if (existing._indexFile) promoted._indexFile = existing._indexFile;
2001
+ if (existing._ts !== void 0) promoted._ts = existing._ts;
1697
2002
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1698
2003
  if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1699
2004
  this.state.messages[nextIdx] = promoted;
@@ -1714,6 +2019,8 @@ var ChatSession = class {
1714
2019
  var existing = this.state.messages[nextIdx];
1715
2020
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true };
1716
2021
  if (existing.isBackgroundTask) promoted.isBackgroundTask = true;
2022
+ if (existing._indexFile) promoted._indexFile = existing._indexFile;
2023
+ if (existing._ts !== void 0) promoted._ts = existing._ts;
1717
2024
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1718
2025
  if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1719
2026
  if (existing.isSendingToServer) promoted.isSendingToServer = true;
@@ -1763,6 +2070,7 @@ var ChatSession = class {
1763
2070
  var repl = { role: "user", content: exist.content };
1764
2071
  if (exist._serverItemId !== void 0) repl._serverItemId = exist._serverItemId;
1765
2072
  if (exist._ownerKey !== void 0) repl._ownerKey = exist._ownerKey;
2073
+ if (exist._ts !== void 0) repl._ts = exist._ts;
1766
2074
  this.state.messages[userIdx] = repl;
1767
2075
  }
1768
2076
  var thinkingIdx = userIdx >= 0 ? this.state.messages.findIndex(function(m, i) {
@@ -1771,6 +2079,7 @@ var ChatSession = class {
1771
2079
  return thinkingIdx !== -1 ? thinkingIdx : userIdx >= 0 ? userIdx + 1 : -1;
1772
2080
  }
1773
2081
  insertAtTarget(msg, targetIdx) {
2082
+ if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = wallClockNow();
1774
2083
  if (targetIdx >= 0 && this.state.messages[targetIdx] && this.state.messages[targetIdx].isPending) this.state.messages[targetIdx] = msg;
1775
2084
  else if (targetIdx >= 0) this.state.messages.splice(targetIdx, 0, msg);
1776
2085
  else this.state.messages.push(msg);
@@ -1814,7 +2123,7 @@ var ChatSession = class {
1814
2123
  this.promoteNextQueuedToRunning();
1815
2124
  this.updateHistoryCache();
1816
2125
  this.host.notify();
1817
- this.host.scrollToBottom(true);
2126
+ this.host.scrollToBottomIfSticky(true);
1818
2127
  }
1819
2128
  onQueuedSendError(_composed, err, serverId, ownerKey) {
1820
2129
  if (serverId) this.historyItemPolls.delete(serverId);
@@ -1864,7 +2173,7 @@ var ChatSession = class {
1864
2173
  this.promoteNextQueuedToRunning();
1865
2174
  this.updateHistoryCache();
1866
2175
  this.host.notify();
1867
- this.host.scrollToBottom(true);
2176
+ this.host.scrollToBottomIfSticky(true);
1868
2177
  return;
1869
2178
  }
1870
2179
  var targetIdx = this.resolveQueuedUserBubble(serverId);
@@ -1878,7 +2187,7 @@ var ChatSession = class {
1878
2187
  this.promoteNextQueuedToRunning();
1879
2188
  this.updateHistoryCache();
1880
2189
  this.host.notify();
1881
- this.host.scrollToBottom(true);
2190
+ this.host.scrollToBottomIfSticky(true);
1882
2191
  }
1883
2192
  cancelQueuedMessage(msg, idx) {
1884
2193
  var self = this;
@@ -1902,6 +2211,7 @@ var ChatSession = class {
1902
2211
  })).then(function(result) {
1903
2212
  if (result && result.removed) {
1904
2213
  self.cancelledServerIds.add(serverId);
2214
+ self._stopPoll(serverId);
1905
2215
  var qi = self.bgTaskQueue.findIndex(function(e) {
1906
2216
  return e.id === serverId;
1907
2217
  });
@@ -1910,7 +2220,13 @@ var ChatSession = class {
1910
2220
  return m._serverItemId === serverId && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user";
1911
2221
  });
1912
2222
  if (removeIdx !== -1) {
1913
- self.state.messages[removeIdx] = { role: "user", content: self.state.messages[removeIdx].content, isCancelled: true, _serverItemId: serverId };
2223
+ var wasMsg = self.state.messages[removeIdx];
2224
+ var cancelledMsg = { role: "user", content: wasMsg.content, isCancelled: true, _serverItemId: serverId };
2225
+ if (wasMsg.isBackgroundTask) cancelledMsg.isBackgroundTask = true;
2226
+ if (wasMsg._indexFile) cancelledMsg._indexFile = wasMsg._indexFile;
2227
+ if (wasMsg._useBgQueue) cancelledMsg._useBgQueue = true;
2228
+ if (wasMsg._ownerKey !== void 0) cancelledMsg._ownerKey = wasMsg._ownerKey;
2229
+ self.state.messages[removeIdx] = cancelledMsg;
1914
2230
  var thById = self.state.messages.findIndex(function(m) {
1915
2231
  return m._serverItemId === serverId && m.isPending && m.role === "assistant";
1916
2232
  });
@@ -1947,6 +2263,42 @@ var ChatSession = class {
1947
2263
  }
1948
2264
  });
1949
2265
  }
2266
+ /**
2267
+ * Stop indexing a file, from its collapsed row — every pass at once, not just
2268
+ * the bubble the user happens to see.
2269
+ *
2270
+ * A big file is indexed as a CHAIN of passes, so cancelling only the live one
2271
+ * accomplishes nothing: the next pass is dispatched as soon as it settles.
2272
+ * Three things end the chain:
2273
+ * 1. every queued/running pass of this file is cancelled server-side
2274
+ * (csr-cancel deletes a queued row and flags a running one "cancelled",
2275
+ * which is also the worker's gate for NOT enqueueing the next window);
2276
+ * 2. the file is remembered in cancelledIndexKeys, so the client-driven
2277
+ * resume (maybeResumeIndexing) stops dispatching CONTINUE passes; and
2278
+ * 3. any of its passes still sitting in bgTaskQueue is dropped by the next
2279
+ * drain rather than surfacing a fresh "Indexing…" bubble.
2280
+ *
2281
+ * Records already written by the passes that DID run are kept — this stops the
2282
+ * work, it does not undo it.
2283
+ */
2284
+ cancelIndexingGroup(group) {
2285
+ var self = this;
2286
+ if (!group || !group.key) return;
2287
+ var scoped = this.getHistoryCacheKey() + "|" + group.key;
2288
+ this.cancelledIndexKeys.add(scoped);
2289
+ var ids = group.cancellableIds || [];
2290
+ if (!ids.length) {
2291
+ this.host.notify();
2292
+ return;
2293
+ }
2294
+ ids.forEach(function(serverId) {
2295
+ var idx = self.state.messages.findIndex(function(m) {
2296
+ return m._serverItemId === serverId && m.role === "user" && (m.isPendingQueued || m.isPendingInProcess);
2297
+ });
2298
+ if (idx === -1) return;
2299
+ self.cancelQueuedMessage(self.state.messages[idx], idx);
2300
+ });
2301
+ }
1950
2302
  // --- typewriter -------------------------------------------------------
1951
2303
  // Reveal `fullText` into a message bubble at a constant wall-clock RATE
1952
2304
  // (chars/second) driven by requestAnimationFrame, rather than a fixed number
@@ -2064,6 +2416,8 @@ var ChatSession = class {
2064
2416
  }
2065
2417
  enqueueTypewrite(idx, fullText, localId) {
2066
2418
  var self = this;
2419
+ var target = this.state.messages[idx];
2420
+ if (target && target._ts === void 0) target._ts = wallClockNow();
2067
2421
  this.typewriterQueue = this.typewriterQueue.then(function() {
2068
2422
  return self.typewriteIntoIndex(idx, fullText, localId);
2069
2423
  });
@@ -2133,6 +2487,8 @@ var ChatSession = class {
2133
2487
  var u = this.state.messages[uIdx];
2134
2488
  var cleaned = { role: "user", content: u.content, _serverItemId: itemId };
2135
2489
  if (u.isBackgroundTask) cleaned.isBackgroundTask = true;
2490
+ if (u._ts !== void 0) cleaned._ts = u._ts;
2491
+ if (u._indexFile) cleaned._indexFile = u._indexFile;
2136
2492
  this.state.messages[uIdx] = cleaned;
2137
2493
  }
2138
2494
  // If an immediate-send request for the current cache key is still in flight
@@ -2150,13 +2506,13 @@ var ChatSession = class {
2150
2506
  return (m.isPending || m.isPendingQueued) && !m.isBackgroundTask && !m._useBgQueue;
2151
2507
  })) return Promise.resolve();
2152
2508
  this.state.sending = true;
2153
- this.host.scrollToBottom(true);
2509
+ this.host.scrollToBottomIfSticky(true);
2154
2510
  return Promise.resolve(pending).catch(function() {
2155
2511
  }).then(function() {
2156
2512
  if (token !== self.state.gateRefreshToken) return;
2157
2513
  self.state.sending = false;
2158
2514
  return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
2159
- self.host.scrollToBottom(true);
2515
+ self.host.scrollToBottomIfSticky(true);
2160
2516
  });
2161
2517
  });
2162
2518
  }
@@ -2175,8 +2531,17 @@ var ChatSession = class {
2175
2531
  });
2176
2532
  if (idx !== -1) {
2177
2533
  this._clearPendingUserBubble(itemId);
2534
+ var wasBgTask = !!this.state.messages[idx].isBackgroundTask;
2178
2535
  if (isErr) {
2179
2536
  this.state.messages[idx] = { role: "assistant", content: answer, isError: true, _serverItemId: itemId };
2537
+ if (wasBgTask) this.state.messages[idx].isBackgroundTask = true;
2538
+ this.host.notify();
2539
+ this.updateHistoryCache();
2540
+ return;
2541
+ }
2542
+ var text = answer || "No text response received from AI provider.";
2543
+ if (wasBgTask) {
2544
+ this.state.messages[idx] = { role: "assistant", content: text, isBackgroundTask: true, _serverItemId: itemId };
2180
2545
  this.host.notify();
2181
2546
  this.updateHistoryCache();
2182
2547
  return;
@@ -2184,7 +2549,7 @@ var ChatSession = class {
2184
2549
  var lid = this._newLocalId();
2185
2550
  this.state.messages[idx] = { role: "assistant", content: "", _localId: lid, _serverItemId: itemId };
2186
2551
  this.host.notify();
2187
- this.enqueueTypewrite(idx, answer || "No text response received from AI provider.", lid);
2552
+ this.enqueueTypewrite(idx, text, lid);
2188
2553
  this.updateHistoryCache();
2189
2554
  return;
2190
2555
  }
@@ -2193,25 +2558,125 @@ var ChatSession = class {
2193
2558
  });
2194
2559
  if (userIdx === -1) return;
2195
2560
  var ex = this.state.messages[userIdx];
2196
- this.state.messages[userIdx] = { role: "user", content: ex.content, _serverItemId: itemId };
2561
+ var settledUser = { role: "user", content: ex.content, _serverItemId: itemId };
2562
+ if (ex.isBackgroundTask) settledUser.isBackgroundTask = true;
2563
+ if (ex._ts !== void 0) settledUser._ts = ex._ts;
2564
+ if (ex._indexFile) settledUser._indexFile = ex._indexFile;
2565
+ if (ex._useBgQueue) settledUser._useBgQueue = true;
2566
+ this.state.messages[userIdx] = settledUser;
2197
2567
  if (isErr) {
2198
- this.state.messages.splice(userIdx + 1, 0, { role: "assistant", content: answer, isError: true, _serverItemId: itemId });
2568
+ var errReply = { role: "assistant", content: answer, isError: true, _serverItemId: itemId };
2569
+ if (ex.isBackgroundTask) errReply.isBackgroundTask = true;
2570
+ this.state.messages.splice(userIdx + 1, 0, errReply);
2571
+ this.host.notify();
2572
+ this.updateHistoryCache();
2573
+ return;
2574
+ }
2575
+ var text2 = answer || "No text response received from AI provider.";
2576
+ if (ex.isBackgroundTask) {
2577
+ this.state.messages.splice(userIdx + 1, 0, { role: "assistant", content: text2, isBackgroundTask: true, _serverItemId: itemId });
2199
2578
  this.host.notify();
2200
2579
  this.updateHistoryCache();
2201
2580
  return;
2202
2581
  }
2203
2582
  var lid2 = this._newLocalId();
2204
- this.state.messages.splice(userIdx + 1, 0, { role: "assistant", content: "", _localId: lid2, _serverItemId: itemId });
2583
+ var reply = { role: "assistant", content: "", _localId: lid2, _serverItemId: itemId };
2584
+ this.state.messages.splice(userIdx + 1, 0, reply);
2205
2585
  this.host.notify();
2206
- this.enqueueTypewrite(userIdx + 1, answer || "No text response received from AI provider.", lid2);
2586
+ this.enqueueTypewrite(userIdx + 1, text2, lid2);
2207
2587
  this.updateHistoryCache();
2208
2588
  }
2589
+ /** How a bg task maps onto a collapsed row: the row's own key (storage path
2590
+ * when known, else the filename), scoped to the chat it belongs to. A storage
2591
+ * path is project-relative ("report.xlsx"), and ONE ChatSession serves every
2592
+ * project — unscoped, stopping a file in one project would silently suppress
2593
+ * the same filename's continuations in another. */
2594
+ _indexKeyOf(entry) {
2595
+ if (!entry) return "";
2596
+ var file = entry.storagePath || entry.filename;
2597
+ if (!file) return "";
2598
+ return entry.serviceId + "#" + entry.platform + "|" + file;
2599
+ }
2600
+ /**
2601
+ * Reconcile the bg queue with the files the user has stopped.
2602
+ *
2603
+ * A FIRST pass (no resumePass) is a fresh indexing request — a re-upload, or a
2604
+ * Reindex from the file manager — so it LIFTS the stop: the key is a storage
2605
+ * path, and without this an earlier cancel would silently kill every future
2606
+ * index of the same path. A continuation of a stopped file is dropped instead,
2607
+ * covering the pass that was dispatched in the moment before the cancel landed.
2608
+ */
2609
+ _applyIndexCancellations() {
2610
+ if (!this.cancelledIndexKeys.size) return;
2611
+ for (var i = this.bgTaskQueue.length - 1; i >= 0; i--) {
2612
+ var entry = this.bgTaskQueue[i];
2613
+ var key = this._indexKeyOf(entry);
2614
+ if (!key || !this.cancelledIndexKeys.has(key)) continue;
2615
+ if (!entry.resumePass) {
2616
+ this.cancelledIndexKeys.delete(key);
2617
+ continue;
2618
+ }
2619
+ this.bgTaskQueue.splice(i, 1);
2620
+ this._stopPoll(entry.id);
2621
+ this._cancelServerItem(entry.id);
2622
+ }
2623
+ }
2624
+ /**
2625
+ * Cancel any live pass of a stopped file that turned up on its own.
2626
+ *
2627
+ * The client is not the only thing that continues a file: for PDFs and (when
2628
+ * windowed indexing is on) text/grid files the WORKER enqueues the next window
2629
+ * itself, and that pass reaches the chat through the history poll, never
2630
+ * through bgTaskQueue. The worker's own gate stops the chain when the running
2631
+ * row is cancelled — but if the row had already finished when the user hit
2632
+ * stop, the next window was queued a moment earlier and still arrives. Stop it
2633
+ * here rather than making the user hit stop again.
2634
+ *
2635
+ * Runs from drainBgTaskQueue, which both clients call after a history load.
2636
+ */
2637
+ _sweepCancelledIndexing() {
2638
+ if (!this.cancelledIndexKeys.size) return;
2639
+ var self = this;
2640
+ var chatKey = this.getHistoryCacheKey();
2641
+ var targets = [];
2642
+ this.state.messages.forEach(function(m, i) {
2643
+ if (!m.isBackgroundTask || m.role !== "user" || !m._serverItemId) return;
2644
+ if (m._cancelling || m.isSendingToServer) return;
2645
+ if (!(m.isPendingQueued || m.isPendingInProcess)) return;
2646
+ var ref = m._indexFile;
2647
+ var file = ref && (ref.path || ref.name);
2648
+ if (!file || !self.cancelledIndexKeys.has(chatKey + "|" + file)) return;
2649
+ targets.push({ msg: m, idx: i });
2650
+ });
2651
+ targets.forEach(function(t) {
2652
+ var idx = self.state.messages.indexOf(t.msg);
2653
+ self.cancelQueuedMessage(t.msg, idx === -1 ? t.idx : idx);
2654
+ });
2655
+ }
2656
+ /** Best-effort server-side cancel of a bg-queue item that has no bubble (so
2657
+ * cancelQueuedMessage, which drives one, has nothing to act on). */
2658
+ _cancelServerItem(serverId) {
2659
+ var id = this.host.getIdentity();
2660
+ if (!serverId || id.platform !== "claude" && id.platform !== "openai") return;
2661
+ var url = id.platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
2662
+ Promise.resolve(this.host.cancelRequest({
2663
+ url,
2664
+ method: "POST",
2665
+ id: serverId,
2666
+ queue: (id.userId || id.serviceId) + BG_INDEXING_QUEUE_SUFFIX,
2667
+ service: id.serviceId,
2668
+ owner: id.owner
2669
+ })).catch(function() {
2670
+ });
2671
+ }
2209
2672
  // Inject "Indexing: <file>" bubbles for queued bg tasks + attach their polls.
2210
2673
  drainBgTaskQueue() {
2211
2674
  var self = this;
2212
2675
  var id = this.host.getIdentity();
2213
2676
  var svcId = id.serviceId, plat = id.platform;
2214
2677
  if (!svcId || plat === "none" || !this.host.isViewMounted()) return;
2678
+ this._applyIndexCancellations();
2679
+ this._sweepCancelledIndexing();
2215
2680
  var presentIds = {};
2216
2681
  var pendingIds = {};
2217
2682
  this.state.messages.forEach(function(m) {
@@ -2229,7 +2694,22 @@ var ChatSession = class {
2229
2694
  if (entry.serviceId !== svcId || entry.platform !== plat) return;
2230
2695
  if (!presentIds[entry.id]) {
2231
2696
  var isRunning = entry.status === "running";
2232
- var userBubble = { role: "user", content: self.host.formatIndexingLabel(entry.filename, entry.mime, entry.size, entry.storagePath, entry.isReindex), isBackgroundTask: true, _serverItemId: entry.id };
2697
+ var userBubble = {
2698
+ role: "user",
2699
+ content: self.host.formatIndexingLabel(entry.filename, entry.mime, entry.size, entry.storagePath, entry.isReindex, !!entry.resumePass),
2700
+ isBackgroundTask: true,
2701
+ _serverItemId: entry.id,
2702
+ // Structured ref so this live pass groups with the same file's passes
2703
+ // rebuilt from history (see indexing_groups.buildChatDisplayList).
2704
+ _indexFile: {
2705
+ name: entry.filename,
2706
+ path: entry.storagePath,
2707
+ mime: entry.mime,
2708
+ size: entry.size,
2709
+ isReindex: !!entry.isReindex,
2710
+ continued: !!entry.resumePass
2711
+ }
2712
+ };
2233
2713
  if (isRunning) userBubble.isPendingInProcess = true;
2234
2714
  else userBubble.isPendingQueued = true;
2235
2715
  self.state.messages.push(userBubble);
@@ -2239,7 +2719,7 @@ var ChatSession = class {
2239
2719
  presentIds[entry.id] = true;
2240
2720
  self.host.notify();
2241
2721
  self.updateHistoryCache();
2242
- self.host.scrollToBottom(false);
2722
+ self.host.scrollToBottomIfSticky(false);
2243
2723
  }
2244
2724
  if (!self.isPollingPaused() && !self.historyItemPolls.has(entry.id) && typeof entry.poll === "function") {
2245
2725
  var capturedId = entry.id, capturedPlat = plat;
@@ -2257,15 +2737,23 @@ var ChatSession = class {
2257
2737
  }).catch(function(err) {
2258
2738
  self.historyItemPolls.delete(capturedId);
2259
2739
  var isNotExists = err && (err.code === "NOT_EXISTS" || err.body && err.body.code === "NOT_EXISTS");
2740
+ self._clearPendingUserBubble(capturedId);
2260
2741
  var bi = self.state.messages.findIndex(function(m) {
2261
2742
  return m.isPending && m._serverItemId === capturedId;
2262
2743
  });
2263
2744
  if (bi !== -1) {
2264
2745
  if (isNotExists) self.state.messages.splice(bi, 1);
2265
2746
  else self.state.messages[bi] = { role: "assistant", content: getErrorMessage(err), isError: true, isBackgroundTask: true, _serverItemId: capturedId };
2266
- self.host.notify();
2267
- self.updateHistoryCache();
2747
+ } else if (!isNotExists) {
2748
+ var ui = self.state.messages.findIndex(function(m) {
2749
+ return m.role === "user" && m._serverItemId === capturedId;
2750
+ });
2751
+ if (ui !== -1) {
2752
+ self.state.messages.splice(ui + 1, 0, { role: "assistant", content: getErrorMessage(err), isError: true, isBackgroundTask: true, _serverItemId: capturedId });
2753
+ }
2268
2754
  }
2755
+ self.host.notify();
2756
+ self.updateHistoryCache();
2269
2757
  }).then(function() {
2270
2758
  if (wasStopped) return;
2271
2759
  var qi = self.bgTaskQueue.findIndex(function(q) {
@@ -2293,8 +2781,10 @@ var ChatSession = class {
2293
2781
  var self = this;
2294
2782
  try {
2295
2783
  if (!entry || !entry.storagePath) return;
2784
+ if (this.cancelledIndexKeys.has(this._indexKeyOf(entry))) return;
2296
2785
  if (!isPagedReadFile(entry.filename, entry.mime)) return;
2297
2786
  if (isImageVisionFile(entry.filename, entry.mime)) return;
2787
+ if (windowedIndexingEnabled() && isWindowedReadFile(entry.filename, entry.mime)) return;
2298
2788
  if (isErrorResponseBody(response)) return;
2299
2789
  var answer = (platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "";
2300
2790
  if (answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1) return;
@@ -2387,6 +2877,7 @@ var ChatSession = class {
2387
2877
  serviceId: id.serviceId,
2388
2878
  formatIndexingLabel: self.host.formatIndexingLabel
2389
2879
  }).messages;
2880
+ var keptOlderPages = false;
2390
2881
  if (fetchMore) {
2391
2882
  self.state.messages = mapped.concat(self.state.messages);
2392
2883
  } else {
@@ -2397,7 +2888,12 @@ var ChatSession = class {
2397
2888
  });
2398
2889
  var locallyCancelled = {};
2399
2890
  self.state.messages.forEach(function(m) {
2400
- if (m.isCancelled && m._serverItemId) locallyCancelled[m._serverItemId] = 1;
2891
+ if (m.isCancelled && m._serverItemId) locallyCancelled[m._serverItemId] = m;
2892
+ });
2893
+ var inFlightCancel = {};
2894
+ self.state.messages.forEach(function(m) {
2895
+ if (!m._serverItemId) return;
2896
+ if (m._cancelling || m._cancelError) inFlightCancel[m._serverItemId] = m;
2401
2897
  });
2402
2898
  var mappedHasPendingAssistant = mapped.some(function(m) {
2403
2899
  return m.isPending && m.role === "assistant" && !m.isBackgroundTask;
@@ -2417,7 +2913,22 @@ var ChatSession = class {
2417
2913
  }
2418
2914
  }
2419
2915
  }
2420
- self.state.messages = mapped;
2916
+ var oldestInPage1 = void 0;
2917
+ mapped.forEach(function(m) {
2918
+ var sid = m._serverItemId;
2919
+ if (typeof sid !== "string") return;
2920
+ if (oldestInPage1 === void 0 || sid < oldestInPage1) oldestInPage1 = sid;
2921
+ });
2922
+ var sharesPage1 = self.state.messages.some(function(m) {
2923
+ return typeof m._serverItemId === "string" && !!serverIds[m._serverItemId];
2924
+ });
2925
+ var retainedOlder = !sharesPage1 || oldestInPage1 === void 0 ? [] : self.state.messages.filter(function(m) {
2926
+ if (typeof m._serverItemId !== "string") return false;
2927
+ if (m._ownerKey !== void 0 && m._ownerKey !== loadKey) return false;
2928
+ return m._serverItemId < oldestInPage1;
2929
+ });
2930
+ keptOlderPages = retainedOlder.length > 0;
2931
+ self.state.messages = keptOlderPages ? retainedOlder.concat(mapped) : mapped;
2421
2932
  rescued.forEach(function(m) {
2422
2933
  self.state.messages.push(m);
2423
2934
  });
@@ -2425,19 +2936,38 @@ var ChatSession = class {
2425
2936
  for (var ci = 0; ci < self.state.messages.length; ci++) {
2426
2937
  var c = self.state.messages[ci];
2427
2938
  if (!c._serverItemId || !locallyCancelled[c._serverItemId] || c.isCancelled) continue;
2428
- self.state.messages[ci] = { role: "user", content: c.content, isCancelled: true, _serverItemId: c._serverItemId };
2939
+ self.state.messages[ci] = {
2940
+ role: "user",
2941
+ content: c.content,
2942
+ isCancelled: true,
2943
+ _serverItemId: c._serverItemId,
2944
+ isBackgroundTask: c.isBackgroundTask,
2945
+ _indexFile: c._indexFile,
2946
+ _useBgQueue: c._useBgQueue,
2947
+ _ownerKey: c._ownerKey
2948
+ };
2429
2949
  if (ci + 1 < self.state.messages.length && self.state.messages[ci + 1].isPending && self.state.messages[ci + 1]._serverItemId === c._serverItemId) {
2430
2950
  self.state.messages.splice(ci + 1, 1);
2431
2951
  }
2432
2952
  }
2433
2953
  }
2954
+ for (var fi = 0; fi < self.state.messages.length; fi++) {
2955
+ var fm = self.state.messages[fi];
2956
+ var was = fm._serverItemId && inFlightCancel[fm._serverItemId];
2957
+ if (!was || fm.isCancelled) continue;
2958
+ if (!(fm.isPendingQueued || fm.isPendingInProcess || fm.isPending)) continue;
2959
+ if (was._cancelling) fm._cancelling = true;
2960
+ if (was._cancelError) fm._cancelError = was._cancelError;
2961
+ }
2434
2962
  }
2435
- self.state.historyEndOfList = !!(history && history.endOfList);
2436
- self.state.historyStartKeyHistory = history && Array.isArray(history.startKeyHistory) ? history.startKeyHistory : [];
2437
- var clearedAt = self.host.getClearedAt();
2438
- if (clearedAt && chatList.length > 0) {
2439
- var oldestUpdated = Number(chatList[chatList.length - 1] && chatList[chatList.length - 1].updated);
2440
- if (isFinite(oldestUpdated) && oldestUpdated <= clearedAt) self.state.historyEndOfList = true;
2963
+ if (!keptOlderPages) {
2964
+ self.state.historyEndOfList = !!(history && history.endOfList);
2965
+ self.state.historyStartKeyHistory = history && Array.isArray(history.startKeyHistory) ? history.startKeyHistory : [];
2966
+ var clearedAt = self.host.getClearedAt();
2967
+ if (clearedAt && chatList.length > 0) {
2968
+ var oldestUpdated = Number(chatList[chatList.length - 1] && chatList[chatList.length - 1].updated);
2969
+ if (isFinite(oldestUpdated) && oldestUpdated <= clearedAt) self.state.historyEndOfList = true;
2970
+ }
2441
2971
  }
2442
2972
  if (self.state.historyRequestToken === token) {
2443
2973
  self.state.loadingHistory = false;
@@ -2466,18 +2996,25 @@ var ChatSession = class {
2466
2996
  return m.isPending && m._serverItemId === capturedId;
2467
2997
  });
2468
2998
  if (isNotExists) {
2469
- var isBg = aIdx !== -1 ? !!self.state.messages[aIdx].isBackgroundTask : false;
2999
+ var uIdx = self.state.messages.findIndex(function(m) {
3000
+ return m.role === "user" && m._serverItemId === capturedId && !m.isCancelled;
3001
+ });
3002
+ var isBg = aIdx !== -1 && !!self.state.messages[aIdx].isBackgroundTask || uIdx !== -1 && !!self.state.messages[uIdx].isBackgroundTask;
2470
3003
  if (aIdx !== -1) self.state.messages.splice(aIdx, 1);
2471
3004
  if (!isBg) {
2472
- var uIdx = self.state.messages.findIndex(function(m) {
2473
- return m.role === "user" && m._serverItemId === capturedId && !m.isCancelled;
2474
- });
2475
3005
  if (uIdx !== -1) {
2476
3006
  var ex = self.state.messages[uIdx];
2477
3007
  self.state.messages[uIdx] = { role: "user", content: ex.content, isCancelled: true, _serverItemId: ex._serverItemId };
2478
3008
  }
2479
3009
  self.cancelledServerIds.delete(capturedId);
2480
3010
  self.promoteNextQueuedToRunning();
3011
+ } else if (uIdx !== -1) {
3012
+ var bex = self.state.messages[uIdx];
3013
+ var bcancelled = { role: "user", content: bex.content, isCancelled: true, isBackgroundTask: true, _serverItemId: bex._serverItemId };
3014
+ if (bex._indexFile) bcancelled._indexFile = bex._indexFile;
3015
+ if (bex._useBgQueue) bcancelled._useBgQueue = true;
3016
+ self.state.messages[uIdx] = bcancelled;
3017
+ self.promoteNextBgQueuedToRunning();
2481
3018
  }
2482
3019
  self.host.notify();
2483
3020
  self.updateHistoryCache();
@@ -2498,7 +3035,7 @@ var ChatSession = class {
2498
3035
  });
2499
3036
  self.drainBgTaskQueue();
2500
3037
  }
2501
- if (!fetchMore) return self.host.scrollToBottom();
3038
+ if (!fetchMore) return self.host.scrollToBottomIfSticky();
2502
3039
  }).catch(function(err) {
2503
3040
  console.warn("[chat-engine] getChatHistory failed", err);
2504
3041
  }).then(function() {
@@ -2508,6 +3045,7 @@ var ChatSession = class {
2508
3045
  self.state.loadingOlderHistory = false;
2509
3046
  if (wasLoading) self.host.notify();
2510
3047
  }
3048
+ if (self.host.onHistoryLoaded) self.host.onHistoryLoaded(!!fetchMore, token);
2511
3049
  });
2512
3050
  }
2513
3051
  // --- attachment upload orchestration ---------------------------------
@@ -2705,6 +3243,194 @@ var ChatSession = class {
2705
3243
  }
2706
3244
  };
2707
3245
 
3246
+ // src/engine/indexing_groups.ts
3247
+ var INDEXING_LABEL_RE = /^(Re)?[Ii]ndexing(\s*\(continuing\))?\s*:?\s+(.+)$/;
3248
+ var LEADING_MD_LINK_RE = /^\[([^\]]+)\]\(([^)]+)\)/;
3249
+ function parseIndexingLabel(content) {
3250
+ if (typeof content !== "string" || !content) return null;
3251
+ var firstLine = content.split("\n")[0].trim();
3252
+ var m = firstLine.match(INDEXING_LABEL_RE);
3253
+ if (!m) return null;
3254
+ var head = m[3].split(" \xB7 ")[0].trim();
3255
+ var link = head.match(LEADING_MD_LINK_RE);
3256
+ var name = link ? link[1].trim() : head;
3257
+ if (!name) return null;
3258
+ return {
3259
+ name,
3260
+ path: link ? link[2].trim() : void 0,
3261
+ continued: !!m[2],
3262
+ isReindex: !!m[1]
3263
+ };
3264
+ }
3265
+ function readFileRef(msg) {
3266
+ var ref = msg && msg._indexFile;
3267
+ if (ref && (ref.path || ref.name)) {
3268
+ return {
3269
+ name: ref.name || ref.path || "",
3270
+ path: ref.path,
3271
+ mime: ref.mime,
3272
+ size: ref.size,
3273
+ isReindex: ref.isReindex,
3274
+ continued: !!ref.continued
3275
+ };
3276
+ }
3277
+ var parsed = parseIndexingLabel(msg && msg.content);
3278
+ if (!parsed) return null;
3279
+ return {
3280
+ name: parsed.name,
3281
+ path: parsed.path,
3282
+ isReindex: parsed.isReindex,
3283
+ continued: parsed.continued
3284
+ };
3285
+ }
3286
+ function isPendingMsg(m) {
3287
+ return !!(m.isPending || m.isPendingInProcess || m.isPendingQueued || m.isSendingToServer);
3288
+ }
3289
+ function buildChatDisplayList(messages, opts) {
3290
+ var list = Array.isArray(messages) ? messages : [];
3291
+ var hasMoreHistory = !!(opts && opts.hasMoreHistory);
3292
+ var groups = {};
3293
+ var order = [];
3294
+ var runOfIndex = new Array(list.length);
3295
+ var runByItemId = {};
3296
+ var keyByName = {};
3297
+ var openRunOfKey = {};
3298
+ var runsOfKey = {};
3299
+ var keyOfRun = {};
3300
+ var runSeq = 0;
3301
+ for (var i = 0; i < list.length; i++) {
3302
+ var msg = list[i];
3303
+ if (!msg || !msg.isBackgroundTask) continue;
3304
+ var runId;
3305
+ var ref = msg.role === "user" ? readFileRef(msg) : null;
3306
+ if (ref) {
3307
+ var key = ref.path || keyByName[ref.name] || ref.name;
3308
+ if (!ref.continued && openRunOfKey[key]) delete openRunOfKey[key];
3309
+ runId = openRunOfKey[key];
3310
+ if (!runId) {
3311
+ runId = "run" + runSeq++;
3312
+ openRunOfKey[key] = runId;
3313
+ keyOfRun[runId] = key;
3314
+ (runsOfKey[key] || (runsOfKey[key] = [])).push(runId);
3315
+ }
3316
+ } else if (msg._serverItemId && runByItemId[msg._serverItemId]) {
3317
+ runId = runByItemId[msg._serverItemId];
3318
+ } else if (msg.role !== "user") {
3319
+ runId = runOfIndex[i - 1];
3320
+ }
3321
+ if (!runId) continue;
3322
+ var g = groups[runId];
3323
+ if (!g) {
3324
+ var fileKey = keyOfRun[runId];
3325
+ g = groups[runId] = {
3326
+ key: fileKey,
3327
+ runKey: runId,
3328
+ // provisional; renumbered newest-first below
3329
+ name: ref ? ref.name : fileKey,
3330
+ path: ref ? ref.path : void 0,
3331
+ mime: ref ? ref.mime : void 0,
3332
+ size: ref ? ref.size : void 0,
3333
+ isReindex: !!(ref && ref.isReindex),
3334
+ members: [],
3335
+ passCount: 0,
3336
+ status: "done",
3337
+ cancellableIds: [],
3338
+ cancelling: false,
3339
+ mayHaveOlder: false,
3340
+ // The run's first loaded pass, and never re-stamped: see the file
3341
+ // docstring. `anchorId` is filled in once every member is known.
3342
+ anchorIndex: i,
3343
+ anchorId: ""
3344
+ };
3345
+ order.push(runId);
3346
+ }
3347
+ if (ref) {
3348
+ if (ref.name) g.name = ref.name;
3349
+ if (ref.path) g.path = ref.path;
3350
+ if (ref.mime) g.mime = ref.mime;
3351
+ if (typeof ref.size === "number") g.size = ref.size;
3352
+ if (ref.isReindex) g.isReindex = true;
3353
+ if (!ref.continued) g.mayHaveOlder = false;
3354
+ g.passCount++;
3355
+ }
3356
+ g.members.push({ msg, index: i });
3357
+ runOfIndex[i] = runId;
3358
+ if (msg._serverItemId) runByItemId[msg._serverItemId] = runId;
3359
+ if (ref && ref.name) keyByName[ref.name] = g.key;
3360
+ }
3361
+ for (var rk in runsOfKey) {
3362
+ var runIds = runsOfKey[rk];
3363
+ for (var ri = 0; ri < runIds.length; ri++) {
3364
+ var grpR = groups[runIds[ri]];
3365
+ if (!grpR) continue;
3366
+ var first = grpR.members[0];
3367
+ var firstId = first && first.msg && (first.msg._serverItemId || first.msg._localId);
3368
+ grpR.runKey = rk + "#" + (firstId || "n" + ri);
3369
+ }
3370
+ }
3371
+ for (var oi = 0; oi < order.length; oi++) {
3372
+ var grp = groups[order[oi]];
3373
+ var lastSettled = -1;
3374
+ for (var si = 0; si < grp.members.length; si++) {
3375
+ if (!isPendingMsg(grp.members[si].msg)) lastSettled = si;
3376
+ }
3377
+ var active = false;
3378
+ for (var mi = lastSettled + 1; mi < grp.members.length; mi++) {
3379
+ if (isPendingMsg(grp.members[mi].msg)) {
3380
+ active = true;
3381
+ break;
3382
+ }
3383
+ }
3384
+ for (var xi = 0; xi < grp.members.length; xi++) {
3385
+ if (grp.members[xi].msg._cancelling) {
3386
+ grp.cancelling = true;
3387
+ break;
3388
+ }
3389
+ }
3390
+ var seenIds = {};
3391
+ for (var ci = 0; ci < grp.members.length; ci++) {
3392
+ var cm = grp.members[ci].msg;
3393
+ if (cm._cancelError && (active || grp.cancelling)) grp.cancelError = cm._cancelError;
3394
+ if (cm.role !== "user" || !cm._serverItemId || cm._cancelling || cm.isSendingToServer) continue;
3395
+ if (!(cm.isPendingQueued || cm.isPendingInProcess)) continue;
3396
+ if (ci < lastSettled) continue;
3397
+ if (seenIds[cm._serverItemId]) continue;
3398
+ seenIds[cm._serverItemId] = true;
3399
+ grp.cancellableIds.push(cm._serverItemId);
3400
+ }
3401
+ if (active) {
3402
+ grp.status = "active";
3403
+ } else {
3404
+ var last = grp.members[grp.members.length - 1].msg;
3405
+ grp.status = last.isError ? "error" : last.isCancelled ? "cancelled" : "done";
3406
+ }
3407
+ var sawFirstPass = false;
3408
+ for (var pi = 0; pi < grp.members.length; pi++) {
3409
+ var pm = grp.members[pi].msg;
3410
+ if (pm.role !== "user") continue;
3411
+ var pref = readFileRef(pm);
3412
+ if (pref && !pref.continued) {
3413
+ sawFirstPass = true;
3414
+ break;
3415
+ }
3416
+ }
3417
+ grp.mayHaveOlder = !sawFirstPass && hasMoreHistory;
3418
+ var anchor = grp.members[0];
3419
+ grp.anchorIndex = anchor.index;
3420
+ grp.anchorId = anchor.msg._serverItemId || anchor.msg._localId || "";
3421
+ }
3422
+ var out = [];
3423
+ for (var j = 0; j < list.length; j++) {
3424
+ var r = runOfIndex[j];
3425
+ if (r === void 0) {
3426
+ out.push({ kind: "message", msg: list[j], index: j });
3427
+ continue;
3428
+ }
3429
+ if (groups[r].anchorIndex === j) out.push({ kind: "indexing", group: groups[r], index: j });
3430
+ }
3431
+ return out;
3432
+ }
3433
+
2708
3434
  exports.BG_INDEXING_QUEUE_SUFFIX = BG_INDEXING_QUEUE_SUFFIX;
2709
3435
  exports.CLAUDE_PER_REQUEST_INPUT_CAP = CLAUDE_PER_REQUEST_INPUT_CAP;
2710
3436
  exports.CONTEXT_WINDOW_BY_MODEL = CONTEXT_WINDOW_BY_MODEL;
@@ -2714,8 +3440,12 @@ exports.DEFAULT_CLAUDE_MODEL = DEFAULT_CLAUDE_MODEL;
2714
3440
  exports.DEFAULT_OPENAI_MODEL = DEFAULT_OPENAI_MODEL;
2715
3441
  exports.EXPIRED_ATTACHMENT_URL_HOST = EXPIRED_ATTACHMENT_URL_HOST;
2716
3442
  exports.EXPIRED_ATTACHMENT_URL_ORIGIN = EXPIRED_ATTACHMENT_URL_ORIGIN;
3443
+ exports.EXPIRED_LINK_REFRESH_EXPIRES_SECONDS = EXPIRED_LINK_REFRESH_EXPIRES_SECONDS;
3444
+ exports.HISTORY_FILL_SLACK_PX = HISTORY_FILL_SLACK_PX;
2717
3445
  exports.HISTORY_TOKEN_BUDGET = HISTORY_TOKEN_BUDGET;
2718
3446
  exports.LINK_LABEL_MAX_DISPLAY_CHARS = LINK_LABEL_MAX_DISPLAY_CHARS;
3447
+ exports.LINK_REFRESH_WINDOW_MS = LINK_REFRESH_WINDOW_MS;
3448
+ exports.MAX_HISTORY_FILL_PAGES = MAX_HISTORY_FILL_PAGES;
2719
3449
  exports.MAX_HISTORY_MESSAGES = MAX_HISTORY_MESSAGES;
2720
3450
  exports.MAX_PARSED_CONTENT_CHARS = MAX_PARSED_CONTENT_CHARS;
2721
3451
  exports.MCP_NAME = MCP_NAME;
@@ -2725,6 +3455,7 @@ exports.POLL_INTERVAL = POLL_INTERVAL;
2725
3455
  exports.RENDER_FROM_TOKEN = RENDER_FROM_TOKEN;
2726
3456
  exports.TOOL_AND_RESPONSE_BUFFER = TOOL_AND_RESPONSE_BUFFER;
2727
3457
  exports.buildBoundedChatMessages = buildBoundedChatMessages;
3458
+ exports.buildChatDisplayList = buildChatDisplayList;
2728
3459
  exports.buildChatSystemPrompt = buildChatSystemPrompt;
2729
3460
  exports.buildDisplayExpiredAttachmentHref = buildDisplayExpiredAttachmentHref;
2730
3461
  exports.buildIndexingContinueMessage = buildIndexingContinueMessage;
@@ -2737,9 +3468,11 @@ exports.callClaudeWithMcp = callClaudeWithMcp;
2737
3468
  exports.callClaudeWithPublicMcp = callClaudeWithPublicMcp;
2738
3469
  exports.callOpenAIWithPublicMcp = callOpenAIWithPublicMcp;
2739
3470
  exports.chatEngineConfig = chatEngineConfig;
3471
+ exports.classifyInlineLink = classifyInlineLink;
2740
3472
  exports.clearAttachmentParsers = clearAttachmentParsers;
2741
3473
  exports.composeUserMessage = composeUserMessage;
2742
3474
  exports.configureChatEngine = configureChatEngine;
3475
+ exports.createHistoryFiller = createHistoryFiller;
2743
3476
  exports.createInlineLinkRegex = createInlineLinkRegex;
2744
3477
  exports.encodePathSegments = encodePathSegments;
2745
3478
  exports.estimateMessageTokens = estimateMessageTokens;
@@ -2748,8 +3481,10 @@ exports.extractClaudeText = extractClaudeText;
2748
3481
  exports.extractLastUserTextFromRequest = extractLastUserTextFromRequest;
2749
3482
  exports.extractOpenAIText = extractOpenAIText;
2750
3483
  exports.extractRemotePathFromAttachmentHref = extractRemotePathFromAttachmentHref;
3484
+ exports.fillHistoryViewport = fillHistoryViewport;
2751
3485
  exports.filterListByClearHorizon = filterListByClearHorizon;
2752
3486
  exports.findAttachmentParser = findAttachmentParser;
3487
+ exports.formatChatTimestamp = formatChatTimestamp;
2753
3488
  exports.getAttachmentParsers = getAttachmentParsers;
2754
3489
  exports.getChatHistory = getChatHistory;
2755
3490
  exports.getContextWindow = getContextWindow;
@@ -2759,6 +3494,7 @@ exports.groupAttachmentFailures = groupAttachmentFailures;
2759
3494
  exports.isAuthExpiredError = isAuthExpiredError;
2760
3495
  exports.isBgIndexingQueue = isBgIndexingQueue;
2761
3496
  exports.isErrorResponseBody = isErrorResponseBody;
3497
+ exports.isHttpUrlLike = isHttpUrlLike;
2762
3498
  exports.isNonRetryableRequestError = isNonRetryableRequestError;
2763
3499
  exports.isOfficeFile = isOfficeFile;
2764
3500
  exports.isServerExtractable = isServerExtractable;
@@ -2769,14 +3505,20 @@ exports.makeExtractPlaceholder = makeExtractPlaceholder;
2769
3505
  exports.mapHistoryListToMessages = mapHistoryListToMessages;
2770
3506
  exports.normalizeAttachmentPathCandidate = normalizeAttachmentPathCandidate;
2771
3507
  exports.normalizeTextContent = normalizeTextContent;
3508
+ exports.normalizeTrailingInlineToken = normalizeTrailingInlineToken;
2772
3509
  exports.notifyAgentSaveAttachment = notifyAgentSaveAttachment;
2773
3510
  exports.parseAttachmentContent = parseAttachmentContent;
3511
+ exports.parseIndexingLabel = parseIndexingLabel;
3512
+ exports.readExpiredAttachmentHref = readExpiredAttachmentHref;
2774
3513
  exports.registerAttachmentParser = registerAttachmentParser;
3514
+ exports.repairUrlEntities = repairUrlEntities;
3515
+ exports.repairUrlWhitespace = repairUrlWhitespace;
2775
3516
  exports.safeDecodeURIComponent = safeDecodeURIComponent;
2776
3517
  exports.sanitizeAttachmentLinksForHistory = sanitizeAttachmentLinksForHistory;
2777
3518
  exports.stripFileBlocksFromHistory = stripFileBlocksFromHistory;
2778
3519
  exports.transformContentWithImages = transformContentWithImages;
2779
3520
  exports.transformContentWithOpenAIImages = transformContentWithOpenAIImages;
2780
3521
  exports.truncateLabelForDisplay = truncateLabelForDisplay;
3522
+ exports.wallClockNow = wallClockNow;
2781
3523
  //# sourceMappingURL=engine.cjs.map
2782
3524
  //# sourceMappingURL=engine.cjs.map