@openviking/openclaw-plugin 2026.9.1 → 2026.9.4-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/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/commands/setup.ts CHANGED
@@ -84,8 +84,8 @@ const CONFIG_KEYS_TO_PRESERVE = [
84
84
  "logFindRequests",
85
85
  ] as const;
86
86
 
87
- type PeerRole = "none" | "assistant" | "person";
88
- const DEFAULT_SETUP_PEER_ROLE: PeerRole = "assistant";
87
+ type PeerRole = "none" | "assistant" | "sender";
88
+ const DEFAULT_SETUP_PEER_ROLE: PeerRole = "none";
89
89
  type RecallTargetType = "resource" | "user" | "agent";
90
90
  const ALLOWED_RECALL_TARGET_TYPES = ["resource", "user", "agent"] as const;
91
91
 
@@ -126,7 +126,8 @@ function isValidPeerPrefixInput(value: string): boolean {
126
126
  function normalizePeerRole(value: unknown): PeerRole | undefined {
127
127
  if (typeof value !== "string") return undefined;
128
128
  const role = value.trim().toLowerCase();
129
- if (role === "none" || role === "assistant" || role === "person") return role;
129
+ if (role === "person") return "sender";
130
+ if (role === "none" || role === "assistant" || role === "sender") return role;
130
131
  return undefined;
131
132
  }
132
133
 
@@ -147,7 +148,7 @@ function resolveSetupPeerRole(value: unknown): PeerRole {
147
148
  if (value === undefined) return DEFAULT_SETUP_PEER_ROLE;
148
149
  const role = normalizePeerRole(value);
149
150
  if (role) return role;
150
- throw new Error('peer_role must be "none", "assistant", or "person"');
151
+ throw new Error('peer_role must be "none", "assistant", or "sender" (legacy alias: "person")');
151
152
  }
152
153
 
153
154
  function normalizeSetupRecallTargetTypes(value: unknown): RecallTargetType[] | undefined {
@@ -206,9 +207,20 @@ async function askPeerRole(
206
207
  q: (prompt: string, def?: string) => Promise<string>,
207
208
  defaultValue: PeerRole,
208
209
  ): Promise<PeerRole> {
210
+ console.log(
211
+ ` ${tr(
212
+ zh,
213
+ "Memory scope — none (default): viking://user/<user_id>/memories, shared across all conversations; " +
214
+ "assistant: viking://user/<user_id>/peers/<assistant_id>/memories; " +
215
+ "sender: viking://user/<user_id>/peers/<sender_id>/memories (legacy alias: person).",
216
+ "记忆归属 —— none(默认):viking://user/<user_id>/memories,所有对话共享;" +
217
+ "assistant:viking://user/<user_id>/peers/<assistant_id>/memories;" +
218
+ "sender:viking://user/<user_id>/peers/<sender_id>/memories(兼容旧值 person)。",
219
+ )}`,
220
+ );
209
221
  while (true) {
210
222
  const value = await q(
211
- tr(zh, "Peer Role (none/assistant/person)", "Peer Role(none/assistant/person)"),
223
+ tr(zh, "Memory scope (none/assistant/sender)", "记忆归属(none/assistant/sender)"),
212
224
  defaultValue,
213
225
  );
214
226
  const role = normalizePeerRole(value);
@@ -216,8 +228,8 @@ async function askPeerRole(
216
228
  console.log(
217
229
  ` ✗ ${tr(
218
230
  zh,
219
- 'Peer Role must be "none", "assistant", or "person".',
220
- 'Peer Role 必须是 "none"、"assistant" 或 "person"。',
231
+ 'Memory scope must be "none", "assistant", or "sender" (legacy "person" is also accepted).',
232
+ '记忆归属必须是 "none"、"assistant" 或 "sender"(也兼容旧值 "person")。',
221
233
  )}`,
222
234
  );
223
235
  }
@@ -605,7 +617,7 @@ export function registerSetupCli(api: any): void {
605
617
  .option("--zh", "Chinese prompts")
606
618
  .option("--base-url <url>", "OpenViking server URL (enables non-interactive mode)")
607
619
  .option("--api-key <key>", "API key for authentication")
608
- .option("--peer-role <role>", "Peer ID role: none, assistant, or person")
620
+ .option("--peer-role <role>", "Memory scope: none (shared), assistant (per assistant), or sender (per sender); person is a legacy alias")
609
621
  .option("--peer-prefix <prefix>", "Prefix for assistant peer_id values")
610
622
  .option("--account-id <id>", "Account ID (required for root API keys)")
611
623
  .option("--user-id <id>", "User ID (required for root API keys)")
@@ -1182,6 +1194,8 @@ async function setupRemote(
1182
1194
  export const __test__ = {
1183
1195
  isLegacyLocalMode,
1184
1196
  isValidPeerPrefixInput,
1197
+ normalizePeerRole,
1198
+ resolveSetupPeerRole,
1185
1199
  normalizeSetupRecallTargetTypes,
1186
1200
  activateContextEngineSlot,
1187
1201
  isContextEngineSlotActive,
package/config.ts CHANGED
@@ -30,7 +30,8 @@ export type OpenVikingSecretRef =
30
30
  export type MemoryOpenVikingConfig = {
31
31
  mode?: "remote";
32
32
  baseUrl?: string;
33
- peer_role?: "none" | "assistant" | "person";
33
+ /** `person` is a legacy alias for `sender`. */
34
+ peer_role?: "none" | "assistant" | "sender" | "person";
34
35
  peer_prefix?: string;
35
36
  apiKey?: string | OpenVikingSecretRef;
36
37
  /** Optional HTTP headers merged into every OpenViking request. */
@@ -45,13 +46,13 @@ export type MemoryOpenVikingConfig = {
45
46
  captureMode?: "semantic" | "keyword";
46
47
  captureMaxLength?: number;
47
48
  autoRecall?: boolean;
48
- /** Outer time budget for the whole auto-recall flow, including search, ranking, and reads. */
49
+ /** Outer time budget for the whole server-assembled auto-recall flow. */
49
50
  autoRecallTimeoutMs?: number;
50
51
  /** Include resources in auto-recall and default memory_recall search. Default false. */
51
52
  recallResources?: boolean;
52
53
  recallLimit?: number;
53
54
  recallScoreThreshold?: number;
54
- /** Maximum total characters injected by auto-recall. */
55
+ /** Legacy character budget, converted to the server context token budget. */
55
56
  recallMaxInjectedChars?: number;
56
57
  /** @deprecated Auto-recall no longer truncates individual memories. */
57
58
  recallMaxContentChars?: number;
@@ -127,10 +128,12 @@ export type MemoryOpenVikingConfig = {
127
128
 
128
129
  /** Runtime config after memoryOpenVikingConfigSchema.parse() has applied defaults. */
129
130
  export type ParsedMemoryOpenVikingConfig = Required<
130
- Omit<MemoryOpenVikingConfig, "agentExperience" | "recallTargetTypes" | "apiKey">
131
+ Omit<MemoryOpenVikingConfig, "agentExperience" | "recallTargetTypes" | "apiKey" | "peer_role">
131
132
  > & {
132
133
  /** parse() resolves SecretRef values, so the runtime shape is always a plain string. */
133
134
  apiKey: string;
135
+ /** Runtime uses the canonical name; legacy `person` input normalizes to `sender`. */
136
+ peer_role: "none" | "assistant" | "sender";
134
137
  agentExperience: Required<NonNullable<MemoryOpenVikingConfig["agentExperience"]>>;
135
138
  recallTargetTypes: Array<"resource" | "user" | "agent">;
136
139
  };
@@ -140,7 +143,9 @@ const DEFAULT_TARGET_URI = "viking://~/memories";
140
143
  const DEFAULT_TIMEOUT_MS = 15000;
141
144
  const DEFAULT_CAPTURE_MODE = "semantic";
142
145
  const DEFAULT_CAPTURE_MAX_LENGTH = 24000;
143
- const DEFAULT_AUTO_RECALL_TIMEOUT_MS = 5000;
146
+ // Session-aware context search may spend up to 5 seconds on query expansion
147
+ // before returning its server-side fallback, so leave headroom for retrieval.
148
+ const DEFAULT_AUTO_RECALL_TIMEOUT_MS = 15000;
144
149
  const DEFAULT_RECALL_LIMIT = 6;
145
150
  const DEFAULT_RECALL_SCORE_THRESHOLD = 0.15;
146
151
  const DEFAULT_RECALL_MAX_CONTENT_CHARS = 5000;
@@ -150,7 +155,7 @@ const DEFAULT_COMMIT_TOKEN_THRESHOLD_RATIO = 0.5;
150
155
  const DEFAULT_COMMIT_KEEP_RECENT_COUNT = 10;
151
156
  const DEFAULT_BYPASS_SESSION_PATTERNS: string[] = [];
152
157
  const DEFAULT_EMIT_STANDARD_DIAGNOSTICS = false;
153
- const DEFAULT_PEER_ROLE = "assistant" as const;
158
+ const DEFAULT_PEER_ROLE = "none" as const;
154
159
  const DEFAULT_PEER_PREFIX = "";
155
160
  const DEFAULT_TRACE_RECALL_DIR = "~/.openclaw/openviking/recall-traces";
156
161
  const DEFAULT_TRACE_RECALL_RETENTION_DAYS = 14;
@@ -290,16 +295,23 @@ function resolveSecret(
290
295
  }
291
296
  }
292
297
 
293
- function resolvePeerRole(configured: unknown) {
298
+ function resolvePeerRole(configured: unknown): "none" | "assistant" | "sender" {
294
299
  if (typeof configured === "string") {
295
300
  const role = configured.trim().toLowerCase();
296
- if (role === "none" || role === "assistant" || role === "person") {
301
+ if (role === "none" || role === "assistant" || role === "sender") {
297
302
  return role;
298
303
  }
299
- throw new Error(`openviking peer_role must be "none", "assistant", or "person"`);
304
+ if (role === "person") {
305
+ return "sender";
306
+ }
307
+ throw new Error(
308
+ `openviking peer_role must be "none", "assistant", or "sender" (legacy alias: "person")`,
309
+ );
300
310
  }
301
311
  if (configured !== undefined) {
302
- throw new Error(`openviking peer_role must be "none", "assistant", or "person"`);
312
+ throw new Error(
313
+ `openviking peer_role must be "none", "assistant", or "sender" (legacy alias: "person")`,
314
+ );
303
315
  }
304
316
  return DEFAULT_PEER_ROLE;
305
317
  }
@@ -774,14 +786,17 @@ export const memoryOpenVikingConfigSchema = {
774
786
  help: "HTTP URL when mode is remote (or use ${OPENVIKING_BASE_URL})",
775
787
  },
776
788
  peer_role: {
777
- label: "Peer Role",
789
+ label: "Memory Scope (peer_role)",
778
790
  placeholder: DEFAULT_PEER_ROLE,
779
- help: 'Controls which session messages get peer_id: "none", "assistant", or "person".',
791
+ help:
792
+ 'Where peer-scoped memories are stored. "none" (default): viking://user/<user_id>/memories. ' +
793
+ '"assistant": viking://user/<user_id>/peers/<assistant_id>/memories. ' +
794
+ '"sender": viking://user/<user_id>/peers/<sender_id>/memories. Legacy "person" is accepted as "sender".',
780
795
  },
781
796
  peer_prefix: {
782
797
  label: "Peer Prefix",
783
798
  placeholder: "optional-prefix",
784
- help: "Optional prefix applied to assistant peer_id values derived from OpenClaw runtime agent IDs.",
799
+ help: 'Only used when Memory Scope is "assistant". Prefix added to the peer id derived from the OpenClaw agent id.',
785
800
  },
786
801
  apiKey: {
787
802
  label: "OpenViking API Key",
@@ -843,7 +858,7 @@ export const memoryOpenVikingConfigSchema = {
843
858
  label: "Auto-Recall Timeout (ms)",
844
859
  placeholder: String(DEFAULT_AUTO_RECALL_TIMEOUT_MS),
845
860
  advanced: true,
846
- help: "Outer time budget for the whole auto-recall flow, including search, ranking, and memory reads.",
861
+ help: "Outer time budget for the server-assembled context search, including session-aware query expansion.",
847
862
  },
848
863
  recallResources: {
849
864
  label: "Recall Resources",
@@ -870,7 +885,7 @@ export const memoryOpenVikingConfigSchema = {
870
885
  label: "Recall Max Injected Chars",
871
886
  placeholder: String(DEFAULT_RECALL_MAX_INJECTED_CHARS),
872
887
  advanced: true,
873
- help: "Maximum total characters for auto-recall memory injection. Complete memories that do not fit are skipped, not truncated.",
888
+ help: "Legacy auto-recall character budget. It is converted to the server context token budget at four characters per token.",
874
889
  },
875
890
  recallMaxContentChars: {
876
891
  label: "Deprecated Recall Max Content Chars",
@@ -881,7 +896,7 @@ export const memoryOpenVikingConfigSchema = {
881
896
  recallPreferAbstract: {
882
897
  label: "Recall Prefer Abstract",
883
898
  advanced: true,
884
- help: "Use memory abstract instead of fetching full content when abstract is available. Reduces token usage.",
899
+ help: "Pin server-assembled auto-recall entries to abstract detail. When disabled, the server chooses each category's default detail tier.",
885
900
  },
886
901
  recallTokenBudget: {
887
902
  label: "Deprecated Recall Token Budget",
package/context-engine.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { OpenVikingClient } from "./client.js";
2
- import type { MemoryOpenVikingConfig } from "./config.js";
2
+ import type { ParsedMemoryOpenVikingConfig } from "./config.js";
3
3
  import type { RuntimeQueryConfigStore } from "./query-config.js";
4
4
  import {
5
5
  AUTO_RECALL_SOURCE_MARKER,
@@ -248,7 +248,7 @@ export function createMemoryOpenVikingContextEngine(params: {
248
248
  id: string;
249
249
  name: string;
250
250
  version?: string;
251
- cfg: Required<MemoryOpenVikingConfig>;
251
+ cfg: ParsedMemoryOpenVikingConfig;
252
252
  logger: Logger;
253
253
  getClient: () => Promise<OpenVikingClient>;
254
254
  /** Extra args help match hook-populated routing when OpenClaw provides sessionKey / OV session id. */
@@ -1,9 +1,11 @@
1
- import { pickMemoriesForInjection, postProcessMemories, summarizeInjectionMemories, toJsonLog, } from "./memory-ranking.js";
1
+ import { toJsonLog } from "./memory-ranking.js";
2
2
  import { quickRecallPrecheck, withTimeout } from "./process-manager.js";
3
3
  import { resolveRecallSearchPlan, } from "./registries/recall-resource-types.js";
4
4
  import { sanitizeUserTextForCapture } from "./text-utils.js";
5
5
  import { estimateTextTokens } from "./token-estimator.js";
6
6
  const RECALL_QUERY_MAX_CHARS = 4_000;
7
+ const AUTO_RECALL_DEDUP_TURNS = 5;
8
+ const LEGACY_CHARS_PER_TOKEN = 4;
7
9
  export const AUTO_RECALL_SOURCE_MARKER = "Source: openviking-auto-recall";
8
10
  const WRITE_OR_EFFECT_RE = /\b(write|edit|modify|delete|remove|migrate|deploy|release|publish|configure|patch)\b|写|改|修改|删除|迁移|部署|发布|配置|打补丁/i;
9
11
  const EXECUTION_RE = /\b(fix|debug|test|build|run|implement|refactor|integrate|repair|troubleshoot)\b|修复|调试|测试|构建|运行|实现|重构|对接|排查/i;
@@ -135,16 +137,25 @@ function preview(value, maxChars) {
135
137
  return undefined;
136
138
  return trimmed.length > maxChars ? trimmed.slice(0, maxChars) : trimmed;
137
139
  }
138
- function toTraceResults(items, resourceType) {
139
- return items.map((item) => ({
140
- uri: item.uri,
140
+ function traceResourceType(entry) {
141
+ if (entry.category === "resources" || entry.uri.startsWith("viking://resources")) {
142
+ return "resource";
143
+ }
144
+ if (entry.origin === "actor_peer") {
145
+ return "agent";
146
+ }
147
+ return "user";
148
+ }
149
+ function toTraceResult(entry) {
150
+ const resourceType = traceResourceType(entry);
151
+ return {
152
+ uri: entry.uri,
141
153
  resourceType,
142
- category: item.category,
143
- score: item.score,
144
- level: item.level,
145
- abstractPreview: preview(item.abstract ?? item.overview, 240),
154
+ category: entry.category,
155
+ score: entry.score,
156
+ abstractPreview: preview(entry.text, 240),
146
157
  resultType: resourceType === "resource" ? "resource" : "memory",
147
- }));
158
+ };
148
159
  }
149
160
  function boundTraceQuery(query, maxChars) {
150
161
  if (query.length <= maxChars) {
@@ -211,7 +222,6 @@ export async function buildAutoRecallContext(params) {
211
222
  return { memoryCount: 0, estimatedTokens: 0 };
212
223
  }
213
224
  return withTimeout((async () => {
214
- const candidateLimit = queryConfig?.candidateLimit ?? Math.max(cfg.recallLimit * 4, 20);
215
225
  const scoreThreshold = queryConfig?.scoreThreshold ?? cfg.recallScoreThreshold;
216
226
  const recallLimit = queryConfig?.recallLimit ?? cfg.recallLimit;
217
227
  const maxInjectedChars = queryConfig?.maxInjectedChars ?? cfg.recallMaxInjectedChars;
@@ -220,89 +230,75 @@ export async function buildAutoRecallContext(params) {
220
230
  ovSessionId: params.ovSessionId,
221
231
  agentId,
222
232
  });
223
- const traceSearches = searchPlan.skipped.map((skipped) => ({
224
- resourceType: skipped.resourceType,
225
- limit: candidateLimit,
226
- scoreThreshold: 0,
227
- durationMs: 0,
228
- total: 0,
229
- results: [],
230
- error: skipped.reason,
231
- }));
232
- const autoRecallPromises = searchPlan.searches.map(async (search) => {
233
- const start = Date.now();
234
- const result = await client.find(queryText, {
235
- targetUri: search.targetUri,
236
- limit: candidateLimit,
237
- scoreThreshold: 0,
238
- contextType: search.contextType,
233
+ const contextTypes = [...new Set(searchPlan.searches.map((search) => search.contextType))];
234
+ const maxTokens = Math.min(32_000, Math.max(64, Math.round(maxInjectedChars / LEGACY_CHARS_PER_TOKEN)));
235
+ const startedAt = Date.now();
236
+ let contextResult;
237
+ let requestError;
238
+ try {
239
+ contextResult = await client.searchContext(queryText, {
240
+ sessionId: params.ovSessionId,
241
+ limit: recallLimit,
242
+ scoreThreshold,
243
+ contextType: contextTypes.length === 1 ? contextTypes[0] : contextTypes,
244
+ queryExpansion: "auto",
245
+ maxTokens,
246
+ detail: recallPreferAbstract ? "abstract" : undefined,
247
+ dedupTurns: params.ovSessionId ? AUTO_RECALL_DEDUP_TURNS : undefined,
248
+ peerScope: "actor",
239
249
  actorPeerId,
250
+ requestTimeoutMs: cfg.autoRecallTimeoutMs,
240
251
  });
241
- return {
242
- resourceType: search.resourceType,
243
- targetUri: search.targetUri,
244
- result,
245
- durationMs: Date.now() - start,
246
- };
247
- });
248
- const autoRecallSettled = await Promise.allSettled(autoRecallPromises);
249
- const allMemories = [];
250
- for (const s of autoRecallSettled) {
251
- if (s.status === "fulfilled") {
252
- const result = s.value.result ?? {};
253
- const memories = result.memories ?? [];
254
- const resources = result.resources ?? [];
255
- allMemories.push(...memories, ...resources);
256
- traceSearches.push({
257
- resourceType: s.value.resourceType,
258
- targetUriInput: s.value.targetUri,
259
- limit: candidateLimit,
260
- scoreThreshold: 0,
261
- durationMs: s.value.durationMs,
262
- total: result.total ?? memories.length + resources.length + (result.skills?.length ?? 0),
263
- results: [
264
- ...toTraceResults(memories, s.value.resourceType),
265
- ...toTraceResults(resources, "resource"),
266
- ...(result.skills ?? []).map((item) => ({
267
- uri: item.uri,
268
- resourceType: s.value.resourceType,
269
- category: item.category,
270
- score: item.score,
271
- level: item.level,
272
- abstractPreview: preview(item.abstract ?? item.overview, 240),
273
- resultType: "skill",
274
- })),
275
- ].slice(0, cfg.traceRecallMaxResultsPerSearch),
276
- });
277
- }
278
- else {
279
- logger.warn?.(`openviking: auto-recall search failed: ${String(s.reason)}`);
280
- const failedIndex = traceSearches.length;
281
- const search = searchPlan.searches[failedIndex - searchPlan.skipped.length];
282
- traceSearches.push({
283
- resourceType: search?.resourceType ?? "user",
284
- targetUriInput: search?.targetUri,
285
- limit: candidateLimit,
286
- scoreThreshold: 0,
287
- durationMs: 0,
288
- total: 0,
289
- results: [],
290
- error: String(s.reason),
291
- });
292
- }
293
252
  }
294
- const uniqueMemories = allMemories.filter((memory, index, self) => index === self.findIndex((m) => m.uri === memory.uri));
295
- const leafOnly = uniqueMemories.filter((m) => !m.level || m.level === 2);
296
- const processed = postProcessMemories(leafOnly, {
297
- limit: candidateLimit,
298
- scoreThreshold,
299
- });
300
- const memories = pickMemoriesForInjection(processed, recallLimit, queryText, scoreThreshold, queryConfig ? {
301
- weights: queryConfig.rankingWeights,
302
- categoryWeights: queryConfig.categoryWeights,
303
- resourceTypeWeights: queryConfig.resourceTypeWeights,
304
- } : undefined);
305
- const recordTrace = async (injectedMemories, injectedCount, estimatedTokens) => {
253
+ catch (error) {
254
+ requestError = error;
255
+ }
256
+ const durationMs = Date.now() - startedAt;
257
+ const entries = (contextResult?.entries ?? []).filter((entry) => Boolean(entry.uri));
258
+ const rawRetrievalErrors = contextResult?.stats?.retrieval_errors;
259
+ const retrievalErrors = Array.isArray(rawRetrievalErrors)
260
+ ? rawRetrievalErrors.map((error) => String(error))
261
+ : [];
262
+ const searchError = requestError
263
+ ? String(requestError)
264
+ : retrievalErrors.length > 0
265
+ ? retrievalErrors.join("; ")
266
+ : undefined;
267
+ if (searchError) {
268
+ logger.warn?.(`openviking: auto-recall context search failed: ${searchError}`);
269
+ }
270
+ const traceSearches = [
271
+ ...searchPlan.skipped.map((skipped) => ({
272
+ resourceType: skipped.resourceType,
273
+ limit: recallLimit,
274
+ scoreThreshold,
275
+ durationMs: 0,
276
+ total: 0,
277
+ results: [],
278
+ error: skipped.reason,
279
+ })),
280
+ ...searchPlan.searches.map((search) => {
281
+ const matchingEntries = entries.filter((entry) => search.contextType === "resource"
282
+ ? traceResourceType(entry) === "resource"
283
+ : traceResourceType(entry) !== "resource");
284
+ return {
285
+ resourceType: search.resourceType,
286
+ contextType: search.contextType,
287
+ limit: recallLimit,
288
+ scoreThreshold,
289
+ durationMs,
290
+ total: matchingEntries.length,
291
+ results: matchingEntries
292
+ .map(toTraceResult)
293
+ .slice(0, cfg.traceRecallMaxResultsPerSearch),
294
+ error: searchError,
295
+ };
296
+ }),
297
+ ];
298
+ const candidateCount = typeof contextResult?.stats?.candidates === "number"
299
+ ? contextResult.stats.candidates
300
+ : entries.length;
301
+ const recordTrace = async (injectedEntries, injectedCount, estimatedTokens) => {
306
302
  const entry = {
307
303
  schemaVersion: "1.0",
308
304
  traceId: newTraceId(),
@@ -321,17 +317,17 @@ export async function buildAutoRecallContext(params) {
321
317
  queryTruncated: params.queryTruncated || queryText.length > cfg.traceRecallQueryMaxChars,
322
318
  },
323
319
  searches: traceSearches,
324
- selected: injectedMemories.map((memory) => ({
325
- uri: memory.uri,
326
- resourceType: memory.uri.startsWith("viking://resources") ? "resource" : undefined,
327
- category: memory.category,
328
- score: memory.score,
329
- abstractPreview: preview(memory.abstract ?? memory.overview, cfg.traceRecallPreviewChars),
320
+ selected: injectedEntries.map((contextEntry) => ({
321
+ uri: contextEntry.uri,
322
+ resourceType: traceResourceType(contextEntry),
323
+ category: contextEntry.category,
324
+ score: contextEntry.score,
325
+ abstractPreview: preview(contextEntry.text, cfg.traceRecallPreviewChars),
330
326
  injected: true,
331
327
  })),
332
328
  stats: {
333
- candidateCount: allMemories.length,
334
- selectedCount: injectedMemories.length,
329
+ candidateCount,
330
+ selectedCount: injectedEntries.length,
335
331
  injectedCount,
336
332
  estimatedTokens,
337
333
  },
@@ -339,24 +335,27 @@ export async function buildAutoRecallContext(params) {
339
335
  // Trace persistence is diagnostic best-effort; never put JSONL flush latency on the auto-recall critical path.
340
336
  params.traceRecorder?.record(entry);
341
337
  };
342
- if (memories.length === 0) {
343
- await recordTrace([], 0, 0);
344
- return { memoryCount: 0, estimatedTokens: 0 };
345
- }
346
- const { lines: memoryLines, estimatedTokens } = await buildMemoryLinesWithBudget(memories, (uri) => client.read(uri, actorPeerId), {
347
- recallPreferAbstract,
348
- recallMaxInjectedChars: maxInjectedChars,
349
- includeUri: true,
350
- });
351
- if (memoryLines.length === 0) {
352
- verbose?.(`openviking: skipping auto-recall injection; no complete memories fit maxInjectedChars=${maxInjectedChars}`);
338
+ const rendered = contextResult?.rendered?.trim() ?? "";
339
+ if (requestError || entries.length === 0 || !rendered) {
353
340
  await recordTrace([], 0, 0);
354
341
  return { memoryCount: 0, estimatedTokens: 0 };
355
342
  }
356
- const block = buildRecallContextBlock(memoryLines);
357
- verbose?.(`openviking: injecting ${memoryLines.length} memories (${block.length} chars, ~${estimatedTokens} tokens, maxInjectedChars=${maxInjectedChars})`);
358
- verbose?.(`openviking: inject-detail ${toJsonLog({ count: memories.length, memories: summarizeInjectionMemories(memories) })}`);
359
- await recordTrace(memories.slice(0, memoryLines.length), memoryLines.length, estimatedTokens);
360
- return { block, memoryCount: memoryLines.length, estimatedTokens };
343
+ const usedTokens = contextResult?.stats?.used_tokens;
344
+ const estimatedTokens = typeof usedTokens === "number" && Number.isFinite(usedTokens)
345
+ ? Math.max(0, Math.ceil(usedTokens))
346
+ : estimateTokenCount(rendered);
347
+ const block = buildRecallContextBlock([rendered]);
348
+ verbose?.(`openviking: injecting ${entries.length} memories (${block.length} chars, ~${estimatedTokens} tokens, serverMaxTokens=${maxTokens})`);
349
+ verbose?.(`openviking: inject-detail ${toJsonLog({
350
+ count: entries.length,
351
+ entries: entries.map((contextEntry) => ({
352
+ uri: contextEntry.uri,
353
+ category: contextEntry.category,
354
+ score: contextEntry.score,
355
+ detail: contextEntry.detail,
356
+ })),
357
+ })}`);
358
+ await recordTrace(entries, entries.length, estimatedTokens);
359
+ return { block, memoryCount: entries.length, estimatedTokens };
361
360
  })(), cfg.autoRecallTimeoutMs, "openviking: auto-recall search timeout");
362
361
  }