bunnyquery 1.9.7 → 1.9.10

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