bunnyquery 1.8.2 → 1.8.3
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 +384 -97
- package/dist/engine.cjs +279 -31
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +113 -3
- package/dist/engine.d.ts +113 -3
- package/dist/engine.mjs +279 -32
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/host.ts +16 -0
- package/src/engine/index.ts +1 -0
- package/src/engine/office.ts +25 -8
- package/src/engine/prompts/chat_system_prompt.ts +1 -0
- package/src/engine/requests.ts +24 -3
- package/src/engine/session.ts +323 -28
package/dist/engine.mjs
CHANGED
|
@@ -217,14 +217,15 @@ function isWindowedReadFile(name, mime) {
|
|
|
217
217
|
}
|
|
218
218
|
function composeUserMessage(text, attachmentUrls) {
|
|
219
219
|
let composed = text;
|
|
220
|
+
let composedForLlm = composed;
|
|
220
221
|
if (attachmentUrls.length > 0) {
|
|
221
222
|
const lines = attachmentUrls.map((u) => `- [${u.name}](${u.url})`);
|
|
222
223
|
composed = `${text}
|
|
223
224
|
|
|
224
225
|
Attached files:
|
|
225
226
|
${lines.join("\n")}`;
|
|
227
|
+
composedForLlm = composed;
|
|
226
228
|
}
|
|
227
|
-
let composedForLlm = composed;
|
|
228
229
|
let extractContent;
|
|
229
230
|
let fileUrls;
|
|
230
231
|
if (attachmentUrls.length > 0) {
|
|
@@ -241,13 +242,13 @@ ${placeholder}
|
|
|
241
242
|
----- END FILE CONTENT -----`;
|
|
242
243
|
});
|
|
243
244
|
extractContent = directives;
|
|
244
|
-
composedForLlm = `${
|
|
245
|
+
composedForLlm = `${composedForLlm}
|
|
245
246
|
|
|
246
247
|
Extracted content of attached office files (read inline below; do NOT fetch their URLs):
|
|
247
248
|
|
|
248
249
|
` + sections.join("\n\n");
|
|
249
250
|
}
|
|
250
|
-
const urlFiles =
|
|
251
|
+
const urlFiles = [];
|
|
251
252
|
if (urlFiles.length > 0) {
|
|
252
253
|
fileUrls = urlFiles.map((u) => ({ path: u.storagePath || u.name, url: u.url }));
|
|
253
254
|
}
|
|
@@ -291,6 +292,7 @@ File attachments: When a user message contains an "Attached files:" section with
|
|
|
291
292
|
- 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.
|
|
292
293
|
- Most attached files (office documents like .docx/.xlsx/.pptx/.hwp/.hwpx/.ods, and text/data/code files like .csv/.tsv/.json/.xml/.txt/.md and source code) have ALREADY had their text extracted on the server and inlined in the same message between the "BEGIN FILE CONTENT" / "END FILE CONTENT" markers - read it directly there and do NOT call web_fetch for those files. A "[skapi: ...]" note in that block means the file could not be extracted.
|
|
293
294
|
- 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.
|
|
295
|
+
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, but 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.
|
|
294
296
|
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 \u2014 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 \u2014 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.
|
|
295
297
|
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. Present each result as a markdown link as described above. Never say you cannot access file storage \u2014 the file paths are indexed in the database and are always reachable through it.
|
|
296
298
|
File generation: When the user asks you to generate a file \u2014 or to produce specifically-formatted text such as HTML, CSV, JSON, or Markdown \u2014 put the file's full contents inside a fenced code block whose info string is the intended filename WITH its extension (e.g. report.csv), NOT a language name like "csv". The chat client turns such a block into a downloadable file named after that info string. Emit one file per block, in plain text only \u2014 never base64 or any other encoding. Example for CSV:
|
|
@@ -1432,7 +1434,7 @@ async function notifyAgentSaveAttachment(info) {
|
|
|
1432
1434
|
const imageDetail = getOpenAIImageDetail(resolvedModel2);
|
|
1433
1435
|
return clientSecretRequest({
|
|
1434
1436
|
clientSecretName: "openai",
|
|
1435
|
-
queue: (info.userId
|
|
1437
|
+
queue: bgIndexingQueueName(info.userId, service),
|
|
1436
1438
|
service,
|
|
1437
1439
|
owner,
|
|
1438
1440
|
...pollOpt(),
|
|
@@ -1476,7 +1478,7 @@ async function notifyAgentSaveAttachment(info) {
|
|
|
1476
1478
|
const resolvedModel = info.model || DEFAULT_CLAUDE_MODEL;
|
|
1477
1479
|
return clientSecretRequest({
|
|
1478
1480
|
clientSecretName: "claude",
|
|
1479
|
-
queue: (info.userId
|
|
1481
|
+
queue: bgIndexingQueueName(info.userId, service),
|
|
1480
1482
|
service,
|
|
1481
1483
|
owner,
|
|
1482
1484
|
...pollOpt(),
|
|
@@ -1590,6 +1592,9 @@ async function listOpenAIModels(service, owner) {
|
|
|
1590
1592
|
});
|
|
1591
1593
|
}
|
|
1592
1594
|
var BG_INDEXING_QUEUE_SUFFIX = "-bg";
|
|
1595
|
+
function bgIndexingQueueName(userId, service) {
|
|
1596
|
+
return (userId || service || "") + BG_INDEXING_QUEUE_SUFFIX;
|
|
1597
|
+
}
|
|
1593
1598
|
function isBgIndexingQueue(queueName) {
|
|
1594
1599
|
if (typeof queueName !== "string" || !queueName) return false;
|
|
1595
1600
|
const prefix = queueName.split("|")[0];
|
|
@@ -1829,6 +1834,11 @@ function createHistoryFiller(base) {
|
|
|
1829
1834
|
// src/engine/session.ts
|
|
1830
1835
|
var WORKER_PASS_ADOPT_LIMIT = 20;
|
|
1831
1836
|
var WORKER_PASS_ADOPT_ATTEMPTS = [0, 2e3, 6e3];
|
|
1837
|
+
var INDEXING_DRAIN_BUSY_POLL_MS = 8e3;
|
|
1838
|
+
var INDEXING_DRAIN_CONFIRM_POLL_MS = 3e3;
|
|
1839
|
+
var INDEXING_DRAIN_IDLE_LOOKS = 2;
|
|
1840
|
+
var INDEXING_DRAIN_MIN_MS = 8e3;
|
|
1841
|
+
var INDEXING_DRAIN_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
1832
1842
|
var _g = typeof globalThis !== "undefined" ? globalThis : {};
|
|
1833
1843
|
function nowMs() {
|
|
1834
1844
|
return _g.performance && typeof _g.performance.now === "function" ? _g.performance.now() : Date.now();
|
|
@@ -1901,6 +1911,25 @@ var ChatSession = class {
|
|
|
1901
1911
|
this._pauseReasons = /* @__PURE__ */ new Set();
|
|
1902
1912
|
this._resuming = false;
|
|
1903
1913
|
this._lidSeq = 0;
|
|
1914
|
+
this._stageSeq = 0;
|
|
1915
|
+
this._uploadBatches = 0;
|
|
1916
|
+
this._indexDispatchesInFlight = 0;
|
|
1917
|
+
}
|
|
1918
|
+
/** Wrap an indexing-request dispatch so awaitIndexingDrained counts it as
|
|
1919
|
+
* live work from the moment it is sent, not from the moment it is acked. */
|
|
1920
|
+
trackIndexDispatch(p) {
|
|
1921
|
+
var self = this;
|
|
1922
|
+
this._indexDispatchesInFlight += 1;
|
|
1923
|
+
var release = function() {
|
|
1924
|
+
self._indexDispatchesInFlight = Math.max(0, self._indexDispatchesInFlight - 1);
|
|
1925
|
+
};
|
|
1926
|
+
return p.then(function(v) {
|
|
1927
|
+
release();
|
|
1928
|
+
return v;
|
|
1929
|
+
}, function(e) {
|
|
1930
|
+
release();
|
|
1931
|
+
throw e;
|
|
1932
|
+
});
|
|
1904
1933
|
}
|
|
1905
1934
|
/**
|
|
1906
1935
|
* Register a live poll so (a) a remount dedupes against it instead of stacking a
|
|
@@ -2006,6 +2035,7 @@ var ChatSession = class {
|
|
|
2006
2035
|
if (!key) return;
|
|
2007
2036
|
this.aiChatHistoryCache[key] = {
|
|
2008
2037
|
messages: this.state.messages.filter(function(m) {
|
|
2038
|
+
if (m._stageId) return false;
|
|
2009
2039
|
return m._ownerKey === void 0 || m._ownerKey === key;
|
|
2010
2040
|
}),
|
|
2011
2041
|
endOfList: this.state.historyEndOfList,
|
|
@@ -2145,14 +2175,189 @@ var ChatSession = class {
|
|
|
2145
2175
|
this.pendingAgentRequests[params.key] = run;
|
|
2146
2176
|
return run;
|
|
2147
2177
|
}
|
|
2178
|
+
/**
|
|
2179
|
+
* Put a turn on screen the INSTANT the user hits Send, before its attachments
|
|
2180
|
+
* have finished uploading. Uploads run in the background now (the composer is
|
|
2181
|
+
* cleared and stays usable), so without a staged bubble the message would
|
|
2182
|
+
* appear only once its files were up — below anything the user sent in the
|
|
2183
|
+
* meantime, in an order that never matches what they typed.
|
|
2184
|
+
*
|
|
2185
|
+
* Staged bubbles carry _useBgQueue because that is where a turn with
|
|
2186
|
+
* attachments ultimately dispatches (behind its own indexing tasks). That flag
|
|
2187
|
+
* is also what keeps promoteNextQueuedToRunning / resolveQueuedUserBubble off
|
|
2188
|
+
* them: those advance the SERVER queue, and a staged turn has no server
|
|
2189
|
+
* request behind it yet.
|
|
2190
|
+
*
|
|
2191
|
+
* Returns the id to hand back as PinnedDispatchContext.stageId at dispatch.
|
|
2192
|
+
*/
|
|
2193
|
+
stageOutgoingMessage(displayText) {
|
|
2194
|
+
this._stageSeq += 1;
|
|
2195
|
+
var stageId = "stg_" + this._stageSeq;
|
|
2196
|
+
var key = this.getHistoryCacheKey();
|
|
2197
|
+
var staged = {
|
|
2198
|
+
role: "user",
|
|
2199
|
+
content: displayText,
|
|
2200
|
+
isPendingQueued: true,
|
|
2201
|
+
isUploadingAttachments: true,
|
|
2202
|
+
isSendingToServer: true,
|
|
2203
|
+
_useBgQueue: true,
|
|
2204
|
+
_stageId: stageId,
|
|
2205
|
+
_ts: wallClockNow()
|
|
2206
|
+
};
|
|
2207
|
+
if (key) staged._ownerKey = key;
|
|
2208
|
+
this.state.messages.push(staged);
|
|
2209
|
+
this.host.notify();
|
|
2210
|
+
this.host.scrollToBottom(true);
|
|
2211
|
+
return stageId;
|
|
2212
|
+
}
|
|
2213
|
+
_stageIndex(list, stageId) {
|
|
2214
|
+
if (!stageId) return -1;
|
|
2215
|
+
for (var i = 0; i < list.length; i++) {
|
|
2216
|
+
if (list[i] && list[i]._stageId === stageId) return i;
|
|
2217
|
+
}
|
|
2218
|
+
return -1;
|
|
2219
|
+
}
|
|
2220
|
+
/**
|
|
2221
|
+
* Where a staged turn belongs once it is finally sent: BELOW every indexing
|
|
2222
|
+
* row, because that is the order it ran in and the order the server history
|
|
2223
|
+
* will report on the next load (its request id is newer than every pass it
|
|
2224
|
+
* waited for). While its files were uploading it sat above them — the rows
|
|
2225
|
+
* are injected as each file's pass starts, after the bubble was staged.
|
|
2226
|
+
*
|
|
2227
|
+
* Returns the index to insert at, or -1 to leave the turn where it is. Never
|
|
2228
|
+
* moves a turn UP: a bubble that already sits below the indexing rows (or a
|
|
2229
|
+
* chat with no indexing at all) must not jump backwards over anything.
|
|
2230
|
+
*/
|
|
2231
|
+
_settledStagePosition(fromIdx) {
|
|
2232
|
+
var lastBg = -1;
|
|
2233
|
+
for (var i = 0; i < this.state.messages.length; i++) {
|
|
2234
|
+
if (this.state.messages[i] && this.state.messages[i].isBackgroundTask) lastBg = i;
|
|
2235
|
+
}
|
|
2236
|
+
if (lastBg <= fromIdx) return -1;
|
|
2237
|
+
return lastBg;
|
|
2238
|
+
}
|
|
2239
|
+
/** The staged turn's files are up; it is now just waiting its place in the
|
|
2240
|
+
* queue. Drops the "(Uploading files...)" note back to "(In queue)". */
|
|
2241
|
+
markStagedMessageQueued(stageId) {
|
|
2242
|
+
var idx = this._stageIndex(this.state.messages, stageId);
|
|
2243
|
+
if (idx === -1) return;
|
|
2244
|
+
var ex = this.state.messages[idx];
|
|
2245
|
+
if (!ex.isUploadingAttachments) return;
|
|
2246
|
+
this.state.messages[idx] = Object.assign({}, ex, { isUploadingAttachments: false });
|
|
2247
|
+
this.host.notify();
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Resolves once this project's background-indexing queue has nothing left to
|
|
2251
|
+
* run, so a chat enqueued right after it is genuinely last.
|
|
2252
|
+
*
|
|
2253
|
+
* Sending the chat as soon as the uploads finish is not enough, which is the
|
|
2254
|
+
* whole reason this exists: indexing a file is a CHAIN, and each pass is only
|
|
2255
|
+
* enqueued once the previous one lands (the client mints CONTINUE passes for
|
|
2256
|
+
* text/grid files, the worker mints them for PDFs and windowed reads). Every
|
|
2257
|
+
* one of those passes therefore queues up BEHIND a chat sent at upload time,
|
|
2258
|
+
* and the model answers from a file it has only partly read.
|
|
2259
|
+
*
|
|
2260
|
+
* The queue is read from the server's status index rather than from
|
|
2261
|
+
* bgTaskQueue: that mirror holds only what this client dispatched or adopted,
|
|
2262
|
+
* and it stops being maintained once the view unmounts. An empty answer has to
|
|
2263
|
+
* repeat before it is believed — see INDEXING_DRAIN_IDLE_LOOKS — and a look
|
|
2264
|
+
* that fails counts as busy, so a dropped request delays the turn instead of
|
|
2265
|
+
* releasing it early.
|
|
2266
|
+
*
|
|
2267
|
+
* Reads the identity PINNED at Send time, never a live one: the user may be in
|
|
2268
|
+
* another project by now, and this must keep asking about the one they sent
|
|
2269
|
+
* from.
|
|
2270
|
+
*/
|
|
2271
|
+
awaitIndexingDrained(identity) {
|
|
2272
|
+
var self = this;
|
|
2273
|
+
var svcId = identity && identity.serviceId;
|
|
2274
|
+
var platform = identity && identity.platform;
|
|
2275
|
+
if (!svcId || platform !== "claude" && platform !== "openai") return Promise.resolve("skipped");
|
|
2276
|
+
var owner = identity.owner;
|
|
2277
|
+
var queue = bgIndexingQueueName(identity.userId, svcId);
|
|
2278
|
+
var startedAt = nowMs();
|
|
2279
|
+
var deadline = startedAt + INDEXING_DRAIN_TIMEOUT_MS;
|
|
2280
|
+
var idleLooks = 0;
|
|
2281
|
+
var ask = function(status) {
|
|
2282
|
+
return Promise.resolve(getChatHistory(
|
|
2283
|
+
{ service: svcId, owner, platform, queue, status },
|
|
2284
|
+
{ limit: WORKER_PASS_ADOPT_LIMIT }
|
|
2285
|
+
)).catch(function() {
|
|
2286
|
+
return null;
|
|
2287
|
+
});
|
|
2288
|
+
};
|
|
2289
|
+
var hasLiveIndexing = function(res) {
|
|
2290
|
+
var list = res && Array.isArray(res.list) ? res.list : [];
|
|
2291
|
+
for (var i = 0; i < list.length; i++) {
|
|
2292
|
+
var item = list[i];
|
|
2293
|
+
if (!item || item.status !== "pending" && item.status !== "running") continue;
|
|
2294
|
+
if (isIndexingRequestText(extractLastUserTextFromRequest(item.request_body))) return true;
|
|
2295
|
+
}
|
|
2296
|
+
return false;
|
|
2297
|
+
};
|
|
2298
|
+
return new Promise(function(resolve) {
|
|
2299
|
+
var again = function() {
|
|
2300
|
+
setTimeout(look, idleLooks > 0 ? INDEXING_DRAIN_CONFIRM_POLL_MS : INDEXING_DRAIN_BUSY_POLL_MS);
|
|
2301
|
+
};
|
|
2302
|
+
var look = function() {
|
|
2303
|
+
if (nowMs() >= deadline) {
|
|
2304
|
+
resolve("timedout");
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
if (self._indexDispatchesInFlight > 0) {
|
|
2308
|
+
idleLooks = 0;
|
|
2309
|
+
again();
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
Promise.all([ask("running"), ask("pending")]).then(function(res) {
|
|
2313
|
+
var unknown = res[0] === null || res[1] === null;
|
|
2314
|
+
if (unknown || hasLiveIndexing(res[0]) || hasLiveIndexing(res[1])) idleLooks = 0;
|
|
2315
|
+
else idleLooks += 1;
|
|
2316
|
+
if (idleLooks >= INDEXING_DRAIN_IDLE_LOOKS && nowMs() - startedAt >= INDEXING_DRAIN_MIN_MS) {
|
|
2317
|
+
resolve("drained");
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
again();
|
|
2321
|
+
}, function() {
|
|
2322
|
+
idleLooks = 0;
|
|
2323
|
+
again();
|
|
2324
|
+
});
|
|
2325
|
+
};
|
|
2326
|
+
look();
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
/**
|
|
2330
|
+
* Abandon a staged turn — its uploads failed outright, so nothing will be
|
|
2331
|
+
* dispatched. The bubble stays (the user's text is not silently thrown away)
|
|
2332
|
+
* but settles into a plain, non-pending message; the caller reports the
|
|
2333
|
+
* failure separately.
|
|
2334
|
+
*/
|
|
2335
|
+
settleStagedMessage(stageId) {
|
|
2336
|
+
var idx = this._stageIndex(this.state.messages, stageId);
|
|
2337
|
+
if (idx === -1) return;
|
|
2338
|
+
var ex = this.state.messages[idx];
|
|
2339
|
+
var settled = { role: "user", content: ex.content };
|
|
2340
|
+
if (ex._ownerKey !== void 0) settled._ownerKey = ex._ownerKey;
|
|
2341
|
+
if (ex._ts !== void 0) settled._ts = ex._ts;
|
|
2342
|
+
this.state.messages[idx] = settled;
|
|
2343
|
+
this.host.notify();
|
|
2344
|
+
this.updateHistoryCache();
|
|
2345
|
+
}
|
|
2148
2346
|
// composed = clean display text; composedForLlm carries office-extraction
|
|
2149
2347
|
// placeholders for the provider only. useBgQueue routes a post-attachment turn
|
|
2150
2348
|
// onto the "-bg" queue so it runs after indexing.
|
|
2151
2349
|
dispatchComposedMessage(composed, useBgQueue, composedForLlm, extractContent, fileUrls, pinned) {
|
|
2152
2350
|
var self = this;
|
|
2153
|
-
|
|
2351
|
+
var stageId = pinned ? pinned.stageId : void 0;
|
|
2352
|
+
if (!composed) {
|
|
2353
|
+
if (stageId) this.settleStagedMessage(stageId);
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2154
2356
|
var id = pinned ? pinned.identity : this.host.getIdentity();
|
|
2155
|
-
if (id.platform === "none")
|
|
2357
|
+
if (id.platform === "none") {
|
|
2358
|
+
if (stageId) this.settleStagedMessage(stageId);
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2156
2361
|
var llmComposed = composedForLlm || composed;
|
|
2157
2362
|
var key = !id.serviceId ? "" : id.serviceId + "#" + id.platform;
|
|
2158
2363
|
var offChat = !!key && key !== this.getHistoryCacheKey();
|
|
@@ -2163,7 +2368,7 @@ var ChatSession = class {
|
|
|
2163
2368
|
var aiModel = id.model || void 0;
|
|
2164
2369
|
var systemPrompt = pinned ? pinned.systemPrompt : this.host.buildSystemPrompt();
|
|
2165
2370
|
var userId = id.userId || id.serviceId;
|
|
2166
|
-
var chatQueue = useBgQueue ? userId
|
|
2371
|
+
var chatQueue = useBgQueue ? bgIndexingQueueName(userId) : userId;
|
|
2167
2372
|
if (offChat) {
|
|
2168
2373
|
var offHistory = (this.aiChatHistoryCache[key] ? this.aiChatHistoryCache[key].messages : []).filter(function(m) {
|
|
2169
2374
|
return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask && !m.isError;
|
|
@@ -2176,9 +2381,16 @@ var ChatSession = class {
|
|
|
2176
2381
|
history: offHistory.concat([{ role: "user", content: llmComposed }])
|
|
2177
2382
|
});
|
|
2178
2383
|
var offExisting = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
|
|
2384
|
+
var offUser = { role: "user", content: composed, _ownerKey: key, _ts: wallClockNow() };
|
|
2385
|
+
var offStage = this._stageIndex(this.state.messages, stageId);
|
|
2386
|
+
if (offStage !== -1) {
|
|
2387
|
+
if (this.state.messages[offStage]._ts !== void 0) offUser._ts = this.state.messages[offStage]._ts;
|
|
2388
|
+
this.state.messages.splice(offStage, 1);
|
|
2389
|
+
this.host.notify();
|
|
2390
|
+
}
|
|
2179
2391
|
this.aiChatHistoryCache[key] = {
|
|
2180
2392
|
messages: offExisting.messages.concat([
|
|
2181
|
-
|
|
2393
|
+
offUser,
|
|
2182
2394
|
{ role: "assistant", content: "", isPending: true, isPendingInProcess: true, _ownerKey: key }
|
|
2183
2395
|
]),
|
|
2184
2396
|
endOfList: offExisting.endOfList,
|
|
@@ -2213,14 +2425,26 @@ var ChatSession = class {
|
|
|
2213
2425
|
var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true, _ts: wallClockNow() };
|
|
2214
2426
|
if (key) queuedBubble._ownerKey = key;
|
|
2215
2427
|
if (useBgQueue) queuedBubble._useBgQueue = true;
|
|
2216
|
-
this.state.messages
|
|
2428
|
+
var qStage = this._stageIndex(this.state.messages, stageId);
|
|
2429
|
+
if (qStage !== -1) {
|
|
2430
|
+
if (this.state.messages[qStage]._ts !== void 0) queuedBubble._ts = this.state.messages[qStage]._ts;
|
|
2431
|
+
var qTarget = this._settledStagePosition(qStage);
|
|
2432
|
+
if (qTarget === -1) {
|
|
2433
|
+
this.state.messages.splice(qStage, 1, queuedBubble);
|
|
2434
|
+
} else {
|
|
2435
|
+
this.state.messages.splice(qStage, 1);
|
|
2436
|
+
this.state.messages.splice(qTarget, 0, queuedBubble);
|
|
2437
|
+
}
|
|
2438
|
+
} else {
|
|
2439
|
+
this.state.messages.push(queuedBubble);
|
|
2440
|
+
}
|
|
2217
2441
|
this.host.notify();
|
|
2218
2442
|
this.updateHistoryCache();
|
|
2219
2443
|
this.host.scrollToBottom(true);
|
|
2220
2444
|
var capturedComposed = composed, capturedPlatform = aiPlatform, capturedKey = key;
|
|
2221
2445
|
Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent, fileUrls, id.serviceId, id.owner)).then(function(result) {
|
|
2222
2446
|
var sendingIdx = self.getHistoryCacheKey() !== capturedKey ? -1 : self.state.messages.findIndex(function(m) {
|
|
2223
|
-
return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user" && (m._ownerKey === void 0 || m._ownerKey === capturedKey);
|
|
2447
|
+
return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user" && !m._stageId && (m._ownerKey === void 0 || m._ownerKey === capturedKey);
|
|
2224
2448
|
});
|
|
2225
2449
|
var serverId = result && typeof result.id === "string" ? result.id : void 0;
|
|
2226
2450
|
if (sendingIdx >= 0) {
|
|
@@ -2245,23 +2469,31 @@ var ChatSession = class {
|
|
|
2245
2469
|
});
|
|
2246
2470
|
return;
|
|
2247
2471
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2472
|
+
var immediateUser = { role: "user", content: composed, _ts: wallClockNow(), ...key ? { _ownerKey: key } : {} };
|
|
2473
|
+
var immediatePlaceholder = { role: "assistant", content: "", isPending: true, isPendingInProcess: true, ...key ? { _ownerKey: key } : {} };
|
|
2474
|
+
var iStage = this._stageIndex(this.state.messages, stageId);
|
|
2475
|
+
if (iStage !== -1) {
|
|
2476
|
+
if (this.state.messages[iStage]._ts !== void 0) immediateUser._ts = this.state.messages[iStage]._ts;
|
|
2477
|
+
var iTarget = this._settledStagePosition(iStage);
|
|
2478
|
+
if (iTarget === -1) {
|
|
2479
|
+
this.state.messages.splice(iStage, 1, immediateUser, immediatePlaceholder);
|
|
2480
|
+
} else {
|
|
2481
|
+
this.state.messages.splice(iStage, 1);
|
|
2482
|
+
this.state.messages.splice(iTarget, 0, immediateUser, immediatePlaceholder);
|
|
2483
|
+
}
|
|
2484
|
+
} else {
|
|
2485
|
+
this.state.messages.push(immediateUser);
|
|
2486
|
+
this.state.messages.push(immediatePlaceholder);
|
|
2487
|
+
}
|
|
2250
2488
|
this.host.notify();
|
|
2251
2489
|
this.updateHistoryCache();
|
|
2252
2490
|
this.state.sending = true;
|
|
2253
2491
|
this.host.scrollToBottom(true);
|
|
2254
2492
|
var historyForLlm = this.state.messages.filter(function(m) {
|
|
2493
|
+
if (m === immediateUser) return false;
|
|
2255
2494
|
return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask && !m.isError;
|
|
2256
2495
|
});
|
|
2257
|
-
|
|
2258
|
-
for (var li = historyForLlm.length - 1; li >= 0; li--) {
|
|
2259
|
-
if (historyForLlm[li].role === "user" && historyForLlm[li].content === composed) {
|
|
2260
|
-
historyForLlm[li] = Object.assign({}, historyForLlm[li], { content: llmComposed });
|
|
2261
|
-
break;
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
}
|
|
2496
|
+
historyForLlm.push({ role: "user", content: llmComposed });
|
|
2265
2497
|
var bounded = buildBoundedChatMessages({
|
|
2266
2498
|
platform: aiPlatform,
|
|
2267
2499
|
model: aiModel,
|
|
@@ -2502,7 +2734,7 @@ var ChatSession = class {
|
|
|
2502
2734
|
if (platform !== "claude" && platform !== "openai") return;
|
|
2503
2735
|
var url = platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
|
|
2504
2736
|
var queueBase = id.userId || id.serviceId;
|
|
2505
|
-
var queue = msg.isBackgroundTask || msg._useBgQueue ? queueBase
|
|
2737
|
+
var queue = msg.isBackgroundTask || msg._useBgQueue ? bgIndexingQueueName(queueBase) : queueBase;
|
|
2506
2738
|
this.state.messages[idx] = Object.assign({}, msg, { _cancelling: true, _cancelError: void 0 });
|
|
2507
2739
|
this.host.notify();
|
|
2508
2740
|
Promise.resolve(this.host.cancelRequest({
|
|
@@ -2988,7 +3220,7 @@ var ChatSession = class {
|
|
|
2988
3220
|
if (!id.serviceId || platform !== "claude" && platform !== "openai") return;
|
|
2989
3221
|
if (this.isPollingPaused() || !this.host.isViewMounted()) return;
|
|
2990
3222
|
var svcId = id.serviceId, owner = id.owner;
|
|
2991
|
-
var queue = (id.userId
|
|
3223
|
+
var queue = bgIndexingQueueName(id.userId, id.serviceId);
|
|
2992
3224
|
var ask = function(status) {
|
|
2993
3225
|
return Promise.resolve(getChatHistory(
|
|
2994
3226
|
{ service: svcId, owner, platform, queue, status },
|
|
@@ -3090,7 +3322,7 @@ var ChatSession = class {
|
|
|
3090
3322
|
url,
|
|
3091
3323
|
method: "POST",
|
|
3092
3324
|
id: serverId,
|
|
3093
|
-
queue: (id.userId
|
|
3325
|
+
queue: bgIndexingQueueName(id.userId, id.serviceId),
|
|
3094
3326
|
service: id.serviceId,
|
|
3095
3327
|
owner: id.owner
|
|
3096
3328
|
})).catch(function() {
|
|
@@ -3219,7 +3451,7 @@ var ChatSession = class {
|
|
|
3219
3451
|
if (pass > MAX_INDEXING_RESUME_PASSES) return;
|
|
3220
3452
|
var id = this.host.getIdentity();
|
|
3221
3453
|
if (!id || id.platform === "none" || id.serviceId !== entry.serviceId) return;
|
|
3222
|
-
notifyAgentContinueIndexing({
|
|
3454
|
+
this.trackIndexDispatch(notifyAgentContinueIndexing({
|
|
3223
3455
|
platform: id.platform,
|
|
3224
3456
|
model: id.model,
|
|
3225
3457
|
service: id.serviceId,
|
|
@@ -3253,7 +3485,7 @@ var ChatSession = class {
|
|
|
3253
3485
|
}
|
|
3254
3486
|
}, function(e) {
|
|
3255
3487
|
console.error("[chat-engine] resume-indexing dispatch failed", e);
|
|
3256
|
-
});
|
|
3488
|
+
}));
|
|
3257
3489
|
} catch (e) {
|
|
3258
3490
|
}
|
|
3259
3491
|
}
|
|
@@ -3331,6 +3563,10 @@ var ChatSession = class {
|
|
|
3331
3563
|
if (mm._ownerKey !== void 0 && mm._ownerKey !== loadKey) continue;
|
|
3332
3564
|
if (mm._serverItemId && serverIds[mm._serverItemId]) continue;
|
|
3333
3565
|
if (!mm._serverItemId) {
|
|
3566
|
+
if (mm._stageId) {
|
|
3567
|
+
rescued.push(mm);
|
|
3568
|
+
continue;
|
|
3569
|
+
}
|
|
3334
3570
|
if (mappedHasPendingAssistant) continue;
|
|
3335
3571
|
if (mm.isSendingToServer || mm.isPendingQueued || mm.isPendingInProcess || mm.isPending) rescued.push(mm);
|
|
3336
3572
|
else if (self.state.sending && mm.role === "user") {
|
|
@@ -3550,7 +3786,7 @@ var ChatSession = class {
|
|
|
3550
3786
|
return preIndex.then(function() {
|
|
3551
3787
|
return parseAttachmentContent(member.file, member.file.name, mime || void 0);
|
|
3552
3788
|
}).then(function(parsedContent) {
|
|
3553
|
-
return notifyAgentSaveAttachment({
|
|
3789
|
+
return self.trackIndexDispatch(notifyAgentSaveAttachment({
|
|
3554
3790
|
platform: id.platform,
|
|
3555
3791
|
model: id.model,
|
|
3556
3792
|
service: id.serviceId,
|
|
@@ -3589,7 +3825,7 @@ var ChatSession = class {
|
|
|
3589
3825
|
att.errorCode = e && (e.code || e.body && e.body.code) || "";
|
|
3590
3826
|
att.errorDetail = e && (e.message || e.body && e.body.message) || (typeof e === "string" ? e : "");
|
|
3591
3827
|
}
|
|
3592
|
-
});
|
|
3828
|
+
}));
|
|
3593
3829
|
});
|
|
3594
3830
|
});
|
|
3595
3831
|
});
|
|
@@ -3608,14 +3844,24 @@ var ChatSession = class {
|
|
|
3608
3844
|
}
|
|
3609
3845
|
// Upload all not-yet-done attachments sequentially. Resolves to the full
|
|
3610
3846
|
// list of { name, url, storagePath } for composing the chat message.
|
|
3611
|
-
|
|
3847
|
+
//
|
|
3848
|
+
// `batchId` scopes the run to the chips stamped with it at Send time. The
|
|
3849
|
+
// composer stays live during an upload, so by the time this runs the
|
|
3850
|
+
// attachment list can already hold chips the user picked for the NEXT
|
|
3851
|
+
// message — uploading those here would attach them to the wrong turn, and
|
|
3852
|
+
// collecting the previous batch's finished urls would attach files the user
|
|
3853
|
+
// already sent. Omitted (no batch) means every chip, the old behavior.
|
|
3854
|
+
uploadPendingAttachments(batchId) {
|
|
3612
3855
|
var self = this;
|
|
3613
3856
|
this.host.resetOverwriteBatch();
|
|
3857
|
+
this._uploadBatches += 1;
|
|
3614
3858
|
this.state.uploadingAttachments = true;
|
|
3615
3859
|
this.host.updateComposerControls();
|
|
3616
3860
|
this.host.renderAttachmentChips();
|
|
3617
3861
|
var collected = [];
|
|
3618
|
-
var snapshot = this.state.attachments.
|
|
3862
|
+
var snapshot = this.state.attachments.filter(function(a) {
|
|
3863
|
+
return batchId ? a._batchId === batchId : true;
|
|
3864
|
+
});
|
|
3619
3865
|
var chain = Promise.resolve();
|
|
3620
3866
|
snapshot.forEach(function(att) {
|
|
3621
3867
|
chain = chain.then(function() {
|
|
@@ -3651,7 +3897,8 @@ var ChatSession = class {
|
|
|
3651
3897
|
});
|
|
3652
3898
|
});
|
|
3653
3899
|
var done = function() {
|
|
3654
|
-
self.
|
|
3900
|
+
self._uploadBatches = Math.max(0, self._uploadBatches - 1);
|
|
3901
|
+
self.state.uploadingAttachments = self._uploadBatches > 0;
|
|
3655
3902
|
self.host.updateComposerControls();
|
|
3656
3903
|
self.host.renderAttachmentChips();
|
|
3657
3904
|
return collected;
|
|
@@ -3857,6 +4104,6 @@ function buildChatDisplayList(messages, opts) {
|
|
|
3857
4104
|
return out;
|
|
3858
4105
|
}
|
|
3859
4106
|
|
|
3860
|
-
export { BG_INDEXING_QUEUE_SUFFIX, BOM, BOM_EXTS, CLAUDE_INPUT_CAP_RATIO, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, ChatSession, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, EXPIRED_LINK_REFRESH_EXPIRES_SECONDS, EXT_CONTENT_TYPES, HISTORY_BUDGET_RATIO, HISTORY_FILL_SLACK_PX, HISTORY_TOKEN_BUDGET, HTML_EXTS, HTML_HEAD_WINDOW, LINK_LABEL_MAX_DISPLAY_CHARS, LINK_REFRESH_WINDOW_MS, MAX_HISTORY_FILL_PAGES, MAX_HISTORY_MESSAGES, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MIN_INPUT_TOKEN_BUDGET, OUTPUT_TOKEN_RESERVE, POLL_INTERVAL, RENDER_FROM_TOKEN, RTF_EXTS, TOOL_AND_RESPONSE_BUFFER, XML_EXTS, applyEncodingDeclaration, buildAiAgentValue, buildBoundedChatMessages, buildChatDisplayList, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, classifyInlineLink, clearAttachmentParsers, composeUserMessage, configureChatEngine, contentTypeForExt, createHistoryFiller, createInlineLinkRegex, encodePathSegments, encodingClassForExt, ensureHtmlCharset, ensureXmlEncoding, escapeRtfNonAscii, estimateMessageTokens, estimateTextTokens, extOf, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, fillHistoryViewport, filterListByClearHorizon, findAttachmentParser, formatChatTimestamp, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, getProjectContextWindow, groupAttachmentFailures, hasBom, isAuthExpiredError, isBgIndexingQueue, isErrorResponseBody, isHttpUrlLike, isIndexingRequestText, isNonRetryableRequestError, isOfficeFile, isServerExtractable, isServiceDbAttachmentHref, listClaudeModels, listOpenAIModels, looksLikeRtf, makeExtractPlaceholder, mapHistoryListToMessages, needsBomForExt, normalizeAttachmentPathCandidate, normalizeExt, normalizeTextContent, normalizeTrailingInlineToken, notifyAgentSaveAttachment, parseAiAgentValue, parseAttachmentContent, parseIndexingLabel, parseIndexingRequestText, prepareDownloadText, readExpiredAttachmentHref, registerAttachmentParser, registerModelContextWindows, repairUrlEntities, repairUrlWhitespace, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, setProjectContextWindow, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay, wallClockNow };
|
|
4107
|
+
export { BG_INDEXING_QUEUE_SUFFIX, BOM, BOM_EXTS, CLAUDE_INPUT_CAP_RATIO, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, ChatSession, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, EXPIRED_LINK_REFRESH_EXPIRES_SECONDS, EXT_CONTENT_TYPES, HISTORY_BUDGET_RATIO, HISTORY_FILL_SLACK_PX, HISTORY_TOKEN_BUDGET, HTML_EXTS, HTML_HEAD_WINDOW, LINK_LABEL_MAX_DISPLAY_CHARS, LINK_REFRESH_WINDOW_MS, MAX_HISTORY_FILL_PAGES, MAX_HISTORY_MESSAGES, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MIN_INPUT_TOKEN_BUDGET, OUTPUT_TOKEN_RESERVE, POLL_INTERVAL, RENDER_FROM_TOKEN, RTF_EXTS, TOOL_AND_RESPONSE_BUFFER, XML_EXTS, applyEncodingDeclaration, bgIndexingQueueName, buildAiAgentValue, buildBoundedChatMessages, buildChatDisplayList, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, classifyInlineLink, clearAttachmentParsers, composeUserMessage, configureChatEngine, contentTypeForExt, createHistoryFiller, createInlineLinkRegex, encodePathSegments, encodingClassForExt, ensureHtmlCharset, ensureXmlEncoding, escapeRtfNonAscii, estimateMessageTokens, estimateTextTokens, extOf, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, fillHistoryViewport, filterListByClearHorizon, findAttachmentParser, formatChatTimestamp, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, getProjectContextWindow, groupAttachmentFailures, hasBom, isAuthExpiredError, isBgIndexingQueue, isErrorResponseBody, isHttpUrlLike, isIndexingRequestText, isNonRetryableRequestError, isOfficeFile, isServerExtractable, isServiceDbAttachmentHref, listClaudeModels, listOpenAIModels, looksLikeRtf, makeExtractPlaceholder, mapHistoryListToMessages, needsBomForExt, normalizeAttachmentPathCandidate, normalizeExt, normalizeTextContent, normalizeTrailingInlineToken, notifyAgentSaveAttachment, parseAiAgentValue, parseAttachmentContent, parseIndexingLabel, parseIndexingRequestText, prepareDownloadText, readExpiredAttachmentHref, registerAttachmentParser, registerModelContextWindows, repairUrlEntities, repairUrlWhitespace, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, setProjectContextWindow, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay, wallClockNow };
|
|
3861
4108
|
//# sourceMappingURL=engine.mjs.map
|
|
3862
4109
|
//# sourceMappingURL=engine.mjs.map
|