@openviking/openclaw-plugin 2026.9.8 → 2026.9.10-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.
@@ -121,6 +121,29 @@ function validTokenBudget(raw) {
121
121
  }
122
122
  return undefined;
123
123
  }
124
+ // OpenClaw 9.1/9.2 still capture via afterTurn even when commitTurn exists.
125
+ // 9.3 (#140024) defers admitted turns to commitTurn; standalone runners keep
126
+ // afterTurn. The registration API's runtime.version is the HOST version.
127
+ function usesDeferredTurnCapture(version) {
128
+ const match = version?.trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
129
+ if (!match)
130
+ return undefined;
131
+ const parts = match.slice(1, 4).map(Number);
132
+ if (!parts.every(Number.isSafeInteger))
133
+ return undefined;
134
+ const [year, month, day] = parts;
135
+ // OpenClaw uses 0.0.0 when version metadata cannot be resolved. More generally,
136
+ // versions outside our supported range cannot prove who owns capture.
137
+ if (year < 2026 || (year === 2026 && (month < 5 || (month === 5 && day < 27)))) {
138
+ return undefined;
139
+ }
140
+ const boundary = year === 2026 && month === 9 && day === 3;
141
+ // The 9.3 prerelease train can straddle the host change. Numeric packaging
142
+ // revisions (-1, -2, ...) refer to the stable release; do not guess for betas.
143
+ if (boundary && match[4] && !/^\d+$/.test(match[4]))
144
+ return undefined;
145
+ return year > 2026 || (year === 2026 && (month > 9 || (month === 9 && day >= 3)));
146
+ }
124
147
  export function createMemoryOpenVikingContextEngine(params) {
125
148
  const { id, name, version, cfg, logger, getClient, resolveAgentId, rememberSessionAgentId, queryConfigStore, traceRecorder, } = params;
126
149
  const diagEnabled = cfg.emitStandardDiagnostics;
@@ -161,6 +184,8 @@ export function createMemoryOpenVikingContextEngine(params) {
161
184
  };
162
185
  }
163
186
  const committedTurnKeys = new Set();
187
+ const inFlightTurns = new Map();
188
+ const deferredTurnCapture = usesDeferredTurnCapture(params.hostVersion);
164
189
  return {
165
190
  info: {
166
191
  id,
@@ -189,6 +214,7 @@ export function createMemoryOpenVikingContextEngine(params) {
189
214
  sessionId: assembleParams.sessionId,
190
215
  sessionKey: resolveSessionKey(assembleParams),
191
216
  messages: assembleParams.messages,
217
+ prompt: assembleParams.prompt,
192
218
  tokenBudget,
193
219
  runtimeContext: assembleParams.runtimeContext,
194
220
  isMainAssemble,
@@ -208,20 +234,57 @@ export function createMemoryOpenVikingContextEngine(params) {
208
234
  prependRecallToLatestUserMessage,
209
235
  });
210
236
  },
211
- // Capture still happens in afterTurn (host calls it per LLM call + on finalize);
212
- // commitTurn only acknowledges the accepted turn so OpenClaw drains its outbox.
213
- // ponytail: in-memory key set, not durable across restarts — the host outbox is.
214
- async commitTurn({ advancementKey, sessionId }) {
237
+ async commitTurn(commitParams) {
238
+ const { advancementKey, sessionId } = commitParams;
239
+ if (deferredTurnCapture === undefined) {
240
+ // ACKing with an unknown capture owner can discard the only copy in the
241
+ // host outbox. Reject instead; legacy afterTurn remains available.
242
+ throw new Error(`openviking: cannot select turn capture for OpenClaw version ${params.hostVersion ?? "(missing)"}`);
243
+ }
215
244
  if (committedTurnKeys.has(advancementKey)) {
216
245
  diag("commitTurn_duplicate", sessionId, { advancementKey });
217
246
  return { status: "duplicate" };
218
247
  }
219
- committedTurnKeys.add(advancementKey);
220
- if (committedTurnKeys.size > 1024) {
221
- committedTurnKeys.delete(committedTurnKeys.values().next().value);
248
+ const pending = inFlightTurns.get(advancementKey);
249
+ if (pending) {
250
+ await pending;
251
+ return { status: "duplicate" };
252
+ }
253
+ // Publish the promise before capture begins so concurrent deliveries share
254
+ // both success and failure. Completion keys are intentionally process-local.
255
+ const capture = Promise.resolve().then(async () => {
256
+ if (deferredTurnCapture) {
257
+ await afterTurnOpenVikingSession({
258
+ sessionId,
259
+ sessionKey: resolveSessionKey(commitParams),
260
+ messages: commitParams.messages,
261
+ prePromptMessageCount: 0, // Host already supplies the closed-turn range.
262
+ isHeartbeat: commitParams.isHeartbeat,
263
+ tokenBudget: 128_000, // Durable delivery does not carry a token budget.
264
+ throwOnError: true,
265
+ cfg,
266
+ getClient,
267
+ logger,
268
+ resolveAgentId,
269
+ rememberSessionAgentId,
270
+ isBypassedSession,
271
+ diag,
272
+ });
273
+ }
274
+ committedTurnKeys.add(advancementKey);
275
+ if (committedTurnKeys.size > 1024) {
276
+ committedTurnKeys.delete(committedTurnKeys.values().next().value);
277
+ }
278
+ diag("commitTurn", sessionId, { advancementKey, deferredTurnCapture });
279
+ });
280
+ inFlightTurns.set(advancementKey, capture);
281
+ try {
282
+ await capture;
283
+ return { status: "committed" };
284
+ }
285
+ finally {
286
+ inFlightTurns.delete(advancementKey);
222
287
  }
223
- diag("commitTurn", sessionId, { advancementKey });
224
- return { status: "committed" };
225
288
  },
226
289
  async afterTurn(afterTurnParams) {
227
290
  const tokenBudget = validTokenBudget(afterTurnParams.tokenBudget) ?? 128_000;
@@ -8,6 +8,7 @@ export function registerOpenVikingContextEngine(deps) {
8
8
  id: deps.plugin.id,
9
9
  name: deps.plugin.name,
10
10
  version: deps.version,
11
+ hostVersion: deps.api.runtime?.version,
11
12
  cfg: deps.cfg,
12
13
  logger: deps.logger,
13
14
  getClient: deps.getClient,
@@ -1,63 +1,5 @@
1
1
  import { appendFile, mkdir, readFile, readdir, unlink } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- const ALLOWED_RESOURCE_TYPES = ["resource", "user", "agent"];
4
- const DEFAULT_RESOURCE_TYPES = ["user", "agent"];
5
- function toResourceTypeEntries(value) {
6
- if (Array.isArray(value)) {
7
- return value
8
- .filter((entry) => typeof entry === "string")
9
- .map((entry) => entry.trim())
10
- .filter(Boolean);
11
- }
12
- if (typeof value === "string") {
13
- return value
14
- .split(/[,\n]/)
15
- .map((entry) => entry.trim())
16
- .filter(Boolean);
17
- }
18
- return [];
19
- }
20
- export function normalizeResourceTypes(value) {
21
- const entries = toResourceTypeEntries(value);
22
- if (entries.length === 0) {
23
- return [...DEFAULT_RESOURCE_TYPES];
24
- }
25
- const seen = new Set();
26
- const normalized = [];
27
- const invalid = [];
28
- for (const entry of entries) {
29
- if (ALLOWED_RESOURCE_TYPES.includes(entry)) {
30
- const typed = entry;
31
- if (!seen.has(typed)) {
32
- seen.add(typed);
33
- normalized.push(typed);
34
- }
35
- }
36
- else {
37
- invalid.push(entry);
38
- }
39
- }
40
- if (invalid.length > 0) {
41
- throw new Error(`invalid resourceTypes: ${invalid.join(", ")}`);
42
- }
43
- return normalized.length > 0 ? normalized : [...DEFAULT_RESOURCE_TYPES];
44
- }
45
- export function resolveRecallSearchPlan(resourceTypes, _ctx) {
46
- const normalized = normalizeResourceTypes(resourceTypes);
47
- const searches = [];
48
- const skipped = [];
49
- let addedMemorySearch = false;
50
- for (const resourceType of normalized) {
51
- if (resourceType === "resource") {
52
- searches.push({ resourceType, contextType: "resource" });
53
- }
54
- else if ((resourceType === "user" || resourceType === "agent") && !addedMemorySearch) {
55
- searches.push({ resourceType: "user", contextType: "memory" });
56
- addedMemorySearch = true;
57
- }
58
- }
59
- return { resourceTypes: normalized, searches, skipped };
60
- }
61
3
  export class RecallTraceMemoryStore {
62
4
  maxEntries;
63
5
  entries = [];
@@ -195,7 +195,73 @@ function isSessionNotFoundError(err) {
195
195
  const errorMessage = String(err);
196
196
  return errorMessage.includes("[NOT_FOUND]") && errorMessage.includes("Session not found");
197
197
  }
198
- export async function assembleOpenVikingSession({ sessionId, sessionKey, messages, tokenBudget, runtimeContext, isMainAssemble, cfg, getClient, logger, resolveAgentId, rememberSessionAgentId, isBypassedSession, queryConfigStore, traceRecorder, diag, roughEstimate, messageDigest, extractAgentMessageText, hasAutoRecallBlock, prependRecallToLatestUserMessage, }) {
198
+ async function recallForAssemble(params, recallQuery) {
199
+ const { sessionId, sessionKey, cfg, getClient, resolveAgentId, queryConfigStore, logger, traceRecorder } = params;
200
+ const ovSessionId = openClawSessionToOvStorageId(sessionId, sessionKey);
201
+ const sender = extractRuntimeSenderId(params.runtimeContext);
202
+ const client = await getClient();
203
+ const routingRef = sessionId ?? sessionKey ?? ovSessionId;
204
+ const agentId = resolveAgentId(routingRef, sessionKey, ovSessionId);
205
+ const actorPeerId = resolveOpenVikingActorPeerId({
206
+ peerRole: cfg.peer_role ?? "none",
207
+ senderPeerId: sanitizeOpenVikingPeerId(sender.senderId),
208
+ assistantPeerId: agentId,
209
+ });
210
+ const queryConfig = await queryConfigStore?.getEffective({
211
+ agentId,
212
+ sessionId,
213
+ sessionKey,
214
+ ovSessionId,
215
+ });
216
+ return buildAutoRecallContext({
217
+ cfg,
218
+ queryConfig,
219
+ client,
220
+ agentId,
221
+ actorPeerId,
222
+ queryText: recallQuery.query,
223
+ logger,
224
+ verbose: (message) => logger.info(message),
225
+ traceRecorder: traceRecorder,
226
+ sessionId,
227
+ sessionKey,
228
+ ovSessionId,
229
+ queryTruncated: recallQuery.truncated,
230
+ rawUserTextPreview: recallQuery.query,
231
+ // System additions are transient; the next turn must be able to recall the same URI.
232
+ dedupTurns: params.isMainAssemble ? 0 : undefined,
233
+ });
234
+ }
235
+ export async function assembleOpenVikingSession(params) {
236
+ const assembled = await assembleSessionContext(params);
237
+ // Current OpenClaw supplies the pending turn separately from history. Keep
238
+ // recalled context out of persisted messages and let the host own that turn.
239
+ if (!params.isMainAssemble || !params.cfg.autoRecall || !params.prompt ||
240
+ params.isBypassedSession(params)) {
241
+ return assembled;
242
+ }
243
+ const query = prepareRecallQuery(params.prompt);
244
+ if (query.query.length < 5)
245
+ return assembled;
246
+ try {
247
+ const recall = await recallForAssemble(params, query);
248
+ if (!recall.block)
249
+ return assembled;
250
+ const systemPromptAddition = [assembled.systemPromptAddition, recall.block].filter(Boolean).join("\n\n");
251
+ const estimatedTokens = assembled.estimatedTokens
252
+ + estimateTextTokens(systemPromptAddition)
253
+ - estimateTextTokens(assembled.systemPromptAddition ?? "");
254
+ if (estimatedTokens > params.tokenBudget)
255
+ return assembled;
256
+ return { ...assembled, systemPromptAddition, estimatedTokens };
257
+ }
258
+ catch (err) {
259
+ params.logger.warn?.(`openviking: auto-recall failed: ${String(err)}`);
260
+ return assembled;
261
+ }
262
+ }
263
+ async function assembleSessionContext(params) {
264
+ const { sessionId, sessionKey, messages, tokenBudget, runtimeContext, isMainAssemble, cfg, getClient, logger, resolveAgentId, rememberSessionAgentId, isBypassedSession, diag, roughEstimate, messageDigest, extractAgentMessageText, hasAutoRecallBlock, prependRecallToLatestUserMessage, } = params;
199
265
  const latestMessage = messages.at(-1);
200
266
  const isTransformContextAssemble = !isMainAssemble;
201
267
  // Tool-loop calls do no recall work on non-user tails; avoid scanning the full history.
@@ -239,36 +305,7 @@ export async function assembleOpenVikingSession({ sessionId, sessionKey, message
239
305
  `chars=${recallQuery.originalChars}->${recallQuery.finalChars})`);
240
306
  }
241
307
  try {
242
- const client = await getClient();
243
- const routingRef = sessionId ?? sessionKey ?? ovSessionId;
244
- const agentId = resolveAgentId(routingRef, sessionKey, ovSessionId);
245
- const actorPeerId = resolveOpenVikingActorPeerId({
246
- peerRole: cfg.peer_role ?? "none",
247
- senderPeerId: sanitizeOpenVikingPeerId(sender.senderId),
248
- assistantPeerId: agentId,
249
- });
250
- const queryConfig = await queryConfigStore?.getEffective({
251
- agentId,
252
- sessionId,
253
- sessionKey,
254
- ovSessionId,
255
- });
256
- const recall = await buildAutoRecallContext({
257
- cfg,
258
- queryConfig,
259
- client,
260
- agentId,
261
- actorPeerId,
262
- queryText: recallQuery.query,
263
- logger,
264
- verbose: (message) => logger.info(message),
265
- traceRecorder: traceRecorder,
266
- sessionId,
267
- sessionKey,
268
- ovSessionId,
269
- queryTruncated: recallQuery.truncated,
270
- rawUserTextPreview: recallQuery.query,
271
- });
308
+ const recall = await recallForAssemble(params, recallQuery);
272
309
  if (!recall.block) {
273
310
  return assemblePassthrough({
274
311
  diag,
@@ -501,7 +538,7 @@ function messageDigest(messages, maxCharsPerMsg = 2000) {
501
538
  };
502
539
  });
503
540
  }
504
- export async function afterTurnOpenVikingSession({ sessionId, sessionKey, messages: rawMessages, prePromptMessageCount, isHeartbeat, tokenBudget, runtimeContext, cfg, getClient, logger, resolveAgentId, rememberSessionAgentId, isBypassedSession, diag, }) {
541
+ export async function afterTurnOpenVikingSession({ throwOnError = false, sessionId, sessionKey, messages: rawMessages, prePromptMessageCount, isHeartbeat, tokenBudget, runtimeContext, cfg, getClient, logger, resolveAgentId, rememberSessionAgentId, isBypassedSession, diag, }) {
505
542
  if (!cfg.autoCapture) {
506
543
  return;
507
544
  }
@@ -650,6 +687,9 @@ export async function afterTurnOpenVikingSession({ sessionId, sessionKey, messag
650
687
  senderIdFound: sender.found,
651
688
  senderId: sender.senderId ?? null,
652
689
  });
690
+ if (throwOnError) {
691
+ throw err;
692
+ }
653
693
  }
654
694
  }
655
695
  function validTokenCount(value) {
@@ -0,0 +1,112 @@
1
+ import { resolveOpenVikingRequestHeaders, } from "../../request-headers.js";
2
+ export const defaultSetupNetworkTransport = (url, init) => fetch(url, init);
3
+ export function createSetupNetworkProbes({ pluginVersion, compatRange, checkVersionCompatibility, transport = defaultSetupNetworkTransport, timeoutMs = 10_000, }) {
4
+ return {
5
+ async probeApiKeyType(baseUrl, apiKey, configuredHeaders) {
6
+ if (!apiKey) {
7
+ return { keyType: "no_key", needsAccountId: false, needsUserId: false, detail: "No API key configured" };
8
+ }
9
+ const controller = new AbortController();
10
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
11
+ const sessionsUrl = `${baseUrl.replace(/\/+$/, "")}/api/v1/sessions?limit=1`;
12
+ try {
13
+ const configuredRequestHeaders = resolveOpenVikingRequestHeaders({ headers: configuredHeaders });
14
+ const headers = {};
15
+ headers["X-API-Key"] = apiKey;
16
+ Object.assign(headers, configuredRequestHeaders);
17
+ const response = await transport(sessionsUrl, {
18
+ headers,
19
+ signal: controller.signal,
20
+ });
21
+ if (response.ok) {
22
+ return { keyType: "user_key", needsAccountId: false, needsUserId: false, detail: "API key has full user context" };
23
+ }
24
+ if ([400, 401, 403, 422].includes(response.status)) {
25
+ let body = "";
26
+ try {
27
+ body = await response.text();
28
+ }
29
+ catch { /* ignore parse errors */ }
30
+ const lower = body.toLowerCase();
31
+ const needsAccount = /x-openviking-account|account[_ ]?id|account context|tenant/.test(lower);
32
+ const needsUser = /x-openviking-user|user[_ ]?id|user context|user key/.test(lower);
33
+ if (needsAccount || needsUser) {
34
+ return {
35
+ keyType: "root_key",
36
+ needsAccountId: needsAccount,
37
+ needsUserId: needsUser,
38
+ detail: body.slice(0, 200),
39
+ };
40
+ }
41
+ try {
42
+ const probeHeaders = {
43
+ "X-API-Key": apiKey,
44
+ "X-OpenViking-Account": "__probe__",
45
+ "X-OpenViking-User": "__probe__",
46
+ ...configuredRequestHeaders,
47
+ };
48
+ const probe2 = await transport(sessionsUrl, {
49
+ headers: probeHeaders,
50
+ signal: controller.signal,
51
+ });
52
+ if (probe2.status !== response.status) {
53
+ return {
54
+ keyType: "root_key",
55
+ needsAccountId: true,
56
+ needsUserId: true,
57
+ detail: body.slice(0, 200) || `HTTP ${response.status} -> ${probe2.status} after adding tenant headers`,
58
+ };
59
+ }
60
+ }
61
+ catch { /* ignore probe errors, fall through to unknown */ }
62
+ if (response.status === 401 || response.status === 403) {
63
+ return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: `HTTP ${response.status} - authentication failed, verify your API key` };
64
+ }
65
+ return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: `HTTP ${response.status}${body ? ` - ${body.slice(0, 160)}` : ""}` };
66
+ }
67
+ return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: `HTTP ${response.status}` };
68
+ }
69
+ catch (err) {
70
+ return { keyType: "unknown", needsAccountId: false, needsUserId: false, detail: String(err instanceof Error ? err.message : err) };
71
+ }
72
+ finally {
73
+ clearTimeout(timeoutId);
74
+ }
75
+ },
76
+ async checkServiceHealth(baseUrl, apiKey, configuredHeaders) {
77
+ const controller = new AbortController();
78
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
79
+ try {
80
+ const configuredRequestHeaders = resolveOpenVikingRequestHeaders({ headers: configuredHeaders });
81
+ const headers = {};
82
+ if (apiKey) {
83
+ headers["X-API-Key"] = apiKey;
84
+ }
85
+ Object.assign(headers, configuredRequestHeaders);
86
+ const response = await transport(`${baseUrl.replace(/\/+$/, "")}/health`, {
87
+ headers,
88
+ signal: controller.signal,
89
+ });
90
+ if (response.ok) {
91
+ try {
92
+ const data = await response.json();
93
+ const result = (data.result ?? data);
94
+ const version = String(result.version ?? data.version ?? "");
95
+ const compatibility = checkVersionCompatibility(version);
96
+ return { ok: true, version, error: "", compatibility, pluginVersion, compatRange };
97
+ }
98
+ catch {
99
+ return { ok: true, version: "", error: "", compatibility: "unknown", pluginVersion, compatRange };
100
+ }
101
+ }
102
+ return { ok: false, version: "", error: `HTTP ${response.status}`, compatibility: "unknown", pluginVersion, compatRange };
103
+ }
104
+ catch (err) {
105
+ return { ok: false, version: "", error: String(err instanceof Error ? err.message : err), compatibility: "unknown", pluginVersion, compatRange };
106
+ }
107
+ finally {
108
+ clearTimeout(timeoutId);
109
+ }
110
+ },
111
+ };
112
+ }
@@ -83,11 +83,21 @@ export function repairDigestUris(digest, validUris = []) {
83
83
  lines.push(line);
84
84
  continue;
85
85
  }
86
+ let tokenized = line;
87
+ for (const uri of [...valid].sort((a, b) => b.length - a.length)) {
88
+ if (/\s/.test(uri))
89
+ tokenized = tokenized.split(uri).join(encodeURI(uri));
90
+ }
86
91
  let dropped = false;
87
- const repaired = line.replace(/viking:\/\/[^\s<>"')\]]+/g, (uri) => {
88
- if (validSet.has(uri))
89
- return uri;
90
- const nearest = nearestUri(uri, valid);
92
+ const repaired = tokenized.replace(/viking:\/\/[^\s<>"')\]]+/g, (uri) => {
93
+ let decoded = uri;
94
+ try {
95
+ decoded = decodeURI(uri);
96
+ }
97
+ catch { /* keep the original candidate */ }
98
+ if (validSet.has(decoded))
99
+ return decoded;
100
+ const nearest = nearestUri(decoded, valid);
91
101
  if (nearest)
92
102
  return nearest;
93
103
  dropped = true;
@@ -146,13 +156,14 @@ async function writeCache(path, value) {
146
156
  }
147
157
  catch { /* best effort */ }
148
158
  }
149
- export async function compressRecallContext({ query, rendered, entries = [], cfg = {}, runCompressor, cachePath = "", now = 0, }) {
159
+ export async function compressRecallContext({ query, rendered, shortContext = rendered, entries = [], cfg = {}, runCompressor, cachePath = "", now = 0, }) {
150
160
  const input = String(rendered || "").trim();
151
161
  if (!input)
152
162
  return { status: COMPRESS_EMPTY, context: "" };
153
163
  const minChars = Math.max(0, Number(cfg.recallCompressMinInputChars ?? 1500));
154
- if (input.length < minChars)
155
- return { status: COMPRESS_OK, context: input };
164
+ if (input.length < minChars) {
165
+ return { status: COMPRESS_OK, context: String(shortContext ?? input).trim() };
166
+ }
156
167
  const maxInputChars = Math.max(1000, Number(cfg.recallCompressMaxInputChars || 18000));
157
168
  const maxBullets = Math.max(1, Number(cfg.recallCompressMaxBullets || 6));
158
169
  const key = recallDigestCacheKey({
@@ -164,7 +175,9 @@ export async function compressRecallContext({ query, rendered, entries = [], cfg
164
175
  });
165
176
  const cached = await readCache(cachePath);
166
177
  if (cached?.key === key && typeof cached.digest === "string") {
167
- return { status: COMPRESS_OK, context: cached.digest };
178
+ const digest = normalizeCompressedContext(cached.digest, 4000, maxBullets);
179
+ if (digest)
180
+ return { status: COMPRESS_OK, context: digest };
168
181
  }
169
182
  const prompt = buildRecallCompressionPrompt({
170
183
  query,
@@ -178,9 +191,10 @@ export async function compressRecallContext({ query, rendered, entries = [], cfg
178
191
  if (!normalized)
179
192
  return { status: COMPRESS_EMPTY, context: "" };
180
193
  const validUris = entries.map((entry) => entry?.uri).filter(Boolean);
181
- const digest = repairDigestUris(normalized, validUris.length
194
+ const repaired = repairDigestUris(normalized, validUris.length
182
195
  ? validUris
183
196
  : (input.match(/viking:\/\/[^\s<>"']+/g) || []));
197
+ const digest = normalizeCompressedContext(repaired, 4000, maxBullets);
184
198
  if (!digest)
185
199
  return { status: COMPRESS_FAILED, context: "" };
186
200
  await writeCache(cachePath, { key, digest, updatedAt: now || 0 });
package/index.ts CHANGED
@@ -95,6 +95,7 @@ type ToolContext = {
95
95
  };
96
96
 
97
97
  type OpenClawPluginApi = {
98
+ runtime?: { version?: string };
98
99
  pluginConfig?: unknown;
99
100
  openVikingTransport?: HttpTransport;
100
101
  logger: PluginLogger;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openviking/openclaw-plugin",
3
- "version": "2026.9.8",
3
+ "version": "2026.9.10-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",
@@ -1,4 +1,5 @@
1
1
  export type OpenVikingContextEngineRegistrationApi = {
2
+ runtime?: { version?: string };
2
3
  registerContextEngine?: (id: string, factory: () => unknown) => void;
3
4
  };
4
5
 
@@ -22,6 +23,7 @@ export type OpenVikingContextEngineCreateParams<
22
23
  id: string;
23
24
  name: string;
24
25
  version: string;
26
+ hostVersion?: string;
25
27
  cfg: TCfg;
26
28
  logger: TLogger;
27
29
  getClient: () => Promise<TClient>;
@@ -80,6 +82,7 @@ export function registerOpenVikingContextEngine<
80
82
  id: deps.plugin.id,
81
83
  name: deps.plugin.name,
82
84
  version: deps.version,
85
+ hostVersion: deps.api.runtime?.version,
83
86
  cfg: deps.cfg,
84
87
  logger: deps.logger,
85
88
  getClient: deps.getClient,
package/recall-trace.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { appendFile, mkdir, readFile, readdir, unlink } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
 
4
- export type RecallResourceType = "resource" | "session" | "user" | "agent";
4
+ import type { RecallResourceType } from "./registries/recall-resource-types.js";
5
5
 
6
6
  export type RecallTraceSource = "auto_recall" | "memory_recall" | "ov_search" | "ov_archive_search";
7
7
 
@@ -27,7 +27,9 @@ export type RecallTraceEntry = {
27
27
  agentId?: string;
28
28
  source: RecallTraceSource;
29
29
  operationType: RecallTraceOperationType;
30
- resourceTypes: RecallResourceType[];
30
+ // "session" labels archive-grep traces over session history; recall targets
31
+ // stay in the registry's narrower union and never include it.
32
+ resourceTypes: Array<RecallResourceType | "session">;
31
33
  trigger: {
32
34
  rawUserTextPreview?: string;
33
35
  query: string;
@@ -92,78 +94,6 @@ export type RecallTraceFlushResult = {
92
94
  warnings: string[];
93
95
  };
94
96
 
95
- const ALLOWED_RESOURCE_TYPES: RecallResourceType[] = ["resource", "user", "agent"];
96
- const DEFAULT_RESOURCE_TYPES: RecallResourceType[] = ["user", "agent"];
97
-
98
- function toResourceTypeEntries(value: unknown): string[] {
99
- if (Array.isArray(value)) {
100
- return value
101
- .filter((entry): entry is string => typeof entry === "string")
102
- .map((entry) => entry.trim())
103
- .filter(Boolean);
104
- }
105
- if (typeof value === "string") {
106
- return value
107
- .split(/[,\n]/)
108
- .map((entry) => entry.trim())
109
- .filter(Boolean);
110
- }
111
- return [];
112
- }
113
-
114
- export function normalizeResourceTypes(value: unknown): RecallResourceType[] {
115
- const entries = toResourceTypeEntries(value);
116
- if (entries.length === 0) {
117
- return [...DEFAULT_RESOURCE_TYPES];
118
- }
119
-
120
- const seen = new Set<RecallResourceType>();
121
- const normalized: RecallResourceType[] = [];
122
- const invalid: string[] = [];
123
- for (const entry of entries) {
124
- if ((ALLOWED_RESOURCE_TYPES as string[]).includes(entry)) {
125
- const typed = entry as RecallResourceType;
126
- if (!seen.has(typed)) {
127
- seen.add(typed);
128
- normalized.push(typed);
129
- }
130
- } else {
131
- invalid.push(entry);
132
- }
133
- }
134
-
135
- if (invalid.length > 0) {
136
- throw new Error(`invalid resourceTypes: ${invalid.join(", ")}`);
137
- }
138
-
139
- return normalized.length > 0 ? normalized : [...DEFAULT_RESOURCE_TYPES];
140
- }
141
-
142
- export function resolveRecallSearchPlan(
143
- resourceTypes: unknown,
144
- _ctx: { ovSessionId?: string; agentId?: string },
145
- ): {
146
- resourceTypes: RecallResourceType[];
147
- searches: Array<{ resourceType: RecallResourceType; targetUri?: string; contextType: "memory" | "resource" }>;
148
- skipped: Array<{ resourceType: RecallResourceType; reason: "missing_session" }>;
149
- } {
150
- const normalized = normalizeResourceTypes(resourceTypes);
151
- const searches: Array<{ resourceType: RecallResourceType; targetUri?: string; contextType: "memory" | "resource" }> = [];
152
- const skipped: Array<{ resourceType: RecallResourceType; reason: "missing_session" }> = [];
153
- let addedMemorySearch = false;
154
-
155
- for (const resourceType of normalized) {
156
- if (resourceType === "resource") {
157
- searches.push({ resourceType, contextType: "resource" });
158
- } else if ((resourceType === "user" || resourceType === "agent") && !addedMemorySearch) {
159
- searches.push({ resourceType: "user", contextType: "memory" });
160
- addedMemorySearch = true;
161
- }
162
- }
163
-
164
- return { resourceTypes: normalized, searches, skipped };
165
- }
166
-
167
97
  export class RecallTraceMemoryStore {
168
98
  private readonly maxEntries: number;
169
99
  private readonly entries: RecallTraceEntry[] = [];
@@ -183,7 +113,7 @@ export class RecallTraceMemoryStore {
183
113
  const limit = Math.max(1, Math.floor(query.limit ?? 20));
184
114
  const turn = query.turn ?? "latest";
185
115
  const resourceTypes = query.resourceTypes && query.resourceTypes.length > 0
186
- ? new Set(query.resourceTypes)
116
+ ? new Set<string>(query.resourceTypes)
187
117
  : undefined;
188
118
 
189
119
  const filtered = this.entries