@openviking/openclaw-plugin 2026.6.18 → 2026.7.15

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.
Files changed (61) hide show
  1. package/INSTALL-AGENT.md +2 -2
  2. package/INSTALL-ZH.md +2 -2
  3. package/INSTALL.md +2 -2
  4. package/README.md +1 -1
  5. package/auto-recall.ts +6 -6
  6. package/client.ts +14 -6
  7. package/commands/setup.ts +40 -10
  8. package/config.ts +44 -8
  9. package/context-engine.ts +2 -1
  10. package/dist/auto-recall.js +5 -6
  11. package/dist/client.js +11 -4
  12. package/dist/commands/setup.js +26 -10
  13. package/dist/config.js +31 -6
  14. package/dist/context-engine.js +2 -1
  15. package/dist/index.js +4 -4
  16. package/dist/plugin/openviking-archive-tools.js +1 -2
  17. package/dist/plugin/openviking-client-runtime.js +7 -1
  18. package/dist/plugin/openviking-command-definitions.js +3 -3
  19. package/dist/plugin/openviking-import-tools.js +2 -2
  20. package/dist/plugin/openviking-memory-recall-tools.js +4 -4
  21. package/dist/plugin/openviking-memory-tools.js +12 -7
  22. package/dist/plugin/openviking-query-runtime.js +1 -1
  23. package/dist/plugin/openviking-query-tools.js +4 -4
  24. package/dist/plugin/openviking-recall-trace-runtime.js +3 -3
  25. package/dist/plugin/openviking-session-routing-runtime.js +9 -2
  26. package/dist/plugin/openviking-tool-result-tools.js +3 -3
  27. package/dist/query-config.js +7 -1
  28. package/dist/request-headers.js +19 -0
  29. package/dist/routing/identity-routing.js +27 -0
  30. package/dist/routing/memory-uri.js +1 -0
  31. package/dist/services/context-lifecycle-service.js +56 -23
  32. package/dist/services/context-message-adapter.js +2 -9
  33. package/dist/text-utils.js +1 -6
  34. package/index.ts +3 -3
  35. package/install-manifest.json +2 -1
  36. package/openclaw.plugin.json +20 -4
  37. package/package.json +6 -6
  38. package/plugin/command-registration.ts +2 -0
  39. package/plugin/openviking-archive-tools.ts +1 -4
  40. package/plugin/openviking-client-runtime.ts +8 -1
  41. package/plugin/openviking-command-definitions.ts +4 -3
  42. package/plugin/openviking-import-tools.ts +4 -2
  43. package/plugin/openviking-memory-recall-tools.ts +6 -4
  44. package/plugin/openviking-memory-tools.ts +19 -11
  45. package/plugin/openviking-query-runtime.ts +1 -1
  46. package/plugin/openviking-query-tools.ts +6 -4
  47. package/plugin/openviking-recall-trace-runtime.ts +4 -3
  48. package/plugin/openviking-recall-trace-tools.ts +2 -0
  49. package/plugin/openviking-session-routing-runtime.ts +15 -1
  50. package/plugin/openviking-tool-result-tools.ts +0 -6
  51. package/query-config.ts +7 -1
  52. package/request-headers.ts +29 -0
  53. package/routing/identity-routing.ts +41 -0
  54. package/routing/memory-uri.ts +1 -0
  55. package/services/context-lifecycle-service.ts +69 -25
  56. package/services/context-message-adapter.ts +2 -9
  57. package/services/setup/probe-service.ts +35 -6
  58. package/services/setup/setup-flow.ts +61 -7
  59. package/skills/install-openviking-memory/SKILL.md +29 -15
  60. package/skills/openviking-context-database/SKILL.md +3 -3
  61. package/text-utils.ts +1 -6
@@ -1,4 +1,5 @@
1
1
  import type { OVMessage } from "../client.js";
2
+ import { sanitizeOpenVikingPeerId } from "../routing/identity-routing.js";
2
3
  import { sanitizeToolUseResultPairing } from "../session-transcript-repair.js";
3
4
 
4
5
  export type AgentMessage = {
@@ -8,15 +9,7 @@ export type AgentMessage = {
8
9
  };
9
10
 
10
11
  export function toRoleId(senderId: string | undefined): string | undefined {
11
- if (!senderId) {
12
- return undefined;
13
- }
14
- const normalized = senderId
15
- .trim()
16
- .replace(/[^a-zA-Z0-9_-]+/g, "_")
17
- .replace(/^_+|_+$/g, "")
18
- .replace(/_+/g, "_");
19
- return normalized || undefined;
12
+ return sanitizeOpenVikingPeerId(senderId);
20
13
  }
21
14
 
22
15
  /**
@@ -1,3 +1,8 @@
1
+ import {
2
+ resolveOpenVikingRequestHeaders,
3
+ type OpenVikingRequestHeaders,
4
+ } from "../../request-headers.js";
5
+
1
6
  export type SetupNetworkTransport = (url: string, init: RequestInit) => Promise<Response>;
2
7
 
3
8
  export type SetupVersionCompatibility = "compatible" | "server_too_old" | "server_too_new" | "unknown";
@@ -27,8 +32,16 @@ export type SetupNetworkProbeOptions = {
27
32
  };
28
33
 
29
34
  export type SetupNetworkProbes = {
30
- probeApiKeyType: (baseUrl: string, apiKey?: string) => Promise<SetupApiKeyProbeResult>;
31
- checkServiceHealth: (baseUrl: string, apiKey?: string) => Promise<SetupHealthResult>;
35
+ probeApiKeyType: (
36
+ baseUrl: string,
37
+ apiKey?: string,
38
+ headers?: OpenVikingRequestHeaders,
39
+ ) => Promise<SetupApiKeyProbeResult>;
40
+ checkServiceHealth: (
41
+ baseUrl: string,
42
+ apiKey?: string,
43
+ headers?: OpenVikingRequestHeaders,
44
+ ) => Promise<SetupHealthResult>;
32
45
  };
33
46
 
34
47
  export const defaultSetupNetworkTransport: SetupNetworkTransport = (url, init) => fetch(url, init);
@@ -41,7 +54,11 @@ export function createSetupNetworkProbes({
41
54
  timeoutMs = 10_000,
42
55
  }: SetupNetworkProbeOptions): SetupNetworkProbes {
43
56
  return {
44
- async probeApiKeyType(baseUrl: string, apiKey?: string): Promise<SetupApiKeyProbeResult> {
57
+ async probeApiKeyType(
58
+ baseUrl: string,
59
+ apiKey?: string,
60
+ configuredHeaders?: OpenVikingRequestHeaders,
61
+ ): Promise<SetupApiKeyProbeResult> {
45
62
  if (!apiKey) {
46
63
  return { keyType: "no_key", needsAccountId: false, needsUserId: false, detail: "No API key configured" };
47
64
  }
@@ -50,7 +67,10 @@ export function createSetupNetworkProbes({
50
67
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
51
68
  const sessionsUrl = `${baseUrl.replace(/\/+$/, "")}/api/v1/sessions?limit=1`;
52
69
  try {
53
- const headers: Record<string, string> = { "X-API-Key": apiKey };
70
+ const configuredRequestHeaders = resolveOpenVikingRequestHeaders({ headers: configuredHeaders });
71
+ const headers: Record<string, string> = {};
72
+ headers["X-API-Key"] = apiKey;
73
+ Object.assign(headers, configuredRequestHeaders);
54
74
  const response = await transport(sessionsUrl, {
55
75
  headers,
56
76
  signal: controller.signal,
@@ -82,6 +102,7 @@ export function createSetupNetworkProbes({
82
102
  "X-API-Key": apiKey,
83
103
  "X-OpenViking-Account": "__probe__",
84
104
  "X-OpenViking-User": "__probe__",
105
+ ...configuredRequestHeaders,
85
106
  };
86
107
  const probe2 = await transport(sessionsUrl, {
87
108
  headers: probeHeaders,
@@ -111,12 +132,20 @@ export function createSetupNetworkProbes({
111
132
  }
112
133
  },
113
134
 
114
- async checkServiceHealth(baseUrl: string, apiKey?: string): Promise<SetupHealthResult> {
135
+ async checkServiceHealth(
136
+ baseUrl: string,
137
+ apiKey?: string,
138
+ configuredHeaders?: OpenVikingRequestHeaders,
139
+ ): Promise<SetupHealthResult> {
115
140
  const controller = new AbortController();
116
141
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
117
142
  try {
143
+ const configuredRequestHeaders = resolveOpenVikingRequestHeaders({ headers: configuredHeaders });
118
144
  const headers: Record<string, string> = {};
119
- if (apiKey) headers["X-API-Key"] = apiKey;
145
+ if (apiKey) {
146
+ headers["X-API-Key"] = apiKey;
147
+ }
148
+ Object.assign(headers, configuredRequestHeaders);
120
149
  const response = await transport(`${baseUrl.replace(/\/+$/, "")}/health`, {
121
150
  headers,
122
151
  signal: controller.signal,
@@ -6,6 +6,10 @@ import {
6
6
  type SetupIO,
7
7
  type SlotActivationResult,
8
8
  } from "./config-writer.js";
9
+ import {
10
+ cleanOpenVikingRequestHeaders,
11
+ type OpenVikingRequestHeaders,
12
+ } from "../../request-headers.js";
9
13
 
10
14
  export type { SetupIO, SlotActivationResult };
11
15
 
@@ -38,6 +42,7 @@ export type SetupResult = {
38
42
  peer_prefix?: string;
39
43
  accountId?: string;
40
44
  userId?: string;
45
+ headers?: OpenVikingRequestHeaders;
41
46
  recallTargetTypes?: string[];
42
47
  };
43
48
  health?: HealthResult;
@@ -65,6 +70,7 @@ export type StatusResult = {
65
70
  export type SetupParams = {
66
71
  baseUrl: string;
67
72
  apiKey?: string;
73
+ headers?: OpenVikingRequestHeaders;
68
74
  peerRole?: "none" | "assistant" | "person";
69
75
  peerPrefix?: string;
70
76
  accountId?: string;
@@ -78,6 +84,7 @@ export type InteractiveRemoteConfigParams = {
78
84
  existing?: Record<string, unknown> | null;
79
85
  baseUrl: string;
80
86
  apiKey?: string;
87
+ headers?: OpenVikingRequestHeaders;
81
88
  peerRole?: "none" | "assistant" | "person";
82
89
  peerPrefix?: string;
83
90
  accountId?: string;
@@ -100,8 +107,16 @@ export type OpenVikingSetupService = {
100
107
  export type OpenVikingSetupServiceDependencies = {
101
108
  io: SetupIO;
102
109
  defaultRemoteUrl?: string;
103
- checkServiceHealth: (baseUrl: string, apiKey?: string) => Promise<HealthResult>;
104
- probeApiKeyType: (baseUrl: string, apiKey?: string) => Promise<ApiKeyProbeResult>;
110
+ checkServiceHealth: (
111
+ baseUrl: string,
112
+ apiKey?: string,
113
+ headers?: OpenVikingRequestHeaders,
114
+ ) => Promise<HealthResult>;
115
+ probeApiKeyType: (
116
+ baseUrl: string,
117
+ apiKey?: string,
118
+ headers?: OpenVikingRequestHeaders,
119
+ ) => Promise<ApiKeyProbeResult>;
105
120
  };
106
121
 
107
122
  export function maskKey(key: string): string {
@@ -114,10 +129,38 @@ export function isLegacyLocalMode(existing: Record<string, unknown>): boolean {
114
129
  return mode !== "remote";
115
130
  }
116
131
 
132
+ function nonEmptyOpenVikingRequestHeaders(value: unknown): OpenVikingRequestHeaders | undefined {
133
+ const headers = cleanOpenVikingRequestHeaders(value);
134
+ return Object.keys(headers).length > 0 ? headers : undefined;
135
+ }
136
+
137
+ function callHealthProbe(
138
+ checkServiceHealth: OpenVikingSetupServiceDependencies["checkServiceHealth"],
139
+ baseUrl: string,
140
+ apiKey: string | undefined,
141
+ headers: OpenVikingRequestHeaders | undefined,
142
+ ): Promise<HealthResult> {
143
+ return headers
144
+ ? checkServiceHealth(baseUrl, apiKey, headers)
145
+ : checkServiceHealth(baseUrl, apiKey);
146
+ }
147
+
148
+ function callApiKeyProbe(
149
+ probeApiKeyType: OpenVikingSetupServiceDependencies["probeApiKeyType"],
150
+ baseUrl: string,
151
+ apiKey: string | undefined,
152
+ headers: OpenVikingRequestHeaders | undefined,
153
+ ): Promise<ApiKeyProbeResult> {
154
+ return headers
155
+ ? probeApiKeyType(baseUrl, apiKey, headers)
156
+ : probeApiKeyType(baseUrl, apiKey);
157
+ }
158
+
117
159
  export function buildInteractiveRemotePluginConfig({
118
160
  existing,
119
161
  baseUrl,
120
162
  apiKey,
163
+ headers,
121
164
  peerRole,
122
165
  peerPrefix,
123
166
  accountId,
@@ -138,6 +181,11 @@ export function buildInteractiveRemotePluginConfig({
138
181
  else delete pluginCfg.accountId;
139
182
  if (userId) pluginCfg.userId = userId;
140
183
  else delete pluginCfg.userId;
184
+ if (headers !== undefined) {
185
+ const cleanHeaders = nonEmptyOpenVikingRequestHeaders(headers);
186
+ if (cleanHeaders) pluginCfg.headers = cleanHeaders;
187
+ else delete pluginCfg.headers;
188
+ }
141
189
  delete pluginCfg.configPath;
142
190
  delete pluginCfg.port;
143
191
  return pluginCfg;
@@ -153,8 +201,9 @@ export function createOpenVikingSetupService({
153
201
  async setupNonInteractive(configPath: string, params: SetupParams): Promise<SetupResult> {
154
202
  try {
155
203
  const { baseUrl, apiKey, peerRole, peerPrefix, accountId, userId, recallTargetTypes, allowOffline, forceSlot } = params;
204
+ const headers = nonEmptyOpenVikingRequestHeaders(params.headers);
156
205
 
157
- const health = await checkServiceHealth(baseUrl, apiKey);
206
+ const health = await callHealthProbe(checkServiceHealth, baseUrl, apiKey, headers);
158
207
 
159
208
  if (!health.ok && !allowOffline) {
160
209
  return {
@@ -167,7 +216,7 @@ export function createOpenVikingSetupService({
167
216
  };
168
217
  }
169
218
 
170
- const keyProbe = health.ok ? await probeApiKeyType(baseUrl, apiKey) : undefined;
219
+ const keyProbe = health.ok ? await callApiKeyProbe(probeApiKeyType, baseUrl, apiKey, headers) : undefined;
171
220
 
172
221
  if (keyProbe?.keyType === "root_key" && (!accountId || !userId)) {
173
222
  const missing: string[] = [];
@@ -190,6 +239,7 @@ export function createOpenVikingSetupService({
190
239
 
191
240
  const pluginCfg: Record<string, unknown> = { mode: "remote", baseUrl };
192
241
  if (apiKey) pluginCfg.apiKey = apiKey;
242
+ if (headers && Object.keys(headers).length > 0) pluginCfg.headers = headers;
193
243
  if (peerRole) pluginCfg.peer_role = peerRole;
194
244
  if (peerPrefix) pluginCfg.peer_prefix = peerPrefix;
195
245
  if (accountId) pluginCfg.accountId = accountId;
@@ -203,6 +253,7 @@ export function createOpenVikingSetupService({
203
253
  mode: "remote",
204
254
  baseUrl,
205
255
  ...(apiKey ? { apiKey: maskKey(apiKey) } : {}),
256
+ ...(headers && Object.keys(headers).length > 0 ? { headers } : {}),
206
257
  ...(peerRole ? { peer_role: peerRole } : {}),
207
258
  ...(peerPrefix ? { peer_prefix: peerPrefix } : {}),
208
259
  ...(accountId ? { accountId } : {}),
@@ -253,7 +304,8 @@ export function createOpenVikingSetupService({
253
304
  async useExistingRemoteConfig(configPath: string, existing: Record<string, unknown>): Promise<SetupResult> {
254
305
  const baseUrl = String(existing.baseUrl ?? defaultRemoteUrl);
255
306
  const apiKey = existing.apiKey ? String(existing.apiKey) : undefined;
256
- const health = await checkServiceHealth(baseUrl, apiKey);
307
+ const headers = nonEmptyOpenVikingRequestHeaders(existing.headers);
308
+ const health = await callHealthProbe(checkServiceHealth, baseUrl, apiKey, headers);
257
309
  const slot = activateContextEngineSlot(configPath, false, io);
258
310
  return {
259
311
  success: true,
@@ -262,6 +314,7 @@ export function createOpenVikingSetupService({
262
314
  mode: String(existing.mode ?? "remote"),
263
315
  baseUrl,
264
316
  ...(apiKey ? { apiKey: maskKey(apiKey) } : {}),
317
+ ...(headers ? { headers } : {}),
265
318
  ...(existing.peer_role ? { peer_role: String(existing.peer_role) as "none" | "assistant" | "person" } : {}),
266
319
  ...(existing.peer_prefix ? { peer_prefix: String(existing.peer_prefix) } : {}),
267
320
  ...(existing.accountId ? { accountId: String(existing.accountId) } : {}),
@@ -283,8 +336,9 @@ export function createOpenVikingSetupService({
283
336
 
284
337
  const baseUrl = String(existing.baseUrl ?? defaultRemoteUrl);
285
338
  const apiKey = existing.apiKey ? String(existing.apiKey) : undefined;
286
- const health = await checkServiceHealth(baseUrl, apiKey);
287
- const keyProbe = health.ok ? await probeApiKeyType(baseUrl, apiKey) : undefined;
339
+ const headers = nonEmptyOpenVikingRequestHeaders(existing.headers);
340
+ const health = await callHealthProbe(checkServiceHealth, baseUrl, apiKey, headers);
341
+ const keyProbe = health.ok ? await callApiKeyProbe(probeApiKeyType, baseUrl, apiKey, headers) : undefined;
288
342
 
289
343
  return {
290
344
  configured: true,
@@ -91,8 +91,8 @@ openclaw --version
91
91
 
92
92
  **If `openclaw` is missing:**
93
93
 
94
- > (CN) 你的系统没有安装 OpenClaw。请先安装 OpenClaw(>= 2026.4.8),然后再回来找我。
95
- > (EN) OpenClaw is not installed. Please install OpenClaw (>= 2026.4.8) first, then come back.
94
+ > (CN) 你的系统没有安装 OpenClaw。请先安装 OpenClaw(>= 2026.5.27),然后再回来找我。
95
+ > (EN) OpenClaw is not installed. Please install OpenClaw (>= 2026.5.27) first, then come back.
96
96
 
97
97
  **Stop. Do NOT continue.**
98
98
 
@@ -413,20 +413,22 @@ Expected output:
413
413
  > (CN) 🎉 全部搞定!OpenViking 长期记忆已经接好了。
414
414
  >
415
415
  > 工作方式:
416
- > - **每一轮**:你的对话会被自动归档到 OpenViking session 里
417
- > - **`/compact` 时**:归档消息会在 OpenViking 服务端被抽取成长期记忆
416
+ > - **每一轮**:你的对话会被自动追加到 OpenViking session 里;自动抽取依赖阈值 commit 或 `/compact`
417
+ > - **明确要记住时**:如果你说“记住/保存/存一下”某个长期事实,我会用 `memory_store` 立即提交到记忆管线
418
+ > - **`/compact` 时**:待提交的 session 消息会在 OpenViking 服务端被抽取成长期记忆
418
419
  > - **后续会话**:每次回复前我都会自动搜一下相关记忆并带进上下文
419
420
  >
420
- > 想验证一下吗?跟我聊几句包含可记忆事实的话(例如"我的邮箱是 test@example.com"),然后调一下 `/compact`,再开新对话问"我的邮箱是?",应该就能召回了。
421
+ > 想验证一下吗?可以直接说“记住我的邮箱是 test@example.com”,让我通过 `memory_store` 立即提交;或者先聊几句包含可记忆事实的话,再调一下 `/compact`。之后开新对话问“我的邮箱是?”,应该就能召回了。
421
422
 
422
423
  > (EN) 🎉 All set! OpenViking long-term memory is connected.
423
424
  >
424
425
  > How it works:
425
- > - **Every turn**: our messages are auto-archived into an OpenViking session
426
- > - **On `/compact`**: archived messages are extracted into long-term memories on the OpenViking server
426
+ > - **Every turn**: our messages are appended to an OpenViking session; automatic extraction depends on a threshold commit or `/compact`
427
+ > - **Explicit remember requests**: if you ask me to remember/save/store a long-term fact, I can use `memory_store` to commit it immediately
428
+ > - **On `/compact`**: pending session messages are extracted into long-term memories on the OpenViking server
427
429
  > - **Future sessions**: relevant memories are auto-retrieved and injected before my replies
428
430
  >
429
- > Want to verify? Tell me a few facts (e.g. "my email is test@example.com"), run `/compact`, then start a new chat and ask "what's my email?" — it should recall.
431
+ > Want to verify? Say "remember my email is test@example.com" so I can commit it via `memory_store`, or tell me a few memory-worthy facts and run `/compact`. Then start a new chat and ask "what's my email?" — it should recall.
430
432
 
431
433
  ---
432
434
 
@@ -434,15 +436,25 @@ Expected output:
434
436
 
435
437
  ## How It Works
436
438
 
437
- The context-engine pipeline has three distinct stages — keep them apart, especially when telling users when memories become searchable long-term memory on the OpenViking server:
439
+ The context-engine pipeline has three distinct stages plus one explicit write path — keep them apart, especially when telling users when memories become searchable long-term memory on the OpenViking server:
438
440
 
439
- - **Archive / capture (context-engine `afterTurn`)**: at the end of a user turn, the plugin commits user/assistant messages to the OpenViking session via `POST /api/v1/sessions/.../messages`. This is **archive only** — no memory extraction yet. You'll see session message counts grow on the server, but no new files under `viking://user/.../memories/`.
440
- - **Memory extraction (on `/compact`)**: memory extraction runs when the user invokes OpenClaw's `/compact` command. The server-side extraction pipeline reads the archived session and writes new memories. The plugin's `after_compaction` hook is currently reserved; the extraction itself is driven by the server. **No `/compact`, no new memory files.**
441
+ - **Archive / capture (context-engine `afterTurn`)**: at the end of a user turn, the plugin appends user/assistant messages to the OpenViking session via `POST /api/v1/sessions/.../messages`. This is **session capture only** unless `pending_tokens` crosses `commitTokenThreshold`; below the threshold, no memory extraction runs yet. You'll see session message counts grow on the server, but no new files under `viking://user/.../memories/`.
442
+ - **Memory extraction (threshold commit or `/compact`)**: memory extraction runs after a session commit. The commit can be triggered asynchronously when `afterTurn` crosses `commitTokenThreshold`, synchronously when the user invokes OpenClaw's `/compact` command, or explicitly by `memory_store`. The server-side extraction pipeline reads the archived session and writes new memories.
441
443
  - `captureMode: "semantic"` (default): server extraction pipeline filters all qualifying text.
442
444
  - `captureMode: "keyword"`: only text matching trigger words (e.g. "remember", "preference") is considered.
443
445
  - **Auto-Recall (context-engine `assemble()`)**: before prompt context is assembled, the plugin queries OpenViking for relevant memories and injects them into context. Recall works even when there are no extracted memories yet — you just won't see anything come back.
444
446
 
445
- **Practical implication for testing**: if you write down a fact and immediately try to recall it without `/compact`, the plugin will only retrieve it as recent session context (archived messages), not as a long-term memory. To verify long-term memory cross-session, you must run `/compact` first.
447
+ **Practical implication for testing**: if you write down a short fact and immediately try to recall it without a threshold commit, `/compact`, or `memory_store`, the plugin may only retrieve it as recent session context, not as a long-term memory. To verify long-term memory cross-session deterministically, run `/compact` or use `memory_store` for the fact being tested.
448
+
449
+ ### Explicit long-term memory writes
450
+
451
+ Auto-capture is best-effort and commit-dependent. When the user explicitly says to remember, save, or store an important long-term fact, preference, project, or decision, the agent should call `memory_store` instead of waiting for ordinary auto-capture.
452
+
453
+ Use `memory_store` as the integration-side reliable path for durable-memory intent:
454
+
455
+ - It writes the supplied text into an OpenViking session and calls `commit(wait=true)`.
456
+ - It complements auto-capture; it does not replace normal session capture.
457
+ - If it commits but extracts 0 memories, the explicit path has done its job. Treat that as a server-side extraction/model/configuration issue and check OpenViking logs.
446
458
 
447
459
  ## Available Tools
448
460
 
@@ -468,6 +480,8 @@ Example: user asks "What programming language did I say I like?"
468
480
  | `role` | No | Session role (default `user`) |
469
481
  | `sessionId` | No | Existing OpenViking session ID |
470
482
 
483
+ Use this when the user explicitly asks to remember/save/store a long-term fact, preference, project, or decision.
484
+
471
485
  Example: user says "Remember my email is xxx@example.com".
472
486
 
473
487
  ### `memory_forget` — Delete Memories
@@ -578,7 +592,7 @@ These are the keys under `plugins.entries.openviking.config` in `openclaw.json`.
578
592
  | `userId` | — | Required when `apiKey` is a root key. |
579
593
  | `targetUri` | `viking://user/memories` | Default search scope URI. |
580
594
  | `timeoutMs` | (plugin default) | HTTP timeout for OpenViking calls. |
581
- | `autoCapture` | `true` | Auto-archive turn messages to OpenViking session at `afterTurn` (extraction itself runs on `/compact`, not here). |
595
+ | `autoCapture` | `true` | Auto-append turn messages to the OpenViking session at `afterTurn`; extraction runs only after a threshold commit, `/compact`, or explicit `memory_store`. |
582
596
  | `captureMode` | `"semantic"` | Filter mode used by the server-side extraction pipeline: `semantic` or `keyword`. |
583
597
  | `captureMaxLength` | `24000` | Max text length per archived turn. |
584
598
  | `autoRecall` | `true` | Auto-recall and inject memories before reply. |
@@ -589,7 +603,7 @@ These are the keys under `plugins.entries.openviking.config` in `openclaw.json`.
589
603
  | `recallMaxInjectedChars` | `4000` | Hard cap on injected character count. Complete memories that do not fit are skipped, not truncated. |
590
604
  | `recallPreferAbstract` | (plugin default) | Prefer abstract memories over raw. |
591
605
  | `recallTokenBudget` | deprecated | Compatibility alias for `recallMaxInjectedChars`. |
592
- | `commitTokenThreshold` | `20000` | Pending-token threshold for async commit after turns; `0` commits every turn. |
606
+ | `commitTokenThresholdRatio` | `0.5` | Async-commit threshold as a fraction (0-1) of the model context window (e.g. 0.5 = 50%); `0` commits every turn. |
593
607
  | `commitKeepRecentCount` | `10` | Recent messages kept live after afterTurn commit. Compact always uses `0`. |
594
608
  | `bypassSessionPatterns` | — | Glob patterns for sessions skipped by capture. |
595
609
  | `ingestReplyAssist` | (plugin default) | Reply-assist ingestion toggle. |
@@ -722,7 +736,7 @@ Match against actual stderr / JSON `error` strings.
722
736
 
723
737
  | Symptom (exact string) | Likely cause | Fix |
724
738
  |---|---|---|
725
- | `command not found: openclaw` | OpenClaw not installed | Stop. Ask user to install OpenClaw >= 2026.4.8. |
739
+ | `command not found: openclaw` | OpenClaw not installed | Stop. Ask user to install OpenClaw >= 2026.5.27. |
726
740
  | `command not found: node` / `'node' is not recognized` | Node missing | Stop. Ask user to install Node.js >= 22. |
727
741
  | `429` / `rate limit` / `Too Many Requests` from `plugins install` | ClawHub throttle | Switch to backup Path B (`ov-install`). |
728
742
  | `not logged in` / `please log in` from `plugins install` | Anonymous user hit limit | Switch to backup Path B (`ov-install`). |
@@ -42,7 +42,7 @@ OpenClaw owns agent execution, prompts, and tool invocation. OpenViking owns lon
42
42
  | Layer | Current behavior |
43
43
  |---|---|
44
44
  | `assemble` | Rebuilds compressed session history from OpenViking and injects relevant recall into the latest user message. |
45
- | `afterTurn` | Appends only the new turn to the OpenViking session; may trigger async commit when `pending_tokens >= commitTokenThreshold`. |
45
+ | `afterTurn` | Appends only the new turn to the OpenViking session; may trigger async commit when `pending_tokens >= tokenBudget * commitTokenThresholdRatio`. |
46
46
  | `compact` | Runs `commit(wait=true)`, waits for archive/extraction completion, and reads back latest archive overview. |
47
47
  | Tools | Memory recall/store/forget, archive search/expand, resource/skill import/search, recall trace query, tool-result list/search/read. |
48
48
 
@@ -77,7 +77,7 @@ Core config lives under `plugins.entries.openviking.config`:
77
77
  | `recallLimit` | `6` | Max selected recall items. |
78
78
  | `recallScoreThreshold` | `0.15` | Min score after post-processing. |
79
79
  | `recallMaxInjectedChars` | `4000` | Total injected character cap; complete memories that do not fit are skipped. |
80
- | `commitTokenThreshold` | `20000` | Pending-token threshold for async commit after turns; `0` commits every turn. |
80
+ | `commitTokenThresholdRatio` | `0.5` | Async-commit threshold as a fraction (0-1) of the model context window (e.g. 0.5 = 50%); `0` commits every turn. |
81
81
  | `commitKeepRecentCount` | `10` | Recent messages kept live after afterTurn commit. Compact always uses `0`. |
82
82
  | `bypassSessionPatterns` | empty | Glob-like session keys that completely bypass OpenViking (`*` segment, `**` multi-segment). |
83
83
  | `emitStandardDiagnostics` | `false` | Emit structured `openviking: diag {...}` lines. |
@@ -272,7 +272,7 @@ Command parsers support quoted args and flags. Resource-only flags are rejected
272
272
  | `configured=false` | Setup did not persist config | Re-run `openclaw openviking setup ... --json`; branch on JSON `error`. |
273
273
  | `slotActive=false` | Another context engine owns the slot or gateway has stale state | Inspect `plugins.slots.contextEngine`; use `--force-slot` only after user confirms. |
274
274
  | `health.ok=false` | Server unreachable or wrong `baseUrl` / key | Check `baseUrl`, network, `/health`, and auth. |
275
- | No long-term memory after a fresh fact | `/compact` or commit/extraction has not run, or server extraction failed | Run `/compact`; check OpenViking server logs. |
275
+ | No long-term memory after a fresh fact | `/compact` or commit/extraction has not run, or server extraction failed | Use `memory_store` for explicit remember/save/store intents; otherwise run `/compact` or wait for threshold commit, then check OpenViking server logs. |
276
276
  | Recall misses shared documents | `resource` target is not enabled | Use `memory_recall` with `resourceTypes:["resource"]` or configure `recallTargetTypes: ["resource"]`. |
277
277
  | Summary lacks exact detail | Archive summary is too coarse | Use `ov_archive_search` with concrete keywords, then `ov_archive_expand`. |
278
278
  | Large tool output preview is insufficient | Tool result was externalized | Use `openviking_tool_result_search/read` with the ref. |
package/text-utils.ts CHANGED
@@ -44,14 +44,9 @@ function looksLikeMetadataJsonBlock(content: string): boolean {
44
44
  return matchedKeys.size >= 3;
45
45
  }
46
46
 
47
- const HEARTBEAT_RE = /\bHEARTBEAT(?:\.md|_OK)\b/;
48
47
  const TOOL_PLACEHOLDER_RE = /^\s*\[tool(?::\s*|Use:\s*)[^\]]+\]\s*$/i;
49
48
 
50
49
  export function sanitizeUserTextForCapture(text: string): string {
51
- // 过滤 HEARTBEAT 健康检查消息
52
- if (HEARTBEAT_RE.test(text)) {
53
- return "";
54
- }
55
50
  // Drop legacy synthetic tool placeholders before they reach memory extraction.
56
51
  if (TOOL_PLACEHOLDER_RE.test(text)) {
57
52
  return "";
@@ -409,7 +404,7 @@ export function extractNewTurnMessages(
409
404
  : undefined);
410
405
  if (output) {
411
406
  result.push({
412
- role: "user",
407
+ role: "assistant",
413
408
  parts: [{
414
409
  type: "tool",
415
410
  toolCallId: toolCallId || undefined,