@openviking/openclaw-plugin 2026.5.26 → 2026.6.17-dev.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.
Files changed (112) hide show
  1. package/INSTALL-AGENT.md +355 -355
  2. package/INSTALL-ZH.md +405 -345
  3. package/INSTALL.md +366 -362
  4. package/README.md +364 -338
  5. package/adapters/http-transport.ts +3 -0
  6. package/adapters/resource-packager.ts +141 -0
  7. package/auto-recall.ts +518 -257
  8. package/client.ts +963 -939
  9. package/commands/setup.ts +1169 -1002
  10. package/config/feature-gates.json +10 -0
  11. package/config.ts +819 -476
  12. package/context-engine.ts +408 -1737
  13. package/dist/adapters/http-transport.js +1 -0
  14. package/dist/adapters/resource-packager.js +114 -0
  15. package/dist/auto-recall.js +228 -32
  16. package/dist/client.js +125 -161
  17. package/dist/commands/setup.js +170 -32
  18. package/dist/config.js +285 -64
  19. package/dist/context-engine.js +64 -1179
  20. package/dist/index.js +209 -1470
  21. package/dist/memory-ranking.js +35 -12
  22. package/dist/plugin/command-registration.js +5 -0
  23. package/dist/plugin/openviking-archive-tools.js +210 -0
  24. package/dist/plugin/openviking-bypass-runtime.js +8 -0
  25. package/dist/plugin/openviking-client-runtime.js +28 -0
  26. package/dist/plugin/openviking-command-args.js +146 -0
  27. package/dist/plugin/openviking-command-definitions.js +206 -0
  28. package/dist/plugin/openviking-context-engine-ref.js +11 -0
  29. package/dist/plugin/openviking-context-engine-registration.js +23 -0
  30. package/dist/plugin/openviking-feature-gates.js +250 -0
  31. package/dist/plugin/openviking-import-runtime.js +50 -0
  32. package/dist/plugin/openviking-import-tools.js +86 -0
  33. package/dist/plugin/openviking-lifecycle-hooks.js +33 -0
  34. package/dist/plugin/openviking-memory-recall-tools.js +233 -0
  35. package/dist/plugin/openviking-memory-tools.js +199 -0
  36. package/dist/plugin/openviking-query-config-command.js +203 -0
  37. package/dist/plugin/openviking-query-runtime.js +367 -0
  38. package/dist/plugin/openviking-query-tools.js +91 -0
  39. package/dist/plugin/openviking-recall-trace-runtime.js +387 -0
  40. package/dist/plugin/openviking-recall-trace-tools.js +38 -0
  41. package/dist/plugin/openviking-runtime-state.js +26 -0
  42. package/dist/plugin/openviking-runtime-utils.js +62 -0
  43. package/dist/plugin/openviking-services.js +20 -0
  44. package/dist/plugin/openviking-session-routing-runtime.js +61 -0
  45. package/dist/plugin/openviking-tool-result-tools.js +268 -0
  46. package/dist/plugin/recall-trace-routes.js +77 -0
  47. package/dist/plugin/tool-registration.js +23 -0
  48. package/dist/process-manager.js +5 -17
  49. package/dist/query-config.js +336 -0
  50. package/dist/recall-trace.js +309 -0
  51. package/dist/registries/recall-resource-types.js +58 -0
  52. package/dist/routing/identity-routing.js +152 -0
  53. package/dist/routing/memory-uri.js +7 -0
  54. package/dist/services/context-lifecycle-service.js +847 -0
  55. package/dist/services/context-message-adapter.js +340 -0
  56. package/dist/session-transcript-repair.js +0 -1
  57. package/dist/text-utils.js +14 -31
  58. package/dist/token-estimator.js +51 -0
  59. package/index.ts +408 -1985
  60. package/install-manifest.json +57 -47
  61. package/memory-ranking.ts +303 -267
  62. package/openclaw.plugin.json +497 -335
  63. package/package.json +86 -76
  64. package/plugin/command-registration.ts +34 -0
  65. package/plugin/openviking-archive-tools.ts +301 -0
  66. package/plugin/openviking-bypass-runtime.ts +26 -0
  67. package/plugin/openviking-client-runtime.ts +74 -0
  68. package/plugin/openviking-command-args.ts +187 -0
  69. package/plugin/openviking-command-definitions.ts +294 -0
  70. package/plugin/openviking-context-engine-ref.ts +16 -0
  71. package/plugin/openviking-context-engine-registration.ts +97 -0
  72. package/plugin/openviking-feature-gates.ts +335 -0
  73. package/plugin/openviking-import-runtime.ts +105 -0
  74. package/plugin/openviking-import-tools.ts +133 -0
  75. package/plugin/openviking-lifecycle-hooks.ts +65 -0
  76. package/plugin/openviking-memory-recall-tools.ts +368 -0
  77. package/plugin/openviking-memory-tools.ts +299 -0
  78. package/plugin/openviking-query-config-command.ts +230 -0
  79. package/plugin/openviking-query-formatters.ts +108 -0
  80. package/plugin/openviking-query-runtime.ts +476 -0
  81. package/plugin/openviking-query-tools.ts +161 -0
  82. package/plugin/openviking-recall-trace-runtime.ts +482 -0
  83. package/plugin/openviking-recall-trace-tools.ts +99 -0
  84. package/plugin/openviking-runtime-state.ts +52 -0
  85. package/plugin/openviking-runtime-utils.ts +67 -0
  86. package/plugin/openviking-services.ts +49 -0
  87. package/plugin/openviking-session-routing-runtime.ts +105 -0
  88. package/plugin/openviking-tool-result-tools.ts +397 -0
  89. package/plugin/recall-trace-routes.ts +133 -0
  90. package/plugin/tool-registration.ts +51 -0
  91. package/process-manager.ts +41 -45
  92. package/query-config.ts +400 -0
  93. package/recall-trace.ts +442 -0
  94. package/registries/openviking-tools.ts +84 -0
  95. package/registries/recall-resource-types.ts +74 -0
  96. package/routing/identity-routing.ts +210 -0
  97. package/routing/memory-uri.ts +8 -0
  98. package/runtime-utils.ts +5 -5
  99. package/services/context-lifecycle-service.ts +1209 -0
  100. package/services/context-message-adapter.ts +375 -0
  101. package/services/setup/config-writer.ts +125 -0
  102. package/services/setup/exit-utils.ts +5 -0
  103. package/services/setup/package-metadata.ts +56 -0
  104. package/services/setup/probe-service.ts +143 -0
  105. package/services/setup/setup-flow.ts +306 -0
  106. package/services/setup/version-compatibility.ts +41 -0
  107. package/session-transcript-repair.ts +528 -530
  108. package/skills/install-openviking-memory/SKILL.md +752 -665
  109. package/skills/openviking-context-database/SKILL.md +286 -0
  110. package/text-utils.ts +488 -507
  111. package/token-estimator.ts +65 -0
  112. package/tool-call-id.ts +331 -331
@@ -0,0 +1,133 @@
1
+ export type RecallTraceRouteRequest = {
2
+ query?: Record<string, unknown>;
3
+ params?: Record<string, unknown>;
4
+ url?: string;
5
+ };
6
+
7
+ export type RecallTraceRouteResult = {
8
+ status?: number;
9
+ body?: unknown;
10
+ };
11
+
12
+ export type RecallTraceRouteHandler = (request?: RecallTraceRouteRequest) => Promise<RecallTraceRouteResult | unknown>;
13
+
14
+ export type RecallTraceRoute = {
15
+ method: "GET";
16
+ path: string;
17
+ handler: RecallTraceRouteHandler;
18
+ };
19
+
20
+ export type RecallTraceRouteAdapter = {
21
+ registerRoute?: (route: RecallTraceRoute) => void;
22
+ registerHttpRoute?: (route: {
23
+ path: string;
24
+ auth: "plugin";
25
+ match: "exact" | "prefix";
26
+ handler: (req: { method?: string; url?: string }, res: {
27
+ statusCode?: number;
28
+ setHeader?: (name: string, value: string) => void;
29
+ end?: (body?: string) => void;
30
+ }) => Promise<boolean>;
31
+ }) => void;
32
+ };
33
+
34
+ export type RecallTraceRouteHandlers = {
35
+ handleRecallTraces: RecallTraceRouteHandler;
36
+ handleUriDetail: RecallTraceRouteHandler;
37
+ handleLatestOvSearchList: RecallTraceRouteHandler;
38
+ };
39
+
40
+ export const RECALL_TRACE_ROUTE_PATHS = [
41
+ "/api/openviking/recall-traces",
42
+ "/api/openviking/uri-detail",
43
+ "/api/openviking/recall-traces/latest-ov-search-list",
44
+ "/api/openviking/recall-traces/:traceId",
45
+ ] as const;
46
+
47
+ export function registerRecallTraceRoutes(
48
+ ctx: unknown,
49
+ handlers: RecallTraceRouteHandlers,
50
+ ): boolean {
51
+ const routeAdapter = ctx as RecallTraceRouteAdapter | undefined;
52
+ const canRegisterLegacyRoute = typeof routeAdapter?.registerRoute === "function";
53
+ const canRegisterHttpRoute = typeof routeAdapter?.registerHttpRoute === "function";
54
+ if (!canRegisterLegacyRoute && !canRegisterHttpRoute) {
55
+ return false;
56
+ }
57
+
58
+ const handle = handlers.handleRecallTraces;
59
+ const routes: RecallTraceRoute[] = [
60
+ { method: "GET", path: RECALL_TRACE_ROUTE_PATHS[0], handler: handle },
61
+ { method: "GET", path: RECALL_TRACE_ROUTE_PATHS[1], handler: handlers.handleUriDetail },
62
+ { method: "GET", path: RECALL_TRACE_ROUTE_PATHS[2], handler: handlers.handleLatestOvSearchList },
63
+ {
64
+ method: "GET",
65
+ path: RECALL_TRACE_ROUTE_PATHS[3],
66
+ handler: (request?: RecallTraceRouteRequest) => handle({
67
+ ...request,
68
+ query: {
69
+ ...(request?.query ?? {}),
70
+ traceId: typeof request?.params?.traceId === "string" ? request.params.traceId : undefined,
71
+ },
72
+ }),
73
+ },
74
+ ];
75
+
76
+ for (const route of routes) {
77
+ routeAdapter?.registerRoute?.(route);
78
+ }
79
+
80
+ if (canRegisterHttpRoute) {
81
+ const sendJson = (
82
+ res: { statusCode?: number; setHeader?: (name: string, value: string) => void; end?: (body?: string) => void },
83
+ status: number,
84
+ body: unknown,
85
+ ) => {
86
+ res.statusCode = status;
87
+ res.setHeader?.("Cache-Control", "no-store");
88
+ res.setHeader?.("Content-Type", "application/json; charset=utf-8");
89
+ res.end?.(JSON.stringify(body));
90
+ };
91
+ const makeHttpHandler = (
92
+ route: RecallTraceRoute,
93
+ getParams?: (url: string) => Record<string, unknown>,
94
+ ) => async (
95
+ req: { method?: string; url?: string },
96
+ res: { statusCode?: number; setHeader?: (name: string, value: string) => void; end?: (body?: string) => void },
97
+ ) => {
98
+ if ((req.method ?? "GET").toUpperCase() !== route.method) {
99
+ sendJson(res, 405, { ok: false, error: { code: "method_not_allowed", message: `${route.method} is required` } });
100
+ return true;
101
+ }
102
+ const url = req.url ?? route.path;
103
+ const result = await route.handler({ url, params: getParams?.(url) });
104
+ const response = result as { status?: number; body?: unknown };
105
+ sendJson(res, typeof response.status === "number" ? response.status : 200, response.body ?? response);
106
+ return true;
107
+ };
108
+ for (const route of routes) {
109
+ if (route.path === RECALL_TRACE_ROUTE_PATHS[3]) {
110
+ const prefix = "/api/openviking/recall-traces/";
111
+ routeAdapter?.registerHttpRoute?.({
112
+ path: "/api/openviking/recall-traces",
113
+ auth: "plugin",
114
+ match: "prefix",
115
+ handler: makeHttpHandler(route, (url) => {
116
+ const pathname = new URL(url, "http://openclaw.local").pathname;
117
+ const traceId = pathname.startsWith(prefix) ? decodeURIComponent(pathname.slice(prefix.length)) : "";
118
+ return traceId ? { traceId } : {};
119
+ }),
120
+ });
121
+ } else {
122
+ routeAdapter?.registerHttpRoute?.({
123
+ path: route.path,
124
+ auth: "plugin",
125
+ match: "exact",
126
+ handler: makeHttpHandler(route),
127
+ });
128
+ }
129
+ }
130
+ }
131
+
132
+ return true;
133
+ }
@@ -0,0 +1,51 @@
1
+ export type OpenVikingToolRegistrationApi = {
2
+ registerTool: (toolOrFactory: unknown, opts: { name: string }) => void;
3
+ };
4
+
5
+ export type OpenVikingToolRegistrationLogger = {
6
+ debug?: (message: string) => void;
7
+ };
8
+
9
+ export type OpenVikingToolRegistrarOptions = {
10
+ api: OpenVikingToolRegistrationApi;
11
+ enabledToolNames: ReadonlySet<string>;
12
+ logger?: OpenVikingToolRegistrationLogger;
13
+ };
14
+
15
+ export type OpenVikingToolRegistrationRuntimeConfig = {
16
+ enabledTools: string[] | string;
17
+ };
18
+
19
+ export function createOpenVikingToolRegistrar({
20
+ api,
21
+ enabledToolNames,
22
+ logger,
23
+ }: OpenVikingToolRegistrarOptions) {
24
+ return (toolOrFactory: unknown, opts: { name: string }): void => {
25
+ if (!enabledToolNames.has(opts.name)) {
26
+ logger?.debug?.(`openviking: tool ${opts.name} disabled by config`);
27
+ return;
28
+ }
29
+ api.registerTool(toolOrFactory, opts);
30
+ };
31
+ }
32
+
33
+ export function createOpenVikingToolRegistrationRuntime<TConfig extends OpenVikingToolRegistrationRuntimeConfig>(options: {
34
+ api: OpenVikingToolRegistrationApi;
35
+ cfg: TConfig;
36
+ logger?: OpenVikingToolRegistrationLogger;
37
+ }) {
38
+ const enabledTools = Array.isArray(options.cfg.enabledTools)
39
+ ? options.cfg.enabledTools
40
+ : [options.cfg.enabledTools];
41
+ const enabledToolNames = new Set<string>(enabledTools);
42
+ const registerOpenVikingTool = createOpenVikingToolRegistrar({
43
+ api: options.api,
44
+ enabledToolNames,
45
+ logger: options.logger,
46
+ });
47
+
48
+ return {
49
+ registerOpenVikingTool,
50
+ };
51
+ }
@@ -1,45 +1,41 @@
1
- export function withTimeout<T>(promise: Promise<T>, timeoutMs: number, timeoutMessage: string): Promise<T> {
2
- return new Promise((resolve, reject) => {
3
- const timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
4
- promise.then(
5
- (value) => {
6
- clearTimeout(timer);
7
- resolve(value);
8
- },
9
- (err) => {
10
- clearTimeout(timer);
11
- reject(err);
12
- },
13
- );
14
- });
15
- }
16
-
17
- export async function quickHealthCheck(baseUrl: string, timeoutMs: number): Promise<boolean> {
18
- const controller = new AbortController();
19
- const timer = setTimeout(() => controller.abort(), timeoutMs);
20
- try {
21
- const response = await fetch(`${baseUrl}/health`, {
22
- method: "GET",
23
- signal: controller.signal,
24
- });
25
- if (!response.ok) {
26
- return false;
27
- }
28
- const body = (await response.json().catch(() => ({}))) as { status?: string };
29
- return body.status === "ok";
30
- } catch {
31
- return false;
32
- } finally {
33
- clearTimeout(timer);
34
- }
35
- }
36
-
37
- export async function quickRecallPrecheck(
38
- baseUrl: string,
39
- ): Promise<{ ok: true } | { ok: false; reason: string }> {
40
- const healthOk = await quickHealthCheck(baseUrl, 500);
41
- if (healthOk) {
42
- return { ok: true };
43
- }
44
- return { ok: false, reason: "health check failed" };
45
- }
1
+ import type { OpenVikingClient } from "./client.js";
2
+
3
+ export function withTimeout<T>(promise: Promise<T>, timeoutMs: number, timeoutMessage: string): Promise<T> {
4
+ return new Promise((resolve, reject) => {
5
+ const timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
6
+ promise.then(
7
+ (value) => {
8
+ clearTimeout(timer);
9
+ resolve(value);
10
+ },
11
+ (err) => {
12
+ clearTimeout(timer);
13
+ reject(err);
14
+ },
15
+ );
16
+ });
17
+ }
18
+
19
+ export async function quickHealthCheck(
20
+ client: OpenVikingClient,
21
+ agentId: string | undefined,
22
+ timeoutMs: number,
23
+ ): Promise<boolean> {
24
+ try {
25
+ await client.healthCheck(timeoutMs, agentId);
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ export async function quickRecallPrecheck(
33
+ client: OpenVikingClient,
34
+ agentId?: string,
35
+ ): Promise<{ ok: true } | { ok: false; reason: string }> {
36
+ const healthOk = await quickHealthCheck(client, agentId, 500);
37
+ if (healthOk) {
38
+ return { ok: true };
39
+ }
40
+ return { ok: false, reason: "health check failed" };
41
+ }
@@ -0,0 +1,400 @@
1
+ import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+
4
+ import type { MemoryOpenVikingConfig } from "./config.js";
5
+ import { normalizeRecallResourceTypes, type RecallResourceType } from "./registries/recall-resource-types.js";
6
+
7
+ export type RuntimeQueryParams = {
8
+ recallLimit?: number;
9
+ candidateLimit?: number;
10
+ candidateMultiplier?: number;
11
+ scoreThreshold?: number;
12
+ maxInjectedChars?: number;
13
+ recallPreferAbstract?: boolean;
14
+ resourceTypes?: RecallResourceType[] | string;
15
+ targetUri?: string;
16
+ ovSearchLimit?: number;
17
+ rankingWeights?: {
18
+ baseScore?: number;
19
+ leaf?: number;
20
+ event?: number;
21
+ preference?: number;
22
+ lexicalOverlapMax?: number;
23
+ };
24
+ resourceTypeWeights?: Partial<Record<RecallResourceType, number>>;
25
+ categoryWeights?: Record<string, number>;
26
+ };
27
+
28
+ type RuntimeQueryParamsPatch = RuntimeQueryParams & Record<string, unknown>;
29
+ type ConfigSource = "request" | "session" | "claw" | "static" | "default";
30
+ type RuntimeScope = "claw" | "session";
31
+
32
+ export type QueryConfigContext = {
33
+ peerId: string;
34
+ sessionId?: string;
35
+ sessionKey?: string;
36
+ ovSessionId?: string;
37
+ };
38
+
39
+ export type EffectiveQueryConfig = {
40
+ recallLimit: number;
41
+ candidateLimit: number;
42
+ candidateMultiplier: number;
43
+ scoreThreshold: number;
44
+ maxInjectedChars: number;
45
+ recallPreferAbstract: boolean;
46
+ resourceTypes: RecallResourceType[];
47
+ targetUri?: string;
48
+ ovSearchLimit: number;
49
+ rankingWeights: {
50
+ baseScore: number;
51
+ leaf: number;
52
+ event: number;
53
+ preference: number;
54
+ lexicalOverlapMax: number;
55
+ };
56
+ resourceTypeWeights: Record<string, number>;
57
+ categoryWeights: Record<string, number>;
58
+ sources: Record<string, ConfigSource>;
59
+ warnings: string[];
60
+ };
61
+
62
+ type RuntimeRecord = {
63
+ params: RuntimeQueryParams;
64
+ updatedAt: number;
65
+ updatedBy?: string;
66
+ peerId?: string;
67
+ expiresAt?: number;
68
+ };
69
+
70
+ type RuntimeFile = {
71
+ schemaVersion: "1.0";
72
+ updatedAt: number;
73
+ claws: Record<string, RuntimeRecord>;
74
+ sessions: Record<string, RuntimeRecord>;
75
+ };
76
+
77
+ const DEFAULT_CANDIDATE_MULTIPLIER = 4;
78
+ const DEFAULT_OV_SEARCH_LIMIT = 10;
79
+ const DEFAULT_RANKING_WEIGHTS = {
80
+ baseScore: 1,
81
+ leaf: 0.12,
82
+ event: 0.1,
83
+ preference: 0.08,
84
+ lexicalOverlapMax: 0.2,
85
+ };
86
+
87
+ function toNumber(value: unknown): number | undefined {
88
+ if (typeof value === "number" && Number.isFinite(value)) return value;
89
+ if (typeof value === "string" && value.trim() !== "") {
90
+ const parsed = Number(value);
91
+ if (Number.isFinite(parsed)) return parsed;
92
+ }
93
+ return undefined;
94
+ }
95
+
96
+ function clampInteger(value: unknown, min: number, max: number): number | undefined {
97
+ const n = toNumber(value);
98
+ if (n === undefined) return undefined;
99
+ return Math.max(min, Math.min(max, Math.floor(n)));
100
+ }
101
+
102
+ function clampNumber(value: unknown, min: number, max: number): number | undefined {
103
+ const n = toNumber(value);
104
+ if (n === undefined) return undefined;
105
+ return Math.max(min, Math.min(max, n));
106
+ }
107
+
108
+ function shallowNumberRecord(value: unknown, min: number, max: number): Record<string, number> | undefined {
109
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
110
+ const result: Record<string, number> = {};
111
+ for (const [key, raw] of Object.entries(value)) {
112
+ const n = clampNumber(raw, min, max);
113
+ if (n !== undefined) result[key] = n;
114
+ }
115
+ return result;
116
+ }
117
+
118
+ export function normalizeRuntimeQueryParams(value: RuntimeQueryParamsPatch): { params: RuntimeQueryParams; warnings: string[] } {
119
+ const warnings: string[] = [];
120
+ const params: RuntimeQueryParams = {};
121
+
122
+ const recallLimit = clampInteger(value.recallLimit, 1, 50);
123
+ if (recallLimit !== undefined) params.recallLimit = recallLimit;
124
+ const candidateMultiplier = clampInteger(value.candidateMultiplier, 1, 20);
125
+ if (candidateMultiplier !== undefined) params.candidateMultiplier = candidateMultiplier;
126
+ const candidateLimit = clampInteger(value.candidateLimit, 1, 200);
127
+ if (candidateLimit !== undefined) params.candidateLimit = candidateLimit;
128
+ const scoreThreshold = clampNumber(value.scoreThreshold, 0, 1);
129
+ if (scoreThreshold !== undefined) params.scoreThreshold = scoreThreshold;
130
+ const maxInjectedChars = clampInteger(value.maxInjectedChars, 100, 50_000);
131
+ if (maxInjectedChars !== undefined) params.maxInjectedChars = maxInjectedChars;
132
+ const ovSearchLimit = clampInteger(value.ovSearchLimit, 1, 100);
133
+ if (ovSearchLimit !== undefined) params.ovSearchLimit = ovSearchLimit;
134
+
135
+ if (typeof value.recallPreferAbstract === "boolean") params.recallPreferAbstract = value.recallPreferAbstract;
136
+ if (value.resourceTypes !== undefined) params.resourceTypes = normalizeRecallResourceTypes(value.resourceTypes);
137
+ if (typeof value.targetUri === "string" && value.targetUri.trim()) {
138
+ const targetUri = value.targetUri.trim();
139
+ if (!targetUri.startsWith("viking://")) throw new Error("targetUri must start with viking://");
140
+ params.targetUri = targetUri;
141
+ }
142
+
143
+ const rankingWeights = shallowNumberRecord(value.rankingWeights, 0, 2);
144
+ if (rankingWeights) params.rankingWeights = rankingWeights;
145
+ const resourceTypeWeights = shallowNumberRecord(value.resourceTypeWeights, -1, 2);
146
+ if (resourceTypeWeights) params.resourceTypeWeights = resourceTypeWeights as Partial<Record<RecallResourceType, number>>;
147
+ const categoryWeights = shallowNumberRecord(value.categoryWeights, -1, 2);
148
+ if (categoryWeights) params.categoryWeights = categoryWeights;
149
+
150
+ if (params.candidateLimit !== undefined && params.recallLimit !== undefined && params.candidateLimit < params.recallLimit) {
151
+ params.candidateLimit = params.recallLimit;
152
+ warnings.push("candidateLimit was raised to recallLimit");
153
+ }
154
+
155
+ return { params, warnings };
156
+ }
157
+
158
+ export function resolveSessionQueryConfigKey(ctx: Partial<QueryConfigContext>): string | undefined {
159
+ if (ctx.ovSessionId) return `ov:${ctx.ovSessionId}`;
160
+ if (ctx.sessionId) return `session:${ctx.sessionId}`;
161
+ if (ctx.sessionKey) return `key:${ctx.sessionKey}`;
162
+ return undefined;
163
+ }
164
+
165
+ function emptyRuntimeFile(): RuntimeFile {
166
+ return { schemaVersion: "1.0", updatedAt: Date.now(), claws: {}, sessions: {} };
167
+ }
168
+
169
+ function isRuntimeFile(value: unknown): value is RuntimeFile {
170
+ return !!value && typeof value === "object" && !Array.isArray(value) && (value as RuntimeFile).schemaVersion === "1.0";
171
+ }
172
+
173
+ export class RuntimeQueryConfigStore {
174
+ private data: RuntimeFile = emptyRuntimeFile();
175
+ private lastMtimeMs = 0;
176
+ private loadPromise: Promise<void> | undefined;
177
+ private writeQueue: Promise<void> = Promise.resolve();
178
+
179
+ static createInMemory(staticConfig: Required<MemoryOpenVikingConfig>): RuntimeQueryConfigStore {
180
+ return new RuntimeQueryConfigStore({ staticConfig });
181
+ }
182
+
183
+ constructor(private readonly options: { staticConfig: Required<MemoryOpenVikingConfig>; path?: string }) {}
184
+
185
+ async load(): Promise<void> {
186
+ if (!this.options.path) return;
187
+ const loadPromise = this.loadFromDisk({ resetOnFailure: true });
188
+ this.loadPromise = loadPromise;
189
+ try {
190
+ await loadPromise;
191
+ } finally {
192
+ if (this.loadPromise === loadPromise) this.loadPromise = undefined;
193
+ }
194
+ }
195
+
196
+ private async loadFromDisk(options?: { resetOnFailure?: boolean }): Promise<void> {
197
+ try {
198
+ const raw = await readFile(this.options.path!, "utf8");
199
+ const parsed = JSON.parse(raw);
200
+ if (isRuntimeFile(parsed)) this.data = parsed;
201
+ const s = await stat(this.options.path!);
202
+ this.lastMtimeMs = s.mtimeMs;
203
+ } catch {
204
+ if (options?.resetOnFailure) this.data = emptyRuntimeFile();
205
+ }
206
+ }
207
+
208
+ private async waitForInitialLoad(): Promise<void> {
209
+ if (this.loadPromise) await this.loadPromise;
210
+ }
211
+
212
+ async reloadIfChanged(options?: { force?: boolean }): Promise<void> {
213
+ if (!this.options.path) return;
214
+ await this.waitForInitialLoad();
215
+ try {
216
+ const s = await stat(this.options.path);
217
+ if (!options?.force && s.mtimeMs === this.lastMtimeMs) return;
218
+ const raw = await readFile(this.options.path, "utf8");
219
+ const parsed = JSON.parse(raw);
220
+ if (isRuntimeFile(parsed)) {
221
+ this.data = parsed;
222
+ this.lastMtimeMs = s.mtimeMs;
223
+ }
224
+ } catch {
225
+ // Keep the last known-good in-memory config.
226
+ }
227
+ }
228
+
229
+ async set(scope: RuntimeScope, ctx: QueryConfigContext, patch: RuntimeQueryParamsPatch): Promise<{ warnings: string[] }> {
230
+ await this.waitForInitialLoad();
231
+ const { params, warnings } = normalizeRuntimeQueryParams(patch);
232
+ const key = this.keyForScope(scope, ctx);
233
+ const bucket = scope === "claw" ? this.data.claws : this.data.sessions;
234
+ bucket[key] = { params, updatedAt: Date.now(), updatedBy: "command", peerId: ctx.peerId };
235
+ this.data.updatedAt = Date.now();
236
+ await this.persist();
237
+ return { warnings };
238
+ }
239
+
240
+ async unset(scope: RuntimeScope, ctx: QueryConfigContext, fields: string[]): Promise<void> {
241
+ await this.waitForInitialLoad();
242
+ const key = this.keyForScope(scope, ctx);
243
+ const bucket = scope === "claw" ? this.data.claws : this.data.sessions;
244
+ const record = bucket[key];
245
+ if (!record) return;
246
+ for (const field of fields) {
247
+ delete (record.params as Record<string, unknown>)[field];
248
+ }
249
+ record.updatedAt = Date.now();
250
+ this.data.updatedAt = Date.now();
251
+ await this.persist();
252
+ }
253
+
254
+ async reset(scope: RuntimeScope, ctx: QueryConfigContext): Promise<void> {
255
+ await this.waitForInitialLoad();
256
+ const key = this.keyForScope(scope, ctx);
257
+ const bucket = scope === "claw" ? this.data.claws : this.data.sessions;
258
+ delete bucket[key];
259
+ this.data.updatedAt = Date.now();
260
+ await this.persist();
261
+ }
262
+
263
+ async getEffective(ctx: QueryConfigContext, requestOverrides?: RuntimeQueryParamsPatch): Promise<EffectiveQueryConfig> {
264
+ await this.reloadIfChanged();
265
+ const cfg = this.options.staticConfig;
266
+ const warnings: string[] = [];
267
+ const effective: EffectiveQueryConfig = {
268
+ recallLimit: cfg.recallLimit,
269
+ candidateMultiplier: DEFAULT_CANDIDATE_MULTIPLIER,
270
+ candidateLimit: Math.max(cfg.recallLimit * DEFAULT_CANDIDATE_MULTIPLIER, 20),
271
+ scoreThreshold: cfg.recallScoreThreshold,
272
+ maxInjectedChars: cfg.recallMaxInjectedChars,
273
+ recallPreferAbstract: cfg.recallPreferAbstract,
274
+ resourceTypes: normalizeRecallResourceTypes(cfg.recallTargetTypes),
275
+ ovSearchLimit: DEFAULT_OV_SEARCH_LIMIT,
276
+ rankingWeights: { ...DEFAULT_RANKING_WEIGHTS },
277
+ resourceTypeWeights: {},
278
+ categoryWeights: {},
279
+ sources: {
280
+ recallLimit: "static",
281
+ candidateMultiplier: "default",
282
+ candidateLimit: "default",
283
+ scoreThreshold: "static",
284
+ maxInjectedChars: "static",
285
+ recallPreferAbstract: "static",
286
+ resourceTypes: "static",
287
+ ovSearchLimit: "default",
288
+ rankingWeights: "default",
289
+ },
290
+ warnings,
291
+ };
292
+
293
+ let candidateLimitExplicit = false;
294
+ candidateLimitExplicit = this.applyLayer(effective, this.data.claws[ctx.peerId]?.params, "claw", candidateLimitExplicit);
295
+ const sessionRecord = this.findSessionRecord(ctx);
296
+ if (sessionRecord) candidateLimitExplicit = this.applyLayer(effective, sessionRecord.params, "session", candidateLimitExplicit);
297
+ if (requestOverrides) {
298
+ const normalized = normalizeRuntimeQueryParams(requestOverrides);
299
+ warnings.push(...normalized.warnings);
300
+ this.applyLayer(effective, normalized.params, "request", candidateLimitExplicit);
301
+ }
302
+ effective.candidateLimit = Math.max(effective.candidateLimit, effective.recallLimit);
303
+ return effective;
304
+ }
305
+
306
+ private applyLayer(
307
+ effective: EffectiveQueryConfig,
308
+ params: RuntimeQueryParams | undefined,
309
+ source: ConfigSource,
310
+ candidateLimitExplicit: boolean,
311
+ ): boolean {
312
+ if (!params) return candidateLimitExplicit;
313
+ if (params.recallLimit !== undefined) {
314
+ effective.recallLimit = params.recallLimit;
315
+ effective.sources.recallLimit = source;
316
+ if (!candidateLimitExplicit) {
317
+ effective.candidateLimit = Math.max(effective.recallLimit * effective.candidateMultiplier, 20);
318
+ effective.sources.candidateLimit = source;
319
+ }
320
+ }
321
+ if (params.candidateMultiplier !== undefined) {
322
+ effective.candidateMultiplier = params.candidateMultiplier;
323
+ effective.sources.candidateMultiplier = source;
324
+ if (!candidateLimitExplicit) {
325
+ effective.candidateLimit = Math.max(effective.recallLimit * params.candidateMultiplier, 20);
326
+ effective.sources.candidateLimit = source;
327
+ }
328
+ }
329
+ if (params.candidateLimit !== undefined) {
330
+ effective.candidateLimit = params.candidateLimit;
331
+ effective.sources.candidateLimit = source;
332
+ candidateLimitExplicit = true;
333
+ }
334
+ if (params.scoreThreshold !== undefined) {
335
+ effective.scoreThreshold = params.scoreThreshold;
336
+ effective.sources.scoreThreshold = source;
337
+ }
338
+ if (params.maxInjectedChars !== undefined) {
339
+ effective.maxInjectedChars = params.maxInjectedChars;
340
+ effective.sources.maxInjectedChars = source;
341
+ }
342
+ if (params.recallPreferAbstract !== undefined) {
343
+ effective.recallPreferAbstract = params.recallPreferAbstract;
344
+ effective.sources.recallPreferAbstract = source;
345
+ }
346
+ if (params.resourceTypes !== undefined) {
347
+ effective.resourceTypes = Array.isArray(params.resourceTypes) ? [...params.resourceTypes] : normalizeRecallResourceTypes(params.resourceTypes);
348
+ effective.sources.resourceTypes = source;
349
+ }
350
+ if (params.targetUri !== undefined) {
351
+ effective.targetUri = params.targetUri;
352
+ effective.sources.targetUri = source;
353
+ }
354
+ if (params.ovSearchLimit !== undefined) {
355
+ effective.ovSearchLimit = params.ovSearchLimit;
356
+ effective.sources.ovSearchLimit = source;
357
+ }
358
+ if (params.rankingWeights) {
359
+ effective.rankingWeights = { ...effective.rankingWeights, ...params.rankingWeights };
360
+ effective.sources.rankingWeights = source;
361
+ }
362
+ if (params.resourceTypeWeights) effective.resourceTypeWeights = { ...effective.resourceTypeWeights, ...params.resourceTypeWeights };
363
+ if (params.categoryWeights) effective.categoryWeights = { ...effective.categoryWeights, ...params.categoryWeights };
364
+ return candidateLimitExplicit;
365
+ }
366
+
367
+ private keyForScope(scope: RuntimeScope, ctx: QueryConfigContext): string {
368
+ if (scope === "claw") return ctx.peerId;
369
+ const key = resolveSessionQueryConfigKey(ctx);
370
+ if (!key) throw new Error("session scope requires ovSessionId, sessionId, or sessionKey");
371
+ return key;
372
+ }
373
+
374
+ private findSessionRecord(ctx: QueryConfigContext): RuntimeRecord | undefined {
375
+ const keys = [
376
+ ctx.ovSessionId ? `ov:${ctx.ovSessionId}` : undefined,
377
+ ctx.sessionId ? `session:${ctx.sessionId}` : undefined,
378
+ ctx.sessionKey ? `key:${ctx.sessionKey}` : undefined,
379
+ ].filter((key): key is string => Boolean(key));
380
+ for (const key of keys) {
381
+ const record = this.data.sessions[key];
382
+ if (record) return record;
383
+ }
384
+ return undefined;
385
+ }
386
+
387
+ private async persist(): Promise<void> {
388
+ if (!this.options.path) return;
389
+ const operation = this.writeQueue.catch(() => undefined).then(async () => {
390
+ await mkdir(dirname(this.options.path!), { recursive: true });
391
+ const temp = `${this.options.path}.${process.pid}.${Date.now()}.tmp`;
392
+ await writeFile(temp, JSON.stringify(this.data, null, 2), "utf8");
393
+ await rename(temp, this.options.path!);
394
+ const s = await stat(this.options.path!);
395
+ this.lastMtimeMs = s.mtimeMs;
396
+ });
397
+ this.writeQueue = operation.catch(() => undefined);
398
+ await operation;
399
+ }
400
+ }