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/dist/engine.cjs
CHANGED
|
@@ -219,14 +219,15 @@ function isWindowedReadFile(name, mime) {
|
|
|
219
219
|
}
|
|
220
220
|
function composeUserMessage(text, attachmentUrls) {
|
|
221
221
|
let composed = text;
|
|
222
|
+
let composedForLlm = composed;
|
|
222
223
|
if (attachmentUrls.length > 0) {
|
|
223
224
|
const lines = attachmentUrls.map((u) => `- [${u.name}](${u.url})`);
|
|
224
225
|
composed = `${text}
|
|
225
226
|
|
|
226
227
|
Attached files:
|
|
227
228
|
${lines.join("\n")}`;
|
|
229
|
+
composedForLlm = composed;
|
|
228
230
|
}
|
|
229
|
-
let composedForLlm = composed;
|
|
230
231
|
let extractContent;
|
|
231
232
|
let fileUrls;
|
|
232
233
|
if (attachmentUrls.length > 0) {
|
|
@@ -243,13 +244,13 @@ ${placeholder}
|
|
|
243
244
|
----- END FILE CONTENT -----`;
|
|
244
245
|
});
|
|
245
246
|
extractContent = directives;
|
|
246
|
-
composedForLlm = `${
|
|
247
|
+
composedForLlm = `${composedForLlm}
|
|
247
248
|
|
|
248
249
|
Extracted content of attached office files (read inline below; do NOT fetch their URLs):
|
|
249
250
|
|
|
250
251
|
` + sections.join("\n\n");
|
|
251
252
|
}
|
|
252
|
-
const urlFiles =
|
|
253
|
+
const urlFiles = [];
|
|
253
254
|
if (urlFiles.length > 0) {
|
|
254
255
|
fileUrls = urlFiles.map((u) => ({ path: u.storagePath || u.name, url: u.url }));
|
|
255
256
|
}
|
|
@@ -293,6 +294,7 @@ File attachments: When a user message contains an "Attached files:" section with
|
|
|
293
294
|
- 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.
|
|
294
295
|
- 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.
|
|
295
296
|
- 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.
|
|
297
|
+
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.
|
|
296
298
|
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.
|
|
297
299
|
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.
|
|
298
300
|
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:
|
|
@@ -439,6 +441,19 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
439
441
|
}
|
|
440
442
|
|
|
441
443
|
// src/engine/errors.ts
|
|
444
|
+
var STATUS_MESSAGE = {
|
|
445
|
+
"408": "The AI provider timed out before it started.",
|
|
446
|
+
"409": "The AI provider rejected the request as conflicting.",
|
|
447
|
+
"413": "The request was too large for the AI provider.",
|
|
448
|
+
"429": "The AI provider is rate limiting requests right now.",
|
|
449
|
+
"500": "The AI provider hit an internal error.",
|
|
450
|
+
"502": "The AI provider is temporarily unreachable.",
|
|
451
|
+
"503": "The AI provider is temporarily unavailable.",
|
|
452
|
+
"504": "The AI provider timed out."
|
|
453
|
+
};
|
|
454
|
+
function isTransientStatus(status) {
|
|
455
|
+
return status === 408 || status === 425 || status === 429 || status >= 500;
|
|
456
|
+
}
|
|
442
457
|
function getErrorMessage(input) {
|
|
443
458
|
if (!input) return "Something went wrong.";
|
|
444
459
|
if (typeof input === "string") return input;
|
|
@@ -446,6 +461,11 @@ function getErrorMessage(input) {
|
|
|
446
461
|
if (input.body && input.body.error && input.body.error.message) return input.body.error.message;
|
|
447
462
|
if (input.body && typeof input.body.message === "string") return input.body.message;
|
|
448
463
|
if (input.message) return input.message;
|
|
464
|
+
var status = typeof input.status_code === "number" ? input.status_code : typeof input.status === "number" ? input.status : 0;
|
|
465
|
+
if (status) {
|
|
466
|
+
var text = STATUS_MESSAGE[String(status)] || (status >= 500 ? "The AI provider returned a server error." : "The AI provider rejected the request.");
|
|
467
|
+
return text + " (error " + status + ")" + (isTransientStatus(status) ? " This is usually temporary, please try again." : "");
|
|
468
|
+
}
|
|
449
469
|
return "Something went wrong.";
|
|
450
470
|
}
|
|
451
471
|
function isErrorResponseBody(response) {
|
|
@@ -869,6 +889,166 @@ function buildBoundedChatMessages(options) {
|
|
|
869
889
|
};
|
|
870
890
|
}
|
|
871
891
|
|
|
892
|
+
// src/engine/download_encoding.ts
|
|
893
|
+
var BOM = "\uFEFF";
|
|
894
|
+
var BOM_EXTS = /* @__PURE__ */ new Set(["csv", "tsv", "tab", "txt", "text", "log"]);
|
|
895
|
+
var HTML_EXTS = /* @__PURE__ */ new Set(["html", "htm", "xhtml"]);
|
|
896
|
+
var XML_EXTS = /* @__PURE__ */ new Set(["xml", "svg", "rss", "atom", "xsl", "xslt", "plist", "kml"]);
|
|
897
|
+
var RTF_EXTS = /* @__PURE__ */ new Set(["rtf"]);
|
|
898
|
+
var EXT_CONTENT_TYPES = {
|
|
899
|
+
csv: "text/csv; charset=utf-8",
|
|
900
|
+
tsv: "text/tab-separated-values; charset=utf-8",
|
|
901
|
+
tab: "text/tab-separated-values; charset=utf-8",
|
|
902
|
+
txt: "text/plain; charset=utf-8",
|
|
903
|
+
text: "text/plain; charset=utf-8",
|
|
904
|
+
log: "text/plain; charset=utf-8",
|
|
905
|
+
md: "text/markdown; charset=utf-8",
|
|
906
|
+
markdown: "text/markdown; charset=utf-8",
|
|
907
|
+
json: "application/json; charset=utf-8",
|
|
908
|
+
jsonl: "application/x-ndjson; charset=utf-8",
|
|
909
|
+
ndjson: "application/x-ndjson; charset=utf-8",
|
|
910
|
+
geojson: "application/geo+json; charset=utf-8",
|
|
911
|
+
yaml: "text/yaml; charset=utf-8",
|
|
912
|
+
yml: "text/yaml; charset=utf-8",
|
|
913
|
+
toml: "text/plain; charset=utf-8",
|
|
914
|
+
ini: "text/plain; charset=utf-8",
|
|
915
|
+
sql: "text/plain; charset=utf-8",
|
|
916
|
+
html: "text/html; charset=utf-8",
|
|
917
|
+
htm: "text/html; charset=utf-8",
|
|
918
|
+
xhtml: "application/xhtml+xml; charset=utf-8",
|
|
919
|
+
xml: "application/xml; charset=utf-8",
|
|
920
|
+
svg: "image/svg+xml; charset=utf-8",
|
|
921
|
+
css: "text/css; charset=utf-8",
|
|
922
|
+
js: "text/javascript; charset=utf-8",
|
|
923
|
+
ts: "text/plain; charset=utf-8",
|
|
924
|
+
py: "text/x-python; charset=utf-8",
|
|
925
|
+
sh: "text/x-shellscript; charset=utf-8",
|
|
926
|
+
srt: "application/x-subrip; charset=utf-8",
|
|
927
|
+
vtt: "text/vtt; charset=utf-8",
|
|
928
|
+
ics: "text/calendar; charset=utf-8",
|
|
929
|
+
vcf: "text/vcard; charset=utf-8",
|
|
930
|
+
// RTF is 7-bit ASCII by specification, so it takes no charset parameter.
|
|
931
|
+
rtf: "application/rtf",
|
|
932
|
+
// Binary types the model can only ever REFERENCE, never author in a fence, but
|
|
933
|
+
// which keep the type sensible if one ever shows up.
|
|
934
|
+
pdf: "application/pdf",
|
|
935
|
+
png: "image/png",
|
|
936
|
+
jpg: "image/jpeg",
|
|
937
|
+
jpeg: "image/jpeg",
|
|
938
|
+
gif: "image/gif",
|
|
939
|
+
webp: "image/webp"
|
|
940
|
+
};
|
|
941
|
+
function normalizeExt(ext) {
|
|
942
|
+
return String(ext || "").trim().replace(/^\./, "").toLowerCase();
|
|
943
|
+
}
|
|
944
|
+
function extOf(filename) {
|
|
945
|
+
const name = String(filename || "");
|
|
946
|
+
const dot = name.lastIndexOf(".");
|
|
947
|
+
return dot > 0 ? normalizeExt(name.slice(dot + 1)) : "";
|
|
948
|
+
}
|
|
949
|
+
function encodingClassForExt(ext) {
|
|
950
|
+
const e = normalizeExt(ext);
|
|
951
|
+
if (BOM_EXTS.has(e)) return "bom";
|
|
952
|
+
if (HTML_EXTS.has(e)) return "html";
|
|
953
|
+
if (XML_EXTS.has(e)) return "xml";
|
|
954
|
+
if (RTF_EXTS.has(e)) return "rtf";
|
|
955
|
+
return "none";
|
|
956
|
+
}
|
|
957
|
+
function needsBomForExt(ext) {
|
|
958
|
+
return encodingClassForExt(ext) === "bom";
|
|
959
|
+
}
|
|
960
|
+
function contentTypeForExt(ext, fallback = "text/plain; charset=utf-8") {
|
|
961
|
+
return EXT_CONTENT_TYPES[normalizeExt(ext)] || fallback;
|
|
962
|
+
}
|
|
963
|
+
function hasBom(text) {
|
|
964
|
+
return typeof text === "string" && text.charCodeAt(0) === 65279;
|
|
965
|
+
}
|
|
966
|
+
var HTML_HEAD_WINDOW = 4096;
|
|
967
|
+
var META_CHARSET_RE = /<meta[^>]+charset\s*=\s*["']?\s*([a-z0-9_-]+)/i;
|
|
968
|
+
var META_HTTP_EQUIV_RE = /<meta[^>]+http-equiv\s*=\s*["']?content-type["']?[^>]*>/i;
|
|
969
|
+
function ensureHtmlCharset(text) {
|
|
970
|
+
const src = String(text == null ? "" : text);
|
|
971
|
+
const head = src.slice(0, HTML_HEAD_WINDOW);
|
|
972
|
+
const declared = META_CHARSET_RE.exec(head);
|
|
973
|
+
if (declared) {
|
|
974
|
+
if (declared[1].toLowerCase().replace(/[^a-z0-9]/g, "") === "utf8") return src;
|
|
975
|
+
const start = declared.index + declared[0].length - declared[1].length;
|
|
976
|
+
return src.slice(0, start) + "utf-8" + src.slice(start + declared[1].length);
|
|
977
|
+
}
|
|
978
|
+
const httpEquiv = META_HTTP_EQUIV_RE.exec(head);
|
|
979
|
+
if (httpEquiv) {
|
|
980
|
+
return src.slice(0, httpEquiv.index) + '<meta charset="utf-8">' + src.slice(httpEquiv.index + httpEquiv[0].length);
|
|
981
|
+
}
|
|
982
|
+
const tag = '<meta charset="utf-8">';
|
|
983
|
+
const headOpen = /<head[^>]*>/i.exec(head);
|
|
984
|
+
if (headOpen) {
|
|
985
|
+
const at = headOpen.index + headOpen[0].length;
|
|
986
|
+
return src.slice(0, at) + "\n" + tag + src.slice(at);
|
|
987
|
+
}
|
|
988
|
+
const htmlOpen = /<html[^>]*>/i.exec(head);
|
|
989
|
+
if (htmlOpen) {
|
|
990
|
+
const at = htmlOpen.index + htmlOpen[0].length;
|
|
991
|
+
return src.slice(0, at) + "\n<head>" + tag + "</head>" + src.slice(at);
|
|
992
|
+
}
|
|
993
|
+
const doctype = /<!doctype[^>]*>/i.exec(head);
|
|
994
|
+
if (doctype) {
|
|
995
|
+
const at = doctype.index + doctype[0].length;
|
|
996
|
+
return src.slice(0, at) + "\n" + tag + src.slice(at);
|
|
997
|
+
}
|
|
998
|
+
return tag + "\n" + src;
|
|
999
|
+
}
|
|
1000
|
+
var XML_DECL_RE = /^\s*<\?xml\s[^?]*\?>/i;
|
|
1001
|
+
function ensureXmlEncoding(text) {
|
|
1002
|
+
const src = String(text == null ? "" : text);
|
|
1003
|
+
const decl = XML_DECL_RE.exec(src);
|
|
1004
|
+
if (!decl) return src;
|
|
1005
|
+
const found = /encoding\s*=\s*["']([^"']*)["']/i.exec(decl[0]);
|
|
1006
|
+
if (!found) return src;
|
|
1007
|
+
if (found[1].toLowerCase().replace(/[^a-z0-9]/g, "") === "utf8") return src;
|
|
1008
|
+
const fixedDecl = decl[0].slice(0, found.index) + found[0].replace(found[1], "UTF-8") + decl[0].slice(found.index + found[0].length);
|
|
1009
|
+
return fixedDecl + src.slice(decl[0].length);
|
|
1010
|
+
}
|
|
1011
|
+
var RTF_SIGNATURE_RE = /^[\s]*\{\\rtf/i;
|
|
1012
|
+
function looksLikeRtf(text) {
|
|
1013
|
+
return RTF_SIGNATURE_RE.test(String(text == null ? "" : text));
|
|
1014
|
+
}
|
|
1015
|
+
function escapeRtfNonAscii(text) {
|
|
1016
|
+
const src = String(text == null ? "" : text);
|
|
1017
|
+
let out = "";
|
|
1018
|
+
let plainFrom = 0;
|
|
1019
|
+
for (let i = 0; i < src.length; i++) {
|
|
1020
|
+
const code = src.charCodeAt(i);
|
|
1021
|
+
if (code < 128) continue;
|
|
1022
|
+
out += src.slice(plainFrom, i);
|
|
1023
|
+
out += `\\u${code > 32767 ? code - 65536 : code}?`;
|
|
1024
|
+
plainFrom = i + 1;
|
|
1025
|
+
}
|
|
1026
|
+
return plainFrom === 0 ? src : out + src.slice(plainFrom);
|
|
1027
|
+
}
|
|
1028
|
+
function applyEncodingDeclaration(text, ext) {
|
|
1029
|
+
const src = String(text == null ? "" : text);
|
|
1030
|
+
switch (encodingClassForExt(ext)) {
|
|
1031
|
+
case "bom":
|
|
1032
|
+
return hasBom(src) ? src : BOM + src;
|
|
1033
|
+
case "html":
|
|
1034
|
+
return ensureHtmlCharset(src);
|
|
1035
|
+
case "xml":
|
|
1036
|
+
return ensureXmlEncoding(src);
|
|
1037
|
+
case "rtf":
|
|
1038
|
+
return looksLikeRtf(src) ? escapeRtfNonAscii(src) : hasBom(src) ? src : BOM + src;
|
|
1039
|
+
default:
|
|
1040
|
+
return src;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
function prepareDownloadText(filename, body) {
|
|
1044
|
+
const ext = extOf(filename);
|
|
1045
|
+
return {
|
|
1046
|
+
ext,
|
|
1047
|
+
text: applyEncodingDeclaration(body, ext),
|
|
1048
|
+
contentType: contentTypeForExt(ext)
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
|
|
872
1052
|
// src/engine/time.ts
|
|
873
1053
|
function wallClockNow() {
|
|
874
1054
|
return Date.now();
|
|
@@ -1041,7 +1221,7 @@ function applyHistoryCacheBreakpoint(messages) {
|
|
|
1041
1221
|
return { ...m, content: blocks };
|
|
1042
1222
|
});
|
|
1043
1223
|
}
|
|
1044
|
-
var POLL_INTERVAL =
|
|
1224
|
+
var POLL_INTERVAL = 3e3;
|
|
1045
1225
|
async function callClaudeWithMcp({
|
|
1046
1226
|
prompt,
|
|
1047
1227
|
messages,
|
|
@@ -1256,7 +1436,7 @@ async function notifyAgentSaveAttachment(info) {
|
|
|
1256
1436
|
const imageDetail = getOpenAIImageDetail(resolvedModel2);
|
|
1257
1437
|
return clientSecretRequest({
|
|
1258
1438
|
clientSecretName: "openai",
|
|
1259
|
-
queue: (info.userId
|
|
1439
|
+
queue: bgIndexingQueueName(info.userId, service),
|
|
1260
1440
|
service,
|
|
1261
1441
|
owner,
|
|
1262
1442
|
...pollOpt(),
|
|
@@ -1300,7 +1480,7 @@ async function notifyAgentSaveAttachment(info) {
|
|
|
1300
1480
|
const resolvedModel = info.model || DEFAULT_CLAUDE_MODEL;
|
|
1301
1481
|
return clientSecretRequest({
|
|
1302
1482
|
clientSecretName: "claude",
|
|
1303
|
-
queue: (info.userId
|
|
1483
|
+
queue: bgIndexingQueueName(info.userId, service),
|
|
1304
1484
|
service,
|
|
1305
1485
|
owner,
|
|
1306
1486
|
...pollOpt(),
|
|
@@ -1414,6 +1594,9 @@ async function listOpenAIModels(service, owner) {
|
|
|
1414
1594
|
});
|
|
1415
1595
|
}
|
|
1416
1596
|
var BG_INDEXING_QUEUE_SUFFIX = "-bg";
|
|
1597
|
+
function bgIndexingQueueName(userId, service) {
|
|
1598
|
+
return (userId || service || "") + BG_INDEXING_QUEUE_SUFFIX;
|
|
1599
|
+
}
|
|
1417
1600
|
function isBgIndexingQueue(queueName) {
|
|
1418
1601
|
if (typeof queueName !== "string" || !queueName) return false;
|
|
1419
1602
|
const prefix = queueName.split("|")[0];
|
|
@@ -1653,6 +1836,11 @@ function createHistoryFiller(base) {
|
|
|
1653
1836
|
// src/engine/session.ts
|
|
1654
1837
|
var WORKER_PASS_ADOPT_LIMIT = 20;
|
|
1655
1838
|
var WORKER_PASS_ADOPT_ATTEMPTS = [0, 2e3, 6e3];
|
|
1839
|
+
var INDEXING_DRAIN_BUSY_POLL_MS = 8e3;
|
|
1840
|
+
var INDEXING_DRAIN_CONFIRM_POLL_MS = 3e3;
|
|
1841
|
+
var INDEXING_DRAIN_IDLE_LOOKS = 2;
|
|
1842
|
+
var INDEXING_DRAIN_MIN_MS = 8e3;
|
|
1843
|
+
var INDEXING_DRAIN_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
1656
1844
|
var _g = typeof globalThis !== "undefined" ? globalThis : {};
|
|
1657
1845
|
function nowMs() {
|
|
1658
1846
|
return _g.performance && typeof _g.performance.now === "function" ? _g.performance.now() : Date.now();
|
|
@@ -1725,6 +1913,25 @@ var ChatSession = class {
|
|
|
1725
1913
|
this._pauseReasons = /* @__PURE__ */ new Set();
|
|
1726
1914
|
this._resuming = false;
|
|
1727
1915
|
this._lidSeq = 0;
|
|
1916
|
+
this._stageSeq = 0;
|
|
1917
|
+
this._uploadBatches = 0;
|
|
1918
|
+
this._indexDispatchesInFlight = 0;
|
|
1919
|
+
}
|
|
1920
|
+
/** Wrap an indexing-request dispatch so awaitIndexingDrained counts it as
|
|
1921
|
+
* live work from the moment it is sent, not from the moment it is acked. */
|
|
1922
|
+
trackIndexDispatch(p) {
|
|
1923
|
+
var self = this;
|
|
1924
|
+
this._indexDispatchesInFlight += 1;
|
|
1925
|
+
var release = function() {
|
|
1926
|
+
self._indexDispatchesInFlight = Math.max(0, self._indexDispatchesInFlight - 1);
|
|
1927
|
+
};
|
|
1928
|
+
return p.then(function(v) {
|
|
1929
|
+
release();
|
|
1930
|
+
return v;
|
|
1931
|
+
}, function(e) {
|
|
1932
|
+
release();
|
|
1933
|
+
throw e;
|
|
1934
|
+
});
|
|
1728
1935
|
}
|
|
1729
1936
|
/**
|
|
1730
1937
|
* Register a live poll so (a) a remount dedupes against it instead of stacking a
|
|
@@ -1830,6 +2037,7 @@ var ChatSession = class {
|
|
|
1830
2037
|
if (!key) return;
|
|
1831
2038
|
this.aiChatHistoryCache[key] = {
|
|
1832
2039
|
messages: this.state.messages.filter(function(m) {
|
|
2040
|
+
if (m._stageId) return false;
|
|
1833
2041
|
return m._ownerKey === void 0 || m._ownerKey === key;
|
|
1834
2042
|
}),
|
|
1835
2043
|
endOfList: this.state.historyEndOfList,
|
|
@@ -1969,14 +2177,189 @@ var ChatSession = class {
|
|
|
1969
2177
|
this.pendingAgentRequests[params.key] = run;
|
|
1970
2178
|
return run;
|
|
1971
2179
|
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Put a turn on screen the INSTANT the user hits Send, before its attachments
|
|
2182
|
+
* have finished uploading. Uploads run in the background now (the composer is
|
|
2183
|
+
* cleared and stays usable), so without a staged bubble the message would
|
|
2184
|
+
* appear only once its files were up — below anything the user sent in the
|
|
2185
|
+
* meantime, in an order that never matches what they typed.
|
|
2186
|
+
*
|
|
2187
|
+
* Staged bubbles carry _useBgQueue because that is where a turn with
|
|
2188
|
+
* attachments ultimately dispatches (behind its own indexing tasks). That flag
|
|
2189
|
+
* is also what keeps promoteNextQueuedToRunning / resolveQueuedUserBubble off
|
|
2190
|
+
* them: those advance the SERVER queue, and a staged turn has no server
|
|
2191
|
+
* request behind it yet.
|
|
2192
|
+
*
|
|
2193
|
+
* Returns the id to hand back as PinnedDispatchContext.stageId at dispatch.
|
|
2194
|
+
*/
|
|
2195
|
+
stageOutgoingMessage(displayText) {
|
|
2196
|
+
this._stageSeq += 1;
|
|
2197
|
+
var stageId = "stg_" + this._stageSeq;
|
|
2198
|
+
var key = this.getHistoryCacheKey();
|
|
2199
|
+
var staged = {
|
|
2200
|
+
role: "user",
|
|
2201
|
+
content: displayText,
|
|
2202
|
+
isPendingQueued: true,
|
|
2203
|
+
isUploadingAttachments: true,
|
|
2204
|
+
isSendingToServer: true,
|
|
2205
|
+
_useBgQueue: true,
|
|
2206
|
+
_stageId: stageId,
|
|
2207
|
+
_ts: wallClockNow()
|
|
2208
|
+
};
|
|
2209
|
+
if (key) staged._ownerKey = key;
|
|
2210
|
+
this.state.messages.push(staged);
|
|
2211
|
+
this.host.notify();
|
|
2212
|
+
this.host.scrollToBottom(true);
|
|
2213
|
+
return stageId;
|
|
2214
|
+
}
|
|
2215
|
+
_stageIndex(list, stageId) {
|
|
2216
|
+
if (!stageId) return -1;
|
|
2217
|
+
for (var i = 0; i < list.length; i++) {
|
|
2218
|
+
if (list[i] && list[i]._stageId === stageId) return i;
|
|
2219
|
+
}
|
|
2220
|
+
return -1;
|
|
2221
|
+
}
|
|
2222
|
+
/**
|
|
2223
|
+
* Where a staged turn belongs once it is finally sent: BELOW every indexing
|
|
2224
|
+
* row, because that is the order it ran in and the order the server history
|
|
2225
|
+
* will report on the next load (its request id is newer than every pass it
|
|
2226
|
+
* waited for). While its files were uploading it sat above them — the rows
|
|
2227
|
+
* are injected as each file's pass starts, after the bubble was staged.
|
|
2228
|
+
*
|
|
2229
|
+
* Returns the index to insert at, or -1 to leave the turn where it is. Never
|
|
2230
|
+
* moves a turn UP: a bubble that already sits below the indexing rows (or a
|
|
2231
|
+
* chat with no indexing at all) must not jump backwards over anything.
|
|
2232
|
+
*/
|
|
2233
|
+
_settledStagePosition(fromIdx) {
|
|
2234
|
+
var lastBg = -1;
|
|
2235
|
+
for (var i = 0; i < this.state.messages.length; i++) {
|
|
2236
|
+
if (this.state.messages[i] && this.state.messages[i].isBackgroundTask) lastBg = i;
|
|
2237
|
+
}
|
|
2238
|
+
if (lastBg <= fromIdx) return -1;
|
|
2239
|
+
return lastBg;
|
|
2240
|
+
}
|
|
2241
|
+
/** The staged turn's files are up; it is now just waiting its place in the
|
|
2242
|
+
* queue. Drops the "(Uploading files...)" note back to "(In queue)". */
|
|
2243
|
+
markStagedMessageQueued(stageId) {
|
|
2244
|
+
var idx = this._stageIndex(this.state.messages, stageId);
|
|
2245
|
+
if (idx === -1) return;
|
|
2246
|
+
var ex = this.state.messages[idx];
|
|
2247
|
+
if (!ex.isUploadingAttachments) return;
|
|
2248
|
+
this.state.messages[idx] = Object.assign({}, ex, { isUploadingAttachments: false });
|
|
2249
|
+
this.host.notify();
|
|
2250
|
+
}
|
|
2251
|
+
/**
|
|
2252
|
+
* Resolves once this project's background-indexing queue has nothing left to
|
|
2253
|
+
* run, so a chat enqueued right after it is genuinely last.
|
|
2254
|
+
*
|
|
2255
|
+
* Sending the chat as soon as the uploads finish is not enough, which is the
|
|
2256
|
+
* whole reason this exists: indexing a file is a CHAIN, and each pass is only
|
|
2257
|
+
* enqueued once the previous one lands (the client mints CONTINUE passes for
|
|
2258
|
+
* text/grid files, the worker mints them for PDFs and windowed reads). Every
|
|
2259
|
+
* one of those passes therefore queues up BEHIND a chat sent at upload time,
|
|
2260
|
+
* and the model answers from a file it has only partly read.
|
|
2261
|
+
*
|
|
2262
|
+
* The queue is read from the server's status index rather than from
|
|
2263
|
+
* bgTaskQueue: that mirror holds only what this client dispatched or adopted,
|
|
2264
|
+
* and it stops being maintained once the view unmounts. An empty answer has to
|
|
2265
|
+
* repeat before it is believed — see INDEXING_DRAIN_IDLE_LOOKS — and a look
|
|
2266
|
+
* that fails counts as busy, so a dropped request delays the turn instead of
|
|
2267
|
+
* releasing it early.
|
|
2268
|
+
*
|
|
2269
|
+
* Reads the identity PINNED at Send time, never a live one: the user may be in
|
|
2270
|
+
* another project by now, and this must keep asking about the one they sent
|
|
2271
|
+
* from.
|
|
2272
|
+
*/
|
|
2273
|
+
awaitIndexingDrained(identity) {
|
|
2274
|
+
var self = this;
|
|
2275
|
+
var svcId = identity && identity.serviceId;
|
|
2276
|
+
var platform = identity && identity.platform;
|
|
2277
|
+
if (!svcId || platform !== "claude" && platform !== "openai") return Promise.resolve("skipped");
|
|
2278
|
+
var owner = identity.owner;
|
|
2279
|
+
var queue = bgIndexingQueueName(identity.userId, svcId);
|
|
2280
|
+
var startedAt = nowMs();
|
|
2281
|
+
var deadline = startedAt + INDEXING_DRAIN_TIMEOUT_MS;
|
|
2282
|
+
var idleLooks = 0;
|
|
2283
|
+
var ask = function(status) {
|
|
2284
|
+
return Promise.resolve(getChatHistory(
|
|
2285
|
+
{ service: svcId, owner, platform, queue, status },
|
|
2286
|
+
{ limit: WORKER_PASS_ADOPT_LIMIT }
|
|
2287
|
+
)).catch(function() {
|
|
2288
|
+
return null;
|
|
2289
|
+
});
|
|
2290
|
+
};
|
|
2291
|
+
var hasLiveIndexing = function(res) {
|
|
2292
|
+
var list = res && Array.isArray(res.list) ? res.list : [];
|
|
2293
|
+
for (var i = 0; i < list.length; i++) {
|
|
2294
|
+
var item = list[i];
|
|
2295
|
+
if (!item || item.status !== "pending" && item.status !== "running") continue;
|
|
2296
|
+
if (isIndexingRequestText(extractLastUserTextFromRequest(item.request_body))) return true;
|
|
2297
|
+
}
|
|
2298
|
+
return false;
|
|
2299
|
+
};
|
|
2300
|
+
return new Promise(function(resolve) {
|
|
2301
|
+
var again = function() {
|
|
2302
|
+
setTimeout(look, idleLooks > 0 ? INDEXING_DRAIN_CONFIRM_POLL_MS : INDEXING_DRAIN_BUSY_POLL_MS);
|
|
2303
|
+
};
|
|
2304
|
+
var look = function() {
|
|
2305
|
+
if (nowMs() >= deadline) {
|
|
2306
|
+
resolve("timedout");
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
if (self._indexDispatchesInFlight > 0) {
|
|
2310
|
+
idleLooks = 0;
|
|
2311
|
+
again();
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2314
|
+
Promise.all([ask("running"), ask("pending")]).then(function(res) {
|
|
2315
|
+
var unknown = res[0] === null || res[1] === null;
|
|
2316
|
+
if (unknown || hasLiveIndexing(res[0]) || hasLiveIndexing(res[1])) idleLooks = 0;
|
|
2317
|
+
else idleLooks += 1;
|
|
2318
|
+
if (idleLooks >= INDEXING_DRAIN_IDLE_LOOKS && nowMs() - startedAt >= INDEXING_DRAIN_MIN_MS) {
|
|
2319
|
+
resolve("drained");
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
again();
|
|
2323
|
+
}, function() {
|
|
2324
|
+
idleLooks = 0;
|
|
2325
|
+
again();
|
|
2326
|
+
});
|
|
2327
|
+
};
|
|
2328
|
+
look();
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
2332
|
+
* Abandon a staged turn — its uploads failed outright, so nothing will be
|
|
2333
|
+
* dispatched. The bubble stays (the user's text is not silently thrown away)
|
|
2334
|
+
* but settles into a plain, non-pending message; the caller reports the
|
|
2335
|
+
* failure separately.
|
|
2336
|
+
*/
|
|
2337
|
+
settleStagedMessage(stageId) {
|
|
2338
|
+
var idx = this._stageIndex(this.state.messages, stageId);
|
|
2339
|
+
if (idx === -1) return;
|
|
2340
|
+
var ex = this.state.messages[idx];
|
|
2341
|
+
var settled = { role: "user", content: ex.content };
|
|
2342
|
+
if (ex._ownerKey !== void 0) settled._ownerKey = ex._ownerKey;
|
|
2343
|
+
if (ex._ts !== void 0) settled._ts = ex._ts;
|
|
2344
|
+
this.state.messages[idx] = settled;
|
|
2345
|
+
this.host.notify();
|
|
2346
|
+
this.updateHistoryCache();
|
|
2347
|
+
}
|
|
1972
2348
|
// composed = clean display text; composedForLlm carries office-extraction
|
|
1973
2349
|
// placeholders for the provider only. useBgQueue routes a post-attachment turn
|
|
1974
2350
|
// onto the "-bg" queue so it runs after indexing.
|
|
1975
2351
|
dispatchComposedMessage(composed, useBgQueue, composedForLlm, extractContent, fileUrls, pinned) {
|
|
1976
2352
|
var self = this;
|
|
1977
|
-
|
|
2353
|
+
var stageId = pinned ? pinned.stageId : void 0;
|
|
2354
|
+
if (!composed) {
|
|
2355
|
+
if (stageId) this.settleStagedMessage(stageId);
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
1978
2358
|
var id = pinned ? pinned.identity : this.host.getIdentity();
|
|
1979
|
-
if (id.platform === "none")
|
|
2359
|
+
if (id.platform === "none") {
|
|
2360
|
+
if (stageId) this.settleStagedMessage(stageId);
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
1980
2363
|
var llmComposed = composedForLlm || composed;
|
|
1981
2364
|
var key = !id.serviceId ? "" : id.serviceId + "#" + id.platform;
|
|
1982
2365
|
var offChat = !!key && key !== this.getHistoryCacheKey();
|
|
@@ -1987,10 +2370,10 @@ var ChatSession = class {
|
|
|
1987
2370
|
var aiModel = id.model || void 0;
|
|
1988
2371
|
var systemPrompt = pinned ? pinned.systemPrompt : this.host.buildSystemPrompt();
|
|
1989
2372
|
var userId = id.userId || id.serviceId;
|
|
1990
|
-
var chatQueue = useBgQueue ? userId
|
|
2373
|
+
var chatQueue = useBgQueue ? bgIndexingQueueName(userId) : userId;
|
|
1991
2374
|
if (offChat) {
|
|
1992
2375
|
var offHistory = (this.aiChatHistoryCache[key] ? this.aiChatHistoryCache[key].messages : []).filter(function(m) {
|
|
1993
|
-
return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask;
|
|
2376
|
+
return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask && !m.isError;
|
|
1994
2377
|
});
|
|
1995
2378
|
var offBounded = buildBoundedChatMessages({
|
|
1996
2379
|
platform: aiPlatform,
|
|
@@ -2000,9 +2383,16 @@ var ChatSession = class {
|
|
|
2000
2383
|
history: offHistory.concat([{ role: "user", content: llmComposed }])
|
|
2001
2384
|
});
|
|
2002
2385
|
var offExisting = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
|
|
2386
|
+
var offUser = { role: "user", content: composed, _ownerKey: key, _ts: wallClockNow() };
|
|
2387
|
+
var offStage = this._stageIndex(this.state.messages, stageId);
|
|
2388
|
+
if (offStage !== -1) {
|
|
2389
|
+
if (this.state.messages[offStage]._ts !== void 0) offUser._ts = this.state.messages[offStage]._ts;
|
|
2390
|
+
this.state.messages.splice(offStage, 1);
|
|
2391
|
+
this.host.notify();
|
|
2392
|
+
}
|
|
2003
2393
|
this.aiChatHistoryCache[key] = {
|
|
2004
2394
|
messages: offExisting.messages.concat([
|
|
2005
|
-
|
|
2395
|
+
offUser,
|
|
2006
2396
|
{ role: "assistant", content: "", isPending: true, isPendingInProcess: true, _ownerKey: key }
|
|
2007
2397
|
]),
|
|
2008
2398
|
endOfList: offExisting.endOfList,
|
|
@@ -2025,7 +2415,7 @@ var ChatSession = class {
|
|
|
2025
2415
|
}
|
|
2026
2416
|
if (isQueuedSend) {
|
|
2027
2417
|
var resolvedHistory = this.state.messages.filter(function(m) {
|
|
2028
|
-
return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask;
|
|
2418
|
+
return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask && !m.isError;
|
|
2029
2419
|
});
|
|
2030
2420
|
var boundedQ = buildBoundedChatMessages({
|
|
2031
2421
|
platform: aiPlatform,
|
|
@@ -2037,14 +2427,26 @@ var ChatSession = class {
|
|
|
2037
2427
|
var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true, _ts: wallClockNow() };
|
|
2038
2428
|
if (key) queuedBubble._ownerKey = key;
|
|
2039
2429
|
if (useBgQueue) queuedBubble._useBgQueue = true;
|
|
2040
|
-
this.state.messages
|
|
2430
|
+
var qStage = this._stageIndex(this.state.messages, stageId);
|
|
2431
|
+
if (qStage !== -1) {
|
|
2432
|
+
if (this.state.messages[qStage]._ts !== void 0) queuedBubble._ts = this.state.messages[qStage]._ts;
|
|
2433
|
+
var qTarget = this._settledStagePosition(qStage);
|
|
2434
|
+
if (qTarget === -1) {
|
|
2435
|
+
this.state.messages.splice(qStage, 1, queuedBubble);
|
|
2436
|
+
} else {
|
|
2437
|
+
this.state.messages.splice(qStage, 1);
|
|
2438
|
+
this.state.messages.splice(qTarget, 0, queuedBubble);
|
|
2439
|
+
}
|
|
2440
|
+
} else {
|
|
2441
|
+
this.state.messages.push(queuedBubble);
|
|
2442
|
+
}
|
|
2041
2443
|
this.host.notify();
|
|
2042
2444
|
this.updateHistoryCache();
|
|
2043
2445
|
this.host.scrollToBottom(true);
|
|
2044
2446
|
var capturedComposed = composed, capturedPlatform = aiPlatform, capturedKey = key;
|
|
2045
2447
|
Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent, fileUrls, id.serviceId, id.owner)).then(function(result) {
|
|
2046
2448
|
var sendingIdx = self.getHistoryCacheKey() !== capturedKey ? -1 : self.state.messages.findIndex(function(m) {
|
|
2047
|
-
return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user" && (m._ownerKey === void 0 || m._ownerKey === capturedKey);
|
|
2449
|
+
return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user" && !m._stageId && (m._ownerKey === void 0 || m._ownerKey === capturedKey);
|
|
2048
2450
|
});
|
|
2049
2451
|
var serverId = result && typeof result.id === "string" ? result.id : void 0;
|
|
2050
2452
|
if (sendingIdx >= 0) {
|
|
@@ -2069,23 +2471,31 @@ var ChatSession = class {
|
|
|
2069
2471
|
});
|
|
2070
2472
|
return;
|
|
2071
2473
|
}
|
|
2072
|
-
|
|
2073
|
-
|
|
2474
|
+
var immediateUser = { role: "user", content: composed, _ts: wallClockNow(), ...key ? { _ownerKey: key } : {} };
|
|
2475
|
+
var immediatePlaceholder = { role: "assistant", content: "", isPending: true, isPendingInProcess: true, ...key ? { _ownerKey: key } : {} };
|
|
2476
|
+
var iStage = this._stageIndex(this.state.messages, stageId);
|
|
2477
|
+
if (iStage !== -1) {
|
|
2478
|
+
if (this.state.messages[iStage]._ts !== void 0) immediateUser._ts = this.state.messages[iStage]._ts;
|
|
2479
|
+
var iTarget = this._settledStagePosition(iStage);
|
|
2480
|
+
if (iTarget === -1) {
|
|
2481
|
+
this.state.messages.splice(iStage, 1, immediateUser, immediatePlaceholder);
|
|
2482
|
+
} else {
|
|
2483
|
+
this.state.messages.splice(iStage, 1);
|
|
2484
|
+
this.state.messages.splice(iTarget, 0, immediateUser, immediatePlaceholder);
|
|
2485
|
+
}
|
|
2486
|
+
} else {
|
|
2487
|
+
this.state.messages.push(immediateUser);
|
|
2488
|
+
this.state.messages.push(immediatePlaceholder);
|
|
2489
|
+
}
|
|
2074
2490
|
this.host.notify();
|
|
2075
2491
|
this.updateHistoryCache();
|
|
2076
2492
|
this.state.sending = true;
|
|
2077
2493
|
this.host.scrollToBottom(true);
|
|
2078
2494
|
var historyForLlm = this.state.messages.filter(function(m) {
|
|
2079
|
-
|
|
2495
|
+
if (m === immediateUser) return false;
|
|
2496
|
+
return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask && !m.isError;
|
|
2080
2497
|
});
|
|
2081
|
-
|
|
2082
|
-
for (var li = historyForLlm.length - 1; li >= 0; li--) {
|
|
2083
|
-
if (historyForLlm[li].role === "user" && historyForLlm[li].content === composed) {
|
|
2084
|
-
historyForLlm[li] = Object.assign({}, historyForLlm[li], { content: llmComposed });
|
|
2085
|
-
break;
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2498
|
+
historyForLlm.push({ role: "user", content: llmComposed });
|
|
2089
2499
|
var bounded = buildBoundedChatMessages({
|
|
2090
2500
|
platform: aiPlatform,
|
|
2091
2501
|
model: aiModel,
|
|
@@ -2326,7 +2736,7 @@ var ChatSession = class {
|
|
|
2326
2736
|
if (platform !== "claude" && platform !== "openai") return;
|
|
2327
2737
|
var url = platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
|
|
2328
2738
|
var queueBase = id.userId || id.serviceId;
|
|
2329
|
-
var queue = msg.isBackgroundTask || msg._useBgQueue ? queueBase
|
|
2739
|
+
var queue = msg.isBackgroundTask || msg._useBgQueue ? bgIndexingQueueName(queueBase) : queueBase;
|
|
2330
2740
|
this.state.messages[idx] = Object.assign({}, msg, { _cancelling: true, _cancelError: void 0 });
|
|
2331
2741
|
this.host.notify();
|
|
2332
2742
|
Promise.resolve(this.host.cancelRequest({
|
|
@@ -2812,7 +3222,7 @@ var ChatSession = class {
|
|
|
2812
3222
|
if (!id.serviceId || platform !== "claude" && platform !== "openai") return;
|
|
2813
3223
|
if (this.isPollingPaused() || !this.host.isViewMounted()) return;
|
|
2814
3224
|
var svcId = id.serviceId, owner = id.owner;
|
|
2815
|
-
var queue = (id.userId
|
|
3225
|
+
var queue = bgIndexingQueueName(id.userId, id.serviceId);
|
|
2816
3226
|
var ask = function(status) {
|
|
2817
3227
|
return Promise.resolve(getChatHistory(
|
|
2818
3228
|
{ service: svcId, owner, platform, queue, status },
|
|
@@ -2914,7 +3324,7 @@ var ChatSession = class {
|
|
|
2914
3324
|
url,
|
|
2915
3325
|
method: "POST",
|
|
2916
3326
|
id: serverId,
|
|
2917
|
-
queue: (id.userId
|
|
3327
|
+
queue: bgIndexingQueueName(id.userId, id.serviceId),
|
|
2918
3328
|
service: id.serviceId,
|
|
2919
3329
|
owner: id.owner
|
|
2920
3330
|
})).catch(function() {
|
|
@@ -3043,7 +3453,7 @@ var ChatSession = class {
|
|
|
3043
3453
|
if (pass > MAX_INDEXING_RESUME_PASSES) return;
|
|
3044
3454
|
var id = this.host.getIdentity();
|
|
3045
3455
|
if (!id || id.platform === "none" || id.serviceId !== entry.serviceId) return;
|
|
3046
|
-
notifyAgentContinueIndexing({
|
|
3456
|
+
this.trackIndexDispatch(notifyAgentContinueIndexing({
|
|
3047
3457
|
platform: id.platform,
|
|
3048
3458
|
model: id.model,
|
|
3049
3459
|
service: id.serviceId,
|
|
@@ -3077,7 +3487,7 @@ var ChatSession = class {
|
|
|
3077
3487
|
}
|
|
3078
3488
|
}, function(e) {
|
|
3079
3489
|
console.error("[chat-engine] resume-indexing dispatch failed", e);
|
|
3080
|
-
});
|
|
3490
|
+
}));
|
|
3081
3491
|
} catch (e) {
|
|
3082
3492
|
}
|
|
3083
3493
|
}
|
|
@@ -3155,6 +3565,10 @@ var ChatSession = class {
|
|
|
3155
3565
|
if (mm._ownerKey !== void 0 && mm._ownerKey !== loadKey) continue;
|
|
3156
3566
|
if (mm._serverItemId && serverIds[mm._serverItemId]) continue;
|
|
3157
3567
|
if (!mm._serverItemId) {
|
|
3568
|
+
if (mm._stageId) {
|
|
3569
|
+
rescued.push(mm);
|
|
3570
|
+
continue;
|
|
3571
|
+
}
|
|
3158
3572
|
if (mappedHasPendingAssistant) continue;
|
|
3159
3573
|
if (mm.isSendingToServer || mm.isPendingQueued || mm.isPendingInProcess || mm.isPending) rescued.push(mm);
|
|
3160
3574
|
else if (self.state.sending && mm.role === "user") {
|
|
@@ -3374,7 +3788,7 @@ var ChatSession = class {
|
|
|
3374
3788
|
return preIndex.then(function() {
|
|
3375
3789
|
return parseAttachmentContent(member.file, member.file.name, mime || void 0);
|
|
3376
3790
|
}).then(function(parsedContent) {
|
|
3377
|
-
return notifyAgentSaveAttachment({
|
|
3791
|
+
return self.trackIndexDispatch(notifyAgentSaveAttachment({
|
|
3378
3792
|
platform: id.platform,
|
|
3379
3793
|
model: id.model,
|
|
3380
3794
|
service: id.serviceId,
|
|
@@ -3413,7 +3827,7 @@ var ChatSession = class {
|
|
|
3413
3827
|
att.errorCode = e && (e.code || e.body && e.body.code) || "";
|
|
3414
3828
|
att.errorDetail = e && (e.message || e.body && e.body.message) || (typeof e === "string" ? e : "");
|
|
3415
3829
|
}
|
|
3416
|
-
});
|
|
3830
|
+
}));
|
|
3417
3831
|
});
|
|
3418
3832
|
});
|
|
3419
3833
|
});
|
|
@@ -3432,14 +3846,24 @@ var ChatSession = class {
|
|
|
3432
3846
|
}
|
|
3433
3847
|
// Upload all not-yet-done attachments sequentially. Resolves to the full
|
|
3434
3848
|
// list of { name, url, storagePath } for composing the chat message.
|
|
3435
|
-
|
|
3849
|
+
//
|
|
3850
|
+
// `batchId` scopes the run to the chips stamped with it at Send time. The
|
|
3851
|
+
// composer stays live during an upload, so by the time this runs the
|
|
3852
|
+
// attachment list can already hold chips the user picked for the NEXT
|
|
3853
|
+
// message — uploading those here would attach them to the wrong turn, and
|
|
3854
|
+
// collecting the previous batch's finished urls would attach files the user
|
|
3855
|
+
// already sent. Omitted (no batch) means every chip, the old behavior.
|
|
3856
|
+
uploadPendingAttachments(batchId) {
|
|
3436
3857
|
var self = this;
|
|
3437
3858
|
this.host.resetOverwriteBatch();
|
|
3859
|
+
this._uploadBatches += 1;
|
|
3438
3860
|
this.state.uploadingAttachments = true;
|
|
3439
3861
|
this.host.updateComposerControls();
|
|
3440
3862
|
this.host.renderAttachmentChips();
|
|
3441
3863
|
var collected = [];
|
|
3442
|
-
var snapshot = this.state.attachments.
|
|
3864
|
+
var snapshot = this.state.attachments.filter(function(a) {
|
|
3865
|
+
return batchId ? a._batchId === batchId : true;
|
|
3866
|
+
});
|
|
3443
3867
|
var chain = Promise.resolve();
|
|
3444
3868
|
snapshot.forEach(function(att) {
|
|
3445
3869
|
chain = chain.then(function() {
|
|
@@ -3475,7 +3899,8 @@ var ChatSession = class {
|
|
|
3475
3899
|
});
|
|
3476
3900
|
});
|
|
3477
3901
|
var done = function() {
|
|
3478
|
-
self.
|
|
3902
|
+
self._uploadBatches = Math.max(0, self._uploadBatches - 1);
|
|
3903
|
+
self.state.uploadingAttachments = self._uploadBatches > 0;
|
|
3479
3904
|
self.host.updateComposerControls();
|
|
3480
3905
|
self.host.renderAttachmentChips();
|
|
3481
3906
|
return collected;
|
|
@@ -3682,6 +4107,8 @@ function buildChatDisplayList(messages, opts) {
|
|
|
3682
4107
|
}
|
|
3683
4108
|
|
|
3684
4109
|
exports.BG_INDEXING_QUEUE_SUFFIX = BG_INDEXING_QUEUE_SUFFIX;
|
|
4110
|
+
exports.BOM = BOM;
|
|
4111
|
+
exports.BOM_EXTS = BOM_EXTS;
|
|
3685
4112
|
exports.CLAUDE_INPUT_CAP_RATIO = CLAUDE_INPUT_CAP_RATIO;
|
|
3686
4113
|
exports.CLAUDE_PER_REQUEST_INPUT_CAP = CLAUDE_PER_REQUEST_INPUT_CAP;
|
|
3687
4114
|
exports.CONTEXT_WINDOW_BY_MODEL = CONTEXT_WINDOW_BY_MODEL;
|
|
@@ -3692,9 +4119,12 @@ exports.DEFAULT_OPENAI_MODEL = DEFAULT_OPENAI_MODEL;
|
|
|
3692
4119
|
exports.EXPIRED_ATTACHMENT_URL_HOST = EXPIRED_ATTACHMENT_URL_HOST;
|
|
3693
4120
|
exports.EXPIRED_ATTACHMENT_URL_ORIGIN = EXPIRED_ATTACHMENT_URL_ORIGIN;
|
|
3694
4121
|
exports.EXPIRED_LINK_REFRESH_EXPIRES_SECONDS = EXPIRED_LINK_REFRESH_EXPIRES_SECONDS;
|
|
4122
|
+
exports.EXT_CONTENT_TYPES = EXT_CONTENT_TYPES;
|
|
3695
4123
|
exports.HISTORY_BUDGET_RATIO = HISTORY_BUDGET_RATIO;
|
|
3696
4124
|
exports.HISTORY_FILL_SLACK_PX = HISTORY_FILL_SLACK_PX;
|
|
3697
4125
|
exports.HISTORY_TOKEN_BUDGET = HISTORY_TOKEN_BUDGET;
|
|
4126
|
+
exports.HTML_EXTS = HTML_EXTS;
|
|
4127
|
+
exports.HTML_HEAD_WINDOW = HTML_HEAD_WINDOW;
|
|
3698
4128
|
exports.LINK_LABEL_MAX_DISPLAY_CHARS = LINK_LABEL_MAX_DISPLAY_CHARS;
|
|
3699
4129
|
exports.LINK_REFRESH_WINDOW_MS = LINK_REFRESH_WINDOW_MS;
|
|
3700
4130
|
exports.MAX_HISTORY_FILL_PAGES = MAX_HISTORY_FILL_PAGES;
|
|
@@ -3705,7 +4135,11 @@ exports.MIN_INPUT_TOKEN_BUDGET = MIN_INPUT_TOKEN_BUDGET;
|
|
|
3705
4135
|
exports.OUTPUT_TOKEN_RESERVE = OUTPUT_TOKEN_RESERVE;
|
|
3706
4136
|
exports.POLL_INTERVAL = POLL_INTERVAL;
|
|
3707
4137
|
exports.RENDER_FROM_TOKEN = RENDER_FROM_TOKEN;
|
|
4138
|
+
exports.RTF_EXTS = RTF_EXTS;
|
|
3708
4139
|
exports.TOOL_AND_RESPONSE_BUFFER = TOOL_AND_RESPONSE_BUFFER;
|
|
4140
|
+
exports.XML_EXTS = XML_EXTS;
|
|
4141
|
+
exports.applyEncodingDeclaration = applyEncodingDeclaration;
|
|
4142
|
+
exports.bgIndexingQueueName = bgIndexingQueueName;
|
|
3709
4143
|
exports.buildAiAgentValue = buildAiAgentValue;
|
|
3710
4144
|
exports.buildBoundedChatMessages = buildBoundedChatMessages;
|
|
3711
4145
|
exports.buildChatDisplayList = buildChatDisplayList;
|
|
@@ -3725,11 +4159,17 @@ exports.classifyInlineLink = classifyInlineLink;
|
|
|
3725
4159
|
exports.clearAttachmentParsers = clearAttachmentParsers;
|
|
3726
4160
|
exports.composeUserMessage = composeUserMessage;
|
|
3727
4161
|
exports.configureChatEngine = configureChatEngine;
|
|
4162
|
+
exports.contentTypeForExt = contentTypeForExt;
|
|
3728
4163
|
exports.createHistoryFiller = createHistoryFiller;
|
|
3729
4164
|
exports.createInlineLinkRegex = createInlineLinkRegex;
|
|
3730
4165
|
exports.encodePathSegments = encodePathSegments;
|
|
4166
|
+
exports.encodingClassForExt = encodingClassForExt;
|
|
4167
|
+
exports.ensureHtmlCharset = ensureHtmlCharset;
|
|
4168
|
+
exports.ensureXmlEncoding = ensureXmlEncoding;
|
|
4169
|
+
exports.escapeRtfNonAscii = escapeRtfNonAscii;
|
|
3731
4170
|
exports.estimateMessageTokens = estimateMessageTokens;
|
|
3732
4171
|
exports.estimateTextTokens = estimateTextTokens;
|
|
4172
|
+
exports.extOf = extOf;
|
|
3733
4173
|
exports.extractClaudeText = extractClaudeText;
|
|
3734
4174
|
exports.extractLastUserTextFromRequest = extractLastUserTextFromRequest;
|
|
3735
4175
|
exports.extractOpenAIText = extractOpenAIText;
|
|
@@ -3745,6 +4185,7 @@ exports.getErrorMessage = getErrorMessage;
|
|
|
3745
4185
|
exports.getExpiredAttachmentVisiblePath = getExpiredAttachmentVisiblePath;
|
|
3746
4186
|
exports.getProjectContextWindow = getProjectContextWindow;
|
|
3747
4187
|
exports.groupAttachmentFailures = groupAttachmentFailures;
|
|
4188
|
+
exports.hasBom = hasBom;
|
|
3748
4189
|
exports.isAuthExpiredError = isAuthExpiredError;
|
|
3749
4190
|
exports.isBgIndexingQueue = isBgIndexingQueue;
|
|
3750
4191
|
exports.isErrorResponseBody = isErrorResponseBody;
|
|
@@ -3756,9 +4197,12 @@ exports.isServerExtractable = isServerExtractable;
|
|
|
3756
4197
|
exports.isServiceDbAttachmentHref = isServiceDbAttachmentHref;
|
|
3757
4198
|
exports.listClaudeModels = listClaudeModels;
|
|
3758
4199
|
exports.listOpenAIModels = listOpenAIModels;
|
|
4200
|
+
exports.looksLikeRtf = looksLikeRtf;
|
|
3759
4201
|
exports.makeExtractPlaceholder = makeExtractPlaceholder;
|
|
3760
4202
|
exports.mapHistoryListToMessages = mapHistoryListToMessages;
|
|
4203
|
+
exports.needsBomForExt = needsBomForExt;
|
|
3761
4204
|
exports.normalizeAttachmentPathCandidate = normalizeAttachmentPathCandidate;
|
|
4205
|
+
exports.normalizeExt = normalizeExt;
|
|
3762
4206
|
exports.normalizeTextContent = normalizeTextContent;
|
|
3763
4207
|
exports.normalizeTrailingInlineToken = normalizeTrailingInlineToken;
|
|
3764
4208
|
exports.notifyAgentSaveAttachment = notifyAgentSaveAttachment;
|
|
@@ -3766,6 +4210,7 @@ exports.parseAiAgentValue = parseAiAgentValue;
|
|
|
3766
4210
|
exports.parseAttachmentContent = parseAttachmentContent;
|
|
3767
4211
|
exports.parseIndexingLabel = parseIndexingLabel;
|
|
3768
4212
|
exports.parseIndexingRequestText = parseIndexingRequestText;
|
|
4213
|
+
exports.prepareDownloadText = prepareDownloadText;
|
|
3769
4214
|
exports.readExpiredAttachmentHref = readExpiredAttachmentHref;
|
|
3770
4215
|
exports.registerAttachmentParser = registerAttachmentParser;
|
|
3771
4216
|
exports.registerModelContextWindows = registerModelContextWindows;
|