@openviking/openclaw-plugin 2026.5.21 → 2026.6.2

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
@@ -23,8 +23,9 @@ The agent runs install → setup → restart → verify automatically. See [INST
23
23
 
24
24
  | Stage | What happens |
25
25
  |-------|-------------|
26
- | **Every turn** (`afterTurn`) | Your messages are archived into an OpenViking session |
27
- | **On `/compact`** (`compact`) | Archived messages are extracted into long-term memories |
26
+ | **Every turn** (`afterTurn`) | New messages are appended to an OpenViking session; commit/extraction is threshold-triggered |
27
+ | **Explicit remember** (`memory_store`) | Important long-term facts can be written and committed immediately |
28
+ | **On `/compact`** (`compact`) | Pending session messages are committed and extracted into long-term memories |
28
29
  | **Before each reply** (`assemble`) | Relevant memories are auto-retrieved and injected into context |
29
30
 
30
31
  ## Tools
@@ -34,13 +35,16 @@ Once installed, the plugin provides these agent tools:
34
35
  | Tool | Purpose |
35
36
  |------|---------|
36
37
  | `memory_recall` | Explicit long-term memory search |
37
- | `memory_store` | Persist important information immediately |
38
+ | `memory_store` | Persist explicit long-term facts immediately |
38
39
  | `memory_forget` | Delete memories by URI or query |
39
40
  | `ov_archive_search` | Search across archives by keyword |
40
41
  | `ov_archive_expand` | Expand an archive back to raw messages |
41
42
  | `add_resource` | Import documents, URLs, or Git repos |
42
43
  | `add_skill` | Import agent skills |
43
- | `memory_search` | Search imported resources and skills |
44
+ | `ov_search` | Search imported resources and skills |
45
+ | `openviking_tool_result_read` | Restore the full original content of an externalized tool result |
46
+ | `openviking_tool_result_search` | Search inside an externalized tool result by keyword |
47
+ | `openviking_tool_result_list` | List externalized tool results in the current session |
44
48
 
45
49
  ## Data Flow & Privacy
46
50
 
@@ -203,6 +207,18 @@ After that, the plugin checks `pending_tokens`. Once the session crosses `commit
203
207
  - the current turn is not blocked waiting for extraction
204
208
  - if `logFindRequests` is enabled, the logs include the task id and follow-up extraction detail
205
209
 
210
+ This automatic path is best-effort and commit-dependent. Short but important facts can stay only in the live session until a threshold commit, `/compact`, or an explicit store happens.
211
+
212
+ ### Explicit long-term memory writes
213
+
214
+ When the user explicitly asks the agent to remember, save, or store an important long-term fact, preference, project, or decision, prefer `memory_store` over waiting for normal auto-capture. `memory_store` writes the text to an OpenViking session and calls `commit(wait=true)`, so it is the reliable integration-side path for facts that should be available as long-term memory as soon as possible.
215
+
216
+ Use it as a complement to auto-capture, not a replacement:
217
+
218
+ - auto-capture still preserves ordinary conversation flow and batches extraction for cost and latency
219
+ - `memory_store` is for explicit durable-memory intent such as "remember my main project is X" or "save this preference"
220
+ - if `memory_store` commits but extracts 0 memories, check the OpenViking server extraction/model configuration; the explicit path triggered extraction, but the extractor did not produce a memory
221
+
206
222
  ### What `compact()` does
207
223
 
208
224
  `compact()` is the stricter synchronous boundary:
@@ -219,22 +235,22 @@ So `afterTurn()` is closer to "incremental append plus threshold-triggered async
219
235
  Beyond automatic behavior, the plugin exposes seven tools directly:
220
236
 
221
237
  - `memory_recall`: explicit long-term memory search
222
- - `memory_store`: write text into an OpenViking session and trigger commit
238
+ - `memory_store`: write explicit long-term facts into an OpenViking session and trigger commit
223
239
  - `memory_forget`: delete by URI, or search first and remove a single strong match
224
240
  - `ov_archive_expand`: expand a concrete archive back into raw messages
225
241
  - `add_resource`: import a document, directory, URL, or Git repository as an OpenViking resource
226
242
  - `add_skill`: import or register an OpenViking agent skill
227
- - `memory_search`: search OpenViking resources and skills, especially after importing them
243
+ - `ov_search`: search OpenViking resources and skills, especially after importing them
228
244
 
229
245
  They serve different roles:
230
246
 
231
247
  - automatic recall covers the default case where the model does not know what to search yet
232
248
  - `memory_recall` gives the model an explicit follow-up search path
233
- - `memory_store` is for immediately persisting clearly important information
249
+ - `memory_store` is for immediately persisting clearly important information when the user expresses durable-memory intent
234
250
  - `ov_archive_expand` is the "go back to archive detail" escape hatch when summaries are not enough
235
251
  - `add_resource` lets the agent save explicit document or repository import requests without asking the user to remember slash commands
236
252
  - `add_skill` imports skills into OpenViking, while `add_resource` imports resources
237
- - `memory_search` closes the loop after import by letting the user or agent confirm and consume resources and skills
253
+ - `ov_search` closes the loop after import by letting the user or agent confirm and consume resources and skills
238
254
 
239
255
  `ov_archive_expand` is especially important because `assemble()` normally returns archive summaries and indexes, not the full raw transcript.
240
256
 
@@ -250,8 +266,8 @@ The plugin also registers explicit slash commands for manual imports:
250
266
  ```text
251
267
  /add-resource ./README.md --to viking://resources/openviking-readme --wait
252
268
  /add-skill ./skills/install-openviking-memory --wait
253
- /memory-search "OpenViking install" --uri viking://resources/openviking-readme
254
- /memory-search "memory install skill" --uri viking://agent/skills
269
+ /ov-search "OpenViking install" --uri viking://resources/openviking-readme
270
+ /ov-search "memory install skill" --uri viking://agent/skills
255
271
  ```
256
272
 
257
273
  Resource import supports remote URLs, Git URLs, local files, local directories, and uploaded zip files. OpenViking's built-in parsers cover common documents and media such as Markdown, text, PDF, HTML, Word, PowerPoint, Excel, EPUB, images, audio, and video. Directory imports also accept common code, documentation, and config file extensions such as `.py`, `.js`, `.ts`, `.go`, `.rs`, `.java`, `.cpp`, `.json`, `.yaml`, `.toml`, `.csv`, `.rst`, `.proto`, `.tf`, and `.vue`.
package/auto-recall.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  } from "./memory-ranking.js";
9
9
  import { quickRecallPrecheck, withTimeout } from "./process-manager.js";
10
10
  import { sanitizeUserTextForCapture } from "./text-utils.js";
11
+ import { estimateTextTokens } from "./token-estimator.js";
11
12
 
12
13
  const AUTO_RECALL_TIMEOUT_MS = 5_000;
13
14
  const RECALL_QUERY_MAX_CHARS = 4_000;
@@ -51,16 +52,44 @@ export function prepareRecallQuery(rawText: string): PreparedRecallQuery {
51
52
  };
52
53
  }
53
54
 
54
- /** Estimate token count using chars/4 heuristic for diagnostics. */
55
+ /** Estimate token count using the shared CJK-aware fallback for diagnostics. */
55
56
  export function estimateTokenCount(text: string): number {
56
- if (!text) return 0;
57
- return Math.ceil(text.length / 4);
57
+ return estimateTextTokens(text);
58
58
  }
59
59
 
60
60
  export type BuildMemoryLinesOptions = {
61
61
  recallPreferAbstract: boolean;
62
+ includeUri?: boolean;
62
63
  };
63
64
 
65
+ function memoryCategory(item: FindResultItem): string {
66
+ return item.category?.trim() || "memory";
67
+ }
68
+
69
+ function indentContent(content: string): string {
70
+ return content
71
+ .split("\n")
72
+ .map((line) => ` ${line}`)
73
+ .join("\n");
74
+ }
75
+
76
+ function formatMemoryLine(
77
+ item: FindResultItem,
78
+ content: string,
79
+ options: BuildMemoryLinesOptions,
80
+ ): string {
81
+ const category = memoryCategory(item);
82
+ if (!options.includeUri) {
83
+ return `- [${category}] ${content}`;
84
+ }
85
+
86
+ return [
87
+ `- [${category}]`,
88
+ ` <uri>${item.uri}</uri>`,
89
+ indentContent(content),
90
+ ].join("\n");
91
+ }
92
+
64
93
  async function resolveMemoryContent(
65
94
  item: FindResultItem,
66
95
  readFn: (uri: string) => Promise<string>,
@@ -95,7 +124,7 @@ export async function buildMemoryLines(
95
124
  const lines: string[] = [];
96
125
  for (const item of memories) {
97
126
  const content = await resolveMemoryContent(item, readFn, options);
98
- lines.push(`- [${item.category ?? "memory"}] ${content}`);
127
+ lines.push(formatMemoryLine(item, content, options));
99
128
  }
100
129
  return lines;
101
130
  }
@@ -128,7 +157,7 @@ export async function buildMemoryLinesWithBudget(
128
157
  }
129
158
 
130
159
  const content = await resolveMemoryContent(item, readFn, options);
131
- const line = `- [${item.category ?? "memory"}] ${content}`;
160
+ const line = formatMemoryLine(item, content, options);
132
161
  const separatorChars = lines.length > 0 ? 1 : 0;
133
162
  const projectedChars = totalChars + separatorChars + line.length;
134
163
 
@@ -170,7 +199,7 @@ export async function buildAutoRecallContext(params: {
170
199
  return { memoryCount: 0, estimatedTokens: 0 };
171
200
  }
172
201
 
173
- const precheck = await quickRecallPrecheck(cfg.baseUrl);
202
+ const precheck = await quickRecallPrecheck(client, agentId);
174
203
  if (!precheck.ok) {
175
204
  verbose?.(`openviking: skipping auto-recall because precheck failed (${precheck.reason})`);
176
205
  return { memoryCount: 0, estimatedTokens: 0 };
@@ -231,6 +260,7 @@ export async function buildAutoRecallContext(params: {
231
260
  {
232
261
  recallPreferAbstract: cfg.recallPreferAbstract,
233
262
  recallMaxInjectedChars: cfg.recallMaxInjectedChars,
263
+ includeUri: true,
234
264
  },
235
265
  );
236
266
 
package/client.ts CHANGED
@@ -362,8 +362,8 @@ export class OpenVikingClient {
362
362
  }
363
363
  }
364
364
 
365
- async healthCheck(): Promise<void> {
366
- await this.request<{ status: string }>("/health");
365
+ async healthCheck(agentId?: string, requestTimeoutMs?: number): Promise<void> {
366
+ await this.request<{ status: string }>("/health", {}, agentId, requestTimeoutMs);
367
367
  }
368
368
 
369
369
  private async getRuntimeIdentity(agentId?: string): Promise<RuntimeIdentity> {
package/context-engine.ts CHANGED
@@ -18,6 +18,11 @@ import {
18
18
  toJsonLog,
19
19
  } from "./memory-ranking.js";
20
20
  import { sanitizeToolUseResultPairing } from "./session-transcript-repair.js";
21
+ import {
22
+ estimateAgentMessageTokens,
23
+ estimateAgentMessagesTokens,
24
+ estimateTextTokens,
25
+ } from "./token-estimator.js";
21
26
 
22
27
  type AgentMessage = {
23
28
  role?: string;
@@ -156,14 +161,11 @@ function allocateContextBudget(totalBudget: number, instructionTokens = 0): Cont
156
161
  }
157
162
 
158
163
  function roughEstimate(messages: AgentMessage[]): number {
159
- return Math.ceil(JSON.stringify(messages).length / 4);
164
+ return estimateAgentMessagesTokens(messages);
160
165
  }
161
166
 
162
167
  function msgTokenEstimate(msg: AgentMessage): number {
163
- const raw = (msg as Record<string, unknown>).content;
164
- if (typeof raw === "string") return Math.ceil(raw.length / 4);
165
- if (Array.isArray(raw)) return Math.ceil(JSON.stringify(raw).length / 4);
166
- return 1;
168
+ return estimateAgentMessageTokens(msg);
167
169
  }
168
170
 
169
171
  function normalizeTimestamp(value: unknown): string | undefined {
@@ -671,7 +673,7 @@ function buildSystemPromptAddition(): string {
671
673
 
672
674
  function buildInstructionPrompt(): { text: string; tokens: number } {
673
675
  const text = buildSystemPromptAddition();
674
- return { text, tokens: Math.ceil(text.length / 4) };
676
+ return { text, tokens: estimateTextTokens(text) };
675
677
  }
676
678
 
677
679
  function buildArchiveMemory(
@@ -1,6 +1,7 @@
1
1
  import { pickMemoriesForInjection, postProcessMemories, summarizeInjectionMemories, toJsonLog, } from "./memory-ranking.js";
2
2
  import { quickRecallPrecheck, withTimeout } from "./process-manager.js";
3
3
  import { sanitizeUserTextForCapture } from "./text-utils.js";
4
+ import { estimateTextTokens } from "./token-estimator.js";
4
5
  const AUTO_RECALL_TIMEOUT_MS = 5_000;
5
6
  const RECALL_QUERY_MAX_CHARS = 4_000;
6
7
  export const AUTO_RECALL_SOURCE_MARKER = "Source: openviking-auto-recall";
@@ -25,11 +26,29 @@ export function prepareRecallQuery(rawText) {
25
26
  finalChars: query.length,
26
27
  };
27
28
  }
28
- /** Estimate token count using chars/4 heuristic for diagnostics. */
29
+ /** Estimate token count using the shared CJK-aware fallback for diagnostics. */
29
30
  export function estimateTokenCount(text) {
30
- if (!text)
31
- return 0;
32
- return Math.ceil(text.length / 4);
31
+ return estimateTextTokens(text);
32
+ }
33
+ function memoryCategory(item) {
34
+ return item.category?.trim() || "memory";
35
+ }
36
+ function indentContent(content) {
37
+ return content
38
+ .split("\n")
39
+ .map((line) => ` ${line}`)
40
+ .join("\n");
41
+ }
42
+ function formatMemoryLine(item, content, options) {
43
+ const category = memoryCategory(item);
44
+ if (!options.includeUri) {
45
+ return `- [${category}] ${content}`;
46
+ }
47
+ return [
48
+ `- [${category}]`,
49
+ ` <uri>${item.uri}</uri>`,
50
+ indentContent(content),
51
+ ].join("\n");
33
52
  }
34
53
  async function resolveMemoryContent(item, readFn, options) {
35
54
  let content;
@@ -57,7 +76,7 @@ export async function buildMemoryLines(memories, readFn, options) {
57
76
  const lines = [];
58
77
  for (const item of memories) {
59
78
  const content = await resolveMemoryContent(item, readFn, options);
60
- lines.push(`- [${item.category ?? "memory"}] ${content}`);
79
+ lines.push(formatMemoryLine(item, content, options));
61
80
  }
62
81
  return lines;
63
82
  }
@@ -78,7 +97,7 @@ export async function buildMemoryLinesWithBudget(memories, readFn, options) {
78
97
  break;
79
98
  }
80
99
  const content = await resolveMemoryContent(item, readFn, options);
81
- const line = `- [${item.category ?? "memory"}] ${content}`;
100
+ const line = formatMemoryLine(item, content, options);
82
101
  const separatorChars = lines.length > 0 ? 1 : 0;
83
102
  const projectedChars = totalChars + separatorChars + line.length;
84
103
  if (projectedChars > charBudget) {
@@ -105,7 +124,7 @@ export async function buildAutoRecallContext(params) {
105
124
  if (!cfg.autoRecall || queryText.length < 5) {
106
125
  return { memoryCount: 0, estimatedTokens: 0 };
107
126
  }
108
- const precheck = await quickRecallPrecheck(cfg.baseUrl);
127
+ const precheck = await quickRecallPrecheck(client, agentId);
109
128
  if (!precheck.ok) {
110
129
  verbose?.(`openviking: skipping auto-recall because precheck failed (${precheck.reason})`);
111
130
  return { memoryCount: 0, estimatedTokens: 0 };
@@ -154,6 +173,7 @@ export async function buildAutoRecallContext(params) {
154
173
  const { lines: memoryLines, estimatedTokens } = await buildMemoryLinesWithBudget(memories, (uri) => client.read(uri, agentId), {
155
174
  recallPreferAbstract: cfg.recallPreferAbstract,
156
175
  recallMaxInjectedChars: cfg.recallMaxInjectedChars,
176
+ includeUri: true,
157
177
  });
158
178
  if (memoryLines.length === 0) {
159
179
  verbose?.(`openviking: skipping auto-recall injection; no complete memories fit maxInjectedChars=${cfg.recallMaxInjectedChars}`);
package/dist/client.js CHANGED
@@ -155,8 +155,8 @@ export class OpenVikingClient {
155
155
  clearTimeout(timer);
156
156
  }
157
157
  }
158
- async healthCheck() {
159
- await this.request("/health");
158
+ async healthCheck(agentId, requestTimeoutMs) {
159
+ await this.request("/health", {}, agentId, requestTimeoutMs);
160
160
  }
161
161
  async getRuntimeIdentity(agentId) {
162
162
  const effectiveAgentId = this.resolveEffectiveAgentId(agentId);
@@ -4,6 +4,7 @@ import { AUTO_RECALL_SOURCE_MARKER, buildAutoRecallContext, prepareRecallQuery,
4
4
  import { compileSessionPatterns, extractNewTurnMessages, shouldBypassSession, } from "./text-utils.js";
5
5
  import { toJsonLog, } from "./memory-ranking.js";
6
6
  import { sanitizeToolUseResultPairing } from "./session-transcript-repair.js";
7
+ import { estimateAgentMessageTokens, estimateAgentMessagesTokens, estimateTextTokens, } from "./token-estimator.js";
7
8
  export function toRoleId(senderId) {
8
9
  if (!senderId) {
9
10
  return undefined;
@@ -30,15 +31,10 @@ function allocateContextBudget(totalBudget, instructionTokens = 0) {
30
31
  return { archiveMemory, sessionContext, reserved };
31
32
  }
32
33
  function roughEstimate(messages) {
33
- return Math.ceil(JSON.stringify(messages).length / 4);
34
+ return estimateAgentMessagesTokens(messages);
34
35
  }
35
36
  function msgTokenEstimate(msg) {
36
- const raw = msg.content;
37
- if (typeof raw === "string")
38
- return Math.ceil(raw.length / 4);
39
- if (Array.isArray(raw))
40
- return Math.ceil(JSON.stringify(raw).length / 4);
41
- return 1;
37
+ return estimateAgentMessageTokens(msg);
42
38
  }
43
39
  function normalizeTimestamp(value) {
44
40
  if (typeof value === "number" && Number.isFinite(value)) {
@@ -503,7 +499,7 @@ function buildSystemPromptAddition() {
503
499
  }
504
500
  function buildInstructionPrompt() {
505
501
  const text = buildSystemPromptAddition();
506
- return { text, tokens: Math.ceil(text.length / 4) };
502
+ return { text, tokens: estimateTextTokens(text) };
507
503
  }
508
504
  function buildArchiveMemory(archiveOverview, _preAbstracts, _budget) {
509
505
  const messages = [];
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import { clampScore, postProcessMemories, pickMemoriesForInjection, } from "./me
8
8
  import { createMemoryOpenVikingContextEngine, openClawSessionToOvStorageId, openClawSessionRefToOvStorageId, } from "./context-engine.js";
9
9
  import { buildMemoryLines, buildMemoryLinesWithBudget, estimateTokenCount, prepareRecallQuery, } from "./auto-recall.js";
10
10
  export { buildMemoryLines, buildMemoryLinesWithBudget, estimateTokenCount, prepareRecallQuery, };
11
+ export { estimateAgentMessageTokens, estimateAgentMessagesTokens, estimateSerializedTokens, estimateTextTokens, } from "./token-estimator.js";
11
12
  function parseToolResultRef(value) {
12
13
  const raw = typeof value === "string" ? value.trim() : "";
13
14
  if (!raw) {
@@ -205,13 +206,13 @@ export function parseAddSkillCommandArgs(args) {
205
206
  timeout: getNumberFlag(parsed.flags, "timeout"),
206
207
  };
207
208
  }
208
- export function parseMemorySearchCommandArgs(args) {
209
+ export function parseOVSearchCommandArgs(args) {
209
210
  const parsed = parseFlagArgs(args);
210
- // `/memory-search` only accepts a single query string, so positional segments are
211
+ // `/ov-search` only accepts a single query string, so positional segments are
211
212
  // always re-joined to preserve unquoted multi-word searches.
212
213
  const query = parsed.positionals.join(" ").trim();
213
214
  if (!query) {
214
- throw new Error('Usage: /memory-search "<query>" [--uri URI] [--limit N]');
215
+ throw new Error('Usage: /ov-search "<query>" [--uri URI] [--limit N]');
215
216
  }
216
217
  return {
217
218
  query,
@@ -482,7 +483,7 @@ const contextEnginePlugin = {
482
483
  total: memories.length + resources.length + skills.length,
483
484
  };
484
485
  };
485
- const formatMemorySearchRows = (result) => {
486
+ const formatOVSearchRows = (result) => {
486
487
  const truncateSummary = (value, maxChars = 220) => {
487
488
  const collapsed = value.replace(/\s+/g, " ").trim();
488
489
  if (collapsed.length <= maxChars) {
@@ -519,7 +520,7 @@ const contextEnginePlugin = {
519
520
  }),
520
521
  ];
521
522
  };
522
- const formatMemorySearchText = (query, uri, result) => {
523
+ const formatOVSearchText = (query, uri, result) => {
523
524
  if ((result.total ?? 0) <= 0) {
524
525
  const scope = uri ? ` under ${uri}` : "";
525
526
  return `No OpenViking resource or skill results found for "${query}"${scope}.`;
@@ -528,11 +529,11 @@ const contextEnginePlugin = {
528
529
  const lines = [
529
530
  `Found ${result.total ?? 0} OpenViking results for "${query}"${scope}`,
530
531
  "",
531
- ...formatMemorySearchRows(result),
532
+ ...formatOVSearchRows(result),
532
533
  ].filter((line, index, all) => line || (all[index - 1] && all[index + 1]));
533
534
  return lines.join("\n");
534
535
  };
535
- const memorySearchOpenViking = async (input, agentId) => {
536
+ const searchOpenViking = async (input, agentId) => {
536
537
  const query = input.query.trim();
537
538
  if (!query) {
538
539
  throw new Error("query is required");
@@ -572,7 +573,7 @@ const contextEnginePlugin = {
572
573
  result = mergeFindResults(successful);
573
574
  }
574
575
  return {
575
- content: [{ type: "text", text: formatMemorySearchText(query, input.uri, result) }],
576
+ content: [{ type: "text", text: formatOVSearchText(query, input.uri, result) }],
576
577
  details: {
577
578
  action: "searched",
578
579
  query,
@@ -639,8 +640,8 @@ const contextEnginePlugin = {
639
640
  },
640
641
  }), { name: "add_skill" });
641
642
  api.registerTool((ctx) => ({
642
- name: "memory_search",
643
- label: "Memory Search (OpenViking)",
643
+ name: "ov_search",
644
+ label: "Search (OpenViking)",
644
645
  description: "Search OpenViking resources and skills. Use after importing, or when the user asks to search OpenViking resources or skills.",
645
646
  parameters: Type.Object({
646
647
  query: Type.String({ description: "Search query" }),
@@ -649,16 +650,16 @@ const contextEnginePlugin = {
649
650
  }),
650
651
  async execute(_toolCallId, params) {
651
652
  if (isBypassedSession(ctx)) {
652
- return makeBypassedToolResult("memory_search");
653
+ return makeBypassedToolResult("ov_search");
653
654
  }
654
655
  const session = resolvePluginSessionRouting(ctx);
655
- return memorySearchOpenViking({
656
+ return searchOpenViking({
656
657
  query: String(params.query ?? ""),
657
658
  uri: typeof params.uri === "string" ? params.uri : undefined,
658
659
  limit: typeof params.limit === "number" ? params.limit : undefined,
659
660
  }, session.agentId);
660
661
  },
661
- }), { name: "memory_search" });
662
+ }), { name: "ov_search" });
662
663
  api.registerCommand?.({
663
664
  name: "add-resource",
664
665
  description: "Add a resource into OpenViking.",
@@ -700,22 +701,22 @@ const contextEnginePlugin = {
700
701
  },
701
702
  });
702
703
  api.registerCommand?.({
703
- name: "memory-search",
704
+ name: "ov-search",
704
705
  description: "Search OpenViking resources and skills.",
705
706
  acceptsArgs: true,
706
707
  handler: async (ctx) => {
707
708
  try {
708
709
  if (isBypassedSession(ctx)) {
709
- const bypassed = makeBypassedToolResult("memory_search");
710
+ const bypassed = makeBypassedToolResult("ov_search");
710
711
  return { text: bypassed.content[0].text, details: bypassed.details };
711
712
  }
712
713
  const session = resolvePluginSessionRouting(ctx);
713
- const input = parseMemorySearchCommandArgs(ctx.args ?? "");
714
- const result = await memorySearchOpenViking(input, session.agentId);
714
+ const input = parseOVSearchCommandArgs(ctx.args ?? "");
715
+ const result = await searchOpenViking(input, session.agentId);
715
716
  return { text: result.content[0].text, details: result.details };
716
717
  }
717
718
  catch (err) {
718
- return { text: `OpenViking memory search failed: ${err instanceof Error ? err.message : String(err)}` };
719
+ return { text: `OpenViking search failed: ${err instanceof Error ? err.message : String(err)}` };
719
720
  }
720
721
  },
721
722
  });
@@ -848,7 +849,7 @@ const contextEnginePlugin = {
848
849
  api.registerTool((ctx) => ({
849
850
  name: "memory_store",
850
851
  label: "Memory Store (OpenViking)",
851
- description: "Store text in OpenViking memory pipeline by writing to a session and running memory extraction.",
852
+ description: "Store text in OpenViking memory pipeline by writing to a session and running memory extraction. Use when the user explicitly asks to remember, save, or store an important long-term fact, preference, project, or decision; automatic capture is threshold/commit dependent.",
852
853
  parameters: Type.Object({
853
854
  text: Type.String({ description: "Information to store as memory source text" }),
854
855
  role: Type.Optional(Type.String({ description: "Session role, default user" })),
@@ -914,7 +915,25 @@ const contextEnginePlugin = {
914
915
  }
915
916
  if (memoriesCount === 0) {
916
917
  api.logger.warn(`openviking: memory_store committed but 0 memories extracted (sessionId=${sessionId}). ` +
917
- "Check OpenViking server logs for embedding/extract errors (e.g. 401 API key, or extraction pipeline).");
918
+ "No OpenViking-managed long-term memory was created. Check memory.extraction_enabled, VLM configuration/API keys, or whether the text was judged not durable.");
919
+ return {
920
+ content: [
921
+ {
922
+ type: "text",
923
+ text: `Memory extraction completed for session ${sessionId}, but produced 0 memories. ` +
924
+ "No OpenViking-managed long-term memory was stored. Check memory.extraction_enabled, VLM configuration/API keys, or whether the text is durable enough to remember.",
925
+ },
926
+ ],
927
+ details: {
928
+ action: "failed",
929
+ sessionId,
930
+ status: commitResult.status,
931
+ error: "no_memories_extracted",
932
+ memoriesCount,
933
+ archived: commitResult.archived ?? false,
934
+ usedTempSession,
935
+ },
936
+ };
918
937
  }
919
938
  else {
920
939
  api.logger.info?.(`openviking: memory_store committed, memories=${memoriesCount}`);
@@ -10,29 +10,17 @@ export function withTimeout(promise, timeoutMs, timeoutMessage) {
10
10
  });
11
11
  });
12
12
  }
13
- export async function quickHealthCheck(baseUrl, timeoutMs) {
14
- const controller = new AbortController();
15
- const timer = setTimeout(() => controller.abort(), timeoutMs);
13
+ export async function quickHealthCheck(client, agentId, timeoutMs) {
16
14
  try {
17
- const response = await fetch(`${baseUrl}/health`, {
18
- method: "GET",
19
- signal: controller.signal,
20
- });
21
- if (!response.ok) {
22
- return false;
23
- }
24
- const body = (await response.json().catch(() => ({})));
25
- return body.status === "ok";
15
+ await client.healthCheck(agentId, timeoutMs);
16
+ return true;
26
17
  }
27
18
  catch {
28
19
  return false;
29
20
  }
30
- finally {
31
- clearTimeout(timer);
32
- }
33
21
  }
34
- export async function quickRecallPrecheck(baseUrl) {
35
- const healthOk = await quickHealthCheck(baseUrl, 500);
22
+ export async function quickRecallPrecheck(client, agentId) {
23
+ const healthOk = await quickHealthCheck(client, agentId, 500);
36
24
  if (healthOk) {
37
25
  return { ok: true };
38
26
  }
@@ -0,0 +1,51 @@
1
+ function isCjkCodePoint(codePoint) {
2
+ return ((codePoint >= 0x3400 && codePoint <= 0x4dbf) ||
3
+ (codePoint >= 0x4e00 && codePoint <= 0x9fff) ||
4
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
5
+ (codePoint >= 0x20000 && codePoint <= 0x2ebef) ||
6
+ (codePoint >= 0x3040 && codePoint <= 0x30ff) ||
7
+ (codePoint >= 0x31f0 && codePoint <= 0x31ff) ||
8
+ (codePoint >= 0xac00 && codePoint <= 0xd7af) ||
9
+ (codePoint >= 0x1100 && codePoint <= 0x11ff) ||
10
+ (codePoint >= 0x3130 && codePoint <= 0x318f) ||
11
+ (codePoint >= 0xff00 && codePoint <= 0xffef) ||
12
+ (codePoint >= 0x3000 && codePoint <= 0x303f));
13
+ }
14
+ function codePointWeight(codePoint) {
15
+ if (isCjkCodePoint(codePoint)) {
16
+ return 1.5;
17
+ }
18
+ if (codePoint > 0xffff) {
19
+ return 2;
20
+ }
21
+ return 0.25;
22
+ }
23
+ export function estimateTextTokens(text) {
24
+ if (!text) {
25
+ return 0;
26
+ }
27
+ let weightedTokens = 0;
28
+ for (const char of text) {
29
+ weightedTokens += codePointWeight(char.codePointAt(0) ?? 0);
30
+ }
31
+ return Math.ceil(weightedTokens);
32
+ }
33
+ export function estimateSerializedTokens(value) {
34
+ if (value === null || value === undefined) {
35
+ return 0;
36
+ }
37
+ if (typeof value === "string") {
38
+ return estimateTextTokens(value);
39
+ }
40
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
41
+ return estimateTextTokens(String(value));
42
+ }
43
+ const serialized = JSON.stringify(value);
44
+ return estimateTextTokens(serialized ?? "");
45
+ }
46
+ export function estimateAgentMessageTokens(message) {
47
+ return estimateSerializedTokens(message);
48
+ }
49
+ export function estimateAgentMessagesTokens(messages) {
50
+ return estimateSerializedTokens(messages);
51
+ }
package/index.ts CHANGED
@@ -43,6 +43,12 @@ export {
43
43
  estimateTokenCount,
44
44
  prepareRecallQuery,
45
45
  };
46
+ export {
47
+ estimateAgentMessageTokens,
48
+ estimateAgentMessagesTokens,
49
+ estimateSerializedTokens,
50
+ estimateTextTokens,
51
+ } from "./token-estimator.js";
46
52
  export type {
47
53
  BuildMemoryLinesOptions,
48
54
  BuildMemoryLinesWithBudgetOptions,
@@ -144,7 +150,7 @@ type AddSkillToolInput = {
144
150
  timeout?: number;
145
151
  };
146
152
 
147
- type MemorySearchInput = {
153
+ type OVSearchInput = {
148
154
  query: string;
149
155
  uri?: string;
150
156
  limit?: number;
@@ -405,13 +411,13 @@ export function parseAddSkillCommandArgs(args: string): AddSkillToolInput {
405
411
  };
406
412
  }
407
413
 
408
- export function parseMemorySearchCommandArgs(args: string): MemorySearchInput {
414
+ export function parseOVSearchCommandArgs(args: string): OVSearchInput {
409
415
  const parsed = parseFlagArgs(args);
410
- // `/memory-search` only accepts a single query string, so positional segments are
416
+ // `/ov-search` only accepts a single query string, so positional segments are
411
417
  // always re-joined to preserve unquoted multi-word searches.
412
418
  const query = parsed.positionals.join(" ").trim();
413
419
  if (!query) {
414
- throw new Error('Usage: /memory-search "<query>" [--uri URI] [--limit N]');
420
+ throw new Error('Usage: /ov-search "<query>" [--uri URI] [--limit N]');
415
421
  }
416
422
  return {
417
423
  query,
@@ -761,7 +767,7 @@ const contextEnginePlugin = {
761
767
  };
762
768
  };
763
769
 
764
- const formatMemorySearchRows = (result: FindResult): string[] => {
770
+ const formatOVSearchRows = (result: FindResult): string[] => {
765
771
  const truncateSummary = (value: string, maxChars = 220): string => {
766
772
  const collapsed = value.replace(/\s+/g, " ").trim();
767
773
  if (collapsed.length <= maxChars) {
@@ -802,7 +808,7 @@ const contextEnginePlugin = {
802
808
  ];
803
809
  };
804
810
 
805
- const formatMemorySearchText = (query: string, uri: string | undefined, result: FindResult): string => {
811
+ const formatOVSearchText = (query: string, uri: string | undefined, result: FindResult): string => {
806
812
  if ((result.total ?? 0) <= 0) {
807
813
  const scope = uri ? ` under ${uri}` : "";
808
814
  return `No OpenViking resource or skill results found for "${query}"${scope}.`;
@@ -811,12 +817,12 @@ const contextEnginePlugin = {
811
817
  const lines = [
812
818
  `Found ${result.total ?? 0} OpenViking results for "${query}"${scope}`,
813
819
  "",
814
- ...formatMemorySearchRows(result),
820
+ ...formatOVSearchRows(result),
815
821
  ].filter((line, index, all) => line || (all[index - 1] && all[index + 1]));
816
822
  return lines.join("\n");
817
823
  };
818
824
 
819
- const memorySearchOpenViking = async (input: MemorySearchInput, agentId?: string) => {
825
+ const searchOpenViking = async (input: OVSearchInput, agentId?: string) => {
820
826
  const query = input.query.trim();
821
827
  if (!query) {
822
828
  throw new Error("query is required");
@@ -856,7 +862,7 @@ const contextEnginePlugin = {
856
862
  result = mergeFindResults(successful);
857
863
  }
858
864
  return {
859
- content: [{ type: "text" as const, text: formatMemorySearchText(query, input.uri, result) }],
865
+ content: [{ type: "text" as const, text: formatOVSearchText(query, input.uri, result) }],
860
866
  details: {
861
867
  action: "searched",
862
868
  query,
@@ -935,8 +941,8 @@ const contextEnginePlugin = {
935
941
 
936
942
  api.registerTool(
937
943
  (ctx: ToolContext) => ({
938
- name: "memory_search",
939
- label: "Memory Search (OpenViking)",
944
+ name: "ov_search",
945
+ label: "Search (OpenViking)",
940
946
  description:
941
947
  "Search OpenViking resources and skills. Use after importing, or when the user asks to search OpenViking resources or skills.",
942
948
  parameters: Type.Object({
@@ -946,17 +952,17 @@ const contextEnginePlugin = {
946
952
  }),
947
953
  async execute(_toolCallId: string, params: Record<string, unknown>) {
948
954
  if (isBypassedSession(ctx)) {
949
- return makeBypassedToolResult("memory_search");
955
+ return makeBypassedToolResult("ov_search");
950
956
  }
951
957
  const session = resolvePluginSessionRouting(ctx);
952
- return memorySearchOpenViking({
958
+ return searchOpenViking({
953
959
  query: String((params as { query?: unknown }).query ?? ""),
954
960
  uri: typeof params.uri === "string" ? params.uri : undefined,
955
961
  limit: typeof params.limit === "number" ? params.limit : undefined,
956
962
  }, session.agentId);
957
963
  },
958
964
  }),
959
- { name: "memory_search" },
965
+ { name: "ov_search" },
960
966
  );
961
967
 
962
968
  api.registerCommand?.({
@@ -1000,21 +1006,21 @@ const contextEnginePlugin = {
1000
1006
  });
1001
1007
 
1002
1008
  api.registerCommand?.({
1003
- name: "memory-search",
1009
+ name: "ov-search",
1004
1010
  description: "Search OpenViking resources and skills.",
1005
1011
  acceptsArgs: true,
1006
1012
  handler: async (ctx: PluginCommandContext) => {
1007
1013
  try {
1008
1014
  if (isBypassedSession(ctx)) {
1009
- const bypassed = makeBypassedToolResult("memory_search");
1015
+ const bypassed = makeBypassedToolResult("ov_search");
1010
1016
  return { text: bypassed.content[0]!.text, details: bypassed.details };
1011
1017
  }
1012
1018
  const session = resolvePluginSessionRouting(ctx);
1013
- const input = parseMemorySearchCommandArgs(ctx.args ?? "");
1014
- const result = await memorySearchOpenViking(input, session.agentId);
1019
+ const input = parseOVSearchCommandArgs(ctx.args ?? "");
1020
+ const result = await searchOpenViking(input, session.agentId);
1015
1021
  return { text: result.content[0]!.text, details: result.details };
1016
1022
  } catch (err) {
1017
- return { text: `OpenViking memory search failed: ${err instanceof Error ? err.message : String(err)}` };
1023
+ return { text: `OpenViking search failed: ${err instanceof Error ? err.message : String(err)}` };
1018
1024
  }
1019
1025
  },
1020
1026
  });
@@ -1192,7 +1198,7 @@ const contextEnginePlugin = {
1192
1198
  name: "memory_store",
1193
1199
  label: "Memory Store (OpenViking)",
1194
1200
  description:
1195
- "Store text in OpenViking memory pipeline by writing to a session and running memory extraction.",
1201
+ "Store text in OpenViking memory pipeline by writing to a session and running memory extraction. Use when the user explicitly asks to remember, save, or store an important long-term fact, preference, project, or decision; automatic capture is threshold/commit dependent.",
1196
1202
  parameters: Type.Object({
1197
1203
  text: Type.String({ description: "Information to store as memory source text" }),
1198
1204
  role: Type.Optional(Type.String({ description: "Session role, default user" })),
@@ -1276,8 +1282,27 @@ const contextEnginePlugin = {
1276
1282
  if (memoriesCount === 0) {
1277
1283
  api.logger.warn(
1278
1284
  `openviking: memory_store committed but 0 memories extracted (sessionId=${sessionId}). ` +
1279
- "Check OpenViking server logs for embedding/extract errors (e.g. 401 API key, or extraction pipeline).",
1285
+ "No OpenViking-managed long-term memory was created. Check memory.extraction_enabled, VLM configuration/API keys, or whether the text was judged not durable.",
1280
1286
  );
1287
+ return {
1288
+ content: [
1289
+ {
1290
+ type: "text",
1291
+ text:
1292
+ `Memory extraction completed for session ${sessionId}, but produced 0 memories. ` +
1293
+ "No OpenViking-managed long-term memory was stored. Check memory.extraction_enabled, VLM configuration/API keys, or whether the text is durable enough to remember.",
1294
+ },
1295
+ ],
1296
+ details: {
1297
+ action: "failed",
1298
+ sessionId,
1299
+ status: commitResult.status,
1300
+ error: "no_memories_extracted",
1301
+ memoriesCount,
1302
+ archived: commitResult.archived ?? false,
1303
+ usedTempSession,
1304
+ },
1305
+ };
1281
1306
  } else {
1282
1307
  api.logger.info?.(`openviking: memory_store committed, memories=${memoriesCount}`);
1283
1308
  }
@@ -15,6 +15,7 @@
15
15
  "client.ts",
16
16
  "process-manager.ts",
17
17
  "memory-ranking.ts",
18
+ "token-estimator.ts",
18
19
  "text-utils.ts",
19
20
  "tool-call-id.ts",
20
21
  "session-transcript-repair.ts",
@@ -14,7 +14,7 @@
14
14
  "tools": [
15
15
  "add_resource",
16
16
  "add_skill",
17
- "memory_search",
17
+ "ov_search",
18
18
  "memory_recall",
19
19
  "memory_store",
20
20
  "memory_forget",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openviking/openclaw-plugin",
3
- "version": "2026.5.21",
3
+ "version": "2026.6.2",
4
4
  "displayName": "OpenViking",
5
5
  "description": "OpenClaw context-engine plugin — OpenViking AI memory, long-term memory, knowledge base, semantic search, RAG, context engine. 长期记忆 · 知识库 · 语义检索 · 上下文引擎",
6
6
  "type": "module",
@@ -1,3 +1,5 @@
1
+ import type { OpenVikingClient } from "./client.js";
2
+
1
3
  export function withTimeout<T>(promise: Promise<T>, timeoutMs: number, timeoutMessage: string): Promise<T> {
2
4
  return new Promise((resolve, reject) => {
3
5
  const timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
@@ -14,30 +16,24 @@ export function withTimeout<T>(promise: Promise<T>, timeoutMs: number, timeoutMe
14
16
  });
15
17
  }
16
18
 
17
- export async function quickHealthCheck(baseUrl: string, timeoutMs: number): Promise<boolean> {
18
- const controller = new AbortController();
19
- const timer = setTimeout(() => controller.abort(), timeoutMs);
19
+ export async function quickHealthCheck(
20
+ client: OpenVikingClient,
21
+ agentId: string | undefined,
22
+ timeoutMs: number,
23
+ ): Promise<boolean> {
20
24
  try {
21
- const response = await fetch(`${baseUrl}/health`, {
22
- method: "GET",
23
- signal: controller.signal,
24
- });
25
- if (!response.ok) {
26
- return false;
27
- }
28
- const body = (await response.json().catch(() => ({}))) as { status?: string };
29
- return body.status === "ok";
25
+ await client.healthCheck(agentId, timeoutMs);
26
+ return true;
30
27
  } catch {
31
28
  return false;
32
- } finally {
33
- clearTimeout(timer);
34
29
  }
35
30
  }
36
31
 
37
32
  export async function quickRecallPrecheck(
38
- baseUrl: string,
33
+ client: OpenVikingClient,
34
+ agentId?: string,
39
35
  ): Promise<{ ok: true } | { ok: false; reason: string }> {
40
- const healthOk = await quickHealthCheck(baseUrl, 500);
36
+ const healthOk = await quickHealthCheck(client, agentId, 500);
41
37
  if (healthOk) {
42
38
  return { ok: true };
43
39
  }
@@ -413,20 +413,22 @@ Expected output:
413
413
  > (CN) 🎉 全部搞定!OpenViking 长期记忆已经接好了。
414
414
  >
415
415
  > 工作方式:
416
- > - **每一轮**:你的对话会被自动归档到 OpenViking session 里
417
- > - **`/compact` 时**:归档消息会被抽取成长期记忆,写到 `~/.openviking/data/` 下
416
+ > - **每一轮**:你的对话会被自动追加到 OpenViking session 里;自动抽取依赖阈值 commit 或 `/compact`
417
+ > - **明确要记住时**:如果你说“记住/保存/存一下”某个长期事实,我会用 `memory_store` 立即提交到记忆管线
418
+ > - **`/compact` 时**:待提交的 session 消息会被抽取成长期记忆,写到 `~/.openviking/data/` 下
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 under `~/.openviking/data/`
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 under `~/.openviking/data/`
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 actually appear in `~/.openviking/data/`:
439
+ The context-engine pipeline has three distinct stages plus one explicit write path — keep them apart, especially when telling users when memories actually appear in `~/.openviking/data/`:
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
 
@@ -467,6 +479,8 @@ Example: user asks "What programming language did I say I like?"
467
479
  | `role` | No | Session role (default `user`) |
468
480
  | `sessionId` | No | Existing OpenViking session ID |
469
481
 
482
+ Use this when the user explicitly asks to remember/save/store a long-term fact, preference, project, or decision.
483
+
470
484
  Example: user says "Remember my email is xxx@example.com".
471
485
 
472
486
  ### `memory_forget` — Delete Memories
@@ -495,7 +509,7 @@ These are the keys under `plugins.entries.openviking.config` in `openclaw.json`.
495
509
  | `userId` | — | Required when `apiKey` is a root key. |
496
510
  | `targetUri` | `viking://user/memories` | Default search scope URI. |
497
511
  | `timeoutMs` | (plugin default) | HTTP timeout for OpenViking calls. |
498
- | `autoCapture` | `true` | Auto-archive turn messages to OpenViking session at `afterTurn` (extraction itself runs on `/compact`, not here). |
512
+ | `autoCapture` | `true` | Auto-append turn messages to the OpenViking session at `afterTurn`; extraction runs only after a threshold commit, `/compact`, or explicit `memory_store`. |
499
513
  | `captureMode` | `"semantic"` | Filter mode used by the server-side extraction pipeline: `semantic` or `keyword`. |
500
514
  | `captureMaxLength` | `24000` | Max text length per archived turn. |
501
515
  | `autoRecall` | `true` | Auto-recall and inject memories before reply. |
@@ -0,0 +1,65 @@
1
+ type TokenCountableAgentMessage = {
2
+ role?: string;
3
+ content?: unknown;
4
+ };
5
+
6
+ function isCjkCodePoint(codePoint: number): boolean {
7
+ return (
8
+ (codePoint >= 0x3400 && codePoint <= 0x4dbf) ||
9
+ (codePoint >= 0x4e00 && codePoint <= 0x9fff) ||
10
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
11
+ (codePoint >= 0x20000 && codePoint <= 0x2ebef) ||
12
+ (codePoint >= 0x3040 && codePoint <= 0x30ff) ||
13
+ (codePoint >= 0x31f0 && codePoint <= 0x31ff) ||
14
+ (codePoint >= 0xac00 && codePoint <= 0xd7af) ||
15
+ (codePoint >= 0x1100 && codePoint <= 0x11ff) ||
16
+ (codePoint >= 0x3130 && codePoint <= 0x318f) ||
17
+ (codePoint >= 0xff00 && codePoint <= 0xffef) ||
18
+ (codePoint >= 0x3000 && codePoint <= 0x303f)
19
+ );
20
+ }
21
+
22
+ function codePointWeight(codePoint: number): number {
23
+ if (isCjkCodePoint(codePoint)) {
24
+ return 1.5;
25
+ }
26
+ if (codePoint > 0xffff) {
27
+ return 2;
28
+ }
29
+ return 0.25;
30
+ }
31
+
32
+ export function estimateTextTokens(text: string | null | undefined): number {
33
+ if (!text) {
34
+ return 0;
35
+ }
36
+
37
+ let weightedTokens = 0;
38
+ for (const char of text) {
39
+ weightedTokens += codePointWeight(char.codePointAt(0) ?? 0);
40
+ }
41
+ return Math.ceil(weightedTokens);
42
+ }
43
+
44
+ export function estimateSerializedTokens(value: unknown): number {
45
+ if (value === null || value === undefined) {
46
+ return 0;
47
+ }
48
+ if (typeof value === "string") {
49
+ return estimateTextTokens(value);
50
+ }
51
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
52
+ return estimateTextTokens(String(value));
53
+ }
54
+
55
+ const serialized = JSON.stringify(value);
56
+ return estimateTextTokens(serialized ?? "");
57
+ }
58
+
59
+ export function estimateAgentMessageTokens(message: TokenCountableAgentMessage): number {
60
+ return estimateSerializedTokens(message);
61
+ }
62
+
63
+ export function estimateAgentMessagesTokens(messages: TokenCountableAgentMessage[]): number {
64
+ return estimateSerializedTokens(messages);
65
+ }