bunnyquery 1.5.7 → 1.6.2
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 +522 -65
- package/dist/engine.cjs +516 -63
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +160 -5
- package/dist/engine.d.ts +160 -5
- package/dist/engine.mjs +511 -64
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/config.ts +19 -0
- package/src/engine/host.ts +19 -0
- package/src/engine/index.ts +2 -1
- package/src/engine/office.ts +78 -5
- package/src/engine/prompts/index.ts +5 -0
- package/src/engine/prompts/indexing_system_prompt.ts +5 -3
- package/src/engine/prompts/indexing_user_message.ts +159 -0
- package/src/engine/requests.ts +170 -25
- package/src/engine/session.ts +402 -42
package/bunnyquery.js
CHANGED
|
@@ -56,6 +56,9 @@
|
|
|
56
56
|
}
|
|
57
57
|
return _config;
|
|
58
58
|
}
|
|
59
|
+
function windowedIndexingEnabled() {
|
|
60
|
+
return _config?.windowedIndexing === true;
|
|
61
|
+
}
|
|
59
62
|
function pollOpt() {
|
|
60
63
|
const p = _config?.poll;
|
|
61
64
|
return p === void 0 ? {} : { poll: p };
|
|
@@ -149,19 +152,65 @@
|
|
|
149
152
|
if (isTextMime(m)) return true;
|
|
150
153
|
return m.includes("officedocument") || m.includes("opendocument") || m.includes("hwp") || m.includes("epub") || m === "application/msword" || m === "application/vnd.ms-excel" || m === "application/vnd.ms-powerpoint";
|
|
151
154
|
}
|
|
152
|
-
var PAGED_READ_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
155
|
+
var PAGED_READ_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
156
|
+
// grids
|
|
157
|
+
"xls",
|
|
158
|
+
"xlsx",
|
|
159
|
+
"xlsm",
|
|
160
|
+
"ods",
|
|
161
|
+
// delimited text (row-windowed by the layer)
|
|
162
|
+
"csv",
|
|
163
|
+
"tsv",
|
|
164
|
+
"tab",
|
|
165
|
+
// documents
|
|
166
|
+
"pdf",
|
|
167
|
+
"docx",
|
|
168
|
+
"pptx",
|
|
169
|
+
// plain text / data / markup
|
|
170
|
+
"txt",
|
|
171
|
+
"md",
|
|
172
|
+
"markdown",
|
|
173
|
+
"log",
|
|
174
|
+
"json",
|
|
175
|
+
"jsonl",
|
|
176
|
+
"ndjson",
|
|
177
|
+
"xml",
|
|
178
|
+
"yaml",
|
|
179
|
+
"yml"
|
|
180
|
+
]);
|
|
153
181
|
function isPagedReadFile(name, mime) {
|
|
154
182
|
const ext = (name || "").split(".").pop()?.toLowerCase() || "";
|
|
155
183
|
if (PAGED_READ_EXTENSIONS.has(ext)) return true;
|
|
156
184
|
const m = (mime || "").toLowerCase();
|
|
157
185
|
return m === "application/pdf" || m === "application/vnd.ms-excel" || m.includes("spreadsheetml") || m.includes("opendocument.spreadsheet");
|
|
158
186
|
}
|
|
187
|
+
function isImageVisionFile(name, mime) {
|
|
188
|
+
const ext = (name || "").split(".").pop()?.toLowerCase() || "";
|
|
189
|
+
return ext === "pdf" || (mime || "").toLowerCase() === "application/pdf";
|
|
190
|
+
}
|
|
159
191
|
var _extractPlaceholderSeq = 0;
|
|
160
192
|
function makeExtractPlaceholder(seed) {
|
|
161
193
|
_extractPlaceholderSeq += 1;
|
|
162
194
|
const slug = (seed || "file").replace(/[^a-zA-Z0-9]+/g, "_").slice(-48);
|
|
163
195
|
return `{{SKAPI_FILE_CONTENT::${slug}-${_extractPlaceholderSeq}}}`;
|
|
164
196
|
}
|
|
197
|
+
var _renderPlaceholderSeq = 0;
|
|
198
|
+
function makeRenderPlaceholder(seed) {
|
|
199
|
+
_renderPlaceholderSeq += 1;
|
|
200
|
+
const slug = (seed || "file").replace(/[^a-zA-Z0-9]+/g, "_").slice(-48);
|
|
201
|
+
return `{{SKAPI_RENDER::${slug}-${_renderPlaceholderSeq}}}`;
|
|
202
|
+
}
|
|
203
|
+
var RENDER_PAGES_PER_WINDOW = 5;
|
|
204
|
+
var _windowPlaceholderSeq = 0;
|
|
205
|
+
function makeWindowPlaceholder(seed) {
|
|
206
|
+
_windowPlaceholderSeq += 1;
|
|
207
|
+
const slug = (seed || "file").replace(/[^a-zA-Z0-9]+/g, "_").slice(-48);
|
|
208
|
+
return `{{SKAPI_WINDOW::${slug}-${_windowPlaceholderSeq}}}`;
|
|
209
|
+
}
|
|
210
|
+
function isWindowedReadFile(name, mime) {
|
|
211
|
+
if (isImageVisionFile(name, mime)) return false;
|
|
212
|
+
return isPagedReadFile(name, mime);
|
|
213
|
+
}
|
|
165
214
|
function composeUserMessage(text, attachmentUrls) {
|
|
166
215
|
let composed = text;
|
|
167
216
|
if (attachmentUrls.length > 0) {
|
|
@@ -259,12 +308,14 @@ Project description: """${serviceDescription}"""`;
|
|
|
259
308
|
let systemPrompt = `You are a background indexing agent for project ${service}.
|
|
260
309
|
- 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.
|
|
261
310
|
- Most 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 been extracted on the server and included inline in the user message between the "BEGIN FILE CONTENT" / "END FILE CONTENT" markers - read that directly. If the inline content is a "[skapi: ...]" note, the file could not be extracted - index it from its metadata only.
|
|
262
|
-
- BIG
|
|
263
|
-
-
|
|
311
|
+
- BIG SPREADSHEETS / TEXT: the inline content may be only the FIRST part of a large file (it can end with a truncation or "more remains" note). For big spreadsheets and big text/data files READ THE FILE WITH THE readFileContent TOOL: it returns the file ONE WINDOW at a time (spreadsheets as coordinate-tagged grid rows, text as a range of characters). Pass the file's storage path. After each window: datafy it into records and SAVE them, THEN if the window says MORE REMAINS call readFileContent again with the cursor it gives you. Repeat until it says END OF FILE, so the WHOLE file is indexed - never stop after the first window. (Do NOT call readFileContent on a PDF - see the next line.)
|
|
312
|
+
- PDFs (scanned or not): you do NOT read a PDF with a tool or a URL. Its pages are RENDERED and embedded directly in the user message as IMAGE blocks, a WINDOW of pages at a time. LOOK at the embedded page images and datafy every one. The note beside them tells you whether MORE pages remain: if so, save this window's records and stop (a follow-up pass shows the next window automatically); only when the note says it was the LAST window is the PDF fully seen. Do NOT call readFileContent or web_fetch for a PDF.
|
|
313
|
+
- VISION: when the message (a readFileContent window, an embedded PDF page, or an inline attachment) includes IMAGES - scanned/rendered PDF pages, or photos embedded in a spreadsheet next to a row/block - LOOK at them and capture what they show as record data (the reading/values in a scanned table, the part/defect/condition visible in a photo). The image IS part of the data; correlate each photo with its labelled block ("PHOTO A3" markers tie a photo to that grid row).
|
|
264
314
|
- Whatever the file type, use the file's storage path (the "storage path" metadata line) as the "src::" unique_id - never the inline content or a temporary URL.
|
|
265
315
|
- TABULAR data (any spreadsheet - .csv/.tsv/.xlsx/.xls/.ods, or sheet-like rows): you MUST save EVERY data row as its own record (ONE record per row) with that row's actual column values in the record's "data", keyed by the header names, in a dedicated table (e.g. "spreadsheet_rows"). Do NOT summarize, sample only a few rows, or save just file metadata - index the whole sheet, paging through it with readFileContent when it is large. Make MULTIPLE postRecords calls in batches (e.g. 30-50 rows per call) rather than one oversized call. This per-row completeness OVERRIDES brevity. ALSO save one file-level summary record (file name, sheet name(s), column headers, total row count, overall summary) - this is the record that carries the file's "src::" unique_id - and link EVERY per-row record to it via reference (set each row record's reference to that src:: file record; the row records themselves do NOT carry a src:: unique_id). The per-row records AND this reference linkage are BOTH mandatory: the linkage is what lets the whole sheet be found and cleaned up together when the file is re-indexed.
|
|
266
316
|
- EPUB / e-books / long-form books (.epub or any book-length prose, provided inline in reading order with chapter headings preserved): you MUST save ONE record per CHAPTER (or, when chapters are unclear, per major section/topic) in a dedicated table (e.g. "book_chapters") - never collapse the whole book into a single record. Each chapter record's "data" must capture the chapter title plus its order/number AND a substantive summary of that chapter's content (key events, arguments, characters, places, concepts, terms, notable quotes). Apply AS MANY relevant tags as possible to EVERY chapter record (characters, locations, themes, topics, key concepts, key terms, dates, named entities) so the book is easy to SEARCH and cross-reference later - this is the whole point. ALSO save one book-level record (title, author, language, overall summary, chapter list / table of contents, genre/subjects) and link each chapter record to it via reference. This per-chapter completeness OVERRIDES brevity; human-readable summaries only, never raw/binary bytes.
|
|
267
|
-
- This is a background indexing task: do ALL the MCP saving FIRST, never reply mid-task, and never ask the user questions. Always use the MCP tools to save what you learn - be exhaustive about meaning (and, for tabular data, about every row). SAVE AS YOU GO: persist each window's records before reading the next, so progress is never lost. If the file is so large you cannot finish in one turn, still save everything you have read so far
|
|
317
|
+
- This is a background indexing task: do ALL the MCP saving FIRST, never reply mid-task, and never ask the user questions. Always use the MCP tools to save what you learn - be exhaustive about meaning (and, for tabular data, about every row). SAVE AS YOU GO: persist each window's records before reading the next, so progress is never lost. If the file is so large you cannot finish in one turn, still save everything you have read so far; a follow-up pass will automatically continue from where you stopped. Never store raw or binary bytes (base64, blobs); describe them in human-readable text instead.
|
|
318
|
+
- COMPLETION SIGNAL: only when you have fully read and saved the ENTIRE file (for readFileContent files: reached "END OF FILE"; for PDFs: the embedded page-image note said it was the LAST window - with all rows/pages/items saved), end your final message with the token INDEXING_COMPLETE on its own line. If you did NOT finish the whole file (more rows/pages remain), do NOT write that token - leaving it out is how the system knows to run another pass to continue.
|
|
268
319
|
- Only AFTER every save is done, send exactly ONE final message summarizing what you indexed - never just "Indexing complete", and never a raw/base64/binary value or a large pasted dump. Keep it to a few factual sentences or a short markdown bullet list covering: the file name, its content type, each table you wrote to with its record/row count and the key columns/fields or topics captured, and anything that could not be extracted. Follow this shape - Indexed <file name> (<content type>): saved <N> records to <table(s)> capturing <key columns/fields or topics>; could not extract: <gaps, or none>.`;
|
|
269
320
|
if (serviceDescription) {
|
|
270
321
|
systemPrompt += `
|
|
@@ -307,6 +358,74 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
307
358
|
}
|
|
308
359
|
return head + `- temporary URL (fetch this to read the file contents): ${attachment.url}`;
|
|
309
360
|
}
|
|
361
|
+
var RENDER_FROM_TOKEN = "{{RENDER_FROM}}";
|
|
362
|
+
var WINDOW_CURSOR_TOKEN = RENDER_FROM_TOKEN;
|
|
363
|
+
function buildIndexingRenderMessage(attachment, placeholder, renderFrom) {
|
|
364
|
+
const from = Math.max(0, renderFrom || 0);
|
|
365
|
+
if (from > 0) return buildIndexingRenderContinueTemplate(attachment, placeholder, String(from + 1));
|
|
366
|
+
return `A new file has just been uploaded. Index it now.
|
|
367
|
+
|
|
368
|
+
` + buildRenderMeta(attachment) + `
|
|
369
|
+
This is a PDF. Its pages are delivered to you as RENDERED PAGE IMAGES embedded directly in this message (you do NOT need any tool, URL, or web_fetch to see them). You are shown a WINDOW of pages at a time, starting at page ${from + 1}.
|
|
370
|
+
` + buildRenderDatafy(placeholder);
|
|
371
|
+
}
|
|
372
|
+
function buildIndexingRenderContinueTemplate(attachment, placeholder, pageLabel = RENDER_FROM_TOKEN) {
|
|
373
|
+
const src = `src::${attachment.storagePath}`;
|
|
374
|
+
return `CONTINUE indexing a PDF whose previous pass did not finish.
|
|
375
|
+
|
|
376
|
+
` + buildRenderMeta(attachment) + `
|
|
377
|
+
Records for the earlier pages are ALREADY saved (they reference "${src}"). The NEXT window of rendered page images (starting at page ${pageLabel}) is embedded in this message. Datafy each page as before and do NOT re-save pages that are already saved.
|
|
378
|
+
` + buildRenderDatafy(placeholder);
|
|
379
|
+
}
|
|
380
|
+
function buildRenderMeta(attachment) {
|
|
381
|
+
return `File metadata:
|
|
382
|
+
- name: ${attachment.name}
|
|
383
|
+
- storage path: ${attachment.storagePath}
|
|
384
|
+
` + (attachment.mime ? `- mime type: ${attachment.mime}
|
|
385
|
+
` : "");
|
|
386
|
+
}
|
|
387
|
+
function buildRenderDatafy(placeholder) {
|
|
388
|
+
return `
|
|
389
|
+
${placeholder}
|
|
390
|
+
|
|
391
|
+
LOOK at each rendered page image in this message and DATAFY what it shows: for EVERY page call postRecords and save records - one record per row / table entry / line item visible on the page (or one record for the page if it is prose), capturing every value you can read (OCR the text, read tables cell by cell, describe any photos/diagrams). Use the storage path above for the "src::" unique_id.
|
|
392
|
+
|
|
393
|
+
Save records for THIS window of pages only, then stop and report what you saved. Do NOT try to read the rest of the file and do NOT worry about the pages after this window: if any remain, the next window is rendered and sent to you automatically. Report only the pages you were actually shown - never imply you have seen the whole document.`;
|
|
394
|
+
}
|
|
395
|
+
function buildIndexingWindowMessage(attachment, placeholder, isContinuation, positionLabel) {
|
|
396
|
+
const src = `src::${attachment.storagePath}`;
|
|
397
|
+
const head = isContinuation ? `CONTINUE indexing a file whose previous pass did not finish.
|
|
398
|
+
|
|
399
|
+
` : `A new file has just been uploaded. Index it now.
|
|
400
|
+
|
|
401
|
+
`;
|
|
402
|
+
const where = isContinuation ? `
|
|
403
|
+
Records for the earlier windows are ALREADY saved (they reference "${src}"). The NEXT window (starting at ${WINDOW_CURSOR_TOKEN}) is embedded below. Do NOT re-save windows that are already saved.
|
|
404
|
+
` : `
|
|
405
|
+
This file is delivered to you ONE WINDOW at a time, embedded directly in this message. You do NOT need any tool, URL, or web_fetch to read it.
|
|
406
|
+
`;
|
|
407
|
+
return head + buildRenderMeta(attachment) + where + `
|
|
408
|
+
${placeholder}
|
|
409
|
+
|
|
410
|
+
DATAFY this window: call postRecords and save records for everything in it - ONE RECORD PER ROW for tabular data (keyed by the column headers), or one record per section for prose. Capture every value you can read. Use the storage path above for the "src::" unique_id on the file-level record, and link every row/section record to it by reference.
|
|
411
|
+
|
|
412
|
+
Save records for THIS window only, then stop and report what you saved. Do NOT try to read the rest of the file, and do NOT call readFileContent - if more remains, the next window is read and sent to you automatically. Report only what you were actually shown, and never imply you have seen the whole file when the note beside the window says more remains.`;
|
|
413
|
+
}
|
|
414
|
+
function buildIndexingContinueMessage(attachment) {
|
|
415
|
+
const src = `src::${attachment.storagePath}`;
|
|
416
|
+
return `CONTINUE indexing a file whose previous pass did not finish.
|
|
417
|
+
|
|
418
|
+
File metadata:
|
|
419
|
+
- name: ${attachment.name}
|
|
420
|
+
- storage path: ${attachment.storagePath}
|
|
421
|
+
` + (attachment.mime ? `- mime type: ${attachment.mime}
|
|
422
|
+
` : "") + `
|
|
423
|
+
Records for the earlier windows/pages of this file are ALREADY saved (they reference "${src}"). First call getRecords with reference "${src}" to see how far the previous pass got (the furthest page/row/window already saved). Then call readFileContent with the storage path above and a CURSOR that RESUMES just after that point - do NOT start at the beginning. The cursor is derivable from what you already saved:
|
|
424
|
+
- PDF: the cursor is the NUMBER OF PAGES already read (0-based next page). If you saved up to page N, call readFileContent with cursor="N" to get page N+1 onward.
|
|
425
|
+
- Spreadsheet: the cursor is "<sheetIndex>:<nextRow>" (0-based sheet index, 1-based row). If you saved up to row R of sheet S, use cursor="S:R+1".
|
|
426
|
+
- Text: the cursor is the character offset already read.
|
|
427
|
+
Index the REMAINING windows - one record per row/item, looking at any page images or embedded photos - saving as you go until readFileContent reports END OF FILE. Do NOT re-save windows that are already saved. Use the storage path above for the "src::" unique_id. When the ENTIRE file is finally indexed, end your message with the token INDEXING_COMPLETE.`;
|
|
428
|
+
}
|
|
310
429
|
|
|
311
430
|
// src/engine/errors.ts
|
|
312
431
|
function getErrorMessage(input) {
|
|
@@ -560,19 +679,22 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
560
679
|
var clientSecretRequest = (opts) => chatEngineConfig().clientSecretRequest(opts);
|
|
561
680
|
var getOpenAIImageDetail = (model) => {
|
|
562
681
|
const normalized = (model || DEFAULT_OPENAI_MODEL).trim().toLowerCase();
|
|
563
|
-
const match = normalized.match(/^gpt-(\d+)(?:\.(\d+))?$/);
|
|
682
|
+
const match = normalized.match(/^gpt-(\d+)(?:\.(\d+))?(-[a-z0-9.\-]+)?$/);
|
|
564
683
|
if (!match) {
|
|
565
684
|
return DEFAULT_OPENAI_IMAGE_DETAIL;
|
|
566
685
|
}
|
|
567
686
|
const major = Number(match[1]);
|
|
568
687
|
const minor = match[2] === void 0 ? null : Number(match[2]);
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
return "original";
|
|
688
|
+
const isVariant = !!match[3];
|
|
689
|
+
const supportsOriginal = major > 5 || major === 5 && minor !== null && minor >= 4;
|
|
690
|
+
if (!supportsOriginal) {
|
|
691
|
+
return DEFAULT_OPENAI_IMAGE_DETAIL;
|
|
574
692
|
}
|
|
575
|
-
return
|
|
693
|
+
return isVariant ? "high" : "original";
|
|
694
|
+
};
|
|
695
|
+
var getRenderImageDetail = (model) => {
|
|
696
|
+
const detail = getOpenAIImageDetail(model);
|
|
697
|
+
return detail === DEFAULT_OPENAI_IMAGE_DETAIL ? "high" : detail;
|
|
576
698
|
};
|
|
577
699
|
var IMAGE_URL_REGEX = /\bhttps?:\/\/[^\s<>"'()\[\]]+?\.(?:jpg|jpeg|png|gif|webp)(?:\?[^\s<>"'()\[\]]*)?/gi;
|
|
578
700
|
function transformContentWithImages(content) {
|
|
@@ -808,14 +930,53 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
808
930
|
}
|
|
809
931
|
});
|
|
810
932
|
}
|
|
933
|
+
async function notifyAgentContinueIndexing(info) {
|
|
934
|
+
return notifyAgentSaveAttachment({ ...info, continueIndexing: true });
|
|
935
|
+
}
|
|
811
936
|
async function notifyAgentSaveAttachment(info) {
|
|
812
937
|
const { platform, service, owner, attachment, parsedContent } = info;
|
|
813
|
-
const
|
|
814
|
-
const
|
|
938
|
+
const continuing = !!info.continueIndexing;
|
|
939
|
+
const visionFile = !parsedContent && isImageVisionFile(attachment.name, attachment.mime);
|
|
940
|
+
const renderFrom = Math.max(0, info.renderFrom || 0);
|
|
941
|
+
const renderPlaceholder = visionFile ? makeRenderPlaceholder(attachment.storagePath) : void 0;
|
|
942
|
+
const renderDetail = platform === "openai" ? getRenderImageDetail(info.model || DEFAULT_OPENAI_MODEL) : void 0;
|
|
943
|
+
const skapiRender = visionFile && renderPlaceholder ? {
|
|
944
|
+
_skapi_render: [
|
|
945
|
+
{
|
|
946
|
+
path: attachment.storagePath,
|
|
947
|
+
from: renderFrom,
|
|
948
|
+
count: RENDER_PAGES_PER_WINDOW,
|
|
949
|
+
placeholder: renderPlaceholder,
|
|
950
|
+
name: attachment.name,
|
|
951
|
+
mime: attachment.mime,
|
|
952
|
+
detail: renderDetail,
|
|
953
|
+
auto_continue: true,
|
|
954
|
+
continue_text: buildIndexingRenderContinueTemplate(attachment, renderPlaceholder)
|
|
955
|
+
}
|
|
956
|
+
]
|
|
957
|
+
} : {};
|
|
958
|
+
const windowedRead = !visionFile && !parsedContent && windowedIndexingEnabled() && isWindowedReadFile(attachment.name, attachment.mime);
|
|
959
|
+
const windowPlaceholder = windowedRead ? makeWindowPlaceholder(attachment.storagePath) : void 0;
|
|
960
|
+
const skapiWindow = windowedRead && windowPlaceholder ? {
|
|
961
|
+
_skapi_window: [
|
|
962
|
+
{
|
|
963
|
+
path: attachment.storagePath,
|
|
964
|
+
cursor: null,
|
|
965
|
+
placeholder: windowPlaceholder,
|
|
966
|
+
name: attachment.name,
|
|
967
|
+
mime: attachment.mime,
|
|
968
|
+
kind: "window",
|
|
969
|
+
auto_continue: true,
|
|
970
|
+
continue_text: buildIndexingWindowMessage(attachment, windowPlaceholder, true)
|
|
971
|
+
}
|
|
972
|
+
]
|
|
973
|
+
} : {};
|
|
974
|
+
const pagedRead = !visionFile && !windowedRead && (continuing || !parsedContent && isPagedReadFile(attachment.name, attachment.mime));
|
|
975
|
+
const serverExtract = !visionFile && !windowedRead && !continuing && !parsedContent && !pagedRead && isServerExtractable(attachment.name, attachment.mime);
|
|
815
976
|
const placeholder = serverExtract ? makeExtractPlaceholder(attachment.storagePath) : void 0;
|
|
816
977
|
const extractContent = serverExtract && placeholder ? [{ path: attachment.storagePath, placeholder, name: attachment.name, mime: attachment.mime }] : void 0;
|
|
817
978
|
const skapiExtract = extractContent && extractContent.length ? { _skapi_extract: extractContent } : {};
|
|
818
|
-
const userMessage = buildIndexingUserMessage(
|
|
979
|
+
const userMessage = visionFile && renderPlaceholder ? buildIndexingRenderMessage(attachment, renderPlaceholder, renderFrom) : windowedRead && windowPlaceholder ? buildIndexingWindowMessage(attachment, windowPlaceholder, false) : continuing ? buildIndexingContinueMessage(attachment) : buildIndexingUserMessage(
|
|
819
980
|
attachment,
|
|
820
981
|
parsedContent ? { inlineContent: parsedContent } : placeholder ? { inlineContentPlaceholder: placeholder } : pagedRead ? { pagedRead: true } : void 0
|
|
821
982
|
);
|
|
@@ -843,6 +1004,8 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
843
1004
|
model: resolvedModel2,
|
|
844
1005
|
max_output_tokens: MAX_TOKENS,
|
|
845
1006
|
...skapiExtract,
|
|
1007
|
+
...skapiRender,
|
|
1008
|
+
...skapiWindow,
|
|
846
1009
|
input: [
|
|
847
1010
|
{ role: "system", content: systemPrompt },
|
|
848
1011
|
{
|
|
@@ -887,6 +1050,8 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
887
1050
|
model: resolvedModel,
|
|
888
1051
|
max_tokens: MAX_TOKENS,
|
|
889
1052
|
...skapiExtract,
|
|
1053
|
+
...skapiRender,
|
|
1054
|
+
...skapiWindow,
|
|
890
1055
|
system: [
|
|
891
1056
|
{
|
|
892
1057
|
type: "text",
|
|
@@ -958,6 +1123,15 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
958
1123
|
return "";
|
|
959
1124
|
}
|
|
960
1125
|
var BG_INDEXING_QUEUE_SUFFIX = "-bg";
|
|
1126
|
+
function isBgIndexingQueue(queueName) {
|
|
1127
|
+
if (typeof queueName !== "string" || !queueName) return false;
|
|
1128
|
+
const prefix = queueName.split("|")[0];
|
|
1129
|
+
const idx = prefix.lastIndexOf(":");
|
|
1130
|
+
const name = idx === -1 ? prefix : prefix.slice(idx + 1);
|
|
1131
|
+
return name.slice(-BG_INDEXING_QUEUE_SUFFIX.length) === BG_INDEXING_QUEUE_SUFFIX;
|
|
1132
|
+
}
|
|
1133
|
+
var INDEXING_COMPLETE_MARKER = "INDEXING_COMPLETE";
|
|
1134
|
+
var MAX_INDEXING_RESUME_PASSES = 6;
|
|
961
1135
|
async function getChatHistory(params, fetchOptions) {
|
|
962
1136
|
const url = params.platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
|
|
963
1137
|
const p = Object.assign(
|
|
@@ -1085,6 +1259,9 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1085
1259
|
cb(nowMs());
|
|
1086
1260
|
}, 16);
|
|
1087
1261
|
}
|
|
1262
|
+
function isPollStopped(res) {
|
|
1263
|
+
return !!res && typeof res === "object" && res.status === "stopped";
|
|
1264
|
+
}
|
|
1088
1265
|
var ChatSession = class {
|
|
1089
1266
|
constructor(host) {
|
|
1090
1267
|
this.typewriterQueue = Promise.resolve();
|
|
@@ -1108,8 +1285,83 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1108
1285
|
this.pendingAgentRequests = {};
|
|
1109
1286
|
this.aiChatHistoryCache = {};
|
|
1110
1287
|
this.historyItemPolls = /* @__PURE__ */ new Map();
|
|
1288
|
+
this._pauseReasons = /* @__PURE__ */ new Set();
|
|
1289
|
+
this._resuming = false;
|
|
1111
1290
|
this._lidSeq = 0;
|
|
1112
1291
|
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Register a live poll so (a) a remount dedupes against it instead of stacking a
|
|
1294
|
+
* SECOND poll on the same item, and (b) pausePolling can stop it.
|
|
1295
|
+
*
|
|
1296
|
+
* `stop` comes from the SDK and may be absent on an older skapi-js, in which case the
|
|
1297
|
+
* poll simply cannot be stopped and is left running — see pausePolling.
|
|
1298
|
+
*/
|
|
1299
|
+
_trackPoll(id, kind, p) {
|
|
1300
|
+
var stop = p && typeof p.stop === "function" ? p.stop.bind(p) : void 0;
|
|
1301
|
+
if (!stop) {
|
|
1302
|
+
console.debug("[chat-engine] poll has no stop handle", { id, kind });
|
|
1303
|
+
}
|
|
1304
|
+
this.historyItemPolls.set(id, { kind, stop });
|
|
1305
|
+
return p;
|
|
1306
|
+
}
|
|
1307
|
+
/** True while any pause reason is active. */
|
|
1308
|
+
isPollingPaused() {
|
|
1309
|
+
return this._pauseReasons.size > 0;
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Stop BACKGROUND polling until resumePolling. Foreground polls (a reply the user is
|
|
1313
|
+
* waiting on) keep running deliberately: their results must still land in the history
|
|
1314
|
+
* cache so resumePendingRequest can render them on return, otherwise a user who sends
|
|
1315
|
+
* a message then navigates away comes back to a permanently stuck "Thinking...".
|
|
1316
|
+
*
|
|
1317
|
+
* Server-side work is untouched; this only stops asking about it. That is safe for
|
|
1318
|
+
* document indexing because the worker drives that loop itself.
|
|
1319
|
+
*/
|
|
1320
|
+
pausePolling(reason) {
|
|
1321
|
+
this._pauseReasons.add(reason || "paused");
|
|
1322
|
+
var self = this;
|
|
1323
|
+
var stopped = [];
|
|
1324
|
+
this.historyItemPolls.forEach(function(handle, id) {
|
|
1325
|
+
if (!handle || handle.kind !== "bg") return;
|
|
1326
|
+
if (typeof handle.stop !== "function") return;
|
|
1327
|
+
try {
|
|
1328
|
+
handle.stop();
|
|
1329
|
+
} catch (e) {
|
|
1330
|
+
}
|
|
1331
|
+
stopped.push(id);
|
|
1332
|
+
});
|
|
1333
|
+
stopped.forEach(function(id) {
|
|
1334
|
+
self.historyItemPolls.delete(id);
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Lift a pause reason WITHOUT running the reconcile. For a caller that is about to
|
|
1339
|
+
* reload history anyway (a view remounting), letting resumePolling also reconcile
|
|
1340
|
+
* would race that load and can double-attach.
|
|
1341
|
+
*/
|
|
1342
|
+
clearPauseReason(reason) {
|
|
1343
|
+
this._pauseReasons.delete(reason || "paused");
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Clear a pause reason and, once none remain, re-attach polling and reconcile.
|
|
1347
|
+
* Deliberately does NOT touch gateRefreshToken: bumping it would silently discard
|
|
1348
|
+
* the results of anything still in flight across the pause.
|
|
1349
|
+
*/
|
|
1350
|
+
resumePolling(reason) {
|
|
1351
|
+
this._pauseReasons.delete(reason || "paused");
|
|
1352
|
+
if (this._pauseReasons.size > 0 || this._resuming) return Promise.resolve();
|
|
1353
|
+
if (!this.host.isViewMounted || !this.host.isViewMounted()) return Promise.resolve();
|
|
1354
|
+
var self = this;
|
|
1355
|
+
this._resuming = true;
|
|
1356
|
+
return Promise.resolve().then(function() {
|
|
1357
|
+
self.drainBgTaskQueue();
|
|
1358
|
+
return self.loadHistory(false, self.state.gateRefreshToken);
|
|
1359
|
+
}).catch(function(e) {
|
|
1360
|
+
console.error("[chat-engine] resume polling failed", e);
|
|
1361
|
+
}).then(function() {
|
|
1362
|
+
self._resuming = false;
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1113
1365
|
_newLocalId() {
|
|
1114
1366
|
this._lidSeq += 1;
|
|
1115
1367
|
return "lid_" + this._lidSeq;
|
|
@@ -1123,29 +1375,86 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1123
1375
|
var key = this.getHistoryCacheKey();
|
|
1124
1376
|
if (!key) return;
|
|
1125
1377
|
this.aiChatHistoryCache[key] = {
|
|
1126
|
-
messages: this.state.messages.
|
|
1378
|
+
messages: this.state.messages.filter(function(m) {
|
|
1379
|
+
return m._ownerKey === void 0 || m._ownerKey === key;
|
|
1380
|
+
}),
|
|
1127
1381
|
endOfList: this.state.historyEndOfList,
|
|
1128
1382
|
startKeyHistory: this.state.historyStartKeyHistory.slice()
|
|
1129
1383
|
};
|
|
1130
1384
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1385
|
+
/**
|
|
1386
|
+
* Land a resolved reply in the history cache of a chat that is NOT currently
|
|
1387
|
+
* visible, without touching state.messages. Mirrors the cache-only path in
|
|
1388
|
+
* dispatchAgentRequest: REPLACE the trailing pending "Thinking..." bubble
|
|
1389
|
+
* (append only when there is none), and settle the matching pending user
|
|
1390
|
+
* bubble, so the cached copy never keeps a stuck "Thinking..." that a later
|
|
1391
|
+
* cache-first load would re-render forever.
|
|
1392
|
+
*/
|
|
1393
|
+
_applyReplyToCache(key, reply, serverId) {
|
|
1394
|
+
if (!key) return;
|
|
1395
|
+
var existing = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
|
|
1396
|
+
var msgs = existing.messages.slice();
|
|
1397
|
+
var thIdx = -1;
|
|
1398
|
+
for (var i = msgs.length - 1; i >= 0; i--) {
|
|
1399
|
+
var m = msgs[i];
|
|
1400
|
+
if (!m || !m.isPending || m.role !== "assistant" || m.isBackgroundTask) continue;
|
|
1401
|
+
if (serverId && m._serverItemId && m._serverItemId !== serverId) continue;
|
|
1402
|
+
thIdx = i;
|
|
1403
|
+
break;
|
|
1404
|
+
}
|
|
1405
|
+
if (thIdx !== -1) {
|
|
1406
|
+
if (reply._serverItemId === void 0 && msgs[thIdx]._serverItemId !== void 0) reply._serverItemId = msgs[thIdx]._serverItemId;
|
|
1407
|
+
msgs[thIdx] = reply;
|
|
1408
|
+
} else {
|
|
1409
|
+
msgs.push(reply);
|
|
1410
|
+
}
|
|
1411
|
+
for (var j = 0; j < msgs.length; j++) {
|
|
1412
|
+
var u = msgs[j];
|
|
1413
|
+
if (!u || u.role !== "user" || u.isBackgroundTask) continue;
|
|
1414
|
+
if (!(u.isPendingQueued || u.isPendingInProcess || u.isSendingToServer)) continue;
|
|
1415
|
+
if (serverId && u._serverItemId && u._serverItemId !== serverId) continue;
|
|
1416
|
+
var settled = { role: "user", content: u.content };
|
|
1417
|
+
if (u._serverItemId !== void 0) settled._serverItemId = u._serverItemId;
|
|
1418
|
+
if (u._ownerKey !== void 0) settled._ownerKey = u._ownerKey;
|
|
1419
|
+
msgs[j] = settled;
|
|
1420
|
+
break;
|
|
1421
|
+
}
|
|
1422
|
+
this.aiChatHistoryCache[key] = {
|
|
1423
|
+
messages: msgs,
|
|
1424
|
+
endOfList: existing.endOfList,
|
|
1425
|
+
startKeyHistory: existing.startKeyHistory
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* serviceId/owner are passed explicitly by every caller: a request can be
|
|
1430
|
+
* dispatched after the user moved to another project, and re-reading the live
|
|
1431
|
+
* identity here would silently send the turn to THAT project instead of the
|
|
1432
|
+
* one it was composed for. Falls back to the live read only when a caller
|
|
1433
|
+
* omits them.
|
|
1434
|
+
*/
|
|
1435
|
+
_callProviderFor(platform, prompt, messages, system, model, userId, extractContent, fileUrls, serviceId, owner) {
|
|
1436
|
+
if (serviceId === void 0 || owner === void 0) {
|
|
1437
|
+
var id = this.host.getIdentity();
|
|
1438
|
+
if (serviceId === void 0) serviceId = id.serviceId;
|
|
1439
|
+
if (owner === void 0) owner = id.owner;
|
|
1440
|
+
}
|
|
1441
|
+
return platform === "openai" ? callOpenAIWithPublicMcp(prompt, serviceId, owner, messages, system, model, userId, extractContent, fileUrls) : callClaudeWithPublicMcp(prompt, serviceId, owner, messages, system, model, userId, extractContent, fileUrls);
|
|
1134
1442
|
}
|
|
1135
1443
|
dispatchAgentRequest(params) {
|
|
1136
1444
|
var self = this;
|
|
1137
1445
|
var dispatchItemId;
|
|
1138
1446
|
var sendAndPoll = function() {
|
|
1139
1447
|
return Promise.resolve(
|
|
1140
|
-
self._callProviderFor(params.aiPlatform, params.text, params.boundedMessages, params.systemPrompt, params.aiModel, params.userId, params.extractContent, params.fileUrls)
|
|
1448
|
+
self._callProviderFor(params.aiPlatform, params.text, params.boundedMessages, params.systemPrompt, params.aiModel, params.userId, params.extractContent, params.fileUrls, params.serviceId, params.owner)
|
|
1141
1449
|
).then(function(initial) {
|
|
1142
1450
|
if (initial && initial.poll && (initial.status === "pending" || initial.status === "running")) {
|
|
1143
1451
|
if (initial.id) {
|
|
1144
1452
|
if (dispatchItemId && dispatchItemId !== initial.id) self.historyItemPolls.delete(dispatchItemId);
|
|
1145
1453
|
dispatchItemId = initial.id;
|
|
1146
|
-
self.historyItemPolls.set(initial.id, true);
|
|
1147
1454
|
}
|
|
1148
|
-
|
|
1455
|
+
var dp = initial.poll({ latency: POLL_INTERVAL });
|
|
1456
|
+
if (initial.id) self._trackPoll(initial.id, "fg", dp);
|
|
1457
|
+
return dp;
|
|
1149
1458
|
}
|
|
1150
1459
|
return initial;
|
|
1151
1460
|
});
|
|
@@ -1198,20 +1507,57 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1198
1507
|
// composed = clean display text; composedForLlm carries office-extraction
|
|
1199
1508
|
// placeholders for the provider only. useBgQueue routes a post-attachment turn
|
|
1200
1509
|
// onto the "-bg" queue so it runs after indexing.
|
|
1201
|
-
dispatchComposedMessage(composed, useBgQueue, composedForLlm, extractContent, fileUrls) {
|
|
1510
|
+
dispatchComposedMessage(composed, useBgQueue, composedForLlm, extractContent, fileUrls, pinned) {
|
|
1202
1511
|
var self = this;
|
|
1203
1512
|
if (!composed) return;
|
|
1204
|
-
var id = this.host.getIdentity();
|
|
1513
|
+
var id = pinned ? pinned.identity : this.host.getIdentity();
|
|
1205
1514
|
if (id.platform === "none") return;
|
|
1206
1515
|
var llmComposed = composedForLlm || composed;
|
|
1207
|
-
var
|
|
1516
|
+
var key = !id.serviceId ? "" : id.serviceId + "#" + id.platform;
|
|
1517
|
+
var offChat = !!key && key !== this.getHistoryCacheKey();
|
|
1518
|
+
var isQueuedSend = !offChat && (useBgQueue || this.state.sending || this.state.messages.some(function(m) {
|
|
1208
1519
|
return (m.isPending || m.isPendingQueued) && !m.isBackgroundTask && !m._useBgQueue;
|
|
1209
|
-
});
|
|
1520
|
+
}));
|
|
1210
1521
|
var aiPlatform = id.platform;
|
|
1211
1522
|
var aiModel = id.model || void 0;
|
|
1212
|
-
var systemPrompt = this.host.buildSystemPrompt();
|
|
1523
|
+
var systemPrompt = pinned ? pinned.systemPrompt : this.host.buildSystemPrompt();
|
|
1213
1524
|
var userId = id.userId || id.serviceId;
|
|
1214
1525
|
var chatQueue = useBgQueue ? userId + BG_INDEXING_QUEUE_SUFFIX : userId;
|
|
1526
|
+
if (offChat) {
|
|
1527
|
+
var offHistory = (this.aiChatHistoryCache[key] ? this.aiChatHistoryCache[key].messages : []).filter(function(m) {
|
|
1528
|
+
return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask;
|
|
1529
|
+
});
|
|
1530
|
+
var offBounded = buildBoundedChatMessages({
|
|
1531
|
+
platform: aiPlatform,
|
|
1532
|
+
model: aiModel,
|
|
1533
|
+
systemPrompt,
|
|
1534
|
+
serviceId: id.serviceId,
|
|
1535
|
+
history: offHistory.concat([{ role: "user", content: llmComposed }])
|
|
1536
|
+
});
|
|
1537
|
+
var offExisting = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
|
|
1538
|
+
this.aiChatHistoryCache[key] = {
|
|
1539
|
+
messages: offExisting.messages.concat([
|
|
1540
|
+
{ role: "user", content: composed, _ownerKey: key },
|
|
1541
|
+
{ role: "assistant", content: "", isPending: true, isPendingInProcess: true, _ownerKey: key }
|
|
1542
|
+
]),
|
|
1543
|
+
endOfList: offExisting.endOfList,
|
|
1544
|
+
startKeyHistory: offExisting.startKeyHistory
|
|
1545
|
+
};
|
|
1546
|
+
this.dispatchAgentRequest({
|
|
1547
|
+
key,
|
|
1548
|
+
serviceId: id.serviceId,
|
|
1549
|
+
owner: id.owner,
|
|
1550
|
+
aiPlatform,
|
|
1551
|
+
aiModel,
|
|
1552
|
+
systemPrompt,
|
|
1553
|
+
text: composed,
|
|
1554
|
+
boundedMessages: offBounded.messages,
|
|
1555
|
+
userId: chatQueue,
|
|
1556
|
+
extractContent,
|
|
1557
|
+
fileUrls
|
|
1558
|
+
});
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1215
1561
|
if (isQueuedSend) {
|
|
1216
1562
|
var resolvedHistory = this.state.messages.filter(function(m) {
|
|
1217
1563
|
return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask;
|
|
@@ -1224,15 +1570,16 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1224
1570
|
history: resolvedHistory.concat([{ role: "user", content: llmComposed }])
|
|
1225
1571
|
});
|
|
1226
1572
|
var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true };
|
|
1573
|
+
if (key) queuedBubble._ownerKey = key;
|
|
1227
1574
|
if (useBgQueue) queuedBubble._useBgQueue = true;
|
|
1228
1575
|
this.state.messages.push(queuedBubble);
|
|
1229
1576
|
this.host.notify();
|
|
1230
1577
|
this.updateHistoryCache();
|
|
1231
1578
|
this.host.scrollToBottom(true);
|
|
1232
|
-
var capturedComposed = composed, capturedPlatform = aiPlatform;
|
|
1233
|
-
Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent, fileUrls)).then(function(result) {
|
|
1234
|
-
var sendingIdx = self.state.messages.findIndex(function(m) {
|
|
1235
|
-
return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user";
|
|
1579
|
+
var capturedComposed = composed, capturedPlatform = aiPlatform, capturedKey = key;
|
|
1580
|
+
Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent, fileUrls, id.serviceId, id.owner)).then(function(result) {
|
|
1581
|
+
var sendingIdx = self.getHistoryCacheKey() !== capturedKey ? -1 : self.state.messages.findIndex(function(m) {
|
|
1582
|
+
return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user" && (m._ownerKey === void 0 || m._ownerKey === capturedKey);
|
|
1236
1583
|
});
|
|
1237
1584
|
var serverId = result && typeof result.id === "string" ? result.id : void 0;
|
|
1238
1585
|
if (sendingIdx >= 0) {
|
|
@@ -1242,26 +1589,27 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1242
1589
|
self.host.notify();
|
|
1243
1590
|
}
|
|
1244
1591
|
if (result && result.poll && (result.status === "pending" || result.status === "running")) {
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1592
|
+
var qp = result.poll({ latency: POLL_INTERVAL });
|
|
1593
|
+
if (serverId) self._trackPoll(serverId, "fg", qp);
|
|
1594
|
+
return qp.then(function(res) {
|
|
1595
|
+
if (isPollStopped(res)) return;
|
|
1596
|
+
return self.onQueuedSendResponse(capturedComposed, res, capturedPlatform, serverId, capturedKey);
|
|
1248
1597
|
}).catch(function(err) {
|
|
1249
|
-
return self.onQueuedSendError(capturedComposed, err, serverId);
|
|
1598
|
+
return self.onQueuedSendError(capturedComposed, err, serverId, capturedKey);
|
|
1250
1599
|
});
|
|
1251
1600
|
}
|
|
1252
|
-
return self.onQueuedSendResponse(capturedComposed, result, capturedPlatform, serverId);
|
|
1601
|
+
return self.onQueuedSendResponse(capturedComposed, result, capturedPlatform, serverId, capturedKey);
|
|
1253
1602
|
}).catch(function(err) {
|
|
1254
|
-
return self.onQueuedSendError(capturedComposed, err, void 0);
|
|
1603
|
+
return self.onQueuedSendError(capturedComposed, err, void 0, capturedKey);
|
|
1255
1604
|
});
|
|
1256
1605
|
return;
|
|
1257
1606
|
}
|
|
1258
|
-
this.state.messages.push({ role: "user", content: composed });
|
|
1259
|
-
this.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true });
|
|
1607
|
+
this.state.messages.push({ role: "user", content: composed, ...key ? { _ownerKey: key } : {} });
|
|
1608
|
+
this.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true, ...key ? { _ownerKey: key } : {} });
|
|
1260
1609
|
this.host.notify();
|
|
1261
1610
|
this.updateHistoryCache();
|
|
1262
1611
|
this.state.sending = true;
|
|
1263
1612
|
this.host.scrollToBottom(true);
|
|
1264
|
-
var key = this.getHistoryCacheKey();
|
|
1265
1613
|
var historyForLlm = this.state.messages.filter(function(m) {
|
|
1266
1614
|
return !m.isCancelled && !m.isBackgroundTask;
|
|
1267
1615
|
});
|
|
@@ -1295,8 +1643,8 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1295
1643
|
});
|
|
1296
1644
|
Promise.resolve(run).catch(function() {
|
|
1297
1645
|
}).then(function() {
|
|
1298
|
-
if (!(self.host.isViewMounted() && self.getHistoryCacheKey() === key)) return;
|
|
1299
1646
|
self.state.sending = false;
|
|
1647
|
+
if (!(self.host.isViewMounted() && self.getHistoryCacheKey() === key)) return;
|
|
1300
1648
|
return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
|
|
1301
1649
|
self.host.scrollToBottom(true);
|
|
1302
1650
|
});
|
|
@@ -1313,9 +1661,11 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1313
1661
|
var existing = this.state.messages[nextIdx];
|
|
1314
1662
|
var promoted = { role: "user", content: existing.content, isPendingInProcess: true, isBackgroundTask: true };
|
|
1315
1663
|
if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
|
|
1664
|
+
if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
|
|
1316
1665
|
this.state.messages[nextIdx] = promoted;
|
|
1317
1666
|
var placeholder = { role: "assistant", content: "", isPending: true, isPendingInProcess: true, isBackgroundTask: true };
|
|
1318
1667
|
if (existing._serverItemId !== void 0) placeholder._serverItemId = existing._serverItemId;
|
|
1668
|
+
if (existing._ownerKey !== void 0) placeholder._ownerKey = existing._ownerKey;
|
|
1319
1669
|
this.state.messages.splice(nextIdx + 1, 0, placeholder);
|
|
1320
1670
|
this.host.notify();
|
|
1321
1671
|
}
|
|
@@ -1331,14 +1681,20 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1331
1681
|
var promoted = { role: "user", content: existing.content, isPendingInProcess: true };
|
|
1332
1682
|
if (existing.isBackgroundTask) promoted.isBackgroundTask = true;
|
|
1333
1683
|
if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
|
|
1684
|
+
if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
|
|
1334
1685
|
if (existing.isSendingToServer) promoted.isSendingToServer = true;
|
|
1335
1686
|
this.state.messages[nextIdx] = promoted;
|
|
1336
1687
|
var placeholder = { role: "assistant", content: "", isPending: true };
|
|
1337
1688
|
if (existing._serverItemId !== void 0) placeholder._serverItemId = existing._serverItemId;
|
|
1689
|
+
if (existing._ownerKey !== void 0) placeholder._ownerKey = existing._ownerKey;
|
|
1338
1690
|
this.state.messages.splice(nextIdx + 1, 0, placeholder);
|
|
1339
1691
|
this.host.notify();
|
|
1340
1692
|
}
|
|
1341
1693
|
resolveQueuedUserBubble(serverId) {
|
|
1694
|
+
var liveKey = this.getHistoryCacheKey();
|
|
1695
|
+
var isLocal = function(m) {
|
|
1696
|
+
return m._ownerKey === void 0 || m._ownerKey === liveKey;
|
|
1697
|
+
};
|
|
1342
1698
|
var userIdx = -1;
|
|
1343
1699
|
if (serverId) {
|
|
1344
1700
|
userIdx = this.state.messages.findIndex(function(m) {
|
|
@@ -1347,19 +1703,19 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1347
1703
|
}
|
|
1348
1704
|
if (userIdx === -1) {
|
|
1349
1705
|
userIdx = this.state.messages.findIndex(function(m) {
|
|
1350
|
-
return m.isPendingInProcess && m.role === "user" && !m.isBackgroundTask && !m._useBgQueue;
|
|
1706
|
+
return m.isPendingInProcess && m.role === "user" && !m.isBackgroundTask && !m._useBgQueue && isLocal(m);
|
|
1351
1707
|
});
|
|
1352
1708
|
}
|
|
1353
1709
|
if (userIdx === -1) {
|
|
1354
1710
|
userIdx = this.state.messages.findIndex(function(m) {
|
|
1355
|
-
return m.isPendingQueued && m.role === "user" && !m.isBackgroundTask && !m._useBgQueue;
|
|
1711
|
+
return m.isPendingQueued && m.role === "user" && !m.isBackgroundTask && !m._useBgQueue && isLocal(m);
|
|
1356
1712
|
});
|
|
1357
1713
|
}
|
|
1358
1714
|
if (serverId && this.cancelledServerIds.has(serverId)) {
|
|
1359
1715
|
this.cancelledServerIds.delete(serverId);
|
|
1360
1716
|
if (userIdx >= 0) {
|
|
1361
1717
|
var ex = this.state.messages[userIdx];
|
|
1362
|
-
this.state.messages[userIdx] = { role: "user", content: ex.content, isCancelled: true, _serverItemId: ex._serverItemId };
|
|
1718
|
+
this.state.messages[userIdx] = { role: "user", content: ex.content, isCancelled: true, _serverItemId: ex._serverItemId, ...ex._ownerKey !== void 0 ? { _ownerKey: ex._ownerKey } : {} };
|
|
1363
1719
|
var thIdx = this.state.messages.findIndex(function(m, i) {
|
|
1364
1720
|
return i > userIdx && m.isPending && m.role === "assistant" && !m.isBackgroundTask;
|
|
1365
1721
|
});
|
|
@@ -1372,6 +1728,7 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1372
1728
|
var exist = this.state.messages[userIdx];
|
|
1373
1729
|
var repl = { role: "user", content: exist.content };
|
|
1374
1730
|
if (exist._serverItemId !== void 0) repl._serverItemId = exist._serverItemId;
|
|
1731
|
+
if (exist._ownerKey !== void 0) repl._ownerKey = exist._ownerKey;
|
|
1375
1732
|
this.state.messages[userIdx] = repl;
|
|
1376
1733
|
}
|
|
1377
1734
|
var thinkingIdx = userIdx >= 0 ? this.state.messages.findIndex(function(m, i) {
|
|
@@ -1384,8 +1741,14 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1384
1741
|
else if (targetIdx >= 0) this.state.messages.splice(targetIdx, 0, msg);
|
|
1385
1742
|
else this.state.messages.push(msg);
|
|
1386
1743
|
}
|
|
1387
|
-
onQueuedSendResponse(_composed, response, platform, serverId) {
|
|
1744
|
+
onQueuedSendResponse(_composed, response, platform, serverId, ownerKey) {
|
|
1388
1745
|
if (serverId) this.historyItemPolls.delete(serverId);
|
|
1746
|
+
if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
|
|
1747
|
+
var offReply = isErrorResponseBody(response) ? { role: "assistant", content: getErrorMessage(response), isError: true } : { role: "assistant", content: ((platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "").trim() || "No text response received from AI provider." };
|
|
1748
|
+
this._applyReplyToCache(ownerKey, offReply, serverId);
|
|
1749
|
+
if (serverId) this.cancelledServerIds.delete(serverId);
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1389
1752
|
var targetIdx = this.resolveQueuedUserBubble(serverId);
|
|
1390
1753
|
if (targetIdx === void 0) {
|
|
1391
1754
|
this.host.notify();
|
|
@@ -1419,8 +1782,14 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1419
1782
|
this.host.notify();
|
|
1420
1783
|
this.host.scrollToBottom(true);
|
|
1421
1784
|
}
|
|
1422
|
-
onQueuedSendError(_composed, err, serverId) {
|
|
1785
|
+
onQueuedSendError(_composed, err, serverId, ownerKey) {
|
|
1423
1786
|
if (serverId) this.historyItemPolls.delete(serverId);
|
|
1787
|
+
if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
|
|
1788
|
+
var isGone = err && (err.code === "NOT_EXISTS" || err.body && err.body.code === "NOT_EXISTS");
|
|
1789
|
+
this._applyReplyToCache(ownerKey, isGone ? { role: "assistant", content: "Request was cancelled.", isError: true } : { role: "assistant", content: getErrorMessage(err), isError: true }, serverId);
|
|
1790
|
+
if (serverId) this.cancelledServerIds.delete(serverId);
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1424
1793
|
var isNotExists = err && (err.code === "NOT_EXISTS" || err.body && err.body.code === "NOT_EXISTS");
|
|
1425
1794
|
if (isNotExists) {
|
|
1426
1795
|
var userIdx = serverId ? this.state.messages.findIndex(function(m) {
|
|
@@ -1766,6 +2135,7 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1766
2135
|
this.historyItemPolls.delete(itemId);
|
|
1767
2136
|
var isErr = isErrorResponseBody(response);
|
|
1768
2137
|
var answer = isErr ? getErrorMessage(response) : ((platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "").trim();
|
|
2138
|
+
if (!isErr && answer) answer = answer.split(INDEXING_COMPLETE_MARKER).join("").trim();
|
|
1769
2139
|
var idx = this.state.messages.findIndex(function(m) {
|
|
1770
2140
|
return m.isPending && m._serverItemId === itemId;
|
|
1771
2141
|
});
|
|
@@ -1823,24 +2193,33 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1823
2193
|
}
|
|
1824
2194
|
this.bgTaskQueue.forEach(function(entry) {
|
|
1825
2195
|
if (entry.serviceId !== svcId || entry.platform !== plat) return;
|
|
1826
|
-
if (presentIds[entry.id])
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2196
|
+
if (!presentIds[entry.id]) {
|
|
2197
|
+
var isRunning = entry.status === "running";
|
|
2198
|
+
var userBubble = { role: "user", content: self.host.formatIndexingLabel(entry.filename, entry.mime, entry.size, entry.storagePath, entry.isReindex), isBackgroundTask: true, _serverItemId: entry.id };
|
|
2199
|
+
if (isRunning) userBubble.isPendingInProcess = true;
|
|
2200
|
+
else userBubble.isPendingQueued = true;
|
|
2201
|
+
self.state.messages.push(userBubble);
|
|
2202
|
+
if (isRunning) {
|
|
2203
|
+
self.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true, isBackgroundTask: true, _serverItemId: entry.id });
|
|
2204
|
+
}
|
|
2205
|
+
presentIds[entry.id] = true;
|
|
2206
|
+
self.host.notify();
|
|
2207
|
+
self.updateHistoryCache();
|
|
2208
|
+
self.host.scrollToBottom(false);
|
|
1834
2209
|
}
|
|
1835
|
-
|
|
1836
|
-
self.host.notify();
|
|
1837
|
-
self.updateHistoryCache();
|
|
1838
|
-
self.host.scrollToBottom(false);
|
|
1839
|
-
if (!self.historyItemPolls.has(entry.id) && typeof entry.poll === "function") {
|
|
1840
|
-
self.historyItemPolls.set(entry.id, true);
|
|
2210
|
+
if (!self.isPollingPaused() && !self.historyItemPolls.has(entry.id) && typeof entry.poll === "function") {
|
|
1841
2211
|
var capturedId = entry.id, capturedPlat = plat;
|
|
1842
|
-
|
|
2212
|
+
var capturedEntry = entry;
|
|
2213
|
+
var wasStopped = false;
|
|
2214
|
+
var bp = entry.poll({ latency: POLL_INTERVAL });
|
|
2215
|
+
self._trackPoll(entry.id, "bg", bp);
|
|
2216
|
+
bp.then(function(response) {
|
|
2217
|
+
if (isPollStopped(response)) {
|
|
2218
|
+
wasStopped = true;
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
1843
2221
|
self.handleHistoryItemResolution(capturedId, response, capturedPlat);
|
|
2222
|
+
self.maybeResumeIndexing(capturedEntry, response, capturedPlat);
|
|
1844
2223
|
}).catch(function(err) {
|
|
1845
2224
|
self.historyItemPolls.delete(capturedId);
|
|
1846
2225
|
var isNotExists = err && (err.code === "NOT_EXISTS" || err.body && err.body.code === "NOT_EXISTS");
|
|
@@ -1854,6 +2233,7 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1854
2233
|
self.updateHistoryCache();
|
|
1855
2234
|
}
|
|
1856
2235
|
}).then(function() {
|
|
2236
|
+
if (wasStopped) return;
|
|
1857
2237
|
var qi = self.bgTaskQueue.findIndex(function(q) {
|
|
1858
2238
|
return q.id === capturedId;
|
|
1859
2239
|
});
|
|
@@ -1863,6 +2243,69 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1863
2243
|
});
|
|
1864
2244
|
this.promoteNextBgQueuedToRunning();
|
|
1865
2245
|
}
|
|
2246
|
+
// Resume-across-passes: if a background INDEXING task for a paged file (spreadsheet or
|
|
2247
|
+
// text) finished WITHOUT the completion marker, the agent ran out of room before reading
|
|
2248
|
+
// the whole file - dispatch a CONTINUE pass (up to a cap) that resumes from where the
|
|
2249
|
+
// already-saved records leave off. Additive + guarded so it never loops forever and
|
|
2250
|
+
// never breaks the resolution path.
|
|
2251
|
+
//
|
|
2252
|
+
// VISION files (PDFs rendered to page images) are NOT resumed here: the proxy worker
|
|
2253
|
+
// advances their page window itself, off the renderer's true page count. Driving them
|
|
2254
|
+
// from the browser is what used to lose pages on long documents - the chain lived in tab
|
|
2255
|
+
// memory (a reload or a closed tab ended it), and it stopped whenever the model claimed
|
|
2256
|
+
// completion, which on an 88-page file happened at page 15. Continuing to dispatch here
|
|
2257
|
+
// as well would now double-index every window.
|
|
2258
|
+
maybeResumeIndexing(entry, response, platform) {
|
|
2259
|
+
var self = this;
|
|
2260
|
+
try {
|
|
2261
|
+
if (!entry || !entry.storagePath) return;
|
|
2262
|
+
if (!isPagedReadFile(entry.filename, entry.mime)) return;
|
|
2263
|
+
if (isImageVisionFile(entry.filename, entry.mime)) return;
|
|
2264
|
+
if (isErrorResponseBody(response)) return;
|
|
2265
|
+
var answer = (platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "";
|
|
2266
|
+
if (answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1) return;
|
|
2267
|
+
var pass = (entry.resumePass || 0) + 1;
|
|
2268
|
+
if (pass > MAX_INDEXING_RESUME_PASSES) return;
|
|
2269
|
+
var id = this.host.getIdentity();
|
|
2270
|
+
if (!id || id.platform === "none" || id.serviceId !== entry.serviceId) return;
|
|
2271
|
+
notifyAgentContinueIndexing({
|
|
2272
|
+
platform: id.platform,
|
|
2273
|
+
model: id.model,
|
|
2274
|
+
service: id.serviceId,
|
|
2275
|
+
owner: id.owner,
|
|
2276
|
+
userId: id.userId || id.serviceId,
|
|
2277
|
+
serviceName: id.serviceName,
|
|
2278
|
+
serviceDescription: id.serviceDescription,
|
|
2279
|
+
attachment: {
|
|
2280
|
+
name: entry.filename,
|
|
2281
|
+
storagePath: entry.storagePath,
|
|
2282
|
+
mime: entry.mime,
|
|
2283
|
+
size: entry.size,
|
|
2284
|
+
url: ""
|
|
2285
|
+
}
|
|
2286
|
+
}).then(function(ack) {
|
|
2287
|
+
if (ack && typeof ack.id === "string") {
|
|
2288
|
+
self.bgTaskQueue.push({
|
|
2289
|
+
serviceId: id.serviceId,
|
|
2290
|
+
platform: id.platform,
|
|
2291
|
+
id: ack.id,
|
|
2292
|
+
filename: entry.filename,
|
|
2293
|
+
storagePath: entry.storagePath,
|
|
2294
|
+
isReindex: entry.isReindex,
|
|
2295
|
+
mime: entry.mime,
|
|
2296
|
+
size: entry.size,
|
|
2297
|
+
status: ack.status === "running" ? "running" : "pending",
|
|
2298
|
+
poll: ack.poll,
|
|
2299
|
+
resumePass: pass
|
|
2300
|
+
});
|
|
2301
|
+
self.drainBgTaskQueue();
|
|
2302
|
+
}
|
|
2303
|
+
}, function(e) {
|
|
2304
|
+
console.error("[chat-engine] resume-indexing dispatch failed", e);
|
|
2305
|
+
});
|
|
2306
|
+
} catch (e) {
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
1866
2309
|
// --- history fetch + pagination --------------------------------------
|
|
1867
2310
|
// Initial load (fetchMore=false) replaces the list (with in-flight rescue +
|
|
1868
2311
|
// cancelled-merge) and attaches polls to running/pending items; pagination
|
|
@@ -1872,6 +2315,7 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1872
2315
|
loadHistory(fetchMore, token) {
|
|
1873
2316
|
var self = this;
|
|
1874
2317
|
var id = this.host.getIdentity();
|
|
2318
|
+
var loadKey = !id.serviceId || id.platform === "none" ? "" : id.serviceId + "#" + id.platform;
|
|
1875
2319
|
if (token === void 0) token = this.state.gateRefreshToken;
|
|
1876
2320
|
if (this.state.loadingHistory && this.state.historyRequestToken === token || id.platform === "none" || !id.serviceId) {
|
|
1877
2321
|
return Promise.resolve();
|
|
@@ -1894,9 +2338,9 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1894
2338
|
if (token !== self.state.gateRefreshToken) return;
|
|
1895
2339
|
var chatList = history && Array.isArray(history.list) ? history.list : [];
|
|
1896
2340
|
chatList.forEach(function(item) {
|
|
1897
|
-
if (
|
|
2341
|
+
if (isBgIndexingQueue(item.queue_name)) {
|
|
1898
2342
|
var userText = extractLastUserTextFromRequest(item.request_body);
|
|
1899
|
-
if (typeof userText === "string" && userText.indexOf("A new file has just been uploaded") === 0) item._isBgTask = true;
|
|
2343
|
+
if (typeof userText === "string" && (userText.indexOf("A new file has just been uploaded") === 0 || userText.indexOf("CONTINUE indexing") === 0)) item._isBgTask = true;
|
|
1900
2344
|
else item._isOnBgQueue = true;
|
|
1901
2345
|
}
|
|
1902
2346
|
});
|
|
@@ -1928,6 +2372,7 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1928
2372
|
for (var ri = 0; ri < self.state.messages.length; ri++) {
|
|
1929
2373
|
var mm = self.state.messages[ri];
|
|
1930
2374
|
if (mm.isBackgroundTask) continue;
|
|
2375
|
+
if (mm._ownerKey !== void 0 && mm._ownerKey !== loadKey) continue;
|
|
1931
2376
|
if (mm._serverItemId && serverIds[mm._serverItemId]) continue;
|
|
1932
2377
|
if (!mm._serverItemId) {
|
|
1933
2378
|
if (mappedHasPendingAssistant) continue;
|
|
@@ -1972,11 +2417,12 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1972
2417
|
if (!item.poll || !item.id) return;
|
|
1973
2418
|
if (self.historyItemPolls.has(item.id)) return;
|
|
1974
2419
|
if (self.pendingAgentRequests[self.getHistoryCacheKey()] && !item._isBgTask && !item._isOnBgQueue) return;
|
|
1975
|
-
|
|
2420
|
+
if ((item._isBgTask || item._isOnBgQueue) && self.isPollingPaused()) return;
|
|
1976
2421
|
var capturedId = item.id;
|
|
1977
2422
|
var pp = item.poll({
|
|
1978
2423
|
latency: POLL_INTERVAL,
|
|
1979
2424
|
onResponse: function(response) {
|
|
2425
|
+
if (isPollStopped(response)) return;
|
|
1980
2426
|
self.handleHistoryItemResolution(capturedId, response, platform);
|
|
1981
2427
|
},
|
|
1982
2428
|
onError: function(err) {
|
|
@@ -2012,6 +2458,7 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
2012
2458
|
}
|
|
2013
2459
|
}
|
|
2014
2460
|
});
|
|
2461
|
+
self._trackPoll(capturedId, item._isBgTask || item._isOnBgQueue ? "bg" : "fg", pp);
|
|
2015
2462
|
if (pp && pp.catch) pp.catch(function() {
|
|
2016
2463
|
});
|
|
2017
2464
|
});
|
|
@@ -2228,7 +2675,7 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
2228
2675
|
(function() {
|
|
2229
2676
|
var MCP_PROD = "https://mcp.broadwayinc.computer";
|
|
2230
2677
|
var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
|
|
2231
|
-
var BQ_VERSION = "1.
|
|
2678
|
+
var BQ_VERSION = "1.6.1" ;
|
|
2232
2679
|
var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
|
|
2233
2680
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
2234
2681
|
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
@@ -5491,7 +5938,17 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
5491
5938
|
if (!S._visBound && typeof document !== "undefined" && document.addEventListener) {
|
|
5492
5939
|
S._visBound = true;
|
|
5493
5940
|
document.addEventListener("visibilitychange", function() {
|
|
5494
|
-
if (document.visibilityState === "
|
|
5941
|
+
if (document.visibilityState === "hidden") {
|
|
5942
|
+
if (session && session.pausePolling) session.pausePolling("hidden");
|
|
5943
|
+
return;
|
|
5944
|
+
}
|
|
5945
|
+
if (document.visibilityState === "visible") {
|
|
5946
|
+
var refreshed = S.user ? ensureMcpGrantFresh() : null;
|
|
5947
|
+
Promise.resolve(refreshed).catch(function() {
|
|
5948
|
+
}).then(function() {
|
|
5949
|
+
if (session && session.resumePolling) session.resumePolling("hidden");
|
|
5950
|
+
});
|
|
5951
|
+
}
|
|
5495
5952
|
});
|
|
5496
5953
|
}
|
|
5497
5954
|
boot();
|