bunnyquery 1.11.1 → 1.13.0

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
@@ -357,10 +357,15 @@ function buildChatSystemPrompt(params) {
357
357
  const { projectId, serviceName, serviceDescription, greeting, canUpload, client } = params;
358
358
  const g = params.indexAccessGroup;
359
359
  const indexGroupLiteral = typeof g === "number" ? String(g) : g === "public" || g === "private" || g === "authorized" || g === "admin" ? `"${g}"` : '"authorized"';
360
+ const groundingRules = `ANSWER ONLY FROM THE DATA. Every fact you state about this project's data - a name, a number, a date, a status, what a document says, whether something exists - must come from one of four places: a tool result returned in THIS conversation, a file or image attached to it (including one you read through its attachment link), the project description at the end of this prompt, or what the user told you themselves in this chat. Your general knowledge, the open internet and a plausible guess are NOT sources. Never use them to answer a question about the user's data, to fill a gap in it, or to complete a record that came back partial. A value that is not in what you retrieved is unknown: say it is not in the data instead of supplying a likely one.
361
+ WHEN THE DATA DOES NOT HAVE IT, SAY SO. Once you have searched the way these rules require and nothing answers the question, tell the user plainly, in their language, that this project's data does not contain it, and say what you searched (the tables, files or terms) so they can tell missing data from a missed search. Do not answer from memory instead, and do not turn a missing answer into a general explanation of the topic. Offer what would let you answer it: ${canUpload === false ? `another name or file to look under, or asking the project's owner to add it` : `the document to upload, or another name or file to look under`}.
362
+ KEEP WHAT THE DATA SAYS APART FROM WHAT YOU WORKED OUT. A total you computed, a comparison, a trend, an interpretation or a recommendation is yours, not the data's: label it as such ("calculated from...", "this suggests...") and name the rows or files it was made from. Never present an estimate, an assumption or an inference as something the data states.
363
+ These three rules govern every claim about the project's data. They do not limit the About BunnyQuery section (answer product questions from it), a greeting, or text the user explicitly asks you to write.`;
360
364
  let systemPrompt = `
361
365
  You are a dedicated assistant for the project ID: "${projectId}".
362
366
  Scope: Only answer questions about this project and its data. Do not answer questions about other projects or topics unrelated to this project. When the user refers to "my database", "my data", or "my files", treat those as references to this project's database and file storage. The ONE exception is BunnyQuery itself - what this app is, what it can do, and how to use it - which is always in scope: answer it from the "About BunnyQuery" section at the end of this prompt.
363
367
  Knowledge lookup: Before saying you don't know or that something isn't in the chat history, ALWAYS query this project's database through the available MCP tools to look for the answer. The user's data is the source of truth - the chat transcript is not. Only respond with "I don't know" or "I couldn't find that" after you have actually searched the project's data and come back empty.
368
+ ${groundingRules}
364
369
  NUMBERS FROM A SPREADSHEET: use queryGrid, never mental arithmetic over records. A total, a count, an average, a "how many mention X", a "which one is biggest" - all of those are computed server-side over EVERY row of the file and come back with the sheet, the row count and the row numbers they were made from. Records are a SAMPLE, and a sample added up is a confident wrong number. Quote the row count and the sheet alongside the figure so the reader can check it.
365
370
  CALL queryGrid describe FIRST, before any figure. Workbooks routinely state the same money more than once: a detail sheet, then per-song, per-album and per-artist sheets that each re-total it, plus a summary sheet whose bottom row is the file total. Those look like four different answers and are one. describe names which sheets restate which, and which rows are totals. Pick ONE sheet, say which you picked, and never add figures across a sheet and its summary. If the reply carries a warning about restatement, repeat it to the user.
366
371
  A FILE TOTAL IS NOT A ROW'S TOTAL. The biggest number on a summary sheet is the whole file, not the thing that was asked about. Before quoting any figure, check it is scoped to what the question named: filter by the column that identifies it and report how many rows matched.
@@ -625,6 +630,11 @@ function getErrorMessage(input) {
625
630
  }
626
631
  return "Something went wrong.";
627
632
  }
633
+ function isRetriedFailure(input) {
634
+ var envErr = csrEnvelopeError(input);
635
+ if (envErr !== void 0) input = envErr;
636
+ return !!input && typeof input === "object" && input.retried === true;
637
+ }
628
638
  function isErrorResponseBody(response) {
629
639
  var envErr = csrEnvelopeError(response);
630
640
  if (envErr !== void 0) response = envErr;
@@ -3248,8 +3258,9 @@ function mapHistoryListToMessages(list, platform, opts) {
3248
3258
  var createdTs = Number(item && item.created);
3249
3259
  var updatedTs = Number(item && item.updated);
3250
3260
  var executedTs = Number(item && item.executed);
3261
+ var respondedTs = Number(item && item.responded);
3251
3262
  var userTs = isFinite(createdTs) && createdTs > 0 ? createdTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : void 0;
3252
- var replyTs = isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
3263
+ var replyTs = isFinite(respondedTs) && respondedTs > 0 ? respondedTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
3253
3264
  if (userText) {
3254
3265
  var displayContent;
3255
3266
  var indexFile = void 0;
@@ -3296,6 +3307,7 @@ function mapHistoryListToMessages(list, platform, opts) {
3296
3307
  mapped.push(ph);
3297
3308
  } else if (isQueued) ; else if (isErrorResponse) {
3298
3309
  var em = { role: "assistant", content: getErrorMessage(response), isError: true };
3310
+ if (isRetriedFailure(response)) em.isRetried = true;
3299
3311
  if (item._fromBgChain) em._fromBgChain = true;
3300
3312
  if (item._isBgTask) em.isBackgroundTask = true;
3301
3313
  if (serverItemId !== void 0) em._serverItemId = serverItemId;
@@ -3875,6 +3887,13 @@ var ChatSession = class {
3875
3887
  // the request budget MAX_CONCURRENT_BG_POLLS exists to protect.
3876
3888
  /** Live streams by server item id. One per in-flight streamed turn. */
3877
3889
  this.liveStreams = {};
3890
+ /** When skapi recorded a turn's response as having finished arriving, by server
3891
+ * item id. Read off the terminal envelope of a STREAMED turn (the only settle
3892
+ * that carries it, see _settleLiveStream) and used as the answer bubble's time
3893
+ * in place of this browser's clock. Entries are never removed: a settle reaches
3894
+ * here twice (the poll's onResponse, then the promise), and the value for an id
3895
+ * cannot change. */
3896
+ this._respondedAt = {};
3878
3897
  // ─── compact-stub hydration ─────────────────────────────────────────────
3879
3898
  // Split-fetch bg pages arrive as label stubs (no bodies). When the user
3880
3899
  // expands a row, the real reply text is fetched per item (csr-poll point
@@ -4324,7 +4343,9 @@ var ChatSession = class {
4324
4343
  timers = [];
4325
4344
  };
4326
4345
  var stopBase = base && typeof base.stop === "function" ? base.stop.bind(base) : null;
4346
+ var resolveRaced = null;
4327
4347
  var raced = new Promise(function(resolve, reject) {
4348
+ resolveRaced = resolve;
4328
4349
  base.then(function(res) {
4329
4350
  if (settled) return;
4330
4351
  settled = true;
@@ -4365,9 +4386,16 @@ var ChatSession = class {
4365
4386
  });
4366
4387
  });
4367
4388
  raced.stop = function() {
4389
+ if (settled) return;
4368
4390
  settled = true;
4369
4391
  clearProbes();
4370
- if (stopBase) stopBase();
4392
+ if (stopBase) {
4393
+ try {
4394
+ stopBase();
4395
+ } catch (e) {
4396
+ }
4397
+ }
4398
+ if (resolveRaced) resolveRaced({ id: itemId, status: "stopped" });
4371
4399
  };
4372
4400
  return raced;
4373
4401
  }
@@ -4391,6 +4419,23 @@ var ChatSession = class {
4391
4419
  });
4392
4420
  return n;
4393
4421
  }
4422
+ _noteRespondedAt(itemId, response) {
4423
+ if (!itemId || !isCsrStatusEnvelope(response)) return;
4424
+ var at = Number(response.responded);
4425
+ if (!isFinite(at) || at <= 0) return;
4426
+ if (!this._respondedAt) this._respondedAt = {};
4427
+ this._respondedAt[itemId] = at;
4428
+ }
4429
+ /** The time a live answer bubble is stamped with: when skapi received the
4430
+ * response, if the settle told us, else now. "Now" is what every bubble got
4431
+ * before, and it is still right for a buffered turn, whose settle hands back the
4432
+ * destination's body and nothing of skapi's; a history load then replaces it
4433
+ * with the server's value. */
4434
+ _replyStampFor(msg) {
4435
+ var id = msg ? msg._serverItemId : void 0;
4436
+ var at = typeof id === "string" && id && this._respondedAt ? this._respondedAt[id] : void 0;
4437
+ return typeof at === "number" ? at : wallClockNow();
4438
+ }
4394
4439
  /**
4395
4440
  * Open (or re-open) the live stream for `itemId`, or null when this poll must
4396
4441
  * not carry one.
@@ -4580,6 +4625,7 @@ var ChatSession = class {
4580
4625
  _settleLiveStream(st, response) {
4581
4626
  this._closeLiveStream(st, true);
4582
4627
  if (!isCsrStatusEnvelope(response)) return response;
4628
+ this._noteRespondedAt(st.id, response);
4583
4629
  if (response.status !== "resolved") return response;
4584
4630
  if (!this._mayFinalize(st)) this._rec().incomplete[st.id] = true;
4585
4631
  if (st.finalBody == null) return response;
@@ -5380,6 +5426,7 @@ var ChatSession = class {
5380
5426
  }
5381
5427
  return response;
5382
5428
  }).then(function(response) {
5429
+ if (isPollStopped(response) || self._isCancelledPollResult(response)) return { content: "", isError: false, cancelled: true };
5383
5430
  if (isErrorResponseBody(response)) return { content: getErrorMessage(response), isError: true };
5384
5431
  var answer = params.aiPlatform === "openai" ? extractOpenAIText(response) : extractClaudeText(response);
5385
5432
  answer = (answer || "").trim();
@@ -5389,6 +5436,10 @@ var ChatSession = class {
5389
5436
  }).then(function(result) {
5390
5437
  delete self.pendingAgentRequests[params.key];
5391
5438
  if (dispatchItemId) self.historyItemPolls.delete(dispatchItemId);
5439
+ if (result.cancelled) {
5440
+ if (dispatchItemId) self._settleCancelledTurn(dispatchItemId, params.key);
5441
+ return result;
5442
+ }
5392
5443
  var reply = { role: "assistant", content: result.content, isError: result.isError };
5393
5444
  if (dispatchItemId) reply._serverItemId = dispatchItemId;
5394
5445
  self._applyReplyToCache(params.key, reply, dispatchItemId);
@@ -5867,8 +5918,9 @@ var ChatSession = class {
5867
5918
  }
5868
5919
  });
5869
5920
  Promise.resolve(run).catch(function() {
5870
- }).then(function() {
5921
+ }).then(function(result) {
5871
5922
  self.state.sending = false;
5923
+ if (result && result.cancelled) return;
5872
5924
  if (!(self.host.isViewMounted() && self.getHistoryCacheKey() === key)) return;
5873
5925
  return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
5874
5926
  self.host.scrollToBottomIfSticky(true);
@@ -6015,7 +6067,7 @@ var ChatSession = class {
6015
6067
  return thinkingIdx !== -1 ? thinkingIdx : userIdx >= 0 ? userIdx + 1 : -1;
6016
6068
  }
6017
6069
  insertAtTarget(msg, targetIdx) {
6018
- if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = wallClockNow();
6070
+ if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = this._replyStampFor(msg);
6019
6071
  var tgt = targetIdx >= 0 ? this.state.messages[targetIdx] : void 0;
6020
6072
  var replaceable = !!tgt && !!tgt.isPending && !tgt.isBackgroundTask && this._isOwnPlaceholderOf(targetIdx, this._owningUserIndex(targetIdx));
6021
6073
  if (replaceable) this.state.messages[targetIdx] = msg;
@@ -6047,6 +6099,10 @@ var ChatSession = class {
6047
6099
  }
6048
6100
  onQueuedSendResponse(_composed, response, platform, serverId, ownerKey) {
6049
6101
  if (serverId) this.historyItemPolls.delete(serverId);
6102
+ if (this._isCancelledPollResult(response)) {
6103
+ if (serverId) this._settleCancelledTurn(serverId, ownerKey);
6104
+ return;
6105
+ }
6050
6106
  if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
6051
6107
  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." };
6052
6108
  this._applyReplyToCache(ownerKey, offReply, serverId);
@@ -6246,6 +6302,191 @@ var ChatSession = class {
6246
6302
  }
6247
6303
  });
6248
6304
  }
6305
+ /**
6306
+ * Stop waiting for the reply to the chat turn that is running now, and move on.
6307
+ *
6308
+ * `msg` is the turn's pending ASSISTANT bubble (the dot-trail "Thinking"
6309
+ * placeholder), which is where both views draw the control. A turn's two bubbles
6310
+ * carry the same _serverItemId, so it is also how the request is found.
6311
+ * cancelQueuedMessage is the sibling for a turn still waiting in the queue and
6312
+ * takes the USER bubble; the two differ in what they are handed and in what has
6313
+ * to be unwound afterwards, not in what the server is asked.
6314
+ *
6315
+ * What ends the wait:
6316
+ * 1. csr-cancel marks the running row cancelled. The worker stops relaying and
6317
+ * never stores an answer for it, so the reply cannot turn up later on this
6318
+ * device or another, and history replays the turn as cancelled;
6319
+ * 2. the poll on it is stopped, which settles whichever chain was awaiting it
6320
+ * with a stopped result: dispatchAgentRequest's for an immediate send, the
6321
+ * queued chain for a promoted turn, the history poll after a reload. Each
6322
+ * reads a stopped or cancelled result as "no reply" and releases what it
6323
+ * holds (state.sending, pendingAgentRequests, historyItemPolls), which is
6324
+ * what lets the next send go out immediately instead of queueing;
6325
+ * 3. the bubbles settle at once, without waiting for that chain: the user
6326
+ * bubble takes its cancelled form, the placeholder goes, and the next queued
6327
+ * turn, if there is one, is promoted so its own "Thinking" appears.
6328
+ *
6329
+ * What it does NOT do: the provider call already in flight on the worker runs
6330
+ * to its end (a streamed one stops within about a second). A message sent
6331
+ * behind it on the same queue starts once the worker acks the cancelled row.
6332
+ *
6333
+ * Refused without a server id (the ack has not come back yet; the views disable
6334
+ * the control until it has) and while a cancel is already out. Indexing passes
6335
+ * are not this method's business: their Stop is cancelIndexingGroup, which ends
6336
+ * the file's whole chain.
6337
+ */
6338
+ cancelPendingReply(msg, idx) {
6339
+ var self = this;
6340
+ var id = this.host.getIdentity();
6341
+ var serverId = msg._serverItemId;
6342
+ if (!serverId || msg._cancelling) return;
6343
+ if (msg.role !== "assistant" || !msg.isPending || msg.isBackgroundTask) return;
6344
+ var platform = id.platform;
6345
+ if (platform !== "claude" && platform !== "openai") return;
6346
+ var url = platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
6347
+ var queueBase = id.userId || id.projectId;
6348
+ var queue = msg._useBgQueue ? bgIndexingQueueName(queueBase) : queueBase;
6349
+ var findPlaceholder = function() {
6350
+ return self.state.messages.findIndex(function(m) {
6351
+ return m._serverItemId === serverId && m.role === "assistant" && !!m.isPending;
6352
+ });
6353
+ };
6354
+ var at = this.state.messages[idx] && this.state.messages[idx]._serverItemId === serverId && this.state.messages[idx].role === "assistant" ? idx : findPlaceholder();
6355
+ if (at !== -1) {
6356
+ this.state.messages[at] = Object.assign({}, this.state.messages[at], { _cancelling: true, _cancelError: void 0 });
6357
+ }
6358
+ this.host.notify();
6359
+ var unmark = function(errMsg) {
6360
+ var ci = findPlaceholder();
6361
+ if (ci === -1) return;
6362
+ self.state.messages[ci] = Object.assign({}, self.state.messages[ci], { _cancelling: false, _cancelError: errMsg });
6363
+ self.host.notify();
6364
+ };
6365
+ Promise.resolve(this.host.cancelRequest({
6366
+ url,
6367
+ method: "POST",
6368
+ id: serverId,
6369
+ queue,
6370
+ service: id.projectId,
6371
+ owner: id.owner
6372
+ })).then(function(result) {
6373
+ if (result && result.removed) {
6374
+ self._stopPoll(serverId);
6375
+ self._settleCancelledTurn(serverId);
6376
+ return;
6377
+ }
6378
+ unmark(void 0);
6379
+ }).catch(function(err) {
6380
+ unmark(err && typeof err.message === "string" && err.message ? err.message : "Could not stop this request.");
6381
+ });
6382
+ }
6383
+ /** The cancelled form of a request bubble, with everything that keeps it in
6384
+ * place carried over: the file markers that hold an indexing pass in its
6385
+ * collapsed row, the chat it belongs to, its send time and its render key. */
6386
+ _cancelledCopyOf(m, itemId) {
6387
+ var cancelled = { role: "user", content: m.content, isCancelled: true };
6388
+ if (itemId !== void 0) cancelled._serverItemId = itemId;
6389
+ else if (m._serverItemId !== void 0) cancelled._serverItemId = m._serverItemId;
6390
+ if (m.isBackgroundTask) cancelled.isBackgroundTask = true;
6391
+ if (m._indexFile) cancelled._indexFile = m._indexFile;
6392
+ if (m._useBgQueue) cancelled._useBgQueue = true;
6393
+ if (m._ownerKey !== void 0) cancelled._ownerKey = m._ownerKey;
6394
+ if (m._ts !== void 0) cancelled._ts = m._ts;
6395
+ if (m._localId !== void 0) cancelled._localId = m._localId;
6396
+ return cancelled;
6397
+ }
6398
+ /**
6399
+ * Settle a turn as cancelled wherever it lives, and let the queue move on.
6400
+ *
6401
+ * On the chat on screen: every pending placeholder carrying the id goes (a
6402
+ * history refetch can have re-mapped the running turn into a second one), the
6403
+ * request bubble takes its cancelled form, and the next queued turn is promoted
6404
+ * so the conversation does not sit on an "(In queue)" bubble with nothing
6405
+ * running. For a chat that is NOT on screen (the user moved on while the turn
6406
+ * ran, and it was cancelled from elsewhere) the same edit is made in that chat's
6407
+ * history cache, which is what the next visit renders.
6408
+ *
6409
+ * Idempotent on purpose: a cancel this client made settles the bubbles at once,
6410
+ * and the chain that was awaiting the poll settles again a moment later when the
6411
+ * stop resolves it. The second pass finds nothing to do. Deliberately NOT
6412
+ * _removeStrayPendingAssistants: by then the placeholder on screen is the freshly
6413
+ * promoted turn's, whose user bubble is pending, and the sweep would take it.
6414
+ */
6415
+ _settleCancelledTurn(itemId, ownerKey) {
6416
+ if (!itemId) return;
6417
+ if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
6418
+ this._applyCancelToCache(ownerKey, itemId);
6419
+ return;
6420
+ }
6421
+ var changed = false, wasBg = false;
6422
+ for (var i = this.state.messages.length - 1; i >= 0; i--) {
6423
+ var m = this.state.messages[i];
6424
+ if (!m || m._serverItemId !== itemId) continue;
6425
+ if (m.role === "assistant" && m.isPending) {
6426
+ this.state.messages.splice(i, 1);
6427
+ changed = true;
6428
+ continue;
6429
+ }
6430
+ if (m.role === "user" && !m.isCancelled) {
6431
+ if (m.isBackgroundTask) wasBg = true;
6432
+ this.state.messages[i] = this._cancelledCopyOf(m, itemId);
6433
+ changed = true;
6434
+ }
6435
+ }
6436
+ this.cancelledServerIds.delete(itemId);
6437
+ if (!changed) return;
6438
+ if (wasBg) this.promoteNextBgQueuedToRunning();
6439
+ else this.promoteNextQueuedToRunning();
6440
+ this.updateHistoryCache();
6441
+ this.host.notify();
6442
+ }
6443
+ /**
6444
+ * _settleCancelledTurn for a chat that is not on screen: the same edit, made in
6445
+ * its history cache. Matched by id where the cached bubbles carry one; an
6446
+ * off-chat send caches its pair before the ack (see dispatchComposedMessage), so
6447
+ * the fallback is positional, exactly as _applyReplyToCache's is: the trailing
6448
+ * pending placeholder, and the last request bubble still marked in flight.
6449
+ */
6450
+ _applyCancelToCache(key, itemId) {
6451
+ var existing = key ? this.aiChatHistoryCache[key] : void 0;
6452
+ if (!existing) return;
6453
+ var msgs = existing.messages.slice();
6454
+ var changed = false;
6455
+ var pIdx = -1, uIdx = -1;
6456
+ for (var i = msgs.length - 1; i >= 0; i--) {
6457
+ var m = msgs[i];
6458
+ if (!m || m._serverItemId !== itemId) continue;
6459
+ if (m.role === "assistant" && m.isPending) {
6460
+ msgs.splice(i, 1);
6461
+ changed = true;
6462
+ pIdx = i;
6463
+ } else if (m.role === "user" && !m.isCancelled && uIdx === -1) uIdx = i;
6464
+ }
6465
+ if (pIdx === -1) {
6466
+ for (var p = msgs.length - 1; p >= 0; p--) {
6467
+ var pm = msgs[p];
6468
+ if (!pm || !pm.isPending || pm.role !== "assistant" || pm.isBackgroundTask || pm._serverItemId !== void 0) continue;
6469
+ msgs.splice(p, 1);
6470
+ changed = true;
6471
+ break;
6472
+ }
6473
+ }
6474
+ if (uIdx === -1) {
6475
+ for (var u = msgs.length - 1; u >= 0; u--) {
6476
+ var um = msgs[u];
6477
+ if (!um || um.role !== "user" || um.isBackgroundTask || um._serverItemId !== void 0 || um._stageId) continue;
6478
+ if (!(um.isPendingQueued || um.isPendingInProcess || um.isSendingToServer)) continue;
6479
+ uIdx = u;
6480
+ break;
6481
+ }
6482
+ }
6483
+ if (uIdx !== -1) {
6484
+ msgs[uIdx] = this._cancelledCopyOf(msgs[uIdx], itemId);
6485
+ changed = true;
6486
+ }
6487
+ if (!changed) return;
6488
+ this.aiChatHistoryCache[key] = { messages: msgs, endOfList: existing.endOfList, startKeyHistory: existing.startKeyHistory };
6489
+ }
6249
6490
  /**
6250
6491
  * Stop indexing a file, from its collapsed row — every pass at once, not just
6251
6492
  * the bubble the user happens to see.
@@ -6312,6 +6553,7 @@ var ChatSession = class {
6312
6553
  }
6313
6554
  }
6314
6555
  this._adoptWorkerIndexingPasses(0);
6556
+ this._sweepCancelledIndexing();
6315
6557
  var ids = group.cancellableIds || [];
6316
6558
  if (!ids.length) {
6317
6559
  this.host.notify();
@@ -6448,7 +6690,7 @@ var ChatSession = class {
6448
6690
  enqueueTypewrite(idx, fullText, localId, paintedText) {
6449
6691
  var self = this;
6450
6692
  var target = this.state.messages[idx];
6451
- if (target && target._ts === void 0) target._ts = wallClockNow();
6693
+ if (target && target._ts === void 0) target._ts = this._replyStampFor(target);
6452
6694
  if (!this.typewriterQueue) this.typewriterQueue = Promise.resolve();
6453
6695
  this.typewriterQueue = this.typewriterQueue.then(function() {
6454
6696
  return self.typewriteIntoIndex(idx, fullText, localId, paintedText);
@@ -6594,9 +6836,10 @@ var ChatSession = class {
6594
6836
  this.state.sending = true;
6595
6837
  this.host.scrollToBottomIfSticky(true);
6596
6838
  return Promise.resolve(pending).catch(function() {
6597
- }).then(function() {
6839
+ }).then(function(result) {
6598
6840
  if (token !== self.state.gateRefreshToken) return;
6599
6841
  self.state.sending = false;
6842
+ if (result && result.cancelled) return;
6600
6843
  return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
6601
6844
  self.host.scrollToBottomIfSticky(true);
6602
6845
  });
@@ -8225,8 +8468,19 @@ function buildChatDisplayList(messages, opts) {
8225
8468
  } else if (stopped) {
8226
8469
  grp.status = "cancelled";
8227
8470
  } else {
8228
- var last = grp.members[grp.members.length - 1].msg;
8229
- grp.status = last.isError ? "error" : "done";
8471
+ var retriedIds = {};
8472
+ for (var ri = 0; ri < grp.members.length; ri++) {
8473
+ var rm = grp.members[ri].msg;
8474
+ if (rm.isRetried && rm._serverItemId) retriedIds[rm._serverItemId] = true;
8475
+ }
8476
+ var li = grp.members.length - 1;
8477
+ while (li >= 0 && (grp.members[li].msg.isRetried || grp.members[li].msg._serverItemId && retriedIds[grp.members[li].msg._serverItemId])) li--;
8478
+ if (li < 0) {
8479
+ grp.status = "active";
8480
+ } else {
8481
+ var last = grp.members[li].msg;
8482
+ grp.status = last.isError ? "error" : "done";
8483
+ }
8230
8484
  }
8231
8485
  var sawFirstPass = false;
8232
8486
  for (var pi = 0; pi < grp.members.length; pi++) {
@@ -8539,6 +8793,6 @@ function clearProjectSettings(service) {
8539
8793
  else cache.clear();
8540
8794
  }
8541
8795
 
8542
- export { BG_INDEXING_QUEUE_SUFFIX, BOM, BOM_EXTS, CHAT_GREETING_MAX_LENGTH, CLAUDE_INPUT_CAP_RATIO, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, ChatSession, DEFAULT_CLAUDE_MODEL, DEFAULT_CONTEXT_WINDOW, DEFAULT_OPENAI_MODEL, DEFAULT_UPLOAD_ACCESS_GROUP, EMPTY_INDEXING_REPLY, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, EXPIRED_LINK_REFRESH_EXPIRES_SECONDS, EXT_CONTENT_TYPES, HISTORY_BUDGET_RATIO, HISTORY_FILL_SLACK_PX, HISTORY_TOKEN_BUDGET, HTML_EXTS, HTML_HEAD_WINDOW, IMAGE_PREVIEWS_PER_MESSAGE, INDEXING_COMPLETE_MARKER, INDEXING_MAX_OUTPUT_TOKENS, INLINE_LINK_GLYPH, INLINE_LINK_UNAVAILABLE_GLYPH, INLINE_LINK_UNAVAILABLE_SUFFIX, INPUT_CAP_RATIO, LINK_LABEL_MAX_DISPLAY_CHARS, LINK_REFRESH_WINDOW_MS, MAX_CONCURRENT_BG_POLLS, MAX_HISTORY_FILL_PAGES, MAX_HISTORY_MESSAGES, MAX_OUTPUT_BY_MODEL, MAX_OUTPUT_TOKENS, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MINT_CACHE_GENERATION, MIN_INPUT_TOKEN_BUDGET, MIN_PER_REQUEST_INPUT_CAP, OUTPUT_TOKEN_RESERVE, POLL_INTERVAL, PRESIGN_SAFETY_MARGIN_MS, PREVIEWABLE_IMAGE_CONTENT_TYPES, PREVIEW_BROWSER_CACHE_SECONDS, PREVIEW_LAYOUT_BOX_SELECTOR, PREVIEW_URL_EXPIRES_SECONDS, PROJECT_SETTINGS_ACCESS_GROUP, PROJECT_SETTINGS_TABLE, PROJECT_SETTINGS_UNIQUE_ID, RENDER_FROM_TOKEN, RTF_EXTS, RUN_RECORD_WORKING_STALE_MS, STREAM_POLL_INTERVAL, TOOL_AND_RESPONSE_BUFFER, UPLOAD_ACCESS_GROUPS, UPLOAD_ACCESS_HINTS, UPLOAD_ACCESS_LABELS, UPLOAD_ACCESS_OPTIONS, XML_EXTS, __resetSplitHistoryState, accessSettingFrom, adoptLocalAnswerIntoPage, applyEncodingDeclaration, asksUploadAccessFrom, bgIndexingQueueName, buildAiAgentValue, buildBoundedChatMessages, buildChatDisplayList, buildChatGreeting, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildHistoryItemFullId, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, cachedProjectSettings, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, canonicalizePathForm, chatCacheKey, chatEngineConfig, chatGreetingFrom, chatStreamWiring, classifyInlineLink, clearAttachmentParsers, clearImagePreviewCache, clearProjectSettings, composeUserMessage, configureChatEngine, configureProjectSettings, contentTypeForExt, createHistoryFiller, createInlineLinkRegex, createScrollAnchor, createSseParser, csrEnvelopeError, encodePathSegments, encodingClassForExt, ensureHtmlCharset, ensureXmlEncoding, escapeInlineHtml, escapeRtfNonAscii, estimateMessageTokens, estimateTextTokens, extOf, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, fetchLiveIndexingKeys, fillHistoryViewport, filterListByClearHorizon, findAttachmentParser, formatChatTimestamp, formatDuration, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, getInputTokenBudget, getMaxOutputTokens, getModelContextWindow, getProjectContextWindow, getSplitChatHistory, getVisionProfile, groupAttachmentFailures, hasBom, hydrateImagePreviews, indexDoneUniqueId, indexScopeKey, indexingAccessGroup, isAuthExpiredError, isBgIndexingQueue, isCsrStatusEnvelope, isErrorResponseBody, isHttpUrlLike, isImageVisionFile, isIndexingRequestText, isLinkUnavailable, isNonRetryableRequestError, isOfficeFile, isPagedReadFile, isPreviewableImagePath, isProviderApiKeyError, isServerExtractable, isServiceDbAttachmentHref, isWindowedReadFile, linkUnavailableKeyForHref, linkUnavailableKeyForPath, linkUnavailableKeysForPath, listClaudeModels, listOpenAIModels, liveSafePrefix, loadProjectSettings, looksLikeRtf, makeExtractPlaceholder, mapHistoryListToMessages, markImagePreviewStale, mayKeepStreamedAnswer, mintCacheBustStamp, needsBomForExt, normalizeAttachmentPathCandidate, normalizeChatGreeting, normalizeExt, normalizeProjectAccessSetting, normalizeTextContent, normalizeTrailingInlineToken, normalizeUploadAccessGroup, notifyAgentSaveAttachment, parseAiAgentValue, parseAttachmentContent, parseIndexingLabel, parseIndexingRequestText, patchProjectSettings, peekImagePreviewUrl, prepareDownloadText, presignExpiryEpochMs, previewImageContentType, previewLayoutBox, previewMintCacheToken, previewableExtOf, primeProjectSettings, projectAccessSetting, projectAsksUploadAccess, projectChatGreeting, projectSettingsSettled, projectUploadAccessGroup, readExpiredAttachmentHref, readyProjectSettings, registerAttachmentParser, registerModelContextWindows, renderInlineLinkHtml, repairUrlEntities, repairUrlWhitespace, resolveImagePreviewUrl, runIndexUniqueId, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, setProjectContextWindow, setProjectSettings, shouldRescueInFlightMessage, skapiSupportsStreaming, streamRecoveryEnabled, streamRecoveryLabels, streamRecoveryPhase, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay, typewriterResumeIndex, uploadAccessGroupFrom, upsertIndexRunRecordSafe, wallClockNow };
8796
+ export { BG_INDEXING_QUEUE_SUFFIX, BOM, BOM_EXTS, CHAT_GREETING_MAX_LENGTH, CLAUDE_INPUT_CAP_RATIO, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, ChatSession, DEFAULT_CLAUDE_MODEL, DEFAULT_CONTEXT_WINDOW, DEFAULT_OPENAI_MODEL, DEFAULT_UPLOAD_ACCESS_GROUP, EMPTY_INDEXING_REPLY, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, EXPIRED_LINK_REFRESH_EXPIRES_SECONDS, EXT_CONTENT_TYPES, HISTORY_BUDGET_RATIO, HISTORY_FILL_SLACK_PX, HISTORY_TOKEN_BUDGET, HTML_EXTS, HTML_HEAD_WINDOW, IMAGE_PREVIEWS_PER_MESSAGE, INDEXING_COMPLETE_MARKER, INDEXING_MAX_OUTPUT_TOKENS, INLINE_LINK_GLYPH, INLINE_LINK_UNAVAILABLE_GLYPH, INLINE_LINK_UNAVAILABLE_SUFFIX, INPUT_CAP_RATIO, LINK_LABEL_MAX_DISPLAY_CHARS, LINK_REFRESH_WINDOW_MS, MAX_CONCURRENT_BG_POLLS, MAX_HISTORY_FILL_PAGES, MAX_HISTORY_MESSAGES, MAX_OUTPUT_BY_MODEL, MAX_OUTPUT_TOKENS, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MINT_CACHE_GENERATION, MIN_INPUT_TOKEN_BUDGET, MIN_PER_REQUEST_INPUT_CAP, OUTPUT_TOKEN_RESERVE, POLL_INTERVAL, PRESIGN_SAFETY_MARGIN_MS, PREVIEWABLE_IMAGE_CONTENT_TYPES, PREVIEW_BROWSER_CACHE_SECONDS, PREVIEW_LAYOUT_BOX_SELECTOR, PREVIEW_URL_EXPIRES_SECONDS, PROJECT_SETTINGS_ACCESS_GROUP, PROJECT_SETTINGS_TABLE, PROJECT_SETTINGS_UNIQUE_ID, RENDER_FROM_TOKEN, RTF_EXTS, RUN_RECORD_WORKING_STALE_MS, STREAM_POLL_INTERVAL, TOOL_AND_RESPONSE_BUFFER, UPLOAD_ACCESS_GROUPS, UPLOAD_ACCESS_HINTS, UPLOAD_ACCESS_LABELS, UPLOAD_ACCESS_OPTIONS, XML_EXTS, __resetSplitHistoryState, accessSettingFrom, adoptLocalAnswerIntoPage, applyEncodingDeclaration, asksUploadAccessFrom, bgIndexingQueueName, buildAiAgentValue, buildBoundedChatMessages, buildChatDisplayList, buildChatGreeting, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildHistoryItemFullId, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, cachedProjectSettings, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, canonicalizePathForm, chatCacheKey, chatEngineConfig, chatGreetingFrom, chatStreamWiring, classifyInlineLink, clearAttachmentParsers, clearImagePreviewCache, clearProjectSettings, composeUserMessage, configureChatEngine, configureProjectSettings, contentTypeForExt, createHistoryFiller, createInlineLinkRegex, createScrollAnchor, createSseParser, csrEnvelopeError, encodePathSegments, encodingClassForExt, ensureHtmlCharset, ensureXmlEncoding, escapeInlineHtml, escapeRtfNonAscii, estimateMessageTokens, estimateTextTokens, extOf, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, fetchLiveIndexingKeys, fillHistoryViewport, filterListByClearHorizon, findAttachmentParser, formatChatTimestamp, formatDuration, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, getInputTokenBudget, getMaxOutputTokens, getModelContextWindow, getProjectContextWindow, getSplitChatHistory, getVisionProfile, groupAttachmentFailures, hasBom, hydrateImagePreviews, indexDoneUniqueId, indexScopeKey, indexingAccessGroup, isAuthExpiredError, isBgIndexingQueue, isCsrStatusEnvelope, isErrorResponseBody, isHttpUrlLike, isImageVisionFile, isIndexingRequestText, isLinkUnavailable, isNonRetryableRequestError, isOfficeFile, isPagedReadFile, isPreviewableImagePath, isProviderApiKeyError, isRetriedFailure, isServerExtractable, isServiceDbAttachmentHref, isWindowedReadFile, linkUnavailableKeyForHref, linkUnavailableKeyForPath, linkUnavailableKeysForPath, listClaudeModels, listOpenAIModels, liveSafePrefix, loadProjectSettings, looksLikeRtf, makeExtractPlaceholder, mapHistoryListToMessages, markImagePreviewStale, mayKeepStreamedAnswer, mintCacheBustStamp, needsBomForExt, normalizeAttachmentPathCandidate, normalizeChatGreeting, normalizeExt, normalizeProjectAccessSetting, normalizeTextContent, normalizeTrailingInlineToken, normalizeUploadAccessGroup, notifyAgentSaveAttachment, parseAiAgentValue, parseAttachmentContent, parseIndexingLabel, parseIndexingRequestText, patchProjectSettings, peekImagePreviewUrl, prepareDownloadText, presignExpiryEpochMs, previewImageContentType, previewLayoutBox, previewMintCacheToken, previewableExtOf, primeProjectSettings, projectAccessSetting, projectAsksUploadAccess, projectChatGreeting, projectSettingsSettled, projectUploadAccessGroup, readExpiredAttachmentHref, readyProjectSettings, registerAttachmentParser, registerModelContextWindows, renderInlineLinkHtml, repairUrlEntities, repairUrlWhitespace, resolveImagePreviewUrl, runIndexUniqueId, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, setProjectContextWindow, setProjectSettings, shouldRescueInFlightMessage, skapiSupportsStreaming, streamRecoveryEnabled, streamRecoveryLabels, streamRecoveryPhase, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay, typewriterResumeIndex, uploadAccessGroupFrom, upsertIndexRunRecordSafe, wallClockNow };
8543
8797
  //# sourceMappingURL=engine.mjs.map
8544
8798
  //# sourceMappingURL=engine.mjs.map