bunnyquery 1.13.0 → 1.15.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/dist/engine.cjs CHANGED
@@ -45,6 +45,134 @@ async function parseAttachmentContent(file, name, mime) {
45
45
  return text;
46
46
  }
47
47
 
48
+ // src/engine/project_settings.ts
49
+ var UPLOAD_ACCESS_GROUPS = ["public", "authorized", "private"];
50
+ var DEFAULT_UPLOAD_ACCESS_GROUP = "authorized";
51
+ var PROJECT_SETTINGS_TABLE = "__SETTINGS__";
52
+ var PROJECT_SETTINGS_UNIQUE_ID = "bq::settings";
53
+ var PROJECT_SETTINGS_ACCESS_GROUP = "public";
54
+ var UPLOAD_ACCESS_LABELS = {
55
+ public: "Public",
56
+ authorized: "Signed in users",
57
+ private: "Only me"
58
+ };
59
+ var UPLOAD_ACCESS_HINTS = {
60
+ public: "Anyone can ask about this file, including visitors who are not logged in.",
61
+ authorized: "Only users signed in to this project can ask about this file.",
62
+ private: "Only you can ask about this file."
63
+ };
64
+ var UPLOAD_ACCESS_OPTIONS = UPLOAD_ACCESS_GROUPS.map((value) => ({
65
+ value,
66
+ label: UPLOAD_ACCESS_LABELS[value],
67
+ hint: UPLOAD_ACCESS_HINTS[value]
68
+ }));
69
+ var DEFAULT_WEB_ACCESS = false;
70
+ function normalizeWebAccess(value) {
71
+ return value === true;
72
+ }
73
+ function webAccessFrom(data) {
74
+ return data ? normalizeWebAccess(data.web_access) : DEFAULT_WEB_ACCESS;
75
+ }
76
+ var CHAT_GREETING_MAX_LENGTH = 400;
77
+ function normalizeChatGreeting(value) {
78
+ if (typeof value !== "string") return "";
79
+ const trimmed = value.replace(/\r\n?/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
80
+ return trimmed.slice(0, CHAT_GREETING_MAX_LENGTH);
81
+ }
82
+ function chatGreetingFrom(data) {
83
+ return normalizeChatGreeting(data?.chat_greeting);
84
+ }
85
+ function normalizeUploadAccessGroup(value) {
86
+ return UPLOAD_ACCESS_GROUPS.indexOf(value) === -1 ? DEFAULT_UPLOAD_ACCESS_GROUP : value;
87
+ }
88
+ function normalizeProjectAccessSetting(value) {
89
+ if (value === "ask") return "ask";
90
+ return UPLOAD_ACCESS_GROUPS.indexOf(value) === -1 ? null : value;
91
+ }
92
+ function accessSettingFrom(data) {
93
+ return normalizeProjectAccessSetting(data?.upload_access_group);
94
+ }
95
+ function uploadAccessGroupFrom(data) {
96
+ const v = accessSettingFrom(data);
97
+ return v && v !== "ask" ? v : DEFAULT_UPLOAD_ACCESS_GROUP;
98
+ }
99
+ function asksUploadAccessFrom(data) {
100
+ return accessSettingFrom(data) === "ask";
101
+ }
102
+ var reader = null;
103
+ var cache = /* @__PURE__ */ new Map();
104
+ function configureProjectSettings(fn) {
105
+ reader = fn;
106
+ }
107
+ function entry(service) {
108
+ let e = cache.get(service);
109
+ if (!e) {
110
+ e = { data: null, settled: false, inflight: null };
111
+ cache.set(service, e);
112
+ }
113
+ return e;
114
+ }
115
+ function loadProjectSettings(service) {
116
+ if (!service) return Promise.resolve(null);
117
+ const e = entry(service);
118
+ if (e.settled) return Promise.resolve(e.data);
119
+ if (e.inflight) return e.inflight;
120
+ if (!reader) return Promise.resolve(null);
121
+ const run = reader(service).then((data) => data && typeof data === "object" ? data : null).catch(() => null).then((data) => {
122
+ const cur = entry(service);
123
+ if (cur.inflight === run) {
124
+ cur.data = data;
125
+ cur.settled = true;
126
+ cur.inflight = null;
127
+ }
128
+ return data;
129
+ });
130
+ e.inflight = run;
131
+ return run;
132
+ }
133
+ function primeProjectSettings(service) {
134
+ void loadProjectSettings(service);
135
+ }
136
+ function readyProjectSettings(service) {
137
+ return loadProjectSettings(service);
138
+ }
139
+ function cachedProjectSettings(service) {
140
+ const e = cache.get(service);
141
+ return e && e.settled ? e.data : null;
142
+ }
143
+ function projectSettingsSettled(service) {
144
+ const e = cache.get(service);
145
+ return !!e && e.settled;
146
+ }
147
+ function projectAccessSetting(service) {
148
+ return accessSettingFrom(cachedProjectSettings(service));
149
+ }
150
+ function projectUploadAccessGroup(service) {
151
+ return uploadAccessGroupFrom(cachedProjectSettings(service));
152
+ }
153
+ function projectAsksUploadAccess(service) {
154
+ return asksUploadAccessFrom(cachedProjectSettings(service));
155
+ }
156
+ function projectChatGreeting(service) {
157
+ return chatGreetingFrom(cachedProjectSettings(service));
158
+ }
159
+ function projectWebAccess(service) {
160
+ return webAccessFrom(cachedProjectSettings(service));
161
+ }
162
+ function setProjectSettings(service, data) {
163
+ if (!service) return;
164
+ cache.set(service, { data: data || null, settled: true, inflight: null });
165
+ }
166
+ function patchProjectSettings(service, patch) {
167
+ if (!service) return;
168
+ const cur = cachedProjectSettings(service) || {};
169
+ setProjectSettings(service, Object.assign({}, cur, patch));
170
+ }
171
+ function clearProjectSettings(service) {
172
+ if (service) cache.delete(service);
173
+ else cache.clear();
174
+ }
175
+
48
176
  // src/engine/config.ts
49
177
  var _config = null;
50
178
  function configureChatEngine(config) {
@@ -110,6 +238,17 @@ function liveStreamingRealtimeEnabled() {
110
238
  function liveStreamingEnabled() {
111
239
  return _config?.liveStreaming === true;
112
240
  }
241
+ function webAccessEnabled(service) {
242
+ const opt = _config ? _config.webAccess : void 0;
243
+ if (typeof opt === "boolean") return opt;
244
+ return webAccessFrom(service ? cachedProjectSettings(service) : null);
245
+ }
246
+ function resolveWebAccess(service) {
247
+ const opt = _config ? _config.webAccess : void 0;
248
+ if (typeof opt === "boolean") return Promise.resolve(opt);
249
+ if (!service) return Promise.resolve(false);
250
+ return loadProjectSettings(service).then(webAccessFrom, () => false);
251
+ }
113
252
  function streamRecoveryEnabled() {
114
253
  if (!_config) return false;
115
254
  if (_config.streamRecovery === false) return false;
@@ -357,12 +496,14 @@ function groupAttachmentFailures(attachments) {
357
496
  // src/engine/prompts/chat_system_prompt.ts
358
497
  function buildChatSystemPrompt(params) {
359
498
  const { projectId, serviceName, serviceDescription, greeting, canUpload, client } = params;
499
+ const webAccess = params.webAccess === true;
360
500
  const g = params.indexAccessGroup;
361
501
  const indexGroupLiteral = typeof g === "number" ? String(g) : g === "public" || g === "private" || g === "authorized" || g === "admin" ? `"${g}"` : '"authorized"';
362
502
  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.
363
503
  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`}.
364
504
  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.
365
505
  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.`;
506
+ 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.`}`;
366
507
  let systemPrompt = `
367
508
  You are a dedicated assistant for the project ID: "${projectId}".
368
509
  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.
@@ -371,13 +512,14 @@ ${groundingRules}
371
512
  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.
372
513
  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.
373
514
  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.
515
+ ${documentRules}
374
516
  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.
375
517
  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.
376
518
  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.
377
519
  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.
378
520
  - 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.
379
521
  - 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.
380
- - 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.
522
+ ${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).`}
381
523
  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.
382
524
  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.
383
525
  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.
@@ -1076,6 +1218,224 @@ function truncateLabelForDisplay(label) {
1076
1218
  return "\u2026" + label.slice(label.length - (LINK_LABEL_MAX_DISPLAY_CHARS - 1));
1077
1219
  }
1078
1220
 
1221
+ // src/engine/retrieval_memory.ts
1222
+ var RETRIEVAL_NOTES_MAX = 6;
1223
+ var RETRIEVAL_NOTES_TOKEN_BUDGET = 1200;
1224
+ var RETRIEVAL_NOTE_MAX_ITEMS = 8;
1225
+ var RETRIEVAL_NOTE_MAX_CHARS = 600;
1226
+ var RETRIEVAL_NOTE_PREFIX = "[Retrieval note, not shown to the user";
1227
+ var FILE_KEYS = ["path", "storage_path", "storagePath", "file", "file_path", "filePath"];
1228
+ var TABLE_KEYS = ["table_name", "tableName"];
1229
+ var GROUP_KEYS = ["access_group", "accessGroup"];
1230
+ var SRC_PREFIX = "src::";
1231
+ var RESULT_WALK_MAX_NODES = 400;
1232
+ var RESULT_WALK_MAX_DEPTH = 5;
1233
+ function isWebToolName(name) {
1234
+ return typeof name === "string" && /^web_(search|fetch)/.test(name);
1235
+ }
1236
+ function pushUnique(list, value) {
1237
+ if (typeof value !== "string") return;
1238
+ var v = value.trim();
1239
+ if (!v || list.indexOf(v) !== -1 || list.length >= RETRIEVAL_NOTE_MAX_ITEMS) return;
1240
+ list.push(v);
1241
+ }
1242
+ function pushGroup(list, value) {
1243
+ if (typeof value === "number" && isFinite(value)) pushUnique(list, String(value));
1244
+ else pushUnique(list, value);
1245
+ }
1246
+ function pushHit(list, path, page) {
1247
+ if (typeof path !== "string" || !path.trim()) return;
1248
+ var n = typeof page === "number" ? page : Number(page);
1249
+ if (!isFinite(n) || n <= 0) return;
1250
+ n = Math.floor(n);
1251
+ for (var i = 0; i < list.length; i++) {
1252
+ if (list[i].path === path && list[i].page === n) return;
1253
+ }
1254
+ if (list.length >= RETRIEVAL_NOTE_MAX_ITEMS) return;
1255
+ list.push({ path: path.trim(), page: n });
1256
+ }
1257
+ function pathFromSrcId(value) {
1258
+ if (typeof value !== "string") return null;
1259
+ if (value.indexOf(SRC_PREFIX) !== 0) return null;
1260
+ var rest = value.slice(SRC_PREFIX.length).trim();
1261
+ return rest ? rest : null;
1262
+ }
1263
+ function parseJsonLoose(value) {
1264
+ if (typeof value !== "string") return value;
1265
+ var t = value.trim();
1266
+ if (!t || t[0] !== "{" && t[0] !== "[") return null;
1267
+ try {
1268
+ return JSON.parse(t);
1269
+ } catch (e) {
1270
+ return null;
1271
+ }
1272
+ }
1273
+ function readArguments(note, args) {
1274
+ if (!args || typeof args !== "object") return;
1275
+ var i;
1276
+ for (i = 0; i < FILE_KEYS.length; i++) pushUnique(note.files, args[FILE_KEYS[i]]);
1277
+ for (i = 0; i < TABLE_KEYS.length; i++) pushUnique(note.tables, args[TABLE_KEYS[i]]);
1278
+ for (i = 0; i < GROUP_KEYS.length; i++) pushGroup(note.accessGroups, args[GROUP_KEYS[i]]);
1279
+ if (typeof args.table === "string") pushUnique(note.tables, args.table);
1280
+ else if (args.table && typeof args.table === "object") {
1281
+ pushUnique(note.tables, args.table.name);
1282
+ pushGroup(note.accessGroups, args.table.access_group);
1283
+ }
1284
+ var ref = pathFromSrcId(args.reference);
1285
+ if (ref) pushUnique(note.files, ref);
1286
+ var uid = pathFromSrcId(args.unique_id);
1287
+ if (uid) pushUnique(note.files, uid);
1288
+ }
1289
+ function readHits(note, result) {
1290
+ var parsed = parseJsonLoose(result);
1291
+ if (!parsed || typeof parsed !== "object") return;
1292
+ var visited = 0;
1293
+ var walk = function(node, depth) {
1294
+ if (!node || typeof node !== "object" || depth > RESULT_WALK_MAX_DEPTH) return;
1295
+ if (++visited > RESULT_WALK_MAX_NODES) return;
1296
+ if (Array.isArray(node)) {
1297
+ for (var i = 0; i < node.length && visited <= RESULT_WALK_MAX_NODES; i++) walk(node[i], depth + 1);
1298
+ return;
1299
+ }
1300
+ if (typeof node.path === "string" && node.page !== void 0) {
1301
+ pushHit(note.hits, node.path, node.page);
1302
+ }
1303
+ for (var k in node) {
1304
+ if (!Object.prototype.hasOwnProperty.call(node, k)) continue;
1305
+ var v = node[k];
1306
+ if (v && typeof v === "object") walk(v, depth + 1);
1307
+ }
1308
+ };
1309
+ walk(parsed, 0);
1310
+ }
1311
+ function claudeResultText(block) {
1312
+ var c = block && block.content;
1313
+ if (typeof c === "string") return c;
1314
+ if (Array.isArray(c)) {
1315
+ return c.map(function(part) {
1316
+ return part && typeof part.text === "string" ? part.text : "";
1317
+ }).join("\n");
1318
+ }
1319
+ return "";
1320
+ }
1321
+ function emptyNote(source) {
1322
+ return { files: [], tables: [], accessGroups: [], hits: [], tools: [], web: false, source };
1323
+ }
1324
+ function noteHasContent(note) {
1325
+ return note.files.length > 0 || note.tables.length > 0 || note.accessGroups.length > 0 || note.hits.length > 0 || note.tools.length > 0 || note.web;
1326
+ }
1327
+ function noteFromToolBlocks(body) {
1328
+ if (!body || typeof body !== "object") return null;
1329
+ var note = emptyNote("tools");
1330
+ var sawTool = false;
1331
+ var i;
1332
+ var content = body.content;
1333
+ if (Array.isArray(content)) {
1334
+ var callNames = {};
1335
+ for (i = 0; i < content.length; i++) {
1336
+ var block = content[i];
1337
+ if (!block || typeof block !== "object") continue;
1338
+ var type = block.type;
1339
+ if (type === "mcp_tool_use" || type === "tool_use") {
1340
+ sawTool = true;
1341
+ pushUnique(note.tools, block.name);
1342
+ if (typeof block.id === "string" && typeof block.name === "string") callNames[block.id] = block.name;
1343
+ readArguments(note, block.input);
1344
+ } else if (type === "server_tool_use" || type === "web_search_tool_use") {
1345
+ sawTool = true;
1346
+ if (isWebToolName(block.name) || type === "web_search_tool_use") note.web = true;
1347
+ pushUnique(note.tools, block.name || type);
1348
+ } else if (type === "mcp_tool_result" || type === "tool_result") {
1349
+ sawTool = true;
1350
+ var name = typeof block.tool_use_id === "string" ? callNames[block.tool_use_id] : void 0;
1351
+ if (!block.is_error && (name === void 0 || name === "searchText")) readHits(note, claudeResultText(block));
1352
+ } else if (type === "web_search_tool_result" || type === "web_fetch_tool_result") {
1353
+ sawTool = true;
1354
+ note.web = true;
1355
+ }
1356
+ }
1357
+ }
1358
+ var output = body.output;
1359
+ if (Array.isArray(output)) {
1360
+ for (i = 0; i < output.length; i++) {
1361
+ var item = output[i];
1362
+ if (!item || typeof item !== "object") continue;
1363
+ var kind = typeof item.type === "string" ? item.type : "";
1364
+ if (kind === "mcp_call" || kind === "function_call") {
1365
+ sawTool = true;
1366
+ pushUnique(note.tools, item.name);
1367
+ readArguments(note, parseJsonLoose(item.arguments) || item.arguments);
1368
+ if (!item.error && item.name === "searchText") readHits(note, item.output);
1369
+ } else if (kind.indexOf("web_search") === 0 || kind.indexOf("web_fetch") === 0) {
1370
+ sawTool = true;
1371
+ note.web = true;
1372
+ pushUnique(note.tools, kind);
1373
+ }
1374
+ }
1375
+ }
1376
+ return sawTool ? note : null;
1377
+ }
1378
+ function retrievalNoteFromText(text) {
1379
+ if (typeof text !== "string" || !text) return null;
1380
+ var note = emptyNote("answer");
1381
+ var link = /\]\(db:([^)\s"]+)/g;
1382
+ var m;
1383
+ while ((m = link.exec(text)) !== null) pushUnique(note.files, m[1]);
1384
+ var src = /src::([^\s"'`<>)\]]+)/g;
1385
+ while ((m = src.exec(text)) !== null) {
1386
+ pushUnique(note.files, m[1].replace(/[.,;:!?]+$/, ""));
1387
+ }
1388
+ return noteHasContent(note) ? note : null;
1389
+ }
1390
+ function extractRetrievalNote(body, answerText) {
1391
+ var fromTools = noteFromToolBlocks(body);
1392
+ var fromText = retrievalNoteFromText(answerText);
1393
+ if (!fromTools) {
1394
+ if (fromText) return fromText;
1395
+ return providerBodyWasRead(body) ? emptyNote("body") : null;
1396
+ }
1397
+ if (fromText) {
1398
+ for (var i = 0; i < fromText.files.length; i++) pushUnique(fromTools.files, fromText.files[i]);
1399
+ }
1400
+ return noteHasContent(fromTools) ? fromTools : emptyNote("body");
1401
+ }
1402
+ function providerBodyWasRead(body) {
1403
+ if (!body || typeof body !== "object") return false;
1404
+ return Array.isArray(body.content) || Array.isArray(body.output);
1405
+ }
1406
+ function isRetrievalNote(value) {
1407
+ 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);
1408
+ }
1409
+ function formatRetrievalNote(note) {
1410
+ if (!note || !noteHasContent(note)) return "";
1411
+ var parts = [];
1412
+ if (note.files.length) parts.push("files " + note.files.join(", "));
1413
+ if (note.tables.length) parts.push("tables " + note.tables.join(", "));
1414
+ if (note.accessGroups.length) parts.push("access_group " + note.accessGroups.join(", "));
1415
+ if (note.hits.length) {
1416
+ var byFile = {};
1417
+ var order = [];
1418
+ for (var i = 0; i < note.hits.length; i++) {
1419
+ var h = note.hits[i];
1420
+ if (!byFile[h.path]) {
1421
+ byFile[h.path] = [];
1422
+ order.push(h.path);
1423
+ }
1424
+ byFile[h.path].push(h.page);
1425
+ }
1426
+ parts.push("searchText hits " + order.map(function(p) {
1427
+ return p + " p" + byFile[p].join(", p");
1428
+ }).join("; "));
1429
+ }
1430
+ if (note.tools.length) parts.push("tools " + note.tools.join(", "));
1431
+ if (note.web) parts.push("web tool used");
1432
+ var line = RETRIEVAL_NOTE_PREFIX + ": this turn used " + parts.join(" | ") + "]";
1433
+ if (line.length > RETRIEVAL_NOTE_MAX_CHARS) {
1434
+ line = line.slice(0, RETRIEVAL_NOTE_MAX_CHARS - 4) + "...]";
1435
+ }
1436
+ return line;
1437
+ }
1438
+
1079
1439
  // src/engine/budget.ts
1080
1440
  var CONTEXT_WINDOW_DEFAULT = { claude: 2e5, openai: 128e3 };
1081
1441
  var CONTEXT_WINDOW_BY_MODEL = {
@@ -1096,6 +1456,9 @@ var CONTEXT_WINDOW_BY_MODEL = {
1096
1456
  "claude-haiku-4-5": 2e5,
1097
1457
  "claude-3-5-sonnet": 2e5,
1098
1458
  // openai, exact ids
1459
+ // gpt-6.1: OpenAI's listing carries no window; taken as the gpt-6 family's until a
1460
+ // published figure says otherwise (2026-10-12, the default model).
1461
+ "gpt-6.1-sol": 105e4,
1099
1462
  "gpt-6-astra": 105e4,
1100
1463
  "gpt-6-sol": 105e4,
1101
1464
  "gpt-6-luna": 105e4,
@@ -1115,6 +1478,7 @@ var CONTEXT_WINDOW_BY_MODEL = {
1115
1478
  "claude-opus": 1e6,
1116
1479
  "claude-sonnet": 1e6,
1117
1480
  "claude-haiku": 2e5,
1481
+ "gpt-6.1": 105e4,
1118
1482
  "gpt-6": 105e4,
1119
1483
  "gpt-5.6": 105e4,
1120
1484
  "gpt-5": 128e3
@@ -1132,6 +1496,7 @@ var MAX_OUTPUT_BY_MODEL = {
1132
1496
  "claude-haiku-4-5": 64e3,
1133
1497
  "claude-3-5-sonnet": 8e3,
1134
1498
  // openai
1499
+ "gpt-6.1-sol": 128e3,
1135
1500
  "gpt-6-astra": 128e3,
1136
1501
  "gpt-6-sol": 128e3,
1137
1502
  "gpt-6-luna": 128e3,
@@ -1151,6 +1516,7 @@ var MAX_OUTPUT_BY_MODEL = {
1151
1516
  "claude-opus": 128e3,
1152
1517
  "claude-sonnet": 64e3,
1153
1518
  "claude-haiku": 64e3,
1519
+ "gpt-6.1": 128e3,
1154
1520
  "gpt-6": 128e3,
1155
1521
  "gpt-5.6": 128e3,
1156
1522
  "gpt-5": 128e3
@@ -1247,6 +1613,23 @@ function stripFileBlocksFromHistory(content) {
1247
1613
  if (!content) return content;
1248
1614
  return content.replace(/```([^\n`]+?\.[^\s.`]+)\n[\s\S]*?```/g, "[file previously attached: $1]");
1249
1615
  }
1616
+ function applyRetrievalNotes(messages, latestIndex) {
1617
+ var out = messages.slice();
1618
+ var used = 0, tokens = 0;
1619
+ for (var i = messages.length - 1; i >= 0; i--) {
1620
+ if (used >= RETRIEVAL_NOTES_MAX) break;
1621
+ var m = messages[i];
1622
+ if (i === latestIndex || !m || m.role !== "assistant" || !isRetrievalNote(m._retrieval)) continue;
1623
+ var line = formatRetrievalNote(m._retrieval);
1624
+ if (!line) continue;
1625
+ var cost = estimateTextTokens(line) + 2;
1626
+ if (tokens + cost > RETRIEVAL_NOTES_TOKEN_BUDGET) break;
1627
+ tokens += cost;
1628
+ used++;
1629
+ out[i] = Object.assign({}, m, { content: (m.content || "") + "\n\n" + line });
1630
+ }
1631
+ return { messages: out, notesUsed: used, noteTokens: tokens };
1632
+ }
1250
1633
  function buildBoundedChatMessages(options) {
1251
1634
  var contextBasedBudget = contextBasedBudgetFor(options.platform, options.model, options.projectId);
1252
1635
  var availableInputBudget = getInputTokenBudget(options.platform, options.model, options.projectId);
@@ -1262,6 +1645,7 @@ function buildBoundedChatMessages(options) {
1262
1645
  );
1263
1646
  var windowed = options.history.slice(-maxHistoryMessages);
1264
1647
  var latestIndex = windowed.length - 1;
1648
+ if (options.retrievalNotes !== false) windowed = applyRetrievalNotes(windowed, latestIndex).messages;
1265
1649
  var trimmed = windowed.map(function(m, i2) {
1266
1650
  if (i2 === latestIndex) return m;
1267
1651
  var stripped = stripFileBlocksFromHistory(m.content);
@@ -1285,6 +1669,105 @@ function buildBoundedChatMessages(options) {
1285
1669
  };
1286
1670
  }
1287
1671
 
1672
+ // src/engine/citations.ts
1673
+ var DATA_TOOL_NAMES = [
1674
+ "getRecords",
1675
+ "queryGrid",
1676
+ "readFileContent",
1677
+ "searchText",
1678
+ "getFileOutline",
1679
+ "getUniqueId",
1680
+ "getTables",
1681
+ "getTags",
1682
+ "getIndexes"
1683
+ ];
1684
+ var NO_DATA_TOOL_MARK = "No project data was read for this answer";
1685
+ function citationFileName(path) {
1686
+ var segs = String(path || "").split("/");
1687
+ for (var i = segs.length - 1; i >= 0; i--) {
1688
+ if (segs[i]) return segs[i];
1689
+ }
1690
+ return String(path || "");
1691
+ }
1692
+ function citationLabel(path, page) {
1693
+ return citationFileName(path) + (typeof page === "number" && page > 0 ? " p." + page : "");
1694
+ }
1695
+ function isMintablePath(path) {
1696
+ return !!path && !isHttpUrlLike(path);
1697
+ }
1698
+ function citationsFor(note) {
1699
+ if (!isRetrievalNote(note)) return [];
1700
+ var out = [];
1701
+ var hitPaths = [];
1702
+ var byPath = {};
1703
+ var i;
1704
+ for (i = 0; i < note.hits.length; i++) {
1705
+ var h = note.hits[i];
1706
+ if (!h || typeof h.path !== "string" || !isMintablePath(h.path)) continue;
1707
+ var page = typeof h.page === "number" ? h.page : Number(h.page);
1708
+ if (!isFinite(page) || page <= 0) continue;
1709
+ page = Math.floor(page);
1710
+ if (!byPath[h.path]) {
1711
+ byPath[h.path] = [];
1712
+ hitPaths.push(h.path);
1713
+ }
1714
+ if (byPath[h.path].indexOf(page) === -1) byPath[h.path].push(page);
1715
+ }
1716
+ for (i = 0; i < hitPaths.length; i++) {
1717
+ var p = hitPaths[i];
1718
+ var pages = byPath[p].slice().sort(function(a, b) {
1719
+ return a - b;
1720
+ });
1721
+ for (var j = 0; j < pages.length; j++) {
1722
+ out.push({ path: p, page: pages[j], label: citationLabel(p, pages[j]) });
1723
+ }
1724
+ }
1725
+ for (i = 0; i < note.files.length; i++) {
1726
+ var f = note.files[i];
1727
+ if (typeof f !== "string" || !isMintablePath(f)) continue;
1728
+ if (byPath[f]) continue;
1729
+ var dup = false;
1730
+ for (var k = 0; k < out.length; k++) {
1731
+ if (out[k].path === f && out[k].page === void 0) {
1732
+ dup = true;
1733
+ break;
1734
+ }
1735
+ }
1736
+ if (dup) continue;
1737
+ out.push({ path: f, label: citationLabel(f) });
1738
+ }
1739
+ return out;
1740
+ }
1741
+ function isDataToolName(name) {
1742
+ return DATA_TOOL_NAMES.indexOf(name) !== -1;
1743
+ }
1744
+ function answerUsedNoDataTool(note) {
1745
+ if (!isRetrievalNote(note)) return false;
1746
+ if (note.web) return false;
1747
+ for (var i = 0; i < note.tools.length; i++) {
1748
+ if (isDataToolName(note.tools[i])) return false;
1749
+ }
1750
+ return true;
1751
+ }
1752
+ function retrievalNoteOfAnswer(msg) {
1753
+ if (!msg || msg.role !== "assistant") return null;
1754
+ if (msg.isError || msg.isPending || msg.isBackgroundTask || msg._streamPending) return null;
1755
+ return isRetrievalNote(msg._retrieval) ? msg._retrieval : null;
1756
+ }
1757
+ function citationLinkPart(citation, resolveFreshHref) {
1758
+ var expiredHref = buildDisplayExpiredAttachmentHref(citation.path, citation.label);
1759
+ var cached = resolveFreshHref ? resolveFreshHref(expiredHref) : void 0;
1760
+ return {
1761
+ type: "link",
1762
+ label: truncateLabelForDisplay(citation.label),
1763
+ fullLabel: citation.label,
1764
+ href: cached || expiredHref,
1765
+ expired: !cached,
1766
+ expiredHref,
1767
+ remotePath: citation.path
1768
+ };
1769
+ }
1770
+
1288
1771
  // src/engine/download_encoding.ts
1289
1772
  var BOM = "\uFEFF";
1290
1773
  var BOM_EXTS = /* @__PURE__ */ new Set(["csv", "tsv", "tab", "txt", "text", "log"]);
@@ -2219,13 +2702,29 @@ var ANTHROPIC_PROMPT_CACHING_BETA = "prompt-caching-2024-07-31";
2219
2702
  var ANTHROPIC_BETA_HEADER = `${ANTHROPIC_MCP_BETA},${ANTHROPIC_WEB_FETCH_BETA},${ANTHROPIC_PROMPT_CACHING_BETA}`;
2220
2703
  var WEB_FETCH_MAX_USES = 40;
2221
2704
  var WEB_FETCH_MAX_CONTENT_TOKENS = 2e5;
2705
+ var claudeWebFetchTool = () => ({
2706
+ type: "web_fetch_20250910",
2707
+ name: "web_fetch",
2708
+ max_uses: WEB_FETCH_MAX_USES,
2709
+ citations: { enabled: true },
2710
+ max_content_tokens: WEB_FETCH_MAX_CONTENT_TOKENS
2711
+ });
2222
2712
  var OPENAI_RESPONSES_API_URL = "https://api.openai.com/v1/responses";
2223
2713
  var OPENAI_MODELS_API_URL = "https://api.openai.com/v1/models";
2224
2714
  var DEFAULT_OPENAI_IMAGE_DETAIL = "auto";
2225
2715
  var OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS = true;
2716
+ var openaiWebSearchTool = () => ({
2717
+ type: "web_search",
2718
+ external_web_access: OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS
2719
+ });
2720
+ var webToolsForChat = (platform, webAccess) => {
2721
+ if (webAccess !== true) return [];
2722
+ if (platform === "claude") return [claudeWebFetchTool()];
2723
+ return [openaiWebSearchTool()] ;
2724
+ };
2226
2725
  var MCP_NAME = "BunnyQuery";
2227
- var DEFAULT_CLAUDE_MODEL = "claude-sonnet-5";
2228
- var DEFAULT_OPENAI_MODEL = "gpt-5.6-luna";
2726
+ var DEFAULT_CLAUDE_MODEL = "claude-sonnet-5-5";
2727
+ var DEFAULT_OPENAI_MODEL = "gpt-6.1-sol";
2229
2728
  var mcpUrl = () => chatEngineConfig().mcpBaseUrl;
2230
2729
  function withMcpParams(base, params) {
2231
2730
  if (!base) return base;
@@ -2238,6 +2737,7 @@ function withMcpParams(base, params) {
2238
2737
  }
2239
2738
  var mcpContextParam = (platform, model) => getModelContextWindow(platform, model);
2240
2739
  var mcpIndexingUrl = (platform = "openai", model) => withMcpParams(mcpUrl(), { profile: "index", ctx: mcpContextParam(platform, model) });
2740
+ var mcpChatUrl = (endpointUrl, platform, model) => withMcpParams(endpointUrl, { profile: "chat", ctx: mcpContextParam(platform, model) });
2241
2741
  function mcpEndpointFor(anonymous, publicProjectId, service) {
2242
2742
  if (!anonymous) return { url: mcpUrl(), token: "$ACCESS_TOKEN" };
2243
2743
  const project = publicProjectId || service;
@@ -2423,8 +2923,10 @@ async function callClaudeWithMcp({
2423
2923
  system,
2424
2924
  mcpServer,
2425
2925
  extractContent,
2426
- fileUrls
2926
+ fileUrls,
2927
+ webAccess
2427
2928
  }) {
2929
+ const web = typeof webAccess === "boolean" ? webAccess : await resolveWebAccess(service);
2428
2930
  const mcpServerDefinition = {
2429
2931
  type: "url",
2430
2932
  name: mcpServer.name,
@@ -2483,13 +2985,10 @@ async function callClaudeWithMcp({
2483
2985
  ...mcpServer.defaultConfig ? { default_config: mcpServer.defaultConfig } : {},
2484
2986
  ...mcpServer.configs ? { configs: mcpServer.configs } : {}
2485
2987
  },
2486
- {
2487
- type: "web_fetch_20250910",
2488
- name: "web_fetch",
2489
- max_uses: WEB_FETCH_MAX_USES,
2490
- citations: { enabled: true },
2491
- max_content_tokens: WEB_FETCH_MAX_CONTENT_TOKENS
2492
- }
2988
+ // `web_fetch` rides here only when the project's web switch is on. The
2989
+ // beta header above is left as it is: it only makes the tool TYPE
2990
+ // available, and a request that does not list the tool cannot use it.
2991
+ ...webToolsForChat("claude", web)
2493
2992
  ]
2494
2993
  }
2495
2994
  });
@@ -2509,9 +3008,7 @@ async function callClaudeWithPublicMcp(prompt, service, owner, messages, system,
2509
3008
  fileUrls,
2510
3009
  mcpServer: {
2511
3010
  name: MCP_NAME,
2512
- url: withMcpParams(endpoint.url, {
2513
- ctx: mcpContextParam("claude", model || DEFAULT_CLAUDE_MODEL)
2514
- }),
3011
+ url: mcpChatUrl(endpoint.url, "claude", model || DEFAULT_CLAUDE_MODEL),
2515
3012
  // Omitted entirely for an anonymous turn; the `if (mcpServer.authorizationToken)`
2516
3013
  // guard below drops the key rather than sending an empty one.
2517
3014
  authorizationToken: endpoint.token
@@ -2521,6 +3018,7 @@ async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system,
2521
3018
  const endpoint = mcpEndpointFor(mcpScope?.anonymous, mcpScope?.publicProjectId, service);
2522
3019
  const resolvedModel = model || DEFAULT_OPENAI_MODEL;
2523
3020
  const imageDetail = getOpenAIImageDetail(resolvedModel);
3021
+ const web = await resolveWebAccess(service);
2524
3022
  const messageList = messages && messages.length ? prepareOpenAIMessages(messages, imageDetail) : [
2525
3023
  {
2526
3024
  role: "user",
@@ -2566,19 +3064,15 @@ async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system,
2566
3064
  {
2567
3065
  type: "mcp",
2568
3066
  server_label: MCP_NAME,
2569
- server_url: withMcpParams(endpoint.url, { ctx: mcpContextParam("openai", resolvedModel) }),
3067
+ server_url: mcpChatUrl(endpoint.url, "openai", resolvedModel),
2570
3068
  require_approval: "never",
2571
3069
  // No `headers` at all for an anonymous turn: `Bearer ` with an
2572
3070
  // empty token is a credential the MCP server rejects, and the
2573
3071
  // project-scoped endpoint needs none.
2574
3072
  ...endpoint.token ? { headers: { Authorization: "Bearer " + endpoint.token } } : {}
2575
3073
  },
2576
- ...[
2577
- {
2578
- type: "web_search",
2579
- external_web_access: OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS
2580
- }
2581
- ]
3074
+ // `web_search` rides here only when the project's web switch is on.
3075
+ ...webToolsForChat("openai", web)
2582
3076
  ]
2583
3077
  }
2584
3078
  });
@@ -2726,12 +3220,9 @@ async function notifyAgentSaveAttachment(info) {
2726
3220
  require_approval: "never",
2727
3221
  headers: { Authorization: "Bearer $ACCESS_TOKEN" }
2728
3222
  },
2729
- ...[
2730
- {
2731
- type: "web_search",
2732
- external_web_access: OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS
2733
- }
2734
- ]
3223
+ // UNCONDITIONAL, unlike a chat turn: the project's web switch
3224
+ // governs answers, not indexing. Kept exactly as it was.
3225
+ ...[openaiWebSearchTool()]
2735
3226
  ]
2736
3227
  }
2737
3228
  }));
@@ -2784,13 +3275,9 @@ async function notifyAgentSaveAttachment(info) {
2784
3275
  type: "mcp_toolset",
2785
3276
  mcp_server_name: MCP_NAME
2786
3277
  },
2787
- {
2788
- type: "web_fetch_20250910",
2789
- name: "web_fetch",
2790
- max_uses: WEB_FETCH_MAX_USES,
2791
- citations: { enabled: true },
2792
- max_content_tokens: WEB_FETCH_MAX_CONTENT_TOKENS
2793
- }
3278
+ // UNCONDITIONAL, unlike a chat turn: the project's web switch governs
3279
+ // answers, not indexing. Kept exactly as it was.
3280
+ claudeWebFetchTool()
2794
3281
  ]
2795
3282
  }
2796
3283
  }));
@@ -3329,6 +3816,10 @@ function mapHistoryListToMessages(list, platform, opts) {
3329
3816
  if (item._fromBgChain) okm._fromBgChain = true;
3330
3817
  if (item._isBgTask) okm.isBackgroundTask = true;
3331
3818
  if (isCompact) okm._compact = true;
3819
+ if (!item._isBgTask && !isCompact) {
3820
+ var note = extractRetrievalNote(response, assistantText);
3821
+ if (note) okm._retrieval = note;
3822
+ }
3332
3823
  if (serverItemId !== void 0) okm._serverItemId = serverItemId;
3333
3824
  if (replyTs !== void 0) okm._ts = replyTs;
3334
3825
  if (indexFile && isFinite(executedTs) && executedTs > 0) okm._tsStart = executedTs;
@@ -5024,6 +5515,10 @@ var ChatSession = class {
5024
5515
  return;
5025
5516
  }
5026
5517
  var reply = isErr ? { role: "assistant", content: getErrorMessage(body), isError: true, _serverItemId: itemId } : { role: "assistant", content: text, _serverItemId: itemId };
5518
+ if (!isErr) {
5519
+ var rNote = extractRetrievalNote(body, text);
5520
+ if (rNote) reply._retrieval = rNote;
5521
+ }
5027
5522
  if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
5028
5523
  this._applyReplyToCache(ownerKey, reply, itemId);
5029
5524
  } else {
@@ -5432,7 +5927,7 @@ var ChatSession = class {
5432
5927
  if (isErrorResponseBody(response)) return { content: getErrorMessage(response), isError: true };
5433
5928
  var answer = params.aiPlatform === "openai" ? extractOpenAIText(response) : extractClaudeText(response);
5434
5929
  answer = (answer || "").trim();
5435
- return { content: answer || "No text response received from AI provider.", isError: false };
5930
+ return { content: answer || "No text response received from AI provider.", isError: false, retrieval: extractRetrievalNote(response, answer) };
5436
5931
  }).catch(function(err) {
5437
5932
  return { content: getErrorMessage(err), isError: true };
5438
5933
  }).then(function(result) {
@@ -5444,6 +5939,7 @@ var ChatSession = class {
5444
5939
  }
5445
5940
  var reply = { role: "assistant", content: result.content, isError: result.isError };
5446
5941
  if (dispatchItemId) reply._serverItemId = dispatchItemId;
5942
+ if (result.retrieval) reply._retrieval = result.retrieval;
5447
5943
  self._applyReplyToCache(params.key, reply, dispatchItemId);
5448
5944
  return result;
5449
5945
  });
@@ -6107,6 +6603,10 @@ var ChatSession = class {
6107
6603
  }
6108
6604
  if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
6109
6605
  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." };
6606
+ if (!offReply.isError) {
6607
+ var offNote = extractRetrievalNote(response, offReply.content);
6608
+ if (offNote) offReply._retrieval = offNote;
6609
+ }
6110
6610
  this._applyReplyToCache(ownerKey, offReply, serverId);
6111
6611
  if (serverId) this.cancelledServerIds.delete(serverId);
6112
6612
  return;
@@ -6128,6 +6628,7 @@ var ChatSession = class {
6128
6628
  } else {
6129
6629
  var answer = platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response);
6130
6630
  answer = (answer || "").trim() || "No text response received from AI provider.";
6631
+ var qNote = extractRetrievalNote(response, answer);
6131
6632
  var lid = this._newLocalId();
6132
6633
  if (targetIdx >= 0 && this.state.messages[targetIdx] && this.state.messages[targetIdx].isPending) {
6133
6634
  var qPainted = this._paintedTextAt(targetIdx);
@@ -6135,16 +6636,21 @@ var ChatSession = class {
6135
6636
  var qSettled = { role: "assistant", content: qPainted, _localId: lid };
6136
6637
  if (prevQ._serverItemId) qSettled._serverItemId = prevQ._serverItemId;
6137
6638
  if (prevQ._ownerKey) qSettled._ownerKey = prevQ._ownerKey;
6639
+ if (qNote) qSettled._retrieval = qNote;
6138
6640
  this.state.messages[targetIdx] = qSettled;
6139
6641
  this.host.notify();
6140
6642
  this.enqueueTypewrite(targetIdx, answer, lid, qPainted);
6141
6643
  } else if (targetIdx >= 0) {
6142
- this.state.messages.splice(targetIdx, 0, { role: "assistant", content: "", _localId: lid });
6644
+ var qInsert = { role: "assistant", content: "", _localId: lid };
6645
+ if (qNote) qInsert._retrieval = qNote;
6646
+ this.state.messages.splice(targetIdx, 0, qInsert);
6143
6647
  this.host.notify();
6144
6648
  this.enqueueTypewrite(targetIdx, answer, lid);
6145
6649
  } else {
6146
6650
  var aiIdx = this.state.messages.length;
6147
- this.state.messages.push({ role: "assistant", content: "", _localId: lid });
6651
+ var qAppend = { role: "assistant", content: "", _localId: lid };
6652
+ if (qNote) qAppend._retrieval = qNote;
6653
+ this.state.messages.push(qAppend);
6148
6654
  this.host.notify();
6149
6655
  this.enqueueTypewrite(aiIdx, answer, lid);
6150
6656
  }
@@ -6734,6 +7240,7 @@ var ChatSession = class {
6734
7240
  var settled = { role: "assistant", content: painted, isPending: false, _localId: lid };
6735
7241
  if (prevSettled._serverItemId) settled._serverItemId = prevSettled._serverItemId;
6736
7242
  if (prevSettled._ownerKey) settled._ownerKey = prevSettled._ownerKey;
7243
+ if (latest._retrieval) settled._retrieval = latest._retrieval;
6737
7244
  this.state.messages[pendingIdx] = settled;
6738
7245
  this._removeStrayPendingAssistants();
6739
7246
  this.host.notify();
@@ -6954,6 +7461,7 @@ var ChatSession = class {
6954
7461
  var isErr = isErrorResponseBody(response);
6955
7462
  var answer = isErr ? getErrorMessage(response) : ((platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "").trim();
6956
7463
  var reportedComplete = !isErr && !!answer && answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1;
7464
+ var hNote = isErr ? null : extractRetrievalNote(response, answer);
6957
7465
  var stripMarker = function(t) {
6958
7466
  return reportedComplete ? t.split(INDEXING_COMPLETE_MARKER).join("").trim() : t;
6959
7467
  };
@@ -6979,7 +7487,9 @@ var ChatSession = class {
6979
7487
  }
6980
7488
  var hPainted = this._paintedTextAt(idx);
6981
7489
  var lid = this._newLocalId();
6982
- this.state.messages[idx] = { role: "assistant", content: hPainted, _localId: lid, _serverItemId: itemId };
7490
+ var hReply = { role: "assistant", content: hPainted, _localId: lid, _serverItemId: itemId };
7491
+ if (hNote) hReply._retrieval = hNote;
7492
+ this.state.messages[idx] = hReply;
6983
7493
  this.host.notify();
6984
7494
  this.enqueueTypewrite(idx, text, lid, hPainted);
6985
7495
  this.updateHistoryCache();
@@ -7013,6 +7523,7 @@ var ChatSession = class {
7013
7523
  }
7014
7524
  var lid2 = this._newLocalId();
7015
7525
  var reply = { role: "assistant", content: "", _localId: lid2, _serverItemId: itemId };
7526
+ if (hNote) reply._retrieval = hNote;
7016
7527
  this.state.messages.splice(userIdx + 1, 0, reply);
7017
7528
  this.host.notify();
7018
7529
  this.enqueueTypewrite(userIdx + 1, text2, lid2);
@@ -8677,124 +9188,6 @@ function buildChatDisplayList(messages, opts) {
8677
9188
  return out;
8678
9189
  }
8679
9190
 
8680
- // src/engine/project_settings.ts
8681
- var UPLOAD_ACCESS_GROUPS = ["public", "authorized", "private"];
8682
- var DEFAULT_UPLOAD_ACCESS_GROUP = "authorized";
8683
- var PROJECT_SETTINGS_TABLE = "__SETTINGS__";
8684
- var PROJECT_SETTINGS_UNIQUE_ID = "bq::settings";
8685
- var PROJECT_SETTINGS_ACCESS_GROUP = "public";
8686
- var UPLOAD_ACCESS_LABELS = {
8687
- public: "Public",
8688
- authorized: "Signed in users",
8689
- private: "Only me"
8690
- };
8691
- var UPLOAD_ACCESS_HINTS = {
8692
- public: "Anyone can ask about this file, including visitors who are not logged in.",
8693
- authorized: "Only users signed in to this project can ask about this file.",
8694
- private: "Only you can ask about this file."
8695
- };
8696
- var UPLOAD_ACCESS_OPTIONS = UPLOAD_ACCESS_GROUPS.map((value) => ({
8697
- value,
8698
- label: UPLOAD_ACCESS_LABELS[value],
8699
- hint: UPLOAD_ACCESS_HINTS[value]
8700
- }));
8701
- var CHAT_GREETING_MAX_LENGTH = 400;
8702
- function normalizeChatGreeting(value) {
8703
- if (typeof value !== "string") return "";
8704
- const trimmed = value.replace(/\r\n?/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
8705
- return trimmed.slice(0, CHAT_GREETING_MAX_LENGTH);
8706
- }
8707
- function chatGreetingFrom(data) {
8708
- return normalizeChatGreeting(data?.chat_greeting);
8709
- }
8710
- function normalizeUploadAccessGroup(value) {
8711
- return UPLOAD_ACCESS_GROUPS.indexOf(value) === -1 ? DEFAULT_UPLOAD_ACCESS_GROUP : value;
8712
- }
8713
- function normalizeProjectAccessSetting(value) {
8714
- if (value === "ask") return "ask";
8715
- return UPLOAD_ACCESS_GROUPS.indexOf(value) === -1 ? null : value;
8716
- }
8717
- function accessSettingFrom(data) {
8718
- return normalizeProjectAccessSetting(data?.upload_access_group);
8719
- }
8720
- function uploadAccessGroupFrom(data) {
8721
- const v = accessSettingFrom(data);
8722
- return v && v !== "ask" ? v : DEFAULT_UPLOAD_ACCESS_GROUP;
8723
- }
8724
- function asksUploadAccessFrom(data) {
8725
- return accessSettingFrom(data) === "ask";
8726
- }
8727
- var reader = null;
8728
- var cache = /* @__PURE__ */ new Map();
8729
- function configureProjectSettings(fn) {
8730
- reader = fn;
8731
- }
8732
- function entry(service) {
8733
- let e = cache.get(service);
8734
- if (!e) {
8735
- e = { data: null, settled: false, inflight: null };
8736
- cache.set(service, e);
8737
- }
8738
- return e;
8739
- }
8740
- function loadProjectSettings(service) {
8741
- if (!service) return Promise.resolve(null);
8742
- const e = entry(service);
8743
- if (e.settled) return Promise.resolve(e.data);
8744
- if (e.inflight) return e.inflight;
8745
- if (!reader) return Promise.resolve(null);
8746
- const run = reader(service).then((data) => data && typeof data === "object" ? data : null).catch(() => null).then((data) => {
8747
- const cur = entry(service);
8748
- if (cur.inflight === run) {
8749
- cur.data = data;
8750
- cur.settled = true;
8751
- cur.inflight = null;
8752
- }
8753
- return data;
8754
- });
8755
- e.inflight = run;
8756
- return run;
8757
- }
8758
- function primeProjectSettings(service) {
8759
- void loadProjectSettings(service);
8760
- }
8761
- function readyProjectSettings(service) {
8762
- return loadProjectSettings(service);
8763
- }
8764
- function cachedProjectSettings(service) {
8765
- const e = cache.get(service);
8766
- return e && e.settled ? e.data : null;
8767
- }
8768
- function projectSettingsSettled(service) {
8769
- const e = cache.get(service);
8770
- return !!e && e.settled;
8771
- }
8772
- function projectAccessSetting(service) {
8773
- return accessSettingFrom(cachedProjectSettings(service));
8774
- }
8775
- function projectUploadAccessGroup(service) {
8776
- return uploadAccessGroupFrom(cachedProjectSettings(service));
8777
- }
8778
- function projectAsksUploadAccess(service) {
8779
- return asksUploadAccessFrom(cachedProjectSettings(service));
8780
- }
8781
- function projectChatGreeting(service) {
8782
- return chatGreetingFrom(cachedProjectSettings(service));
8783
- }
8784
- function setProjectSettings(service, data) {
8785
- if (!service) return;
8786
- cache.set(service, { data: data || null, settled: true, inflight: null });
8787
- }
8788
- function patchProjectSettings(service, patch) {
8789
- if (!service) return;
8790
- const cur = cachedProjectSettings(service) || {};
8791
- setProjectSettings(service, Object.assign({}, cur, patch));
8792
- }
8793
- function clearProjectSettings(service) {
8794
- if (service) cache.delete(service);
8795
- else cache.clear();
8796
- }
8797
-
8798
9191
  exports.BG_INDEXING_QUEUE_SUFFIX = BG_INDEXING_QUEUE_SUFFIX;
8799
9192
  exports.BOM = BOM;
8800
9193
  exports.BOM_EXTS = BOM_EXTS;
@@ -8804,10 +9197,12 @@ exports.CLAUDE_PER_REQUEST_INPUT_CAP = CLAUDE_PER_REQUEST_INPUT_CAP;
8804
9197
  exports.CONTEXT_WINDOW_BY_MODEL = CONTEXT_WINDOW_BY_MODEL;
8805
9198
  exports.CONTEXT_WINDOW_DEFAULT = CONTEXT_WINDOW_DEFAULT;
8806
9199
  exports.ChatSession = ChatSession;
9200
+ exports.DATA_TOOL_NAMES = DATA_TOOL_NAMES;
8807
9201
  exports.DEFAULT_CLAUDE_MODEL = DEFAULT_CLAUDE_MODEL;
8808
9202
  exports.DEFAULT_CONTEXT_WINDOW = DEFAULT_CONTEXT_WINDOW;
8809
9203
  exports.DEFAULT_OPENAI_MODEL = DEFAULT_OPENAI_MODEL;
8810
9204
  exports.DEFAULT_UPLOAD_ACCESS_GROUP = DEFAULT_UPLOAD_ACCESS_GROUP;
9205
+ exports.DEFAULT_WEB_ACCESS = DEFAULT_WEB_ACCESS;
8811
9206
  exports.EMPTY_INDEXING_REPLY = EMPTY_INDEXING_REPLY;
8812
9207
  exports.EXPIRED_ATTACHMENT_URL_HOST = EXPIRED_ATTACHMENT_URL_HOST;
8813
9208
  exports.EXPIRED_ATTACHMENT_URL_ORIGIN = EXPIRED_ATTACHMENT_URL_ORIGIN;
@@ -8837,6 +9232,7 @@ exports.MCP_NAME = MCP_NAME;
8837
9232
  exports.MINT_CACHE_GENERATION = MINT_CACHE_GENERATION;
8838
9233
  exports.MIN_INPUT_TOKEN_BUDGET = MIN_INPUT_TOKEN_BUDGET;
8839
9234
  exports.MIN_PER_REQUEST_INPUT_CAP = MIN_PER_REQUEST_INPUT_CAP;
9235
+ exports.NO_DATA_TOOL_MARK = NO_DATA_TOOL_MARK;
8840
9236
  exports.OUTPUT_TOKEN_RESERVE = OUTPUT_TOKEN_RESERVE;
8841
9237
  exports.POLL_INTERVAL = POLL_INTERVAL;
8842
9238
  exports.PRESIGN_SAFETY_MARGIN_MS = PRESIGN_SAFETY_MARGIN_MS;
@@ -8848,6 +9244,11 @@ exports.PROJECT_SETTINGS_ACCESS_GROUP = PROJECT_SETTINGS_ACCESS_GROUP;
8848
9244
  exports.PROJECT_SETTINGS_TABLE = PROJECT_SETTINGS_TABLE;
8849
9245
  exports.PROJECT_SETTINGS_UNIQUE_ID = PROJECT_SETTINGS_UNIQUE_ID;
8850
9246
  exports.RENDER_FROM_TOKEN = RENDER_FROM_TOKEN;
9247
+ exports.RETRIEVAL_NOTES_MAX = RETRIEVAL_NOTES_MAX;
9248
+ exports.RETRIEVAL_NOTES_TOKEN_BUDGET = RETRIEVAL_NOTES_TOKEN_BUDGET;
9249
+ exports.RETRIEVAL_NOTE_MAX_CHARS = RETRIEVAL_NOTE_MAX_CHARS;
9250
+ exports.RETRIEVAL_NOTE_MAX_ITEMS = RETRIEVAL_NOTE_MAX_ITEMS;
9251
+ exports.RETRIEVAL_NOTE_PREFIX = RETRIEVAL_NOTE_PREFIX;
8851
9252
  exports.RTF_EXTS = RTF_EXTS;
8852
9253
  exports.RUN_RECORD_WORKING_STALE_MS = RUN_RECORD_WORKING_STALE_MS;
8853
9254
  exports.STREAM_POLL_INTERVAL = STREAM_POLL_INTERVAL;
@@ -8860,7 +9261,9 @@ exports.XML_EXTS = XML_EXTS;
8860
9261
  exports.__resetSplitHistoryState = __resetSplitHistoryState;
8861
9262
  exports.accessSettingFrom = accessSettingFrom;
8862
9263
  exports.adoptLocalAnswerIntoPage = adoptLocalAnswerIntoPage;
9264
+ exports.answerUsedNoDataTool = answerUsedNoDataTool;
8863
9265
  exports.applyEncodingDeclaration = applyEncodingDeclaration;
9266
+ exports.applyRetrievalNotes = applyRetrievalNotes;
8864
9267
  exports.asksUploadAccessFrom = asksUploadAccessFrom;
8865
9268
  exports.bgIndexingQueueName = bgIndexingQueueName;
8866
9269
  exports.buildAiAgentValue = buildAiAgentValue;
@@ -8885,6 +9288,9 @@ exports.chatCacheKey = chatCacheKey;
8885
9288
  exports.chatEngineConfig = chatEngineConfig;
8886
9289
  exports.chatGreetingFrom = chatGreetingFrom;
8887
9290
  exports.chatStreamWiring = chatStreamWiring;
9291
+ exports.citationFileName = citationFileName;
9292
+ exports.citationLinkPart = citationLinkPart;
9293
+ exports.citationsFor = citationsFor;
8888
9294
  exports.classifyInlineLink = classifyInlineLink;
8889
9295
  exports.clearAttachmentParsers = clearAttachmentParsers;
8890
9296
  exports.clearImagePreviewCache = clearImagePreviewCache;
@@ -8911,12 +9317,14 @@ exports.extractClaudeText = extractClaudeText;
8911
9317
  exports.extractLastUserTextFromRequest = extractLastUserTextFromRequest;
8912
9318
  exports.extractOpenAIText = extractOpenAIText;
8913
9319
  exports.extractRemotePathFromAttachmentHref = extractRemotePathFromAttachmentHref;
9320
+ exports.extractRetrievalNote = extractRetrievalNote;
8914
9321
  exports.fetchLiveIndexingKeys = fetchLiveIndexingKeys;
8915
9322
  exports.fillHistoryViewport = fillHistoryViewport;
8916
9323
  exports.filterListByClearHorizon = filterListByClearHorizon;
8917
9324
  exports.findAttachmentParser = findAttachmentParser;
8918
9325
  exports.formatChatTimestamp = formatChatTimestamp;
8919
9326
  exports.formatDuration = formatDuration;
9327
+ exports.formatRetrievalNote = formatRetrievalNote;
8920
9328
  exports.getAttachmentParsers = getAttachmentParsers;
8921
9329
  exports.getChatHistory = getChatHistory;
8922
9330
  exports.getContextWindow = getContextWindow;
@@ -8937,6 +9345,7 @@ exports.indexingAccessGroup = indexingAccessGroup;
8937
9345
  exports.isAuthExpiredError = isAuthExpiredError;
8938
9346
  exports.isBgIndexingQueue = isBgIndexingQueue;
8939
9347
  exports.isCsrStatusEnvelope = isCsrStatusEnvelope;
9348
+ exports.isDataToolName = isDataToolName;
8940
9349
  exports.isErrorResponseBody = isErrorResponseBody;
8941
9350
  exports.isHttpUrlLike = isHttpUrlLike;
8942
9351
  exports.isImageVisionFile = isImageVisionFile;
@@ -8948,6 +9357,7 @@ exports.isPagedReadFile = isPagedReadFile;
8948
9357
  exports.isPreviewableImagePath = isPreviewableImagePath;
8949
9358
  exports.isProviderApiKeyError = isProviderApiKeyError;
8950
9359
  exports.isRetriedFailure = isRetriedFailure;
9360
+ exports.isRetrievalNote = isRetrievalNote;
8951
9361
  exports.isServerExtractable = isServerExtractable;
8952
9362
  exports.isServiceDbAttachmentHref = isServiceDbAttachmentHref;
8953
9363
  exports.isWindowedReadFile = isWindowedReadFile;
@@ -8972,6 +9382,7 @@ exports.normalizeProjectAccessSetting = normalizeProjectAccessSetting;
8972
9382
  exports.normalizeTextContent = normalizeTextContent;
8973
9383
  exports.normalizeTrailingInlineToken = normalizeTrailingInlineToken;
8974
9384
  exports.normalizeUploadAccessGroup = normalizeUploadAccessGroup;
9385
+ exports.normalizeWebAccess = normalizeWebAccess;
8975
9386
  exports.notifyAgentSaveAttachment = notifyAgentSaveAttachment;
8976
9387
  exports.parseAiAgentValue = parseAiAgentValue;
8977
9388
  exports.parseAttachmentContent = parseAttachmentContent;
@@ -8991,6 +9402,8 @@ exports.projectAsksUploadAccess = projectAsksUploadAccess;
8991
9402
  exports.projectChatGreeting = projectChatGreeting;
8992
9403
  exports.projectSettingsSettled = projectSettingsSettled;
8993
9404
  exports.projectUploadAccessGroup = projectUploadAccessGroup;
9405
+ exports.projectWebAccess = projectWebAccess;
9406
+ exports.providerBodyWasRead = providerBodyWasRead;
8994
9407
  exports.readExpiredAttachmentHref = readExpiredAttachmentHref;
8995
9408
  exports.readyProjectSettings = readyProjectSettings;
8996
9409
  exports.registerAttachmentParser = registerAttachmentParser;
@@ -8999,6 +9412,9 @@ exports.renderInlineLinkHtml = renderInlineLinkHtml;
8999
9412
  exports.repairUrlEntities = repairUrlEntities;
9000
9413
  exports.repairUrlWhitespace = repairUrlWhitespace;
9001
9414
  exports.resolveImagePreviewUrl = resolveImagePreviewUrl;
9415
+ exports.resolveWebAccess = resolveWebAccess;
9416
+ exports.retrievalNoteFromText = retrievalNoteFromText;
9417
+ exports.retrievalNoteOfAnswer = retrievalNoteOfAnswer;
9002
9418
  exports.runIndexUniqueId = runIndexUniqueId;
9003
9419
  exports.safeDecodeURIComponent = safeDecodeURIComponent;
9004
9420
  exports.sanitizeAttachmentLinksForHistory = sanitizeAttachmentLinksForHistory;
@@ -9017,5 +9433,7 @@ exports.typewriterResumeIndex = typewriterResumeIndex;
9017
9433
  exports.uploadAccessGroupFrom = uploadAccessGroupFrom;
9018
9434
  exports.upsertIndexRunRecordSafe = upsertIndexRunRecordSafe;
9019
9435
  exports.wallClockNow = wallClockNow;
9436
+ exports.webAccessEnabled = webAccessEnabled;
9437
+ exports.webAccessFrom = webAccessFrom;
9020
9438
  //# sourceMappingURL=engine.cjs.map
9021
9439
  //# sourceMappingURL=engine.cjs.map