@openviking/openclaw-plugin 2026.6.17-dev.2 → 2026.7.11

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/dist/config.js CHANGED
@@ -11,7 +11,7 @@ const DEFAULT_RECALL_SCORE_THRESHOLD = 0.15;
11
11
  const DEFAULT_RECALL_MAX_CONTENT_CHARS = 5000;
12
12
  const DEFAULT_RECALL_PREFER_ABSTRACT = false;
13
13
  const DEFAULT_RECALL_MAX_INJECTED_CHARS = 4000;
14
- const DEFAULT_COMMIT_TOKEN_THRESHOLD = 20000;
14
+ const DEFAULT_COMMIT_TOKEN_THRESHOLD_RATIO = 0.5;
15
15
  const DEFAULT_COMMIT_KEEP_RECENT_COUNT = 10;
16
16
  const DEFAULT_BYPASS_SESSION_PATTERNS = [];
17
17
  const DEFAULT_EMIT_STANDARD_DIAGNOSTICS = false;
@@ -168,6 +168,23 @@ function toRecord(value) {
168
168
  ? value
169
169
  : {};
170
170
  }
171
+ function toStringRecord(value, label) {
172
+ if (value === undefined || value === null) {
173
+ return {};
174
+ }
175
+ if (typeof value !== "object" || Array.isArray(value)) {
176
+ throw new Error(`${label} must be an object`);
177
+ }
178
+ const record = value;
179
+ const out = {};
180
+ for (const [key, val] of Object.entries(record)) {
181
+ if (typeof val !== "string") {
182
+ throw new Error(`${label}.${key} must be a string`);
183
+ }
184
+ out[key] = val;
185
+ }
186
+ return out;
187
+ }
171
188
  function expandToolSelectors(value, fallback, label) {
172
189
  const entries = toStringArray(value, fallback);
173
190
  const seen = new Set();
@@ -250,6 +267,7 @@ export const memoryOpenVikingConfigSchema = {
250
267
  "peer_prefix",
251
268
  "serverAuthMode",
252
269
  "apiKey",
270
+ "headers",
253
271
  "accountId",
254
272
  "userId",
255
273
  "targetUri",
@@ -267,6 +285,7 @@ export const memoryOpenVikingConfigSchema = {
267
285
  "recallPreferAbstract",
268
286
  "recallTokenBudget",
269
287
  "commitTokenThreshold",
288
+ "commitTokenThresholdRatio",
270
289
  "commitKeepRecentCount",
271
290
  "bypassSessionPatterns",
272
291
  "ingestReplyAssist",
@@ -324,6 +343,7 @@ export const memoryOpenVikingConfigSchema = {
324
343
  peer_role: peerRole,
325
344
  peer_prefix: peerPrefix,
326
345
  apiKey: rawApiKey ? resolveEnvVars(rawApiKey) : "",
346
+ headers: toStringRecord(cfg.headers, "openviking config headers"),
327
347
  accountId,
328
348
  userId,
329
349
  targetUri: typeof cfg.targetUri === "string" ? cfg.targetUri : DEFAULT_TARGET_URI,
@@ -342,7 +362,7 @@ export const memoryOpenVikingConfigSchema = {
342
362
  : DEFAULT_RECALL_PREFER_ABSTRACT,
343
363
  recallMaxInjectedChars,
344
364
  recallTokenBudget: recallMaxInjectedChars,
345
- commitTokenThreshold: Math.max(0, Math.min(100_000, Math.floor(toNumber(cfg.commitTokenThreshold, DEFAULT_COMMIT_TOKEN_THRESHOLD)))),
365
+ commitTokenThresholdRatio: Math.max(0, Math.min(1, toNumber(cfg.commitTokenThresholdRatio, DEFAULT_COMMIT_TOKEN_THRESHOLD_RATIO))),
346
366
  commitKeepRecentCount: Math.max(0, Math.min(1_000, Math.floor(toNumber(cfg.commitKeepRecentCount, DEFAULT_COMMIT_KEEP_RECENT_COUNT)))),
347
367
  bypassSessionPatterns: toStringArray(cfg.bypassSessionPatterns, toStringArray(cfg.ingestReplyAssistIgnoreSessionPatterns, DEFAULT_BYPASS_SESSION_PATTERNS)),
348
368
  emitStandardDiagnostics: typeof cfg.emitStandardDiagnostics === "boolean"
@@ -405,6 +425,11 @@ export const memoryOpenVikingConfigSchema = {
405
425
  placeholder: "${OPENVIKING_API_KEY}",
406
426
  help: "Optional API key for OpenViking server",
407
427
  },
428
+ headers: {
429
+ label: "Headers",
430
+ advanced: true,
431
+ help: "Optional HTTP headers merged into every OpenViking request.",
432
+ },
408
433
  accountId: {
409
434
  label: "Account ID",
410
435
  placeholder: "(derived from API key)",
@@ -503,11 +528,11 @@ export const memoryOpenVikingConfigSchema = {
503
528
  help: "Completely bypass OpenViking for matching session keys. Use * within one segment and ** across segments.",
504
529
  advanced: true,
505
530
  },
506
- commitTokenThreshold: {
507
- label: "Commit Token Threshold",
508
- placeholder: String(DEFAULT_COMMIT_TOKEN_THRESHOLD),
531
+ commitTokenThresholdRatio: {
532
+ label: "Commit Token Threshold Ratio",
533
+ placeholder: String(DEFAULT_COMMIT_TOKEN_THRESHOLD_RATIO),
509
534
  advanced: true,
510
- help: "Minimum estimated pending tokens before auto-commit triggers. Set to 0 to commit every turn.",
535
+ 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.",
511
536
  },
512
537
  commitKeepRecentCount: {
513
538
  label: "Commit Keep Recent Count",
@@ -204,12 +204,14 @@ export function createMemoryOpenVikingContextEngine(params) {
204
204
  });
205
205
  },
206
206
  async afterTurn(afterTurnParams) {
207
+ const tokenBudget = validTokenBudget(afterTurnParams.tokenBudget) ?? 128_000;
207
208
  await afterTurnOpenVikingSession({
208
209
  sessionId: afterTurnParams.sessionId,
209
210
  sessionKey: resolveSessionKey(afterTurnParams),
210
211
  messages: afterTurnParams.messages,
211
212
  prePromptMessageCount: afterTurnParams.prePromptMessageCount,
212
213
  isHeartbeat: afterTurnParams.isHeartbeat,
214
+ tokenBudget,
213
215
  runtimeContext: afterTurnParams.runtimeContext,
214
216
  cfg,
215
217
  getClient,
@@ -1,4 +1,5 @@
1
1
  import { OpenVikingClient } from "../client.js";
2
+ import { resolveOpenVikingRequestHeaders } from "../request-headers.js";
2
3
  export function createOpenVikingClientRuntime(options) {
3
4
  const { cfg, logger } = options;
4
5
  if (cfg.logFindRequests) {
@@ -19,7 +20,12 @@ export function createOpenVikingClientRuntime(options) {
19
20
  logger.info(msg);
20
21
  }
21
22
  : undefined;
22
- const clientPromise = Promise.resolve(new OpenVikingClient(cfg.baseUrl, cfg.apiKey, cfg.peer_prefix, cfg.timeoutMs, cfg.accountId, cfg.userId, routingDebugLog, { transport: options.transport }));
23
+ const clientPromise = Promise.resolve(new OpenVikingClient(cfg.baseUrl, cfg.apiKey, cfg.peer_prefix, cfg.timeoutMs, cfg.accountId, cfg.userId, routingDebugLog, {
24
+ transport: options.transport,
25
+ headers: resolveOpenVikingRequestHeaders({
26
+ headers: cfg.headers,
27
+ }),
28
+ }));
23
29
  const getClient = () => clientPromise;
24
30
  return {
25
31
  getClient,
@@ -11,7 +11,7 @@ export function registerOpenVikingMemoryTools(deps) {
11
11
  deps.registerTool((ctx) => ({
12
12
  name: "memory_store",
13
13
  label: "Memory Store (OpenViking)",
14
- description: "Store text in OpenViking memory pipeline by writing to a session and running memory extraction.",
14
+ description: "Store text in OpenViking memory pipeline by writing to a session and running memory extraction. Use when the user explicitly asks to remember, save, or store an important long-term fact, preference, project, or decision; automatic capture is threshold/commit dependent.",
15
15
  parameters: Type.Object({
16
16
  text: Type.String({ description: "Information to store as memory source text" }),
17
17
  role: Type.Optional(Type.String({ description: "Session role, default user" })),
@@ -171,7 +171,13 @@ export class RuntimeQueryConfigStore {
171
171
  const { params, warnings } = normalizeRuntimeQueryParams(patch);
172
172
  const key = this.keyForScope(scope, ctx);
173
173
  const bucket = scope === "claw" ? this.data.claws : this.data.sessions;
174
- bucket[key] = { params, updatedAt: Date.now(), updatedBy: "command", peerId: ctx.peerId };
174
+ const previous = bucket[key];
175
+ bucket[key] = {
176
+ params: { ...(previous?.params ?? {}), ...params },
177
+ updatedAt: Date.now(),
178
+ updatedBy: "command",
179
+ peerId: ctx.peerId,
180
+ };
175
181
  this.data.updatedAt = Date.now();
176
182
  await this.persist();
177
183
  return { warnings };
@@ -0,0 +1,19 @@
1
+ export function resolveOpenVikingRequestHeaders(options = {}) {
2
+ return cleanOpenVikingRequestHeaders(options.headers);
3
+ }
4
+ export function cleanOpenVikingRequestHeaders(headers) {
5
+ if (headers === undefined || headers === null) {
6
+ return {};
7
+ }
8
+ if (typeof headers !== "object" || Array.isArray(headers)) {
9
+ throw new Error("openviking request headers must be an object");
10
+ }
11
+ const out = {};
12
+ for (const [key, value] of Object.entries(headers)) {
13
+ if (typeof value !== "string") {
14
+ throw new Error(`openviking request header ${key} must be a string`);
15
+ }
16
+ out[key] = value;
17
+ }
18
+ return out;
19
+ }
@@ -191,6 +191,10 @@ function assemblePassthrough(params) {
191
191
  });
192
192
  return { messages: liveMessages, estimatedTokens: originalTokens };
193
193
  }
194
+ function isSessionNotFoundError(err) {
195
+ const errorMessage = String(err);
196
+ return errorMessage.includes("[NOT_FOUND]") && errorMessage.includes("Session not found");
197
+ }
194
198
  export async function assembleOpenVikingSession({ sessionId, sessionKey, messages, tokenBudget, runtimeContext, isMainAssemble, cfg, getClient, logger, resolveAgentId, rememberSessionAgentId, isBypassedSession, queryConfigStore, traceRecorder, diag, roughEstimate, messageDigest, extractAgentMessageText, hasAutoRecallBlock, prependRecallToLatestUserMessage, }) {
195
199
  const ovSessionId = openClawSessionToOvStorageId(sessionId, sessionKey);
196
200
  const sender = extractRuntimeSenderId(runtimeContext);
@@ -368,6 +372,25 @@ export async function assembleOpenVikingSession({ sessionId, sessionKey, message
368
372
  };
369
373
  }
370
374
  catch (err) {
375
+ if (isSessionNotFoundError(err)) {
376
+ const errorMessage = String(err);
377
+ logger.info(`openviking: assemble skipped because OV session does not exist ` +
378
+ `(session=${ovSessionId}, tokenBudget=${tokenBudget}, agentId=${resolveAgentId(ovSessionId)})`);
379
+ return assemblePassthrough({
380
+ diag,
381
+ ovSessionId,
382
+ reason: "session_not_found",
383
+ liveMessages: messages,
384
+ originalTokens,
385
+ extra: {
386
+ error: errorMessage,
387
+ tokenBudget,
388
+ agentId: resolveAgentId(ovSessionId),
389
+ senderIdFound: sender.found,
390
+ senderId: sender.senderId ?? null,
391
+ },
392
+ });
393
+ }
371
394
  logger.warn?.(`openviking: assemble failed for session=${ovSessionId}, ` +
372
395
  `tokenBudget=${tokenBudget}, agentId=${resolveAgentId(ovSessionId)}: ${String(err)}`);
373
396
  diag("assemble_error", ovSessionId, {
@@ -421,7 +444,7 @@ function extractRuntimeAgentId(runtimeContext) {
421
444
  return typeof agentId === "string" && agentId.trim() ? agentId.trim() : undefined;
422
445
  }
423
446
  function isToolOnlyMessage(msg) {
424
- return msg.role === "user" && msg.parts.length > 0 && msg.parts.every((part) => part.type === "tool");
447
+ return msg.role === "assistant" && msg.parts.length > 0 && msg.parts.every((part) => part.type === "tool");
425
448
  }
426
449
  function coalesceConsecutiveToolMessages(messages) {
427
450
  const result = [];
@@ -435,7 +458,7 @@ function coalesceConsecutiveToolMessages(messages) {
435
458
  for (const msg of messages) {
436
459
  if (isToolOnlyMessage(msg)) {
437
460
  if (!pendingTools) {
438
- pendingTools = { role: "user", parts: [] };
461
+ pendingTools = { role: "assistant", parts: [] };
439
462
  }
440
463
  pendingTools.parts.push(...msg.parts);
441
464
  continue;
@@ -480,7 +503,7 @@ function messageDigest(messages, maxCharsPerMsg = 2000) {
480
503
  };
481
504
  });
482
505
  }
483
- export async function afterTurnOpenVikingSession({ sessionId, sessionKey, messages: rawMessages, prePromptMessageCount, isHeartbeat, runtimeContext, cfg, getClient, logger, resolveAgentId, rememberSessionAgentId, isBypassedSession, diag, }) {
506
+ export async function afterTurnOpenVikingSession({ sessionId, sessionKey, messages: rawMessages, prePromptMessageCount, isHeartbeat, tokenBudget, runtimeContext, cfg, getClient, logger, resolveAgentId, rememberSessionAgentId, isBypassedSession, diag, }) {
484
507
  if (!cfg.autoCapture) {
485
508
  return;
486
509
  }
@@ -578,11 +601,14 @@ export async function afterTurnOpenVikingSession({ sessionId, sessionKey, messag
578
601
  }
579
602
  const session = await client.getSession(ovSessionId, agentId);
580
603
  const pendingTokens = session.pending_tokens ?? 0;
581
- if (pendingTokens < cfg.commitTokenThreshold) {
604
+ const commitTokenThreshold = Math.floor(tokenBudget * cfg.commitTokenThresholdRatio);
605
+ if (pendingTokens < commitTokenThreshold) {
582
606
  diag("afterTurn_skip", ovSessionId, {
583
607
  reason: "below_threshold",
584
608
  pendingTokens,
585
- commitTokenThreshold: cfg.commitTokenThreshold,
609
+ commitTokenThreshold,
610
+ commitTokenThresholdRatio: cfg.commitTokenThresholdRatio,
611
+ tokenBudget,
586
612
  senderIdFound: sender.found,
587
613
  senderId: sender.senderId ?? null,
588
614
  });
@@ -598,7 +624,9 @@ export async function afterTurnOpenVikingSession({ sessionId, sessionKey, messag
598
624
  `task_id=${commitResult.task_id ?? "none"}, trace_id=${commitResult.trace_id ?? "none"}`);
599
625
  diag("afterTurn_commit", ovSessionId, {
600
626
  pendingTokens,
601
- commitTokenThreshold: cfg.commitTokenThreshold,
627
+ commitTokenThreshold,
628
+ commitTokenThresholdRatio: cfg.commitTokenThresholdRatio,
629
+ tokenBudget,
602
630
  status: commitResult.status,
603
631
  archived: commitResult.archived ?? false,
604
632
  taskId: commitResult.task_id ?? null,
@@ -823,7 +851,7 @@ export async function compactOpenVikingSession({ sessionId, sessionKey, tokenBud
823
851
  }
824
852
  catch (err) {
825
853
  const errorMessage = String(err);
826
- if (errorMessage.includes("[NOT_FOUND]") && errorMessage.includes("Session not found")) {
854
+ if (isSessionNotFoundError(err)) {
827
855
  logger.info(`openviking: compact skipped because OV session does not exist ` +
828
856
  `(session=${ovSessionId}, agentId=${agentId})`);
829
857
  diag("compact_result", ovSessionId, {
@@ -36,13 +36,8 @@ function looksLikeMetadataJsonBlock(content) {
36
36
  }
37
37
  return matchedKeys.size >= 3;
38
38
  }
39
- const HEARTBEAT_RE = /\bHEARTBEAT(?:\.md|_OK)\b/;
40
39
  const TOOL_PLACEHOLDER_RE = /^\s*\[tool(?::\s*|Use:\s*)[^\]]+\]\s*$/i;
41
40
  export function sanitizeUserTextForCapture(text) {
42
- // 过滤 HEARTBEAT 健康检查消息
43
- if (HEARTBEAT_RE.test(text)) {
44
- return "";
45
- }
46
41
  // Drop legacy synthetic tool placeholders before they reach memory extraction.
47
42
  if (TOOL_PLACEHOLDER_RE.test(text)) {
48
43
  return "";
@@ -344,7 +339,7 @@ export function extractNewTurnMessages(messages, startIndex) {
344
339
  : undefined);
345
340
  if (output) {
346
341
  result.push({
347
- role: "user",
342
+ role: "assistant",
348
343
  parts: [{
349
344
  type: "tool",
350
345
  toolCallId: toolCallId || undefined,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifestVersion": "1.0",
3
- "pluginVersion": "2026.6.11",
3
+ "pluginVersion": "2026.6.18",
4
4
  "plugin": {
5
5
  "id": "openviking",
6
6
  "kind": "context-engine",
@@ -20,6 +20,7 @@
20
20
  "tool-call-id.ts",
21
21
  "session-transcript-repair.ts",
22
22
  "runtime-utils.ts",
23
+ "request-headers.ts",
23
24
  "recall-trace.ts",
24
25
  "query-config.ts",
25
26
  "adapters/",
@@ -32,7 +33,8 @@
32
33
  "tsconfig.json",
33
34
  "tsconfig.build.json",
34
35
  "package.json",
35
- "openclaw.plugin.json"
36
+ "openclaw.plugin.json",
37
+ "install-manifest.json"
36
38
  ],
37
39
  "optional": [
38
40
  "package-lock.json",
@@ -51,7 +53,9 @@
51
53
  "pruneAfterBuild": true
52
54
  },
53
55
  "compatibility": {
54
- "minOpenclawVersion": "2026.4.8",
55
- "minOpenvikingVersion": "0.2.9"
56
+ "minOpenclawVersion": "2026.5.27",
57
+ "recommendedOpenclawVersion": "2026.6.6",
58
+ "minOpenvikingVersion": "0.4.1",
59
+ "recommendedOpenvikingVersion": "0.4.1"
56
60
  }
57
61
  }
@@ -98,6 +98,11 @@
98
98
  "placeholder": "${OPENVIKING_API_KEY}",
99
99
  "help": "Optional API key for OpenViking server"
100
100
  },
101
+ "headers": {
102
+ "label": "Headers",
103
+ "advanced": true,
104
+ "help": "Optional HTTP headers merged into every OpenViking request."
105
+ },
101
106
  "accountId": {
102
107
  "label": "Account ID",
103
108
  "placeholder": "(derived from API key)",
@@ -196,11 +201,11 @@
196
201
  "help": "Completely bypass OpenViking for matching session keys. Use * within one segment and ** across segments.",
197
202
  "advanced": true
198
203
  },
199
- "commitTokenThreshold": {
200
- "label": "Commit Token Threshold",
201
- "placeholder": "20000",
204
+ "commitTokenThresholdRatio": {
205
+ "label": "Commit Token Threshold Ratio",
206
+ "placeholder": "0.5",
202
207
  "advanced": true,
203
- "help": "Minimum estimated pending tokens before auto-commit triggers. Set to 0 to commit every turn."
208
+ "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."
204
209
  },
205
210
  "commitKeepRecentCount": {
206
211
  "label": "Commit Keep Recent Count",
@@ -287,6 +292,12 @@
287
292
  "apiKey": {
288
293
  "type": "string"
289
294
  },
295
+ "headers": {
296
+ "type": "object",
297
+ "additionalProperties": {
298
+ "type": "string"
299
+ }
300
+ },
290
301
  "accountId": {
291
302
  "type": "string"
292
303
  },
@@ -358,6 +369,11 @@
358
369
  "commitTokenThreshold": {
359
370
  "type": "number"
360
371
  },
372
+ "commitTokenThresholdRatio": {
373
+ "type": "number",
374
+ "minimum": 0,
375
+ "maximum": 1
376
+ },
361
377
  "commitKeepRecentCount": {
362
378
  "type": "number",
363
379
  "minimum": 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openviking/openclaw-plugin",
3
- "version": "2026.6.17-dev.2",
3
+ "version": "2026.7.11",
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",
@@ -47,7 +47,7 @@
47
47
  "test:watch": "vitest"
48
48
  },
49
49
  "engines": {
50
- "openclaw": ">=2026.4.8"
50
+ "openclaw": ">=2026.5.27"
51
51
  },
52
52
  "dependencies": {
53
53
  "@sinclair/typebox": "0.34.48",
@@ -69,12 +69,12 @@
69
69
  ],
70
70
  "setupEntry": "./dist/commands/setup.js",
71
71
  "compat": {
72
- "pluginApi": ">=2026.4.8",
73
- "minGatewayVersion": "2026.4.8"
72
+ "pluginApi": ">=2026.5.27",
73
+ "minGatewayVersion": "2026.5.27"
74
74
  },
75
75
  "build": {
76
- "openclawVersion": "2026.4.8",
77
- "pluginSdkVersion": "2026.4.8"
76
+ "openclawVersion": "2026.5.27",
77
+ "pluginSdkVersion": "2026.5.27"
78
78
  }
79
79
  },
80
80
  "devDependencies": {
@@ -1,5 +1,6 @@
1
1
  import { OpenVikingClient } from "../client.js";
2
2
  import type { HttpTransport } from "../adapters/http-transport.js";
3
+ import { resolveOpenVikingRequestHeaders } from "../request-headers.js";
3
4
 
4
5
  type Logger = {
5
6
  info: (message: string) => void;
@@ -13,6 +14,7 @@ type ClientRuntimeConfig = {
13
14
  timeoutMs: number;
14
15
  accountId?: string;
15
16
  userId?: string;
17
+ headers?: Record<string, string>;
16
18
  logFindRequests: boolean;
17
19
  };
18
20
 
@@ -61,7 +63,12 @@ export function createOpenVikingClientRuntime(options: {
61
63
  cfg.accountId,
62
64
  cfg.userId,
63
65
  routingDebugLog,
64
- { transport: options.transport },
66
+ {
67
+ transport: options.transport,
68
+ headers: resolveOpenVikingRequestHeaders({
69
+ headers: cfg.headers,
70
+ }),
71
+ },
65
72
  ),
66
73
  );
67
74
 
@@ -71,7 +71,7 @@ export function registerOpenVikingMemoryTools(deps: OpenVikingMemoryToolsDeps):
71
71
  name: "memory_store",
72
72
  label: "Memory Store (OpenViking)",
73
73
  description:
74
- "Store text in OpenViking memory pipeline by writing to a session and running memory extraction.",
74
+ "Store text in OpenViking memory pipeline by writing to a session and running memory extraction. Use when the user explicitly asks to remember, save, or store an important long-term fact, preference, project, or decision; automatic capture is threshold/commit dependent.",
75
75
  parameters: Type.Object({
76
76
  text: Type.String({ description: "Information to store as memory source text" }),
77
77
  role: Type.Optional(Type.String({ description: "Session role, default user" })),
package/query-config.ts CHANGED
@@ -231,7 +231,13 @@ export class RuntimeQueryConfigStore {
231
231
  const { params, warnings } = normalizeRuntimeQueryParams(patch);
232
232
  const key = this.keyForScope(scope, ctx);
233
233
  const bucket = scope === "claw" ? this.data.claws : this.data.sessions;
234
- bucket[key] = { params, updatedAt: Date.now(), updatedBy: "command", peerId: ctx.peerId };
234
+ const previous = bucket[key];
235
+ bucket[key] = {
236
+ params: { ...(previous?.params ?? {}), ...params },
237
+ updatedAt: Date.now(),
238
+ updatedBy: "command",
239
+ peerId: ctx.peerId,
240
+ };
235
241
  this.data.updatedAt = Date.now();
236
242
  await this.persist();
237
243
  return { warnings };
@@ -0,0 +1,29 @@
1
+ export type OpenVikingRequestHeaders = Record<string, string>;
2
+
3
+ export type ResolveOpenVikingRequestHeadersOptions = {
4
+ headers?: unknown;
5
+ };
6
+
7
+ export function resolveOpenVikingRequestHeaders(
8
+ options: ResolveOpenVikingRequestHeadersOptions = {},
9
+ ): OpenVikingRequestHeaders {
10
+ return cleanOpenVikingRequestHeaders(options.headers);
11
+ }
12
+
13
+ export function cleanOpenVikingRequestHeaders(headers: unknown): OpenVikingRequestHeaders {
14
+ if (headers === undefined || headers === null) {
15
+ return {};
16
+ }
17
+ if (typeof headers !== "object" || Array.isArray(headers)) {
18
+ throw new Error("openviking request headers must be an object");
19
+ }
20
+
21
+ const out: OpenVikingRequestHeaders = {};
22
+ for (const [key, value] of Object.entries(headers as Record<string, unknown>)) {
23
+ if (typeof value !== "string") {
24
+ throw new Error(`openviking request header ${key} must be a string`);
25
+ }
26
+ out[key] = value;
27
+ }
28
+ return out;
29
+ }
@@ -123,10 +123,12 @@ export type AfterTurnOpenVikingSessionParams = {
123
123
  messages?: AgentMessage[];
124
124
  prePromptMessageCount?: number;
125
125
  isHeartbeat?: boolean;
126
+ /** Model context window in tokens; the auto-commit threshold is a fraction of this. */
127
+ tokenBudget: number;
126
128
  runtimeContext?: Record<string, unknown>;
127
129
  cfg: {
128
130
  autoCapture: boolean;
129
- commitTokenThreshold: number;
131
+ commitTokenThresholdRatio: number;
130
132
  commitKeepRecentCount: number;
131
133
  logFindRequests: boolean;
132
134
  };
@@ -404,6 +406,11 @@ function assemblePassthrough(
404
406
  return { messages: liveMessages, estimatedTokens: originalTokens };
405
407
  }
406
408
 
409
+ function isSessionNotFoundError(err: unknown): boolean {
410
+ const errorMessage = String(err);
411
+ return errorMessage.includes("[NOT_FOUND]") && errorMessage.includes("Session not found");
412
+ }
413
+
407
414
  export async function assembleOpenVikingSession({
408
415
  sessionId,
409
416
  sessionKey,
@@ -625,6 +632,27 @@ export async function assembleOpenVikingSession({
625
632
  ...(instruction.text ? { systemPromptAddition: instruction.text } : {}),
626
633
  };
627
634
  } catch (err) {
635
+ if (isSessionNotFoundError(err)) {
636
+ const errorMessage = String(err);
637
+ logger.info(
638
+ `openviking: assemble skipped because OV session does not exist ` +
639
+ `(session=${ovSessionId}, tokenBudget=${tokenBudget}, agentId=${resolveAgentId(ovSessionId)})`,
640
+ );
641
+ return assemblePassthrough({
642
+ diag,
643
+ ovSessionId,
644
+ reason: "session_not_found",
645
+ liveMessages: messages,
646
+ originalTokens,
647
+ extra: {
648
+ error: errorMessage,
649
+ tokenBudget,
650
+ agentId: resolveAgentId(ovSessionId),
651
+ senderIdFound: sender.found,
652
+ senderId: sender.senderId ?? null,
653
+ },
654
+ });
655
+ }
628
656
  logger.warn?.(
629
657
  `openviking: assemble failed for session=${ovSessionId}, ` +
630
658
  `tokenBudget=${tokenBudget}, agentId=${resolveAgentId(ovSessionId)}: ${String(err)}`,
@@ -688,7 +716,7 @@ function extractRuntimeAgentId(runtimeContext: Record<string, unknown> | undefin
688
716
  }
689
717
 
690
718
  function isToolOnlyMessage(msg: ExtractedTurnMessage): boolean {
691
- return msg.role === "user" && msg.parts.length > 0 && msg.parts.every((part) => part.type === "tool");
719
+ return msg.role === "assistant" && msg.parts.length > 0 && msg.parts.every((part) => part.type === "tool");
692
720
  }
693
721
 
694
722
  function coalesceConsecutiveToolMessages(messages: ExtractedTurnMessage[]): ExtractedTurnMessage[] {
@@ -705,7 +733,7 @@ function coalesceConsecutiveToolMessages(messages: ExtractedTurnMessage[]): Extr
705
733
  for (const msg of messages) {
706
734
  if (isToolOnlyMessage(msg)) {
707
735
  if (!pendingTools) {
708
- pendingTools = { role: "user", parts: [] };
736
+ pendingTools = { role: "assistant", parts: [] };
709
737
  }
710
738
  pendingTools.parts.push(...msg.parts);
711
739
  continue;
@@ -753,6 +781,7 @@ export async function afterTurnOpenVikingSession({
753
781
  messages: rawMessages,
754
782
  prePromptMessageCount,
755
783
  isHeartbeat,
784
+ tokenBudget,
756
785
  runtimeContext,
757
786
  cfg,
758
787
  getClient,
@@ -880,11 +909,15 @@ export async function afterTurnOpenVikingSession({
880
909
  const session = await client.getSession(ovSessionId, agentId);
881
910
  const pendingTokens = session.pending_tokens ?? 0;
882
911
 
883
- if (pendingTokens < cfg.commitTokenThreshold) {
912
+ const commitTokenThreshold = Math.floor(tokenBudget * cfg.commitTokenThresholdRatio);
913
+
914
+ if (pendingTokens < commitTokenThreshold) {
884
915
  diag("afterTurn_skip", ovSessionId, {
885
916
  reason: "below_threshold",
886
917
  pendingTokens,
887
- commitTokenThreshold: cfg.commitTokenThreshold,
918
+ commitTokenThreshold,
919
+ commitTokenThresholdRatio: cfg.commitTokenThresholdRatio,
920
+ tokenBudget,
888
921
  senderIdFound: sender.found,
889
922
  senderId: sender.senderId ?? null,
890
923
  });
@@ -904,7 +937,9 @@ export async function afterTurnOpenVikingSession({
904
937
 
905
938
  diag("afterTurn_commit", ovSessionId, {
906
939
  pendingTokens,
907
- commitTokenThreshold: cfg.commitTokenThreshold,
940
+ commitTokenThreshold,
941
+ commitTokenThresholdRatio: cfg.commitTokenThresholdRatio,
942
+ tokenBudget,
908
943
  status: commitResult.status,
909
944
  archived: commitResult.archived ?? false,
910
945
  taskId: commitResult.task_id ?? null,
@@ -1183,7 +1218,7 @@ export async function compactOpenVikingSession({
1183
1218
  };
1184
1219
  } catch (err) {
1185
1220
  const errorMessage = String(err);
1186
- if (errorMessage.includes("[NOT_FOUND]") && errorMessage.includes("Session not found")) {
1221
+ if (isSessionNotFoundError(err)) {
1187
1222
  logger.info(
1188
1223
  `openviking: compact skipped because OV session does not exist ` +
1189
1224
  `(session=${ovSessionId}, agentId=${agentId})`,