@openviking/openclaw-plugin 2026.9.10 → 2026.9.13
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 +8 -0
- package/auto-recall.ts +2 -1
- package/client.ts +17 -1
- package/commands/setup.ts +1 -0
- package/config.ts +20 -3
- package/context-engine.ts +1 -0
- package/dist/auto-recall.js +1 -1
- package/dist/client.js +12 -1
- package/dist/commands/setup.js +1 -0
- package/dist/config.js +14 -1
- package/dist/context-engine.js +1 -0
- package/dist/services/context-lifecycle-service.js +72 -32
- package/openclaw.plugin.json +10 -1
- package/package.json +1 -1
- package/services/context-lifecycle-service.ts +99 -53
package/README.md
CHANGED
|
@@ -227,6 +227,14 @@ After that, the plugin checks `pending_tokens`. Once it reaches `commitTokenThre
|
|
|
227
227
|
|
|
228
228
|
Memory extraction on this automatic path is commit-dependent. Short but important facts can stay only in the live session until a threshold commit, `/compact`, or an explicit store happens.
|
|
229
229
|
|
|
230
|
+
Auto-commit keeps the most recent 10 messages by default (`commitKeepRecentCount`). This count-based window can start mid-turn. With a server that supports turn-aware retention, set `"commitRetentionMode": "turn_budget"` in the plugin config to opt in:
|
|
231
|
+
|
|
232
|
+
- `commitKeepRecentCount` is ignored. The server defaults apply: up to 3 recent user turns, a 12,000-token retention budget, and at least the final assistant/tool step.
|
|
233
|
+
- For an oversized newest turn, the server retains its user question and recent steps and checkpoints the archived prefix. The mandatory tail can exceed the retention budget.
|
|
234
|
+
- `pending_tokens` counts only messages that will leave the live window, not a user question shared with the archive.
|
|
235
|
+
|
|
236
|
+
Manual commit and `/compact` still archive everything. Leave the option unset (or use `"message_count"`) to preserve existing behavior.
|
|
237
|
+
|
|
230
238
|
### Explicit long-term memory writes
|
|
231
239
|
|
|
232
240
|
When the user explicitly asks the agent to remember, save, or store an important long-term fact, preference, project, or decision, prefer `memory_store` over waiting for normal auto-capture. `memory_store` writes the text to an OpenViking session and calls `commit(wait=true)`, so it is the reliable integration-side path for facts that should be available as long-term memory as soon as possible.
|
package/auto-recall.ts
CHANGED
|
@@ -331,6 +331,7 @@ export async function buildAutoRecallContext(params: {
|
|
|
331
331
|
rawUserTextPreview?: string;
|
|
332
332
|
queryTruncated?: boolean;
|
|
333
333
|
resourceTypes?: RecallResourceType[];
|
|
334
|
+
dedupTurns?: number;
|
|
334
335
|
}): Promise<{ block?: string; memoryCount: number; estimatedTokens: number }> {
|
|
335
336
|
const { cfg, client, agentId, actorPeerId, queryText, logger, verbose } = params;
|
|
336
337
|
const queryConfig = params.queryConfig;
|
|
@@ -373,7 +374,7 @@ export async function buildAutoRecallContext(params: {
|
|
|
373
374
|
queryExpansion: "auto",
|
|
374
375
|
maxTokens,
|
|
375
376
|
detail: recallPreferAbstract ? "abstract" : undefined,
|
|
376
|
-
dedupTurns: params.ovSessionId ? AUTO_RECALL_DEDUP_TURNS : undefined,
|
|
377
|
+
dedupTurns: params.dedupTurns ?? (params.ovSessionId ? AUTO_RECALL_DEDUP_TURNS : undefined),
|
|
377
378
|
peerScope: "actor",
|
|
378
379
|
actorPeerId,
|
|
379
380
|
requestTimeoutMs: cfg.autoRecallTimeoutMs,
|
package/client.ts
CHANGED
|
@@ -80,6 +80,7 @@ export type CommitSessionResult = {
|
|
|
80
80
|
task_id?: string;
|
|
81
81
|
archive_uri?: string;
|
|
82
82
|
archived?: boolean;
|
|
83
|
+
reset_context?: boolean;
|
|
83
84
|
/** Present when wait=true and extraction completed. Keyed by category. */
|
|
84
85
|
memories_extracted?: Record<string, number>;
|
|
85
86
|
error?: string;
|
|
@@ -508,6 +509,7 @@ export class OpenVikingClient {
|
|
|
508
509
|
};
|
|
509
510
|
const body = {
|
|
510
511
|
...buildContextSearchBody(contractConfig, { sessionId: options.sessionId }),
|
|
512
|
+
...(options.dedupTurns === 0 ? { dedup_turns: 0 } : {}),
|
|
511
513
|
query,
|
|
512
514
|
...(options.contextType !== undefined ? { context_type: options.contextType } : {}),
|
|
513
515
|
...(options.detail !== undefined ? { detail: options.detail } : {}),
|
|
@@ -844,6 +846,10 @@ export class OpenVikingClient {
|
|
|
844
846
|
* preserves the pre-v2 "archive everything" behavior.
|
|
845
847
|
*/
|
|
846
848
|
keepRecentCount?: number;
|
|
849
|
+
/** Start empty context in the same session after archiving. */
|
|
850
|
+
resetContext?: boolean;
|
|
851
|
+
/** Opt in to the server's turn-aware defaults instead of message-count retention. */
|
|
852
|
+
retentionMode?: "turn_budget";
|
|
847
853
|
agentId?: string;
|
|
848
854
|
},
|
|
849
855
|
): Promise<CommitSessionResult> {
|
|
@@ -858,13 +864,19 @@ export class OpenVikingClient {
|
|
|
858
864
|
sessionId,
|
|
859
865
|
wait: options?.wait ?? false,
|
|
860
866
|
keepRecentCount,
|
|
867
|
+
retentionMode: options?.retentionMode,
|
|
861
868
|
},
|
|
862
869
|
options?.agentId,
|
|
863
870
|
);
|
|
864
871
|
const body: Record<string, unknown> = {};
|
|
865
|
-
if (
|
|
872
|
+
if (options?.retentionMode === "turn_budget") {
|
|
873
|
+
body.retention_mode = "turn_budget";
|
|
874
|
+
} else if (keepRecentCount > 0) {
|
|
866
875
|
body.keep_recent_count = keepRecentCount;
|
|
867
876
|
}
|
|
877
|
+
if (options?.resetContext) {
|
|
878
|
+
body.reset_context = true;
|
|
879
|
+
}
|
|
868
880
|
const result = await this.request<CommitSessionResult>(
|
|
869
881
|
`/api/v1/sessions/${encodeURIComponent(sessionId)}/commit`,
|
|
870
882
|
{ method: "POST", body: JSON.stringify(body) },
|
|
@@ -872,6 +884,10 @@ export class OpenVikingClient {
|
|
|
872
884
|
options?.agentId,
|
|
873
885
|
);
|
|
874
886
|
|
|
887
|
+
if (options?.resetContext && result.reset_context !== true) {
|
|
888
|
+
throw new Error("OpenViking server did not confirm reset_context; upgrade the server with the plugin.");
|
|
889
|
+
}
|
|
890
|
+
|
|
875
891
|
if (!options?.wait || !result.task_id) {
|
|
876
892
|
return result;
|
|
877
893
|
}
|
package/commands/setup.ts
CHANGED
package/config.ts
CHANGED
|
@@ -68,11 +68,13 @@ export type MemoryOpenVikingConfig = {
|
|
|
68
68
|
* compatibility). Set to 0 to commit every turn.
|
|
69
69
|
*/
|
|
70
70
|
commitTokenThresholdRatio?: number;
|
|
71
|
+
/** Auto-commit retention: legacy message count (default) or the server's turn-budget policy. */
|
|
72
|
+
commitRetentionMode?: "message_count" | "turn_budget";
|
|
71
73
|
/**
|
|
72
74
|
* WM v2: number of most-recent messages to keep live after an afterTurn
|
|
73
75
|
* commit so the next turn still has immediate context. Forwarded to the
|
|
74
|
-
* server as `keep_recent_count`. Default 10.
|
|
75
|
-
* value and always passes 0.
|
|
76
|
+
* server as `keep_recent_count`. Default 10. Ignored in turn_budget mode.
|
|
77
|
+
* The compact path ignores this value and always passes 0.
|
|
76
78
|
*/
|
|
77
79
|
commitKeepRecentCount?: number;
|
|
78
80
|
bypassSessionPatterns?: string[];
|
|
@@ -538,6 +540,7 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
538
540
|
"recallTokenBudget",
|
|
539
541
|
"commitTokenThreshold",
|
|
540
542
|
"commitTokenThresholdRatio",
|
|
543
|
+
"commitRetentionMode",
|
|
541
544
|
"commitKeepRecentCount",
|
|
542
545
|
"bypassSessionPatterns",
|
|
543
546
|
"ingestReplyAssist",
|
|
@@ -588,6 +591,14 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
588
591
|
? (cfg.apiKey as string | OpenVikingSecretRef)
|
|
589
592
|
: getEnv("OPENVIKING_API_KEY") || undefined;
|
|
590
593
|
const captureMode = cfg.captureMode;
|
|
594
|
+
const commitRetentionMode = cfg.commitRetentionMode;
|
|
595
|
+
if (
|
|
596
|
+
commitRetentionMode !== undefined &&
|
|
597
|
+
commitRetentionMode !== "message_count" &&
|
|
598
|
+
commitRetentionMode !== "turn_budget"
|
|
599
|
+
) {
|
|
600
|
+
throw new Error('openviking commitRetentionMode must be "message_count" or "turn_budget"');
|
|
601
|
+
}
|
|
591
602
|
if (
|
|
592
603
|
typeof captureMode !== "undefined" &&
|
|
593
604
|
captureMode !== "semantic" &&
|
|
@@ -666,6 +677,7 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
666
677
|
0,
|
|
667
678
|
Math.min(1, toNumber(cfg.commitTokenThresholdRatio, DEFAULT_COMMIT_TOKEN_THRESHOLD_RATIO)),
|
|
668
679
|
),
|
|
680
|
+
commitRetentionMode: commitRetentionMode ?? "message_count",
|
|
669
681
|
commitKeepRecentCount: Math.max(
|
|
670
682
|
0,
|
|
671
683
|
Math.min(
|
|
@@ -916,13 +928,18 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
916
928
|
advanced: true,
|
|
917
929
|
help: "Auto-commit triggers once estimated pending tokens reach this fraction (0-1) of the model context window (e.g. 0.5 = 50%). Set to 0 to commit every turn.",
|
|
918
930
|
},
|
|
931
|
+
commitRetentionMode: {
|
|
932
|
+
label: "Commit Retention Mode",
|
|
933
|
+
advanced: true,
|
|
934
|
+
help: "Auto-commit only: message_count (default) keeps recent messages; turn_budget uses the server's turn-aware defaults. Manual commit and compact still archive everything.",
|
|
935
|
+
},
|
|
919
936
|
commitKeepRecentCount: {
|
|
920
937
|
label: "Commit Keep Recent Count",
|
|
921
938
|
placeholder: String(DEFAULT_COMMIT_KEEP_RECENT_COUNT),
|
|
922
939
|
advanced: true,
|
|
923
940
|
help:
|
|
924
941
|
"Number of most-recent messages to keep live after an afterTurn commit. " +
|
|
925
|
-
"Forwarded as keep_recent_count to the server. Compact path always uses 0.",
|
|
942
|
+
"Forwarded as keep_recent_count to the server in message_count mode; ignored in turn_budget mode. Compact path always uses 0.",
|
|
926
943
|
},
|
|
927
944
|
emitStandardDiagnostics: {
|
|
928
945
|
label: "Standard diagnostics (diag JSON lines)",
|
package/context-engine.ts
CHANGED
|
@@ -394,6 +394,7 @@ export function createMemoryOpenVikingContextEngine(params: {
|
|
|
394
394
|
sessionId: assembleParams.sessionId,
|
|
395
395
|
sessionKey: resolveSessionKey(assembleParams),
|
|
396
396
|
messages: assembleParams.messages,
|
|
397
|
+
prompt: assembleParams.prompt,
|
|
397
398
|
tokenBudget,
|
|
398
399
|
runtimeContext: assembleParams.runtimeContext,
|
|
399
400
|
isMainAssemble,
|
package/dist/auto-recall.js
CHANGED
|
@@ -244,7 +244,7 @@ export async function buildAutoRecallContext(params) {
|
|
|
244
244
|
queryExpansion: "auto",
|
|
245
245
|
maxTokens,
|
|
246
246
|
detail: recallPreferAbstract ? "abstract" : undefined,
|
|
247
|
-
dedupTurns: params.ovSessionId ? AUTO_RECALL_DEDUP_TURNS : undefined,
|
|
247
|
+
dedupTurns: params.dedupTurns ?? (params.ovSessionId ? AUTO_RECALL_DEDUP_TURNS : undefined),
|
|
248
248
|
peerScope: "actor",
|
|
249
249
|
actorPeerId,
|
|
250
250
|
requestTimeoutMs: cfg.autoRecallTimeoutMs,
|
package/dist/client.js
CHANGED
|
@@ -205,6 +205,7 @@ export class OpenVikingClient {
|
|
|
205
205
|
};
|
|
206
206
|
const body = {
|
|
207
207
|
...buildContextSearchBody(contractConfig, { sessionId: options.sessionId }),
|
|
208
|
+
...(options.dedupTurns === 0 ? { dedup_turns: 0 } : {}),
|
|
208
209
|
query,
|
|
209
210
|
...(options.contextType !== undefined ? { context_type: options.contextType } : {}),
|
|
210
211
|
...(options.detail !== undefined ? { detail: options.detail } : {}),
|
|
@@ -409,12 +410,22 @@ export class OpenVikingClient {
|
|
|
409
410
|
sessionId,
|
|
410
411
|
wait: options?.wait ?? false,
|
|
411
412
|
keepRecentCount,
|
|
413
|
+
retentionMode: options?.retentionMode,
|
|
412
414
|
}, options?.agentId);
|
|
413
415
|
const body = {};
|
|
414
|
-
if (
|
|
416
|
+
if (options?.retentionMode === "turn_budget") {
|
|
417
|
+
body.retention_mode = "turn_budget";
|
|
418
|
+
}
|
|
419
|
+
else if (keepRecentCount > 0) {
|
|
415
420
|
body.keep_recent_count = keepRecentCount;
|
|
416
421
|
}
|
|
422
|
+
if (options?.resetContext) {
|
|
423
|
+
body.reset_context = true;
|
|
424
|
+
}
|
|
417
425
|
const result = await this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/commit`, { method: "POST", body: JSON.stringify(body) }, undefined, options?.agentId);
|
|
426
|
+
if (options?.resetContext && result.reset_context !== true) {
|
|
427
|
+
throw new Error("OpenViking server did not confirm reset_context; upgrade the server with the plugin.");
|
|
428
|
+
}
|
|
418
429
|
if (!options?.wait || !result.task_id) {
|
|
419
430
|
return result;
|
|
420
431
|
}
|
package/dist/commands/setup.js
CHANGED
package/dist/config.js
CHANGED
|
@@ -354,6 +354,7 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
354
354
|
"recallTokenBudget",
|
|
355
355
|
"commitTokenThreshold",
|
|
356
356
|
"commitTokenThresholdRatio",
|
|
357
|
+
"commitRetentionMode",
|
|
357
358
|
"commitKeepRecentCount",
|
|
358
359
|
"bypassSessionPatterns",
|
|
359
360
|
"ingestReplyAssist",
|
|
@@ -396,6 +397,12 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
396
397
|
? cfg.apiKey
|
|
397
398
|
: getEnv("OPENVIKING_API_KEY") || undefined;
|
|
398
399
|
const captureMode = cfg.captureMode;
|
|
400
|
+
const commitRetentionMode = cfg.commitRetentionMode;
|
|
401
|
+
if (commitRetentionMode !== undefined &&
|
|
402
|
+
commitRetentionMode !== "message_count" &&
|
|
403
|
+
commitRetentionMode !== "turn_budget") {
|
|
404
|
+
throw new Error('openviking commitRetentionMode must be "message_count" or "turn_budget"');
|
|
405
|
+
}
|
|
399
406
|
if (typeof captureMode !== "undefined" &&
|
|
400
407
|
captureMode !== "semantic" &&
|
|
401
408
|
captureMode !== "keyword") {
|
|
@@ -437,6 +444,7 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
437
444
|
recallMaxInjectedChars,
|
|
438
445
|
recallTokenBudget: recallMaxInjectedChars,
|
|
439
446
|
commitTokenThresholdRatio: Math.max(0, Math.min(1, toNumber(cfg.commitTokenThresholdRatio, DEFAULT_COMMIT_TOKEN_THRESHOLD_RATIO))),
|
|
447
|
+
commitRetentionMode: commitRetentionMode ?? "message_count",
|
|
440
448
|
commitKeepRecentCount: Math.max(0, Math.min(1_000, Math.floor(toNumber(cfg.commitKeepRecentCount, DEFAULT_COMMIT_KEEP_RECENT_COUNT)))),
|
|
441
449
|
bypassSessionPatterns: toStringArray(cfg.bypassSessionPatterns, toStringArray(cfg.ingestReplyAssistIgnoreSessionPatterns, DEFAULT_BYPASS_SESSION_PATTERNS)),
|
|
442
450
|
emitStandardDiagnostics: typeof cfg.emitStandardDiagnostics === "boolean"
|
|
@@ -612,12 +620,17 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
612
620
|
advanced: true,
|
|
613
621
|
help: "Auto-commit triggers once estimated pending tokens reach this fraction (0-1) of the model context window (e.g. 0.5 = 50%). Set to 0 to commit every turn.",
|
|
614
622
|
},
|
|
623
|
+
commitRetentionMode: {
|
|
624
|
+
label: "Commit Retention Mode",
|
|
625
|
+
advanced: true,
|
|
626
|
+
help: "Auto-commit only: message_count (default) keeps recent messages; turn_budget uses the server's turn-aware defaults. Manual commit and compact still archive everything.",
|
|
627
|
+
},
|
|
615
628
|
commitKeepRecentCount: {
|
|
616
629
|
label: "Commit Keep Recent Count",
|
|
617
630
|
placeholder: String(DEFAULT_COMMIT_KEEP_RECENT_COUNT),
|
|
618
631
|
advanced: true,
|
|
619
632
|
help: "Number of most-recent messages to keep live after an afterTurn commit. " +
|
|
620
|
-
"Forwarded as keep_recent_count to the server. Compact path always uses 0.",
|
|
633
|
+
"Forwarded as keep_recent_count to the server in message_count mode; ignored in turn_budget mode. Compact path always uses 0.",
|
|
621
634
|
},
|
|
622
635
|
emitStandardDiagnostics: {
|
|
623
636
|
label: "Standard diagnostics (diag JSON lines)",
|
package/dist/context-engine.js
CHANGED
|
@@ -214,6 +214,7 @@ export function createMemoryOpenVikingContextEngine(params) {
|
|
|
214
214
|
sessionId: assembleParams.sessionId,
|
|
215
215
|
sessionKey: resolveSessionKey(assembleParams),
|
|
216
216
|
messages: assembleParams.messages,
|
|
217
|
+
prompt: assembleParams.prompt,
|
|
217
218
|
tokenBudget,
|
|
218
219
|
runtimeContext: assembleParams.runtimeContext,
|
|
219
220
|
isMainAssemble,
|
|
@@ -157,6 +157,7 @@ export async function commitOpenVikingSession({ sessionId, sessionKey, getClient
|
|
|
157
157
|
const commitResult = await client.commitSession(ovId, {
|
|
158
158
|
wait: true,
|
|
159
159
|
keepRecentCount: 0,
|
|
160
|
+
resetContext: true,
|
|
160
161
|
});
|
|
161
162
|
const memCount = totalExtractedMemories(commitResult.memories_extracted);
|
|
162
163
|
if (commitResult.status === "failed") {
|
|
@@ -195,7 +196,73 @@ function isSessionNotFoundError(err) {
|
|
|
195
196
|
const errorMessage = String(err);
|
|
196
197
|
return errorMessage.includes("[NOT_FOUND]") && errorMessage.includes("Session not found");
|
|
197
198
|
}
|
|
198
|
-
|
|
199
|
+
async function recallForAssemble(params, recallQuery) {
|
|
200
|
+
const { sessionId, sessionKey, cfg, getClient, resolveAgentId, queryConfigStore, logger, traceRecorder } = params;
|
|
201
|
+
const ovSessionId = openClawSessionToOvStorageId(sessionId, sessionKey);
|
|
202
|
+
const sender = extractRuntimeSenderId(params.runtimeContext);
|
|
203
|
+
const client = await getClient();
|
|
204
|
+
const routingRef = sessionId ?? sessionKey ?? ovSessionId;
|
|
205
|
+
const agentId = resolveAgentId(routingRef, sessionKey, ovSessionId);
|
|
206
|
+
const actorPeerId = resolveOpenVikingActorPeerId({
|
|
207
|
+
peerRole: cfg.peer_role ?? "none",
|
|
208
|
+
senderPeerId: sanitizeOpenVikingPeerId(sender.senderId),
|
|
209
|
+
assistantPeerId: agentId,
|
|
210
|
+
});
|
|
211
|
+
const queryConfig = await queryConfigStore?.getEffective({
|
|
212
|
+
agentId,
|
|
213
|
+
sessionId,
|
|
214
|
+
sessionKey,
|
|
215
|
+
ovSessionId,
|
|
216
|
+
});
|
|
217
|
+
return buildAutoRecallContext({
|
|
218
|
+
cfg,
|
|
219
|
+
queryConfig,
|
|
220
|
+
client,
|
|
221
|
+
agentId,
|
|
222
|
+
actorPeerId,
|
|
223
|
+
queryText: recallQuery.query,
|
|
224
|
+
logger,
|
|
225
|
+
verbose: (message) => logger.info(message),
|
|
226
|
+
traceRecorder: traceRecorder,
|
|
227
|
+
sessionId,
|
|
228
|
+
sessionKey,
|
|
229
|
+
ovSessionId,
|
|
230
|
+
queryTruncated: recallQuery.truncated,
|
|
231
|
+
rawUserTextPreview: recallQuery.query,
|
|
232
|
+
// System additions are transient; the next turn must be able to recall the same URI.
|
|
233
|
+
dedupTurns: params.isMainAssemble ? 0 : undefined,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
export async function assembleOpenVikingSession(params) {
|
|
237
|
+
const assembled = await assembleSessionContext(params);
|
|
238
|
+
// Current OpenClaw supplies the pending turn separately from history. Keep
|
|
239
|
+
// recalled context out of persisted messages and let the host own that turn.
|
|
240
|
+
if (!params.isMainAssemble || !params.cfg.autoRecall || !params.prompt ||
|
|
241
|
+
params.isBypassedSession(params)) {
|
|
242
|
+
return assembled;
|
|
243
|
+
}
|
|
244
|
+
const query = prepareRecallQuery(params.prompt);
|
|
245
|
+
if (query.query.length < 5)
|
|
246
|
+
return assembled;
|
|
247
|
+
try {
|
|
248
|
+
const recall = await recallForAssemble(params, query);
|
|
249
|
+
if (!recall.block)
|
|
250
|
+
return assembled;
|
|
251
|
+
const systemPromptAddition = [assembled.systemPromptAddition, recall.block].filter(Boolean).join("\n\n");
|
|
252
|
+
const estimatedTokens = assembled.estimatedTokens
|
|
253
|
+
+ estimateTextTokens(systemPromptAddition)
|
|
254
|
+
- estimateTextTokens(assembled.systemPromptAddition ?? "");
|
|
255
|
+
if (estimatedTokens > params.tokenBudget)
|
|
256
|
+
return assembled;
|
|
257
|
+
return { ...assembled, systemPromptAddition, estimatedTokens };
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
params.logger.warn?.(`openviking: auto-recall failed: ${String(err)}`);
|
|
261
|
+
return assembled;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async function assembleSessionContext(params) {
|
|
265
|
+
const { sessionId, sessionKey, messages, tokenBudget, runtimeContext, isMainAssemble, cfg, getClient, logger, resolveAgentId, rememberSessionAgentId, isBypassedSession, diag, roughEstimate, messageDigest, extractAgentMessageText, hasAutoRecallBlock, prependRecallToLatestUserMessage, } = params;
|
|
199
266
|
const latestMessage = messages.at(-1);
|
|
200
267
|
const isTransformContextAssemble = !isMainAssemble;
|
|
201
268
|
// Tool-loop calls do no recall work on non-user tails; avoid scanning the full history.
|
|
@@ -239,36 +306,7 @@ export async function assembleOpenVikingSession({ sessionId, sessionKey, message
|
|
|
239
306
|
`chars=${recallQuery.originalChars}->${recallQuery.finalChars})`);
|
|
240
307
|
}
|
|
241
308
|
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
|
-
});
|
|
309
|
+
const recall = await recallForAssemble(params, recallQuery);
|
|
272
310
|
if (!recall.block) {
|
|
273
311
|
return assemblePassthrough({
|
|
274
312
|
diag,
|
|
@@ -619,7 +657,9 @@ export async function afterTurnOpenVikingSession({ throwOnError = false, session
|
|
|
619
657
|
}
|
|
620
658
|
const commitResult = await client.commitSession(ovSessionId, {
|
|
621
659
|
wait: false,
|
|
622
|
-
|
|
660
|
+
...(cfg.commitRetentionMode === "turn_budget"
|
|
661
|
+
? { retentionMode: "turn_budget" }
|
|
662
|
+
: { keepRecentCount: cfg.commitKeepRecentCount }),
|
|
623
663
|
});
|
|
624
664
|
logger.info(`openviking: committed session=${ovSessionId}, ` +
|
|
625
665
|
`status=${commitResult.status}, archived=${commitResult.archived ?? false}, ` +
|
package/openclaw.plugin.json
CHANGED
|
@@ -204,11 +204,16 @@
|
|
|
204
204
|
"advanced": true,
|
|
205
205
|
"help": "Auto-commit triggers once estimated pending tokens reach this fraction (0-1) of the model context window (e.g. 0.5 = 50%). Set to 0 to commit every turn."
|
|
206
206
|
},
|
|
207
|
+
"commitRetentionMode": {
|
|
208
|
+
"label": "Commit Retention Mode",
|
|
209
|
+
"advanced": true,
|
|
210
|
+
"help": "Auto-commit only: message_count (default) keeps recent messages; turn_budget uses the server's turn-aware defaults. Manual commit and compact still archive everything."
|
|
211
|
+
},
|
|
207
212
|
"commitKeepRecentCount": {
|
|
208
213
|
"label": "Commit Keep Recent Count",
|
|
209
214
|
"placeholder": "10",
|
|
210
215
|
"advanced": true,
|
|
211
|
-
"help": "
|
|
216
|
+
"help": "Number of most-recent messages kept live after an afterTurn commit in message_count mode; ignored in turn_budget mode. Compact path always uses 0."
|
|
212
217
|
},
|
|
213
218
|
"emitStandardDiagnostics": {
|
|
214
219
|
"label": "Standard diagnostics (diag JSON lines)",
|
|
@@ -405,6 +410,10 @@
|
|
|
405
410
|
"minimum": 0,
|
|
406
411
|
"maximum": 1
|
|
407
412
|
},
|
|
413
|
+
"commitRetentionMode": {
|
|
414
|
+
"type": "string",
|
|
415
|
+
"enum": ["message_count", "turn_budget"]
|
|
416
|
+
},
|
|
408
417
|
"commitKeepRecentCount": {
|
|
409
418
|
"type": "number",
|
|
410
419
|
"minimum": 0,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openviking/openclaw-plugin",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.13",
|
|
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",
|
|
@@ -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;
|
|
@@ -139,6 +140,7 @@ export type AfterTurnOpenVikingSessionParams = {
|
|
|
139
140
|
autoCapture: boolean;
|
|
140
141
|
commitTokenThresholdRatio: number;
|
|
141
142
|
commitKeepRecentCount: number;
|
|
143
|
+
commitRetentionMode?: "message_count" | "turn_budget";
|
|
142
144
|
logFindRequests: boolean;
|
|
143
145
|
peer_role?: OpenVikingPeerRole;
|
|
144
146
|
};
|
|
@@ -369,6 +371,7 @@ export async function commitOpenVikingSession({
|
|
|
369
371
|
const commitResult = await client.commitSession(ovId, {
|
|
370
372
|
wait: true,
|
|
371
373
|
keepRecentCount: 0,
|
|
374
|
+
resetContext: true,
|
|
372
375
|
});
|
|
373
376
|
const memCount = totalExtractedMemories(commitResult.memories_extracted);
|
|
374
377
|
if (commitResult.status === "failed") {
|
|
@@ -423,28 +426,98 @@ function isSessionNotFoundError(err: unknown): boolean {
|
|
|
423
426
|
return errorMessage.includes("[NOT_FOUND]") && errorMessage.includes("Session not found");
|
|
424
427
|
}
|
|
425
428
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
429
|
+
async function recallForAssemble(
|
|
430
|
+
params: AssembleOpenVikingSessionParams,
|
|
431
|
+
recallQuery: ReturnType<typeof prepareRecallQuery>,
|
|
432
|
+
) {
|
|
433
|
+
const { sessionId, sessionKey, cfg, getClient, resolveAgentId, queryConfigStore, logger, traceRecorder } = params;
|
|
434
|
+
const ovSessionId = openClawSessionToOvStorageId(sessionId, sessionKey);
|
|
435
|
+
const sender = extractRuntimeSenderId(params.runtimeContext);
|
|
436
|
+
const client = await getClient();
|
|
437
|
+
const routingRef = sessionId ?? sessionKey ?? ovSessionId;
|
|
438
|
+
const agentId = resolveAgentId(routingRef, sessionKey, ovSessionId);
|
|
439
|
+
const actorPeerId = resolveOpenVikingActorPeerId({
|
|
440
|
+
peerRole: cfg.peer_role ?? "none",
|
|
441
|
+
senderPeerId: sanitizeOpenVikingPeerId(sender.senderId),
|
|
442
|
+
assistantPeerId: agentId,
|
|
443
|
+
});
|
|
444
|
+
const queryConfig = await queryConfigStore?.getEffective({
|
|
445
|
+
agentId,
|
|
446
|
+
sessionId,
|
|
447
|
+
sessionKey,
|
|
448
|
+
ovSessionId,
|
|
449
|
+
});
|
|
450
|
+
return buildAutoRecallContext({
|
|
451
|
+
cfg,
|
|
452
|
+
queryConfig,
|
|
453
|
+
client,
|
|
454
|
+
agentId,
|
|
455
|
+
actorPeerId,
|
|
456
|
+
queryText: recallQuery.query,
|
|
457
|
+
logger,
|
|
458
|
+
verbose: (message) => logger.info(message),
|
|
459
|
+
traceRecorder: traceRecorder as never,
|
|
460
|
+
sessionId,
|
|
461
|
+
sessionKey,
|
|
462
|
+
ovSessionId,
|
|
463
|
+
queryTruncated: recallQuery.truncated,
|
|
464
|
+
rawUserTextPreview: recallQuery.query,
|
|
465
|
+
// System additions are transient; the next turn must be able to recall the same URI.
|
|
466
|
+
dedupTurns: params.isMainAssemble ? 0 : undefined,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export async function assembleOpenVikingSession(
|
|
471
|
+
params: AssembleOpenVikingSessionParams,
|
|
472
|
+
): Promise<AssembleOpenVikingSessionResult> {
|
|
473
|
+
const assembled = await assembleSessionContext(params);
|
|
474
|
+
// Current OpenClaw supplies the pending turn separately from history. Keep
|
|
475
|
+
// recalled context out of persisted messages and let the host own that turn.
|
|
476
|
+
if (
|
|
477
|
+
!params.isMainAssemble || !params.cfg.autoRecall || !params.prompt ||
|
|
478
|
+
params.isBypassedSession(params)
|
|
479
|
+
) {
|
|
480
|
+
return assembled;
|
|
481
|
+
}
|
|
482
|
+
const query = prepareRecallQuery(params.prompt);
|
|
483
|
+
if (query.query.length < 5) return assembled;
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
const recall = await recallForAssemble(params, query);
|
|
487
|
+
if (!recall.block) return assembled;
|
|
488
|
+
const systemPromptAddition = [assembled.systemPromptAddition, recall.block].filter(Boolean).join("\n\n");
|
|
489
|
+
const estimatedTokens = assembled.estimatedTokens
|
|
490
|
+
+ estimateTextTokens(systemPromptAddition)
|
|
491
|
+
- estimateTextTokens(assembled.systemPromptAddition ?? "");
|
|
492
|
+
if (estimatedTokens > params.tokenBudget) return assembled;
|
|
493
|
+
return { ...assembled, systemPromptAddition, estimatedTokens };
|
|
494
|
+
} catch (err) {
|
|
495
|
+
params.logger.warn?.(`openviking: auto-recall failed: ${String(err)}`);
|
|
496
|
+
return assembled;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function assembleSessionContext(params: AssembleOpenVikingSessionParams): Promise<AssembleOpenVikingSessionResult> {
|
|
501
|
+
const {
|
|
502
|
+
sessionId,
|
|
503
|
+
sessionKey,
|
|
504
|
+
messages,
|
|
505
|
+
tokenBudget,
|
|
506
|
+
runtimeContext,
|
|
507
|
+
isMainAssemble,
|
|
508
|
+
cfg,
|
|
509
|
+
getClient,
|
|
510
|
+
logger,
|
|
511
|
+
resolveAgentId,
|
|
512
|
+
rememberSessionAgentId,
|
|
513
|
+
isBypassedSession,
|
|
514
|
+
diag,
|
|
515
|
+
roughEstimate,
|
|
516
|
+
messageDigest,
|
|
517
|
+
extractAgentMessageText,
|
|
518
|
+
hasAutoRecallBlock,
|
|
519
|
+
prependRecallToLatestUserMessage,
|
|
520
|
+
} = params;
|
|
448
521
|
const latestMessage = messages.at(-1);
|
|
449
522
|
const isTransformContextAssemble = !isMainAssemble;
|
|
450
523
|
|
|
@@ -497,36 +570,7 @@ export async function assembleOpenVikingSession({
|
|
|
497
570
|
}
|
|
498
571
|
|
|
499
572
|
try {
|
|
500
|
-
const
|
|
501
|
-
const routingRef = sessionId ?? sessionKey ?? ovSessionId;
|
|
502
|
-
const agentId = resolveAgentId(routingRef, sessionKey, ovSessionId);
|
|
503
|
-
const actorPeerId = resolveOpenVikingActorPeerId({
|
|
504
|
-
peerRole: cfg.peer_role ?? "none",
|
|
505
|
-
senderPeerId: sanitizeOpenVikingPeerId(sender.senderId),
|
|
506
|
-
assistantPeerId: agentId,
|
|
507
|
-
});
|
|
508
|
-
const queryConfig = await queryConfigStore?.getEffective({
|
|
509
|
-
agentId,
|
|
510
|
-
sessionId,
|
|
511
|
-
sessionKey,
|
|
512
|
-
ovSessionId,
|
|
513
|
-
});
|
|
514
|
-
const recall = await buildAutoRecallContext({
|
|
515
|
-
cfg,
|
|
516
|
-
queryConfig,
|
|
517
|
-
client,
|
|
518
|
-
agentId,
|
|
519
|
-
actorPeerId,
|
|
520
|
-
queryText: recallQuery.query,
|
|
521
|
-
logger,
|
|
522
|
-
verbose: (message) => logger.info(message),
|
|
523
|
-
traceRecorder: traceRecorder as never,
|
|
524
|
-
sessionId,
|
|
525
|
-
sessionKey,
|
|
526
|
-
ovSessionId,
|
|
527
|
-
queryTruncated: recallQuery.truncated,
|
|
528
|
-
rawUserTextPreview: recallQuery.query,
|
|
529
|
-
});
|
|
573
|
+
const recall = await recallForAssemble(params, recallQuery);
|
|
530
574
|
|
|
531
575
|
if (!recall.block) {
|
|
532
576
|
return assemblePassthrough({
|
|
@@ -944,7 +988,9 @@ export async function afterTurnOpenVikingSession({
|
|
|
944
988
|
|
|
945
989
|
const commitResult = await client.commitSession(ovSessionId, {
|
|
946
990
|
wait: false,
|
|
947
|
-
|
|
991
|
+
...(cfg.commitRetentionMode === "turn_budget"
|
|
992
|
+
? { retentionMode: "turn_budget" as const }
|
|
993
|
+
: { keepRecentCount: cfg.commitKeepRecentCount }),
|
|
948
994
|
});
|
|
949
995
|
logger.info(
|
|
950
996
|
`openviking: committed session=${ovSessionId}, ` +
|