bunnyquery 1.3.0 → 1.3.4

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.3.0",
3
+ "version": "1.3.4",
4
4
  "description": "Embeddable BunnyQuery AI chat widget + its framework-agnostic chat engine",
5
5
  "main": "bunnyquery.js",
6
6
  "exports": {
Binary file
@@ -34,6 +34,55 @@ export function isErrorResponseBody(response: any): boolean {
34
34
  return false;
35
35
  }
36
36
 
37
+ // A request that failed because the REQUEST ITSELF is malformed — an unknown or
38
+ // rejected parameter on a 400/422 — will deterministically re-fail if resent
39
+ // unchanged; no session refresh or retry can fix it. Detecting it lets the retry
40
+ // gates refuse to auto-resend (otherwise a 400 whose param name merely contains
41
+ // "token", e.g. `max_output_tokens`, trips the `invalid_request + token` branch
42
+ // of isAuthExpiredError below and loops refresh->resend forever, burning tokens).
43
+ // This is the `_skapi_extract` 400 class. Distinct from auth-expiry, which IS
44
+ // retryable after a token refresh — so 401 (auth) and 429/5xx (transient) are
45
+ // intentionally NOT treated as non-retryable here.
46
+ export function isNonRetryableRequestError(input: any): boolean {
47
+ if (!input || typeof input !== 'object') return false;
48
+
49
+ var status = typeof input.status_code === 'number' ? input.status_code
50
+ : typeof input.status === 'number' ? input.status : undefined;
51
+
52
+ // Collect the error envelope wherever it lives (response, response.body, or a
53
+ // thrown error), mirroring isErrorResponseBody / isAuthExpiredError.
54
+ var param: any = undefined;
55
+ var blobs: string[] = [];
56
+ var sources = [input.error, input.body && input.body.error, input.body, input];
57
+ for (var i = 0; i < sources.length; i++) {
58
+ var e = sources[i];
59
+ if (!e) continue;
60
+ if (typeof e === 'string') { blobs.push(e); continue; }
61
+ if (typeof e !== 'object') continue;
62
+ if (param === undefined && e.param != null) param = e.param;
63
+ if (typeof e.code === 'string') blobs.push(e.code);
64
+ if (typeof e.type === 'string') blobs.push(e.type);
65
+ if (typeof e.message === 'string') blobs.push(e.message);
66
+ }
67
+ var hay = blobs.join(' | ').toLowerCase();
68
+
69
+ // Explicit "unknown / unsupported parameter" is always a malformed request.
70
+ if (hay.indexOf('unknown_parameter') !== -1 || hay.indexOf('unknown parameter') !== -1 ||
71
+ hay.indexOf('unsupported_parameter') !== -1 || hay.indexOf('unsupported parameter') !== -1) {
72
+ return true;
73
+ }
74
+
75
+ // A 400/422 invalid_request that points at a specific rejected field: resending
76
+ // the identical body re-fails. (401 -> isAuthExpiredError; 429/5xx -> transient.)
77
+ var isClientReqStatus = status === 400 || status === 422;
78
+ if (isClientReqStatus && param != null && param !== '') return true;
79
+ if (isClientReqStatus && hay.indexOf('invalid_request') !== -1 &&
80
+ (hay.indexOf('parameter') !== -1 || hay.indexOf('param') !== -1)) {
81
+ return true;
82
+ }
83
+ return false;
84
+ }
85
+
37
86
  export function isAuthExpiredError(input: any): boolean {
38
87
  if (!input) return false;
39
88
  var blobs: string[] = [];
@@ -21,11 +21,16 @@ export {
21
21
  type ComposedUserMessage,
22
22
  } from './office';
23
23
 
24
+ export {
25
+ groupAttachmentFailures,
26
+ type AttachmentFailureGroup,
27
+ } from './attachments';
28
+
24
29
  export * from './prompts';
25
30
 
26
31
  // Pure helpers (Tier-1.5): error detection, token budgeting, link/path
27
32
  // normalization, and history mapping — shared so both consumers stay identical.
28
- export { getErrorMessage, isErrorResponseBody, isAuthExpiredError } from './errors';
33
+ export { getErrorMessage, isErrorResponseBody, isAuthExpiredError, isNonRetryableRequestError } from './errors';
29
34
  export * from './budget';
30
35
  export * from './links';
31
36
  export {
@@ -30,7 +30,7 @@ import {
30
30
  OPENAI_RESPONSES_API_URL,
31
31
  type BgTaskEntry,
32
32
  } from './requests';
33
- import { isErrorResponseBody, isAuthExpiredError, getErrorMessage } from './errors';
33
+ import { isErrorResponseBody, isAuthExpiredError, isNonRetryableRequestError, getErrorMessage } from './errors';
34
34
  import { buildBoundedChatMessages } from './budget';
35
35
  import { createInlineLinkRegex } from './links';
36
36
  import { mapHistoryListToMessages, extractLastUserTextFromRequest } from './history';
@@ -116,11 +116,15 @@ export class ChatSession {
116
116
  };
117
117
  var run = sendAndPoll()
118
118
  .catch(function (err: any) {
119
- if (isAuthExpiredError(err)) return self.host.refreshSession().then(sendAndPoll);
119
+ // Only auth-expiry is worth a refresh+resend; a malformed-request 400
120
+ // (e.g. the `_skapi_extract`/unknown-parameter class) re-fails identically,
121
+ // so never loop on it — guard against isAuthExpiredError's heuristic
122
+ // misfiring on a param name that merely contains "token".
123
+ if (isAuthExpiredError(err) && !isNonRetryableRequestError(err)) return self.host.refreshSession().then(sendAndPoll);
120
124
  throw err;
121
125
  })
122
126
  .then(function (response: any) {
123
- if (isErrorResponseBody(response) && isAuthExpiredError(response)) {
127
+ if (isErrorResponseBody(response) && isAuthExpiredError(response) && !isNonRetryableRequestError(response)) {
124
128
  return self.host.refreshSession().then(sendAndPoll);
125
129
  }
126
130
  return response;
@@ -656,7 +660,7 @@ export class ChatSession {
656
660
  var fetchHistory = function () { return getChatHistory({ service: serviceId, owner: owner, platform: platform }, options); };
657
661
 
658
662
  return Promise.resolve().then(fetchHistory).catch(function (err: any) {
659
- if (isAuthExpiredError(err)) return self.host.refreshSession().then(fetchHistory);
663
+ if (isAuthExpiredError(err) && !isNonRetryableRequestError(err)) return self.host.refreshSession().then(fetchHistory);
660
664
  throw err;
661
665
  }).then(function (history: any) {
662
666
  if (token !== self.state.gateRefreshToken) return;
@@ -783,6 +787,7 @@ export class ChatSession {
783
787
  var self = this;
784
788
  var id = this.host.getIdentity();
785
789
  att.status = 'uploading'; att.progress = 0; att.errorMessage = '';
790
+ att.errorCode = ''; att.errorDetail = ''; // clear any prior failure (retry)
786
791
  this.host.renderAttachmentChips();
787
792
  var members = (att.kind === 'folder')
788
793
  ? (att.files || []).map(function (f: any) { return { file: f.file, relPath: f.path, storagePath: self.host.storagePathFor(f.path) }; })
@@ -851,6 +856,11 @@ export class ChatSession {
851
856
  }, function (e: any) {
852
857
  console.error('[chat-engine] indexing request failed', e);
853
858
  anyIndexFailed = true; // uploaded but not indexed → yellow
859
+ // Record the first index error's code/message for the report dialog.
860
+ if (!att.errorCode && !att.errorDetail) {
861
+ att.errorCode = (e && (e.code || (e.body && e.body.code))) || '';
862
+ att.errorDetail = (e && (e.message || (e.body && e.body.message))) || (typeof e === 'string' ? e : '');
863
+ }
854
864
  });
855
865
  });
856
866
  });
@@ -894,6 +904,9 @@ export class ChatSession {
894
904
  if (removed || aborted) return;
895
905
  att.status = 'error';
896
906
  att.errorMessage = 'File upload has failed';
907
+ // Preserve the original error code/message for the report dialog.
908
+ att.errorCode = (err && (err.code || (err.body && err.body.code))) || '';
909
+ att.errorDetail = (err && (err.message || (err.body && err.body.message))) || (typeof err === 'string' ? err : '');
897
910
  self.host.renderAttachmentChips();
898
911
  });
899
912
  });
package/src/widget.css CHANGED
@@ -823,6 +823,13 @@
823
823
  .bq-modal-desc { margin: 0 0 1.25rem; font-size: 0.85rem; color: var(--bq-muted); line-height: 1.6; }
824
824
  .bq-modal-btns { display: flex; gap: 0.75rem; justify-content: flex-end; align-items: center; }
825
825
 
826
+ /* upload error report dialog (failed files grouped by error) */
827
+ .bq-upload-error-list { display: flex; flex-direction: column; gap: 0.85rem; margin: 0 0 1.25rem; max-height: 50vh; overflow-y: auto; }
828
+ .bq-upload-error-group { border-left: 3px solid var(--bq-danger); padding-left: 0.7rem; }
829
+ .bq-upload-error-heading { margin: 0 0 0.3rem; font-size: 0.82rem; font-weight: 600; color: var(--bq-danger); word-break: break-word; }
830
+ .bq-upload-error-files { margin: 0; padding-left: 1.1rem; font-size: 0.8rem; color: var(--bq-ink); }
831
+ .bq-upload-error-files li { margin: 0.1rem 0; word-break: break-word; }
832
+
826
833
  /* delete/clear (danger) modal accents */
827
834
  .bq-modal-delete-header {
828
835
  display: flex;