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.cjs CHANGED
@@ -286,6 +286,9 @@ 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.
@@ -620,6 +623,15 @@ function repairUrlWhitespace(href) {
620
623
  if (/^https?:\/\/[^/\s]+\/download\/[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)?$/i.test(stripped)) return stripped;
621
624
  return href.trim().replace(/\s/g, "%20");
622
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(/&/gi, "&").replace(/&#0*38;/g, "&").replace(/&#x0*26;/gi, "&");
632
+ }
633
+ return out;
634
+ }
623
635
  function normalizeTrailingInlineToken(value) {
624
636
  if (!value) return value;
625
637
  var out = value.replace(/[.,;:!?]+$/, "");
@@ -667,8 +679,9 @@ function classifyInlineLink(full, groups, ctx) {
667
679
  var tail = full.slice(("src::" + rawPath).length);
668
680
  var srcIsUrl = isHttpUrlLike(rawPath);
669
681
  if (srcIsUrl && !isDbHost(rawPath) && !readExpiredAttachmentHref(rawPath)) {
682
+ var srcUrl = repairUrlEntities(rawPath);
670
683
  return {
671
- part: { type: "link", label: truncateLabelForDisplay(rawPath), fullLabel: rawPath, href: rawPath, expired: false },
684
+ part: { type: "link", label: truncateLabelForDisplay(srcUrl), fullLabel: srcUrl, href: srcUrl, expired: false },
672
685
  tail
673
686
  };
674
687
  }
@@ -702,6 +715,7 @@ function classifyInlineLink(full, groups, ctx) {
702
715
  }
703
716
  var originalHref = g3 || g6 || "";
704
717
  if (!originalHref) return null;
718
+ originalHref = repairUrlEntities(originalHref);
705
719
  var urlTail;
706
720
  if (!g3 && g6) {
707
721
  var trimmedUrl = normalizeTrailingInlineToken(originalHref);
@@ -743,25 +757,74 @@ function truncateLabelForDisplay(label) {
743
757
  // src/engine/budget.ts
744
758
  var CONTEXT_WINDOW_DEFAULT = { claude: 2e5, openai: 128e3 };
745
759
  var CONTEXT_WINDOW_BY_MODEL = {
746
- "claude-opus-4-7": 2e5,
760
+ // exact ids
761
+ "claude-opus-5": 1e6,
762
+ "claude-opus-4-8": 1e6,
763
+ "claude-opus-4-7": 1e6,
764
+ "claude-sonnet-5": 1e6,
765
+ "claude-sonnet-4-6": 1e6,
747
766
  "claude-sonnet-4": 2e5,
748
- "gpt-5.4": 128e3
767
+ "claude-haiku-4-5": 2e5,
768
+ "gpt-5.4": 128e3,
769
+ "gpt-5.6-luna": 128e3,
770
+ // family keys
771
+ "claude-opus": 1e6,
772
+ "claude-sonnet": 1e6,
773
+ "claude-haiku": 2e5,
774
+ "gpt-5.6": 128e3,
775
+ "gpt-5": 128e3
749
776
  };
777
+ var apiReportedContextWindows = {};
778
+ function registerModelContextWindows(models) {
779
+ if (!Array.isArray(models)) return;
780
+ for (var i = 0; i < models.length; i++) {
781
+ var m = models[i];
782
+ var id = (m && m.id ? String(m.id) : "").trim().toLowerCase();
783
+ var reported = m ? Number(m.max_input_tokens) : NaN;
784
+ if (id && Number.isFinite(reported) && reported > 0) {
785
+ apiReportedContextWindows[id] = Math.floor(reported);
786
+ }
787
+ }
788
+ }
789
+ var projectContextWindows = {};
790
+ function setProjectContextWindow(serviceId, tokens) {
791
+ var key = (serviceId || "").trim();
792
+ if (!key) return;
793
+ var n = Number(tokens);
794
+ if (Number.isFinite(n) && n > 0) projectContextWindows[key] = Math.floor(n);
795
+ else delete projectContextWindows[key];
796
+ }
797
+ function getProjectContextWindow(serviceId) {
798
+ var key = (serviceId || "").trim();
799
+ return key && projectContextWindows[key] ? projectContextWindows[key] : null;
800
+ }
750
801
  var OUTPUT_TOKEN_RESERVE = 22e3;
751
802
  var TOOL_AND_RESPONSE_BUFFER = 4e3;
752
803
  var MIN_INPUT_TOKEN_BUDGET = 8e3;
753
804
  var CLAUDE_PER_REQUEST_INPUT_CAP = 28e3;
754
805
  var MAX_HISTORY_MESSAGES = 20;
755
806
  var HISTORY_TOKEN_BUDGET = 8e3;
807
+ var CLAUDE_INPUT_CAP_RATIO = 0.16;
808
+ var HISTORY_BUDGET_RATIO = 0.08;
756
809
  function estimateTextTokens(text) {
757
810
  return Math.ceil((text || "").length / 3);
758
811
  }
759
812
  function estimateMessageTokens(msg) {
760
813
  return estimateTextTokens(msg.content) + estimateTextTokens(msg.role) + 6;
761
814
  }
762
- function getContextWindow(platform, model) {
815
+ function getContextWindow(platform, model, serviceId) {
816
+ var override = serviceId ? getProjectContextWindow(serviceId) : null;
817
+ if (override) return override;
763
818
  var normalized = (model || "").trim().toLowerCase();
764
- if (normalized && CONTEXT_WINDOW_BY_MODEL[normalized]) return CONTEXT_WINDOW_BY_MODEL[normalized];
819
+ if (normalized) {
820
+ if (apiReportedContextWindows[normalized]) return apiReportedContextWindows[normalized];
821
+ if (CONTEXT_WINDOW_BY_MODEL[normalized]) return CONTEXT_WINDOW_BY_MODEL[normalized];
822
+ var parts = normalized.split("-");
823
+ for (var end = parts.length - 1; end > 0; end--) {
824
+ var family = parts.slice(0, end).join("-");
825
+ if (CONTEXT_WINDOW_BY_MODEL[family]) return CONTEXT_WINDOW_BY_MODEL[family];
826
+ }
827
+ }
765
828
  return CONTEXT_WINDOW_DEFAULT[platform];
766
829
  }
767
830
  function stripFileBlocksFromHistory(content) {
@@ -769,15 +832,19 @@ function stripFileBlocksFromHistory(content) {
769
832
  return content.replace(/```([^\n`]+?\.[^\s.`]+)\n[\s\S]*?```/g, "[file previously attached: $1]");
770
833
  }
771
834
  function buildBoundedChatMessages(options) {
772
- var contextWindow = getContextWindow(options.platform, options.model);
835
+ var contextWindow = getContextWindow(options.platform, options.model, options.serviceId);
773
836
  var contextBasedBudget = Math.max(
774
837
  MIN_INPUT_TOKEN_BUDGET,
775
838
  contextWindow - OUTPUT_TOKEN_RESERVE - TOOL_AND_RESPONSE_BUFFER
776
839
  );
777
- var availableInputBudget = options.platform === "claude" ? Math.min(contextBasedBudget, CLAUDE_PER_REQUEST_INPUT_CAP) : contextBasedBudget;
840
+ var scaled = !!(options.serviceId && getProjectContextWindow(options.serviceId));
841
+ var claudeInputCap = scaled ? Math.max(CLAUDE_PER_REQUEST_INPUT_CAP, Math.round(contextBasedBudget * CLAUDE_INPUT_CAP_RATIO)) : CLAUDE_PER_REQUEST_INPUT_CAP;
842
+ var availableInputBudget = options.platform === "claude" ? Math.min(contextBasedBudget, claudeInputCap) : contextBasedBudget;
778
843
  var systemCost = estimateTextTokens(options.systemPrompt) + 12;
779
- var budgetForHistory = Math.max(1e3, Math.min(HISTORY_TOKEN_BUDGET, availableInputBudget - systemCost));
780
- var windowed = options.history.slice(-MAX_HISTORY_MESSAGES);
844
+ var historyAllowance = scaled ? Math.max(HISTORY_TOKEN_BUDGET, Math.round(contextBasedBudget * HISTORY_BUDGET_RATIO)) : HISTORY_TOKEN_BUDGET;
845
+ var budgetForHistory = Math.max(1e3, Math.min(historyAllowance, availableInputBudget - systemCost));
846
+ var maxHistoryMessages = scaled ? Math.max(MAX_HISTORY_MESSAGES, Math.round(MAX_HISTORY_MESSAGES * (budgetForHistory / HISTORY_TOKEN_BUDGET))) : MAX_HISTORY_MESSAGES;
847
+ var windowed = options.history.slice(-maxHistoryMessages);
781
848
  var latestIndex = windowed.length - 1;
782
849
  var trimmed = windowed.map(function(m, i2) {
783
850
  if (i2 === latestIndex) return m;
@@ -802,6 +869,60 @@ function buildBoundedChatMessages(options) {
802
869
  };
803
870
  }
804
871
 
872
+ // src/engine/time.ts
873
+ function wallClockNow() {
874
+ return Date.now();
875
+ }
876
+ function formatChatTimestamp(ms) {
877
+ if (typeof ms !== "number" || !isFinite(ms) || ms <= 0) return "";
878
+ try {
879
+ return new Date(ms).toLocaleString(void 0, {
880
+ year: "numeric",
881
+ month: "short",
882
+ day: "numeric",
883
+ hour: "numeric",
884
+ minute: "2-digit",
885
+ second: "2-digit"
886
+ });
887
+ } catch (e) {
888
+ return "";
889
+ }
890
+ }
891
+
892
+ // src/engine/ai_agent.ts
893
+ function normalizePlatform(raw) {
894
+ var p = (raw || "").trim().toLowerCase();
895
+ return p === "claude" || p === "openai" ? p : null;
896
+ }
897
+ function parseAiAgentValue(value) {
898
+ var raw = (value || "").trim();
899
+ if (!raw || raw.toLowerCase() === "none") {
900
+ return { platform: null, model: "", contextWindow: null, hasPlatform: false };
901
+ }
902
+ var firstHash = raw.indexOf("#");
903
+ if (firstHash === -1) {
904
+ var only = normalizePlatform(raw);
905
+ return { platform: only, model: "", contextWindow: null, hasPlatform: !!only };
906
+ }
907
+ var platform = normalizePlatform(raw.slice(0, firstHash));
908
+ var rest = raw.slice(firstHash + 1);
909
+ var secondHash = rest.indexOf("#");
910
+ var model = (secondHash === -1 ? rest : rest.slice(0, secondHash)).trim();
911
+ var windowRaw = secondHash === -1 ? "" : rest.slice(secondHash + 1).trim();
912
+ var parsedWindow = windowRaw ? Number(windowRaw) : NaN;
913
+ var contextWindow = Number.isFinite(parsedWindow) && parsedWindow > 0 ? Math.floor(parsedWindow) : null;
914
+ return { platform, model, contextWindow, hasPlatform: !!platform };
915
+ }
916
+ function buildAiAgentValue(platform, model, contextWindow) {
917
+ var p = (platform || "").trim().toLowerCase();
918
+ if (!p || p === "none") return "none";
919
+ var m = (model || "").trim();
920
+ if (!m) return p;
921
+ var n = Number(contextWindow);
922
+ if (!Number.isFinite(n) || n <= 0) return p + "#" + m;
923
+ return p + "#" + m + "#" + Math.floor(n);
924
+ }
925
+
805
926
  // src/engine/requests.ts
806
927
  var ANTHROPIC_MESSAGES_API_URL = "https://api.anthropic.com/v1/messages";
807
928
  var ANTHROPIC_MODELS_API_URL = "https://api.anthropic.com/v1/models";
@@ -1310,7 +1431,8 @@ async function getChatHistory(params, fetchOptions) {
1310
1431
  method: "POST"
1311
1432
  },
1312
1433
  { service: params.service, owner: params.owner },
1313
- params.queue ? { queue: params.queue } : {}
1434
+ params.queue ? { queue: params.queue } : {},
1435
+ params.status ? { status: params.status } : {}
1314
1436
  );
1315
1437
  return chatEngineConfig().clientSecretRequestHistory(
1316
1438
  p,
@@ -1347,6 +1469,25 @@ function extractLastUserTextFromRequest(requestBody) {
1347
1469
  }
1348
1470
  return "";
1349
1471
  }
1472
+ function isIndexingRequestText(userText) {
1473
+ if (typeof userText !== "string") return false;
1474
+ return userText.indexOf("A new file has just been uploaded") === 0 || userText.indexOf("CONTINUE indexing") === 0;
1475
+ }
1476
+ function parseIndexingRequestText(userText) {
1477
+ if (typeof userText !== "string" || !userText) return null;
1478
+ var nameMatch = userText.match(/^- name: (.+)$/m);
1479
+ if (!nameMatch) return null;
1480
+ var mimeMatch = userText.match(/^- mime type: (.+)$/m);
1481
+ var sizeMatch = userText.match(/^- size \(bytes\): (\d+)$/m);
1482
+ var pathMatch = userText.match(/^- storage path: (.+)$/m);
1483
+ return {
1484
+ name: nameMatch[1].trim(),
1485
+ path: pathMatch ? pathMatch[1].trim() : void 0,
1486
+ mime: mimeMatch ? mimeMatch[1].trim() : void 0,
1487
+ size: sizeMatch ? Number(sizeMatch[1]) : void 0,
1488
+ continued: userText.indexOf("CONTINUE indexing") === 0
1489
+ };
1490
+ }
1350
1491
  function mapHistoryListToMessages(list, platform, opts) {
1351
1492
  var mapped = [], runningItemIds = [];
1352
1493
  var extractAssistantText = platform === "openai" ? extractOpenAIText : extractClaudeText;
@@ -1363,31 +1504,25 @@ function mapHistoryListToMessages(list, platform, opts) {
1363
1504
  var assistantText = isPending ? "" : (extractAssistantText(response) || "").trim() || "";
1364
1505
  var isErrorResponse = !isPending && (isFailed || isErrorResponseBody(response));
1365
1506
  var serverItemId = item && typeof item.id === "string" && item.id ? item.id : void 0;
1507
+ var createdTs = Number(item && item.created);
1508
+ var updatedTs = Number(item && item.updated);
1509
+ var userTs = isFinite(createdTs) && createdTs > 0 ? createdTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : void 0;
1510
+ var replyTs = isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
1366
1511
  if (userText) {
1367
1512
  var displayContent;
1368
1513
  var indexFile = void 0;
1369
1514
  if (item._isBgTask) {
1370
- var nameMatch = userText.match(/^- name: (.+)$/m);
1371
- if (nameMatch) {
1372
- var mimeMatch = userText.match(/^- mime type: (.+)$/m);
1373
- var sizeMatch = userText.match(/^- size \(bytes\): (\d+)$/m);
1374
- var pathMatch = userText.match(/^- storage path: (.+)$/m);
1375
- var isContinuePass = userText.indexOf("CONTINUE indexing") === 0;
1515
+ var ref = parseIndexingRequestText(userText);
1516
+ if (ref) {
1376
1517
  displayContent = opts.formatIndexingLabel(
1377
- nameMatch[1].trim(),
1378
- mimeMatch ? mimeMatch[1].trim() : "",
1379
- sizeMatch ? Number(sizeMatch[1]) : null,
1380
- pathMatch ? pathMatch[1].trim() : void 0,
1518
+ ref.name,
1519
+ ref.mime || "",
1520
+ typeof ref.size === "number" ? ref.size : null,
1521
+ ref.path,
1381
1522
  false,
1382
- isContinuePass
1523
+ ref.continued
1383
1524
  );
1384
- indexFile = {
1385
- name: nameMatch[1].trim(),
1386
- path: pathMatch ? pathMatch[1].trim() : void 0,
1387
- mime: mimeMatch ? mimeMatch[1].trim() : void 0,
1388
- size: sizeMatch ? Number(sizeMatch[1]) : void 0,
1389
- continued: isContinuePass
1390
- };
1525
+ indexFile = ref;
1391
1526
  } else {
1392
1527
  displayContent = userText;
1393
1528
  }
@@ -1402,6 +1537,7 @@ function mapHistoryListToMessages(list, platform, opts) {
1402
1537
  if (indexFile) userMsg._indexFile = indexFile;
1403
1538
  if (item._isOnBgQueue) userMsg._useBgQueue = true;
1404
1539
  if (serverItemId !== void 0) userMsg._serverItemId = serverItemId;
1540
+ if (userTs !== void 0) userMsg._ts = userTs;
1405
1541
  mapped.push(userMsg);
1406
1542
  }
1407
1543
  if (isCancelledItem) ; else if (isInProcess) {
@@ -1416,11 +1552,13 @@ function mapHistoryListToMessages(list, platform, opts) {
1416
1552
  var em = { role: "assistant", content: getErrorMessage(response), isError: true };
1417
1553
  if (item._isBgTask) em.isBackgroundTask = true;
1418
1554
  if (serverItemId !== void 0) em._serverItemId = serverItemId;
1555
+ if (replyTs !== void 0) em._ts = replyTs;
1419
1556
  mapped.push(em);
1420
1557
  } else if (assistantText) {
1421
1558
  var okm = { role: "assistant", content: sanitizeAttachmentLinksForHistory(assistantText, opts.serviceId, true) };
1422
1559
  if (item._isBgTask) okm.isBackgroundTask = true;
1423
1560
  if (serverItemId !== void 0) okm._serverItemId = serverItemId;
1561
+ if (replyTs !== void 0) okm._ts = replyTs;
1424
1562
  mapped.push(okm);
1425
1563
  }
1426
1564
  });
@@ -1513,6 +1651,8 @@ function createHistoryFiller(base) {
1513
1651
  }
1514
1652
 
1515
1653
  // src/engine/session.ts
1654
+ var WORKER_PASS_ADOPT_LIMIT = 20;
1655
+ var WORKER_PASS_ADOPT_ATTEMPTS = [0, 2e3, 6e3];
1516
1656
  var _g = typeof globalThis !== "undefined" ? globalThis : {};
1517
1657
  function nowMs() {
1518
1658
  return _g.performance && typeof _g.performance.now === "function" ? _g.performance.now() : Date.now();
@@ -1532,6 +1672,35 @@ function isPollStopped(res) {
1532
1672
  var ChatSession = class {
1533
1673
  constructor(host) {
1534
1674
  this.typewriterQueue = Promise.resolve();
1675
+ /**
1676
+ * Pick up indexing passes the WORKER minted, which no client ever dispatched.
1677
+ *
1678
+ * For a PDF (and for text/grid when windowed indexing is on) the worker writes
1679
+ * pass N+1's row itself, inside pass N's invocation, right after saving pass
1680
+ * N's result. That row reaches this client through nothing at all: it is not in
1681
+ * bgTaskQueue (the client never asked for it) and it is not in state.messages
1682
+ * (only a first-page history load maps it in, which happens on mount, project
1683
+ * switch or tab return). So between two worker passes every pass the client
1684
+ * knows about is settled, and the collapsed row renders "Indexed N passes"
1685
+ * with no spinner and no Stop, for a file that is still being read. A user who
1686
+ * believes that then asks questions against a half-indexed file — which is what
1687
+ * this exists to prevent.
1688
+ *
1689
+ * So when a background indexing pass settles, ask the bg queue what is still
1690
+ * unresolved on it and adopt anything unknown as an ordinary BgTaskEntry.
1691
+ * drainBgTaskQueue then treats it exactly like a pass this client dispatched:
1692
+ * same bubble, same `_indexFile` (so it joins the file's collapsed row), same
1693
+ * poll — and that poll settling runs this again, so the chain is followed to
1694
+ * its end. `status`-scoped so the reply carries the live items only, never a
1695
+ * page of finished ones with their bodies.
1696
+ *
1697
+ * Termination: the only trigger is a pass SETTLING, which happens once per
1698
+ * pass. An adopted item that is still running is not re-adopted (its id is
1699
+ * already polled), and when the queue holds nothing unknown the chain stops on
1700
+ * its own. Nothing here is periodic — a timer that re-reads history is the
1701
+ * shape that previously looped fetchHistoryPage after an already-DONE index.
1702
+ */
1703
+ this._adoptingWorkerPasses = false;
1535
1704
  this.host = host;
1536
1705
  this.state = {
1537
1706
  messages: [],
@@ -1833,7 +2002,7 @@ var ChatSession = class {
1833
2002
  var offExisting = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
1834
2003
  this.aiChatHistoryCache[key] = {
1835
2004
  messages: offExisting.messages.concat([
1836
- { role: "user", content: composed, _ownerKey: key },
2005
+ { role: "user", content: composed, _ownerKey: key, _ts: wallClockNow() },
1837
2006
  { role: "assistant", content: "", isPending: true, isPendingInProcess: true, _ownerKey: key }
1838
2007
  ]),
1839
2008
  endOfList: offExisting.endOfList,
@@ -1865,7 +2034,7 @@ var ChatSession = class {
1865
2034
  serviceId: id.serviceId,
1866
2035
  history: resolvedHistory.concat([{ role: "user", content: llmComposed }])
1867
2036
  });
1868
- var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true };
2037
+ var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true, _ts: wallClockNow() };
1869
2038
  if (key) queuedBubble._ownerKey = key;
1870
2039
  if (useBgQueue) queuedBubble._useBgQueue = true;
1871
2040
  this.state.messages.push(queuedBubble);
@@ -1900,7 +2069,7 @@ var ChatSession = class {
1900
2069
  });
1901
2070
  return;
1902
2071
  }
1903
- this.state.messages.push({ role: "user", content: composed, ...key ? { _ownerKey: key } : {} });
2072
+ this.state.messages.push({ role: "user", content: composed, _ts: wallClockNow(), ...key ? { _ownerKey: key } : {} });
1904
2073
  this.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true, ...key ? { _ownerKey: key } : {} });
1905
2074
  this.host.notify();
1906
2075
  this.updateHistoryCache();
@@ -1957,6 +2126,7 @@ var ChatSession = class {
1957
2126
  var existing = this.state.messages[nextIdx];
1958
2127
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true, isBackgroundTask: true };
1959
2128
  if (existing._indexFile) promoted._indexFile = existing._indexFile;
2129
+ if (existing._ts !== void 0) promoted._ts = existing._ts;
1960
2130
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1961
2131
  if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1962
2132
  this.state.messages[nextIdx] = promoted;
@@ -1978,6 +2148,7 @@ var ChatSession = class {
1978
2148
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true };
1979
2149
  if (existing.isBackgroundTask) promoted.isBackgroundTask = true;
1980
2150
  if (existing._indexFile) promoted._indexFile = existing._indexFile;
2151
+ if (existing._ts !== void 0) promoted._ts = existing._ts;
1981
2152
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1982
2153
  if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1983
2154
  if (existing.isSendingToServer) promoted.isSendingToServer = true;
@@ -2027,6 +2198,7 @@ var ChatSession = class {
2027
2198
  var repl = { role: "user", content: exist.content };
2028
2199
  if (exist._serverItemId !== void 0) repl._serverItemId = exist._serverItemId;
2029
2200
  if (exist._ownerKey !== void 0) repl._ownerKey = exist._ownerKey;
2201
+ if (exist._ts !== void 0) repl._ts = exist._ts;
2030
2202
  this.state.messages[userIdx] = repl;
2031
2203
  }
2032
2204
  var thinkingIdx = userIdx >= 0 ? this.state.messages.findIndex(function(m, i) {
@@ -2035,6 +2207,7 @@ var ChatSession = class {
2035
2207
  return thinkingIdx !== -1 ? thinkingIdx : userIdx >= 0 ? userIdx + 1 : -1;
2036
2208
  }
2037
2209
  insertAtTarget(msg, targetIdx) {
2210
+ if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = wallClockNow();
2038
2211
  if (targetIdx >= 0 && this.state.messages[targetIdx] && this.state.messages[targetIdx].isPending) this.state.messages[targetIdx] = msg;
2039
2212
  else if (targetIdx >= 0) this.state.messages.splice(targetIdx, 0, msg);
2040
2213
  else this.state.messages.push(msg);
@@ -2371,6 +2544,8 @@ var ChatSession = class {
2371
2544
  }
2372
2545
  enqueueTypewrite(idx, fullText, localId) {
2373
2546
  var self = this;
2547
+ var target = this.state.messages[idx];
2548
+ if (target && target._ts === void 0) target._ts = wallClockNow();
2374
2549
  this.typewriterQueue = this.typewriterQueue.then(function() {
2375
2550
  return self.typewriteIntoIndex(idx, fullText, localId);
2376
2551
  });
@@ -2440,6 +2615,7 @@ var ChatSession = class {
2440
2615
  var u = this.state.messages[uIdx];
2441
2616
  var cleaned = { role: "user", content: u.content, _serverItemId: itemId };
2442
2617
  if (u.isBackgroundTask) cleaned.isBackgroundTask = true;
2618
+ if (u._ts !== void 0) cleaned._ts = u._ts;
2443
2619
  if (u._indexFile) cleaned._indexFile = u._indexFile;
2444
2620
  this.state.messages[uIdx] = cleaned;
2445
2621
  }
@@ -2470,8 +2646,21 @@ var ChatSession = class {
2470
2646
  }
2471
2647
  // --- background-task resolution + drain -------------------------------
2472
2648
  handleHistoryItemResolution(itemId, response, platform) {
2649
+ var indexRef = this._indexRefOfItem(itemId);
2473
2650
  this.applyHistoryItemResolution(itemId, response, platform);
2474
2651
  this.promoteNextBgQueuedToRunning();
2652
+ if (indexRef) this._followWorkerIndexingChain(indexRef.name, indexRef.mime);
2653
+ }
2654
+ /** The file an already-rendered background pass is about, off its request
2655
+ * bubble. Null for an ordinary turn, which is most of them. */
2656
+ _indexRefOfItem(itemId) {
2657
+ if (!itemId) return null;
2658
+ for (var i = 0; i < this.state.messages.length; i++) {
2659
+ var m = this.state.messages[i];
2660
+ if (m._serverItemId !== itemId || m.role !== "user" || !m.isBackgroundTask) continue;
2661
+ return m._indexFile || null;
2662
+ }
2663
+ return null;
2475
2664
  }
2476
2665
  applyHistoryItemResolution(itemId, response, platform) {
2477
2666
  this.historyItemPolls.delete(itemId);
@@ -2512,6 +2701,7 @@ var ChatSession = class {
2512
2701
  var ex = this.state.messages[userIdx];
2513
2702
  var settledUser = { role: "user", content: ex.content, _serverItemId: itemId };
2514
2703
  if (ex.isBackgroundTask) settledUser.isBackgroundTask = true;
2704
+ if (ex._ts !== void 0) settledUser._ts = ex._ts;
2515
2705
  if (ex._indexFile) settledUser._indexFile = ex._indexFile;
2516
2706
  if (ex._useBgQueue) settledUser._useBgQueue = true;
2517
2707
  this.state.messages[userIdx] = settledUser;
@@ -2604,6 +2794,116 @@ var ChatSession = class {
2604
2794
  self.cancelQueuedMessage(t.msg, idx === -1 ? t.idx : idx);
2605
2795
  });
2606
2796
  }
2797
+ /**
2798
+ * True when the WORKER, not this client, drives the rest of this file's chain.
2799
+ * The mirror image of the early returns in maybeResumeIndexing: whatever that
2800
+ * refuses to continue is exactly what nothing client-side is tracking.
2801
+ */
2802
+ _isWorkerDrivenIndexing(filename, mime) {
2803
+ if (!isPagedReadFile(filename, mime)) return false;
2804
+ if (isImageVisionFile(filename, mime)) return true;
2805
+ return windowedIndexingEnabled() && isWindowedReadFile(filename, mime);
2806
+ }
2807
+ _adoptWorkerIndexingPasses(attempt) {
2808
+ var self = this;
2809
+ if (this._adoptingWorkerPasses) return;
2810
+ var id = this.host.getIdentity();
2811
+ var platform = id.platform;
2812
+ if (!id.serviceId || platform !== "claude" && platform !== "openai") return;
2813
+ if (this.isPollingPaused() || !this.host.isViewMounted()) return;
2814
+ var svcId = id.serviceId, owner = id.owner;
2815
+ var queue = (id.userId || id.serviceId) + BG_INDEXING_QUEUE_SUFFIX;
2816
+ var ask = function(status) {
2817
+ return Promise.resolve(getChatHistory(
2818
+ { service: svcId, owner, platform, queue, status },
2819
+ { limit: WORKER_PASS_ADOPT_LIMIT }
2820
+ )).catch(function() {
2821
+ return null;
2822
+ });
2823
+ };
2824
+ this._adoptingWorkerPasses = true;
2825
+ Promise.all([ask("running"), ask("pending")]).then(function(results) {
2826
+ self._adoptingWorkerPasses = false;
2827
+ var now = self.host.getIdentity();
2828
+ if (now.serviceId !== svcId || now.platform !== platform) return;
2829
+ if (!self.host.isViewMounted()) return;
2830
+ var adoptedIds = [];
2831
+ for (var ri = 0; ri < results.length; ri++) {
2832
+ var list = results[ri] && Array.isArray(results[ri].list) ? results[ri].list : [];
2833
+ for (var i = 0; i < list.length; i++) {
2834
+ if (self._adoptWorkerIndexingItem(list[i], svcId, platform)) adoptedIds.push(list[i].id);
2835
+ }
2836
+ }
2837
+ if (adoptedIds.length) {
2838
+ self.drainBgTaskQueue();
2839
+ if (self._isTrackingAny(adoptedIds)) return;
2840
+ }
2841
+ if (attempt + 1 >= WORKER_PASS_ADOPT_ATTEMPTS.length) return;
2842
+ setTimeout(function() {
2843
+ var later = self.host.getIdentity();
2844
+ if (later.serviceId !== svcId || later.platform !== platform) return;
2845
+ if (self.isPollingPaused() || !self.host.isViewMounted()) return;
2846
+ self._adoptWorkerIndexingPasses(attempt + 1);
2847
+ }, WORKER_PASS_ADOPT_ATTEMPTS[attempt + 1]);
2848
+ }, function() {
2849
+ self._adoptingWorkerPasses = false;
2850
+ });
2851
+ }
2852
+ /** Any of these ids still queued or still polled, i.e. surviving work. */
2853
+ _isTrackingAny(ids) {
2854
+ for (var i = 0; i < ids.length; i++) {
2855
+ if (this.historyItemPolls.has(ids[i])) return true;
2856
+ for (var q = 0; q < this.bgTaskQueue.length; q++) {
2857
+ if (this.bgTaskQueue[q].id === ids[i]) return true;
2858
+ }
2859
+ }
2860
+ return false;
2861
+ }
2862
+ /** One live bg-queue item -> a BgTaskEntry, if it is an indexing pass this
2863
+ * client is not already tracking. Returns whether it was adopted. */
2864
+ _adoptWorkerIndexingItem(item, svcId, platform) {
2865
+ if (!item || typeof item.id !== "string" || !item.id) return false;
2866
+ if (item.status !== "pending" && item.status !== "running") return false;
2867
+ if (typeof item.poll !== "function") return false;
2868
+ if (this.historyItemPolls.has(item.id)) return false;
2869
+ for (var q = 0; q < this.bgTaskQueue.length; q++) {
2870
+ if (this.bgTaskQueue[q].id === item.id) return false;
2871
+ }
2872
+ for (var m = 0; m < this.state.messages.length; m++) {
2873
+ var msg = this.state.messages[m];
2874
+ if (msg._serverItemId !== item.id) continue;
2875
+ if (!(msg.isPending || msg.isPendingInProcess || msg.isPendingQueued)) return false;
2876
+ }
2877
+ var body = item.request_body;
2878
+ if (!body || typeof body !== "object") return false;
2879
+ if (platform === "claude" ? !Array.isArray(body.messages) : !Array.isArray(body.input)) return false;
2880
+ var userText = extractLastUserTextFromRequest(body);
2881
+ if (!isIndexingRequestText(userText)) return false;
2882
+ var ref = parseIndexingRequestText(userText);
2883
+ if (!ref || !ref.name) return false;
2884
+ if (!this._isWorkerDrivenIndexing(ref.name, ref.mime)) return false;
2885
+ this.bgTaskQueue.push({
2886
+ serviceId: svcId,
2887
+ platform,
2888
+ id: item.id,
2889
+ filename: ref.name,
2890
+ storagePath: ref.path,
2891
+ mime: ref.mime,
2892
+ size: ref.size,
2893
+ status: item.status === "running" ? "running" : "pending",
2894
+ poll: item.poll,
2895
+ // Drives the "(continuing)" label and marks this as a continuation for
2896
+ // _applyIndexCancellations, which must not read a CONTINUE pass as the
2897
+ // fresh first pass that lifts a stop.
2898
+ resumePass: ref.continued ? 1 : 0
2899
+ });
2900
+ return true;
2901
+ }
2902
+ /** Follow the chain on from a background indexing pass that just settled. */
2903
+ _followWorkerIndexingChain(filename, mime) {
2904
+ if (!this._isWorkerDrivenIndexing(filename, mime)) return;
2905
+ this._adoptWorkerIndexingPasses(0);
2906
+ }
2607
2907
  /** Best-effort server-side cancel of a bg-queue item that has no bubble (so
2608
2908
  * cancelQueuedMessage, which drives one, has nothing to act on). */
2609
2909
  _cancelServerItem(serverId) {
@@ -2814,8 +3114,7 @@ var ChatSession = class {
2814
3114
  var chatList = history && Array.isArray(history.list) ? history.list : [];
2815
3115
  chatList.forEach(function(item) {
2816
3116
  if (isBgIndexingQueue(item.queue_name)) {
2817
- var userText = extractLastUserTextFromRequest(item.request_body);
2818
- if (typeof userText === "string" && (userText.indexOf("A new file has just been uploaded") === 0 || userText.indexOf("CONTINUE indexing") === 0)) item._isBgTask = true;
3117
+ if (isIndexingRequestText(extractLastUserTextFromRequest(item.request_body))) item._isBgTask = true;
2819
3118
  else item._isOnBgQueue = true;
2820
3119
  }
2821
3120
  });
@@ -3288,7 +3587,10 @@ function buildChatDisplayList(messages, opts) {
3288
3587
  cancellableIds: [],
3289
3588
  cancelling: false,
3290
3589
  mayHaveOlder: false,
3291
- anchorIndex: i
3590
+ // The run's first loaded pass, and never re-stamped: see the file
3591
+ // docstring. `anchorId` is filled in once every member is known.
3592
+ anchorIndex: i,
3593
+ anchorId: ""
3292
3594
  };
3293
3595
  order.push(runId);
3294
3596
  }
@@ -3302,7 +3604,6 @@ function buildChatDisplayList(messages, opts) {
3302
3604
  g.passCount++;
3303
3605
  }
3304
3606
  g.members.push({ msg, index: i });
3305
- g.anchorIndex = i;
3306
3607
  runOfIndex[i] = runId;
3307
3608
  if (msg._serverItemId) runByItemId[msg._serverItemId] = runId;
3308
3609
  if (ref && ref.name) keyByName[ref.name] = g.key;
@@ -3364,6 +3665,9 @@ function buildChatDisplayList(messages, opts) {
3364
3665
  }
3365
3666
  }
3366
3667
  grp.mayHaveOlder = !sawFirstPass && hasMoreHistory;
3668
+ var anchor = grp.members[0];
3669
+ grp.anchorIndex = anchor.index;
3670
+ grp.anchorId = anchor.msg._serverItemId || anchor.msg._localId || "";
3367
3671
  }
3368
3672
  var out = [];
3369
3673
  for (var j = 0; j < list.length; j++) {
@@ -3378,6 +3682,7 @@ function buildChatDisplayList(messages, opts) {
3378
3682
  }
3379
3683
 
3380
3684
  exports.BG_INDEXING_QUEUE_SUFFIX = BG_INDEXING_QUEUE_SUFFIX;
3685
+ exports.CLAUDE_INPUT_CAP_RATIO = CLAUDE_INPUT_CAP_RATIO;
3381
3686
  exports.CLAUDE_PER_REQUEST_INPUT_CAP = CLAUDE_PER_REQUEST_INPUT_CAP;
3382
3687
  exports.CONTEXT_WINDOW_BY_MODEL = CONTEXT_WINDOW_BY_MODEL;
3383
3688
  exports.CONTEXT_WINDOW_DEFAULT = CONTEXT_WINDOW_DEFAULT;
@@ -3387,6 +3692,7 @@ exports.DEFAULT_OPENAI_MODEL = DEFAULT_OPENAI_MODEL;
3387
3692
  exports.EXPIRED_ATTACHMENT_URL_HOST = EXPIRED_ATTACHMENT_URL_HOST;
3388
3693
  exports.EXPIRED_ATTACHMENT_URL_ORIGIN = EXPIRED_ATTACHMENT_URL_ORIGIN;
3389
3694
  exports.EXPIRED_LINK_REFRESH_EXPIRES_SECONDS = EXPIRED_LINK_REFRESH_EXPIRES_SECONDS;
3695
+ exports.HISTORY_BUDGET_RATIO = HISTORY_BUDGET_RATIO;
3390
3696
  exports.HISTORY_FILL_SLACK_PX = HISTORY_FILL_SLACK_PX;
3391
3697
  exports.HISTORY_TOKEN_BUDGET = HISTORY_TOKEN_BUDGET;
3392
3698
  exports.LINK_LABEL_MAX_DISPLAY_CHARS = LINK_LABEL_MAX_DISPLAY_CHARS;
@@ -3400,6 +3706,7 @@ exports.OUTPUT_TOKEN_RESERVE = OUTPUT_TOKEN_RESERVE;
3400
3706
  exports.POLL_INTERVAL = POLL_INTERVAL;
3401
3707
  exports.RENDER_FROM_TOKEN = RENDER_FROM_TOKEN;
3402
3708
  exports.TOOL_AND_RESPONSE_BUFFER = TOOL_AND_RESPONSE_BUFFER;
3709
+ exports.buildAiAgentValue = buildAiAgentValue;
3403
3710
  exports.buildBoundedChatMessages = buildBoundedChatMessages;
3404
3711
  exports.buildChatDisplayList = buildChatDisplayList;
3405
3712
  exports.buildChatSystemPrompt = buildChatSystemPrompt;
@@ -3430,16 +3737,19 @@ exports.extractRemotePathFromAttachmentHref = extractRemotePathFromAttachmentHre
3430
3737
  exports.fillHistoryViewport = fillHistoryViewport;
3431
3738
  exports.filterListByClearHorizon = filterListByClearHorizon;
3432
3739
  exports.findAttachmentParser = findAttachmentParser;
3740
+ exports.formatChatTimestamp = formatChatTimestamp;
3433
3741
  exports.getAttachmentParsers = getAttachmentParsers;
3434
3742
  exports.getChatHistory = getChatHistory;
3435
3743
  exports.getContextWindow = getContextWindow;
3436
3744
  exports.getErrorMessage = getErrorMessage;
3437
3745
  exports.getExpiredAttachmentVisiblePath = getExpiredAttachmentVisiblePath;
3746
+ exports.getProjectContextWindow = getProjectContextWindow;
3438
3747
  exports.groupAttachmentFailures = groupAttachmentFailures;
3439
3748
  exports.isAuthExpiredError = isAuthExpiredError;
3440
3749
  exports.isBgIndexingQueue = isBgIndexingQueue;
3441
3750
  exports.isErrorResponseBody = isErrorResponseBody;
3442
3751
  exports.isHttpUrlLike = isHttpUrlLike;
3752
+ exports.isIndexingRequestText = isIndexingRequestText;
3443
3753
  exports.isNonRetryableRequestError = isNonRetryableRequestError;
3444
3754
  exports.isOfficeFile = isOfficeFile;
3445
3755
  exports.isServerExtractable = isServerExtractable;
@@ -3452,16 +3762,22 @@ exports.normalizeAttachmentPathCandidate = normalizeAttachmentPathCandidate;
3452
3762
  exports.normalizeTextContent = normalizeTextContent;
3453
3763
  exports.normalizeTrailingInlineToken = normalizeTrailingInlineToken;
3454
3764
  exports.notifyAgentSaveAttachment = notifyAgentSaveAttachment;
3765
+ exports.parseAiAgentValue = parseAiAgentValue;
3455
3766
  exports.parseAttachmentContent = parseAttachmentContent;
3456
3767
  exports.parseIndexingLabel = parseIndexingLabel;
3768
+ exports.parseIndexingRequestText = parseIndexingRequestText;
3457
3769
  exports.readExpiredAttachmentHref = readExpiredAttachmentHref;
3458
3770
  exports.registerAttachmentParser = registerAttachmentParser;
3771
+ exports.registerModelContextWindows = registerModelContextWindows;
3772
+ exports.repairUrlEntities = repairUrlEntities;
3459
3773
  exports.repairUrlWhitespace = repairUrlWhitespace;
3460
3774
  exports.safeDecodeURIComponent = safeDecodeURIComponent;
3461
3775
  exports.sanitizeAttachmentLinksForHistory = sanitizeAttachmentLinksForHistory;
3776
+ exports.setProjectContextWindow = setProjectContextWindow;
3462
3777
  exports.stripFileBlocksFromHistory = stripFileBlocksFromHistory;
3463
3778
  exports.transformContentWithImages = transformContentWithImages;
3464
3779
  exports.transformContentWithOpenAIImages = transformContentWithOpenAIImages;
3465
3780
  exports.truncateLabelForDisplay = truncateLabelForDisplay;
3781
+ exports.wallClockNow = wallClockNow;
3466
3782
  //# sourceMappingURL=engine.cjs.map
3467
3783
  //# sourceMappingURL=engine.cjs.map