@openviking/openclaw-plugin 2026.6.18 → 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/INSTALL-AGENT.md CHANGED
@@ -153,11 +153,11 @@ openclaw --version
153
153
  Requirements:
154
154
 
155
155
  - Node.js >= 22
156
- - OpenClaw >= 2026.4.8
156
+ - OpenClaw >= 2026.5.27
157
157
 
158
158
  Version boundaries:
159
159
 
160
- - `2026.4.8` is the minimum supported OpenClaw version for the current `@openviking/openclaw-plugin` plugin.
160
+ - `2026.5.27` is the minimum supported OpenClaw version for the current `@openviking/openclaw-plugin` plugin.
161
161
  - `2026.5.3` starts requiring compiled JavaScript runtime output during package install when a plugin package declares TypeScript entries.
162
162
  - `2026.5.4` and later no longer fall back to `.ts` source for installed/global plugin runtime loading when the compiled JavaScript output is missing; the plugin may be skipped.
163
163
  - Published ClawHub packages are built before release and include `dist/*.js`, so normal users do not need to build locally.
package/INSTALL-ZH.md CHANGED
@@ -27,13 +27,13 @@ openclaw plugins install clawhub:@openviking/openclaw-plugin
27
27
  | 组件 | 要求 |
28
28
  | --- | --- |
29
29
  | Node.js | >= 22 |
30
- | OpenClaw | >= 2026.4.8 |
30
+ | OpenClaw | >= 2026.5.27 |
31
31
 
32
32
  插件以远程模式连接到已有的 OpenViking 服务。它不会帮你启动 OpenViking server。需要先启动 OpenViking,并保持服务运行,再把插件的 `baseUrl` 指向这个 HTTP 服务。默认本地地址是 `http://127.0.0.1:1933`。
33
33
 
34
34
  OpenClaw 插件包版本边界:
35
35
 
36
- - `2026.4.8` 是当前插件支持的最低 OpenClaw 版本。
36
+ - `2026.5.27` 是当前插件支持的最低 OpenClaw 版本。这个版本下限包含 2026 年 7 月 2 日 OpenClaw 安全公告批次的修复,包括 GHSA-8wg3-5mcm-fjq8 和 GHSA-83w9-h5wv-j9xm。
37
37
  - `2026.5.3` 开始,OpenClaw 在安装包时会校验 TypeScript 插件入口是否有编译后的 JavaScript 产物。
38
38
  - `2026.5.4` 及之后,已安装/全局插件如果缺少编译后的 JavaScript,运行时不再回退加载 `.ts` 源码,插件可能被跳过。
39
39
  - 推荐的 `openclaw plugins install clawhub:@openviking/openclaw-plugin` 会安装已经发布并包含 `dist/*.js` 的插件包,普通用户不需要本地编译。
package/INSTALL.md CHANGED
@@ -27,13 +27,13 @@ openclaw plugins install clawhub:@openviking/openclaw-plugin
27
27
  | Component | Required |
28
28
  | --- | --- |
29
29
  | Node.js | >= 22 |
30
- | OpenClaw | >= 2026.4.8 |
30
+ | OpenClaw | >= 2026.5.27 |
31
31
 
32
32
  The plugin connects to an existing OpenViking server. It does not start the OpenViking server for you. Start OpenViking first, keep it running, then point the plugin `baseUrl` at that HTTP service. The default local URL is `http://127.0.0.1:1933`.
33
33
 
34
34
  OpenClaw plugin package boundaries:
35
35
 
36
- - `2026.4.8` is the minimum supported OpenClaw version for the current plugin.
36
+ - `2026.5.27` is the minimum supported OpenClaw version for the current plugin. This floor includes the July 2, 2026 OpenClaw advisory batch fixes, including GHSA-8wg3-5mcm-fjq8 and GHSA-83w9-h5wv-j9xm.
37
37
  - `2026.5.3` starts validating package installs so TypeScript plugin entries need compiled JavaScript output.
38
38
  - `2026.5.4` and later stop falling back to `.ts` source for installed/global plugin runtime loading when compiled JavaScript is missing.
39
39
  - The recommended `openclaw plugins install clawhub:@openviking/openclaw-plugin` path installs a published package that already includes `dist/*.js`.
package/README.md CHANGED
@@ -204,7 +204,7 @@ That means OpenClaw sees "compressed history summary + archive index + active me
204
204
  - it strips injected `<openviking-context>` blocks, historical `<relevant-memories>` blocks, and metadata noise before capture
205
205
  - it appends the sanitized turn text into the OpenViking session
206
206
 
207
- After that, the plugin checks `pending_tokens`. Once the session crosses `commitTokenThreshold`, it triggers `commit(wait=false)`:
207
+ After that, the plugin checks `pending_tokens`. Once it reaches `commitTokenThresholdRatio` of the model context window (`tokenBudget`), it triggers `commit(wait=false)`:
208
208
 
209
209
  - archive generation and Phase 2 memory extraction continue asynchronously on the server
210
210
  - the current turn is not blocked waiting for extraction
package/auto-recall.ts CHANGED
@@ -19,7 +19,6 @@ import {
19
19
  import { sanitizeUserTextForCapture } from "./text-utils.js";
20
20
  import { estimateTextTokens } from "./token-estimator.js";
21
21
 
22
- const AUTO_RECALL_TIMEOUT_MS = 5_000;
23
22
  const RECALL_QUERY_MAX_CHARS = 4_000;
24
23
  export const AUTO_RECALL_SOURCE_MARKER = "Source: openviking-auto-recall";
25
24
 
@@ -512,7 +511,7 @@ export async function buildAutoRecallContext(params: {
512
511
  await recordTrace(memories.slice(0, memoryLines.length), memoryLines.length, estimatedTokens);
513
512
  return { block, memoryCount: memoryLines.length, estimatedTokens };
514
513
  })(),
515
- AUTO_RECALL_TIMEOUT_MS,
514
+ cfg.autoRecallTimeoutMs,
516
515
  "openviking: auto-recall search timeout",
517
516
  );
518
517
  }
package/client.ts CHANGED
@@ -41,6 +41,7 @@ function userSessionUri(sessionId: string): string {
41
41
 
42
42
  export type OpenVikingClientOptions = {
43
43
  transport?: HttpTransport;
44
+ headers?: Record<string, string>;
44
45
  resourcePackager?: ResourcePackager;
45
46
  now?: () => number;
46
47
  sleep?: (ms: number) => Promise<void>;
@@ -48,7 +49,7 @@ export type OpenVikingClientOptions = {
48
49
 
49
50
  export type CommitSessionResult = {
50
51
  session_id: string;
51
- /** "accepted" (async), "completed", "failed", or "timeout" (wait mode). */
52
+ /** "accepted" (async), "skipped" (no archive), "completed", "failed", or "timeout" (wait mode). */
52
53
  status: string;
53
54
  task_id?: string;
54
55
  archive_uri?: string;
@@ -254,6 +255,7 @@ async function cleanupUploadTempPath(path?: string): Promise<void> {
254
255
 
255
256
  export class OpenVikingClient {
256
257
  private readonly transport: HttpTransport;
258
+ private readonly configuredHeaders: Record<string, string>;
257
259
  private readonly now: () => number;
258
260
  private readonly sleep: (ms: number) => Promise<void>;
259
261
  private readonly resourcePackager: ResourcePackager;
@@ -277,6 +279,7 @@ export class OpenVikingClient {
277
279
  ? optionsOrLegacyUserScope
278
280
  : (legacyOptions ?? {});
279
281
  this.transport = options.transport ?? defaultHttpTransport;
282
+ this.configuredHeaders = options.headers ?? {};
280
283
  this.now = options.now ?? Date.now;
281
284
  this.sleep = options.sleep ?? sleep;
282
285
  this.resourcePackager = options.resourcePackager ?? defaultResourcePackager;
@@ -357,6 +360,11 @@ export class OpenVikingClient {
357
360
  if (actorPeerHeader) {
358
361
  headers.set("X-OpenViking-Actor-Peer", actorPeerHeader);
359
362
  }
363
+ for (const [key, value] of Object.entries(this.configuredHeaders)) {
364
+ if (key && typeof value === "string" && value.trim()) {
365
+ headers.set(key, value);
366
+ }
367
+ }
360
368
  if (init.body && !(init.body instanceof FormData) && !headers.has("Content-Type")) {
361
369
  headers.set("Content-Type", "application/json");
362
370
  }
@@ -751,19 +759,19 @@ export class OpenVikingClient {
751
759
  }>,
752
760
  actorPeerId?: string,
753
761
  createdAt?: string,
754
- roleId?: string,
762
+ peerId?: string,
755
763
  ): Promise<void> {
756
764
  const body: {
757
765
  role: string;
758
- role_id?: string;
766
+ peer_id?: string;
759
767
  parts: typeof parts;
760
768
  created_at?: string;
761
769
  } = { role, parts };
762
770
  if (createdAt) {
763
771
  body.created_at = createdAt;
764
772
  }
765
- if (roleId) {
766
- body.role_id = roleId;
773
+ if (peerId) {
774
+ body.peer_id = peerId;
767
775
  }
768
776
  await this.emitRoutingDebug(
769
777
  "session message POST (with parts)",
@@ -771,7 +779,7 @@ export class OpenVikingClient {
771
779
  path: `/api/v1/sessions/${encodeURIComponent(sessionId)}/messages`,
772
780
  sessionId,
773
781
  role,
774
- role_id: roleId ?? null,
782
+ peer_id: peerId ?? null,
775
783
  partCount: parts.length,
776
784
  created_at: createdAt ?? null,
777
785
  },
package/commands/setup.ts CHANGED
@@ -4,6 +4,11 @@ import * as path from "node:path";
4
4
  import * as readline from "node:readline";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { getEnv } from "../runtime-utils.js";
7
+ import {
8
+ cleanOpenVikingRequestHeaders,
9
+ resolveOpenVikingRequestHeaders,
10
+ type OpenVikingRequestHeaders,
11
+ } from "../request-headers.js";
7
12
 
8
13
  const HOME = os.homedir();
9
14
  const OPENCLAW_DIR = getEnv("OPENCLAW_STATE_DIR") || path.join(HOME, ".openclaw");
@@ -58,6 +63,7 @@ const PLUGIN_VERSION = readPluginVersion();
58
63
  const { min: COMPATIBLE_SERVER_MIN, max: COMPATIBLE_SERVER_MAX } = readCompatRangeFromManifest();
59
64
  const CONFIG_KEYS_TO_PRESERVE = [
60
65
  "targetUri",
66
+ "headers",
61
67
  "timeoutMs",
62
68
  "autoCapture",
63
69
  "captureMode",
@@ -71,7 +77,7 @@ const CONFIG_KEYS_TO_PRESERVE = [
71
77
  "recallMaxContentChars",
72
78
  "recallPreferAbstract",
73
79
  "recallTokenBudget",
74
- "commitTokenThreshold",
80
+ "commitTokenThresholdRatio",
75
81
  "commitKeepRecentCount",
76
82
  "bypassSessionPatterns",
77
83
  "emitStandardDiagnostics",
@@ -107,6 +113,11 @@ function maskKey(key: string): string {
107
113
  return `${key.slice(0, 4)}...${key.slice(-4)}`;
108
114
  }
109
115
 
116
+ function nonEmptyOpenVikingRequestHeaders(value: unknown): OpenVikingRequestHeaders | undefined {
117
+ const headers = cleanOpenVikingRequestHeaders(value);
118
+ return Object.keys(headers).length > 0 ? headers : undefined;
119
+ }
120
+
110
121
  function isValidPeerPrefixInput(value: string): boolean {
111
122
  const trimmed = value.trim();
112
123
  return !trimmed || /^[a-zA-Z0-9_-]+$/.test(trimmed);
@@ -301,14 +312,21 @@ type ApiKeyProbeResult = {
301
312
  detail: string;
302
313
  };
303
314
 
304
- async function probeApiKeyType(baseUrl: string, apiKey?: string): Promise<ApiKeyProbeResult> {
315
+ async function probeApiKeyType(
316
+ baseUrl: string,
317
+ apiKey?: string,
318
+ configuredHeaders?: OpenVikingRequestHeaders,
319
+ ): Promise<ApiKeyProbeResult> {
305
320
  if (!apiKey) return { keyType: "no_key", needsAccountId: false, needsUserId: false, detail: "No API key configured" };
306
321
 
307
322
  const controller = new AbortController();
308
323
  const timeoutId = setTimeout(() => controller.abort(), 10_000);
309
324
  const sessionsUrl = `${baseUrl.replace(/\/+$/, "")}/api/v1/sessions?limit=1`;
310
325
  try {
311
- const headers: Record<string, string> = { "X-API-Key": apiKey };
326
+ const configuredRequestHeaders = resolveOpenVikingRequestHeaders({ headers: configuredHeaders });
327
+ const headers: Record<string, string> = {};
328
+ headers["X-API-Key"] = apiKey;
329
+ Object.assign(headers, configuredRequestHeaders);
312
330
  const response = await fetch(sessionsUrl, {
313
331
  headers,
314
332
  signal: controller.signal,
@@ -347,6 +365,7 @@ async function probeApiKeyType(baseUrl: string, apiKey?: string): Promise<ApiKey
347
365
  "X-API-Key": apiKey,
348
366
  "X-OpenViking-Account": "__probe__",
349
367
  "X-OpenViking-User": "__probe__",
368
+ ...configuredRequestHeaders,
350
369
  };
351
370
  const probe2 = await fetch(sessionsUrl, {
352
371
  headers: probeHeaders,
@@ -376,12 +395,20 @@ async function probeApiKeyType(baseUrl: string, apiKey?: string): Promise<ApiKey
376
395
  }
377
396
  }
378
397
 
379
- async function checkServiceHealth(baseUrl: string, apiKey?: string): Promise<HealthResult> {
398
+ async function checkServiceHealth(
399
+ baseUrl: string,
400
+ apiKey?: string,
401
+ configuredHeaders?: OpenVikingRequestHeaders,
402
+ ): Promise<HealthResult> {
380
403
  const controller = new AbortController();
381
404
  const timeoutId = setTimeout(() => controller.abort(), 10_000);
382
405
  try {
406
+ const configuredRequestHeaders = resolveOpenVikingRequestHeaders({ headers: configuredHeaders });
383
407
  const headers: Record<string, string> = {};
384
- if (apiKey) headers["X-API-Key"] = apiKey;
408
+ if (apiKey) {
409
+ headers["X-API-Key"] = apiKey;
410
+ }
411
+ Object.assign(headers, configuredRequestHeaders);
385
412
  const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/health`, {
386
413
  headers,
387
414
  signal: controller.signal,
@@ -772,8 +799,9 @@ async function runRemoteCheck(
772
799
  ): Promise<void> {
773
800
  const baseUrl = String(existing.baseUrl ?? DEFAULT_REMOTE_URL);
774
801
  const apiKey = existing.apiKey ? String(existing.apiKey) : undefined;
802
+ const headers = nonEmptyOpenVikingRequestHeaders(existing.headers);
775
803
  console.log(tr(zh, `Testing connectivity to ${baseUrl}...`, `正在测试连接 ${baseUrl}...`));
776
- const health = await checkServiceHealth(baseUrl, apiKey);
804
+ const health = await checkServiceHealth(baseUrl, apiKey, headers);
777
805
  if (health.ok) {
778
806
  const ver = health.version ? ` (version: ${health.version})` : "";
779
807
  console.log(` ✓ ${tr(zh, `Connected successfully${ver}`, `连接成功${ver}`)}`);
@@ -955,8 +983,9 @@ async function getStatus(configPath: string): Promise<StatusResult> {
955
983
 
956
984
  const baseUrl = String(existing.baseUrl ?? DEFAULT_REMOTE_URL);
957
985
  const apiKey = existing.apiKey ? String(existing.apiKey) : undefined;
958
- const health = await checkServiceHealth(baseUrl, apiKey);
959
- const keyProbe = health.ok ? await probeApiKeyType(baseUrl, apiKey) : undefined;
986
+ const headers = nonEmptyOpenVikingRequestHeaders(existing.headers);
987
+ const health = await checkServiceHealth(baseUrl, apiKey, headers);
988
+ const keyProbe = health.ok ? await probeApiKeyType(baseUrl, apiKey, headers) : undefined;
960
989
 
961
990
  const peerPrefix = resolveExistingPeerPrefix(existing);
962
991
  return {
@@ -1068,6 +1097,7 @@ async function setupRemote(
1068
1097
  const defaultApiKey = existing?.apiKey ? String(existing.apiKey) : "";
1069
1098
  const defaultPeerRole = resolveExistingPeerRole(existing);
1070
1099
  const defaultPeerPrefix = resolveExistingPeerPrefix(existing);
1100
+ const headers = nonEmptyOpenVikingRequestHeaders(existing?.headers);
1071
1101
 
1072
1102
  const baseUrl = await q(tr(zh, "OpenViking server URL", "OpenViking 服务器地址"), defaultUrl);
1073
1103
  const apiKey = await q(tr(zh, "API Key (optional)", "API Key(可选)"), defaultApiKey);
@@ -1077,7 +1107,7 @@ async function setupRemote(
1077
1107
 
1078
1108
  if (apiKey) {
1079
1109
  console.log(tr(zh, " Detecting API key type...", " 正在检测 API Key 类型..."));
1080
- const probe = await probeApiKeyType(baseUrl, apiKey);
1110
+ const probe = await probeApiKeyType(baseUrl, apiKey, headers);
1081
1111
  if (probe.keyType === "root_key") {
1082
1112
  console.log(tr(zh,
1083
1113
  " ⚠ Root API key detected. accountId and userId are required.",
@@ -1098,7 +1128,7 @@ async function setupRemote(
1098
1128
  console.log("");
1099
1129
 
1100
1130
  console.log(tr(zh, `Testing connectivity to ${baseUrl}...`, `正在测试连接 ${baseUrl}...`));
1101
- const health = await checkServiceHealth(baseUrl, apiKey || undefined);
1131
+ const health = await checkServiceHealth(baseUrl, apiKey || undefined, headers);
1102
1132
  if (health.ok) {
1103
1133
  const ver = health.version ? ` (version: ${health.version})` : "";
1104
1134
  console.log(` ✓ ${tr(zh, `Connected successfully${ver}`, `连接成功${ver}`)}`);
package/config.ts CHANGED
@@ -8,6 +8,8 @@ export type MemoryOpenVikingConfig = {
8
8
  peer_role?: "none" | "assistant" | "person";
9
9
  peer_prefix?: string;
10
10
  apiKey?: string;
11
+ /** Optional HTTP headers merged into every OpenViking request. */
12
+ headers?: Record<string, string>;
11
13
  /** Advanced option. Only needed when explicitly sending tenant identity headers. With a user key the server derives identity from the key. */
12
14
  accountId?: string;
13
15
  /** Advanced option. Only needed when explicitly sending tenant identity headers. */
@@ -31,7 +33,15 @@ export type MemoryOpenVikingConfig = {
31
33
  recallPreferAbstract?: boolean;
32
34
  /** @deprecated Use recallMaxInjectedChars. */
33
35
  recallTokenBudget?: number;
34
- commitTokenThreshold?: number;
36
+ /**
37
+ * Auto-commit threshold expressed as a fraction (0-1) of the model context
38
+ * window (tokenBudget). afterTurn triggers an async commit once estimated
39
+ * pending tokens reach `commitTokenThresholdRatio * tokenBudget`
40
+ * (e.g. 0.5 = 50% of the window). Replaces the former absolute
41
+ * `commitTokenThreshold` (still accepted but ignored for backward
42
+ * compatibility). Set to 0 to commit every turn.
43
+ */
44
+ commitTokenThresholdRatio?: number;
35
45
  /**
36
46
  * WM v2: number of most-recent messages to keep live after an afterTurn
37
47
  * commit so the next turn still has immediate context. Forwarded to the
@@ -109,7 +119,7 @@ const DEFAULT_RECALL_SCORE_THRESHOLD = 0.15;
109
119
  const DEFAULT_RECALL_MAX_CONTENT_CHARS = 5000;
110
120
  const DEFAULT_RECALL_PREFER_ABSTRACT = false;
111
121
  const DEFAULT_RECALL_MAX_INJECTED_CHARS = 4000;
112
- const DEFAULT_COMMIT_TOKEN_THRESHOLD = 20000;
122
+ const DEFAULT_COMMIT_TOKEN_THRESHOLD_RATIO = 0.5;
113
123
  const DEFAULT_COMMIT_KEEP_RECENT_COUNT = 10;
114
124
  const DEFAULT_BYPASS_SESSION_PATTERNS: string[] = [];
115
125
  const DEFAULT_EMIT_STANDARD_DIAGNOSTICS = false;
@@ -280,6 +290,24 @@ function toRecord(value: unknown): Record<string, unknown> {
280
290
  : {};
281
291
  }
282
292
 
293
+ function toStringRecord(value: unknown, label: string): Record<string, string> {
294
+ if (value === undefined || value === null) {
295
+ return {};
296
+ }
297
+ if (typeof value !== "object" || Array.isArray(value)) {
298
+ throw new Error(`${label} must be an object`);
299
+ }
300
+ const record = value as Record<string, unknown>;
301
+ const out: Record<string, string> = {};
302
+ for (const [key, val] of Object.entries(record)) {
303
+ if (typeof val !== "string") {
304
+ throw new Error(`${label}.${key} must be a string`);
305
+ }
306
+ out[key] = val;
307
+ }
308
+ return out;
309
+ }
310
+
283
311
  function expandToolSelectors(value: unknown, fallback: string[], label: string): OpenVikingToolName[] {
284
312
  const entries = toStringArray(value, fallback);
285
313
  const seen = new Set<OpenVikingToolName>();
@@ -377,6 +405,7 @@ export const memoryOpenVikingConfigSchema = {
377
405
  "peer_prefix",
378
406
  "serverAuthMode",
379
407
  "apiKey",
408
+ "headers",
380
409
  "accountId",
381
410
  "userId",
382
411
  "targetUri",
@@ -394,6 +423,7 @@ export const memoryOpenVikingConfigSchema = {
394
423
  "recallPreferAbstract",
395
424
  "recallTokenBudget",
396
425
  "commitTokenThreshold",
426
+ "commitTokenThresholdRatio",
397
427
  "commitKeepRecentCount",
398
428
  "bypassSessionPatterns",
399
429
  "ingestReplyAssist",
@@ -479,6 +509,7 @@ export const memoryOpenVikingConfigSchema = {
479
509
  peer_role: peerRole,
480
510
  peer_prefix: peerPrefix,
481
511
  apiKey: rawApiKey ? resolveEnvVars(rawApiKey) : "",
512
+ headers: toStringRecord(cfg.headers, "openviking config headers"),
482
513
  accountId,
483
514
  userId,
484
515
  targetUri: typeof cfg.targetUri === "string" ? cfg.targetUri : DEFAULT_TARGET_URI,
@@ -510,9 +541,9 @@ export const memoryOpenVikingConfigSchema = {
510
541
  : DEFAULT_RECALL_PREFER_ABSTRACT,
511
542
  recallMaxInjectedChars,
512
543
  recallTokenBudget: recallMaxInjectedChars,
513
- commitTokenThreshold: Math.max(
544
+ commitTokenThresholdRatio: Math.max(
514
545
  0,
515
- Math.min(100_000, Math.floor(toNumber(cfg.commitTokenThreshold, DEFAULT_COMMIT_TOKEN_THRESHOLD))),
546
+ Math.min(1, toNumber(cfg.commitTokenThresholdRatio, DEFAULT_COMMIT_TOKEN_THRESHOLD_RATIO)),
516
547
  ),
517
548
  commitKeepRecentCount: Math.max(
518
549
  0,
@@ -649,6 +680,11 @@ export const memoryOpenVikingConfigSchema = {
649
680
  placeholder: "${OPENVIKING_API_KEY}",
650
681
  help: "Optional API key for OpenViking server",
651
682
  },
683
+ headers: {
684
+ label: "Headers",
685
+ advanced: true,
686
+ help: "Optional HTTP headers merged into every OpenViking request.",
687
+ },
652
688
  accountId: {
653
689
  label: "Account ID",
654
690
  placeholder: "(derived from API key)",
@@ -747,11 +783,11 @@ export const memoryOpenVikingConfigSchema = {
747
783
  help: "Completely bypass OpenViking for matching session keys. Use * within one segment and ** across segments.",
748
784
  advanced: true,
749
785
  },
750
- commitTokenThreshold: {
751
- label: "Commit Token Threshold",
752
- placeholder: String(DEFAULT_COMMIT_TOKEN_THRESHOLD),
786
+ commitTokenThresholdRatio: {
787
+ label: "Commit Token Threshold Ratio",
788
+ placeholder: String(DEFAULT_COMMIT_TOKEN_THRESHOLD_RATIO),
753
789
  advanced: true,
754
- help: "Minimum estimated pending tokens before auto-commit triggers. Set to 0 to commit every turn.",
790
+ 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.",
755
791
  },
756
792
  commitKeepRecentCount: {
757
793
  label: "Commit Keep Recent Count",
package/context-engine.ts CHANGED
@@ -370,12 +370,14 @@ export function createMemoryOpenVikingContextEngine(params: {
370
370
  },
371
371
 
372
372
  async afterTurn(afterTurnParams): Promise<void> {
373
+ const tokenBudget = validTokenBudget(afterTurnParams.tokenBudget) ?? 128_000;
373
374
  await afterTurnOpenVikingSession({
374
375
  sessionId: afterTurnParams.sessionId,
375
376
  sessionKey: resolveSessionKey(afterTurnParams),
376
377
  messages: afterTurnParams.messages,
377
378
  prePromptMessageCount: afterTurnParams.prePromptMessageCount,
378
379
  isHeartbeat: afterTurnParams.isHeartbeat,
380
+ tokenBudget,
379
381
  runtimeContext: afterTurnParams.runtimeContext,
380
382
  cfg,
381
383
  getClient,
@@ -3,7 +3,6 @@ import { quickRecallPrecheck, withTimeout } from "./process-manager.js";
3
3
  import { resolveRecallSearchPlan, } from "./registries/recall-resource-types.js";
4
4
  import { sanitizeUserTextForCapture } from "./text-utils.js";
5
5
  import { estimateTextTokens } from "./token-estimator.js";
6
- const AUTO_RECALL_TIMEOUT_MS = 5_000;
7
6
  const RECALL_QUERY_MAX_CHARS = 4_000;
8
7
  export const AUTO_RECALL_SOURCE_MARKER = "Source: openviking-auto-recall";
9
8
  const WRITE_OR_EFFECT_RE = /\b(write|edit|modify|delete|remove|migrate|deploy|release|publish|configure|patch)\b|写|改|修改|删除|迁移|部署|发布|配置|打补丁/i;
@@ -359,5 +358,5 @@ export async function buildAutoRecallContext(params) {
359
358
  verbose?.(`openviking: inject-detail ${toJsonLog({ count: memories.length, memories: summarizeInjectionMemories(memories) })}`);
360
359
  await recordTrace(memories.slice(0, memoryLines.length), memoryLines.length, estimatedTokens);
361
360
  return { block, memoryCount: memoryLines.length, estimatedTokens };
362
- })(), AUTO_RECALL_TIMEOUT_MS, "openviking: auto-recall search timeout");
361
+ })(), cfg.autoRecallTimeoutMs, "openviking: auto-recall search timeout");
363
362
  }
package/dist/client.js CHANGED
@@ -51,6 +51,7 @@ export class OpenVikingClient {
51
51
  userId;
52
52
  routingDebugLog;
53
53
  transport;
54
+ configuredHeaders;
54
55
  now;
55
56
  sleep;
56
57
  resourcePackager;
@@ -70,6 +71,7 @@ export class OpenVikingClient {
70
71
  ? optionsOrLegacyUserScope
71
72
  : (legacyOptions ?? {});
72
73
  this.transport = options.transport ?? defaultHttpTransport;
74
+ this.configuredHeaders = options.headers ?? {};
73
75
  this.now = options.now ?? Date.now;
74
76
  this.sleep = options.sleep ?? sleep;
75
77
  this.resourcePackager = options.resourcePackager ?? defaultResourcePackager;
@@ -131,6 +133,11 @@ export class OpenVikingClient {
131
133
  if (actorPeerHeader) {
132
134
  headers.set("X-OpenViking-Actor-Peer", actorPeerHeader);
133
135
  }
136
+ for (const [key, value] of Object.entries(this.configuredHeaders)) {
137
+ if (key && typeof value === "string" && value.trim()) {
138
+ headers.set(key, value);
139
+ }
140
+ }
134
141
  if (init.body && !(init.body instanceof FormData) && !headers.has("Content-Type")) {
135
142
  headers.set("Content-Type", "application/json");
136
143
  }
@@ -387,19 +394,19 @@ export class OpenVikingClient {
387
394
  await this.resourcePackager.cleanup(packagedSource);
388
395
  }
389
396
  }
390
- async addSessionMessage(sessionId, role, parts, actorPeerId, createdAt, roleId) {
397
+ async addSessionMessage(sessionId, role, parts, actorPeerId, createdAt, peerId) {
391
398
  const body = { role, parts };
392
399
  if (createdAt) {
393
400
  body.created_at = createdAt;
394
401
  }
395
- if (roleId) {
396
- body.role_id = roleId;
402
+ if (peerId) {
403
+ body.peer_id = peerId;
397
404
  }
398
405
  await this.emitRoutingDebug("session message POST (with parts)", {
399
406
  path: `/api/v1/sessions/${encodeURIComponent(sessionId)}/messages`,
400
407
  sessionId,
401
408
  role,
402
- role_id: roleId ?? null,
409
+ peer_id: peerId ?? null,
403
410
  partCount: parts.length,
404
411
  created_at: createdAt ?? null,
405
412
  }, actorPeerId);
@@ -4,6 +4,7 @@ import * as path from "node:path";
4
4
  import * as readline from "node:readline";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { getEnv } from "../runtime-utils.js";
7
+ import { cleanOpenVikingRequestHeaders, resolveOpenVikingRequestHeaders, } from "../request-headers.js";
7
8
  const HOME = os.homedir();
8
9
  const OPENCLAW_DIR = getEnv("OPENCLAW_STATE_DIR") || path.join(HOME, ".openclaw");
9
10
  const DEFAULT_REMOTE_URL = "http://127.0.0.1:1933";
@@ -55,6 +56,7 @@ const PLUGIN_VERSION = readPluginVersion();
55
56
  const { min: COMPATIBLE_SERVER_MIN, max: COMPATIBLE_SERVER_MAX } = readCompatRangeFromManifest();
56
57
  const CONFIG_KEYS_TO_PRESERVE = [
57
58
  "targetUri",
59
+ "headers",
58
60
  "timeoutMs",
59
61
  "autoCapture",
60
62
  "captureMode",
@@ -68,7 +70,7 @@ const CONFIG_KEYS_TO_PRESERVE = [
68
70
  "recallMaxContentChars",
69
71
  "recallPreferAbstract",
70
72
  "recallTokenBudget",
71
- "commitTokenThreshold",
73
+ "commitTokenThresholdRatio",
72
74
  "commitKeepRecentCount",
73
75
  "bypassSessionPatterns",
74
76
  "emitStandardDiagnostics",
@@ -84,6 +86,10 @@ function maskKey(key) {
84
86
  return "****";
85
87
  return `${key.slice(0, 4)}...${key.slice(-4)}`;
86
88
  }
89
+ function nonEmptyOpenVikingRequestHeaders(value) {
90
+ const headers = cleanOpenVikingRequestHeaders(value);
91
+ return Object.keys(headers).length > 0 ? headers : undefined;
92
+ }
87
93
  function isValidPeerPrefixInput(value) {
88
94
  const trimmed = value.trim();
89
95
  return !trimmed || /^[a-zA-Z0-9_-]+$/.test(trimmed);
@@ -234,14 +240,17 @@ function formatCompatRange() {
234
240
  return `<= ${COMPATIBLE_SERVER_MAX}`;
235
241
  return "any";
236
242
  }
237
- async function probeApiKeyType(baseUrl, apiKey) {
243
+ async function probeApiKeyType(baseUrl, apiKey, configuredHeaders) {
238
244
  if (!apiKey)
239
245
  return { keyType: "no_key", needsAccountId: false, needsUserId: false, detail: "No API key configured" };
240
246
  const controller = new AbortController();
241
247
  const timeoutId = setTimeout(() => controller.abort(), 10_000);
242
248
  const sessionsUrl = `${baseUrl.replace(/\/+$/, "")}/api/v1/sessions?limit=1`;
243
249
  try {
244
- const headers = { "X-API-Key": apiKey };
250
+ const configuredRequestHeaders = resolveOpenVikingRequestHeaders({ headers: configuredHeaders });
251
+ const headers = {};
252
+ headers["X-API-Key"] = apiKey;
253
+ Object.assign(headers, configuredRequestHeaders);
245
254
  const response = await fetch(sessionsUrl, {
246
255
  headers,
247
256
  signal: controller.signal,
@@ -278,6 +287,7 @@ async function probeApiKeyType(baseUrl, apiKey) {
278
287
  "X-API-Key": apiKey,
279
288
  "X-OpenViking-Account": "__probe__",
280
289
  "X-OpenViking-User": "__probe__",
290
+ ...configuredRequestHeaders,
281
291
  };
282
292
  const probe2 = await fetch(sessionsUrl, {
283
293
  headers: probeHeaders,
@@ -307,13 +317,16 @@ async function probeApiKeyType(baseUrl, apiKey) {
307
317
  clearTimeout(timeoutId);
308
318
  }
309
319
  }
310
- async function checkServiceHealth(baseUrl, apiKey) {
320
+ async function checkServiceHealth(baseUrl, apiKey, configuredHeaders) {
311
321
  const controller = new AbortController();
312
322
  const timeoutId = setTimeout(() => controller.abort(), 10_000);
313
323
  try {
324
+ const configuredRequestHeaders = resolveOpenVikingRequestHeaders({ headers: configuredHeaders });
314
325
  const headers = {};
315
- if (apiKey)
326
+ if (apiKey) {
316
327
  headers["X-API-Key"] = apiKey;
328
+ }
329
+ Object.assign(headers, configuredRequestHeaders);
317
330
  const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/health`, {
318
331
  headers,
319
332
  signal: controller.signal,
@@ -602,8 +615,9 @@ function printCompatibilityWarning(zh, health) {
602
615
  async function runRemoteCheck(zh, existing) {
603
616
  const baseUrl = String(existing.baseUrl ?? DEFAULT_REMOTE_URL);
604
617
  const apiKey = existing.apiKey ? String(existing.apiKey) : undefined;
618
+ const headers = nonEmptyOpenVikingRequestHeaders(existing.headers);
605
619
  console.log(tr(zh, `Testing connectivity to ${baseUrl}...`, `正在测试连接 ${baseUrl}...`));
606
- const health = await checkServiceHealth(baseUrl, apiKey);
620
+ const health = await checkServiceHealth(baseUrl, apiKey, headers);
607
621
  if (health.ok) {
608
622
  const ver = health.version ? ` (version: ${health.version})` : "";
609
623
  console.log(` ✓ ${tr(zh, `Connected successfully${ver}`, `连接成功${ver}`)}`);
@@ -771,8 +785,9 @@ async function getStatus(configPath) {
771
785
  }
772
786
  const baseUrl = String(existing.baseUrl ?? DEFAULT_REMOTE_URL);
773
787
  const apiKey = existing.apiKey ? String(existing.apiKey) : undefined;
774
- const health = await checkServiceHealth(baseUrl, apiKey);
775
- const keyProbe = health.ok ? await probeApiKeyType(baseUrl, apiKey) : undefined;
788
+ const headers = nonEmptyOpenVikingRequestHeaders(existing.headers);
789
+ const health = await checkServiceHealth(baseUrl, apiKey, headers);
790
+ const keyProbe = health.ok ? await probeApiKeyType(baseUrl, apiKey, headers) : undefined;
776
791
  const peerPrefix = resolveExistingPeerPrefix(existing);
777
792
  return {
778
793
  configured: true,
@@ -858,13 +873,14 @@ async function setupRemote(zh, configPath, existing, q) {
858
873
  const defaultApiKey = existing?.apiKey ? String(existing.apiKey) : "";
859
874
  const defaultPeerRole = resolveExistingPeerRole(existing);
860
875
  const defaultPeerPrefix = resolveExistingPeerPrefix(existing);
876
+ const headers = nonEmptyOpenVikingRequestHeaders(existing?.headers);
861
877
  const baseUrl = await q(tr(zh, "OpenViking server URL", "OpenViking 服务器地址"), defaultUrl);
862
878
  const apiKey = await q(tr(zh, "API Key (optional)", "API Key(可选)"), defaultApiKey);
863
879
  let accountId = existing?.accountId ? String(existing.accountId) : "";
864
880
  let userId = existing?.userId ? String(existing.userId) : "";
865
881
  if (apiKey) {
866
882
  console.log(tr(zh, " Detecting API key type...", " 正在检测 API Key 类型..."));
867
- const probe = await probeApiKeyType(baseUrl, apiKey);
883
+ const probe = await probeApiKeyType(baseUrl, apiKey, headers);
868
884
  if (probe.keyType === "root_key") {
869
885
  console.log(tr(zh, " ⚠ Root API key detected. accountId and userId are required.", " ⚠ 检测到 Root API Key,需要提供 accountId 和 userId。"));
870
886
  accountId = await q(tr(zh, "Account ID (required for root key)", "Account ID(root key 必填)"), accountId);
@@ -880,7 +896,7 @@ async function setupRemote(zh, configPath, existing, q) {
880
896
  : "";
881
897
  console.log("");
882
898
  console.log(tr(zh, `Testing connectivity to ${baseUrl}...`, `正在测试连接 ${baseUrl}...`));
883
- const health = await checkServiceHealth(baseUrl, apiKey || undefined);
899
+ const health = await checkServiceHealth(baseUrl, apiKey || undefined, headers);
884
900
  if (health.ok) {
885
901
  const ver = health.version ? ` (version: ${health.version})` : "";
886
902
  console.log(` ✓ ${tr(zh, `Connected successfully${ver}`, `连接成功${ver}`)}`);