@openviking/openclaw-plugin 2026.9.9 → 2026.9.10-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/auto-recall.ts +2 -1
- package/client.ts +1 -0
- package/context-engine.ts +72 -9
- package/dist/auto-recall.js +1 -1
- package/dist/client.js +1 -0
- package/dist/context-engine.js +72 -9
- package/dist/plugin/openviking-context-engine-registration.js +1 -0
- package/dist/services/context-lifecycle-service.js +72 -32
- package/index.ts +1 -0
- package/package.json +1 -1
- package/plugin/openviking-context-engine-registration.ts +3 -0
- package/services/context-lifecycle-service.ts +100 -52
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ The agent runs install → setup → restart → verify automatically. See [INST
|
|
|
23
23
|
|
|
24
24
|
| Stage | What happens |
|
|
25
25
|
|-------|-------------|
|
|
26
|
-
| **Every turn** (`afterTurn`) | New messages are appended to an OpenViking session; commit/extraction is threshold-triggered |
|
|
26
|
+
| **Every turn** (`afterTurn` or `commitTurn`, depending on host/runner) | New messages are appended to an OpenViking session; commit/extraction is threshold-triggered |
|
|
27
27
|
| **Explicit remember** (`memory_store`) | Important long-term facts can be written and committed immediately |
|
|
28
28
|
| **On `/compact`** (`compact`) | Pending session messages are committed and extracted into long-term memories |
|
|
29
29
|
| **Before each reply** (`assemble`) | Relevant memories are auto-retrieved and injected into context |
|
|
@@ -202,6 +202,17 @@ That means OpenClaw sees "compressed history summary + archive index + active me
|
|
|
202
202
|
|
|
203
203
|
`afterTurn()` has a narrower job: append only the new turn into the OpenViking session.
|
|
204
204
|
|
|
205
|
+
Capture ownership depends on the **host** version from `api.runtime.version`:
|
|
206
|
+
|
|
207
|
+
- Before OpenClaw 2026.9.3, `afterTurn` captures messages; `commitTurn` only acknowledges the turn to avoid writing those messages twice.
|
|
208
|
+
- Starting with 2026.9.3, admitted/deferred turns are captured inside `commitTurn` before acknowledgment. Standalone runners still capture via `afterTurn`.
|
|
209
|
+
- The minimum supported OpenClaw version remains 2026.5.27. Missing/unparseable versions and ambiguous 2026.9.3 prereleases reject `commitTurn` rather than silently discarding queued messages; `afterTurn` remains available. Numeric packaging revisions such as `2026.9.3-1` use the 9.3 behavior.
|
|
210
|
+
|
|
211
|
+
Durable capture propagates client/write/commit failures to the host for retry and shares one in-flight write between concurrent deliveries of the same advancement key. Disabled capture, heartbeats, bypassed sessions, and empty turns are intentional skips. The legacy `afterTurn` callback retains best-effort error handling.
|
|
212
|
+
|
|
213
|
+
The host's durable delivery does not supply the model token budget or sender runtime context: this path uses the 128,000-token fallback and cannot attach the runtime sender ID to user messages. Message writes remain non-transactional: replay after partial writes or a process crash can duplicate messages. The in-memory advancement-key cache does not provide server-side exactly-once delivery.
|
|
214
|
+
|
|
215
|
+
|
|
205
216
|
- it slices only the newly added messages
|
|
206
217
|
- it keeps only `user` / `assistant` capture text
|
|
207
218
|
- it preserves `toolCall` / `toolResult` content in the serialized turn text
|
|
@@ -214,7 +225,7 @@ After that, the plugin checks `pending_tokens`. Once it reaches `commitTokenThre
|
|
|
214
225
|
- the current turn is not blocked waiting for extraction
|
|
215
226
|
- if `logFindRequests` is enabled, the logs include the task id and follow-up extraction detail
|
|
216
227
|
|
|
217
|
-
|
|
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.
|
|
218
229
|
|
|
219
230
|
### Explicit long-term memory writes
|
|
220
231
|
|
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
|
@@ -508,6 +508,7 @@ export class OpenVikingClient {
|
|
|
508
508
|
};
|
|
509
509
|
const body = {
|
|
510
510
|
...buildContextSearchBody(contractConfig, { sessionId: options.sessionId }),
|
|
511
|
+
...(options.dedupTurns === 0 ? { dedup_turns: 0 } : {}),
|
|
511
512
|
query,
|
|
512
513
|
...(options.contextType !== undefined ? { context_type: options.contextType } : {}),
|
|
513
514
|
...(options.detail !== undefined ? { detail: options.detail } : {}),
|
package/context-engine.ts
CHANGED
|
@@ -245,10 +245,34 @@ function validTokenBudget(raw: unknown): number | undefined {
|
|
|
245
245
|
return undefined;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
// OpenClaw 9.1/9.2 still capture via afterTurn even when commitTurn exists.
|
|
249
|
+
// 9.3 (#140024) defers admitted turns to commitTurn; standalone runners keep
|
|
250
|
+
// afterTurn. The registration API's runtime.version is the HOST version.
|
|
251
|
+
function usesDeferredTurnCapture(version: string | undefined): boolean | undefined {
|
|
252
|
+
const match = version?.trim().match(
|
|
253
|
+
/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/,
|
|
254
|
+
);
|
|
255
|
+
if (!match) return undefined;
|
|
256
|
+
const parts = match.slice(1, 4).map(Number);
|
|
257
|
+
if (!parts.every(Number.isSafeInteger)) return undefined;
|
|
258
|
+
const [year, month, day] = parts;
|
|
259
|
+
// OpenClaw uses 0.0.0 when version metadata cannot be resolved. More generally,
|
|
260
|
+
// versions outside our supported range cannot prove who owns capture.
|
|
261
|
+
if (year < 2026 || (year === 2026 && (month < 5 || (month === 5 && day < 27)))) {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
const boundary = year === 2026 && month === 9 && day === 3;
|
|
265
|
+
// The 9.3 prerelease train can straddle the host change. Numeric packaging
|
|
266
|
+
// revisions (-1, -2, ...) refer to the stable release; do not guess for betas.
|
|
267
|
+
if (boundary && match[4] && !/^\d+$/.test(match[4])) return undefined;
|
|
268
|
+
return year > 2026 || (year === 2026 && (month > 9 || (month === 9 && day >= 3)));
|
|
269
|
+
}
|
|
270
|
+
|
|
248
271
|
export function createMemoryOpenVikingContextEngine(params: {
|
|
249
272
|
id: string;
|
|
250
273
|
name: string;
|
|
251
274
|
version?: string;
|
|
275
|
+
hostVersion?: string;
|
|
252
276
|
cfg: ParsedMemoryOpenVikingConfig;
|
|
253
277
|
logger: Logger;
|
|
254
278
|
getClient: () => Promise<OpenVikingClient>;
|
|
@@ -333,6 +357,8 @@ export function createMemoryOpenVikingContextEngine(params: {
|
|
|
333
357
|
}
|
|
334
358
|
|
|
335
359
|
const committedTurnKeys = new Set<string>();
|
|
360
|
+
const inFlightTurns = new Map<string, Promise<void>>();
|
|
361
|
+
const deferredTurnCapture = usesDeferredTurnCapture(params.hostVersion);
|
|
336
362
|
|
|
337
363
|
return {
|
|
338
364
|
info: {
|
|
@@ -368,6 +394,7 @@ export function createMemoryOpenVikingContextEngine(params: {
|
|
|
368
394
|
sessionId: assembleParams.sessionId,
|
|
369
395
|
sessionKey: resolveSessionKey(assembleParams),
|
|
370
396
|
messages: assembleParams.messages,
|
|
397
|
+
prompt: assembleParams.prompt,
|
|
371
398
|
tokenBudget,
|
|
372
399
|
runtimeContext: assembleParams.runtimeContext,
|
|
373
400
|
isMainAssemble,
|
|
@@ -388,20 +415,56 @@ export function createMemoryOpenVikingContextEngine(params: {
|
|
|
388
415
|
});
|
|
389
416
|
},
|
|
390
417
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
418
|
+
async commitTurn(commitParams): Promise<{ status: "committed" | "duplicate" }> {
|
|
419
|
+
const { advancementKey, sessionId } = commitParams;
|
|
420
|
+
if (deferredTurnCapture === undefined) {
|
|
421
|
+
// ACKing with an unknown capture owner can discard the only copy in the
|
|
422
|
+
// host outbox. Reject instead; legacy afterTurn remains available.
|
|
423
|
+
throw new Error(`openviking: cannot select turn capture for OpenClaw version ${params.hostVersion ?? "(missing)"}`);
|
|
424
|
+
}
|
|
395
425
|
if (committedTurnKeys.has(advancementKey)) {
|
|
396
426
|
diag("commitTurn_duplicate", sessionId, { advancementKey });
|
|
397
427
|
return { status: "duplicate" };
|
|
398
428
|
}
|
|
399
|
-
|
|
400
|
-
if (
|
|
401
|
-
|
|
429
|
+
const pending = inFlightTurns.get(advancementKey);
|
|
430
|
+
if (pending) {
|
|
431
|
+
await pending;
|
|
432
|
+
return { status: "duplicate" };
|
|
433
|
+
}
|
|
434
|
+
// Publish the promise before capture begins so concurrent deliveries share
|
|
435
|
+
// both success and failure. Completion keys are intentionally process-local.
|
|
436
|
+
const capture = Promise.resolve().then(async () => {
|
|
437
|
+
if (deferredTurnCapture) {
|
|
438
|
+
await afterTurnOpenVikingSession({
|
|
439
|
+
sessionId,
|
|
440
|
+
sessionKey: resolveSessionKey(commitParams),
|
|
441
|
+
messages: commitParams.messages,
|
|
442
|
+
prePromptMessageCount: 0, // Host already supplies the closed-turn range.
|
|
443
|
+
isHeartbeat: commitParams.isHeartbeat,
|
|
444
|
+
tokenBudget: 128_000, // Durable delivery does not carry a token budget.
|
|
445
|
+
throwOnError: true,
|
|
446
|
+
cfg,
|
|
447
|
+
getClient,
|
|
448
|
+
logger,
|
|
449
|
+
resolveAgentId,
|
|
450
|
+
rememberSessionAgentId,
|
|
451
|
+
isBypassedSession,
|
|
452
|
+
diag,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
committedTurnKeys.add(advancementKey);
|
|
456
|
+
if (committedTurnKeys.size > 1024) {
|
|
457
|
+
committedTurnKeys.delete(committedTurnKeys.values().next().value as string);
|
|
458
|
+
}
|
|
459
|
+
diag("commitTurn", sessionId, { advancementKey, deferredTurnCapture });
|
|
460
|
+
});
|
|
461
|
+
inFlightTurns.set(advancementKey, capture);
|
|
462
|
+
try {
|
|
463
|
+
await capture;
|
|
464
|
+
return { status: "committed" };
|
|
465
|
+
} finally {
|
|
466
|
+
inFlightTurns.delete(advancementKey);
|
|
402
467
|
}
|
|
403
|
-
diag("commitTurn", sessionId, { advancementKey });
|
|
404
|
-
return { status: "committed" };
|
|
405
468
|
},
|
|
406
469
|
|
|
407
470
|
async afterTurn(afterTurnParams): Promise<void> {
|
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 } : {}),
|
package/dist/context-engine.js
CHANGED
|
@@ -121,6 +121,29 @@ function validTokenBudget(raw) {
|
|
|
121
121
|
}
|
|
122
122
|
return undefined;
|
|
123
123
|
}
|
|
124
|
+
// OpenClaw 9.1/9.2 still capture via afterTurn even when commitTurn exists.
|
|
125
|
+
// 9.3 (#140024) defers admitted turns to commitTurn; standalone runners keep
|
|
126
|
+
// afterTurn. The registration API's runtime.version is the HOST version.
|
|
127
|
+
function usesDeferredTurnCapture(version) {
|
|
128
|
+
const match = version?.trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
129
|
+
if (!match)
|
|
130
|
+
return undefined;
|
|
131
|
+
const parts = match.slice(1, 4).map(Number);
|
|
132
|
+
if (!parts.every(Number.isSafeInteger))
|
|
133
|
+
return undefined;
|
|
134
|
+
const [year, month, day] = parts;
|
|
135
|
+
// OpenClaw uses 0.0.0 when version metadata cannot be resolved. More generally,
|
|
136
|
+
// versions outside our supported range cannot prove who owns capture.
|
|
137
|
+
if (year < 2026 || (year === 2026 && (month < 5 || (month === 5 && day < 27)))) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
const boundary = year === 2026 && month === 9 && day === 3;
|
|
141
|
+
// The 9.3 prerelease train can straddle the host change. Numeric packaging
|
|
142
|
+
// revisions (-1, -2, ...) refer to the stable release; do not guess for betas.
|
|
143
|
+
if (boundary && match[4] && !/^\d+$/.test(match[4]))
|
|
144
|
+
return undefined;
|
|
145
|
+
return year > 2026 || (year === 2026 && (month > 9 || (month === 9 && day >= 3)));
|
|
146
|
+
}
|
|
124
147
|
export function createMemoryOpenVikingContextEngine(params) {
|
|
125
148
|
const { id, name, version, cfg, logger, getClient, resolveAgentId, rememberSessionAgentId, queryConfigStore, traceRecorder, } = params;
|
|
126
149
|
const diagEnabled = cfg.emitStandardDiagnostics;
|
|
@@ -161,6 +184,8 @@ export function createMemoryOpenVikingContextEngine(params) {
|
|
|
161
184
|
};
|
|
162
185
|
}
|
|
163
186
|
const committedTurnKeys = new Set();
|
|
187
|
+
const inFlightTurns = new Map();
|
|
188
|
+
const deferredTurnCapture = usesDeferredTurnCapture(params.hostVersion);
|
|
164
189
|
return {
|
|
165
190
|
info: {
|
|
166
191
|
id,
|
|
@@ -189,6 +214,7 @@ export function createMemoryOpenVikingContextEngine(params) {
|
|
|
189
214
|
sessionId: assembleParams.sessionId,
|
|
190
215
|
sessionKey: resolveSessionKey(assembleParams),
|
|
191
216
|
messages: assembleParams.messages,
|
|
217
|
+
prompt: assembleParams.prompt,
|
|
192
218
|
tokenBudget,
|
|
193
219
|
runtimeContext: assembleParams.runtimeContext,
|
|
194
220
|
isMainAssemble,
|
|
@@ -208,20 +234,57 @@ export function createMemoryOpenVikingContextEngine(params) {
|
|
|
208
234
|
prependRecallToLatestUserMessage,
|
|
209
235
|
});
|
|
210
236
|
},
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
237
|
+
async commitTurn(commitParams) {
|
|
238
|
+
const { advancementKey, sessionId } = commitParams;
|
|
239
|
+
if (deferredTurnCapture === undefined) {
|
|
240
|
+
// ACKing with an unknown capture owner can discard the only copy in the
|
|
241
|
+
// host outbox. Reject instead; legacy afterTurn remains available.
|
|
242
|
+
throw new Error(`openviking: cannot select turn capture for OpenClaw version ${params.hostVersion ?? "(missing)"}`);
|
|
243
|
+
}
|
|
215
244
|
if (committedTurnKeys.has(advancementKey)) {
|
|
216
245
|
diag("commitTurn_duplicate", sessionId, { advancementKey });
|
|
217
246
|
return { status: "duplicate" };
|
|
218
247
|
}
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
|
|
248
|
+
const pending = inFlightTurns.get(advancementKey);
|
|
249
|
+
if (pending) {
|
|
250
|
+
await pending;
|
|
251
|
+
return { status: "duplicate" };
|
|
252
|
+
}
|
|
253
|
+
// Publish the promise before capture begins so concurrent deliveries share
|
|
254
|
+
// both success and failure. Completion keys are intentionally process-local.
|
|
255
|
+
const capture = Promise.resolve().then(async () => {
|
|
256
|
+
if (deferredTurnCapture) {
|
|
257
|
+
await afterTurnOpenVikingSession({
|
|
258
|
+
sessionId,
|
|
259
|
+
sessionKey: resolveSessionKey(commitParams),
|
|
260
|
+
messages: commitParams.messages,
|
|
261
|
+
prePromptMessageCount: 0, // Host already supplies the closed-turn range.
|
|
262
|
+
isHeartbeat: commitParams.isHeartbeat,
|
|
263
|
+
tokenBudget: 128_000, // Durable delivery does not carry a token budget.
|
|
264
|
+
throwOnError: true,
|
|
265
|
+
cfg,
|
|
266
|
+
getClient,
|
|
267
|
+
logger,
|
|
268
|
+
resolveAgentId,
|
|
269
|
+
rememberSessionAgentId,
|
|
270
|
+
isBypassedSession,
|
|
271
|
+
diag,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
committedTurnKeys.add(advancementKey);
|
|
275
|
+
if (committedTurnKeys.size > 1024) {
|
|
276
|
+
committedTurnKeys.delete(committedTurnKeys.values().next().value);
|
|
277
|
+
}
|
|
278
|
+
diag("commitTurn", sessionId, { advancementKey, deferredTurnCapture });
|
|
279
|
+
});
|
|
280
|
+
inFlightTurns.set(advancementKey, capture);
|
|
281
|
+
try {
|
|
282
|
+
await capture;
|
|
283
|
+
return { status: "committed" };
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
inFlightTurns.delete(advancementKey);
|
|
222
287
|
}
|
|
223
|
-
diag("commitTurn", sessionId, { advancementKey });
|
|
224
|
-
return { status: "committed" };
|
|
225
288
|
},
|
|
226
289
|
async afterTurn(afterTurnParams) {
|
|
227
290
|
const tokenBudget = validTokenBudget(afterTurnParams.tokenBudget) ?? 128_000;
|
|
@@ -195,7 +195,73 @@ function isSessionNotFoundError(err) {
|
|
|
195
195
|
const errorMessage = String(err);
|
|
196
196
|
return errorMessage.includes("[NOT_FOUND]") && errorMessage.includes("Session not found");
|
|
197
197
|
}
|
|
198
|
-
|
|
198
|
+
async function recallForAssemble(params, recallQuery) {
|
|
199
|
+
const { sessionId, sessionKey, cfg, getClient, resolveAgentId, queryConfigStore, logger, traceRecorder } = params;
|
|
200
|
+
const ovSessionId = openClawSessionToOvStorageId(sessionId, sessionKey);
|
|
201
|
+
const sender = extractRuntimeSenderId(params.runtimeContext);
|
|
202
|
+
const client = await getClient();
|
|
203
|
+
const routingRef = sessionId ?? sessionKey ?? ovSessionId;
|
|
204
|
+
const agentId = resolveAgentId(routingRef, sessionKey, ovSessionId);
|
|
205
|
+
const actorPeerId = resolveOpenVikingActorPeerId({
|
|
206
|
+
peerRole: cfg.peer_role ?? "none",
|
|
207
|
+
senderPeerId: sanitizeOpenVikingPeerId(sender.senderId),
|
|
208
|
+
assistantPeerId: agentId,
|
|
209
|
+
});
|
|
210
|
+
const queryConfig = await queryConfigStore?.getEffective({
|
|
211
|
+
agentId,
|
|
212
|
+
sessionId,
|
|
213
|
+
sessionKey,
|
|
214
|
+
ovSessionId,
|
|
215
|
+
});
|
|
216
|
+
return buildAutoRecallContext({
|
|
217
|
+
cfg,
|
|
218
|
+
queryConfig,
|
|
219
|
+
client,
|
|
220
|
+
agentId,
|
|
221
|
+
actorPeerId,
|
|
222
|
+
queryText: recallQuery.query,
|
|
223
|
+
logger,
|
|
224
|
+
verbose: (message) => logger.info(message),
|
|
225
|
+
traceRecorder: traceRecorder,
|
|
226
|
+
sessionId,
|
|
227
|
+
sessionKey,
|
|
228
|
+
ovSessionId,
|
|
229
|
+
queryTruncated: recallQuery.truncated,
|
|
230
|
+
rawUserTextPreview: recallQuery.query,
|
|
231
|
+
// System additions are transient; the next turn must be able to recall the same URI.
|
|
232
|
+
dedupTurns: params.isMainAssemble ? 0 : undefined,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
export async function assembleOpenVikingSession(params) {
|
|
236
|
+
const assembled = await assembleSessionContext(params);
|
|
237
|
+
// Current OpenClaw supplies the pending turn separately from history. Keep
|
|
238
|
+
// recalled context out of persisted messages and let the host own that turn.
|
|
239
|
+
if (!params.isMainAssemble || !params.cfg.autoRecall || !params.prompt ||
|
|
240
|
+
params.isBypassedSession(params)) {
|
|
241
|
+
return assembled;
|
|
242
|
+
}
|
|
243
|
+
const query = prepareRecallQuery(params.prompt);
|
|
244
|
+
if (query.query.length < 5)
|
|
245
|
+
return assembled;
|
|
246
|
+
try {
|
|
247
|
+
const recall = await recallForAssemble(params, query);
|
|
248
|
+
if (!recall.block)
|
|
249
|
+
return assembled;
|
|
250
|
+
const systemPromptAddition = [assembled.systemPromptAddition, recall.block].filter(Boolean).join("\n\n");
|
|
251
|
+
const estimatedTokens = assembled.estimatedTokens
|
|
252
|
+
+ estimateTextTokens(systemPromptAddition)
|
|
253
|
+
- estimateTextTokens(assembled.systemPromptAddition ?? "");
|
|
254
|
+
if (estimatedTokens > params.tokenBudget)
|
|
255
|
+
return assembled;
|
|
256
|
+
return { ...assembled, systemPromptAddition, estimatedTokens };
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
params.logger.warn?.(`openviking: auto-recall failed: ${String(err)}`);
|
|
260
|
+
return assembled;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async function assembleSessionContext(params) {
|
|
264
|
+
const { sessionId, sessionKey, messages, tokenBudget, runtimeContext, isMainAssemble, cfg, getClient, logger, resolveAgentId, rememberSessionAgentId, isBypassedSession, diag, roughEstimate, messageDigest, extractAgentMessageText, hasAutoRecallBlock, prependRecallToLatestUserMessage, } = params;
|
|
199
265
|
const latestMessage = messages.at(-1);
|
|
200
266
|
const isTransformContextAssemble = !isMainAssemble;
|
|
201
267
|
// Tool-loop calls do no recall work on non-user tails; avoid scanning the full history.
|
|
@@ -239,36 +305,7 @@ export async function assembleOpenVikingSession({ sessionId, sessionKey, message
|
|
|
239
305
|
`chars=${recallQuery.originalChars}->${recallQuery.finalChars})`);
|
|
240
306
|
}
|
|
241
307
|
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
|
-
});
|
|
308
|
+
const recall = await recallForAssemble(params, recallQuery);
|
|
272
309
|
if (!recall.block) {
|
|
273
310
|
return assemblePassthrough({
|
|
274
311
|
diag,
|
|
@@ -501,7 +538,7 @@ function messageDigest(messages, maxCharsPerMsg = 2000) {
|
|
|
501
538
|
};
|
|
502
539
|
});
|
|
503
540
|
}
|
|
504
|
-
export async function afterTurnOpenVikingSession({ sessionId, sessionKey, messages: rawMessages, prePromptMessageCount, isHeartbeat, tokenBudget, runtimeContext, cfg, getClient, logger, resolveAgentId, rememberSessionAgentId, isBypassedSession, diag, }) {
|
|
541
|
+
export async function afterTurnOpenVikingSession({ throwOnError = false, sessionId, sessionKey, messages: rawMessages, prePromptMessageCount, isHeartbeat, tokenBudget, runtimeContext, cfg, getClient, logger, resolveAgentId, rememberSessionAgentId, isBypassedSession, diag, }) {
|
|
505
542
|
if (!cfg.autoCapture) {
|
|
506
543
|
return;
|
|
507
544
|
}
|
|
@@ -650,6 +687,9 @@ export async function afterTurnOpenVikingSession({ sessionId, sessionKey, messag
|
|
|
650
687
|
senderIdFound: sender.found,
|
|
651
688
|
senderId: sender.senderId ?? null,
|
|
652
689
|
});
|
|
690
|
+
if (throwOnError) {
|
|
691
|
+
throw err;
|
|
692
|
+
}
|
|
653
693
|
}
|
|
654
694
|
}
|
|
655
695
|
function validTokenCount(value) {
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openviking/openclaw-plugin",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.10-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",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type OpenVikingContextEngineRegistrationApi = {
|
|
2
|
+
runtime?: { version?: string };
|
|
2
3
|
registerContextEngine?: (id: string, factory: () => unknown) => void;
|
|
3
4
|
};
|
|
4
5
|
|
|
@@ -22,6 +23,7 @@ export type OpenVikingContextEngineCreateParams<
|
|
|
22
23
|
id: string;
|
|
23
24
|
name: string;
|
|
24
25
|
version: string;
|
|
26
|
+
hostVersion?: string;
|
|
25
27
|
cfg: TCfg;
|
|
26
28
|
logger: TLogger;
|
|
27
29
|
getClient: () => Promise<TClient>;
|
|
@@ -80,6 +82,7 @@ export function registerOpenVikingContextEngine<
|
|
|
80
82
|
id: deps.plugin.id,
|
|
81
83
|
name: deps.plugin.name,
|
|
82
84
|
version: deps.version,
|
|
85
|
+
hostVersion: deps.api.runtime?.version,
|
|
83
86
|
cfg: deps.cfg,
|
|
84
87
|
logger: deps.logger,
|
|
85
88
|
getClient: deps.getClient,
|
|
@@ -72,6 +72,7 @@ export type AssembleOpenVikingSessionParams = {
|
|
|
72
72
|
sessionId: string;
|
|
73
73
|
sessionKey?: string;
|
|
74
74
|
messages: AgentMessage[];
|
|
75
|
+
prompt?: string;
|
|
75
76
|
tokenBudget: number;
|
|
76
77
|
runtimeContext?: Record<string, unknown>;
|
|
77
78
|
isMainAssemble: boolean;
|
|
@@ -125,6 +126,8 @@ export type CompactOpenVikingSessionParams = {
|
|
|
125
126
|
type AfterTurnClient = Pick<OpenVikingClient, "addSessionMessage" | "getSession" | "commitSession" | "getTask">;
|
|
126
127
|
|
|
127
128
|
export type AfterTurnOpenVikingSessionParams = {
|
|
129
|
+
/** Durable delivery must reject failed writes so the host retains its outbox row. */
|
|
130
|
+
throwOnError?: boolean;
|
|
128
131
|
sessionId: string;
|
|
129
132
|
sessionKey?: string;
|
|
130
133
|
messages?: AgentMessage[];
|
|
@@ -421,28 +424,98 @@ function isSessionNotFoundError(err: unknown): boolean {
|
|
|
421
424
|
return errorMessage.includes("[NOT_FOUND]") && errorMessage.includes("Session not found");
|
|
422
425
|
}
|
|
423
426
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
427
|
+
async function recallForAssemble(
|
|
428
|
+
params: AssembleOpenVikingSessionParams,
|
|
429
|
+
recallQuery: ReturnType<typeof prepareRecallQuery>,
|
|
430
|
+
) {
|
|
431
|
+
const { sessionId, sessionKey, cfg, getClient, resolveAgentId, queryConfigStore, logger, traceRecorder } = params;
|
|
432
|
+
const ovSessionId = openClawSessionToOvStorageId(sessionId, sessionKey);
|
|
433
|
+
const sender = extractRuntimeSenderId(params.runtimeContext);
|
|
434
|
+
const client = await getClient();
|
|
435
|
+
const routingRef = sessionId ?? sessionKey ?? ovSessionId;
|
|
436
|
+
const agentId = resolveAgentId(routingRef, sessionKey, ovSessionId);
|
|
437
|
+
const actorPeerId = resolveOpenVikingActorPeerId({
|
|
438
|
+
peerRole: cfg.peer_role ?? "none",
|
|
439
|
+
senderPeerId: sanitizeOpenVikingPeerId(sender.senderId),
|
|
440
|
+
assistantPeerId: agentId,
|
|
441
|
+
});
|
|
442
|
+
const queryConfig = await queryConfigStore?.getEffective({
|
|
443
|
+
agentId,
|
|
444
|
+
sessionId,
|
|
445
|
+
sessionKey,
|
|
446
|
+
ovSessionId,
|
|
447
|
+
});
|
|
448
|
+
return buildAutoRecallContext({
|
|
449
|
+
cfg,
|
|
450
|
+
queryConfig,
|
|
451
|
+
client,
|
|
452
|
+
agentId,
|
|
453
|
+
actorPeerId,
|
|
454
|
+
queryText: recallQuery.query,
|
|
455
|
+
logger,
|
|
456
|
+
verbose: (message) => logger.info(message),
|
|
457
|
+
traceRecorder: traceRecorder as never,
|
|
458
|
+
sessionId,
|
|
459
|
+
sessionKey,
|
|
460
|
+
ovSessionId,
|
|
461
|
+
queryTruncated: recallQuery.truncated,
|
|
462
|
+
rawUserTextPreview: recallQuery.query,
|
|
463
|
+
// System additions are transient; the next turn must be able to recall the same URI.
|
|
464
|
+
dedupTurns: params.isMainAssemble ? 0 : undefined,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export async function assembleOpenVikingSession(
|
|
469
|
+
params: AssembleOpenVikingSessionParams,
|
|
470
|
+
): Promise<AssembleOpenVikingSessionResult> {
|
|
471
|
+
const assembled = await assembleSessionContext(params);
|
|
472
|
+
// Current OpenClaw supplies the pending turn separately from history. Keep
|
|
473
|
+
// recalled context out of persisted messages and let the host own that turn.
|
|
474
|
+
if (
|
|
475
|
+
!params.isMainAssemble || !params.cfg.autoRecall || !params.prompt ||
|
|
476
|
+
params.isBypassedSession(params)
|
|
477
|
+
) {
|
|
478
|
+
return assembled;
|
|
479
|
+
}
|
|
480
|
+
const query = prepareRecallQuery(params.prompt);
|
|
481
|
+
if (query.query.length < 5) return assembled;
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
const recall = await recallForAssemble(params, query);
|
|
485
|
+
if (!recall.block) return assembled;
|
|
486
|
+
const systemPromptAddition = [assembled.systemPromptAddition, recall.block].filter(Boolean).join("\n\n");
|
|
487
|
+
const estimatedTokens = assembled.estimatedTokens
|
|
488
|
+
+ estimateTextTokens(systemPromptAddition)
|
|
489
|
+
- estimateTextTokens(assembled.systemPromptAddition ?? "");
|
|
490
|
+
if (estimatedTokens > params.tokenBudget) return assembled;
|
|
491
|
+
return { ...assembled, systemPromptAddition, estimatedTokens };
|
|
492
|
+
} catch (err) {
|
|
493
|
+
params.logger.warn?.(`openviking: auto-recall failed: ${String(err)}`);
|
|
494
|
+
return assembled;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function assembleSessionContext(params: AssembleOpenVikingSessionParams): Promise<AssembleOpenVikingSessionResult> {
|
|
499
|
+
const {
|
|
500
|
+
sessionId,
|
|
501
|
+
sessionKey,
|
|
502
|
+
messages,
|
|
503
|
+
tokenBudget,
|
|
504
|
+
runtimeContext,
|
|
505
|
+
isMainAssemble,
|
|
506
|
+
cfg,
|
|
507
|
+
getClient,
|
|
508
|
+
logger,
|
|
509
|
+
resolveAgentId,
|
|
510
|
+
rememberSessionAgentId,
|
|
511
|
+
isBypassedSession,
|
|
512
|
+
diag,
|
|
513
|
+
roughEstimate,
|
|
514
|
+
messageDigest,
|
|
515
|
+
extractAgentMessageText,
|
|
516
|
+
hasAutoRecallBlock,
|
|
517
|
+
prependRecallToLatestUserMessage,
|
|
518
|
+
} = params;
|
|
446
519
|
const latestMessage = messages.at(-1);
|
|
447
520
|
const isTransformContextAssemble = !isMainAssemble;
|
|
448
521
|
|
|
@@ -495,36 +568,7 @@ export async function assembleOpenVikingSession({
|
|
|
495
568
|
}
|
|
496
569
|
|
|
497
570
|
try {
|
|
498
|
-
const
|
|
499
|
-
const routingRef = sessionId ?? sessionKey ?? ovSessionId;
|
|
500
|
-
const agentId = resolveAgentId(routingRef, sessionKey, ovSessionId);
|
|
501
|
-
const actorPeerId = resolveOpenVikingActorPeerId({
|
|
502
|
-
peerRole: cfg.peer_role ?? "none",
|
|
503
|
-
senderPeerId: sanitizeOpenVikingPeerId(sender.senderId),
|
|
504
|
-
assistantPeerId: agentId,
|
|
505
|
-
});
|
|
506
|
-
const queryConfig = await queryConfigStore?.getEffective({
|
|
507
|
-
agentId,
|
|
508
|
-
sessionId,
|
|
509
|
-
sessionKey,
|
|
510
|
-
ovSessionId,
|
|
511
|
-
});
|
|
512
|
-
const recall = await buildAutoRecallContext({
|
|
513
|
-
cfg,
|
|
514
|
-
queryConfig,
|
|
515
|
-
client,
|
|
516
|
-
agentId,
|
|
517
|
-
actorPeerId,
|
|
518
|
-
queryText: recallQuery.query,
|
|
519
|
-
logger,
|
|
520
|
-
verbose: (message) => logger.info(message),
|
|
521
|
-
traceRecorder: traceRecorder as never,
|
|
522
|
-
sessionId,
|
|
523
|
-
sessionKey,
|
|
524
|
-
ovSessionId,
|
|
525
|
-
queryTruncated: recallQuery.truncated,
|
|
526
|
-
rawUserTextPreview: recallQuery.query,
|
|
527
|
-
});
|
|
571
|
+
const recall = await recallForAssemble(params, recallQuery);
|
|
528
572
|
|
|
529
573
|
if (!recall.block) {
|
|
530
574
|
return assemblePassthrough({
|
|
@@ -786,6 +830,7 @@ function messageDigest(messages: AgentMessage[], maxCharsPerMsg = 2000): Array<{
|
|
|
786
830
|
}
|
|
787
831
|
|
|
788
832
|
export async function afterTurnOpenVikingSession({
|
|
833
|
+
throwOnError = false,
|
|
789
834
|
sessionId,
|
|
790
835
|
sessionKey,
|
|
791
836
|
messages: rawMessages,
|
|
@@ -976,6 +1021,9 @@ export async function afterTurnOpenVikingSession({
|
|
|
976
1021
|
senderIdFound: sender.found,
|
|
977
1022
|
senderId: sender.senderId ?? null,
|
|
978
1023
|
});
|
|
1024
|
+
if (throwOnError) {
|
|
1025
|
+
throw err;
|
|
1026
|
+
}
|
|
979
1027
|
}
|
|
980
1028
|
}
|
|
981
1029
|
|