@openviking/openclaw-plugin 2026.9.1 → 2026.9.4

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/README.md CHANGED
@@ -164,16 +164,9 @@ During recall, the plugin:
164
164
  1. Extracts query text from the latest user message.
165
165
  2. Resolves the agent routing for the current `sessionId/sessionKey`.
166
166
  3. Runs a quick availability precheck so model requests do not stall when OpenViking is unavailable.
167
- 4. Queries the configured `recallTargetTypes` (`user,agent` by default; optionally `resource`; use `ov_archive_search` and `ov_archive_expand` for session history).
168
- 5. Deduplicates, threshold-filters, reranks, and trims the results under a token budget.
169
- 6. Prepends the selected memories as a `## Long-term Memories` section inside `<openviking-context>` to the current user message; it does not append a standalone synthetic user message.
170
-
171
- The reranking logic is not pure vector-score sorting. The current implementation also considers:
172
-
173
- - whether a result is a leaf memory with `level == 2`
174
- - whether it looks like a preference memory
175
- - whether it looks like an event memory
176
- - lexical overlap with the current query
167
+ 4. Sends one session-aware context search for the configured `recallTargetTypes` (`user,agent` by default; optionally `resource`; use `ov_archive_search` and `ov_archive_expand` to inspect raw session history).
168
+ 5. Lets OpenViking expand the query from session history, filter and rank candidates, apply cross-turn deduplication, select detail tiers, and assemble the result under the injection budget.
169
+ 6. Prepends the server-rendered context inside `<relevant-memories>` to the current user message; it does not append a standalone synthetic user message.
177
170
 
178
171
  ## Session Lifecycle
179
172
 
package/auto-recall.ts CHANGED
@@ -1,12 +1,7 @@
1
- import type { FindResult, FindResultItem, OpenVikingClient } from "./client.js";
1
+ import type { FindResultItem, OpenVikingClient, SearchContextEntry } from "./client.js";
2
2
  import type { MemoryOpenVikingConfig } from "./config.js";
3
3
  import type { EffectiveQueryConfig } from "./query-config.js";
4
- import {
5
- pickMemoriesForInjection,
6
- postProcessMemories,
7
- summarizeInjectionMemories,
8
- toJsonLog,
9
- } from "./memory-ranking.js";
4
+ import { toJsonLog } from "./memory-ranking.js";
10
5
  import { quickRecallPrecheck, withTimeout } from "./process-manager.js";
11
6
  import {
12
7
  resolveRecallSearchPlan,
@@ -20,6 +15,8 @@ import { sanitizeUserTextForCapture } from "./text-utils.js";
20
15
  import { estimateTextTokens } from "./token-estimator.js";
21
16
 
22
17
  const RECALL_QUERY_MAX_CHARS = 4_000;
18
+ const AUTO_RECALL_DEDUP_TURNS = 5;
19
+ const LEGACY_CHARS_PER_TOKEN = 4;
23
20
  export const AUTO_RECALL_SOURCE_MARKER = "Source: openviking-auto-recall";
24
21
 
25
22
  type Logger = {
@@ -231,16 +228,26 @@ function preview(value: string | undefined, maxChars: number): string | undefine
231
228
  return trimmed.length > maxChars ? trimmed.slice(0, maxChars) : trimmed;
232
229
  }
233
230
 
234
- function toTraceResults(items: FindResultItem[], resourceType: RecallResourceType): RecallTraceResult[] {
235
- return items.map((item) => ({
236
- uri: item.uri,
231
+ function traceResourceType(entry: SearchContextEntry): RecallResourceType {
232
+ if (entry.category === "resources" || entry.uri.startsWith("viking://resources")) {
233
+ return "resource";
234
+ }
235
+ if (entry.origin === "actor_peer") {
236
+ return "agent";
237
+ }
238
+ return "user";
239
+ }
240
+
241
+ function toTraceResult(entry: SearchContextEntry): RecallTraceResult {
242
+ const resourceType = traceResourceType(entry);
243
+ return {
244
+ uri: entry.uri,
237
245
  resourceType,
238
- category: item.category,
239
- score: item.score,
240
- level: item.level,
241
- abstractPreview: preview(item.abstract ?? item.overview, 240),
246
+ category: entry.category,
247
+ score: entry.score,
248
+ abstractPreview: preview(entry.text, 240),
242
249
  resultType: resourceType === "resource" ? "resource" : "memory",
243
- }));
250
+ };
244
251
  }
245
252
 
246
253
  function boundTraceQuery(query: string, maxChars: number): { query: string; queryTruncated?: boolean } {
@@ -340,7 +347,6 @@ export async function buildAutoRecallContext(params: {
340
347
 
341
348
  return withTimeout(
342
349
  (async () => {
343
- const candidateLimit = queryConfig?.candidateLimit ?? Math.max(cfg.recallLimit * 4, 20);
344
350
  const scoreThreshold = queryConfig?.scoreThreshold ?? cfg.recallScoreThreshold;
345
351
  const recallLimit = queryConfig?.recallLimit ?? cfg.recallLimit;
346
352
  const maxInjectedChars = queryConfig?.maxInjectedChars ?? cfg.recallMaxInjectedChars;
@@ -349,98 +355,88 @@ export async function buildAutoRecallContext(params: {
349
355
  ovSessionId: params.ovSessionId,
350
356
  agentId,
351
357
  });
352
- const traceSearches: RecallTraceEntry["searches"] = searchPlan.skipped.map((skipped) => ({
353
- resourceType: skipped.resourceType,
354
- limit: candidateLimit,
355
- scoreThreshold: 0,
356
- durationMs: 0,
357
- total: 0,
358
- results: [],
359
- error: skipped.reason,
360
- }));
361
- const autoRecallPromises: Promise<{
362
- resourceType: RecallResourceType;
363
- targetUri?: string;
364
- result?: FindResult;
365
- durationMs: number;
366
- }>[] = searchPlan.searches.map(async (search) => {
367
- const start = Date.now();
368
- const result = await client.find(queryText, {
369
- targetUri: search.targetUri,
370
- limit: candidateLimit,
371
- scoreThreshold: 0,
372
- contextType: search.contextType,
358
+ const contextTypes = [...new Set(searchPlan.searches.map((search) => search.contextType))];
359
+ const maxTokens = Math.min(
360
+ 32_000,
361
+ Math.max(64, Math.round(maxInjectedChars / LEGACY_CHARS_PER_TOKEN)),
362
+ );
363
+ const startedAt = Date.now();
364
+ let contextResult: Awaited<ReturnType<OpenVikingClient["searchContext"]>> | undefined;
365
+ let requestError: unknown;
366
+
367
+ try {
368
+ contextResult = await client.searchContext(queryText, {
369
+ sessionId: params.ovSessionId,
370
+ limit: recallLimit,
371
+ scoreThreshold,
372
+ contextType: contextTypes.length === 1 ? contextTypes[0] : contextTypes,
373
+ queryExpansion: "auto",
374
+ maxTokens,
375
+ detail: recallPreferAbstract ? "abstract" : undefined,
376
+ dedupTurns: params.ovSessionId ? AUTO_RECALL_DEDUP_TURNS : undefined,
377
+ peerScope: "actor",
373
378
  actorPeerId,
379
+ requestTimeoutMs: cfg.autoRecallTimeoutMs,
374
380
  });
375
- return {
376
- resourceType: search.resourceType,
377
- targetUri: search.targetUri,
378
- result,
379
- durationMs: Date.now() - start,
380
- };
381
- });
382
- const autoRecallSettled = await Promise.allSettled(autoRecallPromises);
383
-
384
- const allMemories: FindResultItem[] = [];
385
- for (const s of autoRecallSettled) {
386
- if (s.status === "fulfilled") {
387
- const result = s.value.result ?? {};
388
- const memories = result.memories ?? [];
389
- const resources = result.resources ?? [];
390
- allMemories.push(...memories, ...resources);
391
- traceSearches.push({
392
- resourceType: s.value.resourceType,
393
- targetUriInput: s.value.targetUri,
394
- limit: candidateLimit,
395
- scoreThreshold: 0,
396
- durationMs: s.value.durationMs,
397
- total: result.total ?? memories.length + resources.length + (result.skills?.length ?? 0),
398
- results: [
399
- ...toTraceResults(memories, s.value.resourceType),
400
- ...toTraceResults(resources, "resource"),
401
- ...(result.skills ?? []).map((item): RecallTraceResult => ({
402
- uri: item.uri,
403
- resourceType: s.value.resourceType,
404
- category: item.category,
405
- score: item.score,
406
- level: item.level,
407
- abstractPreview: preview(item.abstract ?? item.overview, 240),
408
- resultType: "skill",
409
- })),
410
- ].slice(0, cfg.traceRecallMaxResultsPerSearch),
411
- });
412
- } else {
413
- logger.warn?.(`openviking: auto-recall search failed: ${String(s.reason)}`);
414
- const failedIndex = traceSearches.length;
415
- const search = searchPlan.searches[failedIndex - searchPlan.skipped.length];
416
- traceSearches.push({
417
- resourceType: search?.resourceType ?? "user",
418
- targetUriInput: search?.targetUri,
419
- limit: candidateLimit,
420
- scoreThreshold: 0,
421
- durationMs: 0,
422
- total: 0,
423
- results: [],
424
- error: String(s.reason),
425
- });
426
- }
381
+ } catch (error) {
382
+ requestError = error;
427
383
  }
428
384
 
429
- const uniqueMemories = allMemories.filter((memory, index, self) =>
430
- index === self.findIndex((m) => m.uri === memory.uri)
431
- );
432
- const leafOnly = uniqueMemories.filter((m) => !m.level || m.level === 2);
433
- const processed = postProcessMemories(leafOnly, {
434
- limit: candidateLimit,
435
- scoreThreshold,
436
- });
437
- const memories = pickMemoriesForInjection(processed, recallLimit, queryText, scoreThreshold, queryConfig ? {
438
- weights: queryConfig.rankingWeights,
439
- categoryWeights: queryConfig.categoryWeights,
440
- resourceTypeWeights: queryConfig.resourceTypeWeights,
441
- } : undefined);
385
+ const durationMs = Date.now() - startedAt;
386
+ const entries = (contextResult?.entries ?? []).filter((entry) => Boolean(entry.uri));
387
+ const rawRetrievalErrors = contextResult?.stats?.retrieval_errors;
388
+ const retrievalErrors = Array.isArray(rawRetrievalErrors)
389
+ ? rawRetrievalErrors.map((error) => String(error))
390
+ : [];
391
+ const searchError = requestError
392
+ ? String(requestError)
393
+ : retrievalErrors.length > 0
394
+ ? retrievalErrors.join("; ")
395
+ : undefined;
396
+
397
+ if (searchError) {
398
+ logger.warn?.(`openviking: auto-recall context search failed: ${searchError}`);
399
+ }
442
400
 
443
- const recordTrace = async (injectedMemories: FindResultItem[], injectedCount: number, estimatedTokens?: number) => {
401
+ const traceSearches: RecallTraceEntry["searches"] = [
402
+ ...searchPlan.skipped.map((skipped) => ({
403
+ resourceType: skipped.resourceType,
404
+ limit: recallLimit,
405
+ scoreThreshold,
406
+ durationMs: 0,
407
+ total: 0,
408
+ results: [],
409
+ error: skipped.reason,
410
+ })),
411
+ ...searchPlan.searches.map((search) => {
412
+ const matchingEntries = entries.filter((entry) =>
413
+ search.contextType === "resource"
414
+ ? traceResourceType(entry) === "resource"
415
+ : traceResourceType(entry) !== "resource"
416
+ );
417
+ return {
418
+ resourceType: search.resourceType,
419
+ contextType: search.contextType,
420
+ limit: recallLimit,
421
+ scoreThreshold,
422
+ durationMs,
423
+ total: matchingEntries.length,
424
+ results: matchingEntries
425
+ .map(toTraceResult)
426
+ .slice(0, cfg.traceRecallMaxResultsPerSearch),
427
+ error: searchError,
428
+ };
429
+ }),
430
+ ];
431
+
432
+ const candidateCount = typeof contextResult?.stats?.candidates === "number"
433
+ ? contextResult.stats.candidates
434
+ : entries.length;
435
+ const recordTrace = async (
436
+ injectedEntries: SearchContextEntry[],
437
+ injectedCount: number,
438
+ estimatedTokens?: number,
439
+ ) => {
444
440
  const entry: RecallTraceEntry = {
445
441
  schemaVersion: "1.0",
446
442
  traceId: newTraceId(),
@@ -459,17 +455,17 @@ export async function buildAutoRecallContext(params: {
459
455
  queryTruncated: params.queryTruncated || queryText.length > cfg.traceRecallQueryMaxChars,
460
456
  },
461
457
  searches: traceSearches,
462
- selected: injectedMemories.map((memory) => ({
463
- uri: memory.uri,
464
- resourceType: memory.uri.startsWith("viking://resources") ? "resource" : undefined,
465
- category: memory.category,
466
- score: memory.score,
467
- abstractPreview: preview(memory.abstract ?? memory.overview, cfg.traceRecallPreviewChars),
458
+ selected: injectedEntries.map((contextEntry) => ({
459
+ uri: contextEntry.uri,
460
+ resourceType: traceResourceType(contextEntry),
461
+ category: contextEntry.category,
462
+ score: contextEntry.score,
463
+ abstractPreview: preview(contextEntry.text, cfg.traceRecallPreviewChars),
468
464
  injected: true,
469
465
  })),
470
466
  stats: {
471
- candidateCount: allMemories.length,
472
- selectedCount: injectedMemories.length,
467
+ candidateCount,
468
+ selectedCount: injectedEntries.length,
473
469
  injectedCount,
474
470
  estimatedTokens,
475
471
  },
@@ -478,39 +474,34 @@ export async function buildAutoRecallContext(params: {
478
474
  params.traceRecorder?.record(entry);
479
475
  };
480
476
 
481
- if (memories.length === 0) {
482
- await recordTrace([], 0, 0);
483
- return { memoryCount: 0, estimatedTokens: 0 };
484
- }
485
-
486
- const { lines: memoryLines, estimatedTokens } = await buildMemoryLinesWithBudget(
487
- memories,
488
- (uri) => client.read(uri, actorPeerId),
489
- {
490
- recallPreferAbstract,
491
- recallMaxInjectedChars: maxInjectedChars,
492
- includeUri: true,
493
- },
494
- );
495
-
496
- if (memoryLines.length === 0) {
497
- verbose?.(
498
- `openviking: skipping auto-recall injection; no complete memories fit maxInjectedChars=${maxInjectedChars}`,
499
- );
477
+ const rendered = contextResult?.rendered?.trim() ?? "";
478
+ if (requestError || entries.length === 0 || !rendered) {
500
479
  await recordTrace([], 0, 0);
501
480
  return { memoryCount: 0, estimatedTokens: 0 };
502
481
  }
503
482
 
504
- const block = buildRecallContextBlock(memoryLines);
483
+ const usedTokens = contextResult?.stats?.used_tokens;
484
+ const estimatedTokens = typeof usedTokens === "number" && Number.isFinite(usedTokens)
485
+ ? Math.max(0, Math.ceil(usedTokens))
486
+ : estimateTokenCount(rendered);
487
+ const block = buildRecallContextBlock([rendered]);
505
488
  verbose?.(
506
- `openviking: injecting ${memoryLines.length} memories (${block.length} chars, ~${estimatedTokens} tokens, maxInjectedChars=${maxInjectedChars})`,
489
+ `openviking: injecting ${entries.length} memories (${block.length} chars, ~${estimatedTokens} tokens, serverMaxTokens=${maxTokens})`,
507
490
  );
508
491
  verbose?.(
509
- `openviking: inject-detail ${toJsonLog({ count: memories.length, memories: summarizeInjectionMemories(memories) })}`,
492
+ `openviking: inject-detail ${toJsonLog({
493
+ count: entries.length,
494
+ entries: entries.map((contextEntry) => ({
495
+ uri: contextEntry.uri,
496
+ category: contextEntry.category,
497
+ score: contextEntry.score,
498
+ detail: contextEntry.detail,
499
+ })),
500
+ })}`,
510
501
  );
511
502
 
512
- await recordTrace(memories.slice(0, memoryLines.length), memoryLines.length, estimatedTokens);
513
- return { block, memoryCount: memoryLines.length, estimatedTokens };
503
+ await recordTrace(entries, entries.length, estimatedTokens);
504
+ return { block, memoryCount: entries.length, estimatedTokens };
514
505
  })(),
515
506
  cfg.autoRecallTimeoutMs,
516
507
  "openviking: auto-recall search timeout",
package/client.ts CHANGED
@@ -12,6 +12,11 @@ import {
12
12
  defaultResourcePackager,
13
13
  type ResourcePackager,
14
14
  } from "./adapters/resource-packager.js";
15
+ import {
16
+ buildContextSearchBody,
17
+ contextRequestTimeoutMs,
18
+ normalizeContextEntry,
19
+ } from "./shared/recall-core.mjs";
15
20
 
16
21
  export type FindResultItem = {
17
22
  uri: string;
@@ -30,6 +35,36 @@ export type FindResult = {
30
35
  total?: number;
31
36
  };
32
37
 
38
+ export type SearchContextEntry = {
39
+ uri: string;
40
+ category?: string;
41
+ score?: number;
42
+ detail?: string;
43
+ text?: string;
44
+ origin?: string;
45
+ };
46
+
47
+ export type SearchContextResult = {
48
+ entries?: SearchContextEntry[];
49
+ rendered?: string;
50
+ digest?: string;
51
+ stats?: Record<string, unknown>;
52
+ };
53
+
54
+ export type SearchContextOptions = {
55
+ sessionId?: string;
56
+ limit?: number;
57
+ scoreThreshold?: number;
58
+ contextType?: string | string[];
59
+ queryExpansion?: "off" | "auto";
60
+ maxTokens?: number;
61
+ detail?: "abstract" | "overview" | "full";
62
+ dedupTurns?: number;
63
+ peerScope?: "actor" | "all";
64
+ actorPeerId?: string;
65
+ requestTimeoutMs?: number;
66
+ };
67
+
33
68
  export type FsListEntry = string | Record<string, unknown>;
34
69
 
35
70
  export type FsListResult = FsListEntry[];
@@ -488,6 +523,66 @@ export class OpenVikingClient {
488
523
  }, undefined, actorPeerId);
489
524
  }
490
525
 
526
+ async searchContext(
527
+ query: string,
528
+ options: SearchContextOptions = {},
529
+ ): Promise<SearchContextResult> {
530
+ const contractConfig = {
531
+ recallLimit: options.limit,
532
+ recallLimitConfigured: options.limit !== undefined,
533
+ recallMaxTokens: options.maxTokens,
534
+ recallMaxTokensConfigured: options.maxTokens !== undefined,
535
+ scoreThreshold: options.scoreThreshold,
536
+ recallQueryExpansion: options.queryExpansion,
537
+ recallQueryExpansionConfigured: options.queryExpansion !== undefined,
538
+ recallDedupTurns: options.dedupTurns,
539
+ recallPeerScope: options.peerScope,
540
+ recallContextTimeoutMs: options.requestTimeoutMs,
541
+ timeoutMs: this.timeoutMs,
542
+ };
543
+ const body = {
544
+ ...buildContextSearchBody(contractConfig, { sessionId: options.sessionId }),
545
+ query,
546
+ ...(options.contextType !== undefined ? { context_type: options.contextType } : {}),
547
+ ...(options.detail !== undefined ? { detail: options.detail } : {}),
548
+ };
549
+ const requestTimeoutMs = contextRequestTimeoutMs(contractConfig, body);
550
+ const actorPeerId = this.resolveActorPeerHeader(options.actorPeerId);
551
+ const tenantHeaders = this.resolveTenantHeaders();
552
+ this.routingDebugLog?.(
553
+ `openviking: context search POST ${this.baseUrl}/api/v1/search/search ` +
554
+ JSON.stringify({
555
+ X_OpenViking_Account: tenantHeaders.accountId ?? null,
556
+ X_OpenViking_User: tenantHeaders.userId ?? null,
557
+ X_OpenViking_Actor_Peer: actorPeerId ?? null,
558
+ session_id: body.session_id ?? null,
559
+ query:
560
+ query.length > 4000
561
+ ? `${query.slice(0, 4000)}…(+${query.length - 4000} more chars)`
562
+ : query,
563
+ purpose: body.purpose,
564
+ quotas: body.quotas ?? null,
565
+ score_threshold: body.score_threshold,
566
+ context_type: body.context_type ?? null,
567
+ query_expansion: body.query_expansion ?? null,
568
+ max_tokens: body.max_tokens ?? null,
569
+ detail: body.detail ?? null,
570
+ dedup_turns: body.dedup_turns ?? 0,
571
+ peer_scope: body.peer_scope ?? null,
572
+ }),
573
+ );
574
+ const result = await this.request<SearchContextResult>("/api/v1/search/search", {
575
+ method: "POST",
576
+ body: JSON.stringify(body),
577
+ }, requestTimeoutMs, actorPeerId);
578
+ return {
579
+ ...result,
580
+ entries: Array.isArray(result.entries)
581
+ ? result.entries.map((entry) => ({ ...entry, ...normalizeContextEntry(entry) }))
582
+ : result.entries,
583
+ };
584
+ }
585
+
491
586
  async read(uri: string, actorPeerId?: string): Promise<string> {
492
587
  return this.request<string>(
493
588
  `/api/v1/content/read?uri=${encodeURIComponent(uri)}`,
package/config.ts CHANGED
@@ -45,13 +45,13 @@ export type MemoryOpenVikingConfig = {
45
45
  captureMode?: "semantic" | "keyword";
46
46
  captureMaxLength?: number;
47
47
  autoRecall?: boolean;
48
- /** Outer time budget for the whole auto-recall flow, including search, ranking, and reads. */
48
+ /** Outer time budget for the whole server-assembled auto-recall flow. */
49
49
  autoRecallTimeoutMs?: number;
50
50
  /** Include resources in auto-recall and default memory_recall search. Default false. */
51
51
  recallResources?: boolean;
52
52
  recallLimit?: number;
53
53
  recallScoreThreshold?: number;
54
- /** Maximum total characters injected by auto-recall. */
54
+ /** Legacy character budget, converted to the server context token budget. */
55
55
  recallMaxInjectedChars?: number;
56
56
  /** @deprecated Auto-recall no longer truncates individual memories. */
57
57
  recallMaxContentChars?: number;
@@ -140,7 +140,9 @@ const DEFAULT_TARGET_URI = "viking://~/memories";
140
140
  const DEFAULT_TIMEOUT_MS = 15000;
141
141
  const DEFAULT_CAPTURE_MODE = "semantic";
142
142
  const DEFAULT_CAPTURE_MAX_LENGTH = 24000;
143
- const DEFAULT_AUTO_RECALL_TIMEOUT_MS = 5000;
143
+ // Session-aware context search may spend up to 5 seconds on query expansion
144
+ // before returning its server-side fallback, so leave headroom for retrieval.
145
+ const DEFAULT_AUTO_RECALL_TIMEOUT_MS = 15000;
144
146
  const DEFAULT_RECALL_LIMIT = 6;
145
147
  const DEFAULT_RECALL_SCORE_THRESHOLD = 0.15;
146
148
  const DEFAULT_RECALL_MAX_CONTENT_CHARS = 5000;
@@ -843,7 +845,7 @@ export const memoryOpenVikingConfigSchema = {
843
845
  label: "Auto-Recall Timeout (ms)",
844
846
  placeholder: String(DEFAULT_AUTO_RECALL_TIMEOUT_MS),
845
847
  advanced: true,
846
- help: "Outer time budget for the whole auto-recall flow, including search, ranking, and memory reads.",
848
+ help: "Outer time budget for the server-assembled context search, including session-aware query expansion.",
847
849
  },
848
850
  recallResources: {
849
851
  label: "Recall Resources",
@@ -870,7 +872,7 @@ export const memoryOpenVikingConfigSchema = {
870
872
  label: "Recall Max Injected Chars",
871
873
  placeholder: String(DEFAULT_RECALL_MAX_INJECTED_CHARS),
872
874
  advanced: true,
873
- help: "Maximum total characters for auto-recall memory injection. Complete memories that do not fit are skipped, not truncated.",
875
+ help: "Legacy auto-recall character budget. It is converted to the server context token budget at four characters per token.",
874
876
  },
875
877
  recallMaxContentChars: {
876
878
  label: "Deprecated Recall Max Content Chars",
@@ -881,7 +883,7 @@ export const memoryOpenVikingConfigSchema = {
881
883
  recallPreferAbstract: {
882
884
  label: "Recall Prefer Abstract",
883
885
  advanced: true,
884
- help: "Use memory abstract instead of fetching full content when abstract is available. Reduces token usage.",
886
+ help: "Pin server-assembled auto-recall entries to abstract detail. When disabled, the server chooses each category's default detail tier.",
885
887
  },
886
888
  recallTokenBudget: {
887
889
  label: "Deprecated Recall Token Budget",