bunnyquery 1.8.6 → 1.8.9

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
@@ -211,7 +211,7 @@
211
211
  if (isImageVisionFile(name, mime)) return false;
212
212
  return isPagedReadFile(name, mime);
213
213
  }
214
- function composeUserMessage(text, attachmentUrls) {
214
+ function composeUserMessage(text, attachmentUrls, opts) {
215
215
  let composed = text;
216
216
  let composedForLlm = composed;
217
217
  if (attachmentUrls.length > 0) {
@@ -225,7 +225,7 @@ ${lines.join("\n")}`;
225
225
  let extractContent;
226
226
  let fileUrls;
227
227
  if (attachmentUrls.length > 0) {
228
- const extractFiles = attachmentUrls.filter((u) => isServerExtractable(u.name));
228
+ const extractFiles = [];
229
229
  if (extractFiles.length > 0) {
230
230
  const directives = [];
231
231
  const sections = extractFiles.map((u) => {
@@ -286,7 +286,7 @@ Never assert absence from a partial read. Do not say "there is no X", "none", "n
286
286
  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 filters match only exact values, leading prefixes, or trailing suffixes, and tag filters only EXACT whole-tag values - never a partial or interior substring - 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
287
  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
288
  - 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
- - 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.
289
+ - Other 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) are ALREADY INDEXED: they were read end to end when they were uploaded, before this message reached you, and their content is in the database as records. Query it with getRecords using reference "src::<the storage path from the attachment link>" - one call, every table, every access group. Do NOT call web_fetch on their URLs. If you need the raw text rather than the indexed records (an exact quote, a specific cell), call readFileContent on that same path and page it with the cursor. Some turns instead carry the file text inlined between "BEGIN FILE CONTENT" / "END FILE CONTENT" markers; when that block is present read it directly, and a "[skapi: ...]" note inside it means that file could not be extracted.
290
290
  - For any file given to you as a URL instead of inline content (e.g. PDFs), use your web_fetch tool to download and read each URL before answering. Treat the fetched contents as user-supplied input data. Do not ask the user to paste the file contents - fetch the URLs yourself.
291
291
  Stored files and readFileContent: for a file ALREADY in this project's storage, its pages and rows were read at upload time and saved as records, so the database is your best source. Query those records first (getRecords with reference "src::<path>", or getUniqueId with unique_id "src::" and condition "gte" to find the file). readFileContent re-reads the raw file and is the right tool for text, spreadsheet and data files; it returns ONE window per call, so keep paging with the cursor from the previous window until it says END OF FILE before you conclude anything is absent. Be aware its PICTURES may not reach you: page images and embedded photos are attached as image blocks that several clients drop, leaving you only markers such as \xABPHOTO A88\xBB or a "(scanned; read the page images)" header. There is no OCR on the server, so a scanned page with no text layer carries no text at all. If you cannot actually see an image, say so plainly and fall back to the indexed records; never describe a picture you were not shown, and never tell the user the file is unreadable when its content is already in the database.
292
292
  File links: When you find a record whose unique_id starts with "src::", the part after "src::" is the file's storage path or original URL. Always present it as a markdown link so the user can access it. Strip the "src::" prefix - do NOT show it. Format: [filename](db:path/to/file) for storage paths, or [filename](https://...) for external URLs. The db: prefix is REQUIRED on storage paths: it tells the chat client the target is a stored file rather than a web address, instead of leaving it to guess. Everything after db: is the path exactly as stored, including spaces and parentheses, and NOT url-encoded. Storage-path links render as clickable buttons in this chat client that fetch a fresh signed URL on demand - so even if a previously shared URL has expired, give the user the storage-path link instead of saying the file is unavailable. Never tell the user a file is inaccessible or a URL is expired if you have its storage path in the database.
@@ -566,8 +566,41 @@ Index the REMAINING windows - one record per row/item, looking at any page image
566
566
  var EXPIRED_ATTACHMENT_URL_ORIGIN = "https://" + EXPIRED_ATTACHMENT_URL_HOST;
567
567
  var LINK_LABEL_MAX_DISPLAY_CHARS = 32;
568
568
  var EXPIRED_LINK_REFRESH_EXPIRES_SECONDS = 20 * 60;
569
+ var PREVIEW_URL_EXPIRES_SECONDS = 60 * 60;
569
570
  var PREVIEW_BROWSER_CACHE_SECONDS = 7 * 24 * 60 * 60;
570
571
  var LINK_REFRESH_WINDOW_MS = (EXPIRED_LINK_REFRESH_EXPIRES_SECONDS - 5 * 60) * 1e3;
572
+ var MINT_CACHE_GENERATION = 2;
573
+ function mintCacheBustStamp(now) {
574
+ return Math.floor((Date.now() ) / LINK_REFRESH_WINDOW_MS);
575
+ }
576
+ function previewMintCacheToken(refresh) {
577
+ if (!refresh) return String(MINT_CACHE_GENERATION);
578
+ return MINT_CACHE_GENERATION + "." + mintCacheBustStamp();
579
+ }
580
+ var PRESIGN_SAFETY_MARGIN_MS = 60 * 1e3;
581
+ function presignExpiryEpochMs(url) {
582
+ if (!url) return null;
583
+ var q = url.indexOf("?");
584
+ if (q < 0) return null;
585
+ var params;
586
+ try {
587
+ params = new URLSearchParams(url.slice(q + 1));
588
+ } catch (e) {
589
+ return null;
590
+ }
591
+ var v2 = params.get("Expires");
592
+ if (v2 && /^\d+$/.test(v2)) return parseInt(v2, 10) * 1e3;
593
+ var signed = params.get("X-Amz-Date");
594
+ var lifetime = params.get("X-Amz-Expires");
595
+ if (signed && lifetime && /^\d+$/.test(lifetime)) {
596
+ var m = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(signed);
597
+ if (m) {
598
+ var at = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]);
599
+ return at + parseInt(lifetime, 10) * 1e3;
600
+ }
601
+ }
602
+ return null;
603
+ }
571
604
  function createInlineLinkRegex() {
572
605
  return /src::(\S+)|\[([^\]\n]+)\]\((https?:\/\/(?:[^\s()]|\([^\s()]*\))+)\)|\[([^\]\n]+)\]\(((?:[^()\n]|\([^()\n]*\))+)\)|(https?:\/\/[^\s<>"']+)/g;
573
606
  }
@@ -812,6 +845,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
812
845
  function linkUnavailableKeyForHref(href) {
813
846
  return "href:" + (href || "");
814
847
  }
848
+ function linkUnavailableKeysForPath(remotePath) {
849
+ if (!remotePath) return [];
850
+ return [
851
+ linkUnavailableKeyForPath(remotePath),
852
+ linkUnavailableKeyForHref(buildDisplayExpiredAttachmentHref(remotePath))
853
+ ];
854
+ }
815
855
  function isLinkUnavailable(link, map) {
816
856
  if (!link || !map) return false;
817
857
  if (link.remotePath && map[linkUnavailableKeyForPath(link.remotePath)]) return true;
@@ -828,24 +868,71 @@ Index the REMAINING windows - one record per row/item, looking at any page image
828
868
  // src/engine/budget.ts
829
869
  var CONTEXT_WINDOW_DEFAULT = { claude: 2e5, openai: 128e3 };
830
870
  var CONTEXT_WINDOW_BY_MODEL = {
831
- // exact ids
871
+ // claude, exact ids
872
+ "claude-fable-5": 1e6,
832
873
  "claude-opus-5": 1e6,
833
874
  "claude-opus-4-8": 1e6,
834
875
  "claude-opus-4-7": 1e6,
876
+ "claude-opus-4-6": 1e6,
877
+ "claude-opus-4-5": 2e5,
835
878
  "claude-sonnet-5": 1e6,
836
879
  "claude-sonnet-4-6": 1e6,
880
+ "claude-sonnet-4-5": 1e6,
837
881
  "claude-sonnet-4": 2e5,
838
882
  "claude-haiku-4-5": 2e5,
839
- "gpt-5.4": 128e3,
840
- "gpt-5.6-luna": 128e3,
883
+ "claude-3-5-sonnet": 2e5,
884
+ // openai, exact ids
885
+ "gpt-5.6-sol": 105e4,
886
+ "gpt-5.6-terra": 105e4,
887
+ "gpt-5.6-luna": 105e4,
888
+ "gpt-5.5": 1e6,
889
+ "gpt-5.4": 105e4,
890
+ "gpt-5.4-mini": 4e5,
891
+ "gpt-5.4-nano": 4e5,
892
+ "gpt-4.1": 104e4,
893
+ "gpt-4o": 128e3,
894
+ "o1": 2e5,
895
+ "o1-pro": 2e5,
841
896
  // family keys
897
+ "claude-fable": 1e6,
842
898
  "claude-opus": 1e6,
843
899
  "claude-sonnet": 1e6,
844
900
  "claude-haiku": 2e5,
901
+ "gpt-5.6": 105e4,
902
+ "gpt-5": 128e3
903
+ };
904
+ var MAX_OUTPUT_BY_MODEL = {
905
+ // claude
906
+ "claude-fable-5": 128e3,
907
+ "claude-opus-5": 128e3,
908
+ "claude-opus-4-8": 128e3,
909
+ "claude-sonnet-5": 128e3,
910
+ "claude-sonnet-4-6": 64e3,
911
+ "claude-haiku-4-5": 64e3,
912
+ "claude-3-5-sonnet": 8e3,
913
+ // openai
914
+ "gpt-5.6-sol": 128e3,
915
+ "gpt-5.6-terra": 128e3,
916
+ "gpt-5.6-luna": 128e3,
917
+ "gpt-5.5": 128e3,
918
+ "gpt-5.4": 128e3,
919
+ "gpt-5.4-mini": 128e3,
920
+ "gpt-5.4-nano": 128e3,
921
+ "gpt-4.1": 16e3,
922
+ "gpt-4o": 4e3,
923
+ "o1": 1e5,
924
+ "o1-pro": 1e5,
925
+ // family keys
926
+ "claude-fable": 128e3,
927
+ "claude-opus": 128e3,
928
+ "claude-sonnet": 64e3,
929
+ "claude-haiku": 64e3,
845
930
  "gpt-5.6": 128e3,
846
931
  "gpt-5": 128e3
847
932
  };
933
+ var DEFAULT_CONTEXT_WINDOW = 88e4;
848
934
  var apiReportedContextWindows = {};
935
+ var apiReportedMaxOutput = {};
849
936
  var projectContextWindows = {};
850
937
  function setProjectContextWindow(projectId, tokens) {
851
938
  var key = (projectId || "").trim();
@@ -858,13 +945,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
858
945
  var key = (projectId || "").trim();
859
946
  return key && projectContextWindows[key] ? projectContextWindows[key] : null;
860
947
  }
861
- var OUTPUT_TOKEN_RESERVE = 22e3;
948
+ var MAX_OUTPUT_TOKENS = 25e3;
862
949
  var TOOL_AND_RESPONSE_BUFFER = 4e3;
863
950
  var MIN_INPUT_TOKEN_BUDGET = 8e3;
864
- var CLAUDE_PER_REQUEST_INPUT_CAP = 28e3;
951
+ var MIN_PER_REQUEST_INPUT_CAP = 28e3;
865
952
  var MAX_HISTORY_MESSAGES = 20;
866
953
  var HISTORY_TOKEN_BUDGET = 8e3;
867
- var CLAUDE_INPUT_CAP_RATIO = 0.16;
954
+ var INPUT_CAP_RATIO = 0.16;
868
955
  var HISTORY_BUDGET_RATIO = 0.08;
869
956
  function estimateTextTokens(text) {
870
957
  return Math.ceil((text || "").length / 3);
@@ -872,38 +959,61 @@ Index the REMAINING windows - one record per row/item, looking at any page image
872
959
  function estimateMessageTokens(msg) {
873
960
  return estimateTextTokens(msg.content) + estimateTextTokens(msg.role) + 6;
874
961
  }
962
+ function resolveByModelId(apiTable, staticTable, model) {
963
+ var normalized = (model || "").trim().toLowerCase();
964
+ if (!normalized) return 0;
965
+ if (apiTable[normalized]) return apiTable[normalized];
966
+ if (staticTable[normalized]) return staticTable[normalized];
967
+ var parts = normalized.split("-");
968
+ for (var end = parts.length - 1; end > 0; end--) {
969
+ var family = parts.slice(0, end).join("-");
970
+ if (staticTable[family]) return staticTable[family];
971
+ }
972
+ return 0;
973
+ }
974
+ function getModelContextWindow(platform, model) {
975
+ return resolveByModelId(apiReportedContextWindows, CONTEXT_WINDOW_BY_MODEL, model) || CONTEXT_WINDOW_DEFAULT[platform];
976
+ }
977
+ function getMaxOutputTokens(platform, model) {
978
+ var cap = resolveByModelId(apiReportedMaxOutput, MAX_OUTPUT_BY_MODEL, model);
979
+ return cap ? Math.min(MAX_OUTPUT_TOKENS, cap) : MAX_OUTPUT_TOKENS;
980
+ }
875
981
  function getContextWindow(platform, model, projectId) {
982
+ var ceiling = getModelContextWindow(platform, model);
876
983
  var override = projectId ? getProjectContextWindow(projectId) : null;
877
- if (override) return override;
878
- var normalized = (model || "").trim().toLowerCase();
879
- if (normalized) {
880
- if (apiReportedContextWindows[normalized]) return apiReportedContextWindows[normalized];
881
- if (CONTEXT_WINDOW_BY_MODEL[normalized]) return CONTEXT_WINDOW_BY_MODEL[normalized];
882
- var parts = normalized.split("-");
883
- for (var end = parts.length - 1; end > 0; end--) {
884
- var family = parts.slice(0, end).join("-");
885
- if (CONTEXT_WINDOW_BY_MODEL[family]) return CONTEXT_WINDOW_BY_MODEL[family];
886
- }
887
- }
888
- return CONTEXT_WINDOW_DEFAULT[platform];
984
+ return Math.min(override || DEFAULT_CONTEXT_WINDOW, ceiling);
985
+ }
986
+ function contextBasedBudgetFor(platform, model, projectId) {
987
+ var contextWindow = getContextWindow(platform, model, projectId);
988
+ return Math.max(
989
+ MIN_INPUT_TOKEN_BUDGET,
990
+ contextWindow - getMaxOutputTokens(platform, model) - TOOL_AND_RESPONSE_BUFFER
991
+ );
992
+ }
993
+ function getInputTokenBudget(platform, model, projectId) {
994
+ var contextBasedBudget = contextBasedBudgetFor(platform, model, projectId);
995
+ return Math.min(
996
+ contextBasedBudget,
997
+ Math.max(MIN_PER_REQUEST_INPUT_CAP, Math.round(contextBasedBudget * INPUT_CAP_RATIO))
998
+ );
889
999
  }
890
1000
  function stripFileBlocksFromHistory(content) {
891
1001
  if (!content) return content;
892
1002
  return content.replace(/```([^\n`]+?\.[^\s.`]+)\n[\s\S]*?```/g, "[file previously attached: $1]");
893
1003
  }
894
1004
  function buildBoundedChatMessages(options) {
895
- var contextWindow = getContextWindow(options.platform, options.model, options.projectId);
896
- var contextBasedBudget = Math.max(
897
- MIN_INPUT_TOKEN_BUDGET,
898
- contextWindow - OUTPUT_TOKEN_RESERVE - TOOL_AND_RESPONSE_BUFFER
899
- );
900
- var scaled = !!(options.projectId && getProjectContextWindow(options.projectId));
901
- var claudeInputCap = scaled ? Math.max(CLAUDE_PER_REQUEST_INPUT_CAP, Math.round(contextBasedBudget * CLAUDE_INPUT_CAP_RATIO)) : CLAUDE_PER_REQUEST_INPUT_CAP;
902
- var availableInputBudget = options.platform === "claude" ? Math.min(contextBasedBudget, claudeInputCap) : contextBasedBudget;
1005
+ var contextBasedBudget = contextBasedBudgetFor(options.platform, options.model, options.projectId);
1006
+ var availableInputBudget = getInputTokenBudget(options.platform, options.model, options.projectId);
903
1007
  var systemCost = estimateTextTokens(options.systemPrompt) + 12;
904
- var historyAllowance = scaled ? Math.max(HISTORY_TOKEN_BUDGET, Math.round(contextBasedBudget * HISTORY_BUDGET_RATIO)) : HISTORY_TOKEN_BUDGET;
1008
+ var historyAllowance = Math.max(
1009
+ HISTORY_TOKEN_BUDGET,
1010
+ Math.round(contextBasedBudget * HISTORY_BUDGET_RATIO)
1011
+ );
905
1012
  var budgetForHistory = Math.max(1e3, Math.min(historyAllowance, availableInputBudget - systemCost));
906
- var maxHistoryMessages = scaled ? Math.max(MAX_HISTORY_MESSAGES, Math.round(MAX_HISTORY_MESSAGES * (budgetForHistory / HISTORY_TOKEN_BUDGET))) : MAX_HISTORY_MESSAGES;
1013
+ var maxHistoryMessages = Math.max(
1014
+ MAX_HISTORY_MESSAGES,
1015
+ Math.round(MAX_HISTORY_MESSAGES * (budgetForHistory / HISTORY_TOKEN_BUDGET))
1016
+ );
907
1017
  var windowed = options.history.slice(-maxHistoryMessages);
908
1018
  var latestIndex = windowed.length - 1;
909
1019
  var trimmed = windowed.map(function(m, i2) {
@@ -1136,8 +1246,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1136
1246
  }
1137
1247
  function peekImagePreviewUrl(ctx, remotePath) {
1138
1248
  var hit = previewUrlCache[cacheKey(ctx.scope, remotePath)];
1139
- if (hit && Date.now() - hit.at < LINK_REFRESH_WINDOW_MS) return hit.url;
1140
- return null;
1249
+ if (!hit) return null;
1250
+ if (Date.now() - hit.at >= LINK_REFRESH_WINDOW_MS) return null;
1251
+ var dies = presignExpiryEpochMs(hit.url);
1252
+ if (dies !== null && Date.now() >= dies - PRESIGN_SAFETY_MARGIN_MS) return null;
1253
+ return hit.url;
1141
1254
  }
1142
1255
  function resolveImagePreviewUrl(ctx, remotePath, contentType, refresh) {
1143
1256
  var key = cacheKey(ctx.scope, remotePath);
@@ -1187,6 +1300,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1187
1300
  img.setAttribute("data-bq-img-state", "loading");
1188
1301
  img.addEventListener("load", function() {
1189
1302
  img.setAttribute("data-bq-img-state", "ready");
1303
+ img.removeAttribute("data-bq-img-retry");
1190
1304
  if (ctx.onLoad) ctx.onLoad(path);
1191
1305
  });
1192
1306
  img.addEventListener("error", function() {
@@ -1276,7 +1390,6 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1276
1390
  var WEB_FETCH_MAX_USES = 40;
1277
1391
  var WEB_FETCH_MAX_CONTENT_TOKENS = 2e5;
1278
1392
  var OPENAI_RESPONSES_API_URL = "https://api.openai.com/v1/responses";
1279
- var MAX_TOKENS = 25e3;
1280
1393
  var DEFAULT_OPENAI_IMAGE_DETAIL = "auto";
1281
1394
  var OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS = true;
1282
1395
  var MCP_NAME = "BunnyQuery";
@@ -1516,7 +1629,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1516
1629
  owner,
1517
1630
  userId,
1518
1631
  model: model || DEFAULT_CLAUDE_MODEL,
1519
- maxTokens: MAX_TOKENS,
1632
+ maxTokens: getMaxOutputTokens("claude", model || DEFAULT_CLAUDE_MODEL),
1520
1633
  system,
1521
1634
  extractContent,
1522
1635
  fileUrls,
@@ -1561,7 +1674,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1561
1674
  },
1562
1675
  data: {
1563
1676
  model: resolvedModel,
1564
- max_output_tokens: MAX_TOKENS,
1677
+ max_output_tokens: getMaxOutputTokens("openai", resolvedModel),
1565
1678
  ...extractContent && extractContent.length ? { _skapi_extract: extractContent } : {},
1566
1679
  ...fileUrls && fileUrls.length ? { _skapi_file_urls: fileUrls } : {},
1567
1680
  input: responseInput,
@@ -1591,6 +1704,29 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1591
1704
  async function notifyAgentSaveAttachment(info) {
1592
1705
  const { platform, service, owner, attachment, parsedContent } = info;
1593
1706
  const continuing = !!info.continueIndexing;
1707
+ if (!continuing) {
1708
+ upsertIndexRunRecordSafe(service, attachment.storagePath, {
1709
+ status: "working",
1710
+ filename: attachment.name,
1711
+ started: Date.now(),
1712
+ queue: bgIndexingQueueName(info.userId, service),
1713
+ platform
1714
+ });
1715
+ }
1716
+ const tapDispatchFailure = (p) => {
1717
+ if (continuing) return p;
1718
+ return p.then(
1719
+ (ack) => ack,
1720
+ (err) => {
1721
+ upsertIndexRunRecordSafe(service, attachment.storagePath, {
1722
+ status: "error",
1723
+ finished: Date.now(),
1724
+ error: err && (err.message || String(err)) || "The indexing request could not be enqueued."
1725
+ });
1726
+ throw err;
1727
+ }
1728
+ );
1729
+ };
1594
1730
  const visionFile = !parsedContent && isImageVisionFile(attachment.name, attachment.mime);
1595
1731
  const renderFrom = Math.max(0, info.renderFrom || 0);
1596
1732
  const renderPlaceholder = visionFile ? makeRenderPlaceholder(attachment.storagePath) : void 0;
@@ -1650,6 +1786,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1650
1786
  save_media: !continuing
1651
1787
  }))
1652
1788
  } : {};
1789
+ const skapiFileUrls = attachment.url && attachment.storagePath ? { _skapi_file_urls: [{ path: attachment.storagePath, url: attachment.url }] } : {};
1653
1790
  const userMessage = visionFile && renderPlaceholder ? buildIndexingRenderMessage(attachment, renderPlaceholder, renderFrom) : windowedRead && windowPlaceholder ? buildIndexingWindowMessage(attachment, windowPlaceholder, false) : continuing ? buildIndexingContinueMessage(attachment) : buildIndexingUserMessage(
1654
1791
  attachment,
1655
1792
  parsedContent ? { inlineContent: parsedContent } : placeholder ? { inlineContentPlaceholder: placeholder } : pagedRead ? { pagedRead: true } : void 0
@@ -1665,7 +1802,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1665
1802
  if (platform === "openai") {
1666
1803
  const resolvedModel2 = info.model || DEFAULT_OPENAI_MODEL;
1667
1804
  const imageDetail = getOpenAIImageDetail(resolvedModel2);
1668
- return clientSecretRequest({
1805
+ return tapDispatchFailure(clientSecretRequest({
1669
1806
  clientSecretName: "openai",
1670
1807
  queue: bgIndexingQueueName(info.userId, service),
1671
1808
  service,
@@ -1679,12 +1816,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1679
1816
  },
1680
1817
  data: {
1681
1818
  model: resolvedModel2,
1682
- max_output_tokens: MAX_TOKENS,
1819
+ max_output_tokens: getMaxOutputTokens("openai", resolvedModel2),
1683
1820
  // Nano-only transcription knobs. Indexing only; see variantIndexingOptions.
1684
1821
  ...variantIndexingOptions(resolvedModel2),
1685
1822
  ...skapiExtract,
1686
1823
  ...skapiRender,
1687
1824
  ...skapiWindow,
1825
+ ...skapiFileUrls,
1688
1826
  input: [
1689
1827
  { role: "system", content: systemPrompt },
1690
1828
  {
@@ -1708,10 +1846,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1708
1846
  ]
1709
1847
  ]
1710
1848
  }
1711
- });
1849
+ }));
1712
1850
  }
1713
1851
  const resolvedModel = info.model || DEFAULT_CLAUDE_MODEL;
1714
- return clientSecretRequest({
1852
+ return tapDispatchFailure(clientSecretRequest({
1715
1853
  clientSecretName: "claude",
1716
1854
  queue: bgIndexingQueueName(info.userId, service),
1717
1855
  service,
@@ -1727,10 +1865,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1727
1865
  },
1728
1866
  data: {
1729
1867
  model: resolvedModel,
1730
- max_tokens: MAX_TOKENS,
1868
+ max_tokens: getMaxOutputTokens("claude", resolvedModel),
1731
1869
  ...skapiExtract,
1732
1870
  ...skapiRender,
1733
1871
  ...skapiWindow,
1872
+ ...skapiFileUrls,
1734
1873
  system: [
1735
1874
  {
1736
1875
  type: "text",
@@ -1766,7 +1905,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1766
1905
  }
1767
1906
  ]
1768
1907
  }
1769
- });
1908
+ }));
1770
1909
  }
1771
1910
  function extractClaudeText(response) {
1772
1911
  if (!Array.isArray(response?.content)) {
@@ -1802,6 +1941,21 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1802
1941
  return "";
1803
1942
  }
1804
1943
  var BG_INDEXING_QUEUE_SUFFIX = "-bg";
1944
+ function indexDoneUniqueId(storagePath) {
1945
+ return "done::" + storagePath;
1946
+ }
1947
+ function runIndexUniqueId(storagePath) {
1948
+ return "run::" + storagePath;
1949
+ }
1950
+ function upsertIndexRunRecordSafe(service, storagePath, patch) {
1951
+ if (!service || !storagePath) return;
1952
+ try {
1953
+ const hook = chatEngineConfig().upsertIndexRunRecord;
1954
+ if (typeof hook !== "function") return;
1955
+ hook({ service, storagePath, patch });
1956
+ } catch (e) {
1957
+ }
1958
+ }
1805
1959
  function bgIndexingQueueName(userId, service) {
1806
1960
  return (userId || service || "") + BG_INDEXING_QUEUE_SUFFIX;
1807
1961
  }
@@ -1825,13 +1979,20 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1825
1979
  },
1826
1980
  { service: params.service, owner: params.owner },
1827
1981
  params.queue ? { queue: params.queue } : {},
1828
- params.status ? { status: params.status } : {}
1982
+ params.status ? { status: params.status } : {},
1983
+ params.queue_exact ? { queue_exact: true } : {},
1984
+ params.compact ? { compact: true } : {},
1985
+ params.queue_exclude ? { queue_exclude: params.queue_exclude } : {}
1829
1986
  );
1830
1987
  return chatEngineConfig().clientSecretRequestHistory(
1831
1988
  p,
1832
1989
  Object.assign({ ascending: false, limit: CHAT_HISTORY_PAGE_LIMIT }, fetchOptions)
1833
1990
  );
1834
1991
  }
1992
+ function buildHistoryItemFullId(platform, service, itemId) {
1993
+ const url = platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
1994
+ return `[POST]${url.toLowerCase()}#${service}:${itemId}`;
1995
+ }
1835
1996
 
1836
1997
  // src/engine/history.ts
1837
1998
  function filterListByClearHorizon(list, clearedAt) {
@@ -1881,6 +2042,244 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1881
2042
  continued: userText.indexOf("CONTINUE indexing") === 0
1882
2043
  };
1883
2044
  }
2045
+ var BG_PROBE_TTL_MS = 4e3;
2046
+ var bgProbeCache = {};
2047
+ var bgProbeInflight = {};
2048
+ function probeBgQueue(params, opts) {
2049
+ const key = [params.service, params.owner, params.platform, params.queue, params.status, params.limit].join("|");
2050
+ const maxAge = opts && typeof opts.maxAgeMs === "number" ? opts.maxAgeMs : 0;
2051
+ const cached = bgProbeCache[key];
2052
+ if (maxAge > 0 && cached && Date.now() - cached.at < maxAge) {
2053
+ return Promise.resolve(cached);
2054
+ }
2055
+ const inflight = bgProbeInflight[key];
2056
+ if (inflight) return inflight;
2057
+ const p = Promise.resolve(getChatHistory(
2058
+ { service: params.service, owner: params.owner, platform: params.platform, queue: params.queue, status: params.status },
2059
+ { limit: params.limit, fetchMore: false }
2060
+ )).then(function(result) {
2061
+ const entry = { result, at: Date.now() };
2062
+ bgProbeCache[key] = entry;
2063
+ return entry;
2064
+ });
2065
+ bgProbeInflight[key] = p;
2066
+ p.then(function() {
2067
+ delete bgProbeInflight[key];
2068
+ }, function() {
2069
+ delete bgProbeInflight[key];
2070
+ });
2071
+ return p;
2072
+ }
2073
+ var BG_COVERAGE_MAX_PAGES = 2;
2074
+ var splitHistoryStates = {};
2075
+ var splitHistoryLocks = {};
2076
+ function freshSplitState() {
2077
+ return { bgBuffer: [], bgEnd: false, bgStarted: false, surfaceEnd: false, pendingSurface: null, surfaceCarry: [], lastSurfaceKeys: [], newestBgId: "" };
2078
+ }
2079
+ function noteBgIds(state, list) {
2080
+ for (const it of list) {
2081
+ const id = it && typeof it.id === "string" ? it.id : "";
2082
+ if (id && id > state.newestBgId) state.newestBgId = id;
2083
+ }
2084
+ }
2085
+ var createdOf = (it) => {
2086
+ const c = Number(it && it.created);
2087
+ return isFinite(c) && c > 0 ? c : NaN;
2088
+ };
2089
+ var oldestCreated = (lst) => {
2090
+ let m = Infinity;
2091
+ for (const it of lst) {
2092
+ const c = createdOf(it);
2093
+ if (!isNaN(c) && c < m) m = c;
2094
+ }
2095
+ return m;
2096
+ };
2097
+ var SURFACE_EMPTY_MAX_PAGES = 10;
2098
+ async function getSplitChatHistory(params, fetchOptions, _fetchImpl) {
2099
+ const key = [params.service, params.owner, params.platform, params.userId || ""].join("|");
2100
+ const prev = splitHistoryLocks[key] || Promise.resolve();
2101
+ let releaseLock;
2102
+ const lockTail = new Promise((r) => {
2103
+ releaseLock = r;
2104
+ });
2105
+ const run = () => _getSplitChatHistoryLocked(key, params, fetchOptions, releaseLock);
2106
+ const p = prev.then(run, run);
2107
+ p.then((res) => {
2108
+ if (!res || !res.bgPending) releaseLock();
2109
+ }, () => releaseLock());
2110
+ splitHistoryLocks[key] = p.then(() => lockTail, () => lockTail);
2111
+ return p;
2112
+ }
2113
+ async function _getSplitChatHistoryLocked(key, params, fetchOptions, releaseLock, _fetchImpl) {
2114
+ const fetch2 = getChatHistory;
2115
+ const bgQueue = bgIndexingQueueName(params.userId, params.service);
2116
+ const base = { service: params.service, owner: params.owner, platform: params.platform };
2117
+ const fetchMore = !!(fetchOptions && fetchOptions.fetchMore);
2118
+ const limit = fetchOptions && fetchOptions.limit;
2119
+ const firstLoad = !splitHistoryStates[key];
2120
+ let headRefresh = false;
2121
+ if (!splitHistoryStates[key]) {
2122
+ splitHistoryStates[key] = freshSplitState();
2123
+ } else if (!fetchMore) {
2124
+ const prev = splitHistoryStates[key];
2125
+ if (prev.surfaceEnd && prev.bgEnd) {
2126
+ headRefresh = true;
2127
+ prev.pendingSurface = null;
2128
+ prev.surfaceCarry = [];
2129
+ prev.bgBuffer = [];
2130
+ } else {
2131
+ splitHistoryStates[key] = freshSplitState();
2132
+ }
2133
+ }
2134
+ const state = splitHistoryStates[key];
2135
+ if (state.pendingSurface && state.pendingSurface.forFetchMore !== fetchMore) {
2136
+ state.pendingSurface = null;
2137
+ }
2138
+ if (!state.pendingSurface) {
2139
+ if (state.surfaceEnd && !headRefresh) {
2140
+ state.pendingSurface = { list: [], endOfList: true, startKeyHistory: state.lastSurfaceKeys, forFetchMore: fetchMore };
2141
+ } else {
2142
+ const sOpts = { fetchMore };
2143
+ if (limit) sOpts.limit = limit;
2144
+ let s = await fetch2({ ...base, queue_exclude: bgQueue }, sOpts);
2145
+ let hops = 0;
2146
+ while (s && !s.endOfList && !(s.list || []).length && hops < SURFACE_EMPTY_MAX_PAGES) {
2147
+ hops++;
2148
+ const nOpts = { fetchMore: true };
2149
+ if (limit) nOpts.limit = limit;
2150
+ s = await fetch2({ ...base, queue_exclude: bgQueue }, nOpts);
2151
+ }
2152
+ state.pendingSurface = {
2153
+ list: s && Array.isArray(s.list) ? s.list : [],
2154
+ endOfList: !!(s && s.endOfList),
2155
+ startKeyHistory: s && Array.isArray(s.startKeyHistory) ? s.startKeyHistory : [],
2156
+ forFetchMore: fetchMore
2157
+ };
2158
+ }
2159
+ }
2160
+ const surface = state.pendingSurface;
2161
+ if (fetchOptions && fetchOptions.deferBg && (!state.bgEnd || headRefresh)) {
2162
+ const surfaceList0 = state.surfaceCarry.length ? state.surfaceCarry.concat(surface.list) : surface.list.slice();
2163
+ state.surfaceCarry = [];
2164
+ const emitNow = surfaceList0.concat(state.bgBuffer);
2165
+ state.bgBuffer = [];
2166
+ if (!headRefresh) state.surfaceEnd = surface.endOfList;
2167
+ state.lastSurfaceKeys = surface.startKeyHistory;
2168
+ state.pendingSurface = null;
2169
+ const bgPending = (async () => {
2170
+ try {
2171
+ const batch = [];
2172
+ if (headRefresh) {
2173
+ const bOpts = { fetchMore: false };
2174
+ if (limit) bOpts.limit = limit;
2175
+ const b = await fetch2({ ...base, queue: bgQueue, queue_exact: true, compact: true }, bOpts);
2176
+ const bList = b && Array.isArray(b.list) ? b.list : [];
2177
+ for (const it of bList) {
2178
+ if (it && typeof it === "object") it._fromBgChain = true;
2179
+ batch.push(it);
2180
+ }
2181
+ const prevNewest = state.newestBgId;
2182
+ noteBgIds(state, bList);
2183
+ if (prevNewest && !(b && b.endOfList) && !bList.some((it) => it && it.id === prevNewest)) {
2184
+ state.bgEnd = false;
2185
+ state.bgStarted = true;
2186
+ }
2187
+ } else {
2188
+ let hops = 0;
2189
+ while (!state.bgEnd && hops < BG_COVERAGE_MAX_PAGES) {
2190
+ hops++;
2191
+ const bOpts = { fetchMore: state.bgStarted };
2192
+ if (limit) bOpts.limit = limit;
2193
+ const b = await fetch2({ ...base, queue: bgQueue, queue_exact: true, compact: true }, bOpts);
2194
+ state.bgStarted = true;
2195
+ const bList = b && Array.isArray(b.list) ? b.list : [];
2196
+ for (const it of bList) {
2197
+ if (it && typeof it === "object") it._fromBgChain = true;
2198
+ batch.push(it);
2199
+ }
2200
+ noteBgIds(state, bList);
2201
+ state.bgEnd = !!(b && b.endOfList);
2202
+ if (!bList.length && !state.bgEnd) break;
2203
+ if (state.bgEnd) break;
2204
+ }
2205
+ }
2206
+ return { list: batch, endOfList: state.surfaceEnd && state.bgEnd };
2207
+ } finally {
2208
+ releaseLock();
2209
+ }
2210
+ })();
2211
+ return {
2212
+ list: emitNow,
2213
+ // A head-refreshed ended chain KNOWS it is still ended — reporting
2214
+ // the hardcoded false here was what un-gated the fill loop on every
2215
+ // tab return. Mid-walk it computes to false exactly as before (this
2216
+ // branch is only entered with bgEnd false then); the bg batch still
2217
+ // carries the final word for that case.
2218
+ endOfList: state.surfaceEnd && state.bgEnd,
2219
+ startKeyHistory: surface.startKeyHistory,
2220
+ firstLoad,
2221
+ bgPending
2222
+ };
2223
+ }
2224
+ const surfaceList = state.surfaceCarry.length ? state.surfaceCarry.concat(surface.list) : surface.list.slice();
2225
+ const boundary = surface.endOfList ? -Infinity : oldestCreated(surfaceList);
2226
+ if (headRefresh) {
2227
+ const hOpts = { fetchMore: false };
2228
+ if (limit) hOpts.limit = limit;
2229
+ const hb = await fetch2({ ...base, queue: bgQueue, queue_exact: true, compact: true }, hOpts);
2230
+ const hbList = hb && Array.isArray(hb.list) ? hb.list : [];
2231
+ for (const it of hbList) {
2232
+ if (it && typeof it === "object") it._fromBgChain = true;
2233
+ state.bgBuffer.push(it);
2234
+ }
2235
+ const prevNewestH = state.newestBgId;
2236
+ noteBgIds(state, hbList);
2237
+ if (prevNewestH && !(hb && hb.endOfList) && !hbList.some((it) => it && it.id === prevNewestH)) {
2238
+ state.bgEnd = false;
2239
+ state.bgStarted = true;
2240
+ }
2241
+ } else if (boundary !== Infinity || surface.endOfList) {
2242
+ let hops = 0;
2243
+ while (!state.bgEnd && hops < BG_COVERAGE_MAX_PAGES) {
2244
+ const bufOldest = state.bgBuffer.length ? oldestCreated(state.bgBuffer) : Infinity;
2245
+ if (state.bgBuffer.length && bufOldest <= boundary) break;
2246
+ hops++;
2247
+ const bOpts = { fetchMore: state.bgStarted };
2248
+ if (limit) bOpts.limit = limit;
2249
+ const b = await fetch2({ ...base, queue: bgQueue, queue_exact: true, compact: true }, bOpts);
2250
+ state.bgStarted = true;
2251
+ const bList = b && Array.isArray(b.list) ? b.list : [];
2252
+ for (const it of bList) {
2253
+ if (it && typeof it === "object") it._fromBgChain = true;
2254
+ state.bgBuffer.push(it);
2255
+ }
2256
+ noteBgIds(state, bList);
2257
+ state.bgEnd = !!(b && b.endOfList);
2258
+ if (!bList.length && !state.bgEnd) break;
2259
+ if (state.bgEnd) break;
2260
+ }
2261
+ }
2262
+ const emitSurface = surfaceList;
2263
+ state.surfaceCarry = [];
2264
+ const emitBg = state.bgBuffer;
2265
+ state.bgBuffer = [];
2266
+ const seen = {};
2267
+ for (const it of emitSurface) {
2268
+ if (it && typeof it.id === "string") seen[it.id] = true;
2269
+ }
2270
+ const merged = emitSurface.concat(emitBg.filter((it) => !(it && typeof it.id === "string" && seen[it.id])));
2271
+ if (!headRefresh) state.surfaceEnd = surface.endOfList;
2272
+ state.lastSurfaceKeys = surface.startKeyHistory;
2273
+ state.pendingSurface = null;
2274
+ return {
2275
+ list: merged,
2276
+ endOfList: state.surfaceEnd && state.bgEnd && state.bgBuffer.length === 0 && state.surfaceCarry.length === 0,
2277
+ // Bookkeeping only (both the consumers and the SDK treat it opaquely);
2278
+ // the real cursors are the SDK's internal ones plus this module's state.
2279
+ startKeyHistory: surface.startKeyHistory,
2280
+ firstLoad
2281
+ };
2282
+ }
1884
2283
  function mapHistoryListToMessages(list, platform, opts) {
1885
2284
  var mapped = [], runningItemIds = [];
1886
2285
  var extractAssistantText = platform === "openai" ? extractOpenAIText : extractClaudeText;
@@ -1893,10 +2292,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1893
2292
  var isPending = isInProcess || isQueued;
1894
2293
  var isFailed = item && item.status === "failed";
1895
2294
  var response = isFailed ? item.error != null ? item.error : item.response_body : item && item.response_body != null ? item.response_body : item && item.error;
1896
- var userText = extractLastUserTextFromRequest(requestBody);
1897
- var assistantText = isPending ? "" : (extractAssistantText(response) || "").trim() || "";
1898
- var isErrorResponse = !isPending && (isFailed || isErrorResponseBody(response));
1899
- var reportedComplete = !!(item && item._isBgTask) && !isErrorResponse && !!assistantText && assistantText.indexOf(INDEXING_COMPLETE_MARKER) !== -1;
2295
+ var isCompact = !!(item && item.compact);
2296
+ var userText = isCompact ? typeof item.request_text === "string" ? item.request_text : "" : extractLastUserTextFromRequest(requestBody);
2297
+ var assistantText = isPending ? "" : isCompact ? (typeof item.response_text === "string" ? item.response_text : "").trim() : (extractAssistantText(response) || "").trim() || "";
2298
+ var isErrorResponse = !isPending && (isFailed || !isCompact && isErrorResponseBody(response));
2299
+ var reportedComplete = !!(item && item._isBgTask) && !isErrorResponse && (isCompact ? item.response_complete_marker === true : !!assistantText && assistantText.indexOf(INDEXING_COMPLETE_MARKER) !== -1);
1900
2300
  if (reportedComplete) assistantText = assistantText.split(INDEXING_COMPLETE_MARKER).join("").trim();
1901
2301
  var serverItemId = item && typeof item.id === "string" && item.id ? item.id : void 0;
1902
2302
  var createdTs = Number(item && item.created);
@@ -1925,9 +2325,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1925
2325
  displayContent = sanitizeAttachmentLinksForHistory(userText, opts.projectId);
1926
2326
  }
1927
2327
  var userMsg = { role: "user", content: displayContent };
2328
+ if (item._fromBgChain) userMsg._fromBgChain = true;
1928
2329
  if (isInProcess) userMsg.isPendingInProcess = true;
1929
2330
  if (isQueued) userMsg.isPendingQueued = true;
1930
2331
  if (isCancelledItem) userMsg.isCancelled = true;
2332
+ if (isCompact) userMsg._compact = true;
1931
2333
  if (item._isBgTask) userMsg.isBackgroundTask = true;
1932
2334
  if (indexFile) userMsg._indexFile = indexFile;
1933
2335
  if (item._isOnBgQueue) userMsg._useBgQueue = true;
@@ -1937,6 +2339,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1937
2339
  }
1938
2340
  if (isCancelledItem) ; else if (isInProcess) {
1939
2341
  var ph = { role: "assistant", content: "", isPending: true, isPendingInProcess: true };
2342
+ if (userTs !== void 0) ph._ts = userTs;
2343
+ if (item._fromBgChain) ph._fromBgChain = true;
1940
2344
  if (item._isBgTask) ph.isBackgroundTask = true;
1941
2345
  if (serverItemId !== void 0) {
1942
2346
  ph._serverItemId = serverItemId;
@@ -1945,19 +2349,26 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1945
2349
  mapped.push(ph);
1946
2350
  } else if (isQueued) ; else if (isErrorResponse) {
1947
2351
  var em = { role: "assistant", content: getErrorMessage(response), isError: true };
2352
+ if (item._fromBgChain) em._fromBgChain = true;
1948
2353
  if (item._isBgTask) em.isBackgroundTask = true;
1949
2354
  if (serverItemId !== void 0) em._serverItemId = serverItemId;
1950
2355
  if (replyTs !== void 0) em._ts = replyTs;
1951
2356
  mapped.push(em);
1952
2357
  } else if (assistantText || reportedComplete) {
1953
2358
  var okm = { role: "assistant", content: sanitizeAttachmentLinksForHistory(assistantText, opts.projectId, true) || EMPTY_INDEXING_REPLY };
2359
+ if (item._fromBgChain) okm._fromBgChain = true;
1954
2360
  if (item._isBgTask) okm.isBackgroundTask = true;
2361
+ if (isCompact) okm._compact = true;
1955
2362
  if (serverItemId !== void 0) okm._serverItemId = serverItemId;
1956
2363
  if (replyTs !== void 0) okm._ts = replyTs;
1957
2364
  if (reportedComplete) okm._indexComplete = true;
1958
2365
  mapped.push(okm);
1959
2366
  }
1960
2367
  });
2368
+ if (opts.projectId) {
2369
+ var ownerKey = opts.projectId + "#" + platform;
2370
+ for (var oi = 0; oi < mapped.length; oi++) mapped[oi]._ownerKey = ownerKey;
2371
+ }
1961
2372
  return { messages: mapped, runningItemIds };
1962
2373
  }
1963
2374
 
@@ -2075,6 +2486,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2075
2486
  var INDEXING_DRAIN_CONFIRM_POLL_MS = 3e3;
2076
2487
  var INDEXING_DRAIN_IDLE_LOOKS = 2;
2077
2488
  var INDEXING_DRAIN_MIN_MS = 8e3;
2489
+ var _bgHistoryBatchSeq = 0;
2078
2490
  var INDEXING_DRAIN_TIMEOUT_MS = 15 * 60 * 1e3;
2079
2491
  var INDEXING_DRAIN_LOOK_TIMEOUT_MS = 45e3;
2080
2492
  var INDEXING_DRAIN_NUDGE_MIN_GAP_MS = 1500;
@@ -2096,6 +2508,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2096
2508
  }
2097
2509
  var ChatSession = class {
2098
2510
  constructor(host) {
2511
+ // ─── compact-stub hydration ─────────────────────────────────────────────
2512
+ // Split-fetch bg pages arrive as label stubs (no bodies). When the user
2513
+ // expands a row, the real reply text is fetched per item (csr-poll point
2514
+ // lookup) and MEMOIZED per chat: every later remap (first-page refresh,
2515
+ // queue-detect tick, cache restore) re-applies the memo, so a hydrated
2516
+ // bubble can never silently revert to its 200-char head.
2517
+ this._hydratedBodies = {};
2518
+ this._hydratingItems = {};
2099
2519
  this.typewriterQueue = Promise.resolve();
2100
2520
  /**
2101
2521
  * Pick up indexing passes the WORKER minted, which no client ever dispatched.
@@ -2136,6 +2556,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2136
2556
  typingAbort: false,
2137
2557
  loadingHistory: false,
2138
2558
  loadingOlderHistory: false,
2559
+ // A deferred bg stub batch (first-paint split) is still in flight; the
2560
+ // views show a small 'loading indexing history' hint while true.
2561
+ bgHistoryLoading: false,
2139
2562
  historyEndOfList: false,
2140
2563
  historyStartKeyHistory: [],
2141
2564
  historyRequestToken: 0,
@@ -2285,10 +2708,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2285
2708
  }
2286
2709
  var queue = bgIndexingQueueName(id.userId, id.projectId);
2287
2710
  var ask = function(status) {
2288
- return Promise.resolve(getChatHistory(
2289
- { service: id.projectId, owner: id.owner, platform, queue, status },
2290
- { limit: WORKER_PASS_ADOPT_LIMIT }
2291
- )).catch(function() {
2711
+ return Promise.resolve(probeBgQueue(
2712
+ { service: id.projectId, owner: id.owner, platform, queue, status, limit: WORKER_PASS_ADOPT_LIMIT },
2713
+ { maxAgeMs: BG_PROBE_TTL_MS }
2714
+ )).then(function(entry) {
2715
+ return entry.result;
2716
+ }).catch(function() {
2292
2717
  return null;
2293
2718
  });
2294
2719
  };
@@ -2380,7 +2805,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2380
2805
  * instead of merely unconfirmed.
2381
2806
  */
2382
2807
  refreshLiveIndexState() {
2383
- this._adoptWorkerIndexingPasses(0);
2808
+ this._adoptWorkerIndexingPasses(0, true);
2384
2809
  }
2385
2810
  /** Forget what we know about which files are indexing — but ONLY when the
2386
2811
  * snapshot was taken for a different chat than the one on screen now. For a
@@ -2557,6 +2982,66 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2557
2982
  if (!id.projectId || id.platform === "none") return "";
2558
2983
  return id.projectId + "#" + id.platform;
2559
2984
  }
2985
+ /** Re-apply memoized hydrated texts onto freshly-mapped messages. Both
2986
+ * clients call this right after their mapper runs (loadHistory does it
2987
+ * internally); it mutates the given array's items in place. */
2988
+ applyHydratedBodies(messages) {
2989
+ var key = this.getHistoryCacheKey();
2990
+ var memo = key ? this._hydratedBodies[key] : null;
2991
+ if (!memo) return;
2992
+ var id = this.host.getIdentity();
2993
+ for (var i = 0; i < messages.length; i++) {
2994
+ var m = messages[i];
2995
+ if (!m || !m._compact || m.role !== "assistant" || !m._serverItemId) continue;
2996
+ var text = memo[m._serverItemId];
2997
+ if (typeof text !== "string") continue;
2998
+ m.content = sanitizeAttachmentLinksForHistory(text, id.projectId, true) || EMPTY_INDEXING_REPLY;
2999
+ delete m._compact;
3000
+ }
3001
+ }
3002
+ /** Fetch the real response bodies for compact history stubs (one csr-poll
3003
+ * point lookup per item id), memoize, and swap them into the live list.
3004
+ * Best-effort: a failed lookup leaves the stub (its head + fallback line
3005
+ * still render) and a later expand retries. */
3006
+ hydrateCompactItems(itemIds) {
3007
+ var self = this;
3008
+ var lookup = chatEngineConfig().csrHistoryItemLookup;
3009
+ if (!lookup || !itemIds || !itemIds.length) return Promise.resolve();
3010
+ var id = this.host.getIdentity();
3011
+ var platform = id.platform;
3012
+ if (!id.projectId || platform !== "claude" && platform !== "openai") return Promise.resolve();
3013
+ var chatKey = this.getHistoryCacheKey();
3014
+ if (!chatKey) return Promise.resolve();
3015
+ var jobs = itemIds.map(function(itemId) {
3016
+ if (!itemId) return Promise.resolve();
3017
+ var already = self._hydratedBodies[chatKey] && self._hydratedBodies[chatKey][itemId] !== void 0;
3018
+ var inflightKey = chatKey + "|" + itemId;
3019
+ if (already || self._hydratingItems[inflightKey]) return Promise.resolve();
3020
+ self._hydratingItems[inflightKey] = true;
3021
+ return Promise.resolve(lookup(buildHistoryItemFullId(platform, id.projectId, itemId), id.projectId, id.owner)).then(function(body) {
3022
+ var text = ((platform === "openai" ? extractOpenAIText(body) : extractClaudeText(body)) || "").trim();
3023
+ if (text.indexOf(INDEXING_COMPLETE_MARKER) !== -1) text = text.split(INDEXING_COMPLETE_MARKER).join("").trim();
3024
+ if (!self._hydratedBodies[chatKey]) self._hydratedBodies[chatKey] = {};
3025
+ self._hydratedBodies[chatKey][itemId] = text;
3026
+ if (self.getHistoryCacheKey() !== chatKey) return;
3027
+ for (var i = 0; i < self.state.messages.length; i++) {
3028
+ var m = self.state.messages[i];
3029
+ if (m && m._compact && m.role === "assistant" && m._serverItemId === itemId) {
3030
+ m.content = sanitizeAttachmentLinksForHistory(text, id.projectId, true) || EMPTY_INDEXING_REPLY;
3031
+ delete m._compact;
3032
+ }
3033
+ }
3034
+ }).catch(function() {
3035
+ }).then(function() {
3036
+ delete self._hydratingItems[inflightKey];
3037
+ });
3038
+ });
3039
+ return Promise.all(jobs).then(function() {
3040
+ if (self.getHistoryCacheKey() !== chatKey) return;
3041
+ self.host.notify();
3042
+ self.updateHistoryCache();
3043
+ });
3044
+ }
2560
3045
  updateHistoryCache() {
2561
3046
  var key = this.getHistoryCacheKey();
2562
3047
  if (!key) return;
@@ -2879,11 +3364,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2879
3364
  bail = setTimeout(function() {
2880
3365
  settle(null);
2881
3366
  }, INDEXING_DRAIN_LOOK_TIMEOUT_MS);
2882
- Promise.resolve(getChatHistory(
2883
- { service: svcId, owner, platform, queue, status },
2884
- { limit: WORKER_PASS_ADOPT_LIMIT }
2885
- )).then(function(r) {
2886
- settle(r);
3367
+ Promise.resolve(probeBgQueue(
3368
+ { service: svcId, owner, platform, queue, status, limit: WORKER_PASS_ADOPT_LIMIT },
3369
+ { maxAgeMs: 0 }
3370
+ )).then(function(entry) {
3371
+ settle(entry.result);
2887
3372
  }, function() {
2888
3373
  settle(null);
2889
3374
  });
@@ -3510,6 +3995,32 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3510
3995
  if (e && e.id && self._indexKeyOf(e) === scoped) stoppedIds[e.id] = true;
3511
3996
  });
3512
3997
  this.state.stoppedIndexIds = stoppedIds;
3998
+ var runPath = group.path || "";
3999
+ if (!runPath) {
4000
+ (group.members || []).some(function(m) {
4001
+ var p = m && m.msg && m.msg._indexFile && m.msg._indexFile.path;
4002
+ if (p) {
4003
+ runPath = p;
4004
+ return true;
4005
+ }
4006
+ return false;
4007
+ });
4008
+ }
4009
+ if (!runPath) {
4010
+ this.bgTaskQueue.some(function(e) {
4011
+ if (e && e.storagePath && self._indexKeyOf(e) === scoped) {
4012
+ runPath = e.storagePath;
4013
+ return true;
4014
+ }
4015
+ return false;
4016
+ });
4017
+ }
4018
+ if (runPath) {
4019
+ var ident = this.host.getIdentity();
4020
+ if (ident && ident.projectId) {
4021
+ upsertIndexRunRecordSafe(ident.projectId, runPath, { status: "cancelled", finished: Date.now() });
4022
+ }
4023
+ }
3513
4024
  }
3514
4025
  this._adoptWorkerIndexingPasses(0);
3515
4026
  var ids = group.cancellableIds || [];
@@ -4023,7 +4534,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4023
4534
  if (isImageVisionFile(filename, mime)) return true;
4024
4535
  return windowedIndexingEnabled() && isWindowedReadFile(filename, mime);
4025
4536
  }
4026
- _adoptWorkerIndexingPasses(attempt) {
4537
+ _adoptWorkerIndexingPasses(attempt, passive) {
4027
4538
  var self = this;
4028
4539
  if (this._adoptingWorkerPasses) return;
4029
4540
  var id = this.host.getIdentity();
@@ -4033,10 +4544,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4033
4544
  var svcId = id.projectId, owner = id.owner;
4034
4545
  var queue = bgIndexingQueueName(id.userId, id.projectId);
4035
4546
  var ask = function(status) {
4036
- return Promise.resolve(getChatHistory(
4037
- { service: svcId, owner, platform, queue, status },
4038
- { limit: WORKER_PASS_ADOPT_LIMIT }
4039
- )).catch(function() {
4547
+ return Promise.resolve(probeBgQueue(
4548
+ { service: svcId, owner, platform, queue, status, limit: WORKER_PASS_ADOPT_LIMIT },
4549
+ { maxAgeMs: 0 }
4550
+ )).then(function(entry) {
4551
+ return entry.result;
4552
+ }).catch(function() {
4040
4553
  return null;
4041
4554
  });
4042
4555
  };
@@ -4058,6 +4571,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4058
4571
  self.drainBgTaskQueue();
4059
4572
  if (self._isTrackingAny(adoptedIds)) return;
4060
4573
  }
4574
+ if (passive && !self._hasLiveIndexEvidence(svcId)) return;
4061
4575
  if (attempt + 1 >= WORKER_PASS_ADOPT_ATTEMPTS.length) {
4062
4576
  self._nudgeIndexingDrain();
4063
4577
  return;
@@ -4066,12 +4580,30 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4066
4580
  var later = self.host.getIdentity();
4067
4581
  if (later.projectId !== svcId || later.platform !== platform) return;
4068
4582
  if (self.isPollingPaused() || !self.host.isViewMounted()) return;
4069
- self._adoptWorkerIndexingPasses(attempt + 1);
4583
+ self._adoptWorkerIndexingPasses(attempt + 1, passive);
4070
4584
  }, WORKER_PASS_ADOPT_ATTEMPTS[attempt + 1]);
4071
4585
  }, function() {
4072
4586
  self._adoptingWorkerPasses = false;
4073
4587
  });
4074
4588
  }
4589
+ /** Anything at all suggesting THIS project's indexing may be live: a queued
4590
+ * local entry, a recorded live key (the adopt look just wrote them), or an
4591
+ * attached poll. Gates the passive adopt ladder's climb. */
4592
+ _hasLiveIndexEvidence(svcId) {
4593
+ for (var i = 0; i < this.bgTaskQueue.length; i++) {
4594
+ var e = this.bgTaskQueue[i];
4595
+ if (e && e.projectId === svcId) return true;
4596
+ }
4597
+ var keys = this.state.liveIndexKeys || {};
4598
+ for (var k in keys) {
4599
+ if (keys[k]) return true;
4600
+ }
4601
+ var found = false;
4602
+ this.historyItemPolls.forEach(function(h) {
4603
+ if (h && h.kind === "bg") found = true;
4604
+ });
4605
+ return found;
4606
+ }
4075
4607
  /** Any of these ids still queued or still polled, i.e. surviving work. */
4076
4608
  _isTrackingAny(ids) {
4077
4609
  for (var i = 0; i < ids.length; i++) {
@@ -4162,7 +4694,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4162
4694
  for (var i = this.bgTaskQueue.length - 1; i >= 0; i--) {
4163
4695
  var e = this.bgTaskQueue[i];
4164
4696
  if (e.projectId !== svcId || e.platform !== plat) continue;
4165
- if (presentIds[e.id] && !pendingIds[e.id]) this.bgTaskQueue.splice(i, 1);
4697
+ if (presentIds[e.id] && !pendingIds[e.id]) {
4698
+ this._flipRunFromSettledEntry(e);
4699
+ this.bgTaskQueue.splice(i, 1);
4700
+ }
4166
4701
  }
4167
4702
  var bgPollBudget = MAX_CONCURRENT_BG_POLLS - this._countBgPolls();
4168
4703
  var injectedAny = false;
@@ -4236,6 +4771,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4236
4771
  self.host.notify();
4237
4772
  self.updateHistoryCache();
4238
4773
  if (!self._isWorkerDrivenIndexing(capturedEntry.filename, capturedEntry.mime)) {
4774
+ if (isNotExists) self._flipRunRecord(capturedEntry, "cancelled");
4775
+ else self._flipRunRecord(capturedEntry, "error", self._runErrorText(err));
4239
4776
  self._nudgeIndexingDrain();
4240
4777
  }
4241
4778
  }).then(function() {
@@ -4267,6 +4804,74 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4267
4804
  // memory (a reload or a closed tab ended it), and it stopped whenever the model claimed
4268
4805
  // completion, which on an 88-page file happened at page 15. Continuing to dispatch here
4269
4806
  // as well would now double-index every window.
4807
+ /** Fire the consumer's done::-marker hook for a run whose completion this
4808
+ * client knows DETERMINISTICALLY (see the two call sites in
4809
+ * maybeResumeIndexing). Best-effort by contract; identity-checked so a
4810
+ * project switch mid-settle cannot stamp the wrong service. */
4811
+ _mintDoneMarker(entry) {
4812
+ try {
4813
+ var mint = chatEngineConfig().mintIndexDoneMarker;
4814
+ if (!mint || !entry || !entry.storagePath || !entry.projectId) return;
4815
+ var id = this.host.getIdentity();
4816
+ if (!id || id.projectId !== entry.projectId) return;
4817
+ mint({ service: entry.projectId, storagePath: entry.storagePath });
4818
+ } catch (_e) {
4819
+ }
4820
+ }
4821
+ /** Short, storable form of an error body for the run:: record. */
4822
+ _runErrorText(response) {
4823
+ var msg = "";
4824
+ try {
4825
+ msg = String(getErrorMessage(response) || "");
4826
+ } catch (_e) {
4827
+ }
4828
+ msg = msg.replace(/\s+/g, " ").trim();
4829
+ return msg ? msg.slice(0, 300) : "Indexing failed.";
4830
+ }
4831
+ /** Close the records of a run whose pass settled OFF-POLL — the answer came
4832
+ * back as history (hidden tab, dead poll, resume refetch), so none of the
4833
+ * poll-side settle handlers ran. Only for SINGLE-PASS files, where one
4834
+ * settled pass is deterministically the whole run (the same contract as
4835
+ * maybeResumeIndexing's single-pass branch); paged files stay with their
4836
+ * drivers. Outcome is read from the settled bubbles' own flags, which is
4837
+ * all the history mapping left us. Best-effort and idempotent throughout. */
4838
+ _flipRunFromSettledEntry(entry) {
4839
+ try {
4840
+ if (!entry || !entry.storagePath || !entry.id || !entry.projectId) return;
4841
+ if (isPagedReadFile(entry.filename, entry.mime)) return;
4842
+ if (this.cancelledIndexKeys.has(this._indexKeyOf(entry))) return;
4843
+ if (this.state.stoppedIndexIds[entry.id]) return;
4844
+ var userMsg = null, replyMsg = null;
4845
+ this.state.messages.forEach(function(m) {
4846
+ if (m._serverItemId !== entry.id) return;
4847
+ if (m.role === "user") {
4848
+ if (!userMsg) userMsg = m;
4849
+ } else if (!replyMsg) replyMsg = m;
4850
+ });
4851
+ if (userMsg && userMsg.isCancelled || replyMsg && replyMsg.isCancelled) {
4852
+ this._flipRunRecord(entry, "cancelled");
4853
+ } else if (replyMsg && replyMsg.isError) {
4854
+ var errText = typeof replyMsg.content === "string" ? replyMsg.content.replace(/\s+/g, " ").trim().slice(0, 300) : "";
4855
+ this._flipRunRecord(entry, "error", errText || "Indexing failed.");
4856
+ } else if (replyMsg) {
4857
+ this._mintDoneMarker(entry);
4858
+ this._flipRunRecord(entry, "done");
4859
+ }
4860
+ } catch (_e) {
4861
+ }
4862
+ }
4863
+ /** Close the durable run:: record for an ending THIS client observed.
4864
+ * service comes from the ENTRY, not the current identity: unlike the done::
4865
+ * mint above, a status flip must land even if the user switched projects
4866
+ * mid-settle — otherwise the record lies 'working' forever. Best-effort
4867
+ * through upsertIndexRunRecordSafe; the consumer's precedence guard keeps
4868
+ * repeats and races harmless. */
4869
+ _flipRunRecord(entry, status, error) {
4870
+ if (!entry || !entry.storagePath || !entry.projectId) return;
4871
+ var patch = { status, finished: Date.now() };
4872
+ if (error) patch.error = error;
4873
+ upsertIndexRunRecordSafe(entry.projectId, entry.storagePath, patch);
4874
+ }
4270
4875
  maybeResumeIndexing(entry, response, platform) {
4271
4876
  var self = this;
4272
4877
  var endOfClientChain = function() {
@@ -4276,27 +4881,43 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4276
4881
  if (!entry || !entry.storagePath) return;
4277
4882
  if (this.cancelledIndexKeys.has(this._indexKeyOf(entry))) return;
4278
4883
  if (!isPagedReadFile(entry.filename, entry.mime)) {
4884
+ if (!isErrorResponseBody(response) && !this._isCancelledPollResult(response)) {
4885
+ this._mintDoneMarker(entry);
4886
+ this._flipRunRecord(entry, "done");
4887
+ } else if (this._isCancelledPollResult(response)) {
4888
+ this._flipRunRecord(entry, "cancelled");
4889
+ } else {
4890
+ this._flipRunRecord(entry, "error", this._runErrorText(response));
4891
+ }
4279
4892
  endOfClientChain();
4280
4893
  return;
4281
4894
  }
4282
4895
  if (isImageVisionFile(entry.filename, entry.mime)) return;
4283
4896
  if (windowedIndexingEnabled() && isWindowedReadFile(entry.filename, entry.mime)) return;
4284
4897
  if (isErrorResponseBody(response)) {
4898
+ this._flipRunRecord(entry, "error", this._runErrorText(response));
4285
4899
  endOfClientChain();
4286
4900
  return;
4287
4901
  }
4288
4902
  var answer = (platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "";
4289
4903
  if (answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1) {
4904
+ this._mintDoneMarker(entry);
4905
+ this._flipRunRecord(entry, "done");
4290
4906
  endOfClientChain();
4291
4907
  return;
4292
4908
  }
4293
4909
  var pass = (entry.resumePass || 0) + 1;
4294
4910
  if (pass > MAX_INDEXING_RESUME_PASSES) {
4911
+ this._flipRunRecord(entry, "error", "Stopped after " + MAX_INDEXING_RESUME_PASSES + " passes without finishing.");
4295
4912
  endOfClientChain();
4296
4913
  return;
4297
4914
  }
4298
4915
  var id = this.host.getIdentity();
4299
- if (!id || id.platform === "none" || id.projectId !== entry.projectId) return;
4916
+ if (!id || id.platform === "none" || id.projectId !== entry.projectId) {
4917
+ this._flipRunRecord(entry, "error", "Indexing stopped: the session or project changed before the file finished.");
4918
+ endOfClientChain();
4919
+ return;
4920
+ }
4300
4921
  this.trackIndexDispatch(notifyAgentContinueIndexing({
4301
4922
  platform: id.platform,
4302
4923
  model: id.model,
@@ -4373,8 +4994,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4373
4994
  var projectId = id.projectId, owner = id.owner;
4374
4995
  var options = { fetchMore };
4375
4996
  if (fetchMore && this.state.historyStartKeyHistory.length) options.startKeyHistory = this.state.historyStartKeyHistory.slice();
4997
+ if (!fetchMore) options.deferBg = true;
4376
4998
  var fetchHistory = function() {
4377
- return getChatHistory({ service: projectId, owner, platform }, options);
4999
+ return getSplitChatHistory({ service: projectId, owner, platform, userId: id.userId }, options);
4378
5000
  };
4379
5001
  return Promise.resolve().then(fetchHistory).catch(function(err) {
4380
5002
  if (isAuthExpiredError(err) && !isNonRetryableRequestError(err)) return self.host.refreshSession().then(fetchHistory);
@@ -4384,7 +5006,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4384
5006
  var chatList = history && Array.isArray(history.list) ? history.list : [];
4385
5007
  chatList.forEach(function(item) {
4386
5008
  if (isBgIndexingQueue(item.queue_name)) {
4387
- if (isIndexingRequestText(extractLastUserTextFromRequest(item.request_body))) item._isBgTask = true;
5009
+ var clsText = item.compact ? item.request_text : extractLastUserTextFromRequest(item.request_body);
5010
+ if (isIndexingRequestText(clsText)) item._isBgTask = true;
4388
5011
  else item._isOnBgQueue = true;
4389
5012
  }
4390
5013
  });
@@ -4397,15 +5020,55 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4397
5020
  projectId: id.projectId,
4398
5021
  formatIndexingLabel: self.host.formatIndexingLabel
4399
5022
  }).messages;
5023
+ self.applyHydratedBodies(mapped);
4400
5024
  var keptOlderPages = false;
5025
+ var keptScreenAwaitingBg = false;
4401
5026
  if (fetchMore) {
4402
- self.state.messages = mapped.concat(self.state.messages);
5027
+ var incomingKeys = {};
5028
+ mapped.forEach(function(m) {
5029
+ if (m._serverItemId) incomingKeys[m._serverItemId + "|" + m.role] = m;
5030
+ });
5031
+ var existing = self.state.messages.filter(function(m) {
5032
+ if (!m._serverItemId) return true;
5033
+ var inc = incomingKeys[m._serverItemId + "|" + m.role];
5034
+ if (!inc) return true;
5035
+ if (m._cancelling) inc._cancelling = m._cancelling;
5036
+ if (m._cancelError) inc._cancelError = m._cancelError;
5037
+ return false;
5038
+ });
5039
+ var mergedList = [];
5040
+ var pi = 0, ei = 0;
5041
+ while (pi < mapped.length && ei < existing.length) {
5042
+ var pm = mapped[pi], em = existing[ei];
5043
+ var eid = em._serverItemId;
5044
+ if (typeof eid !== "string") break;
5045
+ var pid = pm._serverItemId;
5046
+ if (typeof pid !== "string" || pid <= eid) {
5047
+ mergedList.push(pm);
5048
+ pi++;
5049
+ } else {
5050
+ mergedList.push(em);
5051
+ ei++;
5052
+ }
5053
+ }
5054
+ while (pi < mapped.length) mergedList.push(mapped[pi++]);
5055
+ while (ei < existing.length) mergedList.push(existing[ei++]);
5056
+ self.state.messages = mergedList;
5057
+ } else if (!mapped.length && history && (history.endOfList === false || history.bgPending) && self.state.messages.some(function(m) {
5058
+ return m._ownerKey === void 0 || m._ownerKey === loadKey;
5059
+ })) {
5060
+ if (history.endOfList !== false) keptScreenAwaitingBg = true;
4403
5061
  } else {
4404
5062
  if (self.state.typing) self.state.typingAbort = true;
4405
5063
  var serverIds = {};
4406
5064
  mapped.forEach(function(m) {
4407
5065
  if (m._serverItemId) serverIds[m._serverItemId] = 1;
4408
5066
  });
5067
+ var surfaceOldestId = void 0;
5068
+ mapped.forEach(function(m) {
5069
+ if (typeof m._serverItemId !== "string" || m._fromBgChain) return;
5070
+ if (surfaceOldestId === void 0 || m._serverItemId < surfaceOldestId) surfaceOldestId = m._serverItemId;
5071
+ });
4409
5072
  var locallyCancelled = {};
4410
5073
  self.state.messages.forEach(function(m) {
4411
5074
  if (m.isCancelled && m._serverItemId) locallyCancelled[m._serverItemId] = m;
@@ -4446,13 +5109,45 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4446
5109
  var sharesPage1 = self.state.messages.some(function(m) {
4447
5110
  return typeof m._serverItemId === "string" && !!serverIds[m._serverItemId];
4448
5111
  });
4449
- var retainedOlder = !sharesPage1 || oldestInPage1 === void 0 ? [] : self.state.messages.filter(function(m) {
5112
+ var deferredBg = !!(history && history.bgPending);
5113
+ var retainBoundary = surfaceOldestId !== void 0 ? surfaceOldestId : oldestInPage1;
5114
+ var retainedOlder = !sharesPage1 || retainBoundary === void 0 ? [] : self.state.messages.filter(function(m) {
4450
5115
  if (typeof m._serverItemId !== "string") return false;
4451
5116
  if (m._ownerKey !== void 0 && m._ownerKey !== loadKey) return false;
4452
- return m._serverItemId < oldestInPage1;
5117
+ if (deferredBg && m.isBackgroundTask) return true;
5118
+ if (m._fromBgChain) return true;
5119
+ return m._serverItemId < retainBoundary;
5120
+ });
5121
+ var prependOlder = [];
5122
+ var interleave = [];
5123
+ retainedOlder.forEach(function(m) {
5124
+ var sid = m._serverItemId;
5125
+ if (serverIds[sid]) return;
5126
+ if (retainBoundary !== void 0 && sid < retainBoundary) prependOlder.push(m);
5127
+ else interleave.push(m);
4453
5128
  });
4454
- keptOlderPages = retainedOlder.length > 0;
4455
- self.state.messages = keptOlderPages ? retainedOlder.concat(mapped) : mapped;
5129
+ var page1 = mapped;
5130
+ if (interleave.length) {
5131
+ var mergedP = [];
5132
+ var ii2 = 0, mi2 = 0;
5133
+ while (ii2 < interleave.length && mi2 < mapped.length) {
5134
+ var iv = interleave[ii2], mv = mapped[mi2];
5135
+ var mid2 = typeof mv._serverItemId === "string" ? mv._serverItemId : void 0;
5136
+ if (mid2 === void 0) break;
5137
+ if (iv._serverItemId <= mid2) {
5138
+ mergedP.push(iv);
5139
+ ii2++;
5140
+ } else {
5141
+ mergedP.push(mv);
5142
+ mi2++;
5143
+ }
5144
+ }
5145
+ while (ii2 < interleave.length) mergedP.push(interleave[ii2++]);
5146
+ while (mi2 < mapped.length) mergedP.push(mapped[mi2++]);
5147
+ page1 = mergedP;
5148
+ }
5149
+ keptOlderPages = prependOlder.length > 0 || interleave.length > 0;
5150
+ self.state.messages = prependOlder.length ? prependOlder.concat(page1) : page1;
4456
5151
  rescued.forEach(function(m) {
4457
5152
  self.state.messages.push(m);
4458
5153
  });
@@ -4488,9 +5183,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4488
5183
  self.state.historyEndOfList = !!(history && history.endOfList);
4489
5184
  self.state.historyStartKeyHistory = history && Array.isArray(history.startKeyHistory) ? history.startKeyHistory : [];
4490
5185
  var clearedAt = self.host.getClearedAt();
4491
- if (clearedAt && chatList.length > 0) {
4492
- var oldestUpdated = Number(chatList[chatList.length - 1] && chatList[chatList.length - 1].updated);
4493
- if (isFinite(oldestUpdated) && oldestUpdated <= clearedAt) self.state.historyEndOfList = true;
5186
+ if (clearedAt) {
5187
+ var surfaceItems = chatList.filter(function(it) {
5188
+ return !(it && it._fromBgChain);
5189
+ });
5190
+ if (surfaceItems.length > 0) {
5191
+ var oldestUpdated = Number(surfaceItems[surfaceItems.length - 1] && surfaceItems[surfaceItems.length - 1].updated);
5192
+ if (isFinite(oldestUpdated) && oldestUpdated <= clearedAt) self.state.historyEndOfList = true;
5193
+ }
4494
5194
  }
4495
5195
  }
4496
5196
  if (self.state.historyRequestToken === token) {
@@ -4499,6 +5199,85 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4499
5199
  }
4500
5200
  self.updateHistoryCache();
4501
5201
  self.host.notify();
5202
+ var bgPending = !fetchMore && history && history.bgPending;
5203
+ if (bgPending) {
5204
+ var batchId = ++_bgHistoryBatchSeq;
5205
+ if (history.endOfList !== true && history.firstLoad === true) {
5206
+ self.state.bgHistoryLoading = true;
5207
+ self.host.notify();
5208
+ }
5209
+ var releaseBgFlag = function() {
5210
+ if (_bgHistoryBatchSeq === batchId) self.state.bgHistoryLoading = false;
5211
+ };
5212
+ bgPending.then(function(batch) {
5213
+ if (token !== self.state.gateRefreshToken) {
5214
+ releaseBgFlag();
5215
+ return;
5216
+ }
5217
+ var bList = batch && Array.isArray(batch.list) ? batch.list : [];
5218
+ bList.forEach(function(item) {
5219
+ if (isBgIndexingQueue(item.queue_name)) {
5220
+ var t = item.compact ? item.request_text : extractLastUserTextFromRequest(item.request_body);
5221
+ if (isIndexingRequestText(t)) item._isBgTask = true;
5222
+ else item._isOnBgQueue = true;
5223
+ }
5224
+ });
5225
+ var sorted = bList.sort(function(a, b) {
5226
+ var ai = typeof a.id === "string" ? a.id : "", bi = typeof b.id === "string" ? b.id : "";
5227
+ return ai > bi ? -1 : ai < bi ? 1 : 0;
5228
+ });
5229
+ var m2 = mapHistoryListToMessages(sorted, platform, {
5230
+ clearedAt: self.host.getClearedAt(),
5231
+ projectId: id.projectId,
5232
+ formatIndexingLabel: self.host.formatIndexingLabel
5233
+ }).messages;
5234
+ self.applyHydratedBodies(m2);
5235
+ if (keptScreenAwaitingBg && !m2.length && batch && batch.endOfList === true) {
5236
+ self.state.messages = self.state.messages.filter(function(m) {
5237
+ if (typeof m._serverItemId !== "string") return true;
5238
+ if (m._ownerKey !== void 0 && m._ownerKey !== loadKey) return true;
5239
+ return false;
5240
+ });
5241
+ self.state.historyEndOfList = true;
5242
+ releaseBgFlag();
5243
+ self.updateHistoryCache();
5244
+ self.host.notify();
5245
+ return;
5246
+ }
5247
+ var incoming = {};
5248
+ m2.forEach(function(m) {
5249
+ if (m._serverItemId) incoming[m._serverItemId + "|" + m.role] = true;
5250
+ });
5251
+ var baseList = self.state.messages.filter(function(m) {
5252
+ return !(m._serverItemId && incoming[m._serverItemId + "|" + m.role]);
5253
+ });
5254
+ var mergedList2 = [];
5255
+ var pi2 = 0, ei2 = 0;
5256
+ while (pi2 < m2.length && ei2 < baseList.length) {
5257
+ var pm2 = m2[pi2], em2 = baseList[ei2];
5258
+ var eid2 = em2._serverItemId;
5259
+ if (typeof eid2 !== "string") break;
5260
+ var pid2 = pm2._serverItemId;
5261
+ if (typeof pid2 !== "string" || pid2 <= eid2) {
5262
+ mergedList2.push(pm2);
5263
+ pi2++;
5264
+ } else {
5265
+ mergedList2.push(em2);
5266
+ ei2++;
5267
+ }
5268
+ }
5269
+ while (pi2 < m2.length) mergedList2.push(m2[pi2++]);
5270
+ while (ei2 < baseList.length) mergedList2.push(baseList[ei2++]);
5271
+ self.state.messages = mergedList2;
5272
+ if (batch && batch.endOfList === true) self.state.historyEndOfList = true;
5273
+ releaseBgFlag();
5274
+ self.updateHistoryCache();
5275
+ self.host.notify();
5276
+ }, function() {
5277
+ releaseBgFlag();
5278
+ self.host.notify();
5279
+ });
5280
+ }
4502
5281
  if (!fetchMore) {
4503
5282
  var bgAllow = {};
4504
5283
  var bgHistBudget = MAX_CONCURRENT_BG_POLLS - self._countBgPolls();
@@ -4595,7 +5374,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4595
5374
  var self = this;
4596
5375
  var id = this.host.getIdentity();
4597
5376
  att.status = "uploading";
4598
- att.progress = 0;
5377
+ att.progress = null;
4599
5378
  att.errorMessage = "";
4600
5379
  att.errorCode = "";
4601
5380
  att.errorDetail = "";
@@ -4826,6 +5605,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4826
5605
  };
4827
5606
 
4828
5607
  // src/engine/indexing_groups.ts
5608
+ var RUN_RECORD_WORKING_STALE_MS = 6 * 60 * 60 * 1e3;
4829
5609
  var INDEXING_LABEL_RE = /^(Re)?[Ii]ndexing(\s*\(continuing\))?\s*:?\s+(.+)$/;
4830
5610
  var LEADING_MD_LINK_RE = /^\[([^\]]+)\]\(([^)]+)\)/;
4831
5611
  function parseIndexingLabel(content) {
@@ -4880,10 +5660,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4880
5660
  var list = Array.isArray(messages) ? messages : [];
4881
5661
  var liveIndexKeys = opts && opts.liveIndexKeys || {};
4882
5662
  var liveIndexChecked = !!(opts && opts.liveIndexChecked);
5663
+ var doneKeys = opts && opts.doneKeys || {};
4883
5664
  var stoppedIndexIds = opts && opts.stoppedIndexIds || {};
4884
5665
  var windowedIndexing = opts && opts.windowedIndexing !== void 0 ? !!opts.windowedIndexing : windowedIndexingEnabled();
4885
5666
  var hasMoreHistory = !!(opts && opts.hasMoreHistory);
4886
5667
  var loadingOlderHistory = !!(opts && opts.loadingOlderHistory);
5668
+ var stubPlatform = opts && opts.stubPlatform;
4887
5669
  var groups = {};
4888
5670
  var order = [];
4889
5671
  var runOfIndex = new Array(list.length);
@@ -5056,11 +5838,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5056
5838
  } else if (grp.driver === "client") {
5057
5839
  grp.finished = sawComplete || grp.status === "error" || grp.passCount >= MAX_INDEXING_RESUME_PASSES;
5058
5840
  } else {
5059
- grp.finished = !newestRunOfKey[order[oi]] || liveIndexChecked && !liveIndexKeys[grp.key];
5841
+ grp.finished = !newestRunOfKey[order[oi]] || !!doneKeys[grp.key] && !liveIndexKeys[grp.key] || liveIndexChecked && !liveIndexKeys[grp.key];
5060
5842
  }
5061
5843
  if (grp.status !== "done") {
5062
5844
  grp.resolving = false;
5063
- } else if (grp.mayHaveOlder && loadingOlderHistory && !liveIndexKeys[grp.key] && newestRunOfKey[order[oi]]) {
5845
+ } else if (grp.mayHaveOlder && loadingOlderHistory && !liveIndexKeys[grp.key] && !doneKeys[grp.key] && newestRunOfKey[order[oi]]) {
5064
5846
  grp.resolving = true;
5065
5847
  grp.resolvingReason = "history";
5066
5848
  } else if (!grp.finished && grp.driver === "worker" && !liveIndexChecked && !liveIndexKeys[grp.key]) {
@@ -5070,14 +5852,126 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5070
5852
  grp.resolving = false;
5071
5853
  }
5072
5854
  }
5855
+ var stubList = [];
5856
+ var runStubs = opts && opts.runStubs;
5857
+ if (runStubs) {
5858
+ var coveredPaths = {};
5859
+ var coveredPathlessNames = {};
5860
+ for (var ci = 0; ci < order.length; ci++) {
5861
+ var cg = groups[order[ci]];
5862
+ if (cg.path) {
5863
+ coveredPaths[cg.path] = true;
5864
+ if (cg.key) coveredPaths[cg.key] = true;
5865
+ } else if (cg.name) coveredPathlessNames[cg.name] = true;
5866
+ else if (cg.key) coveredPaths[cg.key] = true;
5867
+ }
5868
+ var now = opts && typeof opts.now === "number" ? opts.now : Date.now();
5869
+ var stubClearedAt = opts && typeof opts.stubClearedAt === "number" && opts.stubClearedAt > 0 ? opts.stubClearedAt : 0;
5870
+ for (var sp in runStubs) {
5871
+ var rec = runStubs[sp];
5872
+ if (!sp || !rec || !rec.status || coveredPaths[sp]) continue;
5873
+ var fname = rec.filename || sp.split("/").pop() || sp;
5874
+ if (coveredPathlessNames[fname]) continue;
5875
+ if (stubPlatform && rec.platform && rec.platform !== stubPlatform) continue;
5876
+ var live = !!liveIndexKeys[sp] || !!liveIndexKeys[fname];
5877
+ var recWhen = typeof rec.finished === "number" ? rec.finished : typeof rec.started === "number" ? rec.started : void 0;
5878
+ if (stubClearedAt && !live && recWhen !== void 0 && recWhen <= stubClearedAt) continue;
5879
+ var st = "active";
5880
+ var fin = false;
5881
+ var res = false;
5882
+ var reason;
5883
+ if (!live) {
5884
+ if (rec.status === "done" || doneKeys[sp] || doneKeys[fname]) {
5885
+ st = "done";
5886
+ fin = true;
5887
+ } else if (rec.status === "error") {
5888
+ st = "error";
5889
+ fin = true;
5890
+ } else if (rec.status === "cancelled") {
5891
+ st = "cancelled";
5892
+ fin = true;
5893
+ } else if (liveIndexChecked) {
5894
+ st = "done";
5895
+ fin = true;
5896
+ } else if (typeof rec.started === "number" && now - rec.started > RUN_RECORD_WORKING_STALE_MS) {
5897
+ st = "error";
5898
+ fin = true;
5899
+ } else {
5900
+ res = true;
5901
+ reason = "status";
5902
+ }
5903
+ }
5904
+ var sg = {
5905
+ key: sp,
5906
+ // ONE identity for the run whether it renders from the record or
5907
+ // from its loaded passes: the views key the DOM off runKey, so a
5908
+ // 'stub:'-prefixed key meant every handoff was an unmount plus a
5909
+ // remount somewhere else. Named after the record's start, which
5910
+ // the real group below reuses when it has one.
5911
+ runKey: "run:" + sp + "#" + (typeof rec.started === "number" ? rec.started : "n"),
5912
+ name: fname,
5913
+ path: sp,
5914
+ mime: void 0,
5915
+ size: void 0,
5916
+ isReindex: false,
5917
+ members: [],
5918
+ passCount: 0,
5919
+ status: st,
5920
+ cancellableIds: [],
5921
+ cancelling: false,
5922
+ stopped: st === "cancelled",
5923
+ mayHaveOlder: hasMoreHistory,
5924
+ anchorIndex: -1,
5925
+ anchorId: "",
5926
+ visibleMembers: [],
5927
+ driver: !isPagedReadFile(fname, void 0) ? "single" : isImageVisionFile(fname, void 0) ? "worker" : windowedIndexing ? "worker" : "client",
5928
+ finished: fin,
5929
+ resolving: res,
5930
+ resolvingReason: reason,
5931
+ stub: true,
5932
+ stubError: rec.error || (st === "error" && !rec.error ? "Indexing did not finish." : void 0)
5933
+ };
5934
+ stubList.push({ started: typeof rec.started === "number" ? rec.started : Infinity, group: sg });
5935
+ }
5936
+ }
5937
+ var suppressAnchor = {};
5938
+ if (runStubs) {
5939
+ for (var ti2 = 0; ti2 < order.length; ti2++) {
5940
+ var tg = groups[order[ti2]];
5941
+ if (!newestRunOfKey[order[ti2]]) continue;
5942
+ var trec = tg.path && runStubs[tg.path] || runStubs[tg.key];
5943
+ if (!trec || typeof trec.started !== "number") continue;
5944
+ if (stubPlatform && trec.platform && trec.platform !== stubPlatform) continue;
5945
+ suppressAnchor[order[ti2]] = true;
5946
+ tg.runKey = "run:" + (tg.path || tg.key) + "#" + trec.started;
5947
+ stubList.push({ started: trec.started, group: tg });
5948
+ }
5949
+ }
5950
+ stubList.sort(function(a, b) {
5951
+ return a.started - b.started;
5952
+ });
5073
5953
  var out = [];
5954
+ var si = 0;
5074
5955
  for (var j = 0; j < list.length; j++) {
5956
+ var mts = list[j] && typeof list[j]._ts === "number" ? list[j]._ts : void 0;
5957
+ if (mts !== void 0) {
5958
+ while (si < stubList.length && stubList[si].started <= mts) {
5959
+ out.push({ kind: "indexing", group: stubList[si].group, index: -1 - si });
5960
+ si++;
5961
+ }
5962
+ }
5075
5963
  var r = runOfIndex[j];
5076
5964
  if (r === void 0) {
5077
5965
  out.push({ kind: "message", msg: list[j], index: j });
5078
5966
  continue;
5079
5967
  }
5080
- if (groups[r].anchorIndex === j) out.push({ kind: "indexing", group: groups[r], index: j });
5968
+ if (groups[r].anchorIndex === j && !suppressAnchor[r]) {
5969
+ out.push({ kind: "indexing", group: groups[r], index: j });
5970
+ }
5971
+ }
5972
+ while (si < stubList.length) {
5973
+ out.push({ kind: "indexing", group: stubList[si].group, index: -1 - si });
5974
+ si++;
5081
5975
  }
5082
5976
  return out;
5083
5977
  }
@@ -5086,7 +5980,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5086
5980
  (function() {
5087
5981
  var MCP_PROD = "https://mcp.broadwayinc.computer";
5088
5982
  var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
5089
- var BQ_VERSION = "1.8.6" ;
5983
+ var BQ_VERSION = "1.8.8" ;
5090
5984
  var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
5091
5985
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
5092
5986
  var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
@@ -6996,6 +7890,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6996
7890
  autoGrowInput(inputEl);
6997
7891
  }
6998
7892
  updateComposerControls();
7893
+ CS.drafting = false;
7894
+ syncDraftingIndicator();
6999
7895
  if (!hasAttachments) {
7000
7896
  session.dispatchComposedMessage(text, false);
7001
7897
  return;
@@ -7079,7 +7975,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
7079
7975
  }
7080
7976
  });
7081
7977
  }
7082
- function parseMsgPartsHtml(content) {
7978
+ function parseMsgPartsHtml(content, opts) {
7979
+ var noPreviews = !!(opts && opts.imagePreviews === false);
7083
7980
  var placeholderHtml = [];
7084
7981
  var PH = function(idx) {
7085
7982
  return "\uE000BQ" + idx + "\uE001";
@@ -7107,7 +8004,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
7107
8004
  codeMasks.push(match);
7108
8005
  return "\uE002C" + idx + "\uE003";
7109
8006
  });
7110
- var previewsLeft = IMAGE_PREVIEWS_PER_MESSAGE;
8007
+ var previewsLeft = noPreviews ? 0 : IMAGE_PREVIEWS_PER_MESSAGE;
7111
8008
  var linkRe = createInlineLinkRegex();
7112
8009
  working = working.replace(linkRe, function(full) {
7113
8010
  var args = Array.prototype.slice.call(arguments, 1, 7);
@@ -7218,9 +8115,118 @@ Index the REMAINING windows - one record per row/item, looking at any page image
7218
8115
  }
7219
8116
  function deleteFileIndexRecordDb(storagePath) {
7220
8117
  if (!storagePath || !S.skapi || typeof S.skapi.deleteRecords !== "function") return Promise.resolve();
8118
+ var doneDelete = S.skapi.deleteRecords({ service: S.projectId, unique_id: indexDoneUniqueId(storagePath) }).catch(function() {
8119
+ });
8120
+ var runDelete = S.skapi.deleteRecords({ service: S.projectId, unique_id: runIndexUniqueId(storagePath) }).catch(function() {
8121
+ });
7221
8122
  return S.skapi.deleteRecords({ service: S.projectId, unique_id: "src::" + storagePath }).catch(function() {
8123
+ }).then(function() {
8124
+ return doneDelete;
8125
+ }).then(function() {
8126
+ return runDelete;
8127
+ });
8128
+ }
8129
+ function mintIndexDoneMarkerDb(service, storagePath) {
8130
+ if (!service || !storagePath || !S.skapi || typeof S.skapi.postRecord !== "function") return Promise.resolve();
8131
+ return Promise.resolve(S.skapi.postRecord(null, {
8132
+ service,
8133
+ unique_id: indexDoneUniqueId(storagePath),
8134
+ table: { name: "__INDEXING__", access_group: "authorized" },
8135
+ reference: "src::" + storagePath,
8136
+ data: { source: storagePath, completed_at: Date.now() }
8137
+ })).catch(function(err) {
8138
+ var msg = String(err && err.message || err || "");
8139
+ if (msg.indexOf("is already taken") === -1) {
8140
+ console.warn("[bunnyquery] mintIndexDoneMarker failed (non-fatal)", storagePath, msg);
8141
+ }
7222
8142
  });
7223
8143
  }
8144
+ function upsertIndexRunRecordDb(service, storagePath, patch) {
8145
+ if (!service || !storagePath || !patch || !patch.status) return Promise.resolve();
8146
+ if (!S.skapi || typeof S.skapi.postRecord !== "function") return Promise.resolve();
8147
+ var uid = runIndexUniqueId(storagePath);
8148
+ var TERMINAL = { done: true, error: true, cancelled: true };
8149
+ function patchData(base) {
8150
+ var d = {};
8151
+ for (var k in base || {}) d[k] = base[k];
8152
+ d.source = storagePath;
8153
+ d.status = patch.status;
8154
+ if (patch.filename) d.filename = patch.filename;
8155
+ if (typeof patch.started === "number") d.started = patch.started;
8156
+ if (typeof patch.finished === "number") d.finished = patch.finished;
8157
+ if (patch.error) d.error = patch.error;
8158
+ if (patch.queue) d.queue = patch.queue;
8159
+ if (patch.platform) d.platform = patch.platform;
8160
+ return d;
8161
+ }
8162
+ function createWith(reference) {
8163
+ var cfg = {
8164
+ service,
8165
+ unique_id: uid,
8166
+ table: { name: "__INDEXING__", access_group: "authorized" },
8167
+ data: patchData(null)
8168
+ };
8169
+ if (reference) cfg.reference = "src::" + storagePath;
8170
+ return S.skapi.postRecord(null, cfg);
8171
+ }
8172
+ function lookup() {
8173
+ return Promise.resolve(S.skapi.getRecords({ service, unique_id: uid })).then(function(found) {
8174
+ return found && found.list && found.list[0] || null;
8175
+ }).catch(function() {
8176
+ return null;
8177
+ });
8178
+ }
8179
+ function updateExisting(rec) {
8180
+ var existing = rec.data || {};
8181
+ if (patch.status === "working" && TERMINAL[String(existing.status)]) {
8182
+ var endedAt = typeof existing.finished === "number" ? existing.finished : typeof existing.started === "number" ? existing.started : 0;
8183
+ if (!(typeof patch.started === "number" && patch.started > endedAt)) return Promise.resolve(null);
8184
+ }
8185
+ if (patch.status !== "working" && String(existing.status) === patch.status) return Promise.resolve(null);
8186
+ return Promise.resolve(S.skapi.postRecord(null, {
8187
+ service,
8188
+ record_id: rec.record_id,
8189
+ data: patchData(existing)
8190
+ }));
8191
+ }
8192
+ function settleAsUpdate() {
8193
+ return lookup().then(function(rec) {
8194
+ if (rec && rec.record_id) return updateExisting(rec);
8195
+ return null;
8196
+ }).catch(function(err) {
8197
+ console.warn("[bunnyquery] upsertIndexRunRecord update failed (non-fatal)", storagePath, String(err && err.message || err || ""));
8198
+ });
8199
+ }
8200
+ function createChain() {
8201
+ return Promise.resolve(createWith(true)).catch(function(err) {
8202
+ var msg = String(err && err.message || err || "");
8203
+ if (msg.indexOf("is already taken") === -1) {
8204
+ return ensureFileIndexRecordDb(storagePath).then(function() {
8205
+ return createWith(true);
8206
+ }).catch(function(errRef) {
8207
+ var msgRef = String(errRef && errRef.message || errRef || "");
8208
+ if (msgRef.indexOf("is already taken") !== -1) return settleAsUpdate();
8209
+ return Promise.resolve(createWith(false)).catch(function(err2) {
8210
+ var msg2 = String(err2 && err2.message || err2 || "");
8211
+ if (msg2.indexOf("is already taken") === -1) {
8212
+ console.warn("[bunnyquery] upsertIndexRunRecord create failed (non-fatal)", storagePath, msg2);
8213
+ return null;
8214
+ }
8215
+ return settleAsUpdate();
8216
+ });
8217
+ });
8218
+ }
8219
+ return settleAsUpdate();
8220
+ });
8221
+ }
8222
+ if (patch.status !== "working") {
8223
+ return lookup().then(function(rec) {
8224
+ if (rec && rec.record_id) return updateExisting(rec);
8225
+ return createChain();
8226
+ });
8227
+ }
8228
+ return createChain();
8229
+ }
7224
8230
  function ensureFileIndexRecordDb(storagePath, meta) {
7225
8231
  if (!storagePath || !S.skapi || typeof S.skapi.postRecord !== "function") return Promise.resolve();
7226
8232
  return Promise.resolve(S.skapi.postRecord(null, {
@@ -7255,7 +8261,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
7255
8261
  if (cdn === false && opts.browserCache) {
7256
8262
  reqOpts.method = "get";
7257
8263
  reqOpts.stableGateway = true;
7258
- if (opts.refresh) reqOpts.revalidate = true;
8264
+ body.nocache = previewMintCacheToken(opts.refresh);
7259
8265
  body.browser_cache = opts.browserCache;
7260
8266
  var uid = S.user && S.user.user_id;
7261
8267
  if (uid) body.uid = uid;
@@ -7314,9 +8320,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
7314
8320
  function currentInputTokenBudget() {
7315
8321
  var platform = S.aiPlatform;
7316
8322
  if (platform !== "claude" && platform !== "openai") return 0;
7317
- var contextWindow = getContextWindow(platform, S.aiModel);
7318
- var contextBased = Math.max(MIN_INPUT_TOKEN_BUDGET, contextWindow - OUTPUT_TOKEN_RESERVE - TOOL_AND_RESPONSE_BUFFER);
7319
- return platform === "claude" ? Math.min(contextBased, CLAUDE_PER_REQUEST_INPUT_CAP) : contextBased;
8323
+ return getInputTokenBudget(platform, S.aiModel, S.projectId);
7320
8324
  }
7321
8325
  function formatTokenCount(tokens) {
7322
8326
  if (tokens >= 1e3) {
@@ -7593,21 +8597,25 @@ Index the REMAINING windows - one record per row/item, looking at any page image
7593
8597
  shown.forEach(function(att) {
7594
8598
  var isFolder = att.kind === "folder";
7595
8599
  var clickable = att.status === "done" && !isFolder && !!att.uploadedUrl;
8600
+ var finalizing = att.status === "uploading" && (att.progress || 0) >= 100;
8601
+ var preparing = att.status === "uploading" && att.progress == null;
7596
8602
  var cls = "bq-attachment";
7597
8603
  if (att.status === "uploading") cls += " is-uploading";
8604
+ if (preparing) cls += " is-preparing";
8605
+ else if (finalizing) cls += " is-finalizing";
7598
8606
  else if (att.status === "error") cls += " is-error";
7599
8607
  else if (att.status === "indexError") cls += " is-index-error";
7600
8608
  else if (att.status === "done") cls += " is-done";
7601
8609
  if (clickable) cls += " is-clickable";
7602
8610
  var chip = h("div", { class: cls });
7603
- if (att.status === "uploading") chip.style.setProperty("--att-progress", (att.progress || 0) + "%");
8611
+ if (att.status === "uploading" && att.progress != null) chip.style.setProperty("--att-progress", att.progress + "%");
7604
8612
  chip.title = att.status === "error" ? "File upload has failed" : att.status === "indexError" ? "File indexing failed" : clickable ? "Open " + att.name : isFolder ? att.name + "/ \u2014 " + (att.files ? att.files.length : 0) + " file(s)" : att.name;
7605
8613
  if (clickable) chip.addEventListener("click", function() {
7606
8614
  window.open(att.uploadedUrl, "_blank", "noopener,noreferrer");
7607
8615
  });
7608
8616
  chip.appendChild(h("span", { class: "bq-attachment-icon", html: isFolder ? FOLDER_ICON_SVG : FILE_ICON_SVG }));
7609
8617
  chip.appendChild(h("span", { class: "bq-attachment-name", text: att.name, title: att.name }));
7610
- var meta = att.status === "error" ? "(Failed)" : att.status === "indexError" ? "(Error)" : att.status === "uploading" ? (att.progress || 0) + "%" : isFolder ? "(" + (att.files ? att.files.length : 0) + ")" : formatBytes(att.file ? att.file.size : att.size);
8618
+ var meta = att.status === "error" ? "(Failed)" : att.status === "indexError" ? "(Error)" : preparing ? "Preparing" : finalizing ? "Finalizing" : att.status === "uploading" ? att.progress + "%" : isFolder ? "(" + (att.files ? att.files.length : 0) + ")" : formatBytes(att.file ? att.file.size : att.size);
7611
8619
  chip.appendChild(h("span", { class: "bq-attachment-meta", text: meta }));
7612
8620
  if (clickable) chip.appendChild(h("span", { class: "bq-attachment-arrow", text: "\u2197" }));
7613
8621
  if (att.status !== "uploading" && att.status !== "done") {
@@ -7772,10 +8780,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
7772
8780
  return getTemporaryUrlDb(remotePath, EXPIRED_LINK_REFRESH_EXPIRES_SECONDS, false);
7773
8781
  }
7774
8782
  var unavailableRepaintQueued = false;
7775
- function markLinkUnavailable(key) {
7776
- if (!key || unavailableLinkMap[key]) return;
7777
- unavailableLinkMap[key] = true;
7778
- if (!refreshedLinkExpiryTimer) scheduleNextLinkExpiryBoundary();
8783
+ function queueUnavailableRepaint() {
7779
8784
  if (unavailableRepaintQueued) return;
7780
8785
  unavailableRepaintQueued = true;
7781
8786
  setTimeout(function() {
@@ -7783,18 +8788,37 @@ Index the REMAINING windows - one record per row/item, looking at any page image
7783
8788
  renderMessages();
7784
8789
  }, 0);
7785
8790
  }
8791
+ function markLinkUnavailable(key) {
8792
+ if (!key || unavailableLinkMap[key]) return;
8793
+ unavailableLinkMap[key] = true;
8794
+ if (!refreshedLinkExpiryTimer) scheduleNextLinkExpiryBoundary();
8795
+ queueUnavailableRepaint();
8796
+ }
8797
+ function clearLinkUnavailable(keys) {
8798
+ var changed = false;
8799
+ for (var i = 0; i < (keys || []).length; i++) {
8800
+ var k = keys[i];
8801
+ if (!k || !unavailableLinkMap[k]) continue;
8802
+ delete unavailableLinkMap[k];
8803
+ changed = true;
8804
+ }
8805
+ if (changed) queueUnavailableRepaint();
8806
+ }
7786
8807
  function imagePreviewCtx() {
7787
8808
  return {
7788
8809
  scope: S.projectId || "default",
7789
8810
  mint: function(remotePath, contentType, refresh) {
7790
- return getTemporaryUrlDb(remotePath, EXPIRED_LINK_REFRESH_EXPIRES_SECONDS, false, contentType, {
8811
+ return getTemporaryUrlDb(remotePath, PREVIEW_URL_EXPIRES_SECONDS, false, contentType, {
7791
8812
  browserCache: PREVIEW_BROWSER_CACHE_SECONDS,
7792
8813
  refresh
7793
8814
  });
7794
8815
  },
7795
8816
  // An image arriving late pushes the conversation down under the
7796
- // viewport. Re-pin only if the user was already at the bottom.
7797
- onLoad: function() {
8817
+ // viewport. Re-pin only if the user was already at the bottom. A
8818
+ // paint is also proof the file is reachable, so it lifts any mark an
8819
+ // earlier failure left on this file's chips.
8820
+ onLoad: function(path) {
8821
+ clearLinkUnavailable(linkUnavailableKeysForPath(path));
7798
8822
  scrollToBottomIfSticky(false);
7799
8823
  },
7800
8824
  // The mint was refused, or the url it minted would not load. Either
@@ -7913,7 +8937,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
7913
8937
  }
7914
8938
  function messagesBoxCanScroll() {
7915
8939
  if (!CS.messagesBox || CS.chatSettingsOpen) return true;
7916
- return CS.messagesBox.scrollHeight - CS.messagesBox.clientHeight > HISTORY_FILL_SLACK_PX;
8940
+ var drafting = CS.draftingEl && CS.draftingEl.parentNode === CS.messagesBox ? CS.draftingEl.offsetHeight : 0;
8941
+ return CS.messagesBox.scrollHeight - drafting - CS.messagesBox.clientHeight > HISTORY_FILL_SLACK_PX;
7917
8942
  }
7918
8943
  function topVisibleRowKey() {
7919
8944
  var box = CS.messagesBox;
@@ -8108,11 +9133,168 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8108
9133
  return indexGroupVerb(group) + " " + nameLabel;
8109
9134
  }
8110
9135
  function indexGroupCount(group) {
9136
+ if (group.stub) return "";
8111
9137
  if (group.passCount <= 1 && !group.mayHaveOlder) return "";
8112
9138
  return group.passCount + (group.mayHaveOlder ? "+" : "") + " passes";
8113
9139
  }
9140
+ var markerSweep = { svc: "", at: 0, gen: 0, done: {}, runs: {}, partial: false, inflight: null };
9141
+ var MARKER_SWEEP_TTL_MS = 3e4;
9142
+ var MARKER_SWEEP_MAX_PAGES = 10;
9143
+ function sweepIndexMarkersDb() {
9144
+ if (!S.skapi || !S.projectId || typeof S.skapi.getRecords !== "function") return Promise.resolve(null);
9145
+ var svc = S.projectId;
9146
+ if (markerSweep.svc === svc && markerSweep.at && Date.now() - markerSweep.at < MARKER_SWEEP_TTL_MS) {
9147
+ return Promise.resolve(markerSweep);
9148
+ }
9149
+ if (markerSweep.inflight) {
9150
+ if (!markerSweep.at) {
9151
+ return markerSweep.inflight.then(function() {
9152
+ return sweepIndexMarkersDb();
9153
+ });
9154
+ }
9155
+ return markerSweep.inflight;
9156
+ }
9157
+ var gen = markerSweep.gen;
9158
+ var done = {};
9159
+ var runs = {};
9160
+ var partial = false;
9161
+ function page(fetchMore, n) {
9162
+ return Promise.resolve(S.skapi.getRecords(
9163
+ { service: svc, table: { name: "__INDEXING__", access_group: "authorized" } },
9164
+ { limit: 1e3, fetchMore, ascending: false }
9165
+ )).then(function(res) {
9166
+ var list = res && res.list || [];
9167
+ for (var i = 0; i < list.length; i++) {
9168
+ var uid = String(list[i] && list[i].unique_id || "");
9169
+ if (uid.indexOf("done::") === 0) {
9170
+ done[uid.slice(6)] = true;
9171
+ } else if (uid.indexOf("run::") === 0) {
9172
+ var path = uid.slice(5);
9173
+ var d = list[i] && list[i].data || {};
9174
+ var st = String(d.status || "");
9175
+ if (path && !runs[path] && (st === "working" || st === "done" || st === "error" || st === "cancelled")) {
9176
+ runs[path] = {
9177
+ status: st,
9178
+ filename: typeof d.filename === "string" ? d.filename : void 0,
9179
+ started: typeof d.started === "number" ? d.started : void 0,
9180
+ finished: typeof d.finished === "number" ? d.finished : void 0,
9181
+ error: typeof d.error === "string" ? d.error : void 0,
9182
+ platform: d.platform === "claude" || d.platform === "openai" ? d.platform : void 0,
9183
+ owner: list[i] && typeof list[i].user_id === "string" ? list[i].user_id : void 0
9184
+ };
9185
+ }
9186
+ }
9187
+ }
9188
+ if (res && res.endOfList === false) {
9189
+ if (n < MARKER_SWEEP_MAX_PAGES - 1) return page(true, n + 1);
9190
+ partial = true;
9191
+ }
9192
+ return null;
9193
+ });
9194
+ }
9195
+ var p = page(false, 0).then(function() {
9196
+ if (S.projectId !== svc || markerSweep.gen !== gen) return markerSweep;
9197
+ markerSweep.svc = svc;
9198
+ markerSweep.at = Date.now();
9199
+ markerSweep.done = done;
9200
+ markerSweep.runs = runs;
9201
+ markerSweep.partial = partial;
9202
+ return markerSweep;
9203
+ });
9204
+ markerSweep.inflight = p;
9205
+ p.then(function() {
9206
+ markerSweep.inflight = null;
9207
+ }, function() {
9208
+ markerSweep.inflight = null;
9209
+ });
9210
+ return p;
9211
+ }
9212
+ function invalidateIndexMarkerSweep() {
9213
+ markerSweep.at = 0;
9214
+ markerSweep.gen++;
9215
+ }
9216
+ var STUB_RECHECK_MS = 3e4;
9217
+ var STUB_RECHECK_MAX_ROUNDS = 5;
9218
+ var stubRecheckTimer = null;
9219
+ var stubRecheckSig = "";
9220
+ var stubRecheckRounds = 0;
9221
+ var markerSweepSettled = false;
9222
+ function armStubRecheck() {
9223
+ if (stubRecheckTimer !== null) return;
9224
+ stubRecheckTimer = setTimeout(function() {
9225
+ stubRecheckTimer = null;
9226
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
9227
+ stubRecheckRounds++;
9228
+ void refreshIndexMarkers();
9229
+ }, STUB_RECHECK_MS);
9230
+ }
9231
+ function maybeArmStubRecheck() {
9232
+ try {
9233
+ if (markerSweep.svc !== S.projectId) return;
9234
+ var lk = session && session.getLiveIndexState().keys || {};
9235
+ var sig = [];
9236
+ for (var pth in markerSweep.runs) {
9237
+ var r = markerSweep.runs[pth];
9238
+ if (!r || r.status !== "working" || lk[pth]) continue;
9239
+ var fn = r.filename || pth.split("/").pop() || pth;
9240
+ if (markerSweep.done[pth] || markerSweep.done[fn]) continue;
9241
+ sig.push(pth);
9242
+ }
9243
+ if (!sig.length) {
9244
+ stubRecheckSig = "";
9245
+ stubRecheckRounds = 0;
9246
+ if (stubRecheckTimer !== null) {
9247
+ clearTimeout(stubRecheckTimer);
9248
+ stubRecheckTimer = null;
9249
+ }
9250
+ return;
9251
+ }
9252
+ var s = sig.sort().join("|");
9253
+ if (s !== stubRecheckSig) {
9254
+ stubRecheckSig = s;
9255
+ stubRecheckRounds = 0;
9256
+ }
9257
+ if (stubRecheckRounds >= STUB_RECHECK_MAX_ROUNDS) return;
9258
+ armStubRecheck();
9259
+ } catch (e) {
9260
+ }
9261
+ }
9262
+ function refreshIndexMarkers(invalidate) {
9263
+ if (invalidate) invalidateIndexMarkerSweep();
9264
+ return sweepIndexMarkersDb().then(function(res) {
9265
+ markerSweepSettled = true;
9266
+ if (res) {
9267
+ maybeArmStubRecheck();
9268
+ renderMessages();
9269
+ }
9270
+ return res;
9271
+ }).catch(function() {
9272
+ markerSweepSettled = true;
9273
+ renderMessages();
9274
+ return null;
9275
+ });
9276
+ }
8114
9277
  function displayListOptions() {
8115
9278
  var liveIndex = session.getLiveIndexState();
9279
+ var fresh = markerSweep.svc === S.projectId;
9280
+ var stubs = void 0;
9281
+ if (fresh) {
9282
+ stubs = {};
9283
+ var myId = S.user && S.user.user_id || "";
9284
+ for (var rp in markerSweep.runs) {
9285
+ var rr = markerSweep.runs[rp];
9286
+ if (rr && rr.owner && myId && rr.owner !== myId) continue;
9287
+ stubs[rp] = {
9288
+ status: rr.status,
9289
+ filename: rr.filename,
9290
+ started: rr.started,
9291
+ finished: rr.finished,
9292
+ error: rr.error,
9293
+ platform: rr.platform,
9294
+ owner: rr.owner
9295
+ };
9296
+ }
9297
+ }
8116
9298
  return {
8117
9299
  hasMoreHistory: !CS.historyEndOfList,
8118
9300
  // Older pages coming in RIGHT NOW. CS.historyFilling, not just the
@@ -8125,12 +9307,22 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8125
9307
  // Runs the user stopped. A stop that landed on a RUNNING pass leaves no
8126
9308
  // cancelled bubble behind (that pass finishes and answers normally), so
8127
9309
  // without this the row reports the stop as a finished "Indexed".
8128
- stoppedIndexIds: session.getStoppedIndexIds()
9310
+ stoppedIndexIds: session.getStoppedIndexIds(),
9311
+ // Durable completion markers + run records (one sweep, see above).
9312
+ // doneKeys settle worker-run greens without a queue round trip;
9313
+ // runStubs paint rows for runs whose passes are not loaded yet.
9314
+ doneKeys: fresh ? markerSweep.done : void 0,
9315
+ runStubs: stubs,
9316
+ // Records are service-wide and horizon-blind; without this every
9317
+ // "Clear chat history" resurrected one row per indexed file.
9318
+ stubClearedAt: getClearedAt(),
9319
+ // A run:: record is per FILE; a chat is per (project, PLATFORM).
9320
+ stubPlatform: S.aiPlatform === "claude" || S.aiPlatform === "openai" ? S.aiPlatform : void 0
8129
9321
  };
8130
9322
  }
8131
9323
  var stopIndexState = { runKey: "", fileKey: "", handle: null };
8132
9324
  function indexGroupStoppable(group) {
8133
- return !!group && !group.finished && !group.resolving && !group.stopped && !group.cancelling;
9325
+ return !!group && !group.stub && !group.finished && !group.resolving && !group.stopped && !group.cancelling;
8134
9326
  }
8135
9327
  function findCancellableIndexGroup(runKey, fileKey) {
8136
9328
  if (!runKey) return null;
@@ -8209,10 +9401,71 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8209
9401
  }
8210
9402
  function toggleIndexGroup(key) {
8211
9403
  if (CS.indexGroupsOpen[key]) delete CS.indexGroupsOpen[key];
8212
- else CS.indexGroupsOpen[key] = true;
9404
+ else {
9405
+ CS.indexGroupsOpen[key] = true;
9406
+ hydrateCompactIndexGroup(key);
9407
+ void loadIndexGroupHistory(key);
9408
+ }
8213
9409
  renderMessages();
8214
9410
  ensureHistoryFillsViewport();
8215
9411
  }
9412
+ var INDEX_GROUP_FETCH_MAX_PAGES = 40;
9413
+ var indexGroupFetching = {};
9414
+ function groupNeedsHistory(key) {
9415
+ if (CS.historyEndOfList) return false;
9416
+ try {
9417
+ var entries = buildChatDisplayList(CS.messages, displayListOptions());
9418
+ for (var i = 0; i < entries.length; i++) {
9419
+ if (entries[i].kind !== "indexing" || entries[i].group.key !== key) continue;
9420
+ return !!(entries[i].group.stub || entries[i].group.mayHaveOlder);
9421
+ }
9422
+ } catch (e) {
9423
+ }
9424
+ return false;
9425
+ }
9426
+ function loadIndexGroupHistory(key) {
9427
+ if (indexGroupFetching[key]) return Promise.resolve();
9428
+ if (!groupNeedsHistory(key)) return Promise.resolve();
9429
+ indexGroupFetching[key] = true;
9430
+ renderMessages();
9431
+ var pages = 0, waits = 0;
9432
+ function step() {
9433
+ if (!CS.indexGroupsOpen[key]) return null;
9434
+ if (!groupNeedsHistory(key)) return null;
9435
+ if (pages >= INDEX_GROUP_FETCH_MAX_PAGES) return null;
9436
+ if (CS.loadingOlderHistory || CS.historyFilling || session.state.bgHistoryLoading) {
9437
+ if (++waits > 240) return null;
9438
+ return new Promise(function(r) {
9439
+ setTimeout(r, 250);
9440
+ }).then(step);
9441
+ }
9442
+ pages++;
9443
+ return fetchOlderHistoryIfNeeded().then(step);
9444
+ }
9445
+ return Promise.resolve(step()).catch(function() {
9446
+ }).then(function() {
9447
+ delete indexGroupFetching[key];
9448
+ hydrateCompactIndexGroup(key);
9449
+ renderMessages();
9450
+ });
9451
+ }
9452
+ function hydrateCompactIndexGroup(key) {
9453
+ try {
9454
+ var entries = buildChatDisplayList(session.state.messages, displayListOptions());
9455
+ for (var i = 0; i < entries.length; i++) {
9456
+ var en = entries[i];
9457
+ if (en.kind !== "indexing" || en.group.key !== key) continue;
9458
+ var ids = [];
9459
+ for (var mi = 0; mi < en.group.members.length; mi++) {
9460
+ var m = en.group.members[mi].msg;
9461
+ if (m && m._compact && m.role === "assistant" && m._serverItemId && !m.isError && !m.isPending) ids.push(m._serverItemId);
9462
+ }
9463
+ if (ids.length) session.hydrateCompactItems(ids);
9464
+ return;
9465
+ }
9466
+ } catch (e) {
9467
+ }
9468
+ }
8216
9469
  function buildIndexGroupEl(group, isOpen) {
8217
9470
  var cls = ["bq-index-group"];
8218
9471
  if (group.resolving) cls.push("is-resolving");
@@ -8223,7 +9476,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8223
9476
  var label = h(
8224
9477
  "span",
8225
9478
  { class: "bq-index-label" },
8226
- h("span", { class: "bq-md", html: parseMsgPartsHtml(indexGroupLabel(group)) })
9479
+ h("span", { class: "bq-md", html: parseMsgPartsHtml(indexGroupLabel(group), { imagePreviews: false }) })
8227
9480
  );
8228
9481
  label.addEventListener("click", function(e) {
8229
9482
  if (e.target && e.target.closest && e.target.closest("a")) e.stopPropagation();
@@ -8267,6 +9520,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8267
9520
  h("span", { class: "bq-index-icon", html: indexGroupIcon(group) }),
8268
9521
  label,
8269
9522
  indexGroupCount(group) ? h("span", { class: "bq-index-count", text: indexGroupCount(group) }) : null,
9523
+ // Spinning arrows while this row's history is being paged in —
9524
+ // separate from the status icon, so a green (done) row spins too.
9525
+ indexGroupFetching[group.key] ? h("span", { class: "bq-index-fetch", html: INDEX_ICON_ACTIVE, title: "Fetching this file's indexing history" }) : null,
8270
9526
  cancelBtn,
8271
9527
  h("span", { class: "bq-index-chevron", text: "\u25B6" })
8272
9528
  );
@@ -8277,10 +9533,23 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8277
9533
  text: "Could not stop this file: " + group.cancelError
8278
9534
  }));
8279
9535
  }
8280
- if (isOpen && group.mayHaveOlder) {
9536
+ if (isOpen && indexGroupFetching[group.key]) {
9537
+ el.appendChild(h(
9538
+ "div",
9539
+ { class: "bq-index-note" },
9540
+ h("span", { text: "Loading this file's indexing history" }),
9541
+ h("span", { class: "bq-loader" })
9542
+ ));
9543
+ } else if (isOpen && group.mayHaveOlder) {
9544
+ var loadingNow = group.resolvingReason === "history" || group.stub && session.state.bgHistoryLoading;
8281
9545
  el.appendChild(h("div", {
8282
9546
  class: "bq-index-note",
8283
- text: "Earlier passes of this file are further back in the conversation. " + (group.resolvingReason === "history" ? "Loading them now." : "Scroll up to load them.")
9547
+ text: "Earlier passes of this file are further back in the conversation. " + (loadingNow ? "Loading them now." : "Scroll up to load them.")
9548
+ }));
9549
+ } else if (isOpen && !group.visibleMembers.length) {
9550
+ el.appendChild(h("div", {
9551
+ class: "bq-index-note",
9552
+ text: "This file's indexing steps aren't in this chat's history. They may belong to another chat or platform, or the conversation was cleared."
8284
9553
  }));
8285
9554
  }
8286
9555
  return el;
@@ -8343,32 +9612,99 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8343
9612
  }
8344
9613
  box.scrollTop = anchor.scrollTop;
8345
9614
  }
9615
+ function syncDraftingIndicator() {
9616
+ if (!CS.messagesBox) return;
9617
+ if (CS.drafting && !CS.chatSettingsOpen) {
9618
+ if (!CS.draftingEl) {
9619
+ CS.draftingEl = h(
9620
+ "div",
9621
+ { class: "bq-message is-user bq-user-drafting", "aria-hidden": "true" },
9622
+ h("div", { class: "bq-bubble" }, h("span", { class: "bq-loader" }))
9623
+ );
9624
+ }
9625
+ CS.messagesBox.appendChild(CS.draftingEl);
9626
+ } else if (CS.draftingEl && CS.draftingEl.parentNode) {
9627
+ CS.draftingEl.parentNode.removeChild(CS.draftingEl);
9628
+ }
9629
+ }
8346
9630
  function renderMessages() {
8347
9631
  syncStopIndexModal();
9632
+ var _lk = session.getLiveIndexState().keys || {};
9633
+ var _lc = 0;
9634
+ for (var _k in _lk) _lc++;
9635
+ if (CS._lastLiveKeyCount > 0 && _lc < CS._lastLiveKeyCount) void refreshIndexMarkers(true);
9636
+ CS._lastLiveKeyCount = _lc;
8348
9637
  if (!CS.messagesBox) return;
8349
9638
  if (CS.chatSettingsOpen) return;
8350
9639
  var anchor = captureScrollAnchor();
8351
9640
  clear(CS.messagesBox);
8352
9641
  CS.messageEls = [];
8353
9642
  if (CS.loadingOlderHistory) CS.messagesBox.appendChild(historyLoadingEl(false));
9643
+ else if (session.state.bgHistoryLoading) {
9644
+ CS.messagesBox.appendChild(h(
9645
+ "div",
9646
+ { class: "bq-history-loading" },
9647
+ h("span", { text: "Loading indexing history" }),
9648
+ h("span", { class: "bq-loader" })
9649
+ ));
9650
+ }
8354
9651
  if (!CS.messages.length) {
8355
9652
  if (CS.loadingHistory && !CS.loadingOlderHistory) {
8356
9653
  CS.messagesBox.appendChild(historyLoadingEl(true));
9654
+ syncDraftingIndicator();
8357
9655
  return;
8358
9656
  }
8359
- var greet = h(
8360
- "div",
8361
- { class: "bq-message is-assistant bq-empty-greeting" },
8362
- h(
9657
+ var emptyStubEls = [];
9658
+ try {
9659
+ var emptyEntries = buildChatDisplayList([], displayListOptions());
9660
+ for (var ge = 0; ge < emptyEntries.length; ge++) {
9661
+ if (emptyEntries[ge].kind !== "indexing") continue;
9662
+ var sg = emptyEntries[ge].group;
9663
+ emptyStubEls.push(buildIndexGroupEl(sg, !!CS.indexGroupsOpen[sg.key]));
9664
+ }
9665
+ } catch (e) {
9666
+ }
9667
+ if (!emptyStubEls.length && !session.state.bgHistoryLoading && markerSweepSettled) {
9668
+ CS.messagesBox.appendChild(h(
8363
9669
  "div",
8364
- { class: "bq-bubble" },
8365
- document.createTextNode("Hi! Ask me anything about " + (S.serviceName ? '"' + S.serviceName + '"' : "your project") + ".")
8366
- )
8367
- );
8368
- CS.messagesBox.appendChild(greet);
9670
+ { class: "bq-message is-assistant bq-empty-greeting" },
9671
+ h(
9672
+ "div",
9673
+ { class: "bq-bubble" },
9674
+ document.createTextNode("Hi! Ask me anything about " + (S.serviceName ? '"' + S.serviceName + '"' : "your project") + ".")
9675
+ )
9676
+ ));
9677
+ }
9678
+ for (var gse = 0; gse < emptyStubEls.length; gse++) CS.messagesBox.appendChild(emptyStubEls[gse]);
9679
+ syncDraftingIndicator();
8369
9680
  return;
8370
9681
  }
8371
9682
  var rows = buildChatDisplayList(CS.messages, displayListOptions());
9683
+ try {
9684
+ if (markerSweep.svc === S.projectId) {
9685
+ var moSeen = S._mintObserved || (S._mintObserved = {});
9686
+ for (var moi = 0; moi < rows.length; moi++) {
9687
+ var moe = rows[moi];
9688
+ if (moe.kind !== "indexing" || moe.group.stub) continue;
9689
+ var mog = moe.group;
9690
+ if (!mog.path || !mog.finished || mog.status !== "done" || mog.resolving) continue;
9691
+ if (!(mog.driver === "single" || markerSweep.done[mog.path])) continue;
9692
+ var morec = markerSweep.runs[mog.path];
9693
+ if ((!morec || morec.status === "working") && !moSeen[mog.path]) {
9694
+ moSeen[mog.path] = true;
9695
+ var moFirst = mog.members && mog.members[0] && mog.members[0].msg && mog.members[0].msg._ts;
9696
+ var moLast = mog.members && mog.members.length && mog.members[mog.members.length - 1].msg && mog.members[mog.members.length - 1].msg._ts;
9697
+ var moPatch = { status: "done", finished: typeof moLast === "number" ? moLast : Date.now() };
9698
+ if (typeof moFirst === "number") moPatch.started = moFirst;
9699
+ if (mog.name) moPatch.filename = mog.name;
9700
+ void upsertIndexRunRecordDb(S.projectId, mog.path, moPatch);
9701
+ if (morec) morec.status = "done";
9702
+ else markerSweep.runs[mog.path] = { status: "done" };
9703
+ }
9704
+ }
9705
+ }
9706
+ } catch (e) {
9707
+ }
8372
9708
  rows.forEach(function(row) {
8373
9709
  if (row.kind === "indexing") {
8374
9710
  var isOpen = !!CS.indexGroupsOpen[row.group.key];
@@ -8397,6 +9733,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8397
9733
  CS.messageEls[row.index] = el;
8398
9734
  CS.messagesBox.appendChild(el);
8399
9735
  });
9736
+ syncDraftingIndicator();
8400
9737
  restoreScrollAnchor(anchor);
8401
9738
  hydrateMessageImagePreviews();
8402
9739
  }
@@ -8419,6 +9756,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8419
9756
  CS.sending = false;
8420
9757
  CS.typing = false;
8421
9758
  CS.typingAbort = true;
9759
+ CS.drafting = false;
9760
+ CS.draftingEl = null;
8422
9761
  CS.historyEndOfList = false;
8423
9762
  CS.historyStartKeyHistory = [];
8424
9763
  CS.stickToBottom = true;
@@ -8503,6 +9842,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8503
9842
  renderAttachmentChips();
8504
9843
  scheduleAttachmentOverflowRecompute();
8505
9844
  }
9845
+ var drafting = !!input.value.trim();
9846
+ if (drafting !== CS.drafting) {
9847
+ CS.drafting = drafting;
9848
+ syncDraftingIndicator();
9849
+ scrollToBottomIfSticky(false);
9850
+ }
8506
9851
  });
8507
9852
  input.addEventListener("keydown", function(e) {
8508
9853
  if (e.key === "Enter" && !e.shiftKey && !composing) {
@@ -8550,6 +9895,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8550
9895
  return h("div", { class: "bq-meta" }, header, chatArea);
8551
9896
  });
8552
9897
  if (S.aiPlatform === "none") return;
9898
+ void refreshIndexMarkers();
8553
9899
  loadMarked().then(function() {
8554
9900
  renderMessages();
8555
9901
  return session.loadHistory(false, CS.gateRefreshToken);
@@ -8823,6 +10169,19 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8823
10169
  clientSecretRequestHistory: function(p, f) {
8824
10170
  return S.skapi.clientSecretRequestHistory(p, f);
8825
10171
  },
10172
+ // Single-item csr-poll point lookup: how the engine hydrates a
10173
+ // compact history stub's real body when an indexing row expands.
10174
+ csrHistoryItemLookup: function(fullId, service, owner) {
10175
+ return S.skapi.util.request("csr-poll", { id: fullId, service, owner }, { auth: true });
10176
+ },
10177
+ // Durable index markers. Both read S lazily at call time — S.skapi /
10178
+ // S.projectId are not set yet when init() runs.
10179
+ mintIndexDoneMarker: function(info) {
10180
+ void mintIndexDoneMarkerDb(info.service, info.storagePath);
10181
+ },
10182
+ upsertIndexRunRecord: function(info) {
10183
+ void upsertIndexRunRecordDb(info.service, info.storagePath, info.patch);
10184
+ },
8826
10185
  mcpBaseUrl: mcpBaseUrl(),
8827
10186
  poll: 0,
8828
10187
  // Server-driven windowed indexing. Off by default in the engine because the