bunnyquery 1.9.7 → 1.9.10
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 +5 -0
- package/bunnyquery.css +53 -2
- package/bunnyquery.js +1822 -177
- package/dist/engine.cjs +1746 -114
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +1271 -6
- package/dist/engine.d.ts +1271 -6
- package/dist/engine.mjs +1709 -115
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- 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 +108 -2
- package/src/engine/project_settings.ts +303 -0
- package/src/engine/prompts/chat_system_prompt.ts +15 -3
- package/src/engine/requests.ts +130 -1
- 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 };
|
|
@@ -305,7 +318,7 @@ function buildChatSystemPrompt(params) {
|
|
|
305
318
|
You are a dedicated assistant for the project ID: "${projectId}".
|
|
306
319
|
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
320
|
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
|
-
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
|
|
321
|
+
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
322
|
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
323
|
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
324
|
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.
|
|
@@ -524,7 +537,17 @@ var STATUS_MESSAGE = {
|
|
|
524
537
|
function isTransientStatus(status) {
|
|
525
538
|
return status === 408 || status === 425 || status === 429 || status >= 500;
|
|
526
539
|
}
|
|
540
|
+
function isCsrStatusEnvelope(res) {
|
|
541
|
+
return !!res && typeof res === "object" && !Array.isArray(res) && typeof res.status === "string" && typeof res.id === "string" && "in_queue" in res;
|
|
542
|
+
}
|
|
543
|
+
function csrEnvelopeError(input) {
|
|
544
|
+
if (!isCsrStatusEnvelope(input)) return void 0;
|
|
545
|
+
if (input.status !== "failed") return void 0;
|
|
546
|
+
return input.error != null ? input.error : { message: "The AI provider request failed." };
|
|
547
|
+
}
|
|
527
548
|
function getErrorMessage(input) {
|
|
549
|
+
var envErr = csrEnvelopeError(input);
|
|
550
|
+
if (envErr !== void 0) input = envErr;
|
|
528
551
|
if (!input) return "Something went wrong.";
|
|
529
552
|
if (typeof input === "string") return input;
|
|
530
553
|
if (input.error && input.error.message) return input.error.message;
|
|
@@ -539,7 +562,9 @@ function getErrorMessage(input) {
|
|
|
539
562
|
return "Something went wrong.";
|
|
540
563
|
}
|
|
541
564
|
function isErrorResponseBody(response) {
|
|
542
|
-
|
|
565
|
+
var envErr = csrEnvelopeError(response);
|
|
566
|
+
if (envErr !== void 0) response = envErr;
|
|
567
|
+
if (!response || typeof response !== "object") return envErr !== void 0;
|
|
543
568
|
if (typeof response.status_code === "number" && response.status_code >= 400) return true;
|
|
544
569
|
if (response.type === "error") return true;
|
|
545
570
|
if (response.error && (response.error.message || response.error.type)) return true;
|
|
@@ -556,6 +581,8 @@ function isErrorResponseBody(response) {
|
|
|
556
581
|
return false;
|
|
557
582
|
}
|
|
558
583
|
function isNonRetryableRequestError(input) {
|
|
584
|
+
var envErr = csrEnvelopeError(input);
|
|
585
|
+
if (envErr !== void 0) input = envErr;
|
|
559
586
|
if (!input || typeof input !== "object") return false;
|
|
560
587
|
var status = typeof input.status_code === "number" ? input.status_code : typeof input.status === "number" ? input.status : void 0;
|
|
561
588
|
var param = void 0;
|
|
@@ -586,6 +613,8 @@ function isNonRetryableRequestError(input) {
|
|
|
586
613
|
return false;
|
|
587
614
|
}
|
|
588
615
|
function isAuthExpiredError(input) {
|
|
616
|
+
var envErr = csrEnvelopeError(input);
|
|
617
|
+
if (envErr !== void 0) input = envErr;
|
|
589
618
|
if (!input) return false;
|
|
590
619
|
var blobs = [];
|
|
591
620
|
var push = function(v) {
|
|
@@ -616,6 +645,8 @@ function isAuthExpiredError(input) {
|
|
|
616
645
|
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
646
|
}
|
|
618
647
|
function isProviderApiKeyError(input) {
|
|
648
|
+
var envErr = csrEnvelopeError(input);
|
|
649
|
+
if (envErr !== void 0) input = envErr;
|
|
619
650
|
if (!input) return false;
|
|
620
651
|
var blobs = [];
|
|
621
652
|
var push = function(v) {
|
|
@@ -1538,6 +1569,541 @@ function buildAiAgentValue(platform, model, contextWindow) {
|
|
|
1538
1569
|
return p + "#" + m + "#" + Math.floor(n);
|
|
1539
1570
|
}
|
|
1540
1571
|
|
|
1572
|
+
// src/engine/sse.ts
|
|
1573
|
+
var CLAUDE_EVENTS = {
|
|
1574
|
+
message_start: true,
|
|
1575
|
+
message_delta: true,
|
|
1576
|
+
message_stop: true,
|
|
1577
|
+
content_block_start: true,
|
|
1578
|
+
content_block_delta: true,
|
|
1579
|
+
content_block_stop: true,
|
|
1580
|
+
ping: true
|
|
1581
|
+
};
|
|
1582
|
+
var CLAUDE_TOOL_BLOCKS = {
|
|
1583
|
+
tool_use: true,
|
|
1584
|
+
server_tool_use: true,
|
|
1585
|
+
mcp_tool_use: true,
|
|
1586
|
+
web_search_tool_use: true
|
|
1587
|
+
};
|
|
1588
|
+
var OPENAI_TOOL_ITEMS = {
|
|
1589
|
+
function_call: true,
|
|
1590
|
+
mcp_call: true,
|
|
1591
|
+
web_search_call: true,
|
|
1592
|
+
file_search_call: true,
|
|
1593
|
+
code_interpreter_call: true,
|
|
1594
|
+
computer_call: true,
|
|
1595
|
+
image_generation_call: true
|
|
1596
|
+
};
|
|
1597
|
+
function detectProvider(type) {
|
|
1598
|
+
if (!type) return null;
|
|
1599
|
+
if (type.indexOf("response.") === 0) return "openai";
|
|
1600
|
+
if (CLAUDE_EVENTS[type]) return "claude";
|
|
1601
|
+
return null;
|
|
1602
|
+
}
|
|
1603
|
+
function lineEnd(s, from) {
|
|
1604
|
+
for (var i = from; i < s.length; i++) {
|
|
1605
|
+
var c = s.charCodeAt(i);
|
|
1606
|
+
if (c === 10) return { at: i, len: 1 };
|
|
1607
|
+
if (c === 13) {
|
|
1608
|
+
if (i + 1 >= s.length) return null;
|
|
1609
|
+
return { at: i, len: s.charCodeAt(i + 1) === 10 ? 2 : 1 };
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
function readFrame(lines) {
|
|
1615
|
+
var event = "";
|
|
1616
|
+
var data = [];
|
|
1617
|
+
var framed = false;
|
|
1618
|
+
for (var i = 0; i < lines.length; i++) {
|
|
1619
|
+
var line = lines[i];
|
|
1620
|
+
if (!line.length) continue;
|
|
1621
|
+
if (line.charCodeAt(0) === 58) {
|
|
1622
|
+
framed = true;
|
|
1623
|
+
continue;
|
|
1624
|
+
}
|
|
1625
|
+
var colon = line.indexOf(":");
|
|
1626
|
+
var field = colon === -1 ? line : line.slice(0, colon);
|
|
1627
|
+
var value = colon === -1 ? "" : line.slice(colon + 1);
|
|
1628
|
+
if (value.charCodeAt(0) === 32) value = value.slice(1);
|
|
1629
|
+
if (field === "data") {
|
|
1630
|
+
framed = true;
|
|
1631
|
+
data.push(value);
|
|
1632
|
+
} else if (field === "event") {
|
|
1633
|
+
framed = true;
|
|
1634
|
+
event = value;
|
|
1635
|
+
} else if (field === "id" || field === "retry") {
|
|
1636
|
+
framed = true;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
return { event, data: data.join("\n"), framed };
|
|
1640
|
+
}
|
|
1641
|
+
function createSseParser() {
|
|
1642
|
+
var buf = "";
|
|
1643
|
+
var lines = [];
|
|
1644
|
+
var lastSeq = 0;
|
|
1645
|
+
var sawFraming = false;
|
|
1646
|
+
var raw = "";
|
|
1647
|
+
var rawHasContent = false;
|
|
1648
|
+
var ended = false;
|
|
1649
|
+
var rawParsed = false;
|
|
1650
|
+
var rawBody = null;
|
|
1651
|
+
var provider = null;
|
|
1652
|
+
var terminalEvent = null;
|
|
1653
|
+
var errored = false;
|
|
1654
|
+
var error = null;
|
|
1655
|
+
var stopReason = null;
|
|
1656
|
+
var toolCalls = [];
|
|
1657
|
+
var malformedFrames = 0;
|
|
1658
|
+
var malformedToolJson = 0;
|
|
1659
|
+
var message = null;
|
|
1660
|
+
var blocks = /* @__PURE__ */ new Map();
|
|
1661
|
+
var parts = /* @__PURE__ */ new Map();
|
|
1662
|
+
var reasoning = /* @__PURE__ */ new Map();
|
|
1663
|
+
var response = null;
|
|
1664
|
+
var textCache = null;
|
|
1665
|
+
var thinkingCache = null;
|
|
1666
|
+
function feed(text) {
|
|
1667
|
+
if (typeof text !== "string" || !text.length) return;
|
|
1668
|
+
if (!sawFraming) {
|
|
1669
|
+
raw += text;
|
|
1670
|
+
if (!rawHasContent) rawHasContent = /\S/.test(text);
|
|
1671
|
+
rawParsed = false;
|
|
1672
|
+
rawBody = null;
|
|
1673
|
+
}
|
|
1674
|
+
buf += text;
|
|
1675
|
+
var i = 0;
|
|
1676
|
+
for (; ; ) {
|
|
1677
|
+
var end2 = lineEnd(buf, i);
|
|
1678
|
+
if (!end2) break;
|
|
1679
|
+
var line = buf.slice(i, end2.at);
|
|
1680
|
+
i = end2.at + end2.len;
|
|
1681
|
+
if (line.length === 0) dispatch();
|
|
1682
|
+
else lines.push(line);
|
|
1683
|
+
}
|
|
1684
|
+
if (i > 0) buf = buf.slice(i);
|
|
1685
|
+
}
|
|
1686
|
+
function feedChunks(chunks) {
|
|
1687
|
+
if (!chunks || !chunks.length) return;
|
|
1688
|
+
for (var i = 0; i < chunks.length; i++) {
|
|
1689
|
+
var c = chunks[i];
|
|
1690
|
+
if (!c || typeof c !== "object") continue;
|
|
1691
|
+
var seq = typeof c.seq === "number" ? c.seq : 0;
|
|
1692
|
+
if (seq && seq <= lastSeq) continue;
|
|
1693
|
+
if (seq > lastSeq) lastSeq = seq;
|
|
1694
|
+
feed(typeof c.txt === "string" ? c.txt : "");
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
function end() {
|
|
1698
|
+
if (buf.length) {
|
|
1699
|
+
var tail = buf.charCodeAt(buf.length - 1) === 13 ? buf.slice(0, -1) : buf;
|
|
1700
|
+
if (tail.length) lines.push(tail);
|
|
1701
|
+
buf = "";
|
|
1702
|
+
}
|
|
1703
|
+
if (lines.length) dispatch();
|
|
1704
|
+
ended = true;
|
|
1705
|
+
}
|
|
1706
|
+
function isUnframed() {
|
|
1707
|
+
return ended && !sawFraming && rawHasContent;
|
|
1708
|
+
}
|
|
1709
|
+
function dispatch() {
|
|
1710
|
+
var pending = lines;
|
|
1711
|
+
lines = [];
|
|
1712
|
+
if (!pending.length) return;
|
|
1713
|
+
try {
|
|
1714
|
+
var frame = readFrame(pending);
|
|
1715
|
+
if (frame.framed && !sawFraming) {
|
|
1716
|
+
sawFraming = true;
|
|
1717
|
+
raw = "";
|
|
1718
|
+
rawHasContent = false;
|
|
1719
|
+
}
|
|
1720
|
+
if (!frame.data.length) return;
|
|
1721
|
+
if (frame.data === "[DONE]") return;
|
|
1722
|
+
var ev = JSON.parse(frame.data);
|
|
1723
|
+
if (!ev || typeof ev !== "object") {
|
|
1724
|
+
malformedFrames++;
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
var type = typeof ev.type === "string" && ev.type ? ev.type : frame.event;
|
|
1728
|
+
if (!type) {
|
|
1729
|
+
malformedFrames++;
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
if (!provider) provider = detectProvider(type);
|
|
1733
|
+
if (provider === "openai") handleOpenAI(type, ev);
|
|
1734
|
+
else if (provider === "claude") handleClaude(type, ev);
|
|
1735
|
+
else handleUnattributed(type, ev);
|
|
1736
|
+
} catch (e) {
|
|
1737
|
+
malformedFrames++;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
function handleUnattributed(type, ev) {
|
|
1741
|
+
if (type === "error") {
|
|
1742
|
+
takeError(ev && ev.error ? ev : { type: "error", error: ev });
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
malformedFrames++;
|
|
1746
|
+
}
|
|
1747
|
+
function takeError(payload) {
|
|
1748
|
+
errored = true;
|
|
1749
|
+
terminalEvent = "error";
|
|
1750
|
+
error = payload;
|
|
1751
|
+
}
|
|
1752
|
+
function handleClaude(type, ev) {
|
|
1753
|
+
if (type === "ping") return;
|
|
1754
|
+
if (type === "error") {
|
|
1755
|
+
takeError({ type: "error", error: ev && ev.error ? ev.error : ev });
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
if (type === "message_start") {
|
|
1759
|
+
message = ev && ev.message ? shallowClone(ev.message) : { type: "message", role: "assistant" };
|
|
1760
|
+
if (typeof message.stop_reason === "string") stopReason = message.stop_reason;
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
if (type === "content_block_start") {
|
|
1764
|
+
var idx = numberOr(ev.index, -1);
|
|
1765
|
+
if (idx < 0) {
|
|
1766
|
+
malformedFrames++;
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
var block = ev.content_block ? shallowClone(ev.content_block) : {};
|
|
1770
|
+
blocks.set(idx, { block, json: "", sawJson: false });
|
|
1771
|
+
invalidate();
|
|
1772
|
+
if (block && typeof block.type === "string" && CLAUDE_TOOL_BLOCKS[block.type]) {
|
|
1773
|
+
var call = {
|
|
1774
|
+
index: idx,
|
|
1775
|
+
name: typeof block.name === "string" && block.name ? block.name : block.type,
|
|
1776
|
+
type: block.type
|
|
1777
|
+
};
|
|
1778
|
+
if (typeof block.server_name === "string") call.serverName = block.server_name;
|
|
1779
|
+
toolCalls.push(call);
|
|
1780
|
+
}
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
if (type === "content_block_delta") {
|
|
1784
|
+
var i = numberOr(ev.index, -1);
|
|
1785
|
+
var d = ev.delta;
|
|
1786
|
+
if (i < 0 || !d || typeof d !== "object") {
|
|
1787
|
+
malformedFrames++;
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
var st = blocks.get(i);
|
|
1791
|
+
if (!st) {
|
|
1792
|
+
st = { block: { type: deltaBlockType(d.type) }, json: "", sawJson: false };
|
|
1793
|
+
blocks.set(i, st);
|
|
1794
|
+
}
|
|
1795
|
+
applyClaudeDelta(st, d);
|
|
1796
|
+
invalidate();
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
if (type === "content_block_stop") {
|
|
1800
|
+
var j = numberOr(ev.index, -1);
|
|
1801
|
+
var s = j >= 0 ? blocks.get(j) : void 0;
|
|
1802
|
+
if (s && s.sawJson) finishToolJson(s);
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
if (type === "message_delta") {
|
|
1806
|
+
if (!message) message = { type: "message", role: "assistant" };
|
|
1807
|
+
var delta = ev.delta;
|
|
1808
|
+
if (delta && typeof delta === "object") {
|
|
1809
|
+
for (var k in delta) {
|
|
1810
|
+
if (Object.prototype.hasOwnProperty.call(delta, k)) message[k] = delta[k];
|
|
1811
|
+
}
|
|
1812
|
+
if (typeof delta.stop_reason === "string") stopReason = delta.stop_reason;
|
|
1813
|
+
}
|
|
1814
|
+
if (ev.usage && typeof ev.usage === "object") {
|
|
1815
|
+
message.usage = mergeInto(shallowClone(message.usage) || {}, ev.usage);
|
|
1816
|
+
}
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
if (type === "message_stop") {
|
|
1820
|
+
terminalEvent = "message_stop";
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
malformedFrames++;
|
|
1824
|
+
}
|
|
1825
|
+
function applyClaudeDelta(st, d) {
|
|
1826
|
+
var t = d.type;
|
|
1827
|
+
if (t === "text_delta") {
|
|
1828
|
+
st.block.text = (st.block.text || "") + str(d.text);
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
if (t === "thinking_delta") {
|
|
1832
|
+
st.block.thinking = (st.block.thinking || "") + str(d.thinking);
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
if (t === "signature_delta") {
|
|
1836
|
+
st.block.signature = (st.block.signature || "") + str(d.signature);
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
if (t === "input_json_delta") {
|
|
1840
|
+
st.json += str(d.partial_json);
|
|
1841
|
+
st.sawJson = true;
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
if (t === "citations_delta") {
|
|
1845
|
+
if (d.citation) {
|
|
1846
|
+
if (!Array.isArray(st.block.citations)) st.block.citations = [];
|
|
1847
|
+
st.block.citations.push(d.citation);
|
|
1848
|
+
}
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
malformedFrames++;
|
|
1852
|
+
}
|
|
1853
|
+
function finishToolJson(st) {
|
|
1854
|
+
if (!st.json.length) {
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
try {
|
|
1858
|
+
st.block.input = JSON.parse(st.json);
|
|
1859
|
+
} catch (e) {
|
|
1860
|
+
malformedToolJson++;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
function deltaBlockType(deltaType) {
|
|
1864
|
+
if (deltaType === "thinking_delta" || deltaType === "signature_delta") return "thinking";
|
|
1865
|
+
if (deltaType === "input_json_delta") return "tool_use";
|
|
1866
|
+
return "text";
|
|
1867
|
+
}
|
|
1868
|
+
function handleOpenAI(type, ev) {
|
|
1869
|
+
if (type === "response.output_text.delta") {
|
|
1870
|
+
putPart(ev, str(ev.delta), false);
|
|
1871
|
+
invalidate();
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
if (type === "response.output_text.done") {
|
|
1875
|
+
if (typeof ev.text === "string") {
|
|
1876
|
+
putPart(ev, ev.text, true);
|
|
1877
|
+
invalidate();
|
|
1878
|
+
}
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
if (type === "response.reasoning_summary_text.delta" || type === "response.reasoning_text.delta") {
|
|
1882
|
+
putReasoning(type, ev, str(ev.delta), false);
|
|
1883
|
+
invalidate();
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
if (type === "response.reasoning_summary_text.done" || type === "response.reasoning_text.done") {
|
|
1887
|
+
if (typeof ev.text === "string") {
|
|
1888
|
+
putReasoning(type, ev, ev.text, true);
|
|
1889
|
+
invalidate();
|
|
1890
|
+
}
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
if (type === "response.output_item.added") {
|
|
1894
|
+
var item = ev.item;
|
|
1895
|
+
if (item && typeof item.type === "string" && OPENAI_TOOL_ITEMS[item.type]) {
|
|
1896
|
+
toolCalls.push({
|
|
1897
|
+
index: numberOr(ev.output_index, toolCalls.length),
|
|
1898
|
+
// A built-in tool (web_search_call) has no name of its own, so the item
|
|
1899
|
+
// type is the only label there is and a row can still be drawn.
|
|
1900
|
+
name: typeof item.name === "string" && item.name ? item.name : item.type,
|
|
1901
|
+
type: item.type
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
if (type === "response.completed" || type === "response.incomplete" || type === "response.failed") {
|
|
1907
|
+
terminalEvent = type;
|
|
1908
|
+
if (ev.response && typeof ev.response === "object") {
|
|
1909
|
+
response = ev.response;
|
|
1910
|
+
var st = response.status;
|
|
1911
|
+
if (st === "incomplete") {
|
|
1912
|
+
var reason = response.incomplete_details && response.incomplete_details.reason;
|
|
1913
|
+
stopReason = typeof reason === "string" && reason ? reason : "incomplete";
|
|
1914
|
+
} else if (typeof st === "string" && st) {
|
|
1915
|
+
stopReason = st;
|
|
1916
|
+
}
|
|
1917
|
+
if (response.error && (response.error.message || response.error.code)) {
|
|
1918
|
+
errored = true;
|
|
1919
|
+
error = response;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
if (type === "response.failed") errored = true;
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
if (type === "response.error" || type === "error") {
|
|
1926
|
+
takeError(ev && ev.error ? ev : { type: "error", error: ev });
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
function putReasoning(type, ev, text, replace) {
|
|
1931
|
+
var summary = type.indexOf("response.reasoning_summary_text.") === 0;
|
|
1932
|
+
var oi = numberOr(ev.output_index, 0);
|
|
1933
|
+
var idx = numberOr(summary ? ev.summary_index : ev.content_index, 0);
|
|
1934
|
+
var key = oi + ":" + (summary ? "s" : "r") + ":" + idx;
|
|
1935
|
+
var r = reasoning.get(key);
|
|
1936
|
+
if (!r) {
|
|
1937
|
+
r = { oi, idx, kind: summary ? 0 : 1, text: "" };
|
|
1938
|
+
reasoning.set(key, r);
|
|
1939
|
+
}
|
|
1940
|
+
r.text = replace ? text : r.text + text;
|
|
1941
|
+
}
|
|
1942
|
+
function putPart(ev, text, replace) {
|
|
1943
|
+
var oi = numberOr(ev.output_index, 0);
|
|
1944
|
+
var ci = numberOr(ev.content_index, 0);
|
|
1945
|
+
var key = oi + ":" + ci;
|
|
1946
|
+
var p = parts.get(key);
|
|
1947
|
+
if (!p) {
|
|
1948
|
+
p = { oi, ci, text: "" };
|
|
1949
|
+
parts.set(key, p);
|
|
1950
|
+
}
|
|
1951
|
+
p.text = replace ? text : p.text + text;
|
|
1952
|
+
}
|
|
1953
|
+
function invalidate() {
|
|
1954
|
+
textCache = null;
|
|
1955
|
+
thinkingCache = null;
|
|
1956
|
+
}
|
|
1957
|
+
function claudeTextBlocks() {
|
|
1958
|
+
return orderedBlocks().filter(function(b) {
|
|
1959
|
+
return b && b.type === "text";
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
function orderedBlocks() {
|
|
1963
|
+
var idx = [];
|
|
1964
|
+
blocks.forEach(function(_v, k) {
|
|
1965
|
+
idx.push(k);
|
|
1966
|
+
});
|
|
1967
|
+
idx.sort(function(a, b) {
|
|
1968
|
+
return a - b;
|
|
1969
|
+
});
|
|
1970
|
+
var out = [];
|
|
1971
|
+
for (var i = 0; i < idx.length; i++) out.push(blocks.get(idx[i]).block);
|
|
1972
|
+
return out;
|
|
1973
|
+
}
|
|
1974
|
+
function orderedParts() {
|
|
1975
|
+
var out = [];
|
|
1976
|
+
parts.forEach(function(p) {
|
|
1977
|
+
out.push(p);
|
|
1978
|
+
});
|
|
1979
|
+
out.sort(function(a, b) {
|
|
1980
|
+
return a.oi !== b.oi ? a.oi - b.oi : a.ci - b.ci;
|
|
1981
|
+
});
|
|
1982
|
+
return out;
|
|
1983
|
+
}
|
|
1984
|
+
function currentText() {
|
|
1985
|
+
if (textCache !== null) return textCache;
|
|
1986
|
+
var out;
|
|
1987
|
+
if (provider === "openai") {
|
|
1988
|
+
out = orderedParts().map(function(p) {
|
|
1989
|
+
return p.text;
|
|
1990
|
+
}).join("\n");
|
|
1991
|
+
} else {
|
|
1992
|
+
out = claudeTextBlocks().map(function(b) {
|
|
1993
|
+
return b.text || "";
|
|
1994
|
+
}).join("\n");
|
|
1995
|
+
}
|
|
1996
|
+
textCache = out;
|
|
1997
|
+
return out;
|
|
1998
|
+
}
|
|
1999
|
+
function currentThinking() {
|
|
2000
|
+
if (thinkingCache !== null) return thinkingCache;
|
|
2001
|
+
var out;
|
|
2002
|
+
if (provider === "openai") {
|
|
2003
|
+
var rs = [];
|
|
2004
|
+
reasoning.forEach(function(r) {
|
|
2005
|
+
rs.push(r);
|
|
2006
|
+
});
|
|
2007
|
+
rs.sort(function(a, b) {
|
|
2008
|
+
if (a.oi !== b.oi) return a.oi - b.oi;
|
|
2009
|
+
if (a.idx !== b.idx) return a.idx - b.idx;
|
|
2010
|
+
return a.kind - b.kind;
|
|
2011
|
+
});
|
|
2012
|
+
out = rs.map(function(r) {
|
|
2013
|
+
return r.text;
|
|
2014
|
+
}).join("\n");
|
|
2015
|
+
} else {
|
|
2016
|
+
out = orderedBlocks().filter(function(b) {
|
|
2017
|
+
return b && b.type === "thinking";
|
|
2018
|
+
}).map(function(b) {
|
|
2019
|
+
return b.thinking || "";
|
|
2020
|
+
}).join("\n");
|
|
2021
|
+
}
|
|
2022
|
+
thinkingCache = out;
|
|
2023
|
+
return out;
|
|
2024
|
+
}
|
|
2025
|
+
function buildBody() {
|
|
2026
|
+
if (provider === "openai") {
|
|
2027
|
+
if (response) return response;
|
|
2028
|
+
} else if (blocks.size || message) {
|
|
2029
|
+
var base = message ? shallowClone(message) : { type: "message", role: "assistant" };
|
|
2030
|
+
base.content = orderedBlocks();
|
|
2031
|
+
return base;
|
|
2032
|
+
}
|
|
2033
|
+
if (errored && error) return error;
|
|
2034
|
+
return unframedBody();
|
|
2035
|
+
}
|
|
2036
|
+
function unframedBody() {
|
|
2037
|
+
if (!isUnframed()) return null;
|
|
2038
|
+
if (rawParsed) return rawBody;
|
|
2039
|
+
rawParsed = true;
|
|
2040
|
+
try {
|
|
2041
|
+
var v = JSON.parse(raw);
|
|
2042
|
+
rawBody = v && typeof v === "object" ? v : null;
|
|
2043
|
+
} catch (e) {
|
|
2044
|
+
rawBody = null;
|
|
2045
|
+
}
|
|
2046
|
+
return rawBody;
|
|
2047
|
+
}
|
|
2048
|
+
function snapshot() {
|
|
2049
|
+
return {
|
|
2050
|
+
provider,
|
|
2051
|
+
text: currentText(),
|
|
2052
|
+
thinkingText: currentThinking(),
|
|
2053
|
+
toolCalls: toolCalls.slice(),
|
|
2054
|
+
toolNames: toolCalls.map(function(t) {
|
|
2055
|
+
return t.name;
|
|
2056
|
+
}),
|
|
2057
|
+
stopReason,
|
|
2058
|
+
complete: terminalEvent !== null,
|
|
2059
|
+
// A terminal event that ENDED the answer rather than KILLED it. The
|
|
2060
|
+
// `errored` term covers all three ways a stream dies with a terminal event
|
|
2061
|
+
// on it: an Anthropic or OpenAI `error` frame (takeError sets both), a
|
|
2062
|
+
// response.failed, and a response.completed/incomplete whose Response
|
|
2063
|
+
// object carries an error payload. See the field's own doc for the loss
|
|
2064
|
+
// this separation prevents.
|
|
2065
|
+
answerComplete: terminalEvent !== null && terminalEvent !== "error" && !errored,
|
|
2066
|
+
terminalEvent,
|
|
2067
|
+
errored,
|
|
2068
|
+
error,
|
|
2069
|
+
malformedFrames,
|
|
2070
|
+
malformedToolJson,
|
|
2071
|
+
unframed: isUnframed(),
|
|
2072
|
+
unframedText: isUnframed() ? raw : null,
|
|
2073
|
+
lastSeq
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
return {
|
|
2077
|
+
feed,
|
|
2078
|
+
feedChunks,
|
|
2079
|
+
end,
|
|
2080
|
+
snapshot,
|
|
2081
|
+
finalBody: buildBody
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
function str(v) {
|
|
2085
|
+
return typeof v === "string" ? v : "";
|
|
2086
|
+
}
|
|
2087
|
+
function numberOr(v, fallback) {
|
|
2088
|
+
return typeof v === "number" && isFinite(v) ? v : fallback;
|
|
2089
|
+
}
|
|
2090
|
+
function shallowClone(o) {
|
|
2091
|
+
if (!o || typeof o !== "object") return o;
|
|
2092
|
+
var out = Array.isArray(o) ? o.slice() : {};
|
|
2093
|
+
if (!Array.isArray(o)) {
|
|
2094
|
+
for (var k in o) {
|
|
2095
|
+
if (Object.prototype.hasOwnProperty.call(o, k)) out[k] = o[k];
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
return out;
|
|
2099
|
+
}
|
|
2100
|
+
function mergeInto(target, src) {
|
|
2101
|
+
for (var k in src) {
|
|
2102
|
+
if (Object.prototype.hasOwnProperty.call(src, k)) target[k] = src[k];
|
|
2103
|
+
}
|
|
2104
|
+
return target;
|
|
2105
|
+
}
|
|
2106
|
+
|
|
1541
2107
|
// src/engine/requests.ts
|
|
1542
2108
|
var ANTHROPIC_MESSAGES_API_URL = "https://api.anthropic.com/v1/messages";
|
|
1543
2109
|
var ANTHROPIC_MODELS_API_URL = "https://api.anthropic.com/v1/models";
|
|
@@ -1562,6 +2128,26 @@ function mcpEndpointFor(anonymous, publicProjectId, service) {
|
|
|
1562
2128
|
return { url: String(mcpUrl()).replace(/\/+$/, "") + "/p/" + project };
|
|
1563
2129
|
}
|
|
1564
2130
|
var clientSecretRequest = (opts) => chatEngineConfig().clientSecretRequest(opts);
|
|
2131
|
+
var CHAT_STREAM_ON = Object.freeze({
|
|
2132
|
+
transport: Object.freeze({ stream: true }),
|
|
2133
|
+
body: Object.freeze({ stream: true })
|
|
2134
|
+
});
|
|
2135
|
+
var CHAT_STREAM_OFF = Object.freeze({
|
|
2136
|
+
transport: Object.freeze({}),
|
|
2137
|
+
body: Object.freeze({})
|
|
2138
|
+
});
|
|
2139
|
+
var CHAT_STREAM_ON_REALTIME = Object.freeze({
|
|
2140
|
+
// `realtime` rides on the TRANSPORT arm only. It is a skapi option, not a field
|
|
2141
|
+
// the destination understands, so it must never reach `data`: the body arm stays
|
|
2142
|
+
// exactly what it is with the socket off.
|
|
2143
|
+
transport: Object.freeze({ stream: true, realtime: true }),
|
|
2144
|
+
body: Object.freeze({ stream: true })
|
|
2145
|
+
});
|
|
2146
|
+
function chatStreamWiring(queue) {
|
|
2147
|
+
if (!liveStreamingEnabled()) return CHAT_STREAM_OFF;
|
|
2148
|
+
if (isBgIndexingQueue(queue)) return CHAT_STREAM_OFF;
|
|
2149
|
+
return liveStreamingRealtimeEnabled() ? CHAT_STREAM_ON_REALTIME : CHAT_STREAM_ON;
|
|
2150
|
+
}
|
|
1565
2151
|
var VARIANT_IMAGE_DETAIL = "original";
|
|
1566
2152
|
var VARIANT_TEXT_VERBOSITY = "high";
|
|
1567
2153
|
var OLDEST_NANO_REASONING_EFFORT = "high";
|
|
@@ -1708,6 +2294,7 @@ function applyHistoryCacheBreakpoint(messages) {
|
|
|
1708
2294
|
});
|
|
1709
2295
|
}
|
|
1710
2296
|
var POLL_INTERVAL = 3e3;
|
|
2297
|
+
var STREAM_POLL_INTERVAL = 1e3;
|
|
1711
2298
|
var MAX_CONCURRENT_BG_POLLS = 6;
|
|
1712
2299
|
async function callClaudeWithMcp({
|
|
1713
2300
|
prompt,
|
|
@@ -1730,12 +2317,14 @@ async function callClaudeWithMcp({
|
|
|
1730
2317
|
if (mcpServer.authorizationToken) {
|
|
1731
2318
|
mcpServerDefinition.authorization_token = mcpServer.authorizationToken;
|
|
1732
2319
|
}
|
|
2320
|
+
const stream = chatStreamWiring(userId || service);
|
|
1733
2321
|
return clientSecretRequest({
|
|
1734
2322
|
clientSecretName: "claude",
|
|
1735
2323
|
queue: userId || service,
|
|
1736
2324
|
service,
|
|
1737
2325
|
owner,
|
|
1738
2326
|
...pollOpt(),
|
|
2327
|
+
...stream.transport,
|
|
1739
2328
|
url: ANTHROPIC_MESSAGES_API_URL,
|
|
1740
2329
|
method: "POST",
|
|
1741
2330
|
headers: {
|
|
@@ -1747,6 +2336,9 @@ async function callClaudeWithMcp({
|
|
|
1747
2336
|
data: {
|
|
1748
2337
|
model,
|
|
1749
2338
|
max_tokens: maxTokens,
|
|
2339
|
+
// Top level beside model/messages/mcp_servers, which is where the
|
|
2340
|
+
// Messages API takes it.
|
|
2341
|
+
...stream.body,
|
|
1750
2342
|
...extractContent && extractContent.length ? { _skapi_extract: extractContent } : {},
|
|
1751
2343
|
...fileUrls && fileUrls.length ? { _skapi_file_urls: fileUrls } : {},
|
|
1752
2344
|
...system ? {
|
|
@@ -1829,12 +2421,14 @@ async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system,
|
|
|
1829
2421
|
content: m.content
|
|
1830
2422
|
}))
|
|
1831
2423
|
];
|
|
2424
|
+
const stream = chatStreamWiring(userId || service);
|
|
1832
2425
|
return clientSecretRequest({
|
|
1833
2426
|
clientSecretName: "openai",
|
|
1834
2427
|
queue: userId || service,
|
|
1835
2428
|
service,
|
|
1836
2429
|
owner,
|
|
1837
2430
|
...pollOpt(),
|
|
2431
|
+
...stream.transport,
|
|
1838
2432
|
url: OPENAI_RESPONSES_API_URL,
|
|
1839
2433
|
method: "POST",
|
|
1840
2434
|
headers: {
|
|
@@ -1844,6 +2438,9 @@ async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system,
|
|
|
1844
2438
|
data: {
|
|
1845
2439
|
model: resolvedModel,
|
|
1846
2440
|
max_output_tokens: getMaxOutputTokens("openai", resolvedModel),
|
|
2441
|
+
// Top level beside model/input/tools, which is where the Responses API
|
|
2442
|
+
// takes it.
|
|
2443
|
+
...stream.body,
|
|
1847
2444
|
...extractContent && extractContent.length ? { _skapi_extract: extractContent } : {},
|
|
1848
2445
|
...fileUrls && fileUrls.length ? { _skapi_file_urls: fileUrls } : {},
|
|
1849
2446
|
input: responseInput,
|
|
@@ -2257,9 +2854,9 @@ function probeBgQueue(params, opts) {
|
|
|
2257
2854
|
{ service: params.service, owner: params.owner, platform: params.platform, queue: params.queue, status: params.status },
|
|
2258
2855
|
{ limit: params.limit, fetchMore: false }
|
|
2259
2856
|
)).then(function(result) {
|
|
2260
|
-
const
|
|
2261
|
-
bgProbeCache[key] =
|
|
2262
|
-
return
|
|
2857
|
+
const entry2 = { result, at: Date.now() };
|
|
2858
|
+
bgProbeCache[key] = entry2;
|
|
2859
|
+
return entry2;
|
|
2263
2860
|
});
|
|
2264
2861
|
bgProbeInflight[key] = p;
|
|
2265
2862
|
p.then(function() {
|
|
@@ -2278,8 +2875,8 @@ async function fetchLiveIndexingKeys(params) {
|
|
|
2278
2875
|
]);
|
|
2279
2876
|
const keys = /* @__PURE__ */ new Set();
|
|
2280
2877
|
let truncated = false;
|
|
2281
|
-
for (const
|
|
2282
|
-
const res =
|
|
2878
|
+
for (const entry2 of [pending, running]) {
|
|
2879
|
+
const res = entry2.result;
|
|
2283
2880
|
const list = res && Array.isArray(res.list) ? res.list : [];
|
|
2284
2881
|
if (list.length >= LIVE_INDEX_PROBE_LIMIT) truncated = true;
|
|
2285
2882
|
for (const item of list) {
|
|
@@ -2522,8 +3119,9 @@ function indexScopeKey(projectId, platform) {
|
|
|
2522
3119
|
return projectId + "#" + platform;
|
|
2523
3120
|
}
|
|
2524
3121
|
function mapHistoryListToMessages(list, platform, opts) {
|
|
2525
|
-
var mapped = [], runningItemIds = [];
|
|
3122
|
+
var mapped = [], runningItemIds = [], streamPendingItemIds = [];
|
|
2526
3123
|
var extractAssistantText = platform === "openai" ? extractOpenAIText : extractClaudeText;
|
|
3124
|
+
var canRecoverStreams = streamRecoveryEnabled();
|
|
2527
3125
|
var filtered = filterListByClearHorizon(list, opts.clearedAt);
|
|
2528
3126
|
filtered.slice().reverse().forEach(function(item) {
|
|
2529
3127
|
var requestBody = item && item.request_body;
|
|
@@ -2537,6 +3135,7 @@ function mapHistoryListToMessages(list, platform, opts) {
|
|
|
2537
3135
|
var userText = isCompact ? typeof item.request_text === "string" ? item.request_text : "" : extractLastUserTextFromRequest(requestBody);
|
|
2538
3136
|
var assistantText = isPending ? "" : isCompact ? (typeof item.response_text === "string" ? item.response_text : "").trim() : (extractAssistantText(response) || "").trim() || "";
|
|
2539
3137
|
var isErrorResponse = !isPending && (isFailed || !isCompact && isErrorResponseBody(response));
|
|
3138
|
+
var isStreamPending = canRecoverStreams && !isCompact && !isPending && !isCancelledItem && !isErrorResponse && !item._isBgTask && !item._isOnBgQueue && item.status === "resolved" && item.response_body == null && item.error == null && !assistantText;
|
|
2540
3139
|
var reportedComplete = !!(item && item._isBgTask) && !isErrorResponse && (isCompact ? item.response_complete_marker === true : !!assistantText && assistantText.indexOf(INDEXING_COMPLETE_MARKER) !== -1);
|
|
2541
3140
|
if (reportedComplete) assistantText = assistantText.split(INDEXING_COMPLETE_MARKER).join("").trim();
|
|
2542
3141
|
var serverItemId = item && typeof item.id === "string" && item.id ? item.id : void 0;
|
|
@@ -2595,6 +3194,14 @@ function mapHistoryListToMessages(list, platform, opts) {
|
|
|
2595
3194
|
if (serverItemId !== void 0) em._serverItemId = serverItemId;
|
|
2596
3195
|
if (replyTs !== void 0) em._ts = replyTs;
|
|
2597
3196
|
mapped.push(em);
|
|
3197
|
+
} else if (isStreamPending) {
|
|
3198
|
+
var sp = { role: "assistant", content: "", _streamPending: true };
|
|
3199
|
+
if (serverItemId !== void 0) {
|
|
3200
|
+
sp._serverItemId = serverItemId;
|
|
3201
|
+
streamPendingItemIds.push(serverItemId);
|
|
3202
|
+
}
|
|
3203
|
+
if (replyTs !== void 0) sp._ts = replyTs;
|
|
3204
|
+
mapped.push(sp);
|
|
2598
3205
|
} else if (assistantText || reportedComplete) {
|
|
2599
3206
|
var okm = { role: "assistant", content: sanitizeAttachmentLinksForHistory(assistantText, opts.projectId, true) || EMPTY_INDEXING_REPLY };
|
|
2600
3207
|
if (item._fromBgChain) okm._fromBgChain = true;
|
|
@@ -2610,7 +3217,23 @@ function mapHistoryListToMessages(list, platform, opts) {
|
|
|
2610
3217
|
var ownerKey = chatCacheKey(opts.projectId, platform, opts.userId);
|
|
2611
3218
|
for (var oi = 0; oi < mapped.length; oi++) mapped[oi]._ownerKey = ownerKey;
|
|
2612
3219
|
}
|
|
2613
|
-
return { messages: mapped, runningItemIds };
|
|
3220
|
+
return { messages: mapped, runningItemIds, streamPendingItemIds };
|
|
3221
|
+
}
|
|
3222
|
+
function adoptLocalAnswerIntoPage(incoming, local) {
|
|
3223
|
+
if (!incoming || !local || !incoming._streamPending) return false;
|
|
3224
|
+
if (incoming.role !== "assistant" || local.role !== "assistant") return false;
|
|
3225
|
+
var hasText = typeof local.content === "string" && local.content.length > 0;
|
|
3226
|
+
var isLive = !!(local.isPending || local._streaming);
|
|
3227
|
+
if (!hasText && !isLive) return false;
|
|
3228
|
+
if (hasText) {
|
|
3229
|
+
incoming.content = local.content;
|
|
3230
|
+
incoming._streamPending = false;
|
|
3231
|
+
}
|
|
3232
|
+
if (local._localId !== void 0) incoming._localId = local._localId;
|
|
3233
|
+
if (local.isPending) incoming.isPending = true;
|
|
3234
|
+
if (local.isPendingInProcess) incoming.isPendingInProcess = true;
|
|
3235
|
+
if (local._streaming) incoming._streaming = true;
|
|
3236
|
+
return true;
|
|
2614
3237
|
}
|
|
2615
3238
|
function shouldRescueInFlightMessage(m, ctx) {
|
|
2616
3239
|
if (!m) return false;
|
|
@@ -2618,6 +3241,7 @@ function shouldRescueInFlightMessage(m, ctx) {
|
|
|
2618
3241
|
if (m._ownerKey !== void 0 && ctx.loadKey !== void 0 && m._ownerKey !== ctx.loadKey) return false;
|
|
2619
3242
|
if (m._serverItemId && ctx.hasServerId(m._serverItemId)) return false;
|
|
2620
3243
|
if (m._stageId) return true;
|
|
3244
|
+
if (m._streaming && !m._serverItemId) return true;
|
|
2621
3245
|
if (!m._serverItemId && ctx.pageHasPendingAssistant) return false;
|
|
2622
3246
|
if (m.isSendingToServer || m.isPendingQueued || m.isPendingInProcess || m.isPending) return true;
|
|
2623
3247
|
if (ctx.sending && m.role === "user") {
|
|
@@ -3004,8 +3628,143 @@ function nextFrame(cb) {
|
|
|
3004
3628
|
function isPollStopped(res) {
|
|
3005
3629
|
return !!res && typeof res === "object" && res.status === "stopped";
|
|
3006
3630
|
}
|
|
3631
|
+
var LIVE_PENDING_LINK_WINDOW = 512;
|
|
3632
|
+
var STREAM_RECOVERY_PER_LOAD = 2;
|
|
3633
|
+
function liveSafePrefix(text) {
|
|
3634
|
+
if (!text) return "";
|
|
3635
|
+
var cut = text.length;
|
|
3636
|
+
var fenceAt = -1, fences = 0, from = 0, hit;
|
|
3637
|
+
for (; ; ) {
|
|
3638
|
+
hit = text.indexOf("```", from);
|
|
3639
|
+
if (hit === -1) break;
|
|
3640
|
+
fences++;
|
|
3641
|
+
fenceAt = hit;
|
|
3642
|
+
from = hit + 3;
|
|
3643
|
+
}
|
|
3644
|
+
if (fences % 2 === 1 && fenceAt !== -1) cut = fenceAt;
|
|
3645
|
+
var head = text.slice(0, cut);
|
|
3646
|
+
var lineStart = head.lastIndexOf("\n") + 1;
|
|
3647
|
+
var line = head.slice(lineStart);
|
|
3648
|
+
var open = line.lastIndexOf("[");
|
|
3649
|
+
if (open !== -1 && line.length - open <= LIVE_PENDING_LINK_WINDOW) {
|
|
3650
|
+
var rest = line.slice(open);
|
|
3651
|
+
var close = rest.indexOf("]");
|
|
3652
|
+
if (close === -1) {
|
|
3653
|
+
cut = lineStart + open;
|
|
3654
|
+
} else if (rest.charAt(close + 1) === "(" && rest.indexOf(")", close + 1) === -1) {
|
|
3655
|
+
cut = lineStart + open;
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
var tokStart = line.length;
|
|
3659
|
+
while (tokStart > 0 && !/\s/.test(line.charAt(tokStart - 1))) tokStart--;
|
|
3660
|
+
var tok = line.slice(tokStart);
|
|
3661
|
+
if (tok && /^(?:https?:\/\/|src::)/i.test(tok)) {
|
|
3662
|
+
var tokCut = lineStart + tokStart;
|
|
3663
|
+
if (tokCut < cut) cut = tokCut;
|
|
3664
|
+
}
|
|
3665
|
+
if (line.indexOf("```") === -1) {
|
|
3666
|
+
var ticks = 0, lastTick = -1;
|
|
3667
|
+
for (var i = 0; i < line.length; i++) {
|
|
3668
|
+
if (line.charAt(i) === "`") {
|
|
3669
|
+
ticks++;
|
|
3670
|
+
lastTick = i;
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
if (ticks % 2 === 1 && lastTick !== -1) {
|
|
3674
|
+
var tickCut = lineStart + lastTick;
|
|
3675
|
+
if (tickCut < cut) cut = tickCut;
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
if (cut >= text.length) return text;
|
|
3679
|
+
if (cut < 0) cut = 0;
|
|
3680
|
+
return text.slice(0, cut);
|
|
3681
|
+
}
|
|
3682
|
+
function commonPrefixLength(a, b) {
|
|
3683
|
+
var n = Math.min(a.length, b.length), i = 0;
|
|
3684
|
+
while (i < n && a.charCodeAt(i) === b.charCodeAt(i)) i++;
|
|
3685
|
+
if (i > 0) {
|
|
3686
|
+
var prev = a.charCodeAt(i - 1);
|
|
3687
|
+
if (prev >= 55296 && prev <= 56319) i--;
|
|
3688
|
+
}
|
|
3689
|
+
return i;
|
|
3690
|
+
}
|
|
3691
|
+
function typewriterResumeIndex(painted, fullText, regions) {
|
|
3692
|
+
if (!painted || !fullText) return 0;
|
|
3693
|
+
if (/^\s/.test(painted) && !/^\s/.test(fullText)) {
|
|
3694
|
+
painted = painted.replace(/^\s+/, "");
|
|
3695
|
+
if (!painted) return 0;
|
|
3696
|
+
}
|
|
3697
|
+
var i = commonPrefixLength(painted, fullText);
|
|
3698
|
+
if (i <= 0) return 0;
|
|
3699
|
+
if (i >= fullText.length) return fullText.length;
|
|
3700
|
+
for (var changed = true; changed; ) {
|
|
3701
|
+
changed = false;
|
|
3702
|
+
for (var k = 0; k < regions.length; k++) {
|
|
3703
|
+
var r = regions[k];
|
|
3704
|
+
if (i > r.start && i < r.end) {
|
|
3705
|
+
i = r.end;
|
|
3706
|
+
changed = true;
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
return i > fullText.length ? fullText.length : i;
|
|
3711
|
+
}
|
|
3712
|
+
function mayKeepStreamedAnswer(snap, rowStatus) {
|
|
3713
|
+
if (rowStatus !== void 0 && rowStatus !== null && rowStatus !== "" && rowStatus !== "resolved") return false;
|
|
3714
|
+
if (!snap || typeof snap !== "object") return false;
|
|
3715
|
+
if (snap.errored) return false;
|
|
3716
|
+
if (snap.answerComplete) return true;
|
|
3717
|
+
if (snap.unframed) return true;
|
|
3718
|
+
return false;
|
|
3719
|
+
}
|
|
3720
|
+
function streamRecoveryPhase(msg) {
|
|
3721
|
+
if (!msg || !msg._streamPending || msg.content || !msg._serverItemId) return "";
|
|
3722
|
+
if (msg._streamRecovery === "active") return "active";
|
|
3723
|
+
if (msg._streamRecovery === "failed") return "failed";
|
|
3724
|
+
return "idle";
|
|
3725
|
+
}
|
|
3726
|
+
function streamRecoveryLabels(phase) {
|
|
3727
|
+
if (phase === "failed") {
|
|
3728
|
+
return { note: "Could not load this answer.", action: "Try again" };
|
|
3729
|
+
}
|
|
3730
|
+
return { note: "This answer was not saved with the conversation.", action: "Load answer" };
|
|
3731
|
+
}
|
|
3732
|
+
var LIVE_PAINT_MIN_MS = 250;
|
|
3733
|
+
var LIVE_TYPE_MAX_STEP = 1200;
|
|
3007
3734
|
var ChatSession = class {
|
|
3008
3735
|
constructor(host) {
|
|
3736
|
+
// --- live streaming ----------------------------------------------------
|
|
3737
|
+
//
|
|
3738
|
+
// A streamed turn's answer NEVER reaches the polling row: the relay appends the
|
|
3739
|
+
// destination's raw bytes to a chunk table and the row settles with a status and
|
|
3740
|
+
// nothing else. So for a streamed turn this parser is not a nicety that makes the
|
|
3741
|
+
// wait prettier, it is the only place the answer exists until csr-finalize stores
|
|
3742
|
+
// one. Three things follow, and all three are load-bearing:
|
|
3743
|
+
//
|
|
3744
|
+
// 1. EVERY foreground poll gets a sink while streaming is on, not just the one
|
|
3745
|
+
// the dispatch attaches. A tab return, a reload, a resumePolling all
|
|
3746
|
+
// re-attach a poll to a still-running item, and skapi's reader sends
|
|
3747
|
+
// `since: 0` on its first tick, so a fresh sink REPLAYS the whole stream from
|
|
3748
|
+
// the beginning. Attaching without one settles that turn on an envelope and
|
|
3749
|
+
// the user's answer is gone.
|
|
3750
|
+
// 2. The parser is keyed by SERVER ITEM ID, and so is the bubble it paints into.
|
|
3751
|
+
// A history refetch replaces the local pending bubble with the server's copy
|
|
3752
|
+
// of the same turn; that copy carries the same _serverItemId, so the next
|
|
3753
|
+
// paint finds it and carries on. Nothing has to be rescued and nothing can be
|
|
3754
|
+
// painted twice.
|
|
3755
|
+
// 3. The stream is never the source of truth. At settle the parser's ASSEMBLED
|
|
3756
|
+
// body (byte equivalent to what a buffered call returns) goes through the
|
|
3757
|
+
// same extractClaudeText / extractOpenAIText the buffered path uses, and a
|
|
3758
|
+
// row that does hold a stored body wins outright.
|
|
3759
|
+
//
|
|
3760
|
+
// Background polls never get a sink, and that is safe because nothing on the bg
|
|
3761
|
+
// queue ever streams: an indexing pass must not (the worker READS its reply), and
|
|
3762
|
+
// a chat turn sent with attachments is deliberately left buffered for exactly the
|
|
3763
|
+
// reason point 1 gives, since the re-attach loop would poll it as a background
|
|
3764
|
+
// item and hand it no reader. See chatStreamWiring. A sink there would also spend
|
|
3765
|
+
// the request budget MAX_CONCURRENT_BG_POLLS exists to protect.
|
|
3766
|
+
/** Live streams by server item id. One per in-flight streamed turn. */
|
|
3767
|
+
this.liveStreams = {};
|
|
3009
3768
|
// ─── compact-stub hydration ─────────────────────────────────────────────
|
|
3010
3769
|
// Split-fetch bg pages arrive as label stubs (no bodies). When the user
|
|
3011
3770
|
// expands a row, the real reply text is fetched per item (csr-poll point
|
|
@@ -3210,8 +3969,8 @@ var ChatSession = class {
|
|
|
3210
3969
|
return Promise.resolve(probeBgQueue(
|
|
3211
3970
|
{ service: id.projectId, owner: id.owner, platform, queue, status, limit: WORKER_PASS_ADOPT_LIMIT },
|
|
3212
3971
|
{ maxAgeMs: BG_PROBE_TTL_MS }
|
|
3213
|
-
)).then(function(
|
|
3214
|
-
return
|
|
3972
|
+
)).then(function(entry2) {
|
|
3973
|
+
return entry2.result;
|
|
3215
3974
|
}).catch(function() {
|
|
3216
3975
|
return null;
|
|
3217
3976
|
});
|
|
@@ -3398,10 +4157,49 @@ var ChatSession = class {
|
|
|
3398
4157
|
* and they are the ones bounded by MAX_CONCURRENT_BG_POLLS, so adding probes there would spend
|
|
3399
4158
|
* the request budget the cap exists to protect.
|
|
3400
4159
|
*/
|
|
3401
|
-
attachForegroundPoll(source, itemId, opts) {
|
|
3402
|
-
return this._fgPollWithEarlyProbe(source, itemId, opts);
|
|
4160
|
+
attachForegroundPoll(source, itemId, opts, ctx) {
|
|
4161
|
+
return this._fgPollWithEarlyProbe(source, itemId, opts, ctx);
|
|
3403
4162
|
}
|
|
3404
|
-
_fgPollWithEarlyProbe(source, itemId, opts) {
|
|
4163
|
+
_fgPollWithEarlyProbe(source, itemId, opts, ctx) {
|
|
4164
|
+
var self = this;
|
|
4165
|
+
var live = this._beginLiveStream(itemId, ctx);
|
|
4166
|
+
if (live) {
|
|
4167
|
+
var inner = opts || {};
|
|
4168
|
+
var callerResponse = typeof inner.onResponse === "function" ? inner.onResponse : null;
|
|
4169
|
+
var callerError = typeof inner.onError === "function" ? inner.onError : null;
|
|
4170
|
+
var streamOpts = Object.assign({}, inner, {
|
|
4171
|
+
onStream: function(chunk, _seq, via) {
|
|
4172
|
+
self._feedLiveStream(live, chunk, via);
|
|
4173
|
+
},
|
|
4174
|
+
onResponse: function(res) {
|
|
4175
|
+
var effective = res;
|
|
4176
|
+
if (isPollStopped(res)) self._closeLiveStream(live, false);
|
|
4177
|
+
else effective = self._settleLiveStream(live, res);
|
|
4178
|
+
if (callerResponse) callerResponse(effective);
|
|
4179
|
+
},
|
|
4180
|
+
onError: function(err) {
|
|
4181
|
+
self._closeLiveStream(live, false);
|
|
4182
|
+
if (callerError) callerError(err);
|
|
4183
|
+
}
|
|
4184
|
+
});
|
|
4185
|
+
var lp = source.poll(Object.assign({ latency: STREAM_POLL_INTERVAL }, streamOpts));
|
|
4186
|
+
var stopLp = lp && typeof lp.stop === "function" ? lp.stop.bind(lp) : null;
|
|
4187
|
+
var wrapped = Promise.resolve(lp).then(function(res) {
|
|
4188
|
+
if (isPollStopped(res)) {
|
|
4189
|
+
self._closeLiveStream(live, false);
|
|
4190
|
+
return res;
|
|
4191
|
+
}
|
|
4192
|
+
return self._settleLiveStream(live, res);
|
|
4193
|
+
}, function(err) {
|
|
4194
|
+
self._closeLiveStream(live, false);
|
|
4195
|
+
throw err;
|
|
4196
|
+
});
|
|
4197
|
+
wrapped.stop = function() {
|
|
4198
|
+
self._closeLiveStream(live, false);
|
|
4199
|
+
if (stopLp) stopLp();
|
|
4200
|
+
};
|
|
4201
|
+
return wrapped;
|
|
4202
|
+
}
|
|
3405
4203
|
var base = source.poll(Object.assign({ latency: POLL_INTERVAL }, opts || {}));
|
|
3406
4204
|
var lookup = chatEngineConfig().csrHistoryItemLookup;
|
|
3407
4205
|
var ident = this.host.getIdentity();
|
|
@@ -3483,6 +4281,661 @@ var ChatSession = class {
|
|
|
3483
4281
|
});
|
|
3484
4282
|
return n;
|
|
3485
4283
|
}
|
|
4284
|
+
/**
|
|
4285
|
+
* Open (or re-open) the live stream for `itemId`, or null when this poll must
|
|
4286
|
+
* not carry one.
|
|
4287
|
+
*
|
|
4288
|
+
* Re-entrant on purpose: an auth-refresh retry re-dispatches the SAME turn under
|
|
4289
|
+
* a NEW id, and a re-attach after a tab return replays an existing id from seq 0.
|
|
4290
|
+
* Either way the bytes about to arrive are a whole stream, so an existing entry
|
|
4291
|
+
* is discarded and a fresh parser takes its place - feeding a replay into the old
|
|
4292
|
+
* parser would concatenate the answer with itself.
|
|
4293
|
+
*
|
|
4294
|
+
* `ctx` IS THE TURN'S OWN IDENTITY, and every caller that has one passes it.
|
|
4295
|
+
* This used to read the LIVE getIdentity(), which is a bug of exactly the kind
|
|
4296
|
+
* _callProviderFor documents and threads its own parameters to avoid: the user
|
|
4297
|
+
* hits Send, then switches project or platform inside the ack round trip, and the
|
|
4298
|
+
* stream that opens for the OLD turn is stamped with the NEW identity. What that
|
|
4299
|
+
* costs is not cosmetic - `platform` picks which url csr-finalize is addressed
|
|
4300
|
+
* with and which extractor reads the assembled body, `projectId`/`owner` scope
|
|
4301
|
+
* the finalize itself, and `ownerKey` decides which chat the answer is painted
|
|
4302
|
+
* into. Get them from the live read at the wrong moment and the turn is finalized
|
|
4303
|
+
* against the wrong service (so its answer is never stored), parsed with the
|
|
4304
|
+
* wrong provider's extractor, or painted into a conversation it does not belong
|
|
4305
|
+
* to. The live read stays only as the fallback for a caller with nothing pinned.
|
|
4306
|
+
*/
|
|
4307
|
+
_beginLiveStream(itemId, ctx) {
|
|
4308
|
+
if (!liveStreamingEnabled()) return null;
|
|
4309
|
+
if (!itemId) {
|
|
4310
|
+
console.warn("[chat-engine] live streaming is on but the dispatch reported no item id");
|
|
4311
|
+
return null;
|
|
4312
|
+
}
|
|
4313
|
+
var pinnedPlatform = ctx && (ctx.platform === "claude" || ctx.platform === "openai") ? ctx.platform : void 0;
|
|
4314
|
+
var ident = pinnedPlatform && ctx && ctx.projectId !== void 0 && ctx.owner !== void 0 && ctx.ownerKey !== void 0 ? null : this.host.getIdentity();
|
|
4315
|
+
var platform = pinnedPlatform || (ident ? ident.platform : void 0);
|
|
4316
|
+
if (platform !== "claude" && platform !== "openai") return null;
|
|
4317
|
+
var projectId = ctx && ctx.projectId !== void 0 ? ctx.projectId : ident ? ident.projectId : "";
|
|
4318
|
+
var owner = ctx && ctx.owner !== void 0 ? ctx.owner : ident ? ident.owner : "";
|
|
4319
|
+
var ownerKey = ctx && ctx.ownerKey !== void 0 ? ctx.ownerKey : this.getHistoryCacheKey();
|
|
4320
|
+
var prev = this.liveStreams[itemId];
|
|
4321
|
+
if (prev) this._closeLiveStream(prev, false);
|
|
4322
|
+
var st = {
|
|
4323
|
+
id: itemId,
|
|
4324
|
+
ownerKey,
|
|
4325
|
+
platform,
|
|
4326
|
+
projectId,
|
|
4327
|
+
owner,
|
|
4328
|
+
parser: createSseParser(),
|
|
4329
|
+
painted: "",
|
|
4330
|
+
started: false,
|
|
4331
|
+
fed: false,
|
|
4332
|
+
ended: false,
|
|
4333
|
+
timer: null,
|
|
4334
|
+
lastPaintAt: 0,
|
|
4335
|
+
finalBody: null,
|
|
4336
|
+
transport: { socket: 0, poll: 0 }
|
|
4337
|
+
};
|
|
4338
|
+
this.liveStreams[itemId] = st;
|
|
4339
|
+
return st;
|
|
4340
|
+
}
|
|
4341
|
+
/** The chunk sink handed to skapi's poll. Raw relayed text, in order, never parsed
|
|
4342
|
+
* here: the parser owns the grammar and this owns the pacing. */
|
|
4343
|
+
_feedLiveStream(st, chunk, via) {
|
|
4344
|
+
if (st.ended || typeof chunk !== "string" || !chunk) return;
|
|
4345
|
+
st.fed = true;
|
|
4346
|
+
if (via === "socket") st.transport.socket++;
|
|
4347
|
+
else if (via === "poll") st.transport.poll++;
|
|
4348
|
+
st.parser.feed(chunk);
|
|
4349
|
+
if (st.timer) return;
|
|
4350
|
+
var self = this;
|
|
4351
|
+
var wait = st.lastPaintAt ? Math.max(0, LIVE_PAINT_MIN_MS - (nowMs() - st.lastPaintAt)) : 0;
|
|
4352
|
+
st.timer = setTimeout(function() {
|
|
4353
|
+
st.timer = null;
|
|
4354
|
+
self._paintLiveStream(st);
|
|
4355
|
+
}, wait);
|
|
4356
|
+
}
|
|
4357
|
+
/**
|
|
4358
|
+
* Write the safe prefix of the answer so far into the turn's bubble.
|
|
4359
|
+
*
|
|
4360
|
+
* notify() is spent EXACTLY ONCE per turn, on the first paint, because that is a
|
|
4361
|
+
* state change the per-bubble refresh cannot express: the bubble stops being a
|
|
4362
|
+
* "Thinking..." spinner and becomes text. Every paint after it goes through
|
|
4363
|
+
* refreshMessageBubble, which is what keeps a growing answer from rebuilding the
|
|
4364
|
+
* whole display list once a second.
|
|
4365
|
+
*/
|
|
4366
|
+
_paintLiveStream(st) {
|
|
4367
|
+
if (st.ended) return;
|
|
4368
|
+
st.lastPaintAt = nowMs();
|
|
4369
|
+
if (this.getHistoryCacheKey() !== st.ownerKey) return;
|
|
4370
|
+
var idx = this._liveTargetIndex(st.id);
|
|
4371
|
+
if (idx === -1) return;
|
|
4372
|
+
var msg = this.state.messages[idx];
|
|
4373
|
+
if (!msg) return;
|
|
4374
|
+
var snap = st.parser.snapshot();
|
|
4375
|
+
var next = liveSafePrefix(snap.text);
|
|
4376
|
+
if (next.length <= st.painted.length) return;
|
|
4377
|
+
var prev = st.painted;
|
|
4378
|
+
st.painted = next;
|
|
4379
|
+
var grew = next.length - prev.length;
|
|
4380
|
+
var animate = grew > 0 && grew <= LIVE_TYPE_MAX_STEP;
|
|
4381
|
+
if (animate) {
|
|
4382
|
+
if (!msg._localId) msg._localId = this._newLocalId();
|
|
4383
|
+
if (!msg._streaming) {
|
|
4384
|
+
msg._streaming = true;
|
|
4385
|
+
this.host.notify();
|
|
4386
|
+
}
|
|
4387
|
+
this.enqueueTypewrite(idx, next, msg._localId, prev);
|
|
4388
|
+
} else {
|
|
4389
|
+
msg.content = next;
|
|
4390
|
+
if (!msg._streaming) {
|
|
4391
|
+
msg._streaming = true;
|
|
4392
|
+
this.host.notify();
|
|
4393
|
+
} else this.host.refreshMessageBubble(idx);
|
|
4394
|
+
}
|
|
4395
|
+
this.host.scrollToBottomIfSticky();
|
|
4396
|
+
this._reportLiveStream(st, st.started ? "update" : "start", snap, next);
|
|
4397
|
+
st.started = true;
|
|
4398
|
+
}
|
|
4399
|
+
/** The bubble a live stream paints into: the turn's pending assistant placeholder,
|
|
4400
|
+
* found by server item id. Not by _localId, deliberately - a history refetch
|
|
4401
|
+
* replaces the local copy with the server's, and only the id survives that. */
|
|
4402
|
+
_liveTargetIndex(itemId) {
|
|
4403
|
+
return this.state.messages.findIndex(function(m) {
|
|
4404
|
+
return !!m && m.role === "assistant" && !m.isBackgroundTask && m._serverItemId === itemId && (!!m.isPending || !!m._streaming);
|
|
4405
|
+
});
|
|
4406
|
+
}
|
|
4407
|
+
/** Hand the host its optional observation update. Guarded: this runs on the paint
|
|
4408
|
+
* path, and a throwing hook must not cost the user the rest of their answer. */
|
|
4409
|
+
_reportLiveStream(st, phase, snap, text) {
|
|
4410
|
+
var hook = chatEngineConfig().onLiveStreamUpdate;
|
|
4411
|
+
if (!hook) return;
|
|
4412
|
+
try {
|
|
4413
|
+
hook({
|
|
4414
|
+
serverItemId: st.id,
|
|
4415
|
+
ownerKey: st.ownerKey,
|
|
4416
|
+
phase,
|
|
4417
|
+
text,
|
|
4418
|
+
thinkingText: snap && snap.thinkingText || "",
|
|
4419
|
+
toolNames: snap && snap.toolNames ? snap.toolNames.slice() : [],
|
|
4420
|
+
complete: !!(snap && snap.complete),
|
|
4421
|
+
// Reported alongside `complete`, never instead of it: a host drawing
|
|
4422
|
+
// "still arriving" wants complete, a host drawing "this answer is
|
|
4423
|
+
// partial" wants this one, and an `error` frame is the case where the
|
|
4424
|
+
// two disagree. See sse.ts answerComplete.
|
|
4425
|
+
answerComplete: !!(snap && snap.answerComplete),
|
|
4426
|
+
errored: !!(snap && snap.errored),
|
|
4427
|
+
transport: { socket: st.transport.socket, poll: st.transport.poll }
|
|
4428
|
+
});
|
|
4429
|
+
} catch (e) {
|
|
4430
|
+
console.warn("[chat-engine] onLiveStreamUpdate threw", e);
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
/** Stop painting and (when the turn really ended) assemble the body. `finished`
|
|
4434
|
+
* is false for a stream being discarded rather than settled: a retry replacing
|
|
4435
|
+
* it, or a stop, neither of which has an answer to assemble. */
|
|
4436
|
+
_closeLiveStream(st, finished) {
|
|
4437
|
+
var first = !st.ended;
|
|
4438
|
+
if (st.timer) {
|
|
4439
|
+
clearTimeout(st.timer);
|
|
4440
|
+
st.timer = null;
|
|
4441
|
+
}
|
|
4442
|
+
if (first) {
|
|
4443
|
+
st.ended = true;
|
|
4444
|
+
if (finished && st.fed) {
|
|
4445
|
+
st.parser.end();
|
|
4446
|
+
st.finalBody = st.parser.finalBody();
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
if (this.liveStreams[st.id] === st) delete this.liveStreams[st.id];
|
|
4450
|
+
if (first && st.started) this._reportLiveStream(st, "end", st.parser.snapshot(), "");
|
|
4451
|
+
if (this.getHistoryCacheKey() !== st.ownerKey) return;
|
|
4452
|
+
var idx = this._liveTargetIndex(st.id);
|
|
4453
|
+
if (idx !== -1 && this.state.messages[idx] && this.state.messages[idx]._streaming) {
|
|
4454
|
+
this.state.messages[idx]._streaming = false;
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
/**
|
|
4458
|
+
* Settle a streamed turn: end the parse, decide the body the rest of the session
|
|
4459
|
+
* will read, and release the chunks.
|
|
4460
|
+
*
|
|
4461
|
+
* The substitution is one-directional and never a merge. A response that is a
|
|
4462
|
+
* real stored body (a buffered turn, or a streamed one somebody already
|
|
4463
|
+
* finalized) is returned untouched, because that is the destination's own answer
|
|
4464
|
+
* and the stream is not entitled to overwrite it. Only a STATUS ENVELOPE - the
|
|
4465
|
+
* shape a streamed row settles as, having stored nothing - is replaced, and then
|
|
4466
|
+
* by the assembled body, which every caller downstream reads with the same
|
|
4467
|
+
* extractor it uses for a buffered reply. Idempotent, because it is reached both
|
|
4468
|
+
* through the poll's onResponse and through the promise it resolves.
|
|
4469
|
+
*/
|
|
4470
|
+
_settleLiveStream(st, response) {
|
|
4471
|
+
this._closeLiveStream(st, true);
|
|
4472
|
+
if (!isCsrStatusEnvelope(response)) return response;
|
|
4473
|
+
if (response.status !== "resolved") return response;
|
|
4474
|
+
if (!this._mayFinalize(st)) this._rec().incomplete[st.id] = true;
|
|
4475
|
+
if (st.finalBody == null) return response;
|
|
4476
|
+
this._finalizeStreamedTurn(st);
|
|
4477
|
+
return st.finalBody;
|
|
4478
|
+
}
|
|
4479
|
+
/**
|
|
4480
|
+
* May this parse be STORED as the turn's permanent answer?
|
|
4481
|
+
*
|
|
4482
|
+
* THE FAILURE THIS PREVENTS. Finalizing does two things at once: it stores what
|
|
4483
|
+
* you give it as the row's result, and it DELETES the chunks it was assembled
|
|
4484
|
+
* from. So finalizing a truncated parse is not a cosmetic loss, it is the
|
|
4485
|
+
* permanent one: the truncation becomes the stored answer and the only copy of
|
|
4486
|
+
* the missing part is deleted in the same call. And a truncated parse is a shape
|
|
4487
|
+
* this repo has already paid for - a degraded chunk read (the poller degrades to
|
|
4488
|
+
* "no chunks this tick, more=true" on any transient chunk-table error, and caps
|
|
4489
|
+
* a long answer at 500k characters per response) can hand the settle a stream
|
|
4490
|
+
* that stopped mid-answer. The row can settle 'resolved' on top of that, because
|
|
4491
|
+
* the ROW's status describes the destination's request, not the client's read of
|
|
4492
|
+
* it.
|
|
4493
|
+
*
|
|
4494
|
+
* THE POLICY ITSELF IS mayKeepStreamedAnswer (top of this file), shared with the
|
|
4495
|
+
* recovery path so the two cannot drift apart again - they did, and the drift was
|
|
4496
|
+
* silent: the live settle refused a failed turn while the recovery finalized one.
|
|
4497
|
+
* What is local to this method is only the two things the free function cannot
|
|
4498
|
+
* know: that there is an assembled body at all, and that this call site is
|
|
4499
|
+
* reached only on a row that settled 'resolved' (the caller returns before it
|
|
4500
|
+
* otherwise), which is the status it therefore states.
|
|
4501
|
+
*
|
|
4502
|
+
* The test the policy applies is deliberately NOT `complete`: a terminal event
|
|
4503
|
+
* arrived and the answer finished are two claims, and an `error` frame satisfies
|
|
4504
|
+
* the first while truncating the second. See sse.ts's answerComplete.
|
|
4505
|
+
*/
|
|
4506
|
+
_mayFinalize(st) {
|
|
4507
|
+
if (st.finalBody == null) return false;
|
|
4508
|
+
return mayKeepStreamedAnswer(st.parser.snapshot(), "resolved");
|
|
4509
|
+
}
|
|
4510
|
+
/**
|
|
4511
|
+
* Store the assembled body as the version history keeps, which is also what
|
|
4512
|
+
* releases this request's chunks.
|
|
4513
|
+
*
|
|
4514
|
+
* The ASSEMBLED BODY and not the extracted text, because the row is read back by
|
|
4515
|
+
* mapHistoryListToMessages through extractClaudeText / extractOpenAIText: storing
|
|
4516
|
+
* the provider's own document is what makes a streamed turn indistinguishable
|
|
4517
|
+
* from a buffered one on the next load, with no branch anywhere in the mapper.
|
|
4518
|
+
*
|
|
4519
|
+
* BEST EFFORT, and loudly so: the answer is already on screen and already in the
|
|
4520
|
+
* history cache by the time this fires. A failure costs the chunks (they stay,
|
|
4521
|
+
* and the turn stays re-readable) and a row that reads back empty, never the
|
|
4522
|
+
* user's answer in front of them.
|
|
4523
|
+
*
|
|
4524
|
+
* WHAT IS DELIBERATELY NEVER FINALIZED, because finalize is also the only way to
|
|
4525
|
+
* release chunks and it is tempting to reach for it as a cleanup:
|
|
4526
|
+
*
|
|
4527
|
+
* - an INCOMPLETE parse (see _mayFinalize). Storing a truncation makes it
|
|
4528
|
+
* permanent AND deletes the part that was missing from it. A stream killed by
|
|
4529
|
+
* an `error` frame is one of these however terminal it looks: the frame ends
|
|
4530
|
+
* the stream, so `complete` is true, while the text is only what arrived
|
|
4531
|
+
* before the error. That is why the gate reads answerComplete.
|
|
4532
|
+
* - a FAILED turn. Its chunks hold the part of the answer that did arrive,
|
|
4533
|
+
* which is the only copy of that text there is, and the two ways to release
|
|
4534
|
+
* them both cost something real: storing the partial makes a truncated answer
|
|
4535
|
+
* the turn's permanent history AND masks the failure on read (csr-poll hands
|
|
4536
|
+
* back a finalized body before it ever looks at the row's error, so the turn
|
|
4537
|
+
* would read back as a clean short answer), while storing the error throws
|
|
4538
|
+
* the partial away outright. Keeping them costs storage on rows that produced
|
|
4539
|
+
* bytes and then failed, which is rare - a failure before the first byte (a
|
|
4540
|
+
* wrong API key, the common case) has no chunks to keep - and the poller
|
|
4541
|
+
* hands those chunks back alongside the error on every later read, so nothing
|
|
4542
|
+
* is stranded, only retained. Retention is the honest trade here; deletion is
|
|
4543
|
+
* not reversible.
|
|
4544
|
+
* - a CANCELLED turn, for the same reason plus one: the user's Stop means the
|
|
4545
|
+
* half answer is to be discarded, so writing it into history as the kept
|
|
4546
|
+
* version would resurrect exactly what the stop was for.
|
|
4547
|
+
*/
|
|
4548
|
+
_finalizeStreamedTurn(st) {
|
|
4549
|
+
if (st.finalized) return;
|
|
4550
|
+
if (!this._mayFinalize(st)) return;
|
|
4551
|
+
var fin = chatEngineConfig().clientSecretRequestFinalize;
|
|
4552
|
+
if (!fin || st.finalBody == null) return;
|
|
4553
|
+
st.finalized = true;
|
|
4554
|
+
var url = st.platform === "openai" ? OPENAI_RESPONSES_API_URL : ANTHROPIC_MESSAGES_API_URL;
|
|
4555
|
+
try {
|
|
4556
|
+
Promise.resolve(fin(st.id, st.finalBody, {
|
|
4557
|
+
url,
|
|
4558
|
+
method: "POST",
|
|
4559
|
+
service: st.projectId,
|
|
4560
|
+
owner: st.owner
|
|
4561
|
+
})).catch(function(err) {
|
|
4562
|
+
console.warn("[chat-engine] clientSecretRequestFinalize failed", err);
|
|
4563
|
+
});
|
|
4564
|
+
} catch (e) {
|
|
4565
|
+
console.warn("[chat-engine] clientSecretRequestFinalize threw", e);
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4568
|
+
/** Painted-but-unsettled live text on a bubble, for the typewriter to resume from.
|
|
4569
|
+
* A pending assistant placeholder is created with content '' by every path that
|
|
4570
|
+
* makes one, so non-empty content on one can only have been painted here. */
|
|
4571
|
+
_paintedTextAt(idx) {
|
|
4572
|
+
var m = idx >= 0 ? this.state.messages[idx] : void 0;
|
|
4573
|
+
if (!m || m.role !== "assistant" || typeof m.content !== "string") return "";
|
|
4574
|
+
return m.content;
|
|
4575
|
+
}
|
|
4576
|
+
/** The recovery bookkeeping, created on first touch.
|
|
4577
|
+
*
|
|
4578
|
+
* LAZY, not constructor-initialised, and for a concrete reason: ChatSession is
|
|
4579
|
+
* also built with Object.create(ChatSession.prototype) by the engine's own test
|
|
4580
|
+
* harnesses, which drive one method against a hand-built state rather than a
|
|
4581
|
+
* whole session. A field only the constructor creates is undefined there, and
|
|
4582
|
+
* the method that reaches for it throws, turning a test of the settle into a
|
|
4583
|
+
* crash about bookkeeping. */
|
|
4584
|
+
_rec() {
|
|
4585
|
+
if (!this._streamRecovery) this._streamRecovery = { incomplete: {}, attempted: {}, inflight: {}, failed: {}, queue: [], running: false };
|
|
4586
|
+
return this._streamRecovery;
|
|
4587
|
+
}
|
|
4588
|
+
/**
|
|
4589
|
+
* Put this session's fetching state onto the turn's bubble, so a view can tell a
|
|
4590
|
+
* loader that means something from one that means nothing.
|
|
4591
|
+
*
|
|
4592
|
+
* ONLY EVER ONTO A STILL-MARKED BUBBLE. Once `_streamPending` is off the turn has
|
|
4593
|
+
* an answer (or was proven to have none) and this says nothing about it; writing
|
|
4594
|
+
* it there would leave a stale 'active' on a settled bubble forever.
|
|
4595
|
+
*
|
|
4596
|
+
* host.notify() is what redraws the widget, whose renderer is imperative. It is a
|
|
4597
|
+
* no-op in agent.vue, whose state is a Vue reactive() - the property write above
|
|
4598
|
+
* is what redraws there. Both are covered by doing both, and neither is a
|
|
4599
|
+
* substitute for the other.
|
|
4600
|
+
*/
|
|
4601
|
+
_markRecoveryPhase(itemId, phase) {
|
|
4602
|
+
var changed = false;
|
|
4603
|
+
for (var i = 0; i < this.state.messages.length; i++) {
|
|
4604
|
+
var m = this.state.messages[i];
|
|
4605
|
+
if (!m || m.role !== "assistant" || m._serverItemId !== itemId || !m._streamPending) continue;
|
|
4606
|
+
var next = phase === null ? void 0 : phase;
|
|
4607
|
+
if (m._streamRecovery === next) continue;
|
|
4608
|
+
if (next === void 0) delete m._streamRecovery;
|
|
4609
|
+
else m._streamRecovery = next;
|
|
4610
|
+
changed = true;
|
|
4611
|
+
}
|
|
4612
|
+
if (changed) this.host.notify();
|
|
4613
|
+
}
|
|
4614
|
+
/**
|
|
4615
|
+
* Let LOCAL answers survive a freshly-mapped page whose copies of them are
|
|
4616
|
+
* authoritative-but-empty. Call with the page BEFORE it replaces or merges into
|
|
4617
|
+
* state.messages; mutates the page's bubbles in place.
|
|
4618
|
+
*
|
|
4619
|
+
* The adoption itself is history.ts's adoptLocalAnswerIntoPage (shared, so the
|
|
4620
|
+
* clients' own mappers cannot fork it). What lives here is the one thing the
|
|
4621
|
+
* pure function cannot know: whether the local text is the WHOLE answer. Text
|
|
4622
|
+
* left by a stream that ended without a terminal event is not, so that bubble
|
|
4623
|
+
* keeps its marker and gets read back even though it has content - otherwise a
|
|
4624
|
+
* truncated answer would adopt itself over the row and never be corrected.
|
|
4625
|
+
*/
|
|
4626
|
+
_adoptLocalAnswers(mapped, loadKey) {
|
|
4627
|
+
if (!mapped || !mapped.length) return;
|
|
4628
|
+
var pendingIncoming = [];
|
|
4629
|
+
for (var i = 0; i < mapped.length; i++) {
|
|
4630
|
+
if (mapped[i] && mapped[i]._streamPending) pendingIncoming.push(mapped[i]);
|
|
4631
|
+
}
|
|
4632
|
+
if (!pendingIncoming.length) return;
|
|
4633
|
+
var locals = {};
|
|
4634
|
+
for (var j = 0; j < this.state.messages.length; j++) {
|
|
4635
|
+
var lm = this.state.messages[j];
|
|
4636
|
+
if (!lm || lm.role !== "assistant" || !lm._serverItemId) continue;
|
|
4637
|
+
if (lm._ownerKey !== void 0 && loadKey !== void 0 && lm._ownerKey !== loadKey) continue;
|
|
4638
|
+
if (locals[lm._serverItemId] === void 0) locals[lm._serverItemId] = lm;
|
|
4639
|
+
}
|
|
4640
|
+
for (var k = 0; k < pendingIncoming.length; k++) {
|
|
4641
|
+
var inc = pendingIncoming[k];
|
|
4642
|
+
var id = inc._serverItemId;
|
|
4643
|
+
if (!id) continue;
|
|
4644
|
+
var local = locals[id];
|
|
4645
|
+
if (!local) continue;
|
|
4646
|
+
if (!adoptLocalAnswerIntoPage(inc, local)) continue;
|
|
4647
|
+
if (this._rec().incomplete[id]) inc._streamPending = true;
|
|
4648
|
+
}
|
|
4649
|
+
for (var p = 0; p < pendingIncoming.length; p++) {
|
|
4650
|
+
var pi = pendingIncoming[p];
|
|
4651
|
+
if (!pi._streamPending || !pi._serverItemId) continue;
|
|
4652
|
+
var phase = this._recoveryPhaseFor(pi._serverItemId);
|
|
4653
|
+
if (phase === null) delete pi._streamRecovery;
|
|
4654
|
+
else pi._streamRecovery = phase;
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
/**
|
|
4658
|
+
* This session's fetching state for one turn, from the bookkeeping rather than
|
|
4659
|
+
* from any bubble. A queued entry counts as 'active': it is committed to be read,
|
|
4660
|
+
* serially, and the reader has no way to tell "being read" from "next in line"
|
|
4661
|
+
* apart from the wait.
|
|
4662
|
+
*/
|
|
4663
|
+
_recoveryPhaseFor(itemId) {
|
|
4664
|
+
var rec = this._rec();
|
|
4665
|
+
if (rec.inflight[itemId]) return "active";
|
|
4666
|
+
for (var i = 0; i < rec.queue.length; i++) if (rec.queue[i].id === itemId) return "active";
|
|
4667
|
+
if (rec.failed[itemId]) return "failed";
|
|
4668
|
+
return null;
|
|
4669
|
+
}
|
|
4670
|
+
/**
|
|
4671
|
+
* PUBLIC DELEGATE, for a client that maps and merges its own history page.
|
|
4672
|
+
*
|
|
4673
|
+
* agent.vue keeps a forked mapper and a forked first-page merge (its mount path
|
|
4674
|
+
* runs them, while resumePolling routes through loadHistory below), so both
|
|
4675
|
+
* paths are live for the SAME row inside one component. Adoption is part of the
|
|
4676
|
+
* merge contract, not an optional extra: without it that fork erases a streamed
|
|
4677
|
+
* answer off the screen on every turn, which is the whole of MAJOR 3.
|
|
4678
|
+
*
|
|
4679
|
+
* Exposed rather than reimplemented because the rule needs the session's own
|
|
4680
|
+
* `incomplete` set, which the pure helper (history.ts adoptLocalAnswerIntoPage)
|
|
4681
|
+
* cannot see. A client that reached for the helper alone would adopt a TRUNCATED
|
|
4682
|
+
* answer over the row and clear the marker that would have gone back for the
|
|
4683
|
+
* rest - a fork that reads as correct and loses text.
|
|
4684
|
+
*
|
|
4685
|
+
* Call it exactly where loadHistory does: on the freshly mapped page, after
|
|
4686
|
+
* applyHydratedBodies and BEFORE the page replaces or merges into state.messages.
|
|
4687
|
+
*/
|
|
4688
|
+
adoptLocalAnswers(mapped, loadKey) {
|
|
4689
|
+
this._adoptLocalAnswers(mapped, loadKey);
|
|
4690
|
+
}
|
|
4691
|
+
/**
|
|
4692
|
+
* Queue the on-screen turns whose answer is only in the chunk store, newest
|
|
4693
|
+
* first, and start draining. Never blocks and never throws.
|
|
4694
|
+
*
|
|
4695
|
+
* `ownerKey` is the chat the queue entries belong to, snapshotted by the caller:
|
|
4696
|
+
* a recovery that lands after the user has moved on writes into that chat's
|
|
4697
|
+
* cache, never into whatever list is on screen by then.
|
|
4698
|
+
*/
|
|
4699
|
+
_scheduleStreamRecovery(ownerKey, platform, projectId, owner) {
|
|
4700
|
+
if (!streamRecoveryEnabled()) return;
|
|
4701
|
+
var rec = this._rec();
|
|
4702
|
+
var wanted = [];
|
|
4703
|
+
for (var i = this.state.messages.length - 1; i >= 0; i--) {
|
|
4704
|
+
var m = this.state.messages[i];
|
|
4705
|
+
if (!m || m.role !== "assistant" || !m._streamPending || !m._serverItemId) continue;
|
|
4706
|
+
var id = m._serverItemId;
|
|
4707
|
+
if (rec.attempted[id]) continue;
|
|
4708
|
+
if (this.liveStreams[id]) continue;
|
|
4709
|
+
if (rec.queue.some(function(e) {
|
|
4710
|
+
return e.id === id;
|
|
4711
|
+
})) continue;
|
|
4712
|
+
wanted.push(id);
|
|
4713
|
+
if (wanted.length >= STREAM_RECOVERY_PER_LOAD) break;
|
|
4714
|
+
}
|
|
4715
|
+
if (!wanted.length) return;
|
|
4716
|
+
for (var w = 0; w < wanted.length; w++) {
|
|
4717
|
+
rec.queue.push({ id: wanted[w], ownerKey, platform, projectId, owner });
|
|
4718
|
+
this._markRecoveryPhase(wanted[w], "active");
|
|
4719
|
+
}
|
|
4720
|
+
this._drainStreamRecovery();
|
|
4721
|
+
}
|
|
4722
|
+
/**
|
|
4723
|
+
* PUBLIC DELEGATE, the other half of what a forked history path needs.
|
|
4724
|
+
*
|
|
4725
|
+
* Same reason as adoptLocalAnswers: agent.vue's mount path never calls
|
|
4726
|
+
* loadHistory, so without this its pages would MARK unfinalized streamed turns
|
|
4727
|
+
* and then never read them back - CRITICAL 1 left unfixed on the client's
|
|
4728
|
+
* primary path, with the marker making it look handled.
|
|
4729
|
+
*
|
|
4730
|
+
* Takes the load's SNAPSHOTTED identity rather than reading it live, and that is
|
|
4731
|
+
* the reason this exists instead of the caller looping over recoverStreamedAnswer:
|
|
4732
|
+
* that one reads getIdentity() at call time (right, for an on-demand affordance
|
|
4733
|
+
* the user just clicked), which after a project switch racing the load would
|
|
4734
|
+
* finalize the turn against the project they switched TO. Call it AFTER the page
|
|
4735
|
+
* is rendered and the loading flags are cleared - it must never hold up the
|
|
4736
|
+
* conversation it belongs to.
|
|
4737
|
+
*/
|
|
4738
|
+
scheduleStreamRecovery(ownerKey, platform, projectId, owner) {
|
|
4739
|
+
this._scheduleStreamRecovery(ownerKey, platform, projectId, owner);
|
|
4740
|
+
}
|
|
4741
|
+
/** Serial drain of the recovery queue. Each entry is one full chunk read. */
|
|
4742
|
+
_drainStreamRecovery() {
|
|
4743
|
+
var rec = this._rec();
|
|
4744
|
+
if (rec.running) return;
|
|
4745
|
+
var next = rec.queue.shift();
|
|
4746
|
+
if (!next) return;
|
|
4747
|
+
rec.running = true;
|
|
4748
|
+
var self = this;
|
|
4749
|
+
this._readBackStreamedTurn(next.id, next.ownerKey, next.platform, next.projectId, next.owner).catch(function() {
|
|
4750
|
+
}).then(function() {
|
|
4751
|
+
self._rec().running = false;
|
|
4752
|
+
self._drainStreamRecovery();
|
|
4753
|
+
});
|
|
4754
|
+
}
|
|
4755
|
+
/**
|
|
4756
|
+
* Read one unfinalized streamed turn back out of the chunk store and put its
|
|
4757
|
+
* answer where the turn's answer belongs.
|
|
4758
|
+
*
|
|
4759
|
+
* Public because the cap above is deliberately small: a host that wants to offer
|
|
4760
|
+
* "load the rest" on an older recoverable turn calls this with its
|
|
4761
|
+
* `_serverItemId`, and gets the same path the automatic recovery uses. Safe to
|
|
4762
|
+
* call for an id that turns out not to be recoverable, and safe to call twice -
|
|
4763
|
+
* a second call while the first is still in flight is a no-op.
|
|
4764
|
+
*
|
|
4765
|
+
* THIS IS THE USER ASKING, and that is why it passes `manual`. The automatic
|
|
4766
|
+
* recovery refuses a row it has already tried, so that a re-render, or the
|
|
4767
|
+
* history load that every visibilitychange fires, cannot loop on the same
|
|
4768
|
+
* chunks. A click is neither of those: it is one bounded request that a person
|
|
4769
|
+
* asked for, and applying the loop guard to it made the affordance a button that
|
|
4770
|
+
* silently did nothing for exactly the rows most likely to have it - every row
|
|
4771
|
+
* an earlier read touched and could not settle.
|
|
4772
|
+
*/
|
|
4773
|
+
recoverStreamedAnswer(itemId) {
|
|
4774
|
+
if (!itemId) return Promise.resolve();
|
|
4775
|
+
var id = this.host.getIdentity();
|
|
4776
|
+
var platform = id && id.platform === "openai" ? "openai" : "claude";
|
|
4777
|
+
return this._readBackStreamedTurn(itemId, this.getHistoryCacheKey(), platform, id ? id.projectId : "", id ? id.owner : "", true);
|
|
4778
|
+
}
|
|
4779
|
+
_readBackStreamedTurn(itemId, ownerKey, platform, projectId, owner, manual) {
|
|
4780
|
+
var cfg = chatEngineConfig();
|
|
4781
|
+
var read = cfg.clientSecretRequestStream;
|
|
4782
|
+
if (!read || !itemId) return Promise.resolve();
|
|
4783
|
+
if (this._rec().inflight[itemId]) return Promise.resolve();
|
|
4784
|
+
if (!manual && this._rec().attempted[itemId]) {
|
|
4785
|
+
this._markRecoveryPhase(itemId, null);
|
|
4786
|
+
return Promise.resolve();
|
|
4787
|
+
}
|
|
4788
|
+
this._rec().attempted[itemId] = true;
|
|
4789
|
+
this._rec().inflight[itemId] = true;
|
|
4790
|
+
delete this._rec().failed[itemId];
|
|
4791
|
+
this._markRecoveryPhase(itemId, "active");
|
|
4792
|
+
var self = this;
|
|
4793
|
+
var url = platform === "openai" ? OPENAI_RESPONSES_API_URL : ANTHROPIC_MESSAGES_API_URL;
|
|
4794
|
+
var parser = createSseParser();
|
|
4795
|
+
var fed = false;
|
|
4796
|
+
return Promise.resolve(read(itemId, {
|
|
4797
|
+
url,
|
|
4798
|
+
method: "POST",
|
|
4799
|
+
service: projectId,
|
|
4800
|
+
owner,
|
|
4801
|
+
since: 0,
|
|
4802
|
+
onStream: function(chunk) {
|
|
4803
|
+
if (typeof chunk !== "string" || !chunk) return;
|
|
4804
|
+
fed = true;
|
|
4805
|
+
parser.feed(chunk);
|
|
4806
|
+
}
|
|
4807
|
+
})).then(function(res) {
|
|
4808
|
+
if (isPollStopped(res)) {
|
|
4809
|
+
delete self._rec().attempted[itemId];
|
|
4810
|
+
delete self._rec().inflight[itemId];
|
|
4811
|
+
self._markRecoveryPhase(itemId, null);
|
|
4812
|
+
return;
|
|
4813
|
+
}
|
|
4814
|
+
var envelope = isCsrStatusEnvelope(res);
|
|
4815
|
+
var body = null;
|
|
4816
|
+
if (res && !envelope) {
|
|
4817
|
+
body = res;
|
|
4818
|
+
} else if (fed) {
|
|
4819
|
+
parser.end();
|
|
4820
|
+
body = parser.finalBody();
|
|
4821
|
+
}
|
|
4822
|
+
var snap = parser.snapshot();
|
|
4823
|
+
var fromRow = !!(res && !envelope);
|
|
4824
|
+
var rowStatus = envelope && typeof res.status === "string" ? res.status : void 0;
|
|
4825
|
+
var degraded = !!(envelope && res && res.more === true);
|
|
4826
|
+
var store = !fromRow && !degraded && body != null && mayKeepStreamedAnswer(snap, rowStatus);
|
|
4827
|
+
delete self._rec().inflight[itemId];
|
|
4828
|
+
delete self._rec().failed[itemId];
|
|
4829
|
+
self._markRecoveryPhase(itemId, null);
|
|
4830
|
+
if (degraded) {
|
|
4831
|
+
delete self._rec().attempted[itemId];
|
|
4832
|
+
}
|
|
4833
|
+
self._applyRecoveredAnswer(itemId, ownerKey, platform, projectId, owner, body, store, degraded);
|
|
4834
|
+
}, function(err) {
|
|
4835
|
+
console.warn("[chat-engine] could not read back a streamed turn", itemId, err);
|
|
4836
|
+
delete self._rec().attempted[itemId];
|
|
4837
|
+
delete self._rec().inflight[itemId];
|
|
4838
|
+
self._rec().failed[itemId] = true;
|
|
4839
|
+
self._markRecoveryPhase(itemId, "failed");
|
|
4840
|
+
});
|
|
4841
|
+
}
|
|
4842
|
+
/**
|
|
4843
|
+
* Write a recovered answer into the turn's bubble (or into the owning chat's
|
|
4844
|
+
* cache when the reader has moved on), then store it as the version history
|
|
4845
|
+
* keeps.
|
|
4846
|
+
*
|
|
4847
|
+
* FINALIZING IS WHAT MAKES THIS RUN ONCE. It copies the answer onto the row and
|
|
4848
|
+
* releases the chunks, so the next load reads an ordinary turn and no recovery is
|
|
4849
|
+
* scheduled for it ever again, by anyone, in any tab. `store` is the caller's
|
|
4850
|
+
* decision and carries two gates at once: mayKeepStreamedAnswer, the SAME keep
|
|
4851
|
+
* policy the live settle applies (an incomplete, errored or failed read is shown
|
|
4852
|
+
* but never stored, because storing it would make the truncation permanent and
|
|
4853
|
+
* delete the part that was missing), and whether the body is new at all (one
|
|
4854
|
+
* that came off the row is already stored).
|
|
4855
|
+
*/
|
|
4856
|
+
_applyRecoveredAnswer(itemId, ownerKey, platform, projectId, owner, body, store, degraded) {
|
|
4857
|
+
var text = "";
|
|
4858
|
+
var isErr = isErrorResponseBody(body);
|
|
4859
|
+
if (body != null && !isErr) {
|
|
4860
|
+
text = ((platform === "openai" ? extractOpenAIText(body) : extractClaudeText(body)) || "").trim();
|
|
4861
|
+
}
|
|
4862
|
+
if (!text && !isErr) {
|
|
4863
|
+
if (degraded) {
|
|
4864
|
+
return;
|
|
4865
|
+
}
|
|
4866
|
+
this._clearStreamPendingMark(itemId, ownerKey, true);
|
|
4867
|
+
return;
|
|
4868
|
+
}
|
|
4869
|
+
var reply = isErr ? { role: "assistant", content: getErrorMessage(body), isError: true, _serverItemId: itemId } : { role: "assistant", content: text, _serverItemId: itemId };
|
|
4870
|
+
if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
|
|
4871
|
+
this._applyReplyToCache(ownerKey, reply, itemId);
|
|
4872
|
+
} else {
|
|
4873
|
+
var idx = -1;
|
|
4874
|
+
for (var i = 0; i < this.state.messages.length; i++) {
|
|
4875
|
+
var m = this.state.messages[i];
|
|
4876
|
+
if (m && m.role === "assistant" && m._serverItemId === itemId) {
|
|
4877
|
+
idx = i;
|
|
4878
|
+
break;
|
|
4879
|
+
}
|
|
4880
|
+
}
|
|
4881
|
+
if (idx === -1) {
|
|
4882
|
+
this._applyReplyToCache(ownerKey, reply, itemId);
|
|
4883
|
+
} else {
|
|
4884
|
+
var prev = this.state.messages[idx];
|
|
4885
|
+
if (prev._ts !== void 0) reply._ts = prev._ts;
|
|
4886
|
+
if (prev._ownerKey !== void 0) reply._ownerKey = prev._ownerKey;
|
|
4887
|
+
this.state.messages[idx] = reply;
|
|
4888
|
+
this.updateHistoryCache();
|
|
4889
|
+
this.host.notify();
|
|
4890
|
+
}
|
|
4891
|
+
}
|
|
4892
|
+
if (!degraded) delete this._rec().incomplete[itemId];
|
|
4893
|
+
if (!store || body == null || isErr) return;
|
|
4894
|
+
var fin = chatEngineConfig().clientSecretRequestFinalize;
|
|
4895
|
+
if (!fin) return;
|
|
4896
|
+
var url = platform === "openai" ? OPENAI_RESPONSES_API_URL : ANTHROPIC_MESSAGES_API_URL;
|
|
4897
|
+
try {
|
|
4898
|
+
Promise.resolve(fin(itemId, body, { url, method: "POST", service: projectId, owner })).catch(function(err) {
|
|
4899
|
+
console.warn("[chat-engine] finalize of a recovered turn failed", err);
|
|
4900
|
+
});
|
|
4901
|
+
} catch (e) {
|
|
4902
|
+
console.warn("[chat-engine] finalize of a recovered turn threw", e);
|
|
4903
|
+
}
|
|
4904
|
+
}
|
|
4905
|
+
/**
|
|
4906
|
+
* Take the "answer is elsewhere" marker off a turn once it is settled one way or
|
|
4907
|
+
* the other. `drop` removes an assistant bubble that turned out to have no answer
|
|
4908
|
+
* at all, which restores exactly the list the mapper used to produce for such a
|
|
4909
|
+
* row (none), rather than leaving a permanently empty bubble behind.
|
|
4910
|
+
*
|
|
4911
|
+
* ONLY EVER CALLED FOR A TURN THAT WAS ACTUALLY READ. The marker is the one thing
|
|
4912
|
+
* that keeps an unrecovered answer reachable, so it comes off only on the strength
|
|
4913
|
+
* of an answer (the recovery wrote one) or of a read that came back empty. A read
|
|
4914
|
+
* that FAILED, or one that was STOPPED, knows neither, and taking the marker off
|
|
4915
|
+
* on either of those is how a bubble ends up empty forever with its answer still
|
|
4916
|
+
* in the chunk table. `drop` is likewise never passed for a bubble that HAS
|
|
4917
|
+
* content: an empty row is an empty turn, a failed read is not.
|
|
4918
|
+
*/
|
|
4919
|
+
_clearStreamPendingMark(itemId, ownerKey, drop) {
|
|
4920
|
+
if (ownerKey && this.getHistoryCacheKey() !== ownerKey) return;
|
|
4921
|
+
var changed = false;
|
|
4922
|
+
for (var i = this.state.messages.length - 1; i >= 0; i--) {
|
|
4923
|
+
var m = this.state.messages[i];
|
|
4924
|
+
if (!m || m.role !== "assistant" || m._serverItemId !== itemId) continue;
|
|
4925
|
+
if (!m._streamPending) continue;
|
|
4926
|
+
if (drop && !m.content) {
|
|
4927
|
+
this.state.messages.splice(i, 1);
|
|
4928
|
+
changed = true;
|
|
4929
|
+
continue;
|
|
4930
|
+
}
|
|
4931
|
+
m._streamPending = false;
|
|
4932
|
+
changed = true;
|
|
4933
|
+
}
|
|
4934
|
+
if (changed) {
|
|
4935
|
+
this.updateHistoryCache();
|
|
4936
|
+
this.host.notify();
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
3486
4939
|
/**
|
|
3487
4940
|
* Stop and forget one item's poll. Used after a cancel: the row is either gone
|
|
3488
4941
|
* (cancelled while queued) or flagged cancelled (cancelled while running), so
|
|
@@ -3797,7 +5250,12 @@ var ChatSession = class {
|
|
|
3797
5250
|
dispatchItemId = initial.id;
|
|
3798
5251
|
if (typeof params.onItemId === "function") params.onItemId(initial.id);
|
|
3799
5252
|
}
|
|
3800
|
-
var dp = self._fgPollWithEarlyProbe(initial, initial.id
|
|
5253
|
+
var dp = self._fgPollWithEarlyProbe(initial, initial.id, void 0, {
|
|
5254
|
+
platform: params.aiPlatform,
|
|
5255
|
+
projectId: params.projectId,
|
|
5256
|
+
owner: params.owner,
|
|
5257
|
+
ownerKey: params.key
|
|
5258
|
+
});
|
|
3801
5259
|
if (initial.id) self._trackPoll(initial.id, "fg", dp);
|
|
3802
5260
|
return dp;
|
|
3803
5261
|
}
|
|
@@ -4010,8 +5468,8 @@ var ChatSession = class {
|
|
|
4010
5468
|
Promise.resolve(probeBgQueue(
|
|
4011
5469
|
{ service: svcId, owner, platform, queue, status, limit: WORKER_PASS_ADOPT_LIMIT },
|
|
4012
5470
|
{ maxAgeMs: 0 }
|
|
4013
|
-
)).then(function(
|
|
4014
|
-
settle(
|
|
5471
|
+
)).then(function(entry2) {
|
|
5472
|
+
settle(entry2.result);
|
|
4015
5473
|
}, function() {
|
|
4016
5474
|
settle(null);
|
|
4017
5475
|
});
|
|
@@ -4229,7 +5687,12 @@ var ChatSession = class {
|
|
|
4229
5687
|
}
|
|
4230
5688
|
if (serverId) self._stampTurnWithItemId(capturedKey, capturedQueuedLid, void 0, serverId);
|
|
4231
5689
|
if (result && result.poll && (result.status === "pending" || result.status === "running")) {
|
|
4232
|
-
var qp = self._fgPollWithEarlyProbe(result, serverId
|
|
5690
|
+
var qp = self._fgPollWithEarlyProbe(result, serverId, void 0, {
|
|
5691
|
+
platform: capturedPlatform,
|
|
5692
|
+
projectId: id.projectId,
|
|
5693
|
+
owner: id.owner,
|
|
5694
|
+
ownerKey: capturedKey
|
|
5695
|
+
});
|
|
4233
5696
|
if (serverId) self._trackPoll(serverId, "fg", qp);
|
|
4234
5697
|
return qp.then(function(res) {
|
|
4235
5698
|
if (isPollStopped(res)) return;
|
|
@@ -4500,9 +5963,14 @@ var ChatSession = class {
|
|
|
4500
5963
|
answer = (answer || "").trim() || "No text response received from AI provider.";
|
|
4501
5964
|
var lid = this._newLocalId();
|
|
4502
5965
|
if (targetIdx >= 0 && this.state.messages[targetIdx] && this.state.messages[targetIdx].isPending) {
|
|
4503
|
-
this.
|
|
5966
|
+
var qPainted = this._paintedTextAt(targetIdx);
|
|
5967
|
+
var prevQ = this.state.messages[targetIdx] || {};
|
|
5968
|
+
var qSettled = { role: "assistant", content: qPainted, _localId: lid };
|
|
5969
|
+
if (prevQ._serverItemId) qSettled._serverItemId = prevQ._serverItemId;
|
|
5970
|
+
if (prevQ._ownerKey) qSettled._ownerKey = prevQ._ownerKey;
|
|
5971
|
+
this.state.messages[targetIdx] = qSettled;
|
|
4504
5972
|
this.host.notify();
|
|
4505
|
-
this.enqueueTypewrite(targetIdx, answer, lid);
|
|
5973
|
+
this.enqueueTypewrite(targetIdx, answer, lid, qPainted);
|
|
4506
5974
|
} else if (targetIdx >= 0) {
|
|
4507
5975
|
this.state.messages.splice(targetIdx, 0, { role: "assistant", content: "", _localId: lid });
|
|
4508
5976
|
this.host.notify();
|
|
@@ -4764,7 +6232,14 @@ var ChatSession = class {
|
|
|
4764
6232
|
// renders self-throttles to what the machine can actually paint.
|
|
4765
6233
|
// * rAF paces us to the browser's paint cycle and pauses in background
|
|
4766
6234
|
// tabs, so we never queue work faster than it can be drawn.
|
|
4767
|
-
|
|
6235
|
+
//
|
|
6236
|
+
// `paintedText` is what a LIVE STREAM already put in this bubble. The reveal
|
|
6237
|
+
// starts from the point the two texts stop agreeing rather than from zero: the
|
|
6238
|
+
// authoritative answer still replaces the live one character for character (it is
|
|
6239
|
+
// the only source of truth, and this method writes fullText and nothing else), but
|
|
6240
|
+
// retyping a paragraph the reader has just watched arrive is the one thing that
|
|
6241
|
+
// would make a streamed turn look worse than an unstreamed one.
|
|
6242
|
+
typewriteIntoIndex(idx, fullText, localId, paintedText) {
|
|
4768
6243
|
var self = this;
|
|
4769
6244
|
if (!fullText) return Promise.resolve();
|
|
4770
6245
|
var CHARS_PER_SEC = 300;
|
|
@@ -4780,7 +6255,7 @@ var ChatSession = class {
|
|
|
4780
6255
|
});
|
|
4781
6256
|
this.state.typing = true;
|
|
4782
6257
|
this.state.typingAbort = false;
|
|
4783
|
-
var i = 0;
|
|
6258
|
+
var i = paintedText ? typewriterResumeIndex(paintedText, fullText, regions) : 0;
|
|
4784
6259
|
var last = nowMs();
|
|
4785
6260
|
return new Promise(function(resolve) {
|
|
4786
6261
|
var done = false;
|
|
@@ -4795,16 +6270,14 @@ var ChatSession = class {
|
|
|
4795
6270
|
if (done) return;
|
|
4796
6271
|
done = true;
|
|
4797
6272
|
cleanup();
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
self.host.refreshMessageBubble(fi);
|
|
4807
|
-
}
|
|
6273
|
+
var fi = localId ? self.state.messages.findIndex(function(mm) {
|
|
6274
|
+
return mm._localId === localId;
|
|
6275
|
+
}) : idx;
|
|
6276
|
+
if (fi !== -1) {
|
|
6277
|
+
var t = self.state.messages[fi];
|
|
6278
|
+
if (t) {
|
|
6279
|
+
t.content = fullText;
|
|
6280
|
+
self.host.refreshMessageBubble(fi);
|
|
4808
6281
|
}
|
|
4809
6282
|
}
|
|
4810
6283
|
self.state.typing = false;
|
|
@@ -4863,12 +6336,13 @@ var ChatSession = class {
|
|
|
4863
6336
|
nextFrame(frame);
|
|
4864
6337
|
});
|
|
4865
6338
|
}
|
|
4866
|
-
enqueueTypewrite(idx, fullText, localId) {
|
|
6339
|
+
enqueueTypewrite(idx, fullText, localId, paintedText) {
|
|
4867
6340
|
var self = this;
|
|
4868
6341
|
var target = this.state.messages[idx];
|
|
4869
6342
|
if (target && target._ts === void 0) target._ts = wallClockNow();
|
|
6343
|
+
if (!this.typewriterQueue) this.typewriterQueue = Promise.resolve();
|
|
4870
6344
|
this.typewriterQueue = this.typewriterQueue.then(function() {
|
|
4871
|
-
return self.typewriteIntoIndex(idx, fullText, localId);
|
|
6345
|
+
return self.typewriteIntoIndex(idx, fullText, localId, paintedText);
|
|
4872
6346
|
});
|
|
4873
6347
|
return this.typewriterQueue;
|
|
4874
6348
|
}
|
|
@@ -4901,12 +6375,17 @@ var ChatSession = class {
|
|
|
4901
6375
|
this.promoteNextQueuedToRunning();
|
|
4902
6376
|
return Promise.resolve();
|
|
4903
6377
|
}
|
|
6378
|
+
var painted = this._paintedTextAt(pendingIdx);
|
|
4904
6379
|
var lid = this._newLocalId();
|
|
4905
|
-
this.state.messages[pendingIdx]
|
|
6380
|
+
var prevSettled = this.state.messages[pendingIdx] || {};
|
|
6381
|
+
var settled = { role: "assistant", content: painted, isPending: false, _localId: lid };
|
|
6382
|
+
if (prevSettled._serverItemId) settled._serverItemId = prevSettled._serverItemId;
|
|
6383
|
+
if (prevSettled._ownerKey) settled._ownerKey = prevSettled._ownerKey;
|
|
6384
|
+
this.state.messages[pendingIdx] = settled;
|
|
4906
6385
|
this._removeStrayPendingAssistants();
|
|
4907
6386
|
this.host.notify();
|
|
4908
6387
|
this.promoteNextQueuedToRunning();
|
|
4909
|
-
return this.enqueueTypewrite(pendingIdx, latest.content, lid);
|
|
6388
|
+
return this.enqueueTypewrite(pendingIdx, latest.content, lid, painted);
|
|
4910
6389
|
}
|
|
4911
6390
|
// Remove leftover non-background pending ("Thinking…") assistant bubbles: the
|
|
4912
6391
|
// duplicate that appears when a concurrent history refetch re-maps the still-
|
|
@@ -4936,6 +6415,7 @@ var ChatSession = class {
|
|
|
4936
6415
|
for (var k = this.state.messages.length - 1; k >= 0; k--) {
|
|
4937
6416
|
var m = this.state.messages[k];
|
|
4938
6417
|
if (!m || !m.isPending || m.role !== "assistant" || m.isBackgroundTask) continue;
|
|
6418
|
+
if (m._streaming) continue;
|
|
4939
6419
|
if (this._isLiveImmediatePlaceholder(k)) continue;
|
|
4940
6420
|
this.state.messages.splice(k, 1);
|
|
4941
6421
|
}
|
|
@@ -5113,10 +6593,11 @@ var ChatSession = class {
|
|
|
5113
6593
|
this.updateHistoryCache();
|
|
5114
6594
|
return;
|
|
5115
6595
|
}
|
|
6596
|
+
var hPainted = this._paintedTextAt(idx);
|
|
5116
6597
|
var lid = this._newLocalId();
|
|
5117
|
-
this.state.messages[idx] = { role: "assistant", content:
|
|
6598
|
+
this.state.messages[idx] = { role: "assistant", content: hPainted, _localId: lid, _serverItemId: itemId };
|
|
5118
6599
|
this.host.notify();
|
|
5119
|
-
this.enqueueTypewrite(idx, text, lid);
|
|
6600
|
+
this.enqueueTypewrite(idx, text, lid, hPainted);
|
|
5120
6601
|
this.updateHistoryCache();
|
|
5121
6602
|
return;
|
|
5122
6603
|
}
|
|
@@ -5158,11 +6639,11 @@ var ChatSession = class {
|
|
|
5158
6639
|
* path is project-relative ("report.xlsx"), and ONE ChatSession serves every
|
|
5159
6640
|
* project — unscoped, stopping a file in one project would silently suppress
|
|
5160
6641
|
* the same filename's continuations in another. */
|
|
5161
|
-
_indexKeyOf(
|
|
5162
|
-
if (!
|
|
5163
|
-
var file =
|
|
6642
|
+
_indexKeyOf(entry2) {
|
|
6643
|
+
if (!entry2) return "";
|
|
6644
|
+
var file = entry2.storagePath || entry2.filename;
|
|
5164
6645
|
if (!file) return "";
|
|
5165
|
-
return indexScopeKey(
|
|
6646
|
+
return indexScopeKey(entry2.projectId, entry2.platform) + "|" + file;
|
|
5166
6647
|
}
|
|
5167
6648
|
/**
|
|
5168
6649
|
* Reconcile the bg queue with the files the user has stopped.
|
|
@@ -5191,17 +6672,17 @@ var ChatSession = class {
|
|
|
5191
6672
|
if (m.isPending || m.isPendingQueued || m.isPendingInProcess) surfaced[m._serverItemId] = true;
|
|
5192
6673
|
});
|
|
5193
6674
|
for (var i = this.bgTaskQueue.length - 1; i >= 0; i--) {
|
|
5194
|
-
var
|
|
5195
|
-
var key = this._indexKeyOf(
|
|
6675
|
+
var entry2 = this.bgTaskQueue[i];
|
|
6676
|
+
var key = this._indexKeyOf(entry2);
|
|
5196
6677
|
if (!key || !this.cancelledIndexKeys.has(key)) continue;
|
|
5197
|
-
if (!
|
|
6678
|
+
if (!entry2.resumePass && !this.state.stoppedIndexIds[entry2.id]) {
|
|
5198
6679
|
this.cancelledIndexKeys.delete(key);
|
|
5199
6680
|
continue;
|
|
5200
6681
|
}
|
|
5201
|
-
if (surfaced[
|
|
6682
|
+
if (surfaced[entry2.id]) continue;
|
|
5202
6683
|
this.bgTaskQueue.splice(i, 1);
|
|
5203
|
-
this._stopPoll(
|
|
5204
|
-
this._cancelServerItem(
|
|
6684
|
+
this._stopPoll(entry2.id);
|
|
6685
|
+
this._cancelServerItem(entry2.id);
|
|
5205
6686
|
}
|
|
5206
6687
|
}
|
|
5207
6688
|
/**
|
|
@@ -5259,8 +6740,8 @@ var ChatSession = class {
|
|
|
5259
6740
|
return Promise.resolve(probeBgQueue(
|
|
5260
6741
|
{ service: svcId, owner, platform, queue, status, limit: WORKER_PASS_ADOPT_LIMIT },
|
|
5261
6742
|
{ maxAgeMs: 0 }
|
|
5262
|
-
)).then(function(
|
|
5263
|
-
return
|
|
6743
|
+
)).then(function(entry2) {
|
|
6744
|
+
return entry2.result;
|
|
5264
6745
|
}).catch(function() {
|
|
5265
6746
|
return null;
|
|
5266
6747
|
});
|
|
@@ -5413,30 +6894,30 @@ var ChatSession = class {
|
|
|
5413
6894
|
}
|
|
5414
6895
|
var bgPollBudget = MAX_CONCURRENT_BG_POLLS - this._countBgPolls();
|
|
5415
6896
|
var injectedAny = false;
|
|
5416
|
-
this.bgTaskQueue.forEach(function(
|
|
5417
|
-
if (
|
|
5418
|
-
if (!presentIds[
|
|
5419
|
-
var isRunning =
|
|
6897
|
+
this.bgTaskQueue.forEach(function(entry2) {
|
|
6898
|
+
if (entry2.projectId !== svcId || entry2.platform !== plat) return;
|
|
6899
|
+
if (!presentIds[entry2.id]) {
|
|
6900
|
+
var isRunning = entry2.status === "running";
|
|
5420
6901
|
var userBubble = {
|
|
5421
6902
|
role: "user",
|
|
5422
|
-
content: self.host.formatIndexingLabel(
|
|
6903
|
+
content: self.host.formatIndexingLabel(entry2.filename, entry2.mime, entry2.size, entry2.storagePath, entry2.isReindex, !!entry2.resumePass),
|
|
5423
6904
|
isBackgroundTask: true,
|
|
5424
|
-
_serverItemId:
|
|
6905
|
+
_serverItemId: entry2.id,
|
|
5425
6906
|
// Structured ref so this live pass groups with the same file's passes
|
|
5426
6907
|
// rebuilt from history (see indexing_groups.buildChatDisplayList).
|
|
5427
6908
|
_indexFile: {
|
|
5428
|
-
name:
|
|
5429
|
-
path:
|
|
5430
|
-
mime:
|
|
5431
|
-
size:
|
|
5432
|
-
isReindex: !!
|
|
5433
|
-
continued: !!
|
|
6909
|
+
name: entry2.filename,
|
|
6910
|
+
path: entry2.storagePath,
|
|
6911
|
+
mime: entry2.mime,
|
|
6912
|
+
size: entry2.size,
|
|
6913
|
+
isReindex: !!entry2.isReindex,
|
|
6914
|
+
continued: !!entry2.resumePass
|
|
5434
6915
|
}
|
|
5435
6916
|
};
|
|
5436
6917
|
if (isRunning) userBubble.isPendingInProcess = true;
|
|
5437
6918
|
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:
|
|
6919
|
+
var stageAt = self._stageIndex(self.state.messages, entry2.stageId);
|
|
6920
|
+
var runningBubble = isRunning ? { role: "assistant", content: "", isPending: true, isPendingInProcess: true, isBackgroundTask: true, _serverItemId: entry2.id } : null;
|
|
5440
6921
|
if (stageAt === -1) {
|
|
5441
6922
|
self.state.messages.push(userBubble);
|
|
5442
6923
|
if (runningBubble) self.state.messages.push(runningBubble);
|
|
@@ -5445,16 +6926,16 @@ var ChatSession = class {
|
|
|
5445
6926
|
} else {
|
|
5446
6927
|
self.state.messages.splice(stageAt, 0, userBubble);
|
|
5447
6928
|
}
|
|
5448
|
-
presentIds[
|
|
6929
|
+
presentIds[entry2.id] = true;
|
|
5449
6930
|
injectedAny = true;
|
|
5450
6931
|
}
|
|
5451
|
-
if (bgPollBudget > 0 && !self.isPollingPaused() && !self.historyItemPolls.has(
|
|
6932
|
+
if (bgPollBudget > 0 && !self.isPollingPaused() && !self.historyItemPolls.has(entry2.id) && typeof entry2.poll === "function") {
|
|
5452
6933
|
bgPollBudget--;
|
|
5453
|
-
var capturedId =
|
|
5454
|
-
var capturedEntry =
|
|
6934
|
+
var capturedId = entry2.id, capturedPlat = plat;
|
|
6935
|
+
var capturedEntry = entry2;
|
|
5455
6936
|
var wasStopped = false;
|
|
5456
|
-
var bp =
|
|
5457
|
-
self._trackPoll(
|
|
6937
|
+
var bp = entry2.poll({ latency: POLL_INTERVAL });
|
|
6938
|
+
self._trackPoll(entry2.id, "bg", bp);
|
|
5458
6939
|
bp.then(function(response) {
|
|
5459
6940
|
if (isPollStopped(response)) {
|
|
5460
6941
|
wasStopped = true;
|
|
@@ -5520,13 +7001,13 @@ var ChatSession = class {
|
|
|
5520
7001
|
* client knows DETERMINISTICALLY (see the two call sites in
|
|
5521
7002
|
* maybeResumeIndexing). Best-effort by contract; identity-checked so a
|
|
5522
7003
|
* project switch mid-settle cannot stamp the wrong service. */
|
|
5523
|
-
_mintDoneMarker(
|
|
7004
|
+
_mintDoneMarker(entry2) {
|
|
5524
7005
|
try {
|
|
5525
7006
|
var mint = chatEngineConfig().mintIndexDoneMarker;
|
|
5526
|
-
if (!mint || !
|
|
7007
|
+
if (!mint || !entry2 || !entry2.storagePath || !entry2.projectId) return;
|
|
5527
7008
|
var id = this.host.getIdentity();
|
|
5528
|
-
if (!id || id.projectId !==
|
|
5529
|
-
mint({ service:
|
|
7009
|
+
if (!id || id.projectId !== entry2.projectId) return;
|
|
7010
|
+
mint({ service: entry2.projectId, storagePath: entry2.storagePath });
|
|
5530
7011
|
} catch (_e) {
|
|
5531
7012
|
}
|
|
5532
7013
|
}
|
|
@@ -5547,27 +7028,27 @@ var ChatSession = class {
|
|
|
5547
7028
|
* maybeResumeIndexing's single-pass branch); paged files stay with their
|
|
5548
7029
|
* drivers. Outcome is read from the settled bubbles' own flags, which is
|
|
5549
7030
|
* all the history mapping left us. Best-effort and idempotent throughout. */
|
|
5550
|
-
_flipRunFromSettledEntry(
|
|
7031
|
+
_flipRunFromSettledEntry(entry2) {
|
|
5551
7032
|
try {
|
|
5552
|
-
if (!
|
|
5553
|
-
if (isPagedReadFile(
|
|
5554
|
-
if (this.cancelledIndexKeys.has(this._indexKeyOf(
|
|
5555
|
-
if (this.state.stoppedIndexIds[
|
|
7033
|
+
if (!entry2 || !entry2.storagePath || !entry2.id || !entry2.projectId) return;
|
|
7034
|
+
if (isPagedReadFile(entry2.filename, entry2.mime)) return;
|
|
7035
|
+
if (this.cancelledIndexKeys.has(this._indexKeyOf(entry2))) return;
|
|
7036
|
+
if (this.state.stoppedIndexIds[entry2.id]) return;
|
|
5556
7037
|
var userMsg = null, replyMsg = null;
|
|
5557
7038
|
this.state.messages.forEach(function(m) {
|
|
5558
|
-
if (m._serverItemId !==
|
|
7039
|
+
if (m._serverItemId !== entry2.id) return;
|
|
5559
7040
|
if (m.role === "user") {
|
|
5560
7041
|
if (!userMsg) userMsg = m;
|
|
5561
7042
|
} else if (!replyMsg) replyMsg = m;
|
|
5562
7043
|
});
|
|
5563
7044
|
if (userMsg && userMsg.isCancelled || replyMsg && replyMsg.isCancelled) {
|
|
5564
|
-
this._flipRunRecord(
|
|
7045
|
+
this._flipRunRecord(entry2, "cancelled");
|
|
5565
7046
|
} else if (replyMsg && replyMsg.isError) {
|
|
5566
7047
|
var errText = typeof replyMsg.content === "string" ? replyMsg.content.replace(/\s+/g, " ").trim().slice(0, 300) : "";
|
|
5567
|
-
this._flipRunRecord(
|
|
7048
|
+
this._flipRunRecord(entry2, "error", errText || "Indexing failed.");
|
|
5568
7049
|
} else if (replyMsg) {
|
|
5569
|
-
this._mintDoneMarker(
|
|
5570
|
-
this._flipRunRecord(
|
|
7050
|
+
this._mintDoneMarker(entry2);
|
|
7051
|
+
this._flipRunRecord(entry2, "done");
|
|
5571
7052
|
}
|
|
5572
7053
|
} catch (_e) {
|
|
5573
7054
|
}
|
|
@@ -5578,55 +7059,55 @@ var ChatSession = class {
|
|
|
5578
7059
|
* mid-settle — otherwise the record lies 'working' forever. Best-effort
|
|
5579
7060
|
* through upsertIndexRunRecordSafe; the consumer's precedence guard keeps
|
|
5580
7061
|
* repeats and races harmless. */
|
|
5581
|
-
_flipRunRecord(
|
|
5582
|
-
if (!
|
|
7062
|
+
_flipRunRecord(entry2, status, error) {
|
|
7063
|
+
if (!entry2 || !entry2.storagePath || !entry2.projectId) return;
|
|
5583
7064
|
var patch = { status, finished: Date.now() };
|
|
5584
7065
|
if (error) patch.error = error;
|
|
5585
|
-
upsertIndexRunRecordSafe(
|
|
7066
|
+
upsertIndexRunRecordSafe(entry2.projectId, entry2.storagePath, patch);
|
|
5586
7067
|
}
|
|
5587
|
-
maybeResumeIndexing(
|
|
7068
|
+
maybeResumeIndexing(entry2, response, platform) {
|
|
5588
7069
|
var self = this;
|
|
5589
7070
|
var endOfClientChain = function() {
|
|
5590
7071
|
self._nudgeIndexingDrain();
|
|
5591
7072
|
};
|
|
5592
7073
|
try {
|
|
5593
|
-
if (!
|
|
5594
|
-
if (this.cancelledIndexKeys.has(this._indexKeyOf(
|
|
5595
|
-
if (!isPagedReadFile(
|
|
7074
|
+
if (!entry2 || !entry2.storagePath) return;
|
|
7075
|
+
if (this.cancelledIndexKeys.has(this._indexKeyOf(entry2))) return;
|
|
7076
|
+
if (!isPagedReadFile(entry2.filename, entry2.mime)) {
|
|
5596
7077
|
if (!isErrorResponseBody(response) && !this._isCancelledPollResult(response)) {
|
|
5597
|
-
this._mintDoneMarker(
|
|
5598
|
-
this._flipRunRecord(
|
|
7078
|
+
this._mintDoneMarker(entry2);
|
|
7079
|
+
this._flipRunRecord(entry2, "done");
|
|
5599
7080
|
} else if (this._isCancelledPollResult(response)) {
|
|
5600
|
-
this._flipRunRecord(
|
|
7081
|
+
this._flipRunRecord(entry2, "cancelled");
|
|
5601
7082
|
} else {
|
|
5602
|
-
this._flipRunRecord(
|
|
7083
|
+
this._flipRunRecord(entry2, "error", this._runErrorText(response));
|
|
5603
7084
|
}
|
|
5604
7085
|
endOfClientChain();
|
|
5605
7086
|
return;
|
|
5606
7087
|
}
|
|
5607
|
-
if (isImageVisionFile(
|
|
5608
|
-
if (windowedIndexingEnabled() && isWindowedReadFile(
|
|
7088
|
+
if (isImageVisionFile(entry2.filename, entry2.mime)) return;
|
|
7089
|
+
if (windowedIndexingEnabled() && isWindowedReadFile(entry2.filename, entry2.mime)) return;
|
|
5609
7090
|
if (isErrorResponseBody(response)) {
|
|
5610
|
-
this._flipRunRecord(
|
|
7091
|
+
this._flipRunRecord(entry2, "error", this._runErrorText(response));
|
|
5611
7092
|
endOfClientChain();
|
|
5612
7093
|
return;
|
|
5613
7094
|
}
|
|
5614
7095
|
var answer = (platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "";
|
|
5615
7096
|
if (answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1) {
|
|
5616
|
-
this._mintDoneMarker(
|
|
5617
|
-
this._flipRunRecord(
|
|
7097
|
+
this._mintDoneMarker(entry2);
|
|
7098
|
+
this._flipRunRecord(entry2, "done");
|
|
5618
7099
|
endOfClientChain();
|
|
5619
7100
|
return;
|
|
5620
7101
|
}
|
|
5621
|
-
var pass = (
|
|
7102
|
+
var pass = (entry2.resumePass || 0) + 1;
|
|
5622
7103
|
if (pass > MAX_INDEXING_RESUME_PASSES) {
|
|
5623
|
-
this._flipRunRecord(
|
|
7104
|
+
this._flipRunRecord(entry2, "error", "Stopped after " + MAX_INDEXING_RESUME_PASSES + " passes without finishing.");
|
|
5624
7105
|
endOfClientChain();
|
|
5625
7106
|
return;
|
|
5626
7107
|
}
|
|
5627
7108
|
var id = this.host.getIdentity();
|
|
5628
|
-
if (!id || id.platform === "none" || id.projectId !==
|
|
5629
|
-
this._flipRunRecord(
|
|
7109
|
+
if (!id || id.platform === "none" || id.projectId !== entry2.projectId) {
|
|
7110
|
+
this._flipRunRecord(entry2, "error", "Indexing stopped: the session or project changed before the file finished.");
|
|
5630
7111
|
endOfClientChain();
|
|
5631
7112
|
return;
|
|
5632
7113
|
}
|
|
@@ -5644,10 +7125,10 @@ var ChatSession = class {
|
|
|
5644
7125
|
serviceName: id.serviceName,
|
|
5645
7126
|
serviceDescription: id.serviceDescription,
|
|
5646
7127
|
attachment: {
|
|
5647
|
-
name:
|
|
5648
|
-
storagePath:
|
|
5649
|
-
mime:
|
|
5650
|
-
size:
|
|
7128
|
+
name: entry2.filename,
|
|
7129
|
+
storagePath: entry2.storagePath,
|
|
7130
|
+
mime: entry2.mime,
|
|
7131
|
+
size: entry2.size,
|
|
5651
7132
|
url: ""
|
|
5652
7133
|
}
|
|
5653
7134
|
}).then(function(ack) {
|
|
@@ -5656,11 +7137,11 @@ var ChatSession = class {
|
|
|
5656
7137
|
projectId: id.projectId,
|
|
5657
7138
|
platform: id.platform,
|
|
5658
7139
|
id: ack.id,
|
|
5659
|
-
filename:
|
|
5660
|
-
storagePath:
|
|
5661
|
-
isReindex:
|
|
5662
|
-
mime:
|
|
5663
|
-
size:
|
|
7140
|
+
filename: entry2.filename,
|
|
7141
|
+
storagePath: entry2.storagePath,
|
|
7142
|
+
isReindex: entry2.isReindex,
|
|
7143
|
+
mime: entry2.mime,
|
|
7144
|
+
size: entry2.size,
|
|
5664
7145
|
status: ack.status === "running" ? "running" : "pending",
|
|
5665
7146
|
poll: ack.poll,
|
|
5666
7147
|
resumePass: pass
|
|
@@ -5747,6 +7228,7 @@ var ChatSession = class {
|
|
|
5747
7228
|
formatIndexingLabel: self.host.formatIndexingLabel
|
|
5748
7229
|
}).messages;
|
|
5749
7230
|
self.applyHydratedBodies(mapped);
|
|
7231
|
+
self._adoptLocalAnswers(mapped, loadKey);
|
|
5750
7232
|
var keptOlderPages = false;
|
|
5751
7233
|
var keptScreenAwaitingBg = false;
|
|
5752
7234
|
if (fetchMore) {
|
|
@@ -5922,6 +7404,7 @@ var ChatSession = class {
|
|
|
5922
7404
|
}
|
|
5923
7405
|
self.updateHistoryCache();
|
|
5924
7406
|
self.host.notify();
|
|
7407
|
+
self._scheduleStreamRecovery(loadKey, platform, projectId, owner);
|
|
5925
7408
|
var bgPending = !fetchMore && history && history.bgPending;
|
|
5926
7409
|
if (bgPending) {
|
|
5927
7410
|
var batchId = ++_bgHistoryBatchSeq;
|
|
@@ -6071,7 +7554,12 @@ var ChatSession = class {
|
|
|
6071
7554
|
}
|
|
6072
7555
|
}
|
|
6073
7556
|
};
|
|
6074
|
-
var pp = isBg ? item.poll(Object.assign({ latency: POLL_INTERVAL }, pollOpts)) : self._fgPollWithEarlyProbe(item, capturedId, pollOpts
|
|
7557
|
+
var pp = isBg ? item.poll(Object.assign({ latency: POLL_INTERVAL }, pollOpts)) : self._fgPollWithEarlyProbe(item, capturedId, pollOpts, {
|
|
7558
|
+
platform,
|
|
7559
|
+
projectId,
|
|
7560
|
+
owner,
|
|
7561
|
+
ownerKey: loadKey
|
|
7562
|
+
});
|
|
6075
7563
|
self._trackPoll(capturedId, item._isBgTask || item._isOnBgQueue ? "bg" : "fg", pp);
|
|
6076
7564
|
if (pp && pp.catch) pp.catch(function() {
|
|
6077
7565
|
});
|
|
@@ -6741,6 +8229,112 @@ function buildChatDisplayList(messages, opts) {
|
|
|
6741
8229
|
return out;
|
|
6742
8230
|
}
|
|
6743
8231
|
|
|
8232
|
+
// src/engine/project_settings.ts
|
|
8233
|
+
var UPLOAD_ACCESS_GROUPS = ["public", "authorized", "private"];
|
|
8234
|
+
var DEFAULT_UPLOAD_ACCESS_GROUP = "authorized";
|
|
8235
|
+
var PROJECT_SETTINGS_TABLE = "__SETTINGS__";
|
|
8236
|
+
var PROJECT_SETTINGS_UNIQUE_ID = "bq::settings";
|
|
8237
|
+
var PROJECT_SETTINGS_ACCESS_GROUP = "public";
|
|
8238
|
+
var UPLOAD_ACCESS_LABELS = {
|
|
8239
|
+
public: "Public",
|
|
8240
|
+
authorized: "Signed in users",
|
|
8241
|
+
private: "Only me"
|
|
8242
|
+
};
|
|
8243
|
+
var UPLOAD_ACCESS_HINTS = {
|
|
8244
|
+
public: "Anyone can ask about this file, including visitors who are not logged in.",
|
|
8245
|
+
authorized: "Only users signed in to this project can ask about this file.",
|
|
8246
|
+
private: "Only you can ask about this file."
|
|
8247
|
+
};
|
|
8248
|
+
var UPLOAD_ACCESS_OPTIONS = UPLOAD_ACCESS_GROUPS.map((value) => ({
|
|
8249
|
+
value,
|
|
8250
|
+
label: UPLOAD_ACCESS_LABELS[value],
|
|
8251
|
+
hint: UPLOAD_ACCESS_HINTS[value]
|
|
8252
|
+
}));
|
|
8253
|
+
function normalizeUploadAccessGroup(value) {
|
|
8254
|
+
return UPLOAD_ACCESS_GROUPS.indexOf(value) === -1 ? DEFAULT_UPLOAD_ACCESS_GROUP : value;
|
|
8255
|
+
}
|
|
8256
|
+
function normalizeProjectAccessSetting(value) {
|
|
8257
|
+
if (value === "ask") return "ask";
|
|
8258
|
+
return UPLOAD_ACCESS_GROUPS.indexOf(value) === -1 ? null : value;
|
|
8259
|
+
}
|
|
8260
|
+
function accessSettingFrom(data) {
|
|
8261
|
+
return normalizeProjectAccessSetting(data?.upload_access_group);
|
|
8262
|
+
}
|
|
8263
|
+
function uploadAccessGroupFrom(data) {
|
|
8264
|
+
const v = accessSettingFrom(data);
|
|
8265
|
+
return v && v !== "ask" ? v : DEFAULT_UPLOAD_ACCESS_GROUP;
|
|
8266
|
+
}
|
|
8267
|
+
function asksUploadAccessFrom(data) {
|
|
8268
|
+
return accessSettingFrom(data) === "ask";
|
|
8269
|
+
}
|
|
8270
|
+
var reader = null;
|
|
8271
|
+
var cache = /* @__PURE__ */ new Map();
|
|
8272
|
+
function configureProjectSettings(fn) {
|
|
8273
|
+
reader = fn;
|
|
8274
|
+
}
|
|
8275
|
+
function entry(service) {
|
|
8276
|
+
let e = cache.get(service);
|
|
8277
|
+
if (!e) {
|
|
8278
|
+
e = { data: null, settled: false, inflight: null };
|
|
8279
|
+
cache.set(service, e);
|
|
8280
|
+
}
|
|
8281
|
+
return e;
|
|
8282
|
+
}
|
|
8283
|
+
function loadProjectSettings(service) {
|
|
8284
|
+
if (!service) return Promise.resolve(null);
|
|
8285
|
+
const e = entry(service);
|
|
8286
|
+
if (e.settled) return Promise.resolve(e.data);
|
|
8287
|
+
if (e.inflight) return e.inflight;
|
|
8288
|
+
if (!reader) return Promise.resolve(null);
|
|
8289
|
+
const run = reader(service).then((data) => data && typeof data === "object" ? data : null).catch(() => null).then((data) => {
|
|
8290
|
+
const cur = entry(service);
|
|
8291
|
+
if (cur.inflight === run) {
|
|
8292
|
+
cur.data = data;
|
|
8293
|
+
cur.settled = true;
|
|
8294
|
+
cur.inflight = null;
|
|
8295
|
+
}
|
|
8296
|
+
return data;
|
|
8297
|
+
});
|
|
8298
|
+
e.inflight = run;
|
|
8299
|
+
return run;
|
|
8300
|
+
}
|
|
8301
|
+
function primeProjectSettings(service) {
|
|
8302
|
+
void loadProjectSettings(service);
|
|
8303
|
+
}
|
|
8304
|
+
function readyProjectSettings(service) {
|
|
8305
|
+
return loadProjectSettings(service);
|
|
8306
|
+
}
|
|
8307
|
+
function cachedProjectSettings(service) {
|
|
8308
|
+
const e = cache.get(service);
|
|
8309
|
+
return e && e.settled ? e.data : null;
|
|
8310
|
+
}
|
|
8311
|
+
function projectSettingsSettled(service) {
|
|
8312
|
+
const e = cache.get(service);
|
|
8313
|
+
return !!e && e.settled;
|
|
8314
|
+
}
|
|
8315
|
+
function projectAccessSetting(service) {
|
|
8316
|
+
return accessSettingFrom(cachedProjectSettings(service));
|
|
8317
|
+
}
|
|
8318
|
+
function projectUploadAccessGroup(service) {
|
|
8319
|
+
return uploadAccessGroupFrom(cachedProjectSettings(service));
|
|
8320
|
+
}
|
|
8321
|
+
function projectAsksUploadAccess(service) {
|
|
8322
|
+
return asksUploadAccessFrom(cachedProjectSettings(service));
|
|
8323
|
+
}
|
|
8324
|
+
function setProjectSettings(service, data) {
|
|
8325
|
+
if (!service) return;
|
|
8326
|
+
cache.set(service, { data: data || null, settled: true, inflight: null });
|
|
8327
|
+
}
|
|
8328
|
+
function patchProjectSettings(service, patch) {
|
|
8329
|
+
if (!service) return;
|
|
8330
|
+
const cur = cachedProjectSettings(service) || {};
|
|
8331
|
+
setProjectSettings(service, Object.assign({}, cur, patch));
|
|
8332
|
+
}
|
|
8333
|
+
function clearProjectSettings(service) {
|
|
8334
|
+
if (service) cache.delete(service);
|
|
8335
|
+
else cache.clear();
|
|
8336
|
+
}
|
|
8337
|
+
|
|
6744
8338
|
exports.BG_INDEXING_QUEUE_SUFFIX = BG_INDEXING_QUEUE_SUFFIX;
|
|
6745
8339
|
exports.BOM = BOM;
|
|
6746
8340
|
exports.BOM_EXTS = BOM_EXTS;
|
|
@@ -6752,6 +8346,7 @@ exports.ChatSession = ChatSession;
|
|
|
6752
8346
|
exports.DEFAULT_CLAUDE_MODEL = DEFAULT_CLAUDE_MODEL;
|
|
6753
8347
|
exports.DEFAULT_CONTEXT_WINDOW = DEFAULT_CONTEXT_WINDOW;
|
|
6754
8348
|
exports.DEFAULT_OPENAI_MODEL = DEFAULT_OPENAI_MODEL;
|
|
8349
|
+
exports.DEFAULT_UPLOAD_ACCESS_GROUP = DEFAULT_UPLOAD_ACCESS_GROUP;
|
|
6755
8350
|
exports.EMPTY_INDEXING_REPLY = EMPTY_INDEXING_REPLY;
|
|
6756
8351
|
exports.EXPIRED_ATTACHMENT_URL_HOST = EXPIRED_ATTACHMENT_URL_HOST;
|
|
6757
8352
|
exports.EXPIRED_ATTACHMENT_URL_ORIGIN = EXPIRED_ATTACHMENT_URL_ORIGIN;
|
|
@@ -6787,13 +8382,24 @@ exports.PREVIEWABLE_IMAGE_CONTENT_TYPES = PREVIEWABLE_IMAGE_CONTENT_TYPES;
|
|
|
6787
8382
|
exports.PREVIEW_BROWSER_CACHE_SECONDS = PREVIEW_BROWSER_CACHE_SECONDS;
|
|
6788
8383
|
exports.PREVIEW_LAYOUT_BOX_SELECTOR = PREVIEW_LAYOUT_BOX_SELECTOR;
|
|
6789
8384
|
exports.PREVIEW_URL_EXPIRES_SECONDS = PREVIEW_URL_EXPIRES_SECONDS;
|
|
8385
|
+
exports.PROJECT_SETTINGS_ACCESS_GROUP = PROJECT_SETTINGS_ACCESS_GROUP;
|
|
8386
|
+
exports.PROJECT_SETTINGS_TABLE = PROJECT_SETTINGS_TABLE;
|
|
8387
|
+
exports.PROJECT_SETTINGS_UNIQUE_ID = PROJECT_SETTINGS_UNIQUE_ID;
|
|
6790
8388
|
exports.RENDER_FROM_TOKEN = RENDER_FROM_TOKEN;
|
|
6791
8389
|
exports.RTF_EXTS = RTF_EXTS;
|
|
6792
8390
|
exports.RUN_RECORD_WORKING_STALE_MS = RUN_RECORD_WORKING_STALE_MS;
|
|
8391
|
+
exports.STREAM_POLL_INTERVAL = STREAM_POLL_INTERVAL;
|
|
6793
8392
|
exports.TOOL_AND_RESPONSE_BUFFER = TOOL_AND_RESPONSE_BUFFER;
|
|
8393
|
+
exports.UPLOAD_ACCESS_GROUPS = UPLOAD_ACCESS_GROUPS;
|
|
8394
|
+
exports.UPLOAD_ACCESS_HINTS = UPLOAD_ACCESS_HINTS;
|
|
8395
|
+
exports.UPLOAD_ACCESS_LABELS = UPLOAD_ACCESS_LABELS;
|
|
8396
|
+
exports.UPLOAD_ACCESS_OPTIONS = UPLOAD_ACCESS_OPTIONS;
|
|
6794
8397
|
exports.XML_EXTS = XML_EXTS;
|
|
6795
8398
|
exports.__resetSplitHistoryState = __resetSplitHistoryState;
|
|
8399
|
+
exports.accessSettingFrom = accessSettingFrom;
|
|
8400
|
+
exports.adoptLocalAnswerIntoPage = adoptLocalAnswerIntoPage;
|
|
6796
8401
|
exports.applyEncodingDeclaration = applyEncodingDeclaration;
|
|
8402
|
+
exports.asksUploadAccessFrom = asksUploadAccessFrom;
|
|
6797
8403
|
exports.bgIndexingQueueName = bgIndexingQueueName;
|
|
6798
8404
|
exports.buildAiAgentValue = buildAiAgentValue;
|
|
6799
8405
|
exports.buildBoundedChatMessages = buildBoundedChatMessages;
|
|
@@ -6808,21 +8414,27 @@ exports.buildIndexingRenderMessage = buildIndexingRenderMessage;
|
|
|
6808
8414
|
exports.buildIndexingSystemPrompt = buildIndexingSystemPrompt;
|
|
6809
8415
|
exports.buildIndexingUserMessage = buildIndexingUserMessage;
|
|
6810
8416
|
exports.buildIndexingWindowMessage = buildIndexingWindowMessage;
|
|
8417
|
+
exports.cachedProjectSettings = cachedProjectSettings;
|
|
6811
8418
|
exports.callClaudeWithMcp = callClaudeWithMcp;
|
|
6812
8419
|
exports.callClaudeWithPublicMcp = callClaudeWithPublicMcp;
|
|
6813
8420
|
exports.callOpenAIWithPublicMcp = callOpenAIWithPublicMcp;
|
|
6814
8421
|
exports.canonicalizePathForm = canonicalizePathForm;
|
|
6815
8422
|
exports.chatCacheKey = chatCacheKey;
|
|
6816
8423
|
exports.chatEngineConfig = chatEngineConfig;
|
|
8424
|
+
exports.chatStreamWiring = chatStreamWiring;
|
|
6817
8425
|
exports.classifyInlineLink = classifyInlineLink;
|
|
6818
8426
|
exports.clearAttachmentParsers = clearAttachmentParsers;
|
|
6819
8427
|
exports.clearImagePreviewCache = clearImagePreviewCache;
|
|
8428
|
+
exports.clearProjectSettings = clearProjectSettings;
|
|
6820
8429
|
exports.composeUserMessage = composeUserMessage;
|
|
6821
8430
|
exports.configureChatEngine = configureChatEngine;
|
|
8431
|
+
exports.configureProjectSettings = configureProjectSettings;
|
|
6822
8432
|
exports.contentTypeForExt = contentTypeForExt;
|
|
6823
8433
|
exports.createHistoryFiller = createHistoryFiller;
|
|
6824
8434
|
exports.createInlineLinkRegex = createInlineLinkRegex;
|
|
6825
8435
|
exports.createScrollAnchor = createScrollAnchor;
|
|
8436
|
+
exports.createSseParser = createSseParser;
|
|
8437
|
+
exports.csrEnvelopeError = csrEnvelopeError;
|
|
6826
8438
|
exports.encodePathSegments = encodePathSegments;
|
|
6827
8439
|
exports.encodingClassForExt = encodingClassForExt;
|
|
6828
8440
|
exports.ensureHtmlCharset = ensureHtmlCharset;
|
|
@@ -6860,6 +8472,7 @@ exports.indexScopeKey = indexScopeKey;
|
|
|
6860
8472
|
exports.indexingAccessGroup = indexingAccessGroup;
|
|
6861
8473
|
exports.isAuthExpiredError = isAuthExpiredError;
|
|
6862
8474
|
exports.isBgIndexingQueue = isBgIndexingQueue;
|
|
8475
|
+
exports.isCsrStatusEnvelope = isCsrStatusEnvelope;
|
|
6863
8476
|
exports.isErrorResponseBody = isErrorResponseBody;
|
|
6864
8477
|
exports.isHttpUrlLike = isHttpUrlLike;
|
|
6865
8478
|
exports.isIndexingRequestText = isIndexingRequestText;
|
|
@@ -6875,21 +8488,27 @@ exports.linkUnavailableKeyForPath = linkUnavailableKeyForPath;
|
|
|
6875
8488
|
exports.linkUnavailableKeysForPath = linkUnavailableKeysForPath;
|
|
6876
8489
|
exports.listClaudeModels = listClaudeModels;
|
|
6877
8490
|
exports.listOpenAIModels = listOpenAIModels;
|
|
8491
|
+
exports.liveSafePrefix = liveSafePrefix;
|
|
8492
|
+
exports.loadProjectSettings = loadProjectSettings;
|
|
6878
8493
|
exports.looksLikeRtf = looksLikeRtf;
|
|
6879
8494
|
exports.makeExtractPlaceholder = makeExtractPlaceholder;
|
|
6880
8495
|
exports.mapHistoryListToMessages = mapHistoryListToMessages;
|
|
6881
8496
|
exports.markImagePreviewStale = markImagePreviewStale;
|
|
8497
|
+
exports.mayKeepStreamedAnswer = mayKeepStreamedAnswer;
|
|
6882
8498
|
exports.mintCacheBustStamp = mintCacheBustStamp;
|
|
6883
8499
|
exports.needsBomForExt = needsBomForExt;
|
|
6884
8500
|
exports.normalizeAttachmentPathCandidate = normalizeAttachmentPathCandidate;
|
|
6885
8501
|
exports.normalizeExt = normalizeExt;
|
|
8502
|
+
exports.normalizeProjectAccessSetting = normalizeProjectAccessSetting;
|
|
6886
8503
|
exports.normalizeTextContent = normalizeTextContent;
|
|
6887
8504
|
exports.normalizeTrailingInlineToken = normalizeTrailingInlineToken;
|
|
8505
|
+
exports.normalizeUploadAccessGroup = normalizeUploadAccessGroup;
|
|
6888
8506
|
exports.notifyAgentSaveAttachment = notifyAgentSaveAttachment;
|
|
6889
8507
|
exports.parseAiAgentValue = parseAiAgentValue;
|
|
6890
8508
|
exports.parseAttachmentContent = parseAttachmentContent;
|
|
6891
8509
|
exports.parseIndexingLabel = parseIndexingLabel;
|
|
6892
8510
|
exports.parseIndexingRequestText = parseIndexingRequestText;
|
|
8511
|
+
exports.patchProjectSettings = patchProjectSettings;
|
|
6893
8512
|
exports.peekImagePreviewUrl = peekImagePreviewUrl;
|
|
6894
8513
|
exports.prepareDownloadText = prepareDownloadText;
|
|
6895
8514
|
exports.presignExpiryEpochMs = presignExpiryEpochMs;
|
|
@@ -6897,7 +8516,13 @@ exports.previewImageContentType = previewImageContentType;
|
|
|
6897
8516
|
exports.previewLayoutBox = previewLayoutBox;
|
|
6898
8517
|
exports.previewMintCacheToken = previewMintCacheToken;
|
|
6899
8518
|
exports.previewableExtOf = previewableExtOf;
|
|
8519
|
+
exports.primeProjectSettings = primeProjectSettings;
|
|
8520
|
+
exports.projectAccessSetting = projectAccessSetting;
|
|
8521
|
+
exports.projectAsksUploadAccess = projectAsksUploadAccess;
|
|
8522
|
+
exports.projectSettingsSettled = projectSettingsSettled;
|
|
8523
|
+
exports.projectUploadAccessGroup = projectUploadAccessGroup;
|
|
6900
8524
|
exports.readExpiredAttachmentHref = readExpiredAttachmentHref;
|
|
8525
|
+
exports.readyProjectSettings = readyProjectSettings;
|
|
6901
8526
|
exports.registerAttachmentParser = registerAttachmentParser;
|
|
6902
8527
|
exports.registerModelContextWindows = registerModelContextWindows;
|
|
6903
8528
|
exports.renderInlineLinkHtml = renderInlineLinkHtml;
|
|
@@ -6908,11 +8533,18 @@ exports.runIndexUniqueId = runIndexUniqueId;
|
|
|
6908
8533
|
exports.safeDecodeURIComponent = safeDecodeURIComponent;
|
|
6909
8534
|
exports.sanitizeAttachmentLinksForHistory = sanitizeAttachmentLinksForHistory;
|
|
6910
8535
|
exports.setProjectContextWindow = setProjectContextWindow;
|
|
8536
|
+
exports.setProjectSettings = setProjectSettings;
|
|
6911
8537
|
exports.shouldRescueInFlightMessage = shouldRescueInFlightMessage;
|
|
8538
|
+
exports.skapiSupportsStreaming = skapiSupportsStreaming;
|
|
8539
|
+
exports.streamRecoveryEnabled = streamRecoveryEnabled;
|
|
8540
|
+
exports.streamRecoveryLabels = streamRecoveryLabels;
|
|
8541
|
+
exports.streamRecoveryPhase = streamRecoveryPhase;
|
|
6912
8542
|
exports.stripFileBlocksFromHistory = stripFileBlocksFromHistory;
|
|
6913
8543
|
exports.transformContentWithImages = transformContentWithImages;
|
|
6914
8544
|
exports.transformContentWithOpenAIImages = transformContentWithOpenAIImages;
|
|
6915
8545
|
exports.truncateLabelForDisplay = truncateLabelForDisplay;
|
|
8546
|
+
exports.typewriterResumeIndex = typewriterResumeIndex;
|
|
8547
|
+
exports.uploadAccessGroupFrom = uploadAccessGroupFrom;
|
|
6916
8548
|
exports.upsertIndexRunRecordSafe = upsertIndexRunRecordSafe;
|
|
6917
8549
|
exports.wallClockNow = wallClockNow;
|
|
6918
8550
|
//# sourceMappingURL=engine.cjs.map
|