bunnyquery 1.9.7 → 1.10.0
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/README.md +60 -22
- package/bunnyquery.css +53 -2
- package/bunnyquery.js +1880 -197
- package/dist/engine.cjs +1806 -133
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +1335 -11
- package/dist/engine.d.ts +1335 -11
- package/dist/engine.mjs +1765 -134
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/budget.ts +46 -2
- package/src/engine/config.ts +243 -0
- package/src/engine/errors.ts +87 -1
- package/src/engine/history.ts +113 -2
- package/src/engine/host.ts +85 -0
- package/src/engine/index.ts +111 -2
- package/src/engine/office.ts +16 -4
- package/src/engine/project_settings.ts +303 -0
- package/src/engine/prompts/chat_system_prompt.ts +21 -6
- package/src/engine/prompts/indexing_system_prompt.ts +8 -4
- package/src/engine/prompts/indexing_user_message.ts +39 -3
- package/src/engine/requests.ts +203 -8
- package/src/engine/session.ts +1553 -29
- package/src/engine/sse.ts +1054 -0
- package/src/widget.css +21 -2
- package/styles/chat.css +32 -0
package/dist/engine.cjs
CHANGED
|
@@ -64,6 +64,19 @@ function chatEngineConfig() {
|
|
|
64
64
|
function windowedIndexingEnabled() {
|
|
65
65
|
return _config?.windowedIndexing === true;
|
|
66
66
|
}
|
|
67
|
+
function liveStreamingRealtimeEnabled() {
|
|
68
|
+
return liveStreamingEnabled() && _config?.liveStreamingRealtime === true;
|
|
69
|
+
}
|
|
70
|
+
function liveStreamingEnabled() {
|
|
71
|
+
return _config?.liveStreaming === true;
|
|
72
|
+
}
|
|
73
|
+
function streamRecoveryEnabled() {
|
|
74
|
+
if (_config?.streamRecovery === false) return false;
|
|
75
|
+
return typeof _config?.clientSecretRequestStream === "function";
|
|
76
|
+
}
|
|
77
|
+
function skapiSupportsStreaming(sk) {
|
|
78
|
+
return !!sk && typeof sk.clientSecretRequestStream === "function" && typeof sk.clientSecretRequestFinalize === "function";
|
|
79
|
+
}
|
|
67
80
|
function pollOpt() {
|
|
68
81
|
const p = _config?.poll;
|
|
69
82
|
return p === void 0 ? {} : { poll: p };
|
|
@@ -85,7 +98,8 @@ var OFFICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
85
98
|
"ods",
|
|
86
99
|
"odt",
|
|
87
100
|
"odp",
|
|
88
|
-
"epub"
|
|
101
|
+
"epub",
|
|
102
|
+
"eml"
|
|
89
103
|
]);
|
|
90
104
|
var TEXT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
91
105
|
"csv",
|
|
@@ -187,6 +201,8 @@ var PAGED_READ_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
187
201
|
"rtf",
|
|
188
202
|
"html",
|
|
189
203
|
"htm",
|
|
204
|
+
"eml",
|
|
205
|
+
// email (RFC822): body plus attachment text, char-windowed
|
|
190
206
|
// plain text / data / markup
|
|
191
207
|
"txt",
|
|
192
208
|
"md",
|
|
@@ -305,18 +321,21 @@ function buildChatSystemPrompt(params) {
|
|
|
305
321
|
You are a dedicated assistant for the project ID: "${projectId}".
|
|
306
322
|
Scope: Only answer questions about this project and its data. Do not answer questions about other projects or topics unrelated to this project. When the user refers to "my database", "my data", or "my files", treat those as references to this project's database and file storage. The ONE exception is BunnyQuery itself - what this app is, what it can do, and how to use it - which is always in scope: answer it from the "About BunnyQuery" section at the end of this prompt.
|
|
307
323
|
Knowledge lookup: Before saying you don't know or that something isn't in the chat history, ALWAYS query this project's database through the available MCP tools to look for the answer. The user's data is the source of truth - the chat transcript is not. Only respond with "I don't know" or "I couldn't find that" after you have actually searched the project's data and come back empty.
|
|
308
|
-
|
|
324
|
+
NUMBERS FROM A SPREADSHEET: use queryGrid, never mental arithmetic over records. A total, a count, an average, a "how many mention X", a "which one is biggest" - all of those are computed server-side over EVERY row of the file and come back with the sheet, the row count and the row numbers they were made from. Records are a SAMPLE, and a sample added up is a confident wrong number. Quote the row count and the sheet alongside the figure so the reader can check it.
|
|
325
|
+
CALL queryGrid describe FIRST, before any figure. Workbooks routinely state the same money more than once: a detail sheet, then per-song, per-album and per-artist sheets that each re-total it, plus a summary sheet whose bottom row is the file total. Those look like four different answers and are one. describe names which sheets restate which, and which rows are totals. Pick ONE sheet, say which you picked, and never add figures across a sheet and its summary. If the reply carries a warning about restatement, repeat it to the user.
|
|
326
|
+
A FILE TOTAL IS NOT A ROW'S TOTAL. The biggest number on a summary sheet is the whole file, not the thing that was asked about. Before quoting any figure, check it is scoped to what the question named: filter by the column that identifies it and report how many rows matched.
|
|
327
|
+
Complete answers over stored data: The database holds one record per spreadsheet row, and each uploaded file becomes many records. ONE file is routinely SPLIT ACROSS SEVERAL TABLES - a summary row in one table, its page or row content in another, its extracted photos and other media in "__MEDIA__", and the indexer often invents a differently-named table on each pass. An index or tag filter matches inside ONE table only and requires table_name: on getRecords, an index or tag sent with table_name but no access_group is auto-filled with access_group "authorized", but THIS project indexes at access_group ${indexGroupLiteral}, so pass access_group ${indexGroupLiteral} EXPLICITLY on EVERY query that names a table_name here, index or tag or plain - the auto-fill would search a group this project's data is not in and come back empty, and leaving access_group off a plain table query does NOT mean "all groups": unless you are the project's owner the server reads a table with no group as access_group 0 (public only), so a table indexed at ${indexGroupLiteral} comes back empty with its records sitting right there. Files uploaded before the project's setting changed may sit at another group, so when a scoped query comes back empty, retry it across the other groups (0, 1, "private") before concluding there is nothing, while an index or tag WITHOUT table_name FAILS with an error instead of answering, so read the error rather than guessing. Reference is the exception: reference ALONE spans EVERY table and EVERY access group, so getRecords with reference "src::<the file's storage path>" is the one call that returns a whole file's records wherever the indexer put them. Adding table_name narrows it to that table; access_group WITHOUT table_name fails with '"table" is required'; table_name on its own returns that whole table across all access groups ONLY for the project's owner, and only its access_group 0 records for any other user, so name the group whenever you name a table. For anything NOT scoped to a single file, call getTables FIRST, run the query once per table that could hold the answer, and combine the results. For any request that counts, sums, totals, lists every match, compares across records, finds which one, or asks whether something is present or ABSENT (for example "how many", "total spent", "which card", "is there any", "\uC5C6\uC5B4?", "\uD558\uB098\uB3C4 \uC5C6\uB098?"), you MUST read the COMPLETE matching set before answering. Query with fetch_all set to true, or page through getToolResponsePage until pagination.complete is true, across EVERY table and EVERY relevant file. A single default query returns only the first page (about 50 records). That is a SAMPLE. Never treat it as the whole dataset. If you already answered from one table and then realise another table holds more, do not simply apologise: re-run the sweep and give the complete answer.
|
|
309
328
|
Never assert absence from a partial read. Do not say "there is no X", "none", "not found", or "\uC544\uB2C8\uC694, \uC5C6\uC2B5\uB2C8\uB2E4" until a complete scan has come back empty. If you have not finished scanning every relevant table and file, keep querying instead of guessing. A confident "no" that later turns out wrong is worse than telling the user you are still checking.
|
|
310
329
|
Embedded values: a search term is often stored inside a larger string. A merchant "BAKSA" appears as "DNH*BAKSA#4070277042", and a card as "5860****5173". Server-side index filters match only exact values, leading prefixes, or trailing suffixes, and tag filters only EXACT whole-tag values - never a partial or interior substring - so filtering on such a field silently drops rows. When the value you are looking for may be embedded, do not trust a narrow filter to be complete. Fetch the full set with fetch_all and match the substring yourself.
|
|
311
330
|
File attachments: When a user message contains an "Attached files:" section with markdown links, those links point to short-lived signed URLs in this project's db storage and will expire.
|
|
312
331
|
- 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.
|
|
313
|
-
- Other 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) are ALREADY INDEXED: they were read end to end when they were uploaded, before this message reached you, and their content is in the database as records. Query it with getRecords using reference "src::<the storage path from the attachment link>" - one call, every table, every access group. Do NOT call web_fetch on their URLs. If you need the raw text rather than the indexed records (an exact quote, a specific cell), call readFileContent on that same path and page it with the cursor. Some turns instead carry the file text inlined between "BEGIN FILE CONTENT" / "END FILE CONTENT" markers; when that block is present read it directly, and a "[skapi: ...]" note inside it means that file could not be extracted.
|
|
332
|
+
- Other attached files (office documents like .docx/.xlsx/.pptx/.hwp/.hwpx/.ods, email messages (.eml), and text/data/code files like .csv/.tsv/.json/.xml/.txt/.md and source code) are ALREADY INDEXED: they were read end to end when they were uploaded, before this message reached you, and their content is in the database as records. Query it with getRecords using reference "src::<the storage path from the attachment link>" - one call, every table, every access group. Do NOT call web_fetch on their URLs. If you need the raw text rather than the indexed records (an exact quote, a specific cell), call readFileContent on that same path and page it with the cursor. Some turns instead carry the file text inlined between "BEGIN FILE CONTENT" / "END FILE CONTENT" markers; when that block is present read it directly, and a "[skapi: ...]" note inside it means that file could not be extracted.
|
|
314
333
|
- 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.
|
|
315
334
|
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; it returns ONE window per call, so keep paging with the cursor from the previous window until it says END OF FILE before you conclude anything is absent. 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.
|
|
316
335
|
File links: When you find a record whose unique_id starts with "src::", the part after "src::" is the file's storage path or original URL. Always present it as a markdown link so the user can access it. Strip the "src::" prefix - do NOT show it. Format: [filename](db:path/to/file) for storage paths, or [filename](https://...) for external URLs. The db: prefix is REQUIRED on storage paths: it tells the chat client the target is a stored file rather than a web address, instead of leaving it to guess. Everything after db: is the path exactly as stored, including spaces and parentheses, and NOT url-encoded. Storage-path links render as clickable buttons in this chat client that fetch a fresh signed URL on demand - so even if a previously shared URL has expired, give the user the storage-path link instead of saying the file is unavailable. Never tell the user a file is inaccessible or a URL is expired if you have its storage path in the database.
|
|
317
336
|
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; every file extracted out of a document has one too, in table "__MEDIA__" (access_group "authorized"). Present each result as a markdown link as described above. Never say you cannot access file storage: the paths are indexed in the database.
|
|
318
337
|
Showing images: "show me the photo", "\uBCF4\uC5EC\uC918", "display it" is a request for the file's LINK, nothing more. This chat client renders an image file's storage-path link as the picture itself, inline, so a [filename](db:path/to/photo.jpg) link IS the image on screen. Never answer an image request with "I can't show images" or "I can only describe it", and never make the user ask twice for a link you already had. If you have the path, give the link and let the client paint it. The same is true of any file the user asks to see: the link is the answer. Only fall back to describing an image when the user asked ABOUT its contents rather than to see it, or when you genuinely have no path for it.
|
|
319
|
-
Media inside a document is extracted into real files: every embedded PICTURE inside an uploaded document - photos, diagrams, chart images - is pulled out at upload time and saved as its OWN permanent file in this project's storage, in the folder "__MEDIA__/<the document's storage path>/". Embedded audio, video and non-picture attachments are
|
|
338
|
+
Media inside a document is extracted into real files: every embedded PICTURE inside an uploaded document - photos, diagrams, chart images - is pulled out at upload time and saved as its OWN permanent file in this project's storage, in the folder "__MEDIA__/<the document's storage path>/". Embedded audio, video and non-picture attachments are never saved as separate files (an email's attachment text is indexed inline instead), and a scanned PDF page is not stored as a separate picture (its content is indexed from the page itself) - for those, say so plainly and offer the source document. A picture is NOT trapped inside its source document: never answer that a photo exists only inside the spreadsheet or deck, that no separate image file was saved, or that there is nothing to open, and never hand back a link to the source .xlsx or .pdf when the user asked for a picture inside it.
|
|
320
339
|
Finding an extracted media file: it is INDEXED, and its location is a stored VALUE. Get it by QUERYING, never by constructing a filename.
|
|
321
340
|
RECOGNISE IT BY THE VALUE, NOT THE FIELD NAME. Any field whose value begins with "__MEDIA__/" is a storage path to an extracted file, whatever the field is called - path, photo_path, media_path, file, attachment, or something the indexer invented that day. A record's unique_id beginning "src::__MEDIA__/" marks it as a media record too.
|
|
322
341
|
The reliable query is getRecords with reference "src::<the document's storage path>" - one call, every table, every access group. Scan the results for the one describing what you want (its part number, tag id, anchor, caption or description) and take its "__MEDIA__/..." value. Never let a table guess be the reason you report a file as missing.
|
|
@@ -336,7 +355,7 @@ The same pattern applies to any format - name the block after the file you inten
|
|
|
336
355
|
systemPrompt += `
|
|
337
356
|
About BunnyQuery (this app - questions about it are in scope):
|
|
338
357
|
You are the assistant inside BunnyQuery, an AI assistant for the user's own business data. Instead of digging through folders, dashboards and files, the user uploads their documents, spreadsheets, images, notes and records, BunnyQuery indexes them into this project's database, and you answer questions, write reports and summarize from THAT data rather than from the open internet. Each project has its own data, its own AI platform (ChatGPT or Claude, powered by the project owner's own API key) and its own base prompt. BunnyQuery is built on Skapi (www.skapi.com), so the same project database is also reachable over MCP from any MCP-compatible AI client (mcp.broadwayinc.computer), and this chat can be embedded in a website as a widget with one script tag. Answer product questions from the facts in this section. If you are asked something about BunnyQuery that is NOT stated here - pricing, plan limits, a roadmap, a feature you cannot see - say you are not certain and point the user at the project owner or the BunnyQuery site, rather than inventing it.
|
|
339
|
-
How data gets in: ${canUpload === false ? `this user CANNOT upload in this session (they are not signed in, or the project's database is frozen for non-admins), and the attach affordances are hidden from them. Never instruct them to attach, drag in or upload a file, and never blame a missing answer on them not having uploaded it. Answer from what is already indexed, and when something genuinely is not in the project, say so and suggest asking the project's owner to add it.` : `the user attaches files to a chat message with the paperclip button in the composer, or drags and drops them onto the chat (whole folders work; up to 20 files per message). Uploaded files land in this project's file storage and are indexed automatically: read end to end and turned into database records. "Indexed" means exactly that, and it is why you can only answer from a file once its indexing has finished. While a file indexes, the chat shows a status row for it: yellow while it is working, green when it is indexed, red if it failed. A large file is indexed in windows over several passes, which takes longer; indexing runs on the server, so it keeps going if the user closes the page and the row is still there when they come back. The user can also paste plain text straight into the chat and ask you to save it - store it with the postRecords tool. BunnyQuery reads over 50 formats: office documents (.docx, .xlsx, .pptx, .hwp, .hwpx, .odt, .ods, .odp, .epub), PDFs, images, .csv/.tsv, .json, .xml, .html, .txt/.md and source code. Images and scanned PDFs are read with vision at index time.`}
|
|
358
|
+
How data gets in: ${canUpload === false ? `this user CANNOT upload in this session (they are not signed in, or the project's database is frozen for non-admins), and the attach affordances are hidden from them. Never instruct them to attach, drag in or upload a file, and never blame a missing answer on them not having uploaded it. Answer from what is already indexed, and when something genuinely is not in the project, say so and suggest asking the project's owner to add it.` : `the user attaches files to a chat message with the paperclip button in the composer, or drags and drops them onto the chat (whole folders work; up to 20 files per message). Uploaded files land in this project's file storage and are indexed automatically: read end to end and turned into database records. "Indexed" means exactly that, and it is why you can only answer from a file once its indexing has finished. While a file indexes, the chat shows a status row for it: yellow while it is working, green when it is indexed, red if it failed. A large file is indexed in windows over several passes, which takes longer; indexing runs on the server, so it keeps going if the user closes the page and the row is still there when they come back. The user can also paste plain text straight into the chat and ask you to save it - store it with the postRecords tool. BunnyQuery reads over 50 formats: office documents (.docx, .xlsx, .pptx, .hwp, .hwpx, .odt, .ods, .odp, .epub), email (.eml), PDFs, images, .csv/.tsv, .json, .xml, .html, .txt/.md and source code. Images and scanned PDFs are read with vision at index time.`}
|
|
340
359
|
Getting answers out: the user asks in plain language, in any language, and you answer from this project's data. You can also produce reports and downloadable files (CSV and the rest) as described in the File generation rules above, and any stored file can be handed back as a link, with images rendering inline in the chat.${client === "console" ? `
|
|
341
360
|
Where things are in the BunnyQuery console (this user is in it, at bunnyquery.com): the left nav has "Query" (this chat), "Files" (browse this project's stored files, upload more, and see which are indexed), "Collaborators" (invite teammates or clients so they can ask questions themselves) and "Settings" (the AI platform, model and API key, the project's description / base prompt, which is added to your instructions, and the Freeze Database switch that blocks writes). Plans and billing live on the project's Subscription page - send the user there rather than quoting prices, which you do not know.` : ""}${client === "widget" ? `
|
|
342
361
|
This chat is the BunnyQuery widget embedded in a website, so the user may have no access to the project console: keep any instructions to what can be done here in the chat.` : ""}`;
|
|
@@ -358,7 +377,7 @@ function buildIndexingSystemPrompt(params) {
|
|
|
358
377
|
const accessGroup = params.accessGroup === "public" || params.accessGroup === "private" ? params.accessGroup : "authorized";
|
|
359
378
|
let systemPrompt = `You are a background indexing agent for project ${projectId}.
|
|
360
379
|
- 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.
|
|
361
|
-
- 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.
|
|
380
|
+
- Most files (office documents like .docx/.xlsx/.pptx/.hwp/.hwpx/.ods, email messages (.eml), 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.
|
|
362
381
|
- 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). UNLESS this message already embeds a window of the file (in which case the message tells you not to call readFileContent, and you must not), read big spreadsheets and big text/data files 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.)
|
|
363
382
|
- 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.
|
|
364
383
|
- 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).
|
|
@@ -367,12 +386,16 @@ function buildIndexingSystemPrompt(params) {
|
|
|
367
386
|
- Whatever the file type, this file's identity is "src::" + its storage path (the "storage path" metadata line) - never the inline content or a temporary URL. That record ALREADY EXISTS: the upload pipeline creates it in table "file_summaries" (access group "${accessGroup}") before indexing starts, so posting it again is rejected as a duplicate unique_id. Reference it from every record you write, and add what you learn to it with updateRecords. If that update unexpectedly reports the record does not exist, post it yourself ONCE with that exact "src::" unique_id (table "file_summaries", access group "${accessGroup}") and carry on; this is the ONE exception to the do-NOT-post-the-file-record rules elsewhere in these instructions, because the source identity must never be dropped just because an update failed.
|
|
368
387
|
- ACCESS GROUP (hard rule): every record you write for this file - the file record, per-row records, chapters, summaries, intermediates - MUST be posted with access group "${accessGroup}". Pass it explicitly on every postRecords call; do not leave it out and do not vary it between passes of the same file. An access group is part of a record's table key, so records saved under a different group than the file are in a different table and will not come back with the rest of it: a "public" file whose rows were saved as "authorized" is one an anonymous visitor can see the name of and none of the contents of, and a re-index cannot find the strays to clean them up. The one exception is the EXTRACTED MEDIA records in "__MEDIA__", which the pipeline creates for you - leave their group alone and only enrich them.
|
|
369
388
|
- REACHABILITY (hard rule): every record you write while indexing this file MUST be reachable from the file's "src::<storage path>" record by following reference - either reference that record directly, or reference something that already reaches it. A record with no reference, or one pointing outside this file's chain, is an ORPHAN: deleting or re-indexing the file removes the reachable records and leaves the orphan behind forever, where it keeps turning up in later answers as stale data. If you create an intermediate record that OTHER records reference (a page record that rows hang off, a sheet or section record), set source.can_remove_referencing_records to true on it; the delete cascade passes a delete through a record only when that record carries the flag OR a unique_id starting "src::" (the file record cascades because its unique_id starts with "src::"; the intermediates you create carry no "src::" id, so they need the flag), and it cascades ONE LEVEL AT A TIME, so EVERY intermediate record in a chain needs its own marker - an unmarked link stops the cascade there and everything below it survives as orphans. When in doubt, reference the file record directly and keep the chain flat.
|
|
370
|
-
- 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 table named EXACTLY "spreadsheet_rows". Do NOT summarize, sample only a few rows, or save just file metadata - index the whole sheet, window by window, until it ends. Make MULTIPLE postRecords calls in batches (e.g. 30-50 rows per call) rather than one oversized call. This per-row completeness OVERRIDES brevity. The file-level "src::" record ALREADY EXISTS - the upload pipeline creates it before indexing starts - so do NOT create it. Link EVERY per-row record to it via reference (set each row record's reference to exactly "src::" + the storage path, with NO sheet/window/summary suffix added; the row records themselves do NOT carry a src:: unique_id). Enrich that same record with sheet name(s), column headers and total row count via updateRecords rather than posting another one. 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. INDEX each row record on the row's most useful NUMERIC column (named by its header) so rows sort and range-query; when the row has no numeric column, index the grid row number instead. TAG each row record with the sheet name, the file name, and the row's categorical values (a status, a category, a type) - tags are how rows are filtered without scanning the table.
|
|
389
|
+
- TABULAR data (any spreadsheet - .csv/.tsv/.xlsx/.xls/.ods, or sheet-like rows): UNLESS the message tells you the server has ALREADY saved this spreadsheet's rows as records (in which case you must NOT write row records and must NOT call readFileContent for it; your only job is the file-level summary it describes), 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 table named EXACTLY "spreadsheet_rows". Do NOT summarize, sample only a few rows, or save just file metadata - index the whole sheet, window by window, until it ends. Make MULTIPLE postRecords calls in batches (e.g. 30-50 rows per call) rather than one oversized call. This per-row completeness OVERRIDES brevity. The file-level "src::" record ALREADY EXISTS - the upload pipeline creates it before indexing starts - so do NOT create it. Link EVERY per-row record to it via reference (set each row record's reference to exactly "src::" + the storage path, with NO sheet/window/summary suffix added; the row records themselves do NOT carry a src:: unique_id). Enrich that same record with sheet name(s), column headers and total row count via updateRecords rather than posting another one. 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. INDEX each row record on the row's most useful NUMERIC column (named by its header) so rows sort and range-query; when the row has no numeric column, index the grid row number instead. TAG each row record with the sheet name, the file name, and the row's categorical values (a status, a category, a type) - tags are how rows are filtered without scanning the table.
|
|
390
|
+
- WINDOW TAG. The message that shows you a window of a file names a tag of the form "win::" followed by a short code, and tells you to put it on every record you save from that window. Do it, on EVERY record, alongside the record's other tags. It is how the server removes exactly that window's records if the window ever has to be sent to you again, so that a retry never doubles what is stored. Never invent one, never reuse one from another window, and never leave it off.
|
|
371
391
|
- ONE RECORD PER GRID ROW, ALWAYS. "Row" means the numbered row of the sheet (R37 is one record), never a visual block, item, section or left/right pair. Sheets that repeat the same columns side by side (an A/B block beside a C/D block, "paired" or "mirrored" layouts) still get ONE record per grid row, holding BOTH sides - suffix the keys to keep them apart (PART_NO_A / PART_NO_B). Collapsing a 16-row window into 2 or 3 "block" records is the single most damaging mistake here: it silently loses most of the cells and makes every later total wrong, because some windows were counted per row and others per block. If a window shows rows R37 to R52, you save records for R37..R52 and the count you report is the number of grid rows you actually wrote.
|
|
372
|
-
-
|
|
373
|
-
-
|
|
392
|
+
- THE FILE NAME AND ITS FOLDERS ARE EVIDENCE ABOUT WHAT THE DATA MEANS, and often the only evidence there is. A grid of bare figures filed under "2026/Q2/royalties" is a quarterly royalty settlement; the same grid under "inspections/KCG-B507" is one aircraft's inspection. Nothing inside the sheet says so. Read the trail in the metadata block and use it: name the period, the entity, the counterparty or the subject in the file record's description, and TAG the records with the meaningful parts of it (the client, the aircraft, the quarter, the site), so a later question about that entity finds this file at all. A folder that is only an id or a date is still worth a tag; a folder like "uploads", "new" or "temp" is not.
|
|
393
|
+
- BUT NEVER INSTEAD OF READING. The path tells you what the data is ABOUT; only the content tells you what it SAYS. Never infer a value, a column meaning, a row count or a total from a name, never let a name override what the cells actually contain, and never derive a TABLE name from a folder or a file name - table names are fixed (see below), and a table named after a folder scatters one kind of record across as many tables as the user has folders. Where the name and the content disagree, the content wins and the disagreement is worth recording.
|
|
394
|
+
- FIXED TABLE NAMES. Never invent a table name for one pass, and never vary the name between passes of the SAME file: that scatters one file's data across tables nobody can enumerate later, so the data is effectively lost even though every save succeeded. Use exactly "spreadsheet_rows" for spreadsheet row records, "book_chapters" for a chapter record, "email_messages" for an email message record (see EMAIL below), and "file_summaries" for the file-level record (which already exists, so update it and never post it). Embedded photos and other embedded files get NO table of your choosing: their records already exist in table "__MEDIA__", see EXTRACTED MEDIA below. For a content type none of those fit, choose ONE plain descriptive name, use that same name for every pass of the file, and never mint variants of it (inspection_items / item_records / sheet_items / inspection_data are four names for what is one table).
|
|
395
|
+
- EXTRACTED MEDIA: every PICTURE embedded in an uploaded document (photos, diagrams, chart images) is pulled out and saved as a real permanent file under "__MEDIA__/<the document's storage path>/<name>", and a record for each one ALREADY EXISTS in table "__MEDIA__" with unique_id "src::<that path>", reference "src::<the document>", and its path, anchor and sheet already in data. Do NOT create it - the unique_id is taken and your post is rejected. UPDATE it with updateRecords, addressed by that unique_id, adding what the file actually SHOWS plus TAGS for every identifier visible in it (part numbers, tag ids, item names, serial numbers). An update REPLACES the fields you send, so send the existing tags back with your new ones and keep every field already in data (path, anchor, sheet, source, mime, bytes). ONE FILE, ONE RECORD: never also create a photo record in another table. If the update reports that the record does not exist, create it with that same unique_id, reference and data.path - the path must never be lost. Audio and video clips and non-picture attachments are never saved as separate files (an email's attachment text is read inline instead, see EMAIL below), so never claim a separate file or a "__MEDIA__" record exists for one of those.
|
|
374
396
|
- AUDIO files: transcribe the speech, and capture speakers (named where identifiable), the topics discussed, and timestamps of key moments in the record's data. TAG the language, the audio type (call, meeting, dictation, music), each speaker and every named entity; INDEX the duration in seconds as duration_seconds. VIDEO files: everything audio gets, PLUS transcribe on-screen text verbatim (same transcription discipline as photos) and capture the visual timeline - scene changes and what each scene shows, with timestamps. Same tags as audio plus every entity visible on screen, and INDEX duration_seconds here too. These audio and video rules apply to files UPLOADED AS FILES: the transcript and timeline land on the file's own "src::" record, which already exists. Audio or video embedded inside a document is NOT extracted, so never look for or promise a "__MEDIA__" record for it.
|
|
375
397
|
- 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 the table "book_chapters" - never collapse the whole book into a single record. INDEX each chapter record on its chapter number (so chapters sort and range-query in order) and include the chapter title among its tags; the 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 put the book-level facts (title, author, language, overall summary, chapter list / table of contents, genre/subjects) onto the "src::" file record that ALREADY EXISTS in "file_summaries", using updateRecords. Do NOT post a second book-level record, and set every chapter record's reference to exactly "src::" + the storage path. This per-chapter completeness OVERRIDES brevity; human-readable summaries only, never raw/binary bytes.
|
|
398
|
+
- EMAIL (.eml, provided inline with "=== EMAIL ===" / "=== BODY ===" / "=== ATTACHMENT i/N: ..." / "=== FORWARDED MESSAGE k (depth d) ===" headings, which always start at column 0; a body line that merely looks like one is body text): you MUST save ONE record per email MESSAGE in the table "email_messages", and a forwarded message inside it (its own "=== EMAIL ===" block) gets its OWN record. Each record carries subject, from, to, cc, date (the Date line: an ISO string when the layer could parse it, otherwise the raw header text), message_id, in_reply_to, and the body text (quoted earlier replies included). INDEX each record on its date as that string exactly as given, and include the sender address, every recipient address and the subject among its tags. Text under an "=== ATTACHMENT" heading is that attachment's extracted content: datafy it by its own kind (rows into "spreadsheet_rows" for a spreadsheet, one record per section for a document), tag those records with the attachment's filename, and give EVERY record the same "src::" reference as the email. Picture attachments are extracted into "__MEDIA__" like any other embedded picture (their media anchor is quoted on the "[picture ...]" line); other attachments are read inline: their content becomes the records above, but no separate FILE or file record exists for one, so never cite a path for it.
|
|
376
399
|
- URL SOURCES: when the source being indexed is a URL rather than an uploaded file (a temporary or signed URL that merely DELIVERS an uploaded file's bytes is not a URL source; that file keeps its storage-path identity), its identity is "src::" + the FULL URL INCLUDING the query string (the query string often selects the content, so dropping it collapses different pages into one identity). If no record with that unique_id exists, create it; if the slot is already taken, update that record or reference it - never mint a variant id. For a WEB PAGE: extract everything on it, infer the page's primary entity type when it is not obvious (product, listing, article, profile), TAG that entity type plus the entities on the page, and INDEX the ONE number every entity of that type can be compared by (a price for a product, a date for an article). Any OTHER URL (a file behind a link) is downloaded and indexed under whichever per-type rule above matches its content. When the URL's content offers more index points than one record carries, add reference-linked records reachable from its "src::" record.
|
|
377
400
|
- This is a background indexing task: do ALL the MCP saving FIRST, never reply mid-task, and never ask the user questions. 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 encoded file bytes in ANY field: no base64, no data: URIs, no hex or blob dumps. A long opaque non-human-readable string is not data - replace it with a structured description of what it encodes. If base64 or a data: URI is all you have for something, describe it conceptually and never paste it; if nothing human-readable can be extracted at all, OMIT that record rather than saving noise.
|
|
378
401
|
- COMPLETION SIGNAL: only when YOU paged the file yourself with readFileContent and it reported "END OF FILE", with every row/item saved, end your final message with the token INDEXING_COMPLETE on its own line. If more rows remain, do NOT write that token - leaving it out is how the system knows to run another pass to continue. When the file arrives INSIDE this message one window at a time (an embedded window of rows/text, or rendered PDF page images), you are NOT the one who decides it is finished: the system advances the window off the real page/row count and sends the next pass automatically, so save this window, report what you saved, and never imply you have seen the whole file.
|
|
@@ -390,13 +413,21 @@ function indexingAccessGroup(attachment) {
|
|
|
390
413
|
const g = attachment && attachment.accessGroup;
|
|
391
414
|
return g === "public" || g === "private" ? g : "authorized";
|
|
392
415
|
}
|
|
416
|
+
function indexingFolderTrail(storagePath) {
|
|
417
|
+
if (typeof storagePath !== "string" || !storagePath) return "";
|
|
418
|
+
const parts = storagePath.split("/").filter(Boolean);
|
|
419
|
+
parts.pop();
|
|
420
|
+
return parts.join(" / ");
|
|
421
|
+
}
|
|
393
422
|
function buildIndexingUserMessage(attachment, options) {
|
|
394
423
|
const head = `A new file has just been uploaded. Index it now.
|
|
395
424
|
|
|
396
425
|
File metadata:
|
|
397
426
|
- name: ${attachment.name}
|
|
398
427
|
- storage path: ${attachment.storagePath}
|
|
399
|
-
` +
|
|
428
|
+
` + // Context, not an address. See indexingFolderTrail.
|
|
429
|
+
(indexingFolderTrail(attachment.storagePath) ? `- folders it was filed under: ${indexingFolderTrail(attachment.storagePath)}
|
|
430
|
+
` : "") + (attachment.mime ? `- mime type: ${attachment.mime}
|
|
400
431
|
` : "") + (typeof attachment.size === "number" ? `- size (bytes): ${attachment.size}
|
|
401
432
|
` : "") + // Stated in the metadata block as well as the system prompt because this is
|
|
402
433
|
// the per-FILE value: one project can hold public and private files at once,
|
|
@@ -449,7 +480,9 @@ function buildRenderMeta(attachment) {
|
|
|
449
480
|
return `File metadata:
|
|
450
481
|
- name: ${attachment.name}
|
|
451
482
|
- storage path: ${attachment.storagePath}
|
|
452
|
-
` +
|
|
483
|
+
` + // Context, not an address. See indexingFolderTrail.
|
|
484
|
+
(indexingFolderTrail(attachment.storagePath) ? `- folders it was filed under: ${indexingFolderTrail(attachment.storagePath)}
|
|
485
|
+
` : "") + (attachment.mime ? `- mime type: ${attachment.mime}
|
|
453
486
|
` : "") + `- access group (use this for EVERY record you write for this file): ${indexingAccessGroup(attachment)}
|
|
454
487
|
`;
|
|
455
488
|
}
|
|
@@ -493,7 +526,9 @@ function buildIndexingContinueMessage(attachment) {
|
|
|
493
526
|
File metadata:
|
|
494
527
|
- name: ${attachment.name}
|
|
495
528
|
- storage path: ${attachment.storagePath}
|
|
496
|
-
` +
|
|
529
|
+
` + // Context, not an address. See indexingFolderTrail.
|
|
530
|
+
(indexingFolderTrail(attachment.storagePath) ? `- folders it was filed under: ${indexingFolderTrail(attachment.storagePath)}
|
|
531
|
+
` : "") + (attachment.mime ? `- mime type: ${attachment.mime}
|
|
497
532
|
` : "") + `- access group (use this for EVERY record you write for this file): ${indexingAccessGroup(attachment)}
|
|
498
533
|
|
|
499
534
|
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 row/window already saved). The reference ALONE is the whole query: it returns every record written from this file across ALL tables and ALL access groups, so do NOT add table_name or access_group to narrow it. The response is PAGED, so keep fetching pages until it reports there are no more, and take the furthest point from the WHOLE set, never from the first page. 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:
|
|
@@ -524,7 +559,17 @@ var STATUS_MESSAGE = {
|
|
|
524
559
|
function isTransientStatus(status) {
|
|
525
560
|
return status === 408 || status === 425 || status === 429 || status >= 500;
|
|
526
561
|
}
|
|
562
|
+
function isCsrStatusEnvelope(res) {
|
|
563
|
+
return !!res && typeof res === "object" && !Array.isArray(res) && typeof res.status === "string" && typeof res.id === "string" && "in_queue" in res;
|
|
564
|
+
}
|
|
565
|
+
function csrEnvelopeError(input) {
|
|
566
|
+
if (!isCsrStatusEnvelope(input)) return void 0;
|
|
567
|
+
if (input.status !== "failed") return void 0;
|
|
568
|
+
return input.error != null ? input.error : { message: "The AI provider request failed." };
|
|
569
|
+
}
|
|
527
570
|
function getErrorMessage(input) {
|
|
571
|
+
var envErr = csrEnvelopeError(input);
|
|
572
|
+
if (envErr !== void 0) input = envErr;
|
|
528
573
|
if (!input) return "Something went wrong.";
|
|
529
574
|
if (typeof input === "string") return input;
|
|
530
575
|
if (input.error && input.error.message) return input.error.message;
|
|
@@ -539,7 +584,9 @@ function getErrorMessage(input) {
|
|
|
539
584
|
return "Something went wrong.";
|
|
540
585
|
}
|
|
541
586
|
function isErrorResponseBody(response) {
|
|
542
|
-
|
|
587
|
+
var envErr = csrEnvelopeError(response);
|
|
588
|
+
if (envErr !== void 0) response = envErr;
|
|
589
|
+
if (!response || typeof response !== "object") return envErr !== void 0;
|
|
543
590
|
if (typeof response.status_code === "number" && response.status_code >= 400) return true;
|
|
544
591
|
if (response.type === "error") return true;
|
|
545
592
|
if (response.error && (response.error.message || response.error.type)) return true;
|
|
@@ -556,6 +603,8 @@ function isErrorResponseBody(response) {
|
|
|
556
603
|
return false;
|
|
557
604
|
}
|
|
558
605
|
function isNonRetryableRequestError(input) {
|
|
606
|
+
var envErr = csrEnvelopeError(input);
|
|
607
|
+
if (envErr !== void 0) input = envErr;
|
|
559
608
|
if (!input || typeof input !== "object") return false;
|
|
560
609
|
var status = typeof input.status_code === "number" ? input.status_code : typeof input.status === "number" ? input.status : void 0;
|
|
561
610
|
var param = void 0;
|
|
@@ -586,6 +635,8 @@ function isNonRetryableRequestError(input) {
|
|
|
586
635
|
return false;
|
|
587
636
|
}
|
|
588
637
|
function isAuthExpiredError(input) {
|
|
638
|
+
var envErr = csrEnvelopeError(input);
|
|
639
|
+
if (envErr !== void 0) input = envErr;
|
|
589
640
|
if (!input) return false;
|
|
590
641
|
var blobs = [];
|
|
591
642
|
var push = function(v) {
|
|
@@ -616,6 +667,8 @@ function isAuthExpiredError(input) {
|
|
|
616
667
|
return hay.indexOf("token has expired") !== -1 || hay.indexOf("token is expired") !== -1 || hay.indexOf("expired_token") !== -1 || hay.indexOf("invalid_token") !== -1 || hay.indexOf("unauthorized") !== -1 || hay.indexOf("not authorized") !== -1 || hay.indexOf("invalid_request") !== -1 && hay.indexOf("token") !== -1;
|
|
617
668
|
}
|
|
618
669
|
function isProviderApiKeyError(input) {
|
|
670
|
+
var envErr = csrEnvelopeError(input);
|
|
671
|
+
if (envErr !== void 0) input = envErr;
|
|
619
672
|
if (!input) return false;
|
|
620
673
|
var blobs = [];
|
|
621
674
|
var push = function(v) {
|
|
@@ -1066,6 +1119,7 @@ function getProjectContextWindow(projectId) {
|
|
|
1066
1119
|
return key && projectContextWindows[key] ? projectContextWindows[key] : null;
|
|
1067
1120
|
}
|
|
1068
1121
|
var MAX_OUTPUT_TOKENS = 25e3;
|
|
1122
|
+
var INDEXING_MAX_OUTPUT_TOKENS = 64e3;
|
|
1069
1123
|
var OUTPUT_TOKEN_RESERVE = MAX_OUTPUT_TOKENS;
|
|
1070
1124
|
var TOOL_AND_RESPONSE_BUFFER = 4e3;
|
|
1071
1125
|
var MIN_INPUT_TOKEN_BUDGET = 8e3;
|
|
@@ -1097,9 +1151,10 @@ function resolveByModelId(apiTable, staticTable, model) {
|
|
|
1097
1151
|
function getModelContextWindow(platform, model) {
|
|
1098
1152
|
return resolveByModelId(apiReportedContextWindows, CONTEXT_WINDOW_BY_MODEL, model) || CONTEXT_WINDOW_DEFAULT[platform];
|
|
1099
1153
|
}
|
|
1100
|
-
function getMaxOutputTokens(platform, model) {
|
|
1154
|
+
function getMaxOutputTokens(platform, model, purpose) {
|
|
1155
|
+
var want = purpose === "indexing" ? INDEXING_MAX_OUTPUT_TOKENS : MAX_OUTPUT_TOKENS;
|
|
1101
1156
|
var cap = resolveByModelId(apiReportedMaxOutput, MAX_OUTPUT_BY_MODEL, model);
|
|
1102
|
-
return cap ? Math.min(
|
|
1157
|
+
return cap ? Math.min(want, cap) : want;
|
|
1103
1158
|
}
|
|
1104
1159
|
function getContextWindow(platform, model, projectId) {
|
|
1105
1160
|
var ceiling = getModelContextWindow(platform, model);
|
|
@@ -1538,6 +1593,541 @@ function buildAiAgentValue(platform, model, contextWindow) {
|
|
|
1538
1593
|
return p + "#" + m + "#" + Math.floor(n);
|
|
1539
1594
|
}
|
|
1540
1595
|
|
|
1596
|
+
// src/engine/sse.ts
|
|
1597
|
+
var CLAUDE_EVENTS = {
|
|
1598
|
+
message_start: true,
|
|
1599
|
+
message_delta: true,
|
|
1600
|
+
message_stop: true,
|
|
1601
|
+
content_block_start: true,
|
|
1602
|
+
content_block_delta: true,
|
|
1603
|
+
content_block_stop: true,
|
|
1604
|
+
ping: true
|
|
1605
|
+
};
|
|
1606
|
+
var CLAUDE_TOOL_BLOCKS = {
|
|
1607
|
+
tool_use: true,
|
|
1608
|
+
server_tool_use: true,
|
|
1609
|
+
mcp_tool_use: true,
|
|
1610
|
+
web_search_tool_use: true
|
|
1611
|
+
};
|
|
1612
|
+
var OPENAI_TOOL_ITEMS = {
|
|
1613
|
+
function_call: true,
|
|
1614
|
+
mcp_call: true,
|
|
1615
|
+
web_search_call: true,
|
|
1616
|
+
file_search_call: true,
|
|
1617
|
+
code_interpreter_call: true,
|
|
1618
|
+
computer_call: true,
|
|
1619
|
+
image_generation_call: true
|
|
1620
|
+
};
|
|
1621
|
+
function detectProvider(type) {
|
|
1622
|
+
if (!type) return null;
|
|
1623
|
+
if (type.indexOf("response.") === 0) return "openai";
|
|
1624
|
+
if (CLAUDE_EVENTS[type]) return "claude";
|
|
1625
|
+
return null;
|
|
1626
|
+
}
|
|
1627
|
+
function lineEnd(s, from) {
|
|
1628
|
+
for (var i = from; i < s.length; i++) {
|
|
1629
|
+
var c = s.charCodeAt(i);
|
|
1630
|
+
if (c === 10) return { at: i, len: 1 };
|
|
1631
|
+
if (c === 13) {
|
|
1632
|
+
if (i + 1 >= s.length) return null;
|
|
1633
|
+
return { at: i, len: s.charCodeAt(i + 1) === 10 ? 2 : 1 };
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
return null;
|
|
1637
|
+
}
|
|
1638
|
+
function readFrame(lines) {
|
|
1639
|
+
var event = "";
|
|
1640
|
+
var data = [];
|
|
1641
|
+
var framed = false;
|
|
1642
|
+
for (var i = 0; i < lines.length; i++) {
|
|
1643
|
+
var line = lines[i];
|
|
1644
|
+
if (!line.length) continue;
|
|
1645
|
+
if (line.charCodeAt(0) === 58) {
|
|
1646
|
+
framed = true;
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
var colon = line.indexOf(":");
|
|
1650
|
+
var field = colon === -1 ? line : line.slice(0, colon);
|
|
1651
|
+
var value = colon === -1 ? "" : line.slice(colon + 1);
|
|
1652
|
+
if (value.charCodeAt(0) === 32) value = value.slice(1);
|
|
1653
|
+
if (field === "data") {
|
|
1654
|
+
framed = true;
|
|
1655
|
+
data.push(value);
|
|
1656
|
+
} else if (field === "event") {
|
|
1657
|
+
framed = true;
|
|
1658
|
+
event = value;
|
|
1659
|
+
} else if (field === "id" || field === "retry") {
|
|
1660
|
+
framed = true;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
return { event, data: data.join("\n"), framed };
|
|
1664
|
+
}
|
|
1665
|
+
function createSseParser() {
|
|
1666
|
+
var buf = "";
|
|
1667
|
+
var lines = [];
|
|
1668
|
+
var lastSeq = 0;
|
|
1669
|
+
var sawFraming = false;
|
|
1670
|
+
var raw = "";
|
|
1671
|
+
var rawHasContent = false;
|
|
1672
|
+
var ended = false;
|
|
1673
|
+
var rawParsed = false;
|
|
1674
|
+
var rawBody = null;
|
|
1675
|
+
var provider = null;
|
|
1676
|
+
var terminalEvent = null;
|
|
1677
|
+
var errored = false;
|
|
1678
|
+
var error = null;
|
|
1679
|
+
var stopReason = null;
|
|
1680
|
+
var toolCalls = [];
|
|
1681
|
+
var malformedFrames = 0;
|
|
1682
|
+
var malformedToolJson = 0;
|
|
1683
|
+
var message = null;
|
|
1684
|
+
var blocks = /* @__PURE__ */ new Map();
|
|
1685
|
+
var parts = /* @__PURE__ */ new Map();
|
|
1686
|
+
var reasoning = /* @__PURE__ */ new Map();
|
|
1687
|
+
var response = null;
|
|
1688
|
+
var textCache = null;
|
|
1689
|
+
var thinkingCache = null;
|
|
1690
|
+
function feed(text) {
|
|
1691
|
+
if (typeof text !== "string" || !text.length) return;
|
|
1692
|
+
if (!sawFraming) {
|
|
1693
|
+
raw += text;
|
|
1694
|
+
if (!rawHasContent) rawHasContent = /\S/.test(text);
|
|
1695
|
+
rawParsed = false;
|
|
1696
|
+
rawBody = null;
|
|
1697
|
+
}
|
|
1698
|
+
buf += text;
|
|
1699
|
+
var i = 0;
|
|
1700
|
+
for (; ; ) {
|
|
1701
|
+
var end2 = lineEnd(buf, i);
|
|
1702
|
+
if (!end2) break;
|
|
1703
|
+
var line = buf.slice(i, end2.at);
|
|
1704
|
+
i = end2.at + end2.len;
|
|
1705
|
+
if (line.length === 0) dispatch();
|
|
1706
|
+
else lines.push(line);
|
|
1707
|
+
}
|
|
1708
|
+
if (i > 0) buf = buf.slice(i);
|
|
1709
|
+
}
|
|
1710
|
+
function feedChunks(chunks) {
|
|
1711
|
+
if (!chunks || !chunks.length) return;
|
|
1712
|
+
for (var i = 0; i < chunks.length; i++) {
|
|
1713
|
+
var c = chunks[i];
|
|
1714
|
+
if (!c || typeof c !== "object") continue;
|
|
1715
|
+
var seq = typeof c.seq === "number" ? c.seq : 0;
|
|
1716
|
+
if (seq && seq <= lastSeq) continue;
|
|
1717
|
+
if (seq > lastSeq) lastSeq = seq;
|
|
1718
|
+
feed(typeof c.txt === "string" ? c.txt : "");
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
function end() {
|
|
1722
|
+
if (buf.length) {
|
|
1723
|
+
var tail = buf.charCodeAt(buf.length - 1) === 13 ? buf.slice(0, -1) : buf;
|
|
1724
|
+
if (tail.length) lines.push(tail);
|
|
1725
|
+
buf = "";
|
|
1726
|
+
}
|
|
1727
|
+
if (lines.length) dispatch();
|
|
1728
|
+
ended = true;
|
|
1729
|
+
}
|
|
1730
|
+
function isUnframed() {
|
|
1731
|
+
return ended && !sawFraming && rawHasContent;
|
|
1732
|
+
}
|
|
1733
|
+
function dispatch() {
|
|
1734
|
+
var pending = lines;
|
|
1735
|
+
lines = [];
|
|
1736
|
+
if (!pending.length) return;
|
|
1737
|
+
try {
|
|
1738
|
+
var frame = readFrame(pending);
|
|
1739
|
+
if (frame.framed && !sawFraming) {
|
|
1740
|
+
sawFraming = true;
|
|
1741
|
+
raw = "";
|
|
1742
|
+
rawHasContent = false;
|
|
1743
|
+
}
|
|
1744
|
+
if (!frame.data.length) return;
|
|
1745
|
+
if (frame.data === "[DONE]") return;
|
|
1746
|
+
var ev = JSON.parse(frame.data);
|
|
1747
|
+
if (!ev || typeof ev !== "object") {
|
|
1748
|
+
malformedFrames++;
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
var type = typeof ev.type === "string" && ev.type ? ev.type : frame.event;
|
|
1752
|
+
if (!type) {
|
|
1753
|
+
malformedFrames++;
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
if (!provider) provider = detectProvider(type);
|
|
1757
|
+
if (provider === "openai") handleOpenAI(type, ev);
|
|
1758
|
+
else if (provider === "claude") handleClaude(type, ev);
|
|
1759
|
+
else handleUnattributed(type, ev);
|
|
1760
|
+
} catch (e) {
|
|
1761
|
+
malformedFrames++;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
function handleUnattributed(type, ev) {
|
|
1765
|
+
if (type === "error") {
|
|
1766
|
+
takeError(ev && ev.error ? ev : { type: "error", error: ev });
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
malformedFrames++;
|
|
1770
|
+
}
|
|
1771
|
+
function takeError(payload) {
|
|
1772
|
+
errored = true;
|
|
1773
|
+
terminalEvent = "error";
|
|
1774
|
+
error = payload;
|
|
1775
|
+
}
|
|
1776
|
+
function handleClaude(type, ev) {
|
|
1777
|
+
if (type === "ping") return;
|
|
1778
|
+
if (type === "error") {
|
|
1779
|
+
takeError({ type: "error", error: ev && ev.error ? ev.error : ev });
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
if (type === "message_start") {
|
|
1783
|
+
message = ev && ev.message ? shallowClone(ev.message) : { type: "message", role: "assistant" };
|
|
1784
|
+
if (typeof message.stop_reason === "string") stopReason = message.stop_reason;
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
if (type === "content_block_start") {
|
|
1788
|
+
var idx = numberOr(ev.index, -1);
|
|
1789
|
+
if (idx < 0) {
|
|
1790
|
+
malformedFrames++;
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
var block = ev.content_block ? shallowClone(ev.content_block) : {};
|
|
1794
|
+
blocks.set(idx, { block, json: "", sawJson: false });
|
|
1795
|
+
invalidate();
|
|
1796
|
+
if (block && typeof block.type === "string" && CLAUDE_TOOL_BLOCKS[block.type]) {
|
|
1797
|
+
var call = {
|
|
1798
|
+
index: idx,
|
|
1799
|
+
name: typeof block.name === "string" && block.name ? block.name : block.type,
|
|
1800
|
+
type: block.type
|
|
1801
|
+
};
|
|
1802
|
+
if (typeof block.server_name === "string") call.serverName = block.server_name;
|
|
1803
|
+
toolCalls.push(call);
|
|
1804
|
+
}
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (type === "content_block_delta") {
|
|
1808
|
+
var i = numberOr(ev.index, -1);
|
|
1809
|
+
var d = ev.delta;
|
|
1810
|
+
if (i < 0 || !d || typeof d !== "object") {
|
|
1811
|
+
malformedFrames++;
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
var st = blocks.get(i);
|
|
1815
|
+
if (!st) {
|
|
1816
|
+
st = { block: { type: deltaBlockType(d.type) }, json: "", sawJson: false };
|
|
1817
|
+
blocks.set(i, st);
|
|
1818
|
+
}
|
|
1819
|
+
applyClaudeDelta(st, d);
|
|
1820
|
+
invalidate();
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
if (type === "content_block_stop") {
|
|
1824
|
+
var j = numberOr(ev.index, -1);
|
|
1825
|
+
var s = j >= 0 ? blocks.get(j) : void 0;
|
|
1826
|
+
if (s && s.sawJson) finishToolJson(s);
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
if (type === "message_delta") {
|
|
1830
|
+
if (!message) message = { type: "message", role: "assistant" };
|
|
1831
|
+
var delta = ev.delta;
|
|
1832
|
+
if (delta && typeof delta === "object") {
|
|
1833
|
+
for (var k in delta) {
|
|
1834
|
+
if (Object.prototype.hasOwnProperty.call(delta, k)) message[k] = delta[k];
|
|
1835
|
+
}
|
|
1836
|
+
if (typeof delta.stop_reason === "string") stopReason = delta.stop_reason;
|
|
1837
|
+
}
|
|
1838
|
+
if (ev.usage && typeof ev.usage === "object") {
|
|
1839
|
+
message.usage = mergeInto(shallowClone(message.usage) || {}, ev.usage);
|
|
1840
|
+
}
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
if (type === "message_stop") {
|
|
1844
|
+
terminalEvent = "message_stop";
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
malformedFrames++;
|
|
1848
|
+
}
|
|
1849
|
+
function applyClaudeDelta(st, d) {
|
|
1850
|
+
var t = d.type;
|
|
1851
|
+
if (t === "text_delta") {
|
|
1852
|
+
st.block.text = (st.block.text || "") + str(d.text);
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
if (t === "thinking_delta") {
|
|
1856
|
+
st.block.thinking = (st.block.thinking || "") + str(d.thinking);
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
if (t === "signature_delta") {
|
|
1860
|
+
st.block.signature = (st.block.signature || "") + str(d.signature);
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
if (t === "input_json_delta") {
|
|
1864
|
+
st.json += str(d.partial_json);
|
|
1865
|
+
st.sawJson = true;
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
if (t === "citations_delta") {
|
|
1869
|
+
if (d.citation) {
|
|
1870
|
+
if (!Array.isArray(st.block.citations)) st.block.citations = [];
|
|
1871
|
+
st.block.citations.push(d.citation);
|
|
1872
|
+
}
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
malformedFrames++;
|
|
1876
|
+
}
|
|
1877
|
+
function finishToolJson(st) {
|
|
1878
|
+
if (!st.json.length) {
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
try {
|
|
1882
|
+
st.block.input = JSON.parse(st.json);
|
|
1883
|
+
} catch (e) {
|
|
1884
|
+
malformedToolJson++;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
function deltaBlockType(deltaType) {
|
|
1888
|
+
if (deltaType === "thinking_delta" || deltaType === "signature_delta") return "thinking";
|
|
1889
|
+
if (deltaType === "input_json_delta") return "tool_use";
|
|
1890
|
+
return "text";
|
|
1891
|
+
}
|
|
1892
|
+
function handleOpenAI(type, ev) {
|
|
1893
|
+
if (type === "response.output_text.delta") {
|
|
1894
|
+
putPart(ev, str(ev.delta), false);
|
|
1895
|
+
invalidate();
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
if (type === "response.output_text.done") {
|
|
1899
|
+
if (typeof ev.text === "string") {
|
|
1900
|
+
putPart(ev, ev.text, true);
|
|
1901
|
+
invalidate();
|
|
1902
|
+
}
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
if (type === "response.reasoning_summary_text.delta" || type === "response.reasoning_text.delta") {
|
|
1906
|
+
putReasoning(type, ev, str(ev.delta), false);
|
|
1907
|
+
invalidate();
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
if (type === "response.reasoning_summary_text.done" || type === "response.reasoning_text.done") {
|
|
1911
|
+
if (typeof ev.text === "string") {
|
|
1912
|
+
putReasoning(type, ev, ev.text, true);
|
|
1913
|
+
invalidate();
|
|
1914
|
+
}
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
if (type === "response.output_item.added") {
|
|
1918
|
+
var item = ev.item;
|
|
1919
|
+
if (item && typeof item.type === "string" && OPENAI_TOOL_ITEMS[item.type]) {
|
|
1920
|
+
toolCalls.push({
|
|
1921
|
+
index: numberOr(ev.output_index, toolCalls.length),
|
|
1922
|
+
// A built-in tool (web_search_call) has no name of its own, so the item
|
|
1923
|
+
// type is the only label there is and a row can still be drawn.
|
|
1924
|
+
name: typeof item.name === "string" && item.name ? item.name : item.type,
|
|
1925
|
+
type: item.type
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
if (type === "response.completed" || type === "response.incomplete" || type === "response.failed") {
|
|
1931
|
+
terminalEvent = type;
|
|
1932
|
+
if (ev.response && typeof ev.response === "object") {
|
|
1933
|
+
response = ev.response;
|
|
1934
|
+
var st = response.status;
|
|
1935
|
+
if (st === "incomplete") {
|
|
1936
|
+
var reason = response.incomplete_details && response.incomplete_details.reason;
|
|
1937
|
+
stopReason = typeof reason === "string" && reason ? reason : "incomplete";
|
|
1938
|
+
} else if (typeof st === "string" && st) {
|
|
1939
|
+
stopReason = st;
|
|
1940
|
+
}
|
|
1941
|
+
if (response.error && (response.error.message || response.error.code)) {
|
|
1942
|
+
errored = true;
|
|
1943
|
+
error = response;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
if (type === "response.failed") errored = true;
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
if (type === "response.error" || type === "error") {
|
|
1950
|
+
takeError(ev && ev.error ? ev : { type: "error", error: ev });
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
function putReasoning(type, ev, text, replace) {
|
|
1955
|
+
var summary = type.indexOf("response.reasoning_summary_text.") === 0;
|
|
1956
|
+
var oi = numberOr(ev.output_index, 0);
|
|
1957
|
+
var idx = numberOr(summary ? ev.summary_index : ev.content_index, 0);
|
|
1958
|
+
var key = oi + ":" + (summary ? "s" : "r") + ":" + idx;
|
|
1959
|
+
var r = reasoning.get(key);
|
|
1960
|
+
if (!r) {
|
|
1961
|
+
r = { oi, idx, kind: summary ? 0 : 1, text: "" };
|
|
1962
|
+
reasoning.set(key, r);
|
|
1963
|
+
}
|
|
1964
|
+
r.text = replace ? text : r.text + text;
|
|
1965
|
+
}
|
|
1966
|
+
function putPart(ev, text, replace) {
|
|
1967
|
+
var oi = numberOr(ev.output_index, 0);
|
|
1968
|
+
var ci = numberOr(ev.content_index, 0);
|
|
1969
|
+
var key = oi + ":" + ci;
|
|
1970
|
+
var p = parts.get(key);
|
|
1971
|
+
if (!p) {
|
|
1972
|
+
p = { oi, ci, text: "" };
|
|
1973
|
+
parts.set(key, p);
|
|
1974
|
+
}
|
|
1975
|
+
p.text = replace ? text : p.text + text;
|
|
1976
|
+
}
|
|
1977
|
+
function invalidate() {
|
|
1978
|
+
textCache = null;
|
|
1979
|
+
thinkingCache = null;
|
|
1980
|
+
}
|
|
1981
|
+
function claudeTextBlocks() {
|
|
1982
|
+
return orderedBlocks().filter(function(b) {
|
|
1983
|
+
return b && b.type === "text";
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
function orderedBlocks() {
|
|
1987
|
+
var idx = [];
|
|
1988
|
+
blocks.forEach(function(_v, k) {
|
|
1989
|
+
idx.push(k);
|
|
1990
|
+
});
|
|
1991
|
+
idx.sort(function(a, b) {
|
|
1992
|
+
return a - b;
|
|
1993
|
+
});
|
|
1994
|
+
var out = [];
|
|
1995
|
+
for (var i = 0; i < idx.length; i++) out.push(blocks.get(idx[i]).block);
|
|
1996
|
+
return out;
|
|
1997
|
+
}
|
|
1998
|
+
function orderedParts() {
|
|
1999
|
+
var out = [];
|
|
2000
|
+
parts.forEach(function(p) {
|
|
2001
|
+
out.push(p);
|
|
2002
|
+
});
|
|
2003
|
+
out.sort(function(a, b) {
|
|
2004
|
+
return a.oi !== b.oi ? a.oi - b.oi : a.ci - b.ci;
|
|
2005
|
+
});
|
|
2006
|
+
return out;
|
|
2007
|
+
}
|
|
2008
|
+
function currentText() {
|
|
2009
|
+
if (textCache !== null) return textCache;
|
|
2010
|
+
var out;
|
|
2011
|
+
if (provider === "openai") {
|
|
2012
|
+
out = orderedParts().map(function(p) {
|
|
2013
|
+
return p.text;
|
|
2014
|
+
}).join("\n");
|
|
2015
|
+
} else {
|
|
2016
|
+
out = claudeTextBlocks().map(function(b) {
|
|
2017
|
+
return b.text || "";
|
|
2018
|
+
}).join("\n");
|
|
2019
|
+
}
|
|
2020
|
+
textCache = out;
|
|
2021
|
+
return out;
|
|
2022
|
+
}
|
|
2023
|
+
function currentThinking() {
|
|
2024
|
+
if (thinkingCache !== null) return thinkingCache;
|
|
2025
|
+
var out;
|
|
2026
|
+
if (provider === "openai") {
|
|
2027
|
+
var rs = [];
|
|
2028
|
+
reasoning.forEach(function(r) {
|
|
2029
|
+
rs.push(r);
|
|
2030
|
+
});
|
|
2031
|
+
rs.sort(function(a, b) {
|
|
2032
|
+
if (a.oi !== b.oi) return a.oi - b.oi;
|
|
2033
|
+
if (a.idx !== b.idx) return a.idx - b.idx;
|
|
2034
|
+
return a.kind - b.kind;
|
|
2035
|
+
});
|
|
2036
|
+
out = rs.map(function(r) {
|
|
2037
|
+
return r.text;
|
|
2038
|
+
}).join("\n");
|
|
2039
|
+
} else {
|
|
2040
|
+
out = orderedBlocks().filter(function(b) {
|
|
2041
|
+
return b && b.type === "thinking";
|
|
2042
|
+
}).map(function(b) {
|
|
2043
|
+
return b.thinking || "";
|
|
2044
|
+
}).join("\n");
|
|
2045
|
+
}
|
|
2046
|
+
thinkingCache = out;
|
|
2047
|
+
return out;
|
|
2048
|
+
}
|
|
2049
|
+
function buildBody() {
|
|
2050
|
+
if (provider === "openai") {
|
|
2051
|
+
if (response) return response;
|
|
2052
|
+
} else if (blocks.size || message) {
|
|
2053
|
+
var base = message ? shallowClone(message) : { type: "message", role: "assistant" };
|
|
2054
|
+
base.content = orderedBlocks();
|
|
2055
|
+
return base;
|
|
2056
|
+
}
|
|
2057
|
+
if (errored && error) return error;
|
|
2058
|
+
return unframedBody();
|
|
2059
|
+
}
|
|
2060
|
+
function unframedBody() {
|
|
2061
|
+
if (!isUnframed()) return null;
|
|
2062
|
+
if (rawParsed) return rawBody;
|
|
2063
|
+
rawParsed = true;
|
|
2064
|
+
try {
|
|
2065
|
+
var v = JSON.parse(raw);
|
|
2066
|
+
rawBody = v && typeof v === "object" ? v : null;
|
|
2067
|
+
} catch (e) {
|
|
2068
|
+
rawBody = null;
|
|
2069
|
+
}
|
|
2070
|
+
return rawBody;
|
|
2071
|
+
}
|
|
2072
|
+
function snapshot() {
|
|
2073
|
+
return {
|
|
2074
|
+
provider,
|
|
2075
|
+
text: currentText(),
|
|
2076
|
+
thinkingText: currentThinking(),
|
|
2077
|
+
toolCalls: toolCalls.slice(),
|
|
2078
|
+
toolNames: toolCalls.map(function(t) {
|
|
2079
|
+
return t.name;
|
|
2080
|
+
}),
|
|
2081
|
+
stopReason,
|
|
2082
|
+
complete: terminalEvent !== null,
|
|
2083
|
+
// A terminal event that ENDED the answer rather than KILLED it. The
|
|
2084
|
+
// `errored` term covers all three ways a stream dies with a terminal event
|
|
2085
|
+
// on it: an Anthropic or OpenAI `error` frame (takeError sets both), a
|
|
2086
|
+
// response.failed, and a response.completed/incomplete whose Response
|
|
2087
|
+
// object carries an error payload. See the field's own doc for the loss
|
|
2088
|
+
// this separation prevents.
|
|
2089
|
+
answerComplete: terminalEvent !== null && terminalEvent !== "error" && !errored,
|
|
2090
|
+
terminalEvent,
|
|
2091
|
+
errored,
|
|
2092
|
+
error,
|
|
2093
|
+
malformedFrames,
|
|
2094
|
+
malformedToolJson,
|
|
2095
|
+
unframed: isUnframed(),
|
|
2096
|
+
unframedText: isUnframed() ? raw : null,
|
|
2097
|
+
lastSeq
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
return {
|
|
2101
|
+
feed,
|
|
2102
|
+
feedChunks,
|
|
2103
|
+
end,
|
|
2104
|
+
snapshot,
|
|
2105
|
+
finalBody: buildBody
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
function str(v) {
|
|
2109
|
+
return typeof v === "string" ? v : "";
|
|
2110
|
+
}
|
|
2111
|
+
function numberOr(v, fallback) {
|
|
2112
|
+
return typeof v === "number" && isFinite(v) ? v : fallback;
|
|
2113
|
+
}
|
|
2114
|
+
function shallowClone(o) {
|
|
2115
|
+
if (!o || typeof o !== "object") return o;
|
|
2116
|
+
var out = Array.isArray(o) ? o.slice() : {};
|
|
2117
|
+
if (!Array.isArray(o)) {
|
|
2118
|
+
for (var k in o) {
|
|
2119
|
+
if (Object.prototype.hasOwnProperty.call(o, k)) out[k] = o[k];
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
return out;
|
|
2123
|
+
}
|
|
2124
|
+
function mergeInto(target, src) {
|
|
2125
|
+
for (var k in src) {
|
|
2126
|
+
if (Object.prototype.hasOwnProperty.call(src, k)) target[k] = src[k];
|
|
2127
|
+
}
|
|
2128
|
+
return target;
|
|
2129
|
+
}
|
|
2130
|
+
|
|
1541
2131
|
// src/engine/requests.ts
|
|
1542
2132
|
var ANTHROPIC_MESSAGES_API_URL = "https://api.anthropic.com/v1/messages";
|
|
1543
2133
|
var ANTHROPIC_MODELS_API_URL = "https://api.anthropic.com/v1/models";
|
|
@@ -1556,12 +2146,43 @@ var MCP_NAME = "BunnyQuery";
|
|
|
1556
2146
|
var DEFAULT_CLAUDE_MODEL = "claude-sonnet-5";
|
|
1557
2147
|
var DEFAULT_OPENAI_MODEL = "gpt-5.6-luna";
|
|
1558
2148
|
var mcpUrl = () => chatEngineConfig().mcpBaseUrl;
|
|
2149
|
+
function withMcpParams(base, params) {
|
|
2150
|
+
if (!base) return base;
|
|
2151
|
+
const pairs = Object.keys(params).filter((k) => params[k] !== void 0 && params[k] !== null && params[k] !== "").map((k) => encodeURIComponent(k) + "=" + encodeURIComponent(String(params[k])));
|
|
2152
|
+
if (!pairs.length) return base;
|
|
2153
|
+
const [addr, existing] = base.split("?");
|
|
2154
|
+
const hasPath = /^[a-z][a-z0-9+.-]*:\/\/[^/]+\/./i.test(addr);
|
|
2155
|
+
const path = hasPath ? addr : addr.replace(/\/+$/, "") + "/";
|
|
2156
|
+
return path + "?" + (existing ? existing + "&" : "") + pairs.join("&");
|
|
2157
|
+
}
|
|
2158
|
+
var mcpContextParam = (platform, model) => getModelContextWindow(platform, model);
|
|
2159
|
+
var mcpIndexingUrl = (platform = "openai", model) => withMcpParams(mcpUrl(), { profile: "index", ctx: mcpContextParam(platform, model) });
|
|
1559
2160
|
function mcpEndpointFor(anonymous, publicProjectId, service) {
|
|
1560
2161
|
if (!anonymous) return { url: mcpUrl(), token: "$ACCESS_TOKEN" };
|
|
1561
2162
|
const project = publicProjectId || service;
|
|
1562
2163
|
return { url: String(mcpUrl()).replace(/\/+$/, "") + "/p/" + project };
|
|
1563
2164
|
}
|
|
1564
2165
|
var clientSecretRequest = (opts) => chatEngineConfig().clientSecretRequest(opts);
|
|
2166
|
+
var CHAT_STREAM_ON = Object.freeze({
|
|
2167
|
+
transport: Object.freeze({ stream: true }),
|
|
2168
|
+
body: Object.freeze({ stream: true })
|
|
2169
|
+
});
|
|
2170
|
+
var CHAT_STREAM_OFF = Object.freeze({
|
|
2171
|
+
transport: Object.freeze({}),
|
|
2172
|
+
body: Object.freeze({})
|
|
2173
|
+
});
|
|
2174
|
+
var CHAT_STREAM_ON_REALTIME = Object.freeze({
|
|
2175
|
+
// `realtime` rides on the TRANSPORT arm only. It is a skapi option, not a field
|
|
2176
|
+
// the destination understands, so it must never reach `data`: the body arm stays
|
|
2177
|
+
// exactly what it is with the socket off.
|
|
2178
|
+
transport: Object.freeze({ stream: true, realtime: true }),
|
|
2179
|
+
body: Object.freeze({ stream: true })
|
|
2180
|
+
});
|
|
2181
|
+
function chatStreamWiring(queue) {
|
|
2182
|
+
if (!liveStreamingEnabled()) return CHAT_STREAM_OFF;
|
|
2183
|
+
if (isBgIndexingQueue(queue)) return CHAT_STREAM_OFF;
|
|
2184
|
+
return liveStreamingRealtimeEnabled() ? CHAT_STREAM_ON_REALTIME : CHAT_STREAM_ON;
|
|
2185
|
+
}
|
|
1565
2186
|
var VARIANT_IMAGE_DETAIL = "original";
|
|
1566
2187
|
var VARIANT_TEXT_VERBOSITY = "high";
|
|
1567
2188
|
var OLDEST_NANO_REASONING_EFFORT = "high";
|
|
@@ -1708,6 +2329,7 @@ function applyHistoryCacheBreakpoint(messages) {
|
|
|
1708
2329
|
});
|
|
1709
2330
|
}
|
|
1710
2331
|
var POLL_INTERVAL = 3e3;
|
|
2332
|
+
var STREAM_POLL_INTERVAL = 1e3;
|
|
1711
2333
|
var MAX_CONCURRENT_BG_POLLS = 6;
|
|
1712
2334
|
async function callClaudeWithMcp({
|
|
1713
2335
|
prompt,
|
|
@@ -1730,12 +2352,14 @@ async function callClaudeWithMcp({
|
|
|
1730
2352
|
if (mcpServer.authorizationToken) {
|
|
1731
2353
|
mcpServerDefinition.authorization_token = mcpServer.authorizationToken;
|
|
1732
2354
|
}
|
|
2355
|
+
const stream = chatStreamWiring(userId || service);
|
|
1733
2356
|
return clientSecretRequest({
|
|
1734
2357
|
clientSecretName: "claude",
|
|
1735
2358
|
queue: userId || service,
|
|
1736
2359
|
service,
|
|
1737
2360
|
owner,
|
|
1738
2361
|
...pollOpt(),
|
|
2362
|
+
...stream.transport,
|
|
1739
2363
|
url: ANTHROPIC_MESSAGES_API_URL,
|
|
1740
2364
|
method: "POST",
|
|
1741
2365
|
headers: {
|
|
@@ -1747,6 +2371,9 @@ async function callClaudeWithMcp({
|
|
|
1747
2371
|
data: {
|
|
1748
2372
|
model,
|
|
1749
2373
|
max_tokens: maxTokens,
|
|
2374
|
+
// Top level beside model/messages/mcp_servers, which is where the
|
|
2375
|
+
// Messages API takes it.
|
|
2376
|
+
...stream.body,
|
|
1750
2377
|
...extractContent && extractContent.length ? { _skapi_extract: extractContent } : {},
|
|
1751
2378
|
...fileUrls && fileUrls.length ? { _skapi_file_urls: fileUrls } : {},
|
|
1752
2379
|
...system ? {
|
|
@@ -1801,7 +2428,9 @@ async function callClaudeWithPublicMcp(prompt, service, owner, messages, system,
|
|
|
1801
2428
|
fileUrls,
|
|
1802
2429
|
mcpServer: {
|
|
1803
2430
|
name: MCP_NAME,
|
|
1804
|
-
url: endpoint.url,
|
|
2431
|
+
url: withMcpParams(endpoint.url, {
|
|
2432
|
+
ctx: mcpContextParam("claude", model || DEFAULT_CLAUDE_MODEL)
|
|
2433
|
+
}),
|
|
1805
2434
|
// Omitted entirely for an anonymous turn; the `if (mcpServer.authorizationToken)`
|
|
1806
2435
|
// guard below drops the key rather than sending an empty one.
|
|
1807
2436
|
authorizationToken: endpoint.token
|
|
@@ -1829,12 +2458,14 @@ async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system,
|
|
|
1829
2458
|
content: m.content
|
|
1830
2459
|
}))
|
|
1831
2460
|
];
|
|
2461
|
+
const stream = chatStreamWiring(userId || service);
|
|
1832
2462
|
return clientSecretRequest({
|
|
1833
2463
|
clientSecretName: "openai",
|
|
1834
2464
|
queue: userId || service,
|
|
1835
2465
|
service,
|
|
1836
2466
|
owner,
|
|
1837
2467
|
...pollOpt(),
|
|
2468
|
+
...stream.transport,
|
|
1838
2469
|
url: OPENAI_RESPONSES_API_URL,
|
|
1839
2470
|
method: "POST",
|
|
1840
2471
|
headers: {
|
|
@@ -1844,6 +2475,9 @@ async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system,
|
|
|
1844
2475
|
data: {
|
|
1845
2476
|
model: resolvedModel,
|
|
1846
2477
|
max_output_tokens: getMaxOutputTokens("openai", resolvedModel),
|
|
2478
|
+
// Top level beside model/input/tools, which is where the Responses API
|
|
2479
|
+
// takes it.
|
|
2480
|
+
...stream.body,
|
|
1847
2481
|
...extractContent && extractContent.length ? { _skapi_extract: extractContent } : {},
|
|
1848
2482
|
...fileUrls && fileUrls.length ? { _skapi_file_urls: fileUrls } : {},
|
|
1849
2483
|
input: responseInput,
|
|
@@ -1851,7 +2485,7 @@ async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system,
|
|
|
1851
2485
|
{
|
|
1852
2486
|
type: "mcp",
|
|
1853
2487
|
server_label: MCP_NAME,
|
|
1854
|
-
server_url: endpoint.url,
|
|
2488
|
+
server_url: withMcpParams(endpoint.url, { ctx: mcpContextParam("openai", resolvedModel) }),
|
|
1855
2489
|
require_approval: "never",
|
|
1856
2490
|
// No `headers` at all for an anonymous turn: `Bearer ` with an
|
|
1857
2491
|
// empty token is a credential the MCP server rejects, and the
|
|
@@ -1989,7 +2623,7 @@ async function notifyAgentSaveAttachment(info) {
|
|
|
1989
2623
|
},
|
|
1990
2624
|
data: {
|
|
1991
2625
|
model: resolvedModel2,
|
|
1992
|
-
max_output_tokens: getMaxOutputTokens("openai", resolvedModel2),
|
|
2626
|
+
max_output_tokens: getMaxOutputTokens("openai", resolvedModel2, "indexing"),
|
|
1993
2627
|
// Nano-only transcription knobs. Indexing only; see variantIndexingOptions.
|
|
1994
2628
|
...variantIndexingOptions(resolvedModel2),
|
|
1995
2629
|
...skapiExtract,
|
|
@@ -2007,7 +2641,7 @@ async function notifyAgentSaveAttachment(info) {
|
|
|
2007
2641
|
{
|
|
2008
2642
|
type: "mcp",
|
|
2009
2643
|
server_label: MCP_NAME,
|
|
2010
|
-
server_url:
|
|
2644
|
+
server_url: mcpIndexingUrl("openai", resolvedModel2),
|
|
2011
2645
|
require_approval: "never",
|
|
2012
2646
|
headers: { Authorization: "Bearer $ACCESS_TOKEN" }
|
|
2013
2647
|
},
|
|
@@ -2038,7 +2672,7 @@ async function notifyAgentSaveAttachment(info) {
|
|
|
2038
2672
|
},
|
|
2039
2673
|
data: {
|
|
2040
2674
|
model: resolvedModel,
|
|
2041
|
-
max_tokens: getMaxOutputTokens("claude", resolvedModel),
|
|
2675
|
+
max_tokens: getMaxOutputTokens("claude", resolvedModel, "indexing"),
|
|
2042
2676
|
...skapiExtract,
|
|
2043
2677
|
...skapiRender,
|
|
2044
2678
|
...skapiWindow,
|
|
@@ -2060,7 +2694,7 @@ async function notifyAgentSaveAttachment(info) {
|
|
|
2060
2694
|
{
|
|
2061
2695
|
type: "url",
|
|
2062
2696
|
name: MCP_NAME,
|
|
2063
|
-
url:
|
|
2697
|
+
url: mcpIndexingUrl("claude", resolvedModel),
|
|
2064
2698
|
authorization_token: "$ACCESS_TOKEN"
|
|
2065
2699
|
}
|
|
2066
2700
|
],
|
|
@@ -2257,9 +2891,9 @@ function probeBgQueue(params, opts) {
|
|
|
2257
2891
|
{ service: params.service, owner: params.owner, platform: params.platform, queue: params.queue, status: params.status },
|
|
2258
2892
|
{ limit: params.limit, fetchMore: false }
|
|
2259
2893
|
)).then(function(result) {
|
|
2260
|
-
const
|
|
2261
|
-
bgProbeCache[key] =
|
|
2262
|
-
return
|
|
2894
|
+
const entry2 = { result, at: Date.now() };
|
|
2895
|
+
bgProbeCache[key] = entry2;
|
|
2896
|
+
return entry2;
|
|
2263
2897
|
});
|
|
2264
2898
|
bgProbeInflight[key] = p;
|
|
2265
2899
|
p.then(function() {
|
|
@@ -2278,8 +2912,8 @@ async function fetchLiveIndexingKeys(params) {
|
|
|
2278
2912
|
]);
|
|
2279
2913
|
const keys = /* @__PURE__ */ new Set();
|
|
2280
2914
|
let truncated = false;
|
|
2281
|
-
for (const
|
|
2282
|
-
const res =
|
|
2915
|
+
for (const entry2 of [pending, running]) {
|
|
2916
|
+
const res = entry2.result;
|
|
2283
2917
|
const list = res && Array.isArray(res.list) ? res.list : [];
|
|
2284
2918
|
if (list.length >= LIVE_INDEX_PROBE_LIMIT) truncated = true;
|
|
2285
2919
|
for (const item of list) {
|
|
@@ -2522,8 +3156,9 @@ function indexScopeKey(projectId, platform) {
|
|
|
2522
3156
|
return projectId + "#" + platform;
|
|
2523
3157
|
}
|
|
2524
3158
|
function mapHistoryListToMessages(list, platform, opts) {
|
|
2525
|
-
var mapped = [], runningItemIds = [];
|
|
3159
|
+
var mapped = [], runningItemIds = [], streamPendingItemIds = [];
|
|
2526
3160
|
var extractAssistantText = platform === "openai" ? extractOpenAIText : extractClaudeText;
|
|
3161
|
+
var canRecoverStreams = streamRecoveryEnabled();
|
|
2527
3162
|
var filtered = filterListByClearHorizon(list, opts.clearedAt);
|
|
2528
3163
|
filtered.slice().reverse().forEach(function(item) {
|
|
2529
3164
|
var requestBody = item && item.request_body;
|
|
@@ -2537,6 +3172,7 @@ function mapHistoryListToMessages(list, platform, opts) {
|
|
|
2537
3172
|
var userText = isCompact ? typeof item.request_text === "string" ? item.request_text : "" : extractLastUserTextFromRequest(requestBody);
|
|
2538
3173
|
var assistantText = isPending ? "" : isCompact ? (typeof item.response_text === "string" ? item.response_text : "").trim() : (extractAssistantText(response) || "").trim() || "";
|
|
2539
3174
|
var isErrorResponse = !isPending && (isFailed || !isCompact && isErrorResponseBody(response));
|
|
3175
|
+
var isStreamPending = canRecoverStreams && !isCompact && !isPending && !isCancelledItem && !isErrorResponse && !item._isBgTask && !item._isOnBgQueue && item.status === "resolved" && item.response_body == null && item.error == null && !assistantText;
|
|
2540
3176
|
var reportedComplete = !!(item && item._isBgTask) && !isErrorResponse && (isCompact ? item.response_complete_marker === true : !!assistantText && assistantText.indexOf(INDEXING_COMPLETE_MARKER) !== -1);
|
|
2541
3177
|
if (reportedComplete) assistantText = assistantText.split(INDEXING_COMPLETE_MARKER).join("").trim();
|
|
2542
3178
|
var serverItemId = item && typeof item.id === "string" && item.id ? item.id : void 0;
|
|
@@ -2595,6 +3231,14 @@ function mapHistoryListToMessages(list, platform, opts) {
|
|
|
2595
3231
|
if (serverItemId !== void 0) em._serverItemId = serverItemId;
|
|
2596
3232
|
if (replyTs !== void 0) em._ts = replyTs;
|
|
2597
3233
|
mapped.push(em);
|
|
3234
|
+
} else if (isStreamPending) {
|
|
3235
|
+
var sp = { role: "assistant", content: "", _streamPending: true };
|
|
3236
|
+
if (serverItemId !== void 0) {
|
|
3237
|
+
sp._serverItemId = serverItemId;
|
|
3238
|
+
streamPendingItemIds.push(serverItemId);
|
|
3239
|
+
}
|
|
3240
|
+
if (replyTs !== void 0) sp._ts = replyTs;
|
|
3241
|
+
mapped.push(sp);
|
|
2598
3242
|
} else if (assistantText || reportedComplete) {
|
|
2599
3243
|
var okm = { role: "assistant", content: sanitizeAttachmentLinksForHistory(assistantText, opts.projectId, true) || EMPTY_INDEXING_REPLY };
|
|
2600
3244
|
if (item._fromBgChain) okm._fromBgChain = true;
|
|
@@ -2610,7 +3254,23 @@ function mapHistoryListToMessages(list, platform, opts) {
|
|
|
2610
3254
|
var ownerKey = chatCacheKey(opts.projectId, platform, opts.userId);
|
|
2611
3255
|
for (var oi = 0; oi < mapped.length; oi++) mapped[oi]._ownerKey = ownerKey;
|
|
2612
3256
|
}
|
|
2613
|
-
return { messages: mapped, runningItemIds };
|
|
3257
|
+
return { messages: mapped, runningItemIds, streamPendingItemIds };
|
|
3258
|
+
}
|
|
3259
|
+
function adoptLocalAnswerIntoPage(incoming, local) {
|
|
3260
|
+
if (!incoming || !local || !incoming._streamPending) return false;
|
|
3261
|
+
if (incoming.role !== "assistant" || local.role !== "assistant") return false;
|
|
3262
|
+
var hasText = typeof local.content === "string" && local.content.length > 0;
|
|
3263
|
+
var isLive = !!(local.isPending || local._streaming);
|
|
3264
|
+
if (!hasText && !isLive) return false;
|
|
3265
|
+
if (hasText) {
|
|
3266
|
+
incoming.content = local.content;
|
|
3267
|
+
incoming._streamPending = false;
|
|
3268
|
+
}
|
|
3269
|
+
if (local._localId !== void 0) incoming._localId = local._localId;
|
|
3270
|
+
if (local.isPending) incoming.isPending = true;
|
|
3271
|
+
if (local.isPendingInProcess) incoming.isPendingInProcess = true;
|
|
3272
|
+
if (local._streaming) incoming._streaming = true;
|
|
3273
|
+
return true;
|
|
2614
3274
|
}
|
|
2615
3275
|
function shouldRescueInFlightMessage(m, ctx) {
|
|
2616
3276
|
if (!m) return false;
|
|
@@ -2618,6 +3278,7 @@ function shouldRescueInFlightMessage(m, ctx) {
|
|
|
2618
3278
|
if (m._ownerKey !== void 0 && ctx.loadKey !== void 0 && m._ownerKey !== ctx.loadKey) return false;
|
|
2619
3279
|
if (m._serverItemId && ctx.hasServerId(m._serverItemId)) return false;
|
|
2620
3280
|
if (m._stageId) return true;
|
|
3281
|
+
if (m._streaming && !m._serverItemId) return true;
|
|
2621
3282
|
if (!m._serverItemId && ctx.pageHasPendingAssistant) return false;
|
|
2622
3283
|
if (m.isSendingToServer || m.isPendingQueued || m.isPendingInProcess || m.isPending) return true;
|
|
2623
3284
|
if (ctx.sending && m.role === "user") {
|
|
@@ -3004,8 +3665,143 @@ function nextFrame(cb) {
|
|
|
3004
3665
|
function isPollStopped(res) {
|
|
3005
3666
|
return !!res && typeof res === "object" && res.status === "stopped";
|
|
3006
3667
|
}
|
|
3668
|
+
var LIVE_PENDING_LINK_WINDOW = 512;
|
|
3669
|
+
var STREAM_RECOVERY_PER_LOAD = 2;
|
|
3670
|
+
function liveSafePrefix(text) {
|
|
3671
|
+
if (!text) return "";
|
|
3672
|
+
var cut = text.length;
|
|
3673
|
+
var fenceAt = -1, fences = 0, from = 0, hit;
|
|
3674
|
+
for (; ; ) {
|
|
3675
|
+
hit = text.indexOf("```", from);
|
|
3676
|
+
if (hit === -1) break;
|
|
3677
|
+
fences++;
|
|
3678
|
+
fenceAt = hit;
|
|
3679
|
+
from = hit + 3;
|
|
3680
|
+
}
|
|
3681
|
+
if (fences % 2 === 1 && fenceAt !== -1) cut = fenceAt;
|
|
3682
|
+
var head = text.slice(0, cut);
|
|
3683
|
+
var lineStart = head.lastIndexOf("\n") + 1;
|
|
3684
|
+
var line = head.slice(lineStart);
|
|
3685
|
+
var open = line.lastIndexOf("[");
|
|
3686
|
+
if (open !== -1 && line.length - open <= LIVE_PENDING_LINK_WINDOW) {
|
|
3687
|
+
var rest = line.slice(open);
|
|
3688
|
+
var close = rest.indexOf("]");
|
|
3689
|
+
if (close === -1) {
|
|
3690
|
+
cut = lineStart + open;
|
|
3691
|
+
} else if (rest.charAt(close + 1) === "(" && rest.indexOf(")", close + 1) === -1) {
|
|
3692
|
+
cut = lineStart + open;
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
var tokStart = line.length;
|
|
3696
|
+
while (tokStart > 0 && !/\s/.test(line.charAt(tokStart - 1))) tokStart--;
|
|
3697
|
+
var tok = line.slice(tokStart);
|
|
3698
|
+
if (tok && /^(?:https?:\/\/|src::)/i.test(tok)) {
|
|
3699
|
+
var tokCut = lineStart + tokStart;
|
|
3700
|
+
if (tokCut < cut) cut = tokCut;
|
|
3701
|
+
}
|
|
3702
|
+
if (line.indexOf("```") === -1) {
|
|
3703
|
+
var ticks = 0, lastTick = -1;
|
|
3704
|
+
for (var i = 0; i < line.length; i++) {
|
|
3705
|
+
if (line.charAt(i) === "`") {
|
|
3706
|
+
ticks++;
|
|
3707
|
+
lastTick = i;
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
if (ticks % 2 === 1 && lastTick !== -1) {
|
|
3711
|
+
var tickCut = lineStart + lastTick;
|
|
3712
|
+
if (tickCut < cut) cut = tickCut;
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
if (cut >= text.length) return text;
|
|
3716
|
+
if (cut < 0) cut = 0;
|
|
3717
|
+
return text.slice(0, cut);
|
|
3718
|
+
}
|
|
3719
|
+
function commonPrefixLength(a, b) {
|
|
3720
|
+
var n = Math.min(a.length, b.length), i = 0;
|
|
3721
|
+
while (i < n && a.charCodeAt(i) === b.charCodeAt(i)) i++;
|
|
3722
|
+
if (i > 0) {
|
|
3723
|
+
var prev = a.charCodeAt(i - 1);
|
|
3724
|
+
if (prev >= 55296 && prev <= 56319) i--;
|
|
3725
|
+
}
|
|
3726
|
+
return i;
|
|
3727
|
+
}
|
|
3728
|
+
function typewriterResumeIndex(painted, fullText, regions) {
|
|
3729
|
+
if (!painted || !fullText) return 0;
|
|
3730
|
+
if (/^\s/.test(painted) && !/^\s/.test(fullText)) {
|
|
3731
|
+
painted = painted.replace(/^\s+/, "");
|
|
3732
|
+
if (!painted) return 0;
|
|
3733
|
+
}
|
|
3734
|
+
var i = commonPrefixLength(painted, fullText);
|
|
3735
|
+
if (i <= 0) return 0;
|
|
3736
|
+
if (i >= fullText.length) return fullText.length;
|
|
3737
|
+
for (var changed = true; changed; ) {
|
|
3738
|
+
changed = false;
|
|
3739
|
+
for (var k = 0; k < regions.length; k++) {
|
|
3740
|
+
var r = regions[k];
|
|
3741
|
+
if (i > r.start && i < r.end) {
|
|
3742
|
+
i = r.end;
|
|
3743
|
+
changed = true;
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
return i > fullText.length ? fullText.length : i;
|
|
3748
|
+
}
|
|
3749
|
+
function mayKeepStreamedAnswer(snap, rowStatus) {
|
|
3750
|
+
if (rowStatus !== void 0 && rowStatus !== null && rowStatus !== "" && rowStatus !== "resolved") return false;
|
|
3751
|
+
if (!snap || typeof snap !== "object") return false;
|
|
3752
|
+
if (snap.errored) return false;
|
|
3753
|
+
if (snap.answerComplete) return true;
|
|
3754
|
+
if (snap.unframed) return true;
|
|
3755
|
+
return false;
|
|
3756
|
+
}
|
|
3757
|
+
function streamRecoveryPhase(msg) {
|
|
3758
|
+
if (!msg || !msg._streamPending || msg.content || !msg._serverItemId) return "";
|
|
3759
|
+
if (msg._streamRecovery === "active") return "active";
|
|
3760
|
+
if (msg._streamRecovery === "failed") return "failed";
|
|
3761
|
+
return "idle";
|
|
3762
|
+
}
|
|
3763
|
+
function streamRecoveryLabels(phase) {
|
|
3764
|
+
if (phase === "failed") {
|
|
3765
|
+
return { note: "Could not load this answer.", action: "Try again" };
|
|
3766
|
+
}
|
|
3767
|
+
return { note: "This answer was not saved with the conversation.", action: "Load answer" };
|
|
3768
|
+
}
|
|
3769
|
+
var LIVE_PAINT_MIN_MS = 250;
|
|
3770
|
+
var LIVE_TYPE_MAX_STEP = 1200;
|
|
3007
3771
|
var ChatSession = class {
|
|
3008
3772
|
constructor(host) {
|
|
3773
|
+
// --- live streaming ----------------------------------------------------
|
|
3774
|
+
//
|
|
3775
|
+
// A streamed turn's answer NEVER reaches the polling row: the relay appends the
|
|
3776
|
+
// destination's raw bytes to a chunk table and the row settles with a status and
|
|
3777
|
+
// nothing else. So for a streamed turn this parser is not a nicety that makes the
|
|
3778
|
+
// wait prettier, it is the only place the answer exists until csr-finalize stores
|
|
3779
|
+
// one. Three things follow, and all three are load-bearing:
|
|
3780
|
+
//
|
|
3781
|
+
// 1. EVERY foreground poll gets a sink while streaming is on, not just the one
|
|
3782
|
+
// the dispatch attaches. A tab return, a reload, a resumePolling all
|
|
3783
|
+
// re-attach a poll to a still-running item, and skapi's reader sends
|
|
3784
|
+
// `since: 0` on its first tick, so a fresh sink REPLAYS the whole stream from
|
|
3785
|
+
// the beginning. Attaching without one settles that turn on an envelope and
|
|
3786
|
+
// the user's answer is gone.
|
|
3787
|
+
// 2. The parser is keyed by SERVER ITEM ID, and so is the bubble it paints into.
|
|
3788
|
+
// A history refetch replaces the local pending bubble with the server's copy
|
|
3789
|
+
// of the same turn; that copy carries the same _serverItemId, so the next
|
|
3790
|
+
// paint finds it and carries on. Nothing has to be rescued and nothing can be
|
|
3791
|
+
// painted twice.
|
|
3792
|
+
// 3. The stream is never the source of truth. At settle the parser's ASSEMBLED
|
|
3793
|
+
// body (byte equivalent to what a buffered call returns) goes through the
|
|
3794
|
+
// same extractClaudeText / extractOpenAIText the buffered path uses, and a
|
|
3795
|
+
// row that does hold a stored body wins outright.
|
|
3796
|
+
//
|
|
3797
|
+
// Background polls never get a sink, and that is safe because nothing on the bg
|
|
3798
|
+
// queue ever streams: an indexing pass must not (the worker READS its reply), and
|
|
3799
|
+
// a chat turn sent with attachments is deliberately left buffered for exactly the
|
|
3800
|
+
// reason point 1 gives, since the re-attach loop would poll it as a background
|
|
3801
|
+
// item and hand it no reader. See chatStreamWiring. A sink there would also spend
|
|
3802
|
+
// the request budget MAX_CONCURRENT_BG_POLLS exists to protect.
|
|
3803
|
+
/** Live streams by server item id. One per in-flight streamed turn. */
|
|
3804
|
+
this.liveStreams = {};
|
|
3009
3805
|
// ─── compact-stub hydration ─────────────────────────────────────────────
|
|
3010
3806
|
// Split-fetch bg pages arrive as label stubs (no bodies). When the user
|
|
3011
3807
|
// expands a row, the real reply text is fetched per item (csr-poll point
|
|
@@ -3210,8 +4006,8 @@ var ChatSession = class {
|
|
|
3210
4006
|
return Promise.resolve(probeBgQueue(
|
|
3211
4007
|
{ service: id.projectId, owner: id.owner, platform, queue, status, limit: WORKER_PASS_ADOPT_LIMIT },
|
|
3212
4008
|
{ maxAgeMs: BG_PROBE_TTL_MS }
|
|
3213
|
-
)).then(function(
|
|
3214
|
-
return
|
|
4009
|
+
)).then(function(entry2) {
|
|
4010
|
+
return entry2.result;
|
|
3215
4011
|
}).catch(function() {
|
|
3216
4012
|
return null;
|
|
3217
4013
|
});
|
|
@@ -3398,10 +4194,49 @@ var ChatSession = class {
|
|
|
3398
4194
|
* and they are the ones bounded by MAX_CONCURRENT_BG_POLLS, so adding probes there would spend
|
|
3399
4195
|
* the request budget the cap exists to protect.
|
|
3400
4196
|
*/
|
|
3401
|
-
attachForegroundPoll(source, itemId, opts) {
|
|
3402
|
-
return this._fgPollWithEarlyProbe(source, itemId, opts);
|
|
4197
|
+
attachForegroundPoll(source, itemId, opts, ctx) {
|
|
4198
|
+
return this._fgPollWithEarlyProbe(source, itemId, opts, ctx);
|
|
3403
4199
|
}
|
|
3404
|
-
_fgPollWithEarlyProbe(source, itemId, opts) {
|
|
4200
|
+
_fgPollWithEarlyProbe(source, itemId, opts, ctx) {
|
|
4201
|
+
var self = this;
|
|
4202
|
+
var live = this._beginLiveStream(itemId, ctx);
|
|
4203
|
+
if (live) {
|
|
4204
|
+
var inner = opts || {};
|
|
4205
|
+
var callerResponse = typeof inner.onResponse === "function" ? inner.onResponse : null;
|
|
4206
|
+
var callerError = typeof inner.onError === "function" ? inner.onError : null;
|
|
4207
|
+
var streamOpts = Object.assign({}, inner, {
|
|
4208
|
+
onStream: function(chunk, _seq, via) {
|
|
4209
|
+
self._feedLiveStream(live, chunk, via);
|
|
4210
|
+
},
|
|
4211
|
+
onResponse: function(res) {
|
|
4212
|
+
var effective = res;
|
|
4213
|
+
if (isPollStopped(res)) self._closeLiveStream(live, false);
|
|
4214
|
+
else effective = self._settleLiveStream(live, res);
|
|
4215
|
+
if (callerResponse) callerResponse(effective);
|
|
4216
|
+
},
|
|
4217
|
+
onError: function(err) {
|
|
4218
|
+
self._closeLiveStream(live, false);
|
|
4219
|
+
if (callerError) callerError(err);
|
|
4220
|
+
}
|
|
4221
|
+
});
|
|
4222
|
+
var lp = source.poll(Object.assign({ latency: STREAM_POLL_INTERVAL }, streamOpts));
|
|
4223
|
+
var stopLp = lp && typeof lp.stop === "function" ? lp.stop.bind(lp) : null;
|
|
4224
|
+
var wrapped = Promise.resolve(lp).then(function(res) {
|
|
4225
|
+
if (isPollStopped(res)) {
|
|
4226
|
+
self._closeLiveStream(live, false);
|
|
4227
|
+
return res;
|
|
4228
|
+
}
|
|
4229
|
+
return self._settleLiveStream(live, res);
|
|
4230
|
+
}, function(err) {
|
|
4231
|
+
self._closeLiveStream(live, false);
|
|
4232
|
+
throw err;
|
|
4233
|
+
});
|
|
4234
|
+
wrapped.stop = function() {
|
|
4235
|
+
self._closeLiveStream(live, false);
|
|
4236
|
+
if (stopLp) stopLp();
|
|
4237
|
+
};
|
|
4238
|
+
return wrapped;
|
|
4239
|
+
}
|
|
3405
4240
|
var base = source.poll(Object.assign({ latency: POLL_INTERVAL }, opts || {}));
|
|
3406
4241
|
var lookup = chatEngineConfig().csrHistoryItemLookup;
|
|
3407
4242
|
var ident = this.host.getIdentity();
|
|
@@ -3483,6 +4318,661 @@ var ChatSession = class {
|
|
|
3483
4318
|
});
|
|
3484
4319
|
return n;
|
|
3485
4320
|
}
|
|
4321
|
+
/**
|
|
4322
|
+
* Open (or re-open) the live stream for `itemId`, or null when this poll must
|
|
4323
|
+
* not carry one.
|
|
4324
|
+
*
|
|
4325
|
+
* Re-entrant on purpose: an auth-refresh retry re-dispatches the SAME turn under
|
|
4326
|
+
* a NEW id, and a re-attach after a tab return replays an existing id from seq 0.
|
|
4327
|
+
* Either way the bytes about to arrive are a whole stream, so an existing entry
|
|
4328
|
+
* is discarded and a fresh parser takes its place - feeding a replay into the old
|
|
4329
|
+
* parser would concatenate the answer with itself.
|
|
4330
|
+
*
|
|
4331
|
+
* `ctx` IS THE TURN'S OWN IDENTITY, and every caller that has one passes it.
|
|
4332
|
+
* This used to read the LIVE getIdentity(), which is a bug of exactly the kind
|
|
4333
|
+
* _callProviderFor documents and threads its own parameters to avoid: the user
|
|
4334
|
+
* hits Send, then switches project or platform inside the ack round trip, and the
|
|
4335
|
+
* stream that opens for the OLD turn is stamped with the NEW identity. What that
|
|
4336
|
+
* costs is not cosmetic - `platform` picks which url csr-finalize is addressed
|
|
4337
|
+
* with and which extractor reads the assembled body, `projectId`/`owner` scope
|
|
4338
|
+
* the finalize itself, and `ownerKey` decides which chat the answer is painted
|
|
4339
|
+
* into. Get them from the live read at the wrong moment and the turn is finalized
|
|
4340
|
+
* against the wrong service (so its answer is never stored), parsed with the
|
|
4341
|
+
* wrong provider's extractor, or painted into a conversation it does not belong
|
|
4342
|
+
* to. The live read stays only as the fallback for a caller with nothing pinned.
|
|
4343
|
+
*/
|
|
4344
|
+
_beginLiveStream(itemId, ctx) {
|
|
4345
|
+
if (!liveStreamingEnabled()) return null;
|
|
4346
|
+
if (!itemId) {
|
|
4347
|
+
console.warn("[chat-engine] live streaming is on but the dispatch reported no item id");
|
|
4348
|
+
return null;
|
|
4349
|
+
}
|
|
4350
|
+
var pinnedPlatform = ctx && (ctx.platform === "claude" || ctx.platform === "openai") ? ctx.platform : void 0;
|
|
4351
|
+
var ident = pinnedPlatform && ctx && ctx.projectId !== void 0 && ctx.owner !== void 0 && ctx.ownerKey !== void 0 ? null : this.host.getIdentity();
|
|
4352
|
+
var platform = pinnedPlatform || (ident ? ident.platform : void 0);
|
|
4353
|
+
if (platform !== "claude" && platform !== "openai") return null;
|
|
4354
|
+
var projectId = ctx && ctx.projectId !== void 0 ? ctx.projectId : ident ? ident.projectId : "";
|
|
4355
|
+
var owner = ctx && ctx.owner !== void 0 ? ctx.owner : ident ? ident.owner : "";
|
|
4356
|
+
var ownerKey = ctx && ctx.ownerKey !== void 0 ? ctx.ownerKey : this.getHistoryCacheKey();
|
|
4357
|
+
var prev = this.liveStreams[itemId];
|
|
4358
|
+
if (prev) this._closeLiveStream(prev, false);
|
|
4359
|
+
var st = {
|
|
4360
|
+
id: itemId,
|
|
4361
|
+
ownerKey,
|
|
4362
|
+
platform,
|
|
4363
|
+
projectId,
|
|
4364
|
+
owner,
|
|
4365
|
+
parser: createSseParser(),
|
|
4366
|
+
painted: "",
|
|
4367
|
+
started: false,
|
|
4368
|
+
fed: false,
|
|
4369
|
+
ended: false,
|
|
4370
|
+
timer: null,
|
|
4371
|
+
lastPaintAt: 0,
|
|
4372
|
+
finalBody: null,
|
|
4373
|
+
transport: { socket: 0, poll: 0 }
|
|
4374
|
+
};
|
|
4375
|
+
this.liveStreams[itemId] = st;
|
|
4376
|
+
return st;
|
|
4377
|
+
}
|
|
4378
|
+
/** The chunk sink handed to skapi's poll. Raw relayed text, in order, never parsed
|
|
4379
|
+
* here: the parser owns the grammar and this owns the pacing. */
|
|
4380
|
+
_feedLiveStream(st, chunk, via) {
|
|
4381
|
+
if (st.ended || typeof chunk !== "string" || !chunk) return;
|
|
4382
|
+
st.fed = true;
|
|
4383
|
+
if (via === "socket") st.transport.socket++;
|
|
4384
|
+
else if (via === "poll") st.transport.poll++;
|
|
4385
|
+
st.parser.feed(chunk);
|
|
4386
|
+
if (st.timer) return;
|
|
4387
|
+
var self = this;
|
|
4388
|
+
var wait = st.lastPaintAt ? Math.max(0, LIVE_PAINT_MIN_MS - (nowMs() - st.lastPaintAt)) : 0;
|
|
4389
|
+
st.timer = setTimeout(function() {
|
|
4390
|
+
st.timer = null;
|
|
4391
|
+
self._paintLiveStream(st);
|
|
4392
|
+
}, wait);
|
|
4393
|
+
}
|
|
4394
|
+
/**
|
|
4395
|
+
* Write the safe prefix of the answer so far into the turn's bubble.
|
|
4396
|
+
*
|
|
4397
|
+
* notify() is spent EXACTLY ONCE per turn, on the first paint, because that is a
|
|
4398
|
+
* state change the per-bubble refresh cannot express: the bubble stops being a
|
|
4399
|
+
* "Thinking..." spinner and becomes text. Every paint after it goes through
|
|
4400
|
+
* refreshMessageBubble, which is what keeps a growing answer from rebuilding the
|
|
4401
|
+
* whole display list once a second.
|
|
4402
|
+
*/
|
|
4403
|
+
_paintLiveStream(st) {
|
|
4404
|
+
if (st.ended) return;
|
|
4405
|
+
st.lastPaintAt = nowMs();
|
|
4406
|
+
if (this.getHistoryCacheKey() !== st.ownerKey) return;
|
|
4407
|
+
var idx = this._liveTargetIndex(st.id);
|
|
4408
|
+
if (idx === -1) return;
|
|
4409
|
+
var msg = this.state.messages[idx];
|
|
4410
|
+
if (!msg) return;
|
|
4411
|
+
var snap = st.parser.snapshot();
|
|
4412
|
+
var next = liveSafePrefix(snap.text);
|
|
4413
|
+
if (next.length <= st.painted.length) return;
|
|
4414
|
+
var prev = st.painted;
|
|
4415
|
+
st.painted = next;
|
|
4416
|
+
var grew = next.length - prev.length;
|
|
4417
|
+
var animate = grew > 0 && grew <= LIVE_TYPE_MAX_STEP;
|
|
4418
|
+
if (animate) {
|
|
4419
|
+
if (!msg._localId) msg._localId = this._newLocalId();
|
|
4420
|
+
if (!msg._streaming) {
|
|
4421
|
+
msg._streaming = true;
|
|
4422
|
+
this.host.notify();
|
|
4423
|
+
}
|
|
4424
|
+
this.enqueueTypewrite(idx, next, msg._localId, prev);
|
|
4425
|
+
} else {
|
|
4426
|
+
msg.content = next;
|
|
4427
|
+
if (!msg._streaming) {
|
|
4428
|
+
msg._streaming = true;
|
|
4429
|
+
this.host.notify();
|
|
4430
|
+
} else this.host.refreshMessageBubble(idx);
|
|
4431
|
+
}
|
|
4432
|
+
this.host.scrollToBottomIfSticky();
|
|
4433
|
+
this._reportLiveStream(st, st.started ? "update" : "start", snap, next);
|
|
4434
|
+
st.started = true;
|
|
4435
|
+
}
|
|
4436
|
+
/** The bubble a live stream paints into: the turn's pending assistant placeholder,
|
|
4437
|
+
* found by server item id. Not by _localId, deliberately - a history refetch
|
|
4438
|
+
* replaces the local copy with the server's, and only the id survives that. */
|
|
4439
|
+
_liveTargetIndex(itemId) {
|
|
4440
|
+
return this.state.messages.findIndex(function(m) {
|
|
4441
|
+
return !!m && m.role === "assistant" && !m.isBackgroundTask && m._serverItemId === itemId && (!!m.isPending || !!m._streaming);
|
|
4442
|
+
});
|
|
4443
|
+
}
|
|
4444
|
+
/** Hand the host its optional observation update. Guarded: this runs on the paint
|
|
4445
|
+
* path, and a throwing hook must not cost the user the rest of their answer. */
|
|
4446
|
+
_reportLiveStream(st, phase, snap, text) {
|
|
4447
|
+
var hook = chatEngineConfig().onLiveStreamUpdate;
|
|
4448
|
+
if (!hook) return;
|
|
4449
|
+
try {
|
|
4450
|
+
hook({
|
|
4451
|
+
serverItemId: st.id,
|
|
4452
|
+
ownerKey: st.ownerKey,
|
|
4453
|
+
phase,
|
|
4454
|
+
text,
|
|
4455
|
+
thinkingText: snap && snap.thinkingText || "",
|
|
4456
|
+
toolNames: snap && snap.toolNames ? snap.toolNames.slice() : [],
|
|
4457
|
+
complete: !!(snap && snap.complete),
|
|
4458
|
+
// Reported alongside `complete`, never instead of it: a host drawing
|
|
4459
|
+
// "still arriving" wants complete, a host drawing "this answer is
|
|
4460
|
+
// partial" wants this one, and an `error` frame is the case where the
|
|
4461
|
+
// two disagree. See sse.ts answerComplete.
|
|
4462
|
+
answerComplete: !!(snap && snap.answerComplete),
|
|
4463
|
+
errored: !!(snap && snap.errored),
|
|
4464
|
+
transport: { socket: st.transport.socket, poll: st.transport.poll }
|
|
4465
|
+
});
|
|
4466
|
+
} catch (e) {
|
|
4467
|
+
console.warn("[chat-engine] onLiveStreamUpdate threw", e);
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
4470
|
+
/** Stop painting and (when the turn really ended) assemble the body. `finished`
|
|
4471
|
+
* is false for a stream being discarded rather than settled: a retry replacing
|
|
4472
|
+
* it, or a stop, neither of which has an answer to assemble. */
|
|
4473
|
+
_closeLiveStream(st, finished) {
|
|
4474
|
+
var first = !st.ended;
|
|
4475
|
+
if (st.timer) {
|
|
4476
|
+
clearTimeout(st.timer);
|
|
4477
|
+
st.timer = null;
|
|
4478
|
+
}
|
|
4479
|
+
if (first) {
|
|
4480
|
+
st.ended = true;
|
|
4481
|
+
if (finished && st.fed) {
|
|
4482
|
+
st.parser.end();
|
|
4483
|
+
st.finalBody = st.parser.finalBody();
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
if (this.liveStreams[st.id] === st) delete this.liveStreams[st.id];
|
|
4487
|
+
if (first && st.started) this._reportLiveStream(st, "end", st.parser.snapshot(), "");
|
|
4488
|
+
if (this.getHistoryCacheKey() !== st.ownerKey) return;
|
|
4489
|
+
var idx = this._liveTargetIndex(st.id);
|
|
4490
|
+
if (idx !== -1 && this.state.messages[idx] && this.state.messages[idx]._streaming) {
|
|
4491
|
+
this.state.messages[idx]._streaming = false;
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
/**
|
|
4495
|
+
* Settle a streamed turn: end the parse, decide the body the rest of the session
|
|
4496
|
+
* will read, and release the chunks.
|
|
4497
|
+
*
|
|
4498
|
+
* The substitution is one-directional and never a merge. A response that is a
|
|
4499
|
+
* real stored body (a buffered turn, or a streamed one somebody already
|
|
4500
|
+
* finalized) is returned untouched, because that is the destination's own answer
|
|
4501
|
+
* and the stream is not entitled to overwrite it. Only a STATUS ENVELOPE - the
|
|
4502
|
+
* shape a streamed row settles as, having stored nothing - is replaced, and then
|
|
4503
|
+
* by the assembled body, which every caller downstream reads with the same
|
|
4504
|
+
* extractor it uses for a buffered reply. Idempotent, because it is reached both
|
|
4505
|
+
* through the poll's onResponse and through the promise it resolves.
|
|
4506
|
+
*/
|
|
4507
|
+
_settleLiveStream(st, response) {
|
|
4508
|
+
this._closeLiveStream(st, true);
|
|
4509
|
+
if (!isCsrStatusEnvelope(response)) return response;
|
|
4510
|
+
if (response.status !== "resolved") return response;
|
|
4511
|
+
if (!this._mayFinalize(st)) this._rec().incomplete[st.id] = true;
|
|
4512
|
+
if (st.finalBody == null) return response;
|
|
4513
|
+
this._finalizeStreamedTurn(st);
|
|
4514
|
+
return st.finalBody;
|
|
4515
|
+
}
|
|
4516
|
+
/**
|
|
4517
|
+
* May this parse be STORED as the turn's permanent answer?
|
|
4518
|
+
*
|
|
4519
|
+
* THE FAILURE THIS PREVENTS. Finalizing does two things at once: it stores what
|
|
4520
|
+
* you give it as the row's result, and it DELETES the chunks it was assembled
|
|
4521
|
+
* from. So finalizing a truncated parse is not a cosmetic loss, it is the
|
|
4522
|
+
* permanent one: the truncation becomes the stored answer and the only copy of
|
|
4523
|
+
* the missing part is deleted in the same call. And a truncated parse is a shape
|
|
4524
|
+
* this repo has already paid for - a degraded chunk read (the poller degrades to
|
|
4525
|
+
* "no chunks this tick, more=true" on any transient chunk-table error, and caps
|
|
4526
|
+
* a long answer at 500k characters per response) can hand the settle a stream
|
|
4527
|
+
* that stopped mid-answer. The row can settle 'resolved' on top of that, because
|
|
4528
|
+
* the ROW's status describes the destination's request, not the client's read of
|
|
4529
|
+
* it.
|
|
4530
|
+
*
|
|
4531
|
+
* THE POLICY ITSELF IS mayKeepStreamedAnswer (top of this file), shared with the
|
|
4532
|
+
* recovery path so the two cannot drift apart again - they did, and the drift was
|
|
4533
|
+
* silent: the live settle refused a failed turn while the recovery finalized one.
|
|
4534
|
+
* What is local to this method is only the two things the free function cannot
|
|
4535
|
+
* know: that there is an assembled body at all, and that this call site is
|
|
4536
|
+
* reached only on a row that settled 'resolved' (the caller returns before it
|
|
4537
|
+
* otherwise), which is the status it therefore states.
|
|
4538
|
+
*
|
|
4539
|
+
* The test the policy applies is deliberately NOT `complete`: a terminal event
|
|
4540
|
+
* arrived and the answer finished are two claims, and an `error` frame satisfies
|
|
4541
|
+
* the first while truncating the second. See sse.ts's answerComplete.
|
|
4542
|
+
*/
|
|
4543
|
+
_mayFinalize(st) {
|
|
4544
|
+
if (st.finalBody == null) return false;
|
|
4545
|
+
return mayKeepStreamedAnswer(st.parser.snapshot(), "resolved");
|
|
4546
|
+
}
|
|
4547
|
+
/**
|
|
4548
|
+
* Store the assembled body as the version history keeps, which is also what
|
|
4549
|
+
* releases this request's chunks.
|
|
4550
|
+
*
|
|
4551
|
+
* The ASSEMBLED BODY and not the extracted text, because the row is read back by
|
|
4552
|
+
* mapHistoryListToMessages through extractClaudeText / extractOpenAIText: storing
|
|
4553
|
+
* the provider's own document is what makes a streamed turn indistinguishable
|
|
4554
|
+
* from a buffered one on the next load, with no branch anywhere in the mapper.
|
|
4555
|
+
*
|
|
4556
|
+
* BEST EFFORT, and loudly so: the answer is already on screen and already in the
|
|
4557
|
+
* history cache by the time this fires. A failure costs the chunks (they stay,
|
|
4558
|
+
* and the turn stays re-readable) and a row that reads back empty, never the
|
|
4559
|
+
* user's answer in front of them.
|
|
4560
|
+
*
|
|
4561
|
+
* WHAT IS DELIBERATELY NEVER FINALIZED, because finalize is also the only way to
|
|
4562
|
+
* release chunks and it is tempting to reach for it as a cleanup:
|
|
4563
|
+
*
|
|
4564
|
+
* - an INCOMPLETE parse (see _mayFinalize). Storing a truncation makes it
|
|
4565
|
+
* permanent AND deletes the part that was missing from it. A stream killed by
|
|
4566
|
+
* an `error` frame is one of these however terminal it looks: the frame ends
|
|
4567
|
+
* the stream, so `complete` is true, while the text is only what arrived
|
|
4568
|
+
* before the error. That is why the gate reads answerComplete.
|
|
4569
|
+
* - a FAILED turn. Its chunks hold the part of the answer that did arrive,
|
|
4570
|
+
* which is the only copy of that text there is, and the two ways to release
|
|
4571
|
+
* them both cost something real: storing the partial makes a truncated answer
|
|
4572
|
+
* the turn's permanent history AND masks the failure on read (csr-poll hands
|
|
4573
|
+
* back a finalized body before it ever looks at the row's error, so the turn
|
|
4574
|
+
* would read back as a clean short answer), while storing the error throws
|
|
4575
|
+
* the partial away outright. Keeping them costs storage on rows that produced
|
|
4576
|
+
* bytes and then failed, which is rare - a failure before the first byte (a
|
|
4577
|
+
* wrong API key, the common case) has no chunks to keep - and the poller
|
|
4578
|
+
* hands those chunks back alongside the error on every later read, so nothing
|
|
4579
|
+
* is stranded, only retained. Retention is the honest trade here; deletion is
|
|
4580
|
+
* not reversible.
|
|
4581
|
+
* - a CANCELLED turn, for the same reason plus one: the user's Stop means the
|
|
4582
|
+
* half answer is to be discarded, so writing it into history as the kept
|
|
4583
|
+
* version would resurrect exactly what the stop was for.
|
|
4584
|
+
*/
|
|
4585
|
+
_finalizeStreamedTurn(st) {
|
|
4586
|
+
if (st.finalized) return;
|
|
4587
|
+
if (!this._mayFinalize(st)) return;
|
|
4588
|
+
var fin = chatEngineConfig().clientSecretRequestFinalize;
|
|
4589
|
+
if (!fin || st.finalBody == null) return;
|
|
4590
|
+
st.finalized = true;
|
|
4591
|
+
var url = st.platform === "openai" ? OPENAI_RESPONSES_API_URL : ANTHROPIC_MESSAGES_API_URL;
|
|
4592
|
+
try {
|
|
4593
|
+
Promise.resolve(fin(st.id, st.finalBody, {
|
|
4594
|
+
url,
|
|
4595
|
+
method: "POST",
|
|
4596
|
+
service: st.projectId,
|
|
4597
|
+
owner: st.owner
|
|
4598
|
+
})).catch(function(err) {
|
|
4599
|
+
console.warn("[chat-engine] clientSecretRequestFinalize failed", err);
|
|
4600
|
+
});
|
|
4601
|
+
} catch (e) {
|
|
4602
|
+
console.warn("[chat-engine] clientSecretRequestFinalize threw", e);
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4605
|
+
/** Painted-but-unsettled live text on a bubble, for the typewriter to resume from.
|
|
4606
|
+
* A pending assistant placeholder is created with content '' by every path that
|
|
4607
|
+
* makes one, so non-empty content on one can only have been painted here. */
|
|
4608
|
+
_paintedTextAt(idx) {
|
|
4609
|
+
var m = idx >= 0 ? this.state.messages[idx] : void 0;
|
|
4610
|
+
if (!m || m.role !== "assistant" || typeof m.content !== "string") return "";
|
|
4611
|
+
return m.content;
|
|
4612
|
+
}
|
|
4613
|
+
/** The recovery bookkeeping, created on first touch.
|
|
4614
|
+
*
|
|
4615
|
+
* LAZY, not constructor-initialised, and for a concrete reason: ChatSession is
|
|
4616
|
+
* also built with Object.create(ChatSession.prototype) by the engine's own test
|
|
4617
|
+
* harnesses, which drive one method against a hand-built state rather than a
|
|
4618
|
+
* whole session. A field only the constructor creates is undefined there, and
|
|
4619
|
+
* the method that reaches for it throws, turning a test of the settle into a
|
|
4620
|
+
* crash about bookkeeping. */
|
|
4621
|
+
_rec() {
|
|
4622
|
+
if (!this._streamRecovery) this._streamRecovery = { incomplete: {}, attempted: {}, inflight: {}, failed: {}, queue: [], running: false };
|
|
4623
|
+
return this._streamRecovery;
|
|
4624
|
+
}
|
|
4625
|
+
/**
|
|
4626
|
+
* Put this session's fetching state onto the turn's bubble, so a view can tell a
|
|
4627
|
+
* loader that means something from one that means nothing.
|
|
4628
|
+
*
|
|
4629
|
+
* ONLY EVER ONTO A STILL-MARKED BUBBLE. Once `_streamPending` is off the turn has
|
|
4630
|
+
* an answer (or was proven to have none) and this says nothing about it; writing
|
|
4631
|
+
* it there would leave a stale 'active' on a settled bubble forever.
|
|
4632
|
+
*
|
|
4633
|
+
* host.notify() is what redraws the widget, whose renderer is imperative. It is a
|
|
4634
|
+
* no-op in agent.vue, whose state is a Vue reactive() - the property write above
|
|
4635
|
+
* is what redraws there. Both are covered by doing both, and neither is a
|
|
4636
|
+
* substitute for the other.
|
|
4637
|
+
*/
|
|
4638
|
+
_markRecoveryPhase(itemId, phase) {
|
|
4639
|
+
var changed = false;
|
|
4640
|
+
for (var i = 0; i < this.state.messages.length; i++) {
|
|
4641
|
+
var m = this.state.messages[i];
|
|
4642
|
+
if (!m || m.role !== "assistant" || m._serverItemId !== itemId || !m._streamPending) continue;
|
|
4643
|
+
var next = phase === null ? void 0 : phase;
|
|
4644
|
+
if (m._streamRecovery === next) continue;
|
|
4645
|
+
if (next === void 0) delete m._streamRecovery;
|
|
4646
|
+
else m._streamRecovery = next;
|
|
4647
|
+
changed = true;
|
|
4648
|
+
}
|
|
4649
|
+
if (changed) this.host.notify();
|
|
4650
|
+
}
|
|
4651
|
+
/**
|
|
4652
|
+
* Let LOCAL answers survive a freshly-mapped page whose copies of them are
|
|
4653
|
+
* authoritative-but-empty. Call with the page BEFORE it replaces or merges into
|
|
4654
|
+
* state.messages; mutates the page's bubbles in place.
|
|
4655
|
+
*
|
|
4656
|
+
* The adoption itself is history.ts's adoptLocalAnswerIntoPage (shared, so the
|
|
4657
|
+
* clients' own mappers cannot fork it). What lives here is the one thing the
|
|
4658
|
+
* pure function cannot know: whether the local text is the WHOLE answer. Text
|
|
4659
|
+
* left by a stream that ended without a terminal event is not, so that bubble
|
|
4660
|
+
* keeps its marker and gets read back even though it has content - otherwise a
|
|
4661
|
+
* truncated answer would adopt itself over the row and never be corrected.
|
|
4662
|
+
*/
|
|
4663
|
+
_adoptLocalAnswers(mapped, loadKey) {
|
|
4664
|
+
if (!mapped || !mapped.length) return;
|
|
4665
|
+
var pendingIncoming = [];
|
|
4666
|
+
for (var i = 0; i < mapped.length; i++) {
|
|
4667
|
+
if (mapped[i] && mapped[i]._streamPending) pendingIncoming.push(mapped[i]);
|
|
4668
|
+
}
|
|
4669
|
+
if (!pendingIncoming.length) return;
|
|
4670
|
+
var locals = {};
|
|
4671
|
+
for (var j = 0; j < this.state.messages.length; j++) {
|
|
4672
|
+
var lm = this.state.messages[j];
|
|
4673
|
+
if (!lm || lm.role !== "assistant" || !lm._serverItemId) continue;
|
|
4674
|
+
if (lm._ownerKey !== void 0 && loadKey !== void 0 && lm._ownerKey !== loadKey) continue;
|
|
4675
|
+
if (locals[lm._serverItemId] === void 0) locals[lm._serverItemId] = lm;
|
|
4676
|
+
}
|
|
4677
|
+
for (var k = 0; k < pendingIncoming.length; k++) {
|
|
4678
|
+
var inc = pendingIncoming[k];
|
|
4679
|
+
var id = inc._serverItemId;
|
|
4680
|
+
if (!id) continue;
|
|
4681
|
+
var local = locals[id];
|
|
4682
|
+
if (!local) continue;
|
|
4683
|
+
if (!adoptLocalAnswerIntoPage(inc, local)) continue;
|
|
4684
|
+
if (this._rec().incomplete[id]) inc._streamPending = true;
|
|
4685
|
+
}
|
|
4686
|
+
for (var p = 0; p < pendingIncoming.length; p++) {
|
|
4687
|
+
var pi = pendingIncoming[p];
|
|
4688
|
+
if (!pi._streamPending || !pi._serverItemId) continue;
|
|
4689
|
+
var phase = this._recoveryPhaseFor(pi._serverItemId);
|
|
4690
|
+
if (phase === null) delete pi._streamRecovery;
|
|
4691
|
+
else pi._streamRecovery = phase;
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
4694
|
+
/**
|
|
4695
|
+
* This session's fetching state for one turn, from the bookkeeping rather than
|
|
4696
|
+
* from any bubble. A queued entry counts as 'active': it is committed to be read,
|
|
4697
|
+
* serially, and the reader has no way to tell "being read" from "next in line"
|
|
4698
|
+
* apart from the wait.
|
|
4699
|
+
*/
|
|
4700
|
+
_recoveryPhaseFor(itemId) {
|
|
4701
|
+
var rec = this._rec();
|
|
4702
|
+
if (rec.inflight[itemId]) return "active";
|
|
4703
|
+
for (var i = 0; i < rec.queue.length; i++) if (rec.queue[i].id === itemId) return "active";
|
|
4704
|
+
if (rec.failed[itemId]) return "failed";
|
|
4705
|
+
return null;
|
|
4706
|
+
}
|
|
4707
|
+
/**
|
|
4708
|
+
* PUBLIC DELEGATE, for a client that maps and merges its own history page.
|
|
4709
|
+
*
|
|
4710
|
+
* agent.vue keeps a forked mapper and a forked first-page merge (its mount path
|
|
4711
|
+
* runs them, while resumePolling routes through loadHistory below), so both
|
|
4712
|
+
* paths are live for the SAME row inside one component. Adoption is part of the
|
|
4713
|
+
* merge contract, not an optional extra: without it that fork erases a streamed
|
|
4714
|
+
* answer off the screen on every turn, which is the whole of MAJOR 3.
|
|
4715
|
+
*
|
|
4716
|
+
* Exposed rather than reimplemented because the rule needs the session's own
|
|
4717
|
+
* `incomplete` set, which the pure helper (history.ts adoptLocalAnswerIntoPage)
|
|
4718
|
+
* cannot see. A client that reached for the helper alone would adopt a TRUNCATED
|
|
4719
|
+
* answer over the row and clear the marker that would have gone back for the
|
|
4720
|
+
* rest - a fork that reads as correct and loses text.
|
|
4721
|
+
*
|
|
4722
|
+
* Call it exactly where loadHistory does: on the freshly mapped page, after
|
|
4723
|
+
* applyHydratedBodies and BEFORE the page replaces or merges into state.messages.
|
|
4724
|
+
*/
|
|
4725
|
+
adoptLocalAnswers(mapped, loadKey) {
|
|
4726
|
+
this._adoptLocalAnswers(mapped, loadKey);
|
|
4727
|
+
}
|
|
4728
|
+
/**
|
|
4729
|
+
* Queue the on-screen turns whose answer is only in the chunk store, newest
|
|
4730
|
+
* first, and start draining. Never blocks and never throws.
|
|
4731
|
+
*
|
|
4732
|
+
* `ownerKey` is the chat the queue entries belong to, snapshotted by the caller:
|
|
4733
|
+
* a recovery that lands after the user has moved on writes into that chat's
|
|
4734
|
+
* cache, never into whatever list is on screen by then.
|
|
4735
|
+
*/
|
|
4736
|
+
_scheduleStreamRecovery(ownerKey, platform, projectId, owner) {
|
|
4737
|
+
if (!streamRecoveryEnabled()) return;
|
|
4738
|
+
var rec = this._rec();
|
|
4739
|
+
var wanted = [];
|
|
4740
|
+
for (var i = this.state.messages.length - 1; i >= 0; i--) {
|
|
4741
|
+
var m = this.state.messages[i];
|
|
4742
|
+
if (!m || m.role !== "assistant" || !m._streamPending || !m._serverItemId) continue;
|
|
4743
|
+
var id = m._serverItemId;
|
|
4744
|
+
if (rec.attempted[id]) continue;
|
|
4745
|
+
if (this.liveStreams[id]) continue;
|
|
4746
|
+
if (rec.queue.some(function(e) {
|
|
4747
|
+
return e.id === id;
|
|
4748
|
+
})) continue;
|
|
4749
|
+
wanted.push(id);
|
|
4750
|
+
if (wanted.length >= STREAM_RECOVERY_PER_LOAD) break;
|
|
4751
|
+
}
|
|
4752
|
+
if (!wanted.length) return;
|
|
4753
|
+
for (var w = 0; w < wanted.length; w++) {
|
|
4754
|
+
rec.queue.push({ id: wanted[w], ownerKey, platform, projectId, owner });
|
|
4755
|
+
this._markRecoveryPhase(wanted[w], "active");
|
|
4756
|
+
}
|
|
4757
|
+
this._drainStreamRecovery();
|
|
4758
|
+
}
|
|
4759
|
+
/**
|
|
4760
|
+
* PUBLIC DELEGATE, the other half of what a forked history path needs.
|
|
4761
|
+
*
|
|
4762
|
+
* Same reason as adoptLocalAnswers: agent.vue's mount path never calls
|
|
4763
|
+
* loadHistory, so without this its pages would MARK unfinalized streamed turns
|
|
4764
|
+
* and then never read them back - CRITICAL 1 left unfixed on the client's
|
|
4765
|
+
* primary path, with the marker making it look handled.
|
|
4766
|
+
*
|
|
4767
|
+
* Takes the load's SNAPSHOTTED identity rather than reading it live, and that is
|
|
4768
|
+
* the reason this exists instead of the caller looping over recoverStreamedAnswer:
|
|
4769
|
+
* that one reads getIdentity() at call time (right, for an on-demand affordance
|
|
4770
|
+
* the user just clicked), which after a project switch racing the load would
|
|
4771
|
+
* finalize the turn against the project they switched TO. Call it AFTER the page
|
|
4772
|
+
* is rendered and the loading flags are cleared - it must never hold up the
|
|
4773
|
+
* conversation it belongs to.
|
|
4774
|
+
*/
|
|
4775
|
+
scheduleStreamRecovery(ownerKey, platform, projectId, owner) {
|
|
4776
|
+
this._scheduleStreamRecovery(ownerKey, platform, projectId, owner);
|
|
4777
|
+
}
|
|
4778
|
+
/** Serial drain of the recovery queue. Each entry is one full chunk read. */
|
|
4779
|
+
_drainStreamRecovery() {
|
|
4780
|
+
var rec = this._rec();
|
|
4781
|
+
if (rec.running) return;
|
|
4782
|
+
var next = rec.queue.shift();
|
|
4783
|
+
if (!next) return;
|
|
4784
|
+
rec.running = true;
|
|
4785
|
+
var self = this;
|
|
4786
|
+
this._readBackStreamedTurn(next.id, next.ownerKey, next.platform, next.projectId, next.owner).catch(function() {
|
|
4787
|
+
}).then(function() {
|
|
4788
|
+
self._rec().running = false;
|
|
4789
|
+
self._drainStreamRecovery();
|
|
4790
|
+
});
|
|
4791
|
+
}
|
|
4792
|
+
/**
|
|
4793
|
+
* Read one unfinalized streamed turn back out of the chunk store and put its
|
|
4794
|
+
* answer where the turn's answer belongs.
|
|
4795
|
+
*
|
|
4796
|
+
* Public because the cap above is deliberately small: a host that wants to offer
|
|
4797
|
+
* "load the rest" on an older recoverable turn calls this with its
|
|
4798
|
+
* `_serverItemId`, and gets the same path the automatic recovery uses. Safe to
|
|
4799
|
+
* call for an id that turns out not to be recoverable, and safe to call twice -
|
|
4800
|
+
* a second call while the first is still in flight is a no-op.
|
|
4801
|
+
*
|
|
4802
|
+
* THIS IS THE USER ASKING, and that is why it passes `manual`. The automatic
|
|
4803
|
+
* recovery refuses a row it has already tried, so that a re-render, or the
|
|
4804
|
+
* history load that every visibilitychange fires, cannot loop on the same
|
|
4805
|
+
* chunks. A click is neither of those: it is one bounded request that a person
|
|
4806
|
+
* asked for, and applying the loop guard to it made the affordance a button that
|
|
4807
|
+
* silently did nothing for exactly the rows most likely to have it - every row
|
|
4808
|
+
* an earlier read touched and could not settle.
|
|
4809
|
+
*/
|
|
4810
|
+
recoverStreamedAnswer(itemId) {
|
|
4811
|
+
if (!itemId) return Promise.resolve();
|
|
4812
|
+
var id = this.host.getIdentity();
|
|
4813
|
+
var platform = id && id.platform === "openai" ? "openai" : "claude";
|
|
4814
|
+
return this._readBackStreamedTurn(itemId, this.getHistoryCacheKey(), platform, id ? id.projectId : "", id ? id.owner : "", true);
|
|
4815
|
+
}
|
|
4816
|
+
_readBackStreamedTurn(itemId, ownerKey, platform, projectId, owner, manual) {
|
|
4817
|
+
var cfg = chatEngineConfig();
|
|
4818
|
+
var read = cfg.clientSecretRequestStream;
|
|
4819
|
+
if (!read || !itemId) return Promise.resolve();
|
|
4820
|
+
if (this._rec().inflight[itemId]) return Promise.resolve();
|
|
4821
|
+
if (!manual && this._rec().attempted[itemId]) {
|
|
4822
|
+
this._markRecoveryPhase(itemId, null);
|
|
4823
|
+
return Promise.resolve();
|
|
4824
|
+
}
|
|
4825
|
+
this._rec().attempted[itemId] = true;
|
|
4826
|
+
this._rec().inflight[itemId] = true;
|
|
4827
|
+
delete this._rec().failed[itemId];
|
|
4828
|
+
this._markRecoveryPhase(itemId, "active");
|
|
4829
|
+
var self = this;
|
|
4830
|
+
var url = platform === "openai" ? OPENAI_RESPONSES_API_URL : ANTHROPIC_MESSAGES_API_URL;
|
|
4831
|
+
var parser = createSseParser();
|
|
4832
|
+
var fed = false;
|
|
4833
|
+
return Promise.resolve(read(itemId, {
|
|
4834
|
+
url,
|
|
4835
|
+
method: "POST",
|
|
4836
|
+
service: projectId,
|
|
4837
|
+
owner,
|
|
4838
|
+
since: 0,
|
|
4839
|
+
onStream: function(chunk) {
|
|
4840
|
+
if (typeof chunk !== "string" || !chunk) return;
|
|
4841
|
+
fed = true;
|
|
4842
|
+
parser.feed(chunk);
|
|
4843
|
+
}
|
|
4844
|
+
})).then(function(res) {
|
|
4845
|
+
if (isPollStopped(res)) {
|
|
4846
|
+
delete self._rec().attempted[itemId];
|
|
4847
|
+
delete self._rec().inflight[itemId];
|
|
4848
|
+
self._markRecoveryPhase(itemId, null);
|
|
4849
|
+
return;
|
|
4850
|
+
}
|
|
4851
|
+
var envelope = isCsrStatusEnvelope(res);
|
|
4852
|
+
var body = null;
|
|
4853
|
+
if (res && !envelope) {
|
|
4854
|
+
body = res;
|
|
4855
|
+
} else if (fed) {
|
|
4856
|
+
parser.end();
|
|
4857
|
+
body = parser.finalBody();
|
|
4858
|
+
}
|
|
4859
|
+
var snap = parser.snapshot();
|
|
4860
|
+
var fromRow = !!(res && !envelope);
|
|
4861
|
+
var rowStatus = envelope && typeof res.status === "string" ? res.status : void 0;
|
|
4862
|
+
var degraded = !!(envelope && res && res.more === true);
|
|
4863
|
+
var store = !fromRow && !degraded && body != null && mayKeepStreamedAnswer(snap, rowStatus);
|
|
4864
|
+
delete self._rec().inflight[itemId];
|
|
4865
|
+
delete self._rec().failed[itemId];
|
|
4866
|
+
self._markRecoveryPhase(itemId, null);
|
|
4867
|
+
if (degraded) {
|
|
4868
|
+
delete self._rec().attempted[itemId];
|
|
4869
|
+
}
|
|
4870
|
+
self._applyRecoveredAnswer(itemId, ownerKey, platform, projectId, owner, body, store, degraded);
|
|
4871
|
+
}, function(err) {
|
|
4872
|
+
console.warn("[chat-engine] could not read back a streamed turn", itemId, err);
|
|
4873
|
+
delete self._rec().attempted[itemId];
|
|
4874
|
+
delete self._rec().inflight[itemId];
|
|
4875
|
+
self._rec().failed[itemId] = true;
|
|
4876
|
+
self._markRecoveryPhase(itemId, "failed");
|
|
4877
|
+
});
|
|
4878
|
+
}
|
|
4879
|
+
/**
|
|
4880
|
+
* Write a recovered answer into the turn's bubble (or into the owning chat's
|
|
4881
|
+
* cache when the reader has moved on), then store it as the version history
|
|
4882
|
+
* keeps.
|
|
4883
|
+
*
|
|
4884
|
+
* FINALIZING IS WHAT MAKES THIS RUN ONCE. It copies the answer onto the row and
|
|
4885
|
+
* releases the chunks, so the next load reads an ordinary turn and no recovery is
|
|
4886
|
+
* scheduled for it ever again, by anyone, in any tab. `store` is the caller's
|
|
4887
|
+
* decision and carries two gates at once: mayKeepStreamedAnswer, the SAME keep
|
|
4888
|
+
* policy the live settle applies (an incomplete, errored or failed read is shown
|
|
4889
|
+
* but never stored, because storing it would make the truncation permanent and
|
|
4890
|
+
* delete the part that was missing), and whether the body is new at all (one
|
|
4891
|
+
* that came off the row is already stored).
|
|
4892
|
+
*/
|
|
4893
|
+
_applyRecoveredAnswer(itemId, ownerKey, platform, projectId, owner, body, store, degraded) {
|
|
4894
|
+
var text = "";
|
|
4895
|
+
var isErr = isErrorResponseBody(body);
|
|
4896
|
+
if (body != null && !isErr) {
|
|
4897
|
+
text = ((platform === "openai" ? extractOpenAIText(body) : extractClaudeText(body)) || "").trim();
|
|
4898
|
+
}
|
|
4899
|
+
if (!text && !isErr) {
|
|
4900
|
+
if (degraded) {
|
|
4901
|
+
return;
|
|
4902
|
+
}
|
|
4903
|
+
this._clearStreamPendingMark(itemId, ownerKey, true);
|
|
4904
|
+
return;
|
|
4905
|
+
}
|
|
4906
|
+
var reply = isErr ? { role: "assistant", content: getErrorMessage(body), isError: true, _serverItemId: itemId } : { role: "assistant", content: text, _serverItemId: itemId };
|
|
4907
|
+
if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
|
|
4908
|
+
this._applyReplyToCache(ownerKey, reply, itemId);
|
|
4909
|
+
} else {
|
|
4910
|
+
var idx = -1;
|
|
4911
|
+
for (var i = 0; i < this.state.messages.length; i++) {
|
|
4912
|
+
var m = this.state.messages[i];
|
|
4913
|
+
if (m && m.role === "assistant" && m._serverItemId === itemId) {
|
|
4914
|
+
idx = i;
|
|
4915
|
+
break;
|
|
4916
|
+
}
|
|
4917
|
+
}
|
|
4918
|
+
if (idx === -1) {
|
|
4919
|
+
this._applyReplyToCache(ownerKey, reply, itemId);
|
|
4920
|
+
} else {
|
|
4921
|
+
var prev = this.state.messages[idx];
|
|
4922
|
+
if (prev._ts !== void 0) reply._ts = prev._ts;
|
|
4923
|
+
if (prev._ownerKey !== void 0) reply._ownerKey = prev._ownerKey;
|
|
4924
|
+
this.state.messages[idx] = reply;
|
|
4925
|
+
this.updateHistoryCache();
|
|
4926
|
+
this.host.notify();
|
|
4927
|
+
}
|
|
4928
|
+
}
|
|
4929
|
+
if (!degraded) delete this._rec().incomplete[itemId];
|
|
4930
|
+
if (!store || body == null || isErr) return;
|
|
4931
|
+
var fin = chatEngineConfig().clientSecretRequestFinalize;
|
|
4932
|
+
if (!fin) return;
|
|
4933
|
+
var url = platform === "openai" ? OPENAI_RESPONSES_API_URL : ANTHROPIC_MESSAGES_API_URL;
|
|
4934
|
+
try {
|
|
4935
|
+
Promise.resolve(fin(itemId, body, { url, method: "POST", service: projectId, owner })).catch(function(err) {
|
|
4936
|
+
console.warn("[chat-engine] finalize of a recovered turn failed", err);
|
|
4937
|
+
});
|
|
4938
|
+
} catch (e) {
|
|
4939
|
+
console.warn("[chat-engine] finalize of a recovered turn threw", e);
|
|
4940
|
+
}
|
|
4941
|
+
}
|
|
4942
|
+
/**
|
|
4943
|
+
* Take the "answer is elsewhere" marker off a turn once it is settled one way or
|
|
4944
|
+
* the other. `drop` removes an assistant bubble that turned out to have no answer
|
|
4945
|
+
* at all, which restores exactly the list the mapper used to produce for such a
|
|
4946
|
+
* row (none), rather than leaving a permanently empty bubble behind.
|
|
4947
|
+
*
|
|
4948
|
+
* ONLY EVER CALLED FOR A TURN THAT WAS ACTUALLY READ. The marker is the one thing
|
|
4949
|
+
* that keeps an unrecovered answer reachable, so it comes off only on the strength
|
|
4950
|
+
* of an answer (the recovery wrote one) or of a read that came back empty. A read
|
|
4951
|
+
* that FAILED, or one that was STOPPED, knows neither, and taking the marker off
|
|
4952
|
+
* on either of those is how a bubble ends up empty forever with its answer still
|
|
4953
|
+
* in the chunk table. `drop` is likewise never passed for a bubble that HAS
|
|
4954
|
+
* content: an empty row is an empty turn, a failed read is not.
|
|
4955
|
+
*/
|
|
4956
|
+
_clearStreamPendingMark(itemId, ownerKey, drop) {
|
|
4957
|
+
if (ownerKey && this.getHistoryCacheKey() !== ownerKey) return;
|
|
4958
|
+
var changed = false;
|
|
4959
|
+
for (var i = this.state.messages.length - 1; i >= 0; i--) {
|
|
4960
|
+
var m = this.state.messages[i];
|
|
4961
|
+
if (!m || m.role !== "assistant" || m._serverItemId !== itemId) continue;
|
|
4962
|
+
if (!m._streamPending) continue;
|
|
4963
|
+
if (drop && !m.content) {
|
|
4964
|
+
this.state.messages.splice(i, 1);
|
|
4965
|
+
changed = true;
|
|
4966
|
+
continue;
|
|
4967
|
+
}
|
|
4968
|
+
m._streamPending = false;
|
|
4969
|
+
changed = true;
|
|
4970
|
+
}
|
|
4971
|
+
if (changed) {
|
|
4972
|
+
this.updateHistoryCache();
|
|
4973
|
+
this.host.notify();
|
|
4974
|
+
}
|
|
4975
|
+
}
|
|
3486
4976
|
/**
|
|
3487
4977
|
* Stop and forget one item's poll. Used after a cancel: the row is either gone
|
|
3488
4978
|
* (cancelled while queued) or flagged cancelled (cancelled while running), so
|
|
@@ -3797,7 +5287,12 @@ var ChatSession = class {
|
|
|
3797
5287
|
dispatchItemId = initial.id;
|
|
3798
5288
|
if (typeof params.onItemId === "function") params.onItemId(initial.id);
|
|
3799
5289
|
}
|
|
3800
|
-
var dp = self._fgPollWithEarlyProbe(initial, initial.id
|
|
5290
|
+
var dp = self._fgPollWithEarlyProbe(initial, initial.id, void 0, {
|
|
5291
|
+
platform: params.aiPlatform,
|
|
5292
|
+
projectId: params.projectId,
|
|
5293
|
+
owner: params.owner,
|
|
5294
|
+
ownerKey: params.key
|
|
5295
|
+
});
|
|
3801
5296
|
if (initial.id) self._trackPoll(initial.id, "fg", dp);
|
|
3802
5297
|
return dp;
|
|
3803
5298
|
}
|
|
@@ -4010,8 +5505,8 @@ var ChatSession = class {
|
|
|
4010
5505
|
Promise.resolve(probeBgQueue(
|
|
4011
5506
|
{ service: svcId, owner, platform, queue, status, limit: WORKER_PASS_ADOPT_LIMIT },
|
|
4012
5507
|
{ maxAgeMs: 0 }
|
|
4013
|
-
)).then(function(
|
|
4014
|
-
settle(
|
|
5508
|
+
)).then(function(entry2) {
|
|
5509
|
+
settle(entry2.result);
|
|
4015
5510
|
}, function() {
|
|
4016
5511
|
settle(null);
|
|
4017
5512
|
});
|
|
@@ -4229,7 +5724,12 @@ var ChatSession = class {
|
|
|
4229
5724
|
}
|
|
4230
5725
|
if (serverId) self._stampTurnWithItemId(capturedKey, capturedQueuedLid, void 0, serverId);
|
|
4231
5726
|
if (result && result.poll && (result.status === "pending" || result.status === "running")) {
|
|
4232
|
-
var qp = self._fgPollWithEarlyProbe(result, serverId
|
|
5727
|
+
var qp = self._fgPollWithEarlyProbe(result, serverId, void 0, {
|
|
5728
|
+
platform: capturedPlatform,
|
|
5729
|
+
projectId: id.projectId,
|
|
5730
|
+
owner: id.owner,
|
|
5731
|
+
ownerKey: capturedKey
|
|
5732
|
+
});
|
|
4233
5733
|
if (serverId) self._trackPoll(serverId, "fg", qp);
|
|
4234
5734
|
return qp.then(function(res) {
|
|
4235
5735
|
if (isPollStopped(res)) return;
|
|
@@ -4500,9 +6000,14 @@ var ChatSession = class {
|
|
|
4500
6000
|
answer = (answer || "").trim() || "No text response received from AI provider.";
|
|
4501
6001
|
var lid = this._newLocalId();
|
|
4502
6002
|
if (targetIdx >= 0 && this.state.messages[targetIdx] && this.state.messages[targetIdx].isPending) {
|
|
4503
|
-
this.
|
|
6003
|
+
var qPainted = this._paintedTextAt(targetIdx);
|
|
6004
|
+
var prevQ = this.state.messages[targetIdx] || {};
|
|
6005
|
+
var qSettled = { role: "assistant", content: qPainted, _localId: lid };
|
|
6006
|
+
if (prevQ._serverItemId) qSettled._serverItemId = prevQ._serverItemId;
|
|
6007
|
+
if (prevQ._ownerKey) qSettled._ownerKey = prevQ._ownerKey;
|
|
6008
|
+
this.state.messages[targetIdx] = qSettled;
|
|
4504
6009
|
this.host.notify();
|
|
4505
|
-
this.enqueueTypewrite(targetIdx, answer, lid);
|
|
6010
|
+
this.enqueueTypewrite(targetIdx, answer, lid, qPainted);
|
|
4506
6011
|
} else if (targetIdx >= 0) {
|
|
4507
6012
|
this.state.messages.splice(targetIdx, 0, { role: "assistant", content: "", _localId: lid });
|
|
4508
6013
|
this.host.notify();
|
|
@@ -4764,7 +6269,14 @@ var ChatSession = class {
|
|
|
4764
6269
|
// renders self-throttles to what the machine can actually paint.
|
|
4765
6270
|
// * rAF paces us to the browser's paint cycle and pauses in background
|
|
4766
6271
|
// tabs, so we never queue work faster than it can be drawn.
|
|
4767
|
-
|
|
6272
|
+
//
|
|
6273
|
+
// `paintedText` is what a LIVE STREAM already put in this bubble. The reveal
|
|
6274
|
+
// starts from the point the two texts stop agreeing rather than from zero: the
|
|
6275
|
+
// authoritative answer still replaces the live one character for character (it is
|
|
6276
|
+
// the only source of truth, and this method writes fullText and nothing else), but
|
|
6277
|
+
// retyping a paragraph the reader has just watched arrive is the one thing that
|
|
6278
|
+
// would make a streamed turn look worse than an unstreamed one.
|
|
6279
|
+
typewriteIntoIndex(idx, fullText, localId, paintedText) {
|
|
4768
6280
|
var self = this;
|
|
4769
6281
|
if (!fullText) return Promise.resolve();
|
|
4770
6282
|
var CHARS_PER_SEC = 300;
|
|
@@ -4780,7 +6292,7 @@ var ChatSession = class {
|
|
|
4780
6292
|
});
|
|
4781
6293
|
this.state.typing = true;
|
|
4782
6294
|
this.state.typingAbort = false;
|
|
4783
|
-
var i = 0;
|
|
6295
|
+
var i = paintedText ? typewriterResumeIndex(paintedText, fullText, regions) : 0;
|
|
4784
6296
|
var last = nowMs();
|
|
4785
6297
|
return new Promise(function(resolve) {
|
|
4786
6298
|
var done = false;
|
|
@@ -4795,16 +6307,14 @@ var ChatSession = class {
|
|
|
4795
6307
|
if (done) return;
|
|
4796
6308
|
done = true;
|
|
4797
6309
|
cleanup();
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
self.host.refreshMessageBubble(fi);
|
|
4807
|
-
}
|
|
6310
|
+
var fi = localId ? self.state.messages.findIndex(function(mm) {
|
|
6311
|
+
return mm._localId === localId;
|
|
6312
|
+
}) : idx;
|
|
6313
|
+
if (fi !== -1) {
|
|
6314
|
+
var t = self.state.messages[fi];
|
|
6315
|
+
if (t) {
|
|
6316
|
+
t.content = fullText;
|
|
6317
|
+
self.host.refreshMessageBubble(fi);
|
|
4808
6318
|
}
|
|
4809
6319
|
}
|
|
4810
6320
|
self.state.typing = false;
|
|
@@ -4863,12 +6373,13 @@ var ChatSession = class {
|
|
|
4863
6373
|
nextFrame(frame);
|
|
4864
6374
|
});
|
|
4865
6375
|
}
|
|
4866
|
-
enqueueTypewrite(idx, fullText, localId) {
|
|
6376
|
+
enqueueTypewrite(idx, fullText, localId, paintedText) {
|
|
4867
6377
|
var self = this;
|
|
4868
6378
|
var target = this.state.messages[idx];
|
|
4869
6379
|
if (target && target._ts === void 0) target._ts = wallClockNow();
|
|
6380
|
+
if (!this.typewriterQueue) this.typewriterQueue = Promise.resolve();
|
|
4870
6381
|
this.typewriterQueue = this.typewriterQueue.then(function() {
|
|
4871
|
-
return self.typewriteIntoIndex(idx, fullText, localId);
|
|
6382
|
+
return self.typewriteIntoIndex(idx, fullText, localId, paintedText);
|
|
4872
6383
|
});
|
|
4873
6384
|
return this.typewriterQueue;
|
|
4874
6385
|
}
|
|
@@ -4901,12 +6412,17 @@ var ChatSession = class {
|
|
|
4901
6412
|
this.promoteNextQueuedToRunning();
|
|
4902
6413
|
return Promise.resolve();
|
|
4903
6414
|
}
|
|
6415
|
+
var painted = this._paintedTextAt(pendingIdx);
|
|
4904
6416
|
var lid = this._newLocalId();
|
|
4905
|
-
this.state.messages[pendingIdx]
|
|
6417
|
+
var prevSettled = this.state.messages[pendingIdx] || {};
|
|
6418
|
+
var settled = { role: "assistant", content: painted, isPending: false, _localId: lid };
|
|
6419
|
+
if (prevSettled._serverItemId) settled._serverItemId = prevSettled._serverItemId;
|
|
6420
|
+
if (prevSettled._ownerKey) settled._ownerKey = prevSettled._ownerKey;
|
|
6421
|
+
this.state.messages[pendingIdx] = settled;
|
|
4906
6422
|
this._removeStrayPendingAssistants();
|
|
4907
6423
|
this.host.notify();
|
|
4908
6424
|
this.promoteNextQueuedToRunning();
|
|
4909
|
-
return this.enqueueTypewrite(pendingIdx, latest.content, lid);
|
|
6425
|
+
return this.enqueueTypewrite(pendingIdx, latest.content, lid, painted);
|
|
4910
6426
|
}
|
|
4911
6427
|
// Remove leftover non-background pending ("Thinking…") assistant bubbles: the
|
|
4912
6428
|
// duplicate that appears when a concurrent history refetch re-maps the still-
|
|
@@ -4936,6 +6452,7 @@ var ChatSession = class {
|
|
|
4936
6452
|
for (var k = this.state.messages.length - 1; k >= 0; k--) {
|
|
4937
6453
|
var m = this.state.messages[k];
|
|
4938
6454
|
if (!m || !m.isPending || m.role !== "assistant" || m.isBackgroundTask) continue;
|
|
6455
|
+
if (m._streaming) continue;
|
|
4939
6456
|
if (this._isLiveImmediatePlaceholder(k)) continue;
|
|
4940
6457
|
this.state.messages.splice(k, 1);
|
|
4941
6458
|
}
|
|
@@ -5113,10 +6630,11 @@ var ChatSession = class {
|
|
|
5113
6630
|
this.updateHistoryCache();
|
|
5114
6631
|
return;
|
|
5115
6632
|
}
|
|
6633
|
+
var hPainted = this._paintedTextAt(idx);
|
|
5116
6634
|
var lid = this._newLocalId();
|
|
5117
|
-
this.state.messages[idx] = { role: "assistant", content:
|
|
6635
|
+
this.state.messages[idx] = { role: "assistant", content: hPainted, _localId: lid, _serverItemId: itemId };
|
|
5118
6636
|
this.host.notify();
|
|
5119
|
-
this.enqueueTypewrite(idx, text, lid);
|
|
6637
|
+
this.enqueueTypewrite(idx, text, lid, hPainted);
|
|
5120
6638
|
this.updateHistoryCache();
|
|
5121
6639
|
return;
|
|
5122
6640
|
}
|
|
@@ -5158,11 +6676,11 @@ var ChatSession = class {
|
|
|
5158
6676
|
* path is project-relative ("report.xlsx"), and ONE ChatSession serves every
|
|
5159
6677
|
* project — unscoped, stopping a file in one project would silently suppress
|
|
5160
6678
|
* the same filename's continuations in another. */
|
|
5161
|
-
_indexKeyOf(
|
|
5162
|
-
if (!
|
|
5163
|
-
var file =
|
|
6679
|
+
_indexKeyOf(entry2) {
|
|
6680
|
+
if (!entry2) return "";
|
|
6681
|
+
var file = entry2.storagePath || entry2.filename;
|
|
5164
6682
|
if (!file) return "";
|
|
5165
|
-
return indexScopeKey(
|
|
6683
|
+
return indexScopeKey(entry2.projectId, entry2.platform) + "|" + file;
|
|
5166
6684
|
}
|
|
5167
6685
|
/**
|
|
5168
6686
|
* Reconcile the bg queue with the files the user has stopped.
|
|
@@ -5191,17 +6709,17 @@ var ChatSession = class {
|
|
|
5191
6709
|
if (m.isPending || m.isPendingQueued || m.isPendingInProcess) surfaced[m._serverItemId] = true;
|
|
5192
6710
|
});
|
|
5193
6711
|
for (var i = this.bgTaskQueue.length - 1; i >= 0; i--) {
|
|
5194
|
-
var
|
|
5195
|
-
var key = this._indexKeyOf(
|
|
6712
|
+
var entry2 = this.bgTaskQueue[i];
|
|
6713
|
+
var key = this._indexKeyOf(entry2);
|
|
5196
6714
|
if (!key || !this.cancelledIndexKeys.has(key)) continue;
|
|
5197
|
-
if (!
|
|
6715
|
+
if (!entry2.resumePass && !this.state.stoppedIndexIds[entry2.id]) {
|
|
5198
6716
|
this.cancelledIndexKeys.delete(key);
|
|
5199
6717
|
continue;
|
|
5200
6718
|
}
|
|
5201
|
-
if (surfaced[
|
|
6719
|
+
if (surfaced[entry2.id]) continue;
|
|
5202
6720
|
this.bgTaskQueue.splice(i, 1);
|
|
5203
|
-
this._stopPoll(
|
|
5204
|
-
this._cancelServerItem(
|
|
6721
|
+
this._stopPoll(entry2.id);
|
|
6722
|
+
this._cancelServerItem(entry2.id);
|
|
5205
6723
|
}
|
|
5206
6724
|
}
|
|
5207
6725
|
/**
|
|
@@ -5259,8 +6777,8 @@ var ChatSession = class {
|
|
|
5259
6777
|
return Promise.resolve(probeBgQueue(
|
|
5260
6778
|
{ service: svcId, owner, platform, queue, status, limit: WORKER_PASS_ADOPT_LIMIT },
|
|
5261
6779
|
{ maxAgeMs: 0 }
|
|
5262
|
-
)).then(function(
|
|
5263
|
-
return
|
|
6780
|
+
)).then(function(entry2) {
|
|
6781
|
+
return entry2.result;
|
|
5264
6782
|
}).catch(function() {
|
|
5265
6783
|
return null;
|
|
5266
6784
|
});
|
|
@@ -5413,30 +6931,30 @@ var ChatSession = class {
|
|
|
5413
6931
|
}
|
|
5414
6932
|
var bgPollBudget = MAX_CONCURRENT_BG_POLLS - this._countBgPolls();
|
|
5415
6933
|
var injectedAny = false;
|
|
5416
|
-
this.bgTaskQueue.forEach(function(
|
|
5417
|
-
if (
|
|
5418
|
-
if (!presentIds[
|
|
5419
|
-
var isRunning =
|
|
6934
|
+
this.bgTaskQueue.forEach(function(entry2) {
|
|
6935
|
+
if (entry2.projectId !== svcId || entry2.platform !== plat) return;
|
|
6936
|
+
if (!presentIds[entry2.id]) {
|
|
6937
|
+
var isRunning = entry2.status === "running";
|
|
5420
6938
|
var userBubble = {
|
|
5421
6939
|
role: "user",
|
|
5422
|
-
content: self.host.formatIndexingLabel(
|
|
6940
|
+
content: self.host.formatIndexingLabel(entry2.filename, entry2.mime, entry2.size, entry2.storagePath, entry2.isReindex, !!entry2.resumePass),
|
|
5423
6941
|
isBackgroundTask: true,
|
|
5424
|
-
_serverItemId:
|
|
6942
|
+
_serverItemId: entry2.id,
|
|
5425
6943
|
// Structured ref so this live pass groups with the same file's passes
|
|
5426
6944
|
// rebuilt from history (see indexing_groups.buildChatDisplayList).
|
|
5427
6945
|
_indexFile: {
|
|
5428
|
-
name:
|
|
5429
|
-
path:
|
|
5430
|
-
mime:
|
|
5431
|
-
size:
|
|
5432
|
-
isReindex: !!
|
|
5433
|
-
continued: !!
|
|
6946
|
+
name: entry2.filename,
|
|
6947
|
+
path: entry2.storagePath,
|
|
6948
|
+
mime: entry2.mime,
|
|
6949
|
+
size: entry2.size,
|
|
6950
|
+
isReindex: !!entry2.isReindex,
|
|
6951
|
+
continued: !!entry2.resumePass
|
|
5434
6952
|
}
|
|
5435
6953
|
};
|
|
5436
6954
|
if (isRunning) userBubble.isPendingInProcess = true;
|
|
5437
6955
|
else userBubble.isPendingQueued = true;
|
|
5438
|
-
var stageAt = self._stageIndex(self.state.messages,
|
|
5439
|
-
var runningBubble = isRunning ? { role: "assistant", content: "", isPending: true, isPendingInProcess: true, isBackgroundTask: true, _serverItemId:
|
|
6956
|
+
var stageAt = self._stageIndex(self.state.messages, entry2.stageId);
|
|
6957
|
+
var runningBubble = isRunning ? { role: "assistant", content: "", isPending: true, isPendingInProcess: true, isBackgroundTask: true, _serverItemId: entry2.id } : null;
|
|
5440
6958
|
if (stageAt === -1) {
|
|
5441
6959
|
self.state.messages.push(userBubble);
|
|
5442
6960
|
if (runningBubble) self.state.messages.push(runningBubble);
|
|
@@ -5445,16 +6963,16 @@ var ChatSession = class {
|
|
|
5445
6963
|
} else {
|
|
5446
6964
|
self.state.messages.splice(stageAt, 0, userBubble);
|
|
5447
6965
|
}
|
|
5448
|
-
presentIds[
|
|
6966
|
+
presentIds[entry2.id] = true;
|
|
5449
6967
|
injectedAny = true;
|
|
5450
6968
|
}
|
|
5451
|
-
if (bgPollBudget > 0 && !self.isPollingPaused() && !self.historyItemPolls.has(
|
|
6969
|
+
if (bgPollBudget > 0 && !self.isPollingPaused() && !self.historyItemPolls.has(entry2.id) && typeof entry2.poll === "function") {
|
|
5452
6970
|
bgPollBudget--;
|
|
5453
|
-
var capturedId =
|
|
5454
|
-
var capturedEntry =
|
|
6971
|
+
var capturedId = entry2.id, capturedPlat = plat;
|
|
6972
|
+
var capturedEntry = entry2;
|
|
5455
6973
|
var wasStopped = false;
|
|
5456
|
-
var bp =
|
|
5457
|
-
self._trackPoll(
|
|
6974
|
+
var bp = entry2.poll({ latency: POLL_INTERVAL });
|
|
6975
|
+
self._trackPoll(entry2.id, "bg", bp);
|
|
5458
6976
|
bp.then(function(response) {
|
|
5459
6977
|
if (isPollStopped(response)) {
|
|
5460
6978
|
wasStopped = true;
|
|
@@ -5520,13 +7038,13 @@ var ChatSession = class {
|
|
|
5520
7038
|
* client knows DETERMINISTICALLY (see the two call sites in
|
|
5521
7039
|
* maybeResumeIndexing). Best-effort by contract; identity-checked so a
|
|
5522
7040
|
* project switch mid-settle cannot stamp the wrong service. */
|
|
5523
|
-
_mintDoneMarker(
|
|
7041
|
+
_mintDoneMarker(entry2) {
|
|
5524
7042
|
try {
|
|
5525
7043
|
var mint = chatEngineConfig().mintIndexDoneMarker;
|
|
5526
|
-
if (!mint || !
|
|
7044
|
+
if (!mint || !entry2 || !entry2.storagePath || !entry2.projectId) return;
|
|
5527
7045
|
var id = this.host.getIdentity();
|
|
5528
|
-
if (!id || id.projectId !==
|
|
5529
|
-
mint({ service:
|
|
7046
|
+
if (!id || id.projectId !== entry2.projectId) return;
|
|
7047
|
+
mint({ service: entry2.projectId, storagePath: entry2.storagePath });
|
|
5530
7048
|
} catch (_e) {
|
|
5531
7049
|
}
|
|
5532
7050
|
}
|
|
@@ -5547,27 +7065,27 @@ var ChatSession = class {
|
|
|
5547
7065
|
* maybeResumeIndexing's single-pass branch); paged files stay with their
|
|
5548
7066
|
* drivers. Outcome is read from the settled bubbles' own flags, which is
|
|
5549
7067
|
* all the history mapping left us. Best-effort and idempotent throughout. */
|
|
5550
|
-
_flipRunFromSettledEntry(
|
|
7068
|
+
_flipRunFromSettledEntry(entry2) {
|
|
5551
7069
|
try {
|
|
5552
|
-
if (!
|
|
5553
|
-
if (isPagedReadFile(
|
|
5554
|
-
if (this.cancelledIndexKeys.has(this._indexKeyOf(
|
|
5555
|
-
if (this.state.stoppedIndexIds[
|
|
7070
|
+
if (!entry2 || !entry2.storagePath || !entry2.id || !entry2.projectId) return;
|
|
7071
|
+
if (isPagedReadFile(entry2.filename, entry2.mime)) return;
|
|
7072
|
+
if (this.cancelledIndexKeys.has(this._indexKeyOf(entry2))) return;
|
|
7073
|
+
if (this.state.stoppedIndexIds[entry2.id]) return;
|
|
5556
7074
|
var userMsg = null, replyMsg = null;
|
|
5557
7075
|
this.state.messages.forEach(function(m) {
|
|
5558
|
-
if (m._serverItemId !==
|
|
7076
|
+
if (m._serverItemId !== entry2.id) return;
|
|
5559
7077
|
if (m.role === "user") {
|
|
5560
7078
|
if (!userMsg) userMsg = m;
|
|
5561
7079
|
} else if (!replyMsg) replyMsg = m;
|
|
5562
7080
|
});
|
|
5563
7081
|
if (userMsg && userMsg.isCancelled || replyMsg && replyMsg.isCancelled) {
|
|
5564
|
-
this._flipRunRecord(
|
|
7082
|
+
this._flipRunRecord(entry2, "cancelled");
|
|
5565
7083
|
} else if (replyMsg && replyMsg.isError) {
|
|
5566
7084
|
var errText = typeof replyMsg.content === "string" ? replyMsg.content.replace(/\s+/g, " ").trim().slice(0, 300) : "";
|
|
5567
|
-
this._flipRunRecord(
|
|
7085
|
+
this._flipRunRecord(entry2, "error", errText || "Indexing failed.");
|
|
5568
7086
|
} else if (replyMsg) {
|
|
5569
|
-
this._mintDoneMarker(
|
|
5570
|
-
this._flipRunRecord(
|
|
7087
|
+
this._mintDoneMarker(entry2);
|
|
7088
|
+
this._flipRunRecord(entry2, "done");
|
|
5571
7089
|
}
|
|
5572
7090
|
} catch (_e) {
|
|
5573
7091
|
}
|
|
@@ -5578,55 +7096,55 @@ var ChatSession = class {
|
|
|
5578
7096
|
* mid-settle — otherwise the record lies 'working' forever. Best-effort
|
|
5579
7097
|
* through upsertIndexRunRecordSafe; the consumer's precedence guard keeps
|
|
5580
7098
|
* repeats and races harmless. */
|
|
5581
|
-
_flipRunRecord(
|
|
5582
|
-
if (!
|
|
7099
|
+
_flipRunRecord(entry2, status, error) {
|
|
7100
|
+
if (!entry2 || !entry2.storagePath || !entry2.projectId) return;
|
|
5583
7101
|
var patch = { status, finished: Date.now() };
|
|
5584
7102
|
if (error) patch.error = error;
|
|
5585
|
-
upsertIndexRunRecordSafe(
|
|
7103
|
+
upsertIndexRunRecordSafe(entry2.projectId, entry2.storagePath, patch);
|
|
5586
7104
|
}
|
|
5587
|
-
maybeResumeIndexing(
|
|
7105
|
+
maybeResumeIndexing(entry2, response, platform) {
|
|
5588
7106
|
var self = this;
|
|
5589
7107
|
var endOfClientChain = function() {
|
|
5590
7108
|
self._nudgeIndexingDrain();
|
|
5591
7109
|
};
|
|
5592
7110
|
try {
|
|
5593
|
-
if (!
|
|
5594
|
-
if (this.cancelledIndexKeys.has(this._indexKeyOf(
|
|
5595
|
-
if (!isPagedReadFile(
|
|
7111
|
+
if (!entry2 || !entry2.storagePath) return;
|
|
7112
|
+
if (this.cancelledIndexKeys.has(this._indexKeyOf(entry2))) return;
|
|
7113
|
+
if (!isPagedReadFile(entry2.filename, entry2.mime)) {
|
|
5596
7114
|
if (!isErrorResponseBody(response) && !this._isCancelledPollResult(response)) {
|
|
5597
|
-
this._mintDoneMarker(
|
|
5598
|
-
this._flipRunRecord(
|
|
7115
|
+
this._mintDoneMarker(entry2);
|
|
7116
|
+
this._flipRunRecord(entry2, "done");
|
|
5599
7117
|
} else if (this._isCancelledPollResult(response)) {
|
|
5600
|
-
this._flipRunRecord(
|
|
7118
|
+
this._flipRunRecord(entry2, "cancelled");
|
|
5601
7119
|
} else {
|
|
5602
|
-
this._flipRunRecord(
|
|
7120
|
+
this._flipRunRecord(entry2, "error", this._runErrorText(response));
|
|
5603
7121
|
}
|
|
5604
7122
|
endOfClientChain();
|
|
5605
7123
|
return;
|
|
5606
7124
|
}
|
|
5607
|
-
if (isImageVisionFile(
|
|
5608
|
-
if (windowedIndexingEnabled() && isWindowedReadFile(
|
|
7125
|
+
if (isImageVisionFile(entry2.filename, entry2.mime)) return;
|
|
7126
|
+
if (windowedIndexingEnabled() && isWindowedReadFile(entry2.filename, entry2.mime)) return;
|
|
5609
7127
|
if (isErrorResponseBody(response)) {
|
|
5610
|
-
this._flipRunRecord(
|
|
7128
|
+
this._flipRunRecord(entry2, "error", this._runErrorText(response));
|
|
5611
7129
|
endOfClientChain();
|
|
5612
7130
|
return;
|
|
5613
7131
|
}
|
|
5614
7132
|
var answer = (platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "";
|
|
5615
7133
|
if (answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1) {
|
|
5616
|
-
this._mintDoneMarker(
|
|
5617
|
-
this._flipRunRecord(
|
|
7134
|
+
this._mintDoneMarker(entry2);
|
|
7135
|
+
this._flipRunRecord(entry2, "done");
|
|
5618
7136
|
endOfClientChain();
|
|
5619
7137
|
return;
|
|
5620
7138
|
}
|
|
5621
|
-
var pass = (
|
|
7139
|
+
var pass = (entry2.resumePass || 0) + 1;
|
|
5622
7140
|
if (pass > MAX_INDEXING_RESUME_PASSES) {
|
|
5623
|
-
this._flipRunRecord(
|
|
7141
|
+
this._flipRunRecord(entry2, "error", "Stopped after " + MAX_INDEXING_RESUME_PASSES + " passes without finishing.");
|
|
5624
7142
|
endOfClientChain();
|
|
5625
7143
|
return;
|
|
5626
7144
|
}
|
|
5627
7145
|
var id = this.host.getIdentity();
|
|
5628
|
-
if (!id || id.platform === "none" || id.projectId !==
|
|
5629
|
-
this._flipRunRecord(
|
|
7146
|
+
if (!id || id.platform === "none" || id.projectId !== entry2.projectId) {
|
|
7147
|
+
this._flipRunRecord(entry2, "error", "Indexing stopped: the session or project changed before the file finished.");
|
|
5630
7148
|
endOfClientChain();
|
|
5631
7149
|
return;
|
|
5632
7150
|
}
|
|
@@ -5644,10 +7162,10 @@ var ChatSession = class {
|
|
|
5644
7162
|
serviceName: id.serviceName,
|
|
5645
7163
|
serviceDescription: id.serviceDescription,
|
|
5646
7164
|
attachment: {
|
|
5647
|
-
name:
|
|
5648
|
-
storagePath:
|
|
5649
|
-
mime:
|
|
5650
|
-
size:
|
|
7165
|
+
name: entry2.filename,
|
|
7166
|
+
storagePath: entry2.storagePath,
|
|
7167
|
+
mime: entry2.mime,
|
|
7168
|
+
size: entry2.size,
|
|
5651
7169
|
url: ""
|
|
5652
7170
|
}
|
|
5653
7171
|
}).then(function(ack) {
|
|
@@ -5656,11 +7174,11 @@ var ChatSession = class {
|
|
|
5656
7174
|
projectId: id.projectId,
|
|
5657
7175
|
platform: id.platform,
|
|
5658
7176
|
id: ack.id,
|
|
5659
|
-
filename:
|
|
5660
|
-
storagePath:
|
|
5661
|
-
isReindex:
|
|
5662
|
-
mime:
|
|
5663
|
-
size:
|
|
7177
|
+
filename: entry2.filename,
|
|
7178
|
+
storagePath: entry2.storagePath,
|
|
7179
|
+
isReindex: entry2.isReindex,
|
|
7180
|
+
mime: entry2.mime,
|
|
7181
|
+
size: entry2.size,
|
|
5664
7182
|
status: ack.status === "running" ? "running" : "pending",
|
|
5665
7183
|
poll: ack.poll,
|
|
5666
7184
|
resumePass: pass
|
|
@@ -5747,6 +7265,7 @@ var ChatSession = class {
|
|
|
5747
7265
|
formatIndexingLabel: self.host.formatIndexingLabel
|
|
5748
7266
|
}).messages;
|
|
5749
7267
|
self.applyHydratedBodies(mapped);
|
|
7268
|
+
self._adoptLocalAnswers(mapped, loadKey);
|
|
5750
7269
|
var keptOlderPages = false;
|
|
5751
7270
|
var keptScreenAwaitingBg = false;
|
|
5752
7271
|
if (fetchMore) {
|
|
@@ -5922,6 +7441,7 @@ var ChatSession = class {
|
|
|
5922
7441
|
}
|
|
5923
7442
|
self.updateHistoryCache();
|
|
5924
7443
|
self.host.notify();
|
|
7444
|
+
self._scheduleStreamRecovery(loadKey, platform, projectId, owner);
|
|
5925
7445
|
var bgPending = !fetchMore && history && history.bgPending;
|
|
5926
7446
|
if (bgPending) {
|
|
5927
7447
|
var batchId = ++_bgHistoryBatchSeq;
|
|
@@ -6071,7 +7591,12 @@ var ChatSession = class {
|
|
|
6071
7591
|
}
|
|
6072
7592
|
}
|
|
6073
7593
|
};
|
|
6074
|
-
var pp = isBg ? item.poll(Object.assign({ latency: POLL_INTERVAL }, pollOpts)) : self._fgPollWithEarlyProbe(item, capturedId, pollOpts
|
|
7594
|
+
var pp = isBg ? item.poll(Object.assign({ latency: POLL_INTERVAL }, pollOpts)) : self._fgPollWithEarlyProbe(item, capturedId, pollOpts, {
|
|
7595
|
+
platform,
|
|
7596
|
+
projectId,
|
|
7597
|
+
owner,
|
|
7598
|
+
ownerKey: loadKey
|
|
7599
|
+
});
|
|
6075
7600
|
self._trackPoll(capturedId, item._isBgTask || item._isOnBgQueue ? "bg" : "fg", pp);
|
|
6076
7601
|
if (pp && pp.catch) pp.catch(function() {
|
|
6077
7602
|
});
|
|
@@ -6741,6 +8266,112 @@ function buildChatDisplayList(messages, opts) {
|
|
|
6741
8266
|
return out;
|
|
6742
8267
|
}
|
|
6743
8268
|
|
|
8269
|
+
// src/engine/project_settings.ts
|
|
8270
|
+
var UPLOAD_ACCESS_GROUPS = ["public", "authorized", "private"];
|
|
8271
|
+
var DEFAULT_UPLOAD_ACCESS_GROUP = "authorized";
|
|
8272
|
+
var PROJECT_SETTINGS_TABLE = "__SETTINGS__";
|
|
8273
|
+
var PROJECT_SETTINGS_UNIQUE_ID = "bq::settings";
|
|
8274
|
+
var PROJECT_SETTINGS_ACCESS_GROUP = "public";
|
|
8275
|
+
var UPLOAD_ACCESS_LABELS = {
|
|
8276
|
+
public: "Public",
|
|
8277
|
+
authorized: "Signed in users",
|
|
8278
|
+
private: "Only me"
|
|
8279
|
+
};
|
|
8280
|
+
var UPLOAD_ACCESS_HINTS = {
|
|
8281
|
+
public: "Anyone can ask about this file, including visitors who are not logged in.",
|
|
8282
|
+
authorized: "Only users signed in to this project can ask about this file.",
|
|
8283
|
+
private: "Only you can ask about this file."
|
|
8284
|
+
};
|
|
8285
|
+
var UPLOAD_ACCESS_OPTIONS = UPLOAD_ACCESS_GROUPS.map((value) => ({
|
|
8286
|
+
value,
|
|
8287
|
+
label: UPLOAD_ACCESS_LABELS[value],
|
|
8288
|
+
hint: UPLOAD_ACCESS_HINTS[value]
|
|
8289
|
+
}));
|
|
8290
|
+
function normalizeUploadAccessGroup(value) {
|
|
8291
|
+
return UPLOAD_ACCESS_GROUPS.indexOf(value) === -1 ? DEFAULT_UPLOAD_ACCESS_GROUP : value;
|
|
8292
|
+
}
|
|
8293
|
+
function normalizeProjectAccessSetting(value) {
|
|
8294
|
+
if (value === "ask") return "ask";
|
|
8295
|
+
return UPLOAD_ACCESS_GROUPS.indexOf(value) === -1 ? null : value;
|
|
8296
|
+
}
|
|
8297
|
+
function accessSettingFrom(data) {
|
|
8298
|
+
return normalizeProjectAccessSetting(data?.upload_access_group);
|
|
8299
|
+
}
|
|
8300
|
+
function uploadAccessGroupFrom(data) {
|
|
8301
|
+
const v = accessSettingFrom(data);
|
|
8302
|
+
return v && v !== "ask" ? v : DEFAULT_UPLOAD_ACCESS_GROUP;
|
|
8303
|
+
}
|
|
8304
|
+
function asksUploadAccessFrom(data) {
|
|
8305
|
+
return accessSettingFrom(data) === "ask";
|
|
8306
|
+
}
|
|
8307
|
+
var reader = null;
|
|
8308
|
+
var cache = /* @__PURE__ */ new Map();
|
|
8309
|
+
function configureProjectSettings(fn) {
|
|
8310
|
+
reader = fn;
|
|
8311
|
+
}
|
|
8312
|
+
function entry(service) {
|
|
8313
|
+
let e = cache.get(service);
|
|
8314
|
+
if (!e) {
|
|
8315
|
+
e = { data: null, settled: false, inflight: null };
|
|
8316
|
+
cache.set(service, e);
|
|
8317
|
+
}
|
|
8318
|
+
return e;
|
|
8319
|
+
}
|
|
8320
|
+
function loadProjectSettings(service) {
|
|
8321
|
+
if (!service) return Promise.resolve(null);
|
|
8322
|
+
const e = entry(service);
|
|
8323
|
+
if (e.settled) return Promise.resolve(e.data);
|
|
8324
|
+
if (e.inflight) return e.inflight;
|
|
8325
|
+
if (!reader) return Promise.resolve(null);
|
|
8326
|
+
const run = reader(service).then((data) => data && typeof data === "object" ? data : null).catch(() => null).then((data) => {
|
|
8327
|
+
const cur = entry(service);
|
|
8328
|
+
if (cur.inflight === run) {
|
|
8329
|
+
cur.data = data;
|
|
8330
|
+
cur.settled = true;
|
|
8331
|
+
cur.inflight = null;
|
|
8332
|
+
}
|
|
8333
|
+
return data;
|
|
8334
|
+
});
|
|
8335
|
+
e.inflight = run;
|
|
8336
|
+
return run;
|
|
8337
|
+
}
|
|
8338
|
+
function primeProjectSettings(service) {
|
|
8339
|
+
void loadProjectSettings(service);
|
|
8340
|
+
}
|
|
8341
|
+
function readyProjectSettings(service) {
|
|
8342
|
+
return loadProjectSettings(service);
|
|
8343
|
+
}
|
|
8344
|
+
function cachedProjectSettings(service) {
|
|
8345
|
+
const e = cache.get(service);
|
|
8346
|
+
return e && e.settled ? e.data : null;
|
|
8347
|
+
}
|
|
8348
|
+
function projectSettingsSettled(service) {
|
|
8349
|
+
const e = cache.get(service);
|
|
8350
|
+
return !!e && e.settled;
|
|
8351
|
+
}
|
|
8352
|
+
function projectAccessSetting(service) {
|
|
8353
|
+
return accessSettingFrom(cachedProjectSettings(service));
|
|
8354
|
+
}
|
|
8355
|
+
function projectUploadAccessGroup(service) {
|
|
8356
|
+
return uploadAccessGroupFrom(cachedProjectSettings(service));
|
|
8357
|
+
}
|
|
8358
|
+
function projectAsksUploadAccess(service) {
|
|
8359
|
+
return asksUploadAccessFrom(cachedProjectSettings(service));
|
|
8360
|
+
}
|
|
8361
|
+
function setProjectSettings(service, data) {
|
|
8362
|
+
if (!service) return;
|
|
8363
|
+
cache.set(service, { data: data || null, settled: true, inflight: null });
|
|
8364
|
+
}
|
|
8365
|
+
function patchProjectSettings(service, patch) {
|
|
8366
|
+
if (!service) return;
|
|
8367
|
+
const cur = cachedProjectSettings(service) || {};
|
|
8368
|
+
setProjectSettings(service, Object.assign({}, cur, patch));
|
|
8369
|
+
}
|
|
8370
|
+
function clearProjectSettings(service) {
|
|
8371
|
+
if (service) cache.delete(service);
|
|
8372
|
+
else cache.clear();
|
|
8373
|
+
}
|
|
8374
|
+
|
|
6744
8375
|
exports.BG_INDEXING_QUEUE_SUFFIX = BG_INDEXING_QUEUE_SUFFIX;
|
|
6745
8376
|
exports.BOM = BOM;
|
|
6746
8377
|
exports.BOM_EXTS = BOM_EXTS;
|
|
@@ -6752,6 +8383,7 @@ exports.ChatSession = ChatSession;
|
|
|
6752
8383
|
exports.DEFAULT_CLAUDE_MODEL = DEFAULT_CLAUDE_MODEL;
|
|
6753
8384
|
exports.DEFAULT_CONTEXT_WINDOW = DEFAULT_CONTEXT_WINDOW;
|
|
6754
8385
|
exports.DEFAULT_OPENAI_MODEL = DEFAULT_OPENAI_MODEL;
|
|
8386
|
+
exports.DEFAULT_UPLOAD_ACCESS_GROUP = DEFAULT_UPLOAD_ACCESS_GROUP;
|
|
6755
8387
|
exports.EMPTY_INDEXING_REPLY = EMPTY_INDEXING_REPLY;
|
|
6756
8388
|
exports.EXPIRED_ATTACHMENT_URL_HOST = EXPIRED_ATTACHMENT_URL_HOST;
|
|
6757
8389
|
exports.EXPIRED_ATTACHMENT_URL_ORIGIN = EXPIRED_ATTACHMENT_URL_ORIGIN;
|
|
@@ -6764,6 +8396,7 @@ exports.HTML_EXTS = HTML_EXTS;
|
|
|
6764
8396
|
exports.HTML_HEAD_WINDOW = HTML_HEAD_WINDOW;
|
|
6765
8397
|
exports.IMAGE_PREVIEWS_PER_MESSAGE = IMAGE_PREVIEWS_PER_MESSAGE;
|
|
6766
8398
|
exports.INDEXING_COMPLETE_MARKER = INDEXING_COMPLETE_MARKER;
|
|
8399
|
+
exports.INDEXING_MAX_OUTPUT_TOKENS = INDEXING_MAX_OUTPUT_TOKENS;
|
|
6767
8400
|
exports.INLINE_LINK_GLYPH = INLINE_LINK_GLYPH;
|
|
6768
8401
|
exports.INLINE_LINK_UNAVAILABLE_GLYPH = INLINE_LINK_UNAVAILABLE_GLYPH;
|
|
6769
8402
|
exports.INLINE_LINK_UNAVAILABLE_SUFFIX = INLINE_LINK_UNAVAILABLE_SUFFIX;
|
|
@@ -6787,13 +8420,24 @@ exports.PREVIEWABLE_IMAGE_CONTENT_TYPES = PREVIEWABLE_IMAGE_CONTENT_TYPES;
|
|
|
6787
8420
|
exports.PREVIEW_BROWSER_CACHE_SECONDS = PREVIEW_BROWSER_CACHE_SECONDS;
|
|
6788
8421
|
exports.PREVIEW_LAYOUT_BOX_SELECTOR = PREVIEW_LAYOUT_BOX_SELECTOR;
|
|
6789
8422
|
exports.PREVIEW_URL_EXPIRES_SECONDS = PREVIEW_URL_EXPIRES_SECONDS;
|
|
8423
|
+
exports.PROJECT_SETTINGS_ACCESS_GROUP = PROJECT_SETTINGS_ACCESS_GROUP;
|
|
8424
|
+
exports.PROJECT_SETTINGS_TABLE = PROJECT_SETTINGS_TABLE;
|
|
8425
|
+
exports.PROJECT_SETTINGS_UNIQUE_ID = PROJECT_SETTINGS_UNIQUE_ID;
|
|
6790
8426
|
exports.RENDER_FROM_TOKEN = RENDER_FROM_TOKEN;
|
|
6791
8427
|
exports.RTF_EXTS = RTF_EXTS;
|
|
6792
8428
|
exports.RUN_RECORD_WORKING_STALE_MS = RUN_RECORD_WORKING_STALE_MS;
|
|
8429
|
+
exports.STREAM_POLL_INTERVAL = STREAM_POLL_INTERVAL;
|
|
6793
8430
|
exports.TOOL_AND_RESPONSE_BUFFER = TOOL_AND_RESPONSE_BUFFER;
|
|
8431
|
+
exports.UPLOAD_ACCESS_GROUPS = UPLOAD_ACCESS_GROUPS;
|
|
8432
|
+
exports.UPLOAD_ACCESS_HINTS = UPLOAD_ACCESS_HINTS;
|
|
8433
|
+
exports.UPLOAD_ACCESS_LABELS = UPLOAD_ACCESS_LABELS;
|
|
8434
|
+
exports.UPLOAD_ACCESS_OPTIONS = UPLOAD_ACCESS_OPTIONS;
|
|
6794
8435
|
exports.XML_EXTS = XML_EXTS;
|
|
6795
8436
|
exports.__resetSplitHistoryState = __resetSplitHistoryState;
|
|
8437
|
+
exports.accessSettingFrom = accessSettingFrom;
|
|
8438
|
+
exports.adoptLocalAnswerIntoPage = adoptLocalAnswerIntoPage;
|
|
6796
8439
|
exports.applyEncodingDeclaration = applyEncodingDeclaration;
|
|
8440
|
+
exports.asksUploadAccessFrom = asksUploadAccessFrom;
|
|
6797
8441
|
exports.bgIndexingQueueName = bgIndexingQueueName;
|
|
6798
8442
|
exports.buildAiAgentValue = buildAiAgentValue;
|
|
6799
8443
|
exports.buildBoundedChatMessages = buildBoundedChatMessages;
|
|
@@ -6808,21 +8452,27 @@ exports.buildIndexingRenderMessage = buildIndexingRenderMessage;
|
|
|
6808
8452
|
exports.buildIndexingSystemPrompt = buildIndexingSystemPrompt;
|
|
6809
8453
|
exports.buildIndexingUserMessage = buildIndexingUserMessage;
|
|
6810
8454
|
exports.buildIndexingWindowMessage = buildIndexingWindowMessage;
|
|
8455
|
+
exports.cachedProjectSettings = cachedProjectSettings;
|
|
6811
8456
|
exports.callClaudeWithMcp = callClaudeWithMcp;
|
|
6812
8457
|
exports.callClaudeWithPublicMcp = callClaudeWithPublicMcp;
|
|
6813
8458
|
exports.callOpenAIWithPublicMcp = callOpenAIWithPublicMcp;
|
|
6814
8459
|
exports.canonicalizePathForm = canonicalizePathForm;
|
|
6815
8460
|
exports.chatCacheKey = chatCacheKey;
|
|
6816
8461
|
exports.chatEngineConfig = chatEngineConfig;
|
|
8462
|
+
exports.chatStreamWiring = chatStreamWiring;
|
|
6817
8463
|
exports.classifyInlineLink = classifyInlineLink;
|
|
6818
8464
|
exports.clearAttachmentParsers = clearAttachmentParsers;
|
|
6819
8465
|
exports.clearImagePreviewCache = clearImagePreviewCache;
|
|
8466
|
+
exports.clearProjectSettings = clearProjectSettings;
|
|
6820
8467
|
exports.composeUserMessage = composeUserMessage;
|
|
6821
8468
|
exports.configureChatEngine = configureChatEngine;
|
|
8469
|
+
exports.configureProjectSettings = configureProjectSettings;
|
|
6822
8470
|
exports.contentTypeForExt = contentTypeForExt;
|
|
6823
8471
|
exports.createHistoryFiller = createHistoryFiller;
|
|
6824
8472
|
exports.createInlineLinkRegex = createInlineLinkRegex;
|
|
6825
8473
|
exports.createScrollAnchor = createScrollAnchor;
|
|
8474
|
+
exports.createSseParser = createSseParser;
|
|
8475
|
+
exports.csrEnvelopeError = csrEnvelopeError;
|
|
6826
8476
|
exports.encodePathSegments = encodePathSegments;
|
|
6827
8477
|
exports.encodingClassForExt = encodingClassForExt;
|
|
6828
8478
|
exports.ensureHtmlCharset = ensureHtmlCharset;
|
|
@@ -6860,36 +8510,46 @@ exports.indexScopeKey = indexScopeKey;
|
|
|
6860
8510
|
exports.indexingAccessGroup = indexingAccessGroup;
|
|
6861
8511
|
exports.isAuthExpiredError = isAuthExpiredError;
|
|
6862
8512
|
exports.isBgIndexingQueue = isBgIndexingQueue;
|
|
8513
|
+
exports.isCsrStatusEnvelope = isCsrStatusEnvelope;
|
|
6863
8514
|
exports.isErrorResponseBody = isErrorResponseBody;
|
|
6864
8515
|
exports.isHttpUrlLike = isHttpUrlLike;
|
|
8516
|
+
exports.isImageVisionFile = isImageVisionFile;
|
|
6865
8517
|
exports.isIndexingRequestText = isIndexingRequestText;
|
|
6866
8518
|
exports.isLinkUnavailable = isLinkUnavailable;
|
|
6867
8519
|
exports.isNonRetryableRequestError = isNonRetryableRequestError;
|
|
6868
8520
|
exports.isOfficeFile = isOfficeFile;
|
|
8521
|
+
exports.isPagedReadFile = isPagedReadFile;
|
|
6869
8522
|
exports.isPreviewableImagePath = isPreviewableImagePath;
|
|
6870
8523
|
exports.isProviderApiKeyError = isProviderApiKeyError;
|
|
6871
8524
|
exports.isServerExtractable = isServerExtractable;
|
|
6872
8525
|
exports.isServiceDbAttachmentHref = isServiceDbAttachmentHref;
|
|
8526
|
+
exports.isWindowedReadFile = isWindowedReadFile;
|
|
6873
8527
|
exports.linkUnavailableKeyForHref = linkUnavailableKeyForHref;
|
|
6874
8528
|
exports.linkUnavailableKeyForPath = linkUnavailableKeyForPath;
|
|
6875
8529
|
exports.linkUnavailableKeysForPath = linkUnavailableKeysForPath;
|
|
6876
8530
|
exports.listClaudeModels = listClaudeModels;
|
|
6877
8531
|
exports.listOpenAIModels = listOpenAIModels;
|
|
8532
|
+
exports.liveSafePrefix = liveSafePrefix;
|
|
8533
|
+
exports.loadProjectSettings = loadProjectSettings;
|
|
6878
8534
|
exports.looksLikeRtf = looksLikeRtf;
|
|
6879
8535
|
exports.makeExtractPlaceholder = makeExtractPlaceholder;
|
|
6880
8536
|
exports.mapHistoryListToMessages = mapHistoryListToMessages;
|
|
6881
8537
|
exports.markImagePreviewStale = markImagePreviewStale;
|
|
8538
|
+
exports.mayKeepStreamedAnswer = mayKeepStreamedAnswer;
|
|
6882
8539
|
exports.mintCacheBustStamp = mintCacheBustStamp;
|
|
6883
8540
|
exports.needsBomForExt = needsBomForExt;
|
|
6884
8541
|
exports.normalizeAttachmentPathCandidate = normalizeAttachmentPathCandidate;
|
|
6885
8542
|
exports.normalizeExt = normalizeExt;
|
|
8543
|
+
exports.normalizeProjectAccessSetting = normalizeProjectAccessSetting;
|
|
6886
8544
|
exports.normalizeTextContent = normalizeTextContent;
|
|
6887
8545
|
exports.normalizeTrailingInlineToken = normalizeTrailingInlineToken;
|
|
8546
|
+
exports.normalizeUploadAccessGroup = normalizeUploadAccessGroup;
|
|
6888
8547
|
exports.notifyAgentSaveAttachment = notifyAgentSaveAttachment;
|
|
6889
8548
|
exports.parseAiAgentValue = parseAiAgentValue;
|
|
6890
8549
|
exports.parseAttachmentContent = parseAttachmentContent;
|
|
6891
8550
|
exports.parseIndexingLabel = parseIndexingLabel;
|
|
6892
8551
|
exports.parseIndexingRequestText = parseIndexingRequestText;
|
|
8552
|
+
exports.patchProjectSettings = patchProjectSettings;
|
|
6893
8553
|
exports.peekImagePreviewUrl = peekImagePreviewUrl;
|
|
6894
8554
|
exports.prepareDownloadText = prepareDownloadText;
|
|
6895
8555
|
exports.presignExpiryEpochMs = presignExpiryEpochMs;
|
|
@@ -6897,7 +8557,13 @@ exports.previewImageContentType = previewImageContentType;
|
|
|
6897
8557
|
exports.previewLayoutBox = previewLayoutBox;
|
|
6898
8558
|
exports.previewMintCacheToken = previewMintCacheToken;
|
|
6899
8559
|
exports.previewableExtOf = previewableExtOf;
|
|
8560
|
+
exports.primeProjectSettings = primeProjectSettings;
|
|
8561
|
+
exports.projectAccessSetting = projectAccessSetting;
|
|
8562
|
+
exports.projectAsksUploadAccess = projectAsksUploadAccess;
|
|
8563
|
+
exports.projectSettingsSettled = projectSettingsSettled;
|
|
8564
|
+
exports.projectUploadAccessGroup = projectUploadAccessGroup;
|
|
6900
8565
|
exports.readExpiredAttachmentHref = readExpiredAttachmentHref;
|
|
8566
|
+
exports.readyProjectSettings = readyProjectSettings;
|
|
6901
8567
|
exports.registerAttachmentParser = registerAttachmentParser;
|
|
6902
8568
|
exports.registerModelContextWindows = registerModelContextWindows;
|
|
6903
8569
|
exports.renderInlineLinkHtml = renderInlineLinkHtml;
|
|
@@ -6908,11 +8574,18 @@ exports.runIndexUniqueId = runIndexUniqueId;
|
|
|
6908
8574
|
exports.safeDecodeURIComponent = safeDecodeURIComponent;
|
|
6909
8575
|
exports.sanitizeAttachmentLinksForHistory = sanitizeAttachmentLinksForHistory;
|
|
6910
8576
|
exports.setProjectContextWindow = setProjectContextWindow;
|
|
8577
|
+
exports.setProjectSettings = setProjectSettings;
|
|
6911
8578
|
exports.shouldRescueInFlightMessage = shouldRescueInFlightMessage;
|
|
8579
|
+
exports.skapiSupportsStreaming = skapiSupportsStreaming;
|
|
8580
|
+
exports.streamRecoveryEnabled = streamRecoveryEnabled;
|
|
8581
|
+
exports.streamRecoveryLabels = streamRecoveryLabels;
|
|
8582
|
+
exports.streamRecoveryPhase = streamRecoveryPhase;
|
|
6912
8583
|
exports.stripFileBlocksFromHistory = stripFileBlocksFromHistory;
|
|
6913
8584
|
exports.transformContentWithImages = transformContentWithImages;
|
|
6914
8585
|
exports.transformContentWithOpenAIImages = transformContentWithOpenAIImages;
|
|
6915
8586
|
exports.truncateLabelForDisplay = truncateLabelForDisplay;
|
|
8587
|
+
exports.typewriterResumeIndex = typewriterResumeIndex;
|
|
8588
|
+
exports.uploadAccessGroupFrom = uploadAccessGroupFrom;
|
|
6916
8589
|
exports.upsertIndexRunRecordSafe = upsertIndexRunRecordSafe;
|
|
6917
8590
|
exports.wallClockNow = wallClockNow;
|
|
6918
8591
|
//# sourceMappingURL=engine.cjs.map
|