bunnyquery 1.8.1 → 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/dist/engine.d.mts CHANGED
@@ -372,6 +372,102 @@ declare function buildBoundedChatMessages(options: BoundedChatOptions): {
372
372
  estimatedBudget: number;
373
373
  };
374
374
 
375
+ /**
376
+ * How a text file the chat offers as a download is encoded and labelled, so that
377
+ * whatever opens it reads the characters correctly, in any language.
378
+ *
379
+ * When the model answers with a fenced ```name.ext block, the client turns that
380
+ * block into a Blob and an <a download>. We always write UTF-8, but several very
381
+ * common consumers do not assume UTF-8 when nothing in the file says so, and fall
382
+ * back to the reader's local ANSI codepage: CP949 in Korea, CP932 in Japan,
383
+ * CP936/CP950 in China and Taiwan, CP1251 for Cyrillic. The file is valid and
384
+ * every non-ASCII character still opens as mojibake.
385
+ *
386
+ * There is no single fix, because the way a file declares "this is UTF-8" is a
387
+ * property of the FORMAT:
388
+ *
389
+ * - a spreadsheet (csv/tsv) and a Windows text editor read a BOM;
390
+ * - HTML is opened from disk with no HTTP headers, so only an in-document
391
+ * <meta charset> survives;
392
+ * - XML carries its encoding in its declaration, and a WRONG declaration makes
393
+ * a conforming parser fail outright;
394
+ * - RTF is 7-bit, so non-ASCII has to be escaped into \uNNNN?;
395
+ * - JSON, JSONL and YAML are UTF-8 by specification, and a BOM BREAKS them.
396
+ *
397
+ * Anything unrecognised is left byte-for-byte alone: an unknown extension is far
398
+ * more likely to be machine-parsed, where an uninvited BOM is a new bug, than to
399
+ * be opened by a legacy editor.
400
+ *
401
+ * MIRROR of skapi-mcp/download-encoding.js, which does the same job for files the
402
+ * server publishes (writeReport / exportRecordsToFile). A file the user gets from
403
+ * a fenced block and the same file published as a download must behave
404
+ * identically, so the two have to change together.
405
+ */
406
+ declare const BOM = "\uFEFF";
407
+ /** Files a spreadsheet or a Windows text editor opens directly. */
408
+ declare const BOM_EXTS: Set<string>;
409
+ /** Read from disk with no HTTP headers, so the declaration must be in the file. */
410
+ declare const HTML_EXTS: Set<string>;
411
+ declare const XML_EXTS: Set<string>;
412
+ declare const RTF_EXTS: Set<string>;
413
+ /** Content types by extension. Every text family carries an explicit charset. */
414
+ declare const EXT_CONTENT_TYPES: Record<string, string>;
415
+ type EncodingClass = 'bom' | 'html' | 'xml' | 'rtf' | 'none';
416
+ declare function normalizeExt(ext: string | null | undefined): string;
417
+ /** Extension of a filename, '' when it has none. */
418
+ declare function extOf(filename: string | null | undefined): string;
419
+ /** Which encoding declaration this format understands. */
420
+ declare function encodingClassForExt(ext: string | null | undefined): EncodingClass;
421
+ /** True when a file with this extension must be written BOM-first. */
422
+ declare function needsBomForExt(ext: string | null | undefined): boolean;
423
+ /**
424
+ * Content type to declare. Everything textual carries an explicit charset:
425
+ * without one the receiving end guesses, and it guesses the local codepage.
426
+ */
427
+ declare function contentTypeForExt(ext: string | null | undefined, fallback?: string): string;
428
+ declare function hasBom(text: string): boolean;
429
+ declare const HTML_HEAD_WINDOW = 4096;
430
+ /**
431
+ * Make an HTML document state its own encoding. Downloaded HTML is opened from
432
+ * disk, where the Content-Type we set no longer exists, so a document with no
433
+ * <meta charset> is decoded with the browser's locale default.
434
+ */
435
+ declare function ensureHtmlCharset(text: string): string;
436
+ /**
437
+ * Correct an XML declaration that names the wrong encoding.
438
+ *
439
+ * A MISSING declaration is left alone on purpose: XML with none is UTF-8 by
440
+ * specification, so every conforming parser already gets it right. A declaration
441
+ * naming EUC-KR over UTF-8 bytes, on the other hand, makes a parser fail outright.
442
+ */
443
+ declare function ensureXmlEncoding(text: string): string;
444
+ /** True when the body really is RTF rather than text merely named .rtf. */
445
+ declare function looksLikeRtf(text: string): boolean;
446
+ /**
447
+ * Escape every non-ASCII character into RTF's \uNNNN? form.
448
+ *
449
+ * RTF is 7-bit: a literal UTF-8 byte in the body is read through the codepage the
450
+ * header declares, which is how Korean, Japanese and Cyrillic RTF turns to
451
+ * mojibake in Word. \uNNNN? is codepage-independent.
452
+ *
453
+ * ASCII is never touched, which matters: backslashes and braces in an RTF body
454
+ * are control syntax, and "escaping" them would destroy the document. \u takes a
455
+ * SIGNED 16-bit value, so anything above 0x7FFF is emitted negative, and astral
456
+ * characters are emitted as their two surrogates.
457
+ */
458
+ declare function escapeRtfNonAscii(text: string): string;
459
+ /** Apply the format's encoding declaration to a whole document. */
460
+ declare function applyEncodingDeclaration(text: string, ext: string | null | undefined): string;
461
+ /**
462
+ * Everything a client needs to turn a fenced ```name.ext block into a download:
463
+ * the exact text to put in the Blob and the type to give it.
464
+ */
465
+ declare function prepareDownloadText(filename: string, body: string): {
466
+ ext: string;
467
+ text: string;
468
+ contentType: string;
469
+ };
470
+
375
471
  /**
376
472
  * Pure link/path helpers (no DOM, no marked). Moved verbatim from the chatbox.
377
473
  * `serviceId` is passed as a PARAMETER (the original read it from a global) so
@@ -721,7 +817,7 @@ type CallClaudeWithMcpParams = {
721
817
  onResponse?: (res: any) => void;
722
818
  onError?: (err: any) => void;
723
819
  };
724
- declare const POLL_INTERVAL = 1500;
820
+ declare const POLL_INTERVAL = 3000;
725
821
  declare function callClaudeWithMcp({ prompt, messages, service, owner, userId, model, maxTokens, system, mcpServer, extractContent, fileUrls, }: CallClaudeWithMcpParams): Promise<any>;
726
822
  declare function callClaudeWithPublicMcp(prompt: string, service: string, owner: string, messages?: ClaudeMessage[], system?: string, model?: string, userId?: string, extractContent?: ExtractDirective[], fileUrls?: FileUrlDirective[], onResponse?: (res: any) => void, onError?: (err: any) => void): Promise<any>;
727
823
  declare function callOpenAIWithPublicMcp(prompt: string, service: string, owner: string, messages?: OpenAIMessage[], system?: string, model?: string, userId?: string, extractContent?: ExtractDirective[], fileUrls?: FileUrlDirective[], onResponse?: (res: any) => void, onError?: (err: any) => void): Promise<any>;
@@ -730,7 +826,17 @@ type AttachmentSaveInfo = {
730
826
  model?: string;
731
827
  service: string;
732
828
  owner: string;
733
- userId?: string;
829
+ /**
830
+ * Queue base for this indexing pass: "<userId>-bg". REQUIRED, and it must be
831
+ * the SAME value the chat turn uses (ChatSession.dispatchComposedMessage's
832
+ * `id.userId || id.serviceId`) — the backend serialises requests that share a
833
+ * queue name and runs different ones IN PARALLEL, so a pass enqueued under a
834
+ * different base does not hold the chat back at all. It was optional once,
835
+ * defaulting to `service`; the chatbox omitted it, and its files were indexed
836
+ * on "<serviceId>-bg" while its question ran on "<userId>-bg" — the question
837
+ * was answered from a file nothing had read yet. Pass `userId || serviceId`.
838
+ */
839
+ userId: string;
734
840
  serviceName?: string;
735
841
  serviceDescription?: string;
736
842
  attachment: {
@@ -765,6 +871,14 @@ declare function extractOpenAIText(response: any): any;
765
871
  declare function listClaudeModels(service: string, owner: string): Promise<any>;
766
872
  declare function listOpenAIModels(service: string, owner: string): Promise<any>;
767
873
  declare const BG_INDEXING_QUEUE_SUFFIX = "-bg";
874
+ /**
875
+ * The one place the background-indexing queue name is spelled out. The backend
876
+ * serialises requests sharing a queue name and runs different names in PARALLEL,
877
+ * so every indexing pass AND the chat turn that must wait behind them have to
878
+ * resolve to the identical string — see AttachmentSaveInfo.userId for what
879
+ * happens when they do not.
880
+ */
881
+ declare function bgIndexingQueueName(userId?: string, service?: string): string;
768
882
  /**
769
883
  * True when a request belongs to the background-indexing queue.
770
884
  *
@@ -835,6 +949,11 @@ interface ChatIdentity {
835
949
  interface PinnedDispatchContext {
836
950
  identity: ChatIdentity;
837
951
  systemPrompt: string;
952
+ /** Id returned by stageOutgoingMessage. The turn's bubble is already on
953
+ * screen (staged while its attachments upload), so dispatchComposedMessage
954
+ * REPLACES that bubble in place instead of pushing a second one at the
955
+ * bottom — the message keeps the position it was sent in. */
956
+ stageId?: string;
838
957
  }
839
958
  /**
840
959
  * The file a background-indexing bubble belongs to. Stamped on the REQUEST
@@ -868,6 +987,17 @@ interface ChatMessage {
868
987
  /** Set on background-indexing REQUEST bubbles only (see IndexingFileRef). */
869
988
  _indexFile?: IndexingFileRef;
870
989
  _useBgQueue?: boolean;
990
+ /** Local id of a turn STAGED at Send time while its attachments upload. The
991
+ * bubble exists before any server request does, so it is never matched by
992
+ * _serverItemId and is never promoted/cancelled by the queue machinery —
993
+ * dispatchComposedMessage consumes it (pinned.stageId) when the turn is
994
+ * finally sent. Staged bubbles are deliberately kept OUT of the history
995
+ * cache: an unmount kills the upload that would resolve them, so a cached
996
+ * copy would replay as a bubble that uploads forever. */
997
+ _stageId?: string;
998
+ /** True on a staged bubble while its files are still uploading (renders
999
+ * "(Uploading files...)" instead of "(In queue)"). */
1000
+ isUploadingAttachments?: boolean;
871
1001
  _serverItemId?: string;
872
1002
  _localId?: string;
873
1003
  _cancelling?: boolean;
@@ -1142,7 +1272,21 @@ declare class ChatSession {
1142
1272
  private _pauseReasons;
1143
1273
  private _resuming;
1144
1274
  private _lidSeq;
1275
+ private _stageSeq;
1276
+ /** How many attachment-upload batches are running. uploadingAttachments is a
1277
+ * single flag but batches overlap (the composer stays live, so the user can
1278
+ * send a second one while the first uploads), and a nested finish must not
1279
+ * clear the flag out from under the batch still running. */
1280
+ private _uploadBatches;
1281
+ /** Indexing requests whose ack has not come back yet. Until it does the item
1282
+ * is not on the server's queue, so awaitIndexingDrained cannot see it — and
1283
+ * would read the gap between "pass N settled" and "pass N+1 accepted" as the
1284
+ * file being finished. */
1285
+ private _indexDispatchesInFlight;
1145
1286
  constructor(host: ChatHost);
1287
+ /** Wrap an indexing-request dispatch so awaitIndexingDrained counts it as
1288
+ * live work from the moment it is sent, not from the moment it is acked. */
1289
+ trackIndexDispatch<T>(p: Promise<T>): Promise<T>;
1146
1290
  /**
1147
1291
  * Register a live poll so (a) a remount dedupes against it instead of stacking a
1148
1292
  * SECOND poll on the same item, and (b) pausePolling can stop it.
@@ -1204,6 +1348,68 @@ declare class ChatSession {
1204
1348
  */
1205
1349
  private _callProviderFor;
1206
1350
  dispatchAgentRequest(params: any): Promise<any>;
1351
+ /**
1352
+ * Put a turn on screen the INSTANT the user hits Send, before its attachments
1353
+ * have finished uploading. Uploads run in the background now (the composer is
1354
+ * cleared and stays usable), so without a staged bubble the message would
1355
+ * appear only once its files were up — below anything the user sent in the
1356
+ * meantime, in an order that never matches what they typed.
1357
+ *
1358
+ * Staged bubbles carry _useBgQueue because that is where a turn with
1359
+ * attachments ultimately dispatches (behind its own indexing tasks). That flag
1360
+ * is also what keeps promoteNextQueuedToRunning / resolveQueuedUserBubble off
1361
+ * them: those advance the SERVER queue, and a staged turn has no server
1362
+ * request behind it yet.
1363
+ *
1364
+ * Returns the id to hand back as PinnedDispatchContext.stageId at dispatch.
1365
+ */
1366
+ stageOutgoingMessage(displayText: string): string;
1367
+ private _stageIndex;
1368
+ /**
1369
+ * Where a staged turn belongs once it is finally sent: BELOW every indexing
1370
+ * row, because that is the order it ran in and the order the server history
1371
+ * will report on the next load (its request id is newer than every pass it
1372
+ * waited for). While its files were uploading it sat above them — the rows
1373
+ * are injected as each file's pass starts, after the bubble was staged.
1374
+ *
1375
+ * Returns the index to insert at, or -1 to leave the turn where it is. Never
1376
+ * moves a turn UP: a bubble that already sits below the indexing rows (or a
1377
+ * chat with no indexing at all) must not jump backwards over anything.
1378
+ */
1379
+ private _settledStagePosition;
1380
+ /** The staged turn's files are up; it is now just waiting its place in the
1381
+ * queue. Drops the "(Uploading files...)" note back to "(In queue)". */
1382
+ markStagedMessageQueued(stageId: string): void;
1383
+ /**
1384
+ * Resolves once this project's background-indexing queue has nothing left to
1385
+ * run, so a chat enqueued right after it is genuinely last.
1386
+ *
1387
+ * Sending the chat as soon as the uploads finish is not enough, which is the
1388
+ * whole reason this exists: indexing a file is a CHAIN, and each pass is only
1389
+ * enqueued once the previous one lands (the client mints CONTINUE passes for
1390
+ * text/grid files, the worker mints them for PDFs and windowed reads). Every
1391
+ * one of those passes therefore queues up BEHIND a chat sent at upload time,
1392
+ * and the model answers from a file it has only partly read.
1393
+ *
1394
+ * The queue is read from the server's status index rather than from
1395
+ * bgTaskQueue: that mirror holds only what this client dispatched or adopted,
1396
+ * and it stops being maintained once the view unmounts. An empty answer has to
1397
+ * repeat before it is believed — see INDEXING_DRAIN_IDLE_LOOKS — and a look
1398
+ * that fails counts as busy, so a dropped request delays the turn instead of
1399
+ * releasing it early.
1400
+ *
1401
+ * Reads the identity PINNED at Send time, never a live one: the user may be in
1402
+ * another project by now, and this must keep asking about the one they sent
1403
+ * from.
1404
+ */
1405
+ awaitIndexingDrained(identity: ChatIdentity): Promise<'drained' | 'timedout' | 'skipped'>;
1406
+ /**
1407
+ * Abandon a staged turn — its uploads failed outright, so nothing will be
1408
+ * dispatched. The bubble stays (the user's text is not silently thrown away)
1409
+ * but settles into a plain, non-pending message; the caller reports the
1410
+ * failure separately.
1411
+ */
1412
+ settleStagedMessage(stageId: string): void;
1207
1413
  dispatchComposedMessage(composed: string, useBgQueue?: boolean, composedForLlm?: string, extractContent?: any, fileUrls?: any, pinned?: PinnedDispatchContext): void;
1208
1414
  promoteNextBgQueuedToRunning(): void;
1209
1415
  promoteNextQueuedToRunning(): void;
@@ -1327,7 +1533,7 @@ declare class ChatSession {
1327
1533
  url: string;
1328
1534
  storagePath: string;
1329
1535
  }>>;
1330
- uploadPendingAttachments(): Promise<Array<{
1536
+ uploadPendingAttachments(batchId?: string): Promise<Array<{
1331
1537
  name: string;
1332
1538
  url: string;
1333
1539
  storagePath?: string;
@@ -1336,4 +1542,4 @@ declare class ChatSession {
1336
1542
  bumpGate(): void;
1337
1543
  }
1338
1544
 
1339
- export { type AiAgentPlatform, type AttachmentFailureGroup, type AttachmentParser, type AttachmentSaveInfo, BG_INDEXING_QUEUE_SUFFIX, type BgTaskEntry, type BoundedChatOptions, type BuildDisplayListOptions, type BuildIndexingUserMessageOptions, CLAUDE_INPUT_CAP_RATIO, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, type CallClaudeWithMcpParams, type ChatEngineConfig, type ChatHost, type ChatIdentity, type ChatMessage, ChatSession, type ChatState, type ChatSystemPromptParams, type ClaudeMcpServerRequest, type ClaudeMcpToolConfig, type ClaudeMessage, type ClaudeRole, type ComposedUserMessage, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, type DisplayEntry, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, EXPIRED_LINK_REFRESH_EXPIRES_SECONDS, type ExtractDirective, type FillHistoryViewportOptions, HISTORY_BUDGET_RATIO, HISTORY_FILL_SLACK_PX, HISTORY_TOKEN_BUDGET, type IndexingAttachmentInfo, type IndexingFileRef, type IndexingGroup, type IndexingGroupStatus, type IndexingRequestRef, type IndexingSystemPromptParams, type InlineLinkContext, type InlineLinkPart, 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, type MapHistoryOptions, OUTPUT_TOKEN_RESERVE, type OpenAIMessage, POLL_INTERVAL, type ParsedAiAgent, type PinnedDispatchContext, RENDER_FROM_TOKEN, TOOL_AND_RESPONSE_BUFFER, buildAiAgentValue, buildBoundedChatMessages, buildChatDisplayList, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, classifyInlineLink, clearAttachmentParsers, composeUserMessage, configureChatEngine, createHistoryFiller, createInlineLinkRegex, encodePathSegments, estimateMessageTokens, estimateTextTokens, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, fillHistoryViewport, filterListByClearHorizon, findAttachmentParser, formatChatTimestamp, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, getProjectContextWindow, groupAttachmentFailures, isAuthExpiredError, isBgIndexingQueue, isErrorResponseBody, isHttpUrlLike, isIndexingRequestText, isNonRetryableRequestError, isOfficeFile, isServerExtractable, isServiceDbAttachmentHref, listClaudeModels, listOpenAIModels, makeExtractPlaceholder, mapHistoryListToMessages, normalizeAttachmentPathCandidate, normalizeTextContent, normalizeTrailingInlineToken, notifyAgentSaveAttachment, parseAiAgentValue, parseAttachmentContent, parseIndexingLabel, parseIndexingRequestText, readExpiredAttachmentHref, registerAttachmentParser, registerModelContextWindows, repairUrlEntities, repairUrlWhitespace, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, setProjectContextWindow, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay, wallClockNow };
1545
+ export { type AiAgentPlatform, type AttachmentFailureGroup, type AttachmentParser, type AttachmentSaveInfo, BG_INDEXING_QUEUE_SUFFIX, BOM, BOM_EXTS, type BgTaskEntry, type BoundedChatOptions, type BuildDisplayListOptions, type BuildIndexingUserMessageOptions, CLAUDE_INPUT_CAP_RATIO, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, type CallClaudeWithMcpParams, type ChatEngineConfig, type ChatHost, type ChatIdentity, type ChatMessage, ChatSession, type ChatState, type ChatSystemPromptParams, type ClaudeMcpServerRequest, type ClaudeMcpToolConfig, type ClaudeMessage, type ClaudeRole, type ComposedUserMessage, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, type DisplayEntry, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, EXPIRED_LINK_REFRESH_EXPIRES_SECONDS, EXT_CONTENT_TYPES, type EncodingClass, type ExtractDirective, type FillHistoryViewportOptions, HISTORY_BUDGET_RATIO, HISTORY_FILL_SLACK_PX, HISTORY_TOKEN_BUDGET, HTML_EXTS, HTML_HEAD_WINDOW, type IndexingAttachmentInfo, type IndexingFileRef, type IndexingGroup, type IndexingGroupStatus, type IndexingRequestRef, type IndexingSystemPromptParams, type InlineLinkContext, type InlineLinkPart, 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, type MapHistoryOptions, OUTPUT_TOKEN_RESERVE, type OpenAIMessage, POLL_INTERVAL, type ParsedAiAgent, type PinnedDispatchContext, 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 };
package/dist/engine.d.ts CHANGED
@@ -372,6 +372,102 @@ declare function buildBoundedChatMessages(options: BoundedChatOptions): {
372
372
  estimatedBudget: number;
373
373
  };
374
374
 
375
+ /**
376
+ * How a text file the chat offers as a download is encoded and labelled, so that
377
+ * whatever opens it reads the characters correctly, in any language.
378
+ *
379
+ * When the model answers with a fenced ```name.ext block, the client turns that
380
+ * block into a Blob and an <a download>. We always write UTF-8, but several very
381
+ * common consumers do not assume UTF-8 when nothing in the file says so, and fall
382
+ * back to the reader's local ANSI codepage: CP949 in Korea, CP932 in Japan,
383
+ * CP936/CP950 in China and Taiwan, CP1251 for Cyrillic. The file is valid and
384
+ * every non-ASCII character still opens as mojibake.
385
+ *
386
+ * There is no single fix, because the way a file declares "this is UTF-8" is a
387
+ * property of the FORMAT:
388
+ *
389
+ * - a spreadsheet (csv/tsv) and a Windows text editor read a BOM;
390
+ * - HTML is opened from disk with no HTTP headers, so only an in-document
391
+ * <meta charset> survives;
392
+ * - XML carries its encoding in its declaration, and a WRONG declaration makes
393
+ * a conforming parser fail outright;
394
+ * - RTF is 7-bit, so non-ASCII has to be escaped into \uNNNN?;
395
+ * - JSON, JSONL and YAML are UTF-8 by specification, and a BOM BREAKS them.
396
+ *
397
+ * Anything unrecognised is left byte-for-byte alone: an unknown extension is far
398
+ * more likely to be machine-parsed, where an uninvited BOM is a new bug, than to
399
+ * be opened by a legacy editor.
400
+ *
401
+ * MIRROR of skapi-mcp/download-encoding.js, which does the same job for files the
402
+ * server publishes (writeReport / exportRecordsToFile). A file the user gets from
403
+ * a fenced block and the same file published as a download must behave
404
+ * identically, so the two have to change together.
405
+ */
406
+ declare const BOM = "\uFEFF";
407
+ /** Files a spreadsheet or a Windows text editor opens directly. */
408
+ declare const BOM_EXTS: Set<string>;
409
+ /** Read from disk with no HTTP headers, so the declaration must be in the file. */
410
+ declare const HTML_EXTS: Set<string>;
411
+ declare const XML_EXTS: Set<string>;
412
+ declare const RTF_EXTS: Set<string>;
413
+ /** Content types by extension. Every text family carries an explicit charset. */
414
+ declare const EXT_CONTENT_TYPES: Record<string, string>;
415
+ type EncodingClass = 'bom' | 'html' | 'xml' | 'rtf' | 'none';
416
+ declare function normalizeExt(ext: string | null | undefined): string;
417
+ /** Extension of a filename, '' when it has none. */
418
+ declare function extOf(filename: string | null | undefined): string;
419
+ /** Which encoding declaration this format understands. */
420
+ declare function encodingClassForExt(ext: string | null | undefined): EncodingClass;
421
+ /** True when a file with this extension must be written BOM-first. */
422
+ declare function needsBomForExt(ext: string | null | undefined): boolean;
423
+ /**
424
+ * Content type to declare. Everything textual carries an explicit charset:
425
+ * without one the receiving end guesses, and it guesses the local codepage.
426
+ */
427
+ declare function contentTypeForExt(ext: string | null | undefined, fallback?: string): string;
428
+ declare function hasBom(text: string): boolean;
429
+ declare const HTML_HEAD_WINDOW = 4096;
430
+ /**
431
+ * Make an HTML document state its own encoding. Downloaded HTML is opened from
432
+ * disk, where the Content-Type we set no longer exists, so a document with no
433
+ * <meta charset> is decoded with the browser's locale default.
434
+ */
435
+ declare function ensureHtmlCharset(text: string): string;
436
+ /**
437
+ * Correct an XML declaration that names the wrong encoding.
438
+ *
439
+ * A MISSING declaration is left alone on purpose: XML with none is UTF-8 by
440
+ * specification, so every conforming parser already gets it right. A declaration
441
+ * naming EUC-KR over UTF-8 bytes, on the other hand, makes a parser fail outright.
442
+ */
443
+ declare function ensureXmlEncoding(text: string): string;
444
+ /** True when the body really is RTF rather than text merely named .rtf. */
445
+ declare function looksLikeRtf(text: string): boolean;
446
+ /**
447
+ * Escape every non-ASCII character into RTF's \uNNNN? form.
448
+ *
449
+ * RTF is 7-bit: a literal UTF-8 byte in the body is read through the codepage the
450
+ * header declares, which is how Korean, Japanese and Cyrillic RTF turns to
451
+ * mojibake in Word. \uNNNN? is codepage-independent.
452
+ *
453
+ * ASCII is never touched, which matters: backslashes and braces in an RTF body
454
+ * are control syntax, and "escaping" them would destroy the document. \u takes a
455
+ * SIGNED 16-bit value, so anything above 0x7FFF is emitted negative, and astral
456
+ * characters are emitted as their two surrogates.
457
+ */
458
+ declare function escapeRtfNonAscii(text: string): string;
459
+ /** Apply the format's encoding declaration to a whole document. */
460
+ declare function applyEncodingDeclaration(text: string, ext: string | null | undefined): string;
461
+ /**
462
+ * Everything a client needs to turn a fenced ```name.ext block into a download:
463
+ * the exact text to put in the Blob and the type to give it.
464
+ */
465
+ declare function prepareDownloadText(filename: string, body: string): {
466
+ ext: string;
467
+ text: string;
468
+ contentType: string;
469
+ };
470
+
375
471
  /**
376
472
  * Pure link/path helpers (no DOM, no marked). Moved verbatim from the chatbox.
377
473
  * `serviceId` is passed as a PARAMETER (the original read it from a global) so
@@ -721,7 +817,7 @@ type CallClaudeWithMcpParams = {
721
817
  onResponse?: (res: any) => void;
722
818
  onError?: (err: any) => void;
723
819
  };
724
- declare const POLL_INTERVAL = 1500;
820
+ declare const POLL_INTERVAL = 3000;
725
821
  declare function callClaudeWithMcp({ prompt, messages, service, owner, userId, model, maxTokens, system, mcpServer, extractContent, fileUrls, }: CallClaudeWithMcpParams): Promise<any>;
726
822
  declare function callClaudeWithPublicMcp(prompt: string, service: string, owner: string, messages?: ClaudeMessage[], system?: string, model?: string, userId?: string, extractContent?: ExtractDirective[], fileUrls?: FileUrlDirective[], onResponse?: (res: any) => void, onError?: (err: any) => void): Promise<any>;
727
823
  declare function callOpenAIWithPublicMcp(prompt: string, service: string, owner: string, messages?: OpenAIMessage[], system?: string, model?: string, userId?: string, extractContent?: ExtractDirective[], fileUrls?: FileUrlDirective[], onResponse?: (res: any) => void, onError?: (err: any) => void): Promise<any>;
@@ -730,7 +826,17 @@ type AttachmentSaveInfo = {
730
826
  model?: string;
731
827
  service: string;
732
828
  owner: string;
733
- userId?: string;
829
+ /**
830
+ * Queue base for this indexing pass: "<userId>-bg". REQUIRED, and it must be
831
+ * the SAME value the chat turn uses (ChatSession.dispatchComposedMessage's
832
+ * `id.userId || id.serviceId`) — the backend serialises requests that share a
833
+ * queue name and runs different ones IN PARALLEL, so a pass enqueued under a
834
+ * different base does not hold the chat back at all. It was optional once,
835
+ * defaulting to `service`; the chatbox omitted it, and its files were indexed
836
+ * on "<serviceId>-bg" while its question ran on "<userId>-bg" — the question
837
+ * was answered from a file nothing had read yet. Pass `userId || serviceId`.
838
+ */
839
+ userId: string;
734
840
  serviceName?: string;
735
841
  serviceDescription?: string;
736
842
  attachment: {
@@ -765,6 +871,14 @@ declare function extractOpenAIText(response: any): any;
765
871
  declare function listClaudeModels(service: string, owner: string): Promise<any>;
766
872
  declare function listOpenAIModels(service: string, owner: string): Promise<any>;
767
873
  declare const BG_INDEXING_QUEUE_SUFFIX = "-bg";
874
+ /**
875
+ * The one place the background-indexing queue name is spelled out. The backend
876
+ * serialises requests sharing a queue name and runs different names in PARALLEL,
877
+ * so every indexing pass AND the chat turn that must wait behind them have to
878
+ * resolve to the identical string — see AttachmentSaveInfo.userId for what
879
+ * happens when they do not.
880
+ */
881
+ declare function bgIndexingQueueName(userId?: string, service?: string): string;
768
882
  /**
769
883
  * True when a request belongs to the background-indexing queue.
770
884
  *
@@ -835,6 +949,11 @@ interface ChatIdentity {
835
949
  interface PinnedDispatchContext {
836
950
  identity: ChatIdentity;
837
951
  systemPrompt: string;
952
+ /** Id returned by stageOutgoingMessage. The turn's bubble is already on
953
+ * screen (staged while its attachments upload), so dispatchComposedMessage
954
+ * REPLACES that bubble in place instead of pushing a second one at the
955
+ * bottom — the message keeps the position it was sent in. */
956
+ stageId?: string;
838
957
  }
839
958
  /**
840
959
  * The file a background-indexing bubble belongs to. Stamped on the REQUEST
@@ -868,6 +987,17 @@ interface ChatMessage {
868
987
  /** Set on background-indexing REQUEST bubbles only (see IndexingFileRef). */
869
988
  _indexFile?: IndexingFileRef;
870
989
  _useBgQueue?: boolean;
990
+ /** Local id of a turn STAGED at Send time while its attachments upload. The
991
+ * bubble exists before any server request does, so it is never matched by
992
+ * _serverItemId and is never promoted/cancelled by the queue machinery —
993
+ * dispatchComposedMessage consumes it (pinned.stageId) when the turn is
994
+ * finally sent. Staged bubbles are deliberately kept OUT of the history
995
+ * cache: an unmount kills the upload that would resolve them, so a cached
996
+ * copy would replay as a bubble that uploads forever. */
997
+ _stageId?: string;
998
+ /** True on a staged bubble while its files are still uploading (renders
999
+ * "(Uploading files...)" instead of "(In queue)"). */
1000
+ isUploadingAttachments?: boolean;
871
1001
  _serverItemId?: string;
872
1002
  _localId?: string;
873
1003
  _cancelling?: boolean;
@@ -1142,7 +1272,21 @@ declare class ChatSession {
1142
1272
  private _pauseReasons;
1143
1273
  private _resuming;
1144
1274
  private _lidSeq;
1275
+ private _stageSeq;
1276
+ /** How many attachment-upload batches are running. uploadingAttachments is a
1277
+ * single flag but batches overlap (the composer stays live, so the user can
1278
+ * send a second one while the first uploads), and a nested finish must not
1279
+ * clear the flag out from under the batch still running. */
1280
+ private _uploadBatches;
1281
+ /** Indexing requests whose ack has not come back yet. Until it does the item
1282
+ * is not on the server's queue, so awaitIndexingDrained cannot see it — and
1283
+ * would read the gap between "pass N settled" and "pass N+1 accepted" as the
1284
+ * file being finished. */
1285
+ private _indexDispatchesInFlight;
1145
1286
  constructor(host: ChatHost);
1287
+ /** Wrap an indexing-request dispatch so awaitIndexingDrained counts it as
1288
+ * live work from the moment it is sent, not from the moment it is acked. */
1289
+ trackIndexDispatch<T>(p: Promise<T>): Promise<T>;
1146
1290
  /**
1147
1291
  * Register a live poll so (a) a remount dedupes against it instead of stacking a
1148
1292
  * SECOND poll on the same item, and (b) pausePolling can stop it.
@@ -1204,6 +1348,68 @@ declare class ChatSession {
1204
1348
  */
1205
1349
  private _callProviderFor;
1206
1350
  dispatchAgentRequest(params: any): Promise<any>;
1351
+ /**
1352
+ * Put a turn on screen the INSTANT the user hits Send, before its attachments
1353
+ * have finished uploading. Uploads run in the background now (the composer is
1354
+ * cleared and stays usable), so without a staged bubble the message would
1355
+ * appear only once its files were up — below anything the user sent in the
1356
+ * meantime, in an order that never matches what they typed.
1357
+ *
1358
+ * Staged bubbles carry _useBgQueue because that is where a turn with
1359
+ * attachments ultimately dispatches (behind its own indexing tasks). That flag
1360
+ * is also what keeps promoteNextQueuedToRunning / resolveQueuedUserBubble off
1361
+ * them: those advance the SERVER queue, and a staged turn has no server
1362
+ * request behind it yet.
1363
+ *
1364
+ * Returns the id to hand back as PinnedDispatchContext.stageId at dispatch.
1365
+ */
1366
+ stageOutgoingMessage(displayText: string): string;
1367
+ private _stageIndex;
1368
+ /**
1369
+ * Where a staged turn belongs once it is finally sent: BELOW every indexing
1370
+ * row, because that is the order it ran in and the order the server history
1371
+ * will report on the next load (its request id is newer than every pass it
1372
+ * waited for). While its files were uploading it sat above them — the rows
1373
+ * are injected as each file's pass starts, after the bubble was staged.
1374
+ *
1375
+ * Returns the index to insert at, or -1 to leave the turn where it is. Never
1376
+ * moves a turn UP: a bubble that already sits below the indexing rows (or a
1377
+ * chat with no indexing at all) must not jump backwards over anything.
1378
+ */
1379
+ private _settledStagePosition;
1380
+ /** The staged turn's files are up; it is now just waiting its place in the
1381
+ * queue. Drops the "(Uploading files...)" note back to "(In queue)". */
1382
+ markStagedMessageQueued(stageId: string): void;
1383
+ /**
1384
+ * Resolves once this project's background-indexing queue has nothing left to
1385
+ * run, so a chat enqueued right after it is genuinely last.
1386
+ *
1387
+ * Sending the chat as soon as the uploads finish is not enough, which is the
1388
+ * whole reason this exists: indexing a file is a CHAIN, and each pass is only
1389
+ * enqueued once the previous one lands (the client mints CONTINUE passes for
1390
+ * text/grid files, the worker mints them for PDFs and windowed reads). Every
1391
+ * one of those passes therefore queues up BEHIND a chat sent at upload time,
1392
+ * and the model answers from a file it has only partly read.
1393
+ *
1394
+ * The queue is read from the server's status index rather than from
1395
+ * bgTaskQueue: that mirror holds only what this client dispatched or adopted,
1396
+ * and it stops being maintained once the view unmounts. An empty answer has to
1397
+ * repeat before it is believed — see INDEXING_DRAIN_IDLE_LOOKS — and a look
1398
+ * that fails counts as busy, so a dropped request delays the turn instead of
1399
+ * releasing it early.
1400
+ *
1401
+ * Reads the identity PINNED at Send time, never a live one: the user may be in
1402
+ * another project by now, and this must keep asking about the one they sent
1403
+ * from.
1404
+ */
1405
+ awaitIndexingDrained(identity: ChatIdentity): Promise<'drained' | 'timedout' | 'skipped'>;
1406
+ /**
1407
+ * Abandon a staged turn — its uploads failed outright, so nothing will be
1408
+ * dispatched. The bubble stays (the user's text is not silently thrown away)
1409
+ * but settles into a plain, non-pending message; the caller reports the
1410
+ * failure separately.
1411
+ */
1412
+ settleStagedMessage(stageId: string): void;
1207
1413
  dispatchComposedMessage(composed: string, useBgQueue?: boolean, composedForLlm?: string, extractContent?: any, fileUrls?: any, pinned?: PinnedDispatchContext): void;
1208
1414
  promoteNextBgQueuedToRunning(): void;
1209
1415
  promoteNextQueuedToRunning(): void;
@@ -1327,7 +1533,7 @@ declare class ChatSession {
1327
1533
  url: string;
1328
1534
  storagePath: string;
1329
1535
  }>>;
1330
- uploadPendingAttachments(): Promise<Array<{
1536
+ uploadPendingAttachments(batchId?: string): Promise<Array<{
1331
1537
  name: string;
1332
1538
  url: string;
1333
1539
  storagePath?: string;
@@ -1336,4 +1542,4 @@ declare class ChatSession {
1336
1542
  bumpGate(): void;
1337
1543
  }
1338
1544
 
1339
- export { type AiAgentPlatform, type AttachmentFailureGroup, type AttachmentParser, type AttachmentSaveInfo, BG_INDEXING_QUEUE_SUFFIX, type BgTaskEntry, type BoundedChatOptions, type BuildDisplayListOptions, type BuildIndexingUserMessageOptions, CLAUDE_INPUT_CAP_RATIO, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, type CallClaudeWithMcpParams, type ChatEngineConfig, type ChatHost, type ChatIdentity, type ChatMessage, ChatSession, type ChatState, type ChatSystemPromptParams, type ClaudeMcpServerRequest, type ClaudeMcpToolConfig, type ClaudeMessage, type ClaudeRole, type ComposedUserMessage, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, type DisplayEntry, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, EXPIRED_LINK_REFRESH_EXPIRES_SECONDS, type ExtractDirective, type FillHistoryViewportOptions, HISTORY_BUDGET_RATIO, HISTORY_FILL_SLACK_PX, HISTORY_TOKEN_BUDGET, type IndexingAttachmentInfo, type IndexingFileRef, type IndexingGroup, type IndexingGroupStatus, type IndexingRequestRef, type IndexingSystemPromptParams, type InlineLinkContext, type InlineLinkPart, 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, type MapHistoryOptions, OUTPUT_TOKEN_RESERVE, type OpenAIMessage, POLL_INTERVAL, type ParsedAiAgent, type PinnedDispatchContext, RENDER_FROM_TOKEN, TOOL_AND_RESPONSE_BUFFER, buildAiAgentValue, buildBoundedChatMessages, buildChatDisplayList, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, classifyInlineLink, clearAttachmentParsers, composeUserMessage, configureChatEngine, createHistoryFiller, createInlineLinkRegex, encodePathSegments, estimateMessageTokens, estimateTextTokens, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, fillHistoryViewport, filterListByClearHorizon, findAttachmentParser, formatChatTimestamp, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, getProjectContextWindow, groupAttachmentFailures, isAuthExpiredError, isBgIndexingQueue, isErrorResponseBody, isHttpUrlLike, isIndexingRequestText, isNonRetryableRequestError, isOfficeFile, isServerExtractable, isServiceDbAttachmentHref, listClaudeModels, listOpenAIModels, makeExtractPlaceholder, mapHistoryListToMessages, normalizeAttachmentPathCandidate, normalizeTextContent, normalizeTrailingInlineToken, notifyAgentSaveAttachment, parseAiAgentValue, parseAttachmentContent, parseIndexingLabel, parseIndexingRequestText, readExpiredAttachmentHref, registerAttachmentParser, registerModelContextWindows, repairUrlEntities, repairUrlWhitespace, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, setProjectContextWindow, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay, wallClockNow };
1545
+ export { type AiAgentPlatform, type AttachmentFailureGroup, type AttachmentParser, type AttachmentSaveInfo, BG_INDEXING_QUEUE_SUFFIX, BOM, BOM_EXTS, type BgTaskEntry, type BoundedChatOptions, type BuildDisplayListOptions, type BuildIndexingUserMessageOptions, CLAUDE_INPUT_CAP_RATIO, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, type CallClaudeWithMcpParams, type ChatEngineConfig, type ChatHost, type ChatIdentity, type ChatMessage, ChatSession, type ChatState, type ChatSystemPromptParams, type ClaudeMcpServerRequest, type ClaudeMcpToolConfig, type ClaudeMessage, type ClaudeRole, type ComposedUserMessage, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, type DisplayEntry, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, EXPIRED_LINK_REFRESH_EXPIRES_SECONDS, EXT_CONTENT_TYPES, type EncodingClass, type ExtractDirective, type FillHistoryViewportOptions, HISTORY_BUDGET_RATIO, HISTORY_FILL_SLACK_PX, HISTORY_TOKEN_BUDGET, HTML_EXTS, HTML_HEAD_WINDOW, type IndexingAttachmentInfo, type IndexingFileRef, type IndexingGroup, type IndexingGroupStatus, type IndexingRequestRef, type IndexingSystemPromptParams, type InlineLinkContext, type InlineLinkPart, 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, type MapHistoryOptions, OUTPUT_TOKEN_RESERVE, type OpenAIMessage, POLL_INTERVAL, type ParsedAiAgent, type PinnedDispatchContext, 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 };