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

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,336 @@
1
+ import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { normalizeRecallResourceTypes } from "./registries/recall-resource-types.js";
4
+ const DEFAULT_CANDIDATE_MULTIPLIER = 4;
5
+ const DEFAULT_OV_SEARCH_LIMIT = 10;
6
+ const DEFAULT_RANKING_WEIGHTS = {
7
+ baseScore: 1,
8
+ leaf: 0.12,
9
+ event: 0.1,
10
+ preference: 0.08,
11
+ lexicalOverlapMax: 0.2,
12
+ };
13
+ function toNumber(value) {
14
+ if (typeof value === "number" && Number.isFinite(value))
15
+ return value;
16
+ if (typeof value === "string" && value.trim() !== "") {
17
+ const parsed = Number(value);
18
+ if (Number.isFinite(parsed))
19
+ return parsed;
20
+ }
21
+ return undefined;
22
+ }
23
+ function clampInteger(value, min, max) {
24
+ const n = toNumber(value);
25
+ if (n === undefined)
26
+ return undefined;
27
+ return Math.max(min, Math.min(max, Math.floor(n)));
28
+ }
29
+ function clampNumber(value, min, max) {
30
+ const n = toNumber(value);
31
+ if (n === undefined)
32
+ return undefined;
33
+ return Math.max(min, Math.min(max, n));
34
+ }
35
+ function shallowNumberRecord(value, min, max) {
36
+ if (!value || typeof value !== "object" || Array.isArray(value))
37
+ return undefined;
38
+ const result = {};
39
+ for (const [key, raw] of Object.entries(value)) {
40
+ const n = clampNumber(raw, min, max);
41
+ if (n !== undefined)
42
+ result[key] = n;
43
+ }
44
+ return result;
45
+ }
46
+ export function normalizeRuntimeQueryParams(value) {
47
+ const warnings = [];
48
+ const params = {};
49
+ const recallLimit = clampInteger(value.recallLimit, 1, 50);
50
+ if (recallLimit !== undefined)
51
+ params.recallLimit = recallLimit;
52
+ const candidateMultiplier = clampInteger(value.candidateMultiplier, 1, 20);
53
+ if (candidateMultiplier !== undefined)
54
+ params.candidateMultiplier = candidateMultiplier;
55
+ const candidateLimit = clampInteger(value.candidateLimit, 1, 200);
56
+ if (candidateLimit !== undefined)
57
+ params.candidateLimit = candidateLimit;
58
+ const scoreThreshold = clampNumber(value.scoreThreshold, 0, 1);
59
+ if (scoreThreshold !== undefined)
60
+ params.scoreThreshold = scoreThreshold;
61
+ const maxInjectedChars = clampInteger(value.maxInjectedChars, 100, 50_000);
62
+ if (maxInjectedChars !== undefined)
63
+ params.maxInjectedChars = maxInjectedChars;
64
+ const ovSearchLimit = clampInteger(value.ovSearchLimit, 1, 100);
65
+ if (ovSearchLimit !== undefined)
66
+ params.ovSearchLimit = ovSearchLimit;
67
+ if (typeof value.recallPreferAbstract === "boolean")
68
+ params.recallPreferAbstract = value.recallPreferAbstract;
69
+ if (value.resourceTypes !== undefined)
70
+ params.resourceTypes = normalizeRecallResourceTypes(value.resourceTypes);
71
+ if (typeof value.targetUri === "string" && value.targetUri.trim()) {
72
+ const targetUri = value.targetUri.trim();
73
+ if (!targetUri.startsWith("viking://"))
74
+ throw new Error("targetUri must start with viking://");
75
+ params.targetUri = targetUri;
76
+ }
77
+ const rankingWeights = shallowNumberRecord(value.rankingWeights, 0, 2);
78
+ if (rankingWeights)
79
+ params.rankingWeights = rankingWeights;
80
+ const resourceTypeWeights = shallowNumberRecord(value.resourceTypeWeights, -1, 2);
81
+ if (resourceTypeWeights)
82
+ params.resourceTypeWeights = resourceTypeWeights;
83
+ const categoryWeights = shallowNumberRecord(value.categoryWeights, -1, 2);
84
+ if (categoryWeights)
85
+ params.categoryWeights = categoryWeights;
86
+ if (params.candidateLimit !== undefined && params.recallLimit !== undefined && params.candidateLimit < params.recallLimit) {
87
+ params.candidateLimit = params.recallLimit;
88
+ warnings.push("candidateLimit was raised to recallLimit");
89
+ }
90
+ return { params, warnings };
91
+ }
92
+ export function resolveSessionQueryConfigKey(ctx) {
93
+ if (ctx.ovSessionId)
94
+ return `ov:${ctx.ovSessionId}`;
95
+ if (ctx.sessionId)
96
+ return `session:${ctx.sessionId}`;
97
+ if (ctx.sessionKey)
98
+ return `key:${ctx.sessionKey}`;
99
+ return undefined;
100
+ }
101
+ function emptyRuntimeFile() {
102
+ return { schemaVersion: "1.0", updatedAt: Date.now(), claws: {}, sessions: {} };
103
+ }
104
+ function isRuntimeFile(value) {
105
+ return !!value && typeof value === "object" && !Array.isArray(value) && value.schemaVersion === "1.0";
106
+ }
107
+ export class RuntimeQueryConfigStore {
108
+ options;
109
+ data = emptyRuntimeFile();
110
+ lastMtimeMs = 0;
111
+ loadPromise;
112
+ writeQueue = Promise.resolve();
113
+ static createInMemory(staticConfig) {
114
+ return new RuntimeQueryConfigStore({ staticConfig });
115
+ }
116
+ constructor(options) {
117
+ this.options = options;
118
+ }
119
+ async load() {
120
+ if (!this.options.path)
121
+ return;
122
+ const loadPromise = this.loadFromDisk({ resetOnFailure: true });
123
+ this.loadPromise = loadPromise;
124
+ try {
125
+ await loadPromise;
126
+ }
127
+ finally {
128
+ if (this.loadPromise === loadPromise)
129
+ this.loadPromise = undefined;
130
+ }
131
+ }
132
+ async loadFromDisk(options) {
133
+ try {
134
+ const raw = await readFile(this.options.path, "utf8");
135
+ const parsed = JSON.parse(raw);
136
+ if (isRuntimeFile(parsed))
137
+ this.data = parsed;
138
+ const s = await stat(this.options.path);
139
+ this.lastMtimeMs = s.mtimeMs;
140
+ }
141
+ catch {
142
+ if (options?.resetOnFailure)
143
+ this.data = emptyRuntimeFile();
144
+ }
145
+ }
146
+ async waitForInitialLoad() {
147
+ if (this.loadPromise)
148
+ await this.loadPromise;
149
+ }
150
+ async reloadIfChanged(options) {
151
+ if (!this.options.path)
152
+ return;
153
+ await this.waitForInitialLoad();
154
+ try {
155
+ const s = await stat(this.options.path);
156
+ if (!options?.force && s.mtimeMs === this.lastMtimeMs)
157
+ return;
158
+ const raw = await readFile(this.options.path, "utf8");
159
+ const parsed = JSON.parse(raw);
160
+ if (isRuntimeFile(parsed)) {
161
+ this.data = parsed;
162
+ this.lastMtimeMs = s.mtimeMs;
163
+ }
164
+ }
165
+ catch {
166
+ // Keep the last known-good in-memory config.
167
+ }
168
+ }
169
+ async set(scope, ctx, patch) {
170
+ await this.waitForInitialLoad();
171
+ const { params, warnings } = normalizeRuntimeQueryParams(patch);
172
+ const key = this.keyForScope(scope, ctx);
173
+ const bucket = scope === "claw" ? this.data.claws : this.data.sessions;
174
+ bucket[key] = { params, updatedAt: Date.now(), updatedBy: "command", peerId: ctx.peerId };
175
+ this.data.updatedAt = Date.now();
176
+ await this.persist();
177
+ return { warnings };
178
+ }
179
+ async unset(scope, ctx, fields) {
180
+ await this.waitForInitialLoad();
181
+ const key = this.keyForScope(scope, ctx);
182
+ const bucket = scope === "claw" ? this.data.claws : this.data.sessions;
183
+ const record = bucket[key];
184
+ if (!record)
185
+ return;
186
+ for (const field of fields) {
187
+ delete record.params[field];
188
+ }
189
+ record.updatedAt = Date.now();
190
+ this.data.updatedAt = Date.now();
191
+ await this.persist();
192
+ }
193
+ async reset(scope, ctx) {
194
+ await this.waitForInitialLoad();
195
+ const key = this.keyForScope(scope, ctx);
196
+ const bucket = scope === "claw" ? this.data.claws : this.data.sessions;
197
+ delete bucket[key];
198
+ this.data.updatedAt = Date.now();
199
+ await this.persist();
200
+ }
201
+ async getEffective(ctx, requestOverrides) {
202
+ await this.reloadIfChanged();
203
+ const cfg = this.options.staticConfig;
204
+ const warnings = [];
205
+ const effective = {
206
+ recallLimit: cfg.recallLimit,
207
+ candidateMultiplier: DEFAULT_CANDIDATE_MULTIPLIER,
208
+ candidateLimit: Math.max(cfg.recallLimit * DEFAULT_CANDIDATE_MULTIPLIER, 20),
209
+ scoreThreshold: cfg.recallScoreThreshold,
210
+ maxInjectedChars: cfg.recallMaxInjectedChars,
211
+ recallPreferAbstract: cfg.recallPreferAbstract,
212
+ resourceTypes: normalizeRecallResourceTypes(cfg.recallTargetTypes),
213
+ ovSearchLimit: DEFAULT_OV_SEARCH_LIMIT,
214
+ rankingWeights: { ...DEFAULT_RANKING_WEIGHTS },
215
+ resourceTypeWeights: {},
216
+ categoryWeights: {},
217
+ sources: {
218
+ recallLimit: "static",
219
+ candidateMultiplier: "default",
220
+ candidateLimit: "default",
221
+ scoreThreshold: "static",
222
+ maxInjectedChars: "static",
223
+ recallPreferAbstract: "static",
224
+ resourceTypes: "static",
225
+ ovSearchLimit: "default",
226
+ rankingWeights: "default",
227
+ },
228
+ warnings,
229
+ };
230
+ let candidateLimitExplicit = false;
231
+ candidateLimitExplicit = this.applyLayer(effective, this.data.claws[ctx.peerId]?.params, "claw", candidateLimitExplicit);
232
+ const sessionRecord = this.findSessionRecord(ctx);
233
+ if (sessionRecord)
234
+ candidateLimitExplicit = this.applyLayer(effective, sessionRecord.params, "session", candidateLimitExplicit);
235
+ if (requestOverrides) {
236
+ const normalized = normalizeRuntimeQueryParams(requestOverrides);
237
+ warnings.push(...normalized.warnings);
238
+ this.applyLayer(effective, normalized.params, "request", candidateLimitExplicit);
239
+ }
240
+ effective.candidateLimit = Math.max(effective.candidateLimit, effective.recallLimit);
241
+ return effective;
242
+ }
243
+ applyLayer(effective, params, source, candidateLimitExplicit) {
244
+ if (!params)
245
+ return candidateLimitExplicit;
246
+ if (params.recallLimit !== undefined) {
247
+ effective.recallLimit = params.recallLimit;
248
+ effective.sources.recallLimit = source;
249
+ if (!candidateLimitExplicit) {
250
+ effective.candidateLimit = Math.max(effective.recallLimit * effective.candidateMultiplier, 20);
251
+ effective.sources.candidateLimit = source;
252
+ }
253
+ }
254
+ if (params.candidateMultiplier !== undefined) {
255
+ effective.candidateMultiplier = params.candidateMultiplier;
256
+ effective.sources.candidateMultiplier = source;
257
+ if (!candidateLimitExplicit) {
258
+ effective.candidateLimit = Math.max(effective.recallLimit * params.candidateMultiplier, 20);
259
+ effective.sources.candidateLimit = source;
260
+ }
261
+ }
262
+ if (params.candidateLimit !== undefined) {
263
+ effective.candidateLimit = params.candidateLimit;
264
+ effective.sources.candidateLimit = source;
265
+ candidateLimitExplicit = true;
266
+ }
267
+ if (params.scoreThreshold !== undefined) {
268
+ effective.scoreThreshold = params.scoreThreshold;
269
+ effective.sources.scoreThreshold = source;
270
+ }
271
+ if (params.maxInjectedChars !== undefined) {
272
+ effective.maxInjectedChars = params.maxInjectedChars;
273
+ effective.sources.maxInjectedChars = source;
274
+ }
275
+ if (params.recallPreferAbstract !== undefined) {
276
+ effective.recallPreferAbstract = params.recallPreferAbstract;
277
+ effective.sources.recallPreferAbstract = source;
278
+ }
279
+ if (params.resourceTypes !== undefined) {
280
+ effective.resourceTypes = Array.isArray(params.resourceTypes) ? [...params.resourceTypes] : normalizeRecallResourceTypes(params.resourceTypes);
281
+ effective.sources.resourceTypes = source;
282
+ }
283
+ if (params.targetUri !== undefined) {
284
+ effective.targetUri = params.targetUri;
285
+ effective.sources.targetUri = source;
286
+ }
287
+ if (params.ovSearchLimit !== undefined) {
288
+ effective.ovSearchLimit = params.ovSearchLimit;
289
+ effective.sources.ovSearchLimit = source;
290
+ }
291
+ if (params.rankingWeights) {
292
+ effective.rankingWeights = { ...effective.rankingWeights, ...params.rankingWeights };
293
+ effective.sources.rankingWeights = source;
294
+ }
295
+ if (params.resourceTypeWeights)
296
+ effective.resourceTypeWeights = { ...effective.resourceTypeWeights, ...params.resourceTypeWeights };
297
+ if (params.categoryWeights)
298
+ effective.categoryWeights = { ...effective.categoryWeights, ...params.categoryWeights };
299
+ return candidateLimitExplicit;
300
+ }
301
+ keyForScope(scope, ctx) {
302
+ if (scope === "claw")
303
+ return ctx.peerId;
304
+ const key = resolveSessionQueryConfigKey(ctx);
305
+ if (!key)
306
+ throw new Error("session scope requires ovSessionId, sessionId, or sessionKey");
307
+ return key;
308
+ }
309
+ findSessionRecord(ctx) {
310
+ const keys = [
311
+ ctx.ovSessionId ? `ov:${ctx.ovSessionId}` : undefined,
312
+ ctx.sessionId ? `session:${ctx.sessionId}` : undefined,
313
+ ctx.sessionKey ? `key:${ctx.sessionKey}` : undefined,
314
+ ].filter((key) => Boolean(key));
315
+ for (const key of keys) {
316
+ const record = this.data.sessions[key];
317
+ if (record)
318
+ return record;
319
+ }
320
+ return undefined;
321
+ }
322
+ async persist() {
323
+ if (!this.options.path)
324
+ return;
325
+ const operation = this.writeQueue.catch(() => undefined).then(async () => {
326
+ await mkdir(dirname(this.options.path), { recursive: true });
327
+ const temp = `${this.options.path}.${process.pid}.${Date.now()}.tmp`;
328
+ await writeFile(temp, JSON.stringify(this.data, null, 2), "utf8");
329
+ await rename(temp, this.options.path);
330
+ const s = await stat(this.options.path);
331
+ this.lastMtimeMs = s.mtimeMs;
332
+ });
333
+ this.writeQueue = operation.catch(() => undefined);
334
+ await operation;
335
+ }
336
+ }
@@ -0,0 +1,309 @@
1
+ import { appendFile, mkdir, readFile, readdir, unlink } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ const ALLOWED_RESOURCE_TYPES = ["resource", "user", "agent"];
4
+ const DEFAULT_RESOURCE_TYPES = ["user", "agent"];
5
+ function toResourceTypeEntries(value) {
6
+ if (Array.isArray(value)) {
7
+ return value
8
+ .filter((entry) => typeof entry === "string")
9
+ .map((entry) => entry.trim())
10
+ .filter(Boolean);
11
+ }
12
+ if (typeof value === "string") {
13
+ return value
14
+ .split(/[,\n]/)
15
+ .map((entry) => entry.trim())
16
+ .filter(Boolean);
17
+ }
18
+ return [];
19
+ }
20
+ export function normalizeResourceTypes(value) {
21
+ const entries = toResourceTypeEntries(value);
22
+ if (entries.length === 0) {
23
+ return [...DEFAULT_RESOURCE_TYPES];
24
+ }
25
+ const seen = new Set();
26
+ const normalized = [];
27
+ const invalid = [];
28
+ for (const entry of entries) {
29
+ if (ALLOWED_RESOURCE_TYPES.includes(entry)) {
30
+ const typed = entry;
31
+ if (!seen.has(typed)) {
32
+ seen.add(typed);
33
+ normalized.push(typed);
34
+ }
35
+ }
36
+ else {
37
+ invalid.push(entry);
38
+ }
39
+ }
40
+ if (invalid.length > 0) {
41
+ throw new Error(`invalid resourceTypes: ${invalid.join(", ")}`);
42
+ }
43
+ return normalized.length > 0 ? normalized : [...DEFAULT_RESOURCE_TYPES];
44
+ }
45
+ export function resolveRecallSearchPlan(resourceTypes, _ctx) {
46
+ const normalized = normalizeResourceTypes(resourceTypes);
47
+ const searches = [];
48
+ const skipped = [];
49
+ let addedMemorySearch = false;
50
+ for (const resourceType of normalized) {
51
+ if (resourceType === "resource") {
52
+ searches.push({ resourceType, contextType: "resource" });
53
+ }
54
+ else if ((resourceType === "user" || resourceType === "agent") && !addedMemorySearch) {
55
+ searches.push({ resourceType: "user", contextType: "memory" });
56
+ addedMemorySearch = true;
57
+ }
58
+ }
59
+ return { resourceTypes: normalized, searches, skipped };
60
+ }
61
+ export class RecallTraceMemoryStore {
62
+ maxEntries;
63
+ entries = [];
64
+ constructor(maxEntries) {
65
+ this.maxEntries = Math.max(1, Math.floor(maxEntries));
66
+ }
67
+ record(entry) {
68
+ this.entries.push(entry);
69
+ while (this.entries.length > this.maxEntries) {
70
+ this.entries.shift();
71
+ }
72
+ }
73
+ query(query) {
74
+ const limit = Math.max(1, Math.floor(query.limit ?? 20));
75
+ const turn = query.turn ?? "latest";
76
+ const resourceTypes = query.resourceTypes && query.resourceTypes.length > 0
77
+ ? new Set(query.resourceTypes)
78
+ : undefined;
79
+ const filtered = this.entries
80
+ .filter((entry) => {
81
+ if (query.traceId && entry.traceId !== query.traceId)
82
+ return false;
83
+ if (query.source && entry.source !== query.source)
84
+ return false;
85
+ if (query.sessionId && entry.sessionId !== query.sessionId)
86
+ return false;
87
+ if (query.sessionKey && entry.sessionKey !== query.sessionKey)
88
+ return false;
89
+ if (query.ovSessionId && entry.ovSessionId !== query.ovSessionId)
90
+ return false;
91
+ if (typeof query.since === "number" && entry.ts < query.since)
92
+ return false;
93
+ if (typeof query.until === "number" && entry.ts > query.until)
94
+ return false;
95
+ if (resourceTypes && !entry.resourceTypes.some((resourceType) => resourceTypes.has(resourceType))) {
96
+ return false;
97
+ }
98
+ return true;
99
+ })
100
+ .sort((a, b) => b.ts - a.ts);
101
+ return { entries: filtered.slice(0, turn === "latest" ? 1 : limit), lookupLayer: "memory", warnings: [] };
102
+ }
103
+ }
104
+ function jsonlFileNameForTimestamp(ts) {
105
+ return `${new Date(ts).toISOString().slice(0, 10)}.jsonl`;
106
+ }
107
+ function startOfUtcDay(ts) {
108
+ const d = new Date(ts);
109
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
110
+ }
111
+ function timestampFromJsonlFileName(name) {
112
+ const match = /^(\d{4})-(\d{2})-(\d{2})\.jsonl$/.exec(name);
113
+ if (!match) {
114
+ return undefined;
115
+ }
116
+ const ts = Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
117
+ return Number.isFinite(ts) ? ts : undefined;
118
+ }
119
+ function fileMayOverlapQueryWindow(name, since, until) {
120
+ const dayStart = timestampFromJsonlFileName(name);
121
+ if (dayStart === undefined) {
122
+ return true;
123
+ }
124
+ const dayEnd = dayStart + 86_400_000 - 1;
125
+ return dayEnd >= since && dayStart <= until;
126
+ }
127
+ function isRecallTraceEntry(value) {
128
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
129
+ return false;
130
+ }
131
+ const candidate = value;
132
+ return candidate.schemaVersion === "1.0" &&
133
+ typeof candidate.traceId === "string" &&
134
+ typeof candidate.ts === "number" &&
135
+ typeof candidate.source === "string" &&
136
+ typeof candidate.operationType === "string" &&
137
+ Array.isArray(candidate.resourceTypes) &&
138
+ !!candidate.trigger &&
139
+ typeof candidate.trigger.query === "string";
140
+ }
141
+ export class RecallTraceJsonlStore {
142
+ dir;
143
+ includeRawUserPreview;
144
+ retentionDays;
145
+ queryMaxDays;
146
+ pending = [];
147
+ warnings = [];
148
+ constructor(options) {
149
+ this.dir = options.dir;
150
+ this.includeRawUserPreview = options.includeRawUserPreview === true;
151
+ this.retentionDays = Math.max(1, Math.floor(options.retentionDays ?? 14));
152
+ this.queryMaxDays = Math.max(1, Math.floor(options.queryMaxDays ?? 14));
153
+ }
154
+ entryForPersistence(entry) {
155
+ if (this.includeRawUserPreview || entry.trigger.rawUserTextPreview === undefined) {
156
+ return entry;
157
+ }
158
+ return {
159
+ ...entry,
160
+ trigger: {
161
+ ...entry.trigger,
162
+ rawUserTextPreview: undefined,
163
+ },
164
+ };
165
+ }
166
+ append(entry) {
167
+ const write = (async () => {
168
+ await mkdir(this.dir, { recursive: true });
169
+ await this.pruneExpiredFiles(entry.ts);
170
+ await appendFile(join(this.dir, jsonlFileNameForTimestamp(entry.ts)), `${JSON.stringify(this.entryForPersistence(entry))}\n`, "utf8");
171
+ })().catch((err) => {
172
+ this.warnings.push(`Failed to append recall trace JSONL: ${err instanceof Error ? err.message : String(err)}`);
173
+ });
174
+ this.pending.push(write);
175
+ return write;
176
+ }
177
+ async pruneExpiredFiles(nowTs) {
178
+ const cutoff = startOfUtcDay(nowTs - this.retentionDays * 86_400_000);
179
+ let files;
180
+ try {
181
+ files = await readdir(this.dir);
182
+ }
183
+ catch {
184
+ return;
185
+ }
186
+ await Promise.all(files
187
+ .filter((name) => name.endsWith(".jsonl"))
188
+ .filter((name) => {
189
+ const ts = timestampFromJsonlFileName(name);
190
+ return ts !== undefined && ts < cutoff;
191
+ })
192
+ .map(async (name) => {
193
+ try {
194
+ await unlink(join(this.dir, name));
195
+ }
196
+ catch (err) {
197
+ this.warnings.push(`Failed to prune recall trace file ${name}: ${err instanceof Error ? err.message : String(err)}`);
198
+ }
199
+ }));
200
+ }
201
+ async flush() {
202
+ const pending = this.pending.splice(0);
203
+ await Promise.all(pending);
204
+ return { warnings: [...this.warnings] };
205
+ }
206
+ async query(query) {
207
+ await this.flush();
208
+ const warnings = [...this.warnings];
209
+ const entries = [];
210
+ let files;
211
+ try {
212
+ files = await readdir(this.dir);
213
+ }
214
+ catch (err) {
215
+ const code = typeof err === "object" && err !== null && "code" in err ? String(err.code) : "";
216
+ if (code === "ENOENT") {
217
+ return { entries: [], lookupLayer: "persistent", warnings };
218
+ }
219
+ return {
220
+ entries: [],
221
+ lookupLayer: "persistent",
222
+ warnings: [...warnings, `Failed to read recall trace directory: ${err instanceof Error ? err.message : String(err)}`],
223
+ };
224
+ }
225
+ const queryStart = typeof query.since === "number"
226
+ ? query.since
227
+ : Date.now() - this.queryMaxDays * 86_400_000;
228
+ const queryEnd = typeof query.until === "number" ? query.until : Date.now();
229
+ for (const file of files
230
+ .filter((name) => name.endsWith(".jsonl"))
231
+ .filter((name) => fileMayOverlapQueryWindow(name, queryStart, queryEnd))
232
+ .sort()) {
233
+ const path = join(this.dir, file);
234
+ let content;
235
+ try {
236
+ content = await readFile(path, "utf8");
237
+ }
238
+ catch (err) {
239
+ warnings.push(`Failed to read recall trace file ${file}: ${err instanceof Error ? err.message : String(err)}`);
240
+ continue;
241
+ }
242
+ const lines = content.split("\n");
243
+ for (let index = 0; index < lines.length; index++) {
244
+ const line = lines[index].trim();
245
+ if (!line) {
246
+ continue;
247
+ }
248
+ try {
249
+ const parsed = JSON.parse(line);
250
+ if (isRecallTraceEntry(parsed)) {
251
+ entries.push(parsed);
252
+ }
253
+ else {
254
+ warnings.push(`Skipping corrupted recall trace line ${file}:${index + 1}`);
255
+ }
256
+ }
257
+ catch {
258
+ warnings.push(`Skipping corrupted recall trace line ${file}:${index + 1}`);
259
+ }
260
+ }
261
+ }
262
+ const memory = new RecallTraceMemoryStore(Math.max(1, entries.length));
263
+ for (const entry of entries) {
264
+ memory.record(entry);
265
+ }
266
+ const filtered = memory.query(query);
267
+ return { entries: filtered.entries, lookupLayer: "persistent", warnings };
268
+ }
269
+ }
270
+ export class RecallTraceRecorder {
271
+ memory;
272
+ persistent;
273
+ constructor(options) {
274
+ this.memory = new RecallTraceMemoryStore(options.memoryMaxEntries);
275
+ this.persistent = options.persist ? new RecallTraceJsonlStore({
276
+ dir: options.traceDir,
277
+ includeRawUserPreview: options.includeRawUserPreview,
278
+ retentionDays: options.retentionDays,
279
+ queryMaxDays: options.queryMaxDays,
280
+ }) : undefined;
281
+ }
282
+ record(entry) {
283
+ this.memory.record(entry);
284
+ void this.persistent?.append(entry);
285
+ }
286
+ async recordAndFlush(entry) {
287
+ this.memory.record(entry);
288
+ await this.persistent?.append(entry);
289
+ return this.flush();
290
+ }
291
+ query(query) {
292
+ return this.memory.query(query);
293
+ }
294
+ async queryWithFallback(query) {
295
+ const memoryResult = this.memory.query(query);
296
+ if (memoryResult.entries.length > 0 || !this.persistent) {
297
+ return memoryResult;
298
+ }
299
+ const persistentResult = await this.persistent.query(query);
300
+ return {
301
+ entries: persistentResult.entries,
302
+ lookupLayer: "persistent",
303
+ warnings: [...memoryResult.warnings, ...persistentResult.warnings],
304
+ };
305
+ }
306
+ async flush() {
307
+ return this.persistent ? this.persistent.flush() : { warnings: [] };
308
+ }
309
+ }
@@ -0,0 +1,58 @@
1
+ export const ALLOWED_RECALL_RESOURCE_TYPES = ["resource", "user", "agent"];
2
+ export const DEFAULT_RECALL_RESOURCE_TYPES = ["user", "agent"];
3
+ function toResourceTypeEntries(value) {
4
+ if (Array.isArray(value)) {
5
+ return value
6
+ .filter((entry) => typeof entry === "string")
7
+ .map((entry) => entry.trim())
8
+ .filter(Boolean);
9
+ }
10
+ if (typeof value === "string") {
11
+ return value
12
+ .split(/[,\n]/)
13
+ .map((entry) => entry.trim())
14
+ .filter(Boolean);
15
+ }
16
+ return [];
17
+ }
18
+ export function normalizeRecallResourceTypes(value) {
19
+ const entries = toResourceTypeEntries(value);
20
+ if (entries.length === 0) {
21
+ return [...DEFAULT_RECALL_RESOURCE_TYPES];
22
+ }
23
+ const seen = new Set();
24
+ const normalized = [];
25
+ const invalid = [];
26
+ for (const entry of entries) {
27
+ if (ALLOWED_RECALL_RESOURCE_TYPES.includes(entry)) {
28
+ const typed = entry;
29
+ if (!seen.has(typed)) {
30
+ seen.add(typed);
31
+ normalized.push(typed);
32
+ }
33
+ }
34
+ else {
35
+ invalid.push(entry);
36
+ }
37
+ }
38
+ if (invalid.length > 0) {
39
+ throw new Error(`invalid resourceTypes: ${invalid.join(", ")}`);
40
+ }
41
+ return normalized.length > 0 ? normalized : [...DEFAULT_RECALL_RESOURCE_TYPES];
42
+ }
43
+ export function resolveRecallSearchPlan(resourceTypes, _ctx) {
44
+ const normalized = normalizeRecallResourceTypes(resourceTypes);
45
+ const searches = [];
46
+ const skipped = [];
47
+ let addedMemorySearch = false;
48
+ for (const resourceType of normalized) {
49
+ if (resourceType === "resource") {
50
+ searches.push({ resourceType, contextType: "resource" });
51
+ }
52
+ else if ((resourceType === "user" || resourceType === "agent") && !addedMemorySearch) {
53
+ searches.push({ resourceType: "user", contextType: "memory" });
54
+ addedMemorySearch = true;
55
+ }
56
+ }
57
+ return { resourceTypes: normalized, searches, skipped };
58
+ }