@openviking/openclaw-plugin 2026.6.2 → 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 (108) hide show
  1. package/INSTALL-AGENT.md +4 -4
  2. package/INSTALL-ZH.md +68 -8
  3. package/INSTALL.md +12 -8
  4. package/README.md +45 -35
  5. package/adapters/http-transport.ts +3 -0
  6. package/adapters/resource-packager.ts +141 -0
  7. package/auto-recall.ts +258 -27
  8. package/client.ts +201 -177
  9. package/commands/setup.ts +200 -33
  10. package/config/feature-gates.json +10 -0
  11. package/config.ts +428 -85
  12. package/context-engine.ts +71 -1402
  13. package/dist/adapters/http-transport.js +1 -0
  14. package/dist/adapters/resource-packager.js +114 -0
  15. package/dist/auto-recall.js +201 -25
  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 +62 -1173
  20. package/dist/index.js +209 -1489
  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 +1 -1
  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/index.ts +271 -1873
  59. package/install-manifest.json +10 -1
  60. package/memory-ranking.ts +46 -10
  61. package/openclaw.plugin.json +198 -36
  62. package/package.json +14 -4
  63. package/plugin/command-registration.ts +34 -0
  64. package/plugin/openviking-archive-tools.ts +301 -0
  65. package/plugin/openviking-bypass-runtime.ts +26 -0
  66. package/plugin/openviking-client-runtime.ts +74 -0
  67. package/plugin/openviking-command-args.ts +187 -0
  68. package/plugin/openviking-command-definitions.ts +294 -0
  69. package/plugin/openviking-context-engine-ref.ts +16 -0
  70. package/plugin/openviking-context-engine-registration.ts +97 -0
  71. package/plugin/openviking-feature-gates.ts +335 -0
  72. package/plugin/openviking-import-runtime.ts +105 -0
  73. package/plugin/openviking-import-tools.ts +133 -0
  74. package/plugin/openviking-lifecycle-hooks.ts +65 -0
  75. package/plugin/openviking-memory-recall-tools.ts +368 -0
  76. package/plugin/openviking-memory-tools.ts +299 -0
  77. package/plugin/openviking-query-config-command.ts +230 -0
  78. package/plugin/openviking-query-formatters.ts +108 -0
  79. package/plugin/openviking-query-runtime.ts +476 -0
  80. package/plugin/openviking-query-tools.ts +161 -0
  81. package/plugin/openviking-recall-trace-runtime.ts +482 -0
  82. package/plugin/openviking-recall-trace-tools.ts +99 -0
  83. package/plugin/openviking-runtime-state.ts +52 -0
  84. package/plugin/openviking-runtime-utils.ts +67 -0
  85. package/plugin/openviking-services.ts +49 -0
  86. package/plugin/openviking-session-routing-runtime.ts +105 -0
  87. package/plugin/openviking-tool-result-tools.ts +397 -0
  88. package/plugin/recall-trace-routes.ts +133 -0
  89. package/plugin/tool-registration.ts +51 -0
  90. package/process-manager.ts +1 -1
  91. package/query-config.ts +400 -0
  92. package/recall-trace.ts +442 -0
  93. package/registries/openviking-tools.ts +84 -0
  94. package/registries/recall-resource-types.ts +74 -0
  95. package/routing/identity-routing.ts +210 -0
  96. package/routing/memory-uri.ts +8 -0
  97. package/services/context-lifecycle-service.ts +1209 -0
  98. package/services/context-message-adapter.ts +375 -0
  99. package/services/setup/config-writer.ts +125 -0
  100. package/services/setup/exit-utils.ts +5 -0
  101. package/services/setup/package-metadata.ts +56 -0
  102. package/services/setup/probe-service.ts +143 -0
  103. package/services/setup/setup-flow.ts +306 -0
  104. package/services/setup/version-compatibility.ts +41 -0
  105. package/session-transcript-repair.ts +0 -2
  106. package/skills/install-openviking-memory/SKILL.md +123 -50
  107. package/skills/openviking-context-database/SKILL.md +286 -0
  108. package/text-utils.ts +15 -34
@@ -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
+ }
@@ -22,7 +22,7 @@ export async function quickHealthCheck(
22
22
  timeoutMs: number,
23
23
  ): Promise<boolean> {
24
24
  try {
25
- await client.healthCheck(agentId, timeoutMs);
25
+ await client.healthCheck(timeoutMs, agentId);
26
26
  return true;
27
27
  } catch {
28
28
  return false;
@@ -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
+ }