@openviking/openclaw-plugin 2026.9.1 → 2026.9.4-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.
- package/INSTALL-AGENT.md +25 -4
- package/INSTALL-ZH.md +22 -4
- package/INSTALL.md +22 -4
- package/README.md +19 -15
- package/auto-recall.ts +126 -135
- package/client.ts +95 -0
- package/commands/setup.ts +22 -8
- package/config.ts +31 -16
- package/context-engine.ts +2 -2
- package/dist/auto-recall.js +113 -114
- package/dist/client.js +54 -0
- package/dist/commands/setup.js +15 -6
- package/dist/config.js +18 -11
- package/dist/plugin/openviking-memory-tools.js +1 -1
- package/dist/plugin/openviking-session-routing-runtime.js +2 -2
- package/dist/routing/identity-routing.js +5 -5
- package/dist/services/context-lifecycle-service.js +4 -4
- package/dist/shared/recall-compress-core.mjs +188 -0
- package/dist/shared/recall-core.mjs +645 -0
- package/install-manifest.json +1 -0
- package/openclaw.plugin.json +9 -8
- package/package.json +2 -1
- package/plugin/openviking-client-runtime.ts +1 -1
- package/plugin/openviking-memory-tools.ts +1 -1
- package/plugin/openviking-session-routing-runtime.ts +2 -2
- package/routing/identity-routing.ts +8 -8
- package/services/context-lifecycle-service.ts +4 -4
- package/services/setup/setup-flow.ts +8 -6
- package/shared/recall-compress-core.mjs +195 -0
- package/shared/recall-core.d.mts +39 -0
- package/shared/recall-core.mjs +678 -0
- package/skills/install-openviking-memory/SKILL.md +33 -10
- package/skills/openviking-context-database/SKILL.md +2 -2
package/client.ts
CHANGED
|
@@ -12,6 +12,11 @@ import {
|
|
|
12
12
|
defaultResourcePackager,
|
|
13
13
|
type ResourcePackager,
|
|
14
14
|
} from "./adapters/resource-packager.js";
|
|
15
|
+
import {
|
|
16
|
+
buildContextSearchBody,
|
|
17
|
+
contextRequestTimeoutMs,
|
|
18
|
+
normalizeContextEntry,
|
|
19
|
+
} from "./shared/recall-core.mjs";
|
|
15
20
|
|
|
16
21
|
export type FindResultItem = {
|
|
17
22
|
uri: string;
|
|
@@ -30,6 +35,36 @@ export type FindResult = {
|
|
|
30
35
|
total?: number;
|
|
31
36
|
};
|
|
32
37
|
|
|
38
|
+
export type SearchContextEntry = {
|
|
39
|
+
uri: string;
|
|
40
|
+
category?: string;
|
|
41
|
+
score?: number;
|
|
42
|
+
detail?: string;
|
|
43
|
+
text?: string;
|
|
44
|
+
origin?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type SearchContextResult = {
|
|
48
|
+
entries?: SearchContextEntry[];
|
|
49
|
+
rendered?: string;
|
|
50
|
+
digest?: string;
|
|
51
|
+
stats?: Record<string, unknown>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type SearchContextOptions = {
|
|
55
|
+
sessionId?: string;
|
|
56
|
+
limit?: number;
|
|
57
|
+
scoreThreshold?: number;
|
|
58
|
+
contextType?: string | string[];
|
|
59
|
+
queryExpansion?: "off" | "auto";
|
|
60
|
+
maxTokens?: number;
|
|
61
|
+
detail?: "abstract" | "overview" | "full";
|
|
62
|
+
dedupTurns?: number;
|
|
63
|
+
peerScope?: "actor" | "all";
|
|
64
|
+
actorPeerId?: string;
|
|
65
|
+
requestTimeoutMs?: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
33
68
|
export type FsListEntry = string | Record<string, unknown>;
|
|
34
69
|
|
|
35
70
|
export type FsListResult = FsListEntry[];
|
|
@@ -488,6 +523,66 @@ export class OpenVikingClient {
|
|
|
488
523
|
}, undefined, actorPeerId);
|
|
489
524
|
}
|
|
490
525
|
|
|
526
|
+
async searchContext(
|
|
527
|
+
query: string,
|
|
528
|
+
options: SearchContextOptions = {},
|
|
529
|
+
): Promise<SearchContextResult> {
|
|
530
|
+
const contractConfig = {
|
|
531
|
+
recallLimit: options.limit,
|
|
532
|
+
recallLimitConfigured: options.limit !== undefined,
|
|
533
|
+
recallMaxTokens: options.maxTokens,
|
|
534
|
+
recallMaxTokensConfigured: options.maxTokens !== undefined,
|
|
535
|
+
scoreThreshold: options.scoreThreshold,
|
|
536
|
+
recallQueryExpansion: options.queryExpansion,
|
|
537
|
+
recallQueryExpansionConfigured: options.queryExpansion !== undefined,
|
|
538
|
+
recallDedupTurns: options.dedupTurns,
|
|
539
|
+
recallPeerScope: options.peerScope,
|
|
540
|
+
recallContextTimeoutMs: options.requestTimeoutMs,
|
|
541
|
+
timeoutMs: this.timeoutMs,
|
|
542
|
+
};
|
|
543
|
+
const body = {
|
|
544
|
+
...buildContextSearchBody(contractConfig, { sessionId: options.sessionId }),
|
|
545
|
+
query,
|
|
546
|
+
...(options.contextType !== undefined ? { context_type: options.contextType } : {}),
|
|
547
|
+
...(options.detail !== undefined ? { detail: options.detail } : {}),
|
|
548
|
+
};
|
|
549
|
+
const requestTimeoutMs = contextRequestTimeoutMs(contractConfig, body);
|
|
550
|
+
const actorPeerId = this.resolveActorPeerHeader(options.actorPeerId);
|
|
551
|
+
const tenantHeaders = this.resolveTenantHeaders();
|
|
552
|
+
this.routingDebugLog?.(
|
|
553
|
+
`openviking: context search POST ${this.baseUrl}/api/v1/search/search ` +
|
|
554
|
+
JSON.stringify({
|
|
555
|
+
X_OpenViking_Account: tenantHeaders.accountId ?? null,
|
|
556
|
+
X_OpenViking_User: tenantHeaders.userId ?? null,
|
|
557
|
+
X_OpenViking_Actor_Peer: actorPeerId ?? null,
|
|
558
|
+
session_id: body.session_id ?? null,
|
|
559
|
+
query:
|
|
560
|
+
query.length > 4000
|
|
561
|
+
? `${query.slice(0, 4000)}…(+${query.length - 4000} more chars)`
|
|
562
|
+
: query,
|
|
563
|
+
purpose: body.purpose,
|
|
564
|
+
quotas: body.quotas ?? null,
|
|
565
|
+
score_threshold: body.score_threshold,
|
|
566
|
+
context_type: body.context_type ?? null,
|
|
567
|
+
query_expansion: body.query_expansion ?? null,
|
|
568
|
+
max_tokens: body.max_tokens ?? null,
|
|
569
|
+
detail: body.detail ?? null,
|
|
570
|
+
dedup_turns: body.dedup_turns ?? 0,
|
|
571
|
+
peer_scope: body.peer_scope ?? null,
|
|
572
|
+
}),
|
|
573
|
+
);
|
|
574
|
+
const result = await this.request<SearchContextResult>("/api/v1/search/search", {
|
|
575
|
+
method: "POST",
|
|
576
|
+
body: JSON.stringify(body),
|
|
577
|
+
}, requestTimeoutMs, actorPeerId);
|
|
578
|
+
return {
|
|
579
|
+
...result,
|
|
580
|
+
entries: Array.isArray(result.entries)
|
|
581
|
+
? result.entries.map((entry) => ({ ...entry, ...normalizeContextEntry(entry) }))
|
|
582
|
+
: result.entries,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
491
586
|
async read(uri: string, actorPeerId?: string): Promise<string> {
|
|
492
587
|
return this.request<string>(
|
|
493
588
|
`/api/v1/content/read?uri=${encodeURIComponent(uri)}`,
|
package/commands/setup.ts
CHANGED
|
@@ -84,8 +84,8 @@ const CONFIG_KEYS_TO_PRESERVE = [
|
|
|
84
84
|
"logFindRequests",
|
|
85
85
|
] as const;
|
|
86
86
|
|
|
87
|
-
type PeerRole = "none" | "assistant" | "
|
|
88
|
-
const DEFAULT_SETUP_PEER_ROLE: PeerRole = "
|
|
87
|
+
type PeerRole = "none" | "assistant" | "sender";
|
|
88
|
+
const DEFAULT_SETUP_PEER_ROLE: PeerRole = "none";
|
|
89
89
|
type RecallTargetType = "resource" | "user" | "agent";
|
|
90
90
|
const ALLOWED_RECALL_TARGET_TYPES = ["resource", "user", "agent"] as const;
|
|
91
91
|
|
|
@@ -126,7 +126,8 @@ function isValidPeerPrefixInput(value: string): boolean {
|
|
|
126
126
|
function normalizePeerRole(value: unknown): PeerRole | undefined {
|
|
127
127
|
if (typeof value !== "string") return undefined;
|
|
128
128
|
const role = value.trim().toLowerCase();
|
|
129
|
-
if (role === "
|
|
129
|
+
if (role === "person") return "sender";
|
|
130
|
+
if (role === "none" || role === "assistant" || role === "sender") return role;
|
|
130
131
|
return undefined;
|
|
131
132
|
}
|
|
132
133
|
|
|
@@ -147,7 +148,7 @@ function resolveSetupPeerRole(value: unknown): PeerRole {
|
|
|
147
148
|
if (value === undefined) return DEFAULT_SETUP_PEER_ROLE;
|
|
148
149
|
const role = normalizePeerRole(value);
|
|
149
150
|
if (role) return role;
|
|
150
|
-
throw new Error('peer_role must be "none", "assistant", or "person"');
|
|
151
|
+
throw new Error('peer_role must be "none", "assistant", or "sender" (legacy alias: "person")');
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
function normalizeSetupRecallTargetTypes(value: unknown): RecallTargetType[] | undefined {
|
|
@@ -206,9 +207,20 @@ async function askPeerRole(
|
|
|
206
207
|
q: (prompt: string, def?: string) => Promise<string>,
|
|
207
208
|
defaultValue: PeerRole,
|
|
208
209
|
): Promise<PeerRole> {
|
|
210
|
+
console.log(
|
|
211
|
+
` ${tr(
|
|
212
|
+
zh,
|
|
213
|
+
"Memory scope — none (default): viking://user/<user_id>/memories, shared across all conversations; " +
|
|
214
|
+
"assistant: viking://user/<user_id>/peers/<assistant_id>/memories; " +
|
|
215
|
+
"sender: viking://user/<user_id>/peers/<sender_id>/memories (legacy alias: person).",
|
|
216
|
+
"记忆归属 —— none(默认):viking://user/<user_id>/memories,所有对话共享;" +
|
|
217
|
+
"assistant:viking://user/<user_id>/peers/<assistant_id>/memories;" +
|
|
218
|
+
"sender:viking://user/<user_id>/peers/<sender_id>/memories(兼容旧值 person)。",
|
|
219
|
+
)}`,
|
|
220
|
+
);
|
|
209
221
|
while (true) {
|
|
210
222
|
const value = await q(
|
|
211
|
-
tr(zh, "
|
|
223
|
+
tr(zh, "Memory scope (none/assistant/sender)", "记忆归属(none/assistant/sender)"),
|
|
212
224
|
defaultValue,
|
|
213
225
|
);
|
|
214
226
|
const role = normalizePeerRole(value);
|
|
@@ -216,8 +228,8 @@ async function askPeerRole(
|
|
|
216
228
|
console.log(
|
|
217
229
|
` ✗ ${tr(
|
|
218
230
|
zh,
|
|
219
|
-
'
|
|
220
|
-
'
|
|
231
|
+
'Memory scope must be "none", "assistant", or "sender" (legacy "person" is also accepted).',
|
|
232
|
+
'记忆归属必须是 "none"、"assistant" 或 "sender"(也兼容旧值 "person")。',
|
|
221
233
|
)}`,
|
|
222
234
|
);
|
|
223
235
|
}
|
|
@@ -605,7 +617,7 @@ export function registerSetupCli(api: any): void {
|
|
|
605
617
|
.option("--zh", "Chinese prompts")
|
|
606
618
|
.option("--base-url <url>", "OpenViking server URL (enables non-interactive mode)")
|
|
607
619
|
.option("--api-key <key>", "API key for authentication")
|
|
608
|
-
.option("--peer-role <role>", "
|
|
620
|
+
.option("--peer-role <role>", "Memory scope: none (shared), assistant (per assistant), or sender (per sender); person is a legacy alias")
|
|
609
621
|
.option("--peer-prefix <prefix>", "Prefix for assistant peer_id values")
|
|
610
622
|
.option("--account-id <id>", "Account ID (required for root API keys)")
|
|
611
623
|
.option("--user-id <id>", "User ID (required for root API keys)")
|
|
@@ -1182,6 +1194,8 @@ async function setupRemote(
|
|
|
1182
1194
|
export const __test__ = {
|
|
1183
1195
|
isLegacyLocalMode,
|
|
1184
1196
|
isValidPeerPrefixInput,
|
|
1197
|
+
normalizePeerRole,
|
|
1198
|
+
resolveSetupPeerRole,
|
|
1185
1199
|
normalizeSetupRecallTargetTypes,
|
|
1186
1200
|
activateContextEngineSlot,
|
|
1187
1201
|
isContextEngineSlotActive,
|
package/config.ts
CHANGED
|
@@ -30,7 +30,8 @@ export type OpenVikingSecretRef =
|
|
|
30
30
|
export type MemoryOpenVikingConfig = {
|
|
31
31
|
mode?: "remote";
|
|
32
32
|
baseUrl?: string;
|
|
33
|
-
|
|
33
|
+
/** `person` is a legacy alias for `sender`. */
|
|
34
|
+
peer_role?: "none" | "assistant" | "sender" | "person";
|
|
34
35
|
peer_prefix?: string;
|
|
35
36
|
apiKey?: string | OpenVikingSecretRef;
|
|
36
37
|
/** Optional HTTP headers merged into every OpenViking request. */
|
|
@@ -45,13 +46,13 @@ export type MemoryOpenVikingConfig = {
|
|
|
45
46
|
captureMode?: "semantic" | "keyword";
|
|
46
47
|
captureMaxLength?: number;
|
|
47
48
|
autoRecall?: boolean;
|
|
48
|
-
/** Outer time budget for the whole auto-recall flow
|
|
49
|
+
/** Outer time budget for the whole server-assembled auto-recall flow. */
|
|
49
50
|
autoRecallTimeoutMs?: number;
|
|
50
51
|
/** Include resources in auto-recall and default memory_recall search. Default false. */
|
|
51
52
|
recallResources?: boolean;
|
|
52
53
|
recallLimit?: number;
|
|
53
54
|
recallScoreThreshold?: number;
|
|
54
|
-
/**
|
|
55
|
+
/** Legacy character budget, converted to the server context token budget. */
|
|
55
56
|
recallMaxInjectedChars?: number;
|
|
56
57
|
/** @deprecated Auto-recall no longer truncates individual memories. */
|
|
57
58
|
recallMaxContentChars?: number;
|
|
@@ -127,10 +128,12 @@ export type MemoryOpenVikingConfig = {
|
|
|
127
128
|
|
|
128
129
|
/** Runtime config after memoryOpenVikingConfigSchema.parse() has applied defaults. */
|
|
129
130
|
export type ParsedMemoryOpenVikingConfig = Required<
|
|
130
|
-
Omit<MemoryOpenVikingConfig, "agentExperience" | "recallTargetTypes" | "apiKey">
|
|
131
|
+
Omit<MemoryOpenVikingConfig, "agentExperience" | "recallTargetTypes" | "apiKey" | "peer_role">
|
|
131
132
|
> & {
|
|
132
133
|
/** parse() resolves SecretRef values, so the runtime shape is always a plain string. */
|
|
133
134
|
apiKey: string;
|
|
135
|
+
/** Runtime uses the canonical name; legacy `person` input normalizes to `sender`. */
|
|
136
|
+
peer_role: "none" | "assistant" | "sender";
|
|
134
137
|
agentExperience: Required<NonNullable<MemoryOpenVikingConfig["agentExperience"]>>;
|
|
135
138
|
recallTargetTypes: Array<"resource" | "user" | "agent">;
|
|
136
139
|
};
|
|
@@ -140,7 +143,9 @@ const DEFAULT_TARGET_URI = "viking://~/memories";
|
|
|
140
143
|
const DEFAULT_TIMEOUT_MS = 15000;
|
|
141
144
|
const DEFAULT_CAPTURE_MODE = "semantic";
|
|
142
145
|
const DEFAULT_CAPTURE_MAX_LENGTH = 24000;
|
|
143
|
-
|
|
146
|
+
// Session-aware context search may spend up to 5 seconds on query expansion
|
|
147
|
+
// before returning its server-side fallback, so leave headroom for retrieval.
|
|
148
|
+
const DEFAULT_AUTO_RECALL_TIMEOUT_MS = 15000;
|
|
144
149
|
const DEFAULT_RECALL_LIMIT = 6;
|
|
145
150
|
const DEFAULT_RECALL_SCORE_THRESHOLD = 0.15;
|
|
146
151
|
const DEFAULT_RECALL_MAX_CONTENT_CHARS = 5000;
|
|
@@ -150,7 +155,7 @@ const DEFAULT_COMMIT_TOKEN_THRESHOLD_RATIO = 0.5;
|
|
|
150
155
|
const DEFAULT_COMMIT_KEEP_RECENT_COUNT = 10;
|
|
151
156
|
const DEFAULT_BYPASS_SESSION_PATTERNS: string[] = [];
|
|
152
157
|
const DEFAULT_EMIT_STANDARD_DIAGNOSTICS = false;
|
|
153
|
-
const DEFAULT_PEER_ROLE = "
|
|
158
|
+
const DEFAULT_PEER_ROLE = "none" as const;
|
|
154
159
|
const DEFAULT_PEER_PREFIX = "";
|
|
155
160
|
const DEFAULT_TRACE_RECALL_DIR = "~/.openclaw/openviking/recall-traces";
|
|
156
161
|
const DEFAULT_TRACE_RECALL_RETENTION_DAYS = 14;
|
|
@@ -290,16 +295,23 @@ function resolveSecret(
|
|
|
290
295
|
}
|
|
291
296
|
}
|
|
292
297
|
|
|
293
|
-
function resolvePeerRole(configured: unknown) {
|
|
298
|
+
function resolvePeerRole(configured: unknown): "none" | "assistant" | "sender" {
|
|
294
299
|
if (typeof configured === "string") {
|
|
295
300
|
const role = configured.trim().toLowerCase();
|
|
296
|
-
if (role === "none" || role === "assistant" || role === "
|
|
301
|
+
if (role === "none" || role === "assistant" || role === "sender") {
|
|
297
302
|
return role;
|
|
298
303
|
}
|
|
299
|
-
|
|
304
|
+
if (role === "person") {
|
|
305
|
+
return "sender";
|
|
306
|
+
}
|
|
307
|
+
throw new Error(
|
|
308
|
+
`openviking peer_role must be "none", "assistant", or "sender" (legacy alias: "person")`,
|
|
309
|
+
);
|
|
300
310
|
}
|
|
301
311
|
if (configured !== undefined) {
|
|
302
|
-
throw new Error(
|
|
312
|
+
throw new Error(
|
|
313
|
+
`openviking peer_role must be "none", "assistant", or "sender" (legacy alias: "person")`,
|
|
314
|
+
);
|
|
303
315
|
}
|
|
304
316
|
return DEFAULT_PEER_ROLE;
|
|
305
317
|
}
|
|
@@ -774,14 +786,17 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
774
786
|
help: "HTTP URL when mode is remote (or use ${OPENVIKING_BASE_URL})",
|
|
775
787
|
},
|
|
776
788
|
peer_role: {
|
|
777
|
-
label: "
|
|
789
|
+
label: "Memory Scope (peer_role)",
|
|
778
790
|
placeholder: DEFAULT_PEER_ROLE,
|
|
779
|
-
help:
|
|
791
|
+
help:
|
|
792
|
+
'Where peer-scoped memories are stored. "none" (default): viking://user/<user_id>/memories. ' +
|
|
793
|
+
'"assistant": viking://user/<user_id>/peers/<assistant_id>/memories. ' +
|
|
794
|
+
'"sender": viking://user/<user_id>/peers/<sender_id>/memories. Legacy "person" is accepted as "sender".',
|
|
780
795
|
},
|
|
781
796
|
peer_prefix: {
|
|
782
797
|
label: "Peer Prefix",
|
|
783
798
|
placeholder: "optional-prefix",
|
|
784
|
-
help: "
|
|
799
|
+
help: 'Only used when Memory Scope is "assistant". Prefix added to the peer id derived from the OpenClaw agent id.',
|
|
785
800
|
},
|
|
786
801
|
apiKey: {
|
|
787
802
|
label: "OpenViking API Key",
|
|
@@ -843,7 +858,7 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
843
858
|
label: "Auto-Recall Timeout (ms)",
|
|
844
859
|
placeholder: String(DEFAULT_AUTO_RECALL_TIMEOUT_MS),
|
|
845
860
|
advanced: true,
|
|
846
|
-
help: "Outer time budget for the
|
|
861
|
+
help: "Outer time budget for the server-assembled context search, including session-aware query expansion.",
|
|
847
862
|
},
|
|
848
863
|
recallResources: {
|
|
849
864
|
label: "Recall Resources",
|
|
@@ -870,7 +885,7 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
870
885
|
label: "Recall Max Injected Chars",
|
|
871
886
|
placeholder: String(DEFAULT_RECALL_MAX_INJECTED_CHARS),
|
|
872
887
|
advanced: true,
|
|
873
|
-
help: "
|
|
888
|
+
help: "Legacy auto-recall character budget. It is converted to the server context token budget at four characters per token.",
|
|
874
889
|
},
|
|
875
890
|
recallMaxContentChars: {
|
|
876
891
|
label: "Deprecated Recall Max Content Chars",
|
|
@@ -881,7 +896,7 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
881
896
|
recallPreferAbstract: {
|
|
882
897
|
label: "Recall Prefer Abstract",
|
|
883
898
|
advanced: true,
|
|
884
|
-
help: "
|
|
899
|
+
help: "Pin server-assembled auto-recall entries to abstract detail. When disabled, the server chooses each category's default detail tier.",
|
|
885
900
|
},
|
|
886
901
|
recallTokenBudget: {
|
|
887
902
|
label: "Deprecated Recall Token Budget",
|
package/context-engine.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OpenVikingClient } from "./client.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ParsedMemoryOpenVikingConfig } from "./config.js";
|
|
3
3
|
import type { RuntimeQueryConfigStore } from "./query-config.js";
|
|
4
4
|
import {
|
|
5
5
|
AUTO_RECALL_SOURCE_MARKER,
|
|
@@ -248,7 +248,7 @@ export function createMemoryOpenVikingContextEngine(params: {
|
|
|
248
248
|
id: string;
|
|
249
249
|
name: string;
|
|
250
250
|
version?: string;
|
|
251
|
-
cfg:
|
|
251
|
+
cfg: ParsedMemoryOpenVikingConfig;
|
|
252
252
|
logger: Logger;
|
|
253
253
|
getClient: () => Promise<OpenVikingClient>;
|
|
254
254
|
/** Extra args help match hook-populated routing when OpenClaw provides sessionKey / OV session id. */
|
package/dist/auto-recall.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { toJsonLog } from "./memory-ranking.js";
|
|
2
2
|
import { quickRecallPrecheck, withTimeout } from "./process-manager.js";
|
|
3
3
|
import { resolveRecallSearchPlan, } from "./registries/recall-resource-types.js";
|
|
4
4
|
import { sanitizeUserTextForCapture } from "./text-utils.js";
|
|
5
5
|
import { estimateTextTokens } from "./token-estimator.js";
|
|
6
6
|
const RECALL_QUERY_MAX_CHARS = 4_000;
|
|
7
|
+
const AUTO_RECALL_DEDUP_TURNS = 5;
|
|
8
|
+
const LEGACY_CHARS_PER_TOKEN = 4;
|
|
7
9
|
export const AUTO_RECALL_SOURCE_MARKER = "Source: openviking-auto-recall";
|
|
8
10
|
const WRITE_OR_EFFECT_RE = /\b(write|edit|modify|delete|remove|migrate|deploy|release|publish|configure|patch)\b|写|改|修改|删除|迁移|部署|发布|配置|打补丁/i;
|
|
9
11
|
const EXECUTION_RE = /\b(fix|debug|test|build|run|implement|refactor|integrate|repair|troubleshoot)\b|修复|调试|测试|构建|运行|实现|重构|对接|排查/i;
|
|
@@ -135,16 +137,25 @@ function preview(value, maxChars) {
|
|
|
135
137
|
return undefined;
|
|
136
138
|
return trimmed.length > maxChars ? trimmed.slice(0, maxChars) : trimmed;
|
|
137
139
|
}
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
function traceResourceType(entry) {
|
|
141
|
+
if (entry.category === "resources" || entry.uri.startsWith("viking://resources")) {
|
|
142
|
+
return "resource";
|
|
143
|
+
}
|
|
144
|
+
if (entry.origin === "actor_peer") {
|
|
145
|
+
return "agent";
|
|
146
|
+
}
|
|
147
|
+
return "user";
|
|
148
|
+
}
|
|
149
|
+
function toTraceResult(entry) {
|
|
150
|
+
const resourceType = traceResourceType(entry);
|
|
151
|
+
return {
|
|
152
|
+
uri: entry.uri,
|
|
141
153
|
resourceType,
|
|
142
|
-
category:
|
|
143
|
-
score:
|
|
144
|
-
|
|
145
|
-
abstractPreview: preview(item.abstract ?? item.overview, 240),
|
|
154
|
+
category: entry.category,
|
|
155
|
+
score: entry.score,
|
|
156
|
+
abstractPreview: preview(entry.text, 240),
|
|
146
157
|
resultType: resourceType === "resource" ? "resource" : "memory",
|
|
147
|
-
}
|
|
158
|
+
};
|
|
148
159
|
}
|
|
149
160
|
function boundTraceQuery(query, maxChars) {
|
|
150
161
|
if (query.length <= maxChars) {
|
|
@@ -211,7 +222,6 @@ export async function buildAutoRecallContext(params) {
|
|
|
211
222
|
return { memoryCount: 0, estimatedTokens: 0 };
|
|
212
223
|
}
|
|
213
224
|
return withTimeout((async () => {
|
|
214
|
-
const candidateLimit = queryConfig?.candidateLimit ?? Math.max(cfg.recallLimit * 4, 20);
|
|
215
225
|
const scoreThreshold = queryConfig?.scoreThreshold ?? cfg.recallScoreThreshold;
|
|
216
226
|
const recallLimit = queryConfig?.recallLimit ?? cfg.recallLimit;
|
|
217
227
|
const maxInjectedChars = queryConfig?.maxInjectedChars ?? cfg.recallMaxInjectedChars;
|
|
@@ -220,89 +230,75 @@ export async function buildAutoRecallContext(params) {
|
|
|
220
230
|
ovSessionId: params.ovSessionId,
|
|
221
231
|
agentId,
|
|
222
232
|
});
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
233
|
+
const contextTypes = [...new Set(searchPlan.searches.map((search) => search.contextType))];
|
|
234
|
+
const maxTokens = Math.min(32_000, Math.max(64, Math.round(maxInjectedChars / LEGACY_CHARS_PER_TOKEN)));
|
|
235
|
+
const startedAt = Date.now();
|
|
236
|
+
let contextResult;
|
|
237
|
+
let requestError;
|
|
238
|
+
try {
|
|
239
|
+
contextResult = await client.searchContext(queryText, {
|
|
240
|
+
sessionId: params.ovSessionId,
|
|
241
|
+
limit: recallLimit,
|
|
242
|
+
scoreThreshold,
|
|
243
|
+
contextType: contextTypes.length === 1 ? contextTypes[0] : contextTypes,
|
|
244
|
+
queryExpansion: "auto",
|
|
245
|
+
maxTokens,
|
|
246
|
+
detail: recallPreferAbstract ? "abstract" : undefined,
|
|
247
|
+
dedupTurns: params.ovSessionId ? AUTO_RECALL_DEDUP_TURNS : undefined,
|
|
248
|
+
peerScope: "actor",
|
|
239
249
|
actorPeerId,
|
|
250
|
+
requestTimeoutMs: cfg.autoRecallTimeoutMs,
|
|
240
251
|
});
|
|
241
|
-
return {
|
|
242
|
-
resourceType: search.resourceType,
|
|
243
|
-
targetUri: search.targetUri,
|
|
244
|
-
result,
|
|
245
|
-
durationMs: Date.now() - start,
|
|
246
|
-
};
|
|
247
|
-
});
|
|
248
|
-
const autoRecallSettled = await Promise.allSettled(autoRecallPromises);
|
|
249
|
-
const allMemories = [];
|
|
250
|
-
for (const s of autoRecallSettled) {
|
|
251
|
-
if (s.status === "fulfilled") {
|
|
252
|
-
const result = s.value.result ?? {};
|
|
253
|
-
const memories = result.memories ?? [];
|
|
254
|
-
const resources = result.resources ?? [];
|
|
255
|
-
allMemories.push(...memories, ...resources);
|
|
256
|
-
traceSearches.push({
|
|
257
|
-
resourceType: s.value.resourceType,
|
|
258
|
-
targetUriInput: s.value.targetUri,
|
|
259
|
-
limit: candidateLimit,
|
|
260
|
-
scoreThreshold: 0,
|
|
261
|
-
durationMs: s.value.durationMs,
|
|
262
|
-
total: result.total ?? memories.length + resources.length + (result.skills?.length ?? 0),
|
|
263
|
-
results: [
|
|
264
|
-
...toTraceResults(memories, s.value.resourceType),
|
|
265
|
-
...toTraceResults(resources, "resource"),
|
|
266
|
-
...(result.skills ?? []).map((item) => ({
|
|
267
|
-
uri: item.uri,
|
|
268
|
-
resourceType: s.value.resourceType,
|
|
269
|
-
category: item.category,
|
|
270
|
-
score: item.score,
|
|
271
|
-
level: item.level,
|
|
272
|
-
abstractPreview: preview(item.abstract ?? item.overview, 240),
|
|
273
|
-
resultType: "skill",
|
|
274
|
-
})),
|
|
275
|
-
].slice(0, cfg.traceRecallMaxResultsPerSearch),
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
logger.warn?.(`openviking: auto-recall search failed: ${String(s.reason)}`);
|
|
280
|
-
const failedIndex = traceSearches.length;
|
|
281
|
-
const search = searchPlan.searches[failedIndex - searchPlan.skipped.length];
|
|
282
|
-
traceSearches.push({
|
|
283
|
-
resourceType: search?.resourceType ?? "user",
|
|
284
|
-
targetUriInput: search?.targetUri,
|
|
285
|
-
limit: candidateLimit,
|
|
286
|
-
scoreThreshold: 0,
|
|
287
|
-
durationMs: 0,
|
|
288
|
-
total: 0,
|
|
289
|
-
results: [],
|
|
290
|
-
error: String(s.reason),
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
252
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
253
|
+
catch (error) {
|
|
254
|
+
requestError = error;
|
|
255
|
+
}
|
|
256
|
+
const durationMs = Date.now() - startedAt;
|
|
257
|
+
const entries = (contextResult?.entries ?? []).filter((entry) => Boolean(entry.uri));
|
|
258
|
+
const rawRetrievalErrors = contextResult?.stats?.retrieval_errors;
|
|
259
|
+
const retrievalErrors = Array.isArray(rawRetrievalErrors)
|
|
260
|
+
? rawRetrievalErrors.map((error) => String(error))
|
|
261
|
+
: [];
|
|
262
|
+
const searchError = requestError
|
|
263
|
+
? String(requestError)
|
|
264
|
+
: retrievalErrors.length > 0
|
|
265
|
+
? retrievalErrors.join("; ")
|
|
266
|
+
: undefined;
|
|
267
|
+
if (searchError) {
|
|
268
|
+
logger.warn?.(`openviking: auto-recall context search failed: ${searchError}`);
|
|
269
|
+
}
|
|
270
|
+
const traceSearches = [
|
|
271
|
+
...searchPlan.skipped.map((skipped) => ({
|
|
272
|
+
resourceType: skipped.resourceType,
|
|
273
|
+
limit: recallLimit,
|
|
274
|
+
scoreThreshold,
|
|
275
|
+
durationMs: 0,
|
|
276
|
+
total: 0,
|
|
277
|
+
results: [],
|
|
278
|
+
error: skipped.reason,
|
|
279
|
+
})),
|
|
280
|
+
...searchPlan.searches.map((search) => {
|
|
281
|
+
const matchingEntries = entries.filter((entry) => search.contextType === "resource"
|
|
282
|
+
? traceResourceType(entry) === "resource"
|
|
283
|
+
: traceResourceType(entry) !== "resource");
|
|
284
|
+
return {
|
|
285
|
+
resourceType: search.resourceType,
|
|
286
|
+
contextType: search.contextType,
|
|
287
|
+
limit: recallLimit,
|
|
288
|
+
scoreThreshold,
|
|
289
|
+
durationMs,
|
|
290
|
+
total: matchingEntries.length,
|
|
291
|
+
results: matchingEntries
|
|
292
|
+
.map(toTraceResult)
|
|
293
|
+
.slice(0, cfg.traceRecallMaxResultsPerSearch),
|
|
294
|
+
error: searchError,
|
|
295
|
+
};
|
|
296
|
+
}),
|
|
297
|
+
];
|
|
298
|
+
const candidateCount = typeof contextResult?.stats?.candidates === "number"
|
|
299
|
+
? contextResult.stats.candidates
|
|
300
|
+
: entries.length;
|
|
301
|
+
const recordTrace = async (injectedEntries, injectedCount, estimatedTokens) => {
|
|
306
302
|
const entry = {
|
|
307
303
|
schemaVersion: "1.0",
|
|
308
304
|
traceId: newTraceId(),
|
|
@@ -321,17 +317,17 @@ export async function buildAutoRecallContext(params) {
|
|
|
321
317
|
queryTruncated: params.queryTruncated || queryText.length > cfg.traceRecallQueryMaxChars,
|
|
322
318
|
},
|
|
323
319
|
searches: traceSearches,
|
|
324
|
-
selected:
|
|
325
|
-
uri:
|
|
326
|
-
resourceType:
|
|
327
|
-
category:
|
|
328
|
-
score:
|
|
329
|
-
abstractPreview: preview(
|
|
320
|
+
selected: injectedEntries.map((contextEntry) => ({
|
|
321
|
+
uri: contextEntry.uri,
|
|
322
|
+
resourceType: traceResourceType(contextEntry),
|
|
323
|
+
category: contextEntry.category,
|
|
324
|
+
score: contextEntry.score,
|
|
325
|
+
abstractPreview: preview(contextEntry.text, cfg.traceRecallPreviewChars),
|
|
330
326
|
injected: true,
|
|
331
327
|
})),
|
|
332
328
|
stats: {
|
|
333
|
-
candidateCount
|
|
334
|
-
selectedCount:
|
|
329
|
+
candidateCount,
|
|
330
|
+
selectedCount: injectedEntries.length,
|
|
335
331
|
injectedCount,
|
|
336
332
|
estimatedTokens,
|
|
337
333
|
},
|
|
@@ -339,24 +335,27 @@ export async function buildAutoRecallContext(params) {
|
|
|
339
335
|
// Trace persistence is diagnostic best-effort; never put JSONL flush latency on the auto-recall critical path.
|
|
340
336
|
params.traceRecorder?.record(entry);
|
|
341
337
|
};
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
return { memoryCount: 0, estimatedTokens: 0 };
|
|
345
|
-
}
|
|
346
|
-
const { lines: memoryLines, estimatedTokens } = await buildMemoryLinesWithBudget(memories, (uri) => client.read(uri, actorPeerId), {
|
|
347
|
-
recallPreferAbstract,
|
|
348
|
-
recallMaxInjectedChars: maxInjectedChars,
|
|
349
|
-
includeUri: true,
|
|
350
|
-
});
|
|
351
|
-
if (memoryLines.length === 0) {
|
|
352
|
-
verbose?.(`openviking: skipping auto-recall injection; no complete memories fit maxInjectedChars=${maxInjectedChars}`);
|
|
338
|
+
const rendered = contextResult?.rendered?.trim() ?? "";
|
|
339
|
+
if (requestError || entries.length === 0 || !rendered) {
|
|
353
340
|
await recordTrace([], 0, 0);
|
|
354
341
|
return { memoryCount: 0, estimatedTokens: 0 };
|
|
355
342
|
}
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
343
|
+
const usedTokens = contextResult?.stats?.used_tokens;
|
|
344
|
+
const estimatedTokens = typeof usedTokens === "number" && Number.isFinite(usedTokens)
|
|
345
|
+
? Math.max(0, Math.ceil(usedTokens))
|
|
346
|
+
: estimateTokenCount(rendered);
|
|
347
|
+
const block = buildRecallContextBlock([rendered]);
|
|
348
|
+
verbose?.(`openviking: injecting ${entries.length} memories (${block.length} chars, ~${estimatedTokens} tokens, serverMaxTokens=${maxTokens})`);
|
|
349
|
+
verbose?.(`openviking: inject-detail ${toJsonLog({
|
|
350
|
+
count: entries.length,
|
|
351
|
+
entries: entries.map((contextEntry) => ({
|
|
352
|
+
uri: contextEntry.uri,
|
|
353
|
+
category: contextEntry.category,
|
|
354
|
+
score: contextEntry.score,
|
|
355
|
+
detail: contextEntry.detail,
|
|
356
|
+
})),
|
|
357
|
+
})}`);
|
|
358
|
+
await recordTrace(entries, entries.length, estimatedTokens);
|
|
359
|
+
return { block, memoryCount: entries.length, estimatedTokens };
|
|
361
360
|
})(), cfg.autoRecallTimeoutMs, "openviking: auto-recall search timeout");
|
|
362
361
|
}
|