bunnyquery 1.8.6 → 1.8.9

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
@@ -95,6 +95,57 @@ interface ChatEngineConfig {
95
95
  * until the worker is deployed, then flip it per environment.
96
96
  */
97
97
  windowedIndexing?: boolean;
98
+ /**
99
+ * Mint the durable "indexing finished" marker record ("done::<path>",
100
+ * reference "src::<path>", table __INDEXING__ — same shape the backend
101
+ * worker writes via /internal/index-complete) for runs whose completion
102
+ * THIS CLIENT knows deterministically: a single-pass file's settled pass,
103
+ * or a client-driven chain whose reply carried the completion token.
104
+ * Worker-driven chains are NOT minted from the client (their completion is
105
+ * only ever inferred here); the worker writes their marker itself.
106
+ * Must be best-effort: tolerate the marker already existing and never
107
+ * throw. Optional so older consumers keep the pre-marker inference.
108
+ */
109
+ mintIndexDoneMarker?: (info: {
110
+ service: string;
111
+ storagePath: string;
112
+ }) => void;
113
+ /**
114
+ * Create-or-update the per-file indexing RUN record ("run::<path>",
115
+ * reference "src::<path>", table __INDEXING__). The record is the durable
116
+ * "a run exists and this is its status" signal that lets chat rows and
117
+ * files-page badges paint without scanning bg history.
118
+ *
119
+ * The consumer implements upsert semantics (the records API has none):
120
+ * create, and on "is already taken" look the record up by unique_id and
121
+ * re-post with its record_id, merging `patch` over the stored data.
122
+ * Status precedence is the consumer's job too: 'working' must NEVER
123
+ * overwrite a terminal status (done/error/cancelled) — a late create from
124
+ * a slow enqueue must not resurrect a run another writer already closed.
125
+ * Must be best-effort and never throw. Optional: without it the engine
126
+ * behaves exactly as before (legacy scan/probe path).
127
+ */
128
+ upsertIndexRunRecord?: (info: {
129
+ service: string;
130
+ storagePath: string;
131
+ patch: {
132
+ status: 'working' | 'done' | 'error' | 'cancelled';
133
+ filename?: string;
134
+ started?: number;
135
+ finished?: number;
136
+ error?: string;
137
+ queue?: string;
138
+ };
139
+ }) => void;
140
+ /**
141
+ * Single-item csr-poll point lookup (skapi.util.request('csr-poll', {id,
142
+ * service, owner}, {auth:true})). For a RESOLVED item the backend returns
143
+ * the provider response body itself; for a failed one, the resolved error.
144
+ * Used by ChatSession.hydrateCompactItems to fetch the real bodies of
145
+ * compact history stubs when the user expands an indexing row. Optional:
146
+ * without it, stubs keep their server-extracted heads.
147
+ */
148
+ csrHistoryItemLookup?: (fullId: string, service: string, owner: string) => Promise<any>;
98
149
  }
99
150
  declare function configureChatEngine(config: ChatEngineConfig): void;
100
151
  declare function chatEngineConfig(): ChatEngineConfig;
@@ -149,7 +200,26 @@ declare function composeUserMessage(text: string, attachmentUrls: Array<{
149
200
  name: string;
150
201
  url: string;
151
202
  storagePath?: string;
152
- }>): ComposedUserMessage;
203
+ }>, opts?: {
204
+ /**
205
+ * Inline each server-extractable attachment's whole text into the
206
+ * prompt (the `_skapi_extract` directives + BEGIN/END FILE CONTENT
207
+ * block). Default true, which is right when the file's content is
208
+ * nowhere else yet.
209
+ *
210
+ * Pass FALSE when the turn is dispatched AFTER the file's indexing run
211
+ * has drained. Extraction is the same server-side download+parse the
212
+ * indexing pass already performed, so inlining repeats it: the worker
213
+ * fetches and re-parses every attachment a second time (which reads,
214
+ * from the outside, exactly like the file being indexed again), and the
215
+ * whole file text is re-sent as prompt tokens. It is also the WORSE
216
+ * copy for anything large, because inline extraction truncates at
217
+ * MAX_EXTRACTED_CHARS while the indexed records cover the file end to
218
+ * end. The model reaches the content through the records
219
+ * (getRecords with reference "src::<path>") or readFileContent.
220
+ */
221
+ inlineExtractedContent?: boolean;
222
+ }): ComposedUserMessage;
153
223
 
154
224
  /**
155
225
  * Attachment helpers shared by every consumer's view layer.
@@ -309,26 +379,56 @@ declare function getErrorMessage(input: any): string;
309
379
  declare function isErrorResponseBody(response: any): boolean;
310
380
  declare function isNonRetryableRequestError(input: any): boolean;
311
381
  declare function isAuthExpiredError(input: any): boolean;
382
+ /**
383
+ * True when the AI PROVIDER rejected the project's own API key.
384
+ *
385
+ * Deliberately narrow, and deliberately NOT the same question as
386
+ * isAuthExpiredError: that one is about OUR session/MCP bearer going stale,
387
+ * which the client fixes by refreshing and resending. This one means the key
388
+ * the project owner pasted is wrong or revoked, which only a human can fix.
389
+ *
390
+ * A bare 401 is NOT enough to conclude it (the MCP bearer expiring is also a
391
+ * 401), so this matches only the provider's key-specific markers:
392
+ * Anthropic -> `authentication_error`, "invalid x-api-key"
393
+ * OpenAI -> `invalid_api_key`, "Incorrect API key provided"
394
+ * Accepts a response object, a thrown error, or the message string those get
395
+ * reduced to by getErrorMessage, because the view usually only keeps the text.
396
+ */
397
+ declare function isProviderApiKeyError(input: any): boolean;
312
398
 
313
399
  declare var CONTEXT_WINDOW_DEFAULT: Record<string, number>;
314
400
  declare var CONTEXT_WINDOW_BY_MODEL: Record<string, number>;
401
+ declare var MAX_OUTPUT_BY_MODEL: Record<string, number>;
402
+ declare var DEFAULT_CONTEXT_WINDOW: number;
315
403
  /**
316
- * Record context windows from a provider models listing. Accepts the raw list
317
- * items and reads `max_input_tokens` (Anthropic); items without it are skipped,
318
- * so passing an OpenAI listing is a no-op rather than an error.
404
+ * Record context windows and output caps from a provider models listing. Reads
405
+ * `max_input_tokens` and `max_tokens` (Anthropic); items without them are
406
+ * skipped, so passing an OpenAI listing is a no-op rather than an error.
407
+ *
408
+ * Note the asymmetry against the static table: Anthropic reports
409
+ * `max_input_tokens` (input only) where CONTEXT_WINDOW_BY_MODEL holds totals, so
410
+ * a registered Claude window is treated as a total and loses its output cap
411
+ * worth of budget. That is deliberate — under-spending the window is safe, and
412
+ * with no compaction beta enabled overrunning it is a hard error.
319
413
  */
320
414
  declare function registerModelContextWindows(models: Array<{
321
415
  id?: string;
322
416
  max_input_tokens?: number;
417
+ max_tokens?: number;
323
418
  }> | null | undefined): void;
324
419
  declare function setProjectContextWindow(projectId: string, tokens: number | null | undefined): void;
325
420
  declare function getProjectContextWindow(projectId: string): number | null;
421
+ declare var MAX_OUTPUT_TOKENS: number;
326
422
  declare var OUTPUT_TOKEN_RESERVE: number;
327
423
  declare var TOOL_AND_RESPONSE_BUFFER: number;
328
424
  declare var MIN_INPUT_TOKEN_BUDGET: number;
425
+ declare var MIN_PER_REQUEST_INPUT_CAP: number;
426
+ /** @deprecated renamed to {@link MIN_PER_REQUEST_INPUT_CAP} (no longer Claude-only). */
329
427
  declare var CLAUDE_PER_REQUEST_INPUT_CAP: number;
330
428
  declare var MAX_HISTORY_MESSAGES: number;
331
429
  declare var HISTORY_TOKEN_BUDGET: number;
430
+ declare var INPUT_CAP_RATIO: number;
431
+ /** @deprecated renamed to {@link INPUT_CAP_RATIO} (no longer Claude-only). */
332
432
  declare var CLAUDE_INPUT_CAP_RATIO: number;
333
433
  declare var HISTORY_BUDGET_RATIO: number;
334
434
  declare function estimateTextTokens(text: string): number;
@@ -336,20 +436,21 @@ declare function estimateMessageTokens(msg: {
336
436
  role: string;
337
437
  content: string;
338
438
  }): number;
439
+ declare function getModelContextWindow(platform: string, model?: string): number;
339
440
  /**
340
- * Resolve a model's context window, most specific source first:
341
- * 1. per-project override (project settings)
342
- * 2. the provider's own models listing (Anthropic `max_input_tokens`)
343
- * 3. an exact entry in CONTEXT_WINDOW_BY_MODEL
344
- * 4. a family entry, by dropping trailing '-' segments off the id
345
- * 5. the platform default
346
- *
347
- * Step 4 is why a new or suffixed id no longer drops straight to the platform
348
- * default: 'gpt-5.6-luna' resolves via 'gpt-5.6', and a dated Claude snapshot
349
- * such as 'claude-opus-4-7-20260101' resolves via 'claude-opus-4-7'. The walk
350
- * stops at the first hit, so a more specific entry always wins over its family.
441
+ * How many output tokens to ask for. We never want more than MAX_OUTPUT_TOKENS,
442
+ * but a model whose own cap is lower rejects the request outright, so clamp to
443
+ * whichever is smaller. Models with no known cap keep MAX_OUTPUT_TOKENS.
444
+ */
445
+ declare function getMaxOutputTokens(platform: string, model?: string): number;
446
+ /**
447
+ * The window a request is actually budgeted at: the per-project override when
448
+ * one is set, otherwise DEFAULT_CONTEXT_WINDOW. Both are clamped to the model's
449
+ * hard ceiling, because a budget above the ceiling builds a request the provider
450
+ * rejects, and a stored override outlives the model it was chosen under.
351
451
  */
352
452
  declare function getContextWindow(platform: string, model?: string, projectId?: string): number;
453
+ declare function getInputTokenBudget(platform: string, model?: string, projectId?: string): number;
353
454
  declare function stripFileBlocksFromHistory(content: string): string;
354
455
  type BoundedChatOptions = {
355
456
  platform: string;
@@ -488,6 +589,23 @@ declare var LINK_LABEL_MAX_DISPLAY_CHARS: number;
488
589
  * not, which is precisely the kind of divergence a shared constant exists to stop.
489
590
  */
490
591
  declare var EXPIRED_LINK_REFRESH_EXPIRES_SECONDS: number;
592
+ /**
593
+ * Lifetime of the url minted for an inline image PREVIEW.
594
+ *
595
+ * Longer than the click url above, and for a different reason. A click hands the
596
+ * user a url they may keep, so it stays short. A preview url is consumed by the
597
+ * page itself and never leaves it, and it is the ONE lever on how long the
598
+ * downloaded picture stays reusable: get_signed_url will not cache a mint for
599
+ * longer than the credential inside it survives, so `browser_cache` cannot buy
600
+ * local availability that `expires` has not paid for. Twenty minutes meant every
601
+ * image re-downloaded three times an hour of ordinary reading.
602
+ *
603
+ * An hour, giving 55 minutes of cache once the server's five minute headroom is
604
+ * taken off. Short enough that a leaked preview url is not a standing grant, long
605
+ * enough that a conversation does not re-fetch its own pictures while the user is
606
+ * still reading it.
607
+ */
608
+ declare var PREVIEW_URL_EXPIRES_SECONDS: number;
491
609
  /**
492
610
  * Seconds the browser may reuse a minted preview url (`browser_cache`).
493
611
  *
@@ -497,12 +615,17 @@ declare var EXPIRED_LINK_REFRESH_EXPIRES_SECONDS: number;
497
615
  * url comes back out of the browser cache, so the body already on disk stays
498
616
  * addressable.
499
617
  *
500
- * Deliberately far longer than EXPIRED_LINK_REFRESH_EXPIRES_SECONDS above, and
501
- * that is the whole trick: the url is short-lived while the file stays available
502
- * locally for a WEEK. What keeps an image painting is the cached BODY, not a live
503
- * url. Once the browser evicts that body it refetches with a url that has since
504
- * expired, gets a 403, and the error path re-mints with `refresh`. That path is
505
- * therefore load-bearing, not a rare fallback.
618
+ * A CEILING, not a promise. get_signed_url caps what it grants at the lifetime of
619
+ * the url inside the response (expires minus headroom, so 15 minutes for the
620
+ * platform's 20 minute url), because a mint cached for longer than its own
621
+ * credential is a guaranteed 403 that the browser keeps serving from its own
622
+ * store. Asking for the week is still right: it says what this client would
623
+ * reuse if the url were stable by construction, and the server decides.
624
+ *
625
+ * What keeps an image painting is the cached BODY, not a live url. Once the
626
+ * browser evicts that body it refetches with a url that has since expired, gets a
627
+ * 403, and the error path re-mints with `refresh` and mintCacheBustStamp. That
628
+ * path is load-bearing, not a rare fallback.
506
629
  *
507
630
  * A week is the platform default for reading a private file, not a number chosen
508
631
  * here: skapi-js reads every private record file with
@@ -525,6 +648,69 @@ declare var PREVIEW_BROWSER_CACHE_SECONDS: number;
525
648
  * dead url with no way to notice; deriving it makes that unrepresentable.
526
649
  */
527
650
  declare var LINK_REFRESH_WINDOW_MS: number;
651
+ /**
652
+ * Cache generation for the mint request url. BUMP THIS to abandon every mint
653
+ * response browsers are currently holding.
654
+ *
655
+ * Generation 2 retires the entries written before 2026-08-11. Those were stored
656
+ * with `max-age=604800` around a presign that dies in twenty minutes, so from
657
+ * minute 21 each one is a guaranteed 403 that the browser keeps serving from its
658
+ * own store for the rest of the week. The server no longer grants a lifetime a
659
+ * url cannot back (get_signed_url resolve_browser_cache), but that fixes what is
660
+ * written from now on and cannot reach what is already stored on a user's
661
+ * device. Changing the url is the only thing that can: an entry nobody requests
662
+ * again is an entry that cannot answer again.
663
+ */
664
+ declare var MINT_CACHE_GENERATION: number;
665
+ /**
666
+ * Window stamp for a REFRESH mint.
667
+ *
668
+ * WINDOWED, not Date.now(): a per-call stamp is a new cache key per image per
669
+ * retry, which is what made the original `nocache` parameter worse than the
670
+ * disease. One stamp per refresh window means every repair inside those minutes
671
+ * shares a single entry, and it rotates before the url it carries can die.
672
+ */
673
+ declare function mintCacheBustStamp(now?: number): number;
674
+ /**
675
+ * The `nocache` value for a preview mint: the generation, plus a window stamp
676
+ * when this mint is a repair.
677
+ *
678
+ * A repair MUST reach the origin, and the request header the clients used to
679
+ * rely on cannot do it. `Cache-Control: no-cache` is not a CORS-safelisted
680
+ * request header, and the record gateway's preflight answers
681
+ * `Access-Control-Allow-Headers` WITHOUT it (verified against the live api on
682
+ * 2026-08-11), so a mint carrying that header is rejected by the browser before
683
+ * it is ever sent. Every repair therefore failed, in every browser, and the chip
684
+ * went straight to "(unavailable)". Only a phone noticed, because only a phone
685
+ * drops image bodies often enough to need the repair at all.
686
+ *
687
+ * A query parameter has no such problem: it is part of the url, so it needs no
688
+ * preflight and no cooperation from the cache.
689
+ */
690
+ declare function previewMintCacheToken(refresh?: boolean): string;
691
+ /**
692
+ * How long before a presign dies we stop handing it out.
693
+ *
694
+ * A url served with one second left is a 403 with extra steps: the request still
695
+ * has to reach S3, and an image body still has to start arriving.
696
+ */
697
+ declare var PRESIGN_SAFETY_MARGIN_MS: number;
698
+ /**
699
+ * When the url in hand actually dies, read out of the url itself, or null if it
700
+ * carries no expiry we recognise.
701
+ *
702
+ * Every client-side cache here ages a url from the moment it ARRIVED, which is
703
+ * only the same thing as its lifetime when the mint went to the network. Once
704
+ * mint responses are cacheable that assumption breaks: a mint answered from the
705
+ * browser's store can be nearly as old as its own max-age, and the client then
706
+ * adds its own reuse window on top, so a 20 minute credential can be handed to an
707
+ * <img> half an hour after it was signed. Asking the url when it dies removes the
708
+ * stacking instead of trying to budget for it.
709
+ *
710
+ * Both signature versions, because the platform mints SigV2 through the host
711
+ * bucket and SigV4 elsewhere.
712
+ */
713
+ declare function presignExpiryEpochMs(url: string): number | null;
528
714
  declare function createInlineLinkRegex(): RegExp;
529
715
  declare function safeDecodeURIComponent(v: string): string;
530
716
  declare function encodePathSegments(path: string): string;
@@ -681,6 +867,17 @@ declare function classifyInlineLink(full: string, groups: Array<string | undefin
681
867
  */
682
868
  declare function linkUnavailableKeyForPath(remotePath: string): string;
683
869
  declare function linkUnavailableKeyForHref(href: string): string;
870
+ /**
871
+ * Every key a stored file can be marked under, given only its path.
872
+ *
873
+ * Marking writes ONE key (whichever identifier the failing call had) and the
874
+ * lookup ORs all of them, which is fine in one direction and wrong in the other:
875
+ * a view that later learns the file is reachable knows only the path, and
876
+ * clearing `path:` alone leaves a chip greyed by a failed CLICK (which marks
877
+ * `href:` too) exactly as dead as before. The placeholder href is derived from
878
+ * the path, so both keys can be rebuilt from it.
879
+ */
880
+ declare function linkUnavailableKeysForPath(remotePath: string): string[];
684
881
  declare function isLinkUnavailable(link: {
685
882
  href?: string;
686
883
  expiredHref?: string;
@@ -889,153 +1086,6 @@ type ParsedAiAgent = {
889
1086
  declare function parseAiAgentValue(value: string | null | undefined): ParsedAiAgent;
890
1087
  declare function buildAiAgentValue(platform: string | null | undefined, model?: string | null, contextWindow?: number | null): string;
891
1088
 
892
- declare function filterListByClearHorizon(list: any[], clearedAt: number): any[];
893
- declare function normalizeTextContent(content: any): string;
894
- declare function extractLastUserTextFromRequest(requestBody: any): string;
895
- /** The two openings an indexing prompt can have. A bg-queue item that starts with
896
- * neither is an ordinary chat that happened to be routed onto that queue. */
897
- declare function isIndexingRequestText(userText: any): boolean;
898
- type IndexingRequestRef = {
899
- name: string;
900
- path?: string;
901
- mime?: string;
902
- size?: number;
903
- /** A CONTINUE pass rather than the run's first. */
904
- continued: boolean;
905
- };
906
- /**
907
- * The file an indexing prompt is about, read back out of the prompt itself.
908
- *
909
- * The prompt is the only description of the pass that survives on the server, so
910
- * this is how BOTH a history rebuild and a worker-minted pass the client never
911
- * dispatched (ChatSession._adoptWorkerIndexingPasses) recover the file. Shared so
912
- * the two produce the same `_indexFile`, which is what makes them group together.
913
- */
914
- declare function parseIndexingRequestText(userText: any): IndexingRequestRef | null;
915
- type MapHistoryOptions = {
916
- clearedAt: number;
917
- projectId: string;
918
- /** View-side display formatter for "Indexing:/Reindexing: …" bubbles. */
919
- formatIndexingLabel: (name: string, mime?: string, size?: number | null, storagePath?: string, reindex?: boolean, continued?: boolean) => string;
920
- };
921
- declare function mapHistoryListToMessages(list: any[], platform: 'claude' | 'openai', opts: MapHistoryOptions): {
922
- messages: any[];
923
- runningItemIds: string[];
924
- };
925
-
926
- /**
927
- * Keep older history REACHABLE by paging until the message box actually gains
928
- * something to scroll to.
929
- *
930
- * Older history is paged in by one trigger only: the user scrolling to the top
931
- * of the message box. That trigger has two ways to die, and collapsed indexing
932
- * rows cause both:
933
- *
934
- * 1. The box never scrolls. A file's every indexing pass (the first plus every
935
- * CONTINUE pass, request AND response bubble each) folds into ONE row, so a
936
- * full history page — twenty-plus messages — can render as a single line.
937
- * Content shorter than the viewport fires no scroll event, so page 2 is
938
- * never requested and any conversation the user had before that upload is
939
- * permanently out of reach.
940
- * 2. The fetched page adds no height. A page that is entirely the same file's
941
- * earlier passes joins the collapsed row already on screen and renders
942
- * nothing new. The user, sitting at scrollTop 0, scrolls up again — and
943
- * because the position never changed, no further scroll event fires.
944
- *
945
- * Both are the same shape: fetch, re-measure, and keep going until the user
946
- * genuinely gained reachable content, history ran out, or the pager stopped
947
- * advancing. `isSatisfied` is what differs between the two (can the box scroll
948
- * at all / did it grow), so the loop below takes it as a predicate.
949
- *
950
- * DOM-free like the rest of the engine — the caller supplies the measurement and
951
- * awaits its own render before measuring, so agent.vue and the widget run the
952
- * identical loop over their own pagers.
953
- */
954
- /** Overflow (px) that counts as "the user can scroll here". Comfortably more
955
- * than the 60px top threshold that triggers the next page, so a filled box has
956
- * real room to scroll rather than sitting one pixel from the trigger. */
957
- declare const HISTORY_FILL_SLACK_PX = 64;
958
- /** Pages one fill pass will request before giving up. Reached only by a chat
959
- * whose history really is dozens of pages of one file's indexing passes; the
960
- * cap exists so a pager that stops advancing can never spin forever. */
961
- declare const MAX_HISTORY_FILL_PAGES = 24;
962
- type FillHistoryViewportOptions = {
963
- /** The user has reachable content and paging can stop. Called AFTER the
964
- * caller's own render has settled (nextTick / rAF), since only the caller
965
- * knows when its view has painted — hence the allowance for a promise. */
966
- isSatisfied: () => boolean | Promise<boolean>;
967
- /** All history is loaded — nothing left to page in. */
968
- isEndOfList: () => boolean;
969
- /** A history request is already in flight. Waited out, not treated as a stop
970
- * condition: a background first-page refresh (the queue-detect tick fires one
971
- * every couple of seconds while a file is indexing) would otherwise swallow
972
- * the user's scroll-up entirely, and scrolling up again from scrollTop 0
973
- * produces no second event to retry with. */
974
- isLoading: () => boolean;
975
- /** Messages currently loaded. Used to detect a page that added nothing, which
976
- * means the pager is not advancing and looping would never terminate. */
977
- messageCount: () => number;
978
- /** Fetch ONE older page (the caller's own fetchMore path, scroll-restore and
979
- * all). Return `false` when the request was NOT issued (the caller's own
980
- * single-flight guard swallowed it) so the loop retries instead of reading
981
- * the unchanged message count as an exhausted pager. Anything else, including
982
- * undefined, means it was attempted. */
983
- fetchOlder: () => Promise<boolean | void | any>;
984
- /** The chat this fill was started for is gone (project switched, view
985
- * unmounted, gate token bumped). Checked between pages so a stale fill can
986
- * never keep paging another chat's history. */
987
- isStale?: () => boolean;
988
- maxPages?: number;
989
- };
990
- /**
991
- * Page older history until `isSatisfied`, until history runs out, or until the
992
- * pager stops advancing. Never throws: a failed page ends the fill, and the
993
- * user's own scrolling remains the fallback trigger.
994
- */
995
- declare function fillHistoryViewport(opts: FillHistoryViewportOptions): Promise<void>;
996
- /**
997
- * One fill loop per view, with predicates COMBINED rather than dropped.
998
- *
999
- * Fills come from several places at once — a first page finishing, a window
1000
- * resize, a row being collapsed, and the user's own scroll to the top — and a
1001
- * plain "one at a time, drop the rest" guard picks the wrong winner: a resize
1002
- * fill (satisfied the moment the box can scroll at all) would swallow the user's
1003
- * scroll-up (which needs content specifically ABOVE them), and the scroll-up
1004
- * cannot be retried, because a reader parked at scrollTop 0 produces no further
1005
- * scroll event. Dropping the guard entirely is no better: every frame of a
1006
- * window drag would start its own 24-page loop.
1007
- *
1008
- * So a request that arrives mid-loop ANDs its predicate into the running one:
1009
- * the loop then keeps paging until EVERY caller is satisfied. Predicates that
1010
- * come true are dropped as it goes, so the cost stays flat.
1011
- */
1012
- declare function createHistoryFiller(base: Omit<FillHistoryViewportOptions, 'isSatisfied'> & {
1013
- /** Fired when the loop starts FETCHING and when it stops, and only on a real
1014
- * change.
1015
- *
1016
- * This — not the caller's own per-request `isLoading` — is what "older
1017
- * history is still coming in" means to a view. A fill is many pages, and
1018
- * `isLoading` drops to false between every one of them, so anything
1019
- * rendered off it flickers once per page for the whole loop. A collapsed
1020
- * indexing row whose run begins above the loaded window renders exactly
1021
- * that ("still loading this run" vs a status it cannot know yet), which is
1022
- * why the loop has to publish its own span.
1023
- *
1024
- * Fetching, NOT requested. Most fills fetch nothing: they are fired on every
1025
- * window resize, every row a user collapses, and every first-page load, and
1026
- * the overwhelmingly common outcome is `isSatisfied` returning true on the
1027
- * first look. Announcing at request time published a true/false pair for
1028
- * each of those, and the widget's own satisfied-check spans two animation
1029
- * frames — long enough for the browser to PAINT the intermediate state. Every
1030
- * collapsed row strobed through "loading" on every resize tick. So the span
1031
- * opens at the first actual page request, which is also the first moment the
1032
- * claim is true. */
1033
- onRunningChange?: (running: boolean) => void;
1034
- }): {
1035
- fill: (isSatisfied: () => boolean | Promise<boolean>) => Promise<void>;
1036
- isRunning: () => boolean;
1037
- };
1038
-
1039
1089
  declare const MCP_NAME = "BunnyQuery";
1040
1090
  declare const DEFAULT_CLAUDE_MODEL = "claude-sonnet-5";
1041
1091
  declare const DEFAULT_OPENAI_MODEL = "gpt-5.6-luna";
@@ -1160,6 +1210,61 @@ declare function extractOpenAIText(response: any): any;
1160
1210
  declare function listClaudeModels(service: string, owner: string): Promise<any>;
1161
1211
  declare function listOpenAIModels(service: string, owner: string): Promise<any>;
1162
1212
  declare const BG_INDEXING_QUEUE_SUFFIX = "-bg";
1213
+ /**
1214
+ * unique_id of the durable "indexing finished" marker record for a stored file.
1215
+ *
1216
+ * Written by the BACKEND (the polling worker calls the MCP server's
1217
+ * /internal/index-complete at the end of an auto_continue chain whose final
1218
+ * window reported no more content) and, for completions a client knows
1219
+ * deterministically (single-pass settle, client-chain completion token), by the
1220
+ * consumer's mintIndexDoneMarker hook — never by the model.
1221
+ * The marker record carries `reference: "src::<path>"`, so the reindex flow's
1222
+ * delete of the src:: record cascades to it and a re-run starts unmarked.
1223
+ *
1224
+ * Existence semantics: present = the whole file was read to the end. Absent =
1225
+ * unknown (still running, failed partway, indexed before this marker existed,
1226
+ * or a single-pass run minted before the client hook existed) - callers must
1227
+ * fall back to the live-queue probe (fetchLiveIndexingKeys) before reading
1228
+ * absence as anything.
1229
+ */
1230
+ declare function indexDoneUniqueId(storagePath: string): string;
1231
+ /**
1232
+ * unique_id of the per-file indexing RUN record.
1233
+ *
1234
+ * One record per storage path, newest run wins (a reindex's delete-then-repost
1235
+ * of src:: cascade-deletes the old record first, exactly like done::). Minted
1236
+ * status='working' by the client the moment it enqueues a run's FIRST pass, and
1237
+ * closed (done/error/cancelled) by whichever side observes the ending: the
1238
+ * worker via the MCP internal routes for worker-driven chains, the client for
1239
+ * deterministic settles, cancels, and dispatch failures. It exists so chat rows
1240
+ * and files-page badges can answer "which runs exist and how did they end"
1241
+ * from ONE records query instead of scanning bg history.
1242
+ *
1243
+ * A 'working' record is a claim, not proof: a chain that dies without reaching
1244
+ * any error path leaves it dangling, so readers must treat a stale 'working'
1245
+ * (old `started`, no live-queue confirmation) as unknown, never as live.
1246
+ */
1247
+ declare function runIndexUniqueId(storagePath: string): string;
1248
+ type IndexRunStatus = 'working' | 'done' | 'error' | 'cancelled';
1249
+ type IndexRunPatch = {
1250
+ status: IndexRunStatus;
1251
+ filename?: string;
1252
+ started?: number;
1253
+ finished?: number;
1254
+ error?: string;
1255
+ queue?: string;
1256
+ /** Chat that owns this run. A run:: record is keyed by storage path alone,
1257
+ * but a chat is per (project, platform) — without this the Claude chat's
1258
+ * runs surfaced as rows in the same project's ChatGPT chat, where their
1259
+ * passes can never load and the queue probe can never see them. */
1260
+ platform?: 'claude' | 'openai';
1261
+ };
1262
+ /**
1263
+ * Fire-and-forget wrapper over the consumer's upsertIndexRunRecord hook.
1264
+ * Safe everywhere: missing hook, unconfigured engine, and consumer throws all
1265
+ * reduce to a no-op — a run record must never be able to break the run itself.
1266
+ */
1267
+ declare function upsertIndexRunRecordSafe(service: string, storagePath: string, patch: IndexRunPatch): void;
1163
1268
  /**
1164
1269
  * The one place the background-indexing queue name is spelled out. The backend
1165
1270
  * serialises requests sharing a queue name and runs different names in PARALLEL,
@@ -1217,7 +1322,234 @@ declare function getChatHistory(params: {
1217
1322
  platform: 'claude' | 'openai';
1218
1323
  queue?: string;
1219
1324
  status?: 'pending' | 'running' | 'resolved' | 'failed';
1325
+ /** Exact-queue listing: without it the qid range is a PREFIX match, so
1326
+ * queue "u1" also returns "u1-bg" rows. Requires the updated polling
1327
+ * lambda; older backends ignore it (harmless, wider results). */
1328
+ queue_exact?: boolean;
1329
+ /** Label/marker STUBS instead of full bodies (see the polling lambda).
1330
+ * Older backends ignore it and return full items. */
1331
+ compact?: boolean;
1332
+ /** Drop one queue's rows from an id-prefix listing — how the surface
1333
+ * chat is fetched WITHOUT the bg-indexing queue while legacy items on
1334
+ * odd queue names survive. Older backends ignore it. */
1335
+ queue_exclude?: string;
1220
1336
  }, fetchOptions: Record<string, any>): Promise<any>;
1337
+ /** Full server-side id of one history item, for a csr-poll POINT LOOKUP (the
1338
+ * single-item path returns the item WITH bodies — how an expanded row fetches
1339
+ * the passes a compact listing stubbed out). Mirrors the id the SDK builds:
1340
+ * `[METHOD]url#service:` + the item's own `stamp:entropy` id. */
1341
+ declare function buildHistoryItemFullId(platform: 'claude' | 'openai', service: string, itemId: string): string;
1342
+
1343
+ /**
1344
+ * History mapping (pure). Moved verbatim from the chatbox. The clear-horizon
1345
+ * timestamp and the "Indexing: …" display label are INJECTED (clearedAt param,
1346
+ * formatIndexingLabel callback) so the engine touches neither localStorage nor
1347
+ * view-specific display formatting. projectId is passed for link sanitization.
1348
+ */
1349
+
1350
+ declare function filterListByClearHorizon(list: any[], clearedAt: number): any[];
1351
+ declare function normalizeTextContent(content: any): string;
1352
+ declare function extractLastUserTextFromRequest(requestBody: any): string;
1353
+ /** The two openings an indexing prompt can have. A bg-queue item that starts with
1354
+ * neither is an ordinary chat that happened to be routed onto that queue. */
1355
+ declare function isIndexingRequestText(userText: any): boolean;
1356
+ type IndexingRequestRef = {
1357
+ name: string;
1358
+ path?: string;
1359
+ mime?: string;
1360
+ size?: number;
1361
+ /** A CONTINUE pass rather than the run's first. */
1362
+ continued: boolean;
1363
+ };
1364
+ /**
1365
+ * The file an indexing prompt is about, read back out of the prompt itself.
1366
+ *
1367
+ * The prompt is the only description of the pass that survives on the server, so
1368
+ * this is how BOTH a history rebuild and a worker-minted pass the client never
1369
+ * dispatched (ChatSession._adoptWorkerIndexingPasses) recover the file. Shared so
1370
+ * the two produce the same `_indexFile`, which is what makes them group together.
1371
+ */
1372
+ declare function parseIndexingRequestText(userText: any): IndexingRequestRef | null;
1373
+ /**
1374
+ * One bounded look at the background-indexing queue: which files still have a
1375
+ * pass pending or running? This is the same negative signal ChatSession's
1376
+ * display layer relies on - for a worker-driven (auto_continue) run, only the
1377
+ * queue can say the run is over, because the worker enqueues continuation
1378
+ * passes the client never dispatched.
1379
+ *
1380
+ * Returns every storage path AND file name found on live passes (both, because
1381
+ * older prompts may lack the storage-path line), plus `checked`: false when a
1382
+ * page came back full, in which case absence from `keys` proves nothing and
1383
+ * the caller must keep whatever state it already had.
1384
+ *
1385
+ * SCOPE: the probed queue is "<userId>-bg" - THIS user's dispatches only. A
1386
+ * chain launched by another collaborator or a widget end-user lives on their
1387
+ * queue and is invisible here, so "idle" must never be read as "nobody is
1388
+ * indexing this file", only as "this user's runs are over". The durable done::
1389
+ * marker (indexDoneUniqueId) is the cross-user signal.
1390
+ */
1391
+ declare function fetchLiveIndexingKeys(params: {
1392
+ service: string;
1393
+ owner: string;
1394
+ platform: 'claude' | 'openai';
1395
+ /** Same value the dispatch used - see bgIndexingQueueName. */
1396
+ userId?: string;
1397
+ }): Promise<{
1398
+ keys: Set<string>;
1399
+ checked: boolean;
1400
+ at: number;
1401
+ }>;
1402
+ /** Test hook: drop split-fetch state (all keys, or one). */
1403
+ declare function __resetSplitHistoryState(key?: string): void;
1404
+ type SplitHistoryResult = {
1405
+ list: any[];
1406
+ endOfList: boolean;
1407
+ startKeyHistory: any[];
1408
+ /** True when this chat had never been walked in this session — the first
1409
+ * paint. Consumers gate the "Loading indexing history" hint on it: a
1410
+ * mid-walk tab return restarts the walk for cursor safety but must stay
1411
+ * silent (flashing the hint on every return was the reported bug). */
1412
+ firstLoad?: boolean;
1413
+ /** Present only when `deferBg` was requested AND bg work remains: resolves
1414
+ * with the stub batch fetched in the background (the per-key lock is held
1415
+ * until it settles, so no other history call can interleave). The caller
1416
+ * merges the batch by timestamp — the same path older pages use. */
1417
+ bgPending?: Promise<{
1418
+ list: any[];
1419
+ endOfList: boolean;
1420
+ }>;
1421
+ };
1422
+ declare function getSplitChatHistory(params: {
1423
+ service: string;
1424
+ owner: string;
1425
+ platform: 'claude' | 'openai';
1426
+ userId?: string;
1427
+ }, fetchOptions: Record<string, any>,
1428
+ /** Test seam: replaces getChatHistory. Not for production callers. */
1429
+ _fetchImpl?: typeof getChatHistory): Promise<SplitHistoryResult>;
1430
+ type MapHistoryOptions = {
1431
+ clearedAt: number;
1432
+ projectId: string;
1433
+ /** View-side display formatter for "Indexing:/Reindexing: …" bubbles. */
1434
+ formatIndexingLabel: (name: string, mime?: string, size?: number | null, storagePath?: string, reindex?: boolean, continued?: boolean) => string;
1435
+ };
1436
+ declare function mapHistoryListToMessages(list: any[], platform: 'claude' | 'openai', opts: MapHistoryOptions): {
1437
+ messages: any[];
1438
+ runningItemIds: string[];
1439
+ };
1440
+
1441
+ /**
1442
+ * Keep older history REACHABLE by paging until the message box actually gains
1443
+ * something to scroll to.
1444
+ *
1445
+ * Older history is paged in by one trigger only: the user scrolling to the top
1446
+ * of the message box. That trigger has two ways to die, and collapsed indexing
1447
+ * rows cause both:
1448
+ *
1449
+ * 1. The box never scrolls. A file's every indexing pass (the first plus every
1450
+ * CONTINUE pass, request AND response bubble each) folds into ONE row, so a
1451
+ * full history page — twenty-plus messages — can render as a single line.
1452
+ * Content shorter than the viewport fires no scroll event, so page 2 is
1453
+ * never requested and any conversation the user had before that upload is
1454
+ * permanently out of reach.
1455
+ * 2. The fetched page adds no height. A page that is entirely the same file's
1456
+ * earlier passes joins the collapsed row already on screen and renders
1457
+ * nothing new. The user, sitting at scrollTop 0, scrolls up again — and
1458
+ * because the position never changed, no further scroll event fires.
1459
+ *
1460
+ * Both are the same shape: fetch, re-measure, and keep going until the user
1461
+ * genuinely gained reachable content, history ran out, or the pager stopped
1462
+ * advancing. `isSatisfied` is what differs between the two (can the box scroll
1463
+ * at all / did it grow), so the loop below takes it as a predicate.
1464
+ *
1465
+ * DOM-free like the rest of the engine — the caller supplies the measurement and
1466
+ * awaits its own render before measuring, so agent.vue and the widget run the
1467
+ * identical loop over their own pagers.
1468
+ */
1469
+ /** Overflow (px) that counts as "the user can scroll here". Comfortably more
1470
+ * than the 60px top threshold that triggers the next page, so a filled box has
1471
+ * real room to scroll rather than sitting one pixel from the trigger. */
1472
+ declare const HISTORY_FILL_SLACK_PX = 64;
1473
+ /** Pages one fill pass will request before giving up. Reached only by a chat
1474
+ * whose history really is dozens of pages of one file's indexing passes; the
1475
+ * cap exists so a pager that stops advancing can never spin forever. */
1476
+ declare const MAX_HISTORY_FILL_PAGES = 24;
1477
+ type FillHistoryViewportOptions = {
1478
+ /** The user has reachable content and paging can stop. Called AFTER the
1479
+ * caller's own render has settled (nextTick / rAF), since only the caller
1480
+ * knows when its view has painted — hence the allowance for a promise. */
1481
+ isSatisfied: () => boolean | Promise<boolean>;
1482
+ /** All history is loaded — nothing left to page in. */
1483
+ isEndOfList: () => boolean;
1484
+ /** A history request is already in flight. Waited out, not treated as a stop
1485
+ * condition: a background first-page refresh (the queue-detect tick fires one
1486
+ * every couple of seconds while a file is indexing) would otherwise swallow
1487
+ * the user's scroll-up entirely, and scrolling up again from scrollTop 0
1488
+ * produces no second event to retry with. */
1489
+ isLoading: () => boolean;
1490
+ /** Messages currently loaded. Used to detect a page that added nothing, which
1491
+ * means the pager is not advancing and looping would never terminate. */
1492
+ messageCount: () => number;
1493
+ /** Fetch ONE older page (the caller's own fetchMore path, scroll-restore and
1494
+ * all). Return `false` when the request was NOT issued (the caller's own
1495
+ * single-flight guard swallowed it) so the loop retries instead of reading
1496
+ * the unchanged message count as an exhausted pager. Anything else, including
1497
+ * undefined, means it was attempted. */
1498
+ fetchOlder: () => Promise<boolean | void | any>;
1499
+ /** The chat this fill was started for is gone (project switched, view
1500
+ * unmounted, gate token bumped). Checked between pages so a stale fill can
1501
+ * never keep paging another chat's history. */
1502
+ isStale?: () => boolean;
1503
+ maxPages?: number;
1504
+ };
1505
+ /**
1506
+ * Page older history until `isSatisfied`, until history runs out, or until the
1507
+ * pager stops advancing. Never throws: a failed page ends the fill, and the
1508
+ * user's own scrolling remains the fallback trigger.
1509
+ */
1510
+ declare function fillHistoryViewport(opts: FillHistoryViewportOptions): Promise<void>;
1511
+ /**
1512
+ * One fill loop per view, with predicates COMBINED rather than dropped.
1513
+ *
1514
+ * Fills come from several places at once — a first page finishing, a window
1515
+ * resize, a row being collapsed, and the user's own scroll to the top — and a
1516
+ * plain "one at a time, drop the rest" guard picks the wrong winner: a resize
1517
+ * fill (satisfied the moment the box can scroll at all) would swallow the user's
1518
+ * scroll-up (which needs content specifically ABOVE them), and the scroll-up
1519
+ * cannot be retried, because a reader parked at scrollTop 0 produces no further
1520
+ * scroll event. Dropping the guard entirely is no better: every frame of a
1521
+ * window drag would start its own 24-page loop.
1522
+ *
1523
+ * So a request that arrives mid-loop ANDs its predicate into the running one:
1524
+ * the loop then keeps paging until EVERY caller is satisfied. Predicates that
1525
+ * come true are dropped as it goes, so the cost stays flat.
1526
+ */
1527
+ declare function createHistoryFiller(base: Omit<FillHistoryViewportOptions, 'isSatisfied'> & {
1528
+ /** Fired when the loop starts FETCHING and when it stops, and only on a real
1529
+ * change.
1530
+ *
1531
+ * This — not the caller's own per-request `isLoading` — is what "older
1532
+ * history is still coming in" means to a view. A fill is many pages, and
1533
+ * `isLoading` drops to false between every one of them, so anything
1534
+ * rendered off it flickers once per page for the whole loop. A collapsed
1535
+ * indexing row whose run begins above the loaded window renders exactly
1536
+ * that ("still loading this run" vs a status it cannot know yet), which is
1537
+ * why the loop has to publish its own span.
1538
+ *
1539
+ * Fetching, NOT requested. Most fills fetch nothing: they are fired on every
1540
+ * window resize, every row a user collapses, and every first-page load, and
1541
+ * the overwhelmingly common outcome is `isSatisfied` returning true on the
1542
+ * first look. Announcing at request time published a true/false pair for
1543
+ * each of those, and the widget's own satisfied-check spans two animation
1544
+ * frames — long enough for the browser to PAINT the intermediate state. Every
1545
+ * collapsed row strobed through "loading" on every resize tick. So the span
1546
+ * opens at the first actual page request, which is also the first moment the
1547
+ * claim is true. */
1548
+ onRunningChange?: (running: boolean) => void;
1549
+ }): {
1550
+ fill: (isSatisfied: () => boolean | Promise<boolean>) => Promise<void>;
1551
+ isRunning: () => boolean;
1552
+ };
1221
1553
 
1222
1554
  /**
1223
1555
  * ChatSession host adapter + state types.
@@ -1314,6 +1646,10 @@ interface ChatMessage {
1314
1646
  * which is how an 88-page file once "finished" at page 15. */
1315
1647
  _indexComplete?: boolean;
1316
1648
  _useBgQueue?: boolean;
1649
+ /** Mapped from an item delivered by the bg chain of the split history fetch
1650
+ * (stubs or deferred chats). Surface-frontier logic (retention boundary,
1651
+ * clear-horizon) skips these — their ids reach arbitrarily deep. */
1652
+ _fromBgChain?: boolean;
1317
1653
  /** Local id of a turn STAGED at Send time while its attachments upload. The
1318
1654
  * bubble exists before any server request does, so it is never matched by
1319
1655
  * _serverItemId and is never promoted/cancelled by the queue machinery —
@@ -1353,6 +1689,9 @@ interface ChatState {
1353
1689
  typingAbort: boolean;
1354
1690
  loadingHistory: boolean;
1355
1691
  loadingOlderHistory: boolean;
1692
+ /** A deferred bg stub batch (first-paint split fetch) is still in flight;
1693
+ * views show a small 'loading indexing history' hint while true. */
1694
+ bgHistoryLoading: boolean;
1356
1695
  historyEndOfList: boolean;
1357
1696
  historyStartKeyHistory: string[];
1358
1697
  historyRequestToken: number;
@@ -1656,6 +1995,15 @@ type IndexingGroup = {
1656
1995
  * loaded ones. 'status': the queue has not yet said whether this file is still
1657
1996
  * being worked on, which is the only thing that can end a worker-driven run. */
1658
1997
  resolvingReason?: 'history' | 'status';
1998
+ /** Synthesized from a durable run:: record: none of the run's passes are
1999
+ * among the loaded messages (bg history still deferred, or the run is older
2000
+ * than the paging cap). Header-and-status only — members/visibleMembers are
2001
+ * empty and there is nothing to cancel; the row is replaced by the real
2002
+ * group the moment actual passes load (same `key`, so expansion state
2003
+ * carries over). */
2004
+ stub?: boolean;
2005
+ /** The run:: record's stored error text, for a stub row's meta line. */
2006
+ stubError?: string;
1659
2007
  };
1660
2008
  type DisplayEntry = {
1661
2009
  kind: 'message';
@@ -1686,6 +2034,16 @@ type BuildDisplayListOptions = {
1686
2034
  /** Whether `liveIndexKeys` has been answered at least once for this chat. False
1687
2035
  * is "we do not know", and a worker-driven run stays unfinished on it. */
1688
2036
  liveIndexChecked?: boolean;
2037
+ /** Files carrying the durable done:: completion marker (one prefix sweep),
2038
+ * keyed like IndexingGroup.key (storage path; the bare-name fallback keys
2039
+ * of very old prompts simply never match — they keep the queue inference).
2040
+ * A marker is PROOF the file was read to the end: it settles a worker-run
2041
+ * green without waiting for the queue answer, and it is never withheld by
2042
+ * the resolving logic. A live queue hit still outranks it (a re-index in
2043
+ * flight whose marker-cascade delete lagged). */
2044
+ doneKeys?: {
2045
+ [fileKey: string]: boolean;
2046
+ };
1689
2047
  /** Server item ids of passes that were on a row when the user STOPPED it
1690
2048
  * (ChatSession.state.stoppedIndexIds). A run holding any of them is a run the
1691
2049
  * user stopped — see the status derivation for why a stop usually leaves no
@@ -1698,7 +2056,51 @@ type BuildDisplayListOptions = {
1698
2056
  * windowedIndexing). Passed in rather than read from config so this stays a
1699
2057
  * pure function of its inputs and can be exercised for both settings. */
1700
2058
  windowedIndexing?: boolean;
2059
+ /** Durable run:: records, keyed by STORAGE PATH (the consumer's marker
2060
+ * sweep). Each key with no real group in the loaded messages gets a
2061
+ * synthesized header-only row (see IndexingGroup.stub), placed by its
2062
+ * `started` timestamp. A real group for the same file — matched by key,
2063
+ * path, or name — always suppresses the stub: loaded passes are evidence,
2064
+ * the record is only a summary. */
2065
+ runStubs?: {
2066
+ [storagePath: string]: RunStubInfo;
2067
+ };
2068
+ /** The platform whose chat this list is for. run:: records are per-FILE,
2069
+ * but a chat is per (project, platform): a run started under Claude has
2070
+ * its passes in the Claude conversation and is invisible to the
2071
+ * OpenAI-scoped queue probe, so its stub could never be covered and never
2072
+ * be confirmed — it just sat there, in a chat it did not belong to.
2073
+ * Records minted before this was stamped carry no platform and are shown
2074
+ * in both, which keeps the leak to the historical set. */
2075
+ stubPlatform?: 'claude' | 'openai';
2076
+ /** The chat's clear-history horizon (ms epoch). Run records are service-
2077
+ * wide and know nothing about a cleared chat, so without this every
2078
+ * "Clear chat history" resurrects one row per indexed file. A stub whose
2079
+ * run ended (or, unfinished, began) at or before this moment is dropped —
2080
+ * unless the queue says the file is live RIGHT NOW, which no horizon can
2081
+ * make untrue. */
2082
+ stubClearedAt?: number;
2083
+ /** Clock injection for tests; defaults to Date.now(). Only run-stub
2084
+ * staleness reads it. */
2085
+ now?: number;
2086
+ };
2087
+ /** The display-relevant fields of a run:: record (see requests.ts
2088
+ * runIndexUniqueId for the record's contract). */
2089
+ type RunStubInfo = {
2090
+ status: 'working' | 'done' | 'error' | 'cancelled';
2091
+ filename?: string;
2092
+ started?: number;
2093
+ finished?: number;
2094
+ error?: string;
2095
+ /** Chat that owns this run. Absent on records minted before it was
2096
+ * stamped; see BuildDisplayListOptions.stubPlatform. */
2097
+ platform?: 'claude' | 'openai';
1701
2098
  };
2099
+ /** A 'working' run record older than this with no live-queue confirmation is
2100
+ * treated as unknown rather than live: a chain that died without reaching any
2101
+ * error path leaves 'working' dangling, and a row must not spin forever on a
2102
+ * claim nothing can end. */
2103
+ declare const RUN_RECORD_WORKING_STALE_MS: number;
1702
2104
  declare function parseIndexingLabel(content: string): {
1703
2105
  name: string;
1704
2106
  path?: string;
@@ -2004,6 +2406,17 @@ declare class ChatSession {
2004
2406
  resumePolling(reason: string): Promise<void>;
2005
2407
  private _newLocalId;
2006
2408
  getHistoryCacheKey(): string;
2409
+ private _hydratedBodies;
2410
+ private _hydratingItems;
2411
+ /** Re-apply memoized hydrated texts onto freshly-mapped messages. Both
2412
+ * clients call this right after their mapper runs (loadHistory does it
2413
+ * internally); it mutates the given array's items in place. */
2414
+ applyHydratedBodies(messages: ChatMessage[]): void;
2415
+ /** Fetch the real response bodies for compact history stubs (one csr-poll
2416
+ * point lookup per item id), memoize, and swap them into the live list.
2417
+ * Best-effort: a failed lookup leaves the stub (its head + fallback line
2418
+ * still render) and a later expand retries. */
2419
+ hydrateCompactItems(itemIds: string[]): Promise<void>;
2007
2420
  updateHistoryCache(): void;
2008
2421
  /**
2009
2422
  * Land a resolved reply in the history cache of a chat that is NOT currently
@@ -2274,6 +2687,10 @@ declare class ChatSession {
2274
2687
  */
2275
2688
  private _adoptingWorkerPasses;
2276
2689
  private _adoptWorkerIndexingPasses;
2690
+ /** Anything at all suggesting THIS project's indexing may be live: a queued
2691
+ * local entry, a recorded live key (the adopt look just wrote them), or an
2692
+ * attached poll. Gates the passive adopt ladder's climb. */
2693
+ private _hasLiveIndexEvidence;
2277
2694
  /** Any of these ids still queued or still polled, i.e. surviving work. */
2278
2695
  private _isTrackingAny;
2279
2696
  /** One live bg-queue item -> a BgTaskEntry, if it is an indexing pass this
@@ -2285,6 +2702,28 @@ declare class ChatSession {
2285
2702
  * cancelQueuedMessage, which drives one, has nothing to act on). */
2286
2703
  private _cancelServerItem;
2287
2704
  drainBgTaskQueue(): void;
2705
+ /** Fire the consumer's done::-marker hook for a run whose completion this
2706
+ * client knows DETERMINISTICALLY (see the two call sites in
2707
+ * maybeResumeIndexing). Best-effort by contract; identity-checked so a
2708
+ * project switch mid-settle cannot stamp the wrong service. */
2709
+ _mintDoneMarker(entry: BgTaskEntry): void;
2710
+ /** Short, storable form of an error body for the run:: record. */
2711
+ _runErrorText(response: any): string;
2712
+ /** Close the records of a run whose pass settled OFF-POLL — the answer came
2713
+ * back as history (hidden tab, dead poll, resume refetch), so none of the
2714
+ * poll-side settle handlers ran. Only for SINGLE-PASS files, where one
2715
+ * settled pass is deterministically the whole run (the same contract as
2716
+ * maybeResumeIndexing's single-pass branch); paged files stay with their
2717
+ * drivers. Outcome is read from the settled bubbles' own flags, which is
2718
+ * all the history mapping left us. Best-effort and idempotent throughout. */
2719
+ _flipRunFromSettledEntry(entry: BgTaskEntry): void;
2720
+ /** Close the durable run:: record for an ending THIS client observed.
2721
+ * service comes from the ENTRY, not the current identity: unlike the done::
2722
+ * mint above, a status flip must land even if the user switched projects
2723
+ * mid-settle — otherwise the record lies 'working' forever. Best-effort
2724
+ * through upsertIndexRunRecordSafe; the consumer's precedence guard keeps
2725
+ * repeats and races harmless. */
2726
+ _flipRunRecord(entry: BgTaskEntry, status: IndexRunStatus, error?: string): void;
2288
2727
  maybeResumeIndexing(entry: BgTaskEntry, response: any, platform: string): void;
2289
2728
  loadHistory(fetchMore?: boolean, token?: number): Promise<void>;
2290
2729
  uploadSingleAttachment(att: any, stageId?: string): Promise<Array<{
@@ -2301,4 +2740,4 @@ declare class ChatSession {
2301
2740
  bumpGate(): void;
2302
2741
  }
2303
2742
 
2304
- 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, 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, INLINE_LINK_GLYPH, INLINE_LINK_UNAVAILABLE_GLYPH, INLINE_LINK_UNAVAILABLE_SUFFIX, type ImagePreviewContext, 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, MAX_CONCURRENT_BG_POLLS, 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, PREVIEWABLE_IMAGE_CONTENT_TYPES, PREVIEW_BROWSER_CACHE_SECONDS, type ParsedAiAgent, type PinnedDispatchContext, type PreviewImageEl, RENDER_FROM_TOKEN, RTF_EXTS, type RenderableInlineLink, TOOL_AND_RESPONSE_BUFFER, type VisionProfile, XML_EXTS, applyEncodingDeclaration, bgIndexingQueueName, buildAiAgentValue, buildBoundedChatMessages, buildChatDisplayList, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, classifyInlineLink, clearAttachmentParsers, clearImagePreviewCache, composeUserMessage, configureChatEngine, contentTypeForExt, createHistoryFiller, createInlineLinkRegex, encodePathSegments, encodingClassForExt, ensureHtmlCharset, ensureXmlEncoding, escapeInlineHtml, escapeRtfNonAscii, estimateMessageTokens, estimateTextTokens, extOf, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, fillHistoryViewport, filterListByClearHorizon, findAttachmentParser, formatChatTimestamp, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, getProjectContextWindow, getVisionProfile, groupAttachmentFailures, hasBom, hydrateImagePreviews, isAuthExpiredError, isBgIndexingQueue, isErrorResponseBody, isHttpUrlLike, isIndexingRequestText, isLinkUnavailable, isNonRetryableRequestError, isOfficeFile, isPreviewableImagePath, isServerExtractable, isServiceDbAttachmentHref, linkUnavailableKeyForHref, linkUnavailableKeyForPath, listClaudeModels, listOpenAIModels, looksLikeRtf, makeExtractPlaceholder, mapHistoryListToMessages, markImagePreviewStale, needsBomForExt, normalizeAttachmentPathCandidate, normalizeExt, normalizeTextContent, normalizeTrailingInlineToken, notifyAgentSaveAttachment, parseAiAgentValue, parseAttachmentContent, parseIndexingLabel, parseIndexingRequestText, peekImagePreviewUrl, prepareDownloadText, previewImageContentType, previewableExtOf, readExpiredAttachmentHref, registerAttachmentParser, registerModelContextWindows, renderInlineLinkHtml, repairUrlEntities, repairUrlWhitespace, resolveImagePreviewUrl, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, setProjectContextWindow, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay, wallClockNow };
2743
+ 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_CONTEXT_WINDOW, DEFAULT_OPENAI_MODEL, 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, 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, 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, OUTPUT_TOKEN_RESERVE, type OpenAIMessage, POLL_INTERVAL, PRESIGN_SAFETY_MARGIN_MS, PREVIEWABLE_IMAGE_CONTENT_TYPES, PREVIEW_BROWSER_CACHE_SECONDS, PREVIEW_URL_EXPIRES_SECONDS, type ParsedAiAgent, type PinnedDispatchContext, type PreviewImageEl, RENDER_FROM_TOKEN, RTF_EXTS, RUN_RECORD_WORKING_STALE_MS, type RenderableInlineLink, type RunStubInfo, TOOL_AND_RESPONSE_BUFFER, type VisionProfile, XML_EXTS, __resetSplitHistoryState, applyEncodingDeclaration, bgIndexingQueueName, buildAiAgentValue, buildBoundedChatMessages, buildChatDisplayList, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildHistoryItemFullId, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, classifyInlineLink, clearAttachmentParsers, clearImagePreviewCache, composeUserMessage, configureChatEngine, contentTypeForExt, createHistoryFiller, createInlineLinkRegex, encodePathSegments, encodingClassForExt, ensureHtmlCharset, ensureXmlEncoding, escapeInlineHtml, escapeRtfNonAscii, estimateMessageTokens, estimateTextTokens, extOf, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, fetchLiveIndexingKeys, fillHistoryViewport, filterListByClearHorizon, findAttachmentParser, formatChatTimestamp, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, getInputTokenBudget, getMaxOutputTokens, getModelContextWindow, getProjectContextWindow, getSplitChatHistory, getVisionProfile, groupAttachmentFailures, hasBom, hydrateImagePreviews, indexDoneUniqueId, isAuthExpiredError, isBgIndexingQueue, isErrorResponseBody, isHttpUrlLike, isIndexingRequestText, isLinkUnavailable, isNonRetryableRequestError, isOfficeFile, isPreviewableImagePath, isProviderApiKeyError, isServerExtractable, isServiceDbAttachmentHref, linkUnavailableKeyForHref, linkUnavailableKeyForPath, linkUnavailableKeysForPath, listClaudeModels, listOpenAIModels, looksLikeRtf, makeExtractPlaceholder, mapHistoryListToMessages, markImagePreviewStale, mintCacheBustStamp, needsBomForExt, normalizeAttachmentPathCandidate, normalizeExt, normalizeTextContent, normalizeTrailingInlineToken, notifyAgentSaveAttachment, parseAiAgentValue, parseAttachmentContent, parseIndexingLabel, parseIndexingRequestText, peekImagePreviewUrl, prepareDownloadText, presignExpiryEpochMs, previewImageContentType, previewMintCacheToken, previewableExtOf, readExpiredAttachmentHref, registerAttachmentParser, registerModelContextWindows, renderInlineLinkHtml, repairUrlEntities, repairUrlWhitespace, resolveImagePreviewUrl, runIndexUniqueId, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, setProjectContextWindow, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay, upsertIndexRunRecordSafe, wallClockNow };