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/bunnyquery.js CHANGED
@@ -280,6 +280,9 @@ Extracted content of attached office files (read inline below; do NOT fetch thei
280
280
  You are a dedicated assistant for the project ID: "${formattedServiceId}".
281
281
  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.
282
282
  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.
283
+ 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.
284
+ 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.
285
+ 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.
283
286
  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.
284
287
  - 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.
285
288
  - 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.
@@ -614,6 +617,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
614
617
  if (/^https?:\/\/[^/\s]+\/download\/[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)?$/i.test(stripped)) return stripped;
615
618
  return href.trim().replace(/\s/g, "%20");
616
619
  }
620
+ function repairUrlEntities(href) {
621
+ if (!href || href.indexOf("&") === -1) return href;
622
+ var out = href, prev = "";
623
+ while (out !== prev) {
624
+ prev = out;
625
+ out = out.replace(/&/gi, "&").replace(/&#0*38;/g, "&").replace(/&#x0*26;/gi, "&");
626
+ }
627
+ return out;
628
+ }
617
629
  function normalizeTrailingInlineToken(value) {
618
630
  if (!value) return value;
619
631
  var out = value.replace(/[.,;:!?]+$/, "");
@@ -661,8 +673,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
661
673
  var tail = full.slice(("src::" + rawPath).length);
662
674
  var srcIsUrl = isHttpUrlLike(rawPath);
663
675
  if (srcIsUrl && !isDbHost(rawPath) && !readExpiredAttachmentHref(rawPath)) {
676
+ var srcUrl = repairUrlEntities(rawPath);
664
677
  return {
665
- part: { type: "link", label: truncateLabelForDisplay(rawPath), fullLabel: rawPath, href: rawPath, expired: false },
678
+ part: { type: "link", label: truncateLabelForDisplay(srcUrl), fullLabel: srcUrl, href: srcUrl, expired: false },
666
679
  tail
667
680
  };
668
681
  }
@@ -696,6 +709,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
696
709
  }
697
710
  var originalHref = g3 || g6 || "";
698
711
  if (!originalHref) return null;
712
+ originalHref = repairUrlEntities(originalHref);
699
713
  var urlTail;
700
714
  if (!g3 && g6) {
701
715
  var trimmedUrl = normalizeTrailingInlineToken(originalHref);
@@ -737,24 +751,63 @@ Index the REMAINING windows - one record per row/item, looking at any page image
737
751
  // src/engine/budget.ts
738
752
  var CONTEXT_WINDOW_DEFAULT = { claude: 2e5, openai: 128e3 };
739
753
  var CONTEXT_WINDOW_BY_MODEL = {
740
- "claude-opus-4-7": 2e5,
754
+ // exact ids
755
+ "claude-opus-5": 1e6,
756
+ "claude-opus-4-8": 1e6,
757
+ "claude-opus-4-7": 1e6,
758
+ "claude-sonnet-5": 1e6,
759
+ "claude-sonnet-4-6": 1e6,
741
760
  "claude-sonnet-4": 2e5,
742
- "gpt-5.4": 128e3
761
+ "claude-haiku-4-5": 2e5,
762
+ "gpt-5.4": 128e3,
763
+ "gpt-5.6-luna": 128e3,
764
+ // family keys
765
+ "claude-opus": 1e6,
766
+ "claude-sonnet": 1e6,
767
+ "claude-haiku": 2e5,
768
+ "gpt-5.6": 128e3,
769
+ "gpt-5": 128e3
743
770
  };
771
+ var apiReportedContextWindows = {};
772
+ var projectContextWindows = {};
773
+ function setProjectContextWindow(serviceId, tokens) {
774
+ var key = (serviceId || "").trim();
775
+ if (!key) return;
776
+ var n = Number(tokens);
777
+ if (Number.isFinite(n) && n > 0) projectContextWindows[key] = Math.floor(n);
778
+ else delete projectContextWindows[key];
779
+ }
780
+ function getProjectContextWindow(serviceId) {
781
+ var key = (serviceId || "").trim();
782
+ return key && projectContextWindows[key] ? projectContextWindows[key] : null;
783
+ }
744
784
  var OUTPUT_TOKEN_RESERVE = 22e3;
745
785
  var TOOL_AND_RESPONSE_BUFFER = 4e3;
746
786
  var MIN_INPUT_TOKEN_BUDGET = 8e3;
747
787
  var CLAUDE_PER_REQUEST_INPUT_CAP = 28e3;
788
+ var MAX_HISTORY_MESSAGES = 20;
748
789
  var HISTORY_TOKEN_BUDGET = 8e3;
790
+ var CLAUDE_INPUT_CAP_RATIO = 0.16;
791
+ var HISTORY_BUDGET_RATIO = 0.08;
749
792
  function estimateTextTokens(text) {
750
793
  return Math.ceil((text || "").length / 3);
751
794
  }
752
795
  function estimateMessageTokens(msg) {
753
796
  return estimateTextTokens(msg.content) + estimateTextTokens(msg.role) + 6;
754
797
  }
755
- function getContextWindow(platform, model) {
798
+ function getContextWindow(platform, model, serviceId) {
799
+ var override = serviceId ? getProjectContextWindow(serviceId) : null;
800
+ if (override) return override;
756
801
  var normalized = (model || "").trim().toLowerCase();
757
- if (normalized && CONTEXT_WINDOW_BY_MODEL[normalized]) return CONTEXT_WINDOW_BY_MODEL[normalized];
802
+ if (normalized) {
803
+ if (apiReportedContextWindows[normalized]) return apiReportedContextWindows[normalized];
804
+ if (CONTEXT_WINDOW_BY_MODEL[normalized]) return CONTEXT_WINDOW_BY_MODEL[normalized];
805
+ var parts = normalized.split("-");
806
+ for (var end = parts.length - 1; end > 0; end--) {
807
+ var family = parts.slice(0, end).join("-");
808
+ if (CONTEXT_WINDOW_BY_MODEL[family]) return CONTEXT_WINDOW_BY_MODEL[family];
809
+ }
810
+ }
758
811
  return CONTEXT_WINDOW_DEFAULT[platform];
759
812
  }
760
813
  function stripFileBlocksFromHistory(content) {
@@ -762,15 +815,19 @@ Index the REMAINING windows - one record per row/item, looking at any page image
762
815
  return content.replace(/```([^\n`]+?\.[^\s.`]+)\n[\s\S]*?```/g, "[file previously attached: $1]");
763
816
  }
764
817
  function buildBoundedChatMessages(options) {
765
- var contextWindow = getContextWindow(options.platform, options.model);
818
+ var contextWindow = getContextWindow(options.platform, options.model, options.serviceId);
766
819
  var contextBasedBudget = Math.max(
767
820
  MIN_INPUT_TOKEN_BUDGET,
768
821
  contextWindow - OUTPUT_TOKEN_RESERVE - TOOL_AND_RESPONSE_BUFFER
769
822
  );
770
- var availableInputBudget = options.platform === "claude" ? Math.min(contextBasedBudget, CLAUDE_PER_REQUEST_INPUT_CAP) : contextBasedBudget;
823
+ var scaled = !!(options.serviceId && getProjectContextWindow(options.serviceId));
824
+ var claudeInputCap = scaled ? Math.max(CLAUDE_PER_REQUEST_INPUT_CAP, Math.round(contextBasedBudget * CLAUDE_INPUT_CAP_RATIO)) : CLAUDE_PER_REQUEST_INPUT_CAP;
825
+ var availableInputBudget = options.platform === "claude" ? Math.min(contextBasedBudget, claudeInputCap) : contextBasedBudget;
771
826
  var systemCost = estimateTextTokens(options.systemPrompt) + 12;
772
- var budgetForHistory = Math.max(1e3, Math.min(HISTORY_TOKEN_BUDGET, availableInputBudget - systemCost));
773
- var windowed = options.history.slice(-20);
827
+ var historyAllowance = scaled ? Math.max(HISTORY_TOKEN_BUDGET, Math.round(contextBasedBudget * HISTORY_BUDGET_RATIO)) : HISTORY_TOKEN_BUDGET;
828
+ var budgetForHistory = Math.max(1e3, Math.min(historyAllowance, availableInputBudget - systemCost));
829
+ var maxHistoryMessages = scaled ? Math.max(MAX_HISTORY_MESSAGES, Math.round(MAX_HISTORY_MESSAGES * (budgetForHistory / HISTORY_TOKEN_BUDGET))) : MAX_HISTORY_MESSAGES;
830
+ var windowed = options.history.slice(-maxHistoryMessages);
774
831
  var latestIndex = windowed.length - 1;
775
832
  var trimmed = windowed.map(function(m, i2) {
776
833
  if (i2 === latestIndex) return m;
@@ -795,6 +852,51 @@ Index the REMAINING windows - one record per row/item, looking at any page image
795
852
  };
796
853
  }
797
854
 
855
+ // src/engine/time.ts
856
+ function wallClockNow() {
857
+ return Date.now();
858
+ }
859
+ function formatChatTimestamp(ms) {
860
+ if (typeof ms !== "number" || !isFinite(ms) || ms <= 0) return "";
861
+ try {
862
+ return new Date(ms).toLocaleString(void 0, {
863
+ year: "numeric",
864
+ month: "short",
865
+ day: "numeric",
866
+ hour: "numeric",
867
+ minute: "2-digit",
868
+ second: "2-digit"
869
+ });
870
+ } catch (e) {
871
+ return "";
872
+ }
873
+ }
874
+
875
+ // src/engine/ai_agent.ts
876
+ function normalizePlatform(raw) {
877
+ var p = (raw || "").trim().toLowerCase();
878
+ return p === "claude" || p === "openai" ? p : null;
879
+ }
880
+ function parseAiAgentValue(value) {
881
+ var raw = (value || "").trim();
882
+ if (!raw || raw.toLowerCase() === "none") {
883
+ return { platform: null, model: "", contextWindow: null, hasPlatform: false };
884
+ }
885
+ var firstHash = raw.indexOf("#");
886
+ if (firstHash === -1) {
887
+ var only = normalizePlatform(raw);
888
+ return { platform: only, model: "", contextWindow: null, hasPlatform: !!only };
889
+ }
890
+ var platform = normalizePlatform(raw.slice(0, firstHash));
891
+ var rest = raw.slice(firstHash + 1);
892
+ var secondHash = rest.indexOf("#");
893
+ var model = (secondHash === -1 ? rest : rest.slice(0, secondHash)).trim();
894
+ var windowRaw = secondHash === -1 ? "" : rest.slice(secondHash + 1).trim();
895
+ var parsedWindow = windowRaw ? Number(windowRaw) : NaN;
896
+ var contextWindow = Number.isFinite(parsedWindow) && parsedWindow > 0 ? Math.floor(parsedWindow) : null;
897
+ return { platform, model, contextWindow, hasPlatform: !!platform };
898
+ }
899
+
798
900
  // src/engine/requests.ts
799
901
  var ANTHROPIC_MESSAGES_API_URL = "https://api.anthropic.com/v1/messages";
800
902
  var ANTHROPIC_VERSION = "2023-06-01";
@@ -1276,7 +1378,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1276
1378
  method: "POST"
1277
1379
  },
1278
1380
  { service: params.service, owner: params.owner },
1279
- params.queue ? { queue: params.queue } : {}
1381
+ params.queue ? { queue: params.queue } : {},
1382
+ params.status ? { status: params.status } : {}
1280
1383
  );
1281
1384
  return chatEngineConfig().clientSecretRequestHistory(
1282
1385
  p,
@@ -1313,6 +1416,25 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1313
1416
  }
1314
1417
  return "";
1315
1418
  }
1419
+ function isIndexingRequestText(userText) {
1420
+ if (typeof userText !== "string") return false;
1421
+ return userText.indexOf("A new file has just been uploaded") === 0 || userText.indexOf("CONTINUE indexing") === 0;
1422
+ }
1423
+ function parseIndexingRequestText(userText) {
1424
+ if (typeof userText !== "string" || !userText) return null;
1425
+ var nameMatch = userText.match(/^- name: (.+)$/m);
1426
+ if (!nameMatch) return null;
1427
+ var mimeMatch = userText.match(/^- mime type: (.+)$/m);
1428
+ var sizeMatch = userText.match(/^- size \(bytes\): (\d+)$/m);
1429
+ var pathMatch = userText.match(/^- storage path: (.+)$/m);
1430
+ return {
1431
+ name: nameMatch[1].trim(),
1432
+ path: pathMatch ? pathMatch[1].trim() : void 0,
1433
+ mime: mimeMatch ? mimeMatch[1].trim() : void 0,
1434
+ size: sizeMatch ? Number(sizeMatch[1]) : void 0,
1435
+ continued: userText.indexOf("CONTINUE indexing") === 0
1436
+ };
1437
+ }
1316
1438
  function mapHistoryListToMessages(list, platform, opts) {
1317
1439
  var mapped = [], runningItemIds = [];
1318
1440
  var extractAssistantText = platform === "openai" ? extractOpenAIText : extractClaudeText;
@@ -1329,31 +1451,25 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1329
1451
  var assistantText = isPending ? "" : (extractAssistantText(response) || "").trim() || "";
1330
1452
  var isErrorResponse = !isPending && (isFailed || isErrorResponseBody(response));
1331
1453
  var serverItemId = item && typeof item.id === "string" && item.id ? item.id : void 0;
1454
+ var createdTs = Number(item && item.created);
1455
+ var updatedTs = Number(item && item.updated);
1456
+ var userTs = isFinite(createdTs) && createdTs > 0 ? createdTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : void 0;
1457
+ var replyTs = isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
1332
1458
  if (userText) {
1333
1459
  var displayContent;
1334
1460
  var indexFile = void 0;
1335
1461
  if (item._isBgTask) {
1336
- var nameMatch = userText.match(/^- name: (.+)$/m);
1337
- if (nameMatch) {
1338
- var mimeMatch = userText.match(/^- mime type: (.+)$/m);
1339
- var sizeMatch = userText.match(/^- size \(bytes\): (\d+)$/m);
1340
- var pathMatch = userText.match(/^- storage path: (.+)$/m);
1341
- var isContinuePass = userText.indexOf("CONTINUE indexing") === 0;
1462
+ var ref = parseIndexingRequestText(userText);
1463
+ if (ref) {
1342
1464
  displayContent = opts.formatIndexingLabel(
1343
- nameMatch[1].trim(),
1344
- mimeMatch ? mimeMatch[1].trim() : "",
1345
- sizeMatch ? Number(sizeMatch[1]) : null,
1346
- pathMatch ? pathMatch[1].trim() : void 0,
1465
+ ref.name,
1466
+ ref.mime || "",
1467
+ typeof ref.size === "number" ? ref.size : null,
1468
+ ref.path,
1347
1469
  false,
1348
- isContinuePass
1470
+ ref.continued
1349
1471
  );
1350
- indexFile = {
1351
- name: nameMatch[1].trim(),
1352
- path: pathMatch ? pathMatch[1].trim() : void 0,
1353
- mime: mimeMatch ? mimeMatch[1].trim() : void 0,
1354
- size: sizeMatch ? Number(sizeMatch[1]) : void 0,
1355
- continued: isContinuePass
1356
- };
1472
+ indexFile = ref;
1357
1473
  } else {
1358
1474
  displayContent = userText;
1359
1475
  }
@@ -1368,6 +1484,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1368
1484
  if (indexFile) userMsg._indexFile = indexFile;
1369
1485
  if (item._isOnBgQueue) userMsg._useBgQueue = true;
1370
1486
  if (serverItemId !== void 0) userMsg._serverItemId = serverItemId;
1487
+ if (userTs !== void 0) userMsg._ts = userTs;
1371
1488
  mapped.push(userMsg);
1372
1489
  }
1373
1490
  if (isCancelledItem) ; else if (isInProcess) {
@@ -1382,11 +1499,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1382
1499
  var em = { role: "assistant", content: getErrorMessage(response), isError: true };
1383
1500
  if (item._isBgTask) em.isBackgroundTask = true;
1384
1501
  if (serverItemId !== void 0) em._serverItemId = serverItemId;
1502
+ if (replyTs !== void 0) em._ts = replyTs;
1385
1503
  mapped.push(em);
1386
1504
  } else if (assistantText) {
1387
1505
  var okm = { role: "assistant", content: sanitizeAttachmentLinksForHistory(assistantText, opts.serviceId, true) };
1388
1506
  if (item._isBgTask) okm.isBackgroundTask = true;
1389
1507
  if (serverItemId !== void 0) okm._serverItemId = serverItemId;
1508
+ if (replyTs !== void 0) okm._ts = replyTs;
1390
1509
  mapped.push(okm);
1391
1510
  }
1392
1511
  });
@@ -1479,6 +1598,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1479
1598
  }
1480
1599
 
1481
1600
  // src/engine/session.ts
1601
+ var WORKER_PASS_ADOPT_LIMIT = 20;
1602
+ var WORKER_PASS_ADOPT_ATTEMPTS = [0, 2e3, 6e3];
1482
1603
  var _g = typeof globalThis !== "undefined" ? globalThis : {};
1483
1604
  function nowMs() {
1484
1605
  return _g.performance && typeof _g.performance.now === "function" ? _g.performance.now() : Date.now();
@@ -1498,6 +1619,35 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1498
1619
  var ChatSession = class {
1499
1620
  constructor(host) {
1500
1621
  this.typewriterQueue = Promise.resolve();
1622
+ /**
1623
+ * Pick up indexing passes the WORKER minted, which no client ever dispatched.
1624
+ *
1625
+ * For a PDF (and for text/grid when windowed indexing is on) the worker writes
1626
+ * pass N+1's row itself, inside pass N's invocation, right after saving pass
1627
+ * N's result. That row reaches this client through nothing at all: it is not in
1628
+ * bgTaskQueue (the client never asked for it) and it is not in state.messages
1629
+ * (only a first-page history load maps it in, which happens on mount, project
1630
+ * switch or tab return). So between two worker passes every pass the client
1631
+ * knows about is settled, and the collapsed row renders "Indexed N passes"
1632
+ * with no spinner and no Stop, for a file that is still being read. A user who
1633
+ * believes that then asks questions against a half-indexed file — which is what
1634
+ * this exists to prevent.
1635
+ *
1636
+ * So when a background indexing pass settles, ask the bg queue what is still
1637
+ * unresolved on it and adopt anything unknown as an ordinary BgTaskEntry.
1638
+ * drainBgTaskQueue then treats it exactly like a pass this client dispatched:
1639
+ * same bubble, same `_indexFile` (so it joins the file's collapsed row), same
1640
+ * poll — and that poll settling runs this again, so the chain is followed to
1641
+ * its end. `status`-scoped so the reply carries the live items only, never a
1642
+ * page of finished ones with their bodies.
1643
+ *
1644
+ * Termination: the only trigger is a pass SETTLING, which happens once per
1645
+ * pass. An adopted item that is still running is not re-adopted (its id is
1646
+ * already polled), and when the queue holds nothing unknown the chain stops on
1647
+ * its own. Nothing here is periodic — a timer that re-reads history is the
1648
+ * shape that previously looped fetchHistoryPage after an already-DONE index.
1649
+ */
1650
+ this._adoptingWorkerPasses = false;
1501
1651
  this.host = host;
1502
1652
  this.state = {
1503
1653
  messages: [],
@@ -1799,7 +1949,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1799
1949
  var offExisting = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
1800
1950
  this.aiChatHistoryCache[key] = {
1801
1951
  messages: offExisting.messages.concat([
1802
- { role: "user", content: composed, _ownerKey: key },
1952
+ { role: "user", content: composed, _ownerKey: key, _ts: wallClockNow() },
1803
1953
  { role: "assistant", content: "", isPending: true, isPendingInProcess: true, _ownerKey: key }
1804
1954
  ]),
1805
1955
  endOfList: offExisting.endOfList,
@@ -1831,7 +1981,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1831
1981
  serviceId: id.serviceId,
1832
1982
  history: resolvedHistory.concat([{ role: "user", content: llmComposed }])
1833
1983
  });
1834
- var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true };
1984
+ var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true, _ts: wallClockNow() };
1835
1985
  if (key) queuedBubble._ownerKey = key;
1836
1986
  if (useBgQueue) queuedBubble._useBgQueue = true;
1837
1987
  this.state.messages.push(queuedBubble);
@@ -1866,7 +2016,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1866
2016
  });
1867
2017
  return;
1868
2018
  }
1869
- this.state.messages.push({ role: "user", content: composed, ...key ? { _ownerKey: key } : {} });
2019
+ this.state.messages.push({ role: "user", content: composed, _ts: wallClockNow(), ...key ? { _ownerKey: key } : {} });
1870
2020
  this.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true, ...key ? { _ownerKey: key } : {} });
1871
2021
  this.host.notify();
1872
2022
  this.updateHistoryCache();
@@ -1923,6 +2073,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1923
2073
  var existing = this.state.messages[nextIdx];
1924
2074
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true, isBackgroundTask: true };
1925
2075
  if (existing._indexFile) promoted._indexFile = existing._indexFile;
2076
+ if (existing._ts !== void 0) promoted._ts = existing._ts;
1926
2077
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1927
2078
  if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1928
2079
  this.state.messages[nextIdx] = promoted;
@@ -1944,6 +2095,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1944
2095
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true };
1945
2096
  if (existing.isBackgroundTask) promoted.isBackgroundTask = true;
1946
2097
  if (existing._indexFile) promoted._indexFile = existing._indexFile;
2098
+ if (existing._ts !== void 0) promoted._ts = existing._ts;
1947
2099
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1948
2100
  if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1949
2101
  if (existing.isSendingToServer) promoted.isSendingToServer = true;
@@ -1993,6 +2145,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1993
2145
  var repl = { role: "user", content: exist.content };
1994
2146
  if (exist._serverItemId !== void 0) repl._serverItemId = exist._serverItemId;
1995
2147
  if (exist._ownerKey !== void 0) repl._ownerKey = exist._ownerKey;
2148
+ if (exist._ts !== void 0) repl._ts = exist._ts;
1996
2149
  this.state.messages[userIdx] = repl;
1997
2150
  }
1998
2151
  var thinkingIdx = userIdx >= 0 ? this.state.messages.findIndex(function(m, i) {
@@ -2001,6 +2154,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2001
2154
  return thinkingIdx !== -1 ? thinkingIdx : userIdx >= 0 ? userIdx + 1 : -1;
2002
2155
  }
2003
2156
  insertAtTarget(msg, targetIdx) {
2157
+ if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = wallClockNow();
2004
2158
  if (targetIdx >= 0 && this.state.messages[targetIdx] && this.state.messages[targetIdx].isPending) this.state.messages[targetIdx] = msg;
2005
2159
  else if (targetIdx >= 0) this.state.messages.splice(targetIdx, 0, msg);
2006
2160
  else this.state.messages.push(msg);
@@ -2337,6 +2491,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2337
2491
  }
2338
2492
  enqueueTypewrite(idx, fullText, localId) {
2339
2493
  var self = this;
2494
+ var target = this.state.messages[idx];
2495
+ if (target && target._ts === void 0) target._ts = wallClockNow();
2340
2496
  this.typewriterQueue = this.typewriterQueue.then(function() {
2341
2497
  return self.typewriteIntoIndex(idx, fullText, localId);
2342
2498
  });
@@ -2406,6 +2562,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2406
2562
  var u = this.state.messages[uIdx];
2407
2563
  var cleaned = { role: "user", content: u.content, _serverItemId: itemId };
2408
2564
  if (u.isBackgroundTask) cleaned.isBackgroundTask = true;
2565
+ if (u._ts !== void 0) cleaned._ts = u._ts;
2409
2566
  if (u._indexFile) cleaned._indexFile = u._indexFile;
2410
2567
  this.state.messages[uIdx] = cleaned;
2411
2568
  }
@@ -2436,8 +2593,21 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2436
2593
  }
2437
2594
  // --- background-task resolution + drain -------------------------------
2438
2595
  handleHistoryItemResolution(itemId, response, platform) {
2596
+ var indexRef = this._indexRefOfItem(itemId);
2439
2597
  this.applyHistoryItemResolution(itemId, response, platform);
2440
2598
  this.promoteNextBgQueuedToRunning();
2599
+ if (indexRef) this._followWorkerIndexingChain(indexRef.name, indexRef.mime);
2600
+ }
2601
+ /** The file an already-rendered background pass is about, off its request
2602
+ * bubble. Null for an ordinary turn, which is most of them. */
2603
+ _indexRefOfItem(itemId) {
2604
+ if (!itemId) return null;
2605
+ for (var i = 0; i < this.state.messages.length; i++) {
2606
+ var m = this.state.messages[i];
2607
+ if (m._serverItemId !== itemId || m.role !== "user" || !m.isBackgroundTask) continue;
2608
+ return m._indexFile || null;
2609
+ }
2610
+ return null;
2441
2611
  }
2442
2612
  applyHistoryItemResolution(itemId, response, platform) {
2443
2613
  this.historyItemPolls.delete(itemId);
@@ -2478,6 +2648,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2478
2648
  var ex = this.state.messages[userIdx];
2479
2649
  var settledUser = { role: "user", content: ex.content, _serverItemId: itemId };
2480
2650
  if (ex.isBackgroundTask) settledUser.isBackgroundTask = true;
2651
+ if (ex._ts !== void 0) settledUser._ts = ex._ts;
2481
2652
  if (ex._indexFile) settledUser._indexFile = ex._indexFile;
2482
2653
  if (ex._useBgQueue) settledUser._useBgQueue = true;
2483
2654
  this.state.messages[userIdx] = settledUser;
@@ -2570,6 +2741,116 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2570
2741
  self.cancelQueuedMessage(t.msg, idx === -1 ? t.idx : idx);
2571
2742
  });
2572
2743
  }
2744
+ /**
2745
+ * True when the WORKER, not this client, drives the rest of this file's chain.
2746
+ * The mirror image of the early returns in maybeResumeIndexing: whatever that
2747
+ * refuses to continue is exactly what nothing client-side is tracking.
2748
+ */
2749
+ _isWorkerDrivenIndexing(filename, mime) {
2750
+ if (!isPagedReadFile(filename, mime)) return false;
2751
+ if (isImageVisionFile(filename, mime)) return true;
2752
+ return windowedIndexingEnabled() && isWindowedReadFile(filename, mime);
2753
+ }
2754
+ _adoptWorkerIndexingPasses(attempt) {
2755
+ var self = this;
2756
+ if (this._adoptingWorkerPasses) return;
2757
+ var id = this.host.getIdentity();
2758
+ var platform = id.platform;
2759
+ if (!id.serviceId || platform !== "claude" && platform !== "openai") return;
2760
+ if (this.isPollingPaused() || !this.host.isViewMounted()) return;
2761
+ var svcId = id.serviceId, owner = id.owner;
2762
+ var queue = (id.userId || id.serviceId) + BG_INDEXING_QUEUE_SUFFIX;
2763
+ var ask = function(status) {
2764
+ return Promise.resolve(getChatHistory(
2765
+ { service: svcId, owner, platform, queue, status },
2766
+ { limit: WORKER_PASS_ADOPT_LIMIT }
2767
+ )).catch(function() {
2768
+ return null;
2769
+ });
2770
+ };
2771
+ this._adoptingWorkerPasses = true;
2772
+ Promise.all([ask("running"), ask("pending")]).then(function(results) {
2773
+ self._adoptingWorkerPasses = false;
2774
+ var now = self.host.getIdentity();
2775
+ if (now.serviceId !== svcId || now.platform !== platform) return;
2776
+ if (!self.host.isViewMounted()) return;
2777
+ var adoptedIds = [];
2778
+ for (var ri = 0; ri < results.length; ri++) {
2779
+ var list = results[ri] && Array.isArray(results[ri].list) ? results[ri].list : [];
2780
+ for (var i = 0; i < list.length; i++) {
2781
+ if (self._adoptWorkerIndexingItem(list[i], svcId, platform)) adoptedIds.push(list[i].id);
2782
+ }
2783
+ }
2784
+ if (adoptedIds.length) {
2785
+ self.drainBgTaskQueue();
2786
+ if (self._isTrackingAny(adoptedIds)) return;
2787
+ }
2788
+ if (attempt + 1 >= WORKER_PASS_ADOPT_ATTEMPTS.length) return;
2789
+ setTimeout(function() {
2790
+ var later = self.host.getIdentity();
2791
+ if (later.serviceId !== svcId || later.platform !== platform) return;
2792
+ if (self.isPollingPaused() || !self.host.isViewMounted()) return;
2793
+ self._adoptWorkerIndexingPasses(attempt + 1);
2794
+ }, WORKER_PASS_ADOPT_ATTEMPTS[attempt + 1]);
2795
+ }, function() {
2796
+ self._adoptingWorkerPasses = false;
2797
+ });
2798
+ }
2799
+ /** Any of these ids still queued or still polled, i.e. surviving work. */
2800
+ _isTrackingAny(ids) {
2801
+ for (var i = 0; i < ids.length; i++) {
2802
+ if (this.historyItemPolls.has(ids[i])) return true;
2803
+ for (var q = 0; q < this.bgTaskQueue.length; q++) {
2804
+ if (this.bgTaskQueue[q].id === ids[i]) return true;
2805
+ }
2806
+ }
2807
+ return false;
2808
+ }
2809
+ /** One live bg-queue item -> a BgTaskEntry, if it is an indexing pass this
2810
+ * client is not already tracking. Returns whether it was adopted. */
2811
+ _adoptWorkerIndexingItem(item, svcId, platform) {
2812
+ if (!item || typeof item.id !== "string" || !item.id) return false;
2813
+ if (item.status !== "pending" && item.status !== "running") return false;
2814
+ if (typeof item.poll !== "function") return false;
2815
+ if (this.historyItemPolls.has(item.id)) return false;
2816
+ for (var q = 0; q < this.bgTaskQueue.length; q++) {
2817
+ if (this.bgTaskQueue[q].id === item.id) return false;
2818
+ }
2819
+ for (var m = 0; m < this.state.messages.length; m++) {
2820
+ var msg = this.state.messages[m];
2821
+ if (msg._serverItemId !== item.id) continue;
2822
+ if (!(msg.isPending || msg.isPendingInProcess || msg.isPendingQueued)) return false;
2823
+ }
2824
+ var body = item.request_body;
2825
+ if (!body || typeof body !== "object") return false;
2826
+ if (platform === "claude" ? !Array.isArray(body.messages) : !Array.isArray(body.input)) return false;
2827
+ var userText = extractLastUserTextFromRequest(body);
2828
+ if (!isIndexingRequestText(userText)) return false;
2829
+ var ref = parseIndexingRequestText(userText);
2830
+ if (!ref || !ref.name) return false;
2831
+ if (!this._isWorkerDrivenIndexing(ref.name, ref.mime)) return false;
2832
+ this.bgTaskQueue.push({
2833
+ serviceId: svcId,
2834
+ platform,
2835
+ id: item.id,
2836
+ filename: ref.name,
2837
+ storagePath: ref.path,
2838
+ mime: ref.mime,
2839
+ size: ref.size,
2840
+ status: item.status === "running" ? "running" : "pending",
2841
+ poll: item.poll,
2842
+ // Drives the "(continuing)" label and marks this as a continuation for
2843
+ // _applyIndexCancellations, which must not read a CONTINUE pass as the
2844
+ // fresh first pass that lifts a stop.
2845
+ resumePass: ref.continued ? 1 : 0
2846
+ });
2847
+ return true;
2848
+ }
2849
+ /** Follow the chain on from a background indexing pass that just settled. */
2850
+ _followWorkerIndexingChain(filename, mime) {
2851
+ if (!this._isWorkerDrivenIndexing(filename, mime)) return;
2852
+ this._adoptWorkerIndexingPasses(0);
2853
+ }
2573
2854
  /** Best-effort server-side cancel of a bg-queue item that has no bubble (so
2574
2855
  * cancelQueuedMessage, which drives one, has nothing to act on). */
2575
2856
  _cancelServerItem(serverId) {
@@ -2780,8 +3061,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2780
3061
  var chatList = history && Array.isArray(history.list) ? history.list : [];
2781
3062
  chatList.forEach(function(item) {
2782
3063
  if (isBgIndexingQueue(item.queue_name)) {
2783
- var userText = extractLastUserTextFromRequest(item.request_body);
2784
- if (typeof userText === "string" && (userText.indexOf("A new file has just been uploaded") === 0 || userText.indexOf("CONTINUE indexing") === 0)) item._isBgTask = true;
3064
+ if (isIndexingRequestText(extractLastUserTextFromRequest(item.request_body))) item._isBgTask = true;
2785
3065
  else item._isOnBgQueue = true;
2786
3066
  }
2787
3067
  });
@@ -3254,7 +3534,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3254
3534
  cancellableIds: [],
3255
3535
  cancelling: false,
3256
3536
  mayHaveOlder: false,
3257
- anchorIndex: i
3537
+ // The run's first loaded pass, and never re-stamped: see the file
3538
+ // docstring. `anchorId` is filled in once every member is known.
3539
+ anchorIndex: i,
3540
+ anchorId: ""
3258
3541
  };
3259
3542
  order.push(runId);
3260
3543
  }
@@ -3268,7 +3551,6 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3268
3551
  g.passCount++;
3269
3552
  }
3270
3553
  g.members.push({ msg, index: i });
3271
- g.anchorIndex = i;
3272
3554
  runOfIndex[i] = runId;
3273
3555
  if (msg._serverItemId) runByItemId[msg._serverItemId] = runId;
3274
3556
  if (ref && ref.name) keyByName[ref.name] = g.key;
@@ -3330,6 +3612,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3330
3612
  }
3331
3613
  }
3332
3614
  grp.mayHaveOlder = !sawFirstPass && hasMoreHistory;
3615
+ var anchor = grp.members[0];
3616
+ grp.anchorIndex = anchor.index;
3617
+ grp.anchorId = anchor.msg._serverItemId || anchor.msg._localId || "";
3333
3618
  }
3334
3619
  var out = [];
3335
3620
  for (var j = 0; j < list.length; j++) {
@@ -3347,7 +3632,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3347
3632
  (function() {
3348
3633
  var MCP_PROD = "https://mcp.broadwayinc.computer";
3349
3634
  var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
3350
- var BQ_VERSION = "1.7.0" ;
3635
+ var BQ_VERSION = "1.8.1" ;
3351
3636
  var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
3352
3637
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
3353
3638
  var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
@@ -6170,12 +6455,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6170
6455
  });
6171
6456
  bubble.appendChild(cancelBtn);
6172
6457
  }
6173
- var md = h("div", { class: "bq-md", html: parseMsgPartsHtml(msg.content) });
6458
+ var md = h("div", { class: "bq-md", translate: "no", html: parseMsgPartsHtml(msg.content) });
6174
6459
  md.addEventListener("click", onBubbleLinkClick);
6175
6460
  bubble.appendChild(md);
6176
6461
  if (msg.isPendingQueued) bubble.appendChild(h("span", { class: "bq-pending-note", text: "(In queue)" }));
6177
6462
  if (msg.isCancelled) bubble.appendChild(h("span", { class: "bq-cancel-error", text: "(cancelled)" }));
6178
6463
  if (msg._cancelError) bubble.appendChild(h("span", { class: "bq-cancel-error", text: msg._cancelError }));
6464
+ var ts = formatChatTimestamp(msg._ts);
6465
+ if (ts) bubble.appendChild(h("time", { class: "bq-msg-time", text: ts }));
6179
6466
  }
6180
6467
  return h("div", { class: cls.join(" "), dataset: { msgIndex: String(idx) } }, bubble);
6181
6468
  }
@@ -6296,8 +6583,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6296
6583
  return id ? "s" + id + ":" + msg.role : "i" + index;
6297
6584
  }
6298
6585
  function indexGroupAnchorId(group) {
6299
- var last = group.members[group.members.length - 1];
6300
- return last && last.msg && last.msg._serverItemId || "";
6586
+ return group.anchorId || "";
6301
6587
  }
6302
6588
  function captureScrollAnchor() {
6303
6589
  var box = CS.messagesBox;
@@ -6648,7 +6934,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6648
6934
  if (S.aiPlatform === "none") return "No agent configured";
6649
6935
  return S.serviceName || "BunnyQuery";
6650
6936
  }
6651
- function parseAiAgentValue(value) {
6937
+ function parseAiAgentValue2(value) {
6652
6938
  var raw = (value || "").trim();
6653
6939
  var platform = raw, model = "";
6654
6940
  if (raw.indexOf("#") !== -1) {
@@ -6662,9 +6948,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6662
6948
  function applyAgentConfig() {
6663
6949
  var conn = S.service || {};
6664
6950
  var raw = conn.ai_agent || "";
6665
- var parsed = parseAiAgentValue(raw);
6951
+ var parsed = parseAiAgentValue2(raw);
6666
6952
  S.aiPlatform = parsed.platform;
6667
6953
  S.aiModel = parsed.model;
6954
+ setProjectContextWindow(S.serviceId, parseAiAgentValue(raw).contextWindow);
6668
6955
  S.serviceName = conn.service_name || "";
6669
6956
  S.serviceDescription = conn.service_description || "";
6670
6957
  }
@@ -6806,6 +7093,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6806
7093
  },
6807
7094
  mcpBaseUrl: mcpBaseUrl(),
6808
7095
  poll: 0,
7096
+ // Server-driven windowed indexing. Off by default in the engine because the
7097
+ // worker must strip `_skapi_window` first, or the provider rejects the whole
7098
+ // call with no retry. `apply_file_windows` is deployed in every region
7099
+ // (verified 2026-07-27 against the deployed ClientSecretKeyPollingWorker in
7100
+ // all 7), so the widget now opts in like agent.vue does. Without it the
7101
+ // widget fell back to the client-driven CONTINUE loop capped at
7102
+ // MAX_INDEXING_RESUME_PASSES, which needs the tab kept open and stops early
7103
+ // on a big file. Pass windowedIndexing: false in init opts to opt back out.
7104
+ windowedIndexing: S.opts.windowedIndexing !== false,
6809
7105
  // Client-side attachment parsers (e.g. an .hwp parser) passed via init opts.
6810
7106
  attachmentParsers: S.opts.attachmentParsers || void 0
6811
7107
  });