bunnyquery 1.10.7 → 1.10.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunnyquery",
3
- "version": "1.10.7",
3
+ "version": "1.10.10",
4
4
  "description": "Embeddable BunnyQuery AI chat widget + its framework-agnostic chat engine",
5
5
  "main": "bunnyquery.js",
6
6
  "exports": {
@@ -858,6 +858,13 @@ export function mapHistoryListToMessages(list: any[], platform: 'claude' | 'open
858
858
  if (item._isBgTask) em.isBackgroundTask = true;
859
859
  if (serverItemId !== undefined) em._serverItemId = serverItemId;
860
860
  if (replyTs !== undefined) em._ts = replyTs;
861
+ // A pass that FAILED still ran, and how long it ran before failing is worth
862
+ // as much as a successful pass's duration -- often more, since that is where
863
+ // a timeout shows itself. `att` is stamped before the upstream call fires,
864
+ // so a failed row carries it exactly as a resolved one does. (Same stamp as
865
+ // the success branch below; the stream-pending branch deliberately has none,
866
+ // having no end time yet.)
867
+ if (indexFile && isFinite(executedTs) && executedTs > 0) em._tsStart = executedTs;
861
868
  mapped.push(em);
862
869
  } else if (isStreamPending) {
863
870
  // The bubble stands for the turn so the merge has something to key on and
@@ -1452,7 +1452,11 @@ export type BgTaskEntry = {
1452
1452
  mime?: string;
1453
1453
  size?: number;
1454
1454
  status: 'running' | 'pending';
1455
- poll: ((opts: { latency: number }) => Promise<any>) | undefined;
1455
+ // `onResponse` receives the settled value plus a `meta` of facts about the
1456
+ // REQUEST rather than the response: `executed` is when the worker began
1457
+ // running it, which rides on a running poll tick's status envelope and is
1458
+ // therefore free to a caller already polling.
1459
+ poll: ((opts: { latency: number; onResponse?: (res: any, meta?: { executed?: number }) => void }) => Promise<any>) | undefined;
1456
1460
  /** How many CONTINUE passes have already run for this file (resume-across-passes). */
1457
1461
  resumePass?: number;
1458
1462
  /** The STAGED chat turn these files were attached to (ChatSession.stageOutgoingMessage).
@@ -4025,7 +4025,52 @@ export class ChatSession {
4025
4025
  }
4026
4026
 
4027
4027
  // --- background-task resolution + drain -------------------------------
4028
- handleHistoryItemResolution(itemId: string, response: any, platform: string): void {
4028
+ /** Record how long a background indexing pass took, on the bubble that just
4029
+ * settled, so the duration shows immediately instead of waiting for whatever
4030
+ * next refetches history.
4031
+ *
4032
+ * `_tsStart` is otherwise written only by the history mapper, which reads the
4033
+ * row's `executed`; a live settle never sees the row at all (the poll resolves
4034
+ * with the destination's body). This is the same value by the other route.
4035
+ *
4036
+ * `_ts` is stamped too when the branch that built the bubble left it without
4037
+ * one: it is the END of the pass, and the pass ended now. A later history load
4038
+ * replaces both with the server's own numbers.
4039
+ *
4040
+ * Indexing passes only: an ordinary turn has no duration to show, and
4041
+ * `_tsStart`'s presence is what both views read to decide. */
4042
+ private _stampPassDuration(itemId: string, executedAt?: number): void {
4043
+ if (!itemId) return;
4044
+ if (!this._indexRefOfItem(itemId)) return;
4045
+ var hasStart = typeof executedAt === 'number' && executedAt > 0;
4046
+ for (var i = this.state.messages.length - 1; i >= 0; i--) {
4047
+ var m: any = this.state.messages[i];
4048
+ if (!m || m.role !== 'assistant' || m._serverItemId !== itemId) continue;
4049
+ if (m.isPending) continue;
4050
+ // `_ts` is stamped whether or not an execution start arrived. The two
4051
+ // background reply builds in applyHistoryItemResolution are the only ones
4052
+ // in the engine that bypass both enqueueTypewrite and insertAtTarget --
4053
+ // they write a literal and return -- so this is where an indexing reply
4054
+ // gets its timestamp at all. Leaving it inside the duration guard meant a
4055
+ // pass with no `executed` (the poll never saw a running tick) rendered with
4056
+ // NO TIME LINE whatsoever, because both clients gate the whole element on
4057
+ // `_ts`. Losing the duration is the intended degradation; losing the
4058
+ // timestamp with it was not.
4059
+ if (typeof m._ts !== 'number') m._ts = Date.now();
4060
+ if (hasStart) m._tsStart = executedAt;
4061
+ // Both of these, because the stamp lands AFTER applyHistoryItemResolution
4062
+ // has already notified and cached. Without the notify the widget never
4063
+ // repaints (its notify() IS the render, so the duration only appeared on
4064
+ // the next unrelated render); without the re-cache the entry written a
4065
+ // moment ago is the one missing `_tsStart`, so a rebuild from cache -- a
4066
+ // remount, a tab return -- silently dropped the duration again.
4067
+ this.host.notify();
4068
+ this.updateHistoryCache();
4069
+ break;
4070
+ }
4071
+ }
4072
+
4073
+ handleHistoryItemResolution(itemId: string, response: any, platform: string, executedAt?: number): void {
4029
4074
  // Which file this turn was indexing, read BEFORE the resolution rewrites the
4030
4075
  // bubbles. Every settling turn funnels through here — the bg drain's own
4031
4076
  // poll, the history poll, and agent.vue's forked history poll, which calls
@@ -4033,6 +4078,17 @@ export class ChatSession {
4033
4078
  // makes it work in both clients without either of them forking a call site.
4034
4079
  var indexRef = this._indexRefOfItem(itemId);
4035
4080
  this.applyHistoryItemResolution(itemId, response, platform);
4081
+ // After the settle, never inside it: applyHistoryItemResolution builds the
4082
+ // assistant bubble in four separate branches (answer, empty reply, error,
4083
+ // bg) and stamping in each would be four chances to forget one.
4084
+ //
4085
+ // It lives HERE rather than at the call sites because this method is the one
4086
+ // funnel every settle goes through, and the call sites are the thing that
4087
+ // kept getting missed: agent.vue runs its OWN forked history poll, and that
4088
+ // fork -- not the engine's drain -- is what settles a pass uploaded from the
4089
+ // files page, so a stamp wired only into the drain never ran for the exact
4090
+ // case it was written for. A fourth caller added later inherits this for free.
4091
+ this._stampPassDuration(itemId, executedAt);
4036
4092
  this.promoteNextBgQueuedToRunning();
4037
4093
  // applyHistoryItemResolution just released this item's poll slot. Under
4038
4094
  // MAX_CONCURRENT_BG_POLLS that slot is what the next-oldest unpolled entry
@@ -4678,13 +4734,25 @@ export class ChatSession {
4678
4734
  var capturedId = entry.id, capturedPlat = plat;
4679
4735
  var capturedEntry = entry;
4680
4736
  var wasStopped = false;
4681
- var bp = entry.poll({ latency: POLL_INTERVAL });
4737
+ // When the worker BEGAN running this pass. It rides on the poll's status
4738
+ // envelope (a running tick), never on the terminal read, which hands back
4739
+ // the destination's own body. Captured here so a settled pass can show how
4740
+ // long it took WITHOUT a second request: the alternative is waiting for
4741
+ // whatever next refetches history, which for a pass that settles in front
4742
+ // of the user is not until they leave the chat and come back.
4743
+ var executedAt: number | undefined;
4744
+ var bp = entry.poll({
4745
+ latency: POLL_INTERVAL,
4746
+ onResponse: function (_res: any, meta: any) {
4747
+ if (meta && typeof meta.executed === 'number' && meta.executed > 0) executedAt = meta.executed;
4748
+ },
4749
+ });
4682
4750
  self._trackPoll(entry.id, 'bg', bp);
4683
4751
  bp.then(function (response: any) {
4684
4752
  // A stopped poll is not a result: leave the bubble and the queue entry
4685
4753
  // exactly as they were so resumePolling can re-attach.
4686
4754
  if (isPollStopped(response)) { wasStopped = true; return; }
4687
- self.handleHistoryItemResolution(capturedId, response, capturedPlat);
4755
+ self.handleHistoryItemResolution(capturedId, response, capturedPlat, executedAt);
4688
4756
  self.maybeResumeIndexing(capturedEntry, response, capturedPlat);
4689
4757
  }).catch(function (err: any) {
4690
4758
  self.historyItemPolls.delete(capturedId);
@@ -5466,7 +5534,15 @@ export class ChatSession {
5466
5534
  // items keep the flat cadence: nobody is watching, and they are what the
5467
5535
  // MAX_CONCURRENT_BG_POLLS budget exists to protect.
5468
5536
  var pollOpts = {
5469
- onResponse: function (response: any) { if (isPollStopped(response)) return; self.handleHistoryItemResolution(capturedId, response, platform); },
5537
+ // `meta.executed` rides on the poll's running ticks, so a pass this
5538
+ // path re-attached to (a reload or a remount while its chain was
5539
+ // still going) shows its duration at settle exactly as one polled
5540
+ // from the drain does. Without it, only passes started in this page
5541
+ // life would be stamped.
5542
+ onResponse: function (response: any, meta: any) {
5543
+ if (isPollStopped(response)) return;
5544
+ self.handleHistoryItemResolution(capturedId, response, platform, meta && meta.executed);
5545
+ },
5470
5546
  onError: function (err: any) {
5471
5547
  self.historyItemPolls.delete(capturedId);
5472
5548
  var isNotExists = err && (err.code === 'NOT_EXISTS' || (err.body && err.body.code === 'NOT_EXISTS'));