bunnyquery 1.6.0 → 1.6.2

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
@@ -56,6 +56,9 @@
56
56
  }
57
57
  return _config;
58
58
  }
59
+ function windowedIndexingEnabled() {
60
+ return _config?.windowedIndexing === true;
61
+ }
59
62
  function pollOpt() {
60
63
  const p = _config?.poll;
61
64
  return p === void 0 ? {} : { poll: p };
@@ -149,7 +152,32 @@
149
152
  if (isTextMime(m)) return true;
150
153
  return m.includes("officedocument") || m.includes("opendocument") || m.includes("hwp") || m.includes("epub") || m === "application/msword" || m === "application/vnd.ms-excel" || m === "application/vnd.ms-powerpoint";
151
154
  }
152
- var PAGED_READ_EXTENSIONS = /* @__PURE__ */ new Set(["xls", "xlsx", "xlsm", "ods", "pdf"]);
155
+ var PAGED_READ_EXTENSIONS = /* @__PURE__ */ new Set([
156
+ // grids
157
+ "xls",
158
+ "xlsx",
159
+ "xlsm",
160
+ "ods",
161
+ // delimited text (row-windowed by the layer)
162
+ "csv",
163
+ "tsv",
164
+ "tab",
165
+ // documents
166
+ "pdf",
167
+ "docx",
168
+ "pptx",
169
+ // plain text / data / markup
170
+ "txt",
171
+ "md",
172
+ "markdown",
173
+ "log",
174
+ "json",
175
+ "jsonl",
176
+ "ndjson",
177
+ "xml",
178
+ "yaml",
179
+ "yml"
180
+ ]);
153
181
  function isPagedReadFile(name, mime) {
154
182
  const ext = (name || "").split(".").pop()?.toLowerCase() || "";
155
183
  if (PAGED_READ_EXTENSIONS.has(ext)) return true;
@@ -173,6 +201,16 @@
173
201
  return `{{SKAPI_RENDER::${slug}-${_renderPlaceholderSeq}}}`;
174
202
  }
175
203
  var RENDER_PAGES_PER_WINDOW = 5;
204
+ var _windowPlaceholderSeq = 0;
205
+ function makeWindowPlaceholder(seed) {
206
+ _windowPlaceholderSeq += 1;
207
+ const slug = (seed || "file").replace(/[^a-zA-Z0-9]+/g, "_").slice(-48);
208
+ return `{{SKAPI_WINDOW::${slug}-${_windowPlaceholderSeq}}}`;
209
+ }
210
+ function isWindowedReadFile(name, mime) {
211
+ if (isImageVisionFile(name, mime)) return false;
212
+ return isPagedReadFile(name, mime);
213
+ }
176
214
  function composeUserMessage(text, attachmentUrls) {
177
215
  let composed = text;
178
216
  if (attachmentUrls.length > 0) {
@@ -320,32 +358,58 @@ Read this file with the readFileContent tool, using the storage path above - do
320
358
  }
321
359
  return head + `- temporary URL (fetch this to read the file contents): ${attachment.url}`;
322
360
  }
361
+ var RENDER_FROM_TOKEN = "{{RENDER_FROM}}";
362
+ var WINDOW_CURSOR_TOKEN = RENDER_FROM_TOKEN;
323
363
  function buildIndexingRenderMessage(attachment, placeholder, renderFrom) {
324
364
  const from = Math.max(0, renderFrom || 0);
365
+ if (from > 0) return buildIndexingRenderContinueTemplate(attachment, placeholder, String(from + 1));
366
+ return `A new file has just been uploaded. Index it now.
367
+
368
+ ` + buildRenderMeta(attachment) + `
369
+ This is a PDF. Its pages are delivered to you as RENDERED PAGE IMAGES embedded directly in this message (you do NOT need any tool, URL, or web_fetch to see them). You are shown a WINDOW of pages at a time, starting at page ${from + 1}.
370
+ ` + buildRenderDatafy(placeholder);
371
+ }
372
+ function buildIndexingRenderContinueTemplate(attachment, placeholder, pageLabel = RENDER_FROM_TOKEN) {
325
373
  const src = `src::${attachment.storagePath}`;
326
- const meta = `File metadata:
374
+ return `CONTINUE indexing a PDF whose previous pass did not finish.
375
+
376
+ ` + buildRenderMeta(attachment) + `
377
+ Records for the earlier pages are ALREADY saved (they reference "${src}"). The NEXT window of rendered page images (starting at page ${pageLabel}) is embedded in this message. Datafy each page as before and do NOT re-save pages that are already saved.
378
+ ` + buildRenderDatafy(placeholder);
379
+ }
380
+ function buildRenderMeta(attachment) {
381
+ return `File metadata:
327
382
  - name: ${attachment.name}
328
383
  - storage path: ${attachment.storagePath}
329
384
  ` + (attachment.mime ? `- mime type: ${attachment.mime}
330
385
  ` : "");
331
- const datafy = `
386
+ }
387
+ function buildRenderDatafy(placeholder) {
388
+ return `
332
389
  ${placeholder}
333
390
 
334
391
  LOOK at each rendered page image in this message and DATAFY what it shows: for EVERY page call postRecords and save records - one record per row / table entry / line item visible on the page (or one record for the page if it is prose), capturing every value you can read (OCR the text, read tables cell by cell, describe any photos/diagrams). Use the storage path above for the "src::" unique_id.
335
392
 
336
- The note next to the images tells you whether MORE pages remain after this window. If MORE remain: save this window's records and STOP - do NOT write INDEXING_COMPLETE; another pass shows the next window automatically. Only when the note says this is the LAST window (you have seen the whole file) AND everything is saved, end your message with the token INDEXING_COMPLETE.`;
337
- if (from === 0) {
338
- return `A new file has just been uploaded. Index it now.
393
+ Save records for THIS window of pages only, then stop and report what you saved. Do NOT try to read the rest of the file and do NOT worry about the pages after this window: if any remain, the next window is rendered and sent to you automatically. Report only the pages you were actually shown - never imply you have seen the whole document.`;
394
+ }
395
+ function buildIndexingWindowMessage(attachment, placeholder, isContinuation, positionLabel) {
396
+ const src = `src::${attachment.storagePath}`;
397
+ const head = isContinuation ? `CONTINUE indexing a file whose previous pass did not finish.
339
398
 
340
- ` + meta + `
341
- This is a PDF. Its pages are delivered to you as RENDERED PAGE IMAGES embedded directly in this message (you do NOT need any tool, URL, or web_fetch to see them). You are shown a WINDOW of pages at a time, starting at page ${from + 1}.
342
- ` + datafy;
343
- }
344
- return `CONTINUE indexing a PDF whose previous pass did not finish.
399
+ ` : `A new file has just been uploaded. Index it now.
400
+
401
+ `;
402
+ const where = isContinuation ? `
403
+ Records for the earlier windows are ALREADY saved (they reference "${src}"). The NEXT window (starting at ${WINDOW_CURSOR_TOKEN}) is embedded below. Do NOT re-save windows that are already saved.
404
+ ` : `
405
+ This file is delivered to you ONE WINDOW at a time, embedded directly in this message. You do NOT need any tool, URL, or web_fetch to read it.
406
+ `;
407
+ return head + buildRenderMeta(attachment) + where + `
408
+ ${placeholder}
409
+
410
+ DATAFY this window: call postRecords and save records for everything in it - ONE RECORD PER ROW for tabular data (keyed by the column headers), or one record per section for prose. Capture every value you can read. Use the storage path above for the "src::" unique_id on the file-level record, and link every row/section record to it by reference.
345
411
 
346
- ` + meta + `
347
- Records for the earlier pages are ALREADY saved (they reference "${src}"). The NEXT window of rendered page images (starting at page ${from + 1}) is embedded in this message. Datafy each page as before and do NOT re-save pages that are already saved.
348
- ` + datafy;
412
+ Save records for THIS window only, then stop and report what you saved. Do NOT try to read the rest of the file, and do NOT call readFileContent - if more remains, the next window is read and sent to you automatically. Report only what you were actually shown, and never imply you have seen the whole file when the note beside the window says more remains.`;
349
413
  }
350
414
  function buildIndexingContinueMessage(attachment) {
351
415
  const src = `src::${attachment.storagePath}`;
@@ -615,19 +679,22 @@ Index the REMAINING windows - one record per row/item, looking at any page image
615
679
  var clientSecretRequest = (opts) => chatEngineConfig().clientSecretRequest(opts);
616
680
  var getOpenAIImageDetail = (model) => {
617
681
  const normalized = (model || DEFAULT_OPENAI_MODEL).trim().toLowerCase();
618
- const match = normalized.match(/^gpt-(\d+)(?:\.(\d+))?$/);
682
+ const match = normalized.match(/^gpt-(\d+)(?:\.(\d+))?(-[a-z0-9.\-]+)?$/);
619
683
  if (!match) {
620
684
  return DEFAULT_OPENAI_IMAGE_DETAIL;
621
685
  }
622
686
  const major = Number(match[1]);
623
687
  const minor = match[2] === void 0 ? null : Number(match[2]);
624
- if (major > 5) {
625
- return "original";
626
- }
627
- if (major === 5 && minor !== null && minor >= 4) {
628
- return "original";
688
+ const isVariant = !!match[3];
689
+ const supportsOriginal = major > 5 || major === 5 && minor !== null && minor >= 4;
690
+ if (!supportsOriginal) {
691
+ return DEFAULT_OPENAI_IMAGE_DETAIL;
629
692
  }
630
- return DEFAULT_OPENAI_IMAGE_DETAIL;
693
+ return isVariant ? "high" : "original";
694
+ };
695
+ var getRenderImageDetail = (model) => {
696
+ const detail = getOpenAIImageDetail(model);
697
+ return detail === DEFAULT_OPENAI_IMAGE_DETAIL ? "high" : detail;
631
698
  };
632
699
  var IMAGE_URL_REGEX = /\bhttps?:\/\/[^\s<>"'()\[\]]+?\.(?:jpg|jpeg|png|gif|webp)(?:\?[^\s<>"'()\[\]]*)?/gi;
633
700
  function transformContentWithImages(content) {
@@ -872,17 +939,44 @@ Index the REMAINING windows - one record per row/item, looking at any page image
872
939
  const visionFile = !parsedContent && isImageVisionFile(attachment.name, attachment.mime);
873
940
  const renderFrom = Math.max(0, info.renderFrom || 0);
874
941
  const renderPlaceholder = visionFile ? makeRenderPlaceholder(attachment.storagePath) : void 0;
942
+ const renderDetail = platform === "openai" ? getRenderImageDetail(info.model || DEFAULT_OPENAI_MODEL) : void 0;
875
943
  const skapiRender = visionFile && renderPlaceholder ? {
876
944
  _skapi_render: [
877
- { path: attachment.storagePath, from: renderFrom, count: RENDER_PAGES_PER_WINDOW, placeholder: renderPlaceholder, name: attachment.name, mime: attachment.mime }
945
+ {
946
+ path: attachment.storagePath,
947
+ from: renderFrom,
948
+ count: RENDER_PAGES_PER_WINDOW,
949
+ placeholder: renderPlaceholder,
950
+ name: attachment.name,
951
+ mime: attachment.mime,
952
+ detail: renderDetail,
953
+ auto_continue: true,
954
+ continue_text: buildIndexingRenderContinueTemplate(attachment, renderPlaceholder)
955
+ }
878
956
  ]
879
957
  } : {};
880
- const pagedRead = !visionFile && (continuing || !parsedContent && isPagedReadFile(attachment.name, attachment.mime));
881
- const serverExtract = !visionFile && !continuing && !parsedContent && !pagedRead && isServerExtractable(attachment.name, attachment.mime);
958
+ const windowedRead = !visionFile && !parsedContent && windowedIndexingEnabled() && isWindowedReadFile(attachment.name, attachment.mime);
959
+ const windowPlaceholder = windowedRead ? makeWindowPlaceholder(attachment.storagePath) : void 0;
960
+ const skapiWindow = windowedRead && windowPlaceholder ? {
961
+ _skapi_window: [
962
+ {
963
+ path: attachment.storagePath,
964
+ cursor: null,
965
+ placeholder: windowPlaceholder,
966
+ name: attachment.name,
967
+ mime: attachment.mime,
968
+ kind: "window",
969
+ auto_continue: true,
970
+ continue_text: buildIndexingWindowMessage(attachment, windowPlaceholder, true)
971
+ }
972
+ ]
973
+ } : {};
974
+ const pagedRead = !visionFile && !windowedRead && (continuing || !parsedContent && isPagedReadFile(attachment.name, attachment.mime));
975
+ const serverExtract = !visionFile && !windowedRead && !continuing && !parsedContent && !pagedRead && isServerExtractable(attachment.name, attachment.mime);
882
976
  const placeholder = serverExtract ? makeExtractPlaceholder(attachment.storagePath) : void 0;
883
977
  const extractContent = serverExtract && placeholder ? [{ path: attachment.storagePath, placeholder, name: attachment.name, mime: attachment.mime }] : void 0;
884
978
  const skapiExtract = extractContent && extractContent.length ? { _skapi_extract: extractContent } : {};
885
- const userMessage = visionFile && renderPlaceholder ? buildIndexingRenderMessage(attachment, renderPlaceholder, renderFrom) : continuing ? buildIndexingContinueMessage(attachment) : buildIndexingUserMessage(
979
+ const userMessage = visionFile && renderPlaceholder ? buildIndexingRenderMessage(attachment, renderPlaceholder, renderFrom) : windowedRead && windowPlaceholder ? buildIndexingWindowMessage(attachment, windowPlaceholder, false) : continuing ? buildIndexingContinueMessage(attachment) : buildIndexingUserMessage(
886
980
  attachment,
887
981
  parsedContent ? { inlineContent: parsedContent } : placeholder ? { inlineContentPlaceholder: placeholder } : pagedRead ? { pagedRead: true } : void 0
888
982
  );
@@ -911,6 +1005,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
911
1005
  max_output_tokens: MAX_TOKENS,
912
1006
  ...skapiExtract,
913
1007
  ...skapiRender,
1008
+ ...skapiWindow,
914
1009
  input: [
915
1010
  { role: "system", content: systemPrompt },
916
1011
  {
@@ -956,6 +1051,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
956
1051
  max_tokens: MAX_TOKENS,
957
1052
  ...skapiExtract,
958
1053
  ...skapiRender,
1054
+ ...skapiWindow,
959
1055
  system: [
960
1056
  {
961
1057
  type: "text",
@@ -1027,9 +1123,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1027
1123
  return "";
1028
1124
  }
1029
1125
  var BG_INDEXING_QUEUE_SUFFIX = "-bg";
1126
+ function isBgIndexingQueue(queueName) {
1127
+ if (typeof queueName !== "string" || !queueName) return false;
1128
+ const prefix = queueName.split("|")[0];
1129
+ const idx = prefix.lastIndexOf(":");
1130
+ const name = idx === -1 ? prefix : prefix.slice(idx + 1);
1131
+ return name.slice(-BG_INDEXING_QUEUE_SUFFIX.length) === BG_INDEXING_QUEUE_SUFFIX;
1132
+ }
1030
1133
  var INDEXING_COMPLETE_MARKER = "INDEXING_COMPLETE";
1031
1134
  var MAX_INDEXING_RESUME_PASSES = 6;
1032
- var MAX_VISION_RESUME_PASSES = 40;
1033
1135
  async function getChatHistory(params, fetchOptions) {
1034
1136
  const url = params.platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
1035
1137
  const p = Object.assign(
@@ -1157,6 +1259,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1157
1259
  cb(nowMs());
1158
1260
  }, 16);
1159
1261
  }
1262
+ function isPollStopped(res) {
1263
+ return !!res && typeof res === "object" && res.status === "stopped";
1264
+ }
1160
1265
  var ChatSession = class {
1161
1266
  constructor(host) {
1162
1267
  this.typewriterQueue = Promise.resolve();
@@ -1180,8 +1285,83 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1180
1285
  this.pendingAgentRequests = {};
1181
1286
  this.aiChatHistoryCache = {};
1182
1287
  this.historyItemPolls = /* @__PURE__ */ new Map();
1288
+ this._pauseReasons = /* @__PURE__ */ new Set();
1289
+ this._resuming = false;
1183
1290
  this._lidSeq = 0;
1184
1291
  }
1292
+ /**
1293
+ * Register a live poll so (a) a remount dedupes against it instead of stacking a
1294
+ * SECOND poll on the same item, and (b) pausePolling can stop it.
1295
+ *
1296
+ * `stop` comes from the SDK and may be absent on an older skapi-js, in which case the
1297
+ * poll simply cannot be stopped and is left running — see pausePolling.
1298
+ */
1299
+ _trackPoll(id, kind, p) {
1300
+ var stop = p && typeof p.stop === "function" ? p.stop.bind(p) : void 0;
1301
+ if (!stop) {
1302
+ console.debug("[chat-engine] poll has no stop handle", { id, kind });
1303
+ }
1304
+ this.historyItemPolls.set(id, { kind, stop });
1305
+ return p;
1306
+ }
1307
+ /** True while any pause reason is active. */
1308
+ isPollingPaused() {
1309
+ return this._pauseReasons.size > 0;
1310
+ }
1311
+ /**
1312
+ * Stop BACKGROUND polling until resumePolling. Foreground polls (a reply the user is
1313
+ * waiting on) keep running deliberately: their results must still land in the history
1314
+ * cache so resumePendingRequest can render them on return, otherwise a user who sends
1315
+ * a message then navigates away comes back to a permanently stuck "Thinking...".
1316
+ *
1317
+ * Server-side work is untouched; this only stops asking about it. That is safe for
1318
+ * document indexing because the worker drives that loop itself.
1319
+ */
1320
+ pausePolling(reason) {
1321
+ this._pauseReasons.add(reason || "paused");
1322
+ var self = this;
1323
+ var stopped = [];
1324
+ this.historyItemPolls.forEach(function(handle, id) {
1325
+ if (!handle || handle.kind !== "bg") return;
1326
+ if (typeof handle.stop !== "function") return;
1327
+ try {
1328
+ handle.stop();
1329
+ } catch (e) {
1330
+ }
1331
+ stopped.push(id);
1332
+ });
1333
+ stopped.forEach(function(id) {
1334
+ self.historyItemPolls.delete(id);
1335
+ });
1336
+ }
1337
+ /**
1338
+ * Lift a pause reason WITHOUT running the reconcile. For a caller that is about to
1339
+ * reload history anyway (a view remounting), letting resumePolling also reconcile
1340
+ * would race that load and can double-attach.
1341
+ */
1342
+ clearPauseReason(reason) {
1343
+ this._pauseReasons.delete(reason || "paused");
1344
+ }
1345
+ /**
1346
+ * Clear a pause reason and, once none remain, re-attach polling and reconcile.
1347
+ * Deliberately does NOT touch gateRefreshToken: bumping it would silently discard
1348
+ * the results of anything still in flight across the pause.
1349
+ */
1350
+ resumePolling(reason) {
1351
+ this._pauseReasons.delete(reason || "paused");
1352
+ if (this._pauseReasons.size > 0 || this._resuming) return Promise.resolve();
1353
+ if (!this.host.isViewMounted || !this.host.isViewMounted()) return Promise.resolve();
1354
+ var self = this;
1355
+ this._resuming = true;
1356
+ return Promise.resolve().then(function() {
1357
+ self.drainBgTaskQueue();
1358
+ return self.loadHistory(false, self.state.gateRefreshToken);
1359
+ }).catch(function(e) {
1360
+ console.error("[chat-engine] resume polling failed", e);
1361
+ }).then(function() {
1362
+ self._resuming = false;
1363
+ });
1364
+ }
1185
1365
  _newLocalId() {
1186
1366
  this._lidSeq += 1;
1187
1367
  return "lid_" + this._lidSeq;
@@ -1195,29 +1375,86 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1195
1375
  var key = this.getHistoryCacheKey();
1196
1376
  if (!key) return;
1197
1377
  this.aiChatHistoryCache[key] = {
1198
- messages: this.state.messages.slice(),
1378
+ messages: this.state.messages.filter(function(m) {
1379
+ return m._ownerKey === void 0 || m._ownerKey === key;
1380
+ }),
1199
1381
  endOfList: this.state.historyEndOfList,
1200
1382
  startKeyHistory: this.state.historyStartKeyHistory.slice()
1201
1383
  };
1202
1384
  }
1203
- _callProviderFor(platform, prompt, messages, system, model, userId, extractContent, fileUrls) {
1204
- var id = this.host.getIdentity();
1205
- return platform === "openai" ? callOpenAIWithPublicMcp(prompt, id.serviceId, id.owner, messages, system, model, userId, extractContent, fileUrls) : callClaudeWithPublicMcp(prompt, id.serviceId, id.owner, messages, system, model, userId, extractContent, fileUrls);
1385
+ /**
1386
+ * Land a resolved reply in the history cache of a chat that is NOT currently
1387
+ * visible, without touching state.messages. Mirrors the cache-only path in
1388
+ * dispatchAgentRequest: REPLACE the trailing pending "Thinking..." bubble
1389
+ * (append only when there is none), and settle the matching pending user
1390
+ * bubble, so the cached copy never keeps a stuck "Thinking..." that a later
1391
+ * cache-first load would re-render forever.
1392
+ */
1393
+ _applyReplyToCache(key, reply, serverId) {
1394
+ if (!key) return;
1395
+ var existing = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
1396
+ var msgs = existing.messages.slice();
1397
+ var thIdx = -1;
1398
+ for (var i = msgs.length - 1; i >= 0; i--) {
1399
+ var m = msgs[i];
1400
+ if (!m || !m.isPending || m.role !== "assistant" || m.isBackgroundTask) continue;
1401
+ if (serverId && m._serverItemId && m._serverItemId !== serverId) continue;
1402
+ thIdx = i;
1403
+ break;
1404
+ }
1405
+ if (thIdx !== -1) {
1406
+ if (reply._serverItemId === void 0 && msgs[thIdx]._serverItemId !== void 0) reply._serverItemId = msgs[thIdx]._serverItemId;
1407
+ msgs[thIdx] = reply;
1408
+ } else {
1409
+ msgs.push(reply);
1410
+ }
1411
+ for (var j = 0; j < msgs.length; j++) {
1412
+ var u = msgs[j];
1413
+ if (!u || u.role !== "user" || u.isBackgroundTask) continue;
1414
+ if (!(u.isPendingQueued || u.isPendingInProcess || u.isSendingToServer)) continue;
1415
+ if (serverId && u._serverItemId && u._serverItemId !== serverId) continue;
1416
+ var settled = { role: "user", content: u.content };
1417
+ if (u._serverItemId !== void 0) settled._serverItemId = u._serverItemId;
1418
+ if (u._ownerKey !== void 0) settled._ownerKey = u._ownerKey;
1419
+ msgs[j] = settled;
1420
+ break;
1421
+ }
1422
+ this.aiChatHistoryCache[key] = {
1423
+ messages: msgs,
1424
+ endOfList: existing.endOfList,
1425
+ startKeyHistory: existing.startKeyHistory
1426
+ };
1427
+ }
1428
+ /**
1429
+ * serviceId/owner are passed explicitly by every caller: a request can be
1430
+ * dispatched after the user moved to another project, and re-reading the live
1431
+ * identity here would silently send the turn to THAT project instead of the
1432
+ * one it was composed for. Falls back to the live read only when a caller
1433
+ * omits them.
1434
+ */
1435
+ _callProviderFor(platform, prompt, messages, system, model, userId, extractContent, fileUrls, serviceId, owner) {
1436
+ if (serviceId === void 0 || owner === void 0) {
1437
+ var id = this.host.getIdentity();
1438
+ if (serviceId === void 0) serviceId = id.serviceId;
1439
+ if (owner === void 0) owner = id.owner;
1440
+ }
1441
+ return platform === "openai" ? callOpenAIWithPublicMcp(prompt, serviceId, owner, messages, system, model, userId, extractContent, fileUrls) : callClaudeWithPublicMcp(prompt, serviceId, owner, messages, system, model, userId, extractContent, fileUrls);
1206
1442
  }
1207
1443
  dispatchAgentRequest(params) {
1208
1444
  var self = this;
1209
1445
  var dispatchItemId;
1210
1446
  var sendAndPoll = function() {
1211
1447
  return Promise.resolve(
1212
- self._callProviderFor(params.aiPlatform, params.text, params.boundedMessages, params.systemPrompt, params.aiModel, params.userId, params.extractContent, params.fileUrls)
1448
+ self._callProviderFor(params.aiPlatform, params.text, params.boundedMessages, params.systemPrompt, params.aiModel, params.userId, params.extractContent, params.fileUrls, params.serviceId, params.owner)
1213
1449
  ).then(function(initial) {
1214
1450
  if (initial && initial.poll && (initial.status === "pending" || initial.status === "running")) {
1215
1451
  if (initial.id) {
1216
1452
  if (dispatchItemId && dispatchItemId !== initial.id) self.historyItemPolls.delete(dispatchItemId);
1217
1453
  dispatchItemId = initial.id;
1218
- self.historyItemPolls.set(initial.id, true);
1219
1454
  }
1220
- return initial.poll({ latency: POLL_INTERVAL });
1455
+ var dp = initial.poll({ latency: POLL_INTERVAL });
1456
+ if (initial.id) self._trackPoll(initial.id, "fg", dp);
1457
+ return dp;
1221
1458
  }
1222
1459
  return initial;
1223
1460
  });
@@ -1270,20 +1507,57 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1270
1507
  // composed = clean display text; composedForLlm carries office-extraction
1271
1508
  // placeholders for the provider only. useBgQueue routes a post-attachment turn
1272
1509
  // onto the "-bg" queue so it runs after indexing.
1273
- dispatchComposedMessage(composed, useBgQueue, composedForLlm, extractContent, fileUrls) {
1510
+ dispatchComposedMessage(composed, useBgQueue, composedForLlm, extractContent, fileUrls, pinned) {
1274
1511
  var self = this;
1275
1512
  if (!composed) return;
1276
- var id = this.host.getIdentity();
1513
+ var id = pinned ? pinned.identity : this.host.getIdentity();
1277
1514
  if (id.platform === "none") return;
1278
1515
  var llmComposed = composedForLlm || composed;
1279
- var isQueuedSend = useBgQueue || this.state.sending || this.state.messages.some(function(m) {
1516
+ var key = !id.serviceId ? "" : id.serviceId + "#" + id.platform;
1517
+ var offChat = !!key && key !== this.getHistoryCacheKey();
1518
+ var isQueuedSend = !offChat && (useBgQueue || this.state.sending || this.state.messages.some(function(m) {
1280
1519
  return (m.isPending || m.isPendingQueued) && !m.isBackgroundTask && !m._useBgQueue;
1281
- });
1520
+ }));
1282
1521
  var aiPlatform = id.platform;
1283
1522
  var aiModel = id.model || void 0;
1284
- var systemPrompt = this.host.buildSystemPrompt();
1523
+ var systemPrompt = pinned ? pinned.systemPrompt : this.host.buildSystemPrompt();
1285
1524
  var userId = id.userId || id.serviceId;
1286
1525
  var chatQueue = useBgQueue ? userId + BG_INDEXING_QUEUE_SUFFIX : userId;
1526
+ if (offChat) {
1527
+ var offHistory = (this.aiChatHistoryCache[key] ? this.aiChatHistoryCache[key].messages : []).filter(function(m) {
1528
+ return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask;
1529
+ });
1530
+ var offBounded = buildBoundedChatMessages({
1531
+ platform: aiPlatform,
1532
+ model: aiModel,
1533
+ systemPrompt,
1534
+ serviceId: id.serviceId,
1535
+ history: offHistory.concat([{ role: "user", content: llmComposed }])
1536
+ });
1537
+ var offExisting = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
1538
+ this.aiChatHistoryCache[key] = {
1539
+ messages: offExisting.messages.concat([
1540
+ { role: "user", content: composed, _ownerKey: key },
1541
+ { role: "assistant", content: "", isPending: true, isPendingInProcess: true, _ownerKey: key }
1542
+ ]),
1543
+ endOfList: offExisting.endOfList,
1544
+ startKeyHistory: offExisting.startKeyHistory
1545
+ };
1546
+ this.dispatchAgentRequest({
1547
+ key,
1548
+ serviceId: id.serviceId,
1549
+ owner: id.owner,
1550
+ aiPlatform,
1551
+ aiModel,
1552
+ systemPrompt,
1553
+ text: composed,
1554
+ boundedMessages: offBounded.messages,
1555
+ userId: chatQueue,
1556
+ extractContent,
1557
+ fileUrls
1558
+ });
1559
+ return;
1560
+ }
1287
1561
  if (isQueuedSend) {
1288
1562
  var resolvedHistory = this.state.messages.filter(function(m) {
1289
1563
  return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask;
@@ -1296,15 +1570,16 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1296
1570
  history: resolvedHistory.concat([{ role: "user", content: llmComposed }])
1297
1571
  });
1298
1572
  var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true };
1573
+ if (key) queuedBubble._ownerKey = key;
1299
1574
  if (useBgQueue) queuedBubble._useBgQueue = true;
1300
1575
  this.state.messages.push(queuedBubble);
1301
1576
  this.host.notify();
1302
1577
  this.updateHistoryCache();
1303
1578
  this.host.scrollToBottom(true);
1304
- var capturedComposed = composed, capturedPlatform = aiPlatform;
1305
- Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent, fileUrls)).then(function(result) {
1306
- var sendingIdx = self.state.messages.findIndex(function(m) {
1307
- return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user";
1579
+ var capturedComposed = composed, capturedPlatform = aiPlatform, capturedKey = key;
1580
+ Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent, fileUrls, id.serviceId, id.owner)).then(function(result) {
1581
+ var sendingIdx = self.getHistoryCacheKey() !== capturedKey ? -1 : self.state.messages.findIndex(function(m) {
1582
+ return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user" && (m._ownerKey === void 0 || m._ownerKey === capturedKey);
1308
1583
  });
1309
1584
  var serverId = result && typeof result.id === "string" ? result.id : void 0;
1310
1585
  if (sendingIdx >= 0) {
@@ -1314,26 +1589,27 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1314
1589
  self.host.notify();
1315
1590
  }
1316
1591
  if (result && result.poll && (result.status === "pending" || result.status === "running")) {
1317
- if (serverId) self.historyItemPolls.set(serverId, true);
1318
- return result.poll({ latency: POLL_INTERVAL }).then(function(res) {
1319
- return self.onQueuedSendResponse(capturedComposed, res, capturedPlatform, serverId);
1592
+ var qp = result.poll({ latency: POLL_INTERVAL });
1593
+ if (serverId) self._trackPoll(serverId, "fg", qp);
1594
+ return qp.then(function(res) {
1595
+ if (isPollStopped(res)) return;
1596
+ return self.onQueuedSendResponse(capturedComposed, res, capturedPlatform, serverId, capturedKey);
1320
1597
  }).catch(function(err) {
1321
- return self.onQueuedSendError(capturedComposed, err, serverId);
1598
+ return self.onQueuedSendError(capturedComposed, err, serverId, capturedKey);
1322
1599
  });
1323
1600
  }
1324
- return self.onQueuedSendResponse(capturedComposed, result, capturedPlatform, serverId);
1601
+ return self.onQueuedSendResponse(capturedComposed, result, capturedPlatform, serverId, capturedKey);
1325
1602
  }).catch(function(err) {
1326
- return self.onQueuedSendError(capturedComposed, err, void 0);
1603
+ return self.onQueuedSendError(capturedComposed, err, void 0, capturedKey);
1327
1604
  });
1328
1605
  return;
1329
1606
  }
1330
- this.state.messages.push({ role: "user", content: composed });
1331
- this.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true });
1607
+ this.state.messages.push({ role: "user", content: composed, ...key ? { _ownerKey: key } : {} });
1608
+ this.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true, ...key ? { _ownerKey: key } : {} });
1332
1609
  this.host.notify();
1333
1610
  this.updateHistoryCache();
1334
1611
  this.state.sending = true;
1335
1612
  this.host.scrollToBottom(true);
1336
- var key = this.getHistoryCacheKey();
1337
1613
  var historyForLlm = this.state.messages.filter(function(m) {
1338
1614
  return !m.isCancelled && !m.isBackgroundTask;
1339
1615
  });
@@ -1367,8 +1643,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1367
1643
  });
1368
1644
  Promise.resolve(run).catch(function() {
1369
1645
  }).then(function() {
1370
- if (!(self.host.isViewMounted() && self.getHistoryCacheKey() === key)) return;
1371
1646
  self.state.sending = false;
1647
+ if (!(self.host.isViewMounted() && self.getHistoryCacheKey() === key)) return;
1372
1648
  return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
1373
1649
  self.host.scrollToBottom(true);
1374
1650
  });
@@ -1385,9 +1661,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1385
1661
  var existing = this.state.messages[nextIdx];
1386
1662
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true, isBackgroundTask: true };
1387
1663
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1664
+ if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1388
1665
  this.state.messages[nextIdx] = promoted;
1389
1666
  var placeholder = { role: "assistant", content: "", isPending: true, isPendingInProcess: true, isBackgroundTask: true };
1390
1667
  if (existing._serverItemId !== void 0) placeholder._serverItemId = existing._serverItemId;
1668
+ if (existing._ownerKey !== void 0) placeholder._ownerKey = existing._ownerKey;
1391
1669
  this.state.messages.splice(nextIdx + 1, 0, placeholder);
1392
1670
  this.host.notify();
1393
1671
  }
@@ -1403,14 +1681,20 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1403
1681
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true };
1404
1682
  if (existing.isBackgroundTask) promoted.isBackgroundTask = true;
1405
1683
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1684
+ if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1406
1685
  if (existing.isSendingToServer) promoted.isSendingToServer = true;
1407
1686
  this.state.messages[nextIdx] = promoted;
1408
1687
  var placeholder = { role: "assistant", content: "", isPending: true };
1409
1688
  if (existing._serverItemId !== void 0) placeholder._serverItemId = existing._serverItemId;
1689
+ if (existing._ownerKey !== void 0) placeholder._ownerKey = existing._ownerKey;
1410
1690
  this.state.messages.splice(nextIdx + 1, 0, placeholder);
1411
1691
  this.host.notify();
1412
1692
  }
1413
1693
  resolveQueuedUserBubble(serverId) {
1694
+ var liveKey = this.getHistoryCacheKey();
1695
+ var isLocal = function(m) {
1696
+ return m._ownerKey === void 0 || m._ownerKey === liveKey;
1697
+ };
1414
1698
  var userIdx = -1;
1415
1699
  if (serverId) {
1416
1700
  userIdx = this.state.messages.findIndex(function(m) {
@@ -1419,19 +1703,19 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1419
1703
  }
1420
1704
  if (userIdx === -1) {
1421
1705
  userIdx = this.state.messages.findIndex(function(m) {
1422
- return m.isPendingInProcess && m.role === "user" && !m.isBackgroundTask && !m._useBgQueue;
1706
+ return m.isPendingInProcess && m.role === "user" && !m.isBackgroundTask && !m._useBgQueue && isLocal(m);
1423
1707
  });
1424
1708
  }
1425
1709
  if (userIdx === -1) {
1426
1710
  userIdx = this.state.messages.findIndex(function(m) {
1427
- return m.isPendingQueued && m.role === "user" && !m.isBackgroundTask && !m._useBgQueue;
1711
+ return m.isPendingQueued && m.role === "user" && !m.isBackgroundTask && !m._useBgQueue && isLocal(m);
1428
1712
  });
1429
1713
  }
1430
1714
  if (serverId && this.cancelledServerIds.has(serverId)) {
1431
1715
  this.cancelledServerIds.delete(serverId);
1432
1716
  if (userIdx >= 0) {
1433
1717
  var ex = this.state.messages[userIdx];
1434
- this.state.messages[userIdx] = { role: "user", content: ex.content, isCancelled: true, _serverItemId: ex._serverItemId };
1718
+ this.state.messages[userIdx] = { role: "user", content: ex.content, isCancelled: true, _serverItemId: ex._serverItemId, ...ex._ownerKey !== void 0 ? { _ownerKey: ex._ownerKey } : {} };
1435
1719
  var thIdx = this.state.messages.findIndex(function(m, i) {
1436
1720
  return i > userIdx && m.isPending && m.role === "assistant" && !m.isBackgroundTask;
1437
1721
  });
@@ -1444,6 +1728,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1444
1728
  var exist = this.state.messages[userIdx];
1445
1729
  var repl = { role: "user", content: exist.content };
1446
1730
  if (exist._serverItemId !== void 0) repl._serverItemId = exist._serverItemId;
1731
+ if (exist._ownerKey !== void 0) repl._ownerKey = exist._ownerKey;
1447
1732
  this.state.messages[userIdx] = repl;
1448
1733
  }
1449
1734
  var thinkingIdx = userIdx >= 0 ? this.state.messages.findIndex(function(m, i) {
@@ -1456,8 +1741,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1456
1741
  else if (targetIdx >= 0) this.state.messages.splice(targetIdx, 0, msg);
1457
1742
  else this.state.messages.push(msg);
1458
1743
  }
1459
- onQueuedSendResponse(_composed, response, platform, serverId) {
1744
+ onQueuedSendResponse(_composed, response, platform, serverId, ownerKey) {
1460
1745
  if (serverId) this.historyItemPolls.delete(serverId);
1746
+ if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
1747
+ var offReply = isErrorResponseBody(response) ? { role: "assistant", content: getErrorMessage(response), isError: true } : { role: "assistant", content: ((platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "").trim() || "No text response received from AI provider." };
1748
+ this._applyReplyToCache(ownerKey, offReply, serverId);
1749
+ if (serverId) this.cancelledServerIds.delete(serverId);
1750
+ return;
1751
+ }
1461
1752
  var targetIdx = this.resolveQueuedUserBubble(serverId);
1462
1753
  if (targetIdx === void 0) {
1463
1754
  this.host.notify();
@@ -1491,8 +1782,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1491
1782
  this.host.notify();
1492
1783
  this.host.scrollToBottom(true);
1493
1784
  }
1494
- onQueuedSendError(_composed, err, serverId) {
1785
+ onQueuedSendError(_composed, err, serverId, ownerKey) {
1495
1786
  if (serverId) this.historyItemPolls.delete(serverId);
1787
+ if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
1788
+ var isGone = err && (err.code === "NOT_EXISTS" || err.body && err.body.code === "NOT_EXISTS");
1789
+ this._applyReplyToCache(ownerKey, isGone ? { role: "assistant", content: "Request was cancelled.", isError: true } : { role: "assistant", content: getErrorMessage(err), isError: true }, serverId);
1790
+ if (serverId) this.cancelledServerIds.delete(serverId);
1791
+ return;
1792
+ }
1496
1793
  var isNotExists = err && (err.code === "NOT_EXISTS" || err.body && err.body.code === "NOT_EXISTS");
1497
1794
  if (isNotExists) {
1498
1795
  var userIdx = serverId ? this.state.messages.findIndex(function(m) {
@@ -1896,24 +2193,31 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1896
2193
  }
1897
2194
  this.bgTaskQueue.forEach(function(entry) {
1898
2195
  if (entry.serviceId !== svcId || entry.platform !== plat) return;
1899
- if (presentIds[entry.id]) return;
1900
- var isRunning = entry.status === "running";
1901
- var userBubble = { role: "user", content: self.host.formatIndexingLabel(entry.filename, entry.mime, entry.size, entry.storagePath, entry.isReindex), isBackgroundTask: true, _serverItemId: entry.id };
1902
- if (isRunning) userBubble.isPendingInProcess = true;
1903
- else userBubble.isPendingQueued = true;
1904
- self.state.messages.push(userBubble);
1905
- if (isRunning) {
1906
- self.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true, isBackgroundTask: true, _serverItemId: entry.id });
2196
+ if (!presentIds[entry.id]) {
2197
+ var isRunning = entry.status === "running";
2198
+ var userBubble = { role: "user", content: self.host.formatIndexingLabel(entry.filename, entry.mime, entry.size, entry.storagePath, entry.isReindex), isBackgroundTask: true, _serverItemId: entry.id };
2199
+ if (isRunning) userBubble.isPendingInProcess = true;
2200
+ else userBubble.isPendingQueued = true;
2201
+ self.state.messages.push(userBubble);
2202
+ if (isRunning) {
2203
+ self.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true, isBackgroundTask: true, _serverItemId: entry.id });
2204
+ }
2205
+ presentIds[entry.id] = true;
2206
+ self.host.notify();
2207
+ self.updateHistoryCache();
2208
+ self.host.scrollToBottom(false);
1907
2209
  }
1908
- presentIds[entry.id] = true;
1909
- self.host.notify();
1910
- self.updateHistoryCache();
1911
- self.host.scrollToBottom(false);
1912
- if (!self.historyItemPolls.has(entry.id) && typeof entry.poll === "function") {
1913
- self.historyItemPolls.set(entry.id, true);
2210
+ if (!self.isPollingPaused() && !self.historyItemPolls.has(entry.id) && typeof entry.poll === "function") {
1914
2211
  var capturedId = entry.id, capturedPlat = plat;
1915
2212
  var capturedEntry = entry;
1916
- entry.poll({ latency: POLL_INTERVAL }).then(function(response) {
2213
+ var wasStopped = false;
2214
+ var bp = entry.poll({ latency: POLL_INTERVAL });
2215
+ self._trackPoll(entry.id, "bg", bp);
2216
+ bp.then(function(response) {
2217
+ if (isPollStopped(response)) {
2218
+ wasStopped = true;
2219
+ return;
2220
+ }
1917
2221
  self.handleHistoryItemResolution(capturedId, response, capturedPlat);
1918
2222
  self.maybeResumeIndexing(capturedEntry, response, capturedPlat);
1919
2223
  }).catch(function(err) {
@@ -1929,6 +2233,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1929
2233
  self.updateHistoryCache();
1930
2234
  }
1931
2235
  }).then(function() {
2236
+ if (wasStopped) return;
1932
2237
  var qi = self.bgTaskQueue.findIndex(function(q) {
1933
2238
  return q.id === capturedId;
1934
2239
  });
@@ -1939,25 +2244,30 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1939
2244
  this.promoteNextBgQueuedToRunning();
1940
2245
  }
1941
2246
  // Resume-across-passes: if a background INDEXING task for a paged file (spreadsheet or
1942
- // PDF) finished WITHOUT the completion marker, the agent ran out of room before reading
2247
+ // text) finished WITHOUT the completion marker, the agent ran out of room before reading
1943
2248
  // the whole file - dispatch a CONTINUE pass (up to a cap) that resumes from where the
1944
2249
  // already-saved records leave off. Additive + guarded so it never loops forever and
1945
2250
  // never breaks the resolution path.
2251
+ //
2252
+ // VISION files (PDFs rendered to page images) are NOT resumed here: the proxy worker
2253
+ // advances their page window itself, off the renderer's true page count. Driving them
2254
+ // from the browser is what used to lose pages on long documents - the chain lived in tab
2255
+ // memory (a reload or a closed tab ended it), and it stopped whenever the model claimed
2256
+ // completion, which on an 88-page file happened at page 15. Continuing to dispatch here
2257
+ // as well would now double-index every window.
1946
2258
  maybeResumeIndexing(entry, response, platform) {
1947
2259
  var self = this;
1948
2260
  try {
1949
2261
  if (!entry || !entry.storagePath) return;
1950
2262
  if (!isPagedReadFile(entry.filename, entry.mime)) return;
2263
+ if (isImageVisionFile(entry.filename, entry.mime)) return;
1951
2264
  if (isErrorResponseBody(response)) return;
1952
2265
  var answer = (platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "";
1953
2266
  if (answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1) return;
1954
2267
  var pass = (entry.resumePass || 0) + 1;
1955
- var isVision = isImageVisionFile(entry.filename, entry.mime);
1956
- var maxPasses = isVision ? MAX_VISION_RESUME_PASSES : MAX_INDEXING_RESUME_PASSES;
1957
- if (pass > maxPasses) return;
2268
+ if (pass > MAX_INDEXING_RESUME_PASSES) return;
1958
2269
  var id = this.host.getIdentity();
1959
2270
  if (!id || id.platform === "none" || id.serviceId !== entry.serviceId) return;
1960
- var renderFrom = isVision ? pass * RENDER_PAGES_PER_WINDOW : void 0;
1961
2271
  notifyAgentContinueIndexing({
1962
2272
  platform: id.platform,
1963
2273
  model: id.model,
@@ -1966,7 +2276,6 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1966
2276
  userId: id.userId || id.serviceId,
1967
2277
  serviceName: id.serviceName,
1968
2278
  serviceDescription: id.serviceDescription,
1969
- renderFrom,
1970
2279
  attachment: {
1971
2280
  name: entry.filename,
1972
2281
  storagePath: entry.storagePath,
@@ -2006,6 +2315,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2006
2315
  loadHistory(fetchMore, token) {
2007
2316
  var self = this;
2008
2317
  var id = this.host.getIdentity();
2318
+ var loadKey = !id.serviceId || id.platform === "none" ? "" : id.serviceId + "#" + id.platform;
2009
2319
  if (token === void 0) token = this.state.gateRefreshToken;
2010
2320
  if (this.state.loadingHistory && this.state.historyRequestToken === token || id.platform === "none" || !id.serviceId) {
2011
2321
  return Promise.resolve();
@@ -2028,9 +2338,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2028
2338
  if (token !== self.state.gateRefreshToken) return;
2029
2339
  var chatList = history && Array.isArray(history.list) ? history.list : [];
2030
2340
  chatList.forEach(function(item) {
2031
- if (typeof item.queue_name === "string" && item.queue_name.slice(-BG_INDEXING_QUEUE_SUFFIX.length) === BG_INDEXING_QUEUE_SUFFIX) {
2341
+ if (isBgIndexingQueue(item.queue_name)) {
2032
2342
  var userText = extractLastUserTextFromRequest(item.request_body);
2033
- if (typeof userText === "string" && userText.indexOf("A new file has just been uploaded") === 0) item._isBgTask = true;
2343
+ if (typeof userText === "string" && (userText.indexOf("A new file has just been uploaded") === 0 || userText.indexOf("CONTINUE indexing") === 0)) item._isBgTask = true;
2034
2344
  else item._isOnBgQueue = true;
2035
2345
  }
2036
2346
  });
@@ -2062,6 +2372,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2062
2372
  for (var ri = 0; ri < self.state.messages.length; ri++) {
2063
2373
  var mm = self.state.messages[ri];
2064
2374
  if (mm.isBackgroundTask) continue;
2375
+ if (mm._ownerKey !== void 0 && mm._ownerKey !== loadKey) continue;
2065
2376
  if (mm._serverItemId && serverIds[mm._serverItemId]) continue;
2066
2377
  if (!mm._serverItemId) {
2067
2378
  if (mappedHasPendingAssistant) continue;
@@ -2106,11 +2417,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2106
2417
  if (!item.poll || !item.id) return;
2107
2418
  if (self.historyItemPolls.has(item.id)) return;
2108
2419
  if (self.pendingAgentRequests[self.getHistoryCacheKey()] && !item._isBgTask && !item._isOnBgQueue) return;
2109
- self.historyItemPolls.set(item.id, true);
2420
+ if ((item._isBgTask || item._isOnBgQueue) && self.isPollingPaused()) return;
2110
2421
  var capturedId = item.id;
2111
2422
  var pp = item.poll({
2112
2423
  latency: POLL_INTERVAL,
2113
2424
  onResponse: function(response) {
2425
+ if (isPollStopped(response)) return;
2114
2426
  self.handleHistoryItemResolution(capturedId, response, platform);
2115
2427
  },
2116
2428
  onError: function(err) {
@@ -2146,6 +2458,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2146
2458
  }
2147
2459
  }
2148
2460
  });
2461
+ self._trackPoll(capturedId, item._isBgTask || item._isOnBgQueue ? "bg" : "fg", pp);
2149
2462
  if (pp && pp.catch) pp.catch(function() {
2150
2463
  });
2151
2464
  });
@@ -2362,7 +2675,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2362
2675
  (function() {
2363
2676
  var MCP_PROD = "https://mcp.broadwayinc.computer";
2364
2677
  var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
2365
- var BQ_VERSION = "1.6.0" ;
2678
+ var BQ_VERSION = "1.6.1" ;
2366
2679
  var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
2367
2680
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
2368
2681
  var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
@@ -5625,7 +5938,17 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5625
5938
  if (!S._visBound && typeof document !== "undefined" && document.addEventListener) {
5626
5939
  S._visBound = true;
5627
5940
  document.addEventListener("visibilitychange", function() {
5628
- if (document.visibilityState === "visible" && S.user) ensureMcpGrantFresh();
5941
+ if (document.visibilityState === "hidden") {
5942
+ if (session && session.pausePolling) session.pausePolling("hidden");
5943
+ return;
5944
+ }
5945
+ if (document.visibilityState === "visible") {
5946
+ var refreshed = S.user ? ensureMcpGrantFresh() : null;
5947
+ Promise.resolve(refreshed).catch(function() {
5948
+ }).then(function() {
5949
+ if (session && session.resumePolling) session.resumePolling("hidden");
5950
+ });
5951
+ }
5629
5952
  });
5630
5953
  }
5631
5954
  boot();