@openviking/openclaw-plugin 2026.9.4 → 2026.9.8-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL-AGENT.md +25 -4
- package/INSTALL-ZH.md +22 -4
- package/INSTALL.md +22 -4
- package/README.md +16 -5
- package/client.ts +0 -91
- package/commands/setup.ts +41 -150
- package/config.ts +24 -11
- package/context-engine.ts +7 -2
- package/dist/client.js +0 -82
- package/dist/commands/setup.js +29 -127
- package/dist/config.js +13 -8
- package/dist/context-engine.js +5 -0
- package/dist/plugin/openclaw-runtime-compaction.js +20 -0
- package/dist/plugin/openviking-memory-tools.js +1 -1
- package/dist/plugin/openviking-session-routing-runtime.js +2 -2
- package/dist/routing/identity-routing.js +5 -5
- package/dist/services/context-lifecycle-service.js +17 -5
- package/dist/services/setup/probe-service.js +112 -0
- package/openclaw.plugin.json +8 -7
- package/package.json +1 -7
- package/plugin/openclaw-runtime-compaction.ts +41 -0
- package/plugin/openviking-client-runtime.ts +1 -1
- package/plugin/openviking-memory-tools.ts +1 -1
- package/plugin/openviking-session-routing-runtime.ts +2 -2
- package/routing/identity-routing.ts +8 -8
- package/services/context-lifecycle-service.ts +19 -4
- package/services/setup/setup-flow.ts +8 -6
- package/skills/install-openviking-memory/SKILL.md +33 -10
- package/skills/openviking-context-database/SKILL.md +2 -2
package/config.ts
CHANGED
|
@@ -30,7 +30,8 @@ export type OpenVikingSecretRef =
|
|
|
30
30
|
export type MemoryOpenVikingConfig = {
|
|
31
31
|
mode?: "remote";
|
|
32
32
|
baseUrl?: string;
|
|
33
|
-
|
|
33
|
+
/** `person` is a legacy alias for `sender`. */
|
|
34
|
+
peer_role?: "none" | "assistant" | "sender" | "person";
|
|
34
35
|
peer_prefix?: string;
|
|
35
36
|
apiKey?: string | OpenVikingSecretRef;
|
|
36
37
|
/** Optional HTTP headers merged into every OpenViking request. */
|
|
@@ -127,10 +128,12 @@ export type MemoryOpenVikingConfig = {
|
|
|
127
128
|
|
|
128
129
|
/** Runtime config after memoryOpenVikingConfigSchema.parse() has applied defaults. */
|
|
129
130
|
export type ParsedMemoryOpenVikingConfig = Required<
|
|
130
|
-
Omit<MemoryOpenVikingConfig, "agentExperience" | "recallTargetTypes" | "apiKey">
|
|
131
|
+
Omit<MemoryOpenVikingConfig, "agentExperience" | "recallTargetTypes" | "apiKey" | "peer_role">
|
|
131
132
|
> & {
|
|
132
133
|
/** parse() resolves SecretRef values, so the runtime shape is always a plain string. */
|
|
133
134
|
apiKey: string;
|
|
135
|
+
/** Runtime uses the canonical name; legacy `person` input normalizes to `sender`. */
|
|
136
|
+
peer_role: "none" | "assistant" | "sender";
|
|
134
137
|
agentExperience: Required<NonNullable<MemoryOpenVikingConfig["agentExperience"]>>;
|
|
135
138
|
recallTargetTypes: Array<"resource" | "user" | "agent">;
|
|
136
139
|
};
|
|
@@ -152,7 +155,7 @@ const DEFAULT_COMMIT_TOKEN_THRESHOLD_RATIO = 0.5;
|
|
|
152
155
|
const DEFAULT_COMMIT_KEEP_RECENT_COUNT = 10;
|
|
153
156
|
const DEFAULT_BYPASS_SESSION_PATTERNS: string[] = [];
|
|
154
157
|
const DEFAULT_EMIT_STANDARD_DIAGNOSTICS = false;
|
|
155
|
-
const DEFAULT_PEER_ROLE = "
|
|
158
|
+
const DEFAULT_PEER_ROLE = "none" as const;
|
|
156
159
|
const DEFAULT_PEER_PREFIX = "";
|
|
157
160
|
const DEFAULT_TRACE_RECALL_DIR = "~/.openclaw/openviking/recall-traces";
|
|
158
161
|
const DEFAULT_TRACE_RECALL_RETENTION_DAYS = 14;
|
|
@@ -292,16 +295,23 @@ function resolveSecret(
|
|
|
292
295
|
}
|
|
293
296
|
}
|
|
294
297
|
|
|
295
|
-
function resolvePeerRole(configured: unknown) {
|
|
298
|
+
function resolvePeerRole(configured: unknown): "none" | "assistant" | "sender" {
|
|
296
299
|
if (typeof configured === "string") {
|
|
297
300
|
const role = configured.trim().toLowerCase();
|
|
298
|
-
if (role === "none" || role === "assistant" || role === "
|
|
301
|
+
if (role === "none" || role === "assistant" || role === "sender") {
|
|
299
302
|
return role;
|
|
300
303
|
}
|
|
301
|
-
|
|
304
|
+
if (role === "person") {
|
|
305
|
+
return "sender";
|
|
306
|
+
}
|
|
307
|
+
throw new Error(
|
|
308
|
+
`openviking peer_role must be "none", "assistant", or "sender" (legacy alias: "person")`,
|
|
309
|
+
);
|
|
302
310
|
}
|
|
303
311
|
if (configured !== undefined) {
|
|
304
|
-
throw new Error(
|
|
312
|
+
throw new Error(
|
|
313
|
+
`openviking peer_role must be "none", "assistant", or "sender" (legacy alias: "person")`,
|
|
314
|
+
);
|
|
305
315
|
}
|
|
306
316
|
return DEFAULT_PEER_ROLE;
|
|
307
317
|
}
|
|
@@ -776,14 +786,17 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
776
786
|
help: "HTTP URL when mode is remote (or use ${OPENVIKING_BASE_URL})",
|
|
777
787
|
},
|
|
778
788
|
peer_role: {
|
|
779
|
-
label: "
|
|
789
|
+
label: "Memory Scope (peer_role)",
|
|
780
790
|
placeholder: DEFAULT_PEER_ROLE,
|
|
781
|
-
help:
|
|
791
|
+
help:
|
|
792
|
+
'Where peer-scoped memories are stored. "none" (default): viking://user/<user_id>/memories. ' +
|
|
793
|
+
'"assistant": viking://user/<user_id>/peers/<assistant_id>/memories. ' +
|
|
794
|
+
'"sender": viking://user/<user_id>/peers/<sender_id>/memories. Legacy "person" is accepted as "sender".',
|
|
782
795
|
},
|
|
783
796
|
peer_prefix: {
|
|
784
797
|
label: "Peer Prefix",
|
|
785
798
|
placeholder: "optional-prefix",
|
|
786
|
-
help: "
|
|
799
|
+
help: 'Only used when Memory Scope is "assistant". Prefix added to the peer id derived from the OpenClaw agent id.',
|
|
787
800
|
},
|
|
788
801
|
apiKey: {
|
|
789
802
|
label: "OpenViking API Key",
|
|
@@ -894,7 +907,7 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
894
907
|
bypassSessionPatterns: {
|
|
895
908
|
label: "Bypass Session Patterns",
|
|
896
909
|
placeholder: "agent:*:cron:**",
|
|
897
|
-
help: "Completely bypass OpenViking for matching session keys. Use * within one segment and ** across segments.",
|
|
910
|
+
help: "Completely bypass OpenViking for matching session keys (no capture, recall, or commit; compaction falls back to OpenClaw's native compactor). Use * within one segment and ** across segments.",
|
|
898
911
|
advanced: true,
|
|
899
912
|
},
|
|
900
913
|
commitTokenThresholdRatio: {
|
package/context-engine.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OpenVikingClient } from "./client.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ParsedMemoryOpenVikingConfig } from "./config.js";
|
|
3
3
|
import type { RuntimeQueryConfigStore } from "./query-config.js";
|
|
4
4
|
import {
|
|
5
5
|
AUTO_RECALL_SOURCE_MARKER,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
compactOpenVikingSession,
|
|
20
20
|
commitOpenVikingSession,
|
|
21
21
|
} from "./services/context-lifecycle-service.js";
|
|
22
|
+
import { loadRuntimeCompactionDelegate } from "./plugin/openclaw-runtime-compaction.js";
|
|
22
23
|
|
|
23
24
|
type ContextEngineInfo = {
|
|
24
25
|
id: string;
|
|
@@ -248,7 +249,7 @@ export function createMemoryOpenVikingContextEngine(params: {
|
|
|
248
249
|
id: string;
|
|
249
250
|
name: string;
|
|
250
251
|
version?: string;
|
|
251
|
-
cfg:
|
|
252
|
+
cfg: ParsedMemoryOpenVikingConfig;
|
|
252
253
|
logger: Logger;
|
|
253
254
|
getClient: () => Promise<OpenVikingClient>;
|
|
254
255
|
/** Extra args help match hook-populated routing when OpenClaw provides sessionKey / OV session id. */
|
|
@@ -437,6 +438,10 @@ export function createMemoryOpenVikingContextEngine(params: {
|
|
|
437
438
|
logger,
|
|
438
439
|
resolveAgentId,
|
|
439
440
|
isBypassedSession,
|
|
441
|
+
runtimeCompact: async () => {
|
|
442
|
+
const delegate = await loadRuntimeCompactionDelegate();
|
|
443
|
+
return delegate ? delegate(compactParams) : undefined;
|
|
444
|
+
},
|
|
440
445
|
diag,
|
|
441
446
|
});
|
|
442
447
|
},
|
package/dist/client.js
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { once } from "node:events";
|
|
3
|
-
import { createWriteStream } from "node:fs";
|
|
4
|
-
import { mkdtemp, readdir, readFile, rm, stat } from "node:fs/promises";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import { basename, dirname, join, relative } from "node:path";
|
|
7
|
-
import { Zip, ZipDeflate } from "fflate";
|
|
8
1
|
import { defaultHttpTransport } from "./adapters/http-transport.js";
|
|
9
2
|
import { defaultResourcePackager, } from "./adapters/resource-packager.js";
|
|
10
3
|
import { buildContextSearchBody, contextRequestTimeoutMs, normalizeContextEntry, } from "./shared/recall-core.mjs";
|
|
@@ -17,32 +10,12 @@ const WAIT_REQUEST_TIMEOUT_BUFFER_MS = 5_000;
|
|
|
17
10
|
function sleep(ms) {
|
|
18
11
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
12
|
}
|
|
20
|
-
const MEMORY_URI_PATTERNS = [
|
|
21
|
-
/^viking:\/\/user\/(?:[^/]+\/)?memories(?:\/|$)/,
|
|
22
|
-
];
|
|
23
|
-
const REMOTE_RESOURCE_PREFIXES = ["http://", "https://", "git@", "ssh://", "git://"];
|
|
24
|
-
export function isMemoryUri(uri) {
|
|
25
|
-
return MEMORY_URI_PATTERNS.some((pattern) => pattern.test(uri));
|
|
26
|
-
}
|
|
27
|
-
function isRemoteResourceSource(source) {
|
|
28
|
-
return REMOTE_RESOURCE_PREFIXES.some((prefix) => source.startsWith(prefix));
|
|
29
|
-
}
|
|
30
|
-
function toBlobPart(value) {
|
|
31
|
-
return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
|
|
32
|
-
}
|
|
33
13
|
function resolveWaitRequestTimeoutMs(defaultTimeoutMs, waitTimeoutSeconds) {
|
|
34
14
|
const requestedMs = typeof waitTimeoutSeconds === "number" && Number.isFinite(waitTimeoutSeconds) && waitTimeoutSeconds > 0
|
|
35
15
|
? Math.ceil(waitTimeoutSeconds * 1000) + WAIT_REQUEST_TIMEOUT_BUFFER_MS
|
|
36
16
|
: DEFAULT_WAIT_REQUEST_TIMEOUT_MS;
|
|
37
17
|
return Math.max(defaultTimeoutMs, requestedMs);
|
|
38
18
|
}
|
|
39
|
-
async function cleanupUploadTempPath(path) {
|
|
40
|
-
if (!path) {
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
await rm(path, { force: true }).catch(() => undefined);
|
|
44
|
-
await rm(dirname(path), { recursive: true, force: true }).catch(() => undefined);
|
|
45
|
-
}
|
|
46
19
|
export class OpenVikingClient {
|
|
47
20
|
baseUrl;
|
|
48
21
|
apiKey;
|
|
@@ -323,61 +296,6 @@ export class OpenVikingClient {
|
|
|
323
296
|
}
|
|
324
297
|
return result.temp_file_id;
|
|
325
298
|
}
|
|
326
|
-
async zipDirectoryForUpload(dirPath) {
|
|
327
|
-
const rootStats = await stat(dirPath);
|
|
328
|
-
if (!rootStats.isDirectory()) {
|
|
329
|
-
throw new Error(`Not a directory: ${dirPath}`);
|
|
330
|
-
}
|
|
331
|
-
const zipDir = await mkdtemp(join(tmpdir(), "openviking-openclaw-upload-"));
|
|
332
|
-
const zipPath = join(zipDir, `${basename(dirPath).replace(/[^a-zA-Z0-9._-]/g, "_")}-${randomUUID()}.zip`);
|
|
333
|
-
const output = createWriteStream(zipPath);
|
|
334
|
-
const outputClosed = once(output, "close");
|
|
335
|
-
const outputErrored = once(output, "error").then(([err]) => Promise.reject(err));
|
|
336
|
-
const zip = new Zip((err, chunk, final) => {
|
|
337
|
-
if (err) {
|
|
338
|
-
output.destroy(err);
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
if (chunk?.length) {
|
|
342
|
-
output.write(Buffer.from(chunk));
|
|
343
|
-
}
|
|
344
|
-
if (final) {
|
|
345
|
-
output.end();
|
|
346
|
-
}
|
|
347
|
-
});
|
|
348
|
-
const walk = async (currentDir) => {
|
|
349
|
-
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
350
|
-
for (const entry of entries) {
|
|
351
|
-
const fullPath = join(currentDir, entry.name);
|
|
352
|
-
if (entry.isDirectory()) {
|
|
353
|
-
await walk(fullPath);
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
if (!entry.isFile()) {
|
|
357
|
-
continue;
|
|
358
|
-
}
|
|
359
|
-
const relPath = relative(dirPath, fullPath).replace(/\\/g, "/");
|
|
360
|
-
if (!relPath || relPath.startsWith("../") || relPath.includes("/../")) {
|
|
361
|
-
throw new Error(`Unsafe relative path while zipping: ${relPath}`);
|
|
362
|
-
}
|
|
363
|
-
const file = new ZipDeflate(relPath);
|
|
364
|
-
zip.add(file);
|
|
365
|
-
file.push(new Uint8Array(await readFile(fullPath)), true);
|
|
366
|
-
}
|
|
367
|
-
};
|
|
368
|
-
try {
|
|
369
|
-
await walk(dirPath);
|
|
370
|
-
zip.end();
|
|
371
|
-
await Promise.race([outputClosed, outputErrored]);
|
|
372
|
-
}
|
|
373
|
-
catch (err) {
|
|
374
|
-
zip.terminate();
|
|
375
|
-
output.destroy(err);
|
|
376
|
-
await cleanupUploadTempPath(zipPath);
|
|
377
|
-
throw err;
|
|
378
|
-
}
|
|
379
|
-
return zipPath;
|
|
380
|
-
}
|
|
381
299
|
async addResource(input, actorPeerId) {
|
|
382
300
|
const pathOrUrl = input.pathOrUrl.trim();
|
|
383
301
|
if (!pathOrUrl) {
|
package/dist/commands/setup.js
CHANGED
|
@@ -4,7 +4,8 @@ import * as path from "node:path";
|
|
|
4
4
|
import * as readline from "node:readline";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { getEnv } from "../runtime-utils.js";
|
|
7
|
-
import { cleanOpenVikingRequestHeaders,
|
|
7
|
+
import { cleanOpenVikingRequestHeaders, } from "../request-headers.js";
|
|
8
|
+
import { createSetupNetworkProbes, } from "../services/setup/probe-service.js";
|
|
8
9
|
const HOME = os.homedir();
|
|
9
10
|
const OPENCLAW_DIR = getEnv("OPENCLAW_STATE_DIR") || path.join(HOME, ".openclaw");
|
|
10
11
|
const DEFAULT_REMOTE_URL = "http://127.0.0.1:1933";
|
|
@@ -76,7 +77,7 @@ const CONFIG_KEYS_TO_PRESERVE = [
|
|
|
76
77
|
"emitStandardDiagnostics",
|
|
77
78
|
"logFindRequests",
|
|
78
79
|
];
|
|
79
|
-
const DEFAULT_SETUP_PEER_ROLE = "
|
|
80
|
+
const DEFAULT_SETUP_PEER_ROLE = "none";
|
|
80
81
|
const ALLOWED_RECALL_TARGET_TYPES = ["resource", "user", "agent"];
|
|
81
82
|
function tr(langZh, en, zh) {
|
|
82
83
|
return langZh ? zh : en;
|
|
@@ -98,7 +99,9 @@ function normalizePeerRole(value) {
|
|
|
98
99
|
if (typeof value !== "string")
|
|
99
100
|
return undefined;
|
|
100
101
|
const role = value.trim().toLowerCase();
|
|
101
|
-
if (role === "
|
|
102
|
+
if (role === "person")
|
|
103
|
+
return "sender";
|
|
104
|
+
if (role === "none" || role === "assistant" || role === "sender")
|
|
102
105
|
return role;
|
|
103
106
|
return undefined;
|
|
104
107
|
}
|
|
@@ -121,7 +124,7 @@ function resolveSetupPeerRole(value) {
|
|
|
121
124
|
const role = normalizePeerRole(value);
|
|
122
125
|
if (role)
|
|
123
126
|
return role;
|
|
124
|
-
throw new Error('peer_role must be "none", "assistant", or "person"');
|
|
127
|
+
throw new Error('peer_role must be "none", "assistant", or "sender" (legacy alias: "person")');
|
|
125
128
|
}
|
|
126
129
|
function normalizeSetupRecallTargetTypes(value) {
|
|
127
130
|
if (value === undefined || value === null || value === "") {
|
|
@@ -172,12 +175,17 @@ function preserveCurrentConfig(existing) {
|
|
|
172
175
|
return config;
|
|
173
176
|
}
|
|
174
177
|
async function askPeerRole(zh, q, defaultValue) {
|
|
178
|
+
console.log(` ${tr(zh, "Memory scope — none (default): viking://user/<user_id>/memories, shared across all conversations; " +
|
|
179
|
+
"assistant: viking://user/<user_id>/peers/<assistant_id>/memories; " +
|
|
180
|
+
"sender: viking://user/<user_id>/peers/<sender_id>/memories (legacy alias: person).", "记忆归属 —— none(默认):viking://user/<user_id>/memories,所有对话共享;" +
|
|
181
|
+
"assistant:viking://user/<user_id>/peers/<assistant_id>/memories;" +
|
|
182
|
+
"sender:viking://user/<user_id>/peers/<sender_id>/memories(兼容旧值 person)。")}`);
|
|
175
183
|
while (true) {
|
|
176
|
-
const value = await q(tr(zh, "
|
|
184
|
+
const value = await q(tr(zh, "Memory scope (none/assistant/sender)", "记忆归属(none/assistant/sender)"), defaultValue);
|
|
177
185
|
const role = normalizePeerRole(value);
|
|
178
186
|
if (role)
|
|
179
187
|
return role;
|
|
180
|
-
console.log(` ✗ ${tr(zh, '
|
|
188
|
+
console.log(` ✗ ${tr(zh, 'Memory scope must be "none", "assistant", or "sender" (legacy "person" is also accepted).', '记忆归属必须是 "none"、"assistant" 或 "sender"(也兼容旧值 "person")。')}`);
|
|
181
189
|
}
|
|
182
190
|
}
|
|
183
191
|
async function askPeerPrefix(zh, q, defaultValue) {
|
|
@@ -240,118 +248,11 @@ function formatCompatRange() {
|
|
|
240
248
|
return `<= ${COMPATIBLE_SERVER_MAX}`;
|
|
241
249
|
return "any";
|
|
242
250
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const sessionsUrl = `${baseUrl.replace(/\/+$/, "")}/api/v1/sessions?limit=1`;
|
|
249
|
-
try {
|
|
250
|
-
const configuredRequestHeaders = resolveOpenVikingRequestHeaders({ headers: configuredHeaders });
|
|
251
|
-
const headers = {};
|
|
252
|
-
headers["X-API-Key"] = apiKey;
|
|
253
|
-
Object.assign(headers, configuredRequestHeaders);
|
|
254
|
-
const response = await fetch(sessionsUrl, {
|
|
255
|
-
headers,
|
|
256
|
-
signal: controller.signal,
|
|
257
|
-
});
|
|
258
|
-
if (response.ok) {
|
|
259
|
-
return { keyType: "user_key", needsAccountId: false, needsUserId: false, detail: "API key has full user context" };
|
|
260
|
-
}
|
|
261
|
-
// Server raises InvalidArgumentError (-> HTTP 400) when a ROOT key calls
|
|
262
|
-
// tenant-scoped endpoints without X-OpenViking-Account / X-OpenViking-User.
|
|
263
|
-
// 401/403 may also be returned by older versions, FastAPI validation can
|
|
264
|
-
// surface as 422. Treat all four as candidates for tenant-context errors.
|
|
265
|
-
if ([400, 401, 403, 422].includes(response.status)) {
|
|
266
|
-
let body = "";
|
|
267
|
-
try {
|
|
268
|
-
body = await response.text();
|
|
269
|
-
}
|
|
270
|
-
catch { /* ignore parse errors */ }
|
|
271
|
-
const lower = body.toLowerCase();
|
|
272
|
-
const needsAccount = /x-openviking-account|account[_ ]?id|account context|tenant/.test(lower);
|
|
273
|
-
const needsUser = /x-openviking-user|user[_ ]?id|user context|user key/.test(lower);
|
|
274
|
-
if (needsAccount || needsUser) {
|
|
275
|
-
return {
|
|
276
|
-
keyType: "root_key",
|
|
277
|
-
needsAccountId: needsAccount,
|
|
278
|
-
needsUserId: needsUser,
|
|
279
|
-
detail: body.slice(0, 200),
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
// Body did not name account/user/tenant explicitly (custom auth middleware,
|
|
283
|
-
// localized message, etc.). Re-probe with placeholder tenant headers; if
|
|
284
|
-
// the failure was due to missing tenant headers the response will change.
|
|
285
|
-
try {
|
|
286
|
-
const probeHeaders = {
|
|
287
|
-
"X-API-Key": apiKey,
|
|
288
|
-
"X-OpenViking-Account": "__probe__",
|
|
289
|
-
"X-OpenViking-User": "__probe__",
|
|
290
|
-
...configuredRequestHeaders,
|
|
291
|
-
};
|
|
292
|
-
const probe2 = await fetch(sessionsUrl, {
|
|
293
|
-
headers: probeHeaders,
|
|
294
|
-
signal: controller.signal,
|
|
295
|
-
});
|
|
296
|
-
if (probe2.status !== response.status) {
|
|
297
|
-
return {
|
|
298
|
-
keyType: "root_key",
|
|
299
|
-
needsAccountId: true,
|
|
300
|
-
needsUserId: true,
|
|
301
|
-
detail: body.slice(0, 200) || `HTTP ${response.status} -> ${probe2.status} after adding tenant headers`,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
catch { /* ignore probe errors, fall through to unknown */ }
|
|
306
|
-
if (response.status === 401 || response.status === 403) {
|
|
307
|
-
return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: `HTTP ${response.status} - authentication failed, verify your API key` };
|
|
308
|
-
}
|
|
309
|
-
return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: `HTTP ${response.status}${body ? ` - ${body.slice(0, 160)}` : ""}` };
|
|
310
|
-
}
|
|
311
|
-
return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: `HTTP ${response.status}` };
|
|
312
|
-
}
|
|
313
|
-
catch (err) {
|
|
314
|
-
return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: String(err instanceof Error ? err.message : err) };
|
|
315
|
-
}
|
|
316
|
-
finally {
|
|
317
|
-
clearTimeout(timeoutId);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
async function checkServiceHealth(baseUrl, apiKey, configuredHeaders) {
|
|
321
|
-
const controller = new AbortController();
|
|
322
|
-
const timeoutId = setTimeout(() => controller.abort(), 10_000);
|
|
323
|
-
try {
|
|
324
|
-
const configuredRequestHeaders = resolveOpenVikingRequestHeaders({ headers: configuredHeaders });
|
|
325
|
-
const headers = {};
|
|
326
|
-
if (apiKey) {
|
|
327
|
-
headers["X-API-Key"] = apiKey;
|
|
328
|
-
}
|
|
329
|
-
Object.assign(headers, configuredRequestHeaders);
|
|
330
|
-
const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/health`, {
|
|
331
|
-
headers,
|
|
332
|
-
signal: controller.signal,
|
|
333
|
-
});
|
|
334
|
-
if (response.ok) {
|
|
335
|
-
try {
|
|
336
|
-
const data = await response.json();
|
|
337
|
-
const result = (data.result ?? data);
|
|
338
|
-
const version = String(result.version ?? data.version ?? "");
|
|
339
|
-
const compatibility = checkVersionCompatibility(version);
|
|
340
|
-
return { ok: true, version, error: "", compatibility, pluginVersion: PLUGIN_VERSION, compatRange: formatCompatRange() };
|
|
341
|
-
}
|
|
342
|
-
catch {
|
|
343
|
-
return { ok: true, version: "", error: "", compatibility: "unknown", pluginVersion: PLUGIN_VERSION, compatRange: formatCompatRange() };
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
return { ok: false, version: "", error: `HTTP ${response.status}`, compatibility: "unknown", pluginVersion: PLUGIN_VERSION, compatRange: formatCompatRange() };
|
|
347
|
-
}
|
|
348
|
-
catch (err) {
|
|
349
|
-
return { ok: false, version: "", error: String(err instanceof Error ? err.message : err), compatibility: "unknown", pluginVersion: PLUGIN_VERSION, compatRange: formatCompatRange() };
|
|
350
|
-
}
|
|
351
|
-
finally {
|
|
352
|
-
clearTimeout(timeoutId);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
251
|
+
const setupProbes = createSetupNetworkProbes({
|
|
252
|
+
pluginVersion: PLUGIN_VERSION,
|
|
253
|
+
compatRange: formatCompatRange(),
|
|
254
|
+
checkVersionCompatibility,
|
|
255
|
+
});
|
|
355
256
|
function readOpenClawConfig(configPath) {
|
|
356
257
|
if (!fs.existsSync(configPath))
|
|
357
258
|
return {};
|
|
@@ -473,7 +374,7 @@ export function registerSetupCli(api) {
|
|
|
473
374
|
.option("--zh", "Chinese prompts")
|
|
474
375
|
.option("--base-url <url>", "OpenViking server URL (enables non-interactive mode)")
|
|
475
376
|
.option("--api-key <key>", "API key for authentication")
|
|
476
|
-
.option("--peer-role <role>", "
|
|
377
|
+
.option("--peer-role <role>", "Memory scope: none (shared), assistant (per assistant), or sender (per sender); person is a legacy alias")
|
|
477
378
|
.option("--peer-prefix <prefix>", "Prefix for assistant peer_id values")
|
|
478
379
|
.option("--account-id <id>", "Account ID (required for root API keys)")
|
|
479
380
|
.option("--user-id <id>", "User ID (required for root API keys)")
|
|
@@ -617,7 +518,7 @@ async function runRemoteCheck(zh, existing) {
|
|
|
617
518
|
const apiKey = existing.apiKey ? String(existing.apiKey) : undefined;
|
|
618
519
|
const headers = nonEmptyOpenVikingRequestHeaders(existing.headers);
|
|
619
520
|
console.log(tr(zh, `Testing connectivity to ${baseUrl}...`, `正在测试连接 ${baseUrl}...`));
|
|
620
|
-
const health = await checkServiceHealth(baseUrl, apiKey, headers);
|
|
521
|
+
const health = await setupProbes.checkServiceHealth(baseUrl, apiKey, headers);
|
|
621
522
|
if (health.ok) {
|
|
622
523
|
const ver = health.version ? ` (version: ${health.version})` : "";
|
|
623
524
|
console.log(` ✓ ${tr(zh, `Connected successfully${ver}`, `连接成功${ver}`)}`);
|
|
@@ -637,7 +538,7 @@ async function setupNonInteractive(configPath, params) {
|
|
|
637
538
|
throw new Error("peer_prefix may only contain letters, digits, underscores, and hyphens, or be empty");
|
|
638
539
|
}
|
|
639
540
|
// Phase 1: validate connectivity and key type BEFORE writing config
|
|
640
|
-
const health = await checkServiceHealth(baseUrl, apiKey);
|
|
541
|
+
const health = await setupProbes.checkServiceHealth(baseUrl, apiKey);
|
|
641
542
|
if (!health.ok && !allowOffline) {
|
|
642
543
|
return {
|
|
643
544
|
success: false,
|
|
@@ -648,7 +549,7 @@ async function setupNonInteractive(configPath, params) {
|
|
|
648
549
|
error: `Server unreachable: ${health.error}. Use --allow-offline to save config anyway.`,
|
|
649
550
|
};
|
|
650
551
|
}
|
|
651
|
-
const keyProbe = health.ok ? await probeApiKeyType(baseUrl, apiKey) : undefined;
|
|
552
|
+
const keyProbe = health.ok ? await setupProbes.probeApiKeyType(baseUrl, apiKey) : undefined;
|
|
652
553
|
if (keyProbe?.keyType === "root_key" && (!accountId || !userId)) {
|
|
653
554
|
const missing = [];
|
|
654
555
|
if (!accountId)
|
|
@@ -786,8 +687,8 @@ async function getStatus(configPath) {
|
|
|
786
687
|
const baseUrl = String(existing.baseUrl ?? DEFAULT_REMOTE_URL);
|
|
787
688
|
const apiKey = existing.apiKey ? String(existing.apiKey) : undefined;
|
|
788
689
|
const headers = nonEmptyOpenVikingRequestHeaders(existing.headers);
|
|
789
|
-
const health = await checkServiceHealth(baseUrl, apiKey, headers);
|
|
790
|
-
const keyProbe = health.ok ? await probeApiKeyType(baseUrl, apiKey, headers) : undefined;
|
|
690
|
+
const health = await setupProbes.checkServiceHealth(baseUrl, apiKey, headers);
|
|
691
|
+
const keyProbe = health.ok ? await setupProbes.probeApiKeyType(baseUrl, apiKey, headers) : undefined;
|
|
791
692
|
const peerPrefix = resolveExistingPeerPrefix(existing);
|
|
792
693
|
return {
|
|
793
694
|
configured: true,
|
|
@@ -880,7 +781,7 @@ async function setupRemote(zh, configPath, existing, q) {
|
|
|
880
781
|
let userId = existing?.userId ? String(existing.userId) : "";
|
|
881
782
|
if (apiKey) {
|
|
882
783
|
console.log(tr(zh, " Detecting API key type...", " 正在检测 API Key 类型..."));
|
|
883
|
-
const probe = await probeApiKeyType(baseUrl, apiKey, headers);
|
|
784
|
+
const probe = await setupProbes.probeApiKeyType(baseUrl, apiKey, headers);
|
|
884
785
|
if (probe.keyType === "root_key") {
|
|
885
786
|
console.log(tr(zh, " ⚠ Root API key detected. accountId and userId are required.", " ⚠ 检测到 Root API Key,需要提供 accountId 和 userId。"));
|
|
886
787
|
accountId = await q(tr(zh, "Account ID (required for root key)", "Account ID(root key 必填)"), accountId);
|
|
@@ -896,7 +797,7 @@ async function setupRemote(zh, configPath, existing, q) {
|
|
|
896
797
|
: "";
|
|
897
798
|
console.log("");
|
|
898
799
|
console.log(tr(zh, `Testing connectivity to ${baseUrl}...`, `正在测试连接 ${baseUrl}...`));
|
|
899
|
-
const health = await checkServiceHealth(baseUrl, apiKey || undefined, headers);
|
|
800
|
+
const health = await setupProbes.checkServiceHealth(baseUrl, apiKey || undefined, headers);
|
|
900
801
|
if (health.ok) {
|
|
901
802
|
const ver = health.version ? ` (version: ${health.version})` : "";
|
|
902
803
|
console.log(` ✓ ${tr(zh, `Connected successfully${ver}`, `连接成功${ver}`)}`);
|
|
@@ -952,6 +853,8 @@ async function setupRemote(zh, configPath, existing, q) {
|
|
|
952
853
|
export const __test__ = {
|
|
953
854
|
isLegacyLocalMode,
|
|
954
855
|
isValidPeerPrefixInput,
|
|
856
|
+
normalizePeerRole,
|
|
857
|
+
resolveSetupPeerRole,
|
|
955
858
|
normalizeSetupRecallTargetTypes,
|
|
956
859
|
activateContextEngineSlot,
|
|
957
860
|
isContextEngineSlotActive,
|
|
@@ -960,7 +863,6 @@ export const __test__ = {
|
|
|
960
863
|
checkVersionCompatibility,
|
|
961
864
|
parseVersionTuple,
|
|
962
865
|
compareVersions,
|
|
963
|
-
probeApiKeyType,
|
|
964
866
|
ensureInstallRecord,
|
|
965
867
|
findPluginPackageRoot,
|
|
966
868
|
readCompatRangeFromManifest,
|
package/dist/config.js
CHANGED
|
@@ -18,7 +18,7 @@ const DEFAULT_COMMIT_TOKEN_THRESHOLD_RATIO = 0.5;
|
|
|
18
18
|
const DEFAULT_COMMIT_KEEP_RECENT_COUNT = 10;
|
|
19
19
|
const DEFAULT_BYPASS_SESSION_PATTERNS = [];
|
|
20
20
|
const DEFAULT_EMIT_STANDARD_DIAGNOSTICS = false;
|
|
21
|
-
const DEFAULT_PEER_ROLE = "
|
|
21
|
+
const DEFAULT_PEER_ROLE = "none";
|
|
22
22
|
const DEFAULT_PEER_PREFIX = "";
|
|
23
23
|
const DEFAULT_TRACE_RECALL_DIR = "~/.openclaw/openviking/recall-traces";
|
|
24
24
|
const DEFAULT_TRACE_RECALL_RETENTION_DAYS = 14;
|
|
@@ -144,13 +144,16 @@ function resolveSecret(value, label) {
|
|
|
144
144
|
function resolvePeerRole(configured) {
|
|
145
145
|
if (typeof configured === "string") {
|
|
146
146
|
const role = configured.trim().toLowerCase();
|
|
147
|
-
if (role === "none" || role === "assistant" || role === "
|
|
147
|
+
if (role === "none" || role === "assistant" || role === "sender") {
|
|
148
148
|
return role;
|
|
149
149
|
}
|
|
150
|
-
|
|
150
|
+
if (role === "person") {
|
|
151
|
+
return "sender";
|
|
152
|
+
}
|
|
153
|
+
throw new Error(`openviking peer_role must be "none", "assistant", or "sender" (legacy alias: "person")`);
|
|
151
154
|
}
|
|
152
155
|
if (configured !== undefined) {
|
|
153
|
-
throw new Error(`openviking peer_role must be "none", "assistant", or "person"`);
|
|
156
|
+
throw new Error(`openviking peer_role must be "none", "assistant", or "sender" (legacy alias: "person")`);
|
|
154
157
|
}
|
|
155
158
|
return DEFAULT_PEER_ROLE;
|
|
156
159
|
}
|
|
@@ -481,14 +484,16 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
481
484
|
help: "HTTP URL when mode is remote (or use ${OPENVIKING_BASE_URL})",
|
|
482
485
|
},
|
|
483
486
|
peer_role: {
|
|
484
|
-
label: "
|
|
487
|
+
label: "Memory Scope (peer_role)",
|
|
485
488
|
placeholder: DEFAULT_PEER_ROLE,
|
|
486
|
-
help: '
|
|
489
|
+
help: 'Where peer-scoped memories are stored. "none" (default): viking://user/<user_id>/memories. ' +
|
|
490
|
+
'"assistant": viking://user/<user_id>/peers/<assistant_id>/memories. ' +
|
|
491
|
+
'"sender": viking://user/<user_id>/peers/<sender_id>/memories. Legacy "person" is accepted as "sender".',
|
|
487
492
|
},
|
|
488
493
|
peer_prefix: {
|
|
489
494
|
label: "Peer Prefix",
|
|
490
495
|
placeholder: "optional-prefix",
|
|
491
|
-
help: "
|
|
496
|
+
help: 'Only used when Memory Scope is "assistant". Prefix added to the peer id derived from the OpenClaw agent id.',
|
|
492
497
|
},
|
|
493
498
|
apiKey: {
|
|
494
499
|
label: "OpenViking API Key",
|
|
@@ -598,7 +603,7 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
598
603
|
bypassSessionPatterns: {
|
|
599
604
|
label: "Bypass Session Patterns",
|
|
600
605
|
placeholder: "agent:*:cron:**",
|
|
601
|
-
help: "Completely bypass OpenViking for matching session keys. Use * within one segment and ** across segments.",
|
|
606
|
+
help: "Completely bypass OpenViking for matching session keys (no capture, recall, or commit; compaction falls back to OpenClaw's native compactor). Use * within one segment and ** across segments.",
|
|
602
607
|
advanced: true,
|
|
603
608
|
},
|
|
604
609
|
commitTokenThresholdRatio: {
|
package/dist/context-engine.js
CHANGED
|
@@ -3,6 +3,7 @@ import { compileSessionPatterns, shouldBypassSession, } from "./text-utils.js";
|
|
|
3
3
|
import { estimateAgentMessageTokens, estimateAgentMessagesTokens } from "./token-estimator.js";
|
|
4
4
|
import { openClawSessionToOvStorageId } from "./routing/identity-routing.js";
|
|
5
5
|
import { assembleOpenVikingSession, afterTurnOpenVikingSession, compactOpenVikingSession, commitOpenVikingSession, } from "./services/context-lifecycle-service.js";
|
|
6
|
+
import { loadRuntimeCompactionDelegate } from "./plugin/openclaw-runtime-compaction.js";
|
|
6
7
|
function roughEstimate(messages) {
|
|
7
8
|
return estimateAgentMessagesTokens(messages);
|
|
8
9
|
}
|
|
@@ -255,6 +256,10 @@ export function createMemoryOpenVikingContextEngine(params) {
|
|
|
255
256
|
logger,
|
|
256
257
|
resolveAgentId,
|
|
257
258
|
isBypassedSession,
|
|
259
|
+
runtimeCompact: async () => {
|
|
260
|
+
const delegate = await loadRuntimeCompactionDelegate();
|
|
261
|
+
return delegate ? delegate(compactParams) : undefined;
|
|
262
|
+
},
|
|
258
263
|
diag,
|
|
259
264
|
});
|
|
260
265
|
},
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Bridge to OpenClaw's built-in compactor for sessions that bypass OpenViking.
|
|
2
|
+
// The host exports `delegateCompactionToRuntime` from `openclaw/plugin-sdk/core`
|
|
3
|
+
// (present since the earliest supported host); it is resolved lazily because the
|
|
4
|
+
// plugin builds without the host SDK installed.
|
|
5
|
+
const PLUGIN_SDK_CORE_SPECIFIER = "openclaw/plugin-sdk/core";
|
|
6
|
+
let cached;
|
|
7
|
+
export function loadRuntimeCompactionDelegate() {
|
|
8
|
+
cached ??= (async () => {
|
|
9
|
+
try {
|
|
10
|
+
const mod = (await import(PLUGIN_SDK_CORE_SPECIFIER));
|
|
11
|
+
return typeof mod?.delegateCompactionToRuntime === "function"
|
|
12
|
+
? mod.delegateCompactionToRuntime
|
|
13
|
+
: undefined;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
})();
|
|
19
|
+
return cached;
|
|
20
|
+
}
|
|
@@ -45,7 +45,7 @@ export function registerOpenVikingMemoryTools(deps) {
|
|
|
45
45
|
const peerId = resolveOpenVikingMessagePeerId({
|
|
46
46
|
peerRole: deps.peerRole,
|
|
47
47
|
role,
|
|
48
|
-
|
|
48
|
+
senderPeerId: session.actorPeerId,
|
|
49
49
|
assistantPeerId: session.actorPeerId,
|
|
50
50
|
});
|
|
51
51
|
await client.addSessionMessage(sessionId, role, [{ type: "text", text }], undefined, undefined, peerId);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSessionAgentResolver, openClawSessionToOvStorageId, resolveOpenVikingActorPeerId, sanitizeOpenVikingPeerId, } from "../routing/identity-routing.js";
|
|
2
2
|
export function createOpenVikingSessionRoutingRuntime(options) {
|
|
3
|
-
const peerRole = options.peerRole ?? "
|
|
3
|
+
const peerRole = options.peerRole ?? "none";
|
|
4
4
|
const sessionAgentResolver = createSessionAgentResolver(options.peerPrefix);
|
|
5
5
|
const rememberSessionAgentId = (ctx) => {
|
|
6
6
|
sessionAgentResolver.remember(ctx);
|
|
@@ -48,7 +48,7 @@ export function createOpenVikingSessionRoutingRuntime(options) {
|
|
|
48
48
|
agentId,
|
|
49
49
|
actorPeerId: resolveOpenVikingActorPeerId({
|
|
50
50
|
peerRole,
|
|
51
|
-
|
|
51
|
+
senderPeerId: sanitizeOpenVikingPeerId(ctx?.requesterSenderId ?? ctx?.senderId),
|
|
52
52
|
assistantPeerId: agentId,
|
|
53
53
|
}),
|
|
54
54
|
};
|
|
@@ -12,8 +12,8 @@ export function sanitizeOpenVikingPeerId(raw) {
|
|
|
12
12
|
return normalized || undefined;
|
|
13
13
|
}
|
|
14
14
|
export function resolveOpenVikingMessagePeerId(params) {
|
|
15
|
-
if (params.peerRole === "
|
|
16
|
-
return params.
|
|
15
|
+
if (params.peerRole === "sender" && params.role === "user") {
|
|
16
|
+
return params.senderPeerId;
|
|
17
17
|
}
|
|
18
18
|
if (params.peerRole === "assistant" && params.role === "assistant") {
|
|
19
19
|
return params.assistantPeerId;
|
|
@@ -23,10 +23,10 @@ export function resolveOpenVikingMessagePeerId(params) {
|
|
|
23
23
|
export function resolveOpenVikingActorPeerId(params) {
|
|
24
24
|
const actorPeerId = resolveOpenVikingMessagePeerId({
|
|
25
25
|
...params,
|
|
26
|
-
role: params.peerRole === "
|
|
26
|
+
role: params.peerRole === "sender" ? "user" : "assistant",
|
|
27
27
|
});
|
|
28
|
-
if (params.peerRole === "
|
|
29
|
-
throw new Error("openviking: peer_role=
|
|
28
|
+
if (params.peerRole === "sender" && !actorPeerId) {
|
|
29
|
+
throw new Error("openviking: peer_role=sender requires a sender identity");
|
|
30
30
|
}
|
|
31
31
|
return actorPeerId;
|
|
32
32
|
}
|