@openviking/openclaw-plugin 2026.10.7 → 2026.10.8

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/client.ts CHANGED
@@ -554,11 +554,11 @@ export class OpenVikingClient {
554
554
  };
555
555
  }
556
556
 
557
- async read(uri: string, actorPeerId?: string): Promise<string> {
557
+ async read(uri: string, actorPeerId?: string, requestTimeoutMs?: number): Promise<string> {
558
558
  return this.request<string>(
559
559
  `/api/v1/content/read?uri=${encodeURIComponent(uri)}`,
560
560
  {},
561
- undefined,
561
+ requestTimeoutMs,
562
562
  actorPeerId,
563
563
  );
564
564
  }
package/dist/client.js CHANGED
@@ -244,8 +244,8 @@ export class OpenVikingClient {
244
244
  : result.entries,
245
245
  };
246
246
  }
247
- async read(uri, actorPeerId) {
248
- return this.request(`/api/v1/content/read?uri=${encodeURIComponent(uri)}`, {}, undefined, actorPeerId);
247
+ async read(uri, actorPeerId, requestTimeoutMs) {
248
+ return this.request(`/api/v1/content/read?uri=${encodeURIComponent(uri)}`, {}, requestTimeoutMs, actorPeerId);
249
249
  }
250
250
  async list(uri, options) {
251
251
  const normalizedUri = uri.trim().replace(/\/+$/, "");
@@ -196,11 +196,12 @@ function isSessionNotFoundError(err) {
196
196
  const errorMessage = String(err);
197
197
  return errorMessage.includes("[NOT_FOUND]") && errorMessage.includes("Session not found");
198
198
  }
199
- async function recallForAssemble(params, recallQuery) {
200
- const { sessionId, sessionKey, cfg, getClient, resolveAgentId, queryConfigStore, logger, traceRecorder } = params;
199
+ const PROFILE_MAX_CHARS = 8000;
200
+ const PROFILE_READ_TIMEOUT_MS = 3000;
201
+ function resolveAssembleRouting(params) {
202
+ const { sessionId, sessionKey, cfg, resolveAgentId, logger } = params;
201
203
  const ovSessionId = openClawSessionToOvStorageId(sessionId, sessionKey);
202
204
  const sender = extractRuntimeSenderId(params.runtimeContext);
203
- const client = await getClient();
204
205
  const routingRef = sessionId ?? sessionKey ?? ovSessionId;
205
206
  const agentId = resolveAgentId(routingRef, sessionKey, ovSessionId);
206
207
  const actorPeerId = resolveOpenVikingActorPeerId({
@@ -209,6 +210,34 @@ async function recallForAssemble(params, recallQuery) {
209
210
  assistantPeerId: agentId,
210
211
  warn: (message) => logger.warn?.(message),
211
212
  });
213
+ return { ovSessionId, agentId, actorPeerId };
214
+ }
215
+ // Server-side context assembly leaves profile.md out on the assumption that the
216
+ // client injects it, so read it here: the user's profile and, with peer scope, the actor's.
217
+ async function readProfileBlock(client, actorPeerId, timeoutMs, logger) {
218
+ const uris = ["viking://~/memories/profile.md"];
219
+ if (actorPeerId)
220
+ uris.push(`viking://~/peers/${actorPeerId}/memories/profile.md`);
221
+ const reads = await Promise.allSettled(uris.map((uri) => client.read(uri, actorPeerId, timeoutMs)));
222
+ return reads
223
+ .map((read, i) => {
224
+ if (read.status === "rejected" && !String(read.reason).includes("[NOT_FOUND]")) {
225
+ logger.warn?.(`openviking: profile read failed (uri=${uris[i]}): ${String(read.reason)}`);
226
+ }
227
+ const text = read.status === "fulfilled" && typeof read.value === "string" ? read.value.trim() : "";
228
+ if (!text)
229
+ return "";
230
+ const capped = text.length > PROFILE_MAX_CHARS
231
+ ? `${text.slice(0, PROFILE_MAX_CHARS).trimEnd()}\n... [profile truncated]`
232
+ : text;
233
+ return `<user-profile uri="${uris[i]}">\n${capped}\n</user-profile>`;
234
+ })
235
+ .filter(Boolean)
236
+ .join("\n\n");
237
+ }
238
+ async function recallForAssemble(params, recallQuery, client, routing) {
239
+ const { sessionId, sessionKey, cfg, queryConfigStore, logger, traceRecorder } = params;
240
+ const { ovSessionId, agentId, actorPeerId } = routing;
212
241
  const queryConfig = await queryConfigStore?.getEffective({
213
242
  agentId,
214
243
  sessionId,
@@ -238,24 +267,32 @@ export async function assembleOpenVikingSession(params) {
238
267
  const assembled = await assembleSessionContext(params);
239
268
  // Current OpenClaw supplies the pending turn separately from history. Keep
240
269
  // recalled context out of persisted messages and let the host own that turn.
241
- if (!params.isMainAssemble || !params.cfg.autoRecall || !params.prompt ||
242
- params.isBypassedSession(params)) {
270
+ if (!params.isMainAssemble || !params.cfg.autoRecall || params.isBypassedSession(params)) {
243
271
  return assembled;
244
272
  }
245
- const query = prepareRecallQuery(params.prompt);
246
- if (query.query.length < 5)
247
- return assembled;
273
+ const query = prepareRecallQuery(params.prompt ?? "");
248
274
  try {
249
- const recall = await recallForAssemble(params, query);
250
- if (!recall.block)
251
- return assembled;
252
- const systemPromptAddition = [assembled.systemPromptAddition, recall.block].filter(Boolean).join("\n\n");
253
- const estimatedTokens = assembled.estimatedTokens
254
- + estimateTextTokens(systemPromptAddition)
255
- - estimateTextTokens(assembled.systemPromptAddition ?? "");
256
- if (estimatedTokens > params.tokenBudget)
257
- return assembled;
258
- return { ...assembled, systemPromptAddition, estimatedTokens };
275
+ const routing = resolveAssembleRouting(params);
276
+ const client = await params.getClient();
277
+ const [profileBlock, recallBlock] = await Promise.all([
278
+ readProfileBlock(client, routing.actorPeerId, Math.min(params.cfg.autoRecallTimeoutMs ?? PROFILE_READ_TIMEOUT_MS, PROFILE_READ_TIMEOUT_MS), params.logger),
279
+ query.query.length < 5 ? "" : recallForAssemble(params, query, client, routing)
280
+ .then((recall) => recall.block)
281
+ .catch((err) => {
282
+ params.logger.warn?.(`openviking: auto-recall failed: ${String(err)}`);
283
+ return "";
284
+ }),
285
+ ]);
286
+ const baseTokens = estimateTextTokens(assembled.systemPromptAddition ?? "");
287
+ for (const blocks of [[profileBlock, recallBlock], [profileBlock], [recallBlock]]) {
288
+ if (!blocks.some(Boolean))
289
+ continue;
290
+ const systemPromptAddition = [assembled.systemPromptAddition, ...blocks].filter(Boolean).join("\n\n");
291
+ const estimatedTokens = assembled.estimatedTokens + estimateTextTokens(systemPromptAddition) - baseTokens;
292
+ if (estimatedTokens <= params.tokenBudget)
293
+ return { ...assembled, systemPromptAddition, estimatedTokens };
294
+ }
295
+ return assembled;
259
296
  }
260
297
  catch (err) {
261
298
  params.logger.warn?.(`openviking: auto-recall failed: ${String(err)}`);
@@ -307,7 +344,7 @@ async function assembleSessionContext(params) {
307
344
  `chars=${recallQuery.originalChars}->${recallQuery.finalChars})`);
308
345
  }
309
346
  try {
310
- const recall = await recallForAssemble(params, recallQuery);
347
+ const recall = await recallForAssemble(params, recallQuery, await getClient(), resolveAssembleRouting(params));
311
348
  if (!recall.block) {
312
349
  return assemblePassthrough({
313
350
  diag,
@@ -422,7 +459,7 @@ async function assembleSessionContext(params) {
422
459
  catch (err) {
423
460
  if (isSessionNotFoundError(err)) {
424
461
  const errorMessage = String(err);
425
- logger.info(`openviking: assemble skipped because OV session does not exist ` +
462
+ logger.info(`openviking: session history not assembled because OV session does not exist ` +
426
463
  `(session=${ovSessionId}, tokenBudget=${tokenBudget}, agentId=${resolveAgentId(ovSessionId)})`);
427
464
  return assemblePassthrough({
428
465
  diag,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openviking/openclaw-plugin",
3
- "version": "2026.10.7",
3
+ "version": "2026.10.8",
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",
@@ -428,14 +428,13 @@ function isSessionNotFoundError(err: unknown): boolean {
428
428
  return errorMessage.includes("[NOT_FOUND]") && errorMessage.includes("Session not found");
429
429
  }
430
430
 
431
- async function recallForAssemble(
432
- params: AssembleOpenVikingSessionParams,
433
- recallQuery: ReturnType<typeof prepareRecallQuery>,
434
- ) {
435
- const { sessionId, sessionKey, cfg, getClient, resolveAgentId, queryConfigStore, logger, traceRecorder } = params;
431
+ const PROFILE_MAX_CHARS = 8000;
432
+ const PROFILE_READ_TIMEOUT_MS = 3000;
433
+
434
+ function resolveAssembleRouting(params: AssembleOpenVikingSessionParams) {
435
+ const { sessionId, sessionKey, cfg, resolveAgentId, logger } = params;
436
436
  const ovSessionId = openClawSessionToOvStorageId(sessionId, sessionKey);
437
437
  const sender = extractRuntimeSenderId(params.runtimeContext);
438
- const client = await getClient();
439
438
  const routingRef = sessionId ?? sessionKey ?? ovSessionId;
440
439
  const agentId = resolveAgentId(routingRef, sessionKey, ovSessionId);
441
440
  const actorPeerId = resolveOpenVikingActorPeerId({
@@ -444,6 +443,44 @@ async function recallForAssemble(
444
443
  assistantPeerId: agentId,
445
444
  warn: (message) => logger.warn?.(message),
446
445
  });
446
+ return { ovSessionId, agentId, actorPeerId };
447
+ }
448
+
449
+ // Server-side context assembly leaves profile.md out on the assumption that the
450
+ // client injects it, so read it here: the user's profile and, with peer scope, the actor's.
451
+ async function readProfileBlock(
452
+ client: OpenVikingClient,
453
+ actorPeerId: string | undefined,
454
+ timeoutMs: number,
455
+ logger: ContextEngineLifecycleLogger,
456
+ ): Promise<string> {
457
+ const uris = ["viking://~/memories/profile.md"];
458
+ if (actorPeerId) uris.push(`viking://~/peers/${actorPeerId}/memories/profile.md`);
459
+ const reads = await Promise.allSettled(uris.map((uri) => client.read(uri, actorPeerId, timeoutMs)));
460
+ return reads
461
+ .map((read, i) => {
462
+ if (read.status === "rejected" && !String(read.reason).includes("[NOT_FOUND]")) {
463
+ logger.warn?.(`openviking: profile read failed (uri=${uris[i]}): ${String(read.reason)}`);
464
+ }
465
+ const text = read.status === "fulfilled" && typeof read.value === "string" ? read.value.trim() : "";
466
+ if (!text) return "";
467
+ const capped = text.length > PROFILE_MAX_CHARS
468
+ ? `${text.slice(0, PROFILE_MAX_CHARS).trimEnd()}\n... [profile truncated]`
469
+ : text;
470
+ return `<user-profile uri="${uris[i]}">\n${capped}\n</user-profile>`;
471
+ })
472
+ .filter(Boolean)
473
+ .join("\n\n");
474
+ }
475
+
476
+ async function recallForAssemble(
477
+ params: AssembleOpenVikingSessionParams,
478
+ recallQuery: ReturnType<typeof prepareRecallQuery>,
479
+ client: OpenVikingClient,
480
+ routing: ReturnType<typeof resolveAssembleRouting>,
481
+ ) {
482
+ const { sessionId, sessionKey, cfg, queryConfigStore, logger, traceRecorder } = params;
483
+ const { ovSessionId, agentId, actorPeerId } = routing;
447
484
  const queryConfig = await queryConfigStore?.getEffective({
448
485
  agentId,
449
486
  sessionId,
@@ -476,24 +513,36 @@ export async function assembleOpenVikingSession(
476
513
  const assembled = await assembleSessionContext(params);
477
514
  // Current OpenClaw supplies the pending turn separately from history. Keep
478
515
  // recalled context out of persisted messages and let the host own that turn.
479
- if (
480
- !params.isMainAssemble || !params.cfg.autoRecall || !params.prompt ||
481
- params.isBypassedSession(params)
482
- ) {
516
+ if (!params.isMainAssemble || !params.cfg.autoRecall || params.isBypassedSession(params)) {
483
517
  return assembled;
484
518
  }
485
- const query = prepareRecallQuery(params.prompt);
486
- if (query.query.length < 5) return assembled;
519
+ const query = prepareRecallQuery(params.prompt ?? "");
487
520
 
488
521
  try {
489
- const recall = await recallForAssemble(params, query);
490
- if (!recall.block) return assembled;
491
- const systemPromptAddition = [assembled.systemPromptAddition, recall.block].filter(Boolean).join("\n\n");
492
- const estimatedTokens = assembled.estimatedTokens
493
- + estimateTextTokens(systemPromptAddition)
494
- - estimateTextTokens(assembled.systemPromptAddition ?? "");
495
- if (estimatedTokens > params.tokenBudget) return assembled;
496
- return { ...assembled, systemPromptAddition, estimatedTokens };
522
+ const routing = resolveAssembleRouting(params);
523
+ const client = await params.getClient();
524
+ const [profileBlock, recallBlock] = await Promise.all([
525
+ readProfileBlock(
526
+ client,
527
+ routing.actorPeerId,
528
+ Math.min(params.cfg.autoRecallTimeoutMs ?? PROFILE_READ_TIMEOUT_MS, PROFILE_READ_TIMEOUT_MS),
529
+ params.logger,
530
+ ),
531
+ query.query.length < 5 ? "" : recallForAssemble(params, query, client, routing)
532
+ .then((recall) => recall.block)
533
+ .catch((err) => {
534
+ params.logger.warn?.(`openviking: auto-recall failed: ${String(err)}`);
535
+ return "";
536
+ }),
537
+ ]);
538
+ const baseTokens = estimateTextTokens(assembled.systemPromptAddition ?? "");
539
+ for (const blocks of [[profileBlock, recallBlock], [profileBlock], [recallBlock]]) {
540
+ if (!blocks.some(Boolean)) continue;
541
+ const systemPromptAddition = [assembled.systemPromptAddition, ...blocks].filter(Boolean).join("\n\n");
542
+ const estimatedTokens = assembled.estimatedTokens + estimateTextTokens(systemPromptAddition) - baseTokens;
543
+ if (estimatedTokens <= params.tokenBudget) return { ...assembled, systemPromptAddition, estimatedTokens };
544
+ }
545
+ return assembled;
497
546
  } catch (err) {
498
547
  params.logger.warn?.(`openviking: auto-recall failed: ${String(err)}`);
499
548
  return assembled;
@@ -573,7 +622,7 @@ async function assembleSessionContext(params: AssembleOpenVikingSessionParams):
573
622
  }
574
623
 
575
624
  try {
576
- const recall = await recallForAssemble(params, recallQuery);
625
+ const recall = await recallForAssemble(params, recallQuery, await getClient(), resolveAssembleRouting(params));
577
626
 
578
627
  if (!recall.block) {
579
628
  return assemblePassthrough({
@@ -707,7 +756,7 @@ async function assembleSessionContext(params: AssembleOpenVikingSessionParams):
707
756
  if (isSessionNotFoundError(err)) {
708
757
  const errorMessage = String(err);
709
758
  logger.info(
710
- `openviking: assemble skipped because OV session does not exist ` +
759
+ `openviking: session history not assembled because OV session does not exist ` +
711
760
  `(session=${ovSessionId}, tokenBudget=${tokenBudget}, agentId=${resolveAgentId(ovSessionId)})`,
712
761
  );
713
762
  return assemblePassthrough({