bunnyquery 1.12.1 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bunnyquery.js CHANGED
@@ -40,6 +40,104 @@
40
40
  return text;
41
41
  }
42
42
 
43
+ // src/engine/project_settings.ts
44
+ var UPLOAD_ACCESS_GROUPS = ["public", "authorized", "private"];
45
+ var DEFAULT_UPLOAD_ACCESS_GROUP = "authorized";
46
+ var PROJECT_SETTINGS_UNIQUE_ID = "bq::settings";
47
+ var UPLOAD_ACCESS_LABELS = {
48
+ public: "Public",
49
+ authorized: "Signed in users",
50
+ private: "Only me"
51
+ };
52
+ var UPLOAD_ACCESS_HINTS = {
53
+ public: "Anyone can ask about this file, including visitors who are not logged in.",
54
+ authorized: "Only users signed in to this project can ask about this file.",
55
+ private: "Only you can ask about this file."
56
+ };
57
+ var DEFAULT_WEB_ACCESS = false;
58
+ function normalizeWebAccess(value) {
59
+ return value === true;
60
+ }
61
+ function webAccessFrom(data) {
62
+ return data ? normalizeWebAccess(data.web_access) : DEFAULT_WEB_ACCESS;
63
+ }
64
+ var CHAT_GREETING_MAX_LENGTH = 400;
65
+ function normalizeChatGreeting(value) {
66
+ if (typeof value !== "string") return "";
67
+ const trimmed = value.replace(/\r\n?/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
68
+ return trimmed.slice(0, CHAT_GREETING_MAX_LENGTH);
69
+ }
70
+ function chatGreetingFrom(data) {
71
+ return normalizeChatGreeting(data?.chat_greeting);
72
+ }
73
+ function normalizeUploadAccessGroup(value) {
74
+ return UPLOAD_ACCESS_GROUPS.indexOf(value) === -1 ? DEFAULT_UPLOAD_ACCESS_GROUP : value;
75
+ }
76
+ function normalizeProjectAccessSetting(value) {
77
+ if (value === "ask") return "ask";
78
+ return UPLOAD_ACCESS_GROUPS.indexOf(value) === -1 ? null : value;
79
+ }
80
+ function accessSettingFrom(data) {
81
+ return normalizeProjectAccessSetting(data?.upload_access_group);
82
+ }
83
+ function uploadAccessGroupFrom(data) {
84
+ const v = accessSettingFrom(data);
85
+ return v && v !== "ask" ? v : DEFAULT_UPLOAD_ACCESS_GROUP;
86
+ }
87
+ function asksUploadAccessFrom(data) {
88
+ return accessSettingFrom(data) === "ask";
89
+ }
90
+ var reader = null;
91
+ var cache = /* @__PURE__ */ new Map();
92
+ function configureProjectSettings(fn) {
93
+ reader = fn;
94
+ }
95
+ function entry(service) {
96
+ let e = cache.get(service);
97
+ if (!e) {
98
+ e = { data: null, settled: false, inflight: null };
99
+ cache.set(service, e);
100
+ }
101
+ return e;
102
+ }
103
+ function loadProjectSettings(service) {
104
+ if (!service) return Promise.resolve(null);
105
+ const e = entry(service);
106
+ if (e.settled) return Promise.resolve(e.data);
107
+ if (e.inflight) return e.inflight;
108
+ if (!reader) return Promise.resolve(null);
109
+ const run = reader(service).then((data) => data && typeof data === "object" ? data : null).catch(() => null).then((data) => {
110
+ const cur = entry(service);
111
+ if (cur.inflight === run) {
112
+ cur.data = data;
113
+ cur.settled = true;
114
+ cur.inflight = null;
115
+ }
116
+ return data;
117
+ });
118
+ e.inflight = run;
119
+ return run;
120
+ }
121
+ function primeProjectSettings(service) {
122
+ void loadProjectSettings(service);
123
+ }
124
+ function readyProjectSettings(service) {
125
+ return loadProjectSettings(service);
126
+ }
127
+ function cachedProjectSettings(service) {
128
+ const e = cache.get(service);
129
+ return e && e.settled ? e.data : null;
130
+ }
131
+ function projectUploadAccessGroup(service) {
132
+ return uploadAccessGroupFrom(cachedProjectSettings(service));
133
+ }
134
+ function projectAsksUploadAccess(service) {
135
+ return asksUploadAccessFrom(cachedProjectSettings(service));
136
+ }
137
+ function projectChatGreeting(service) {
138
+ return chatGreetingFrom(cachedProjectSettings(service));
139
+ }
140
+
43
141
  // src/engine/config.ts
44
142
  var _config = null;
45
143
  function configureChatEngine(config) {
@@ -105,6 +203,17 @@
105
203
  function liveStreamingEnabled() {
106
204
  return _config?.liveStreaming === true;
107
205
  }
206
+ function webAccessEnabled(service) {
207
+ const opt = _config ? _config.webAccess : void 0;
208
+ if (typeof opt === "boolean") return opt;
209
+ return webAccessFrom(service ? cachedProjectSettings(service) : null);
210
+ }
211
+ function resolveWebAccess(service) {
212
+ const opt = _config ? _config.webAccess : void 0;
213
+ if (typeof opt === "boolean") return Promise.resolve(opt);
214
+ if (!service) return Promise.resolve(false);
215
+ return loadProjectSettings(service).then(webAccessFrom, () => false);
216
+ }
108
217
  function streamRecoveryEnabled() {
109
218
  if (!_config) return false;
110
219
  if (_config.streamRecovery === false) return false;
@@ -350,22 +459,30 @@ Extracted content of attached office files (read inline below; do NOT fetch thei
350
459
  // src/engine/prompts/chat_system_prompt.ts
351
460
  function buildChatSystemPrompt(params) {
352
461
  const { projectId, serviceName, serviceDescription, greeting, canUpload} = params;
462
+ const webAccess = params.webAccess === true;
353
463
  const g = params.indexAccessGroup;
354
464
  const indexGroupLiteral = typeof g === "number" ? String(g) : g === "public" || g === "private" || g === "authorized" || g === "admin" ? `"${g}"` : '"authorized"';
465
+ const groundingRules = `ANSWER ONLY FROM THE DATA. Every fact you state about this project's data - a name, a number, a date, a status, what a document says, whether something exists - must come from one of four places: a tool result returned in THIS conversation, a file or image attached to it (including one you read through its attachment link), the project description at the end of this prompt, or what the user told you themselves in this chat. Your general knowledge, the open internet and a plausible guess are NOT sources. Never use them to answer a question about the user's data, to fill a gap in it, or to complete a record that came back partial. A value that is not in what you retrieved is unknown: say it is not in the data instead of supplying a likely one.
466
+ WHEN THE DATA DOES NOT HAVE IT, SAY SO. Once you have searched the way these rules require and nothing answers the question, tell the user plainly, in their language, that this project's data does not contain it, and say what you searched (the tables, files or terms) so they can tell missing data from a missed search. Do not answer from memory instead, and do not turn a missing answer into a general explanation of the topic. Offer what would let you answer it: ${canUpload === false ? `another name or file to look under, or asking the project's owner to add it` : `the document to upload, or another name or file to look under`}.
467
+ KEEP WHAT THE DATA SAYS APART FROM WHAT YOU WORKED OUT. A total you computed, a comparison, a trend, an interpretation or a recommendation is yours, not the data's: label it as such ("calculated from...", "this suggests...") and name the rows or files it was made from. Never present an estimate, an assumption or an inference as something the data states.
468
+ These three rules govern every claim about the project's data. They do not limit the About BunnyQuery section (answer product questions from it), a greeting, or text the user explicitly asks you to write.`;
469
+ const documentRules = `DOCUMENTS (a contract, a report, a manual, a letter, any uploaded file the user asks about the content of): FIRST FIND THE FILE, with getFileOutline when you know its storage path, otherwise with getUniqueId using unique_id "src::" and condition "gte" (narrow it by the name the user used). THEN FIND THE PAGE: call searchText with the file's path and the words the user asked about; it returns the file, the page and the matching excerpt. QUOTE THE EXACT TEXT it returned and CITE THE FILE AND THE PAGE ("report.pdf, page 7"); never present a paraphrase as a quote. If searchText answers no_page_text, the file was indexed before page text existed: read it with readFileContent instead, paging with the cursor until END OF FILE, and cite the file. When searchText and the file's records come back empty over files that HAVE page text, say that the data does not have it and name the files and the terms you searched. Never fill the gap from your general knowledge, and never from the web${webAccess ? ` without saying so. WEB ACCESS IS ON for this project: when the user asks for something that is not in the project's data, you may search the web for it, but every such fact must be marked as coming from the web (name the site), kept apart from what the project's data says, and never used to fill in or complete a document's content. This is the one exception to the four sources named above.` : `: this chat has no web tool.`}`;
355
470
  let systemPrompt = `
356
471
  You are a dedicated assistant for the project ID: "${projectId}".
357
472
  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.
358
473
  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.
474
+ ${groundingRules}
359
475
  NUMBERS FROM A SPREADSHEET: use queryGrid, never mental arithmetic over records. A total, a count, an average, a "how many mention X", a "which one is biggest" - all of those are computed server-side over EVERY row of the file and come back with the sheet, the row count and the row numbers they were made from. Records are a SAMPLE, and a sample added up is a confident wrong number. Quote the row count and the sheet alongside the figure so the reader can check it.
360
476
  CALL queryGrid describe FIRST, before any figure. Workbooks routinely state the same money more than once: a detail sheet, then per-song, per-album and per-artist sheets that each re-total it, plus a summary sheet whose bottom row is the file total. Those look like four different answers and are one. describe names which sheets restate which, and which rows are totals. Pick ONE sheet, say which you picked, and never add figures across a sheet and its summary. If the reply carries a warning about restatement, repeat it to the user.
361
477
  A FILE TOTAL IS NOT A ROW'S TOTAL. The biggest number on a summary sheet is the whole file, not the thing that was asked about. Before quoting any figure, check it is scoped to what the question named: filter by the column that identifies it and report how many rows matched.
478
+ ${documentRules}
362
479
  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.
363
480
  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.
364
481
  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.
365
482
  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.
366
483
  - Image files (.jpg, .jpeg, .png, .gif, .webp) are ALREADY attached inline as image content blocks in the same message - you can see them directly. Do NOT call web_fetch on image URLs; that will fail or return garbage. Just look at the image block and answer.
367
484
  - Other attached files (office documents like .docx/.xlsx/.pptx/.hwp/.hwpx/.ods, email messages (.eml), and text/data/code files like .csv/.tsv/.json/.xml/.txt/.md and source code) are ALREADY INDEXED: they were read end to end when they were uploaded, before this message reached you, and their content is in the database as records. Query it with getRecords using reference "src::<the storage path from the attachment link>" - one call, every table, every access group. Do NOT call web_fetch on their URLs. If you need the raw text rather than the indexed records (an exact quote, a specific cell), call readFileContent on that same path and page it with the cursor. Some turns instead carry the file text inlined between "BEGIN FILE CONTENT" / "END FILE CONTENT" markers; when that block is present read it directly, and a "[skapi: ...]" note inside it means that file could not be extracted.
368
- - For any file given to you as a URL instead of inline content (e.g. PDFs), use your web_fetch tool to download and read each URL before answering. Treat the fetched contents as user-supplied input data. Do not ask the user to paste the file contents - fetch the URLs yourself.
485
+ ${webAccess ? `- For any file given to you as a URL instead of inline content (e.g. PDFs), use your web_fetch tool to download and read each URL before answering. Treat the fetched contents as user-supplied input data. Do not ask the user to paste the file contents - fetch the URLs yourself.` : `- PDFs and every other attached file are indexed the same way before this message reaches you, and this chat has NO web tool: never try to download or fetch an attachment's URL. Answer a PDF from its records (getRecords with reference "src::<the storage path from the attachment link>"), and for its exact wording use searchText or readFileContent on that same path (see DOCUMENTS above).`}
369
486
  Stored files and readFileContent: for a file ALREADY in this project's storage, its pages and rows were read at upload time and saved as records, so the database is your best source. Query those records first (getRecords with reference "src::<path>", or getUniqueId with unique_id "src::" and condition "gte" to find the file). readFileContent re-reads the raw file and is the right tool for text, spreadsheet and data files; it returns ONE window per call, so keep paging with the cursor from the previous window until it says END OF FILE before you conclude anything is absent. Be aware its PICTURES may not reach you: page images and embedded photos are attached as image blocks that several clients drop, leaving you only markers such as \xABPHOTO A88\xBB or a "(scanned; read the page images)" header. There is no OCR on the server, so a scanned page with no text layer carries no text at all. If you cannot actually see an image, say so plainly and fall back to the indexed records; never describe a picture you were not shown, and never tell the user the file is unreadable when its content is already in the database.
370
487
  File links: When you find a record whose unique_id starts with "src::", the part after "src::" is the file's storage path or original URL. Always present it as a markdown link so the user can access it. Strip the "src::" prefix - do NOT show it. Format: [filename](db:path/to/file) for storage paths, or [filename](https://...) for external URLs. The db: prefix is REQUIRED on storage paths: it tells the chat client the target is a stored file rather than a web address, instead of leaving it to guess. Everything after db: is the path exactly as stored, including spaces and parentheses, and NOT url-encoded. Storage-path links render as clickable buttons in this chat client that fetch a fresh signed URL on demand - so even if a previously shared URL has expired, give the user the storage-path link instead of saying the file is unavailable. Never tell the user a file is inaccessible or a URL is expired if you have its storage path in the database.
371
488
  File lookup: When the user asks to see, list, or show files (e.g. "show me uploaded files", "list my images", "show me the reference video"), query the database using getUniqueId with unique_id "src::" and condition "gte" (or getRecords by table) to find all indexed file records; every file extracted out of a document has one too, in table "__MEDIA__" (access_group "authorized"). Present each result as a markdown link as described above. Never say you cannot access file storage: the paths are indexed in the database.
@@ -1025,6 +1142,224 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1025
1142
  return "\u2026" + label.slice(label.length - (LINK_LABEL_MAX_DISPLAY_CHARS - 1));
1026
1143
  }
1027
1144
 
1145
+ // src/engine/retrieval_memory.ts
1146
+ var RETRIEVAL_NOTES_MAX = 6;
1147
+ var RETRIEVAL_NOTES_TOKEN_BUDGET = 1200;
1148
+ var RETRIEVAL_NOTE_MAX_ITEMS = 8;
1149
+ var RETRIEVAL_NOTE_MAX_CHARS = 600;
1150
+ var RETRIEVAL_NOTE_PREFIX = "[Retrieval note, not shown to the user";
1151
+ var FILE_KEYS = ["path", "storage_path", "storagePath", "file", "file_path", "filePath"];
1152
+ var TABLE_KEYS = ["table_name", "tableName"];
1153
+ var GROUP_KEYS = ["access_group", "accessGroup"];
1154
+ var SRC_PREFIX = "src::";
1155
+ var RESULT_WALK_MAX_NODES = 400;
1156
+ var RESULT_WALK_MAX_DEPTH = 5;
1157
+ function isWebToolName(name) {
1158
+ return typeof name === "string" && /^web_(search|fetch)/.test(name);
1159
+ }
1160
+ function pushUnique(list, value) {
1161
+ if (typeof value !== "string") return;
1162
+ var v = value.trim();
1163
+ if (!v || list.indexOf(v) !== -1 || list.length >= RETRIEVAL_NOTE_MAX_ITEMS) return;
1164
+ list.push(v);
1165
+ }
1166
+ function pushGroup(list, value) {
1167
+ if (typeof value === "number" && isFinite(value)) pushUnique(list, String(value));
1168
+ else pushUnique(list, value);
1169
+ }
1170
+ function pushHit(list, path, page) {
1171
+ if (typeof path !== "string" || !path.trim()) return;
1172
+ var n = typeof page === "number" ? page : Number(page);
1173
+ if (!isFinite(n) || n <= 0) return;
1174
+ n = Math.floor(n);
1175
+ for (var i = 0; i < list.length; i++) {
1176
+ if (list[i].path === path && list[i].page === n) return;
1177
+ }
1178
+ if (list.length >= RETRIEVAL_NOTE_MAX_ITEMS) return;
1179
+ list.push({ path: path.trim(), page: n });
1180
+ }
1181
+ function pathFromSrcId(value) {
1182
+ if (typeof value !== "string") return null;
1183
+ if (value.indexOf(SRC_PREFIX) !== 0) return null;
1184
+ var rest = value.slice(SRC_PREFIX.length).trim();
1185
+ return rest ? rest : null;
1186
+ }
1187
+ function parseJsonLoose(value) {
1188
+ if (typeof value !== "string") return value;
1189
+ var t = value.trim();
1190
+ if (!t || t[0] !== "{" && t[0] !== "[") return null;
1191
+ try {
1192
+ return JSON.parse(t);
1193
+ } catch (e) {
1194
+ return null;
1195
+ }
1196
+ }
1197
+ function readArguments(note, args) {
1198
+ if (!args || typeof args !== "object") return;
1199
+ var i;
1200
+ for (i = 0; i < FILE_KEYS.length; i++) pushUnique(note.files, args[FILE_KEYS[i]]);
1201
+ for (i = 0; i < TABLE_KEYS.length; i++) pushUnique(note.tables, args[TABLE_KEYS[i]]);
1202
+ for (i = 0; i < GROUP_KEYS.length; i++) pushGroup(note.accessGroups, args[GROUP_KEYS[i]]);
1203
+ if (typeof args.table === "string") pushUnique(note.tables, args.table);
1204
+ else if (args.table && typeof args.table === "object") {
1205
+ pushUnique(note.tables, args.table.name);
1206
+ pushGroup(note.accessGroups, args.table.access_group);
1207
+ }
1208
+ var ref = pathFromSrcId(args.reference);
1209
+ if (ref) pushUnique(note.files, ref);
1210
+ var uid = pathFromSrcId(args.unique_id);
1211
+ if (uid) pushUnique(note.files, uid);
1212
+ }
1213
+ function readHits(note, result) {
1214
+ var parsed = parseJsonLoose(result);
1215
+ if (!parsed || typeof parsed !== "object") return;
1216
+ var visited = 0;
1217
+ var walk = function(node, depth) {
1218
+ if (!node || typeof node !== "object" || depth > RESULT_WALK_MAX_DEPTH) return;
1219
+ if (++visited > RESULT_WALK_MAX_NODES) return;
1220
+ if (Array.isArray(node)) {
1221
+ for (var i = 0; i < node.length && visited <= RESULT_WALK_MAX_NODES; i++) walk(node[i], depth + 1);
1222
+ return;
1223
+ }
1224
+ if (typeof node.path === "string" && node.page !== void 0) {
1225
+ pushHit(note.hits, node.path, node.page);
1226
+ }
1227
+ for (var k in node) {
1228
+ if (!Object.prototype.hasOwnProperty.call(node, k)) continue;
1229
+ var v = node[k];
1230
+ if (v && typeof v === "object") walk(v, depth + 1);
1231
+ }
1232
+ };
1233
+ walk(parsed, 0);
1234
+ }
1235
+ function claudeResultText(block) {
1236
+ var c = block && block.content;
1237
+ if (typeof c === "string") return c;
1238
+ if (Array.isArray(c)) {
1239
+ return c.map(function(part) {
1240
+ return part && typeof part.text === "string" ? part.text : "";
1241
+ }).join("\n");
1242
+ }
1243
+ return "";
1244
+ }
1245
+ function emptyNote(source) {
1246
+ return { files: [], tables: [], accessGroups: [], hits: [], tools: [], web: false, source };
1247
+ }
1248
+ function noteHasContent(note) {
1249
+ return note.files.length > 0 || note.tables.length > 0 || note.accessGroups.length > 0 || note.hits.length > 0 || note.tools.length > 0 || note.web;
1250
+ }
1251
+ function noteFromToolBlocks(body) {
1252
+ if (!body || typeof body !== "object") return null;
1253
+ var note = emptyNote("tools");
1254
+ var sawTool = false;
1255
+ var i;
1256
+ var content = body.content;
1257
+ if (Array.isArray(content)) {
1258
+ var callNames = {};
1259
+ for (i = 0; i < content.length; i++) {
1260
+ var block = content[i];
1261
+ if (!block || typeof block !== "object") continue;
1262
+ var type = block.type;
1263
+ if (type === "mcp_tool_use" || type === "tool_use") {
1264
+ sawTool = true;
1265
+ pushUnique(note.tools, block.name);
1266
+ if (typeof block.id === "string" && typeof block.name === "string") callNames[block.id] = block.name;
1267
+ readArguments(note, block.input);
1268
+ } else if (type === "server_tool_use" || type === "web_search_tool_use") {
1269
+ sawTool = true;
1270
+ if (isWebToolName(block.name) || type === "web_search_tool_use") note.web = true;
1271
+ pushUnique(note.tools, block.name || type);
1272
+ } else if (type === "mcp_tool_result" || type === "tool_result") {
1273
+ sawTool = true;
1274
+ var name = typeof block.tool_use_id === "string" ? callNames[block.tool_use_id] : void 0;
1275
+ if (!block.is_error && (name === void 0 || name === "searchText")) readHits(note, claudeResultText(block));
1276
+ } else if (type === "web_search_tool_result" || type === "web_fetch_tool_result") {
1277
+ sawTool = true;
1278
+ note.web = true;
1279
+ }
1280
+ }
1281
+ }
1282
+ var output = body.output;
1283
+ if (Array.isArray(output)) {
1284
+ for (i = 0; i < output.length; i++) {
1285
+ var item = output[i];
1286
+ if (!item || typeof item !== "object") continue;
1287
+ var kind = typeof item.type === "string" ? item.type : "";
1288
+ if (kind === "mcp_call" || kind === "function_call") {
1289
+ sawTool = true;
1290
+ pushUnique(note.tools, item.name);
1291
+ readArguments(note, parseJsonLoose(item.arguments) || item.arguments);
1292
+ if (!item.error && item.name === "searchText") readHits(note, item.output);
1293
+ } else if (kind.indexOf("web_search") === 0 || kind.indexOf("web_fetch") === 0) {
1294
+ sawTool = true;
1295
+ note.web = true;
1296
+ pushUnique(note.tools, kind);
1297
+ }
1298
+ }
1299
+ }
1300
+ return sawTool ? note : null;
1301
+ }
1302
+ function retrievalNoteFromText(text) {
1303
+ if (typeof text !== "string" || !text) return null;
1304
+ var note = emptyNote("answer");
1305
+ var link = /\]\(db:([^)\s"]+)/g;
1306
+ var m;
1307
+ while ((m = link.exec(text)) !== null) pushUnique(note.files, m[1]);
1308
+ var src = /src::([^\s"'`<>)\]]+)/g;
1309
+ while ((m = src.exec(text)) !== null) {
1310
+ pushUnique(note.files, m[1].replace(/[.,;:!?]+$/, ""));
1311
+ }
1312
+ return noteHasContent(note) ? note : null;
1313
+ }
1314
+ function extractRetrievalNote(body, answerText) {
1315
+ var fromTools = noteFromToolBlocks(body);
1316
+ var fromText = retrievalNoteFromText(answerText);
1317
+ if (!fromTools) {
1318
+ if (fromText) return fromText;
1319
+ return providerBodyWasRead(body) ? emptyNote("body") : null;
1320
+ }
1321
+ if (fromText) {
1322
+ for (var i = 0; i < fromText.files.length; i++) pushUnique(fromTools.files, fromText.files[i]);
1323
+ }
1324
+ return noteHasContent(fromTools) ? fromTools : emptyNote("body");
1325
+ }
1326
+ function providerBodyWasRead(body) {
1327
+ if (!body || typeof body !== "object") return false;
1328
+ return Array.isArray(body.content) || Array.isArray(body.output);
1329
+ }
1330
+ function isRetrievalNote(value) {
1331
+ return !!value && typeof value === "object" && Array.isArray(value.files) && Array.isArray(value.tables) && Array.isArray(value.accessGroups) && Array.isArray(value.hits) && Array.isArray(value.tools);
1332
+ }
1333
+ function formatRetrievalNote(note) {
1334
+ if (!note || !noteHasContent(note)) return "";
1335
+ var parts = [];
1336
+ if (note.files.length) parts.push("files " + note.files.join(", "));
1337
+ if (note.tables.length) parts.push("tables " + note.tables.join(", "));
1338
+ if (note.accessGroups.length) parts.push("access_group " + note.accessGroups.join(", "));
1339
+ if (note.hits.length) {
1340
+ var byFile = {};
1341
+ var order = [];
1342
+ for (var i = 0; i < note.hits.length; i++) {
1343
+ var h = note.hits[i];
1344
+ if (!byFile[h.path]) {
1345
+ byFile[h.path] = [];
1346
+ order.push(h.path);
1347
+ }
1348
+ byFile[h.path].push(h.page);
1349
+ }
1350
+ parts.push("searchText hits " + order.map(function(p) {
1351
+ return p + " p" + byFile[p].join(", p");
1352
+ }).join("; "));
1353
+ }
1354
+ if (note.tools.length) parts.push("tools " + note.tools.join(", "));
1355
+ if (note.web) parts.push("web tool used");
1356
+ var line = RETRIEVAL_NOTE_PREFIX + ": this turn used " + parts.join(" | ") + "]";
1357
+ if (line.length > RETRIEVAL_NOTE_MAX_CHARS) {
1358
+ line = line.slice(0, RETRIEVAL_NOTE_MAX_CHARS - 4) + "...]";
1359
+ }
1360
+ return line;
1361
+ }
1362
+
1028
1363
  // src/engine/budget.ts
1029
1364
  var CONTEXT_WINDOW_DEFAULT = { claude: 2e5, openai: 128e3 };
1030
1365
  var CONTEXT_WINDOW_BY_MODEL = {
@@ -1177,6 +1512,23 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1177
1512
  if (!content) return content;
1178
1513
  return content.replace(/```([^\n`]+?\.[^\s.`]+)\n[\s\S]*?```/g, "[file previously attached: $1]");
1179
1514
  }
1515
+ function applyRetrievalNotes(messages, latestIndex) {
1516
+ var out = messages.slice();
1517
+ var used = 0, tokens = 0;
1518
+ for (var i = messages.length - 1; i >= 0; i--) {
1519
+ if (used >= RETRIEVAL_NOTES_MAX) break;
1520
+ var m = messages[i];
1521
+ if (i === latestIndex || !m || m.role !== "assistant" || !isRetrievalNote(m._retrieval)) continue;
1522
+ var line = formatRetrievalNote(m._retrieval);
1523
+ if (!line) continue;
1524
+ var cost = estimateTextTokens(line) + 2;
1525
+ if (tokens + cost > RETRIEVAL_NOTES_TOKEN_BUDGET) break;
1526
+ tokens += cost;
1527
+ used++;
1528
+ out[i] = Object.assign({}, m, { content: (m.content || "") + "\n\n" + line });
1529
+ }
1530
+ return { messages: out, notesUsed: used, noteTokens: tokens };
1531
+ }
1180
1532
  function buildBoundedChatMessages(options) {
1181
1533
  var contextBasedBudget = contextBasedBudgetFor(options.platform, options.model, options.projectId);
1182
1534
  var availableInputBudget = getInputTokenBudget(options.platform, options.model, options.projectId);
@@ -1192,6 +1544,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1192
1544
  );
1193
1545
  var windowed = options.history.slice(-maxHistoryMessages);
1194
1546
  var latestIndex = windowed.length - 1;
1547
+ if (options.retrievalNotes !== false) windowed = applyRetrievalNotes(windowed, latestIndex).messages;
1195
1548
  var trimmed = windowed.map(function(m, i2) {
1196
1549
  if (i2 === latestIndex) return m;
1197
1550
  var stripped = stripFileBlocksFromHistory(m.content);
@@ -1215,6 +1568,105 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1215
1568
  };
1216
1569
  }
1217
1570
 
1571
+ // src/engine/citations.ts
1572
+ var DATA_TOOL_NAMES = [
1573
+ "getRecords",
1574
+ "queryGrid",
1575
+ "readFileContent",
1576
+ "searchText",
1577
+ "getFileOutline",
1578
+ "getUniqueId",
1579
+ "getTables",
1580
+ "getTags",
1581
+ "getIndexes"
1582
+ ];
1583
+ var NO_DATA_TOOL_MARK = "No project data was read for this answer";
1584
+ function citationFileName(path) {
1585
+ var segs = String(path || "").split("/");
1586
+ for (var i = segs.length - 1; i >= 0; i--) {
1587
+ if (segs[i]) return segs[i];
1588
+ }
1589
+ return String(path || "");
1590
+ }
1591
+ function citationLabel(path, page) {
1592
+ return citationFileName(path) + (typeof page === "number" && page > 0 ? " p." + page : "");
1593
+ }
1594
+ function isMintablePath(path) {
1595
+ return !!path && !isHttpUrlLike(path);
1596
+ }
1597
+ function citationsFor(note) {
1598
+ if (!isRetrievalNote(note)) return [];
1599
+ var out = [];
1600
+ var hitPaths = [];
1601
+ var byPath = {};
1602
+ var i;
1603
+ for (i = 0; i < note.hits.length; i++) {
1604
+ var h = note.hits[i];
1605
+ if (!h || typeof h.path !== "string" || !isMintablePath(h.path)) continue;
1606
+ var page = typeof h.page === "number" ? h.page : Number(h.page);
1607
+ if (!isFinite(page) || page <= 0) continue;
1608
+ page = Math.floor(page);
1609
+ if (!byPath[h.path]) {
1610
+ byPath[h.path] = [];
1611
+ hitPaths.push(h.path);
1612
+ }
1613
+ if (byPath[h.path].indexOf(page) === -1) byPath[h.path].push(page);
1614
+ }
1615
+ for (i = 0; i < hitPaths.length; i++) {
1616
+ var p = hitPaths[i];
1617
+ var pages = byPath[p].slice().sort(function(a, b) {
1618
+ return a - b;
1619
+ });
1620
+ for (var j = 0; j < pages.length; j++) {
1621
+ out.push({ path: p, page: pages[j], label: citationLabel(p, pages[j]) });
1622
+ }
1623
+ }
1624
+ for (i = 0; i < note.files.length; i++) {
1625
+ var f = note.files[i];
1626
+ if (typeof f !== "string" || !isMintablePath(f)) continue;
1627
+ if (byPath[f]) continue;
1628
+ var dup = false;
1629
+ for (var k = 0; k < out.length; k++) {
1630
+ if (out[k].path === f && out[k].page === void 0) {
1631
+ dup = true;
1632
+ break;
1633
+ }
1634
+ }
1635
+ if (dup) continue;
1636
+ out.push({ path: f, label: citationLabel(f) });
1637
+ }
1638
+ return out;
1639
+ }
1640
+ function isDataToolName(name) {
1641
+ return DATA_TOOL_NAMES.indexOf(name) !== -1;
1642
+ }
1643
+ function answerUsedNoDataTool(note) {
1644
+ if (!isRetrievalNote(note)) return false;
1645
+ if (note.web) return false;
1646
+ for (var i = 0; i < note.tools.length; i++) {
1647
+ if (isDataToolName(note.tools[i])) return false;
1648
+ }
1649
+ return true;
1650
+ }
1651
+ function retrievalNoteOfAnswer(msg) {
1652
+ if (!msg || msg.role !== "assistant") return null;
1653
+ if (msg.isError || msg.isPending || msg.isBackgroundTask || msg._streamPending) return null;
1654
+ return isRetrievalNote(msg._retrieval) ? msg._retrieval : null;
1655
+ }
1656
+ function citationLinkPart(citation, resolveFreshHref) {
1657
+ var expiredHref = buildDisplayExpiredAttachmentHref(citation.path, citation.label);
1658
+ var cached = resolveFreshHref ? resolveFreshHref(expiredHref) : void 0;
1659
+ return {
1660
+ type: "link",
1661
+ label: truncateLabelForDisplay(citation.label),
1662
+ fullLabel: citation.label,
1663
+ href: cached || expiredHref,
1664
+ expired: !cached,
1665
+ expiredHref,
1666
+ remotePath: citation.path
1667
+ };
1668
+ }
1669
+
1218
1670
  // src/engine/download_encoding.ts
1219
1671
  var BOM = "\uFEFF";
1220
1672
  var BOM_EXTS = /* @__PURE__ */ new Set(["csv", "tsv", "tab", "txt", "text", "log"]);
@@ -2130,9 +2582,25 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2130
2582
  var ANTHROPIC_BETA_HEADER = `${ANTHROPIC_MCP_BETA},${ANTHROPIC_WEB_FETCH_BETA},${ANTHROPIC_PROMPT_CACHING_BETA}`;
2131
2583
  var WEB_FETCH_MAX_USES = 40;
2132
2584
  var WEB_FETCH_MAX_CONTENT_TOKENS = 2e5;
2585
+ var claudeWebFetchTool = () => ({
2586
+ type: "web_fetch_20250910",
2587
+ name: "web_fetch",
2588
+ max_uses: WEB_FETCH_MAX_USES,
2589
+ citations: { enabled: true },
2590
+ max_content_tokens: WEB_FETCH_MAX_CONTENT_TOKENS
2591
+ });
2133
2592
  var OPENAI_RESPONSES_API_URL = "https://api.openai.com/v1/responses";
2134
2593
  var DEFAULT_OPENAI_IMAGE_DETAIL = "auto";
2135
2594
  var OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS = true;
2595
+ var openaiWebSearchTool = () => ({
2596
+ type: "web_search",
2597
+ external_web_access: OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS
2598
+ });
2599
+ var webToolsForChat = (platform, webAccess) => {
2600
+ if (webAccess !== true) return [];
2601
+ if (platform === "claude") return [claudeWebFetchTool()];
2602
+ return [openaiWebSearchTool()] ;
2603
+ };
2136
2604
  var MCP_NAME = "BunnyQuery";
2137
2605
  var DEFAULT_CLAUDE_MODEL = "claude-sonnet-5";
2138
2606
  var DEFAULT_OPENAI_MODEL = "gpt-5.6-luna";
@@ -2148,6 +2616,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2148
2616
  }
2149
2617
  var mcpContextParam = (platform, model) => getModelContextWindow(platform, model);
2150
2618
  var mcpIndexingUrl = (platform = "openai", model) => withMcpParams(mcpUrl(), { profile: "index", ctx: mcpContextParam(platform, model) });
2619
+ var mcpChatUrl = (endpointUrl, platform, model) => withMcpParams(endpointUrl, { profile: "chat", ctx: mcpContextParam(platform, model) });
2151
2620
  function mcpEndpointFor(anonymous, publicProjectId, service) {
2152
2621
  if (!anonymous) return { url: mcpUrl(), token: "$ACCESS_TOKEN" };
2153
2622
  const project = publicProjectId || service;
@@ -2333,8 +2802,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2333
2802
  system,
2334
2803
  mcpServer,
2335
2804
  extractContent,
2336
- fileUrls
2805
+ fileUrls,
2806
+ webAccess
2337
2807
  }) {
2808
+ const web = typeof webAccess === "boolean" ? webAccess : await resolveWebAccess(service);
2338
2809
  const mcpServerDefinition = {
2339
2810
  type: "url",
2340
2811
  name: mcpServer.name,
@@ -2393,13 +2864,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2393
2864
  ...mcpServer.defaultConfig ? { default_config: mcpServer.defaultConfig } : {},
2394
2865
  ...mcpServer.configs ? { configs: mcpServer.configs } : {}
2395
2866
  },
2396
- {
2397
- type: "web_fetch_20250910",
2398
- name: "web_fetch",
2399
- max_uses: WEB_FETCH_MAX_USES,
2400
- citations: { enabled: true },
2401
- max_content_tokens: WEB_FETCH_MAX_CONTENT_TOKENS
2402
- }
2867
+ // `web_fetch` rides here only when the project's web switch is on. The
2868
+ // beta header above is left as it is: it only makes the tool TYPE
2869
+ // available, and a request that does not list the tool cannot use it.
2870
+ ...webToolsForChat("claude", web)
2403
2871
  ]
2404
2872
  }
2405
2873
  });
@@ -2419,9 +2887,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2419
2887
  fileUrls,
2420
2888
  mcpServer: {
2421
2889
  name: MCP_NAME,
2422
- url: withMcpParams(endpoint.url, {
2423
- ctx: mcpContextParam("claude", model || DEFAULT_CLAUDE_MODEL)
2424
- }),
2890
+ url: mcpChatUrl(endpoint.url, "claude", model || DEFAULT_CLAUDE_MODEL),
2425
2891
  // Omitted entirely for an anonymous turn; the `if (mcpServer.authorizationToken)`
2426
2892
  // guard below drops the key rather than sending an empty one.
2427
2893
  authorizationToken: endpoint.token
@@ -2431,6 +2897,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2431
2897
  const endpoint = mcpEndpointFor(mcpScope?.anonymous, mcpScope?.publicProjectId, service);
2432
2898
  const resolvedModel = model || DEFAULT_OPENAI_MODEL;
2433
2899
  const imageDetail = getOpenAIImageDetail(resolvedModel);
2900
+ const web = await resolveWebAccess(service);
2434
2901
  const messageList = messages && messages.length ? prepareOpenAIMessages(messages, imageDetail) : [
2435
2902
  {
2436
2903
  role: "user",
@@ -2476,19 +2943,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2476
2943
  {
2477
2944
  type: "mcp",
2478
2945
  server_label: MCP_NAME,
2479
- server_url: withMcpParams(endpoint.url, { ctx: mcpContextParam("openai", resolvedModel) }),
2946
+ server_url: mcpChatUrl(endpoint.url, "openai", resolvedModel),
2480
2947
  require_approval: "never",
2481
2948
  // No `headers` at all for an anonymous turn: `Bearer ` with an
2482
2949
  // empty token is a credential the MCP server rejects, and the
2483
2950
  // project-scoped endpoint needs none.
2484
2951
  ...endpoint.token ? { headers: { Authorization: "Bearer " + endpoint.token } } : {}
2485
2952
  },
2486
- ...[
2487
- {
2488
- type: "web_search",
2489
- external_web_access: OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS
2490
- }
2491
- ]
2953
+ // `web_search` rides here only when the project's web switch is on.
2954
+ ...webToolsForChat("openai", web)
2492
2955
  ]
2493
2956
  }
2494
2957
  });
@@ -2636,12 +3099,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2636
3099
  require_approval: "never",
2637
3100
  headers: { Authorization: "Bearer $ACCESS_TOKEN" }
2638
3101
  },
2639
- ...[
2640
- {
2641
- type: "web_search",
2642
- external_web_access: OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS
2643
- }
2644
- ]
3102
+ // UNCONDITIONAL, unlike a chat turn: the project's web switch
3103
+ // governs answers, not indexing. Kept exactly as it was.
3104
+ ...[openaiWebSearchTool()]
2645
3105
  ]
2646
3106
  }
2647
3107
  }));
@@ -2694,13 +3154,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2694
3154
  type: "mcp_toolset",
2695
3155
  mcp_server_name: MCP_NAME
2696
3156
  },
2697
- {
2698
- type: "web_fetch_20250910",
2699
- name: "web_fetch",
2700
- max_uses: WEB_FETCH_MAX_USES,
2701
- citations: { enabled: true },
2702
- max_content_tokens: WEB_FETCH_MAX_CONTENT_TOKENS
2703
- }
3157
+ // UNCONDITIONAL, unlike a chat turn: the project's web switch governs
3158
+ // answers, not indexing. Kept exactly as it was.
3159
+ claudeWebFetchTool()
2704
3160
  ]
2705
3161
  }
2706
3162
  }));
@@ -3111,8 +3567,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3111
3567
  var createdTs = Number(item && item.created);
3112
3568
  var updatedTs = Number(item && item.updated);
3113
3569
  var executedTs = Number(item && item.executed);
3570
+ var respondedTs = Number(item && item.responded);
3114
3571
  var userTs = isFinite(createdTs) && createdTs > 0 ? createdTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : void 0;
3115
- var replyTs = isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
3572
+ var replyTs = isFinite(respondedTs) && respondedTs > 0 ? respondedTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
3116
3573
  if (userText) {
3117
3574
  var displayContent;
3118
3575
  var indexFile = void 0;
@@ -3179,6 +3636,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3179
3636
  if (item._fromBgChain) okm._fromBgChain = true;
3180
3637
  if (item._isBgTask) okm.isBackgroundTask = true;
3181
3638
  if (isCompact) okm._compact = true;
3639
+ if (!item._isBgTask && !isCompact) {
3640
+ var note = extractRetrievalNote(response, assistantText);
3641
+ if (note) okm._retrieval = note;
3642
+ }
3182
3643
  if (serverItemId !== void 0) okm._serverItemId = serverItemId;
3183
3644
  if (replyTs !== void 0) okm._ts = replyTs;
3184
3645
  if (indexFile && isFinite(executedTs) && executedTs > 0) okm._tsStart = executedTs;
@@ -3739,6 +4200,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3739
4200
  // the request budget MAX_CONCURRENT_BG_POLLS exists to protect.
3740
4201
  /** Live streams by server item id. One per in-flight streamed turn. */
3741
4202
  this.liveStreams = {};
4203
+ /** When skapi recorded a turn's response as having finished arriving, by server
4204
+ * item id. Read off the terminal envelope of a STREAMED turn (the only settle
4205
+ * that carries it, see _settleLiveStream) and used as the answer bubble's time
4206
+ * in place of this browser's clock. Entries are never removed: a settle reaches
4207
+ * here twice (the poll's onResponse, then the promise), and the value for an id
4208
+ * cannot change. */
4209
+ this._respondedAt = {};
3742
4210
  // ─── compact-stub hydration ─────────────────────────────────────────────
3743
4211
  // Split-fetch bg pages arrive as label stubs (no bodies). When the user
3744
4212
  // expands a row, the real reply text is fetched per item (csr-poll point
@@ -4264,6 +4732,23 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4264
4732
  });
4265
4733
  return n;
4266
4734
  }
4735
+ _noteRespondedAt(itemId, response) {
4736
+ if (!itemId || !isCsrStatusEnvelope(response)) return;
4737
+ var at = Number(response.responded);
4738
+ if (!isFinite(at) || at <= 0) return;
4739
+ if (!this._respondedAt) this._respondedAt = {};
4740
+ this._respondedAt[itemId] = at;
4741
+ }
4742
+ /** The time a live answer bubble is stamped with: when skapi received the
4743
+ * response, if the settle told us, else now. "Now" is what every bubble got
4744
+ * before, and it is still right for a buffered turn, whose settle hands back the
4745
+ * destination's body and nothing of skapi's; a history load then replaces it
4746
+ * with the server's value. */
4747
+ _replyStampFor(msg) {
4748
+ var id = msg ? msg._serverItemId : void 0;
4749
+ var at = typeof id === "string" && id && this._respondedAt ? this._respondedAt[id] : void 0;
4750
+ return typeof at === "number" ? at : wallClockNow();
4751
+ }
4267
4752
  /**
4268
4753
  * Open (or re-open) the live stream for `itemId`, or null when this poll must
4269
4754
  * not carry one.
@@ -4453,6 +4938,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4453
4938
  _settleLiveStream(st, response) {
4454
4939
  this._closeLiveStream(st, true);
4455
4940
  if (!isCsrStatusEnvelope(response)) return response;
4941
+ this._noteRespondedAt(st.id, response);
4456
4942
  if (response.status !== "resolved") return response;
4457
4943
  if (!this._mayFinalize(st)) this._rec().incomplete[st.id] = true;
4458
4944
  if (st.finalBody == null) return response;
@@ -4849,6 +5335,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
4849
5335
  return;
4850
5336
  }
4851
5337
  var reply = isErr ? { role: "assistant", content: getErrorMessage(body), isError: true, _serverItemId: itemId } : { role: "assistant", content: text, _serverItemId: itemId };
5338
+ if (!isErr) {
5339
+ var rNote = extractRetrievalNote(body, text);
5340
+ if (rNote) reply._retrieval = rNote;
5341
+ }
4852
5342
  if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
4853
5343
  this._applyReplyToCache(ownerKey, reply, itemId);
4854
5344
  } else {
@@ -5257,7 +5747,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5257
5747
  if (isErrorResponseBody(response)) return { content: getErrorMessage(response), isError: true };
5258
5748
  var answer = params.aiPlatform === "openai" ? extractOpenAIText(response) : extractClaudeText(response);
5259
5749
  answer = (answer || "").trim();
5260
- return { content: answer || "No text response received from AI provider.", isError: false };
5750
+ return { content: answer || "No text response received from AI provider.", isError: false, retrieval: extractRetrievalNote(response, answer) };
5261
5751
  }).catch(function(err) {
5262
5752
  return { content: getErrorMessage(err), isError: true };
5263
5753
  }).then(function(result) {
@@ -5269,6 +5759,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5269
5759
  }
5270
5760
  var reply = { role: "assistant", content: result.content, isError: result.isError };
5271
5761
  if (dispatchItemId) reply._serverItemId = dispatchItemId;
5762
+ if (result.retrieval) reply._retrieval = result.retrieval;
5272
5763
  self._applyReplyToCache(params.key, reply, dispatchItemId);
5273
5764
  return result;
5274
5765
  });
@@ -5894,7 +6385,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5894
6385
  return thinkingIdx !== -1 ? thinkingIdx : userIdx >= 0 ? userIdx + 1 : -1;
5895
6386
  }
5896
6387
  insertAtTarget(msg, targetIdx) {
5897
- if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = wallClockNow();
6388
+ if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = this._replyStampFor(msg);
5898
6389
  var tgt = targetIdx >= 0 ? this.state.messages[targetIdx] : void 0;
5899
6390
  var replaceable = !!tgt && !!tgt.isPending && !tgt.isBackgroundTask && this._isOwnPlaceholderOf(targetIdx, this._owningUserIndex(targetIdx));
5900
6391
  if (replaceable) this.state.messages[targetIdx] = msg;
@@ -5932,6 +6423,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5932
6423
  }
5933
6424
  if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
5934
6425
  var offReply = isErrorResponseBody(response) ? { role: "assistant", content: getErrorMessage(response), isError: true } : { role: "assistant", content: ((platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "").trim() || "No text response received from AI provider." };
6426
+ if (!offReply.isError) {
6427
+ var offNote = extractRetrievalNote(response, offReply.content);
6428
+ if (offNote) offReply._retrieval = offNote;
6429
+ }
5935
6430
  this._applyReplyToCache(ownerKey, offReply, serverId);
5936
6431
  if (serverId) this.cancelledServerIds.delete(serverId);
5937
6432
  return;
@@ -5953,6 +6448,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5953
6448
  } else {
5954
6449
  var answer = platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response);
5955
6450
  answer = (answer || "").trim() || "No text response received from AI provider.";
6451
+ var qNote = extractRetrievalNote(response, answer);
5956
6452
  var lid = this._newLocalId();
5957
6453
  if (targetIdx >= 0 && this.state.messages[targetIdx] && this.state.messages[targetIdx].isPending) {
5958
6454
  var qPainted = this._paintedTextAt(targetIdx);
@@ -5960,16 +6456,21 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5960
6456
  var qSettled = { role: "assistant", content: qPainted, _localId: lid };
5961
6457
  if (prevQ._serverItemId) qSettled._serverItemId = prevQ._serverItemId;
5962
6458
  if (prevQ._ownerKey) qSettled._ownerKey = prevQ._ownerKey;
6459
+ if (qNote) qSettled._retrieval = qNote;
5963
6460
  this.state.messages[targetIdx] = qSettled;
5964
6461
  this.host.notify();
5965
6462
  this.enqueueTypewrite(targetIdx, answer, lid, qPainted);
5966
6463
  } else if (targetIdx >= 0) {
5967
- this.state.messages.splice(targetIdx, 0, { role: "assistant", content: "", _localId: lid });
6464
+ var qInsert = { role: "assistant", content: "", _localId: lid };
6465
+ if (qNote) qInsert._retrieval = qNote;
6466
+ this.state.messages.splice(targetIdx, 0, qInsert);
5968
6467
  this.host.notify();
5969
6468
  this.enqueueTypewrite(targetIdx, answer, lid);
5970
6469
  } else {
5971
6470
  var aiIdx = this.state.messages.length;
5972
- this.state.messages.push({ role: "assistant", content: "", _localId: lid });
6471
+ var qAppend = { role: "assistant", content: "", _localId: lid };
6472
+ if (qNote) qAppend._retrieval = qNote;
6473
+ this.state.messages.push(qAppend);
5973
6474
  this.host.notify();
5974
6475
  this.enqueueTypewrite(aiIdx, answer, lid);
5975
6476
  }
@@ -6517,7 +7018,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6517
7018
  enqueueTypewrite(idx, fullText, localId, paintedText) {
6518
7019
  var self = this;
6519
7020
  var target = this.state.messages[idx];
6520
- if (target && target._ts === void 0) target._ts = wallClockNow();
7021
+ if (target && target._ts === void 0) target._ts = this._replyStampFor(target);
6521
7022
  if (!this.typewriterQueue) this.typewriterQueue = Promise.resolve();
6522
7023
  this.typewriterQueue = this.typewriterQueue.then(function() {
6523
7024
  return self.typewriteIntoIndex(idx, fullText, localId, paintedText);
@@ -6559,6 +7060,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6559
7060
  var settled = { role: "assistant", content: painted, isPending: false, _localId: lid };
6560
7061
  if (prevSettled._serverItemId) settled._serverItemId = prevSettled._serverItemId;
6561
7062
  if (prevSettled._ownerKey) settled._ownerKey = prevSettled._ownerKey;
7063
+ if (latest._retrieval) settled._retrieval = latest._retrieval;
6562
7064
  this.state.messages[pendingIdx] = settled;
6563
7065
  this._removeStrayPendingAssistants();
6564
7066
  this.host.notify();
@@ -6779,6 +7281,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6779
7281
  var isErr = isErrorResponseBody(response);
6780
7282
  var answer = isErr ? getErrorMessage(response) : ((platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "").trim();
6781
7283
  var reportedComplete = !isErr && !!answer && answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1;
7284
+ var hNote = isErr ? null : extractRetrievalNote(response, answer);
6782
7285
  var stripMarker = function(t) {
6783
7286
  return reportedComplete ? t.split(INDEXING_COMPLETE_MARKER).join("").trim() : t;
6784
7287
  };
@@ -6804,7 +7307,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6804
7307
  }
6805
7308
  var hPainted = this._paintedTextAt(idx);
6806
7309
  var lid = this._newLocalId();
6807
- this.state.messages[idx] = { role: "assistant", content: hPainted, _localId: lid, _serverItemId: itemId };
7310
+ var hReply = { role: "assistant", content: hPainted, _localId: lid, _serverItemId: itemId };
7311
+ if (hNote) hReply._retrieval = hNote;
7312
+ this.state.messages[idx] = hReply;
6808
7313
  this.host.notify();
6809
7314
  this.enqueueTypewrite(idx, text, lid, hPainted);
6810
7315
  this.updateHistoryCache();
@@ -6838,6 +7343,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
6838
7343
  }
6839
7344
  var lid2 = this._newLocalId();
6840
7345
  var reply = { role: "assistant", content: "", _localId: lid2, _serverItemId: itemId };
7346
+ if (hNote) reply._retrieval = hNote;
6841
7347
  this.state.messages.splice(userIdx + 1, 0, reply);
6842
7348
  this.host.notify();
6843
7349
  this.enqueueTypewrite(userIdx + 1, text2, lid2);
@@ -8502,102 +9008,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8502
9008
  return out;
8503
9009
  }
8504
9010
 
8505
- // src/engine/project_settings.ts
8506
- var UPLOAD_ACCESS_GROUPS = ["public", "authorized", "private"];
8507
- var DEFAULT_UPLOAD_ACCESS_GROUP = "authorized";
8508
- var PROJECT_SETTINGS_UNIQUE_ID = "bq::settings";
8509
- var UPLOAD_ACCESS_LABELS = {
8510
- public: "Public",
8511
- authorized: "Signed in users",
8512
- private: "Only me"
8513
- };
8514
- var UPLOAD_ACCESS_HINTS = {
8515
- public: "Anyone can ask about this file, including visitors who are not logged in.",
8516
- authorized: "Only users signed in to this project can ask about this file.",
8517
- private: "Only you can ask about this file."
8518
- };
8519
- var CHAT_GREETING_MAX_LENGTH = 400;
8520
- function normalizeChatGreeting(value) {
8521
- if (typeof value !== "string") return "";
8522
- const trimmed = value.replace(/\r\n?/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
8523
- return trimmed.slice(0, CHAT_GREETING_MAX_LENGTH);
8524
- }
8525
- function chatGreetingFrom(data) {
8526
- return normalizeChatGreeting(data?.chat_greeting);
8527
- }
8528
- function normalizeUploadAccessGroup(value) {
8529
- return UPLOAD_ACCESS_GROUPS.indexOf(value) === -1 ? DEFAULT_UPLOAD_ACCESS_GROUP : value;
8530
- }
8531
- function normalizeProjectAccessSetting(value) {
8532
- if (value === "ask") return "ask";
8533
- return UPLOAD_ACCESS_GROUPS.indexOf(value) === -1 ? null : value;
8534
- }
8535
- function accessSettingFrom(data) {
8536
- return normalizeProjectAccessSetting(data?.upload_access_group);
8537
- }
8538
- function uploadAccessGroupFrom(data) {
8539
- const v = accessSettingFrom(data);
8540
- return v && v !== "ask" ? v : DEFAULT_UPLOAD_ACCESS_GROUP;
8541
- }
8542
- function asksUploadAccessFrom(data) {
8543
- return accessSettingFrom(data) === "ask";
8544
- }
8545
- var reader = null;
8546
- var cache = /* @__PURE__ */ new Map();
8547
- function configureProjectSettings(fn) {
8548
- reader = fn;
8549
- }
8550
- function entry(service) {
8551
- let e = cache.get(service);
8552
- if (!e) {
8553
- e = { data: null, settled: false, inflight: null };
8554
- cache.set(service, e);
8555
- }
8556
- return e;
8557
- }
8558
- function loadProjectSettings(service) {
8559
- if (!service) return Promise.resolve(null);
8560
- const e = entry(service);
8561
- if (e.settled) return Promise.resolve(e.data);
8562
- if (e.inflight) return e.inflight;
8563
- if (!reader) return Promise.resolve(null);
8564
- const run = reader(service).then((data) => data && typeof data === "object" ? data : null).catch(() => null).then((data) => {
8565
- const cur = entry(service);
8566
- if (cur.inflight === run) {
8567
- cur.data = data;
8568
- cur.settled = true;
8569
- cur.inflight = null;
8570
- }
8571
- return data;
8572
- });
8573
- e.inflight = run;
8574
- return run;
8575
- }
8576
- function primeProjectSettings(service) {
8577
- void loadProjectSettings(service);
8578
- }
8579
- function readyProjectSettings(service) {
8580
- return loadProjectSettings(service);
8581
- }
8582
- function cachedProjectSettings(service) {
8583
- const e = cache.get(service);
8584
- return e && e.settled ? e.data : null;
8585
- }
8586
- function projectUploadAccessGroup(service) {
8587
- return uploadAccessGroupFrom(cachedProjectSettings(service));
8588
- }
8589
- function projectAsksUploadAccess(service) {
8590
- return asksUploadAccessFrom(cachedProjectSettings(service));
8591
- }
8592
- function projectChatGreeting(service) {
8593
- return chatGreetingFrom(cachedProjectSettings(service));
8594
- }
8595
-
8596
9011
  // src/index.js
8597
9012
  (function() {
8598
9013
  var MCP_PROD = "https://mcp.broadwayinc.computer";
8599
9014
  var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
8600
- var BQ_VERSION = "1.12.1" ;
9015
+ var BQ_VERSION = "1.14.0" ;
8601
9016
  var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
8602
9017
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
8603
9018
  var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
@@ -10569,7 +10984,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
10569
10984
  // reaches it has settled the fetch first: sendMessage awaits
10570
10985
  // readyProjectSettings on the text-only branch, and the attachment
10571
10986
  // branch resolves the upload group before it dispatches.
10572
- indexAccessGroup: projectUploadAccessGroup(S.projectId)});
10987
+ indexAccessGroup: projectUploadAccessGroup(S.projectId),
10988
+ // Whether this chat may reach the open web. The request builders
10989
+ // attach web_fetch / web_search on the same switch, so the prompt
10990
+ // follows the tool list. Same sync cache read as the group above,
10991
+ // settled by the same readyProjectSettings before a send.
10992
+ webAccess: webAccessEnabled(S.projectId)});
10573
10993
  }
10574
10994
  function refreshSkapiSession() {
10575
10995
  return S.skapi.getProfile({ refreshToken: true }).then(function() {
@@ -10752,6 +11172,17 @@ Index the REMAINING windows - one record per row/item, looking at any page image
10752
11172
  unavailable: isLinkUnavailable(link, unavailableLinkMap)
10753
11173
  });
10754
11174
  }
11175
+ function citationChipsHtml(note) {
11176
+ var cites = citationsFor(note);
11177
+ var html = "";
11178
+ for (var i = 0; i < cites.length; i++) {
11179
+ var part = citationLinkPart(cites[i], function(expiredHref) {
11180
+ return refreshedExpiredLinkMap[expiredHref];
11181
+ });
11182
+ html += linkToAnchorHtml(part, false);
11183
+ }
11184
+ return html;
11185
+ }
10755
11186
  function buildLinkPartFromGroups(full, g1, g2, g3, g4, g5, g6) {
10756
11187
  return classifyInlineLink(full, [g1, g2, g3, g4, g5, g6], {
10757
11188
  projectId: S.projectId,
@@ -11932,6 +12363,16 @@ Index the REMAINING windows - one record per row/item, looking at any page image
11932
12363
  var md = h("div", { class: "bq-md", translate: "no", html: parseMsgPartsHtml(msg.content) });
11933
12364
  md.addEventListener("click", onBubbleLinkClick);
11934
12365
  bubble.appendChild(md);
12366
+ var citeNote = retrievalNoteOfAnswer(msg);
12367
+ if (citeNote) {
12368
+ var citeHtml = citationChipsHtml(citeNote);
12369
+ if (citeHtml) {
12370
+ var citeRow = h("div", { class: "bq-citations", translate: "no", html: citeHtml });
12371
+ citeRow.addEventListener("click", onBubbleLinkClick);
12372
+ bubble.appendChild(citeRow);
12373
+ }
12374
+ if (answerUsedNoDataTool(citeNote)) bubble.appendChild(h("span", { class: "bq-no-source-note", text: NO_DATA_TOOL_MARK }));
12375
+ }
11935
12376
  if (msg.isUploadingAttachments) bubble.appendChild(h("span", { class: "bq-pending-note", text: "(Uploading files...)" }));
11936
12377
  else if (msg.isAwaitingIndexing) bubble.appendChild(h("span", { class: "bq-pending-note", text: "(Indexing files...)" }));
11937
12378
  else if (msg.isPendingQueued) bubble.appendChild(h("span", { class: "bq-pending-note", text: "(In queue)" }));
@@ -12173,6 +12614,42 @@ Index the REMAINING windows - one record per row/item, looking at any page image
12173
12614
  }
12174
12615
  return byFile;
12175
12616
  }
12617
+ var stopIndexOthers = { checked: false, row: null, text: null, box: null };
12618
+ function otherStoppableIndexGroups(target) {
12619
+ if (!target) return [];
12620
+ var list = buildChatDisplayList(CS.messages, displayListOptions());
12621
+ var others = [];
12622
+ for (var i = 0; i < list.length; i++) {
12623
+ var row = list[i];
12624
+ if (row.kind !== "indexing") continue;
12625
+ if (!indexGroupStoppable(row.group) || row.group.key === target.key) continue;
12626
+ others.push(row.group);
12627
+ }
12628
+ return others;
12629
+ }
12630
+ function countIndexFiles(groups) {
12631
+ var seen = {}, n = 0;
12632
+ for (var i = 0; i < groups.length; i++) {
12633
+ if (seen[groups[i].key]) continue;
12634
+ seen[groups[i].key] = true;
12635
+ n++;
12636
+ }
12637
+ return n;
12638
+ }
12639
+ function stopOthersLabel(n) {
12640
+ return n === 1 ? "Also stop the other file being indexed" : "Also stop the " + n + " other files being indexed";
12641
+ }
12642
+ function refreshStopIndexOthers() {
12643
+ if (!stopIndexOthers.row) return;
12644
+ var n = countIndexFiles(otherStoppableIndexGroups(findCancellableIndexGroup(stopIndexState.runKey, stopIndexState.fileKey)));
12645
+ if (!n) {
12646
+ stopIndexOthers.checked = false;
12647
+ if (stopIndexOthers.box) stopIndexOthers.box.checked = false;
12648
+ } else if (stopIndexOthers.text) {
12649
+ stopIndexOthers.text.textContent = stopOthersLabel(n);
12650
+ }
12651
+ stopIndexOthers.row.style.display = n ? "" : "none";
12652
+ }
12176
12653
  function stopIndexModalIsOpen() {
12177
12654
  var hnd = stopIndexState.handle;
12178
12655
  if (hnd && hnd.root && hnd.root.parentNode) return true;
@@ -12191,6 +12668,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
12191
12668
  function syncStopIndexModal() {
12192
12669
  if (!stopIndexModalIsOpen()) return;
12193
12670
  if (!findCancellableIndexGroup(stopIndexState.runKey, stopIndexState.fileKey)) closeStopIndexModal();
12671
+ else refreshStopIndexOthers();
12194
12672
  }
12195
12673
  function openStopIndexModal(group) {
12196
12674
  if (!indexGroupStoppable(group)) return;
@@ -12203,10 +12681,24 @@ Index the REMAINING windows - one record per row/item, looking at any page image
12203
12681
  stopBtn.addEventListener("click", function() {
12204
12682
  var runKey = stopIndexState.runKey;
12205
12683
  var fileKey = stopIndexState.fileKey;
12684
+ var alsoOthers = stopIndexOthers.checked;
12206
12685
  closeStopIndexModal();
12207
12686
  var live = findCancellableIndexGroup(runKey, fileKey);
12687
+ var others = alsoOthers ? otherStoppableIndexGroups(live) : [];
12208
12688
  if (live) session.cancelIndexingGroup(live);
12689
+ for (var oi = 0; oi < others.length; oi++) session.cancelIndexingGroup(others[oi]);
12690
+ });
12691
+ var othersBox = h("input", { type: "checkbox" });
12692
+ othersBox.addEventListener("change", function() {
12693
+ stopIndexOthers.checked = !!othersBox.checked;
12209
12694
  });
12695
+ var othersText = h("span");
12696
+ var othersRow = h("label", { class: "bq-overwrite-applyall" }, othersBox, othersText);
12697
+ stopIndexOthers.checked = false;
12698
+ stopIndexOthers.box = othersBox;
12699
+ stopIndexOthers.text = othersText;
12700
+ stopIndexOthers.row = othersRow;
12701
+ refreshStopIndexOthers();
12210
12702
  return h(
12211
12703
  "div",
12212
12704
  { class: "bq-modal" },
@@ -12226,6 +12718,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
12226
12718
  { class: "bq-modal-desc bq-modal-delete-warn" },
12227
12719
  "Whatever has been indexed so far stays searchable, and the pass already running finishes on the server. The remaining passes are dropped, not paused, so the file stays partly indexed until you reindex it."
12228
12720
  ),
12721
+ othersRow,
12229
12722
  h(
12230
12723
  "div",
12231
12724
  { class: "bq-modal-btns" },
@@ -13157,16 +13650,24 @@ Index the REMAINING windows - one record per row/item, looking at any page image
13157
13650
  // null → follow the project's own "Allow anonymous users" setting
13158
13651
  // (getConnectionInfo().conf.require_login); true/false pins it.
13159
13652
  allowAnonymous: null,
13653
+ // Whether the chat's answers may reach the open web (Claude web_fetch,
13654
+ // OpenAI web_search). null = follow the project's own "Web access for
13655
+ // answers" setting on its Project Settings page (`web_access` in the
13656
+ // bq::settings record), which is off unless the owner turned it on;
13657
+ // true/false pins it for this page, both ways. Read at
13658
+ // configureChatEngine time and when each chat request is built.
13659
+ webAccess: null,
13160
13660
  // Server-driven windowed indexing; read at configureChatEngine time.
13161
13661
  // Listed here so the defaults object is the full opt surface.
13162
13662
  windowedIndexing: true,
13163
13663
  // Live streaming of chat turns; read at configureChatEngine time.
13164
- // OFF until the region's polling worker relays the response bytes:
13165
- // see the configureChatEngine call for what goes wrong without it.
13166
- // A REQUEST, not a switch: it is also refused (with a warning, falling
13664
+ // ON by default: every region's polling worker relays the response
13665
+ // bytes now, so the answer paints as it arrives instead of up to one
13666
+ // poll tick after it finished. Pass false for buffered replies.
13667
+ // Still a REQUEST, not a switch: it is refused (with a warning, falling
13167
13668
  // back to buffered) when the embedder's own skapi-js is too old to
13168
13669
  // carry skapi's half of the stream flag. See skapiSupportsStreaming.
13169
- liveStreaming: false,
13670
+ liveStreaming: true,
13170
13671
  // Socket delivery for the streamed reply. OFF unless the embedder asks,
13171
13672
  // and separately from liveStreaming, because this widget runs on someone
13172
13673
  // else's page with someone else's skapi instance: skapi's joinRealtime
@@ -13190,7 +13691,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
13190
13691
  S.booted = true;
13191
13692
  console.log("[bunnyquery] v" + BQ_VERSION);
13192
13693
  var canStream = skapiSupportsStreaming(S.skapi);
13193
- var liveStreaming = S.opts.liveStreaming === true;
13694
+ var liveStreaming = S.opts.liveStreaming == null ? true : S.opts.liveStreaming === true;
13194
13695
  if (liveStreaming && !canStream) {
13195
13696
  liveStreaming = false;
13196
13697
  console.warn(
@@ -13231,17 +13732,22 @@ Index the REMAINING windows - one record per row/item, looking at any page image
13231
13732
  windowedIndexing: S.opts.windowedIndexing !== false,
13232
13733
  // Client-side attachment parsers (e.g. an .hwp parser) passed via init opts.
13233
13734
  attachmentParsers: S.opts.attachmentParsers || void 0,
13735
+ // Web access for chat answers. Handed over ONLY when the embedder
13736
+ // pinned it: an undefined here means "ask the project", and the engine
13737
+ // then reads the project's `web_access` setting when it builds each
13738
+ // chat request (off unless the owner turned it on). Passing the null
13739
+ // default through would read as a decision.
13740
+ webAccess: typeof S.opts.webAccess === "boolean" ? S.opts.webAccess : void 0,
13234
13741
  // ---- live streaming (mirrored in agent.vue's ai_agent.ts) --------
13235
- // Off by default, and for the same shipping-order reason
13236
- // windowedIndexing had one: THE RELAYING POLLING WORKER MUST SHIP
13237
- // FIRST. With this on against a region whose worker does not relay,
13238
- // the request either has its `since` cursor rejected or the row keeps
13239
- // an SSE transcript where the readers expect a parsed document, and
13240
- // the turn reads back as an empty answer. A streamed row settles with
13241
- // a STATUS AND NO BODY on purpose, so there is no fallback to read.
13242
- // Flip it per environment once the worker is deployed there; the
13243
- // widget takes it as an init opt because an embed picks its own
13244
- // region, where agent.vue flips one module constant.
13742
+ // On by default since every region's polling worker relays the
13743
+ // response bytes. It used to be off for the shipping-order reason
13744
+ // windowedIndexing had: against a worker that does not relay, the
13745
+ // request has its `since` cursor rejected or the row keeps an SSE
13746
+ // transcript where the readers expect a parsed document, and the turn
13747
+ // reads back as an empty answer (a streamed row settles with a STATUS
13748
+ // AND NO BODY on purpose). An embed against such a worker passes
13749
+ // liveStreaming: false. The widget takes it as an init opt because an
13750
+ // embed picks its own region, where agent.vue flips one module constant.
13245
13751
  // It also needs a skapi-js that supports `stream`/`onStream`, and the
13246
13752
  // page's pin is the EMBEDDER's, so the request is granted above by
13247
13753
  // skapiSupportsStreaming rather than taken on trust here.