@openviking/openclaw-plugin 2026.10.6 → 2026.10.7-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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # OpenViking for OpenClaw
2
2
 
3
+ > **Working memory is now opt-in.** Update installed plugins separately from the OV server, then restart the host. Existing explicit settings still take precedence. See the [default-off upgrade guide](../../docs/en/guides/working-memory-default-off.md) for native history, re-enabling WM, and old-conversation handoffs.
4
+
3
5
  Use [OpenViking](https://github.com/volcengine/OpenViking) as OpenClaw's long-term context engine: automatic recall, session archive, memory extraction, semantic search, and RAG over a remote OpenViking server.
4
6
 
5
7
  ## Quick Start
@@ -243,7 +245,7 @@ Use it as a complement to auto-capture, not a replacement:
243
245
 
244
246
  - auto-capture still preserves ordinary conversation flow and batches extraction for cost and latency
245
247
  - `memory_store` is for explicit durable-memory intent such as "remember my main project is X" or "save this preference"
246
- - if `memory_store` commits but extracts 0 memories, check the OpenViking server extraction/model configuration; the explicit path triggered extraction, but the extractor did not produce a memory
248
+ - if `memory_store` commits but extracts 0 memories, it returns `action: "failed"` with `error: "no_memories_extracted"` and the agent tells the user no new memory was created; possible causes are that the content is already stored, extraction is disabled, or the extraction/model configuration needs checking
247
249
 
248
250
  ### What `compact()` does
249
251
 
package/client.ts CHANGED
@@ -75,6 +75,7 @@ export type OpenVikingClientOptions = {
75
75
  };
76
76
 
77
77
  export type CommitSessionResult = {
78
+ effective_enable_working_memory?: boolean;
78
79
  session_id: string;
79
80
  /** "accepted" (async), "skipped" (no archive), "completed", "failed", or "timeout" (wait mode). */
80
81
  status: string;
@@ -553,11 +554,11 @@ export class OpenVikingClient {
553
554
  };
554
555
  }
555
556
 
556
- async read(uri: string, actorPeerId?: string): Promise<string> {
557
+ async read(uri: string, actorPeerId?: string, requestTimeoutMs?: number): Promise<string> {
557
558
  return this.request<string>(
558
559
  `/api/v1/content/read?uri=${encodeURIComponent(uri)}`,
559
560
  {},
560
- undefined,
561
+ requestTimeoutMs,
561
562
  actorPeerId,
562
563
  );
563
564
  }
@@ -841,6 +842,7 @@ export class OpenVikingClient {
841
842
  sessionId: string,
842
843
  options?: {
843
844
  wait?: boolean;
845
+ enableWorkingMemory?: boolean;
844
846
  timeoutMs?: number;
845
847
  /**
846
848
  * WM v2: number of most-recent messages to keep live after commit.
@@ -871,6 +873,9 @@ export class OpenVikingClient {
871
873
  options?.agentId,
872
874
  );
873
875
  const body: Record<string, unknown> = {};
876
+ if (options?.enableWorkingMemory !== undefined) {
877
+ body.enable_working_memory = options.enableWorkingMemory;
878
+ }
874
879
  if (options?.retentionMode === "turn_budget") {
875
880
  body.retention_mode = "turn_budget";
876
881
  } else if (keepRecentCount > 0) {
package/commands/setup.ts CHANGED
@@ -66,6 +66,7 @@ function readCompatRangeFromManifest(): { min: string; max: string } {
66
66
  const PLUGIN_VERSION = readPluginVersion();
67
67
  const { min: COMPATIBLE_SERVER_MIN, max: COMPATIBLE_SERVER_MAX } = readCompatRangeFromManifest();
68
68
  const CONFIG_KEYS_TO_PRESERVE = [
69
+ "contextManagementMode",
69
70
  "targetUri",
70
71
  "headers",
71
72
  "timeoutMs",
package/config.ts CHANGED
@@ -42,6 +42,7 @@ export type MemoryOpenVikingConfig = {
42
42
  userId?: string;
43
43
  targetUri?: string;
44
44
  timeoutMs?: number;
45
+ contextManagementMode?: "native" | "openviking";
45
46
  autoCapture?: boolean;
46
47
  captureMode?: "semantic" | "keyword";
47
48
  captureMaxLength?: number;
@@ -514,6 +515,9 @@ export const memoryOpenVikingConfigSchema = {
514
515
  value = {};
515
516
  }
516
517
  const cfg = value as Record<string, unknown>;
518
+ if (cfg.contextManagementMode !== undefined && !["native", "openviking"].includes(String(cfg.contextManagementMode))) {
519
+ throw new Error("contextManagementMode must be native or openviking");
520
+ }
517
521
  assertAllowedKeys(
518
522
  cfg,
519
523
  [
@@ -528,6 +532,7 @@ export const memoryOpenVikingConfigSchema = {
528
532
  "userId",
529
533
  "targetUri",
530
534
  "timeoutMs",
535
+ "contextManagementMode",
531
536
  "autoCapture",
532
537
  "captureMode",
533
538
  "captureMaxLength",
@@ -655,6 +660,7 @@ export const memoryOpenVikingConfigSchema = {
655
660
  userId,
656
661
  targetUri: typeof cfg.targetUri === "string" ? cfg.targetUri : DEFAULT_TARGET_URI,
657
662
  timeoutMs: Math.max(1000, Math.floor(toNumber(cfg.timeoutMs, DEFAULT_TIMEOUT_MS))),
663
+ contextManagementMode: cfg.contextManagementMode === "openviking" ? "openviking" : "native",
658
664
  autoCapture: cfg.autoCapture !== false,
659
665
  captureMode: captureMode ?? DEFAULT_CAPTURE_MODE,
660
666
  captureMaxLength: Math.max(
package/context-engine.ts CHANGED
@@ -25,7 +25,7 @@ type ContextEngineInfo = {
25
25
  id: string;
26
26
  name: string;
27
27
  version?: string;
28
- ownsCompaction: true;
28
+ ownsCompaction: boolean;
29
29
  /** OpenClaw >=2026.8.1: without both declarations (and commitTurn) the engine is degraded to "legacy" every turn. */
30
30
  transcriptSemantics: {
31
31
  currentTurnFence: "before-current-turn-entry-v1";
@@ -365,7 +365,7 @@ export function createMemoryOpenVikingContextEngine(params: {
365
365
  id,
366
366
  name,
367
367
  version,
368
- ownsCompaction: true,
368
+ ownsCompaction: cfg.contextManagementMode === "openviking",
369
369
  transcriptSemantics: {
370
370
  currentTurnFence: "before-current-turn-entry-v1",
371
371
  turnAdvancementIdempotency: "atomic-idempotent-v1",
@@ -490,6 +490,7 @@ export function createMemoryOpenVikingContextEngine(params: {
490
490
  async compact(compactParams): Promise<CompactResult> {
491
491
  const tokenBudget = validTokenBudget(compactParams.tokenBudget) ?? 128_000;
492
492
  return compactOpenVikingSession({
493
+ contextManagementMode: cfg.contextManagementMode,
493
494
  sessionId: compactParams.sessionId,
494
495
  sessionKey: resolveSessionKey(compactParams),
495
496
  tokenBudget,
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(/\/+$/, "");
@@ -414,6 +414,9 @@ export class OpenVikingClient {
414
414
  retentionMode: options?.retentionMode,
415
415
  }, options?.agentId);
416
416
  const body = {};
417
+ if (options?.enableWorkingMemory !== undefined) {
418
+ body.enable_working_memory = options.enableWorkingMemory;
419
+ }
417
420
  if (options?.retentionMode === "turn_budget") {
418
421
  body.retention_mode = "turn_budget";
419
422
  }
@@ -56,6 +56,7 @@ function readCompatRangeFromManifest() {
56
56
  const PLUGIN_VERSION = readPluginVersion();
57
57
  const { min: COMPATIBLE_SERVER_MIN, max: COMPATIBLE_SERVER_MAX } = readCompatRangeFromManifest();
58
58
  const CONFIG_KEYS_TO_PRESERVE = [
59
+ "contextManagementMode",
59
60
  "targetUri",
60
61
  "headers",
61
62
  "timeoutMs",
package/dist/config.js CHANGED
@@ -328,6 +328,9 @@ export const memoryOpenVikingConfigSchema = {
328
328
  value = {};
329
329
  }
330
330
  const cfg = value;
331
+ if (cfg.contextManagementMode !== undefined && !["native", "openviking"].includes(String(cfg.contextManagementMode))) {
332
+ throw new Error("contextManagementMode must be native or openviking");
333
+ }
331
334
  assertAllowedKeys(cfg, [
332
335
  "mode",
333
336
  "baseUrl",
@@ -340,6 +343,7 @@ export const memoryOpenVikingConfigSchema = {
340
343
  "userId",
341
344
  "targetUri",
342
345
  "timeoutMs",
346
+ "contextManagementMode",
343
347
  "autoCapture",
344
348
  "captureMode",
345
349
  "captureMaxLength",
@@ -436,6 +440,7 @@ export const memoryOpenVikingConfigSchema = {
436
440
  userId,
437
441
  targetUri: typeof cfg.targetUri === "string" ? cfg.targetUri : DEFAULT_TARGET_URI,
438
442
  timeoutMs: Math.max(1000, Math.floor(toNumber(cfg.timeoutMs, DEFAULT_TIMEOUT_MS))),
443
+ contextManagementMode: cfg.contextManagementMode === "openviking" ? "openviking" : "native",
439
444
  autoCapture: cfg.autoCapture !== false,
440
445
  captureMode: captureMode ?? DEFAULT_CAPTURE_MODE,
441
446
  captureMaxLength: Math.max(200, Math.min(200_000, Math.floor(toNumber(cfg.captureMaxLength, DEFAULT_CAPTURE_MAX_LENGTH)))),
@@ -191,7 +191,7 @@ export function createMemoryOpenVikingContextEngine(params) {
191
191
  id,
192
192
  name,
193
193
  version,
194
- ownsCompaction: true,
194
+ ownsCompaction: cfg.contextManagementMode === "openviking",
195
195
  transcriptSemantics: {
196
196
  currentTurnFence: "before-current-turn-entry-v1",
197
197
  turnAdvancementIdempotency: "atomic-idempotent-v1",
@@ -308,6 +308,7 @@ export function createMemoryOpenVikingContextEngine(params) {
308
308
  async compact(compactParams) {
309
309
  const tokenBudget = validTokenBudget(compactParams.tokenBudget) ?? 128_000;
310
310
  return compactOpenVikingSession({
311
+ contextManagementMode: cfg.contextManagementMode,
311
312
  sessionId: compactParams.sessionId,
312
313
  sessionKey: resolveSessionKey(compactParams),
313
314
  tokenBudget,
@@ -95,13 +95,31 @@ export function registerOpenVikingMemoryTools(deps) {
95
95
  };
96
96
  }
97
97
  if (memoriesCount === 0) {
98
- deps.logger.warn(`openviking: memory_store committed but 0 memories extracted (sessionId=${sessionId}). ` +
99
- "Check OpenViking server logs for embedding/extract errors (e.g. 401 API key, or extraction pipeline).");
100
- }
101
- else {
102
- deps.logger.info?.(`openviking: memory_store committed, memories=${memoriesCount}, ` +
103
- `trace_id=${commitResult.trace_id ?? "none"}`);
98
+ deps.logger.warn(`openviking: memory_store committed but 0 memories extracted (sessionId=${sessionId}), ` +
99
+ `trace_id=${commitResult.trace_id ?? "none"}. ` +
100
+ "No memory was created or updated. Check memory.extraction_enabled, VLM configuration/API keys, or whether the content is already stored.");
101
+ return {
102
+ content: [{
103
+ type: "text",
104
+ text: `Memory extraction completed for session ${sessionId}, but created or updated no memory. ` +
105
+ "The content may already be stored, memory extraction may be disabled, or the extractor found nothing new to store. " +
106
+ "Tell the user no new memory was created." +
107
+ (commitResult.trace_id ? ` (trace_id=${commitResult.trace_id})` : ""),
108
+ }],
109
+ details: {
110
+ action: "failed",
111
+ sessionId,
112
+ status: commitResult.status,
113
+ error: "no_memories_extracted",
114
+ memoriesCount,
115
+ archived: commitResult.archived ?? false,
116
+ traceId: commitResult.trace_id,
117
+ usedTempSession,
118
+ },
119
+ };
104
120
  }
121
+ deps.logger.info?.(`openviking: memory_store committed, memories=${memoriesCount}, ` +
122
+ `trace_id=${commitResult.trace_id ?? "none"}`);
105
123
  return {
106
124
  content: [
107
125
  {
@@ -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,
@@ -344,12 +381,23 @@ async function assembleSessionContext(params) {
344
381
  });
345
382
  }
346
383
  }
384
+ if (cfg.contextManagementMode !== "openviking") {
385
+ return assemblePassthrough({ diag, ovSessionId, reason: "native_context", liveMessages: messages, originalTokens });
386
+ }
347
387
  try {
348
388
  const client = await getClient();
349
389
  const ctx = await client.getSessionContext(ovSessionId, tokenBudget);
350
390
  const preAbstracts = ctx?.pre_archive_abstracts ?? [];
351
391
  const hasArchives = !!ctx?.latest_archive_overview || preAbstracts.length > 0;
352
392
  const activeCount = ctx?.messages?.length ?? 0;
393
+ // A completed WM-off archive can leave a non-empty active tail. That tail
394
+ // alone cannot replace the host's earlier history in explicit OV mode.
395
+ if (!hasArchives && (ctx?.stats?.totalArchives ?? 0) > 0) {
396
+ logger.warn?.("openviking: archived context has no summary; keeping native history");
397
+ return assemblePassthrough({
398
+ diag, ovSessionId, reason: "archive_summary_missing", liveMessages: messages, originalTokens,
399
+ });
400
+ }
353
401
  if (!ctx || (!hasArchives && activeCount === 0)) {
354
402
  return assemblePassthrough({
355
403
  diag,
@@ -411,7 +459,7 @@ async function assembleSessionContext(params) {
411
459
  catch (err) {
412
460
  if (isSessionNotFoundError(err)) {
413
461
  const errorMessage = String(err);
414
- 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 ` +
415
463
  `(session=${ovSessionId}, tokenBudget=${tokenBudget}, agentId=${resolveAgentId(ovSessionId)})`);
416
464
  return assemblePassthrough({
417
465
  diag,
@@ -658,6 +706,7 @@ export async function afterTurnOpenVikingSession({ throwOnError = false, session
658
706
  }
659
707
  const commitResult = await client.commitSession(ovSessionId, {
660
708
  wait: false,
709
+ ...(cfg.contextManagementMode === "openviking" ? { enableWorkingMemory: true } : {}),
661
710
  ...(cfg.commitRetentionMode === "turn_budget"
662
711
  ? { retentionMode: "turn_budget" }
663
712
  : { keepRecentCount: cfg.commitKeepRecentCount }),
@@ -666,6 +715,7 @@ export async function afterTurnOpenVikingSession({ throwOnError = false, session
666
715
  `status=${commitResult.status}, archived=${commitResult.archived ?? false}, ` +
667
716
  `task_id=${commitResult.task_id ?? "none"}, trace_id=${commitResult.trace_id ?? "none"}`);
668
717
  diag("afterTurn_commit", ovSessionId, {
718
+ effectiveEnableWorkingMemory: commitResult.effective_enable_working_memory,
669
719
  pendingTokens,
670
720
  commitTokenThreshold,
671
721
  commitTokenThresholdRatio: cfg.commitTokenThresholdRatio,
@@ -715,7 +765,7 @@ function compactFailureResult(reason, tokensBefore, details) {
715
765
  },
716
766
  };
717
767
  }
718
- export async function compactOpenVikingSession({ sessionId, sessionKey, tokenBudget, currentTokenCount, force, compactionTarget, customInstructions, getClient, logger, resolveAgentId, isBypassedSession, runtimeCompact, diag, }) {
768
+ export async function compactOpenVikingSession({ contextManagementMode = "native", sessionId, sessionKey, tokenBudget, currentTokenCount, force, compactionTarget, customInstructions, getClient, logger, resolveAgentId, isBypassedSession, runtimeCompact, diag, }) {
719
769
  const ovSessionId = openClawSessionToOvStorageId(sessionId, sessionKey);
720
770
  diag("compact_entry", ovSessionId, {
721
771
  tokenBudget,
@@ -749,6 +799,9 @@ export async function compactOpenVikingSession({ sessionId, sessionKey, tokenBud
749
799
  reason: "session_bypassed",
750
800
  };
751
801
  }
802
+ if (contextManagementMode === "native") {
803
+ return await runtimeCompact?.() ?? compactFailureResult("native_compaction_unavailable", validTokenCount(currentTokenCount) ?? -1, { message: "Upgrade OpenClaw to a version with runtime compaction delegation." });
804
+ }
752
805
  const client = await getClient();
753
806
  const agentId = resolveAgentId(sessionId, sessionKey, ovSessionId);
754
807
  const tokensBeforeOriginal = validTokenCount(currentTokenCount);
@@ -771,6 +824,7 @@ export async function compactOpenVikingSession({ sessionId, sessionKey, tokenBud
771
824
  const commitResult = await client.commitSession(ovSessionId, {
772
825
  wait: true,
773
826
  keepRecentCount: 0,
827
+ enableWorkingMemory: true,
774
828
  });
775
829
  const memCount = totalExtractedMemories(commitResult.memories_extracted);
776
830
  if (commitResult.status === "failed") {
@@ -828,6 +882,9 @@ export async function compactOpenVikingSession({ sessionId, sessionKey, tokenBud
828
882
  },
829
883
  };
830
884
  }
885
+ if (commitResult.effective_enable_working_memory !== true) {
886
+ return await runtimeCompact?.() ?? compactFailureResult("working_memory_disabled", tokensBefore, { commit: commitResult });
887
+ }
831
888
  let summary = "";
832
889
  const firstKeptEntryId = commitResult.archive_uri?.split("/").pop() ?? "";
833
890
  let tokensAfter;
@@ -875,6 +932,9 @@ export async function compactOpenVikingSession({ sessionId, sessionKey, tokenBud
875
932
  logger.info(`openviking: compact tokens session=${ovSessionId}, ` +
876
933
  `tokensBefore=${tokensBefore}, tokensAfter=${tokensAfter ?? "unknown"}, ` +
877
934
  `latestArchiveId=${firstKeptEntryId || "none"}`);
935
+ if (!summary) {
936
+ return await runtimeCompact?.() ?? compactFailureResult("archive_summary_missing", tokensBefore, { commit: commitResult });
937
+ }
878
938
  diag("compact_result", ovSessionId, {
879
939
  ok: true,
880
940
  compacted: true,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifestVersion": "1.0",
3
- "pluginVersion": "2026.9.21",
3
+ "pluginVersion": "2026.9.22",
4
4
  "plugin": {
5
5
  "id": "openviking",
6
6
  "kind": "context-engine",
@@ -353,6 +353,12 @@
353
353
  "timeoutMs": {
354
354
  "type": "number"
355
355
  },
356
+ "contextManagementMode": {
357
+ "type": "string",
358
+ "enum": ["native", "openviking"],
359
+ "default": "native",
360
+ "description": "Use host history and compaction; openviking explicitly enables Working Memory context management."
361
+ },
356
362
  "autoCapture": {
357
363
  "type": "boolean"
358
364
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openviking/openclaw-plugin",
3
- "version": "2026.10.6",
3
+ "version": "2026.10.7-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",
@@ -176,15 +176,34 @@ export function registerOpenVikingMemoryTools(deps: OpenVikingMemoryToolsDeps):
176
176
  }
177
177
  if (memoriesCount === 0) {
178
178
  deps.logger.warn(
179
- `openviking: memory_store committed but 0 memories extracted (sessionId=${sessionId}). ` +
180
- "Check OpenViking server logs for embedding/extract errors (e.g. 401 API key, or extraction pipeline).",
181
- );
182
- } else {
183
- deps.logger.info?.(
184
- `openviking: memory_store committed, memories=${memoriesCount}, ` +
185
- `trace_id=${commitResult.trace_id ?? "none"}`,
179
+ `openviking: memory_store committed but 0 memories extracted (sessionId=${sessionId}), ` +
180
+ `trace_id=${commitResult.trace_id ?? "none"}. ` +
181
+ "No memory was created or updated. Check memory.extraction_enabled, VLM configuration/API keys, or whether the content is already stored.",
186
182
  );
183
+ return {
184
+ content: [{
185
+ type: "text",
186
+ text: `Memory extraction completed for session ${sessionId}, but created or updated no memory. ` +
187
+ "The content may already be stored, memory extraction may be disabled, or the extractor found nothing new to store. " +
188
+ "Tell the user no new memory was created." +
189
+ (commitResult.trace_id ? ` (trace_id=${commitResult.trace_id})` : ""),
190
+ }],
191
+ details: {
192
+ action: "failed",
193
+ sessionId,
194
+ status: commitResult.status,
195
+ error: "no_memories_extracted",
196
+ memoriesCount,
197
+ archived: commitResult.archived ?? false,
198
+ traceId: commitResult.trace_id,
199
+ usedTempSession,
200
+ },
201
+ };
187
202
  }
203
+ deps.logger.info?.(
204
+ `openviking: memory_store committed, memories=${memoriesCount}, ` +
205
+ `trace_id=${commitResult.trace_id ?? "none"}`,
206
+ );
188
207
  return {
189
208
  content: [
190
209
  {
@@ -107,6 +107,7 @@ export type AssembleOpenVikingSessionParams = {
107
107
  type CompactClient = Pick<OpenVikingClient, "commitSession" | "getSessionContext">;
108
108
 
109
109
  export type CompactOpenVikingSessionParams = {
110
+ contextManagementMode?: "native" | "openviking";
110
111
  sessionId: string;
111
112
  sessionKey?: string;
112
113
  tokenBudget: number;
@@ -138,6 +139,7 @@ export type AfterTurnOpenVikingSessionParams = {
138
139
  runtimeContext?: Record<string, unknown>;
139
140
  cfg: {
140
141
  autoCapture: boolean;
142
+ contextManagementMode?: "native" | "openviking";
141
143
  commitTokenThresholdRatio: number;
142
144
  commitKeepRecentCount: number;
143
145
  commitRetentionMode?: "message_count" | "turn_budget";
@@ -426,14 +428,13 @@ function isSessionNotFoundError(err: unknown): boolean {
426
428
  return errorMessage.includes("[NOT_FOUND]") && errorMessage.includes("Session not found");
427
429
  }
428
430
 
429
- async function recallForAssemble(
430
- params: AssembleOpenVikingSessionParams,
431
- recallQuery: ReturnType<typeof prepareRecallQuery>,
432
- ) {
433
- 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;
434
436
  const ovSessionId = openClawSessionToOvStorageId(sessionId, sessionKey);
435
437
  const sender = extractRuntimeSenderId(params.runtimeContext);
436
- const client = await getClient();
437
438
  const routingRef = sessionId ?? sessionKey ?? ovSessionId;
438
439
  const agentId = resolveAgentId(routingRef, sessionKey, ovSessionId);
439
440
  const actorPeerId = resolveOpenVikingActorPeerId({
@@ -442,6 +443,44 @@ async function recallForAssemble(
442
443
  assistantPeerId: agentId,
443
444
  warn: (message) => logger.warn?.(message),
444
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;
445
484
  const queryConfig = await queryConfigStore?.getEffective({
446
485
  agentId,
447
486
  sessionId,
@@ -474,24 +513,36 @@ export async function assembleOpenVikingSession(
474
513
  const assembled = await assembleSessionContext(params);
475
514
  // Current OpenClaw supplies the pending turn separately from history. Keep
476
515
  // recalled context out of persisted messages and let the host own that turn.
477
- if (
478
- !params.isMainAssemble || !params.cfg.autoRecall || !params.prompt ||
479
- params.isBypassedSession(params)
480
- ) {
516
+ if (!params.isMainAssemble || !params.cfg.autoRecall || params.isBypassedSession(params)) {
481
517
  return assembled;
482
518
  }
483
- const query = prepareRecallQuery(params.prompt);
484
- if (query.query.length < 5) return assembled;
519
+ const query = prepareRecallQuery(params.prompt ?? "");
485
520
 
486
521
  try {
487
- const recall = await recallForAssemble(params, query);
488
- if (!recall.block) return assembled;
489
- const systemPromptAddition = [assembled.systemPromptAddition, recall.block].filter(Boolean).join("\n\n");
490
- const estimatedTokens = assembled.estimatedTokens
491
- + estimateTextTokens(systemPromptAddition)
492
- - estimateTextTokens(assembled.systemPromptAddition ?? "");
493
- if (estimatedTokens > params.tokenBudget) return assembled;
494
- 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;
495
546
  } catch (err) {
496
547
  params.logger.warn?.(`openviking: auto-recall failed: ${String(err)}`);
497
548
  return assembled;
@@ -571,7 +622,7 @@ async function assembleSessionContext(params: AssembleOpenVikingSessionParams):
571
622
  }
572
623
 
573
624
  try {
574
- const recall = await recallForAssemble(params, recallQuery);
625
+ const recall = await recallForAssemble(params, recallQuery, await getClient(), resolveAssembleRouting(params));
575
626
 
576
627
  if (!recall.block) {
577
628
  return assemblePassthrough({
@@ -610,6 +661,10 @@ async function assembleSessionContext(params: AssembleOpenVikingSessionParams):
610
661
  }
611
662
  }
612
663
 
664
+ if (cfg.contextManagementMode !== "openviking") {
665
+ return assemblePassthrough({ diag, ovSessionId, reason: "native_context", liveMessages: messages, originalTokens });
666
+ }
667
+
613
668
  try {
614
669
  const client = await getClient();
615
670
  const ctx = await client.getSessionContext(ovSessionId, tokenBudget);
@@ -618,6 +673,15 @@ async function assembleSessionContext(params: AssembleOpenVikingSessionParams):
618
673
  const hasArchives = !!ctx?.latest_archive_overview || preAbstracts.length > 0;
619
674
  const activeCount = ctx?.messages?.length ?? 0;
620
675
 
676
+ // A completed WM-off archive can leave a non-empty active tail. That tail
677
+ // alone cannot replace the host's earlier history in explicit OV mode.
678
+ if (!hasArchives && (ctx?.stats?.totalArchives ?? 0) > 0) {
679
+ logger.warn?.("openviking: archived context has no summary; keeping native history");
680
+ return assemblePassthrough({
681
+ diag, ovSessionId, reason: "archive_summary_missing", liveMessages: messages, originalTokens,
682
+ });
683
+ }
684
+
621
685
  if (!ctx || (!hasArchives && activeCount === 0)) {
622
686
  return assemblePassthrough({
623
687
  diag,
@@ -692,7 +756,7 @@ async function assembleSessionContext(params: AssembleOpenVikingSessionParams):
692
756
  if (isSessionNotFoundError(err)) {
693
757
  const errorMessage = String(err);
694
758
  logger.info(
695
- `openviking: assemble skipped because OV session does not exist ` +
759
+ `openviking: session history not assembled because OV session does not exist ` +
696
760
  `(session=${ovSessionId}, tokenBudget=${tokenBudget}, agentId=${resolveAgentId(ovSessionId)})`,
697
761
  );
698
762
  return assemblePassthrough({
@@ -989,6 +1053,7 @@ export async function afterTurnOpenVikingSession({
989
1053
 
990
1054
  const commitResult = await client.commitSession(ovSessionId, {
991
1055
  wait: false,
1056
+ ...(cfg.contextManagementMode === "openviking" ? { enableWorkingMemory: true } : {}),
992
1057
  ...(cfg.commitRetentionMode === "turn_budget"
993
1058
  ? { retentionMode: "turn_budget" as const }
994
1059
  : { keepRecentCount: cfg.commitKeepRecentCount }),
@@ -1000,6 +1065,7 @@ export async function afterTurnOpenVikingSession({
1000
1065
  );
1001
1066
 
1002
1067
  diag("afterTurn_commit", ovSessionId, {
1068
+ effectiveEnableWorkingMemory: commitResult.effective_enable_working_memory,
1003
1069
  pendingTokens,
1004
1070
  commitTokenThreshold,
1005
1071
  commitTokenThresholdRatio: cfg.commitTokenThresholdRatio,
@@ -1058,6 +1124,7 @@ function compactFailureResult(
1058
1124
  }
1059
1125
 
1060
1126
  export async function compactOpenVikingSession({
1127
+ contextManagementMode = "native",
1061
1128
  sessionId,
1062
1129
  sessionKey,
1063
1130
  tokenBudget,
@@ -1107,6 +1174,13 @@ export async function compactOpenVikingSession({
1107
1174
  };
1108
1175
  }
1109
1176
 
1177
+ if (contextManagementMode === "native") {
1178
+ return await runtimeCompact?.() ?? compactFailureResult(
1179
+ "native_compaction_unavailable", validTokenCount(currentTokenCount) ?? -1,
1180
+ { message: "Upgrade OpenClaw to a version with runtime compaction delegation." },
1181
+ );
1182
+ }
1183
+
1110
1184
  const client = await getClient();
1111
1185
  const agentId = resolveAgentId(sessionId, sessionKey, ovSessionId);
1112
1186
  const tokensBeforeOriginal = validTokenCount(currentTokenCount);
@@ -1134,6 +1208,7 @@ export async function compactOpenVikingSession({
1134
1208
  const commitResult = await client.commitSession(ovSessionId, {
1135
1209
  wait: true,
1136
1210
  keepRecentCount: 0,
1211
+ enableWorkingMemory: true,
1137
1212
  });
1138
1213
  const memCount = totalExtractedMemories(commitResult.memories_extracted);
1139
1214
 
@@ -1204,6 +1279,10 @@ export async function compactOpenVikingSession({
1204
1279
  };
1205
1280
  }
1206
1281
 
1282
+ if (commitResult.effective_enable_working_memory !== true) {
1283
+ return await runtimeCompact?.() ?? compactFailureResult("working_memory_disabled", tokensBefore, { commit: commitResult });
1284
+ }
1285
+
1207
1286
  let summary = "";
1208
1287
  const firstKeptEntryId = commitResult.archive_uri?.split("/").pop() ?? "";
1209
1288
  let tokensAfter: number | undefined;
@@ -1264,6 +1343,10 @@ export async function compactOpenVikingSession({
1264
1343
  `latestArchiveId=${firstKeptEntryId || "none"}`,
1265
1344
  );
1266
1345
 
1346
+ if (!summary) {
1347
+ return await runtimeCompact?.() ?? compactFailureResult("archive_summary_missing", tokensBefore, { commit: commitResult });
1348
+ }
1349
+
1267
1350
  diag("compact_result", ovSessionId, {
1268
1351
  ok: true,
1269
1352
  compacted: true,
@@ -30,7 +30,7 @@ Choose the registered names that match the current runtime:
30
30
  | Runtime | Search | Read |
31
31
  | --- | --- | --- |
32
32
  | OpenViking MCP, Codex, Claude Code | `find` or `search` | `read` |
33
- | OpenCode | `openviking_find` or `openviking_search` | `openviking_read` |
33
+ | OpenCode, pi | `openviking_find` or `openviking_search` | `openviking_read` |
34
34
  | OpenClaw | `ov_search` | `ov_read` or `ov_multi_read` |
35
35
 
36
36
  The host may display MCP names with a namespace such as