@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 +26 -10
- package/auto-recall.ts +36 -6
- package/client.ts +2 -2
- package/context-engine.ts +8 -6
- package/dist/auto-recall.js +27 -7
- package/dist/client.js +2 -2
- package/dist/context-engine.js +4 -8
- package/dist/index.js +39 -20
- package/dist/process-manager.js +5 -17
- package/dist/token-estimator.js +51 -0
- package/index.ts +46 -21
- package/install-manifest.json +1 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/process-manager.ts +12 -16
- package/skills/install-openviking-memory/SKILL.md +25 -11
- package/token-estimator.ts +65 -0
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`) |
|
|
27
|
-
| **
|
|
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
|
|
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
|
-
| `
|
|
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
|
|
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
|
-
- `
|
|
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
|
-
- `
|
|
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
|
-
/
|
|
254
|
-
/
|
|
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
|
|
55
|
+
/** Estimate token count using the shared CJK-aware fallback for diagnostics. */
|
|
55
56
|
export function estimateTokenCount(text: string): number {
|
|
56
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
164
|
+
return estimateAgentMessagesTokens(messages);
|
|
160
165
|
}
|
|
161
166
|
|
|
162
167
|
function msgTokenEstimate(msg: AgentMessage): number {
|
|
163
|
-
|
|
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:
|
|
676
|
+
return { text, tokens: estimateTextTokens(text) };
|
|
675
677
|
}
|
|
676
678
|
|
|
677
679
|
function buildArchiveMemory(
|
package/dist/auto-recall.js
CHANGED
|
@@ -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
|
|
29
|
+
/** Estimate token count using the shared CJK-aware fallback for diagnostics. */
|
|
29
30
|
export function estimateTokenCount(text) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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);
|
package/dist/context-engine.js
CHANGED
|
@@ -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
|
|
34
|
+
return estimateAgentMessagesTokens(messages);
|
|
34
35
|
}
|
|
35
36
|
function msgTokenEstimate(msg) {
|
|
36
|
-
|
|
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:
|
|
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
|
|
209
|
+
export function parseOVSearchCommandArgs(args) {
|
|
209
210
|
const parsed = parseFlagArgs(args);
|
|
210
|
-
// `/
|
|
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: /
|
|
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
|
|
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
|
|
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
|
-
...
|
|
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
|
|
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:
|
|
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: "
|
|
643
|
-
label: "
|
|
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("
|
|
653
|
+
return makeBypassedToolResult("ov_search");
|
|
653
654
|
}
|
|
654
655
|
const session = resolvePluginSessionRouting(ctx);
|
|
655
|
-
return
|
|
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: "
|
|
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: "
|
|
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("
|
|
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 =
|
|
714
|
-
const result = await
|
|
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
|
|
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
|
-
"
|
|
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}`);
|
package/dist/process-manager.js
CHANGED
|
@@ -10,29 +10,17 @@ export function withTimeout(promise, timeoutMs, timeoutMessage) {
|
|
|
10
10
|
});
|
|
11
11
|
});
|
|
12
12
|
}
|
|
13
|
-
export async function quickHealthCheck(
|
|
14
|
-
const controller = new AbortController();
|
|
15
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
13
|
+
export async function quickHealthCheck(client, agentId, timeoutMs) {
|
|
16
14
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
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(
|
|
35
|
-
const healthOk = await quickHealthCheck(
|
|
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
|
|
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
|
|
414
|
+
export function parseOVSearchCommandArgs(args: string): OVSearchInput {
|
|
409
415
|
const parsed = parseFlagArgs(args);
|
|
410
|
-
// `/
|
|
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: /
|
|
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
|
|
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
|
|
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
|
-
...
|
|
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
|
|
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:
|
|
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: "
|
|
939
|
-
label: "
|
|
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("
|
|
955
|
+
return makeBypassedToolResult("ov_search");
|
|
950
956
|
}
|
|
951
957
|
const session = resolvePluginSessionRouting(ctx);
|
|
952
|
-
return
|
|
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: "
|
|
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: "
|
|
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("
|
|
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 =
|
|
1014
|
-
const result = await
|
|
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
|
|
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
|
-
"
|
|
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
|
}
|
package/install-manifest.json
CHANGED
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openviking/openclaw-plugin",
|
|
3
|
-
"version": "2026.
|
|
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",
|
package/process-manager.ts
CHANGED
|
@@ -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(
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
export async function quickHealthCheck(
|
|
20
|
+
client: OpenVikingClient,
|
|
21
|
+
agentId: string | undefined,
|
|
22
|
+
timeoutMs: number,
|
|
23
|
+
): Promise<boolean> {
|
|
20
24
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
33
|
+
client: OpenVikingClient,
|
|
34
|
+
agentId?: string,
|
|
39
35
|
): Promise<{ ok: true } | { ok: false; reason: string }> {
|
|
40
|
-
const healthOk = await quickHealthCheck(
|
|
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
|
-
> -
|
|
417
|
-
> -
|
|
416
|
+
> - **每一轮**:你的对话会被自动追加到 OpenViking session 里;自动抽取依赖阈值 commit 或 `/compact`
|
|
417
|
+
> - **明确要记住时**:如果你说“记住/保存/存一下”某个长期事实,我会用 `memory_store` 立即提交到记忆管线
|
|
418
|
+
> - **`/compact` 时**:待提交的 session 消息会被抽取成长期记忆,写到 `~/.openviking/data/` 下
|
|
418
419
|
> - **后续会话**:每次回复前我都会自动搜一下相关记忆并带进上下文
|
|
419
420
|
>
|
|
420
|
-
>
|
|
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
|
|
426
|
-
> - **
|
|
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?
|
|
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
|
|
440
|
-
- **Memory extraction (
|
|
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
|
|
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-
|
|
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
|
+
}
|