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/dist/engine.mjs CHANGED
@@ -59,6 +59,9 @@ function chatEngineConfig() {
59
59
  }
60
60
  return _config;
61
61
  }
62
+ function windowedIndexingEnabled() {
63
+ return _config?.windowedIndexing === true;
64
+ }
62
65
  function pollOpt() {
63
66
  const p = _config?.poll;
64
67
  return p === void 0 ? {} : { poll: p };
@@ -153,7 +156,32 @@ function isServerExtractable(name, mime) {
153
156
  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";
154
157
  }
155
158
  var isOfficeFile = isServerExtractable;
156
- var PAGED_READ_EXTENSIONS = /* @__PURE__ */ new Set(["xls", "xlsx", "xlsm", "ods", "pdf"]);
159
+ var PAGED_READ_EXTENSIONS = /* @__PURE__ */ new Set([
160
+ // grids
161
+ "xls",
162
+ "xlsx",
163
+ "xlsm",
164
+ "ods",
165
+ // delimited text (row-windowed by the layer)
166
+ "csv",
167
+ "tsv",
168
+ "tab",
169
+ // documents
170
+ "pdf",
171
+ "docx",
172
+ "pptx",
173
+ // plain text / data / markup
174
+ "txt",
175
+ "md",
176
+ "markdown",
177
+ "log",
178
+ "json",
179
+ "jsonl",
180
+ "ndjson",
181
+ "xml",
182
+ "yaml",
183
+ "yml"
184
+ ]);
157
185
  function isPagedReadFile(name, mime) {
158
186
  const ext = (name || "").split(".").pop()?.toLowerCase() || "";
159
187
  if (PAGED_READ_EXTENSIONS.has(ext)) return true;
@@ -177,6 +205,16 @@ function makeRenderPlaceholder(seed) {
177
205
  return `{{SKAPI_RENDER::${slug}-${_renderPlaceholderSeq}}}`;
178
206
  }
179
207
  var RENDER_PAGES_PER_WINDOW = 5;
208
+ var _windowPlaceholderSeq = 0;
209
+ function makeWindowPlaceholder(seed) {
210
+ _windowPlaceholderSeq += 1;
211
+ const slug = (seed || "file").replace(/[^a-zA-Z0-9]+/g, "_").slice(-48);
212
+ return `{{SKAPI_WINDOW::${slug}-${_windowPlaceholderSeq}}}`;
213
+ }
214
+ function isWindowedReadFile(name, mime) {
215
+ if (isImageVisionFile(name, mime)) return false;
216
+ return isPagedReadFile(name, mime);
217
+ }
180
218
  function composeUserMessage(text, attachmentUrls) {
181
219
  let composed = text;
182
220
  if (attachmentUrls.length > 0) {
@@ -324,32 +362,58 @@ Read this file with the readFileContent tool, using the storage path above - do
324
362
  }
325
363
  return head + `- temporary URL (fetch this to read the file contents): ${attachment.url}`;
326
364
  }
365
+ var RENDER_FROM_TOKEN = "{{RENDER_FROM}}";
366
+ var WINDOW_CURSOR_TOKEN = RENDER_FROM_TOKEN;
327
367
  function buildIndexingRenderMessage(attachment, placeholder, renderFrom) {
328
368
  const from = Math.max(0, renderFrom || 0);
369
+ if (from > 0) return buildIndexingRenderContinueTemplate(attachment, placeholder, String(from + 1));
370
+ return `A new file has just been uploaded. Index it now.
371
+
372
+ ` + buildRenderMeta(attachment) + `
373
+ 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}.
374
+ ` + buildRenderDatafy(placeholder);
375
+ }
376
+ function buildIndexingRenderContinueTemplate(attachment, placeholder, pageLabel = RENDER_FROM_TOKEN) {
329
377
  const src = `src::${attachment.storagePath}`;
330
- const meta = `File metadata:
378
+ return `CONTINUE indexing a PDF whose previous pass did not finish.
379
+
380
+ ` + buildRenderMeta(attachment) + `
381
+ 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.
382
+ ` + buildRenderDatafy(placeholder);
383
+ }
384
+ function buildRenderMeta(attachment) {
385
+ return `File metadata:
331
386
  - name: ${attachment.name}
332
387
  - storage path: ${attachment.storagePath}
333
388
  ` + (attachment.mime ? `- mime type: ${attachment.mime}
334
389
  ` : "");
335
- const datafy = `
390
+ }
391
+ function buildRenderDatafy(placeholder) {
392
+ return `
336
393
  ${placeholder}
337
394
 
338
395
  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.
339
396
 
340
- 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.`;
341
- if (from === 0) {
342
- return `A new file has just been uploaded. Index it now.
397
+ 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.`;
398
+ }
399
+ function buildIndexingWindowMessage(attachment, placeholder, isContinuation, positionLabel) {
400
+ const src = `src::${attachment.storagePath}`;
401
+ const head = isContinuation ? `CONTINUE indexing a file whose previous pass did not finish.
343
402
 
344
- ` + meta + `
345
- 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}.
346
- ` + datafy;
347
- }
348
- return `CONTINUE indexing a PDF whose previous pass did not finish.
403
+ ` : `A new file has just been uploaded. Index it now.
404
+
405
+ `;
406
+ const where = isContinuation ? `
407
+ Records for the earlier windows are ALREADY saved (they reference "${src}"). The NEXT window (starting at ${positionLabel || WINDOW_CURSOR_TOKEN}) is embedded below. Do NOT re-save windows that are already saved.
408
+ ` : `
409
+ 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.
410
+ `;
411
+ return head + buildRenderMeta(attachment) + where + `
412
+ ${placeholder}
349
413
 
350
- ` + meta + `
351
- 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.
352
- ` + datafy;
414
+ 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.
415
+
416
+ 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.`;
353
417
  }
354
418
  function buildIndexingContinueMessage(attachment) {
355
419
  const src = `src::${attachment.storagePath}`;
@@ -622,19 +686,22 @@ var mcpUrl = () => chatEngineConfig().mcpBaseUrl;
622
686
  var clientSecretRequest = (opts) => chatEngineConfig().clientSecretRequest(opts);
623
687
  var getOpenAIImageDetail = (model) => {
624
688
  const normalized = (model || DEFAULT_OPENAI_MODEL).trim().toLowerCase();
625
- const match = normalized.match(/^gpt-(\d+)(?:\.(\d+))?$/);
689
+ const match = normalized.match(/^gpt-(\d+)(?:\.(\d+))?(-[a-z0-9.\-]+)?$/);
626
690
  if (!match) {
627
691
  return DEFAULT_OPENAI_IMAGE_DETAIL;
628
692
  }
629
693
  const major = Number(match[1]);
630
694
  const minor = match[2] === void 0 ? null : Number(match[2]);
631
- if (major > 5) {
632
- return "original";
633
- }
634
- if (major === 5 && minor !== null && minor >= 4) {
635
- return "original";
695
+ const isVariant = !!match[3];
696
+ const supportsOriginal = major > 5 || major === 5 && minor !== null && minor >= 4;
697
+ if (!supportsOriginal) {
698
+ return DEFAULT_OPENAI_IMAGE_DETAIL;
636
699
  }
637
- return DEFAULT_OPENAI_IMAGE_DETAIL;
700
+ return isVariant ? "high" : "original";
701
+ };
702
+ var getRenderImageDetail = (model) => {
703
+ const detail = getOpenAIImageDetail(model);
704
+ return detail === DEFAULT_OPENAI_IMAGE_DETAIL ? "high" : detail;
638
705
  };
639
706
  var IMAGE_URL_REGEX = /\bhttps?:\/\/[^\s<>"'()\[\]]+?\.(?:jpg|jpeg|png|gif|webp)(?:\?[^\s<>"'()\[\]]*)?/gi;
640
707
  function transformContentWithImages(content) {
@@ -879,17 +946,44 @@ async function notifyAgentSaveAttachment(info) {
879
946
  const visionFile = !parsedContent && isImageVisionFile(attachment.name, attachment.mime);
880
947
  const renderFrom = Math.max(0, info.renderFrom || 0);
881
948
  const renderPlaceholder = visionFile ? makeRenderPlaceholder(attachment.storagePath) : void 0;
949
+ const renderDetail = platform === "openai" ? getRenderImageDetail(info.model || DEFAULT_OPENAI_MODEL) : void 0;
882
950
  const skapiRender = visionFile && renderPlaceholder ? {
883
951
  _skapi_render: [
884
- { path: attachment.storagePath, from: renderFrom, count: RENDER_PAGES_PER_WINDOW, placeholder: renderPlaceholder, name: attachment.name, mime: attachment.mime }
952
+ {
953
+ path: attachment.storagePath,
954
+ from: renderFrom,
955
+ count: RENDER_PAGES_PER_WINDOW,
956
+ placeholder: renderPlaceholder,
957
+ name: attachment.name,
958
+ mime: attachment.mime,
959
+ detail: renderDetail,
960
+ auto_continue: true,
961
+ continue_text: buildIndexingRenderContinueTemplate(attachment, renderPlaceholder)
962
+ }
963
+ ]
964
+ } : {};
965
+ const windowedRead = !visionFile && !parsedContent && windowedIndexingEnabled() && isWindowedReadFile(attachment.name, attachment.mime);
966
+ const windowPlaceholder = windowedRead ? makeWindowPlaceholder(attachment.storagePath) : void 0;
967
+ const skapiWindow = windowedRead && windowPlaceholder ? {
968
+ _skapi_window: [
969
+ {
970
+ path: attachment.storagePath,
971
+ cursor: null,
972
+ placeholder: windowPlaceholder,
973
+ name: attachment.name,
974
+ mime: attachment.mime,
975
+ kind: "window",
976
+ auto_continue: true,
977
+ continue_text: buildIndexingWindowMessage(attachment, windowPlaceholder, true)
978
+ }
885
979
  ]
886
980
  } : {};
887
- const pagedRead = !visionFile && (continuing || !parsedContent && isPagedReadFile(attachment.name, attachment.mime));
888
- const serverExtract = !visionFile && !continuing && !parsedContent && !pagedRead && isServerExtractable(attachment.name, attachment.mime);
981
+ const pagedRead = !visionFile && !windowedRead && (continuing || !parsedContent && isPagedReadFile(attachment.name, attachment.mime));
982
+ const serverExtract = !visionFile && !windowedRead && !continuing && !parsedContent && !pagedRead && isServerExtractable(attachment.name, attachment.mime);
889
983
  const placeholder = serverExtract ? makeExtractPlaceholder(attachment.storagePath) : void 0;
890
984
  const extractContent = serverExtract && placeholder ? [{ path: attachment.storagePath, placeholder, name: attachment.name, mime: attachment.mime }] : void 0;
891
985
  const skapiExtract = extractContent && extractContent.length ? { _skapi_extract: extractContent } : {};
892
- const userMessage = visionFile && renderPlaceholder ? buildIndexingRenderMessage(attachment, renderPlaceholder, renderFrom) : continuing ? buildIndexingContinueMessage(attachment) : buildIndexingUserMessage(
986
+ const userMessage = visionFile && renderPlaceholder ? buildIndexingRenderMessage(attachment, renderPlaceholder, renderFrom) : windowedRead && windowPlaceholder ? buildIndexingWindowMessage(attachment, windowPlaceholder, false) : continuing ? buildIndexingContinueMessage(attachment) : buildIndexingUserMessage(
893
987
  attachment,
894
988
  parsedContent ? { inlineContent: parsedContent } : placeholder ? { inlineContentPlaceholder: placeholder } : pagedRead ? { pagedRead: true } : void 0
895
989
  );
@@ -918,6 +1012,7 @@ async function notifyAgentSaveAttachment(info) {
918
1012
  max_output_tokens: MAX_TOKENS,
919
1013
  ...skapiExtract,
920
1014
  ...skapiRender,
1015
+ ...skapiWindow,
921
1016
  input: [
922
1017
  { role: "system", content: systemPrompt },
923
1018
  {
@@ -963,6 +1058,7 @@ async function notifyAgentSaveAttachment(info) {
963
1058
  max_tokens: MAX_TOKENS,
964
1059
  ...skapiExtract,
965
1060
  ...skapiRender,
1061
+ ...skapiWindow,
966
1062
  system: [
967
1063
  {
968
1064
  type: "text",
@@ -1059,9 +1155,15 @@ async function listOpenAIModels(service, owner) {
1059
1155
  });
1060
1156
  }
1061
1157
  var BG_INDEXING_QUEUE_SUFFIX = "-bg";
1158
+ function isBgIndexingQueue(queueName) {
1159
+ if (typeof queueName !== "string" || !queueName) return false;
1160
+ const prefix = queueName.split("|")[0];
1161
+ const idx = prefix.lastIndexOf(":");
1162
+ const name = idx === -1 ? prefix : prefix.slice(idx + 1);
1163
+ return name.slice(-BG_INDEXING_QUEUE_SUFFIX.length) === BG_INDEXING_QUEUE_SUFFIX;
1164
+ }
1062
1165
  var INDEXING_COMPLETE_MARKER = "INDEXING_COMPLETE";
1063
1166
  var MAX_INDEXING_RESUME_PASSES = 6;
1064
- var MAX_VISION_RESUME_PASSES = 40;
1065
1167
  async function getChatHistory(params, fetchOptions) {
1066
1168
  const url = params.platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
1067
1169
  const p = Object.assign(
@@ -1189,6 +1291,9 @@ function nextFrame(cb) {
1189
1291
  cb(nowMs());
1190
1292
  }, 16);
1191
1293
  }
1294
+ function isPollStopped(res) {
1295
+ return !!res && typeof res === "object" && res.status === "stopped";
1296
+ }
1192
1297
  var ChatSession = class {
1193
1298
  constructor(host) {
1194
1299
  this.typewriterQueue = Promise.resolve();
@@ -1212,8 +1317,83 @@ var ChatSession = class {
1212
1317
  this.pendingAgentRequests = {};
1213
1318
  this.aiChatHistoryCache = {};
1214
1319
  this.historyItemPolls = /* @__PURE__ */ new Map();
1320
+ this._pauseReasons = /* @__PURE__ */ new Set();
1321
+ this._resuming = false;
1215
1322
  this._lidSeq = 0;
1216
1323
  }
1324
+ /**
1325
+ * Register a live poll so (a) a remount dedupes against it instead of stacking a
1326
+ * SECOND poll on the same item, and (b) pausePolling can stop it.
1327
+ *
1328
+ * `stop` comes from the SDK and may be absent on an older skapi-js, in which case the
1329
+ * poll simply cannot be stopped and is left running — see pausePolling.
1330
+ */
1331
+ _trackPoll(id, kind, p) {
1332
+ var stop = p && typeof p.stop === "function" ? p.stop.bind(p) : void 0;
1333
+ if (!stop) {
1334
+ console.debug("[chat-engine] poll has no stop handle", { id, kind });
1335
+ }
1336
+ this.historyItemPolls.set(id, { kind, stop });
1337
+ return p;
1338
+ }
1339
+ /** True while any pause reason is active. */
1340
+ isPollingPaused() {
1341
+ return this._pauseReasons.size > 0;
1342
+ }
1343
+ /**
1344
+ * Stop BACKGROUND polling until resumePolling. Foreground polls (a reply the user is
1345
+ * waiting on) keep running deliberately: their results must still land in the history
1346
+ * cache so resumePendingRequest can render them on return, otherwise a user who sends
1347
+ * a message then navigates away comes back to a permanently stuck "Thinking...".
1348
+ *
1349
+ * Server-side work is untouched; this only stops asking about it. That is safe for
1350
+ * document indexing because the worker drives that loop itself.
1351
+ */
1352
+ pausePolling(reason) {
1353
+ this._pauseReasons.add(reason || "paused");
1354
+ var self = this;
1355
+ var stopped = [];
1356
+ this.historyItemPolls.forEach(function(handle, id) {
1357
+ if (!handle || handle.kind !== "bg") return;
1358
+ if (typeof handle.stop !== "function") return;
1359
+ try {
1360
+ handle.stop();
1361
+ } catch (e) {
1362
+ }
1363
+ stopped.push(id);
1364
+ });
1365
+ stopped.forEach(function(id) {
1366
+ self.historyItemPolls.delete(id);
1367
+ });
1368
+ }
1369
+ /**
1370
+ * Lift a pause reason WITHOUT running the reconcile. For a caller that is about to
1371
+ * reload history anyway (a view remounting), letting resumePolling also reconcile
1372
+ * would race that load and can double-attach.
1373
+ */
1374
+ clearPauseReason(reason) {
1375
+ this._pauseReasons.delete(reason || "paused");
1376
+ }
1377
+ /**
1378
+ * Clear a pause reason and, once none remain, re-attach polling and reconcile.
1379
+ * Deliberately does NOT touch gateRefreshToken: bumping it would silently discard
1380
+ * the results of anything still in flight across the pause.
1381
+ */
1382
+ resumePolling(reason) {
1383
+ this._pauseReasons.delete(reason || "paused");
1384
+ if (this._pauseReasons.size > 0 || this._resuming) return Promise.resolve();
1385
+ if (!this.host.isViewMounted || !this.host.isViewMounted()) return Promise.resolve();
1386
+ var self = this;
1387
+ this._resuming = true;
1388
+ return Promise.resolve().then(function() {
1389
+ self.drainBgTaskQueue();
1390
+ return self.loadHistory(false, self.state.gateRefreshToken);
1391
+ }).catch(function(e) {
1392
+ console.error("[chat-engine] resume polling failed", e);
1393
+ }).then(function() {
1394
+ self._resuming = false;
1395
+ });
1396
+ }
1217
1397
  _newLocalId() {
1218
1398
  this._lidSeq += 1;
1219
1399
  return "lid_" + this._lidSeq;
@@ -1227,29 +1407,86 @@ var ChatSession = class {
1227
1407
  var key = this.getHistoryCacheKey();
1228
1408
  if (!key) return;
1229
1409
  this.aiChatHistoryCache[key] = {
1230
- messages: this.state.messages.slice(),
1410
+ messages: this.state.messages.filter(function(m) {
1411
+ return m._ownerKey === void 0 || m._ownerKey === key;
1412
+ }),
1231
1413
  endOfList: this.state.historyEndOfList,
1232
1414
  startKeyHistory: this.state.historyStartKeyHistory.slice()
1233
1415
  };
1234
1416
  }
1235
- _callProviderFor(platform, prompt, messages, system, model, userId, extractContent, fileUrls) {
1236
- var id = this.host.getIdentity();
1237
- 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);
1417
+ /**
1418
+ * Land a resolved reply in the history cache of a chat that is NOT currently
1419
+ * visible, without touching state.messages. Mirrors the cache-only path in
1420
+ * dispatchAgentRequest: REPLACE the trailing pending "Thinking..." bubble
1421
+ * (append only when there is none), and settle the matching pending user
1422
+ * bubble, so the cached copy never keeps a stuck "Thinking..." that a later
1423
+ * cache-first load would re-render forever.
1424
+ */
1425
+ _applyReplyToCache(key, reply, serverId) {
1426
+ if (!key) return;
1427
+ var existing = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
1428
+ var msgs = existing.messages.slice();
1429
+ var thIdx = -1;
1430
+ for (var i = msgs.length - 1; i >= 0; i--) {
1431
+ var m = msgs[i];
1432
+ if (!m || !m.isPending || m.role !== "assistant" || m.isBackgroundTask) continue;
1433
+ if (serverId && m._serverItemId && m._serverItemId !== serverId) continue;
1434
+ thIdx = i;
1435
+ break;
1436
+ }
1437
+ if (thIdx !== -1) {
1438
+ if (reply._serverItemId === void 0 && msgs[thIdx]._serverItemId !== void 0) reply._serverItemId = msgs[thIdx]._serverItemId;
1439
+ msgs[thIdx] = reply;
1440
+ } else {
1441
+ msgs.push(reply);
1442
+ }
1443
+ for (var j = 0; j < msgs.length; j++) {
1444
+ var u = msgs[j];
1445
+ if (!u || u.role !== "user" || u.isBackgroundTask) continue;
1446
+ if (!(u.isPendingQueued || u.isPendingInProcess || u.isSendingToServer)) continue;
1447
+ if (serverId && u._serverItemId && u._serverItemId !== serverId) continue;
1448
+ var settled = { role: "user", content: u.content };
1449
+ if (u._serverItemId !== void 0) settled._serverItemId = u._serverItemId;
1450
+ if (u._ownerKey !== void 0) settled._ownerKey = u._ownerKey;
1451
+ msgs[j] = settled;
1452
+ break;
1453
+ }
1454
+ this.aiChatHistoryCache[key] = {
1455
+ messages: msgs,
1456
+ endOfList: existing.endOfList,
1457
+ startKeyHistory: existing.startKeyHistory
1458
+ };
1459
+ }
1460
+ /**
1461
+ * serviceId/owner are passed explicitly by every caller: a request can be
1462
+ * dispatched after the user moved to another project, and re-reading the live
1463
+ * identity here would silently send the turn to THAT project instead of the
1464
+ * one it was composed for. Falls back to the live read only when a caller
1465
+ * omits them.
1466
+ */
1467
+ _callProviderFor(platform, prompt, messages, system, model, userId, extractContent, fileUrls, serviceId, owner) {
1468
+ if (serviceId === void 0 || owner === void 0) {
1469
+ var id = this.host.getIdentity();
1470
+ if (serviceId === void 0) serviceId = id.serviceId;
1471
+ if (owner === void 0) owner = id.owner;
1472
+ }
1473
+ return platform === "openai" ? callOpenAIWithPublicMcp(prompt, serviceId, owner, messages, system, model, userId, extractContent, fileUrls) : callClaudeWithPublicMcp(prompt, serviceId, owner, messages, system, model, userId, extractContent, fileUrls);
1238
1474
  }
1239
1475
  dispatchAgentRequest(params) {
1240
1476
  var self = this;
1241
1477
  var dispatchItemId;
1242
1478
  var sendAndPoll = function() {
1243
1479
  return Promise.resolve(
1244
- self._callProviderFor(params.aiPlatform, params.text, params.boundedMessages, params.systemPrompt, params.aiModel, params.userId, params.extractContent, params.fileUrls)
1480
+ self._callProviderFor(params.aiPlatform, params.text, params.boundedMessages, params.systemPrompt, params.aiModel, params.userId, params.extractContent, params.fileUrls, params.serviceId, params.owner)
1245
1481
  ).then(function(initial) {
1246
1482
  if (initial && initial.poll && (initial.status === "pending" || initial.status === "running")) {
1247
1483
  if (initial.id) {
1248
1484
  if (dispatchItemId && dispatchItemId !== initial.id) self.historyItemPolls.delete(dispatchItemId);
1249
1485
  dispatchItemId = initial.id;
1250
- self.historyItemPolls.set(initial.id, true);
1251
1486
  }
1252
- return initial.poll({ latency: POLL_INTERVAL });
1487
+ var dp = initial.poll({ latency: POLL_INTERVAL });
1488
+ if (initial.id) self._trackPoll(initial.id, "fg", dp);
1489
+ return dp;
1253
1490
  }
1254
1491
  return initial;
1255
1492
  });
@@ -1302,20 +1539,57 @@ var ChatSession = class {
1302
1539
  // composed = clean display text; composedForLlm carries office-extraction
1303
1540
  // placeholders for the provider only. useBgQueue routes a post-attachment turn
1304
1541
  // onto the "-bg" queue so it runs after indexing.
1305
- dispatchComposedMessage(composed, useBgQueue, composedForLlm, extractContent, fileUrls) {
1542
+ dispatchComposedMessage(composed, useBgQueue, composedForLlm, extractContent, fileUrls, pinned) {
1306
1543
  var self = this;
1307
1544
  if (!composed) return;
1308
- var id = this.host.getIdentity();
1545
+ var id = pinned ? pinned.identity : this.host.getIdentity();
1309
1546
  if (id.platform === "none") return;
1310
1547
  var llmComposed = composedForLlm || composed;
1311
- var isQueuedSend = useBgQueue || this.state.sending || this.state.messages.some(function(m) {
1548
+ var key = !id.serviceId ? "" : id.serviceId + "#" + id.platform;
1549
+ var offChat = !!key && key !== this.getHistoryCacheKey();
1550
+ var isQueuedSend = !offChat && (useBgQueue || this.state.sending || this.state.messages.some(function(m) {
1312
1551
  return (m.isPending || m.isPendingQueued) && !m.isBackgroundTask && !m._useBgQueue;
1313
- });
1552
+ }));
1314
1553
  var aiPlatform = id.platform;
1315
1554
  var aiModel = id.model || void 0;
1316
- var systemPrompt = this.host.buildSystemPrompt();
1555
+ var systemPrompt = pinned ? pinned.systemPrompt : this.host.buildSystemPrompt();
1317
1556
  var userId = id.userId || id.serviceId;
1318
1557
  var chatQueue = useBgQueue ? userId + BG_INDEXING_QUEUE_SUFFIX : userId;
1558
+ if (offChat) {
1559
+ var offHistory = (this.aiChatHistoryCache[key] ? this.aiChatHistoryCache[key].messages : []).filter(function(m) {
1560
+ return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask;
1561
+ });
1562
+ var offBounded = buildBoundedChatMessages({
1563
+ platform: aiPlatform,
1564
+ model: aiModel,
1565
+ systemPrompt,
1566
+ serviceId: id.serviceId,
1567
+ history: offHistory.concat([{ role: "user", content: llmComposed }])
1568
+ });
1569
+ var offExisting = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
1570
+ this.aiChatHistoryCache[key] = {
1571
+ messages: offExisting.messages.concat([
1572
+ { role: "user", content: composed, _ownerKey: key },
1573
+ { role: "assistant", content: "", isPending: true, isPendingInProcess: true, _ownerKey: key }
1574
+ ]),
1575
+ endOfList: offExisting.endOfList,
1576
+ startKeyHistory: offExisting.startKeyHistory
1577
+ };
1578
+ this.dispatchAgentRequest({
1579
+ key,
1580
+ serviceId: id.serviceId,
1581
+ owner: id.owner,
1582
+ aiPlatform,
1583
+ aiModel,
1584
+ systemPrompt,
1585
+ text: composed,
1586
+ boundedMessages: offBounded.messages,
1587
+ userId: chatQueue,
1588
+ extractContent,
1589
+ fileUrls
1590
+ });
1591
+ return;
1592
+ }
1319
1593
  if (isQueuedSend) {
1320
1594
  var resolvedHistory = this.state.messages.filter(function(m) {
1321
1595
  return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask;
@@ -1328,15 +1602,16 @@ var ChatSession = class {
1328
1602
  history: resolvedHistory.concat([{ role: "user", content: llmComposed }])
1329
1603
  });
1330
1604
  var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true };
1605
+ if (key) queuedBubble._ownerKey = key;
1331
1606
  if (useBgQueue) queuedBubble._useBgQueue = true;
1332
1607
  this.state.messages.push(queuedBubble);
1333
1608
  this.host.notify();
1334
1609
  this.updateHistoryCache();
1335
1610
  this.host.scrollToBottom(true);
1336
- var capturedComposed = composed, capturedPlatform = aiPlatform;
1337
- Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent, fileUrls)).then(function(result) {
1338
- var sendingIdx = self.state.messages.findIndex(function(m) {
1339
- return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user";
1611
+ var capturedComposed = composed, capturedPlatform = aiPlatform, capturedKey = key;
1612
+ Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent, fileUrls, id.serviceId, id.owner)).then(function(result) {
1613
+ var sendingIdx = self.getHistoryCacheKey() !== capturedKey ? -1 : self.state.messages.findIndex(function(m) {
1614
+ return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user" && (m._ownerKey === void 0 || m._ownerKey === capturedKey);
1340
1615
  });
1341
1616
  var serverId = result && typeof result.id === "string" ? result.id : void 0;
1342
1617
  if (sendingIdx >= 0) {
@@ -1346,26 +1621,27 @@ var ChatSession = class {
1346
1621
  self.host.notify();
1347
1622
  }
1348
1623
  if (result && result.poll && (result.status === "pending" || result.status === "running")) {
1349
- if (serverId) self.historyItemPolls.set(serverId, true);
1350
- return result.poll({ latency: POLL_INTERVAL }).then(function(res) {
1351
- return self.onQueuedSendResponse(capturedComposed, res, capturedPlatform, serverId);
1624
+ var qp = result.poll({ latency: POLL_INTERVAL });
1625
+ if (serverId) self._trackPoll(serverId, "fg", qp);
1626
+ return qp.then(function(res) {
1627
+ if (isPollStopped(res)) return;
1628
+ return self.onQueuedSendResponse(capturedComposed, res, capturedPlatform, serverId, capturedKey);
1352
1629
  }).catch(function(err) {
1353
- return self.onQueuedSendError(capturedComposed, err, serverId);
1630
+ return self.onQueuedSendError(capturedComposed, err, serverId, capturedKey);
1354
1631
  });
1355
1632
  }
1356
- return self.onQueuedSendResponse(capturedComposed, result, capturedPlatform, serverId);
1633
+ return self.onQueuedSendResponse(capturedComposed, result, capturedPlatform, serverId, capturedKey);
1357
1634
  }).catch(function(err) {
1358
- return self.onQueuedSendError(capturedComposed, err, void 0);
1635
+ return self.onQueuedSendError(capturedComposed, err, void 0, capturedKey);
1359
1636
  });
1360
1637
  return;
1361
1638
  }
1362
- this.state.messages.push({ role: "user", content: composed });
1363
- this.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true });
1639
+ this.state.messages.push({ role: "user", content: composed, ...key ? { _ownerKey: key } : {} });
1640
+ this.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true, ...key ? { _ownerKey: key } : {} });
1364
1641
  this.host.notify();
1365
1642
  this.updateHistoryCache();
1366
1643
  this.state.sending = true;
1367
1644
  this.host.scrollToBottom(true);
1368
- var key = this.getHistoryCacheKey();
1369
1645
  var historyForLlm = this.state.messages.filter(function(m) {
1370
1646
  return !m.isCancelled && !m.isBackgroundTask;
1371
1647
  });
@@ -1399,8 +1675,8 @@ var ChatSession = class {
1399
1675
  });
1400
1676
  Promise.resolve(run).catch(function() {
1401
1677
  }).then(function() {
1402
- if (!(self.host.isViewMounted() && self.getHistoryCacheKey() === key)) return;
1403
1678
  self.state.sending = false;
1679
+ if (!(self.host.isViewMounted() && self.getHistoryCacheKey() === key)) return;
1404
1680
  return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
1405
1681
  self.host.scrollToBottom(true);
1406
1682
  });
@@ -1417,9 +1693,11 @@ var ChatSession = class {
1417
1693
  var existing = this.state.messages[nextIdx];
1418
1694
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true, isBackgroundTask: true };
1419
1695
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1696
+ if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1420
1697
  this.state.messages[nextIdx] = promoted;
1421
1698
  var placeholder = { role: "assistant", content: "", isPending: true, isPendingInProcess: true, isBackgroundTask: true };
1422
1699
  if (existing._serverItemId !== void 0) placeholder._serverItemId = existing._serverItemId;
1700
+ if (existing._ownerKey !== void 0) placeholder._ownerKey = existing._ownerKey;
1423
1701
  this.state.messages.splice(nextIdx + 1, 0, placeholder);
1424
1702
  this.host.notify();
1425
1703
  }
@@ -1435,14 +1713,20 @@ var ChatSession = class {
1435
1713
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true };
1436
1714
  if (existing.isBackgroundTask) promoted.isBackgroundTask = true;
1437
1715
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1716
+ if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1438
1717
  if (existing.isSendingToServer) promoted.isSendingToServer = true;
1439
1718
  this.state.messages[nextIdx] = promoted;
1440
1719
  var placeholder = { role: "assistant", content: "", isPending: true };
1441
1720
  if (existing._serverItemId !== void 0) placeholder._serverItemId = existing._serverItemId;
1721
+ if (existing._ownerKey !== void 0) placeholder._ownerKey = existing._ownerKey;
1442
1722
  this.state.messages.splice(nextIdx + 1, 0, placeholder);
1443
1723
  this.host.notify();
1444
1724
  }
1445
1725
  resolveQueuedUserBubble(serverId) {
1726
+ var liveKey = this.getHistoryCacheKey();
1727
+ var isLocal = function(m) {
1728
+ return m._ownerKey === void 0 || m._ownerKey === liveKey;
1729
+ };
1446
1730
  var userIdx = -1;
1447
1731
  if (serverId) {
1448
1732
  userIdx = this.state.messages.findIndex(function(m) {
@@ -1451,19 +1735,19 @@ var ChatSession = class {
1451
1735
  }
1452
1736
  if (userIdx === -1) {
1453
1737
  userIdx = this.state.messages.findIndex(function(m) {
1454
- return m.isPendingInProcess && m.role === "user" && !m.isBackgroundTask && !m._useBgQueue;
1738
+ return m.isPendingInProcess && m.role === "user" && !m.isBackgroundTask && !m._useBgQueue && isLocal(m);
1455
1739
  });
1456
1740
  }
1457
1741
  if (userIdx === -1) {
1458
1742
  userIdx = this.state.messages.findIndex(function(m) {
1459
- return m.isPendingQueued && m.role === "user" && !m.isBackgroundTask && !m._useBgQueue;
1743
+ return m.isPendingQueued && m.role === "user" && !m.isBackgroundTask && !m._useBgQueue && isLocal(m);
1460
1744
  });
1461
1745
  }
1462
1746
  if (serverId && this.cancelledServerIds.has(serverId)) {
1463
1747
  this.cancelledServerIds.delete(serverId);
1464
1748
  if (userIdx >= 0) {
1465
1749
  var ex = this.state.messages[userIdx];
1466
- this.state.messages[userIdx] = { role: "user", content: ex.content, isCancelled: true, _serverItemId: ex._serverItemId };
1750
+ this.state.messages[userIdx] = { role: "user", content: ex.content, isCancelled: true, _serverItemId: ex._serverItemId, ...ex._ownerKey !== void 0 ? { _ownerKey: ex._ownerKey } : {} };
1467
1751
  var thIdx = this.state.messages.findIndex(function(m, i) {
1468
1752
  return i > userIdx && m.isPending && m.role === "assistant" && !m.isBackgroundTask;
1469
1753
  });
@@ -1476,6 +1760,7 @@ var ChatSession = class {
1476
1760
  var exist = this.state.messages[userIdx];
1477
1761
  var repl = { role: "user", content: exist.content };
1478
1762
  if (exist._serverItemId !== void 0) repl._serverItemId = exist._serverItemId;
1763
+ if (exist._ownerKey !== void 0) repl._ownerKey = exist._ownerKey;
1479
1764
  this.state.messages[userIdx] = repl;
1480
1765
  }
1481
1766
  var thinkingIdx = userIdx >= 0 ? this.state.messages.findIndex(function(m, i) {
@@ -1488,8 +1773,14 @@ var ChatSession = class {
1488
1773
  else if (targetIdx >= 0) this.state.messages.splice(targetIdx, 0, msg);
1489
1774
  else this.state.messages.push(msg);
1490
1775
  }
1491
- onQueuedSendResponse(_composed, response, platform, serverId) {
1776
+ onQueuedSendResponse(_composed, response, platform, serverId, ownerKey) {
1492
1777
  if (serverId) this.historyItemPolls.delete(serverId);
1778
+ if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
1779
+ 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." };
1780
+ this._applyReplyToCache(ownerKey, offReply, serverId);
1781
+ if (serverId) this.cancelledServerIds.delete(serverId);
1782
+ return;
1783
+ }
1493
1784
  var targetIdx = this.resolveQueuedUserBubble(serverId);
1494
1785
  if (targetIdx === void 0) {
1495
1786
  this.host.notify();
@@ -1523,8 +1814,14 @@ var ChatSession = class {
1523
1814
  this.host.notify();
1524
1815
  this.host.scrollToBottom(true);
1525
1816
  }
1526
- onQueuedSendError(_composed, err, serverId) {
1817
+ onQueuedSendError(_composed, err, serverId, ownerKey) {
1527
1818
  if (serverId) this.historyItemPolls.delete(serverId);
1819
+ if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
1820
+ var isGone = err && (err.code === "NOT_EXISTS" || err.body && err.body.code === "NOT_EXISTS");
1821
+ this._applyReplyToCache(ownerKey, isGone ? { role: "assistant", content: "Request was cancelled.", isError: true } : { role: "assistant", content: getErrorMessage(err), isError: true }, serverId);
1822
+ if (serverId) this.cancelledServerIds.delete(serverId);
1823
+ return;
1824
+ }
1528
1825
  var isNotExists = err && (err.code === "NOT_EXISTS" || err.body && err.body.code === "NOT_EXISTS");
1529
1826
  if (isNotExists) {
1530
1827
  var userIdx = serverId ? this.state.messages.findIndex(function(m) {
@@ -1928,24 +2225,31 @@ var ChatSession = class {
1928
2225
  }
1929
2226
  this.bgTaskQueue.forEach(function(entry) {
1930
2227
  if (entry.serviceId !== svcId || entry.platform !== plat) return;
1931
- if (presentIds[entry.id]) return;
1932
- var isRunning = entry.status === "running";
1933
- var userBubble = { role: "user", content: self.host.formatIndexingLabel(entry.filename, entry.mime, entry.size, entry.storagePath, entry.isReindex), isBackgroundTask: true, _serverItemId: entry.id };
1934
- if (isRunning) userBubble.isPendingInProcess = true;
1935
- else userBubble.isPendingQueued = true;
1936
- self.state.messages.push(userBubble);
1937
- if (isRunning) {
1938
- self.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true, isBackgroundTask: true, _serverItemId: entry.id });
2228
+ if (!presentIds[entry.id]) {
2229
+ var isRunning = entry.status === "running";
2230
+ var userBubble = { role: "user", content: self.host.formatIndexingLabel(entry.filename, entry.mime, entry.size, entry.storagePath, entry.isReindex), isBackgroundTask: true, _serverItemId: entry.id };
2231
+ if (isRunning) userBubble.isPendingInProcess = true;
2232
+ else userBubble.isPendingQueued = true;
2233
+ self.state.messages.push(userBubble);
2234
+ if (isRunning) {
2235
+ self.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true, isBackgroundTask: true, _serverItemId: entry.id });
2236
+ }
2237
+ presentIds[entry.id] = true;
2238
+ self.host.notify();
2239
+ self.updateHistoryCache();
2240
+ self.host.scrollToBottom(false);
1939
2241
  }
1940
- presentIds[entry.id] = true;
1941
- self.host.notify();
1942
- self.updateHistoryCache();
1943
- self.host.scrollToBottom(false);
1944
- if (!self.historyItemPolls.has(entry.id) && typeof entry.poll === "function") {
1945
- self.historyItemPolls.set(entry.id, true);
2242
+ if (!self.isPollingPaused() && !self.historyItemPolls.has(entry.id) && typeof entry.poll === "function") {
1946
2243
  var capturedId = entry.id, capturedPlat = plat;
1947
2244
  var capturedEntry = entry;
1948
- entry.poll({ latency: POLL_INTERVAL }).then(function(response) {
2245
+ var wasStopped = false;
2246
+ var bp = entry.poll({ latency: POLL_INTERVAL });
2247
+ self._trackPoll(entry.id, "bg", bp);
2248
+ bp.then(function(response) {
2249
+ if (isPollStopped(response)) {
2250
+ wasStopped = true;
2251
+ return;
2252
+ }
1949
2253
  self.handleHistoryItemResolution(capturedId, response, capturedPlat);
1950
2254
  self.maybeResumeIndexing(capturedEntry, response, capturedPlat);
1951
2255
  }).catch(function(err) {
@@ -1961,6 +2265,7 @@ var ChatSession = class {
1961
2265
  self.updateHistoryCache();
1962
2266
  }
1963
2267
  }).then(function() {
2268
+ if (wasStopped) return;
1964
2269
  var qi = self.bgTaskQueue.findIndex(function(q) {
1965
2270
  return q.id === capturedId;
1966
2271
  });
@@ -1971,25 +2276,30 @@ var ChatSession = class {
1971
2276
  this.promoteNextBgQueuedToRunning();
1972
2277
  }
1973
2278
  // Resume-across-passes: if a background INDEXING task for a paged file (spreadsheet or
1974
- // PDF) finished WITHOUT the completion marker, the agent ran out of room before reading
2279
+ // text) finished WITHOUT the completion marker, the agent ran out of room before reading
1975
2280
  // the whole file - dispatch a CONTINUE pass (up to a cap) that resumes from where the
1976
2281
  // already-saved records leave off. Additive + guarded so it never loops forever and
1977
2282
  // never breaks the resolution path.
2283
+ //
2284
+ // VISION files (PDFs rendered to page images) are NOT resumed here: the proxy worker
2285
+ // advances their page window itself, off the renderer's true page count. Driving them
2286
+ // from the browser is what used to lose pages on long documents - the chain lived in tab
2287
+ // memory (a reload or a closed tab ended it), and it stopped whenever the model claimed
2288
+ // completion, which on an 88-page file happened at page 15. Continuing to dispatch here
2289
+ // as well would now double-index every window.
1978
2290
  maybeResumeIndexing(entry, response, platform) {
1979
2291
  var self = this;
1980
2292
  try {
1981
2293
  if (!entry || !entry.storagePath) return;
1982
2294
  if (!isPagedReadFile(entry.filename, entry.mime)) return;
2295
+ if (isImageVisionFile(entry.filename, entry.mime)) return;
1983
2296
  if (isErrorResponseBody(response)) return;
1984
2297
  var answer = (platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "";
1985
2298
  if (answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1) return;
1986
2299
  var pass = (entry.resumePass || 0) + 1;
1987
- var isVision = isImageVisionFile(entry.filename, entry.mime);
1988
- var maxPasses = isVision ? MAX_VISION_RESUME_PASSES : MAX_INDEXING_RESUME_PASSES;
1989
- if (pass > maxPasses) return;
2300
+ if (pass > MAX_INDEXING_RESUME_PASSES) return;
1990
2301
  var id = this.host.getIdentity();
1991
2302
  if (!id || id.platform === "none" || id.serviceId !== entry.serviceId) return;
1992
- var renderFrom = isVision ? pass * RENDER_PAGES_PER_WINDOW : void 0;
1993
2303
  notifyAgentContinueIndexing({
1994
2304
  platform: id.platform,
1995
2305
  model: id.model,
@@ -1998,7 +2308,6 @@ var ChatSession = class {
1998
2308
  userId: id.userId || id.serviceId,
1999
2309
  serviceName: id.serviceName,
2000
2310
  serviceDescription: id.serviceDescription,
2001
- renderFrom,
2002
2311
  attachment: {
2003
2312
  name: entry.filename,
2004
2313
  storagePath: entry.storagePath,
@@ -2038,6 +2347,7 @@ var ChatSession = class {
2038
2347
  loadHistory(fetchMore, token) {
2039
2348
  var self = this;
2040
2349
  var id = this.host.getIdentity();
2350
+ var loadKey = !id.serviceId || id.platform === "none" ? "" : id.serviceId + "#" + id.platform;
2041
2351
  if (token === void 0) token = this.state.gateRefreshToken;
2042
2352
  if (this.state.loadingHistory && this.state.historyRequestToken === token || id.platform === "none" || !id.serviceId) {
2043
2353
  return Promise.resolve();
@@ -2060,9 +2370,9 @@ var ChatSession = class {
2060
2370
  if (token !== self.state.gateRefreshToken) return;
2061
2371
  var chatList = history && Array.isArray(history.list) ? history.list : [];
2062
2372
  chatList.forEach(function(item) {
2063
- if (typeof item.queue_name === "string" && item.queue_name.slice(-BG_INDEXING_QUEUE_SUFFIX.length) === BG_INDEXING_QUEUE_SUFFIX) {
2373
+ if (isBgIndexingQueue(item.queue_name)) {
2064
2374
  var userText = extractLastUserTextFromRequest(item.request_body);
2065
- if (typeof userText === "string" && userText.indexOf("A new file has just been uploaded") === 0) item._isBgTask = true;
2375
+ if (typeof userText === "string" && (userText.indexOf("A new file has just been uploaded") === 0 || userText.indexOf("CONTINUE indexing") === 0)) item._isBgTask = true;
2066
2376
  else item._isOnBgQueue = true;
2067
2377
  }
2068
2378
  });
@@ -2094,6 +2404,7 @@ var ChatSession = class {
2094
2404
  for (var ri = 0; ri < self.state.messages.length; ri++) {
2095
2405
  var mm = self.state.messages[ri];
2096
2406
  if (mm.isBackgroundTask) continue;
2407
+ if (mm._ownerKey !== void 0 && mm._ownerKey !== loadKey) continue;
2097
2408
  if (mm._serverItemId && serverIds[mm._serverItemId]) continue;
2098
2409
  if (!mm._serverItemId) {
2099
2410
  if (mappedHasPendingAssistant) continue;
@@ -2138,11 +2449,12 @@ var ChatSession = class {
2138
2449
  if (!item.poll || !item.id) return;
2139
2450
  if (self.historyItemPolls.has(item.id)) return;
2140
2451
  if (self.pendingAgentRequests[self.getHistoryCacheKey()] && !item._isBgTask && !item._isOnBgQueue) return;
2141
- self.historyItemPolls.set(item.id, true);
2452
+ if ((item._isBgTask || item._isOnBgQueue) && self.isPollingPaused()) return;
2142
2453
  var capturedId = item.id;
2143
2454
  var pp = item.poll({
2144
2455
  latency: POLL_INTERVAL,
2145
2456
  onResponse: function(response) {
2457
+ if (isPollStopped(response)) return;
2146
2458
  self.handleHistoryItemResolution(capturedId, response, platform);
2147
2459
  },
2148
2460
  onError: function(err) {
@@ -2178,6 +2490,7 @@ var ChatSession = class {
2178
2490
  }
2179
2491
  }
2180
2492
  });
2493
+ self._trackPoll(capturedId, item._isBgTask || item._isOnBgQueue ? "bg" : "fg", pp);
2181
2494
  if (pp && pp.catch) pp.catch(function() {
2182
2495
  });
2183
2496
  });
@@ -2390,6 +2703,6 @@ var ChatSession = class {
2390
2703
  }
2391
2704
  };
2392
2705
 
2393
- export { BG_INDEXING_QUEUE_SUFFIX, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, ChatSession, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, HISTORY_TOKEN_BUDGET, LINK_LABEL_MAX_DISPLAY_CHARS, MAX_HISTORY_MESSAGES, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MIN_INPUT_TOKEN_BUDGET, OUTPUT_TOKEN_RESERVE, POLL_INTERVAL, TOOL_AND_RESPONSE_BUFFER, buildBoundedChatMessages, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingContinueMessage, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, clearAttachmentParsers, composeUserMessage, configureChatEngine, createInlineLinkRegex, encodePathSegments, estimateMessageTokens, estimateTextTokens, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, filterListByClearHorizon, findAttachmentParser, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, groupAttachmentFailures, isAuthExpiredError, isErrorResponseBody, isNonRetryableRequestError, isOfficeFile, isServerExtractable, isServiceDbAttachmentHref, listClaudeModels, listOpenAIModels, makeExtractPlaceholder, mapHistoryListToMessages, normalizeAttachmentPathCandidate, normalizeTextContent, notifyAgentSaveAttachment, parseAttachmentContent, registerAttachmentParser, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay };
2706
+ export { BG_INDEXING_QUEUE_SUFFIX, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, ChatSession, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, HISTORY_TOKEN_BUDGET, LINK_LABEL_MAX_DISPLAY_CHARS, MAX_HISTORY_MESSAGES, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MIN_INPUT_TOKEN_BUDGET, OUTPUT_TOKEN_RESERVE, POLL_INTERVAL, RENDER_FROM_TOKEN, TOOL_AND_RESPONSE_BUFFER, buildBoundedChatMessages, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, clearAttachmentParsers, composeUserMessage, configureChatEngine, createInlineLinkRegex, encodePathSegments, estimateMessageTokens, estimateTextTokens, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, filterListByClearHorizon, findAttachmentParser, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, groupAttachmentFailures, isAuthExpiredError, isBgIndexingQueue, isErrorResponseBody, isNonRetryableRequestError, isOfficeFile, isServerExtractable, isServiceDbAttachmentHref, listClaudeModels, listOpenAIModels, makeExtractPlaceholder, mapHistoryListToMessages, normalizeAttachmentPathCandidate, normalizeTextContent, notifyAgentSaveAttachment, parseAttachmentContent, registerAttachmentParser, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay };
2394
2707
  //# sourceMappingURL=engine.mjs.map
2395
2708
  //# sourceMappingURL=engine.mjs.map