@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.
@@ -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
- export async function assembleOpenVikingSession({
425
- sessionId,
426
- sessionKey,
427
- messages,
428
- tokenBudget,
429
- runtimeContext,
430
- isMainAssemble,
431
- cfg,
432
- getClient,
433
- logger,
434
- resolveAgentId,
435
- rememberSessionAgentId,
436
- isBypassedSession,
437
- queryConfigStore,
438
- traceRecorder,
439
- diag,
440
- roughEstimate,
441
- messageDigest,
442
- extractAgentMessageText,
443
- hasAutoRecallBlock,
444
- prependRecallToLatestUserMessage,
445
- }: AssembleOpenVikingSessionParams): Promise<AssembleOpenVikingSessionResult> {
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 client = await getClient();
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 = line.replace(/viking:\/\/[^\s<>"')\]]+/g, (uri) => {
91
- if (validSet.has(uri)) return uri;
92
- const nearest = nearestUri(uri, valid);
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) return { status: COMPRESS_OK, context: input };
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
- return { status: COMPRESS_OK, context: cached.digest };
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 digest = repairDigestUris(normalized, validUris.length
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,