bunnyquery 1.14.0 → 1.15.1

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.ts CHANGED
@@ -291,18 +291,29 @@ interface ChatEngineConfig {
291
291
  * OpenAI's `web_search` are attached to a chat request only when this
292
292
  * resolves to true.
293
293
  *
294
- * OPTIONAL, AND UNSET IS NOT FALSE. When the host leaves it out, the engine
295
- * reads the project's own setting (`web_access` in the `bq::settings`
296
- * record, see project_settings.ts) and falls back to OFF, so the project
297
- * owner's switch on the Project Settings page decides for every client that
298
- * does not override it. A host that sets a boolean here overrides the
299
- * project for its own page, both ways. Resolution: this option, then the
300
- * project setting, then false.
294
+ * OPTIONAL. The project's own setting (`web_access` in the `bq::settings`
295
+ * record, see project_settings.ts; off unless the owner turned it on) is the
296
+ * CEILING, and this option may only restrict below it:
297
+ * effective = projectSetting && webAccess !== false
298
+ * So `false` here turns the web off for this page whatever the project says,
299
+ * and `true` or unset follows the project. A value above the project setting
300
+ * has no effect: the owner's switch is what lets an answer out onto the web.
301
301
  *
302
302
  * Chat turns only. An indexing pass keeps the tools it has today, since the
303
303
  * worker hands it rendered pages and extracted text and not a URL to fetch.
304
304
  */
305
305
  webAccess?: boolean;
306
+ /**
307
+ * Whether a chat attachment that the signed-in user already uploaded to this
308
+ * project (same file name, same size, same uploader) is SKIPPED: not uploaded
309
+ * again, not indexed again, the turn using the stored copy's url. Same
310
+ * ceiling rule as `webAccess`, against the project's `skip_duplicate_files`
311
+ * setting, which is ON unless the owner turned it off:
312
+ * effective = projectSetting && skipDuplicateFiles !== false
313
+ * `false` here lets this page re-upload duplicates on a project that skips
314
+ * them; `true` or unset follows the project. See duplicates.ts for the check.
315
+ */
316
+ skipDuplicateFiles?: boolean;
306
317
  /**
307
318
  * skapi.forwardRequestFinalize, bound to the consumer's skapi instance.
308
319
  * Stores the version of a streamed turn that history should keep (the engine
@@ -399,20 +410,36 @@ interface ChatEngineConfig {
399
410
  declare function configureChatEngine(config: ChatEngineConfig): void;
400
411
  declare function chatEngineConfig(): ChatEngineConfig;
401
412
  /**
402
- * The web switch for a chat turn, SYNCHRONOUSLY: the host's `webAccess` option
403
- * when it is a boolean, else the project's cached `web_access` setting, else
404
- * false. For a template or a prompt builder, which have nowhere to await. Reads
405
- * the cache as it stands, so before the settings fetch settles it answers the
406
- * default; request builders use resolveWebAccess below instead.
413
+ * The web switch for a chat turn, SYNCHRONOUSLY: the project's cached
414
+ * `web_access` setting (off unless the owner turned it on), restricted by the
415
+ * host's `webAccess` option when that is `false`. For a template or a prompt
416
+ * builder, which have nowhere to await. Reads the cache as it stands, so before
417
+ * the settings fetch settles it answers the default; request builders use
418
+ * resolveWebAccess below instead.
407
419
  */
408
420
  declare function webAccessEnabled(service?: string): boolean;
409
421
  /**
410
- * The same switch, awaiting the project's settings record when the host did not
411
- * decide. Never rejects: a failed or impossible read (no reader injected, a
412
- * signed-out visitor on a require_login project) settles as "unset", which is
413
- * off. Cheap after the first call per project: the store dedupes and caches.
422
+ * The same switch, awaiting the project's settings record. Never rejects: a
423
+ * failed or impossible read (no reader injected, a signed-out visitor on a
424
+ * require_login project) settles as "unset", which is off. Cheap after the first
425
+ * call per project: the store dedupes and caches. `override` is a per-call host
426
+ * value that may only restrict (see underCeiling).
427
+ */
428
+ declare function resolveWebAccess(service?: string, override?: boolean): Promise<boolean>;
429
+ /**
430
+ * The skip-duplicates switch for an upload, SYNCHRONOUSLY: the project's cached
431
+ * `skip_duplicate_files` setting (on unless the owner turned it off), restricted
432
+ * by the host's `skipDuplicateFiles` option when that is `false`. Answers the
433
+ * default before the settings fetch settles; the upload path uses
434
+ * resolveSkipDuplicateFiles below.
414
435
  */
415
- declare function resolveWebAccess(service?: string): Promise<boolean>;
436
+ declare function skipDuplicateFilesEnabled(service?: string): boolean;
437
+ /**
438
+ * The same switch, awaiting the project's settings record. Never rejects: a
439
+ * failed read settles as "unset", which for this switch is ON (the default), so
440
+ * a project that cannot be read still skips duplicates unless the host said no.
441
+ */
442
+ declare function resolveSkipDuplicateFiles(service?: string): Promise<boolean>;
416
443
  /**
417
444
  * True when a streamed turn whose answer never reached its row can be READ BACK.
418
445
  *
@@ -509,6 +536,65 @@ declare function streamRecoveryEnabled(): boolean;
509
536
  */
510
537
  declare function skapiSupportsStreaming(sk: any): boolean;
511
538
 
539
+ /**
540
+ * Duplicate-upload detection for chat attachments (reliability plan, owner's
541
+ * yes 2026-10-12): "has the signed-in user already uploaded THIS file to this
542
+ * project?", answered from the file records the upload path already writes.
543
+ *
544
+ * WHAT COUNTS AS THE SAME FILE. Same name, same size in bytes, same uploader.
545
+ * Every upload creates a `src::<storagePath>` record (table `file_summaries`)
546
+ * whose `data.size_bytes` is the byte count and whose `user_id` is the
547
+ * uploader, so a lookup by name narrowed by those two fields finds a file
548
+ * without reading its bytes. A different user's identical file is NOT a
549
+ * duplicate: that user's copy sits at a different access group and path and
550
+ * may be invisible to this one.
551
+ *
552
+ * HOW THE NAME IS FOUND. `getUniqueId` with `condition: '<='` is an ENDS-WITH
553
+ * match on the record id (served from the reversed proxy rows), and a file
554
+ * record's id ends with the file's name: `src::<name>` for a file at the
555
+ * root, `src::<folders>/<name>` otherwise. The match is then narrowed to
556
+ * exactly those two shapes, so `src::report.pdf` is not found by "port.pdf",
557
+ * and to the `src::` prefix, because the `run::` and `done::` index markers
558
+ * end with the same path and are not files.
559
+ *
560
+ * NEVER THROWS. A failed or refused lookup (a signed-out visitor on a
561
+ * require_login project, a region without the proxy rows yet, a network
562
+ * error) answers null, and null means "upload as usual": the check exists to
563
+ * save an indexing run, and must never be able to cost one.
564
+ *
565
+ * TRANSPORT-FREE like the rest of the engine: the caller hands in the skapi
566
+ * instance it owns. The widget passes its own; a host with no instance simply
567
+ * does not wire the hook and uploads as before.
568
+ */
569
+ /** The record found, with the storage path the stored file lives at. */
570
+ type DuplicateUploadMatch = {
571
+ /** The stored file's db storage path (the record id without `src::`). */
572
+ storagePath: string;
573
+ /** The matching `src::` record id. */
574
+ uniqueId: string;
575
+ /** The file record as read. */
576
+ record: any;
577
+ };
578
+ type DuplicateUploadFile = {
579
+ /** The file name as it is STORED: the last segment of the storage path. */
580
+ name: string;
581
+ /** Size in bytes. */
582
+ size: number;
583
+ };
584
+ /** The `src::` records to read before giving up; a name is rarely shared this widely. */
585
+ declare const DUPLICATE_LOOKUP_MAX_CANDIDATES = 25;
586
+ /** Narrow an ends-with listing to real file records for exactly this name. */
587
+ declare function isDuplicateCandidateId(uniqueId: unknown, name: string): boolean;
588
+ /**
589
+ * The first `src::` record for `file.name` whose `data.size_bytes` equals
590
+ * `file.size` and whose `user_id` is `userId`, or null. `opts.service` is passed
591
+ * through to both skapi calls when given (the widget names the project on every
592
+ * call); otherwise the instance's own project is queried.
593
+ */
594
+ declare function findDuplicateUpload(skapi: any, file: DuplicateUploadFile, userId: string, opts?: {
595
+ service?: string;
596
+ }): Promise<DuplicateUploadMatch | null>;
597
+
512
598
  /**
513
599
  * Office-file server-side extraction helpers.
514
600
  *
@@ -815,6 +901,20 @@ declare function buildIndexingRenderMessage(attachment: IndexingAttachmentInfo,
815
901
  * of the window it is rendering; passing an explicit label produces a ready-to-send message.
816
902
  */
817
903
  declare function buildIndexingRenderContinueTemplate(attachment: IndexingAttachmentInfo, placeholder: string, pageLabel?: string): string;
904
+ /**
905
+ * Verbatim page text for SCANNED pages (reliability plan 3.1, owner's yes 2026-10-12).
906
+ *
907
+ * The worker writes a `__PAGES__` record per page. A page WITH a text layer is written
908
+ * server-side before the model call; a page WITHOUT one (a scan) can only come from the
909
+ * model, and the worker's `_fill_vision_pages` fills it from a posted record that names
910
+ * the page (`page`) and carries `page_text`. The signal the model has is the label the
911
+ * worker puts before each image: "Embedded text layer for this page" appears under the
912
+ * label exactly when the page has one (the layer ships the text with the first strip of
913
+ * a page), so "no such block" is "no text layer", and that is the whole rule. Fixed table
914
+ * name, as the system prompt demands for every content kind. Kept short: the owner pays
915
+ * output tokens for the transcription itself, not for the instruction.
916
+ */
917
+ declare const PAGE_TEXT_TABLE = "page_texts";
818
918
  /**
819
919
  * User message for a WINDOWED file: the worker splices ONE window of the file's rows or
820
920
  * text into this message at `placeholder`, then continues from the reader's own cursor
@@ -2183,8 +2283,8 @@ interface SseParser {
2183
2283
  declare function createSseParser(): SseParser;
2184
2284
 
2185
2285
  declare const MCP_NAME = "BunnyQuery";
2186
- declare const DEFAULT_CLAUDE_MODEL = "claude-sonnet-5";
2187
- declare const DEFAULT_OPENAI_MODEL = "gpt-5.6-luna";
2286
+ declare const DEFAULT_CLAUDE_MODEL = "claude-sonnet-5-5";
2287
+ declare const DEFAULT_OPENAI_MODEL = "gpt-6.1-sol";
2188
2288
  /**
2189
2289
  * THE two `stream` flags of a streamed chat turn, produced together or not at all.
2190
2290
  *
@@ -2289,9 +2389,11 @@ type CallClaudeWithMcpParams = {
2289
2389
  onResponse?: (res: any) => void;
2290
2390
  onError?: (err: any) => void;
2291
2391
  /**
2292
- * Attach Claude's `web_fetch` to this turn. Omitted means "ask the engine":
2293
- * the host's `webAccess` config option, then the project's `web_access`
2294
- * setting, then off. See config.ts resolveWebAccess.
2392
+ * Attach Claude's `web_fetch` to this turn. A per-call host value under the
2393
+ * same ceiling rule as the config option: the project's `web_access` setting
2394
+ * is the most a turn can have, and `false` here (or in the config) takes it
2395
+ * off. Omitted follows the config option and the project. See config.ts
2396
+ * resolveWebAccess.
2295
2397
  */
2296
2398
  webAccess?: boolean;
2297
2399
  };
@@ -2877,6 +2979,25 @@ interface ChatHost {
2877
2979
  * setting existed.
2878
2980
  */
2879
2981
  uploadAccessGroup?(storagePath: string): 'public' | 'authorized' | 'private' | undefined | Promise<'public' | 'authorized' | 'private' | undefined>;
2982
+ /**
2983
+ * "Has the signed-in user already uploaded this file to this project?" Asked
2984
+ * BEFORE a member's bytes go up, only while the skip-duplicates switch resolves
2985
+ * on (config.ts resolveSkipDuplicateFiles: the project's `skip_duplicate_files`
2986
+ * setting, on by default, restricted by the host's `skipDuplicateFiles: false`).
2987
+ * `file.name` is the name as STORED (the last segment of the storage path the
2988
+ * host itself produced), `file.size` the byte count. A match means the member
2989
+ * is neither uploaded nor indexed: the turn takes the stored copy's url and
2990
+ * the chip says so. Null, a rejection or a missing hook all mean "upload as
2991
+ * usual"; the engine's findDuplicateUpload is the ready-made implementation.
2992
+ */
2993
+ findDuplicateUpload?(file: {
2994
+ name: string;
2995
+ size: number;
2996
+ }): Promise<{
2997
+ storagePath: string;
2998
+ } | null> | {
2999
+ storagePath: string;
3000
+ } | null;
2880
3001
  /** Map a relative path to the consumer's db storage key (e.g. uid-prefixed). */
2881
3002
  storagePathFor(relPath: string): string;
2882
3003
  getMimeType(name: string): string | null;
@@ -5022,6 +5143,7 @@ type ProjectSettingsData = {
5022
5143
  upload_access_group?: unknown;
5023
5144
  chat_greeting?: unknown;
5024
5145
  web_access?: unknown;
5146
+ skip_duplicate_files?: unknown;
5025
5147
  [key: string]: unknown;
5026
5148
  };
5027
5149
  /**
@@ -5034,18 +5156,42 @@ type ProjectSettingsData = {
5034
5156
  * database alone. Only the literal `true` turns it on: this record is public and
5035
5157
  * a client must never read a value it did not write as permission.
5036
5158
  *
5037
- * Resolution order, applied by the engine where the chat request is built:
5038
- * 1. the host's `webAccess` init/config option, when it is a boolean;
5039
- * 2. this setting, when the project has set it;
5040
- * 3. off.
5041
- * Indexing requests are not governed by this: an indexing pass keeps the tools
5042
- * it has today (see requests.ts notifyAgentSaveAttachment).
5159
+ * Resolution, applied by the engine where the chat request is built (config.ts
5160
+ * resolveWebAccess): THIS SETTING IS THE CEILING and the host may only restrict.
5161
+ * effective = projectSetting && hostOption !== false
5162
+ * A host's `webAccess: false` turns a project that is on off for its own page;
5163
+ * a host's `true` on a project that is off has no effect, because the owner's
5164
+ * switch is the one that lets an answer out onto the web. Indexing requests are
5165
+ * not governed by this: an indexing pass keeps the tools it has today (see
5166
+ * requests.ts notifyAgentSaveAttachment).
5043
5167
  */
5044
5168
  declare const DEFAULT_WEB_ACCESS = false;
5045
5169
  /** Narrow a stored value to the switch: only `true` is on. */
5046
5170
  declare function normalizeWebAccess(value: any): boolean;
5047
5171
  /** The switch held in a settings-record `data`, false when unset. */
5048
5172
  declare function webAccessFrom(data: ProjectSettingsData | null | undefined): boolean;
5173
+ /**
5174
+ * Whether an upload that is byte-for-byte the SAME FILE the signed-in user
5175
+ * already has in this project (same name, same size, same uploader) is skipped:
5176
+ * not uploaded again and not indexed again, with the chat turn using the stored
5177
+ * copy. Same record, its own `skip_duplicate_files` key.
5178
+ *
5179
+ * ON BY DEFAULT: a second copy of a file costs a second indexing run, which is
5180
+ * the single most expensive thing a project does, and answers from two copies
5181
+ * of one document double every row. Only the literal `false` turns it off: this
5182
+ * record is public and a client must never read a value it did not write.
5183
+ *
5184
+ * Resolution is the same ceiling rule as `web_access` (config.ts
5185
+ * resolveSkipDuplicateFiles): effective = projectSetting && hostOption !== false.
5186
+ * The host's `skipDuplicateFiles: false` lets its page re-upload duplicates on
5187
+ * a project that skips them; a host's `true` on a project that turned it off has
5188
+ * no effect.
5189
+ */
5190
+ declare const DEFAULT_SKIP_DUPLICATE_FILES = true;
5191
+ /** Narrow a stored value to the switch: only the literal `false` is off. */
5192
+ declare function normalizeSkipDuplicateFiles(value: any): boolean;
5193
+ /** The switch held in a settings-record `data`, true when unset. */
5194
+ declare function skipDuplicateFilesFrom(data: ProjectSettingsData | null | undefined): boolean;
5049
5195
  /**
5050
5196
  * Longest custom opening line the dashboard will store.
5051
5197
  *
@@ -5148,6 +5294,12 @@ declare function projectChatGreeting(service: string): string;
5148
5294
  * reads the stored value rather than the cold-cache default.
5149
5295
  */
5150
5296
  declare function projectWebAccess(service: string): boolean;
5297
+ /**
5298
+ * Sync convenience: the project's skip-duplicates setting, true until the fetch
5299
+ * settles or when the project never set it. The upload path prefers the
5300
+ * awaiting form (config.ts resolveSkipDuplicateFiles).
5301
+ */
5302
+ declare function projectSkipDuplicateFiles(service: string): boolean;
5151
5303
  /**
5152
5304
  * Adopt a value the caller just WROTE, so the settings page reflects its own
5153
5305
  * save without a re-fetch.
@@ -5168,4 +5320,4 @@ declare function patchProjectSettings(service: string, patch: ProjectSettingsDat
5168
5320
  */
5169
5321
  declare function clearProjectSettings(service?: string): void;
5170
5322
 
5171
- export { type AiAgentPlatform, type AnchorBoxEl, type AnchorRowEl, type AttachmentFailureGroup, type AttachmentParser, type AttachmentSaveInfo, BG_INDEXING_QUEUE_SUFFIX, BOM, BOM_EXTS, type BgTaskEntry, type BoundedChatOptions, type BuildDisplayListOptions, type BuildIndexingUserMessageOptions, CHAT_GREETING_MAX_LENGTH, CLAUDE_INPUT_CAP_RATIO, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, type CallClaudeWithMcpParams, type ChatEngineConfig, type ChatGreetingParams, type ChatGreetingParts, type ChatHost, type ChatIdentity, type ChatMessage, ChatSession, type ChatState, type ChatStreamWiring, type ChatSystemPromptParams, type Citation, type CitationLinkPart, type ClaudeMcpServerRequest, type ClaudeMcpToolConfig, type ClaudeMessage, type ClaudeRole, type ComposedUserMessage, DATA_TOOL_NAMES, DEFAULT_CLAUDE_MODEL, DEFAULT_CONTEXT_WINDOW, DEFAULT_OPENAI_MODEL, DEFAULT_UPLOAD_ACCESS_GROUP, DEFAULT_WEB_ACCESS, type DisplayEntry, EMPTY_INDEXING_REPLY, 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, IMAGE_PREVIEWS_PER_MESSAGE, INDEXING_COMPLETE_MARKER, INDEXING_MAX_OUTPUT_TOKENS, INLINE_LINK_GLYPH, INLINE_LINK_UNAVAILABLE_GLYPH, INLINE_LINK_UNAVAILABLE_SUFFIX, INPUT_CAP_RATIO, type ImagePreviewContext, type IndexRunPatch, type IndexRunStatus, type IndexingAttachmentInfo, type IndexingFileRef, type IndexingGroup, type IndexingGroupStatus, type IndexingRequestRef, type IndexingSystemPromptParams, type InlineLinkContext, type InlineLinkMarkupOptions, type InlineLinkPart, LINK_LABEL_MAX_DISPLAY_CHARS, LINK_REFRESH_WINDOW_MS, type LiveStreamUpdate, MAX_CONCURRENT_BG_POLLS, MAX_HISTORY_FILL_PAGES, MAX_HISTORY_MESSAGES, MAX_OUTPUT_BY_MODEL, MAX_OUTPUT_TOKENS, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MINT_CACHE_GENERATION, MIN_INPUT_TOKEN_BUDGET, MIN_PER_REQUEST_INPUT_CAP, type MapHistoryOptions, NO_DATA_TOOL_MARK, OUTPUT_TOKEN_RESERVE, type OpenAIMessage, POLL_INTERVAL, PRESIGN_SAFETY_MARGIN_MS, PREVIEWABLE_IMAGE_CONTENT_TYPES, PREVIEW_BROWSER_CACHE_SECONDS, PREVIEW_LAYOUT_BOX_SELECTOR, PREVIEW_URL_EXPIRES_SECONDS, PROJECT_SETTINGS_ACCESS_GROUP, PROJECT_SETTINGS_TABLE, PROJECT_SETTINGS_UNIQUE_ID, type ParsedAiAgent, type PinnedDispatchContext, type PreviewImageEl, type ProjectAccessSetting, type ProjectSettingsData, type ProjectSettingsReader, RENDER_FROM_TOKEN, RETRIEVAL_NOTES_MAX, RETRIEVAL_NOTES_TOKEN_BUDGET, RETRIEVAL_NOTE_MAX_CHARS, RETRIEVAL_NOTE_MAX_ITEMS, RETRIEVAL_NOTE_PREFIX, RTF_EXTS, RUN_RECORD_WORKING_STALE_MS, type RenderableInlineLink, type RescueDecisionContext, type RetrievalHit, type RetrievalNote, type RowAnchor, type RunStubInfo, STREAM_POLL_INTERVAL, type ScrollAnchor, type ScrollAnchorOptions, type SseChunk, type SseParser, type SseProvider, type SseSnapshot, type SseToolCall, type StreamDispatchContext, TOOL_AND_RESPONSE_BUFFER, UPLOAD_ACCESS_GROUPS, UPLOAD_ACCESS_HINTS, UPLOAD_ACCESS_LABELS, UPLOAD_ACCESS_OPTIONS, type UploadAccessGroup, type VisionProfile, XML_EXTS, __resetSplitHistoryState, accessSettingFrom, adoptLocalAnswerIntoPage, answerUsedNoDataTool, applyEncodingDeclaration, applyRetrievalNotes, asksUploadAccessFrom, bgIndexingQueueName, buildAiAgentValue, buildBoundedChatMessages, buildChatDisplayList, buildChatGreeting, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildHistoryItemFullId, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, cachedProjectSettings, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, canonicalizePathForm, chatCacheKey, chatEngineConfig, chatGreetingFrom, chatStreamWiring, citationFileName, citationLinkPart, citationsFor, classifyInlineLink, clearAttachmentParsers, clearImagePreviewCache, clearProjectSettings, composeUserMessage, configureChatEngine, configureProjectSettings, contentTypeForExt, createHistoryFiller, createInlineLinkRegex, createScrollAnchor, createSseParser, csrEnvelopeError, encodePathSegments, encodingClassForExt, ensureHtmlCharset, ensureXmlEncoding, escapeInlineHtml, escapeRtfNonAscii, estimateMessageTokens, estimateTextTokens, extOf, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, extractRetrievalNote, fetchLiveIndexingKeys, fillHistoryViewport, filterListByClearHorizon, findAttachmentParser, formatChatTimestamp, formatDuration, formatRetrievalNote, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, getInputTokenBudget, getMaxOutputTokens, getModelContextWindow, getProjectContextWindow, getSplitChatHistory, getVisionProfile, groupAttachmentFailures, hasBom, hydrateImagePreviews, indexDoneUniqueId, indexScopeKey, indexingAccessGroup, isAuthExpiredError, isBgIndexingQueue, isCsrStatusEnvelope, isDataToolName, isErrorResponseBody, isHttpUrlLike, isImageVisionFile, isIndexingRequestText, isLinkUnavailable, isNonRetryableRequestError, isOfficeFile, isPagedReadFile, isPreviewableImagePath, isProviderApiKeyError, isRetriedFailure, isRetrievalNote, isServerExtractable, isServiceDbAttachmentHref, isWindowedReadFile, linkUnavailableKeyForHref, linkUnavailableKeyForPath, linkUnavailableKeysForPath, listClaudeModels, listOpenAIModels, liveSafePrefix, loadProjectSettings, looksLikeRtf, makeExtractPlaceholder, mapHistoryListToMessages, markImagePreviewStale, mayKeepStreamedAnswer, mintCacheBustStamp, needsBomForExt, normalizeAttachmentPathCandidate, normalizeChatGreeting, normalizeExt, normalizeProjectAccessSetting, normalizeTextContent, normalizeTrailingInlineToken, normalizeUploadAccessGroup, normalizeWebAccess, notifyAgentSaveAttachment, parseAiAgentValue, parseAttachmentContent, parseIndexingLabel, parseIndexingRequestText, patchProjectSettings, peekImagePreviewUrl, prepareDownloadText, presignExpiryEpochMs, previewImageContentType, previewLayoutBox, previewMintCacheToken, previewableExtOf, primeProjectSettings, projectAccessSetting, projectAsksUploadAccess, projectChatGreeting, projectSettingsSettled, projectUploadAccessGroup, projectWebAccess, providerBodyWasRead, readExpiredAttachmentHref, readyProjectSettings, registerAttachmentParser, registerModelContextWindows, renderInlineLinkHtml, repairUrlEntities, repairUrlWhitespace, resolveImagePreviewUrl, resolveWebAccess, retrievalNoteFromText, retrievalNoteOfAnswer, runIndexUniqueId, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, setProjectContextWindow, setProjectSettings, shouldRescueInFlightMessage, skapiSupportsStreaming, streamRecoveryEnabled, streamRecoveryLabels, streamRecoveryPhase, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay, typewriterResumeIndex, uploadAccessGroupFrom, upsertIndexRunRecordSafe, wallClockNow, webAccessEnabled, webAccessFrom };
5323
+ export { type AiAgentPlatform, type AnchorBoxEl, type AnchorRowEl, type AttachmentFailureGroup, type AttachmentParser, type AttachmentSaveInfo, BG_INDEXING_QUEUE_SUFFIX, BOM, BOM_EXTS, type BgTaskEntry, type BoundedChatOptions, type BuildDisplayListOptions, type BuildIndexingUserMessageOptions, CHAT_GREETING_MAX_LENGTH, CLAUDE_INPUT_CAP_RATIO, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, type CallClaudeWithMcpParams, type ChatEngineConfig, type ChatGreetingParams, type ChatGreetingParts, type ChatHost, type ChatIdentity, type ChatMessage, ChatSession, type ChatState, type ChatStreamWiring, type ChatSystemPromptParams, type Citation, type CitationLinkPart, type ClaudeMcpServerRequest, type ClaudeMcpToolConfig, type ClaudeMessage, type ClaudeRole, type ComposedUserMessage, DATA_TOOL_NAMES, DEFAULT_CLAUDE_MODEL, DEFAULT_CONTEXT_WINDOW, DEFAULT_OPENAI_MODEL, DEFAULT_SKIP_DUPLICATE_FILES, DEFAULT_UPLOAD_ACCESS_GROUP, DEFAULT_WEB_ACCESS, DUPLICATE_LOOKUP_MAX_CANDIDATES, type DisplayEntry, type DuplicateUploadFile, type DuplicateUploadMatch, EMPTY_INDEXING_REPLY, 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, IMAGE_PREVIEWS_PER_MESSAGE, INDEXING_COMPLETE_MARKER, INDEXING_MAX_OUTPUT_TOKENS, INLINE_LINK_GLYPH, INLINE_LINK_UNAVAILABLE_GLYPH, INLINE_LINK_UNAVAILABLE_SUFFIX, INPUT_CAP_RATIO, type ImagePreviewContext, type IndexRunPatch, type IndexRunStatus, type IndexingAttachmentInfo, type IndexingFileRef, type IndexingGroup, type IndexingGroupStatus, type IndexingRequestRef, type IndexingSystemPromptParams, type InlineLinkContext, type InlineLinkMarkupOptions, type InlineLinkPart, LINK_LABEL_MAX_DISPLAY_CHARS, LINK_REFRESH_WINDOW_MS, type LiveStreamUpdate, MAX_CONCURRENT_BG_POLLS, MAX_HISTORY_FILL_PAGES, MAX_HISTORY_MESSAGES, MAX_OUTPUT_BY_MODEL, MAX_OUTPUT_TOKENS, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MINT_CACHE_GENERATION, MIN_INPUT_TOKEN_BUDGET, MIN_PER_REQUEST_INPUT_CAP, type MapHistoryOptions, NO_DATA_TOOL_MARK, OUTPUT_TOKEN_RESERVE, type OpenAIMessage, PAGE_TEXT_TABLE, POLL_INTERVAL, PRESIGN_SAFETY_MARGIN_MS, PREVIEWABLE_IMAGE_CONTENT_TYPES, PREVIEW_BROWSER_CACHE_SECONDS, PREVIEW_LAYOUT_BOX_SELECTOR, PREVIEW_URL_EXPIRES_SECONDS, PROJECT_SETTINGS_ACCESS_GROUP, PROJECT_SETTINGS_TABLE, PROJECT_SETTINGS_UNIQUE_ID, type ParsedAiAgent, type PinnedDispatchContext, type PreviewImageEl, type ProjectAccessSetting, type ProjectSettingsData, type ProjectSettingsReader, RENDER_FROM_TOKEN, RETRIEVAL_NOTES_MAX, RETRIEVAL_NOTES_TOKEN_BUDGET, RETRIEVAL_NOTE_MAX_CHARS, RETRIEVAL_NOTE_MAX_ITEMS, RETRIEVAL_NOTE_PREFIX, RTF_EXTS, RUN_RECORD_WORKING_STALE_MS, type RenderableInlineLink, type RescueDecisionContext, type RetrievalHit, type RetrievalNote, type RowAnchor, type RunStubInfo, STREAM_POLL_INTERVAL, type ScrollAnchor, type ScrollAnchorOptions, type SseChunk, type SseParser, type SseProvider, type SseSnapshot, type SseToolCall, type StreamDispatchContext, TOOL_AND_RESPONSE_BUFFER, UPLOAD_ACCESS_GROUPS, UPLOAD_ACCESS_HINTS, UPLOAD_ACCESS_LABELS, UPLOAD_ACCESS_OPTIONS, type UploadAccessGroup, type VisionProfile, XML_EXTS, __resetSplitHistoryState, accessSettingFrom, adoptLocalAnswerIntoPage, answerUsedNoDataTool, applyEncodingDeclaration, applyRetrievalNotes, asksUploadAccessFrom, bgIndexingQueueName, buildAiAgentValue, buildBoundedChatMessages, buildChatDisplayList, buildChatGreeting, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildHistoryItemFullId, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, cachedProjectSettings, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, canonicalizePathForm, chatCacheKey, chatEngineConfig, chatGreetingFrom, chatStreamWiring, citationFileName, citationLinkPart, citationsFor, classifyInlineLink, clearAttachmentParsers, clearImagePreviewCache, clearProjectSettings, composeUserMessage, configureChatEngine, configureProjectSettings, contentTypeForExt, createHistoryFiller, createInlineLinkRegex, createScrollAnchor, createSseParser, csrEnvelopeError, encodePathSegments, encodingClassForExt, ensureHtmlCharset, ensureXmlEncoding, escapeInlineHtml, escapeRtfNonAscii, estimateMessageTokens, estimateTextTokens, extOf, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, extractRetrievalNote, fetchLiveIndexingKeys, fillHistoryViewport, filterListByClearHorizon, findAttachmentParser, findDuplicateUpload, formatChatTimestamp, formatDuration, formatRetrievalNote, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, getInputTokenBudget, getMaxOutputTokens, getModelContextWindow, getProjectContextWindow, getSplitChatHistory, getVisionProfile, groupAttachmentFailures, hasBom, hydrateImagePreviews, indexDoneUniqueId, indexScopeKey, indexingAccessGroup, isAuthExpiredError, isBgIndexingQueue, isCsrStatusEnvelope, isDataToolName, isDuplicateCandidateId, isErrorResponseBody, isHttpUrlLike, isImageVisionFile, isIndexingRequestText, isLinkUnavailable, isNonRetryableRequestError, isOfficeFile, isPagedReadFile, isPreviewableImagePath, isProviderApiKeyError, isRetriedFailure, isRetrievalNote, isServerExtractable, isServiceDbAttachmentHref, isWindowedReadFile, linkUnavailableKeyForHref, linkUnavailableKeyForPath, linkUnavailableKeysForPath, listClaudeModels, listOpenAIModels, liveSafePrefix, loadProjectSettings, looksLikeRtf, makeExtractPlaceholder, mapHistoryListToMessages, markImagePreviewStale, mayKeepStreamedAnswer, mintCacheBustStamp, needsBomForExt, normalizeAttachmentPathCandidate, normalizeChatGreeting, normalizeExt, normalizeProjectAccessSetting, normalizeSkipDuplicateFiles, normalizeTextContent, normalizeTrailingInlineToken, normalizeUploadAccessGroup, normalizeWebAccess, notifyAgentSaveAttachment, parseAiAgentValue, parseAttachmentContent, parseIndexingLabel, parseIndexingRequestText, patchProjectSettings, peekImagePreviewUrl, prepareDownloadText, presignExpiryEpochMs, previewImageContentType, previewLayoutBox, previewMintCacheToken, previewableExtOf, primeProjectSettings, projectAccessSetting, projectAsksUploadAccess, projectChatGreeting, projectSettingsSettled, projectSkipDuplicateFiles, projectUploadAccessGroup, projectWebAccess, providerBodyWasRead, readExpiredAttachmentHref, readyProjectSettings, registerAttachmentParser, registerModelContextWindows, renderInlineLinkHtml, repairUrlEntities, repairUrlWhitespace, resolveImagePreviewUrl, resolveSkipDuplicateFiles, resolveWebAccess, retrievalNoteFromText, retrievalNoteOfAnswer, runIndexUniqueId, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, setProjectContextWindow, setProjectSettings, shouldRescueInFlightMessage, skapiSupportsStreaming, skipDuplicateFilesEnabled, skipDuplicateFilesFrom, streamRecoveryEnabled, streamRecoveryLabels, streamRecoveryPhase, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay, typewriterResumeIndex, uploadAccessGroupFrom, upsertIndexRunRecordSafe, wallClockNow, webAccessEnabled, webAccessFrom };
package/dist/engine.mjs CHANGED
@@ -71,6 +71,13 @@ function normalizeWebAccess(value) {
71
71
  function webAccessFrom(data) {
72
72
  return data ? normalizeWebAccess(data.web_access) : DEFAULT_WEB_ACCESS;
73
73
  }
74
+ var DEFAULT_SKIP_DUPLICATE_FILES = true;
75
+ function normalizeSkipDuplicateFiles(value) {
76
+ return value !== false;
77
+ }
78
+ function skipDuplicateFilesFrom(data) {
79
+ return data ? normalizeSkipDuplicateFiles(data.skip_duplicate_files) : DEFAULT_SKIP_DUPLICATE_FILES;
80
+ }
74
81
  var CHAT_GREETING_MAX_LENGTH = 400;
75
82
  function normalizeChatGreeting(value) {
76
83
  if (typeof value !== "string") return "";
@@ -157,6 +164,9 @@ function projectChatGreeting(service) {
157
164
  function projectWebAccess(service) {
158
165
  return webAccessFrom(cachedProjectSettings(service));
159
166
  }
167
+ function projectSkipDuplicateFiles(service) {
168
+ return skipDuplicateFilesFrom(cachedProjectSettings(service));
169
+ }
160
170
  function setProjectSettings(service, data) {
161
171
  if (!service) return;
162
172
  cache.set(service, { data: data || null, settled: true, inflight: null });
@@ -236,16 +246,29 @@ function liveStreamingRealtimeEnabled() {
236
246
  function liveStreamingEnabled() {
237
247
  return _config?.liveStreaming === true;
238
248
  }
249
+ function underCeiling(projectSetting, option, override) {
250
+ if (!projectSetting) return false;
251
+ if (option === false) return false;
252
+ if (override === false) return false;
253
+ return true;
254
+ }
239
255
  function webAccessEnabled(service) {
240
256
  const opt = _config ? _config.webAccess : void 0;
241
- if (typeof opt === "boolean") return opt;
242
- return webAccessFrom(service ? cachedProjectSettings(service) : null);
257
+ return underCeiling(webAccessFrom(service ? cachedProjectSettings(service) : null), opt);
243
258
  }
244
- function resolveWebAccess(service) {
259
+ function resolveWebAccess(service, override) {
245
260
  const opt = _config ? _config.webAccess : void 0;
246
- if (typeof opt === "boolean") return Promise.resolve(opt);
247
- if (!service) return Promise.resolve(false);
248
- return loadProjectSettings(service).then(webAccessFrom, () => false);
261
+ if (!service) return Promise.resolve(underCeiling(webAccessFrom(null), opt, override));
262
+ return loadProjectSettings(service).then(webAccessFrom, () => false).then((setting) => underCeiling(setting, opt, override));
263
+ }
264
+ function skipDuplicateFilesEnabled(service) {
265
+ const opt = _config ? _config.skipDuplicateFiles : void 0;
266
+ return underCeiling(skipDuplicateFilesFrom(service ? cachedProjectSettings(service) : null), opt);
267
+ }
268
+ function resolveSkipDuplicateFiles(service) {
269
+ const opt = _config ? _config.skipDuplicateFiles : void 0;
270
+ if (!service) return Promise.resolve(underCeiling(skipDuplicateFilesFrom(null), opt));
271
+ return loadProjectSettings(service).then(skipDuplicateFilesFrom, () => skipDuplicateFilesFrom(null)).then((setting) => underCeiling(setting, opt));
249
272
  }
250
273
  function streamRecoveryEnabled() {
251
274
  if (!_config) return false;
@@ -261,6 +284,54 @@ function pollOpt() {
261
284
  return p === void 0 ? {} : { poll: p };
262
285
  }
263
286
 
287
+ // src/engine/duplicates.ts
288
+ var DUPLICATE_LOOKUP_MAX_CANDIDATES = 25;
289
+ var SRC_PREFIX = "src::";
290
+ function isDuplicateCandidateId(uniqueId, name) {
291
+ if (typeof uniqueId !== "string" || !name) return false;
292
+ if (uniqueId.indexOf(SRC_PREFIX) !== 0) return false;
293
+ if (uniqueId === SRC_PREFIX + name) return true;
294
+ const tail = "/" + name;
295
+ return uniqueId.length > SRC_PREFIX.length + tail.length && uniqueId.slice(-tail.length) === tail;
296
+ }
297
+ function listOf(res) {
298
+ if (!res) return [];
299
+ if (Array.isArray(res)) return res;
300
+ return Array.isArray(res.list) ? res.list : [];
301
+ }
302
+ async function findDuplicateUpload(skapi, file, userId, opts) {
303
+ try {
304
+ if (!skapi || typeof skapi.getUniqueId !== "function" || typeof skapi.getRecords !== "function") return null;
305
+ const name = file && typeof file.name === "string" ? file.name : "";
306
+ const size = file && typeof file.size === "number" ? file.size : NaN;
307
+ if (!name || !(size >= 0) || !userId) return null;
308
+ const service = opts && opts.service ? { service: opts.service } : {};
309
+ const found = listOf(await skapi.getUniqueId(Object.assign({ unique_id: name, condition: "<=" }, service)));
310
+ const ids = [];
311
+ for (const item of found) {
312
+ const id = item && typeof item === "object" ? item.unique_id : item;
313
+ if (isDuplicateCandidateId(id, name) && ids.indexOf(id) === -1) ids.push(id);
314
+ if (ids.length >= DUPLICATE_LOOKUP_MAX_CANDIDATES) break;
315
+ }
316
+ for (const uniqueId of ids) {
317
+ let record = null;
318
+ try {
319
+ record = listOf(await skapi.getRecords(Object.assign({ unique_id: uniqueId }, service)))[0] || null;
320
+ } catch {
321
+ continue;
322
+ }
323
+ if (!record || typeof record !== "object") continue;
324
+ const data = record.data && typeof record.data === "object" ? record.data : null;
325
+ if (!data || data.size_bytes !== size) continue;
326
+ if (record.user_id !== userId) continue;
327
+ return { storagePath: uniqueId.slice(SRC_PREFIX.length), uniqueId, record };
328
+ }
329
+ return null;
330
+ } catch {
331
+ return null;
332
+ }
333
+ }
334
+
264
335
  // src/engine/office.ts
265
336
  var OFFICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
266
337
  "doc",
@@ -673,6 +744,10 @@ function buildRenderMeta(attachment) {
673
744
  ` : "") + `- access group (use this for EVERY record you write for this file): ${indexingAccessGroup(attachment)}
674
745
  `;
675
746
  }
747
+ var PAGE_TEXT_TABLE = "page_texts";
748
+ var PAGE_TEXT_RULE = `PAGE TEXT for pages WITHOUT a text layer: for EVERY page whose label (its first label, when the page is split into strips) carries NO "Embedded text layer" block, ALSO post exactly ONE record in table "${PAGE_TEXT_TABLE}" with data {"page": <its document page number from the label, 1-based>, "page_text": "<the page's complete text exactly as printed, verbatim, in reading order, nothing summarised or left out>"}, reference "src::" + the storage path above, in addition to the records above. A page WITH an embedded text layer gets NO such record: its text is already stored.
749
+
750
+ `;
676
751
  function buildRenderDatafy(placeholder) {
677
752
  return `
678
753
  ${placeholder}
@@ -683,7 +758,7 @@ Each image is preceded by a label giving its DOCUMENT PAGE number. That label is
683
758
 
684
759
  Transcribe COMPLETELY, not representatively. A table with twenty rows gets twenty records, not a sample of the first few - if a page has more rows than you can save comfortably, still save them all rather than summarising. Where a page carries an embedded text layer it is quoted above that page's image: it is the exact text and should be preferred over reading the pixels, with the image used for layout, tables, stamps and handwriting.
685
760
 
686
- Save records for THIS window of pages only, then stop and report what you saved. Do NOT try to read the rest of the file and do NOT worry about the pages after this window: if any remain, the next window is rendered and sent to you automatically. Report only the pages you were actually shown - never imply you have seen the whole document.`;
761
+ ` + PAGE_TEXT_RULE + `Save records for THIS window of pages only, then stop and report what you saved. Do NOT try to read the rest of the file and do NOT worry about the pages after this window: if any remain, the next window is rendered and sent to you automatically. Report only the pages you were actually shown - never imply you have seen the whole document.`;
687
762
  }
688
763
  function buildIndexingWindowMessage(attachment, placeholder, isContinuation, positionLabel) {
689
764
  const src = `src::${attachment.storagePath}`;
@@ -1225,7 +1300,7 @@ var RETRIEVAL_NOTE_PREFIX = "[Retrieval note, not shown to the user";
1225
1300
  var FILE_KEYS = ["path", "storage_path", "storagePath", "file", "file_path", "filePath"];
1226
1301
  var TABLE_KEYS = ["table_name", "tableName"];
1227
1302
  var GROUP_KEYS = ["access_group", "accessGroup"];
1228
- var SRC_PREFIX = "src::";
1303
+ var SRC_PREFIX2 = "src::";
1229
1304
  var RESULT_WALK_MAX_NODES = 400;
1230
1305
  var RESULT_WALK_MAX_DEPTH = 5;
1231
1306
  function isWebToolName(name) {
@@ -1254,8 +1329,8 @@ function pushHit(list, path, page) {
1254
1329
  }
1255
1330
  function pathFromSrcId(value) {
1256
1331
  if (typeof value !== "string") return null;
1257
- if (value.indexOf(SRC_PREFIX) !== 0) return null;
1258
- var rest = value.slice(SRC_PREFIX.length).trim();
1332
+ if (value.indexOf(SRC_PREFIX2) !== 0) return null;
1333
+ var rest = value.slice(SRC_PREFIX2.length).trim();
1259
1334
  return rest ? rest : null;
1260
1335
  }
1261
1336
  function parseJsonLoose(value) {
@@ -1454,6 +1529,9 @@ var CONTEXT_WINDOW_BY_MODEL = {
1454
1529
  "claude-haiku-4-5": 2e5,
1455
1530
  "claude-3-5-sonnet": 2e5,
1456
1531
  // openai, exact ids
1532
+ // gpt-6.1: OpenAI's listing carries no window; taken as the gpt-6 family's until a
1533
+ // published figure says otherwise (2026-10-12, the default model).
1534
+ "gpt-6.1-sol": 105e4,
1457
1535
  "gpt-6-astra": 105e4,
1458
1536
  "gpt-6-sol": 105e4,
1459
1537
  "gpt-6-luna": 105e4,
@@ -1473,6 +1551,7 @@ var CONTEXT_WINDOW_BY_MODEL = {
1473
1551
  "claude-opus": 1e6,
1474
1552
  "claude-sonnet": 1e6,
1475
1553
  "claude-haiku": 2e5,
1554
+ "gpt-6.1": 105e4,
1476
1555
  "gpt-6": 105e4,
1477
1556
  "gpt-5.6": 105e4,
1478
1557
  "gpt-5": 128e3
@@ -1490,6 +1569,7 @@ var MAX_OUTPUT_BY_MODEL = {
1490
1569
  "claude-haiku-4-5": 64e3,
1491
1570
  "claude-3-5-sonnet": 8e3,
1492
1571
  // openai
1572
+ "gpt-6.1-sol": 128e3,
1493
1573
  "gpt-6-astra": 128e3,
1494
1574
  "gpt-6-sol": 128e3,
1495
1575
  "gpt-6-luna": 128e3,
@@ -1509,6 +1589,7 @@ var MAX_OUTPUT_BY_MODEL = {
1509
1589
  "claude-opus": 128e3,
1510
1590
  "claude-sonnet": 64e3,
1511
1591
  "claude-haiku": 64e3,
1592
+ "gpt-6.1": 128e3,
1512
1593
  "gpt-6": 128e3,
1513
1594
  "gpt-5.6": 128e3,
1514
1595
  "gpt-5": 128e3
@@ -2715,8 +2796,8 @@ var webToolsForChat = (platform, webAccess) => {
2715
2796
  return [openaiWebSearchTool()] ;
2716
2797
  };
2717
2798
  var MCP_NAME = "BunnyQuery";
2718
- var DEFAULT_CLAUDE_MODEL = "claude-sonnet-5";
2719
- var DEFAULT_OPENAI_MODEL = "gpt-5.6-luna";
2799
+ var DEFAULT_CLAUDE_MODEL = "claude-sonnet-5-5";
2800
+ var DEFAULT_OPENAI_MODEL = "gpt-6.1-sol";
2720
2801
  var mcpUrl = () => chatEngineConfig().mcpBaseUrl;
2721
2802
  function withMcpParams(base, params) {
2722
2803
  if (!base) return base;
@@ -2918,7 +2999,7 @@ async function callClaudeWithMcp({
2918
2999
  fileUrls,
2919
3000
  webAccess
2920
3001
  }) {
2921
- const web = typeof webAccess === "boolean" ? webAccess : await resolveWebAccess(service);
3002
+ const web = await resolveWebAccess(service, typeof webAccess === "boolean" ? webAccess : void 0);
2922
3003
  const mcpServerDefinition = {
2923
3004
  type: "url",
2924
3005
  name: mcpServer.name,
@@ -8507,11 +8588,26 @@ var ChatSession = class {
8507
8588
  if (!total) return Promise.reject(new Error("Empty attachment"));
8508
8589
  var urls = [];
8509
8590
  var anyIndexFailed = false;
8591
+ var reusedCount = 0;
8592
+ att.reused = false;
8510
8593
  var chain = Promise.resolve();
8594
+ var findDuplicate = function(member) {
8595
+ if (typeof self.host.findDuplicateUpload !== "function") return Promise.resolve(null);
8596
+ var storedName = String(member.storagePath || "").split("/").filter(Boolean).pop() || member.file.name;
8597
+ return resolveSkipDuplicateFiles(id.projectId).then(function(on) {
8598
+ if (!on) return null;
8599
+ return Promise.resolve(self.host.findDuplicateUpload({ name: storedName, size: member.file.size })).then(function(hit) {
8600
+ return hit && typeof hit.storagePath === "string" && hit.storagePath ? hit.storagePath : null;
8601
+ });
8602
+ }).catch(function() {
8603
+ return null;
8604
+ });
8605
+ };
8511
8606
  members.forEach(function(member, idx) {
8512
8607
  chain = chain.then(function() {
8513
8608
  var hadExists = false;
8514
8609
  var skipped = false;
8610
+ var duplicateOf = null;
8515
8611
  var existedBefore = false;
8516
8612
  var onProg = function(p) {
8517
8613
  if (p && p.total) {
@@ -8530,7 +8626,14 @@ var ChatSession = class {
8530
8626
  }
8531
8627
  });
8532
8628
  };
8533
- return doMemberUpload(true).catch(function(err) {
8629
+ return findDuplicate(member).then(function(dup) {
8630
+ if (dup) {
8631
+ duplicateOf = dup;
8632
+ console.log("[chat-engine] skipping an upload the user already has:", member.storagePath, "->", dup);
8633
+ return;
8634
+ }
8635
+ return doMemberUpload(true);
8636
+ }).catch(function(err) {
8534
8637
  var code = err && (err.code || err.body && err.body.code);
8535
8638
  var msg = err && (err.message || err.body && err.body.message || (typeof err === "string" ? err : ""));
8536
8639
  var isExists = code === "EXISTS" || msg && /exist/i.test(msg);
@@ -8550,9 +8653,20 @@ var ChatSession = class {
8550
8653
  });
8551
8654
  }).then(function() {
8552
8655
  if (skipped) return;
8553
- return self.host.getTemporaryUrl(member.storagePath);
8656
+ return self.host.getTemporaryUrl(duplicateOf || member.storagePath);
8554
8657
  }).then(function(url) {
8555
8658
  if (skipped) return;
8659
+ if (duplicateOf) {
8660
+ urls.push({ name: member.relPath, url, storagePath: duplicateOf });
8661
+ reusedCount += 1;
8662
+ if (att.kind !== "folder") {
8663
+ att.uploadedUrl = url;
8664
+ att.storagePath = duplicateOf;
8665
+ att.reused = true;
8666
+ } else att.reusedCount = reusedCount;
8667
+ self.host.renderAttachmentChips();
8668
+ return;
8669
+ }
8556
8670
  urls.push({ name: member.relPath, url, storagePath: member.storagePath });
8557
8671
  if (att.kind !== "folder") {
8558
8672
  att.uploadedUrl = url;
@@ -9180,6 +9294,6 @@ function buildChatDisplayList(messages, opts) {
9180
9294
  return out;
9181
9295
  }
9182
9296
 
9183
- export { BG_INDEXING_QUEUE_SUFFIX, BOM, BOM_EXTS, CHAT_GREETING_MAX_LENGTH, CLAUDE_INPUT_CAP_RATIO, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, ChatSession, DATA_TOOL_NAMES, DEFAULT_CLAUDE_MODEL, DEFAULT_CONTEXT_WINDOW, DEFAULT_OPENAI_MODEL, DEFAULT_UPLOAD_ACCESS_GROUP, DEFAULT_WEB_ACCESS, EMPTY_INDEXING_REPLY, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, EXPIRED_LINK_REFRESH_EXPIRES_SECONDS, EXT_CONTENT_TYPES, HISTORY_BUDGET_RATIO, HISTORY_FILL_SLACK_PX, HISTORY_TOKEN_BUDGET, HTML_EXTS, HTML_HEAD_WINDOW, IMAGE_PREVIEWS_PER_MESSAGE, INDEXING_COMPLETE_MARKER, INDEXING_MAX_OUTPUT_TOKENS, INLINE_LINK_GLYPH, INLINE_LINK_UNAVAILABLE_GLYPH, INLINE_LINK_UNAVAILABLE_SUFFIX, INPUT_CAP_RATIO, LINK_LABEL_MAX_DISPLAY_CHARS, LINK_REFRESH_WINDOW_MS, MAX_CONCURRENT_BG_POLLS, MAX_HISTORY_FILL_PAGES, MAX_HISTORY_MESSAGES, MAX_OUTPUT_BY_MODEL, MAX_OUTPUT_TOKENS, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MINT_CACHE_GENERATION, MIN_INPUT_TOKEN_BUDGET, MIN_PER_REQUEST_INPUT_CAP, NO_DATA_TOOL_MARK, OUTPUT_TOKEN_RESERVE, POLL_INTERVAL, PRESIGN_SAFETY_MARGIN_MS, PREVIEWABLE_IMAGE_CONTENT_TYPES, PREVIEW_BROWSER_CACHE_SECONDS, PREVIEW_LAYOUT_BOX_SELECTOR, PREVIEW_URL_EXPIRES_SECONDS, PROJECT_SETTINGS_ACCESS_GROUP, PROJECT_SETTINGS_TABLE, PROJECT_SETTINGS_UNIQUE_ID, RENDER_FROM_TOKEN, RETRIEVAL_NOTES_MAX, RETRIEVAL_NOTES_TOKEN_BUDGET, RETRIEVAL_NOTE_MAX_CHARS, RETRIEVAL_NOTE_MAX_ITEMS, RETRIEVAL_NOTE_PREFIX, RTF_EXTS, RUN_RECORD_WORKING_STALE_MS, STREAM_POLL_INTERVAL, TOOL_AND_RESPONSE_BUFFER, UPLOAD_ACCESS_GROUPS, UPLOAD_ACCESS_HINTS, UPLOAD_ACCESS_LABELS, UPLOAD_ACCESS_OPTIONS, XML_EXTS, __resetSplitHistoryState, accessSettingFrom, adoptLocalAnswerIntoPage, answerUsedNoDataTool, applyEncodingDeclaration, applyRetrievalNotes, asksUploadAccessFrom, bgIndexingQueueName, buildAiAgentValue, buildBoundedChatMessages, buildChatDisplayList, buildChatGreeting, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildHistoryItemFullId, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, cachedProjectSettings, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, canonicalizePathForm, chatCacheKey, chatEngineConfig, chatGreetingFrom, chatStreamWiring, citationFileName, citationLinkPart, citationsFor, classifyInlineLink, clearAttachmentParsers, clearImagePreviewCache, clearProjectSettings, composeUserMessage, configureChatEngine, configureProjectSettings, contentTypeForExt, createHistoryFiller, createInlineLinkRegex, createScrollAnchor, createSseParser, csrEnvelopeError, encodePathSegments, encodingClassForExt, ensureHtmlCharset, ensureXmlEncoding, escapeInlineHtml, escapeRtfNonAscii, estimateMessageTokens, estimateTextTokens, extOf, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, extractRetrievalNote, fetchLiveIndexingKeys, fillHistoryViewport, filterListByClearHorizon, findAttachmentParser, formatChatTimestamp, formatDuration, formatRetrievalNote, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, getInputTokenBudget, getMaxOutputTokens, getModelContextWindow, getProjectContextWindow, getSplitChatHistory, getVisionProfile, groupAttachmentFailures, hasBom, hydrateImagePreviews, indexDoneUniqueId, indexScopeKey, indexingAccessGroup, isAuthExpiredError, isBgIndexingQueue, isCsrStatusEnvelope, isDataToolName, isErrorResponseBody, isHttpUrlLike, isImageVisionFile, isIndexingRequestText, isLinkUnavailable, isNonRetryableRequestError, isOfficeFile, isPagedReadFile, isPreviewableImagePath, isProviderApiKeyError, isRetriedFailure, isRetrievalNote, isServerExtractable, isServiceDbAttachmentHref, isWindowedReadFile, linkUnavailableKeyForHref, linkUnavailableKeyForPath, linkUnavailableKeysForPath, listClaudeModels, listOpenAIModels, liveSafePrefix, loadProjectSettings, looksLikeRtf, makeExtractPlaceholder, mapHistoryListToMessages, markImagePreviewStale, mayKeepStreamedAnswer, mintCacheBustStamp, needsBomForExt, normalizeAttachmentPathCandidate, normalizeChatGreeting, normalizeExt, normalizeProjectAccessSetting, normalizeTextContent, normalizeTrailingInlineToken, normalizeUploadAccessGroup, normalizeWebAccess, notifyAgentSaveAttachment, parseAiAgentValue, parseAttachmentContent, parseIndexingLabel, parseIndexingRequestText, patchProjectSettings, peekImagePreviewUrl, prepareDownloadText, presignExpiryEpochMs, previewImageContentType, previewLayoutBox, previewMintCacheToken, previewableExtOf, primeProjectSettings, projectAccessSetting, projectAsksUploadAccess, projectChatGreeting, projectSettingsSettled, projectUploadAccessGroup, projectWebAccess, providerBodyWasRead, readExpiredAttachmentHref, readyProjectSettings, registerAttachmentParser, registerModelContextWindows, renderInlineLinkHtml, repairUrlEntities, repairUrlWhitespace, resolveImagePreviewUrl, resolveWebAccess, retrievalNoteFromText, retrievalNoteOfAnswer, runIndexUniqueId, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, setProjectContextWindow, setProjectSettings, shouldRescueInFlightMessage, skapiSupportsStreaming, streamRecoveryEnabled, streamRecoveryLabels, streamRecoveryPhase, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay, typewriterResumeIndex, uploadAccessGroupFrom, upsertIndexRunRecordSafe, wallClockNow, webAccessEnabled, webAccessFrom };
9297
+ export { BG_INDEXING_QUEUE_SUFFIX, BOM, BOM_EXTS, CHAT_GREETING_MAX_LENGTH, CLAUDE_INPUT_CAP_RATIO, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, ChatSession, DATA_TOOL_NAMES, DEFAULT_CLAUDE_MODEL, DEFAULT_CONTEXT_WINDOW, DEFAULT_OPENAI_MODEL, DEFAULT_SKIP_DUPLICATE_FILES, DEFAULT_UPLOAD_ACCESS_GROUP, DEFAULT_WEB_ACCESS, DUPLICATE_LOOKUP_MAX_CANDIDATES, EMPTY_INDEXING_REPLY, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, EXPIRED_LINK_REFRESH_EXPIRES_SECONDS, EXT_CONTENT_TYPES, HISTORY_BUDGET_RATIO, HISTORY_FILL_SLACK_PX, HISTORY_TOKEN_BUDGET, HTML_EXTS, HTML_HEAD_WINDOW, IMAGE_PREVIEWS_PER_MESSAGE, INDEXING_COMPLETE_MARKER, INDEXING_MAX_OUTPUT_TOKENS, INLINE_LINK_GLYPH, INLINE_LINK_UNAVAILABLE_GLYPH, INLINE_LINK_UNAVAILABLE_SUFFIX, INPUT_CAP_RATIO, LINK_LABEL_MAX_DISPLAY_CHARS, LINK_REFRESH_WINDOW_MS, MAX_CONCURRENT_BG_POLLS, MAX_HISTORY_FILL_PAGES, MAX_HISTORY_MESSAGES, MAX_OUTPUT_BY_MODEL, MAX_OUTPUT_TOKENS, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MINT_CACHE_GENERATION, MIN_INPUT_TOKEN_BUDGET, MIN_PER_REQUEST_INPUT_CAP, NO_DATA_TOOL_MARK, OUTPUT_TOKEN_RESERVE, PAGE_TEXT_TABLE, POLL_INTERVAL, PRESIGN_SAFETY_MARGIN_MS, PREVIEWABLE_IMAGE_CONTENT_TYPES, PREVIEW_BROWSER_CACHE_SECONDS, PREVIEW_LAYOUT_BOX_SELECTOR, PREVIEW_URL_EXPIRES_SECONDS, PROJECT_SETTINGS_ACCESS_GROUP, PROJECT_SETTINGS_TABLE, PROJECT_SETTINGS_UNIQUE_ID, RENDER_FROM_TOKEN, RETRIEVAL_NOTES_MAX, RETRIEVAL_NOTES_TOKEN_BUDGET, RETRIEVAL_NOTE_MAX_CHARS, RETRIEVAL_NOTE_MAX_ITEMS, RETRIEVAL_NOTE_PREFIX, RTF_EXTS, RUN_RECORD_WORKING_STALE_MS, STREAM_POLL_INTERVAL, TOOL_AND_RESPONSE_BUFFER, UPLOAD_ACCESS_GROUPS, UPLOAD_ACCESS_HINTS, UPLOAD_ACCESS_LABELS, UPLOAD_ACCESS_OPTIONS, XML_EXTS, __resetSplitHistoryState, accessSettingFrom, adoptLocalAnswerIntoPage, answerUsedNoDataTool, applyEncodingDeclaration, applyRetrievalNotes, asksUploadAccessFrom, bgIndexingQueueName, buildAiAgentValue, buildBoundedChatMessages, buildChatDisplayList, buildChatGreeting, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildHistoryItemFullId, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, cachedProjectSettings, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, canonicalizePathForm, chatCacheKey, chatEngineConfig, chatGreetingFrom, chatStreamWiring, citationFileName, citationLinkPart, citationsFor, classifyInlineLink, clearAttachmentParsers, clearImagePreviewCache, clearProjectSettings, composeUserMessage, configureChatEngine, configureProjectSettings, contentTypeForExt, createHistoryFiller, createInlineLinkRegex, createScrollAnchor, createSseParser, csrEnvelopeError, encodePathSegments, encodingClassForExt, ensureHtmlCharset, ensureXmlEncoding, escapeInlineHtml, escapeRtfNonAscii, estimateMessageTokens, estimateTextTokens, extOf, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, extractRetrievalNote, fetchLiveIndexingKeys, fillHistoryViewport, filterListByClearHorizon, findAttachmentParser, findDuplicateUpload, formatChatTimestamp, formatDuration, formatRetrievalNote, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, getInputTokenBudget, getMaxOutputTokens, getModelContextWindow, getProjectContextWindow, getSplitChatHistory, getVisionProfile, groupAttachmentFailures, hasBom, hydrateImagePreviews, indexDoneUniqueId, indexScopeKey, indexingAccessGroup, isAuthExpiredError, isBgIndexingQueue, isCsrStatusEnvelope, isDataToolName, isDuplicateCandidateId, isErrorResponseBody, isHttpUrlLike, isImageVisionFile, isIndexingRequestText, isLinkUnavailable, isNonRetryableRequestError, isOfficeFile, isPagedReadFile, isPreviewableImagePath, isProviderApiKeyError, isRetriedFailure, isRetrievalNote, isServerExtractable, isServiceDbAttachmentHref, isWindowedReadFile, linkUnavailableKeyForHref, linkUnavailableKeyForPath, linkUnavailableKeysForPath, listClaudeModels, listOpenAIModels, liveSafePrefix, loadProjectSettings, looksLikeRtf, makeExtractPlaceholder, mapHistoryListToMessages, markImagePreviewStale, mayKeepStreamedAnswer, mintCacheBustStamp, needsBomForExt, normalizeAttachmentPathCandidate, normalizeChatGreeting, normalizeExt, normalizeProjectAccessSetting, normalizeSkipDuplicateFiles, normalizeTextContent, normalizeTrailingInlineToken, normalizeUploadAccessGroup, normalizeWebAccess, notifyAgentSaveAttachment, parseAiAgentValue, parseAttachmentContent, parseIndexingLabel, parseIndexingRequestText, patchProjectSettings, peekImagePreviewUrl, prepareDownloadText, presignExpiryEpochMs, previewImageContentType, previewLayoutBox, previewMintCacheToken, previewableExtOf, primeProjectSettings, projectAccessSetting, projectAsksUploadAccess, projectChatGreeting, projectSettingsSettled, projectSkipDuplicateFiles, projectUploadAccessGroup, projectWebAccess, providerBodyWasRead, readExpiredAttachmentHref, readyProjectSettings, registerAttachmentParser, registerModelContextWindows, renderInlineLinkHtml, repairUrlEntities, repairUrlWhitespace, resolveImagePreviewUrl, resolveSkipDuplicateFiles, resolveWebAccess, retrievalNoteFromText, retrievalNoteOfAnswer, runIndexUniqueId, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, setProjectContextWindow, setProjectSettings, shouldRescueInFlightMessage, skapiSupportsStreaming, skipDuplicateFilesEnabled, skipDuplicateFilesFrom, streamRecoveryEnabled, streamRecoveryLabels, streamRecoveryPhase, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay, typewriterResumeIndex, uploadAccessGroupFrom, upsertIndexRunRecordSafe, wallClockNow, webAccessEnabled, webAccessFrom };
9184
9298
  //# sourceMappingURL=engine.mjs.map
9185
9299
  //# sourceMappingURL=engine.mjs.map