@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/config.ts CHANGED
@@ -30,7 +30,8 @@ export type OpenVikingSecretRef =
30
30
  export type MemoryOpenVikingConfig = {
31
31
  mode?: "remote";
32
32
  baseUrl?: string;
33
- peer_role?: "none" | "assistant" | "person";
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 = "assistant" as const;
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 === "person") {
301
+ if (role === "none" || role === "assistant" || role === "sender") {
299
302
  return role;
300
303
  }
301
- throw new Error(`openviking peer_role must be "none", "assistant", or "person"`);
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(`openviking peer_role must be "none", "assistant", or "person"`);
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: "Peer Role",
789
+ label: "Memory Scope (peer_role)",
780
790
  placeholder: DEFAULT_PEER_ROLE,
781
- help: 'Controls which session messages get peer_id: "none", "assistant", or "person".',
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: "Optional prefix applied to assistant peer_id values derived from OpenClaw runtime agent IDs.",
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 { MemoryOpenVikingConfig } from "./config.js";
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: Required<MemoryOpenVikingConfig>;
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) {
@@ -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, resolveOpenVikingRequestHeaders, } from "../request-headers.js";
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 = "assistant";
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 === "none" || role === "assistant" || role === "person")
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, "Peer Role (none/assistant/person)", "Peer Role(none/assistant/person)"), defaultValue);
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, 'Peer Role must be "none", "assistant", or "person".', 'Peer Role 必须是 "none"、"assistant" 或 "person"。')}`);
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
- async function probeApiKeyType(baseUrl, apiKey, configuredHeaders) {
244
- if (!apiKey)
245
- return { keyType: "no_key", needsAccountId: false, needsUserId: false, detail: "No API key configured" };
246
- const controller = new AbortController();
247
- const timeoutId = setTimeout(() => controller.abort(), 10_000);
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>", "Peer ID role: none, assistant, or person")
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 = "assistant";
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 === "person") {
147
+ if (role === "none" || role === "assistant" || role === "sender") {
148
148
  return role;
149
149
  }
150
- throw new Error(`openviking peer_role must be "none", "assistant", or "person"`);
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: "Peer Role",
487
+ label: "Memory Scope (peer_role)",
485
488
  placeholder: DEFAULT_PEER_ROLE,
486
- help: 'Controls which session messages get peer_id: "none", "assistant", or "person".',
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: "Optional prefix applied to assistant peer_id values derived from OpenClaw runtime agent IDs.",
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: {
@@ -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
- personPeerId: session.actorPeerId,
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 ?? "assistant";
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
- personPeerId: sanitizeOpenVikingPeerId(ctx?.requesterSenderId ?? ctx?.senderId),
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 === "person" && params.role === "user") {
16
- return params.personPeerId;
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 === "person" ? "user" : "assistant",
26
+ role: params.peerRole === "sender" ? "user" : "assistant",
27
27
  });
28
- if (params.peerRole === "person" && !actorPeerId) {
29
- throw new Error("openviking: peer_role=person requires a sender identity");
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
  }