@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/INSTALL-AGENT.md CHANGED
@@ -42,6 +42,7 @@ Before setup, determine:
42
42
  | --- | --- | --- |
43
43
  | OpenViking base URL | Yes | Ask user or read `OPENVIKING_BASE_URL` |
44
44
  | API key | Usually | Ask user or read `OPENVIKING_API_KEY` |
45
+ | Memory scope | Optional; default `none` | Ask whether memory is shared, separated by assistant, or separated by sender. Use `sender`; accept legacy `person` as an alias. |
45
46
  | Account ID | Only for root API keys | Ask user if setup reports root-key tenant context is needed |
46
47
  | User ID | Only for root API keys | Ask user if setup reports root-key tenant context is needed |
47
48
  | Slot replacement approval | Only if another context engine owns the slot | Ask user before using `--force-slot` |
@@ -73,6 +74,23 @@ Run setup with `--json` whenever possible:
73
74
  openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --json
74
75
  ```
75
76
 
77
+ Choose memory scope before building that command:
78
+
79
+ | Value | Concrete layout | Use when |
80
+ | --- | --- | --- |
81
+ | `none` (default; omit the flag) | Shared `viking://user/<user_id>/memories/...`; no concrete peer memory subtree | General case |
82
+ | `assistant` | Assistant-attributed peer memory under `.../peers/<assistant_id>/memories/...` | A human is the OpenViking user and uses multiple OpenClaw assistants |
83
+ | `sender` | Sender-attributed peer memory under `.../peers/<sender_id>/memories/...` | An agent is the OpenViking user and receives messages from multiple humans |
84
+
85
+ `person` is a legacy alias for `sender`. Accept it from existing config or user input, but write `sender` in new setup commands. OpenViking initializes the managed `peers/` container for every user; `none` only means that no concrete `peers/<peer_id>/memories` subtree is used. Actor-peer recall includes shared user memory plus the current peer memory, and changing the scope does not move existing memories.
86
+
87
+ Examples:
88
+
89
+ ```bash
90
+ openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --peer-role assistant --json
91
+ openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --peer-role sender --json
92
+ ```
93
+
76
94
  Branch on the result:
77
95
 
78
96
  | JSON result | Meaning | Agent action |
@@ -96,10 +114,10 @@ openclaw openviking setup \
96
114
  --json
97
115
  ```
98
116
 
99
- Custom agent routing prefix (optional; only when the user explicitly requests a prefix):
117
+ Custom assistant routing prefix (optional; only with `peer_role=assistant` and when the user explicitly requests a prefix):
100
118
 
101
119
  ```bash
102
- openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --peer-prefix <PREFIX> --json
120
+ openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --peer-role assistant --peer-prefix <PREFIX> --json
103
121
  ```
104
122
 
105
123
  Slot replacement retry, only after user approval:
@@ -139,6 +157,7 @@ Also inspect:
139
157
  | `health.version` | Records server version |
140
158
  | `health.compatibility` | Determines whether to warn |
141
159
  | `config.hasApiKey` | Confirms whether an API key was saved |
160
+ | `config.peer_role` | Confirms `none`, `assistant`, or canonical `sender` scope |
142
161
  | `config.peer_prefix` | Confirms configured peer prefix when present |
143
162
 
144
163
  ## Environment Detection
@@ -267,7 +286,8 @@ Core fields:
267
286
  | `mode` | Legacy compatibility field. Expected value: `remote`. |
268
287
  | `baseUrl` | OpenViking HTTP endpoint |
269
288
  | `apiKey` | OpenViking API key |
270
- | `peer_prefix` | Optional; prefix for OpenClaw agent IDs when set. Interactive setup accepts only letters, digits, `_`, and `-`. If unset, the plugin follows session agent IDs. |
289
+ | `peer_role` | `none` (shared user memory), `assistant` (`peers/<assistant_id>`), or `sender` (`peers/<sender_id>`). Legacy `person` is accepted as `sender`. |
290
+ | `peer_prefix` | Optional only with `peer_role=assistant`; prefix for OpenClaw agent IDs. Interactive setup accepts only letters, digits, `_`, and `-`. If unset, the plugin follows session agent IDs. |
271
291
  | `accountId` | Required for root API keys |
272
292
  | `userId` | Required for root API keys |
273
293
 
@@ -329,7 +349,8 @@ Existing config fields are preserved during migration. The new plugin reads old
329
349
 
330
350
  - `baseUrl`
331
351
  - `apiKey`
332
- - `peer_prefix`: optional; interactive setup accepts only letters, digits, `_`, and `-`
352
+ - `peer_role`: optional; `none`, `assistant`, or `sender`; legacy `person` is accepted as `sender`
353
+ - `peer_prefix`: optional only with `peer_role=assistant`; interactive setup accepts only letters, digits, `_`, and `-`
333
354
 
334
355
  ## Backup Path: ov-install
335
356
 
package/INSTALL-ZH.md CHANGED
@@ -197,12 +197,30 @@ openclaw openviking setup \
197
197
  openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --force-slot --json
198
198
  ```
199
199
 
200
- 如需给默认 assistant peer 路由增加前缀,可以额外传:
200
+ 根据 OpenViking user 代表谁来选择 `--peer-role`:
201
+
202
+ | 值 | 存储示例 | 适用场景 |
203
+ | --- | --- | --- |
204
+ | `none`(默认) | `viking://user/alice/memories/...` | 该 OpenViking 用户下的所有对话共享 user-level 记忆,不使用具体 peer 的记忆子树。 |
205
+ | `assistant` | `viking://user/alice/peers/main/memories/...` | OpenViking user 代表人,并希望把 assistant 归因的 peer 记忆按不同 OpenClaw 助手分开。 |
206
+ | `sender` | `viking://user/support-agent/peers/customer-42/memories/...` | OpenViking user 代表 agent,并希望把 sender 归因的 peer 记忆按不同发送者分开。 |
207
+
208
+ `person` 仍作为 `sender` 的旧配置别名被兼容;新配置请使用 `sender`。
209
+
210
+ 例如,让每个助手使用独立的 peer 记忆,并可选给 assistant id 加前缀:
201
211
 
202
212
  ```bash
203
213
  openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --peer-role assistant --peer-prefix <PREFIX> --json
204
214
  ```
205
215
 
216
+ 如果 OpenViking user 是 agent,按给它发消息的 sender 分开 peer 记忆:
217
+
218
+ ```bash
219
+ openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --peer-role sender --json
220
+ ```
221
+
222
+ OpenViking 会为每个用户初始化受管的 `peers/` 容器。`none` 表示插件不创建、也不路由到具体的 `peers/<peer_id>/memories` 子树。Actor-peer 召回同时包含用户共享记忆和当前 peer 记忆;切换配置不会搬迁已有记忆。
223
+
206
224
  #### 无法执行 CLI 时直接配置文件
207
225
 
208
226
  如果容器内无法执行 `openclaw` CLI,可以把以下字段合并到 OpenClaw 实际读取的配置文件。设置了 `OPENCLAW_CONFIG_PATH` 时使用该路径;否则通常是 `$OPENCLAW_STATE_DIR/openclaw.json`(默认 `~/.openclaw/openclaw.json`)。
@@ -300,7 +318,7 @@ plugins.entries.openviking.config
300
318
  | `mode` | `remote` | 兼容旧配置的字段。当前只支持 remote。 |
301
319
  | `baseUrl` | `http://127.0.0.1:1933` | OpenViking HTTP 地址 |
302
320
  | `apiKey` | 空 | OpenViking API key |
303
- | `peer_role` | `assistant` | Peer 身份模式:`none`、`assistant` 或 `person`。Session message 使用 body `peer_id`;数据面 recall/search 使用 `X-OpenViking-Actor-Peer`。 |
321
+ | `peer_role` | `none` | 记忆归属:`none`(共享 `viking://user/<user_id>/memories`)、`assistant`(`.../peers/<assistant_id>/memories`)或 `sender`(`.../peers/<sender_id>/memories`)。旧值 `person` 作为 `sender` 的别名兼容。Session message 使用 body `peer_id`;数据面 recall/search 使用 `X-OpenViking-Actor-Peer`。 |
304
322
  | `peer_prefix` | 空 | `peer_role=assistant` 时 assistant `peer_id` / actor peer 值的可选前缀。 |
305
323
  | `accountId` | 空 | 使用 root API key 时需要 |
306
324
  | `userId` | 空 | 使用 root API key 时需要 |
@@ -325,7 +343,7 @@ openclaw config get plugins.entries.openviking.config
325
343
  | --- | --- | --- |
326
344
  | `baseUrl` | `http://127.0.0.1:1933` | 远端 OpenViking 服务地址 |
327
345
  | `apiKey` | 空 | 远端 OpenViking API Key;服务端未开启认证时可不填 |
328
- | `peer_role` | `assistant` | Peer 身份模式:`none`、`assistant` 或 `person`;session message 使用 body `peer_id`,数据面 recall/search 使用 `X-OpenViking-Actor-Peer` |
346
+ | `peer_role` | `none` | 记忆归属:`none`、`assistant` 或 `sender`;旧值 `person` 作为 `sender` 的别名兼容。Session message 使用 body `peer_id`,数据面 recall/search 使用 `X-OpenViking-Actor-Peer` |
329
347
  | `peer_prefix` | 空 | `peer_role=assistant` 时 assistant `peer_id` / actor peer 值的可选前缀 |
330
348
 
331
349
  常见设置:
@@ -387,7 +405,7 @@ ov-install
387
405
  | `--current-version` | 查看 helper 记录的当前版本 |
388
406
  | `--base-url URL` | OpenViking 服务器地址(启用非交互模式) |
389
407
  | `--api-key KEY` | OpenViking API key |
390
- | `--peer-role ROLE` | Peer role:`none`、`assistant` 或 `person` |
408
+ | `--peer-role ROLE` | 记忆归属:`none`、`assistant` 或 `sender`;旧值 `person` 作为 `sender` 的别名兼容 |
391
409
  | `--peer-prefix PREFIX` | assistant `peer_id` / actor peer 值的前缀 |
392
410
  | `--update` | 更新 helper 管理的安装 |
393
411
 
package/INSTALL.md CHANGED
@@ -141,12 +141,30 @@ If another context engine already owns the slot, setup will not replace it by de
141
141
  openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --force-slot --json
142
142
  ```
143
143
 
144
- If you want assistant messages to carry a prefixed `peer_id` and data-plane recall/search requests to use the matching actor peer view, pass a prefix explicitly:
144
+ Choose `--peer-role` from what the OpenViking user represents:
145
+
146
+ | Value | Storage example | Use when |
147
+ | --- | --- | --- |
148
+ | `none` (default) | `viking://user/alice/memories/...` | All conversations for this OpenViking user share user-level memory. No peer-specific memory subtree is used. |
149
+ | `assistant` | `viking://user/alice/peers/main/memories/...` | The OpenViking user is a human and assistant-attributed peer memory should be separated by OpenClaw assistant. |
150
+ | `sender` | `viking://user/support-agent/peers/customer-42/memories/...` | The OpenViking user is an agent and sender-attributed peer memory should be separated by human sender. |
151
+
152
+ `person` is still accepted as a legacy alias for `sender`. New configuration should use `sender`.
153
+
154
+ For example, give each assistant its own peer memory and optionally namespace the assistant id:
145
155
 
146
156
  ```bash
147
157
  openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --peer-role assistant --peer-prefix <PREFIX> --json
148
158
  ```
149
159
 
160
+ Or scope peer memory by the sender talking to an agent:
161
+
162
+ ```bash
163
+ openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --peer-role sender --json
164
+ ```
165
+
166
+ OpenViking initializes the managed `peers/` container for every user. `none` means the plugin does not create or route into a concrete `peers/<peer_id>/memories` subtree. Actor-peer recall includes shared user memory plus the current peer memory, and changing the setting does not move existing memories.
167
+
150
168
  #### Configure The File Directly When The CLI Is Unavailable
151
169
 
152
170
  If the `openclaw` CLI cannot run inside the container, merge the following fields into the config file that OpenClaw actually reads. Use `OPENCLAW_CONFIG_PATH` when it is set; otherwise the file is usually `$OPENCLAW_STATE_DIR/openclaw.json` (default: `~/.openclaw/openclaw.json`).
@@ -244,7 +262,7 @@ Core fields:
244
262
  | `mode` | `remote` | Legacy compatibility field. Only remote mode is supported. |
245
263
  | `baseUrl` | `http://127.0.0.1:1933` | OpenViking HTTP endpoint |
246
264
  | `apiKey` | empty | OpenViking API key |
247
- | `peer_role` | `assistant` | Peer identity mode: `none`, `assistant`, or `person`. Session messages use body `peer_id`; data-plane recall/search uses `X-OpenViking-Actor-Peer`. |
265
+ | `peer_role` | `none` | Memory scope: `none` (shared `viking://user/<user_id>/memories`), `assistant` (`.../peers/<assistant_id>/memories`), or `sender` (`.../peers/<sender_id>/memories`). Legacy `person` is accepted as `sender`. Session messages use body `peer_id`; data-plane recall/search uses `X-OpenViking-Actor-Peer`. |
248
266
  | `peer_prefix` | empty | Optional prefix for assistant `peer_id` / actor peer values when `peer_role=assistant`. |
249
267
  | `accountId` | empty | Required when using a root API key |
250
268
  | `userId` | empty | Required when using a root API key |
@@ -311,7 +329,7 @@ Useful backup/source flags:
311
329
  | `--current-version` | Print the version tracked by the helper |
312
330
  | `--base-url URL` | OpenViking server URL (enables non-interactive mode) |
313
331
  | `--api-key KEY` | OpenViking API key |
314
- | `--peer-role ROLE` | Peer role: `none`, `assistant`, or `person` |
332
+ | `--peer-role ROLE` | Memory scope: `none`, `assistant`, or `sender`; legacy `person` is accepted as `sender` |
315
333
  | `--peer-prefix PREFIX` | Prefix for assistant `peer_id` / actor peer values |
316
334
  | `--update` | Update an existing helper-managed install |
317
335
 
@@ -356,7 +374,7 @@ The plugin connects to an existing remote OpenViking server.
356
374
  | --- | --- | --- |
357
375
  | `baseUrl` | `http://127.0.0.1:1933` | Remote OpenViking HTTP endpoint |
358
376
  | `apiKey` | empty | Optional OpenViking API key |
359
- | `peer_role` | `assistant` | Peer identity mode: `none`, `assistant`, or `person`; session messages use body `peer_id`, while data-plane recall/search uses `X-OpenViking-Actor-Peer` |
377
+ | `peer_role` | `none` | Memory scope: `none`, `assistant`, or `sender`; legacy `person` is accepted as `sender`. Session messages use body `peer_id`, while data-plane recall/search uses `X-OpenViking-Actor-Peer` |
360
378
  | `peer_prefix` | empty | Optional prefix for assistant `peer_id` / actor peer values when `peer_role=assistant` |
361
379
 
362
380
  Common settings:
package/README.md CHANGED
@@ -124,15 +124,26 @@ The main rules are:
124
124
  - reuse `sessionId` directly when it is already a UUID
125
125
  - prefer `sessionKey` when deriving a stable `ovSessionId`
126
126
  - normalize unsafe path characters, or fall back to a stable SHA-256 when needed
127
- - `peer_role=assistant` is the default and writes assistant messages with `peer_id=<sessionAgent>`; if `peer_prefix` is set, the value becomes `<peer_prefix>_<sessionAgent>`
128
- - `peer_role=none` disables peer message attribution and actor-peer routing
129
- - `peer_role=person` writes user messages with `peer_id` derived from OpenClaw sender identity; assistant messages do not get `peer_id`
130
- - data-plane recall/search/read/import/delete sends the same resolved peer identity as `X-OpenViking-Actor-Peer` when `peer_role` is `assistant` or `person`
127
+ - `peer_role=none` is the default: messages have no peer attribution and memory stays in the shared user scope, for example `viking://user/alice/memories/...`; no peer-specific memory subtree is used
128
+ - `peer_role=assistant` writes assistant messages with `peer_id=<sessionAgent>` and uses peer-scoped memory such as `viking://user/alice/peers/main/memories/...`; if `peer_prefix` is set, the peer id becomes `<peer_prefix>_<sessionAgent>`
129
+ - `peer_role=sender` writes user messages with the OpenClaw sender identity as `peer_id` and uses peer-scoped memory such as `viking://user/support-agent/peers/customer-42/memories/...`; assistant messages do not get `peer_id`
130
+ - `person` remains accepted as a legacy config alias for `sender`, but new configuration and documentation use `sender`
131
+ - data-plane recall/search/read/import/delete sends the same resolved peer identity as `X-OpenViking-Actor-Peer` when `peer_role` is `assistant` or `sender`
131
132
  - when OpenClaw does not provide a session agent, use its default agent `main` for local session and assistant peer metadata
132
133
  - only add `X-OpenViking-Account` / `X-OpenViking-User` when `accountId` / `userId` are explicitly configured
133
134
 
134
135
  This matters because OpenViking tenant identity is account/user-scoped, while OpenClaw agent identity is runtime metadata.
135
136
 
137
+ Choose the scope from what `viking://user/<user_id>` represents:
138
+
139
+ | Model | Example | Result |
140
+ | --- | --- | --- |
141
+ | General/shared (`none`) | `user_id=alice` uses any OpenClaw assistant | Shared user memory under `viking://user/alice/memories/...` |
142
+ | Human is the OpenViking user (`assistant`) | Alice uses OpenClaw assistants `main` and `research` | Assistant-scoped memories are separated under `.../peers/main/memories/...` and `.../peers/research/memories/...` |
143
+ | Agent is the OpenViking user (`sender`) | `user_id=support-agent` receives messages from `customer-42` and `customer-99` | Sender-scoped memories are separated under `.../peers/customer-42/memories/...` and `.../peers/customer-99/memories/...` |
144
+
145
+ OpenViking creates the managed `peers/` container as part of the user namespace. `none` means that the plugin does not create or route into a specific `peers/<peer_id>/memories` subtree. With `assistant` or `sender`, actor-peer recall includes the shared user memory plus the current peer's memory; changing the setting does not move existing memories.
146
+
136
147
  The recommended remote-mode configuration only needs:
137
148
 
138
149
  - `baseUrl`
@@ -143,7 +154,7 @@ The recommended remote-mode configuration only needs:
143
154
  In this setup:
144
155
 
145
156
  - `apiKey` should usually be a user key
146
- - new installs default to `peer_role=assistant`
157
+ - new installs default to `peer_role=none`
147
158
  - `accountId` / `userId` are advanced options only when the deployment needs explicit identity headers, such as root-key or trusted-server flows
148
159
 
149
160
  ### User namespace
@@ -164,16 +175,9 @@ During recall, the plugin:
164
175
  1. Extracts query text from the latest user message.
165
176
  2. Resolves the agent routing for the current `sessionId/sessionKey`.
166
177
  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
178
+ 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).
179
+ 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.
180
+ 6. Prepends the server-rendered context inside `<relevant-memories>` to the current user message; it does not append a standalone synthetic user message.
177
181
 
178
182
  ## Session Lifecycle
179
183
 
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",