@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.
- package/INSTALL-AGENT.md +4 -4
- package/INSTALL-ZH.md +68 -8
- package/INSTALL.md +12 -8
- package/README.md +45 -35
- package/adapters/http-transport.ts +3 -0
- package/adapters/resource-packager.ts +141 -0
- package/auto-recall.ts +258 -27
- package/client.ts +201 -177
- package/commands/setup.ts +200 -33
- package/config/feature-gates.json +10 -0
- package/config.ts +428 -85
- package/context-engine.ts +71 -1402
- package/dist/adapters/http-transport.js +1 -0
- package/dist/adapters/resource-packager.js +114 -0
- package/dist/auto-recall.js +201 -25
- package/dist/client.js +125 -161
- package/dist/commands/setup.js +170 -32
- package/dist/config.js +285 -64
- package/dist/context-engine.js +62 -1173
- package/dist/index.js +209 -1489
- package/dist/memory-ranking.js +35 -12
- package/dist/plugin/command-registration.js +5 -0
- package/dist/plugin/openviking-archive-tools.js +210 -0
- package/dist/plugin/openviking-bypass-runtime.js +8 -0
- package/dist/plugin/openviking-client-runtime.js +28 -0
- package/dist/plugin/openviking-command-args.js +146 -0
- package/dist/plugin/openviking-command-definitions.js +206 -0
- package/dist/plugin/openviking-context-engine-ref.js +11 -0
- package/dist/plugin/openviking-context-engine-registration.js +23 -0
- package/dist/plugin/openviking-feature-gates.js +250 -0
- package/dist/plugin/openviking-import-runtime.js +50 -0
- package/dist/plugin/openviking-import-tools.js +86 -0
- package/dist/plugin/openviking-lifecycle-hooks.js +33 -0
- package/dist/plugin/openviking-memory-recall-tools.js +233 -0
- package/dist/plugin/openviking-memory-tools.js +199 -0
- package/dist/plugin/openviking-query-config-command.js +203 -0
- package/dist/plugin/openviking-query-runtime.js +367 -0
- package/dist/plugin/openviking-query-tools.js +91 -0
- package/dist/plugin/openviking-recall-trace-runtime.js +387 -0
- package/dist/plugin/openviking-recall-trace-tools.js +38 -0
- package/dist/plugin/openviking-runtime-state.js +26 -0
- package/dist/plugin/openviking-runtime-utils.js +62 -0
- package/dist/plugin/openviking-services.js +20 -0
- package/dist/plugin/openviking-session-routing-runtime.js +61 -0
- package/dist/plugin/openviking-tool-result-tools.js +268 -0
- package/dist/plugin/recall-trace-routes.js +77 -0
- package/dist/plugin/tool-registration.js +23 -0
- package/dist/process-manager.js +1 -1
- package/dist/query-config.js +336 -0
- package/dist/recall-trace.js +309 -0
- package/dist/registries/recall-resource-types.js +58 -0
- package/dist/routing/identity-routing.js +152 -0
- package/dist/routing/memory-uri.js +7 -0
- package/dist/services/context-lifecycle-service.js +847 -0
- package/dist/services/context-message-adapter.js +340 -0
- package/dist/session-transcript-repair.js +0 -1
- package/dist/text-utils.js +14 -31
- package/index.ts +271 -1873
- package/install-manifest.json +10 -1
- package/memory-ranking.ts +46 -10
- package/openclaw.plugin.json +198 -36
- package/package.json +14 -4
- package/plugin/command-registration.ts +34 -0
- package/plugin/openviking-archive-tools.ts +301 -0
- package/plugin/openviking-bypass-runtime.ts +26 -0
- package/plugin/openviking-client-runtime.ts +74 -0
- package/plugin/openviking-command-args.ts +187 -0
- package/plugin/openviking-command-definitions.ts +294 -0
- package/plugin/openviking-context-engine-ref.ts +16 -0
- package/plugin/openviking-context-engine-registration.ts +97 -0
- package/plugin/openviking-feature-gates.ts +335 -0
- package/plugin/openviking-import-runtime.ts +105 -0
- package/plugin/openviking-import-tools.ts +133 -0
- package/plugin/openviking-lifecycle-hooks.ts +65 -0
- package/plugin/openviking-memory-recall-tools.ts +368 -0
- package/plugin/openviking-memory-tools.ts +299 -0
- package/plugin/openviking-query-config-command.ts +230 -0
- package/plugin/openviking-query-formatters.ts +108 -0
- package/plugin/openviking-query-runtime.ts +476 -0
- package/plugin/openviking-query-tools.ts +161 -0
- package/plugin/openviking-recall-trace-runtime.ts +482 -0
- package/plugin/openviking-recall-trace-tools.ts +99 -0
- package/plugin/openviking-runtime-state.ts +52 -0
- package/plugin/openviking-runtime-utils.ts +67 -0
- package/plugin/openviking-services.ts +49 -0
- package/plugin/openviking-session-routing-runtime.ts +105 -0
- package/plugin/openviking-tool-result-tools.ts +397 -0
- package/plugin/recall-trace-routes.ts +133 -0
- package/plugin/tool-registration.ts +51 -0
- package/process-manager.ts +1 -1
- package/query-config.ts +400 -0
- package/recall-trace.ts +442 -0
- package/registries/openviking-tools.ts +84 -0
- package/registries/recall-resource-types.ts +74 -0
- package/routing/identity-routing.ts +210 -0
- package/routing/memory-uri.ts +8 -0
- package/services/context-lifecycle-service.ts +1209 -0
- package/services/context-message-adapter.ts +375 -0
- package/services/setup/config-writer.ts +125 -0
- package/services/setup/exit-utils.ts +5 -0
- package/services/setup/package-metadata.ts +56 -0
- package/services/setup/probe-service.ts +143 -0
- package/services/setup/setup-flow.ts +306 -0
- package/services/setup/version-compatibility.ts +41 -0
- package/session-transcript-repair.ts +0 -2
- package/skills/install-openviking-memory/SKILL.md +123 -50
- package/skills/openviking-context-database/SKILL.md +286 -0
- package/text-utils.ts +15 -34
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RecallTraceEntry,
|
|
3
|
+
RecallTraceQuery,
|
|
4
|
+
RecallTraceResult,
|
|
5
|
+
RecallTraceSource,
|
|
6
|
+
} from "../recall-trace.js";
|
|
7
|
+
import type { RecallResourceType } from "../registries/recall-resource-types.js";
|
|
8
|
+
import type { RecallTraceRouteHandlers, RecallTraceRouteRequest } from "./recall-trace-routes.js";
|
|
9
|
+
|
|
10
|
+
export type OpenVikingRecallTraceInput = {
|
|
11
|
+
turn?: "latest" | "all";
|
|
12
|
+
traceId?: string;
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
sessionKey?: string;
|
|
15
|
+
ovSessionId?: string;
|
|
16
|
+
source?: RecallTraceSource;
|
|
17
|
+
resourceTypes?: RecallResourceType[] | string;
|
|
18
|
+
since?: number;
|
|
19
|
+
until?: number;
|
|
20
|
+
includeContent?: boolean;
|
|
21
|
+
limit?: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type OpenVikingRecallTraceSession = {
|
|
25
|
+
sessionId?: string;
|
|
26
|
+
sessionKey?: string;
|
|
27
|
+
ovSessionId?: string;
|
|
28
|
+
agentId: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type OpenVikingRecallTraceQueryResult = {
|
|
32
|
+
entries: RecallTraceEntry[];
|
|
33
|
+
lookupLayer: "memory" | "persistent";
|
|
34
|
+
warnings: string[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type OpenVikingRecallTraceRuntimeDeps = {
|
|
38
|
+
getClient: () => Promise<{ read: (uri: string, agentId?: string) => Promise<unknown> }>;
|
|
39
|
+
resolvePluginSessionRouting: (ctx?: {
|
|
40
|
+
agentId?: string;
|
|
41
|
+
sessionId?: string;
|
|
42
|
+
sessionKey?: string;
|
|
43
|
+
ovSessionId?: string;
|
|
44
|
+
}) => OpenVikingRecallTraceSession;
|
|
45
|
+
traceRecorder?: {
|
|
46
|
+
queryWithFallback: (query: RecallTraceQuery) => Promise<OpenVikingRecallTraceQueryResult>;
|
|
47
|
+
};
|
|
48
|
+
registerRecallTraceRoutes: (ctx: unknown, handlers: RecallTraceRouteHandlers) => boolean;
|
|
49
|
+
normalizeResourceTypes: (value: unknown) => RecallResourceType[];
|
|
50
|
+
clampScore: (score: number) => number;
|
|
51
|
+
previewText: (value: unknown, maxChars: number) => string | undefined;
|
|
52
|
+
cfg: {
|
|
53
|
+
traceRecallIncludeContentByDefault: boolean;
|
|
54
|
+
traceRecallPreviewChars: number;
|
|
55
|
+
recallMaxContentChars: number;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function getOptionalInteger(value: unknown, fallback: number): number {
|
|
60
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
61
|
+
return fallback;
|
|
62
|
+
}
|
|
63
|
+
return Math.floor(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getPositiveInteger(value: unknown, fallback: number): number {
|
|
67
|
+
return Math.max(1, getOptionalInteger(value, fallback));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function toQueryObject(request?: RecallTraceRouteRequest): Record<string, unknown> {
|
|
71
|
+
const query: Record<string, unknown> = { ...(request?.query ?? {}) };
|
|
72
|
+
if (request?.url) {
|
|
73
|
+
const parsed = new URL(request.url, "http://openclaw.local");
|
|
74
|
+
for (const [key, value] of parsed.searchParams.entries()) {
|
|
75
|
+
query[key] = value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { ...query, ...(request?.params ?? {}) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toBoolean(value: unknown): boolean | undefined {
|
|
82
|
+
if (typeof value === "boolean") return value;
|
|
83
|
+
if (typeof value !== "string") return undefined;
|
|
84
|
+
return ["1", "true", "yes"].includes(value.trim().toLowerCase());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function toNumber(value: unknown): number | undefined {
|
|
88
|
+
if (typeof value === "number") return value;
|
|
89
|
+
if (typeof value === "string" && value.trim()) {
|
|
90
|
+
const parsed = Number(value);
|
|
91
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function toString(value: unknown): string | undefined {
|
|
97
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function queryValue(query: Record<string, unknown>, ...keys: string[]): unknown {
|
|
101
|
+
for (const key of keys) {
|
|
102
|
+
if (query[key] !== undefined) {
|
|
103
|
+
return query[key];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function toSessionKey(query: Record<string, unknown>): string | undefined {
|
|
110
|
+
return toString(queryValue(query, "sessionKey", "sessionkey", "session_key", "session-key"));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function toLimitedInteger(value: unknown, fallback: number, min: number, max: number): number | undefined {
|
|
114
|
+
const parsed = toNumber(value);
|
|
115
|
+
const raw = parsed === undefined ? fallback : Math.floor(parsed);
|
|
116
|
+
return Number.isFinite(raw) && raw >= min && raw <= max ? raw : undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function inferUriType(uri: string): string {
|
|
120
|
+
if (uri.startsWith("viking://resources") || uri.startsWith("viking://resource")) return "resource";
|
|
121
|
+
if (uri.startsWith("viking://session/")) return "session";
|
|
122
|
+
if (uri.startsWith("viking://user/skills") || uri.startsWith("viking://skills")) return "skill";
|
|
123
|
+
if (uri.startsWith("viking://user/")) return "user_memory";
|
|
124
|
+
if (uri.includes("/archive") || uri.includes("/history/")) return "archive";
|
|
125
|
+
return "unknown";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function hasExplicitRecallTraceIdentityFilter(input: OpenVikingRecallTraceInput): boolean {
|
|
129
|
+
const traceId = typeof input.traceId === "string" && input.traceId.trim();
|
|
130
|
+
const sessionId = typeof input.sessionId === "string" && input.sessionId.trim();
|
|
131
|
+
const sessionKey = typeof input.sessionKey === "string" && input.sessionKey.trim();
|
|
132
|
+
const ovSessionId = typeof input.ovSessionId === "string" && input.ovSessionId.trim();
|
|
133
|
+
return !!(traceId || sessionId || sessionKey || ovSessionId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function findTraceItem(entry: RecallTraceEntry, uri: string): {
|
|
137
|
+
category?: string;
|
|
138
|
+
score?: number;
|
|
139
|
+
level?: number;
|
|
140
|
+
abstractPreview?: string;
|
|
141
|
+
resultType?: RecallTraceResult["resultType"];
|
|
142
|
+
sourceTraceId?: string;
|
|
143
|
+
source?: RecallTraceSource;
|
|
144
|
+
} | undefined {
|
|
145
|
+
const selected = entry.selected.find((item) => item.uri === uri);
|
|
146
|
+
const searchResult = entry.searches.flatMap((search) => search.results).find((item) => item.uri === uri);
|
|
147
|
+
if (!selected && !searchResult) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
category: selected?.category ?? searchResult?.category,
|
|
152
|
+
score: selected?.score ?? searchResult?.score,
|
|
153
|
+
level: searchResult?.level,
|
|
154
|
+
abstractPreview: selected?.abstractPreview ?? searchResult?.abstractPreview,
|
|
155
|
+
resultType: searchResult?.resultType,
|
|
156
|
+
sourceTraceId: entry.traceId,
|
|
157
|
+
source: entry.source,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function createOpenVikingRecallTraceRuntime(deps: OpenVikingRecallTraceRuntimeDeps): {
|
|
162
|
+
queryRecallTraces: (input: OpenVikingRecallTraceInput, session: OpenVikingRecallTraceSession) => Promise<OpenVikingRecallTraceQueryResult>;
|
|
163
|
+
formatRecallTraceText: (result: { entries: RecallTraceEntry[]; lookupLayer: string; warnings: string[] }) => string;
|
|
164
|
+
routeHandlers: RecallTraceRouteHandlers;
|
|
165
|
+
registerRecallTraceRoutes: (ctx?: unknown) => boolean;
|
|
166
|
+
} {
|
|
167
|
+
const parseRecallTraceInput = (
|
|
168
|
+
input: OpenVikingRecallTraceInput,
|
|
169
|
+
ctx: { sessionId?: string; sessionKey?: string; ovSessionId?: string },
|
|
170
|
+
): RecallTraceQuery => {
|
|
171
|
+
const traceId = typeof input.traceId === "string" && input.traceId.trim() ? input.traceId.trim() : undefined;
|
|
172
|
+
const explicitSessionId = typeof input.sessionId === "string" && input.sessionId.trim() ? input.sessionId.trim() : undefined;
|
|
173
|
+
const explicitSessionKey = typeof input.sessionKey === "string" && input.sessionKey.trim() ? input.sessionKey.trim() : undefined;
|
|
174
|
+
const explicitOvSessionId = typeof input.ovSessionId === "string" && input.ovSessionId.trim() ? input.ovSessionId.trim() : undefined;
|
|
175
|
+
const hasExplicitIdentityFilter = !!(traceId || explicitSessionId || explicitSessionKey || explicitOvSessionId);
|
|
176
|
+
const defaultSessionKey = !hasExplicitIdentityFilter ? ctx.sessionKey : undefined;
|
|
177
|
+
const defaultSessionId = !hasExplicitIdentityFilter && !defaultSessionKey ? ctx.sessionId : undefined;
|
|
178
|
+
const defaultOvSessionId = !hasExplicitIdentityFilter && !defaultSessionKey && !defaultSessionId ? ctx.ovSessionId : undefined;
|
|
179
|
+
return {
|
|
180
|
+
turn: input.turn === "all" ? "all" : "latest",
|
|
181
|
+
traceId,
|
|
182
|
+
sessionId: explicitSessionId ?? defaultSessionId,
|
|
183
|
+
sessionKey: explicitSessionKey ?? defaultSessionKey,
|
|
184
|
+
ovSessionId: explicitOvSessionId ?? defaultOvSessionId,
|
|
185
|
+
source: typeof input.source === "string" && input.source.trim() ? input.source as RecallTraceSource : undefined,
|
|
186
|
+
resourceTypes: input.resourceTypes ? deps.normalizeResourceTypes(input.resourceTypes) : undefined,
|
|
187
|
+
since: typeof input.since === "number" ? input.since : undefined,
|
|
188
|
+
until: typeof input.until === "number" ? input.until : undefined,
|
|
189
|
+
limit: getPositiveInteger(input.limit, 20),
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const shouldIncludeTraceContent = (input?: { includeContent?: boolean }): boolean =>
|
|
194
|
+
typeof input?.includeContent === "boolean" ? input.includeContent : deps.cfg.traceRecallIncludeContentByDefault;
|
|
195
|
+
|
|
196
|
+
const enrichTraceEntriesWithContent = async (
|
|
197
|
+
result: OpenVikingRecallTraceQueryResult,
|
|
198
|
+
includeContent: boolean,
|
|
199
|
+
agentId?: string,
|
|
200
|
+
): Promise<OpenVikingRecallTraceQueryResult> => {
|
|
201
|
+
if (!includeContent || result.entries.length === 0) {
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
const client = await deps.getClient();
|
|
205
|
+
const warnings = [...result.warnings];
|
|
206
|
+
const entries = await Promise.all(result.entries.map(async (entry) => {
|
|
207
|
+
const selected = await Promise.all(entry.selected.map(async (item) => {
|
|
208
|
+
try {
|
|
209
|
+
const content = await client.read(item.uri, agentId);
|
|
210
|
+
const text = typeof content === "string" ? content : JSON.stringify(content);
|
|
211
|
+
return { ...item, contentPreview: deps.previewText(text, deps.cfg.recallMaxContentChars) };
|
|
212
|
+
} catch (err) {
|
|
213
|
+
const readError = err instanceof Error ? err.message : String(err);
|
|
214
|
+
warnings.push(`Failed to read recall trace content ${item.uri}: ${readError}`);
|
|
215
|
+
return { ...item, readError };
|
|
216
|
+
}
|
|
217
|
+
}));
|
|
218
|
+
return { ...entry, selected };
|
|
219
|
+
}));
|
|
220
|
+
return { ...result, entries, warnings };
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const queryTraceForRoute = async (query: OpenVikingRecallTraceInput, session: OpenVikingRecallTraceSession) => deps.traceRecorder
|
|
224
|
+
? deps.traceRecorder.queryWithFallback(parseRecallTraceInput(query, session))
|
|
225
|
+
: Promise.resolve({ entries: [], lookupLayer: "memory" as const, warnings: ["traceRecall is disabled"] });
|
|
226
|
+
|
|
227
|
+
const queryRecallTraces = async (
|
|
228
|
+
input: OpenVikingRecallTraceInput,
|
|
229
|
+
session: OpenVikingRecallTraceSession,
|
|
230
|
+
): Promise<OpenVikingRecallTraceQueryResult> => {
|
|
231
|
+
const query = parseRecallTraceInput(input, session);
|
|
232
|
+
const base = deps.traceRecorder
|
|
233
|
+
? await deps.traceRecorder.queryWithFallback(query)
|
|
234
|
+
: { entries: [], lookupLayer: "memory" as const, warnings: ["traceRecall is disabled"] };
|
|
235
|
+
if (
|
|
236
|
+
deps.traceRecorder &&
|
|
237
|
+
base.entries.length === 0 &&
|
|
238
|
+
!hasExplicitRecallTraceIdentityFilter(input) &&
|
|
239
|
+
query.sessionKey
|
|
240
|
+
) {
|
|
241
|
+
const fallbackIdentities: RecallTraceQuery[] = [];
|
|
242
|
+
if (session.sessionId) {
|
|
243
|
+
fallbackIdentities.push({ ...query, sessionKey: undefined, sessionId: session.sessionId, ovSessionId: undefined });
|
|
244
|
+
}
|
|
245
|
+
if (session.ovSessionId) {
|
|
246
|
+
fallbackIdentities.push({ ...query, sessionKey: undefined, sessionId: undefined, ovSessionId: session.ovSessionId });
|
|
247
|
+
}
|
|
248
|
+
for (const fallbackQuery of fallbackIdentities) {
|
|
249
|
+
const fallback = await deps.traceRecorder.queryWithFallback(fallbackQuery);
|
|
250
|
+
if (fallback.entries.length > 0) {
|
|
251
|
+
return enrichTraceEntriesWithContent(fallback, shouldIncludeTraceContent(input), session.agentId);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return enrichTraceEntriesWithContent(base, shouldIncludeTraceContent(input), session.agentId);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const handleUriDetail = async (request?: RecallTraceRouteRequest) => {
|
|
259
|
+
const query = toQueryObject(request);
|
|
260
|
+
const uri = toString(query.uri);
|
|
261
|
+
if (!uri || !uri.startsWith("viking://") || uri.endsWith("...") || uri.includes("…")) {
|
|
262
|
+
return { status: 400, body: { ok: false, uri: uri ?? "", readStatus: "not_requested", warnings: [], error: { code: "invalid_uri", message: "uri must be a complete viking:// URI" } } };
|
|
263
|
+
}
|
|
264
|
+
const offset = toLimitedInteger(query.offset, 0, 0, 1_000_000_000);
|
|
265
|
+
const contentLimit = toLimitedInteger(queryValue(query, "contentLimit", "content-limit", "content_limit"), 20_000, 1, 100_000);
|
|
266
|
+
if (offset === undefined || contentLimit === undefined) {
|
|
267
|
+
return { status: 400, body: { ok: false, uri, readStatus: "not_requested", warnings: [], error: { code: "invalid_param", message: "offset or contentLimit is invalid" } } };
|
|
268
|
+
}
|
|
269
|
+
const includeContent = toBoolean(queryValue(query, "includeContent", "include-content", "include_content")) ?? true;
|
|
270
|
+
const preferTracePreview = toBoolean(queryValue(query, "preferTracePreview", "prefer-trace-preview", "prefer_trace_preview")) ?? true;
|
|
271
|
+
const session = deps.resolvePluginSessionRouting(query);
|
|
272
|
+
const traceId = toString(queryValue(query, "traceId", "trace-id", "trace_id"));
|
|
273
|
+
let traceMetadata: ReturnType<typeof findTraceItem> | undefined;
|
|
274
|
+
const warnings: string[] = [];
|
|
275
|
+
if (preferTracePreview && traceId && deps.traceRecorder) {
|
|
276
|
+
const traceResult = await queryTraceForRoute({ traceId, turn: "latest", limit: 1 }, session);
|
|
277
|
+
warnings.push(...traceResult.warnings);
|
|
278
|
+
traceMetadata = traceResult.entries[0] ? findTraceItem(traceResult.entries[0], uri) : undefined;
|
|
279
|
+
}
|
|
280
|
+
const baseBody = {
|
|
281
|
+
ok: true,
|
|
282
|
+
uri,
|
|
283
|
+
uriType: inferUriType(uri),
|
|
284
|
+
abstractPreview: traceMetadata?.abstractPreview,
|
|
285
|
+
metadata: {
|
|
286
|
+
category: traceMetadata?.category,
|
|
287
|
+
score: traceMetadata?.score,
|
|
288
|
+
level: traceMetadata?.level,
|
|
289
|
+
resultType: traceMetadata?.resultType,
|
|
290
|
+
sourceTraceId: traceMetadata?.sourceTraceId,
|
|
291
|
+
source: traceMetadata?.source,
|
|
292
|
+
},
|
|
293
|
+
readStatus: "not_requested" as const,
|
|
294
|
+
warnings,
|
|
295
|
+
};
|
|
296
|
+
if (!includeContent) {
|
|
297
|
+
return { status: 200, body: baseBody };
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const client = await deps.getClient();
|
|
301
|
+
const content = await client.read(uri, toString(query.agentId) ?? session.agentId);
|
|
302
|
+
const text = typeof content === "string" ? content : JSON.stringify(content, null, 2);
|
|
303
|
+
const chars = Array.from(text);
|
|
304
|
+
const slice = chars.slice(offset, offset + contentLimit).join("");
|
|
305
|
+
return {
|
|
306
|
+
status: 200,
|
|
307
|
+
body: {
|
|
308
|
+
...baseBody,
|
|
309
|
+
content: {
|
|
310
|
+
text: slice,
|
|
311
|
+
offset,
|
|
312
|
+
limit: contentLimit,
|
|
313
|
+
returnedChars: Array.from(slice).length,
|
|
314
|
+
totalChars: chars.length,
|
|
315
|
+
hasMore: offset + contentLimit < chars.length,
|
|
316
|
+
},
|
|
317
|
+
readStatus: "ok" as const,
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
} catch (err) {
|
|
321
|
+
return {
|
|
322
|
+
status: 502,
|
|
323
|
+
body: {
|
|
324
|
+
...baseBody,
|
|
325
|
+
ok: false,
|
|
326
|
+
readStatus: "read_failed" as const,
|
|
327
|
+
error: { code: "read_failed", message: err instanceof Error ? err.message : String(err) },
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const handleLatestOvSearchList = async (request?: RecallTraceRouteRequest) => {
|
|
334
|
+
const query = toQueryObject(request);
|
|
335
|
+
const session = deps.resolvePluginSessionRouting(query);
|
|
336
|
+
const limit = toLimitedInteger(query.limit, 20, 1, 100);
|
|
337
|
+
if (limit === undefined) {
|
|
338
|
+
return { status: 400, body: { ok: false, items: [], totalItems: 0, warnings: [], error: { code: "invalid_param", message: "limit is invalid" } } };
|
|
339
|
+
}
|
|
340
|
+
const includeSelected = toBoolean(query.includeSelected) ?? true;
|
|
341
|
+
const dedupe = toBoolean(query.dedupe) ?? true;
|
|
342
|
+
const includeSkills = toBoolean(query.includeSkills) ?? true;
|
|
343
|
+
const strict = toBoolean(query.strict) ?? false;
|
|
344
|
+
const result = await queryTraceForRoute({
|
|
345
|
+
turn: "latest",
|
|
346
|
+
source: "ov_search",
|
|
347
|
+
sessionId: toString(query.sessionId),
|
|
348
|
+
sessionKey: toSessionKey(query),
|
|
349
|
+
ovSessionId: toString(query.ovSessionId),
|
|
350
|
+
limit: 1,
|
|
351
|
+
}, session);
|
|
352
|
+
const agentId = toString(query.agentId);
|
|
353
|
+
const entry = agentId ? result.entries.find((candidate) => candidate.agentId === agentId) : result.entries[0];
|
|
354
|
+
if (!entry) {
|
|
355
|
+
const body = {
|
|
356
|
+
ok: !strict,
|
|
357
|
+
lookupLayer: "none" as const,
|
|
358
|
+
fallbackUsed: false,
|
|
359
|
+
query: { sessionId: toString(query.sessionId), sessionKey: toString(query.sessionKey), ovSessionId: toString(query.ovSessionId), agentId, limit, lookup: toString(query.lookup) ?? "auto" },
|
|
360
|
+
items: [],
|
|
361
|
+
totalItems: 0,
|
|
362
|
+
warnings: [...result.warnings, "no ov_search trace found for latest interaction"],
|
|
363
|
+
...(strict ? { error: { code: "not_found", message: "no ov_search trace found for latest interaction" } } : {}),
|
|
364
|
+
};
|
|
365
|
+
return { status: strict ? 404 : 200, body };
|
|
366
|
+
}
|
|
367
|
+
const searchResultByUri = new Map<string, RecallTraceResult & { targetUri?: string }>();
|
|
368
|
+
for (const search of entry.searches) {
|
|
369
|
+
for (const item of search.results) {
|
|
370
|
+
searchResultByUri.set(item.uri, { ...item, targetUri: search.targetUriResolved ?? search.targetUriInput });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const items: Array<Record<string, unknown>> = [];
|
|
374
|
+
const addItem = (item: { uri: string; resourceType?: unknown; category?: string; score?: number; abstractPreview?: string }, source: "search_result" | "selected") => {
|
|
375
|
+
const matched = searchResultByUri.get(item.uri);
|
|
376
|
+
const resultType = matched?.resultType ?? (inferUriType(item.uri) === "skill" ? "skill" : inferUriType(item.uri).includes("memory") ? "memory" : "resource");
|
|
377
|
+
if (!includeSkills && resultType === "skill") {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const row = {
|
|
381
|
+
uri: item.uri,
|
|
382
|
+
abstractPreview: item.abstractPreview ?? matched?.abstractPreview,
|
|
383
|
+
resourceType: item.resourceType ?? matched?.resourceType,
|
|
384
|
+
resultType,
|
|
385
|
+
category: item.category ?? matched?.category,
|
|
386
|
+
score: item.score ?? matched?.score,
|
|
387
|
+
source,
|
|
388
|
+
targetUri: matched?.targetUri,
|
|
389
|
+
detailUrl: `/api/openviking/uri-detail?uri=${encodeURIComponent(item.uri)}&traceId=${encodeURIComponent(entry.traceId)}`,
|
|
390
|
+
};
|
|
391
|
+
if (dedupe) {
|
|
392
|
+
const existing = items.findIndex((candidate) => candidate.uri === item.uri);
|
|
393
|
+
if (existing >= 0) {
|
|
394
|
+
if (source === "selected") items[existing] = row;
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
items.push(row);
|
|
399
|
+
};
|
|
400
|
+
if (includeSelected) {
|
|
401
|
+
entry.selected.forEach((item) => addItem(item, "selected"));
|
|
402
|
+
}
|
|
403
|
+
for (const search of entry.searches) {
|
|
404
|
+
search.results.forEach((item) => addItem(item, "search_result"));
|
|
405
|
+
}
|
|
406
|
+
const limited = items.slice(0, limit);
|
|
407
|
+
return {
|
|
408
|
+
status: 200,
|
|
409
|
+
body: {
|
|
410
|
+
ok: true,
|
|
411
|
+
lookupLayer: result.lookupLayer,
|
|
412
|
+
fallbackUsed: result.lookupLayer === "persistent",
|
|
413
|
+
query: { sessionId: toString(query.sessionId), sessionKey: toString(query.sessionKey), ovSessionId: toString(query.ovSessionId), agentId, limit, lookup: toString(query.lookup) ?? "auto" },
|
|
414
|
+
trace: {
|
|
415
|
+
traceId: entry.traceId,
|
|
416
|
+
ts: entry.ts,
|
|
417
|
+
isoTime: new Date(entry.ts).toISOString(),
|
|
418
|
+
triggerQuery: entry.trigger.query,
|
|
419
|
+
totalSearches: entry.searches.length,
|
|
420
|
+
},
|
|
421
|
+
items: limited,
|
|
422
|
+
totalItems: limited.length,
|
|
423
|
+
warnings: result.warnings,
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const handleRecallTraces = async (request?: RecallTraceRouteRequest) => {
|
|
429
|
+
const query = toQueryObject(request);
|
|
430
|
+
const session = deps.resolvePluginSessionRouting(query);
|
|
431
|
+
const result = await queryRecallTraces({
|
|
432
|
+
turn: query.turn === "all" ? "all" : "latest",
|
|
433
|
+
traceId: toString(queryValue(query, "traceId", "trace-id", "trace_id")),
|
|
434
|
+
sessionId: typeof query.sessionId === "string" ? query.sessionId : undefined,
|
|
435
|
+
sessionKey: toSessionKey(query),
|
|
436
|
+
ovSessionId: typeof query.ovSessionId === "string" ? query.ovSessionId : undefined,
|
|
437
|
+
source: typeof query.source === "string" ? query.source as RecallTraceSource : undefined,
|
|
438
|
+
resourceTypes: typeof query.resourceTypes === "string" ? query.resourceTypes : undefined,
|
|
439
|
+
since: toNumber(query.since),
|
|
440
|
+
until: toNumber(query.until),
|
|
441
|
+
includeContent: toBoolean(queryValue(query, "includeContent", "include-content", "include_content")),
|
|
442
|
+
limit: toNumber(query.limit),
|
|
443
|
+
}, session);
|
|
444
|
+
return { status: 200, body: { ok: true, ...result } };
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const formatRecallTraceText = (result: { entries: RecallTraceEntry[]; lookupLayer: string; warnings: string[] }): string => {
|
|
448
|
+
if (result.entries.length === 0) {
|
|
449
|
+
return `No OpenViking recall traces found (lookupLayer=${result.lookupLayer}).`;
|
|
450
|
+
}
|
|
451
|
+
const blocks = result.entries.map((entry, index) => {
|
|
452
|
+
const selected = entry.selected.slice(0, 8)
|
|
453
|
+
.map((item) => ` - ${item.uri}${item.score !== undefined ? ` (${(deps.clampScore(item.score) * 100).toFixed(0)}%)` : ""}`)
|
|
454
|
+
.join("\n");
|
|
455
|
+
return [
|
|
456
|
+
`## Trace ${index + 1}: ${entry.source}`,
|
|
457
|
+
`traceId: ${entry.traceId}`,
|
|
458
|
+
`query: ${entry.trigger.query}`,
|
|
459
|
+
`resourceTypes: ${entry.resourceTypes.join(", ")}`,
|
|
460
|
+
`stats: candidates=${entry.stats.candidateCount}, selected=${entry.stats.selectedCount}, injected=${entry.stats.injectedCount}`,
|
|
461
|
+
selected ? `selected:\n${selected}` : "selected: (none)",
|
|
462
|
+
].join("\n");
|
|
463
|
+
});
|
|
464
|
+
const warnings = result.warnings.length > 0
|
|
465
|
+
? `\n\nWarnings:\n${result.warnings.map((w) => `- ${w}`).join("\n")}`
|
|
466
|
+
: "";
|
|
467
|
+
return `${blocks.join("\n\n")}${warnings}`;
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const routeHandlers = {
|
|
471
|
+
handleRecallTraces,
|
|
472
|
+
handleUriDetail,
|
|
473
|
+
handleLatestOvSearchList,
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
queryRecallTraces,
|
|
478
|
+
formatRecallTraceText,
|
|
479
|
+
routeHandlers,
|
|
480
|
+
registerRecallTraceRoutes: (ctx?: unknown) => deps.registerRecallTraceRoutes(ctx, routeHandlers),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
|
|
3
|
+
import type { RecallResourceType } from "../registries/recall-resource-types.js";
|
|
4
|
+
import type {
|
|
5
|
+
RecallTraceEntry,
|
|
6
|
+
RecallTraceSource,
|
|
7
|
+
} from "../recall-trace.js";
|
|
8
|
+
|
|
9
|
+
export type OpenVikingRecallTraceToolContext = {
|
|
10
|
+
sessionKey?: string;
|
|
11
|
+
sessionId?: string;
|
|
12
|
+
agentId?: string;
|
|
13
|
+
senderId?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type OpenVikingRecallTraceSession = {
|
|
17
|
+
sessionId?: string;
|
|
18
|
+
sessionKey?: string;
|
|
19
|
+
ovSessionId?: string;
|
|
20
|
+
agentId: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type RecallTraceToolInput = {
|
|
24
|
+
turn?: "latest" | "all";
|
|
25
|
+
traceId?: string;
|
|
26
|
+
sessionId?: string;
|
|
27
|
+
sessionKey?: string;
|
|
28
|
+
ovSessionId?: string;
|
|
29
|
+
source?: RecallTraceSource;
|
|
30
|
+
resourceTypes?: RecallResourceType[] | string;
|
|
31
|
+
since?: number;
|
|
32
|
+
until?: number;
|
|
33
|
+
includeContent?: boolean;
|
|
34
|
+
limit?: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type RecallTraceToolResult = {
|
|
38
|
+
entries: RecallTraceEntry[];
|
|
39
|
+
lookupLayer: string;
|
|
40
|
+
warnings: string[];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type OpenVikingRecallTraceToolsDeps = {
|
|
44
|
+
registerTool: (toolOrFactory: unknown, opts: { name: string }) => void;
|
|
45
|
+
queryRecallTraces: (
|
|
46
|
+
input: RecallTraceToolInput,
|
|
47
|
+
session: OpenVikingRecallTraceSession,
|
|
48
|
+
) => Promise<RecallTraceToolResult>;
|
|
49
|
+
formatRecallTraceText: (result: RecallTraceToolResult) => string;
|
|
50
|
+
resolvePluginSessionRouting: (
|
|
51
|
+
ctx?: OpenVikingRecallTraceToolContext,
|
|
52
|
+
) => OpenVikingRecallTraceSession;
|
|
53
|
+
isBypassedSession: (ctx?: OpenVikingRecallTraceToolContext) => boolean;
|
|
54
|
+
makeBypassedToolResult: (toolName: string) => unknown;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function registerOpenVikingRecallTraceTools(
|
|
58
|
+
deps: OpenVikingRecallTraceToolsDeps,
|
|
59
|
+
): void {
|
|
60
|
+
deps.registerTool(
|
|
61
|
+
(ctx: OpenVikingRecallTraceToolContext) => ({
|
|
62
|
+
name: "ov_recall_trace",
|
|
63
|
+
label: "Recall Trace (OpenViking)",
|
|
64
|
+
description:
|
|
65
|
+
"Query OpenViking recall trace records captured by auto-recall and explicit recall/search tools.",
|
|
66
|
+
parameters: Type.Object({
|
|
67
|
+
turn: Type.Optional(Type.String({ description: "latest or all (default: latest)" })),
|
|
68
|
+
traceId: Type.Optional(Type.String({ description: "Exact trace id" })),
|
|
69
|
+
sessionId: Type.Optional(Type.String({ description: "OpenClaw session id" })),
|
|
70
|
+
sessionKey: Type.Optional(Type.String({ description: "OpenClaw session key" })),
|
|
71
|
+
ovSessionId: Type.Optional(Type.String({ description: "OpenViking session id" })),
|
|
72
|
+
source: Type.Optional(Type.String({ description: "auto_recall, memory_recall, ov_search, or ov_archive_search" })),
|
|
73
|
+
resourceTypes: Type.Optional(Type.Array(Type.String({ description: "resource, user, or agent" }))),
|
|
74
|
+
since: Type.Optional(Type.Number({ description: "Unix timestamp lower bound in milliseconds" })),
|
|
75
|
+
until: Type.Optional(Type.Number({ description: "Unix timestamp upper bound in milliseconds" })),
|
|
76
|
+
includeContent: Type.Optional(Type.Boolean({ description: "Read selected/displayed URI content previews on demand" })),
|
|
77
|
+
limit: Type.Optional(Type.Number({ description: "Maximum traces to return (default: 20)" })),
|
|
78
|
+
}),
|
|
79
|
+
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
|
80
|
+
if (deps.isBypassedSession(ctx)) {
|
|
81
|
+
return deps.makeBypassedToolResult("ov_recall_trace");
|
|
82
|
+
}
|
|
83
|
+
const session = deps.resolvePluginSessionRouting(ctx);
|
|
84
|
+
const result = await deps.queryRecallTraces(params as RecallTraceToolInput, session);
|
|
85
|
+
return {
|
|
86
|
+
content: [{ type: "text" as const, text: deps.formatRecallTraceText(result) }],
|
|
87
|
+
details: {
|
|
88
|
+
action: "queried",
|
|
89
|
+
count: result.entries.length,
|
|
90
|
+
lookupLayer: result.lookupLayer,
|
|
91
|
+
warnings: result.warnings,
|
|
92
|
+
entries: result.entries,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
{ name: "ov_recall_trace" },
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { MemoryOpenVikingConfig } from "../config.js";
|
|
2
|
+
import { RuntimeQueryConfigStore } from "../query-config.js";
|
|
3
|
+
import { RecallTraceRecorder } from "../recall-trace.js";
|
|
4
|
+
|
|
5
|
+
type Logger = {
|
|
6
|
+
warn?: (message: string) => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type RuntimeStateConfig = Required<
|
|
10
|
+
Pick<
|
|
11
|
+
MemoryOpenVikingConfig,
|
|
12
|
+
| "runtimeQueryConfigPath"
|
|
13
|
+
| "traceRecall"
|
|
14
|
+
| "traceRecallMaxEntries"
|
|
15
|
+
| "traceRecallPersist"
|
|
16
|
+
| "traceRecallDir"
|
|
17
|
+
| "traceRecallIncludeRawUserPreview"
|
|
18
|
+
| "traceRecallRetentionDays"
|
|
19
|
+
| "traceRecallQueryMaxDays"
|
|
20
|
+
>
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
export function createOpenVikingRuntimeState(options: {
|
|
24
|
+
cfg: Required<MemoryOpenVikingConfig> & RuntimeStateConfig;
|
|
25
|
+
logger: Logger;
|
|
26
|
+
}) {
|
|
27
|
+
const { cfg, logger } = options;
|
|
28
|
+
|
|
29
|
+
const queryConfigStore = new RuntimeQueryConfigStore({
|
|
30
|
+
staticConfig: cfg,
|
|
31
|
+
path: cfg.runtimeQueryConfigPath || undefined,
|
|
32
|
+
});
|
|
33
|
+
void queryConfigStore.load().catch((err) => {
|
|
34
|
+
logger.warn?.(`openviking: failed to load runtime query config: ${String(err)}`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const traceRecorder = cfg.traceRecall
|
|
38
|
+
? new RecallTraceRecorder({
|
|
39
|
+
memoryMaxEntries: cfg.traceRecallMaxEntries,
|
|
40
|
+
persist: cfg.traceRecallPersist,
|
|
41
|
+
traceDir: cfg.traceRecallDir,
|
|
42
|
+
includeRawUserPreview: cfg.traceRecallIncludeRawUserPreview,
|
|
43
|
+
retentionDays: cfg.traceRecallRetentionDays,
|
|
44
|
+
queryMaxDays: cfg.traceRecallQueryMaxDays,
|
|
45
|
+
})
|
|
46
|
+
: undefined;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
queryConfigStore,
|
|
50
|
+
traceRecorder,
|
|
51
|
+
};
|
|
52
|
+
}
|