bunnyquery 1.8.1 → 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/bunnyquery.js +566 -108
- package/dist/engine.cjs +480 -35
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +210 -4
- package/dist/engine.d.ts +210 -4
- package/dist/engine.mjs +461 -36
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/download_encoding.ts +281 -0
- package/src/engine/errors.ts +34 -0
- package/src/engine/host.ts +16 -0
- package/src/engine/index.ts +4 -0
- package/src/engine/office.ts +25 -8
- package/src/engine/prompts/chat_system_prompt.ts +1 -0
- package/src/engine/requests.ts +28 -4
- package/src/engine/session.ts +348 -31
package/bunnyquery.js
CHANGED
|
@@ -213,14 +213,15 @@
|
|
|
213
213
|
}
|
|
214
214
|
function composeUserMessage(text, attachmentUrls) {
|
|
215
215
|
let composed = text;
|
|
216
|
+
let composedForLlm = composed;
|
|
216
217
|
if (attachmentUrls.length > 0) {
|
|
217
218
|
const lines = attachmentUrls.map((u) => `- [${u.name}](${u.url})`);
|
|
218
219
|
composed = `${text}
|
|
219
220
|
|
|
220
221
|
Attached files:
|
|
221
222
|
${lines.join("\n")}`;
|
|
223
|
+
composedForLlm = composed;
|
|
222
224
|
}
|
|
223
|
-
let composedForLlm = composed;
|
|
224
225
|
let extractContent;
|
|
225
226
|
let fileUrls;
|
|
226
227
|
if (attachmentUrls.length > 0) {
|
|
@@ -237,13 +238,13 @@ ${placeholder}
|
|
|
237
238
|
----- END FILE CONTENT -----`;
|
|
238
239
|
});
|
|
239
240
|
extractContent = directives;
|
|
240
|
-
composedForLlm = `${
|
|
241
|
+
composedForLlm = `${composedForLlm}
|
|
241
242
|
|
|
242
243
|
Extracted content of attached office files (read inline below; do NOT fetch their URLs):
|
|
243
244
|
|
|
244
245
|
` + sections.join("\n\n");
|
|
245
246
|
}
|
|
246
|
-
const urlFiles =
|
|
247
|
+
const urlFiles = [];
|
|
247
248
|
if (urlFiles.length > 0) {
|
|
248
249
|
fileUrls = urlFiles.map((u) => ({ path: u.storagePath || u.name, url: u.url }));
|
|
249
250
|
}
|
|
@@ -287,6 +288,7 @@ File attachments: When a user message contains an "Attached files:" section with
|
|
|
287
288
|
- 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.
|
|
288
289
|
- 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.
|
|
289
290
|
- 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.
|
|
291
|
+
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 \xABPHOTO A88\xBB 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.
|
|
290
292
|
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 \u2014 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 \u2014 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.
|
|
291
293
|
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 \u2014 the file paths are indexed in the database and are always reachable through it.
|
|
292
294
|
File generation: When the user asks you to generate a file \u2014 or to produce specifically-formatted text such as HTML, CSV, JSON, or Markdown \u2014 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 \u2014 never base64 or any other encoding. Example for CSV:
|
|
@@ -433,6 +435,19 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
433
435
|
}
|
|
434
436
|
|
|
435
437
|
// src/engine/errors.ts
|
|
438
|
+
var STATUS_MESSAGE = {
|
|
439
|
+
"408": "The AI provider timed out before it started.",
|
|
440
|
+
"409": "The AI provider rejected the request as conflicting.",
|
|
441
|
+
"413": "The request was too large for the AI provider.",
|
|
442
|
+
"429": "The AI provider is rate limiting requests right now.",
|
|
443
|
+
"500": "The AI provider hit an internal error.",
|
|
444
|
+
"502": "The AI provider is temporarily unreachable.",
|
|
445
|
+
"503": "The AI provider is temporarily unavailable.",
|
|
446
|
+
"504": "The AI provider timed out."
|
|
447
|
+
};
|
|
448
|
+
function isTransientStatus(status) {
|
|
449
|
+
return status === 408 || status === 425 || status === 429 || status >= 500;
|
|
450
|
+
}
|
|
436
451
|
function getErrorMessage(input) {
|
|
437
452
|
if (!input) return "Something went wrong.";
|
|
438
453
|
if (typeof input === "string") return input;
|
|
@@ -440,6 +455,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
440
455
|
if (input.body && input.body.error && input.body.error.message) return input.body.error.message;
|
|
441
456
|
if (input.body && typeof input.body.message === "string") return input.body.message;
|
|
442
457
|
if (input.message) return input.message;
|
|
458
|
+
var status = typeof input.status_code === "number" ? input.status_code : typeof input.status === "number" ? input.status : 0;
|
|
459
|
+
if (status) {
|
|
460
|
+
var text = STATUS_MESSAGE[String(status)] || (status >= 500 ? "The AI provider returned a server error." : "The AI provider rejected the request.");
|
|
461
|
+
return text + " (error " + status + ")" + (isTransientStatus(status) ? " This is usually temporary, please try again." : "");
|
|
462
|
+
}
|
|
443
463
|
return "Something went wrong.";
|
|
444
464
|
}
|
|
445
465
|
function isErrorResponseBody(response) {
|
|
@@ -852,6 +872,163 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
852
872
|
};
|
|
853
873
|
}
|
|
854
874
|
|
|
875
|
+
// src/engine/download_encoding.ts
|
|
876
|
+
var BOM = "\uFEFF";
|
|
877
|
+
var BOM_EXTS = /* @__PURE__ */ new Set(["csv", "tsv", "tab", "txt", "text", "log"]);
|
|
878
|
+
var HTML_EXTS = /* @__PURE__ */ new Set(["html", "htm", "xhtml"]);
|
|
879
|
+
var XML_EXTS = /* @__PURE__ */ new Set(["xml", "svg", "rss", "atom", "xsl", "xslt", "plist", "kml"]);
|
|
880
|
+
var RTF_EXTS = /* @__PURE__ */ new Set(["rtf"]);
|
|
881
|
+
var EXT_CONTENT_TYPES = {
|
|
882
|
+
csv: "text/csv; charset=utf-8",
|
|
883
|
+
tsv: "text/tab-separated-values; charset=utf-8",
|
|
884
|
+
tab: "text/tab-separated-values; charset=utf-8",
|
|
885
|
+
txt: "text/plain; charset=utf-8",
|
|
886
|
+
text: "text/plain; charset=utf-8",
|
|
887
|
+
log: "text/plain; charset=utf-8",
|
|
888
|
+
md: "text/markdown; charset=utf-8",
|
|
889
|
+
markdown: "text/markdown; charset=utf-8",
|
|
890
|
+
json: "application/json; charset=utf-8",
|
|
891
|
+
jsonl: "application/x-ndjson; charset=utf-8",
|
|
892
|
+
ndjson: "application/x-ndjson; charset=utf-8",
|
|
893
|
+
geojson: "application/geo+json; charset=utf-8",
|
|
894
|
+
yaml: "text/yaml; charset=utf-8",
|
|
895
|
+
yml: "text/yaml; charset=utf-8",
|
|
896
|
+
toml: "text/plain; charset=utf-8",
|
|
897
|
+
ini: "text/plain; charset=utf-8",
|
|
898
|
+
sql: "text/plain; charset=utf-8",
|
|
899
|
+
html: "text/html; charset=utf-8",
|
|
900
|
+
htm: "text/html; charset=utf-8",
|
|
901
|
+
xhtml: "application/xhtml+xml; charset=utf-8",
|
|
902
|
+
xml: "application/xml; charset=utf-8",
|
|
903
|
+
svg: "image/svg+xml; charset=utf-8",
|
|
904
|
+
css: "text/css; charset=utf-8",
|
|
905
|
+
js: "text/javascript; charset=utf-8",
|
|
906
|
+
ts: "text/plain; charset=utf-8",
|
|
907
|
+
py: "text/x-python; charset=utf-8",
|
|
908
|
+
sh: "text/x-shellscript; charset=utf-8",
|
|
909
|
+
srt: "application/x-subrip; charset=utf-8",
|
|
910
|
+
vtt: "text/vtt; charset=utf-8",
|
|
911
|
+
ics: "text/calendar; charset=utf-8",
|
|
912
|
+
vcf: "text/vcard; charset=utf-8",
|
|
913
|
+
// RTF is 7-bit ASCII by specification, so it takes no charset parameter.
|
|
914
|
+
rtf: "application/rtf",
|
|
915
|
+
// Binary types the model can only ever REFERENCE, never author in a fence, but
|
|
916
|
+
// which keep the type sensible if one ever shows up.
|
|
917
|
+
pdf: "application/pdf",
|
|
918
|
+
png: "image/png",
|
|
919
|
+
jpg: "image/jpeg",
|
|
920
|
+
jpeg: "image/jpeg",
|
|
921
|
+
gif: "image/gif",
|
|
922
|
+
webp: "image/webp"
|
|
923
|
+
};
|
|
924
|
+
function normalizeExt(ext) {
|
|
925
|
+
return String(ext || "").trim().replace(/^\./, "").toLowerCase();
|
|
926
|
+
}
|
|
927
|
+
function extOf(filename) {
|
|
928
|
+
const name = String(filename || "");
|
|
929
|
+
const dot = name.lastIndexOf(".");
|
|
930
|
+
return dot > 0 ? normalizeExt(name.slice(dot + 1)) : "";
|
|
931
|
+
}
|
|
932
|
+
function encodingClassForExt(ext) {
|
|
933
|
+
const e = normalizeExt(ext);
|
|
934
|
+
if (BOM_EXTS.has(e)) return "bom";
|
|
935
|
+
if (HTML_EXTS.has(e)) return "html";
|
|
936
|
+
if (XML_EXTS.has(e)) return "xml";
|
|
937
|
+
if (RTF_EXTS.has(e)) return "rtf";
|
|
938
|
+
return "none";
|
|
939
|
+
}
|
|
940
|
+
function contentTypeForExt(ext, fallback = "text/plain; charset=utf-8") {
|
|
941
|
+
return EXT_CONTENT_TYPES[normalizeExt(ext)] || fallback;
|
|
942
|
+
}
|
|
943
|
+
function hasBom(text) {
|
|
944
|
+
return typeof text === "string" && text.charCodeAt(0) === 65279;
|
|
945
|
+
}
|
|
946
|
+
var HTML_HEAD_WINDOW = 4096;
|
|
947
|
+
var META_CHARSET_RE = /<meta[^>]+charset\s*=\s*["']?\s*([a-z0-9_-]+)/i;
|
|
948
|
+
var META_HTTP_EQUIV_RE = /<meta[^>]+http-equiv\s*=\s*["']?content-type["']?[^>]*>/i;
|
|
949
|
+
function ensureHtmlCharset(text) {
|
|
950
|
+
const src = String(text == null ? "" : text);
|
|
951
|
+
const head = src.slice(0, HTML_HEAD_WINDOW);
|
|
952
|
+
const declared = META_CHARSET_RE.exec(head);
|
|
953
|
+
if (declared) {
|
|
954
|
+
if (declared[1].toLowerCase().replace(/[^a-z0-9]/g, "") === "utf8") return src;
|
|
955
|
+
const start = declared.index + declared[0].length - declared[1].length;
|
|
956
|
+
return src.slice(0, start) + "utf-8" + src.slice(start + declared[1].length);
|
|
957
|
+
}
|
|
958
|
+
const httpEquiv = META_HTTP_EQUIV_RE.exec(head);
|
|
959
|
+
if (httpEquiv) {
|
|
960
|
+
return src.slice(0, httpEquiv.index) + '<meta charset="utf-8">' + src.slice(httpEquiv.index + httpEquiv[0].length);
|
|
961
|
+
}
|
|
962
|
+
const tag = '<meta charset="utf-8">';
|
|
963
|
+
const headOpen = /<head[^>]*>/i.exec(head);
|
|
964
|
+
if (headOpen) {
|
|
965
|
+
const at = headOpen.index + headOpen[0].length;
|
|
966
|
+
return src.slice(0, at) + "\n" + tag + src.slice(at);
|
|
967
|
+
}
|
|
968
|
+
const htmlOpen = /<html[^>]*>/i.exec(head);
|
|
969
|
+
if (htmlOpen) {
|
|
970
|
+
const at = htmlOpen.index + htmlOpen[0].length;
|
|
971
|
+
return src.slice(0, at) + "\n<head>" + tag + "</head>" + src.slice(at);
|
|
972
|
+
}
|
|
973
|
+
const doctype = /<!doctype[^>]*>/i.exec(head);
|
|
974
|
+
if (doctype) {
|
|
975
|
+
const at = doctype.index + doctype[0].length;
|
|
976
|
+
return src.slice(0, at) + "\n" + tag + src.slice(at);
|
|
977
|
+
}
|
|
978
|
+
return tag + "\n" + src;
|
|
979
|
+
}
|
|
980
|
+
var XML_DECL_RE = /^\s*<\?xml\s[^?]*\?>/i;
|
|
981
|
+
function ensureXmlEncoding(text) {
|
|
982
|
+
const src = String(text == null ? "" : text);
|
|
983
|
+
const decl = XML_DECL_RE.exec(src);
|
|
984
|
+
if (!decl) return src;
|
|
985
|
+
const found = /encoding\s*=\s*["']([^"']*)["']/i.exec(decl[0]);
|
|
986
|
+
if (!found) return src;
|
|
987
|
+
if (found[1].toLowerCase().replace(/[^a-z0-9]/g, "") === "utf8") return src;
|
|
988
|
+
const fixedDecl = decl[0].slice(0, found.index) + found[0].replace(found[1], "UTF-8") + decl[0].slice(found.index + found[0].length);
|
|
989
|
+
return fixedDecl + src.slice(decl[0].length);
|
|
990
|
+
}
|
|
991
|
+
var RTF_SIGNATURE_RE = /^[\s]*\{\\rtf/i;
|
|
992
|
+
function looksLikeRtf(text) {
|
|
993
|
+
return RTF_SIGNATURE_RE.test(String(text == null ? "" : text));
|
|
994
|
+
}
|
|
995
|
+
function escapeRtfNonAscii(text) {
|
|
996
|
+
const src = String(text == null ? "" : text);
|
|
997
|
+
let out = "";
|
|
998
|
+
let plainFrom = 0;
|
|
999
|
+
for (let i = 0; i < src.length; i++) {
|
|
1000
|
+
const code = src.charCodeAt(i);
|
|
1001
|
+
if (code < 128) continue;
|
|
1002
|
+
out += src.slice(plainFrom, i);
|
|
1003
|
+
out += `\\u${code > 32767 ? code - 65536 : code}?`;
|
|
1004
|
+
plainFrom = i + 1;
|
|
1005
|
+
}
|
|
1006
|
+
return plainFrom === 0 ? src : out + src.slice(plainFrom);
|
|
1007
|
+
}
|
|
1008
|
+
function applyEncodingDeclaration(text, ext) {
|
|
1009
|
+
const src = String(text == null ? "" : text);
|
|
1010
|
+
switch (encodingClassForExt(ext)) {
|
|
1011
|
+
case "bom":
|
|
1012
|
+
return hasBom(src) ? src : BOM + src;
|
|
1013
|
+
case "html":
|
|
1014
|
+
return ensureHtmlCharset(src);
|
|
1015
|
+
case "xml":
|
|
1016
|
+
return ensureXmlEncoding(src);
|
|
1017
|
+
case "rtf":
|
|
1018
|
+
return looksLikeRtf(src) ? escapeRtfNonAscii(src) : hasBom(src) ? src : BOM + src;
|
|
1019
|
+
default:
|
|
1020
|
+
return src;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function prepareDownloadText(filename, body) {
|
|
1024
|
+
const ext = extOf(filename);
|
|
1025
|
+
return {
|
|
1026
|
+
ext,
|
|
1027
|
+
text: applyEncodingDeclaration(body, ext),
|
|
1028
|
+
contentType: contentTypeForExt(ext)
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
855
1032
|
// src/engine/time.ts
|
|
856
1033
|
function wallClockNow() {
|
|
857
1034
|
return Date.now();
|
|
@@ -1013,7 +1190,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1013
1190
|
return { ...m, content: blocks };
|
|
1014
1191
|
});
|
|
1015
1192
|
}
|
|
1016
|
-
var POLL_INTERVAL =
|
|
1193
|
+
var POLL_INTERVAL = 3e3;
|
|
1017
1194
|
async function callClaudeWithMcp({
|
|
1018
1195
|
prompt,
|
|
1019
1196
|
messages,
|
|
@@ -1228,7 +1405,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1228
1405
|
const imageDetail = getOpenAIImageDetail(resolvedModel2);
|
|
1229
1406
|
return clientSecretRequest({
|
|
1230
1407
|
clientSecretName: "openai",
|
|
1231
|
-
queue: (info.userId
|
|
1408
|
+
queue: bgIndexingQueueName(info.userId, service),
|
|
1232
1409
|
service,
|
|
1233
1410
|
owner,
|
|
1234
1411
|
...pollOpt(),
|
|
@@ -1272,7 +1449,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1272
1449
|
const resolvedModel = info.model || DEFAULT_CLAUDE_MODEL;
|
|
1273
1450
|
return clientSecretRequest({
|
|
1274
1451
|
clientSecretName: "claude",
|
|
1275
|
-
queue: (info.userId
|
|
1452
|
+
queue: bgIndexingQueueName(info.userId, service),
|
|
1276
1453
|
service,
|
|
1277
1454
|
owner,
|
|
1278
1455
|
...pollOpt(),
|
|
@@ -1361,6 +1538,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1361
1538
|
return "";
|
|
1362
1539
|
}
|
|
1363
1540
|
var BG_INDEXING_QUEUE_SUFFIX = "-bg";
|
|
1541
|
+
function bgIndexingQueueName(userId, service) {
|
|
1542
|
+
return (userId || service || "") + BG_INDEXING_QUEUE_SUFFIX;
|
|
1543
|
+
}
|
|
1364
1544
|
function isBgIndexingQueue(queueName) {
|
|
1365
1545
|
if (typeof queueName !== "string" || !queueName) return false;
|
|
1366
1546
|
const prefix = queueName.split("|")[0];
|
|
@@ -1600,6 +1780,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1600
1780
|
// src/engine/session.ts
|
|
1601
1781
|
var WORKER_PASS_ADOPT_LIMIT = 20;
|
|
1602
1782
|
var WORKER_PASS_ADOPT_ATTEMPTS = [0, 2e3, 6e3];
|
|
1783
|
+
var INDEXING_DRAIN_BUSY_POLL_MS = 8e3;
|
|
1784
|
+
var INDEXING_DRAIN_CONFIRM_POLL_MS = 3e3;
|
|
1785
|
+
var INDEXING_DRAIN_IDLE_LOOKS = 2;
|
|
1786
|
+
var INDEXING_DRAIN_MIN_MS = 8e3;
|
|
1787
|
+
var INDEXING_DRAIN_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
1603
1788
|
var _g = typeof globalThis !== "undefined" ? globalThis : {};
|
|
1604
1789
|
function nowMs() {
|
|
1605
1790
|
return _g.performance && typeof _g.performance.now === "function" ? _g.performance.now() : Date.now();
|
|
@@ -1672,6 +1857,25 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1672
1857
|
this._pauseReasons = /* @__PURE__ */ new Set();
|
|
1673
1858
|
this._resuming = false;
|
|
1674
1859
|
this._lidSeq = 0;
|
|
1860
|
+
this._stageSeq = 0;
|
|
1861
|
+
this._uploadBatches = 0;
|
|
1862
|
+
this._indexDispatchesInFlight = 0;
|
|
1863
|
+
}
|
|
1864
|
+
/** Wrap an indexing-request dispatch so awaitIndexingDrained counts it as
|
|
1865
|
+
* live work from the moment it is sent, not from the moment it is acked. */
|
|
1866
|
+
trackIndexDispatch(p) {
|
|
1867
|
+
var self = this;
|
|
1868
|
+
this._indexDispatchesInFlight += 1;
|
|
1869
|
+
var release = function() {
|
|
1870
|
+
self._indexDispatchesInFlight = Math.max(0, self._indexDispatchesInFlight - 1);
|
|
1871
|
+
};
|
|
1872
|
+
return p.then(function(v) {
|
|
1873
|
+
release();
|
|
1874
|
+
return v;
|
|
1875
|
+
}, function(e) {
|
|
1876
|
+
release();
|
|
1877
|
+
throw e;
|
|
1878
|
+
});
|
|
1675
1879
|
}
|
|
1676
1880
|
/**
|
|
1677
1881
|
* Register a live poll so (a) a remount dedupes against it instead of stacking a
|
|
@@ -1777,6 +1981,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1777
1981
|
if (!key) return;
|
|
1778
1982
|
this.aiChatHistoryCache[key] = {
|
|
1779
1983
|
messages: this.state.messages.filter(function(m) {
|
|
1984
|
+
if (m._stageId) return false;
|
|
1780
1985
|
return m._ownerKey === void 0 || m._ownerKey === key;
|
|
1781
1986
|
}),
|
|
1782
1987
|
endOfList: this.state.historyEndOfList,
|
|
@@ -1916,14 +2121,189 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1916
2121
|
this.pendingAgentRequests[params.key] = run;
|
|
1917
2122
|
return run;
|
|
1918
2123
|
}
|
|
2124
|
+
/**
|
|
2125
|
+
* Put a turn on screen the INSTANT the user hits Send, before its attachments
|
|
2126
|
+
* have finished uploading. Uploads run in the background now (the composer is
|
|
2127
|
+
* cleared and stays usable), so without a staged bubble the message would
|
|
2128
|
+
* appear only once its files were up — below anything the user sent in the
|
|
2129
|
+
* meantime, in an order that never matches what they typed.
|
|
2130
|
+
*
|
|
2131
|
+
* Staged bubbles carry _useBgQueue because that is where a turn with
|
|
2132
|
+
* attachments ultimately dispatches (behind its own indexing tasks). That flag
|
|
2133
|
+
* is also what keeps promoteNextQueuedToRunning / resolveQueuedUserBubble off
|
|
2134
|
+
* them: those advance the SERVER queue, and a staged turn has no server
|
|
2135
|
+
* request behind it yet.
|
|
2136
|
+
*
|
|
2137
|
+
* Returns the id to hand back as PinnedDispatchContext.stageId at dispatch.
|
|
2138
|
+
*/
|
|
2139
|
+
stageOutgoingMessage(displayText) {
|
|
2140
|
+
this._stageSeq += 1;
|
|
2141
|
+
var stageId = "stg_" + this._stageSeq;
|
|
2142
|
+
var key = this.getHistoryCacheKey();
|
|
2143
|
+
var staged = {
|
|
2144
|
+
role: "user",
|
|
2145
|
+
content: displayText,
|
|
2146
|
+
isPendingQueued: true,
|
|
2147
|
+
isUploadingAttachments: true,
|
|
2148
|
+
isSendingToServer: true,
|
|
2149
|
+
_useBgQueue: true,
|
|
2150
|
+
_stageId: stageId,
|
|
2151
|
+
_ts: wallClockNow()
|
|
2152
|
+
};
|
|
2153
|
+
if (key) staged._ownerKey = key;
|
|
2154
|
+
this.state.messages.push(staged);
|
|
2155
|
+
this.host.notify();
|
|
2156
|
+
this.host.scrollToBottom(true);
|
|
2157
|
+
return stageId;
|
|
2158
|
+
}
|
|
2159
|
+
_stageIndex(list, stageId) {
|
|
2160
|
+
if (!stageId) return -1;
|
|
2161
|
+
for (var i = 0; i < list.length; i++) {
|
|
2162
|
+
if (list[i] && list[i]._stageId === stageId) return i;
|
|
2163
|
+
}
|
|
2164
|
+
return -1;
|
|
2165
|
+
}
|
|
2166
|
+
/**
|
|
2167
|
+
* Where a staged turn belongs once it is finally sent: BELOW every indexing
|
|
2168
|
+
* row, because that is the order it ran in and the order the server history
|
|
2169
|
+
* will report on the next load (its request id is newer than every pass it
|
|
2170
|
+
* waited for). While its files were uploading it sat above them — the rows
|
|
2171
|
+
* are injected as each file's pass starts, after the bubble was staged.
|
|
2172
|
+
*
|
|
2173
|
+
* Returns the index to insert at, or -1 to leave the turn where it is. Never
|
|
2174
|
+
* moves a turn UP: a bubble that already sits below the indexing rows (or a
|
|
2175
|
+
* chat with no indexing at all) must not jump backwards over anything.
|
|
2176
|
+
*/
|
|
2177
|
+
_settledStagePosition(fromIdx) {
|
|
2178
|
+
var lastBg = -1;
|
|
2179
|
+
for (var i = 0; i < this.state.messages.length; i++) {
|
|
2180
|
+
if (this.state.messages[i] && this.state.messages[i].isBackgroundTask) lastBg = i;
|
|
2181
|
+
}
|
|
2182
|
+
if (lastBg <= fromIdx) return -1;
|
|
2183
|
+
return lastBg;
|
|
2184
|
+
}
|
|
2185
|
+
/** The staged turn's files are up; it is now just waiting its place in the
|
|
2186
|
+
* queue. Drops the "(Uploading files...)" note back to "(In queue)". */
|
|
2187
|
+
markStagedMessageQueued(stageId) {
|
|
2188
|
+
var idx = this._stageIndex(this.state.messages, stageId);
|
|
2189
|
+
if (idx === -1) return;
|
|
2190
|
+
var ex = this.state.messages[idx];
|
|
2191
|
+
if (!ex.isUploadingAttachments) return;
|
|
2192
|
+
this.state.messages[idx] = Object.assign({}, ex, { isUploadingAttachments: false });
|
|
2193
|
+
this.host.notify();
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* Resolves once this project's background-indexing queue has nothing left to
|
|
2197
|
+
* run, so a chat enqueued right after it is genuinely last.
|
|
2198
|
+
*
|
|
2199
|
+
* Sending the chat as soon as the uploads finish is not enough, which is the
|
|
2200
|
+
* whole reason this exists: indexing a file is a CHAIN, and each pass is only
|
|
2201
|
+
* enqueued once the previous one lands (the client mints CONTINUE passes for
|
|
2202
|
+
* text/grid files, the worker mints them for PDFs and windowed reads). Every
|
|
2203
|
+
* one of those passes therefore queues up BEHIND a chat sent at upload time,
|
|
2204
|
+
* and the model answers from a file it has only partly read.
|
|
2205
|
+
*
|
|
2206
|
+
* The queue is read from the server's status index rather than from
|
|
2207
|
+
* bgTaskQueue: that mirror holds only what this client dispatched or adopted,
|
|
2208
|
+
* and it stops being maintained once the view unmounts. An empty answer has to
|
|
2209
|
+
* repeat before it is believed — see INDEXING_DRAIN_IDLE_LOOKS — and a look
|
|
2210
|
+
* that fails counts as busy, so a dropped request delays the turn instead of
|
|
2211
|
+
* releasing it early.
|
|
2212
|
+
*
|
|
2213
|
+
* Reads the identity PINNED at Send time, never a live one: the user may be in
|
|
2214
|
+
* another project by now, and this must keep asking about the one they sent
|
|
2215
|
+
* from.
|
|
2216
|
+
*/
|
|
2217
|
+
awaitIndexingDrained(identity) {
|
|
2218
|
+
var self = this;
|
|
2219
|
+
var svcId = identity && identity.serviceId;
|
|
2220
|
+
var platform = identity && identity.platform;
|
|
2221
|
+
if (!svcId || platform !== "claude" && platform !== "openai") return Promise.resolve("skipped");
|
|
2222
|
+
var owner = identity.owner;
|
|
2223
|
+
var queue = bgIndexingQueueName(identity.userId, svcId);
|
|
2224
|
+
var startedAt = nowMs();
|
|
2225
|
+
var deadline = startedAt + INDEXING_DRAIN_TIMEOUT_MS;
|
|
2226
|
+
var idleLooks = 0;
|
|
2227
|
+
var ask = function(status) {
|
|
2228
|
+
return Promise.resolve(getChatHistory(
|
|
2229
|
+
{ service: svcId, owner, platform, queue, status },
|
|
2230
|
+
{ limit: WORKER_PASS_ADOPT_LIMIT }
|
|
2231
|
+
)).catch(function() {
|
|
2232
|
+
return null;
|
|
2233
|
+
});
|
|
2234
|
+
};
|
|
2235
|
+
var hasLiveIndexing = function(res) {
|
|
2236
|
+
var list = res && Array.isArray(res.list) ? res.list : [];
|
|
2237
|
+
for (var i = 0; i < list.length; i++) {
|
|
2238
|
+
var item = list[i];
|
|
2239
|
+
if (!item || item.status !== "pending" && item.status !== "running") continue;
|
|
2240
|
+
if (isIndexingRequestText(extractLastUserTextFromRequest(item.request_body))) return true;
|
|
2241
|
+
}
|
|
2242
|
+
return false;
|
|
2243
|
+
};
|
|
2244
|
+
return new Promise(function(resolve) {
|
|
2245
|
+
var again = function() {
|
|
2246
|
+
setTimeout(look, idleLooks > 0 ? INDEXING_DRAIN_CONFIRM_POLL_MS : INDEXING_DRAIN_BUSY_POLL_MS);
|
|
2247
|
+
};
|
|
2248
|
+
var look = function() {
|
|
2249
|
+
if (nowMs() >= deadline) {
|
|
2250
|
+
resolve("timedout");
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
if (self._indexDispatchesInFlight > 0) {
|
|
2254
|
+
idleLooks = 0;
|
|
2255
|
+
again();
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
Promise.all([ask("running"), ask("pending")]).then(function(res) {
|
|
2259
|
+
var unknown = res[0] === null || res[1] === null;
|
|
2260
|
+
if (unknown || hasLiveIndexing(res[0]) || hasLiveIndexing(res[1])) idleLooks = 0;
|
|
2261
|
+
else idleLooks += 1;
|
|
2262
|
+
if (idleLooks >= INDEXING_DRAIN_IDLE_LOOKS && nowMs() - startedAt >= INDEXING_DRAIN_MIN_MS) {
|
|
2263
|
+
resolve("drained");
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
again();
|
|
2267
|
+
}, function() {
|
|
2268
|
+
idleLooks = 0;
|
|
2269
|
+
again();
|
|
2270
|
+
});
|
|
2271
|
+
};
|
|
2272
|
+
look();
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
/**
|
|
2276
|
+
* Abandon a staged turn — its uploads failed outright, so nothing will be
|
|
2277
|
+
* dispatched. The bubble stays (the user's text is not silently thrown away)
|
|
2278
|
+
* but settles into a plain, non-pending message; the caller reports the
|
|
2279
|
+
* failure separately.
|
|
2280
|
+
*/
|
|
2281
|
+
settleStagedMessage(stageId) {
|
|
2282
|
+
var idx = this._stageIndex(this.state.messages, stageId);
|
|
2283
|
+
if (idx === -1) return;
|
|
2284
|
+
var ex = this.state.messages[idx];
|
|
2285
|
+
var settled = { role: "user", content: ex.content };
|
|
2286
|
+
if (ex._ownerKey !== void 0) settled._ownerKey = ex._ownerKey;
|
|
2287
|
+
if (ex._ts !== void 0) settled._ts = ex._ts;
|
|
2288
|
+
this.state.messages[idx] = settled;
|
|
2289
|
+
this.host.notify();
|
|
2290
|
+
this.updateHistoryCache();
|
|
2291
|
+
}
|
|
1919
2292
|
// composed = clean display text; composedForLlm carries office-extraction
|
|
1920
2293
|
// placeholders for the provider only. useBgQueue routes a post-attachment turn
|
|
1921
2294
|
// onto the "-bg" queue so it runs after indexing.
|
|
1922
2295
|
dispatchComposedMessage(composed, useBgQueue, composedForLlm, extractContent, fileUrls, pinned) {
|
|
1923
2296
|
var self = this;
|
|
1924
|
-
|
|
2297
|
+
var stageId = pinned ? pinned.stageId : void 0;
|
|
2298
|
+
if (!composed) {
|
|
2299
|
+
if (stageId) this.settleStagedMessage(stageId);
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
1925
2302
|
var id = pinned ? pinned.identity : this.host.getIdentity();
|
|
1926
|
-
if (id.platform === "none")
|
|
2303
|
+
if (id.platform === "none") {
|
|
2304
|
+
if (stageId) this.settleStagedMessage(stageId);
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
1927
2307
|
var llmComposed = composedForLlm || composed;
|
|
1928
2308
|
var key = !id.serviceId ? "" : id.serviceId + "#" + id.platform;
|
|
1929
2309
|
var offChat = !!key && key !== this.getHistoryCacheKey();
|
|
@@ -1934,10 +2314,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1934
2314
|
var aiModel = id.model || void 0;
|
|
1935
2315
|
var systemPrompt = pinned ? pinned.systemPrompt : this.host.buildSystemPrompt();
|
|
1936
2316
|
var userId = id.userId || id.serviceId;
|
|
1937
|
-
var chatQueue = useBgQueue ? userId
|
|
2317
|
+
var chatQueue = useBgQueue ? bgIndexingQueueName(userId) : userId;
|
|
1938
2318
|
if (offChat) {
|
|
1939
2319
|
var offHistory = (this.aiChatHistoryCache[key] ? this.aiChatHistoryCache[key].messages : []).filter(function(m) {
|
|
1940
|
-
return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask;
|
|
2320
|
+
return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask && !m.isError;
|
|
1941
2321
|
});
|
|
1942
2322
|
var offBounded = buildBoundedChatMessages({
|
|
1943
2323
|
platform: aiPlatform,
|
|
@@ -1947,9 +2327,16 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1947
2327
|
history: offHistory.concat([{ role: "user", content: llmComposed }])
|
|
1948
2328
|
});
|
|
1949
2329
|
var offExisting = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
|
|
2330
|
+
var offUser = { role: "user", content: composed, _ownerKey: key, _ts: wallClockNow() };
|
|
2331
|
+
var offStage = this._stageIndex(this.state.messages, stageId);
|
|
2332
|
+
if (offStage !== -1) {
|
|
2333
|
+
if (this.state.messages[offStage]._ts !== void 0) offUser._ts = this.state.messages[offStage]._ts;
|
|
2334
|
+
this.state.messages.splice(offStage, 1);
|
|
2335
|
+
this.host.notify();
|
|
2336
|
+
}
|
|
1950
2337
|
this.aiChatHistoryCache[key] = {
|
|
1951
2338
|
messages: offExisting.messages.concat([
|
|
1952
|
-
|
|
2339
|
+
offUser,
|
|
1953
2340
|
{ role: "assistant", content: "", isPending: true, isPendingInProcess: true, _ownerKey: key }
|
|
1954
2341
|
]),
|
|
1955
2342
|
endOfList: offExisting.endOfList,
|
|
@@ -1972,7 +2359,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1972
2359
|
}
|
|
1973
2360
|
if (isQueuedSend) {
|
|
1974
2361
|
var resolvedHistory = this.state.messages.filter(function(m) {
|
|
1975
|
-
return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask;
|
|
2362
|
+
return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask && !m.isError;
|
|
1976
2363
|
});
|
|
1977
2364
|
var boundedQ = buildBoundedChatMessages({
|
|
1978
2365
|
platform: aiPlatform,
|
|
@@ -1984,14 +2371,26 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1984
2371
|
var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true, _ts: wallClockNow() };
|
|
1985
2372
|
if (key) queuedBubble._ownerKey = key;
|
|
1986
2373
|
if (useBgQueue) queuedBubble._useBgQueue = true;
|
|
1987
|
-
this.state.messages
|
|
2374
|
+
var qStage = this._stageIndex(this.state.messages, stageId);
|
|
2375
|
+
if (qStage !== -1) {
|
|
2376
|
+
if (this.state.messages[qStage]._ts !== void 0) queuedBubble._ts = this.state.messages[qStage]._ts;
|
|
2377
|
+
var qTarget = this._settledStagePosition(qStage);
|
|
2378
|
+
if (qTarget === -1) {
|
|
2379
|
+
this.state.messages.splice(qStage, 1, queuedBubble);
|
|
2380
|
+
} else {
|
|
2381
|
+
this.state.messages.splice(qStage, 1);
|
|
2382
|
+
this.state.messages.splice(qTarget, 0, queuedBubble);
|
|
2383
|
+
}
|
|
2384
|
+
} else {
|
|
2385
|
+
this.state.messages.push(queuedBubble);
|
|
2386
|
+
}
|
|
1988
2387
|
this.host.notify();
|
|
1989
2388
|
this.updateHistoryCache();
|
|
1990
2389
|
this.host.scrollToBottom(true);
|
|
1991
2390
|
var capturedComposed = composed, capturedPlatform = aiPlatform, capturedKey = key;
|
|
1992
2391
|
Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent, fileUrls, id.serviceId, id.owner)).then(function(result) {
|
|
1993
2392
|
var sendingIdx = self.getHistoryCacheKey() !== capturedKey ? -1 : self.state.messages.findIndex(function(m) {
|
|
1994
|
-
return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user" && (m._ownerKey === void 0 || m._ownerKey === capturedKey);
|
|
2393
|
+
return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user" && !m._stageId && (m._ownerKey === void 0 || m._ownerKey === capturedKey);
|
|
1995
2394
|
});
|
|
1996
2395
|
var serverId = result && typeof result.id === "string" ? result.id : void 0;
|
|
1997
2396
|
if (sendingIdx >= 0) {
|
|
@@ -2016,23 +2415,31 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2016
2415
|
});
|
|
2017
2416
|
return;
|
|
2018
2417
|
}
|
|
2019
|
-
|
|
2020
|
-
|
|
2418
|
+
var immediateUser = { role: "user", content: composed, _ts: wallClockNow(), ...key ? { _ownerKey: key } : {} };
|
|
2419
|
+
var immediatePlaceholder = { role: "assistant", content: "", isPending: true, isPendingInProcess: true, ...key ? { _ownerKey: key } : {} };
|
|
2420
|
+
var iStage = this._stageIndex(this.state.messages, stageId);
|
|
2421
|
+
if (iStage !== -1) {
|
|
2422
|
+
if (this.state.messages[iStage]._ts !== void 0) immediateUser._ts = this.state.messages[iStage]._ts;
|
|
2423
|
+
var iTarget = this._settledStagePosition(iStage);
|
|
2424
|
+
if (iTarget === -1) {
|
|
2425
|
+
this.state.messages.splice(iStage, 1, immediateUser, immediatePlaceholder);
|
|
2426
|
+
} else {
|
|
2427
|
+
this.state.messages.splice(iStage, 1);
|
|
2428
|
+
this.state.messages.splice(iTarget, 0, immediateUser, immediatePlaceholder);
|
|
2429
|
+
}
|
|
2430
|
+
} else {
|
|
2431
|
+
this.state.messages.push(immediateUser);
|
|
2432
|
+
this.state.messages.push(immediatePlaceholder);
|
|
2433
|
+
}
|
|
2021
2434
|
this.host.notify();
|
|
2022
2435
|
this.updateHistoryCache();
|
|
2023
2436
|
this.state.sending = true;
|
|
2024
2437
|
this.host.scrollToBottom(true);
|
|
2025
2438
|
var historyForLlm = this.state.messages.filter(function(m) {
|
|
2026
|
-
|
|
2439
|
+
if (m === immediateUser) return false;
|
|
2440
|
+
return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask && !m.isError;
|
|
2027
2441
|
});
|
|
2028
|
-
|
|
2029
|
-
for (var li = historyForLlm.length - 1; li >= 0; li--) {
|
|
2030
|
-
if (historyForLlm[li].role === "user" && historyForLlm[li].content === composed) {
|
|
2031
|
-
historyForLlm[li] = Object.assign({}, historyForLlm[li], { content: llmComposed });
|
|
2032
|
-
break;
|
|
2033
|
-
}
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2442
|
+
historyForLlm.push({ role: "user", content: llmComposed });
|
|
2036
2443
|
var bounded = buildBoundedChatMessages({
|
|
2037
2444
|
platform: aiPlatform,
|
|
2038
2445
|
model: aiModel,
|
|
@@ -2273,7 +2680,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2273
2680
|
if (platform !== "claude" && platform !== "openai") return;
|
|
2274
2681
|
var url = platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
|
|
2275
2682
|
var queueBase = id.userId || id.serviceId;
|
|
2276
|
-
var queue = msg.isBackgroundTask || msg._useBgQueue ? queueBase
|
|
2683
|
+
var queue = msg.isBackgroundTask || msg._useBgQueue ? bgIndexingQueueName(queueBase) : queueBase;
|
|
2277
2684
|
this.state.messages[idx] = Object.assign({}, msg, { _cancelling: true, _cancelError: void 0 });
|
|
2278
2685
|
this.host.notify();
|
|
2279
2686
|
Promise.resolve(this.host.cancelRequest({
|
|
@@ -2759,7 +3166,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2759
3166
|
if (!id.serviceId || platform !== "claude" && platform !== "openai") return;
|
|
2760
3167
|
if (this.isPollingPaused() || !this.host.isViewMounted()) return;
|
|
2761
3168
|
var svcId = id.serviceId, owner = id.owner;
|
|
2762
|
-
var queue = (id.userId
|
|
3169
|
+
var queue = bgIndexingQueueName(id.userId, id.serviceId);
|
|
2763
3170
|
var ask = function(status) {
|
|
2764
3171
|
return Promise.resolve(getChatHistory(
|
|
2765
3172
|
{ service: svcId, owner, platform, queue, status },
|
|
@@ -2861,7 +3268,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2861
3268
|
url,
|
|
2862
3269
|
method: "POST",
|
|
2863
3270
|
id: serverId,
|
|
2864
|
-
queue: (id.userId
|
|
3271
|
+
queue: bgIndexingQueueName(id.userId, id.serviceId),
|
|
2865
3272
|
service: id.serviceId,
|
|
2866
3273
|
owner: id.owner
|
|
2867
3274
|
})).catch(function() {
|
|
@@ -2990,7 +3397,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2990
3397
|
if (pass > MAX_INDEXING_RESUME_PASSES) return;
|
|
2991
3398
|
var id = this.host.getIdentity();
|
|
2992
3399
|
if (!id || id.platform === "none" || id.serviceId !== entry.serviceId) return;
|
|
2993
|
-
notifyAgentContinueIndexing({
|
|
3400
|
+
this.trackIndexDispatch(notifyAgentContinueIndexing({
|
|
2994
3401
|
platform: id.platform,
|
|
2995
3402
|
model: id.model,
|
|
2996
3403
|
service: id.serviceId,
|
|
@@ -3024,7 +3431,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3024
3431
|
}
|
|
3025
3432
|
}, function(e) {
|
|
3026
3433
|
console.error("[chat-engine] resume-indexing dispatch failed", e);
|
|
3027
|
-
});
|
|
3434
|
+
}));
|
|
3028
3435
|
} catch (e) {
|
|
3029
3436
|
}
|
|
3030
3437
|
}
|
|
@@ -3102,6 +3509,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3102
3509
|
if (mm._ownerKey !== void 0 && mm._ownerKey !== loadKey) continue;
|
|
3103
3510
|
if (mm._serverItemId && serverIds[mm._serverItemId]) continue;
|
|
3104
3511
|
if (!mm._serverItemId) {
|
|
3512
|
+
if (mm._stageId) {
|
|
3513
|
+
rescued.push(mm);
|
|
3514
|
+
continue;
|
|
3515
|
+
}
|
|
3105
3516
|
if (mappedHasPendingAssistant) continue;
|
|
3106
3517
|
if (mm.isSendingToServer || mm.isPendingQueued || mm.isPendingInProcess || mm.isPending) rescued.push(mm);
|
|
3107
3518
|
else if (self.state.sending && mm.role === "user") {
|
|
@@ -3321,7 +3732,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3321
3732
|
return preIndex.then(function() {
|
|
3322
3733
|
return parseAttachmentContent(member.file, member.file.name, mime || void 0);
|
|
3323
3734
|
}).then(function(parsedContent) {
|
|
3324
|
-
return notifyAgentSaveAttachment({
|
|
3735
|
+
return self.trackIndexDispatch(notifyAgentSaveAttachment({
|
|
3325
3736
|
platform: id.platform,
|
|
3326
3737
|
model: id.model,
|
|
3327
3738
|
service: id.serviceId,
|
|
@@ -3360,7 +3771,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3360
3771
|
att.errorCode = e && (e.code || e.body && e.body.code) || "";
|
|
3361
3772
|
att.errorDetail = e && (e.message || e.body && e.body.message) || (typeof e === "string" ? e : "");
|
|
3362
3773
|
}
|
|
3363
|
-
});
|
|
3774
|
+
}));
|
|
3364
3775
|
});
|
|
3365
3776
|
});
|
|
3366
3777
|
});
|
|
@@ -3379,14 +3790,24 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3379
3790
|
}
|
|
3380
3791
|
// Upload all not-yet-done attachments sequentially. Resolves to the full
|
|
3381
3792
|
// list of { name, url, storagePath } for composing the chat message.
|
|
3382
|
-
|
|
3793
|
+
//
|
|
3794
|
+
// `batchId` scopes the run to the chips stamped with it at Send time. The
|
|
3795
|
+
// composer stays live during an upload, so by the time this runs the
|
|
3796
|
+
// attachment list can already hold chips the user picked for the NEXT
|
|
3797
|
+
// message — uploading those here would attach them to the wrong turn, and
|
|
3798
|
+
// collecting the previous batch's finished urls would attach files the user
|
|
3799
|
+
// already sent. Omitted (no batch) means every chip, the old behavior.
|
|
3800
|
+
uploadPendingAttachments(batchId) {
|
|
3383
3801
|
var self = this;
|
|
3384
3802
|
this.host.resetOverwriteBatch();
|
|
3803
|
+
this._uploadBatches += 1;
|
|
3385
3804
|
this.state.uploadingAttachments = true;
|
|
3386
3805
|
this.host.updateComposerControls();
|
|
3387
3806
|
this.host.renderAttachmentChips();
|
|
3388
3807
|
var collected = [];
|
|
3389
|
-
var snapshot = this.state.attachments.
|
|
3808
|
+
var snapshot = this.state.attachments.filter(function(a) {
|
|
3809
|
+
return batchId ? a._batchId === batchId : true;
|
|
3810
|
+
});
|
|
3390
3811
|
var chain = Promise.resolve();
|
|
3391
3812
|
snapshot.forEach(function(att) {
|
|
3392
3813
|
chain = chain.then(function() {
|
|
@@ -3422,7 +3843,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3422
3843
|
});
|
|
3423
3844
|
});
|
|
3424
3845
|
var done = function() {
|
|
3425
|
-
self.
|
|
3846
|
+
self._uploadBatches = Math.max(0, self._uploadBatches - 1);
|
|
3847
|
+
self.state.uploadingAttachments = self._uploadBatches > 0;
|
|
3426
3848
|
self.host.updateComposerControls();
|
|
3427
3849
|
self.host.renderAttachmentChips();
|
|
3428
3850
|
return collected;
|
|
@@ -3632,7 +4054,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3632
4054
|
(function() {
|
|
3633
4055
|
var MCP_PROD = "https://mcp.broadwayinc.computer";
|
|
3634
4056
|
var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
|
|
3635
|
-
var BQ_VERSION = "1.8.
|
|
4057
|
+
var BQ_VERSION = "1.8.3" ;
|
|
3636
4058
|
var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
|
|
3637
4059
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
3638
4060
|
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
@@ -5260,17 +5682,20 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5260
5682
|
var refreshingLinkPromises = /* @__PURE__ */ new Map();
|
|
5261
5683
|
var fileBlobCache = /* @__PURE__ */ new Map();
|
|
5262
5684
|
var markedReady = null;
|
|
5685
|
+
function currentIdentity() {
|
|
5686
|
+
return {
|
|
5687
|
+
serviceId: S.serviceId,
|
|
5688
|
+
owner: S.owner,
|
|
5689
|
+
userId: S.user && S.user.user_id || S.serviceId,
|
|
5690
|
+
platform: S.aiPlatform,
|
|
5691
|
+
model: S.aiModel || void 0,
|
|
5692
|
+
serviceName: S.serviceName,
|
|
5693
|
+
serviceDescription: S.serviceDescription
|
|
5694
|
+
};
|
|
5695
|
+
}
|
|
5263
5696
|
var session = new ChatSession({
|
|
5264
5697
|
getIdentity: function() {
|
|
5265
|
-
return
|
|
5266
|
-
serviceId: S.serviceId,
|
|
5267
|
-
owner: S.owner,
|
|
5268
|
-
userId: S.user && S.user.user_id || S.serviceId,
|
|
5269
|
-
platform: S.aiPlatform,
|
|
5270
|
-
model: S.aiModel || void 0,
|
|
5271
|
-
serviceName: S.serviceName,
|
|
5272
|
-
serviceDescription: S.serviceDescription
|
|
5273
|
-
};
|
|
5698
|
+
return currentIdentity();
|
|
5274
5699
|
},
|
|
5275
5700
|
buildSystemPrompt: function() {
|
|
5276
5701
|
return buildSystemPrompt();
|
|
@@ -5440,13 +5865,56 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5440
5865
|
return false;
|
|
5441
5866
|
});
|
|
5442
5867
|
}
|
|
5868
|
+
var attachmentUploadChain = Promise.resolve();
|
|
5869
|
+
var attachmentDispatchChain = Promise.resolve();
|
|
5870
|
+
var attachmentBatchSeq = 0;
|
|
5871
|
+
function enqueueAttachmentSend(job) {
|
|
5872
|
+
var uploaded = attachmentUploadChain.catch(function() {
|
|
5873
|
+
}).then(function() {
|
|
5874
|
+
return runAttachmentUpload(job);
|
|
5875
|
+
});
|
|
5876
|
+
attachmentUploadChain = uploaded;
|
|
5877
|
+
attachmentDispatchChain = attachmentDispatchChain.catch(function() {
|
|
5878
|
+
}).then(function() {
|
|
5879
|
+
return uploaded;
|
|
5880
|
+
}).then(function(urls) {
|
|
5881
|
+
return runAttachmentDispatch(job, urls);
|
|
5882
|
+
});
|
|
5883
|
+
}
|
|
5884
|
+
function runAttachmentUpload(job) {
|
|
5885
|
+
return session.uploadPendingAttachments(job.batchId).then(function(attachmentUrls) {
|
|
5886
|
+
var failureGroups = groupAttachmentFailures(CS.attachments.filter(function(a) {
|
|
5887
|
+
return a._batchId === job.batchId;
|
|
5888
|
+
}));
|
|
5889
|
+
clearSuccessfulAttachments(job.batchId);
|
|
5890
|
+
if (failureGroups.length) showUploadErrorReport(failureGroups);
|
|
5891
|
+
return attachmentUrls;
|
|
5892
|
+
}).catch(function(err) {
|
|
5893
|
+
console.error("[bunnyquery] attachment upload failed", err);
|
|
5894
|
+
updateComposerControls();
|
|
5895
|
+
renderAttachmentChips();
|
|
5896
|
+
if (job.stageId) session.settleStagedMessage(job.stageId);
|
|
5897
|
+
CS.messages.push({ role: "assistant", content: "Something went wrong while uploading attachments. " + (err && err.message || ""), isError: true });
|
|
5898
|
+
renderMessages();
|
|
5899
|
+
scrollToBottom(true);
|
|
5900
|
+
return null;
|
|
5901
|
+
});
|
|
5902
|
+
}
|
|
5903
|
+
function runAttachmentDispatch(job, attachmentUrls) {
|
|
5904
|
+
if (!attachmentUrls || !job.text) return Promise.resolve();
|
|
5905
|
+
if (job.stageId) session.markStagedMessageQueued(job.stageId);
|
|
5906
|
+
return session.awaitIndexingDrained(job.pinned.identity).then(function() {
|
|
5907
|
+
var c = composeUserMessage(job.text, attachmentUrls);
|
|
5908
|
+
session.dispatchComposedMessage(c.composed, true, c.composedForLlm, c.extractContent, c.fileUrls, job.pinned);
|
|
5909
|
+
});
|
|
5910
|
+
}
|
|
5443
5911
|
function sendMessage() {
|
|
5444
5912
|
var inputEl = CS.messagesBox && CS.messagesBox.parentNode && CS.messagesBox.parentNode.querySelector(".bq-input");
|
|
5445
5913
|
var text = (inputEl ? inputEl.value : "").trim();
|
|
5446
|
-
var
|
|
5914
|
+
var batchAttachments = composerAttachments();
|
|
5915
|
+
var hasAttachments = batchAttachments.length > 0;
|
|
5447
5916
|
if (!text && !hasAttachments) return;
|
|
5448
5917
|
if (!chatEnabled() || S.aiPlatform === "none") return;
|
|
5449
|
-
if (CS.uploadingAttachments) return;
|
|
5450
5918
|
recomputeAttachmentWarning();
|
|
5451
5919
|
if (CS.attachmentWarning) {
|
|
5452
5920
|
renderAttachmentChips();
|
|
@@ -5461,25 +5929,21 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5461
5929
|
session.dispatchComposedMessage(text, false);
|
|
5462
5930
|
return;
|
|
5463
5931
|
}
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
clearSuccessfulAttachments();
|
|
5469
|
-
if (text) {
|
|
5470
|
-
var c = composeUserMessage(text, attachmentUrls);
|
|
5471
|
-
session.dispatchComposedMessage(c.composed, hasNewIndexing, c.composedForLlm, c.extractContent, c.fileUrls);
|
|
5472
|
-
}
|
|
5473
|
-
if (failureGroups.length) showUploadErrorReport(failureGroups);
|
|
5474
|
-
}).catch(function(err) {
|
|
5475
|
-
console.error("[bunnyquery] attachment upload failed", err);
|
|
5476
|
-
CS.uploadingAttachments = false;
|
|
5477
|
-
updateComposerControls();
|
|
5478
|
-
renderAttachmentChips();
|
|
5479
|
-
CS.messages.push({ role: "assistant", content: "Something went wrong while uploading attachments. " + (err && err.message || ""), isError: true });
|
|
5480
|
-
renderMessages();
|
|
5481
|
-
scrollToBottom(true);
|
|
5932
|
+
attachmentBatchSeq += 1;
|
|
5933
|
+
var batchId = "batch_" + attachmentBatchSeq + "_" + Date.now();
|
|
5934
|
+
batchAttachments.forEach(function(a) {
|
|
5935
|
+
a._batchId = batchId;
|
|
5482
5936
|
});
|
|
5937
|
+
recomputeAttachmentWarning();
|
|
5938
|
+
renderAttachmentChips();
|
|
5939
|
+
updateComposerControls();
|
|
5940
|
+
var stageId = text ? session.stageOutgoingMessage(text) : void 0;
|
|
5941
|
+
var pinned = {
|
|
5942
|
+
identity: currentIdentity(),
|
|
5943
|
+
systemPrompt: buildSystemPrompt(),
|
|
5944
|
+
stageId
|
|
5945
|
+
};
|
|
5946
|
+
enqueueAttachmentSend({ text, batchId, stageId, pinned });
|
|
5483
5947
|
}
|
|
5484
5948
|
function scrollToBottom(smooth) {
|
|
5485
5949
|
return raf2().then(function() {
|
|
@@ -5518,13 +5982,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5518
5982
|
var key = filename + "\0" + body;
|
|
5519
5983
|
var existing = fileBlobCache.get(key);
|
|
5520
5984
|
if (existing) return existing;
|
|
5521
|
-
var
|
|
5522
|
-
var
|
|
5523
|
-
var
|
|
5524
|
-
var needsBom = ext === "csv" || ext === "tsv" || ext === "tab";
|
|
5525
|
-
var type = isText ? contentType + "; charset=utf-8" : contentType;
|
|
5526
|
-
var data = needsBom ? "\uFEFF" + body : body;
|
|
5527
|
-
var href = URL.createObjectURL(new Blob([data], { type }));
|
|
5985
|
+
var prepared = prepareDownloadText(filename, body);
|
|
5986
|
+
var type = EXT_CONTENT_TYPES[extOf(filename)] || mimeGetType(filename) || "text/plain; charset=utf-8";
|
|
5987
|
+
var href = URL.createObjectURL(new Blob([prepared.text], { type }));
|
|
5528
5988
|
fileBlobCache.set(key, href);
|
|
5529
5989
|
return href;
|
|
5530
5990
|
}
|
|
@@ -5737,9 +6197,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5737
6197
|
if (IMAGE_EXTENSION_RE.test(name) || type.indexOf("image/") === 0) return ESTIMATED_IMAGE_TOKENS;
|
|
5738
6198
|
return 0;
|
|
5739
6199
|
}
|
|
6200
|
+
function composerAttachments() {
|
|
6201
|
+
return CS.attachments.filter(function(a) {
|
|
6202
|
+
return !a._batchId;
|
|
6203
|
+
});
|
|
6204
|
+
}
|
|
5740
6205
|
function attachmentsTokenEstimate() {
|
|
5741
6206
|
var total = 0;
|
|
5742
|
-
|
|
6207
|
+
composerAttachments().forEach(function(a) {
|
|
5743
6208
|
if (a.kind === "folder") {
|
|
5744
6209
|
(a.files || []).forEach(function(f) {
|
|
5745
6210
|
total += estimateFileTokenCost(f.file);
|
|
@@ -5750,7 +6215,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5750
6215
|
}
|
|
5751
6216
|
function attachmentFileCount() {
|
|
5752
6217
|
var n = 0;
|
|
5753
|
-
|
|
6218
|
+
composerAttachments().forEach(function(a) {
|
|
5754
6219
|
n += a.kind === "folder" ? a.files ? a.files.length : 0 : 1;
|
|
5755
6220
|
});
|
|
5756
6221
|
return n;
|
|
@@ -5974,27 +6439,22 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5974
6439
|
scheduleAttachmentOverflowRecompute();
|
|
5975
6440
|
}
|
|
5976
6441
|
function clearAttachments() {
|
|
5977
|
-
CS.attachments.
|
|
5978
|
-
|
|
5979
|
-
try {
|
|
5980
|
-
a._abort();
|
|
5981
|
-
} catch (e) {
|
|
5982
|
-
}
|
|
5983
|
-
}
|
|
6442
|
+
CS.attachments = CS.attachments.filter(function(a) {
|
|
6443
|
+
return !!a._batchId;
|
|
5984
6444
|
});
|
|
5985
|
-
CS.attachments = [];
|
|
5986
6445
|
CS.attachmentWarning = "";
|
|
5987
6446
|
CS.attachmentCapNotice = "";
|
|
5988
6447
|
renderAttachmentChips();
|
|
5989
6448
|
updateComposerControls();
|
|
5990
6449
|
scheduleAttachmentOverflowRecompute();
|
|
5991
6450
|
}
|
|
5992
|
-
function clearSuccessfulAttachments() {
|
|
6451
|
+
function clearSuccessfulAttachments(batchId) {
|
|
5993
6452
|
CS.attachments = CS.attachments.filter(function(a) {
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
CS.attachments.forEach(function(a) {
|
|
6453
|
+
if (a._batchId !== batchId) return true;
|
|
6454
|
+
if (a.status !== "error" && a.status !== "indexError") return false;
|
|
5997
6455
|
a._abort = null;
|
|
6456
|
+
delete a._batchId;
|
|
6457
|
+
return true;
|
|
5998
6458
|
});
|
|
5999
6459
|
CS.attachmentCapNotice = "";
|
|
6000
6460
|
recomputeAttachmentWarning();
|
|
@@ -6059,7 +6519,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6059
6519
|
var meta = att.status === "error" ? "(Failed)" : att.status === "indexError" ? "(Error)" : att.status === "uploading" ? (att.progress || 0) + "%" : isFolder ? "(" + (att.files ? att.files.length : 0) + ")" : formatBytes(att.file ? att.file.size : att.size);
|
|
6060
6520
|
chip.appendChild(h("span", { class: "bq-attachment-meta", text: meta }));
|
|
6061
6521
|
if (clickable) chip.appendChild(h("span", { class: "bq-attachment-arrow", text: "\u2197" }));
|
|
6062
|
-
if (
|
|
6522
|
+
if (att.status !== "uploading" && att.status !== "done") {
|
|
6063
6523
|
var rm = h("button", { class: "bq-attachment-remove", type: "button", title: "Remove", text: "\xD7" });
|
|
6064
6524
|
rm.addEventListener("click", function(e) {
|
|
6065
6525
|
e.stopPropagation();
|
|
@@ -6079,23 +6539,21 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6079
6539
|
title: moreNames.join("\n")
|
|
6080
6540
|
});
|
|
6081
6541
|
moreChip.appendChild(h("span", { class: "bq-attachment-name", text: "\u2026(" + hidden.length + ") more" }));
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
moreChip.appendChild(moreRm);
|
|
6096
|
-
}
|
|
6542
|
+
var moreRm = h("button", {
|
|
6543
|
+
class: "bq-attachment-remove",
|
|
6544
|
+
type: "button",
|
|
6545
|
+
title: "Remove these " + hidden.length,
|
|
6546
|
+
text: "\xD7"
|
|
6547
|
+
});
|
|
6548
|
+
moreRm.addEventListener("click", function(e) {
|
|
6549
|
+
e.stopPropagation();
|
|
6550
|
+
removeAttachments(hidden.map(function(a) {
|
|
6551
|
+
return a.id;
|
|
6552
|
+
}));
|
|
6553
|
+
});
|
|
6554
|
+
moreChip.appendChild(moreRm);
|
|
6097
6555
|
row.appendChild(moreChip);
|
|
6098
|
-
} else if (
|
|
6556
|
+
} else if (composerAttachments().length >= 2) {
|
|
6099
6557
|
var removeAll = h("button", {
|
|
6100
6558
|
class: "bq-attachment-remove-all",
|
|
6101
6559
|
type: "button",
|
|
@@ -6115,10 +6573,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6115
6573
|
return ag < 99;
|
|
6116
6574
|
}
|
|
6117
6575
|
function updateComposerControls() {
|
|
6118
|
-
|
|
6119
|
-
if (CS.
|
|
6120
|
-
if (CS.
|
|
6121
|
-
if (CS.sendBtnEl) CS.sendBtnEl.disabled = uploading || !!CS.attachmentWarning;
|
|
6576
|
+
if (CS.attachBtnEl) CS.attachBtnEl.disabled = false;
|
|
6577
|
+
if (CS.inputEl) CS.inputEl.disabled = false;
|
|
6578
|
+
if (CS.sendBtnEl) CS.sendBtnEl.disabled = !!CS.attachmentWarning;
|
|
6122
6579
|
}
|
|
6123
6580
|
function onAttachInputChange(inputEl) {
|
|
6124
6581
|
if (inputEl && inputEl.files && inputEl.files.length) addFilesToAttachments(inputEl.files);
|
|
@@ -6458,7 +6915,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6458
6915
|
var md = h("div", { class: "bq-md", translate: "no", html: parseMsgPartsHtml(msg.content) });
|
|
6459
6916
|
md.addEventListener("click", onBubbleLinkClick);
|
|
6460
6917
|
bubble.appendChild(md);
|
|
6461
|
-
if (msg.
|
|
6918
|
+
if (msg.isUploadingAttachments) bubble.appendChild(h("span", { class: "bq-pending-note", text: "(Uploading files...)" }));
|
|
6919
|
+
else if (msg.isPendingQueued) bubble.appendChild(h("span", { class: "bq-pending-note", text: "(In queue)" }));
|
|
6462
6920
|
if (msg.isCancelled) bubble.appendChild(h("span", { class: "bq-cancel-error", text: "(cancelled)" }));
|
|
6463
6921
|
if (msg._cancelError) bubble.appendChild(h("span", { class: "bq-cancel-error", text: msg._cancelError }));
|
|
6464
6922
|
var ts = formatChatTimestamp(msg._ts);
|