@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
|
@@ -72,6 +72,7 @@ export type AssembleOpenVikingSessionParams = {
|
|
|
72
72
|
sessionId: string;
|
|
73
73
|
sessionKey?: string;
|
|
74
74
|
messages: AgentMessage[];
|
|
75
|
+
prompt?: string;
|
|
75
76
|
tokenBudget: number;
|
|
76
77
|
runtimeContext?: Record<string, unknown>;
|
|
77
78
|
isMainAssemble: boolean;
|
|
@@ -125,6 +126,8 @@ export type CompactOpenVikingSessionParams = {
|
|
|
125
126
|
type AfterTurnClient = Pick<OpenVikingClient, "addSessionMessage" | "getSession" | "commitSession" | "getTask">;
|
|
126
127
|
|
|
127
128
|
export type AfterTurnOpenVikingSessionParams = {
|
|
129
|
+
/** Durable delivery must reject failed writes so the host retains its outbox row. */
|
|
130
|
+
throwOnError?: boolean;
|
|
128
131
|
sessionId: string;
|
|
129
132
|
sessionKey?: string;
|
|
130
133
|
messages?: AgentMessage[];
|
|
@@ -421,28 +424,98 @@ function isSessionNotFoundError(err: unknown): boolean {
|
|
|
421
424
|
return errorMessage.includes("[NOT_FOUND]") && errorMessage.includes("Session not found");
|
|
422
425
|
}
|
|
423
426
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
427
|
+
async function recallForAssemble(
|
|
428
|
+
params: AssembleOpenVikingSessionParams,
|
|
429
|
+
recallQuery: ReturnType<typeof prepareRecallQuery>,
|
|
430
|
+
) {
|
|
431
|
+
const { sessionId, sessionKey, cfg, getClient, resolveAgentId, queryConfigStore, logger, traceRecorder } = params;
|
|
432
|
+
const ovSessionId = openClawSessionToOvStorageId(sessionId, sessionKey);
|
|
433
|
+
const sender = extractRuntimeSenderId(params.runtimeContext);
|
|
434
|
+
const client = await getClient();
|
|
435
|
+
const routingRef = sessionId ?? sessionKey ?? ovSessionId;
|
|
436
|
+
const agentId = resolveAgentId(routingRef, sessionKey, ovSessionId);
|
|
437
|
+
const actorPeerId = resolveOpenVikingActorPeerId({
|
|
438
|
+
peerRole: cfg.peer_role ?? "none",
|
|
439
|
+
senderPeerId: sanitizeOpenVikingPeerId(sender.senderId),
|
|
440
|
+
assistantPeerId: agentId,
|
|
441
|
+
});
|
|
442
|
+
const queryConfig = await queryConfigStore?.getEffective({
|
|
443
|
+
agentId,
|
|
444
|
+
sessionId,
|
|
445
|
+
sessionKey,
|
|
446
|
+
ovSessionId,
|
|
447
|
+
});
|
|
448
|
+
return buildAutoRecallContext({
|
|
449
|
+
cfg,
|
|
450
|
+
queryConfig,
|
|
451
|
+
client,
|
|
452
|
+
agentId,
|
|
453
|
+
actorPeerId,
|
|
454
|
+
queryText: recallQuery.query,
|
|
455
|
+
logger,
|
|
456
|
+
verbose: (message) => logger.info(message),
|
|
457
|
+
traceRecorder: traceRecorder as never,
|
|
458
|
+
sessionId,
|
|
459
|
+
sessionKey,
|
|
460
|
+
ovSessionId,
|
|
461
|
+
queryTruncated: recallQuery.truncated,
|
|
462
|
+
rawUserTextPreview: recallQuery.query,
|
|
463
|
+
// System additions are transient; the next turn must be able to recall the same URI.
|
|
464
|
+
dedupTurns: params.isMainAssemble ? 0 : undefined,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export async function assembleOpenVikingSession(
|
|
469
|
+
params: AssembleOpenVikingSessionParams,
|
|
470
|
+
): Promise<AssembleOpenVikingSessionResult> {
|
|
471
|
+
const assembled = await assembleSessionContext(params);
|
|
472
|
+
// Current OpenClaw supplies the pending turn separately from history. Keep
|
|
473
|
+
// recalled context out of persisted messages and let the host own that turn.
|
|
474
|
+
if (
|
|
475
|
+
!params.isMainAssemble || !params.cfg.autoRecall || !params.prompt ||
|
|
476
|
+
params.isBypassedSession(params)
|
|
477
|
+
) {
|
|
478
|
+
return assembled;
|
|
479
|
+
}
|
|
480
|
+
const query = prepareRecallQuery(params.prompt);
|
|
481
|
+
if (query.query.length < 5) return assembled;
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
const recall = await recallForAssemble(params, query);
|
|
485
|
+
if (!recall.block) return assembled;
|
|
486
|
+
const systemPromptAddition = [assembled.systemPromptAddition, recall.block].filter(Boolean).join("\n\n");
|
|
487
|
+
const estimatedTokens = assembled.estimatedTokens
|
|
488
|
+
+ estimateTextTokens(systemPromptAddition)
|
|
489
|
+
- estimateTextTokens(assembled.systemPromptAddition ?? "");
|
|
490
|
+
if (estimatedTokens > params.tokenBudget) return assembled;
|
|
491
|
+
return { ...assembled, systemPromptAddition, estimatedTokens };
|
|
492
|
+
} catch (err) {
|
|
493
|
+
params.logger.warn?.(`openviking: auto-recall failed: ${String(err)}`);
|
|
494
|
+
return assembled;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function assembleSessionContext(params: AssembleOpenVikingSessionParams): Promise<AssembleOpenVikingSessionResult> {
|
|
499
|
+
const {
|
|
500
|
+
sessionId,
|
|
501
|
+
sessionKey,
|
|
502
|
+
messages,
|
|
503
|
+
tokenBudget,
|
|
504
|
+
runtimeContext,
|
|
505
|
+
isMainAssemble,
|
|
506
|
+
cfg,
|
|
507
|
+
getClient,
|
|
508
|
+
logger,
|
|
509
|
+
resolveAgentId,
|
|
510
|
+
rememberSessionAgentId,
|
|
511
|
+
isBypassedSession,
|
|
512
|
+
diag,
|
|
513
|
+
roughEstimate,
|
|
514
|
+
messageDigest,
|
|
515
|
+
extractAgentMessageText,
|
|
516
|
+
hasAutoRecallBlock,
|
|
517
|
+
prependRecallToLatestUserMessage,
|
|
518
|
+
} = params;
|
|
446
519
|
const latestMessage = messages.at(-1);
|
|
447
520
|
const isTransformContextAssemble = !isMainAssemble;
|
|
448
521
|
|
|
@@ -495,36 +568,7 @@ export async function assembleOpenVikingSession({
|
|
|
495
568
|
}
|
|
496
569
|
|
|
497
570
|
try {
|
|
498
|
-
const
|
|
499
|
-
const routingRef = sessionId ?? sessionKey ?? ovSessionId;
|
|
500
|
-
const agentId = resolveAgentId(routingRef, sessionKey, ovSessionId);
|
|
501
|
-
const actorPeerId = resolveOpenVikingActorPeerId({
|
|
502
|
-
peerRole: cfg.peer_role ?? "none",
|
|
503
|
-
senderPeerId: sanitizeOpenVikingPeerId(sender.senderId),
|
|
504
|
-
assistantPeerId: agentId,
|
|
505
|
-
});
|
|
506
|
-
const queryConfig = await queryConfigStore?.getEffective({
|
|
507
|
-
agentId,
|
|
508
|
-
sessionId,
|
|
509
|
-
sessionKey,
|
|
510
|
-
ovSessionId,
|
|
511
|
-
});
|
|
512
|
-
const recall = await buildAutoRecallContext({
|
|
513
|
-
cfg,
|
|
514
|
-
queryConfig,
|
|
515
|
-
client,
|
|
516
|
-
agentId,
|
|
517
|
-
actorPeerId,
|
|
518
|
-
queryText: recallQuery.query,
|
|
519
|
-
logger,
|
|
520
|
-
verbose: (message) => logger.info(message),
|
|
521
|
-
traceRecorder: traceRecorder as never,
|
|
522
|
-
sessionId,
|
|
523
|
-
sessionKey,
|
|
524
|
-
ovSessionId,
|
|
525
|
-
queryTruncated: recallQuery.truncated,
|
|
526
|
-
rawUserTextPreview: recallQuery.query,
|
|
527
|
-
});
|
|
571
|
+
const recall = await recallForAssemble(params, recallQuery);
|
|
528
572
|
|
|
529
573
|
if (!recall.block) {
|
|
530
574
|
return assemblePassthrough({
|
|
@@ -786,6 +830,7 @@ function messageDigest(messages: AgentMessage[], maxCharsPerMsg = 2000): Array<{
|
|
|
786
830
|
}
|
|
787
831
|
|
|
788
832
|
export async function afterTurnOpenVikingSession({
|
|
833
|
+
throwOnError = false,
|
|
789
834
|
sessionId,
|
|
790
835
|
sessionKey,
|
|
791
836
|
messages: rawMessages,
|
|
@@ -976,6 +1021,9 @@ export async function afterTurnOpenVikingSession({
|
|
|
976
1021
|
senderIdFound: sender.found,
|
|
977
1022
|
senderId: sender.senderId ?? null,
|
|
978
1023
|
});
|
|
1024
|
+
if (throwOnError) {
|
|
1025
|
+
throw err;
|
|
1026
|
+
}
|
|
979
1027
|
}
|
|
980
1028
|
}
|
|
981
1029
|
|
|
@@ -86,10 +86,16 @@ export function repairDigestUris(digest, validUris = []) {
|
|
|
86
86
|
lines.push(line);
|
|
87
87
|
continue;
|
|
88
88
|
}
|
|
89
|
+
let tokenized = line;
|
|
90
|
+
for (const uri of [...valid].sort((a, b) => b.length - a.length)) {
|
|
91
|
+
if (/\s/.test(uri)) tokenized = tokenized.split(uri).join(encodeURI(uri));
|
|
92
|
+
}
|
|
89
93
|
let dropped = false;
|
|
90
|
-
const repaired =
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
const repaired = tokenized.replace(/viking:\/\/[^\s<>"')\]]+/g, (uri) => {
|
|
95
|
+
let decoded = uri;
|
|
96
|
+
try { decoded = decodeURI(uri); } catch { /* keep the original candidate */ }
|
|
97
|
+
if (validSet.has(decoded)) return decoded;
|
|
98
|
+
const nearest = nearestUri(decoded, valid);
|
|
93
99
|
if (nearest) return nearest;
|
|
94
100
|
dropped = true;
|
|
95
101
|
return uri;
|
|
@@ -149,6 +155,7 @@ async function writeCache(path, value) {
|
|
|
149
155
|
export async function compressRecallContext({
|
|
150
156
|
query,
|
|
151
157
|
rendered,
|
|
158
|
+
shortContext = rendered,
|
|
152
159
|
entries = [],
|
|
153
160
|
cfg = {},
|
|
154
161
|
runCompressor,
|
|
@@ -158,7 +165,9 @@ export async function compressRecallContext({
|
|
|
158
165
|
const input = String(rendered || "").trim();
|
|
159
166
|
if (!input) return { status: COMPRESS_EMPTY, context: "" };
|
|
160
167
|
const minChars = Math.max(0, Number(cfg.recallCompressMinInputChars ?? 1500));
|
|
161
|
-
if (input.length < minChars)
|
|
168
|
+
if (input.length < minChars) {
|
|
169
|
+
return { status: COMPRESS_OK, context: String(shortContext ?? input).trim() };
|
|
170
|
+
}
|
|
162
171
|
|
|
163
172
|
const maxInputChars = Math.max(1000, Number(cfg.recallCompressMaxInputChars || 18000));
|
|
164
173
|
const maxBullets = Math.max(1, Number(cfg.recallCompressMaxBullets || 6));
|
|
@@ -171,7 +180,8 @@ export async function compressRecallContext({
|
|
|
171
180
|
});
|
|
172
181
|
const cached = await readCache(cachePath);
|
|
173
182
|
if (cached?.key === key && typeof cached.digest === "string") {
|
|
174
|
-
|
|
183
|
+
const digest = normalizeCompressedContext(cached.digest, 4000, maxBullets);
|
|
184
|
+
if (digest) return { status: COMPRESS_OK, context: digest };
|
|
175
185
|
}
|
|
176
186
|
|
|
177
187
|
const prompt = buildRecallCompressionPrompt({
|
|
@@ -185,9 +195,10 @@ export async function compressRecallContext({
|
|
|
185
195
|
if (!normalized) return { status: COMPRESS_EMPTY, context: "" };
|
|
186
196
|
|
|
187
197
|
const validUris = entries.map((entry) => entry?.uri).filter(Boolean);
|
|
188
|
-
const
|
|
198
|
+
const repaired = repairDigestUris(normalized, validUris.length
|
|
189
199
|
? validUris
|
|
190
200
|
: (input.match(/viking:\/\/[^\s<>"']+/g) || []));
|
|
201
|
+
const digest = normalizeCompressedContext(repaired, 4000, maxBullets);
|
|
191
202
|
if (!digest) return { status: COMPRESS_FAILED, context: "" };
|
|
192
203
|
|
|
193
204
|
await writeCache(cachePath, { key, digest, updatedAt: now || 0 });
|
|
@@ -55,7 +55,8 @@ context or deeper intent analysis is useful.
|
|
|
55
55
|
|
|
56
56
|
For tools using MCP-style parameters, set `target_uri` to this root. For
|
|
57
57
|
OpenClaw `ov_search`, set `uri` to this root. Never hardcode `default`,
|
|
58
|
-
`test`, or another user ID.
|
|
58
|
+
`test`, or another user ID. If the server rejects the URI, resolve the root
|
|
59
|
+
as described in *Servers Without the Home Alias* and retry once.
|
|
59
60
|
4. Start with `limit=5` and the tool's normal score threshold. Judge results by
|
|
60
61
|
task, environment, preconditions, and likely effect; title similarity alone
|
|
61
62
|
is insufficient. If no result is relevant, continue without Experience and
|
|
@@ -73,6 +74,26 @@ context or deeper intent analysis is useful.
|
|
|
73
74
|
follow-up search using the failure evidence, then read only newly relevant
|
|
74
75
|
Experience files.
|
|
75
76
|
|
|
77
|
+
## Servers Without the Home Alias
|
|
78
|
+
|
|
79
|
+
`viking://~` is the home alias for the caller's own space. A server released
|
|
80
|
+
before the alias (OpenViking v0.4.16) does not resolve it and rejects the
|
|
81
|
+
request with `INVALID_URI` (HTTP 400) instead of searching. Retry once against
|
|
82
|
+
the caller's explicit root, resolved from evidence rather than guessed:
|
|
83
|
+
|
|
84
|
+
- If a canonical `viking://user/<user_id>/...` URI is already visible in this
|
|
85
|
+
session — injected memory context, or an earlier OpenViking tool result —
|
|
86
|
+
reuse that `<user_id>` and search
|
|
87
|
+
`viking://user/<user_id>/memories/experiences`.
|
|
88
|
+
- Otherwise repeat the same query with no target scope, narrowed to memories
|
|
89
|
+
with `context_type` when the tool accepts it, and keep only hits whose URI
|
|
90
|
+
contains `/memories/experiences/`. Those hits carry the canonical user ID;
|
|
91
|
+
reuse it for the reads and for any later Experience search in this task.
|
|
92
|
+
|
|
93
|
+
Do not fall back to the uid-less `viking://user/memories/experiences`: only
|
|
94
|
+
some older servers and roles expand it, and current servers reject it. If no
|
|
95
|
+
explicit root can be resolved, continue without Experience.
|
|
96
|
+
|
|
76
97
|
## Applying Retrieved Experience
|
|
77
98
|
|
|
78
99
|
- Treat Experience as reusable procedure, not as a user profile, user intent,
|