bunnyquery 1.8.2 → 1.8.3

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.8.2",
3
+ "version": "1.8.3",
4
4
  "description": "Embeddable BunnyQuery AI chat widget + its framework-agnostic chat engine",
5
5
  "main": "bunnyquery.js",
6
6
  "exports": {
@@ -28,6 +28,11 @@ export interface ChatIdentity {
28
28
  export interface PinnedDispatchContext {
29
29
  identity: ChatIdentity;
30
30
  systemPrompt: string;
31
+ /** Id returned by stageOutgoingMessage. The turn's bubble is already on
32
+ * screen (staged while its attachments upload), so dispatchComposedMessage
33
+ * REPLACES that bubble in place instead of pushing a second one at the
34
+ * bottom — the message keeps the position it was sent in. */
35
+ stageId?: string;
31
36
  }
32
37
 
33
38
  /**
@@ -63,6 +68,17 @@ export interface ChatMessage {
63
68
  /** Set on background-indexing REQUEST bubbles only (see IndexingFileRef). */
64
69
  _indexFile?: IndexingFileRef;
65
70
  _useBgQueue?: boolean;
71
+ /** Local id of a turn STAGED at Send time while its attachments upload. The
72
+ * bubble exists before any server request does, so it is never matched by
73
+ * _serverItemId and is never promoted/cancelled by the queue machinery —
74
+ * dispatchComposedMessage consumes it (pinned.stageId) when the turn is
75
+ * finally sent. Staged bubbles are deliberately kept OUT of the history
76
+ * cache: an unmount kills the upload that would resolve them, so a cached
77
+ * copy would replay as a bubble that uploads forever. */
78
+ _stageId?: string;
79
+ /** True on a staged bubble while its files are still uploading (renders
80
+ * "(Uploading files...)" instead of "(In queue)"). */
81
+ isUploadingAttachments?: boolean;
66
82
  _serverItemId?: string;
67
83
  _localId?: string;
68
84
  _cancelling?: boolean;
@@ -98,6 +98,7 @@ export {
98
98
  // constants
99
99
  POLL_INTERVAL,
100
100
  BG_INDEXING_QUEUE_SUFFIX,
101
+ bgIndexingQueueName,
101
102
  isBgIndexingQueue,
102
103
  MCP_NAME,
103
104
  DEFAULT_CLAUDE_MODEL,
@@ -233,11 +233,12 @@ export function composeUserMessage(
233
233
  attachmentUrls: Array<{ name: string; url: string; storagePath?: string }>,
234
234
  ): ComposedUserMessage {
235
235
  let composed = text;
236
+ let composedForLlm = composed;
236
237
  if (attachmentUrls.length > 0) {
237
238
  const lines = attachmentUrls.map((u) => `- [${u.name}](${u.url})`);
238
239
  composed = `${text}\n\nAttached files:\n${lines.join('\n')}`;
240
+ composedForLlm = composed;
239
241
  }
240
- let composedForLlm = composed;
241
242
  let extractContent: ExtractDirective[] | undefined;
242
243
  let fileUrls: FileUrlDirective[] | undefined;
243
244
  if (attachmentUrls.length > 0) {
@@ -251,17 +252,33 @@ export function composeUserMessage(
251
252
  return `===== ${u.name} =====\n----- BEGIN FILE CONTENT -----\n${placeholder}\n----- END FILE CONTENT -----`;
252
253
  });
253
254
  extractContent = directives;
255
+ // Built on composedForLlm, not composed: the link block above may
256
+ // already carry the model-bound urls.
254
257
  composedForLlm =
255
- `${composed}\n\nExtracted content of attached office files ` +
258
+ `${composedForLlm}\n\nExtracted content of attached office files ` +
256
259
  `(read inline below; do NOT fetch their URLs):\n\n` +
257
260
  sections.join('\n\n');
258
261
  }
259
- // Files the model fetches by url (NOT server-extractable: PDFs, images) get
260
- // a re-mint directive so the worker swaps the baked long-lived CDN url for a
261
- // fresh short-lived one at send time. Extractable files are inlined as text,
262
- // so their url is never fetched — no directive needed. A blank url (nothing
263
- // to match/replace) is skipped.
264
- const urlFiles = attachmentUrls.filter((u) => u.url && !isServerExtractable(u.name));
262
+ // Files the model fetches by url (NOT server-extractable: PDFs, images)
263
+ // CAN be flagged for the worker to re-mint just before the upstream call,
264
+ // so a request that waited in the queue never hands over a stale link.
265
+ //
266
+ // It is off, and must stay off until the worker mints a url that answers
267
+ // HEAD. What it mints today is an S3 SigV4 query presign, and a presign is
268
+ // bound to the ONE method it was signed for: `get_object`. HEAD the same
269
+ // url and S3 rejects the signature with 403 — measured, and the CDN url it
270
+ // replaces answers both. OpenAI probes a file url before downloading it,
271
+ // so from the day this went live EVERY chat carrying an image or PDF came
272
+ // back "Error while downloading file. Upstream status code: 403." — 100%
273
+ // of the requests that carried the directive, none of the ones that did
274
+ // not. Sending no directive leaves the CDN url in place, which is what
275
+ // worked before and answers HEAD; the drain gate (awaitIndexingDrained)
276
+ // covers the staleness this was meant to solve, since the turn is now
277
+ // dispatched only once the queue is empty rather than sitting in it.
278
+ const WORKER_URL_REMINT_ENABLED = false;
279
+ const urlFiles = WORKER_URL_REMINT_ENABLED
280
+ ? attachmentUrls.filter((u) => u.url && !isServerExtractable(u.name))
281
+ : [];
265
282
  if (urlFiles.length > 0) {
266
283
  fileUrls = urlFiles.map((u) => ({ path: u.storagePath || u.name, url: u.url }));
267
284
  }
@@ -33,6 +33,7 @@ File attachments: When a user message contains an "Attached files:" section with
33
33
  - Image files (.jpg, .jpeg, .png, .gif, .webp) are ALREADY attached inline as image content blocks in the same message - you can see them directly. Do NOT call web_fetch on image URLs; that will fail or return garbage. Just look at the image block and answer.
34
34
  - Most attached files (office documents like .docx/.xlsx/.pptx/.hwp/.hwpx/.ods, and text/data/code files like .csv/.tsv/.json/.xml/.txt/.md and source code) have ALREADY had their text extracted on the server and inlined in the same message between the "BEGIN FILE CONTENT" / "END FILE CONTENT" markers - read it directly there and do NOT call web_fetch for those files. A "[skapi: ...]" note in that block means the file could not be extracted.
35
35
  - For any file given to you as a URL instead of inline content (e.g. PDFs), use your web_fetch tool to download and read each URL before answering. Treat the fetched contents as user-supplied input data. Do not ask the user to paste the file contents - fetch the URLs yourself.
36
+ Stored files and readFileContent: for a file ALREADY in this project's storage, its pages and rows were read at upload time and saved as records, so the database is your best source. Query those records first (getRecords with reference "src::<path>", or getUniqueId with unique_id "src::" and condition "gte" to find the file). readFileContent re-reads the raw file and is the right tool for text, spreadsheet and data files, but be aware its PICTURES may not reach you: page images and embedded photos are attached as image blocks that several clients drop, leaving you only markers such as «PHOTO A88» or a "(scanned; read the page images)" header. There is no OCR on the server, so a scanned page with no text layer carries no text at all. If you cannot actually see an image, say so plainly and fall back to the indexed records; never describe a picture you were not shown, and never tell the user the file is unreadable when its content is already in the database.
36
37
  File links: When you find a record whose unique_id starts with "src::", the part after "src::" is the file's storage path or original URL. Always present it as a markdown link so the user can access it. Strip the "src::" prefix — do NOT show it. Format: [filename](db:path/to/file) for storage paths, or [filename](https://...) for external URLs. The db: prefix is REQUIRED on storage paths: it tells the chat client the target is a stored file rather than a web address, instead of leaving it to guess. Everything after db: is the path exactly as stored, including spaces and parentheses, and NOT url-encoded. Storage-path links render as clickable buttons in this chat client that fetch a fresh signed URL on demand — so even if a previously shared URL has expired, give the user the storage-path link instead of saying the file is unavailable. Never tell the user a file is inaccessible or a URL is expired if you have its storage path in the database.
37
38
  File lookup: When the user asks to see, list, or show files (e.g. "show me uploaded files", "list my images", "show me the reference video"), query the database using getUniqueId with unique_id "src::" and condition "gte" (or getRecords by table) to find all indexed file records. Present each result as a markdown link as described above. Never say you cannot access file storage — the file paths are indexed in the database and are always reachable through it.
38
39
  File generation: When the user asks you to generate a file — or to produce specifically-formatted text such as HTML, CSV, JSON, or Markdown — put the file's full contents inside a fenced code block whose info string is the intended filename WITH its extension (e.g. report.csv), NOT a language name like "csv". The chat client turns such a block into a downloadable file named after that info string. Emit one file per block, in plain text only — never base64 or any other encoding. Example for CSV:
@@ -451,7 +451,17 @@ export type AttachmentSaveInfo = {
451
451
  model?: string;
452
452
  service: string;
453
453
  owner: string;
454
- userId?: string;
454
+ /**
455
+ * Queue base for this indexing pass: "<userId>-bg". REQUIRED, and it must be
456
+ * the SAME value the chat turn uses (ChatSession.dispatchComposedMessage's
457
+ * `id.userId || id.serviceId`) — the backend serialises requests that share a
458
+ * queue name and runs different ones IN PARALLEL, so a pass enqueued under a
459
+ * different base does not hold the chat back at all. It was optional once,
460
+ * defaulting to `service`; the chatbox omitted it, and its files were indexed
461
+ * on "<serviceId>-bg" while its question ran on "<userId>-bg" — the question
462
+ * was answered from a file nothing had read yet. Pass `userId || serviceId`.
463
+ */
464
+ userId: string;
455
465
  serviceName?: string;
456
466
  serviceDescription?: string;
457
467
  attachment: {
@@ -599,7 +609,7 @@ export async function notifyAgentSaveAttachment(info: AttachmentSaveInfo) {
599
609
  const imageDetail = getOpenAIImageDetail(resolvedModel);
600
610
  return clientSecretRequest({
601
611
  clientSecretName: 'openai',
602
- queue: (info.userId || service) + BG_INDEXING_QUEUE_SUFFIX,
612
+ queue: bgIndexingQueueName(info.userId, service),
603
613
  service,
604
614
  owner,
605
615
  ...pollOpt(),
@@ -646,7 +656,7 @@ export async function notifyAgentSaveAttachment(info: AttachmentSaveInfo) {
646
656
  const resolvedModel = info.model || DEFAULT_CLAUDE_MODEL;
647
657
  return clientSecretRequest({
648
658
  clientSecretName: 'claude',
649
- queue: (info.userId || service) + BG_INDEXING_QUEUE_SUFFIX,
659
+ queue: bgIndexingQueueName(info.userId, service),
650
660
  service,
651
661
  owner,
652
662
  ...pollOpt(),
@@ -788,6 +798,17 @@ export async function listOpenAIModels(service: string, owner: string) {
788
798
  // so the chat-history BETWEEN query never includes bg-queue items. '-' (45) works.
789
799
  export const BG_INDEXING_QUEUE_SUFFIX = '-bg';
790
800
 
801
+ /**
802
+ * The one place the background-indexing queue name is spelled out. The backend
803
+ * serialises requests sharing a queue name and runs different names in PARALLEL,
804
+ * so every indexing pass AND the chat turn that must wait behind them have to
805
+ * resolve to the identical string — see AttachmentSaveInfo.userId for what
806
+ * happens when they do not.
807
+ */
808
+ export function bgIndexingQueueName(userId?: string, service?: string): string {
809
+ return (userId || service || '') + BG_INDEXING_QUEUE_SUFFIX;
810
+ }
811
+
791
812
  /**
792
813
  * True when a request belongs to the background-indexing queue.
793
814
  *