@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.
- package/README.md +13 -2
- package/auto-recall.ts +2 -1
- package/client.ts +1 -0
- package/commands/setup.ts +19 -142
- package/context-engine.ts +72 -9
- package/dist/auto-recall.js +1 -1
- package/dist/client.js +1 -0
- package/dist/commands/setup.js +14 -121
- package/dist/context-engine.js +72 -9
- package/dist/plugin/openviking-context-engine-registration.js +1 -0
- package/dist/recall-trace.js +0 -58
- package/dist/services/context-lifecycle-service.js +72 -32
- package/dist/services/setup/probe-service.js +112 -0
- package/dist/shared/recall-compress-core.mjs +23 -9
- package/index.ts +1 -0
- package/package.json +1 -1
- package/plugin/openviking-context-engine-registration.ts +3 -0
- package/recall-trace.ts +5 -75
- package/services/context-lifecycle-service.ts +100 -52
- package/shared/recall-compress-core.mjs +17 -6
- package/skills/ov-experience-memory/SKILL.md +22 -1
package/dist/context-engine.js
CHANGED
|
@@ -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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
220
|
-
if (
|
|
221
|
-
|
|
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;
|
package/dist/recall-trace.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openviking/openclaw-plugin",
|
|
3
|
-
"version": "2026.9.
|
|
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
|
-
|
|
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
|
-
|
|
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
|