@openviking/openclaw-plugin 2026.9.1 → 2026.9.4-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.
@@ -80,14 +80,14 @@
80
80
  "help": "HTTP URL when mode is remote (or ${OPENVIKING_BASE_URL})"
81
81
  },
82
82
  "peer_role": {
83
- "label": "Peer Role",
84
- "placeholder": "assistant",
85
- "help": "Controls which messages include peer_id: none, assistant, or person. New installs default to assistant."
83
+ "label": "Memory Scope (peer_role)",
84
+ "placeholder": "none",
85
+ "help": "Where peer-scoped memories are stored. none (default): viking://user/<user_id>/memories. assistant: viking://user/<user_id>/peers/<assistant_id>/memories. sender: viking://user/<user_id>/peers/<sender_id>/memories. Legacy person is accepted as sender."
86
86
  },
87
87
  "peer_prefix": {
88
88
  "label": "Peer Prefix",
89
89
  "placeholder": "optional-prefix",
90
- "help": "Optional prefix for assistant peer_id values derived from OpenClaw runtime agent IDs."
90
+ "help": "Only used when Memory Scope is assistant. Prefix added to the peer id derived from the OpenClaw agent id."
91
91
  },
92
92
  "apiKey": {
93
93
  "label": "OpenViking API Key",
@@ -144,9 +144,9 @@
144
144
  },
145
145
  "autoRecallTimeoutMs": {
146
146
  "label": "Auto-Recall Timeout (ms)",
147
- "placeholder": "5000",
147
+ "placeholder": "15000",
148
148
  "advanced": true,
149
- "help": "Outer time budget for the whole auto-recall flow, including search, ranking, and memory reads."
149
+ "help": "Outer time budget for the server-assembled context search, including session-aware query expansion."
150
150
  },
151
151
  "recallResources": {
152
152
  "label": "Recall Resources",
@@ -279,10 +279,11 @@
279
279
  "enum": [
280
280
  "none",
281
281
  "assistant",
282
+ "sender",
282
283
  "person"
283
284
  ],
284
- "default": "assistant",
285
- "description": "Controls which session messages include peer_id."
285
+ "default": "none",
286
+ "description": "Memory scope. none: shared user memory. assistant: peer memory keyed by OpenClaw assistant id. sender: peer memory keyed by sender id. person is a legacy alias for sender. Also controls which session messages include peer_id."
286
287
  },
287
288
  "peer_prefix": {
288
289
  "type": "string",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openviking/openclaw-plugin",
3
- "version": "2026.9.1",
3
+ "version": "2026.9.4-1",
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",
@@ -26,6 +26,7 @@
26
26
  "config/feature-gates.json",
27
27
  "registries/",
28
28
  "routing/",
29
+ "shared/",
29
30
  "plugin/",
30
31
  "services/",
31
32
  "!vitest.config.ts",
@@ -9,7 +9,7 @@ type Logger = {
9
9
  type ClientRuntimeConfig = {
10
10
  baseUrl: string;
11
11
  apiKey: string;
12
- peer_role: "none" | "assistant" | "person";
12
+ peer_role: "none" | "assistant" | "sender";
13
13
  peer_prefix: string;
14
14
  timeoutMs: number;
15
15
  accountId?: string;
@@ -114,7 +114,7 @@ export function registerOpenVikingMemoryTools(deps: OpenVikingMemoryToolsDeps):
114
114
  const peerId = resolveOpenVikingMessagePeerId({
115
115
  peerRole: deps.peerRole,
116
116
  role,
117
- personPeerId: session.actorPeerId,
117
+ senderPeerId: session.actorPeerId,
118
118
  assistantPeerId: session.actorPeerId,
119
119
  });
120
120
  await client.addSessionMessage(
@@ -34,7 +34,7 @@ export function createOpenVikingSessionRoutingRuntime(options: {
34
34
  logFindRequests: boolean;
35
35
  logger: Logger;
36
36
  }) {
37
- const peerRole = options.peerRole ?? "assistant";
37
+ const peerRole = options.peerRole ?? "none";
38
38
  const sessionAgentResolver = createSessionAgentResolver(options.peerPrefix);
39
39
 
40
40
  const rememberSessionAgentId = (ctx: SessionAgentLookup) => {
@@ -97,7 +97,7 @@ export function createOpenVikingSessionRoutingRuntime(options: {
97
97
  agentId,
98
98
  actorPeerId: resolveOpenVikingActorPeerId({
99
99
  peerRole,
100
- personPeerId: sanitizeOpenVikingPeerId(ctx?.requesterSenderId ?? ctx?.senderId),
100
+ senderPeerId: sanitizeOpenVikingPeerId(ctx?.requesterSenderId ?? ctx?.senderId),
101
101
  assistantPeerId: agentId,
102
102
  }),
103
103
  };
@@ -15,7 +15,7 @@ export type SessionAgentLookup = {
15
15
  ovSessionId?: string;
16
16
  };
17
17
 
18
- export type OpenVikingPeerRole = "none" | "assistant" | "person";
18
+ export type OpenVikingPeerRole = "none" | "assistant" | "sender";
19
19
 
20
20
  export function sanitizeOpenVikingPeerId(raw?: string): string | undefined {
21
21
  const normalized = raw
@@ -29,11 +29,11 @@ export function sanitizeOpenVikingPeerId(raw?: string): string | undefined {
29
29
  export function resolveOpenVikingMessagePeerId(params: {
30
30
  peerRole: OpenVikingPeerRole;
31
31
  role?: string;
32
- personPeerId?: string;
32
+ senderPeerId?: string;
33
33
  assistantPeerId?: string;
34
34
  }): string | undefined {
35
- if (params.peerRole === "person" && params.role === "user") {
36
- return params.personPeerId;
35
+ if (params.peerRole === "sender" && params.role === "user") {
36
+ return params.senderPeerId;
37
37
  }
38
38
  if (params.peerRole === "assistant" && params.role === "assistant") {
39
39
  return params.assistantPeerId;
@@ -43,15 +43,15 @@ export function resolveOpenVikingMessagePeerId(params: {
43
43
 
44
44
  export function resolveOpenVikingActorPeerId(params: {
45
45
  peerRole: OpenVikingPeerRole;
46
- personPeerId?: string;
46
+ senderPeerId?: string;
47
47
  assistantPeerId?: string;
48
48
  }): string | undefined {
49
49
  const actorPeerId = resolveOpenVikingMessagePeerId({
50
50
  ...params,
51
- role: params.peerRole === "person" ? "user" : "assistant",
51
+ role: params.peerRole === "sender" ? "user" : "assistant",
52
52
  });
53
- if (params.peerRole === "person" && !actorPeerId) {
54
- throw new Error("openviking: peer_role=person requires a sender identity");
53
+ if (params.peerRole === "sender" && !actorPeerId) {
54
+ throw new Error("openviking: peer_role=sender requires a sender identity");
55
55
  }
56
56
  return actorPeerId;
57
57
  }
@@ -497,8 +497,8 @@ export async function assembleOpenVikingSession({
497
497
  const routingRef = sessionId ?? sessionKey ?? ovSessionId;
498
498
  const agentId = resolveAgentId(routingRef, sessionKey, ovSessionId);
499
499
  const actorPeerId = resolveOpenVikingActorPeerId({
500
- peerRole: cfg.peer_role ?? "assistant",
501
- personPeerId: sanitizeOpenVikingPeerId(sender.senderId),
500
+ peerRole: cfg.peer_role ?? "none",
501
+ senderPeerId: sanitizeOpenVikingPeerId(sender.senderId),
502
502
  assistantPeerId: agentId,
503
503
  });
504
504
  const queryConfig = await queryConfigStore?.getEffective({
@@ -910,9 +910,9 @@ export async function afterTurnOpenVikingSession({
910
910
  undefined,
911
911
  createdAt,
912
912
  resolveOpenVikingMessagePeerId({
913
- peerRole: cfg.peer_role ?? "assistant",
913
+ peerRole: cfg.peer_role ?? "none",
914
914
  role: msg.role,
915
- personPeerId: senderRoleId,
915
+ senderPeerId: senderRoleId,
916
916
  assistantPeerId: agentId,
917
917
  }),
918
918
  );
@@ -13,6 +13,8 @@ import {
13
13
 
14
14
  export type { SetupIO, SlotActivationResult };
15
15
 
16
+ type SetupPeerRole = "none" | "assistant" | "sender" | "person";
17
+
16
18
  export type VersionCompatibility = "compatible" | "server_too_old" | "server_too_new" | "unknown";
17
19
 
18
20
  export type HealthResult = {
@@ -38,7 +40,7 @@ export type SetupResult = {
38
40
  mode: string;
39
41
  baseUrl: string;
40
42
  apiKey?: string;
41
- peer_role?: "none" | "assistant" | "person";
43
+ peer_role?: SetupPeerRole;
42
44
  peer_prefix?: string;
43
45
  accountId?: string;
44
46
  userId?: string;
@@ -57,7 +59,7 @@ export type StatusResult = {
57
59
  mode: string;
58
60
  baseUrl: string;
59
61
  hasApiKey: boolean;
60
- peer_role?: "none" | "assistant" | "person";
62
+ peer_role?: SetupPeerRole;
61
63
  peer_prefix?: string;
62
64
  hasAccountId: boolean;
63
65
  hasUserId: boolean;
@@ -71,7 +73,7 @@ export type SetupParams = {
71
73
  baseUrl: string;
72
74
  apiKey?: string;
73
75
  headers?: OpenVikingRequestHeaders;
74
- peerRole?: "none" | "assistant" | "person";
76
+ peerRole?: SetupPeerRole;
75
77
  peerPrefix?: string;
76
78
  accountId?: string;
77
79
  userId?: string;
@@ -85,7 +87,7 @@ export type InteractiveRemoteConfigParams = {
85
87
  baseUrl: string;
86
88
  apiKey?: string;
87
89
  headers?: OpenVikingRequestHeaders;
88
- peerRole?: "none" | "assistant" | "person";
90
+ peerRole?: SetupPeerRole;
89
91
  peerPrefix?: string;
90
92
  accountId?: string;
91
93
  userId?: string;
@@ -315,7 +317,7 @@ export function createOpenVikingSetupService({
315
317
  baseUrl,
316
318
  ...(apiKey ? { apiKey: maskKey(apiKey) } : {}),
317
319
  ...(headers ? { headers } : {}),
318
- ...(existing.peer_role ? { peer_role: String(existing.peer_role) as "none" | "assistant" | "person" } : {}),
320
+ ...(existing.peer_role ? { peer_role: String(existing.peer_role) as SetupPeerRole } : {}),
319
321
  ...(existing.peer_prefix ? { peer_prefix: String(existing.peer_prefix) } : {}),
320
322
  ...(existing.accountId ? { accountId: String(existing.accountId) } : {}),
321
323
  ...(existing.userId ? { userId: String(existing.userId) } : {}),
@@ -346,7 +348,7 @@ export function createOpenVikingSetupService({
346
348
  mode: String(existing.mode ?? "remote"),
347
349
  baseUrl,
348
350
  hasApiKey: !!existing.apiKey,
349
- ...(existing.peer_role ? { peer_role: String(existing.peer_role) as "none" | "assistant" | "person" } : {}),
351
+ ...(existing.peer_role ? { peer_role: String(existing.peer_role) as SetupPeerRole } : {}),
350
352
  ...(existing.peer_prefix ? { peer_prefix: String(existing.peer_prefix) } : {}),
351
353
  hasAccountId: !!existing.accountId,
352
354
  hasUserId: !!existing.userId,
@@ -0,0 +1,195 @@
1
+ // GENERATED FROM examples/memory-plugin-shared/lib. DO NOT EDIT.
2
+ import { createHash } from "node:crypto";
3
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
4
+ import { dirname } from "node:path";
5
+
6
+ export const NO_RELEVANT_MEMORY = "NO_RELEVANT_MEMORY";
7
+ export const DIGEST_HEADER = "OpenViking memory digest:";
8
+ const COMPRESS_OK = "ok";
9
+ const COMPRESS_EMPTY = "empty";
10
+ const COMPRESS_FAILED = "failed";
11
+
12
+ // Medium-constraint prompt: state the goal and two structural floors, but no hard
13
+ // bullet-length contract. Hard per-bullet limits pin the digest to headline
14
+ // density; leaving it unconstrained lets small models rewrite long URIs into dead
15
+ // links, which the URI repair below cleans up.
16
+ export function buildRecallCompressionPrompt({ query, rendered, maxBullets = 6 }) {
17
+ return `You are a memory relevance compressor utility.
18
+ Do not use any tools. Do not investigate. Only transform the given text.
19
+
20
+ User query:
21
+ ${query}
22
+
23
+ Retrieved OpenViking context fragments:
24
+ ${rendered}
25
+
26
+ Write a memory digest for a coding agent about to answer that query. Keep the
27
+ concrete facts (paths, identifiers, decisions, constraints); drop pleasantries
28
+ and conversational filler.
29
+
30
+ Format rules:
31
+ - Group related facts by topic, one bullet per topic, at most ${maxBullets} bullets.
32
+ - Start every bullet with "- ".
33
+ - End every bullet with its source, copied verbatim from the fragments above:
34
+ "来源:viking://..." or "source: viking://...". Never edit, shorten, or invent a URI.
35
+ - Output the digest body only. No preamble, no closing remark.
36
+
37
+ If nothing above is relevant to the query, output exactly: ${NO_RELEVANT_MEMORY}`;
38
+ }
39
+
40
+ function editDistance(a, b) {
41
+ if (a === b) return 0;
42
+ const rows = a.length + 1;
43
+ const cols = b.length + 1;
44
+ let prev = Array.from({ length: cols }, (_, i) => i);
45
+ for (let i = 1; i < rows; i += 1) {
46
+ const cur = [i];
47
+ for (let j = 1; j < cols; j += 1) {
48
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
49
+ cur[j] = Math.min(cur[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
50
+ }
51
+ prev = cur;
52
+ }
53
+ return prev[cols - 1];
54
+ }
55
+
56
+ function nearestUri(candidate, validUris) {
57
+ let best = "";
58
+ let bestDistance = Infinity;
59
+ for (const uri of validUris) {
60
+ const distance = editDistance(candidate, uri);
61
+ if (distance < bestDistance) {
62
+ best = uri;
63
+ bestDistance = distance;
64
+ }
65
+ }
66
+ // Only repair near-misses; an unrelated hallucination is dropped instead.
67
+ const tolerance = Math.max(4, Math.floor(candidate.length * 0.25));
68
+ return bestDistance <= tolerance ? best : "";
69
+ }
70
+
71
+ /**
72
+ * Small models occasionally mangle long URIs. Snap every cited URI back onto the
73
+ * set the server actually returned, and drop bullets whose citation cannot be
74
+ * recovered so the digest never carries a dead link.
75
+ */
76
+ export function repairDigestUris(digest, validUris = []) {
77
+ const text = String(digest || "");
78
+ if (!text) return "";
79
+ const valid = validUris.map((uri) => String(uri || "").trim()).filter(Boolean);
80
+ if (!valid.length) return text;
81
+ const validSet = new Set(valid);
82
+
83
+ const lines = [];
84
+ for (const line of text.split(/\r?\n/)) {
85
+ if (!line.trim().startsWith("- ")) {
86
+ lines.push(line);
87
+ continue;
88
+ }
89
+ let dropped = false;
90
+ const repaired = line.replace(/viking:\/\/[^\s<>"')\]]+/g, (uri) => {
91
+ if (validSet.has(uri)) return uri;
92
+ const nearest = nearestUri(uri, valid);
93
+ if (nearest) return nearest;
94
+ dropped = true;
95
+ return uri;
96
+ });
97
+ if (!dropped) lines.push(repaired);
98
+ }
99
+ return lines.join("\n").trim();
100
+ }
101
+
102
+ export function normalizeCompressedContext(raw, maxChars = 4000, maxBullets = 6) {
103
+ const text = String(raw || "").trim();
104
+ if (!text) return null;
105
+ if (text.toUpperCase() === NO_RELEVANT_MEMORY) return "";
106
+ const bullets = text.split(/\r?\n/)
107
+ .map((line) => line.trim())
108
+ .filter((line) => /^[-*]\s+/.test(line) && line.includes("viking://"))
109
+ .slice(0, Math.max(1, maxBullets))
110
+ .map((line) => `- ${line.replace(/^[-*]\s+/, "").slice(0, 500).trim()}`);
111
+ if (!bullets.length) return null;
112
+ return (`${DIGEST_HEADER}\n${bullets.join("\n")}`).slice(0, Math.max(100, maxChars));
113
+ }
114
+
115
+ export function recallDigestCacheKey({
116
+ query = "",
117
+ rendered = "",
118
+ entries = [],
119
+ maxInputChars = 18000,
120
+ maxBullets = 6,
121
+ } = {}) {
122
+ const uris = entries.map((entry) => String(entry?.uri || "").trim()).filter(Boolean).sort();
123
+ const source = JSON.stringify({
124
+ version: 2,
125
+ query: String(query),
126
+ rendered: String(rendered).slice(0, maxInputChars),
127
+ uris,
128
+ maxInputChars,
129
+ maxBullets,
130
+ });
131
+ return createHash("sha256").update(source).digest("hex");
132
+ }
133
+
134
+ async function readCache(path) {
135
+ if (!path) return null;
136
+ try { return JSON.parse(await readFile(path, "utf8")); } catch { return null; }
137
+ }
138
+
139
+ async function writeCache(path, value) {
140
+ if (!path) return;
141
+ try {
142
+ await mkdir(dirname(path), { recursive: true });
143
+ const tmp = `${path}.tmp`;
144
+ await writeFile(tmp, JSON.stringify(value));
145
+ await rename(tmp, path);
146
+ } catch { /* best effort */ }
147
+ }
148
+
149
+ export async function compressRecallContext({
150
+ query,
151
+ rendered,
152
+ entries = [],
153
+ cfg = {},
154
+ runCompressor,
155
+ cachePath = "",
156
+ now = 0,
157
+ }) {
158
+ const input = String(rendered || "").trim();
159
+ if (!input) return { status: COMPRESS_EMPTY, context: "" };
160
+ const minChars = Math.max(0, Number(cfg.recallCompressMinInputChars ?? 1500));
161
+ if (input.length < minChars) return { status: COMPRESS_OK, context: input };
162
+
163
+ const maxInputChars = Math.max(1000, Number(cfg.recallCompressMaxInputChars || 18000));
164
+ const maxBullets = Math.max(1, Number(cfg.recallCompressMaxBullets || 6));
165
+ const key = recallDigestCacheKey({
166
+ query,
167
+ rendered: input,
168
+ entries,
169
+ maxInputChars,
170
+ maxBullets,
171
+ });
172
+ const cached = await readCache(cachePath);
173
+ if (cached?.key === key && typeof cached.digest === "string") {
174
+ return { status: COMPRESS_OK, context: cached.digest };
175
+ }
176
+
177
+ const prompt = buildRecallCompressionPrompt({
178
+ query,
179
+ rendered: input.slice(0, maxInputChars),
180
+ maxBullets,
181
+ });
182
+ const raw = await runCompressor(prompt);
183
+ const normalized = normalizeCompressedContext(raw, 4000, maxBullets);
184
+ if (normalized === null) return { status: COMPRESS_FAILED, context: "" };
185
+ if (!normalized) return { status: COMPRESS_EMPTY, context: "" };
186
+
187
+ const validUris = entries.map((entry) => entry?.uri).filter(Boolean);
188
+ const digest = repairDigestUris(normalized, validUris.length
189
+ ? validUris
190
+ : (input.match(/viking:\/\/[^\s<>"']+/g) || []));
191
+ if (!digest) return { status: COMPRESS_FAILED, context: "" };
192
+
193
+ await writeCache(cachePath, { key, digest, updatedAt: now || 0 });
194
+ return { status: COMPRESS_OK, context: digest };
195
+ }
@@ -0,0 +1,39 @@
1
+ export type ContextSearchBody = {
2
+ query: string;
3
+ mode: "context";
4
+ purpose: "coding";
5
+ score_threshold: number;
6
+ quotas?: Record<string, number>;
7
+ max_tokens?: number;
8
+ peer_scope?: "actor";
9
+ session_id?: string;
10
+ query_expansion?: "off" | "auto";
11
+ dedup_turns?: number;
12
+ exclude_uris?: string[];
13
+ rewrite?: boolean | "auto";
14
+ rewrite_max_bullets?: number;
15
+ };
16
+
17
+ export type NormalizedContextEntry = {
18
+ uri: string;
19
+ category: string;
20
+ detail: string;
21
+ score: number;
22
+ text: string;
23
+ };
24
+
25
+ export function buildContextSearchBody(
26
+ cfg?: Record<string, unknown>,
27
+ options?: {
28
+ sessionId?: string;
29
+ excludeUris?: string[];
30
+ localCompressorAvailable?: boolean;
31
+ },
32
+ ): ContextSearchBody;
33
+
34
+ export function contextRequestTimeoutMs(
35
+ cfg?: Record<string, unknown>,
36
+ body?: Record<string, unknown>,
37
+ ): number | undefined;
38
+
39
+ export function normalizeContextEntry(entry?: unknown): NormalizedContextEntry;