bunnyquery 1.9.6 → 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 +1825 -180
- 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/bunnyquery.js
CHANGED
|
@@ -59,6 +59,19 @@
|
|
|
59
59
|
function windowedIndexingEnabled() {
|
|
60
60
|
return _config?.windowedIndexing === true;
|
|
61
61
|
}
|
|
62
|
+
function liveStreamingRealtimeEnabled() {
|
|
63
|
+
return liveStreamingEnabled() && _config?.liveStreamingRealtime === true;
|
|
64
|
+
}
|
|
65
|
+
function liveStreamingEnabled() {
|
|
66
|
+
return _config?.liveStreaming === true;
|
|
67
|
+
}
|
|
68
|
+
function streamRecoveryEnabled() {
|
|
69
|
+
if (_config?.streamRecovery === false) return false;
|
|
70
|
+
return typeof _config?.clientSecretRequestStream === "function";
|
|
71
|
+
}
|
|
72
|
+
function skapiSupportsStreaming(sk) {
|
|
73
|
+
return !!sk && typeof sk.clientSecretRequestStream === "function" && typeof sk.clientSecretRequestFinalize === "function";
|
|
74
|
+
}
|
|
62
75
|
function pollOpt() {
|
|
63
76
|
const p = _config?.poll;
|
|
64
77
|
return p === void 0 ? {} : { poll: p };
|
|
@@ -298,7 +311,7 @@ Extracted content of attached office files (read inline below; do NOT fetch thei
|
|
|
298
311
|
You are a dedicated assistant for the project ID: "${projectId}".
|
|
299
312
|
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.
|
|
300
313
|
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.
|
|
301
|
-
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
|
|
314
|
+
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.
|
|
302
315
|
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.
|
|
303
316
|
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.
|
|
304
317
|
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.
|
|
@@ -516,7 +529,17 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
516
529
|
function isTransientStatus(status) {
|
|
517
530
|
return status === 408 || status === 425 || status === 429 || status >= 500;
|
|
518
531
|
}
|
|
532
|
+
function isCsrStatusEnvelope(res) {
|
|
533
|
+
return !!res && typeof res === "object" && !Array.isArray(res) && typeof res.status === "string" && typeof res.id === "string" && "in_queue" in res;
|
|
534
|
+
}
|
|
535
|
+
function csrEnvelopeError(input) {
|
|
536
|
+
if (!isCsrStatusEnvelope(input)) return void 0;
|
|
537
|
+
if (input.status !== "failed") return void 0;
|
|
538
|
+
return input.error != null ? input.error : { message: "The AI provider request failed." };
|
|
539
|
+
}
|
|
519
540
|
function getErrorMessage(input) {
|
|
541
|
+
var envErr = csrEnvelopeError(input);
|
|
542
|
+
if (envErr !== void 0) input = envErr;
|
|
520
543
|
if (!input) return "Something went wrong.";
|
|
521
544
|
if (typeof input === "string") return input;
|
|
522
545
|
if (input.error && input.error.message) return input.error.message;
|
|
@@ -531,7 +554,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
531
554
|
return "Something went wrong.";
|
|
532
555
|
}
|
|
533
556
|
function isErrorResponseBody(response) {
|
|
534
|
-
|
|
557
|
+
var envErr = csrEnvelopeError(response);
|
|
558
|
+
if (envErr !== void 0) response = envErr;
|
|
559
|
+
if (!response || typeof response !== "object") return envErr !== void 0;
|
|
535
560
|
if (typeof response.status_code === "number" && response.status_code >= 400) return true;
|
|
536
561
|
if (response.type === "error") return true;
|
|
537
562
|
if (response.error && (response.error.message || response.error.type)) return true;
|
|
@@ -548,6 +573,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
548
573
|
return false;
|
|
549
574
|
}
|
|
550
575
|
function isNonRetryableRequestError(input) {
|
|
576
|
+
var envErr = csrEnvelopeError(input);
|
|
577
|
+
if (envErr !== void 0) input = envErr;
|
|
551
578
|
if (!input || typeof input !== "object") return false;
|
|
552
579
|
var status = typeof input.status_code === "number" ? input.status_code : typeof input.status === "number" ? input.status : void 0;
|
|
553
580
|
var param = void 0;
|
|
@@ -578,6 +605,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
578
605
|
return false;
|
|
579
606
|
}
|
|
580
607
|
function isAuthExpiredError(input) {
|
|
608
|
+
var envErr = csrEnvelopeError(input);
|
|
609
|
+
if (envErr !== void 0) input = envErr;
|
|
581
610
|
if (!input) return false;
|
|
582
611
|
var blobs = [];
|
|
583
612
|
var push = function(v) {
|
|
@@ -1457,6 +1486,541 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1457
1486
|
return { platform, model, contextWindow, hasPlatform: !!platform };
|
|
1458
1487
|
}
|
|
1459
1488
|
|
|
1489
|
+
// src/engine/sse.ts
|
|
1490
|
+
var CLAUDE_EVENTS = {
|
|
1491
|
+
message_start: true,
|
|
1492
|
+
message_delta: true,
|
|
1493
|
+
message_stop: true,
|
|
1494
|
+
content_block_start: true,
|
|
1495
|
+
content_block_delta: true,
|
|
1496
|
+
content_block_stop: true,
|
|
1497
|
+
ping: true
|
|
1498
|
+
};
|
|
1499
|
+
var CLAUDE_TOOL_BLOCKS = {
|
|
1500
|
+
tool_use: true,
|
|
1501
|
+
server_tool_use: true,
|
|
1502
|
+
mcp_tool_use: true,
|
|
1503
|
+
web_search_tool_use: true
|
|
1504
|
+
};
|
|
1505
|
+
var OPENAI_TOOL_ITEMS = {
|
|
1506
|
+
function_call: true,
|
|
1507
|
+
mcp_call: true,
|
|
1508
|
+
web_search_call: true,
|
|
1509
|
+
file_search_call: true,
|
|
1510
|
+
code_interpreter_call: true,
|
|
1511
|
+
computer_call: true,
|
|
1512
|
+
image_generation_call: true
|
|
1513
|
+
};
|
|
1514
|
+
function detectProvider(type) {
|
|
1515
|
+
if (!type) return null;
|
|
1516
|
+
if (type.indexOf("response.") === 0) return "openai";
|
|
1517
|
+
if (CLAUDE_EVENTS[type]) return "claude";
|
|
1518
|
+
return null;
|
|
1519
|
+
}
|
|
1520
|
+
function lineEnd(s, from) {
|
|
1521
|
+
for (var i = from; i < s.length; i++) {
|
|
1522
|
+
var c = s.charCodeAt(i);
|
|
1523
|
+
if (c === 10) return { at: i, len: 1 };
|
|
1524
|
+
if (c === 13) {
|
|
1525
|
+
if (i + 1 >= s.length) return null;
|
|
1526
|
+
return { at: i, len: s.charCodeAt(i + 1) === 10 ? 2 : 1 };
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
return null;
|
|
1530
|
+
}
|
|
1531
|
+
function readFrame(lines) {
|
|
1532
|
+
var event = "";
|
|
1533
|
+
var data = [];
|
|
1534
|
+
var framed = false;
|
|
1535
|
+
for (var i = 0; i < lines.length; i++) {
|
|
1536
|
+
var line = lines[i];
|
|
1537
|
+
if (!line.length) continue;
|
|
1538
|
+
if (line.charCodeAt(0) === 58) {
|
|
1539
|
+
framed = true;
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
var colon = line.indexOf(":");
|
|
1543
|
+
var field = colon === -1 ? line : line.slice(0, colon);
|
|
1544
|
+
var value = colon === -1 ? "" : line.slice(colon + 1);
|
|
1545
|
+
if (value.charCodeAt(0) === 32) value = value.slice(1);
|
|
1546
|
+
if (field === "data") {
|
|
1547
|
+
framed = true;
|
|
1548
|
+
data.push(value);
|
|
1549
|
+
} else if (field === "event") {
|
|
1550
|
+
framed = true;
|
|
1551
|
+
event = value;
|
|
1552
|
+
} else if (field === "id" || field === "retry") {
|
|
1553
|
+
framed = true;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
return { event, data: data.join("\n"), framed };
|
|
1557
|
+
}
|
|
1558
|
+
function createSseParser() {
|
|
1559
|
+
var buf = "";
|
|
1560
|
+
var lines = [];
|
|
1561
|
+
var lastSeq = 0;
|
|
1562
|
+
var sawFraming = false;
|
|
1563
|
+
var raw = "";
|
|
1564
|
+
var rawHasContent = false;
|
|
1565
|
+
var ended = false;
|
|
1566
|
+
var rawParsed = false;
|
|
1567
|
+
var rawBody = null;
|
|
1568
|
+
var provider = null;
|
|
1569
|
+
var terminalEvent = null;
|
|
1570
|
+
var errored = false;
|
|
1571
|
+
var error = null;
|
|
1572
|
+
var stopReason = null;
|
|
1573
|
+
var toolCalls = [];
|
|
1574
|
+
var malformedFrames = 0;
|
|
1575
|
+
var malformedToolJson = 0;
|
|
1576
|
+
var message = null;
|
|
1577
|
+
var blocks = /* @__PURE__ */ new Map();
|
|
1578
|
+
var parts = /* @__PURE__ */ new Map();
|
|
1579
|
+
var reasoning = /* @__PURE__ */ new Map();
|
|
1580
|
+
var response = null;
|
|
1581
|
+
var textCache = null;
|
|
1582
|
+
var thinkingCache = null;
|
|
1583
|
+
function feed(text) {
|
|
1584
|
+
if (typeof text !== "string" || !text.length) return;
|
|
1585
|
+
if (!sawFraming) {
|
|
1586
|
+
raw += text;
|
|
1587
|
+
if (!rawHasContent) rawHasContent = /\S/.test(text);
|
|
1588
|
+
rawParsed = false;
|
|
1589
|
+
rawBody = null;
|
|
1590
|
+
}
|
|
1591
|
+
buf += text;
|
|
1592
|
+
var i = 0;
|
|
1593
|
+
for (; ; ) {
|
|
1594
|
+
var end2 = lineEnd(buf, i);
|
|
1595
|
+
if (!end2) break;
|
|
1596
|
+
var line = buf.slice(i, end2.at);
|
|
1597
|
+
i = end2.at + end2.len;
|
|
1598
|
+
if (line.length === 0) dispatch();
|
|
1599
|
+
else lines.push(line);
|
|
1600
|
+
}
|
|
1601
|
+
if (i > 0) buf = buf.slice(i);
|
|
1602
|
+
}
|
|
1603
|
+
function feedChunks(chunks) {
|
|
1604
|
+
if (!chunks || !chunks.length) return;
|
|
1605
|
+
for (var i = 0; i < chunks.length; i++) {
|
|
1606
|
+
var c = chunks[i];
|
|
1607
|
+
if (!c || typeof c !== "object") continue;
|
|
1608
|
+
var seq = typeof c.seq === "number" ? c.seq : 0;
|
|
1609
|
+
if (seq && seq <= lastSeq) continue;
|
|
1610
|
+
if (seq > lastSeq) lastSeq = seq;
|
|
1611
|
+
feed(typeof c.txt === "string" ? c.txt : "");
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
function end() {
|
|
1615
|
+
if (buf.length) {
|
|
1616
|
+
var tail = buf.charCodeAt(buf.length - 1) === 13 ? buf.slice(0, -1) : buf;
|
|
1617
|
+
if (tail.length) lines.push(tail);
|
|
1618
|
+
buf = "";
|
|
1619
|
+
}
|
|
1620
|
+
if (lines.length) dispatch();
|
|
1621
|
+
ended = true;
|
|
1622
|
+
}
|
|
1623
|
+
function isUnframed() {
|
|
1624
|
+
return ended && !sawFraming && rawHasContent;
|
|
1625
|
+
}
|
|
1626
|
+
function dispatch() {
|
|
1627
|
+
var pending = lines;
|
|
1628
|
+
lines = [];
|
|
1629
|
+
if (!pending.length) return;
|
|
1630
|
+
try {
|
|
1631
|
+
var frame = readFrame(pending);
|
|
1632
|
+
if (frame.framed && !sawFraming) {
|
|
1633
|
+
sawFraming = true;
|
|
1634
|
+
raw = "";
|
|
1635
|
+
rawHasContent = false;
|
|
1636
|
+
}
|
|
1637
|
+
if (!frame.data.length) return;
|
|
1638
|
+
if (frame.data === "[DONE]") return;
|
|
1639
|
+
var ev = JSON.parse(frame.data);
|
|
1640
|
+
if (!ev || typeof ev !== "object") {
|
|
1641
|
+
malformedFrames++;
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
var type = typeof ev.type === "string" && ev.type ? ev.type : frame.event;
|
|
1645
|
+
if (!type) {
|
|
1646
|
+
malformedFrames++;
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
if (!provider) provider = detectProvider(type);
|
|
1650
|
+
if (provider === "openai") handleOpenAI(type, ev);
|
|
1651
|
+
else if (provider === "claude") handleClaude(type, ev);
|
|
1652
|
+
else handleUnattributed(type, ev);
|
|
1653
|
+
} catch (e) {
|
|
1654
|
+
malformedFrames++;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
function handleUnattributed(type, ev) {
|
|
1658
|
+
if (type === "error") {
|
|
1659
|
+
takeError(ev && ev.error ? ev : { type: "error", error: ev });
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
malformedFrames++;
|
|
1663
|
+
}
|
|
1664
|
+
function takeError(payload) {
|
|
1665
|
+
errored = true;
|
|
1666
|
+
terminalEvent = "error";
|
|
1667
|
+
error = payload;
|
|
1668
|
+
}
|
|
1669
|
+
function handleClaude(type, ev) {
|
|
1670
|
+
if (type === "ping") return;
|
|
1671
|
+
if (type === "error") {
|
|
1672
|
+
takeError({ type: "error", error: ev && ev.error ? ev.error : ev });
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
if (type === "message_start") {
|
|
1676
|
+
message = ev && ev.message ? shallowClone(ev.message) : { type: "message", role: "assistant" };
|
|
1677
|
+
if (typeof message.stop_reason === "string") stopReason = message.stop_reason;
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
if (type === "content_block_start") {
|
|
1681
|
+
var idx = numberOr(ev.index, -1);
|
|
1682
|
+
if (idx < 0) {
|
|
1683
|
+
malformedFrames++;
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
var block = ev.content_block ? shallowClone(ev.content_block) : {};
|
|
1687
|
+
blocks.set(idx, { block, json: "", sawJson: false });
|
|
1688
|
+
invalidate();
|
|
1689
|
+
if (block && typeof block.type === "string" && CLAUDE_TOOL_BLOCKS[block.type]) {
|
|
1690
|
+
var call = {
|
|
1691
|
+
index: idx,
|
|
1692
|
+
name: typeof block.name === "string" && block.name ? block.name : block.type,
|
|
1693
|
+
type: block.type
|
|
1694
|
+
};
|
|
1695
|
+
if (typeof block.server_name === "string") call.serverName = block.server_name;
|
|
1696
|
+
toolCalls.push(call);
|
|
1697
|
+
}
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
if (type === "content_block_delta") {
|
|
1701
|
+
var i = numberOr(ev.index, -1);
|
|
1702
|
+
var d = ev.delta;
|
|
1703
|
+
if (i < 0 || !d || typeof d !== "object") {
|
|
1704
|
+
malformedFrames++;
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
var st = blocks.get(i);
|
|
1708
|
+
if (!st) {
|
|
1709
|
+
st = { block: { type: deltaBlockType(d.type) }, json: "", sawJson: false };
|
|
1710
|
+
blocks.set(i, st);
|
|
1711
|
+
}
|
|
1712
|
+
applyClaudeDelta(st, d);
|
|
1713
|
+
invalidate();
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
if (type === "content_block_stop") {
|
|
1717
|
+
var j = numberOr(ev.index, -1);
|
|
1718
|
+
var s = j >= 0 ? blocks.get(j) : void 0;
|
|
1719
|
+
if (s && s.sawJson) finishToolJson(s);
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
if (type === "message_delta") {
|
|
1723
|
+
if (!message) message = { type: "message", role: "assistant" };
|
|
1724
|
+
var delta = ev.delta;
|
|
1725
|
+
if (delta && typeof delta === "object") {
|
|
1726
|
+
for (var k in delta) {
|
|
1727
|
+
if (Object.prototype.hasOwnProperty.call(delta, k)) message[k] = delta[k];
|
|
1728
|
+
}
|
|
1729
|
+
if (typeof delta.stop_reason === "string") stopReason = delta.stop_reason;
|
|
1730
|
+
}
|
|
1731
|
+
if (ev.usage && typeof ev.usage === "object") {
|
|
1732
|
+
message.usage = mergeInto(shallowClone(message.usage) || {}, ev.usage);
|
|
1733
|
+
}
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
if (type === "message_stop") {
|
|
1737
|
+
terminalEvent = "message_stop";
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
malformedFrames++;
|
|
1741
|
+
}
|
|
1742
|
+
function applyClaudeDelta(st, d) {
|
|
1743
|
+
var t = d.type;
|
|
1744
|
+
if (t === "text_delta") {
|
|
1745
|
+
st.block.text = (st.block.text || "") + str(d.text);
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
if (t === "thinking_delta") {
|
|
1749
|
+
st.block.thinking = (st.block.thinking || "") + str(d.thinking);
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
if (t === "signature_delta") {
|
|
1753
|
+
st.block.signature = (st.block.signature || "") + str(d.signature);
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
if (t === "input_json_delta") {
|
|
1757
|
+
st.json += str(d.partial_json);
|
|
1758
|
+
st.sawJson = true;
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
if (t === "citations_delta") {
|
|
1762
|
+
if (d.citation) {
|
|
1763
|
+
if (!Array.isArray(st.block.citations)) st.block.citations = [];
|
|
1764
|
+
st.block.citations.push(d.citation);
|
|
1765
|
+
}
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
malformedFrames++;
|
|
1769
|
+
}
|
|
1770
|
+
function finishToolJson(st) {
|
|
1771
|
+
if (!st.json.length) {
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
try {
|
|
1775
|
+
st.block.input = JSON.parse(st.json);
|
|
1776
|
+
} catch (e) {
|
|
1777
|
+
malformedToolJson++;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
function deltaBlockType(deltaType) {
|
|
1781
|
+
if (deltaType === "thinking_delta" || deltaType === "signature_delta") return "thinking";
|
|
1782
|
+
if (deltaType === "input_json_delta") return "tool_use";
|
|
1783
|
+
return "text";
|
|
1784
|
+
}
|
|
1785
|
+
function handleOpenAI(type, ev) {
|
|
1786
|
+
if (type === "response.output_text.delta") {
|
|
1787
|
+
putPart(ev, str(ev.delta), false);
|
|
1788
|
+
invalidate();
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
if (type === "response.output_text.done") {
|
|
1792
|
+
if (typeof ev.text === "string") {
|
|
1793
|
+
putPart(ev, ev.text, true);
|
|
1794
|
+
invalidate();
|
|
1795
|
+
}
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
if (type === "response.reasoning_summary_text.delta" || type === "response.reasoning_text.delta") {
|
|
1799
|
+
putReasoning(type, ev, str(ev.delta), false);
|
|
1800
|
+
invalidate();
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
if (type === "response.reasoning_summary_text.done" || type === "response.reasoning_text.done") {
|
|
1804
|
+
if (typeof ev.text === "string") {
|
|
1805
|
+
putReasoning(type, ev, ev.text, true);
|
|
1806
|
+
invalidate();
|
|
1807
|
+
}
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
if (type === "response.output_item.added") {
|
|
1811
|
+
var item = ev.item;
|
|
1812
|
+
if (item && typeof item.type === "string" && OPENAI_TOOL_ITEMS[item.type]) {
|
|
1813
|
+
toolCalls.push({
|
|
1814
|
+
index: numberOr(ev.output_index, toolCalls.length),
|
|
1815
|
+
// A built-in tool (web_search_call) has no name of its own, so the item
|
|
1816
|
+
// type is the only label there is and a row can still be drawn.
|
|
1817
|
+
name: typeof item.name === "string" && item.name ? item.name : item.type,
|
|
1818
|
+
type: item.type
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
if (type === "response.completed" || type === "response.incomplete" || type === "response.failed") {
|
|
1824
|
+
terminalEvent = type;
|
|
1825
|
+
if (ev.response && typeof ev.response === "object") {
|
|
1826
|
+
response = ev.response;
|
|
1827
|
+
var st = response.status;
|
|
1828
|
+
if (st === "incomplete") {
|
|
1829
|
+
var reason = response.incomplete_details && response.incomplete_details.reason;
|
|
1830
|
+
stopReason = typeof reason === "string" && reason ? reason : "incomplete";
|
|
1831
|
+
} else if (typeof st === "string" && st) {
|
|
1832
|
+
stopReason = st;
|
|
1833
|
+
}
|
|
1834
|
+
if (response.error && (response.error.message || response.error.code)) {
|
|
1835
|
+
errored = true;
|
|
1836
|
+
error = response;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
if (type === "response.failed") errored = true;
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
if (type === "response.error" || type === "error") {
|
|
1843
|
+
takeError(ev && ev.error ? ev : { type: "error", error: ev });
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
function putReasoning(type, ev, text, replace) {
|
|
1848
|
+
var summary = type.indexOf("response.reasoning_summary_text.") === 0;
|
|
1849
|
+
var oi = numberOr(ev.output_index, 0);
|
|
1850
|
+
var idx = numberOr(summary ? ev.summary_index : ev.content_index, 0);
|
|
1851
|
+
var key = oi + ":" + (summary ? "s" : "r") + ":" + idx;
|
|
1852
|
+
var r = reasoning.get(key);
|
|
1853
|
+
if (!r) {
|
|
1854
|
+
r = { oi, idx, kind: summary ? 0 : 1, text: "" };
|
|
1855
|
+
reasoning.set(key, r);
|
|
1856
|
+
}
|
|
1857
|
+
r.text = replace ? text : r.text + text;
|
|
1858
|
+
}
|
|
1859
|
+
function putPart(ev, text, replace) {
|
|
1860
|
+
var oi = numberOr(ev.output_index, 0);
|
|
1861
|
+
var ci = numberOr(ev.content_index, 0);
|
|
1862
|
+
var key = oi + ":" + ci;
|
|
1863
|
+
var p = parts.get(key);
|
|
1864
|
+
if (!p) {
|
|
1865
|
+
p = { oi, ci, text: "" };
|
|
1866
|
+
parts.set(key, p);
|
|
1867
|
+
}
|
|
1868
|
+
p.text = replace ? text : p.text + text;
|
|
1869
|
+
}
|
|
1870
|
+
function invalidate() {
|
|
1871
|
+
textCache = null;
|
|
1872
|
+
thinkingCache = null;
|
|
1873
|
+
}
|
|
1874
|
+
function claudeTextBlocks() {
|
|
1875
|
+
return orderedBlocks().filter(function(b) {
|
|
1876
|
+
return b && b.type === "text";
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
function orderedBlocks() {
|
|
1880
|
+
var idx = [];
|
|
1881
|
+
blocks.forEach(function(_v, k) {
|
|
1882
|
+
idx.push(k);
|
|
1883
|
+
});
|
|
1884
|
+
idx.sort(function(a, b) {
|
|
1885
|
+
return a - b;
|
|
1886
|
+
});
|
|
1887
|
+
var out = [];
|
|
1888
|
+
for (var i = 0; i < idx.length; i++) out.push(blocks.get(idx[i]).block);
|
|
1889
|
+
return out;
|
|
1890
|
+
}
|
|
1891
|
+
function orderedParts() {
|
|
1892
|
+
var out = [];
|
|
1893
|
+
parts.forEach(function(p) {
|
|
1894
|
+
out.push(p);
|
|
1895
|
+
});
|
|
1896
|
+
out.sort(function(a, b) {
|
|
1897
|
+
return a.oi !== b.oi ? a.oi - b.oi : a.ci - b.ci;
|
|
1898
|
+
});
|
|
1899
|
+
return out;
|
|
1900
|
+
}
|
|
1901
|
+
function currentText() {
|
|
1902
|
+
if (textCache !== null) return textCache;
|
|
1903
|
+
var out;
|
|
1904
|
+
if (provider === "openai") {
|
|
1905
|
+
out = orderedParts().map(function(p) {
|
|
1906
|
+
return p.text;
|
|
1907
|
+
}).join("\n");
|
|
1908
|
+
} else {
|
|
1909
|
+
out = claudeTextBlocks().map(function(b) {
|
|
1910
|
+
return b.text || "";
|
|
1911
|
+
}).join("\n");
|
|
1912
|
+
}
|
|
1913
|
+
textCache = out;
|
|
1914
|
+
return out;
|
|
1915
|
+
}
|
|
1916
|
+
function currentThinking() {
|
|
1917
|
+
if (thinkingCache !== null) return thinkingCache;
|
|
1918
|
+
var out;
|
|
1919
|
+
if (provider === "openai") {
|
|
1920
|
+
var rs = [];
|
|
1921
|
+
reasoning.forEach(function(r) {
|
|
1922
|
+
rs.push(r);
|
|
1923
|
+
});
|
|
1924
|
+
rs.sort(function(a, b) {
|
|
1925
|
+
if (a.oi !== b.oi) return a.oi - b.oi;
|
|
1926
|
+
if (a.idx !== b.idx) return a.idx - b.idx;
|
|
1927
|
+
return a.kind - b.kind;
|
|
1928
|
+
});
|
|
1929
|
+
out = rs.map(function(r) {
|
|
1930
|
+
return r.text;
|
|
1931
|
+
}).join("\n");
|
|
1932
|
+
} else {
|
|
1933
|
+
out = orderedBlocks().filter(function(b) {
|
|
1934
|
+
return b && b.type === "thinking";
|
|
1935
|
+
}).map(function(b) {
|
|
1936
|
+
return b.thinking || "";
|
|
1937
|
+
}).join("\n");
|
|
1938
|
+
}
|
|
1939
|
+
thinkingCache = out;
|
|
1940
|
+
return out;
|
|
1941
|
+
}
|
|
1942
|
+
function buildBody() {
|
|
1943
|
+
if (provider === "openai") {
|
|
1944
|
+
if (response) return response;
|
|
1945
|
+
} else if (blocks.size || message) {
|
|
1946
|
+
var base = message ? shallowClone(message) : { type: "message", role: "assistant" };
|
|
1947
|
+
base.content = orderedBlocks();
|
|
1948
|
+
return base;
|
|
1949
|
+
}
|
|
1950
|
+
if (errored && error) return error;
|
|
1951
|
+
return unframedBody();
|
|
1952
|
+
}
|
|
1953
|
+
function unframedBody() {
|
|
1954
|
+
if (!isUnframed()) return null;
|
|
1955
|
+
if (rawParsed) return rawBody;
|
|
1956
|
+
rawParsed = true;
|
|
1957
|
+
try {
|
|
1958
|
+
var v = JSON.parse(raw);
|
|
1959
|
+
rawBody = v && typeof v === "object" ? v : null;
|
|
1960
|
+
} catch (e) {
|
|
1961
|
+
rawBody = null;
|
|
1962
|
+
}
|
|
1963
|
+
return rawBody;
|
|
1964
|
+
}
|
|
1965
|
+
function snapshot() {
|
|
1966
|
+
return {
|
|
1967
|
+
provider,
|
|
1968
|
+
text: currentText(),
|
|
1969
|
+
thinkingText: currentThinking(),
|
|
1970
|
+
toolCalls: toolCalls.slice(),
|
|
1971
|
+
toolNames: toolCalls.map(function(t) {
|
|
1972
|
+
return t.name;
|
|
1973
|
+
}),
|
|
1974
|
+
stopReason,
|
|
1975
|
+
complete: terminalEvent !== null,
|
|
1976
|
+
// A terminal event that ENDED the answer rather than KILLED it. The
|
|
1977
|
+
// `errored` term covers all three ways a stream dies with a terminal event
|
|
1978
|
+
// on it: an Anthropic or OpenAI `error` frame (takeError sets both), a
|
|
1979
|
+
// response.failed, and a response.completed/incomplete whose Response
|
|
1980
|
+
// object carries an error payload. See the field's own doc for the loss
|
|
1981
|
+
// this separation prevents.
|
|
1982
|
+
answerComplete: terminalEvent !== null && terminalEvent !== "error" && !errored,
|
|
1983
|
+
terminalEvent,
|
|
1984
|
+
errored,
|
|
1985
|
+
error,
|
|
1986
|
+
malformedFrames,
|
|
1987
|
+
malformedToolJson,
|
|
1988
|
+
unframed: isUnframed(),
|
|
1989
|
+
unframedText: isUnframed() ? raw : null,
|
|
1990
|
+
lastSeq
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
return {
|
|
1994
|
+
feed,
|
|
1995
|
+
feedChunks,
|
|
1996
|
+
end,
|
|
1997
|
+
snapshot,
|
|
1998
|
+
finalBody: buildBody
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
function str(v) {
|
|
2002
|
+
return typeof v === "string" ? v : "";
|
|
2003
|
+
}
|
|
2004
|
+
function numberOr(v, fallback) {
|
|
2005
|
+
return typeof v === "number" && isFinite(v) ? v : fallback;
|
|
2006
|
+
}
|
|
2007
|
+
function shallowClone(o) {
|
|
2008
|
+
if (!o || typeof o !== "object") return o;
|
|
2009
|
+
var out = Array.isArray(o) ? o.slice() : {};
|
|
2010
|
+
if (!Array.isArray(o)) {
|
|
2011
|
+
for (var k in o) {
|
|
2012
|
+
if (Object.prototype.hasOwnProperty.call(o, k)) out[k] = o[k];
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
return out;
|
|
2016
|
+
}
|
|
2017
|
+
function mergeInto(target, src) {
|
|
2018
|
+
for (var k in src) {
|
|
2019
|
+
if (Object.prototype.hasOwnProperty.call(src, k)) target[k] = src[k];
|
|
2020
|
+
}
|
|
2021
|
+
return target;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
1460
2024
|
// src/engine/requests.ts
|
|
1461
2025
|
var ANTHROPIC_MESSAGES_API_URL = "https://api.anthropic.com/v1/messages";
|
|
1462
2026
|
var ANTHROPIC_VERSION = "2023-06-01";
|
|
@@ -1479,6 +2043,26 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1479
2043
|
return { url: String(mcpUrl()).replace(/\/+$/, "") + "/p/" + project };
|
|
1480
2044
|
}
|
|
1481
2045
|
var clientSecretRequest = (opts) => chatEngineConfig().clientSecretRequest(opts);
|
|
2046
|
+
var CHAT_STREAM_ON = Object.freeze({
|
|
2047
|
+
transport: Object.freeze({ stream: true }),
|
|
2048
|
+
body: Object.freeze({ stream: true })
|
|
2049
|
+
});
|
|
2050
|
+
var CHAT_STREAM_OFF = Object.freeze({
|
|
2051
|
+
transport: Object.freeze({}),
|
|
2052
|
+
body: Object.freeze({})
|
|
2053
|
+
});
|
|
2054
|
+
var CHAT_STREAM_ON_REALTIME = Object.freeze({
|
|
2055
|
+
// `realtime` rides on the TRANSPORT arm only. It is a skapi option, not a field
|
|
2056
|
+
// the destination understands, so it must never reach `data`: the body arm stays
|
|
2057
|
+
// exactly what it is with the socket off.
|
|
2058
|
+
transport: Object.freeze({ stream: true, realtime: true }),
|
|
2059
|
+
body: Object.freeze({ stream: true })
|
|
2060
|
+
});
|
|
2061
|
+
function chatStreamWiring(queue) {
|
|
2062
|
+
if (!liveStreamingEnabled()) return CHAT_STREAM_OFF;
|
|
2063
|
+
if (isBgIndexingQueue(queue)) return CHAT_STREAM_OFF;
|
|
2064
|
+
return liveStreamingRealtimeEnabled() ? CHAT_STREAM_ON_REALTIME : CHAT_STREAM_ON;
|
|
2065
|
+
}
|
|
1482
2066
|
var VARIANT_IMAGE_DETAIL = "original";
|
|
1483
2067
|
var VARIANT_TEXT_VERBOSITY = "high";
|
|
1484
2068
|
var OLDEST_NANO_REASONING_EFFORT = "high";
|
|
@@ -1625,6 +2209,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1625
2209
|
});
|
|
1626
2210
|
}
|
|
1627
2211
|
var POLL_INTERVAL = 3e3;
|
|
2212
|
+
var STREAM_POLL_INTERVAL = 1e3;
|
|
1628
2213
|
var MAX_CONCURRENT_BG_POLLS = 6;
|
|
1629
2214
|
async function callClaudeWithMcp({
|
|
1630
2215
|
prompt,
|
|
@@ -1647,12 +2232,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1647
2232
|
if (mcpServer.authorizationToken) {
|
|
1648
2233
|
mcpServerDefinition.authorization_token = mcpServer.authorizationToken;
|
|
1649
2234
|
}
|
|
2235
|
+
const stream = chatStreamWiring(userId || service);
|
|
1650
2236
|
return clientSecretRequest({
|
|
1651
2237
|
clientSecretName: "claude",
|
|
1652
2238
|
queue: userId || service,
|
|
1653
2239
|
service,
|
|
1654
2240
|
owner,
|
|
1655
2241
|
...pollOpt(),
|
|
2242
|
+
...stream.transport,
|
|
1656
2243
|
url: ANTHROPIC_MESSAGES_API_URL,
|
|
1657
2244
|
method: "POST",
|
|
1658
2245
|
headers: {
|
|
@@ -1664,6 +2251,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1664
2251
|
data: {
|
|
1665
2252
|
model,
|
|
1666
2253
|
max_tokens: maxTokens,
|
|
2254
|
+
// Top level beside model/messages/mcp_servers, which is where the
|
|
2255
|
+
// Messages API takes it.
|
|
2256
|
+
...stream.body,
|
|
1667
2257
|
...extractContent && extractContent.length ? { _skapi_extract: extractContent } : {},
|
|
1668
2258
|
...fileUrls && fileUrls.length ? { _skapi_file_urls: fileUrls } : {},
|
|
1669
2259
|
...system ? {
|
|
@@ -1746,12 +2336,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1746
2336
|
content: m.content
|
|
1747
2337
|
}))
|
|
1748
2338
|
];
|
|
2339
|
+
const stream = chatStreamWiring(userId || service);
|
|
1749
2340
|
return clientSecretRequest({
|
|
1750
2341
|
clientSecretName: "openai",
|
|
1751
2342
|
queue: userId || service,
|
|
1752
2343
|
service,
|
|
1753
2344
|
owner,
|
|
1754
2345
|
...pollOpt(),
|
|
2346
|
+
...stream.transport,
|
|
1755
2347
|
url: OPENAI_RESPONSES_API_URL,
|
|
1756
2348
|
method: "POST",
|
|
1757
2349
|
headers: {
|
|
@@ -1761,6 +2353,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1761
2353
|
data: {
|
|
1762
2354
|
model: resolvedModel,
|
|
1763
2355
|
max_output_tokens: getMaxOutputTokens("openai", resolvedModel),
|
|
2356
|
+
// Top level beside model/input/tools, which is where the Responses API
|
|
2357
|
+
// takes it.
|
|
2358
|
+
...stream.body,
|
|
1764
2359
|
...extractContent && extractContent.length ? { _skapi_extract: extractContent } : {},
|
|
1765
2360
|
...fileUrls && fileUrls.length ? { _skapi_file_urls: fileUrls } : {},
|
|
1766
2361
|
input: responseInput,
|
|
@@ -2148,9 +2743,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2148
2743
|
{ service: params.service, owner: params.owner, platform: params.platform, queue: params.queue, status: params.status },
|
|
2149
2744
|
{ limit: params.limit, fetchMore: false }
|
|
2150
2745
|
)).then(function(result) {
|
|
2151
|
-
const
|
|
2152
|
-
bgProbeCache[key] =
|
|
2153
|
-
return
|
|
2746
|
+
const entry2 = { result, at: Date.now() };
|
|
2747
|
+
bgProbeCache[key] = entry2;
|
|
2748
|
+
return entry2;
|
|
2154
2749
|
});
|
|
2155
2750
|
bgProbeInflight[key] = p;
|
|
2156
2751
|
p.then(function() {
|
|
@@ -2380,8 +2975,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2380
2975
|
return projectId + "#" + platform;
|
|
2381
2976
|
}
|
|
2382
2977
|
function mapHistoryListToMessages(list, platform, opts) {
|
|
2383
|
-
var mapped = [], runningItemIds = [];
|
|
2978
|
+
var mapped = [], runningItemIds = [], streamPendingItemIds = [];
|
|
2384
2979
|
var extractAssistantText = platform === "openai" ? extractOpenAIText : extractClaudeText;
|
|
2980
|
+
var canRecoverStreams = streamRecoveryEnabled();
|
|
2385
2981
|
var filtered = filterListByClearHorizon(list, opts.clearedAt);
|
|
2386
2982
|
filtered.slice().reverse().forEach(function(item) {
|
|
2387
2983
|
var requestBody = item && item.request_body;
|
|
@@ -2395,6 +2991,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2395
2991
|
var userText = isCompact ? typeof item.request_text === "string" ? item.request_text : "" : extractLastUserTextFromRequest(requestBody);
|
|
2396
2992
|
var assistantText = isPending ? "" : isCompact ? (typeof item.response_text === "string" ? item.response_text : "").trim() : (extractAssistantText(response) || "").trim() || "";
|
|
2397
2993
|
var isErrorResponse = !isPending && (isFailed || !isCompact && isErrorResponseBody(response));
|
|
2994
|
+
var isStreamPending = canRecoverStreams && !isCompact && !isPending && !isCancelledItem && !isErrorResponse && !item._isBgTask && !item._isOnBgQueue && item.status === "resolved" && item.response_body == null && item.error == null && !assistantText;
|
|
2398
2995
|
var reportedComplete = !!(item && item._isBgTask) && !isErrorResponse && (isCompact ? item.response_complete_marker === true : !!assistantText && assistantText.indexOf(INDEXING_COMPLETE_MARKER) !== -1);
|
|
2399
2996
|
if (reportedComplete) assistantText = assistantText.split(INDEXING_COMPLETE_MARKER).join("").trim();
|
|
2400
2997
|
var serverItemId = item && typeof item.id === "string" && item.id ? item.id : void 0;
|
|
@@ -2453,6 +3050,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2453
3050
|
if (serverItemId !== void 0) em._serverItemId = serverItemId;
|
|
2454
3051
|
if (replyTs !== void 0) em._ts = replyTs;
|
|
2455
3052
|
mapped.push(em);
|
|
3053
|
+
} else if (isStreamPending) {
|
|
3054
|
+
var sp = { role: "assistant", content: "", _streamPending: true };
|
|
3055
|
+
if (serverItemId !== void 0) {
|
|
3056
|
+
sp._serverItemId = serverItemId;
|
|
3057
|
+
streamPendingItemIds.push(serverItemId);
|
|
3058
|
+
}
|
|
3059
|
+
if (replyTs !== void 0) sp._ts = replyTs;
|
|
3060
|
+
mapped.push(sp);
|
|
2456
3061
|
} else if (assistantText || reportedComplete) {
|
|
2457
3062
|
var okm = { role: "assistant", content: sanitizeAttachmentLinksForHistory(assistantText, opts.projectId, true) || EMPTY_INDEXING_REPLY };
|
|
2458
3063
|
if (item._fromBgChain) okm._fromBgChain = true;
|
|
@@ -2468,7 +3073,23 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2468
3073
|
var ownerKey = chatCacheKey(opts.projectId, platform, opts.userId);
|
|
2469
3074
|
for (var oi = 0; oi < mapped.length; oi++) mapped[oi]._ownerKey = ownerKey;
|
|
2470
3075
|
}
|
|
2471
|
-
return { messages: mapped, runningItemIds };
|
|
3076
|
+
return { messages: mapped, runningItemIds, streamPendingItemIds };
|
|
3077
|
+
}
|
|
3078
|
+
function adoptLocalAnswerIntoPage(incoming, local) {
|
|
3079
|
+
if (!incoming || !local || !incoming._streamPending) return false;
|
|
3080
|
+
if (incoming.role !== "assistant" || local.role !== "assistant") return false;
|
|
3081
|
+
var hasText = typeof local.content === "string" && local.content.length > 0;
|
|
3082
|
+
var isLive = !!(local.isPending || local._streaming);
|
|
3083
|
+
if (!hasText && !isLive) return false;
|
|
3084
|
+
if (hasText) {
|
|
3085
|
+
incoming.content = local.content;
|
|
3086
|
+
incoming._streamPending = false;
|
|
3087
|
+
}
|
|
3088
|
+
if (local._localId !== void 0) incoming._localId = local._localId;
|
|
3089
|
+
if (local.isPending) incoming.isPending = true;
|
|
3090
|
+
if (local.isPendingInProcess) incoming.isPendingInProcess = true;
|
|
3091
|
+
if (local._streaming) incoming._streaming = true;
|
|
3092
|
+
return true;
|
|
2472
3093
|
}
|
|
2473
3094
|
function shouldRescueInFlightMessage(m, ctx) {
|
|
2474
3095
|
if (!m) return false;
|
|
@@ -2476,6 +3097,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2476
3097
|
if (m._ownerKey !== void 0 && ctx.loadKey !== void 0 && m._ownerKey !== ctx.loadKey) return false;
|
|
2477
3098
|
if (m._serverItemId && ctx.hasServerId(m._serverItemId)) return false;
|
|
2478
3099
|
if (m._stageId) return true;
|
|
3100
|
+
if (m._streaming && !m._serverItemId) return true;
|
|
2479
3101
|
if (!m._serverItemId && ctx.pageHasPendingAssistant) return false;
|
|
2480
3102
|
if (m.isSendingToServer || m.isPendingQueued || m.isPendingInProcess || m.isPending) return true;
|
|
2481
3103
|
if (ctx.sending && m.role === "user") {
|
|
@@ -2862,8 +3484,143 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2862
3484
|
function isPollStopped(res) {
|
|
2863
3485
|
return !!res && typeof res === "object" && res.status === "stopped";
|
|
2864
3486
|
}
|
|
3487
|
+
var LIVE_PENDING_LINK_WINDOW = 512;
|
|
3488
|
+
var STREAM_RECOVERY_PER_LOAD = 2;
|
|
3489
|
+
function liveSafePrefix(text) {
|
|
3490
|
+
if (!text) return "";
|
|
3491
|
+
var cut = text.length;
|
|
3492
|
+
var fenceAt = -1, fences = 0, from = 0, hit;
|
|
3493
|
+
for (; ; ) {
|
|
3494
|
+
hit = text.indexOf("```", from);
|
|
3495
|
+
if (hit === -1) break;
|
|
3496
|
+
fences++;
|
|
3497
|
+
fenceAt = hit;
|
|
3498
|
+
from = hit + 3;
|
|
3499
|
+
}
|
|
3500
|
+
if (fences % 2 === 1 && fenceAt !== -1) cut = fenceAt;
|
|
3501
|
+
var head = text.slice(0, cut);
|
|
3502
|
+
var lineStart = head.lastIndexOf("\n") + 1;
|
|
3503
|
+
var line = head.slice(lineStart);
|
|
3504
|
+
var open = line.lastIndexOf("[");
|
|
3505
|
+
if (open !== -1 && line.length - open <= LIVE_PENDING_LINK_WINDOW) {
|
|
3506
|
+
var rest = line.slice(open);
|
|
3507
|
+
var close = rest.indexOf("]");
|
|
3508
|
+
if (close === -1) {
|
|
3509
|
+
cut = lineStart + open;
|
|
3510
|
+
} else if (rest.charAt(close + 1) === "(" && rest.indexOf(")", close + 1) === -1) {
|
|
3511
|
+
cut = lineStart + open;
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
var tokStart = line.length;
|
|
3515
|
+
while (tokStart > 0 && !/\s/.test(line.charAt(tokStart - 1))) tokStart--;
|
|
3516
|
+
var tok = line.slice(tokStart);
|
|
3517
|
+
if (tok && /^(?:https?:\/\/|src::)/i.test(tok)) {
|
|
3518
|
+
var tokCut = lineStart + tokStart;
|
|
3519
|
+
if (tokCut < cut) cut = tokCut;
|
|
3520
|
+
}
|
|
3521
|
+
if (line.indexOf("```") === -1) {
|
|
3522
|
+
var ticks = 0, lastTick = -1;
|
|
3523
|
+
for (var i = 0; i < line.length; i++) {
|
|
3524
|
+
if (line.charAt(i) === "`") {
|
|
3525
|
+
ticks++;
|
|
3526
|
+
lastTick = i;
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
if (ticks % 2 === 1 && lastTick !== -1) {
|
|
3530
|
+
var tickCut = lineStart + lastTick;
|
|
3531
|
+
if (tickCut < cut) cut = tickCut;
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
if (cut >= text.length) return text;
|
|
3535
|
+
if (cut < 0) cut = 0;
|
|
3536
|
+
return text.slice(0, cut);
|
|
3537
|
+
}
|
|
3538
|
+
function commonPrefixLength(a, b) {
|
|
3539
|
+
var n = Math.min(a.length, b.length), i = 0;
|
|
3540
|
+
while (i < n && a.charCodeAt(i) === b.charCodeAt(i)) i++;
|
|
3541
|
+
if (i > 0) {
|
|
3542
|
+
var prev = a.charCodeAt(i - 1);
|
|
3543
|
+
if (prev >= 55296 && prev <= 56319) i--;
|
|
3544
|
+
}
|
|
3545
|
+
return i;
|
|
3546
|
+
}
|
|
3547
|
+
function typewriterResumeIndex(painted, fullText, regions) {
|
|
3548
|
+
if (!painted || !fullText) return 0;
|
|
3549
|
+
if (/^\s/.test(painted) && !/^\s/.test(fullText)) {
|
|
3550
|
+
painted = painted.replace(/^\s+/, "");
|
|
3551
|
+
if (!painted) return 0;
|
|
3552
|
+
}
|
|
3553
|
+
var i = commonPrefixLength(painted, fullText);
|
|
3554
|
+
if (i <= 0) return 0;
|
|
3555
|
+
if (i >= fullText.length) return fullText.length;
|
|
3556
|
+
for (var changed = true; changed; ) {
|
|
3557
|
+
changed = false;
|
|
3558
|
+
for (var k = 0; k < regions.length; k++) {
|
|
3559
|
+
var r = regions[k];
|
|
3560
|
+
if (i > r.start && i < r.end) {
|
|
3561
|
+
i = r.end;
|
|
3562
|
+
changed = true;
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
return i > fullText.length ? fullText.length : i;
|
|
3567
|
+
}
|
|
3568
|
+
function mayKeepStreamedAnswer(snap, rowStatus) {
|
|
3569
|
+
if (rowStatus !== void 0 && rowStatus !== null && rowStatus !== "" && rowStatus !== "resolved") return false;
|
|
3570
|
+
if (!snap || typeof snap !== "object") return false;
|
|
3571
|
+
if (snap.errored) return false;
|
|
3572
|
+
if (snap.answerComplete) return true;
|
|
3573
|
+
if (snap.unframed) return true;
|
|
3574
|
+
return false;
|
|
3575
|
+
}
|
|
3576
|
+
function streamRecoveryPhase(msg) {
|
|
3577
|
+
if (!msg || !msg._streamPending || msg.content || !msg._serverItemId) return "";
|
|
3578
|
+
if (msg._streamRecovery === "active") return "active";
|
|
3579
|
+
if (msg._streamRecovery === "failed") return "failed";
|
|
3580
|
+
return "idle";
|
|
3581
|
+
}
|
|
3582
|
+
function streamRecoveryLabels(phase) {
|
|
3583
|
+
if (phase === "failed") {
|
|
3584
|
+
return { note: "Could not load this answer.", action: "Try again" };
|
|
3585
|
+
}
|
|
3586
|
+
return { note: "This answer was not saved with the conversation.", action: "Load answer" };
|
|
3587
|
+
}
|
|
3588
|
+
var LIVE_PAINT_MIN_MS = 250;
|
|
3589
|
+
var LIVE_TYPE_MAX_STEP = 1200;
|
|
2865
3590
|
var ChatSession = class {
|
|
2866
3591
|
constructor(host) {
|
|
3592
|
+
// --- live streaming ----------------------------------------------------
|
|
3593
|
+
//
|
|
3594
|
+
// A streamed turn's answer NEVER reaches the polling row: the relay appends the
|
|
3595
|
+
// destination's raw bytes to a chunk table and the row settles with a status and
|
|
3596
|
+
// nothing else. So for a streamed turn this parser is not a nicety that makes the
|
|
3597
|
+
// wait prettier, it is the only place the answer exists until csr-finalize stores
|
|
3598
|
+
// one. Three things follow, and all three are load-bearing:
|
|
3599
|
+
//
|
|
3600
|
+
// 1. EVERY foreground poll gets a sink while streaming is on, not just the one
|
|
3601
|
+
// the dispatch attaches. A tab return, a reload, a resumePolling all
|
|
3602
|
+
// re-attach a poll to a still-running item, and skapi's reader sends
|
|
3603
|
+
// `since: 0` on its first tick, so a fresh sink REPLAYS the whole stream from
|
|
3604
|
+
// the beginning. Attaching without one settles that turn on an envelope and
|
|
3605
|
+
// the user's answer is gone.
|
|
3606
|
+
// 2. The parser is keyed by SERVER ITEM ID, and so is the bubble it paints into.
|
|
3607
|
+
// A history refetch replaces the local pending bubble with the server's copy
|
|
3608
|
+
// of the same turn; that copy carries the same _serverItemId, so the next
|
|
3609
|
+
// paint finds it and carries on. Nothing has to be rescued and nothing can be
|
|
3610
|
+
// painted twice.
|
|
3611
|
+
// 3. The stream is never the source of truth. At settle the parser's ASSEMBLED
|
|
3612
|
+
// body (byte equivalent to what a buffered call returns) goes through the
|
|
3613
|
+
// same extractClaudeText / extractOpenAIText the buffered path uses, and a
|
|
3614
|
+
// row that does hold a stored body wins outright.
|
|
3615
|
+
//
|
|
3616
|
+
// Background polls never get a sink, and that is safe because nothing on the bg
|
|
3617
|
+
// queue ever streams: an indexing pass must not (the worker READS its reply), and
|
|
3618
|
+
// a chat turn sent with attachments is deliberately left buffered for exactly the
|
|
3619
|
+
// reason point 1 gives, since the re-attach loop would poll it as a background
|
|
3620
|
+
// item and hand it no reader. See chatStreamWiring. A sink there would also spend
|
|
3621
|
+
// the request budget MAX_CONCURRENT_BG_POLLS exists to protect.
|
|
3622
|
+
/** Live streams by server item id. One per in-flight streamed turn. */
|
|
3623
|
+
this.liveStreams = {};
|
|
2867
3624
|
// ─── compact-stub hydration ─────────────────────────────────────────────
|
|
2868
3625
|
// Split-fetch bg pages arrive as label stubs (no bodies). When the user
|
|
2869
3626
|
// expands a row, the real reply text is fetched per item (csr-poll point
|
|
@@ -3068,8 +3825,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3068
3825
|
return Promise.resolve(probeBgQueue(
|
|
3069
3826
|
{ service: id.projectId, owner: id.owner, platform, queue, status, limit: WORKER_PASS_ADOPT_LIMIT },
|
|
3070
3827
|
{ maxAgeMs: BG_PROBE_TTL_MS }
|
|
3071
|
-
)).then(function(
|
|
3072
|
-
return
|
|
3828
|
+
)).then(function(entry2) {
|
|
3829
|
+
return entry2.result;
|
|
3073
3830
|
}).catch(function() {
|
|
3074
3831
|
return null;
|
|
3075
3832
|
});
|
|
@@ -3256,10 +4013,49 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3256
4013
|
* and they are the ones bounded by MAX_CONCURRENT_BG_POLLS, so adding probes there would spend
|
|
3257
4014
|
* the request budget the cap exists to protect.
|
|
3258
4015
|
*/
|
|
3259
|
-
attachForegroundPoll(source, itemId, opts) {
|
|
3260
|
-
return this._fgPollWithEarlyProbe(source, itemId, opts);
|
|
4016
|
+
attachForegroundPoll(source, itemId, opts, ctx) {
|
|
4017
|
+
return this._fgPollWithEarlyProbe(source, itemId, opts, ctx);
|
|
3261
4018
|
}
|
|
3262
|
-
_fgPollWithEarlyProbe(source, itemId, opts) {
|
|
4019
|
+
_fgPollWithEarlyProbe(source, itemId, opts, ctx) {
|
|
4020
|
+
var self = this;
|
|
4021
|
+
var live = this._beginLiveStream(itemId, ctx);
|
|
4022
|
+
if (live) {
|
|
4023
|
+
var inner = opts || {};
|
|
4024
|
+
var callerResponse = typeof inner.onResponse === "function" ? inner.onResponse : null;
|
|
4025
|
+
var callerError = typeof inner.onError === "function" ? inner.onError : null;
|
|
4026
|
+
var streamOpts = Object.assign({}, inner, {
|
|
4027
|
+
onStream: function(chunk, _seq, via) {
|
|
4028
|
+
self._feedLiveStream(live, chunk, via);
|
|
4029
|
+
},
|
|
4030
|
+
onResponse: function(res) {
|
|
4031
|
+
var effective = res;
|
|
4032
|
+
if (isPollStopped(res)) self._closeLiveStream(live, false);
|
|
4033
|
+
else effective = self._settleLiveStream(live, res);
|
|
4034
|
+
if (callerResponse) callerResponse(effective);
|
|
4035
|
+
},
|
|
4036
|
+
onError: function(err) {
|
|
4037
|
+
self._closeLiveStream(live, false);
|
|
4038
|
+
if (callerError) callerError(err);
|
|
4039
|
+
}
|
|
4040
|
+
});
|
|
4041
|
+
var lp = source.poll(Object.assign({ latency: STREAM_POLL_INTERVAL }, streamOpts));
|
|
4042
|
+
var stopLp = lp && typeof lp.stop === "function" ? lp.stop.bind(lp) : null;
|
|
4043
|
+
var wrapped = Promise.resolve(lp).then(function(res) {
|
|
4044
|
+
if (isPollStopped(res)) {
|
|
4045
|
+
self._closeLiveStream(live, false);
|
|
4046
|
+
return res;
|
|
4047
|
+
}
|
|
4048
|
+
return self._settleLiveStream(live, res);
|
|
4049
|
+
}, function(err) {
|
|
4050
|
+
self._closeLiveStream(live, false);
|
|
4051
|
+
throw err;
|
|
4052
|
+
});
|
|
4053
|
+
wrapped.stop = function() {
|
|
4054
|
+
self._closeLiveStream(live, false);
|
|
4055
|
+
if (stopLp) stopLp();
|
|
4056
|
+
};
|
|
4057
|
+
return wrapped;
|
|
4058
|
+
}
|
|
3263
4059
|
var base = source.poll(Object.assign({ latency: POLL_INTERVAL }, opts || {}));
|
|
3264
4060
|
var lookup = chatEngineConfig().csrHistoryItemLookup;
|
|
3265
4061
|
var ident = this.host.getIdentity();
|
|
@@ -3341,6 +4137,661 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3341
4137
|
});
|
|
3342
4138
|
return n;
|
|
3343
4139
|
}
|
|
4140
|
+
/**
|
|
4141
|
+
* Open (or re-open) the live stream for `itemId`, or null when this poll must
|
|
4142
|
+
* not carry one.
|
|
4143
|
+
*
|
|
4144
|
+
* Re-entrant on purpose: an auth-refresh retry re-dispatches the SAME turn under
|
|
4145
|
+
* a NEW id, and a re-attach after a tab return replays an existing id from seq 0.
|
|
4146
|
+
* Either way the bytes about to arrive are a whole stream, so an existing entry
|
|
4147
|
+
* is discarded and a fresh parser takes its place - feeding a replay into the old
|
|
4148
|
+
* parser would concatenate the answer with itself.
|
|
4149
|
+
*
|
|
4150
|
+
* `ctx` IS THE TURN'S OWN IDENTITY, and every caller that has one passes it.
|
|
4151
|
+
* This used to read the LIVE getIdentity(), which is a bug of exactly the kind
|
|
4152
|
+
* _callProviderFor documents and threads its own parameters to avoid: the user
|
|
4153
|
+
* hits Send, then switches project or platform inside the ack round trip, and the
|
|
4154
|
+
* stream that opens for the OLD turn is stamped with the NEW identity. What that
|
|
4155
|
+
* costs is not cosmetic - `platform` picks which url csr-finalize is addressed
|
|
4156
|
+
* with and which extractor reads the assembled body, `projectId`/`owner` scope
|
|
4157
|
+
* the finalize itself, and `ownerKey` decides which chat the answer is painted
|
|
4158
|
+
* into. Get them from the live read at the wrong moment and the turn is finalized
|
|
4159
|
+
* against the wrong service (so its answer is never stored), parsed with the
|
|
4160
|
+
* wrong provider's extractor, or painted into a conversation it does not belong
|
|
4161
|
+
* to. The live read stays only as the fallback for a caller with nothing pinned.
|
|
4162
|
+
*/
|
|
4163
|
+
_beginLiveStream(itemId, ctx) {
|
|
4164
|
+
if (!liveStreamingEnabled()) return null;
|
|
4165
|
+
if (!itemId) {
|
|
4166
|
+
console.warn("[chat-engine] live streaming is on but the dispatch reported no item id");
|
|
4167
|
+
return null;
|
|
4168
|
+
}
|
|
4169
|
+
var pinnedPlatform = ctx && (ctx.platform === "claude" || ctx.platform === "openai") ? ctx.platform : void 0;
|
|
4170
|
+
var ident = pinnedPlatform && ctx && ctx.projectId !== void 0 && ctx.owner !== void 0 && ctx.ownerKey !== void 0 ? null : this.host.getIdentity();
|
|
4171
|
+
var platform = pinnedPlatform || (ident ? ident.platform : void 0);
|
|
4172
|
+
if (platform !== "claude" && platform !== "openai") return null;
|
|
4173
|
+
var projectId = ctx && ctx.projectId !== void 0 ? ctx.projectId : ident ? ident.projectId : "";
|
|
4174
|
+
var owner = ctx && ctx.owner !== void 0 ? ctx.owner : ident ? ident.owner : "";
|
|
4175
|
+
var ownerKey = ctx && ctx.ownerKey !== void 0 ? ctx.ownerKey : this.getHistoryCacheKey();
|
|
4176
|
+
var prev = this.liveStreams[itemId];
|
|
4177
|
+
if (prev) this._closeLiveStream(prev, false);
|
|
4178
|
+
var st = {
|
|
4179
|
+
id: itemId,
|
|
4180
|
+
ownerKey,
|
|
4181
|
+
platform,
|
|
4182
|
+
projectId,
|
|
4183
|
+
owner,
|
|
4184
|
+
parser: createSseParser(),
|
|
4185
|
+
painted: "",
|
|
4186
|
+
started: false,
|
|
4187
|
+
fed: false,
|
|
4188
|
+
ended: false,
|
|
4189
|
+
timer: null,
|
|
4190
|
+
lastPaintAt: 0,
|
|
4191
|
+
finalBody: null,
|
|
4192
|
+
transport: { socket: 0, poll: 0 }
|
|
4193
|
+
};
|
|
4194
|
+
this.liveStreams[itemId] = st;
|
|
4195
|
+
return st;
|
|
4196
|
+
}
|
|
4197
|
+
/** The chunk sink handed to skapi's poll. Raw relayed text, in order, never parsed
|
|
4198
|
+
* here: the parser owns the grammar and this owns the pacing. */
|
|
4199
|
+
_feedLiveStream(st, chunk, via) {
|
|
4200
|
+
if (st.ended || typeof chunk !== "string" || !chunk) return;
|
|
4201
|
+
st.fed = true;
|
|
4202
|
+
if (via === "socket") st.transport.socket++;
|
|
4203
|
+
else if (via === "poll") st.transport.poll++;
|
|
4204
|
+
st.parser.feed(chunk);
|
|
4205
|
+
if (st.timer) return;
|
|
4206
|
+
var self = this;
|
|
4207
|
+
var wait = st.lastPaintAt ? Math.max(0, LIVE_PAINT_MIN_MS - (nowMs() - st.lastPaintAt)) : 0;
|
|
4208
|
+
st.timer = setTimeout(function() {
|
|
4209
|
+
st.timer = null;
|
|
4210
|
+
self._paintLiveStream(st);
|
|
4211
|
+
}, wait);
|
|
4212
|
+
}
|
|
4213
|
+
/**
|
|
4214
|
+
* Write the safe prefix of the answer so far into the turn's bubble.
|
|
4215
|
+
*
|
|
4216
|
+
* notify() is spent EXACTLY ONCE per turn, on the first paint, because that is a
|
|
4217
|
+
* state change the per-bubble refresh cannot express: the bubble stops being a
|
|
4218
|
+
* "Thinking..." spinner and becomes text. Every paint after it goes through
|
|
4219
|
+
* refreshMessageBubble, which is what keeps a growing answer from rebuilding the
|
|
4220
|
+
* whole display list once a second.
|
|
4221
|
+
*/
|
|
4222
|
+
_paintLiveStream(st) {
|
|
4223
|
+
if (st.ended) return;
|
|
4224
|
+
st.lastPaintAt = nowMs();
|
|
4225
|
+
if (this.getHistoryCacheKey() !== st.ownerKey) return;
|
|
4226
|
+
var idx = this._liveTargetIndex(st.id);
|
|
4227
|
+
if (idx === -1) return;
|
|
4228
|
+
var msg = this.state.messages[idx];
|
|
4229
|
+
if (!msg) return;
|
|
4230
|
+
var snap = st.parser.snapshot();
|
|
4231
|
+
var next = liveSafePrefix(snap.text);
|
|
4232
|
+
if (next.length <= st.painted.length) return;
|
|
4233
|
+
var prev = st.painted;
|
|
4234
|
+
st.painted = next;
|
|
4235
|
+
var grew = next.length - prev.length;
|
|
4236
|
+
var animate = grew > 0 && grew <= LIVE_TYPE_MAX_STEP;
|
|
4237
|
+
if (animate) {
|
|
4238
|
+
if (!msg._localId) msg._localId = this._newLocalId();
|
|
4239
|
+
if (!msg._streaming) {
|
|
4240
|
+
msg._streaming = true;
|
|
4241
|
+
this.host.notify();
|
|
4242
|
+
}
|
|
4243
|
+
this.enqueueTypewrite(idx, next, msg._localId, prev);
|
|
4244
|
+
} else {
|
|
4245
|
+
msg.content = next;
|
|
4246
|
+
if (!msg._streaming) {
|
|
4247
|
+
msg._streaming = true;
|
|
4248
|
+
this.host.notify();
|
|
4249
|
+
} else this.host.refreshMessageBubble(idx);
|
|
4250
|
+
}
|
|
4251
|
+
this.host.scrollToBottomIfSticky();
|
|
4252
|
+
this._reportLiveStream(st, st.started ? "update" : "start", snap, next);
|
|
4253
|
+
st.started = true;
|
|
4254
|
+
}
|
|
4255
|
+
/** The bubble a live stream paints into: the turn's pending assistant placeholder,
|
|
4256
|
+
* found by server item id. Not by _localId, deliberately - a history refetch
|
|
4257
|
+
* replaces the local copy with the server's, and only the id survives that. */
|
|
4258
|
+
_liveTargetIndex(itemId) {
|
|
4259
|
+
return this.state.messages.findIndex(function(m) {
|
|
4260
|
+
return !!m && m.role === "assistant" && !m.isBackgroundTask && m._serverItemId === itemId && (!!m.isPending || !!m._streaming);
|
|
4261
|
+
});
|
|
4262
|
+
}
|
|
4263
|
+
/** Hand the host its optional observation update. Guarded: this runs on the paint
|
|
4264
|
+
* path, and a throwing hook must not cost the user the rest of their answer. */
|
|
4265
|
+
_reportLiveStream(st, phase, snap, text) {
|
|
4266
|
+
var hook = chatEngineConfig().onLiveStreamUpdate;
|
|
4267
|
+
if (!hook) return;
|
|
4268
|
+
try {
|
|
4269
|
+
hook({
|
|
4270
|
+
serverItemId: st.id,
|
|
4271
|
+
ownerKey: st.ownerKey,
|
|
4272
|
+
phase,
|
|
4273
|
+
text,
|
|
4274
|
+
thinkingText: snap && snap.thinkingText || "",
|
|
4275
|
+
toolNames: snap && snap.toolNames ? snap.toolNames.slice() : [],
|
|
4276
|
+
complete: !!(snap && snap.complete),
|
|
4277
|
+
// Reported alongside `complete`, never instead of it: a host drawing
|
|
4278
|
+
// "still arriving" wants complete, a host drawing "this answer is
|
|
4279
|
+
// partial" wants this one, and an `error` frame is the case where the
|
|
4280
|
+
// two disagree. See sse.ts answerComplete.
|
|
4281
|
+
answerComplete: !!(snap && snap.answerComplete),
|
|
4282
|
+
errored: !!(snap && snap.errored),
|
|
4283
|
+
transport: { socket: st.transport.socket, poll: st.transport.poll }
|
|
4284
|
+
});
|
|
4285
|
+
} catch (e) {
|
|
4286
|
+
console.warn("[chat-engine] onLiveStreamUpdate threw", e);
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
4289
|
+
/** Stop painting and (when the turn really ended) assemble the body. `finished`
|
|
4290
|
+
* is false for a stream being discarded rather than settled: a retry replacing
|
|
4291
|
+
* it, or a stop, neither of which has an answer to assemble. */
|
|
4292
|
+
_closeLiveStream(st, finished) {
|
|
4293
|
+
var first = !st.ended;
|
|
4294
|
+
if (st.timer) {
|
|
4295
|
+
clearTimeout(st.timer);
|
|
4296
|
+
st.timer = null;
|
|
4297
|
+
}
|
|
4298
|
+
if (first) {
|
|
4299
|
+
st.ended = true;
|
|
4300
|
+
if (finished && st.fed) {
|
|
4301
|
+
st.parser.end();
|
|
4302
|
+
st.finalBody = st.parser.finalBody();
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
if (this.liveStreams[st.id] === st) delete this.liveStreams[st.id];
|
|
4306
|
+
if (first && st.started) this._reportLiveStream(st, "end", st.parser.snapshot(), "");
|
|
4307
|
+
if (this.getHistoryCacheKey() !== st.ownerKey) return;
|
|
4308
|
+
var idx = this._liveTargetIndex(st.id);
|
|
4309
|
+
if (idx !== -1 && this.state.messages[idx] && this.state.messages[idx]._streaming) {
|
|
4310
|
+
this.state.messages[idx]._streaming = false;
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
/**
|
|
4314
|
+
* Settle a streamed turn: end the parse, decide the body the rest of the session
|
|
4315
|
+
* will read, and release the chunks.
|
|
4316
|
+
*
|
|
4317
|
+
* The substitution is one-directional and never a merge. A response that is a
|
|
4318
|
+
* real stored body (a buffered turn, or a streamed one somebody already
|
|
4319
|
+
* finalized) is returned untouched, because that is the destination's own answer
|
|
4320
|
+
* and the stream is not entitled to overwrite it. Only a STATUS ENVELOPE - the
|
|
4321
|
+
* shape a streamed row settles as, having stored nothing - is replaced, and then
|
|
4322
|
+
* by the assembled body, which every caller downstream reads with the same
|
|
4323
|
+
* extractor it uses for a buffered reply. Idempotent, because it is reached both
|
|
4324
|
+
* through the poll's onResponse and through the promise it resolves.
|
|
4325
|
+
*/
|
|
4326
|
+
_settleLiveStream(st, response) {
|
|
4327
|
+
this._closeLiveStream(st, true);
|
|
4328
|
+
if (!isCsrStatusEnvelope(response)) return response;
|
|
4329
|
+
if (response.status !== "resolved") return response;
|
|
4330
|
+
if (!this._mayFinalize(st)) this._rec().incomplete[st.id] = true;
|
|
4331
|
+
if (st.finalBody == null) return response;
|
|
4332
|
+
this._finalizeStreamedTurn(st);
|
|
4333
|
+
return st.finalBody;
|
|
4334
|
+
}
|
|
4335
|
+
/**
|
|
4336
|
+
* May this parse be STORED as the turn's permanent answer?
|
|
4337
|
+
*
|
|
4338
|
+
* THE FAILURE THIS PREVENTS. Finalizing does two things at once: it stores what
|
|
4339
|
+
* you give it as the row's result, and it DELETES the chunks it was assembled
|
|
4340
|
+
* from. So finalizing a truncated parse is not a cosmetic loss, it is the
|
|
4341
|
+
* permanent one: the truncation becomes the stored answer and the only copy of
|
|
4342
|
+
* the missing part is deleted in the same call. And a truncated parse is a shape
|
|
4343
|
+
* this repo has already paid for - a degraded chunk read (the poller degrades to
|
|
4344
|
+
* "no chunks this tick, more=true" on any transient chunk-table error, and caps
|
|
4345
|
+
* a long answer at 500k characters per response) can hand the settle a stream
|
|
4346
|
+
* that stopped mid-answer. The row can settle 'resolved' on top of that, because
|
|
4347
|
+
* the ROW's status describes the destination's request, not the client's read of
|
|
4348
|
+
* it.
|
|
4349
|
+
*
|
|
4350
|
+
* THE POLICY ITSELF IS mayKeepStreamedAnswer (top of this file), shared with the
|
|
4351
|
+
* recovery path so the two cannot drift apart again - they did, and the drift was
|
|
4352
|
+
* silent: the live settle refused a failed turn while the recovery finalized one.
|
|
4353
|
+
* What is local to this method is only the two things the free function cannot
|
|
4354
|
+
* know: that there is an assembled body at all, and that this call site is
|
|
4355
|
+
* reached only on a row that settled 'resolved' (the caller returns before it
|
|
4356
|
+
* otherwise), which is the status it therefore states.
|
|
4357
|
+
*
|
|
4358
|
+
* The test the policy applies is deliberately NOT `complete`: a terminal event
|
|
4359
|
+
* arrived and the answer finished are two claims, and an `error` frame satisfies
|
|
4360
|
+
* the first while truncating the second. See sse.ts's answerComplete.
|
|
4361
|
+
*/
|
|
4362
|
+
_mayFinalize(st) {
|
|
4363
|
+
if (st.finalBody == null) return false;
|
|
4364
|
+
return mayKeepStreamedAnswer(st.parser.snapshot(), "resolved");
|
|
4365
|
+
}
|
|
4366
|
+
/**
|
|
4367
|
+
* Store the assembled body as the version history keeps, which is also what
|
|
4368
|
+
* releases this request's chunks.
|
|
4369
|
+
*
|
|
4370
|
+
* The ASSEMBLED BODY and not the extracted text, because the row is read back by
|
|
4371
|
+
* mapHistoryListToMessages through extractClaudeText / extractOpenAIText: storing
|
|
4372
|
+
* the provider's own document is what makes a streamed turn indistinguishable
|
|
4373
|
+
* from a buffered one on the next load, with no branch anywhere in the mapper.
|
|
4374
|
+
*
|
|
4375
|
+
* BEST EFFORT, and loudly so: the answer is already on screen and already in the
|
|
4376
|
+
* history cache by the time this fires. A failure costs the chunks (they stay,
|
|
4377
|
+
* and the turn stays re-readable) and a row that reads back empty, never the
|
|
4378
|
+
* user's answer in front of them.
|
|
4379
|
+
*
|
|
4380
|
+
* WHAT IS DELIBERATELY NEVER FINALIZED, because finalize is also the only way to
|
|
4381
|
+
* release chunks and it is tempting to reach for it as a cleanup:
|
|
4382
|
+
*
|
|
4383
|
+
* - an INCOMPLETE parse (see _mayFinalize). Storing a truncation makes it
|
|
4384
|
+
* permanent AND deletes the part that was missing from it. A stream killed by
|
|
4385
|
+
* an `error` frame is one of these however terminal it looks: the frame ends
|
|
4386
|
+
* the stream, so `complete` is true, while the text is only what arrived
|
|
4387
|
+
* before the error. That is why the gate reads answerComplete.
|
|
4388
|
+
* - a FAILED turn. Its chunks hold the part of the answer that did arrive,
|
|
4389
|
+
* which is the only copy of that text there is, and the two ways to release
|
|
4390
|
+
* them both cost something real: storing the partial makes a truncated answer
|
|
4391
|
+
* the turn's permanent history AND masks the failure on read (csr-poll hands
|
|
4392
|
+
* back a finalized body before it ever looks at the row's error, so the turn
|
|
4393
|
+
* would read back as a clean short answer), while storing the error throws
|
|
4394
|
+
* the partial away outright. Keeping them costs storage on rows that produced
|
|
4395
|
+
* bytes and then failed, which is rare - a failure before the first byte (a
|
|
4396
|
+
* wrong API key, the common case) has no chunks to keep - and the poller
|
|
4397
|
+
* hands those chunks back alongside the error on every later read, so nothing
|
|
4398
|
+
* is stranded, only retained. Retention is the honest trade here; deletion is
|
|
4399
|
+
* not reversible.
|
|
4400
|
+
* - a CANCELLED turn, for the same reason plus one: the user's Stop means the
|
|
4401
|
+
* half answer is to be discarded, so writing it into history as the kept
|
|
4402
|
+
* version would resurrect exactly what the stop was for.
|
|
4403
|
+
*/
|
|
4404
|
+
_finalizeStreamedTurn(st) {
|
|
4405
|
+
if (st.finalized) return;
|
|
4406
|
+
if (!this._mayFinalize(st)) return;
|
|
4407
|
+
var fin = chatEngineConfig().clientSecretRequestFinalize;
|
|
4408
|
+
if (!fin || st.finalBody == null) return;
|
|
4409
|
+
st.finalized = true;
|
|
4410
|
+
var url = st.platform === "openai" ? OPENAI_RESPONSES_API_URL : ANTHROPIC_MESSAGES_API_URL;
|
|
4411
|
+
try {
|
|
4412
|
+
Promise.resolve(fin(st.id, st.finalBody, {
|
|
4413
|
+
url,
|
|
4414
|
+
method: "POST",
|
|
4415
|
+
service: st.projectId,
|
|
4416
|
+
owner: st.owner
|
|
4417
|
+
})).catch(function(err) {
|
|
4418
|
+
console.warn("[chat-engine] clientSecretRequestFinalize failed", err);
|
|
4419
|
+
});
|
|
4420
|
+
} catch (e) {
|
|
4421
|
+
console.warn("[chat-engine] clientSecretRequestFinalize threw", e);
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
/** Painted-but-unsettled live text on a bubble, for the typewriter to resume from.
|
|
4425
|
+
* A pending assistant placeholder is created with content '' by every path that
|
|
4426
|
+
* makes one, so non-empty content on one can only have been painted here. */
|
|
4427
|
+
_paintedTextAt(idx) {
|
|
4428
|
+
var m = idx >= 0 ? this.state.messages[idx] : void 0;
|
|
4429
|
+
if (!m || m.role !== "assistant" || typeof m.content !== "string") return "";
|
|
4430
|
+
return m.content;
|
|
4431
|
+
}
|
|
4432
|
+
/** The recovery bookkeeping, created on first touch.
|
|
4433
|
+
*
|
|
4434
|
+
* LAZY, not constructor-initialised, and for a concrete reason: ChatSession is
|
|
4435
|
+
* also built with Object.create(ChatSession.prototype) by the engine's own test
|
|
4436
|
+
* harnesses, which drive one method against a hand-built state rather than a
|
|
4437
|
+
* whole session. A field only the constructor creates is undefined there, and
|
|
4438
|
+
* the method that reaches for it throws, turning a test of the settle into a
|
|
4439
|
+
* crash about bookkeeping. */
|
|
4440
|
+
_rec() {
|
|
4441
|
+
if (!this._streamRecovery) this._streamRecovery = { incomplete: {}, attempted: {}, inflight: {}, failed: {}, queue: [], running: false };
|
|
4442
|
+
return this._streamRecovery;
|
|
4443
|
+
}
|
|
4444
|
+
/**
|
|
4445
|
+
* Put this session's fetching state onto the turn's bubble, so a view can tell a
|
|
4446
|
+
* loader that means something from one that means nothing.
|
|
4447
|
+
*
|
|
4448
|
+
* ONLY EVER ONTO A STILL-MARKED BUBBLE. Once `_streamPending` is off the turn has
|
|
4449
|
+
* an answer (or was proven to have none) and this says nothing about it; writing
|
|
4450
|
+
* it there would leave a stale 'active' on a settled bubble forever.
|
|
4451
|
+
*
|
|
4452
|
+
* host.notify() is what redraws the widget, whose renderer is imperative. It is a
|
|
4453
|
+
* no-op in agent.vue, whose state is a Vue reactive() - the property write above
|
|
4454
|
+
* is what redraws there. Both are covered by doing both, and neither is a
|
|
4455
|
+
* substitute for the other.
|
|
4456
|
+
*/
|
|
4457
|
+
_markRecoveryPhase(itemId, phase) {
|
|
4458
|
+
var changed = false;
|
|
4459
|
+
for (var i = 0; i < this.state.messages.length; i++) {
|
|
4460
|
+
var m = this.state.messages[i];
|
|
4461
|
+
if (!m || m.role !== "assistant" || m._serverItemId !== itemId || !m._streamPending) continue;
|
|
4462
|
+
var next = phase === null ? void 0 : phase;
|
|
4463
|
+
if (m._streamRecovery === next) continue;
|
|
4464
|
+
if (next === void 0) delete m._streamRecovery;
|
|
4465
|
+
else m._streamRecovery = next;
|
|
4466
|
+
changed = true;
|
|
4467
|
+
}
|
|
4468
|
+
if (changed) this.host.notify();
|
|
4469
|
+
}
|
|
4470
|
+
/**
|
|
4471
|
+
* Let LOCAL answers survive a freshly-mapped page whose copies of them are
|
|
4472
|
+
* authoritative-but-empty. Call with the page BEFORE it replaces or merges into
|
|
4473
|
+
* state.messages; mutates the page's bubbles in place.
|
|
4474
|
+
*
|
|
4475
|
+
* The adoption itself is history.ts's adoptLocalAnswerIntoPage (shared, so the
|
|
4476
|
+
* clients' own mappers cannot fork it). What lives here is the one thing the
|
|
4477
|
+
* pure function cannot know: whether the local text is the WHOLE answer. Text
|
|
4478
|
+
* left by a stream that ended without a terminal event is not, so that bubble
|
|
4479
|
+
* keeps its marker and gets read back even though it has content - otherwise a
|
|
4480
|
+
* truncated answer would adopt itself over the row and never be corrected.
|
|
4481
|
+
*/
|
|
4482
|
+
_adoptLocalAnswers(mapped, loadKey) {
|
|
4483
|
+
if (!mapped || !mapped.length) return;
|
|
4484
|
+
var pendingIncoming = [];
|
|
4485
|
+
for (var i = 0; i < mapped.length; i++) {
|
|
4486
|
+
if (mapped[i] && mapped[i]._streamPending) pendingIncoming.push(mapped[i]);
|
|
4487
|
+
}
|
|
4488
|
+
if (!pendingIncoming.length) return;
|
|
4489
|
+
var locals = {};
|
|
4490
|
+
for (var j = 0; j < this.state.messages.length; j++) {
|
|
4491
|
+
var lm = this.state.messages[j];
|
|
4492
|
+
if (!lm || lm.role !== "assistant" || !lm._serverItemId) continue;
|
|
4493
|
+
if (lm._ownerKey !== void 0 && loadKey !== void 0 && lm._ownerKey !== loadKey) continue;
|
|
4494
|
+
if (locals[lm._serverItemId] === void 0) locals[lm._serverItemId] = lm;
|
|
4495
|
+
}
|
|
4496
|
+
for (var k = 0; k < pendingIncoming.length; k++) {
|
|
4497
|
+
var inc = pendingIncoming[k];
|
|
4498
|
+
var id = inc._serverItemId;
|
|
4499
|
+
if (!id) continue;
|
|
4500
|
+
var local = locals[id];
|
|
4501
|
+
if (!local) continue;
|
|
4502
|
+
if (!adoptLocalAnswerIntoPage(inc, local)) continue;
|
|
4503
|
+
if (this._rec().incomplete[id]) inc._streamPending = true;
|
|
4504
|
+
}
|
|
4505
|
+
for (var p = 0; p < pendingIncoming.length; p++) {
|
|
4506
|
+
var pi = pendingIncoming[p];
|
|
4507
|
+
if (!pi._streamPending || !pi._serverItemId) continue;
|
|
4508
|
+
var phase = this._recoveryPhaseFor(pi._serverItemId);
|
|
4509
|
+
if (phase === null) delete pi._streamRecovery;
|
|
4510
|
+
else pi._streamRecovery = phase;
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
/**
|
|
4514
|
+
* This session's fetching state for one turn, from the bookkeeping rather than
|
|
4515
|
+
* from any bubble. A queued entry counts as 'active': it is committed to be read,
|
|
4516
|
+
* serially, and the reader has no way to tell "being read" from "next in line"
|
|
4517
|
+
* apart from the wait.
|
|
4518
|
+
*/
|
|
4519
|
+
_recoveryPhaseFor(itemId) {
|
|
4520
|
+
var rec = this._rec();
|
|
4521
|
+
if (rec.inflight[itemId]) return "active";
|
|
4522
|
+
for (var i = 0; i < rec.queue.length; i++) if (rec.queue[i].id === itemId) return "active";
|
|
4523
|
+
if (rec.failed[itemId]) return "failed";
|
|
4524
|
+
return null;
|
|
4525
|
+
}
|
|
4526
|
+
/**
|
|
4527
|
+
* PUBLIC DELEGATE, for a client that maps and merges its own history page.
|
|
4528
|
+
*
|
|
4529
|
+
* agent.vue keeps a forked mapper and a forked first-page merge (its mount path
|
|
4530
|
+
* runs them, while resumePolling routes through loadHistory below), so both
|
|
4531
|
+
* paths are live for the SAME row inside one component. Adoption is part of the
|
|
4532
|
+
* merge contract, not an optional extra: without it that fork erases a streamed
|
|
4533
|
+
* answer off the screen on every turn, which is the whole of MAJOR 3.
|
|
4534
|
+
*
|
|
4535
|
+
* Exposed rather than reimplemented because the rule needs the session's own
|
|
4536
|
+
* `incomplete` set, which the pure helper (history.ts adoptLocalAnswerIntoPage)
|
|
4537
|
+
* cannot see. A client that reached for the helper alone would adopt a TRUNCATED
|
|
4538
|
+
* answer over the row and clear the marker that would have gone back for the
|
|
4539
|
+
* rest - a fork that reads as correct and loses text.
|
|
4540
|
+
*
|
|
4541
|
+
* Call it exactly where loadHistory does: on the freshly mapped page, after
|
|
4542
|
+
* applyHydratedBodies and BEFORE the page replaces or merges into state.messages.
|
|
4543
|
+
*/
|
|
4544
|
+
adoptLocalAnswers(mapped, loadKey) {
|
|
4545
|
+
this._adoptLocalAnswers(mapped, loadKey);
|
|
4546
|
+
}
|
|
4547
|
+
/**
|
|
4548
|
+
* Queue the on-screen turns whose answer is only in the chunk store, newest
|
|
4549
|
+
* first, and start draining. Never blocks and never throws.
|
|
4550
|
+
*
|
|
4551
|
+
* `ownerKey` is the chat the queue entries belong to, snapshotted by the caller:
|
|
4552
|
+
* a recovery that lands after the user has moved on writes into that chat's
|
|
4553
|
+
* cache, never into whatever list is on screen by then.
|
|
4554
|
+
*/
|
|
4555
|
+
_scheduleStreamRecovery(ownerKey, platform, projectId, owner) {
|
|
4556
|
+
if (!streamRecoveryEnabled()) return;
|
|
4557
|
+
var rec = this._rec();
|
|
4558
|
+
var wanted = [];
|
|
4559
|
+
for (var i = this.state.messages.length - 1; i >= 0; i--) {
|
|
4560
|
+
var m = this.state.messages[i];
|
|
4561
|
+
if (!m || m.role !== "assistant" || !m._streamPending || !m._serverItemId) continue;
|
|
4562
|
+
var id = m._serverItemId;
|
|
4563
|
+
if (rec.attempted[id]) continue;
|
|
4564
|
+
if (this.liveStreams[id]) continue;
|
|
4565
|
+
if (rec.queue.some(function(e) {
|
|
4566
|
+
return e.id === id;
|
|
4567
|
+
})) continue;
|
|
4568
|
+
wanted.push(id);
|
|
4569
|
+
if (wanted.length >= STREAM_RECOVERY_PER_LOAD) break;
|
|
4570
|
+
}
|
|
4571
|
+
if (!wanted.length) return;
|
|
4572
|
+
for (var w = 0; w < wanted.length; w++) {
|
|
4573
|
+
rec.queue.push({ id: wanted[w], ownerKey, platform, projectId, owner });
|
|
4574
|
+
this._markRecoveryPhase(wanted[w], "active");
|
|
4575
|
+
}
|
|
4576
|
+
this._drainStreamRecovery();
|
|
4577
|
+
}
|
|
4578
|
+
/**
|
|
4579
|
+
* PUBLIC DELEGATE, the other half of what a forked history path needs.
|
|
4580
|
+
*
|
|
4581
|
+
* Same reason as adoptLocalAnswers: agent.vue's mount path never calls
|
|
4582
|
+
* loadHistory, so without this its pages would MARK unfinalized streamed turns
|
|
4583
|
+
* and then never read them back - CRITICAL 1 left unfixed on the client's
|
|
4584
|
+
* primary path, with the marker making it look handled.
|
|
4585
|
+
*
|
|
4586
|
+
* Takes the load's SNAPSHOTTED identity rather than reading it live, and that is
|
|
4587
|
+
* the reason this exists instead of the caller looping over recoverStreamedAnswer:
|
|
4588
|
+
* that one reads getIdentity() at call time (right, for an on-demand affordance
|
|
4589
|
+
* the user just clicked), which after a project switch racing the load would
|
|
4590
|
+
* finalize the turn against the project they switched TO. Call it AFTER the page
|
|
4591
|
+
* is rendered and the loading flags are cleared - it must never hold up the
|
|
4592
|
+
* conversation it belongs to.
|
|
4593
|
+
*/
|
|
4594
|
+
scheduleStreamRecovery(ownerKey, platform, projectId, owner) {
|
|
4595
|
+
this._scheduleStreamRecovery(ownerKey, platform, projectId, owner);
|
|
4596
|
+
}
|
|
4597
|
+
/** Serial drain of the recovery queue. Each entry is one full chunk read. */
|
|
4598
|
+
_drainStreamRecovery() {
|
|
4599
|
+
var rec = this._rec();
|
|
4600
|
+
if (rec.running) return;
|
|
4601
|
+
var next = rec.queue.shift();
|
|
4602
|
+
if (!next) return;
|
|
4603
|
+
rec.running = true;
|
|
4604
|
+
var self = this;
|
|
4605
|
+
this._readBackStreamedTurn(next.id, next.ownerKey, next.platform, next.projectId, next.owner).catch(function() {
|
|
4606
|
+
}).then(function() {
|
|
4607
|
+
self._rec().running = false;
|
|
4608
|
+
self._drainStreamRecovery();
|
|
4609
|
+
});
|
|
4610
|
+
}
|
|
4611
|
+
/**
|
|
4612
|
+
* Read one unfinalized streamed turn back out of the chunk store and put its
|
|
4613
|
+
* answer where the turn's answer belongs.
|
|
4614
|
+
*
|
|
4615
|
+
* Public because the cap above is deliberately small: a host that wants to offer
|
|
4616
|
+
* "load the rest" on an older recoverable turn calls this with its
|
|
4617
|
+
* `_serverItemId`, and gets the same path the automatic recovery uses. Safe to
|
|
4618
|
+
* call for an id that turns out not to be recoverable, and safe to call twice -
|
|
4619
|
+
* a second call while the first is still in flight is a no-op.
|
|
4620
|
+
*
|
|
4621
|
+
* THIS IS THE USER ASKING, and that is why it passes `manual`. The automatic
|
|
4622
|
+
* recovery refuses a row it has already tried, so that a re-render, or the
|
|
4623
|
+
* history load that every visibilitychange fires, cannot loop on the same
|
|
4624
|
+
* chunks. A click is neither of those: it is one bounded request that a person
|
|
4625
|
+
* asked for, and applying the loop guard to it made the affordance a button that
|
|
4626
|
+
* silently did nothing for exactly the rows most likely to have it - every row
|
|
4627
|
+
* an earlier read touched and could not settle.
|
|
4628
|
+
*/
|
|
4629
|
+
recoverStreamedAnswer(itemId) {
|
|
4630
|
+
if (!itemId) return Promise.resolve();
|
|
4631
|
+
var id = this.host.getIdentity();
|
|
4632
|
+
var platform = id && id.platform === "openai" ? "openai" : "claude";
|
|
4633
|
+
return this._readBackStreamedTurn(itemId, this.getHistoryCacheKey(), platform, id ? id.projectId : "", id ? id.owner : "", true);
|
|
4634
|
+
}
|
|
4635
|
+
_readBackStreamedTurn(itemId, ownerKey, platform, projectId, owner, manual) {
|
|
4636
|
+
var cfg = chatEngineConfig();
|
|
4637
|
+
var read = cfg.clientSecretRequestStream;
|
|
4638
|
+
if (!read || !itemId) return Promise.resolve();
|
|
4639
|
+
if (this._rec().inflight[itemId]) return Promise.resolve();
|
|
4640
|
+
if (!manual && this._rec().attempted[itemId]) {
|
|
4641
|
+
this._markRecoveryPhase(itemId, null);
|
|
4642
|
+
return Promise.resolve();
|
|
4643
|
+
}
|
|
4644
|
+
this._rec().attempted[itemId] = true;
|
|
4645
|
+
this._rec().inflight[itemId] = true;
|
|
4646
|
+
delete this._rec().failed[itemId];
|
|
4647
|
+
this._markRecoveryPhase(itemId, "active");
|
|
4648
|
+
var self = this;
|
|
4649
|
+
var url = platform === "openai" ? OPENAI_RESPONSES_API_URL : ANTHROPIC_MESSAGES_API_URL;
|
|
4650
|
+
var parser = createSseParser();
|
|
4651
|
+
var fed = false;
|
|
4652
|
+
return Promise.resolve(read(itemId, {
|
|
4653
|
+
url,
|
|
4654
|
+
method: "POST",
|
|
4655
|
+
service: projectId,
|
|
4656
|
+
owner,
|
|
4657
|
+
since: 0,
|
|
4658
|
+
onStream: function(chunk) {
|
|
4659
|
+
if (typeof chunk !== "string" || !chunk) return;
|
|
4660
|
+
fed = true;
|
|
4661
|
+
parser.feed(chunk);
|
|
4662
|
+
}
|
|
4663
|
+
})).then(function(res) {
|
|
4664
|
+
if (isPollStopped(res)) {
|
|
4665
|
+
delete self._rec().attempted[itemId];
|
|
4666
|
+
delete self._rec().inflight[itemId];
|
|
4667
|
+
self._markRecoveryPhase(itemId, null);
|
|
4668
|
+
return;
|
|
4669
|
+
}
|
|
4670
|
+
var envelope = isCsrStatusEnvelope(res);
|
|
4671
|
+
var body = null;
|
|
4672
|
+
if (res && !envelope) {
|
|
4673
|
+
body = res;
|
|
4674
|
+
} else if (fed) {
|
|
4675
|
+
parser.end();
|
|
4676
|
+
body = parser.finalBody();
|
|
4677
|
+
}
|
|
4678
|
+
var snap = parser.snapshot();
|
|
4679
|
+
var fromRow = !!(res && !envelope);
|
|
4680
|
+
var rowStatus = envelope && typeof res.status === "string" ? res.status : void 0;
|
|
4681
|
+
var degraded = !!(envelope && res && res.more === true);
|
|
4682
|
+
var store = !fromRow && !degraded && body != null && mayKeepStreamedAnswer(snap, rowStatus);
|
|
4683
|
+
delete self._rec().inflight[itemId];
|
|
4684
|
+
delete self._rec().failed[itemId];
|
|
4685
|
+
self._markRecoveryPhase(itemId, null);
|
|
4686
|
+
if (degraded) {
|
|
4687
|
+
delete self._rec().attempted[itemId];
|
|
4688
|
+
}
|
|
4689
|
+
self._applyRecoveredAnswer(itemId, ownerKey, platform, projectId, owner, body, store, degraded);
|
|
4690
|
+
}, function(err) {
|
|
4691
|
+
console.warn("[chat-engine] could not read back a streamed turn", itemId, err);
|
|
4692
|
+
delete self._rec().attempted[itemId];
|
|
4693
|
+
delete self._rec().inflight[itemId];
|
|
4694
|
+
self._rec().failed[itemId] = true;
|
|
4695
|
+
self._markRecoveryPhase(itemId, "failed");
|
|
4696
|
+
});
|
|
4697
|
+
}
|
|
4698
|
+
/**
|
|
4699
|
+
* Write a recovered answer into the turn's bubble (or into the owning chat's
|
|
4700
|
+
* cache when the reader has moved on), then store it as the version history
|
|
4701
|
+
* keeps.
|
|
4702
|
+
*
|
|
4703
|
+
* FINALIZING IS WHAT MAKES THIS RUN ONCE. It copies the answer onto the row and
|
|
4704
|
+
* releases the chunks, so the next load reads an ordinary turn and no recovery is
|
|
4705
|
+
* scheduled for it ever again, by anyone, in any tab. `store` is the caller's
|
|
4706
|
+
* decision and carries two gates at once: mayKeepStreamedAnswer, the SAME keep
|
|
4707
|
+
* policy the live settle applies (an incomplete, errored or failed read is shown
|
|
4708
|
+
* but never stored, because storing it would make the truncation permanent and
|
|
4709
|
+
* delete the part that was missing), and whether the body is new at all (one
|
|
4710
|
+
* that came off the row is already stored).
|
|
4711
|
+
*/
|
|
4712
|
+
_applyRecoveredAnswer(itemId, ownerKey, platform, projectId, owner, body, store, degraded) {
|
|
4713
|
+
var text = "";
|
|
4714
|
+
var isErr = isErrorResponseBody(body);
|
|
4715
|
+
if (body != null && !isErr) {
|
|
4716
|
+
text = ((platform === "openai" ? extractOpenAIText(body) : extractClaudeText(body)) || "").trim();
|
|
4717
|
+
}
|
|
4718
|
+
if (!text && !isErr) {
|
|
4719
|
+
if (degraded) {
|
|
4720
|
+
return;
|
|
4721
|
+
}
|
|
4722
|
+
this._clearStreamPendingMark(itemId, ownerKey, true);
|
|
4723
|
+
return;
|
|
4724
|
+
}
|
|
4725
|
+
var reply = isErr ? { role: "assistant", content: getErrorMessage(body), isError: true, _serverItemId: itemId } : { role: "assistant", content: text, _serverItemId: itemId };
|
|
4726
|
+
if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
|
|
4727
|
+
this._applyReplyToCache(ownerKey, reply, itemId);
|
|
4728
|
+
} else {
|
|
4729
|
+
var idx = -1;
|
|
4730
|
+
for (var i = 0; i < this.state.messages.length; i++) {
|
|
4731
|
+
var m = this.state.messages[i];
|
|
4732
|
+
if (m && m.role === "assistant" && m._serverItemId === itemId) {
|
|
4733
|
+
idx = i;
|
|
4734
|
+
break;
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
if (idx === -1) {
|
|
4738
|
+
this._applyReplyToCache(ownerKey, reply, itemId);
|
|
4739
|
+
} else {
|
|
4740
|
+
var prev = this.state.messages[idx];
|
|
4741
|
+
if (prev._ts !== void 0) reply._ts = prev._ts;
|
|
4742
|
+
if (prev._ownerKey !== void 0) reply._ownerKey = prev._ownerKey;
|
|
4743
|
+
this.state.messages[idx] = reply;
|
|
4744
|
+
this.updateHistoryCache();
|
|
4745
|
+
this.host.notify();
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
if (!degraded) delete this._rec().incomplete[itemId];
|
|
4749
|
+
if (!store || body == null || isErr) return;
|
|
4750
|
+
var fin = chatEngineConfig().clientSecretRequestFinalize;
|
|
4751
|
+
if (!fin) return;
|
|
4752
|
+
var url = platform === "openai" ? OPENAI_RESPONSES_API_URL : ANTHROPIC_MESSAGES_API_URL;
|
|
4753
|
+
try {
|
|
4754
|
+
Promise.resolve(fin(itemId, body, { url, method: "POST", service: projectId, owner })).catch(function(err) {
|
|
4755
|
+
console.warn("[chat-engine] finalize of a recovered turn failed", err);
|
|
4756
|
+
});
|
|
4757
|
+
} catch (e) {
|
|
4758
|
+
console.warn("[chat-engine] finalize of a recovered turn threw", e);
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
/**
|
|
4762
|
+
* Take the "answer is elsewhere" marker off a turn once it is settled one way or
|
|
4763
|
+
* the other. `drop` removes an assistant bubble that turned out to have no answer
|
|
4764
|
+
* at all, which restores exactly the list the mapper used to produce for such a
|
|
4765
|
+
* row (none), rather than leaving a permanently empty bubble behind.
|
|
4766
|
+
*
|
|
4767
|
+
* ONLY EVER CALLED FOR A TURN THAT WAS ACTUALLY READ. The marker is the one thing
|
|
4768
|
+
* that keeps an unrecovered answer reachable, so it comes off only on the strength
|
|
4769
|
+
* of an answer (the recovery wrote one) or of a read that came back empty. A read
|
|
4770
|
+
* that FAILED, or one that was STOPPED, knows neither, and taking the marker off
|
|
4771
|
+
* on either of those is how a bubble ends up empty forever with its answer still
|
|
4772
|
+
* in the chunk table. `drop` is likewise never passed for a bubble that HAS
|
|
4773
|
+
* content: an empty row is an empty turn, a failed read is not.
|
|
4774
|
+
*/
|
|
4775
|
+
_clearStreamPendingMark(itemId, ownerKey, drop) {
|
|
4776
|
+
if (ownerKey && this.getHistoryCacheKey() !== ownerKey) return;
|
|
4777
|
+
var changed = false;
|
|
4778
|
+
for (var i = this.state.messages.length - 1; i >= 0; i--) {
|
|
4779
|
+
var m = this.state.messages[i];
|
|
4780
|
+
if (!m || m.role !== "assistant" || m._serverItemId !== itemId) continue;
|
|
4781
|
+
if (!m._streamPending) continue;
|
|
4782
|
+
if (drop && !m.content) {
|
|
4783
|
+
this.state.messages.splice(i, 1);
|
|
4784
|
+
changed = true;
|
|
4785
|
+
continue;
|
|
4786
|
+
}
|
|
4787
|
+
m._streamPending = false;
|
|
4788
|
+
changed = true;
|
|
4789
|
+
}
|
|
4790
|
+
if (changed) {
|
|
4791
|
+
this.updateHistoryCache();
|
|
4792
|
+
this.host.notify();
|
|
4793
|
+
}
|
|
4794
|
+
}
|
|
3344
4795
|
/**
|
|
3345
4796
|
* Stop and forget one item's poll. Used after a cancel: the row is either gone
|
|
3346
4797
|
* (cancelled while queued) or flagged cancelled (cancelled while running), so
|
|
@@ -3655,7 +5106,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3655
5106
|
dispatchItemId = initial.id;
|
|
3656
5107
|
if (typeof params.onItemId === "function") params.onItemId(initial.id);
|
|
3657
5108
|
}
|
|
3658
|
-
var dp = self._fgPollWithEarlyProbe(initial, initial.id
|
|
5109
|
+
var dp = self._fgPollWithEarlyProbe(initial, initial.id, void 0, {
|
|
5110
|
+
platform: params.aiPlatform,
|
|
5111
|
+
projectId: params.projectId,
|
|
5112
|
+
owner: params.owner,
|
|
5113
|
+
ownerKey: params.key
|
|
5114
|
+
});
|
|
3659
5115
|
if (initial.id) self._trackPoll(initial.id, "fg", dp);
|
|
3660
5116
|
return dp;
|
|
3661
5117
|
}
|
|
@@ -3868,8 +5324,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3868
5324
|
Promise.resolve(probeBgQueue(
|
|
3869
5325
|
{ service: svcId, owner, platform, queue, status, limit: WORKER_PASS_ADOPT_LIMIT },
|
|
3870
5326
|
{ maxAgeMs: 0 }
|
|
3871
|
-
)).then(function(
|
|
3872
|
-
settle(
|
|
5327
|
+
)).then(function(entry2) {
|
|
5328
|
+
settle(entry2.result);
|
|
3873
5329
|
}, function() {
|
|
3874
5330
|
settle(null);
|
|
3875
5331
|
});
|
|
@@ -4087,7 +5543,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4087
5543
|
}
|
|
4088
5544
|
if (serverId) self._stampTurnWithItemId(capturedKey, capturedQueuedLid, void 0, serverId);
|
|
4089
5545
|
if (result && result.poll && (result.status === "pending" || result.status === "running")) {
|
|
4090
|
-
var qp = self._fgPollWithEarlyProbe(result, serverId
|
|
5546
|
+
var qp = self._fgPollWithEarlyProbe(result, serverId, void 0, {
|
|
5547
|
+
platform: capturedPlatform,
|
|
5548
|
+
projectId: id.projectId,
|
|
5549
|
+
owner: id.owner,
|
|
5550
|
+
ownerKey: capturedKey
|
|
5551
|
+
});
|
|
4091
5552
|
if (serverId) self._trackPoll(serverId, "fg", qp);
|
|
4092
5553
|
return qp.then(function(res) {
|
|
4093
5554
|
if (isPollStopped(res)) return;
|
|
@@ -4358,9 +5819,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4358
5819
|
answer = (answer || "").trim() || "No text response received from AI provider.";
|
|
4359
5820
|
var lid = this._newLocalId();
|
|
4360
5821
|
if (targetIdx >= 0 && this.state.messages[targetIdx] && this.state.messages[targetIdx].isPending) {
|
|
4361
|
-
this.
|
|
5822
|
+
var qPainted = this._paintedTextAt(targetIdx);
|
|
5823
|
+
var prevQ = this.state.messages[targetIdx] || {};
|
|
5824
|
+
var qSettled = { role: "assistant", content: qPainted, _localId: lid };
|
|
5825
|
+
if (prevQ._serverItemId) qSettled._serverItemId = prevQ._serverItemId;
|
|
5826
|
+
if (prevQ._ownerKey) qSettled._ownerKey = prevQ._ownerKey;
|
|
5827
|
+
this.state.messages[targetIdx] = qSettled;
|
|
4362
5828
|
this.host.notify();
|
|
4363
|
-
this.enqueueTypewrite(targetIdx, answer, lid);
|
|
5829
|
+
this.enqueueTypewrite(targetIdx, answer, lid, qPainted);
|
|
4364
5830
|
} else if (targetIdx >= 0) {
|
|
4365
5831
|
this.state.messages.splice(targetIdx, 0, { role: "assistant", content: "", _localId: lid });
|
|
4366
5832
|
this.host.notify();
|
|
@@ -4622,7 +6088,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4622
6088
|
// renders self-throttles to what the machine can actually paint.
|
|
4623
6089
|
// * rAF paces us to the browser's paint cycle and pauses in background
|
|
4624
6090
|
// tabs, so we never queue work faster than it can be drawn.
|
|
4625
|
-
|
|
6091
|
+
//
|
|
6092
|
+
// `paintedText` is what a LIVE STREAM already put in this bubble. The reveal
|
|
6093
|
+
// starts from the point the two texts stop agreeing rather than from zero: the
|
|
6094
|
+
// authoritative answer still replaces the live one character for character (it is
|
|
6095
|
+
// the only source of truth, and this method writes fullText and nothing else), but
|
|
6096
|
+
// retyping a paragraph the reader has just watched arrive is the one thing that
|
|
6097
|
+
// would make a streamed turn look worse than an unstreamed one.
|
|
6098
|
+
typewriteIntoIndex(idx, fullText, localId, paintedText) {
|
|
4626
6099
|
var self = this;
|
|
4627
6100
|
if (!fullText) return Promise.resolve();
|
|
4628
6101
|
var CHARS_PER_SEC = 300;
|
|
@@ -4638,7 +6111,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4638
6111
|
});
|
|
4639
6112
|
this.state.typing = true;
|
|
4640
6113
|
this.state.typingAbort = false;
|
|
4641
|
-
var i = 0;
|
|
6114
|
+
var i = paintedText ? typewriterResumeIndex(paintedText, fullText, regions) : 0;
|
|
4642
6115
|
var last = nowMs();
|
|
4643
6116
|
return new Promise(function(resolve) {
|
|
4644
6117
|
var done = false;
|
|
@@ -4653,16 +6126,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4653
6126
|
if (done) return;
|
|
4654
6127
|
done = true;
|
|
4655
6128
|
cleanup();
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
self.host.refreshMessageBubble(fi);
|
|
4665
|
-
}
|
|
6129
|
+
var fi = localId ? self.state.messages.findIndex(function(mm) {
|
|
6130
|
+
return mm._localId === localId;
|
|
6131
|
+
}) : idx;
|
|
6132
|
+
if (fi !== -1) {
|
|
6133
|
+
var t = self.state.messages[fi];
|
|
6134
|
+
if (t) {
|
|
6135
|
+
t.content = fullText;
|
|
6136
|
+
self.host.refreshMessageBubble(fi);
|
|
4666
6137
|
}
|
|
4667
6138
|
}
|
|
4668
6139
|
self.state.typing = false;
|
|
@@ -4721,12 +6192,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4721
6192
|
nextFrame(frame);
|
|
4722
6193
|
});
|
|
4723
6194
|
}
|
|
4724
|
-
enqueueTypewrite(idx, fullText, localId) {
|
|
6195
|
+
enqueueTypewrite(idx, fullText, localId, paintedText) {
|
|
4725
6196
|
var self = this;
|
|
4726
6197
|
var target = this.state.messages[idx];
|
|
4727
6198
|
if (target && target._ts === void 0) target._ts = wallClockNow();
|
|
6199
|
+
if (!this.typewriterQueue) this.typewriterQueue = Promise.resolve();
|
|
4728
6200
|
this.typewriterQueue = this.typewriterQueue.then(function() {
|
|
4729
|
-
return self.typewriteIntoIndex(idx, fullText, localId);
|
|
6201
|
+
return self.typewriteIntoIndex(idx, fullText, localId, paintedText);
|
|
4730
6202
|
});
|
|
4731
6203
|
return this.typewriterQueue;
|
|
4732
6204
|
}
|
|
@@ -4759,12 +6231,17 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4759
6231
|
this.promoteNextQueuedToRunning();
|
|
4760
6232
|
return Promise.resolve();
|
|
4761
6233
|
}
|
|
6234
|
+
var painted = this._paintedTextAt(pendingIdx);
|
|
4762
6235
|
var lid = this._newLocalId();
|
|
4763
|
-
this.state.messages[pendingIdx]
|
|
6236
|
+
var prevSettled = this.state.messages[pendingIdx] || {};
|
|
6237
|
+
var settled = { role: "assistant", content: painted, isPending: false, _localId: lid };
|
|
6238
|
+
if (prevSettled._serverItemId) settled._serverItemId = prevSettled._serverItemId;
|
|
6239
|
+
if (prevSettled._ownerKey) settled._ownerKey = prevSettled._ownerKey;
|
|
6240
|
+
this.state.messages[pendingIdx] = settled;
|
|
4764
6241
|
this._removeStrayPendingAssistants();
|
|
4765
6242
|
this.host.notify();
|
|
4766
6243
|
this.promoteNextQueuedToRunning();
|
|
4767
|
-
return this.enqueueTypewrite(pendingIdx, latest.content, lid);
|
|
6244
|
+
return this.enqueueTypewrite(pendingIdx, latest.content, lid, painted);
|
|
4768
6245
|
}
|
|
4769
6246
|
// Remove leftover non-background pending ("Thinking…") assistant bubbles: the
|
|
4770
6247
|
// duplicate that appears when a concurrent history refetch re-maps the still-
|
|
@@ -4794,6 +6271,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4794
6271
|
for (var k = this.state.messages.length - 1; k >= 0; k--) {
|
|
4795
6272
|
var m = this.state.messages[k];
|
|
4796
6273
|
if (!m || !m.isPending || m.role !== "assistant" || m.isBackgroundTask) continue;
|
|
6274
|
+
if (m._streaming) continue;
|
|
4797
6275
|
if (this._isLiveImmediatePlaceholder(k)) continue;
|
|
4798
6276
|
this.state.messages.splice(k, 1);
|
|
4799
6277
|
}
|
|
@@ -4971,10 +6449,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4971
6449
|
this.updateHistoryCache();
|
|
4972
6450
|
return;
|
|
4973
6451
|
}
|
|
6452
|
+
var hPainted = this._paintedTextAt(idx);
|
|
4974
6453
|
var lid = this._newLocalId();
|
|
4975
|
-
this.state.messages[idx] = { role: "assistant", content:
|
|
6454
|
+
this.state.messages[idx] = { role: "assistant", content: hPainted, _localId: lid, _serverItemId: itemId };
|
|
4976
6455
|
this.host.notify();
|
|
4977
|
-
this.enqueueTypewrite(idx, text, lid);
|
|
6456
|
+
this.enqueueTypewrite(idx, text, lid, hPainted);
|
|
4978
6457
|
this.updateHistoryCache();
|
|
4979
6458
|
return;
|
|
4980
6459
|
}
|
|
@@ -5016,11 +6495,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5016
6495
|
* path is project-relative ("report.xlsx"), and ONE ChatSession serves every
|
|
5017
6496
|
* project — unscoped, stopping a file in one project would silently suppress
|
|
5018
6497
|
* the same filename's continuations in another. */
|
|
5019
|
-
_indexKeyOf(
|
|
5020
|
-
if (!
|
|
5021
|
-
var file =
|
|
6498
|
+
_indexKeyOf(entry2) {
|
|
6499
|
+
if (!entry2) return "";
|
|
6500
|
+
var file = entry2.storagePath || entry2.filename;
|
|
5022
6501
|
if (!file) return "";
|
|
5023
|
-
return indexScopeKey(
|
|
6502
|
+
return indexScopeKey(entry2.projectId, entry2.platform) + "|" + file;
|
|
5024
6503
|
}
|
|
5025
6504
|
/**
|
|
5026
6505
|
* Reconcile the bg queue with the files the user has stopped.
|
|
@@ -5049,17 +6528,17 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5049
6528
|
if (m.isPending || m.isPendingQueued || m.isPendingInProcess) surfaced[m._serverItemId] = true;
|
|
5050
6529
|
});
|
|
5051
6530
|
for (var i = this.bgTaskQueue.length - 1; i >= 0; i--) {
|
|
5052
|
-
var
|
|
5053
|
-
var key = this._indexKeyOf(
|
|
6531
|
+
var entry2 = this.bgTaskQueue[i];
|
|
6532
|
+
var key = this._indexKeyOf(entry2);
|
|
5054
6533
|
if (!key || !this.cancelledIndexKeys.has(key)) continue;
|
|
5055
|
-
if (!
|
|
6534
|
+
if (!entry2.resumePass && !this.state.stoppedIndexIds[entry2.id]) {
|
|
5056
6535
|
this.cancelledIndexKeys.delete(key);
|
|
5057
6536
|
continue;
|
|
5058
6537
|
}
|
|
5059
|
-
if (surfaced[
|
|
6538
|
+
if (surfaced[entry2.id]) continue;
|
|
5060
6539
|
this.bgTaskQueue.splice(i, 1);
|
|
5061
|
-
this._stopPoll(
|
|
5062
|
-
this._cancelServerItem(
|
|
6540
|
+
this._stopPoll(entry2.id);
|
|
6541
|
+
this._cancelServerItem(entry2.id);
|
|
5063
6542
|
}
|
|
5064
6543
|
}
|
|
5065
6544
|
/**
|
|
@@ -5117,8 +6596,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5117
6596
|
return Promise.resolve(probeBgQueue(
|
|
5118
6597
|
{ service: svcId, owner, platform, queue, status, limit: WORKER_PASS_ADOPT_LIMIT },
|
|
5119
6598
|
{ maxAgeMs: 0 }
|
|
5120
|
-
)).then(function(
|
|
5121
|
-
return
|
|
6599
|
+
)).then(function(entry2) {
|
|
6600
|
+
return entry2.result;
|
|
5122
6601
|
}).catch(function() {
|
|
5123
6602
|
return null;
|
|
5124
6603
|
});
|
|
@@ -5271,30 +6750,30 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5271
6750
|
}
|
|
5272
6751
|
var bgPollBudget = MAX_CONCURRENT_BG_POLLS - this._countBgPolls();
|
|
5273
6752
|
var injectedAny = false;
|
|
5274
|
-
this.bgTaskQueue.forEach(function(
|
|
5275
|
-
if (
|
|
5276
|
-
if (!presentIds[
|
|
5277
|
-
var isRunning =
|
|
6753
|
+
this.bgTaskQueue.forEach(function(entry2) {
|
|
6754
|
+
if (entry2.projectId !== svcId || entry2.platform !== plat) return;
|
|
6755
|
+
if (!presentIds[entry2.id]) {
|
|
6756
|
+
var isRunning = entry2.status === "running";
|
|
5278
6757
|
var userBubble = {
|
|
5279
6758
|
role: "user",
|
|
5280
|
-
content: self.host.formatIndexingLabel(
|
|
6759
|
+
content: self.host.formatIndexingLabel(entry2.filename, entry2.mime, entry2.size, entry2.storagePath, entry2.isReindex, !!entry2.resumePass),
|
|
5281
6760
|
isBackgroundTask: true,
|
|
5282
|
-
_serverItemId:
|
|
6761
|
+
_serverItemId: entry2.id,
|
|
5283
6762
|
// Structured ref so this live pass groups with the same file's passes
|
|
5284
6763
|
// rebuilt from history (see indexing_groups.buildChatDisplayList).
|
|
5285
6764
|
_indexFile: {
|
|
5286
|
-
name:
|
|
5287
|
-
path:
|
|
5288
|
-
mime:
|
|
5289
|
-
size:
|
|
5290
|
-
isReindex: !!
|
|
5291
|
-
continued: !!
|
|
6765
|
+
name: entry2.filename,
|
|
6766
|
+
path: entry2.storagePath,
|
|
6767
|
+
mime: entry2.mime,
|
|
6768
|
+
size: entry2.size,
|
|
6769
|
+
isReindex: !!entry2.isReindex,
|
|
6770
|
+
continued: !!entry2.resumePass
|
|
5292
6771
|
}
|
|
5293
6772
|
};
|
|
5294
6773
|
if (isRunning) userBubble.isPendingInProcess = true;
|
|
5295
6774
|
else userBubble.isPendingQueued = true;
|
|
5296
|
-
var stageAt = self._stageIndex(self.state.messages,
|
|
5297
|
-
var runningBubble = isRunning ? { role: "assistant", content: "", isPending: true, isPendingInProcess: true, isBackgroundTask: true, _serverItemId:
|
|
6775
|
+
var stageAt = self._stageIndex(self.state.messages, entry2.stageId);
|
|
6776
|
+
var runningBubble = isRunning ? { role: "assistant", content: "", isPending: true, isPendingInProcess: true, isBackgroundTask: true, _serverItemId: entry2.id } : null;
|
|
5298
6777
|
if (stageAt === -1) {
|
|
5299
6778
|
self.state.messages.push(userBubble);
|
|
5300
6779
|
if (runningBubble) self.state.messages.push(runningBubble);
|
|
@@ -5303,16 +6782,16 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5303
6782
|
} else {
|
|
5304
6783
|
self.state.messages.splice(stageAt, 0, userBubble);
|
|
5305
6784
|
}
|
|
5306
|
-
presentIds[
|
|
6785
|
+
presentIds[entry2.id] = true;
|
|
5307
6786
|
injectedAny = true;
|
|
5308
6787
|
}
|
|
5309
|
-
if (bgPollBudget > 0 && !self.isPollingPaused() && !self.historyItemPolls.has(
|
|
6788
|
+
if (bgPollBudget > 0 && !self.isPollingPaused() && !self.historyItemPolls.has(entry2.id) && typeof entry2.poll === "function") {
|
|
5310
6789
|
bgPollBudget--;
|
|
5311
|
-
var capturedId =
|
|
5312
|
-
var capturedEntry =
|
|
6790
|
+
var capturedId = entry2.id, capturedPlat = plat;
|
|
6791
|
+
var capturedEntry = entry2;
|
|
5313
6792
|
var wasStopped = false;
|
|
5314
|
-
var bp =
|
|
5315
|
-
self._trackPoll(
|
|
6793
|
+
var bp = entry2.poll({ latency: POLL_INTERVAL });
|
|
6794
|
+
self._trackPoll(entry2.id, "bg", bp);
|
|
5316
6795
|
bp.then(function(response) {
|
|
5317
6796
|
if (isPollStopped(response)) {
|
|
5318
6797
|
wasStopped = true;
|
|
@@ -5378,13 +6857,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5378
6857
|
* client knows DETERMINISTICALLY (see the two call sites in
|
|
5379
6858
|
* maybeResumeIndexing). Best-effort by contract; identity-checked so a
|
|
5380
6859
|
* project switch mid-settle cannot stamp the wrong service. */
|
|
5381
|
-
_mintDoneMarker(
|
|
6860
|
+
_mintDoneMarker(entry2) {
|
|
5382
6861
|
try {
|
|
5383
6862
|
var mint = chatEngineConfig().mintIndexDoneMarker;
|
|
5384
|
-
if (!mint || !
|
|
6863
|
+
if (!mint || !entry2 || !entry2.storagePath || !entry2.projectId) return;
|
|
5385
6864
|
var id = this.host.getIdentity();
|
|
5386
|
-
if (!id || id.projectId !==
|
|
5387
|
-
mint({ service:
|
|
6865
|
+
if (!id || id.projectId !== entry2.projectId) return;
|
|
6866
|
+
mint({ service: entry2.projectId, storagePath: entry2.storagePath });
|
|
5388
6867
|
} catch (_e) {
|
|
5389
6868
|
}
|
|
5390
6869
|
}
|
|
@@ -5405,27 +6884,27 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5405
6884
|
* maybeResumeIndexing's single-pass branch); paged files stay with their
|
|
5406
6885
|
* drivers. Outcome is read from the settled bubbles' own flags, which is
|
|
5407
6886
|
* all the history mapping left us. Best-effort and idempotent throughout. */
|
|
5408
|
-
_flipRunFromSettledEntry(
|
|
6887
|
+
_flipRunFromSettledEntry(entry2) {
|
|
5409
6888
|
try {
|
|
5410
|
-
if (!
|
|
5411
|
-
if (isPagedReadFile(
|
|
5412
|
-
if (this.cancelledIndexKeys.has(this._indexKeyOf(
|
|
5413
|
-
if (this.state.stoppedIndexIds[
|
|
6889
|
+
if (!entry2 || !entry2.storagePath || !entry2.id || !entry2.projectId) return;
|
|
6890
|
+
if (isPagedReadFile(entry2.filename, entry2.mime)) return;
|
|
6891
|
+
if (this.cancelledIndexKeys.has(this._indexKeyOf(entry2))) return;
|
|
6892
|
+
if (this.state.stoppedIndexIds[entry2.id]) return;
|
|
5414
6893
|
var userMsg = null, replyMsg = null;
|
|
5415
6894
|
this.state.messages.forEach(function(m) {
|
|
5416
|
-
if (m._serverItemId !==
|
|
6895
|
+
if (m._serverItemId !== entry2.id) return;
|
|
5417
6896
|
if (m.role === "user") {
|
|
5418
6897
|
if (!userMsg) userMsg = m;
|
|
5419
6898
|
} else if (!replyMsg) replyMsg = m;
|
|
5420
6899
|
});
|
|
5421
6900
|
if (userMsg && userMsg.isCancelled || replyMsg && replyMsg.isCancelled) {
|
|
5422
|
-
this._flipRunRecord(
|
|
6901
|
+
this._flipRunRecord(entry2, "cancelled");
|
|
5423
6902
|
} else if (replyMsg && replyMsg.isError) {
|
|
5424
6903
|
var errText = typeof replyMsg.content === "string" ? replyMsg.content.replace(/\s+/g, " ").trim().slice(0, 300) : "";
|
|
5425
|
-
this._flipRunRecord(
|
|
6904
|
+
this._flipRunRecord(entry2, "error", errText || "Indexing failed.");
|
|
5426
6905
|
} else if (replyMsg) {
|
|
5427
|
-
this._mintDoneMarker(
|
|
5428
|
-
this._flipRunRecord(
|
|
6906
|
+
this._mintDoneMarker(entry2);
|
|
6907
|
+
this._flipRunRecord(entry2, "done");
|
|
5429
6908
|
}
|
|
5430
6909
|
} catch (_e) {
|
|
5431
6910
|
}
|
|
@@ -5436,55 +6915,55 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5436
6915
|
* mid-settle — otherwise the record lies 'working' forever. Best-effort
|
|
5437
6916
|
* through upsertIndexRunRecordSafe; the consumer's precedence guard keeps
|
|
5438
6917
|
* repeats and races harmless. */
|
|
5439
|
-
_flipRunRecord(
|
|
5440
|
-
if (!
|
|
6918
|
+
_flipRunRecord(entry2, status, error) {
|
|
6919
|
+
if (!entry2 || !entry2.storagePath || !entry2.projectId) return;
|
|
5441
6920
|
var patch = { status, finished: Date.now() };
|
|
5442
6921
|
if (error) patch.error = error;
|
|
5443
|
-
upsertIndexRunRecordSafe(
|
|
6922
|
+
upsertIndexRunRecordSafe(entry2.projectId, entry2.storagePath, patch);
|
|
5444
6923
|
}
|
|
5445
|
-
maybeResumeIndexing(
|
|
6924
|
+
maybeResumeIndexing(entry2, response, platform) {
|
|
5446
6925
|
var self = this;
|
|
5447
6926
|
var endOfClientChain = function() {
|
|
5448
6927
|
self._nudgeIndexingDrain();
|
|
5449
6928
|
};
|
|
5450
6929
|
try {
|
|
5451
|
-
if (!
|
|
5452
|
-
if (this.cancelledIndexKeys.has(this._indexKeyOf(
|
|
5453
|
-
if (!isPagedReadFile(
|
|
6930
|
+
if (!entry2 || !entry2.storagePath) return;
|
|
6931
|
+
if (this.cancelledIndexKeys.has(this._indexKeyOf(entry2))) return;
|
|
6932
|
+
if (!isPagedReadFile(entry2.filename, entry2.mime)) {
|
|
5454
6933
|
if (!isErrorResponseBody(response) && !this._isCancelledPollResult(response)) {
|
|
5455
|
-
this._mintDoneMarker(
|
|
5456
|
-
this._flipRunRecord(
|
|
6934
|
+
this._mintDoneMarker(entry2);
|
|
6935
|
+
this._flipRunRecord(entry2, "done");
|
|
5457
6936
|
} else if (this._isCancelledPollResult(response)) {
|
|
5458
|
-
this._flipRunRecord(
|
|
6937
|
+
this._flipRunRecord(entry2, "cancelled");
|
|
5459
6938
|
} else {
|
|
5460
|
-
this._flipRunRecord(
|
|
6939
|
+
this._flipRunRecord(entry2, "error", this._runErrorText(response));
|
|
5461
6940
|
}
|
|
5462
6941
|
endOfClientChain();
|
|
5463
6942
|
return;
|
|
5464
6943
|
}
|
|
5465
|
-
if (isImageVisionFile(
|
|
5466
|
-
if (windowedIndexingEnabled() && isWindowedReadFile(
|
|
6944
|
+
if (isImageVisionFile(entry2.filename, entry2.mime)) return;
|
|
6945
|
+
if (windowedIndexingEnabled() && isWindowedReadFile(entry2.filename, entry2.mime)) return;
|
|
5467
6946
|
if (isErrorResponseBody(response)) {
|
|
5468
|
-
this._flipRunRecord(
|
|
6947
|
+
this._flipRunRecord(entry2, "error", this._runErrorText(response));
|
|
5469
6948
|
endOfClientChain();
|
|
5470
6949
|
return;
|
|
5471
6950
|
}
|
|
5472
6951
|
var answer = (platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "";
|
|
5473
6952
|
if (answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1) {
|
|
5474
|
-
this._mintDoneMarker(
|
|
5475
|
-
this._flipRunRecord(
|
|
6953
|
+
this._mintDoneMarker(entry2);
|
|
6954
|
+
this._flipRunRecord(entry2, "done");
|
|
5476
6955
|
endOfClientChain();
|
|
5477
6956
|
return;
|
|
5478
6957
|
}
|
|
5479
|
-
var pass = (
|
|
6958
|
+
var pass = (entry2.resumePass || 0) + 1;
|
|
5480
6959
|
if (pass > MAX_INDEXING_RESUME_PASSES) {
|
|
5481
|
-
this._flipRunRecord(
|
|
6960
|
+
this._flipRunRecord(entry2, "error", "Stopped after " + MAX_INDEXING_RESUME_PASSES + " passes without finishing.");
|
|
5482
6961
|
endOfClientChain();
|
|
5483
6962
|
return;
|
|
5484
6963
|
}
|
|
5485
6964
|
var id = this.host.getIdentity();
|
|
5486
|
-
if (!id || id.platform === "none" || id.projectId !==
|
|
5487
|
-
this._flipRunRecord(
|
|
6965
|
+
if (!id || id.platform === "none" || id.projectId !== entry2.projectId) {
|
|
6966
|
+
this._flipRunRecord(entry2, "error", "Indexing stopped: the session or project changed before the file finished.");
|
|
5488
6967
|
endOfClientChain();
|
|
5489
6968
|
return;
|
|
5490
6969
|
}
|
|
@@ -5502,10 +6981,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5502
6981
|
serviceName: id.serviceName,
|
|
5503
6982
|
serviceDescription: id.serviceDescription,
|
|
5504
6983
|
attachment: {
|
|
5505
|
-
name:
|
|
5506
|
-
storagePath:
|
|
5507
|
-
mime:
|
|
5508
|
-
size:
|
|
6984
|
+
name: entry2.filename,
|
|
6985
|
+
storagePath: entry2.storagePath,
|
|
6986
|
+
mime: entry2.mime,
|
|
6987
|
+
size: entry2.size,
|
|
5509
6988
|
url: ""
|
|
5510
6989
|
}
|
|
5511
6990
|
}).then(function(ack) {
|
|
@@ -5514,11 +6993,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5514
6993
|
projectId: id.projectId,
|
|
5515
6994
|
platform: id.platform,
|
|
5516
6995
|
id: ack.id,
|
|
5517
|
-
filename:
|
|
5518
|
-
storagePath:
|
|
5519
|
-
isReindex:
|
|
5520
|
-
mime:
|
|
5521
|
-
size:
|
|
6996
|
+
filename: entry2.filename,
|
|
6997
|
+
storagePath: entry2.storagePath,
|
|
6998
|
+
isReindex: entry2.isReindex,
|
|
6999
|
+
mime: entry2.mime,
|
|
7000
|
+
size: entry2.size,
|
|
5522
7001
|
status: ack.status === "running" ? "running" : "pending",
|
|
5523
7002
|
poll: ack.poll,
|
|
5524
7003
|
resumePass: pass
|
|
@@ -5605,6 +7084,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5605
7084
|
formatIndexingLabel: self.host.formatIndexingLabel
|
|
5606
7085
|
}).messages;
|
|
5607
7086
|
self.applyHydratedBodies(mapped);
|
|
7087
|
+
self._adoptLocalAnswers(mapped, loadKey);
|
|
5608
7088
|
var keptOlderPages = false;
|
|
5609
7089
|
var keptScreenAwaitingBg = false;
|
|
5610
7090
|
if (fetchMore) {
|
|
@@ -5780,6 +7260,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5780
7260
|
}
|
|
5781
7261
|
self.updateHistoryCache();
|
|
5782
7262
|
self.host.notify();
|
|
7263
|
+
self._scheduleStreamRecovery(loadKey, platform, projectId, owner);
|
|
5783
7264
|
var bgPending = !fetchMore && history && history.bgPending;
|
|
5784
7265
|
if (bgPending) {
|
|
5785
7266
|
var batchId = ++_bgHistoryBatchSeq;
|
|
@@ -5929,7 +7410,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5929
7410
|
}
|
|
5930
7411
|
}
|
|
5931
7412
|
};
|
|
5932
|
-
var pp = isBg ? item.poll(Object.assign({ latency: POLL_INTERVAL }, pollOpts)) : self._fgPollWithEarlyProbe(item, capturedId, pollOpts
|
|
7413
|
+
var pp = isBg ? item.poll(Object.assign({ latency: POLL_INTERVAL }, pollOpts)) : self._fgPollWithEarlyProbe(item, capturedId, pollOpts, {
|
|
7414
|
+
platform,
|
|
7415
|
+
projectId,
|
|
7416
|
+
owner,
|
|
7417
|
+
ownerKey: loadKey
|
|
7418
|
+
});
|
|
5933
7419
|
self._trackPoll(capturedId, item._isBgTask || item._isOnBgQueue ? "bg" : "fg", pp);
|
|
5934
7420
|
if (pp && pp.catch) pp.catch(function() {
|
|
5935
7421
|
});
|
|
@@ -6599,11 +8085,90 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6599
8085
|
return out;
|
|
6600
8086
|
}
|
|
6601
8087
|
|
|
8088
|
+
// src/engine/project_settings.ts
|
|
8089
|
+
var UPLOAD_ACCESS_GROUPS = ["public", "authorized", "private"];
|
|
8090
|
+
var DEFAULT_UPLOAD_ACCESS_GROUP = "authorized";
|
|
8091
|
+
var PROJECT_SETTINGS_UNIQUE_ID = "bq::settings";
|
|
8092
|
+
var UPLOAD_ACCESS_LABELS = {
|
|
8093
|
+
public: "Public",
|
|
8094
|
+
authorized: "Signed in users",
|
|
8095
|
+
private: "Only me"
|
|
8096
|
+
};
|
|
8097
|
+
var UPLOAD_ACCESS_HINTS = {
|
|
8098
|
+
public: "Anyone can ask about this file, including visitors who are not logged in.",
|
|
8099
|
+
authorized: "Only users signed in to this project can ask about this file.",
|
|
8100
|
+
private: "Only you can ask about this file."
|
|
8101
|
+
};
|
|
8102
|
+
function normalizeUploadAccessGroup(value) {
|
|
8103
|
+
return UPLOAD_ACCESS_GROUPS.indexOf(value) === -1 ? DEFAULT_UPLOAD_ACCESS_GROUP : value;
|
|
8104
|
+
}
|
|
8105
|
+
function normalizeProjectAccessSetting(value) {
|
|
8106
|
+
if (value === "ask") return "ask";
|
|
8107
|
+
return UPLOAD_ACCESS_GROUPS.indexOf(value) === -1 ? null : value;
|
|
8108
|
+
}
|
|
8109
|
+
function accessSettingFrom(data) {
|
|
8110
|
+
return normalizeProjectAccessSetting(data?.upload_access_group);
|
|
8111
|
+
}
|
|
8112
|
+
function uploadAccessGroupFrom(data) {
|
|
8113
|
+
const v = accessSettingFrom(data);
|
|
8114
|
+
return v && v !== "ask" ? v : DEFAULT_UPLOAD_ACCESS_GROUP;
|
|
8115
|
+
}
|
|
8116
|
+
function asksUploadAccessFrom(data) {
|
|
8117
|
+
return accessSettingFrom(data) === "ask";
|
|
8118
|
+
}
|
|
8119
|
+
var reader = null;
|
|
8120
|
+
var cache = /* @__PURE__ */ new Map();
|
|
8121
|
+
function configureProjectSettings(fn) {
|
|
8122
|
+
reader = fn;
|
|
8123
|
+
}
|
|
8124
|
+
function entry(service) {
|
|
8125
|
+
let e = cache.get(service);
|
|
8126
|
+
if (!e) {
|
|
8127
|
+
e = { data: null, settled: false, inflight: null };
|
|
8128
|
+
cache.set(service, e);
|
|
8129
|
+
}
|
|
8130
|
+
return e;
|
|
8131
|
+
}
|
|
8132
|
+
function loadProjectSettings(service) {
|
|
8133
|
+
if (!service) return Promise.resolve(null);
|
|
8134
|
+
const e = entry(service);
|
|
8135
|
+
if (e.settled) return Promise.resolve(e.data);
|
|
8136
|
+
if (e.inflight) return e.inflight;
|
|
8137
|
+
if (!reader) return Promise.resolve(null);
|
|
8138
|
+
const run = reader(service).then((data) => data && typeof data === "object" ? data : null).catch(() => null).then((data) => {
|
|
8139
|
+
const cur = entry(service);
|
|
8140
|
+
if (cur.inflight === run) {
|
|
8141
|
+
cur.data = data;
|
|
8142
|
+
cur.settled = true;
|
|
8143
|
+
cur.inflight = null;
|
|
8144
|
+
}
|
|
8145
|
+
return data;
|
|
8146
|
+
});
|
|
8147
|
+
e.inflight = run;
|
|
8148
|
+
return run;
|
|
8149
|
+
}
|
|
8150
|
+
function primeProjectSettings(service) {
|
|
8151
|
+
void loadProjectSettings(service);
|
|
8152
|
+
}
|
|
8153
|
+
function readyProjectSettings(service) {
|
|
8154
|
+
return loadProjectSettings(service);
|
|
8155
|
+
}
|
|
8156
|
+
function cachedProjectSettings(service) {
|
|
8157
|
+
const e = cache.get(service);
|
|
8158
|
+
return e && e.settled ? e.data : null;
|
|
8159
|
+
}
|
|
8160
|
+
function projectUploadAccessGroup(service) {
|
|
8161
|
+
return uploadAccessGroupFrom(cachedProjectSettings(service));
|
|
8162
|
+
}
|
|
8163
|
+
function projectAsksUploadAccess(service) {
|
|
8164
|
+
return asksUploadAccessFrom(cachedProjectSettings(service));
|
|
8165
|
+
}
|
|
8166
|
+
|
|
6602
8167
|
// src/index.js
|
|
6603
8168
|
(function() {
|
|
6604
8169
|
var MCP_PROD = "https://mcp.broadwayinc.computer";
|
|
6605
8170
|
var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
|
|
6606
|
-
var BQ_VERSION = "1.9.
|
|
8171
|
+
var BQ_VERSION = "1.9.10" ;
|
|
6607
8172
|
var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
|
|
6608
8173
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
6609
8174
|
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
@@ -6680,9 +8245,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6680
8245
|
}
|
|
6681
8246
|
}
|
|
6682
8247
|
function base64UrlEncode(bytes) {
|
|
6683
|
-
var
|
|
6684
|
-
for (var i = 0; i < bytes.length; i++)
|
|
6685
|
-
return btoa(
|
|
8248
|
+
var str2 = "";
|
|
8249
|
+
for (var i = 0; i < bytes.length; i++) str2 += String.fromCharCode(bytes[i]);
|
|
8250
|
+
return btoa(str2).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
6686
8251
|
}
|
|
6687
8252
|
function randBytes(n) {
|
|
6688
8253
|
var b = new Uint8Array(n);
|
|
@@ -6779,8 +8344,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6779
8344
|
function anonymousAllowed() {
|
|
6780
8345
|
if (S.opts && typeof S.opts.allowAnonymous === "boolean") return S.opts.allowAnonymous;
|
|
6781
8346
|
var conf = S.service && S.service.conf || null;
|
|
6782
|
-
if (!conf
|
|
6783
|
-
return
|
|
8347
|
+
if (!conf) return false;
|
|
8348
|
+
return conf.require_login === false;
|
|
6784
8349
|
}
|
|
6785
8350
|
function isAnonymousSession() {
|
|
6786
8351
|
return !S.user && anonymousAllowed();
|
|
@@ -8514,10 +10079,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
8514
10079
|
// buildGreetingEl, so the two can never disagree.
|
|
8515
10080
|
greeting: greetingParts().text,
|
|
8516
10081
|
canUpload: !uploadsFrozenForUser(),
|
|
8517
|
-
// Where THIS project's indexer writes
|
|
8518
|
-
// "authorized"; on a project set
|
|
8519
|
-
// search the wrong group and answer
|
|
8520
|
-
|
|
10082
|
+
// Where THIS project's indexer writes, from the "bq::settings"
|
|
10083
|
+
// record. The MCP's auto-fill assumes "authorized"; on a project set
|
|
10084
|
+
// to public or private that would search the wrong group and answer
|
|
10085
|
+
// "nothing found". A SYNC CACHE READ, because the engine calls this
|
|
10086
|
+
// hook from paths with nowhere to put an await. Every send that
|
|
10087
|
+
// reaches it has settled the fetch first: sendMessage awaits
|
|
10088
|
+
// readyProjectSettings on the text-only branch, and the attachment
|
|
10089
|
+
// branch resolves the upload group before it dispatches.
|
|
10090
|
+
indexAccessGroup: projectUploadAccessGroup(S.projectId)});
|
|
8521
10091
|
}
|
|
8522
10092
|
function refreshSkapiSession() {
|
|
8523
10093
|
return S.skapi.getProfile({ refreshToken: true }).then(function() {
|
|
@@ -8593,7 +10163,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
8593
10163
|
CS.drafting = false;
|
|
8594
10164
|
syncDraftingIndicator();
|
|
8595
10165
|
if (!hasAttachments) {
|
|
8596
|
-
|
|
10166
|
+
readyProjectSettings(S.projectId).then(function() {
|
|
10167
|
+
session.dispatchComposedMessage(text, false);
|
|
10168
|
+
});
|
|
8597
10169
|
return;
|
|
8598
10170
|
}
|
|
8599
10171
|
attachmentBatchSeq += 1;
|
|
@@ -9150,32 +10722,32 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
9150
10722
|
});
|
|
9151
10723
|
if (objs.length) appendAttachments(objs);
|
|
9152
10724
|
}
|
|
9153
|
-
function readEntry(
|
|
10725
|
+
function readEntry(entry2, prefix) {
|
|
9154
10726
|
prefix = prefix || "";
|
|
9155
10727
|
return new Promise(function(resolve) {
|
|
9156
|
-
if (!
|
|
10728
|
+
if (!entry2) {
|
|
9157
10729
|
resolve([]);
|
|
9158
10730
|
return;
|
|
9159
10731
|
}
|
|
9160
|
-
if (
|
|
9161
|
-
|
|
10732
|
+
if (entry2.isFile) {
|
|
10733
|
+
entry2.file(function(file) {
|
|
9162
10734
|
resolve([{ file, path: prefix + file.name }]);
|
|
9163
10735
|
}, function() {
|
|
9164
10736
|
resolve([]);
|
|
9165
10737
|
});
|
|
9166
10738
|
return;
|
|
9167
10739
|
}
|
|
9168
|
-
if (
|
|
9169
|
-
var
|
|
10740
|
+
if (entry2.isDirectory) {
|
|
10741
|
+
var reader2 = entry2.createReader();
|
|
9170
10742
|
var all = [];
|
|
9171
10743
|
var readBatch = function() {
|
|
9172
|
-
|
|
10744
|
+
reader2.readEntries(function(entries) {
|
|
9173
10745
|
if (!entries.length) {
|
|
9174
10746
|
resolve(all);
|
|
9175
10747
|
return;
|
|
9176
10748
|
}
|
|
9177
10749
|
Promise.all(entries.map(function(e) {
|
|
9178
|
-
return readEntry(e, prefix +
|
|
10750
|
+
return readEntry(e, prefix + entry2.name + "/");
|
|
9179
10751
|
})).then(function(groups) {
|
|
9180
10752
|
groups.forEach(function(g) {
|
|
9181
10753
|
all.push.apply(all, g);
|
|
@@ -9485,20 +11057,20 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
9485
11057
|
for (var i = 0; i < items.length; i++) {
|
|
9486
11058
|
var it = items[i];
|
|
9487
11059
|
if (it.kind !== "file") continue;
|
|
9488
|
-
var
|
|
9489
|
-
entries.push(
|
|
9490
|
-
}
|
|
9491
|
-
Promise.all(entries.map(function(
|
|
9492
|
-
if (!
|
|
9493
|
-
if (
|
|
9494
|
-
if (
|
|
9495
|
-
return readEntry(
|
|
11060
|
+
var entry2 = it.webkitGetAsEntry ? it.webkitGetAsEntry() : null;
|
|
11061
|
+
entries.push(entry2 || it.getAsFile());
|
|
11062
|
+
}
|
|
11063
|
+
Promise.all(entries.map(function(entry3) {
|
|
11064
|
+
if (!entry3) return Promise.resolve(null);
|
|
11065
|
+
if (entry3 instanceof File) return Promise.resolve(newAttachment({ kind: "file", name: entry3.name, file: entry3 }));
|
|
11066
|
+
if (entry3.isFile) {
|
|
11067
|
+
return readEntry(entry3).then(function(files) {
|
|
9496
11068
|
return files[0] ? newAttachment({ kind: "file", name: files[0].file.name, file: files[0].file }) : null;
|
|
9497
11069
|
});
|
|
9498
11070
|
}
|
|
9499
|
-
if (
|
|
9500
|
-
return readEntry(
|
|
9501
|
-
return newAttachment({ kind: "folder", name:
|
|
11071
|
+
if (entry3.isDirectory) {
|
|
11072
|
+
return readEntry(entry3).then(function(files) {
|
|
11073
|
+
return newAttachment({ kind: "folder", name: entry3.name, files });
|
|
9502
11074
|
});
|
|
9503
11075
|
}
|
|
9504
11076
|
return Promise.resolve(null);
|
|
@@ -9831,8 +11403,18 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
9831
11403
|
if (msg.isPendingQueued || msg.isPendingOlder) cls.push("is-pending-older");
|
|
9832
11404
|
if (msg._dimSending || msg._cancelling) cls.push("is-sending-to-server");
|
|
9833
11405
|
var bubble;
|
|
9834
|
-
if (msg.isPending) {
|
|
11406
|
+
if (msg.isPending && !msg._streaming || streamRecoveryPhase(msg) === "active") {
|
|
9835
11407
|
bubble = h("div", { class: "bq-bubble" }, h("span", { class: "bq-loader" }));
|
|
11408
|
+
} else if (streamRecoveryPhase(msg)) {
|
|
11409
|
+
var labels = streamRecoveryLabels(streamRecoveryPhase(msg));
|
|
11410
|
+
bubble = h("div", { class: "bq-bubble is-stream-recover" + (streamRecoveryPhase(msg) === "failed" ? " is-stream-failed" : "") });
|
|
11411
|
+
bubble.appendChild(h("span", { class: "bq-stream-recover-note", text: labels.note }));
|
|
11412
|
+
var recoverBtn = h("button", { class: "bq-stream-recover-btn", type: "button", text: labels.action });
|
|
11413
|
+
recoverBtn.addEventListener("click", function(e) {
|
|
11414
|
+
e.stopPropagation();
|
|
11415
|
+
session.recoverStreamedAnswer(msg._serverItemId);
|
|
11416
|
+
});
|
|
11417
|
+
bubble.appendChild(recoverBtn);
|
|
9836
11418
|
} else {
|
|
9837
11419
|
bubble = h("div", { class: "bq-bubble" });
|
|
9838
11420
|
if (msg.role === "user" && msg.isPendingQueued) {
|
|
@@ -9857,7 +11439,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
9857
11439
|
else if (msg.isPendingQueued) bubble.appendChild(h("span", { class: "bq-pending-note", text: "(In queue)" }));
|
|
9858
11440
|
if (msg.isCancelled) bubble.appendChild(h("span", { class: "bq-cancel-error", text: "(cancelled)" }));
|
|
9859
11441
|
if (msg._cancelError) bubble.appendChild(h("span", { class: "bq-cancel-error", text: msg._cancelError }));
|
|
9860
|
-
var ts = formatChatTimestamp(msg._ts);
|
|
11442
|
+
var ts = msg.isPending ? "" : formatChatTimestamp(msg._ts);
|
|
9861
11443
|
if (ts) bubble.appendChild(h("time", { class: "bq-msg-time", text: ts }));
|
|
9862
11444
|
}
|
|
9863
11445
|
return h("div", { class: cls.join(" "), dataset: { msgIndex: String(idx) } }, bubble);
|
|
@@ -10329,8 +11911,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
10329
11911
|
return h(
|
|
10330
11912
|
"div",
|
|
10331
11913
|
{ class: "bq-history-loading" },
|
|
10332
|
-
h(
|
|
10333
|
-
|
|
11914
|
+
h(
|
|
11915
|
+
"span",
|
|
11916
|
+
{ class: "bq-history-loading-inner" },
|
|
11917
|
+
h("span", { text: "Fetching history" }),
|
|
11918
|
+
h("span", { class: "bq-loader" })
|
|
11919
|
+
)
|
|
10334
11920
|
);
|
|
10335
11921
|
}
|
|
10336
11922
|
function rowAnchorKey(msg, index) {
|
|
@@ -10395,8 +11981,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
10395
11981
|
CS.messagesBox.appendChild(h(
|
|
10396
11982
|
"div",
|
|
10397
11983
|
{ class: "bq-history-loading" },
|
|
10398
|
-
h(
|
|
10399
|
-
|
|
11984
|
+
h(
|
|
11985
|
+
"span",
|
|
11986
|
+
{ class: "bq-history-loading-inner" },
|
|
11987
|
+
h("span", { text: "Loading indexing history" }),
|
|
11988
|
+
h("span", { class: "bq-loader" })
|
|
11989
|
+
)
|
|
10400
11990
|
));
|
|
10401
11991
|
}
|
|
10402
11992
|
CS.messagesBox.appendChild(buildGreetingEl());
|
|
@@ -10497,6 +12087,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
10497
12087
|
});
|
|
10498
12088
|
}
|
|
10499
12089
|
function renderChat() {
|
|
12090
|
+
primeProjectSettings(S.projectId);
|
|
10500
12091
|
clearImagePreviewCache(S.projectId || "default");
|
|
10501
12092
|
chatScrollAnchor.forget();
|
|
10502
12093
|
for (var uk in unavailableLinkMap) delete unavailableLinkMap[uk];
|
|
@@ -10695,33 +12286,6 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
10695
12286
|
overwriteState.resolver = null;
|
|
10696
12287
|
if (r) r(choice);
|
|
10697
12288
|
}
|
|
10698
|
-
var UPLOAD_ACCESS_GROUPS = ["public", "authorized", "private"];
|
|
10699
|
-
var UPLOAD_ACCESS_LABELS = {
|
|
10700
|
-
public: "Public",
|
|
10701
|
-
authorized: "Signed in users",
|
|
10702
|
-
private: "Only me"
|
|
10703
|
-
};
|
|
10704
|
-
var UPLOAD_ACCESS_HINTS = {
|
|
10705
|
-
public: "Anyone can ask about this file, including visitors who are not logged in.",
|
|
10706
|
-
authorized: "Only users signed in to this project can ask about this file.",
|
|
10707
|
-
private: "Only you can ask about this file."
|
|
10708
|
-
};
|
|
10709
|
-
function normalizeUploadAccessGroup(v) {
|
|
10710
|
-
return UPLOAD_ACCESS_GROUPS.indexOf(v) === -1 ? "authorized" : v;
|
|
10711
|
-
}
|
|
10712
|
-
function projectAccessSetting() {
|
|
10713
|
-
var conf = S.service && S.service.conf || {};
|
|
10714
|
-
var v = conf.default_access_group;
|
|
10715
|
-
if (v === "ask") return "ask";
|
|
10716
|
-
return UPLOAD_ACCESS_GROUPS.indexOf(v) === -1 ? null : v;
|
|
10717
|
-
}
|
|
10718
|
-
function projectUploadAccessGroup() {
|
|
10719
|
-
var v = projectAccessSetting();
|
|
10720
|
-
return v && v !== "ask" ? v : "authorized";
|
|
10721
|
-
}
|
|
10722
|
-
function projectAsksUploadAccess() {
|
|
10723
|
-
return projectAccessSetting() === "ask";
|
|
10724
|
-
}
|
|
10725
12289
|
var accessGroupState = { resolver: null, sticky: null, handle: null, applyToAll: false, choice: "authorized", perPath: {} };
|
|
10726
12290
|
function resetAccessGroupBatch() {
|
|
10727
12291
|
accessGroupState.sticky = null;
|
|
@@ -10741,8 +12305,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
10741
12305
|
}
|
|
10742
12306
|
var accessGroupChain = Promise.resolve();
|
|
10743
12307
|
function resolveUploadAccessGroup(storagePath) {
|
|
10744
|
-
|
|
10745
|
-
|
|
12308
|
+
return readyProjectSettings(S.projectId).then(function() {
|
|
12309
|
+
return decideUploadAccessGroup(storagePath);
|
|
12310
|
+
});
|
|
12311
|
+
}
|
|
12312
|
+
function decideUploadAccessGroup(storagePath) {
|
|
12313
|
+
var svc = S.projectId;
|
|
12314
|
+
if (!projectAsksUploadAccess(svc)) return Promise.resolve(projectUploadAccessGroup(svc));
|
|
12315
|
+
var fallback = projectUploadAccessGroup(svc);
|
|
10746
12316
|
if (accessGroupState.sticky) return Promise.resolve(accessGroupState.sticky);
|
|
10747
12317
|
var pathKey = String(storagePath || "");
|
|
10748
12318
|
if (pathKey && accessGroupState.perPath[pathKey]) {
|
|
@@ -11045,11 +12615,29 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
11045
12615
|
// client-side attachment parsers, e.g. [createHwpParser()]
|
|
11046
12616
|
// Open the chat with no login for visitors without an account.
|
|
11047
12617
|
// null → follow the project's own "Allow anonymous users" setting
|
|
11048
|
-
// (getConnectionInfo().conf.
|
|
12618
|
+
// (getConnectionInfo().conf.require_login); true/false pins it.
|
|
11049
12619
|
allowAnonymous: null,
|
|
11050
12620
|
// Server-driven windowed indexing; read at configureChatEngine time.
|
|
11051
12621
|
// Listed here so the defaults object is the full opt surface.
|
|
11052
|
-
windowedIndexing: true
|
|
12622
|
+
windowedIndexing: true,
|
|
12623
|
+
// Live streaming of chat turns; read at configureChatEngine time.
|
|
12624
|
+
// OFF until the region's polling worker relays the response bytes:
|
|
12625
|
+
// see the configureChatEngine call for what goes wrong without it.
|
|
12626
|
+
// A REQUEST, not a switch: it is also refused (with a warning, falling
|
|
12627
|
+
// back to buffered) when the embedder's own skapi-js is too old to
|
|
12628
|
+
// carry skapi's half of the stream flag. See skapiSupportsStreaming.
|
|
12629
|
+
liveStreaming: false,
|
|
12630
|
+
// Socket delivery for the streamed reply. OFF unless the embedder asks,
|
|
12631
|
+
// and separately from liveStreaming, because this widget runs on someone
|
|
12632
|
+
// else's page with someone else's skapi instance: skapi's joinRealtime
|
|
12633
|
+
// REPLACES the connection's group rather than adding to it, so for the
|
|
12634
|
+
// length of a turn this would take the room out from under whatever the
|
|
12635
|
+
// host app uses realtime for, and the host would see its own messages
|
|
12636
|
+
// simply stop. Only an embedder who knows their app does not use realtime
|
|
12637
|
+
// (or does not mind) can answer that, so only they can turn it on. It is
|
|
12638
|
+
// purely an accelerator: with it off the reply still streams, just on the
|
|
12639
|
+
// poll's cadence rather than as the text is relayed.
|
|
12640
|
+
liveStreamingRealtime: false
|
|
11053
12641
|
}, opts || {});
|
|
11054
12642
|
S.mountEl = mountEl;
|
|
11055
12643
|
clear(mountEl);
|
|
@@ -11058,6 +12646,21 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
11058
12646
|
applyTheme(loadTheme());
|
|
11059
12647
|
S.booted = true;
|
|
11060
12648
|
console.log("[bunnyquery] v" + BQ_VERSION);
|
|
12649
|
+
var canStream = skapiSupportsStreaming(S.skapi);
|
|
12650
|
+
var liveStreaming = S.opts.liveStreaming === true;
|
|
12651
|
+
if (liveStreaming && !canStream) {
|
|
12652
|
+
liveStreaming = false;
|
|
12653
|
+
console.warn(
|
|
12654
|
+
"[bunnyquery] liveStreaming was requested but this page's skapi-js has no clientSecretRequestStream/clientSecretRequestFinalize, so skapi's half of the stream flag would be dropped and every reply would read back empty. Falling back to buffered replies - update skapi-js to enable streaming."
|
|
12655
|
+
);
|
|
12656
|
+
}
|
|
12657
|
+
configureProjectSettings(function(service) {
|
|
12658
|
+
if (!S.skapi || typeof S.skapi.getRecords !== "function") return Promise.resolve(null);
|
|
12659
|
+
return Promise.resolve(S.skapi.getRecords({ service, unique_id: PROJECT_SETTINGS_UNIQUE_ID })).then(function(res) {
|
|
12660
|
+
var rec = res && res.list && res.list[0] || null;
|
|
12661
|
+
return rec && rec.data || null;
|
|
12662
|
+
});
|
|
12663
|
+
});
|
|
11061
12664
|
configureChatEngine({
|
|
11062
12665
|
clientSecretRequest: function(o) {
|
|
11063
12666
|
return S.skapi.clientSecretRequest(o);
|
|
@@ -11090,7 +12693,49 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
11090
12693
|
// on a big file. Pass windowedIndexing: false in init opts to opt back out.
|
|
11091
12694
|
windowedIndexing: S.opts.windowedIndexing !== false,
|
|
11092
12695
|
// Client-side attachment parsers (e.g. an .hwp parser) passed via init opts.
|
|
11093
|
-
attachmentParsers: S.opts.attachmentParsers || void 0
|
|
12696
|
+
attachmentParsers: S.opts.attachmentParsers || void 0,
|
|
12697
|
+
// ---- live streaming (mirrored in agent.vue's ai_agent.ts) --------
|
|
12698
|
+
// Off by default, and for the same shipping-order reason
|
|
12699
|
+
// windowedIndexing had one: THE RELAYING POLLING WORKER MUST SHIP
|
|
12700
|
+
// FIRST. With this on against a region whose worker does not relay,
|
|
12701
|
+
// the request either has its `since` cursor rejected or the row keeps
|
|
12702
|
+
// an SSE transcript where the readers expect a parsed document, and
|
|
12703
|
+
// the turn reads back as an empty answer. A streamed row settles with
|
|
12704
|
+
// a STATUS AND NO BODY on purpose, so there is no fallback to read.
|
|
12705
|
+
// Flip it per environment once the worker is deployed there; the
|
|
12706
|
+
// widget takes it as an init opt because an embed picks its own
|
|
12707
|
+
// region, where agent.vue flips one module constant.
|
|
12708
|
+
// It also needs a skapi-js that supports `stream`/`onStream`, and the
|
|
12709
|
+
// page's pin is the EMBEDDER's, so the request is granted above by
|
|
12710
|
+
// skapiSupportsStreaming rather than taken on trust here.
|
|
12711
|
+
liveStreaming,
|
|
12712
|
+
// Requires liveStreaming, and cannot outlive it: the AND is what stops an
|
|
12713
|
+
// embedder turning on socket delivery for a reply that is not streamed.
|
|
12714
|
+
liveStreamingRealtime: liveStreaming && S.opts.liveStreamingRealtime === true,
|
|
12715
|
+
// What stores the version of a streamed turn that history keeps. The
|
|
12716
|
+
// engine sends the ASSEMBLED provider body, so a streamed turn reads
|
|
12717
|
+
// back through exactly the extractors a buffered one does, with no
|
|
12718
|
+
// branch in the mapper; storing is also what releases the chunks.
|
|
12719
|
+
// Called only for a streamed turn, and best-effort inside the engine.
|
|
12720
|
+
// Handed over only when the SDK actually has it, so the engine's own
|
|
12721
|
+
// "is this host able to?" checks answer honestly instead of a call
|
|
12722
|
+
// reaching an undefined method mid-turn.
|
|
12723
|
+
clientSecretRequestFinalize: canStream ? function(requestId, data, options) {
|
|
12724
|
+
return S.skapi.clientSecretRequestFinalize(requestId, data, options);
|
|
12725
|
+
} : void 0,
|
|
12726
|
+
// THE SECOND HALF OF THE DURABILITY GUARANTEE. A streamed row settles
|
|
12727
|
+
// with a status and NO body: the answer is chunks until finalize copies
|
|
12728
|
+
// a version onto the row. A row that settles while no poll is attached
|
|
12729
|
+
// (the tab was closed, a mobile browser discarded it, the device slept
|
|
12730
|
+
// and the interval stopped) is therefore never finalized, and without
|
|
12731
|
+
// this hook the engine has no way back to it - the answer reads as gone
|
|
12732
|
+
// from the conversation with every byte of it still stored. Given the
|
|
12733
|
+
// request id this drains that turn's chunks in one pass, and the engine
|
|
12734
|
+
// parses them exactly as it parses a live stream, finalizing what it
|
|
12735
|
+
// read so the row becomes ordinary history and is never re-read.
|
|
12736
|
+
clientSecretRequestStream: canStream ? function(requestId, options) {
|
|
12737
|
+
return S.skapi.clientSecretRequestStream(requestId, options);
|
|
12738
|
+
} : void 0
|
|
11094
12739
|
});
|
|
11095
12740
|
if (!S._resizeBound && typeof window !== "undefined" && window.addEventListener) {
|
|
11096
12741
|
S._resizeBound = true;
|