import { homedir } from "node:os"; import { readFileSync } from "node:fs"; import { getEnv } from "./runtime-utils.js"; /** * Subset of OpenClaw's standard `SecretRef` shape supported by the OpenViking * plugin. The full runtime SDK also recognises `source: "exec"` with a * provider plugin, but the plugin self-hosts its own env/file resolution so * users on pluginSdkVersion < 2026.6.x (no host-level secret-resolver) still * get vault-free secret management. * * source: "env" -> `process.env[id]` (same as the bare `${ENV}` string * interpolation, but declarative and visible to UI hints). * source: "file" -> contents of `id` path (``~`` expansion supported), * leading/trailing whitespace trimmed — convenient for * Kubernetes `secretKeyRef` mount volumes and * file-permission-only deployments. * source: "exec" -> NOT supported in the packaged plugin. Marketplace * install scanners (OpenClaw/ArkClaw) block any plugin * whose shipped code can spawn subprocesses, so the exec * resolver was removed from this build; configuring it * fails with a clear error pointing at env/file instead. */ export type OpenVikingSecretRef = | { source: "env"; id: string } | { source: "file"; id: string } | { source: "exec"; provider: string; id: string }; export type MemoryOpenVikingConfig = { mode?: "remote"; baseUrl?: string; peer_role?: "none" | "assistant" | "person"; peer_prefix?: string; apiKey?: string | OpenVikingSecretRef; /** Optional HTTP headers merged into every OpenViking request. */ headers?: Record; /** Advanced option. Only needed when explicitly sending tenant identity headers. With a user key the server derives identity from the key. */ accountId?: string; /** Advanced option. Only needed when explicitly sending tenant identity headers. */ userId?: string; targetUri?: string; timeoutMs?: number; autoCapture?: boolean; captureMode?: "semantic" | "keyword"; captureMaxLength?: number; autoRecall?: boolean; /** Outer time budget for the whole auto-recall flow, including search, ranking, and reads. */ autoRecallTimeoutMs?: number; /** Include resources in auto-recall and default memory_recall search. Default false. */ recallResources?: boolean; recallLimit?: number; recallScoreThreshold?: number; /** Maximum total characters injected by auto-recall. */ recallMaxInjectedChars?: number; /** @deprecated Auto-recall no longer truncates individual memories. */ recallMaxContentChars?: number; recallPreferAbstract?: boolean; /** @deprecated Use recallMaxInjectedChars. */ recallTokenBudget?: number; /** * Auto-commit threshold expressed as a fraction (0-1) of the model context * window (tokenBudget). afterTurn triggers an async commit once estimated * pending tokens reach `commitTokenThresholdRatio * tokenBudget` * (e.g. 0.5 = 50% of the window). Replaces the former absolute * `commitTokenThreshold` (still accepted but ignored for backward * compatibility). Set to 0 to commit every turn. */ commitTokenThresholdRatio?: number; /** * WM v2: number of most-recent messages to keep live after an afterTurn * commit so the next turn still has immediate context. Forwarded to the * server as `keep_recent_count`. Default 10. The compact path ignores this * value and always passes 0. */ commitKeepRecentCount?: number; bypassSessionPatterns?: string[]; /** * When true (default), emit structured `openviking: diag {...}` lines (and any future * standard-diagnostics file writes) for assemble/afterTurn. Set false to disable. */ emitStandardDiagnostics?: boolean; /** When true, log tenant routing for semantic find and session writes (messages/commit) to the plugin logger. */ logFindRequests?: boolean; /** Enable recall trace recording. Default false. */ traceRecall?: boolean; /** Persist recall traces to local JSONL files. Default false. */ traceRecallPersist?: boolean; /** Directory for JSONL recall trace files. */ traceRecallDir?: string; /** Number of days to retain persisted trace files. */ traceRecallRetentionDays?: number; /** Number of recent persisted days to preload on startup. */ traceRecallLoadRecentDays?: number; /** Maximum in-memory recall trace entries. */ traceRecallMaxEntries?: number; /** Maximum candidate results stored per search in trace. */ traceRecallMaxResultsPerSearch?: number; /** Preview character limit for persisted trace summaries. */ traceRecallPreviewChars?: number; /** Maximum query characters preserved in trace. */ traceRecallQueryMaxChars?: number; /** Maximum days to scan when querying persisted traces without explicit time bounds. */ traceRecallQueryMaxDays?: number; /** Whether trace queries include full content by default. */ traceRecallIncludeContentByDefault?: boolean; /** Whether raw user text preview may be persisted. Default false. */ traceRecallIncludeRawUserPreview?: boolean; /** Auto-recall target resource types. Empty means the backward-compatible memory recall set. */ recallTargetTypes?: Array<"resource" | "user" | "agent"> | string; /** Agent-visible add_resource tool is disabled by default; manual /add-resource remains available. */ enableAddResourceTool?: boolean; /** Agent-visible tool allowlist. Supports exact tool names or groups such as "memory" and "resource_query". */ enabledTools?: string[] | string; /** Agent-visible tool blocklist applied after enabledTools. Supports exact tool names or groups. */ disabledTools?: string[] | string; /** Optional JSON file path for runtime query config overrides. Empty means in-memory only. */ runtimeQueryConfigPath?: string; agentExperience?: { enabled?: boolean; recallLimit?: number; scoreThreshold?: number; maxInjectedChars?: number; minQueryChars?: number; }; }; /** Runtime config after memoryOpenVikingConfigSchema.parse() has applied defaults. */ export type ParsedMemoryOpenVikingConfig = Required< Omit > & { /** parse() resolves SecretRef values, so the runtime shape is always a plain string. */ apiKey: string; agentExperience: Required>; recallTargetTypes: Array<"resource" | "user" | "agent">; }; const DEFAULT_BASE_URL = "http://127.0.0.1:1933"; const DEFAULT_TARGET_URI = "viking://~/memories"; const DEFAULT_TIMEOUT_MS = 15000; const DEFAULT_CAPTURE_MODE = "semantic"; const DEFAULT_CAPTURE_MAX_LENGTH = 24000; const DEFAULT_AUTO_RECALL_TIMEOUT_MS = 5000; const DEFAULT_RECALL_LIMIT = 6; const DEFAULT_RECALL_SCORE_THRESHOLD = 0.15; const DEFAULT_RECALL_MAX_CONTENT_CHARS = 5000; const DEFAULT_RECALL_PREFER_ABSTRACT = false; const DEFAULT_RECALL_MAX_INJECTED_CHARS = 4000; const DEFAULT_COMMIT_TOKEN_THRESHOLD_RATIO = 0.5; const DEFAULT_COMMIT_KEEP_RECENT_COUNT = 10; const DEFAULT_BYPASS_SESSION_PATTERNS: string[] = []; const DEFAULT_EMIT_STANDARD_DIAGNOSTICS = false; const DEFAULT_PEER_ROLE = "assistant" as const; const DEFAULT_PEER_PREFIX = ""; const DEFAULT_TRACE_RECALL_DIR = "~/.openclaw/openviking/recall-traces"; const DEFAULT_TRACE_RECALL_RETENTION_DAYS = 14; const DEFAULT_TRACE_RECALL_LOAD_RECENT_DAYS = 2; const DEFAULT_TRACE_RECALL_MAX_ENTRIES = 1000; const DEFAULT_TRACE_RECALL_MAX_RESULTS_PER_SEARCH = 20; const DEFAULT_TRACE_RECALL_PREVIEW_CHARS = 240; const DEFAULT_TRACE_RECALL_QUERY_MAX_CHARS = 4000; const DEFAULT_TRACE_RECALL_QUERY_MAX_DAYS = 14; const ALLOWED_RECALL_TARGET_TYPES = ["resource", "user", "agent"] as const; const DEFAULT_RECALL_TARGET_TYPES = ["user", "agent"] as const; type RecallTargetType = typeof ALLOWED_RECALL_TARGET_TYPES[number]; export const OPENVIKING_ADD_RESOURCE_TOOL_NAME = "add_resource" as const; export const OPENVIKING_DEFAULT_ENABLED_TOOL_NAMES = [ "add_skill", "ov_search", "ov_read", "ov_multi_read", "ov_list", "memory_recall", "ov_recall_trace", "memory_store", "memory_forget", "ov_archive_search", "ov_archive_expand", "openviking_tool_result_read", "openviking_tool_result_search", "openviking_tool_result_list", ] as const; export const OPENVIKING_ALL_TOOL_NAMES = [ OPENVIKING_ADD_RESOURCE_TOOL_NAME, ...OPENVIKING_DEFAULT_ENABLED_TOOL_NAMES, ] as const; export type OpenVikingToolName = typeof OPENVIKING_ALL_TOOL_NAMES[number]; export const OPENVIKING_TOOL_GROUPS: Record = { all: OPENVIKING_ALL_TOOL_NAMES, default: OPENVIKING_DEFAULT_ENABLED_TOOL_NAMES, memory: ["memory_recall", "memory_store", "memory_forget"], resource_query: ["ov_search", "ov_read", "ov_multi_read", "ov_list"], import: ["add_resource", "add_skill"], recall_trace: ["ov_recall_trace"], archive: ["ov_archive_search", "ov_archive_expand"], tool_result: [ "openviking_tool_result_read", "openviking_tool_result_search", "openviking_tool_result_list", ], }; const DEFAULT_AGENT_EXPERIENCE = { enabled: false, recallLimit: 3, scoreThreshold: 0.35, maxInjectedChars: 6000, minQueryChars: 12, }; function resolvePeerPrefix(configured: unknown): string { if (typeof configured === "string" && configured.trim()) { const trimmed = configured.trim(); return trimmed === "default" ? DEFAULT_PEER_PREFIX : trimmed; } return DEFAULT_PEER_PREFIX; } /** * Resolve an {@link OpenVikingSecretRef} to the actual secret string. Plain * strings pass through untouched so callers can compose this with the legacy * `resolveEnvVars` pipeline without branching. * * Intentional behaviours: * * `env`: uses `getEnv` (process.env but exposed for vitest overrides). An * unset / empty env var is an error — the user asked for a named secret * so silent empty-string fallback would mask misconfigurations. * * `file`: ~ expanded, then read as UTF-8, then leading/trailing whitespace * stripped (de-facto standard for files written with `echo xxx > key`). * Missing / unreadable files propagate the original node error with an * error prefix that names the OpenViking field, so the user knows which * secretRef failed to load. * * `exec`: rejected with a "not supported" error. The subprocess-based * resolver was removed because marketplace install scanners block plugins * whose shipped code can spawn processes; wrap the secret-manager CLI * output into an env var or file instead (`OPENVIKING_API_KEY=$(op ...)`). */ function resolveSecret( value: string | OpenVikingSecretRef | undefined | null, label: string, ): string { if (value === undefined || value === null) return ""; if (typeof value === "string") return value; if (!value || typeof value !== "object") { throw new Error( `OpenViking ${label} must be a plain string or a SecretRef object ` + `({source:"env"|"file", id})`, ); } const obj = value as Record; if (typeof obj.id !== "string" || !obj.id) { throw new Error(`OpenViking ${label} SecretRef requires a non-empty string "id"`); } const id = obj.id; switch (obj.source) { case "env": { const envValue = getEnv(id); if (!envValue) { throw new Error( `OpenViking ${label} SecretRef env source: environment variable ${id} is not set or empty`, ); } return envValue; } case "file": { const resolvedPath = expandHomeDir(id); try { const contents = readFileSync(resolvedPath, "utf8"); return contents.trim(); } catch (err) { const msg = err instanceof Error ? err.message : String(err); throw new Error( `OpenViking ${label} SecretRef file source: cannot read ${resolvedPath} (${msg})`, ); } } case "exec": { throw new Error( `OpenViking ${label} SecretRef exec source is not supported in the packaged plugin ` + `(marketplace install scanners block subprocess execution). Export the secret to an ` + `environment variable ({source:"env"}) or a file ({source:"file"}) instead, e.g. ` + `OPENVIKING_API_KEY=$(op read ${id}).`, ); } default: throw new Error( `OpenViking ${label} SecretRef has unknown source "${String( (obj as Record).source, )}". Supported: "env" | "file".`, ); } } function resolvePeerRole(configured: unknown) { if (typeof configured === "string") { const role = configured.trim().toLowerCase(); if (role === "none" || role === "assistant" || role === "person") { return role; } throw new Error(`openviking peer_role must be "none", "assistant", or "person"`); } if (configured !== undefined) { throw new Error(`openviking peer_role must be "none", "assistant", or "person"`); } return DEFAULT_PEER_ROLE; } function resolveEnvVars(value: string): string { return value.replace(/\$\{([^}]+)\}/g, (_, envVar) => { const envValue = getEnv(envVar); if (!envValue) { throw new Error(`Environment variable ${envVar} is not set`); } return envValue; }); } function expandHomeDir(value: string): string { if (value === "~") { return homedir(); } if (value.startsWith("~/")) { return `${homedir()}${value.slice(1)}`; } return value; } function toNumber(value: unknown, fallback: number): number { if (typeof value === "number" && Number.isFinite(value)) { return value; } if (typeof value === "string" && value.trim() !== "") { const parsed = Number(value); if (Number.isFinite(parsed)) { return parsed; } } return fallback; } function toStringArray(value: unknown, fallback: string[]): string[] { if (Array.isArray(value)) { return value .filter((entry): entry is string => typeof entry === "string") .map((entry) => entry.trim()) .filter(Boolean); } if (typeof value === "string") { return value .split(/[,\n]/) .map((entry) => entry.trim()) .filter(Boolean); } return fallback; } function toIntegerInRange(value: unknown, fallback: number, min: number, max: number): number { return Math.max(min, Math.min(max, Math.floor(toNumber(value, fallback)))); } function normalizeRecallTargetTypes(value: unknown, includeResources = false): RecallTargetType[] { const entries = toStringArray(value, [...DEFAULT_RECALL_TARGET_TYPES]); const seen = new Set(); const normalized: RecallTargetType[] = []; const unknown: string[] = []; for (const entry of entries) { if ((ALLOWED_RECALL_TARGET_TYPES as readonly string[]).includes(entry)) { const typed = entry as RecallTargetType; if (!seen.has(typed)) { seen.add(typed); normalized.push(typed); } } else { unknown.push(entry); } } if (unknown.length > 0) { throw new Error(`openviking recallTargetTypes contains unknown resource types: ${unknown.join(", ")}`); } const result = normalized.length > 0 ? normalized : [...DEFAULT_RECALL_TARGET_TYPES]; if (includeResources && !seen.has("resource")) { result.push("resource"); } return result; } function toRecord(value: unknown): Record { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; } function toStringRecord(value: unknown, label: string): Record { if (value === undefined || value === null) { return {}; } if (typeof value !== "object" || Array.isArray(value)) { throw new Error(`${label} must be an object`); } const record = value as Record; const out: Record = {}; for (const [key, val] of Object.entries(record)) { if (typeof val !== "string") { throw new Error(`${label}.${key} must be a string`); } out[key] = val; } return out; } function expandToolSelectors(value: unknown, fallback: string[], label: string): OpenVikingToolName[] { const entries = toStringArray(value, fallback); const seen = new Set(); const normalized: OpenVikingToolName[] = []; const unknown: string[] = []; for (const rawEntry of entries) { const entry = rawEntry.trim(); const group = OPENVIKING_TOOL_GROUPS[entry]; const tools = group ?? ((OPENVIKING_ALL_TOOL_NAMES as readonly string[]).includes(entry) ? [entry as OpenVikingToolName] : undefined); if (!tools) { unknown.push(entry); continue; } for (const tool of tools) { if (!seen.has(tool)) { seen.add(tool); normalized.push(tool); } } } if (unknown.length > 0) { throw new Error(`openviking ${label} contains unknown tool selectors: ${unknown.join(", ")}`); } return normalized; } function normalizeEnabledTools(cfg: Record): { enabledTools: OpenVikingToolName[]; disabledTools: OpenVikingToolName[]; } { const enableAddResourceTool = cfg.enableAddResourceTool === true; const defaultTools = enableAddResourceTool ? [OPENVIKING_ADD_RESOURCE_TOOL_NAME, ...OPENVIKING_DEFAULT_ENABLED_TOOL_NAMES] : [...OPENVIKING_DEFAULT_ENABLED_TOOL_NAMES]; const selected = expandToolSelectors(cfg.enabledTools, defaultTools, "enabledTools"); const disabled = expandToolSelectors(cfg.disabledTools, [], "disabledTools"); const disabledSet = new Set(disabled); if (!enableAddResourceTool) { disabledSet.add(OPENVIKING_ADD_RESOURCE_TOOL_NAME); } const enabledTools = selected.filter((tool) => !disabledSet.has(tool) && (tool !== OPENVIKING_ADD_RESOURCE_TOOL_NAME || enableAddResourceTool) ); return { enabledTools, disabledTools: Array.from(disabledSet), }; } /** True when env is 1 / true / yes (case-insensitive). Used for debug flags without editing plugin JSON. */ function envFlag(name: string): boolean { const v = getEnv(name); if (v == null || v === "") { return false; } const t = String(v).trim().toLowerCase(); return t === "1" || t === "true" || t === "yes"; } function assertAllowedKeys(value: Record, allowed: string[], label: string) { const unknown = Object.keys(value).filter((key) => !allowed.includes(key)); if (unknown.length === 0) { return; } throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`); } function resolveDefaultBaseUrl(): string { const fromEnv = getEnv("OPENVIKING_BASE_URL") || getEnv("OPENVIKING_URL"); if (fromEnv) { return fromEnv; } return DEFAULT_BASE_URL; } export const memoryOpenVikingConfigSchema = { parse(value: unknown): ParsedMemoryOpenVikingConfig { if (!value || typeof value !== "object" || Array.isArray(value)) { value = {}; } const cfg = value as Record; assertAllowedKeys( cfg, [ "mode", "baseUrl", "peer_role", "peer_prefix", "serverAuthMode", "apiKey", "headers", "accountId", "userId", "targetUri", "timeoutMs", "autoCapture", "captureMode", "captureMaxLength", "autoRecall", "autoRecallTimeoutMs", "recallResources", "recallLimit", "recallScoreThreshold", "recallMaxInjectedChars", "recallMaxContentChars", "recallPreferAbstract", "recallTokenBudget", "commitTokenThreshold", "commitTokenThresholdRatio", "commitKeepRecentCount", "bypassSessionPatterns", "ingestReplyAssist", "ingestReplyAssistMinSpeakerTurns", "ingestReplyAssistMinChars", "ingestReplyAssistIgnoreSessionPatterns", "emitStandardDiagnostics", "logFindRequests", "traceRecall", "traceRecallPersist", "traceRecallDir", "traceRecallRetentionDays", "traceRecallLoadRecentDays", "traceRecallMaxEntries", "traceRecallMaxResultsPerSearch", "traceRecallPreviewChars", "traceRecallQueryMaxChars", "traceRecallQueryMaxDays", "traceRecallIncludeContentByDefault", "traceRecallIncludeRawUserPreview", "recallTargetTypes", "enableAddResourceTool", "enabledTools", "disabledTools", "runtimeQueryConfigPath", "agentExperience", ], "openviking config", ); const agentExperienceRaw = toRecord(cfg.agentExperience); assertAllowedKeys( agentExperienceRaw, ["enabled", "recallLimit", "scoreThreshold", "maxInjectedChars", "minQueryChars"], "openviking config agentExperience", ); const mode = "remote" as const; const peerRole = resolvePeerRole(cfg.peer_role); const peerPrefix = resolvePeerPrefix(cfg.peer_prefix); const rawBaseUrl = typeof cfg.baseUrl === "string" ? cfg.baseUrl : resolveDefaultBaseUrl(); const resolvedBaseUrl = resolveEnvVars(rawBaseUrl).replace(/\/+$/, ""); // Support plain string, SecretRef object, and OPENVIKING_API_KEY fallback. // A user writing a SecretRef has explicitly opted out of the bare env // interpolation path, so the fallback kicks in only when nothing is // configured at all (matches existing behaviour). const rawApiKey: string | OpenVikingSecretRef | undefined = cfg.apiKey !== undefined && cfg.apiKey !== null ? (cfg.apiKey as string | OpenVikingSecretRef) : getEnv("OPENVIKING_API_KEY") || undefined; const captureMode = cfg.captureMode; if ( typeof captureMode !== "undefined" && captureMode !== "semantic" && captureMode !== "keyword" ) { throw new Error(`openviking captureMode must be "semantic" or "keyword"`); } const accountId = typeof cfg.accountId === "string" && cfg.accountId.trim() ? cfg.accountId.trim() : (getEnv("OPENVIKING_ACCOUNT_ID")?.trim() || ""); const userId = typeof cfg.userId === "string" && cfg.userId.trim() ? cfg.userId.trim() : (getEnv("OPENVIKING_USER_ID")?.trim() || ""); const recallMaxInjectedChars = Math.max( 100, Math.min( 50000, Math.floor( toNumber( cfg.recallMaxInjectedChars, toNumber(cfg.recallTokenBudget, DEFAULT_RECALL_MAX_INJECTED_CHARS), ), ), ), ); const recallResources = cfg.recallResources === true || envFlag("OPENVIKING_RECALL_RESOURCES"); const recallTargetTypes = normalizeRecallTargetTypes( cfg.recallTargetTypes, !("recallTargetTypes" in cfg) && recallResources, ); const { enabledTools, disabledTools } = normalizeEnabledTools(cfg); return { mode, baseUrl: resolvedBaseUrl, peer_role: peerRole, peer_prefix: peerPrefix, apiKey: rawApiKey ? resolveEnvVars(resolveSecret(rawApiKey, "config.apiKey")) : "", headers: toStringRecord(cfg.headers, "openviking config headers"), accountId, userId, targetUri: typeof cfg.targetUri === "string" ? cfg.targetUri : DEFAULT_TARGET_URI, timeoutMs: Math.max(1000, Math.floor(toNumber(cfg.timeoutMs, DEFAULT_TIMEOUT_MS))), autoCapture: cfg.autoCapture !== false, captureMode: captureMode ?? DEFAULT_CAPTURE_MODE, captureMaxLength: Math.max( 200, Math.min(200_000, Math.floor(toNumber(cfg.captureMaxLength, DEFAULT_CAPTURE_MAX_LENGTH))), ), autoRecall: cfg.autoRecall !== false, autoRecallTimeoutMs: Math.max( 1000, Math.min(300_000, Math.floor(toNumber(cfg.autoRecallTimeoutMs, DEFAULT_AUTO_RECALL_TIMEOUT_MS))), ), recallResources, recallLimit: Math.max(1, Math.floor(toNumber(cfg.recallLimit, DEFAULT_RECALL_LIMIT))), recallScoreThreshold: Math.min( 1, Math.max(0, toNumber(cfg.recallScoreThreshold, DEFAULT_RECALL_SCORE_THRESHOLD)), ), recallMaxContentChars: Math.max( 50, Math.min(10000, Math.floor(toNumber(cfg.recallMaxContentChars, DEFAULT_RECALL_MAX_CONTENT_CHARS))), ), recallPreferAbstract: typeof cfg.recallPreferAbstract === "boolean" ? cfg.recallPreferAbstract : DEFAULT_RECALL_PREFER_ABSTRACT, recallMaxInjectedChars, recallTokenBudget: recallMaxInjectedChars, commitTokenThresholdRatio: Math.max( 0, Math.min(1, toNumber(cfg.commitTokenThresholdRatio, DEFAULT_COMMIT_TOKEN_THRESHOLD_RATIO)), ), commitKeepRecentCount: Math.max( 0, Math.min( 1_000, Math.floor(toNumber(cfg.commitKeepRecentCount, DEFAULT_COMMIT_KEEP_RECENT_COUNT)), ), ), bypassSessionPatterns: toStringArray( cfg.bypassSessionPatterns, toStringArray( cfg.ingestReplyAssistIgnoreSessionPatterns, DEFAULT_BYPASS_SESSION_PATTERNS, ), ), emitStandardDiagnostics: typeof cfg.emitStandardDiagnostics === "boolean" ? cfg.emitStandardDiagnostics : DEFAULT_EMIT_STANDARD_DIAGNOSTICS, logFindRequests: cfg.logFindRequests === true || envFlag("OPENVIKING_LOG_ROUTING") || envFlag("OPENVIKING_DEBUG"), traceRecall: cfg.traceRecall === true, traceRecallPersist: cfg.traceRecallPersist === true, traceRecallDir: typeof cfg.traceRecallDir === "string" && cfg.traceRecallDir.trim() ? expandHomeDir(cfg.traceRecallDir.trim()) : expandHomeDir(DEFAULT_TRACE_RECALL_DIR), traceRecallRetentionDays: toIntegerInRange( cfg.traceRecallRetentionDays, DEFAULT_TRACE_RECALL_RETENTION_DAYS, 1, 3650, ), traceRecallLoadRecentDays: toIntegerInRange( cfg.traceRecallLoadRecentDays, DEFAULT_TRACE_RECALL_LOAD_RECENT_DAYS, 0, 3650, ), traceRecallMaxEntries: toIntegerInRange( cfg.traceRecallMaxEntries, DEFAULT_TRACE_RECALL_MAX_ENTRIES, 1, 1_000_000, ), traceRecallMaxResultsPerSearch: toIntegerInRange( cfg.traceRecallMaxResultsPerSearch, DEFAULT_TRACE_RECALL_MAX_RESULTS_PER_SEARCH, 1, 1_000, ), traceRecallPreviewChars: toIntegerInRange( cfg.traceRecallPreviewChars, DEFAULT_TRACE_RECALL_PREVIEW_CHARS, 20, 10_000, ), traceRecallQueryMaxChars: toIntegerInRange( cfg.traceRecallQueryMaxChars, DEFAULT_TRACE_RECALL_QUERY_MAX_CHARS, 200, 200_000, ), traceRecallQueryMaxDays: toIntegerInRange( cfg.traceRecallQueryMaxDays, DEFAULT_TRACE_RECALL_QUERY_MAX_DAYS, 1, 3650, ), traceRecallIncludeContentByDefault: cfg.traceRecallIncludeContentByDefault === true, traceRecallIncludeRawUserPreview: cfg.traceRecallIncludeRawUserPreview === true, recallTargetTypes, enableAddResourceTool: cfg.enableAddResourceTool === true, enabledTools, disabledTools, runtimeQueryConfigPath: typeof cfg.runtimeQueryConfigPath === "string" && cfg.runtimeQueryConfigPath.trim() ? expandHomeDir(cfg.runtimeQueryConfigPath.trim()) : "", agentExperience: { enabled: typeof agentExperienceRaw.enabled === "boolean" ? agentExperienceRaw.enabled : DEFAULT_AGENT_EXPERIENCE.enabled, recallLimit: Math.max( 1, Math.min( 10, Math.floor(toNumber(agentExperienceRaw.recallLimit, DEFAULT_AGENT_EXPERIENCE.recallLimit)), ), ), scoreThreshold: Math.min( 1, Math.max(0, toNumber(agentExperienceRaw.scoreThreshold, DEFAULT_AGENT_EXPERIENCE.scoreThreshold)), ), maxInjectedChars: Math.max( 500, Math.min( 50_000, Math.floor(toNumber(agentExperienceRaw.maxInjectedChars, DEFAULT_AGENT_EXPERIENCE.maxInjectedChars)), ), ), minQueryChars: Math.max( 1, Math.min( 500, Math.floor(toNumber(agentExperienceRaw.minQueryChars, DEFAULT_AGENT_EXPERIENCE.minQueryChars)), ), ), }, }; }, uiHints: { baseUrl: { label: "OpenViking Base URL", placeholder: DEFAULT_BASE_URL, help: "HTTP URL when mode is remote (or use ${OPENVIKING_BASE_URL})", }, peer_role: { label: "Peer Role", placeholder: DEFAULT_PEER_ROLE, help: 'Controls which session messages get peer_id: "none", "assistant", or "person".', }, peer_prefix: { label: "Peer Prefix", placeholder: "optional-prefix", help: "Optional prefix applied to assistant peer_id values derived from OpenClaw runtime agent IDs.", }, apiKey: { label: "OpenViking API Key", sensitive: true, placeholder: "${OPENVIKING_API_KEY}", help: "Optional API key for OpenViking server. Accepts a plain string, " + "${ENV_VAR} interpolation, or a SecretRef object ({source: env/file, id}). " + "Prefer the SecretRef shapes so the key never sits as plaintext in openclaw.json.", }, headers: { label: "Headers", advanced: true, help: "Optional HTTP headers merged into every OpenViking request.", }, accountId: { label: "Account ID", placeholder: "(derived from API key)", help: "Advanced option. Tenant account ID. Only needed when explicitly sending identity headers, such as root-key or trusted deployments. With a user key the server derives identity from the key.", advanced: true, }, userId: { label: "User ID", placeholder: "(derived from API key)", help: "Advanced option. Tenant user ID. Only needed when explicitly sending identity headers.", advanced: true, }, targetUri: { label: "Search Target URI", placeholder: DEFAULT_TARGET_URI, help: "Default OpenViking target URI for memory search", }, timeoutMs: { label: "Request Timeout (ms)", placeholder: String(DEFAULT_TIMEOUT_MS), advanced: true, }, autoCapture: { label: "Auto-Capture", help: "Extract memories from recent conversation messages via OpenViking sessions", }, captureMode: { label: "Capture Mode", placeholder: DEFAULT_CAPTURE_MODE, advanced: true, help: '"semantic" captures all eligible user text and relies on OpenViking extraction; "keyword" uses trigger regex first.', }, captureMaxLength: { label: "Capture Max Length", placeholder: String(DEFAULT_CAPTURE_MAX_LENGTH), advanced: true, help: "Maximum sanitized user text length allowed for auto-capture.", }, autoRecall: { label: "Auto-Recall", help: "Inject relevant OpenViking memories into agent context", }, autoRecallTimeoutMs: { label: "Auto-Recall Timeout (ms)", placeholder: String(DEFAULT_AUTO_RECALL_TIMEOUT_MS), advanced: true, help: "Outer time budget for the whole auto-recall flow, including search, ranking, and memory reads.", }, recallResources: { label: "Recall Resources", help: "Include resources (viking://resources) in auto-recall and default memory_recall search. Enables account-level shared knowledge retrieval.", advanced: true, }, recallTargetTypes: { label: "Recall Target Types", placeholder: "user,agent", help: "Comma-separated auto-recall and default memory_recall targets: user, agent, resource. Session history is available through ov_archive_search and ov_archive_expand.", advanced: true, }, recallLimit: { label: "Recall Limit", placeholder: String(DEFAULT_RECALL_LIMIT), advanced: true, }, recallScoreThreshold: { label: "Recall Score Threshold", placeholder: String(DEFAULT_RECALL_SCORE_THRESHOLD), advanced: true, }, recallMaxInjectedChars: { label: "Recall Max Injected Chars", placeholder: String(DEFAULT_RECALL_MAX_INJECTED_CHARS), advanced: true, help: "Maximum total characters for auto-recall memory injection. Complete memories that do not fit are skipped, not truncated.", }, recallMaxContentChars: { label: "Deprecated Recall Max Content Chars", placeholder: String(DEFAULT_RECALL_MAX_CONTENT_CHARS), advanced: true, help: "Deprecated compatibility option and will be removed in a future release. Auto-recall now keeps individual memories intact and uses recallMaxInjectedChars.", }, recallPreferAbstract: { label: "Recall Prefer Abstract", advanced: true, help: "Use memory abstract instead of fetching full content when abstract is available. Reduces token usage.", }, recallTokenBudget: { label: "Deprecated Recall Token Budget", placeholder: String(DEFAULT_RECALL_MAX_INJECTED_CHARS), advanced: true, help: "Deprecated compatibility alias and will be removed in a future release. Use recallMaxInjectedChars.", }, bypassSessionPatterns: { label: "Bypass Session Patterns", placeholder: "agent:*:cron:**", help: "Completely bypass OpenViking for matching session keys. Use * within one segment and ** across segments.", advanced: true, }, commitTokenThresholdRatio: { label: "Commit Token Threshold Ratio", placeholder: String(DEFAULT_COMMIT_TOKEN_THRESHOLD_RATIO), advanced: true, 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.", }, commitKeepRecentCount: { label: "Commit Keep Recent Count", placeholder: String(DEFAULT_COMMIT_KEEP_RECENT_COUNT), advanced: true, help: "Number of most-recent messages to keep live after an afterTurn commit. " + "Forwarded as keep_recent_count to the server. Compact path always uses 0.", }, emitStandardDiagnostics: { label: "Standard diagnostics (diag JSON lines)", advanced: true, help: "When enabled, emit structured openviking: diag {...} lines for assemble and afterTurn. Disable to reduce log noise.", }, logFindRequests: { label: "Log find requests", help: "Log tenant routing: POST /api/v1/search/find (query, target_uri) and session POST .../messages + .../commit (sessionId, X-OpenViking-*). Never logs apiKey. " + "Or set env OPENVIKING_LOG_ROUTING=1 or OPENVIKING_DEBUG=1 (no JSON edit).", advanced: true, }, traceRecall: { label: "Trace Recall", placeholder: "false", help: "Enable best-effort recall trace recording for debugging recall and search decisions.", advanced: true, }, traceRecallPersist: { label: "Persist Recall Trace", placeholder: "false", help: "Persist recall traces to local JSONL files. Disabled by default.", advanced: true, }, traceRecallDir: { label: "Recall Trace Directory", placeholder: DEFAULT_TRACE_RECALL_DIR, help: "Directory for persisted recall trace JSONL files.", advanced: true, }, enableAddResourceTool: { label: "Enable Add Resource Tool", placeholder: "false", help: "Disabled by default so search and read flows cannot call add_resource. Set true only when agents should import resources; manual /add-resource remains available.", advanced: true, }, enabledTools: { label: "Enabled Tools", placeholder: "default", help: "Agent-visible tool allowlist. Accepts tool names or groups: default, all, memory, resource_query, import, recall_trace, archive, tool_result. add_resource also requires enableAddResourceTool=true.", advanced: true, }, disabledTools: { label: "Disabled Tools", placeholder: "memory", help: "Agent-visible tool blocklist applied after enabledTools. Accepts the same tool names or groups.", advanced: true, }, runtimeQueryConfigPath: { label: "Runtime Query Config Path", placeholder: "~/.openclaw/openviking/runtime-query-config.json", help: "Optional JSON file for /ov-query-config runtime overrides. Empty keeps overrides in memory only.", advanced: true, }, }, };