bunnyquery 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/engine.mjs CHANGED
@@ -284,6 +284,9 @@ 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.
@@ -618,6 +621,15 @@ function repairUrlWhitespace(href) {
618
621
  if (/^https?:\/\/[^/\s]+\/download\/[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)?$/i.test(stripped)) return stripped;
619
622
  return href.trim().replace(/\s/g, "%20");
620
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(/&/gi, "&").replace(/&#0*38;/g, "&").replace(/&#x0*26;/gi, "&");
630
+ }
631
+ return out;
632
+ }
621
633
  function normalizeTrailingInlineToken(value) {
622
634
  if (!value) return value;
623
635
  var out = value.replace(/[.,;:!?]+$/, "");
@@ -665,8 +677,9 @@ function classifyInlineLink(full, groups, ctx) {
665
677
  var tail = full.slice(("src::" + rawPath).length);
666
678
  var srcIsUrl = isHttpUrlLike(rawPath);
667
679
  if (srcIsUrl && !isDbHost(rawPath) && !readExpiredAttachmentHref(rawPath)) {
680
+ var srcUrl = repairUrlEntities(rawPath);
668
681
  return {
669
- part: { type: "link", label: truncateLabelForDisplay(rawPath), fullLabel: rawPath, href: rawPath, expired: false },
682
+ part: { type: "link", label: truncateLabelForDisplay(srcUrl), fullLabel: srcUrl, href: srcUrl, expired: false },
670
683
  tail
671
684
  };
672
685
  }
@@ -700,6 +713,7 @@ function classifyInlineLink(full, groups, ctx) {
700
713
  }
701
714
  var originalHref = g3 || g6 || "";
702
715
  if (!originalHref) return null;
716
+ originalHref = repairUrlEntities(originalHref);
703
717
  var urlTail;
704
718
  if (!g3 && g6) {
705
719
  var trimmedUrl = normalizeTrailingInlineToken(originalHref);
@@ -741,25 +755,74 @@ function truncateLabelForDisplay(label) {
741
755
  // src/engine/budget.ts
742
756
  var CONTEXT_WINDOW_DEFAULT = { claude: 2e5, openai: 128e3 };
743
757
  var CONTEXT_WINDOW_BY_MODEL = {
744
- "claude-opus-4-7": 2e5,
758
+ // exact ids
759
+ "claude-opus-5": 1e6,
760
+ "claude-opus-4-8": 1e6,
761
+ "claude-opus-4-7": 1e6,
762
+ "claude-sonnet-5": 1e6,
763
+ "claude-sonnet-4-6": 1e6,
745
764
  "claude-sonnet-4": 2e5,
746
- "gpt-5.4": 128e3
765
+ "claude-haiku-4-5": 2e5,
766
+ "gpt-5.4": 128e3,
767
+ "gpt-5.6-luna": 128e3,
768
+ // family keys
769
+ "claude-opus": 1e6,
770
+ "claude-sonnet": 1e6,
771
+ "claude-haiku": 2e5,
772
+ "gpt-5.6": 128e3,
773
+ "gpt-5": 128e3
747
774
  };
775
+ var apiReportedContextWindows = {};
776
+ function registerModelContextWindows(models) {
777
+ if (!Array.isArray(models)) return;
778
+ for (var i = 0; i < models.length; i++) {
779
+ var m = models[i];
780
+ var id = (m && m.id ? String(m.id) : "").trim().toLowerCase();
781
+ var reported = m ? Number(m.max_input_tokens) : NaN;
782
+ if (id && Number.isFinite(reported) && reported > 0) {
783
+ apiReportedContextWindows[id] = Math.floor(reported);
784
+ }
785
+ }
786
+ }
787
+ var projectContextWindows = {};
788
+ function setProjectContextWindow(serviceId, tokens) {
789
+ var key = (serviceId || "").trim();
790
+ if (!key) return;
791
+ var n = Number(tokens);
792
+ if (Number.isFinite(n) && n > 0) projectContextWindows[key] = Math.floor(n);
793
+ else delete projectContextWindows[key];
794
+ }
795
+ function getProjectContextWindow(serviceId) {
796
+ var key = (serviceId || "").trim();
797
+ return key && projectContextWindows[key] ? projectContextWindows[key] : null;
798
+ }
748
799
  var OUTPUT_TOKEN_RESERVE = 22e3;
749
800
  var TOOL_AND_RESPONSE_BUFFER = 4e3;
750
801
  var MIN_INPUT_TOKEN_BUDGET = 8e3;
751
802
  var CLAUDE_PER_REQUEST_INPUT_CAP = 28e3;
752
803
  var MAX_HISTORY_MESSAGES = 20;
753
804
  var HISTORY_TOKEN_BUDGET = 8e3;
805
+ var CLAUDE_INPUT_CAP_RATIO = 0.16;
806
+ var HISTORY_BUDGET_RATIO = 0.08;
754
807
  function estimateTextTokens(text) {
755
808
  return Math.ceil((text || "").length / 3);
756
809
  }
757
810
  function estimateMessageTokens(msg) {
758
811
  return estimateTextTokens(msg.content) + estimateTextTokens(msg.role) + 6;
759
812
  }
760
- function getContextWindow(platform, model) {
813
+ function getContextWindow(platform, model, serviceId) {
814
+ var override = serviceId ? getProjectContextWindow(serviceId) : null;
815
+ if (override) return override;
761
816
  var normalized = (model || "").trim().toLowerCase();
762
- if (normalized && CONTEXT_WINDOW_BY_MODEL[normalized]) return CONTEXT_WINDOW_BY_MODEL[normalized];
817
+ if (normalized) {
818
+ if (apiReportedContextWindows[normalized]) return apiReportedContextWindows[normalized];
819
+ if (CONTEXT_WINDOW_BY_MODEL[normalized]) return CONTEXT_WINDOW_BY_MODEL[normalized];
820
+ var parts = normalized.split("-");
821
+ for (var end = parts.length - 1; end > 0; end--) {
822
+ var family = parts.slice(0, end).join("-");
823
+ if (CONTEXT_WINDOW_BY_MODEL[family]) return CONTEXT_WINDOW_BY_MODEL[family];
824
+ }
825
+ }
763
826
  return CONTEXT_WINDOW_DEFAULT[platform];
764
827
  }
765
828
  function stripFileBlocksFromHistory(content) {
@@ -767,15 +830,19 @@ function stripFileBlocksFromHistory(content) {
767
830
  return content.replace(/```([^\n`]+?\.[^\s.`]+)\n[\s\S]*?```/g, "[file previously attached: $1]");
768
831
  }
769
832
  function buildBoundedChatMessages(options) {
770
- var contextWindow = getContextWindow(options.platform, options.model);
833
+ var contextWindow = getContextWindow(options.platform, options.model, options.serviceId);
771
834
  var contextBasedBudget = Math.max(
772
835
  MIN_INPUT_TOKEN_BUDGET,
773
836
  contextWindow - OUTPUT_TOKEN_RESERVE - TOOL_AND_RESPONSE_BUFFER
774
837
  );
775
- var availableInputBudget = options.platform === "claude" ? Math.min(contextBasedBudget, CLAUDE_PER_REQUEST_INPUT_CAP) : contextBasedBudget;
838
+ var scaled = !!(options.serviceId && getProjectContextWindow(options.serviceId));
839
+ var claudeInputCap = scaled ? Math.max(CLAUDE_PER_REQUEST_INPUT_CAP, Math.round(contextBasedBudget * CLAUDE_INPUT_CAP_RATIO)) : CLAUDE_PER_REQUEST_INPUT_CAP;
840
+ var availableInputBudget = options.platform === "claude" ? Math.min(contextBasedBudget, claudeInputCap) : contextBasedBudget;
776
841
  var systemCost = estimateTextTokens(options.systemPrompt) + 12;
777
- var budgetForHistory = Math.max(1e3, Math.min(HISTORY_TOKEN_BUDGET, availableInputBudget - systemCost));
778
- var windowed = options.history.slice(-MAX_HISTORY_MESSAGES);
842
+ var historyAllowance = scaled ? Math.max(HISTORY_TOKEN_BUDGET, Math.round(contextBasedBudget * HISTORY_BUDGET_RATIO)) : HISTORY_TOKEN_BUDGET;
843
+ var budgetForHistory = Math.max(1e3, Math.min(historyAllowance, availableInputBudget - systemCost));
844
+ var maxHistoryMessages = scaled ? Math.max(MAX_HISTORY_MESSAGES, Math.round(MAX_HISTORY_MESSAGES * (budgetForHistory / HISTORY_TOKEN_BUDGET))) : MAX_HISTORY_MESSAGES;
845
+ var windowed = options.history.slice(-maxHistoryMessages);
779
846
  var latestIndex = windowed.length - 1;
780
847
  var trimmed = windowed.map(function(m, i2) {
781
848
  if (i2 === latestIndex) return m;
@@ -800,6 +867,60 @@ function buildBoundedChatMessages(options) {
800
867
  };
801
868
  }
802
869
 
870
+ // src/engine/time.ts
871
+ function wallClockNow() {
872
+ return Date.now();
873
+ }
874
+ function formatChatTimestamp(ms) {
875
+ if (typeof ms !== "number" || !isFinite(ms) || ms <= 0) return "";
876
+ try {
877
+ return new Date(ms).toLocaleString(void 0, {
878
+ year: "numeric",
879
+ month: "short",
880
+ day: "numeric",
881
+ hour: "numeric",
882
+ minute: "2-digit",
883
+ second: "2-digit"
884
+ });
885
+ } catch (e) {
886
+ return "";
887
+ }
888
+ }
889
+
890
+ // src/engine/ai_agent.ts
891
+ function normalizePlatform(raw) {
892
+ var p = (raw || "").trim().toLowerCase();
893
+ return p === "claude" || p === "openai" ? p : null;
894
+ }
895
+ function parseAiAgentValue(value) {
896
+ var raw = (value || "").trim();
897
+ if (!raw || raw.toLowerCase() === "none") {
898
+ return { platform: null, model: "", contextWindow: null, hasPlatform: false };
899
+ }
900
+ var firstHash = raw.indexOf("#");
901
+ if (firstHash === -1) {
902
+ var only = normalizePlatform(raw);
903
+ return { platform: only, model: "", contextWindow: null, hasPlatform: !!only };
904
+ }
905
+ var platform = normalizePlatform(raw.slice(0, firstHash));
906
+ var rest = raw.slice(firstHash + 1);
907
+ var secondHash = rest.indexOf("#");
908
+ var model = (secondHash === -1 ? rest : rest.slice(0, secondHash)).trim();
909
+ var windowRaw = secondHash === -1 ? "" : rest.slice(secondHash + 1).trim();
910
+ var parsedWindow = windowRaw ? Number(windowRaw) : NaN;
911
+ var contextWindow = Number.isFinite(parsedWindow) && parsedWindow > 0 ? Math.floor(parsedWindow) : null;
912
+ return { platform, model, contextWindow, hasPlatform: !!platform };
913
+ }
914
+ function buildAiAgentValue(platform, model, contextWindow) {
915
+ var p = (platform || "").trim().toLowerCase();
916
+ if (!p || p === "none") return "none";
917
+ var m = (model || "").trim();
918
+ if (!m) return p;
919
+ var n = Number(contextWindow);
920
+ if (!Number.isFinite(n) || n <= 0) return p + "#" + m;
921
+ return p + "#" + m + "#" + Math.floor(n);
922
+ }
923
+
803
924
  // src/engine/requests.ts
804
925
  var ANTHROPIC_MESSAGES_API_URL = "https://api.anthropic.com/v1/messages";
805
926
  var ANTHROPIC_MODELS_API_URL = "https://api.anthropic.com/v1/models";
@@ -1308,7 +1429,8 @@ async function getChatHistory(params, fetchOptions) {
1308
1429
  method: "POST"
1309
1430
  },
1310
1431
  { service: params.service, owner: params.owner },
1311
- params.queue ? { queue: params.queue } : {}
1432
+ params.queue ? { queue: params.queue } : {},
1433
+ params.status ? { status: params.status } : {}
1312
1434
  );
1313
1435
  return chatEngineConfig().clientSecretRequestHistory(
1314
1436
  p,
@@ -1345,6 +1467,25 @@ function extractLastUserTextFromRequest(requestBody) {
1345
1467
  }
1346
1468
  return "";
1347
1469
  }
1470
+ function isIndexingRequestText(userText) {
1471
+ if (typeof userText !== "string") return false;
1472
+ return userText.indexOf("A new file has just been uploaded") === 0 || userText.indexOf("CONTINUE indexing") === 0;
1473
+ }
1474
+ function parseIndexingRequestText(userText) {
1475
+ if (typeof userText !== "string" || !userText) return null;
1476
+ var nameMatch = userText.match(/^- name: (.+)$/m);
1477
+ if (!nameMatch) return null;
1478
+ var mimeMatch = userText.match(/^- mime type: (.+)$/m);
1479
+ var sizeMatch = userText.match(/^- size \(bytes\): (\d+)$/m);
1480
+ var pathMatch = userText.match(/^- storage path: (.+)$/m);
1481
+ return {
1482
+ name: nameMatch[1].trim(),
1483
+ path: pathMatch ? pathMatch[1].trim() : void 0,
1484
+ mime: mimeMatch ? mimeMatch[1].trim() : void 0,
1485
+ size: sizeMatch ? Number(sizeMatch[1]) : void 0,
1486
+ continued: userText.indexOf("CONTINUE indexing") === 0
1487
+ };
1488
+ }
1348
1489
  function mapHistoryListToMessages(list, platform, opts) {
1349
1490
  var mapped = [], runningItemIds = [];
1350
1491
  var extractAssistantText = platform === "openai" ? extractOpenAIText : extractClaudeText;
@@ -1361,31 +1502,25 @@ function mapHistoryListToMessages(list, platform, opts) {
1361
1502
  var assistantText = isPending ? "" : (extractAssistantText(response) || "").trim() || "";
1362
1503
  var isErrorResponse = !isPending && (isFailed || isErrorResponseBody(response));
1363
1504
  var serverItemId = item && typeof item.id === "string" && item.id ? item.id : void 0;
1505
+ var createdTs = Number(item && item.created);
1506
+ var updatedTs = Number(item && item.updated);
1507
+ var userTs = isFinite(createdTs) && createdTs > 0 ? createdTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : void 0;
1508
+ var replyTs = isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
1364
1509
  if (userText) {
1365
1510
  var displayContent;
1366
1511
  var indexFile = void 0;
1367
1512
  if (item._isBgTask) {
1368
- var nameMatch = userText.match(/^- name: (.+)$/m);
1369
- if (nameMatch) {
1370
- var mimeMatch = userText.match(/^- mime type: (.+)$/m);
1371
- var sizeMatch = userText.match(/^- size \(bytes\): (\d+)$/m);
1372
- var pathMatch = userText.match(/^- storage path: (.+)$/m);
1373
- var isContinuePass = userText.indexOf("CONTINUE indexing") === 0;
1513
+ var ref = parseIndexingRequestText(userText);
1514
+ if (ref) {
1374
1515
  displayContent = opts.formatIndexingLabel(
1375
- nameMatch[1].trim(),
1376
- mimeMatch ? mimeMatch[1].trim() : "",
1377
- sizeMatch ? Number(sizeMatch[1]) : null,
1378
- pathMatch ? pathMatch[1].trim() : void 0,
1516
+ ref.name,
1517
+ ref.mime || "",
1518
+ typeof ref.size === "number" ? ref.size : null,
1519
+ ref.path,
1379
1520
  false,
1380
- isContinuePass
1521
+ ref.continued
1381
1522
  );
1382
- indexFile = {
1383
- name: nameMatch[1].trim(),
1384
- path: pathMatch ? pathMatch[1].trim() : void 0,
1385
- mime: mimeMatch ? mimeMatch[1].trim() : void 0,
1386
- size: sizeMatch ? Number(sizeMatch[1]) : void 0,
1387
- continued: isContinuePass
1388
- };
1523
+ indexFile = ref;
1389
1524
  } else {
1390
1525
  displayContent = userText;
1391
1526
  }
@@ -1400,6 +1535,7 @@ function mapHistoryListToMessages(list, platform, opts) {
1400
1535
  if (indexFile) userMsg._indexFile = indexFile;
1401
1536
  if (item._isOnBgQueue) userMsg._useBgQueue = true;
1402
1537
  if (serverItemId !== void 0) userMsg._serverItemId = serverItemId;
1538
+ if (userTs !== void 0) userMsg._ts = userTs;
1403
1539
  mapped.push(userMsg);
1404
1540
  }
1405
1541
  if (isCancelledItem) ; else if (isInProcess) {
@@ -1414,11 +1550,13 @@ function mapHistoryListToMessages(list, platform, opts) {
1414
1550
  var em = { role: "assistant", content: getErrorMessage(response), isError: true };
1415
1551
  if (item._isBgTask) em.isBackgroundTask = true;
1416
1552
  if (serverItemId !== void 0) em._serverItemId = serverItemId;
1553
+ if (replyTs !== void 0) em._ts = replyTs;
1417
1554
  mapped.push(em);
1418
1555
  } else if (assistantText) {
1419
1556
  var okm = { role: "assistant", content: sanitizeAttachmentLinksForHistory(assistantText, opts.serviceId, true) };
1420
1557
  if (item._isBgTask) okm.isBackgroundTask = true;
1421
1558
  if (serverItemId !== void 0) okm._serverItemId = serverItemId;
1559
+ if (replyTs !== void 0) okm._ts = replyTs;
1422
1560
  mapped.push(okm);
1423
1561
  }
1424
1562
  });
@@ -1511,6 +1649,8 @@ function createHistoryFiller(base) {
1511
1649
  }
1512
1650
 
1513
1651
  // src/engine/session.ts
1652
+ var WORKER_PASS_ADOPT_LIMIT = 20;
1653
+ var WORKER_PASS_ADOPT_ATTEMPTS = [0, 2e3, 6e3];
1514
1654
  var _g = typeof globalThis !== "undefined" ? globalThis : {};
1515
1655
  function nowMs() {
1516
1656
  return _g.performance && typeof _g.performance.now === "function" ? _g.performance.now() : Date.now();
@@ -1530,6 +1670,35 @@ function isPollStopped(res) {
1530
1670
  var ChatSession = class {
1531
1671
  constructor(host) {
1532
1672
  this.typewriterQueue = Promise.resolve();
1673
+ /**
1674
+ * Pick up indexing passes the WORKER minted, which no client ever dispatched.
1675
+ *
1676
+ * For a PDF (and for text/grid when windowed indexing is on) the worker writes
1677
+ * pass N+1's row itself, inside pass N's invocation, right after saving pass
1678
+ * N's result. That row reaches this client through nothing at all: it is not in
1679
+ * bgTaskQueue (the client never asked for it) and it is not in state.messages
1680
+ * (only a first-page history load maps it in, which happens on mount, project
1681
+ * switch or tab return). So between two worker passes every pass the client
1682
+ * knows about is settled, and the collapsed row renders "Indexed N passes"
1683
+ * with no spinner and no Stop, for a file that is still being read. A user who
1684
+ * believes that then asks questions against a half-indexed file — which is what
1685
+ * this exists to prevent.
1686
+ *
1687
+ * So when a background indexing pass settles, ask the bg queue what is still
1688
+ * unresolved on it and adopt anything unknown as an ordinary BgTaskEntry.
1689
+ * drainBgTaskQueue then treats it exactly like a pass this client dispatched:
1690
+ * same bubble, same `_indexFile` (so it joins the file's collapsed row), same
1691
+ * poll — and that poll settling runs this again, so the chain is followed to
1692
+ * its end. `status`-scoped so the reply carries the live items only, never a
1693
+ * page of finished ones with their bodies.
1694
+ *
1695
+ * Termination: the only trigger is a pass SETTLING, which happens once per
1696
+ * pass. An adopted item that is still running is not re-adopted (its id is
1697
+ * already polled), and when the queue holds nothing unknown the chain stops on
1698
+ * its own. Nothing here is periodic — a timer that re-reads history is the
1699
+ * shape that previously looped fetchHistoryPage after an already-DONE index.
1700
+ */
1701
+ this._adoptingWorkerPasses = false;
1533
1702
  this.host = host;
1534
1703
  this.state = {
1535
1704
  messages: [],
@@ -1831,7 +2000,7 @@ var ChatSession = class {
1831
2000
  var offExisting = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
1832
2001
  this.aiChatHistoryCache[key] = {
1833
2002
  messages: offExisting.messages.concat([
1834
- { role: "user", content: composed, _ownerKey: key },
2003
+ { role: "user", content: composed, _ownerKey: key, _ts: wallClockNow() },
1835
2004
  { role: "assistant", content: "", isPending: true, isPendingInProcess: true, _ownerKey: key }
1836
2005
  ]),
1837
2006
  endOfList: offExisting.endOfList,
@@ -1863,7 +2032,7 @@ var ChatSession = class {
1863
2032
  serviceId: id.serviceId,
1864
2033
  history: resolvedHistory.concat([{ role: "user", content: llmComposed }])
1865
2034
  });
1866
- var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true };
2035
+ var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true, _ts: wallClockNow() };
1867
2036
  if (key) queuedBubble._ownerKey = key;
1868
2037
  if (useBgQueue) queuedBubble._useBgQueue = true;
1869
2038
  this.state.messages.push(queuedBubble);
@@ -1898,7 +2067,7 @@ var ChatSession = class {
1898
2067
  });
1899
2068
  return;
1900
2069
  }
1901
- this.state.messages.push({ role: "user", content: composed, ...key ? { _ownerKey: key } : {} });
2070
+ this.state.messages.push({ role: "user", content: composed, _ts: wallClockNow(), ...key ? { _ownerKey: key } : {} });
1902
2071
  this.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true, ...key ? { _ownerKey: key } : {} });
1903
2072
  this.host.notify();
1904
2073
  this.updateHistoryCache();
@@ -1955,6 +2124,7 @@ var ChatSession = class {
1955
2124
  var existing = this.state.messages[nextIdx];
1956
2125
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true, isBackgroundTask: true };
1957
2126
  if (existing._indexFile) promoted._indexFile = existing._indexFile;
2127
+ if (existing._ts !== void 0) promoted._ts = existing._ts;
1958
2128
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1959
2129
  if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1960
2130
  this.state.messages[nextIdx] = promoted;
@@ -1976,6 +2146,7 @@ var ChatSession = class {
1976
2146
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true };
1977
2147
  if (existing.isBackgroundTask) promoted.isBackgroundTask = true;
1978
2148
  if (existing._indexFile) promoted._indexFile = existing._indexFile;
2149
+ if (existing._ts !== void 0) promoted._ts = existing._ts;
1979
2150
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1980
2151
  if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1981
2152
  if (existing.isSendingToServer) promoted.isSendingToServer = true;
@@ -2025,6 +2196,7 @@ var ChatSession = class {
2025
2196
  var repl = { role: "user", content: exist.content };
2026
2197
  if (exist._serverItemId !== void 0) repl._serverItemId = exist._serverItemId;
2027
2198
  if (exist._ownerKey !== void 0) repl._ownerKey = exist._ownerKey;
2199
+ if (exist._ts !== void 0) repl._ts = exist._ts;
2028
2200
  this.state.messages[userIdx] = repl;
2029
2201
  }
2030
2202
  var thinkingIdx = userIdx >= 0 ? this.state.messages.findIndex(function(m, i) {
@@ -2033,6 +2205,7 @@ var ChatSession = class {
2033
2205
  return thinkingIdx !== -1 ? thinkingIdx : userIdx >= 0 ? userIdx + 1 : -1;
2034
2206
  }
2035
2207
  insertAtTarget(msg, targetIdx) {
2208
+ if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = wallClockNow();
2036
2209
  if (targetIdx >= 0 && this.state.messages[targetIdx] && this.state.messages[targetIdx].isPending) this.state.messages[targetIdx] = msg;
2037
2210
  else if (targetIdx >= 0) this.state.messages.splice(targetIdx, 0, msg);
2038
2211
  else this.state.messages.push(msg);
@@ -2369,6 +2542,8 @@ var ChatSession = class {
2369
2542
  }
2370
2543
  enqueueTypewrite(idx, fullText, localId) {
2371
2544
  var self = this;
2545
+ var target = this.state.messages[idx];
2546
+ if (target && target._ts === void 0) target._ts = wallClockNow();
2372
2547
  this.typewriterQueue = this.typewriterQueue.then(function() {
2373
2548
  return self.typewriteIntoIndex(idx, fullText, localId);
2374
2549
  });
@@ -2438,6 +2613,7 @@ var ChatSession = class {
2438
2613
  var u = this.state.messages[uIdx];
2439
2614
  var cleaned = { role: "user", content: u.content, _serverItemId: itemId };
2440
2615
  if (u.isBackgroundTask) cleaned.isBackgroundTask = true;
2616
+ if (u._ts !== void 0) cleaned._ts = u._ts;
2441
2617
  if (u._indexFile) cleaned._indexFile = u._indexFile;
2442
2618
  this.state.messages[uIdx] = cleaned;
2443
2619
  }
@@ -2468,8 +2644,21 @@ var ChatSession = class {
2468
2644
  }
2469
2645
  // --- background-task resolution + drain -------------------------------
2470
2646
  handleHistoryItemResolution(itemId, response, platform) {
2647
+ var indexRef = this._indexRefOfItem(itemId);
2471
2648
  this.applyHistoryItemResolution(itemId, response, platform);
2472
2649
  this.promoteNextBgQueuedToRunning();
2650
+ if (indexRef) this._followWorkerIndexingChain(indexRef.name, indexRef.mime);
2651
+ }
2652
+ /** The file an already-rendered background pass is about, off its request
2653
+ * bubble. Null for an ordinary turn, which is most of them. */
2654
+ _indexRefOfItem(itemId) {
2655
+ if (!itemId) return null;
2656
+ for (var i = 0; i < this.state.messages.length; i++) {
2657
+ var m = this.state.messages[i];
2658
+ if (m._serverItemId !== itemId || m.role !== "user" || !m.isBackgroundTask) continue;
2659
+ return m._indexFile || null;
2660
+ }
2661
+ return null;
2473
2662
  }
2474
2663
  applyHistoryItemResolution(itemId, response, platform) {
2475
2664
  this.historyItemPolls.delete(itemId);
@@ -2510,6 +2699,7 @@ var ChatSession = class {
2510
2699
  var ex = this.state.messages[userIdx];
2511
2700
  var settledUser = { role: "user", content: ex.content, _serverItemId: itemId };
2512
2701
  if (ex.isBackgroundTask) settledUser.isBackgroundTask = true;
2702
+ if (ex._ts !== void 0) settledUser._ts = ex._ts;
2513
2703
  if (ex._indexFile) settledUser._indexFile = ex._indexFile;
2514
2704
  if (ex._useBgQueue) settledUser._useBgQueue = true;
2515
2705
  this.state.messages[userIdx] = settledUser;
@@ -2602,6 +2792,116 @@ var ChatSession = class {
2602
2792
  self.cancelQueuedMessage(t.msg, idx === -1 ? t.idx : idx);
2603
2793
  });
2604
2794
  }
2795
+ /**
2796
+ * True when the WORKER, not this client, drives the rest of this file's chain.
2797
+ * The mirror image of the early returns in maybeResumeIndexing: whatever that
2798
+ * refuses to continue is exactly what nothing client-side is tracking.
2799
+ */
2800
+ _isWorkerDrivenIndexing(filename, mime) {
2801
+ if (!isPagedReadFile(filename, mime)) return false;
2802
+ if (isImageVisionFile(filename, mime)) return true;
2803
+ return windowedIndexingEnabled() && isWindowedReadFile(filename, mime);
2804
+ }
2805
+ _adoptWorkerIndexingPasses(attempt) {
2806
+ var self = this;
2807
+ if (this._adoptingWorkerPasses) return;
2808
+ var id = this.host.getIdentity();
2809
+ var platform = id.platform;
2810
+ if (!id.serviceId || platform !== "claude" && platform !== "openai") return;
2811
+ if (this.isPollingPaused() || !this.host.isViewMounted()) return;
2812
+ var svcId = id.serviceId, owner = id.owner;
2813
+ var queue = (id.userId || id.serviceId) + BG_INDEXING_QUEUE_SUFFIX;
2814
+ var ask = function(status) {
2815
+ return Promise.resolve(getChatHistory(
2816
+ { service: svcId, owner, platform, queue, status },
2817
+ { limit: WORKER_PASS_ADOPT_LIMIT }
2818
+ )).catch(function() {
2819
+ return null;
2820
+ });
2821
+ };
2822
+ this._adoptingWorkerPasses = true;
2823
+ Promise.all([ask("running"), ask("pending")]).then(function(results) {
2824
+ self._adoptingWorkerPasses = false;
2825
+ var now = self.host.getIdentity();
2826
+ if (now.serviceId !== svcId || now.platform !== platform) return;
2827
+ if (!self.host.isViewMounted()) return;
2828
+ var adoptedIds = [];
2829
+ for (var ri = 0; ri < results.length; ri++) {
2830
+ var list = results[ri] && Array.isArray(results[ri].list) ? results[ri].list : [];
2831
+ for (var i = 0; i < list.length; i++) {
2832
+ if (self._adoptWorkerIndexingItem(list[i], svcId, platform)) adoptedIds.push(list[i].id);
2833
+ }
2834
+ }
2835
+ if (adoptedIds.length) {
2836
+ self.drainBgTaskQueue();
2837
+ if (self._isTrackingAny(adoptedIds)) return;
2838
+ }
2839
+ if (attempt + 1 >= WORKER_PASS_ADOPT_ATTEMPTS.length) return;
2840
+ setTimeout(function() {
2841
+ var later = self.host.getIdentity();
2842
+ if (later.serviceId !== svcId || later.platform !== platform) return;
2843
+ if (self.isPollingPaused() || !self.host.isViewMounted()) return;
2844
+ self._adoptWorkerIndexingPasses(attempt + 1);
2845
+ }, WORKER_PASS_ADOPT_ATTEMPTS[attempt + 1]);
2846
+ }, function() {
2847
+ self._adoptingWorkerPasses = false;
2848
+ });
2849
+ }
2850
+ /** Any of these ids still queued or still polled, i.e. surviving work. */
2851
+ _isTrackingAny(ids) {
2852
+ for (var i = 0; i < ids.length; i++) {
2853
+ if (this.historyItemPolls.has(ids[i])) return true;
2854
+ for (var q = 0; q < this.bgTaskQueue.length; q++) {
2855
+ if (this.bgTaskQueue[q].id === ids[i]) return true;
2856
+ }
2857
+ }
2858
+ return false;
2859
+ }
2860
+ /** One live bg-queue item -> a BgTaskEntry, if it is an indexing pass this
2861
+ * client is not already tracking. Returns whether it was adopted. */
2862
+ _adoptWorkerIndexingItem(item, svcId, platform) {
2863
+ if (!item || typeof item.id !== "string" || !item.id) return false;
2864
+ if (item.status !== "pending" && item.status !== "running") return false;
2865
+ if (typeof item.poll !== "function") return false;
2866
+ if (this.historyItemPolls.has(item.id)) return false;
2867
+ for (var q = 0; q < this.bgTaskQueue.length; q++) {
2868
+ if (this.bgTaskQueue[q].id === item.id) return false;
2869
+ }
2870
+ for (var m = 0; m < this.state.messages.length; m++) {
2871
+ var msg = this.state.messages[m];
2872
+ if (msg._serverItemId !== item.id) continue;
2873
+ if (!(msg.isPending || msg.isPendingInProcess || msg.isPendingQueued)) return false;
2874
+ }
2875
+ var body = item.request_body;
2876
+ if (!body || typeof body !== "object") return false;
2877
+ if (platform === "claude" ? !Array.isArray(body.messages) : !Array.isArray(body.input)) return false;
2878
+ var userText = extractLastUserTextFromRequest(body);
2879
+ if (!isIndexingRequestText(userText)) return false;
2880
+ var ref = parseIndexingRequestText(userText);
2881
+ if (!ref || !ref.name) return false;
2882
+ if (!this._isWorkerDrivenIndexing(ref.name, ref.mime)) return false;
2883
+ this.bgTaskQueue.push({
2884
+ serviceId: svcId,
2885
+ platform,
2886
+ id: item.id,
2887
+ filename: ref.name,
2888
+ storagePath: ref.path,
2889
+ mime: ref.mime,
2890
+ size: ref.size,
2891
+ status: item.status === "running" ? "running" : "pending",
2892
+ poll: item.poll,
2893
+ // Drives the "(continuing)" label and marks this as a continuation for
2894
+ // _applyIndexCancellations, which must not read a CONTINUE pass as the
2895
+ // fresh first pass that lifts a stop.
2896
+ resumePass: ref.continued ? 1 : 0
2897
+ });
2898
+ return true;
2899
+ }
2900
+ /** Follow the chain on from a background indexing pass that just settled. */
2901
+ _followWorkerIndexingChain(filename, mime) {
2902
+ if (!this._isWorkerDrivenIndexing(filename, mime)) return;
2903
+ this._adoptWorkerIndexingPasses(0);
2904
+ }
2605
2905
  /** Best-effort server-side cancel of a bg-queue item that has no bubble (so
2606
2906
  * cancelQueuedMessage, which drives one, has nothing to act on). */
2607
2907
  _cancelServerItem(serverId) {
@@ -2812,8 +3112,7 @@ var ChatSession = class {
2812
3112
  var chatList = history && Array.isArray(history.list) ? history.list : [];
2813
3113
  chatList.forEach(function(item) {
2814
3114
  if (isBgIndexingQueue(item.queue_name)) {
2815
- var userText = extractLastUserTextFromRequest(item.request_body);
2816
- if (typeof userText === "string" && (userText.indexOf("A new file has just been uploaded") === 0 || userText.indexOf("CONTINUE indexing") === 0)) item._isBgTask = true;
3115
+ if (isIndexingRequestText(extractLastUserTextFromRequest(item.request_body))) item._isBgTask = true;
2817
3116
  else item._isOnBgQueue = true;
2818
3117
  }
2819
3118
  });
@@ -3286,7 +3585,10 @@ function buildChatDisplayList(messages, opts) {
3286
3585
  cancellableIds: [],
3287
3586
  cancelling: false,
3288
3587
  mayHaveOlder: false,
3289
- anchorIndex: i
3588
+ // The run's first loaded pass, and never re-stamped: see the file
3589
+ // docstring. `anchorId` is filled in once every member is known.
3590
+ anchorIndex: i,
3591
+ anchorId: ""
3290
3592
  };
3291
3593
  order.push(runId);
3292
3594
  }
@@ -3300,7 +3602,6 @@ function buildChatDisplayList(messages, opts) {
3300
3602
  g.passCount++;
3301
3603
  }
3302
3604
  g.members.push({ msg, index: i });
3303
- g.anchorIndex = i;
3304
3605
  runOfIndex[i] = runId;
3305
3606
  if (msg._serverItemId) runByItemId[msg._serverItemId] = runId;
3306
3607
  if (ref && ref.name) keyByName[ref.name] = g.key;
@@ -3362,6 +3663,9 @@ function buildChatDisplayList(messages, opts) {
3362
3663
  }
3363
3664
  }
3364
3665
  grp.mayHaveOlder = !sawFirstPass && hasMoreHistory;
3666
+ var anchor = grp.members[0];
3667
+ grp.anchorIndex = anchor.index;
3668
+ grp.anchorId = anchor.msg._serverItemId || anchor.msg._localId || "";
3365
3669
  }
3366
3670
  var out = [];
3367
3671
  for (var j = 0; j < list.length; j++) {
@@ -3375,6 +3679,6 @@ function buildChatDisplayList(messages, opts) {
3375
3679
  return out;
3376
3680
  }
3377
3681
 
3378
- 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, 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, repairUrlWhitespace, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay };
3682
+ export { BG_INDEXING_QUEUE_SUFFIX, CLAUDE_INPUT_CAP_RATIO, 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_BUDGET_RATIO, 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, buildAiAgentValue, 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, getProjectContextWindow, groupAttachmentFailures, isAuthExpiredError, isBgIndexingQueue, isErrorResponseBody, isHttpUrlLike, isIndexingRequestText, isNonRetryableRequestError, isOfficeFile, isServerExtractable, isServiceDbAttachmentHref, listClaudeModels, listOpenAIModels, makeExtractPlaceholder, mapHistoryListToMessages, normalizeAttachmentPathCandidate, normalizeTextContent, normalizeTrailingInlineToken, notifyAgentSaveAttachment, parseAiAgentValue, parseAttachmentContent, parseIndexingLabel, parseIndexingRequestText, readExpiredAttachmentHref, registerAttachmentParser, registerModelContextWindows, repairUrlEntities, repairUrlWhitespace, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, setProjectContextWindow, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay, wallClockNow };
3379
3683
  //# sourceMappingURL=engine.mjs.map
3380
3684
  //# sourceMappingURL=engine.mjs.map