@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/dist/client.js CHANGED
@@ -7,6 +7,7 @@ import { basename, dirname, join, relative } from "node:path";
7
7
  import { Zip, ZipDeflate } from "fflate";
8
8
  import { defaultHttpTransport } from "./adapters/http-transport.js";
9
9
  import { defaultResourcePackager, } from "./adapters/resource-packager.js";
10
+ import { buildContextSearchBody, contextRequestTimeoutMs, normalizeContextEntry, } from "./shared/recall-core.mjs";
10
11
  function userSessionUri(sessionId) {
11
12
  return `viking://~/sessions/${encodeURIComponent(sessionId)}`;
12
13
  }
@@ -215,6 +216,59 @@ export class OpenVikingClient {
215
216
  body: JSON.stringify(body),
216
217
  }, undefined, actorPeerId);
217
218
  }
219
+ async searchContext(query, options = {}) {
220
+ const contractConfig = {
221
+ recallLimit: options.limit,
222
+ recallLimitConfigured: options.limit !== undefined,
223
+ recallMaxTokens: options.maxTokens,
224
+ recallMaxTokensConfigured: options.maxTokens !== undefined,
225
+ scoreThreshold: options.scoreThreshold,
226
+ recallQueryExpansion: options.queryExpansion,
227
+ recallQueryExpansionConfigured: options.queryExpansion !== undefined,
228
+ recallDedupTurns: options.dedupTurns,
229
+ recallPeerScope: options.peerScope,
230
+ recallContextTimeoutMs: options.requestTimeoutMs,
231
+ timeoutMs: this.timeoutMs,
232
+ };
233
+ const body = {
234
+ ...buildContextSearchBody(contractConfig, { sessionId: options.sessionId }),
235
+ query,
236
+ ...(options.contextType !== undefined ? { context_type: options.contextType } : {}),
237
+ ...(options.detail !== undefined ? { detail: options.detail } : {}),
238
+ };
239
+ const requestTimeoutMs = contextRequestTimeoutMs(contractConfig, body);
240
+ const actorPeerId = this.resolveActorPeerHeader(options.actorPeerId);
241
+ const tenantHeaders = this.resolveTenantHeaders();
242
+ this.routingDebugLog?.(`openviking: context search POST ${this.baseUrl}/api/v1/search/search ` +
243
+ JSON.stringify({
244
+ X_OpenViking_Account: tenantHeaders.accountId ?? null,
245
+ X_OpenViking_User: tenantHeaders.userId ?? null,
246
+ X_OpenViking_Actor_Peer: actorPeerId ?? null,
247
+ session_id: body.session_id ?? null,
248
+ query: query.length > 4000
249
+ ? `${query.slice(0, 4000)}…(+${query.length - 4000} more chars)`
250
+ : query,
251
+ purpose: body.purpose,
252
+ quotas: body.quotas ?? null,
253
+ score_threshold: body.score_threshold,
254
+ context_type: body.context_type ?? null,
255
+ query_expansion: body.query_expansion ?? null,
256
+ max_tokens: body.max_tokens ?? null,
257
+ detail: body.detail ?? null,
258
+ dedup_turns: body.dedup_turns ?? 0,
259
+ peer_scope: body.peer_scope ?? null,
260
+ }));
261
+ const result = await this.request("/api/v1/search/search", {
262
+ method: "POST",
263
+ body: JSON.stringify(body),
264
+ }, requestTimeoutMs, actorPeerId);
265
+ return {
266
+ ...result,
267
+ entries: Array.isArray(result.entries)
268
+ ? result.entries.map((entry) => ({ ...entry, ...normalizeContextEntry(entry) }))
269
+ : result.entries,
270
+ };
271
+ }
218
272
  async read(uri, actorPeerId) {
219
273
  return this.request(`/api/v1/content/read?uri=${encodeURIComponent(uri)}`, {}, undefined, actorPeerId);
220
274
  }
@@ -76,7 +76,7 @@ const CONFIG_KEYS_TO_PRESERVE = [
76
76
  "emitStandardDiagnostics",
77
77
  "logFindRequests",
78
78
  ];
79
- const DEFAULT_SETUP_PEER_ROLE = "assistant";
79
+ const DEFAULT_SETUP_PEER_ROLE = "none";
80
80
  const ALLOWED_RECALL_TARGET_TYPES = ["resource", "user", "agent"];
81
81
  function tr(langZh, en, zh) {
82
82
  return langZh ? zh : en;
@@ -98,7 +98,9 @@ function normalizePeerRole(value) {
98
98
  if (typeof value !== "string")
99
99
  return undefined;
100
100
  const role = value.trim().toLowerCase();
101
- if (role === "none" || role === "assistant" || role === "person")
101
+ if (role === "person")
102
+ return "sender";
103
+ if (role === "none" || role === "assistant" || role === "sender")
102
104
  return role;
103
105
  return undefined;
104
106
  }
@@ -121,7 +123,7 @@ function resolveSetupPeerRole(value) {
121
123
  const role = normalizePeerRole(value);
122
124
  if (role)
123
125
  return role;
124
- throw new Error('peer_role must be "none", "assistant", or "person"');
126
+ throw new Error('peer_role must be "none", "assistant", or "sender" (legacy alias: "person")');
125
127
  }
126
128
  function normalizeSetupRecallTargetTypes(value) {
127
129
  if (value === undefined || value === null || value === "") {
@@ -172,12 +174,17 @@ function preserveCurrentConfig(existing) {
172
174
  return config;
173
175
  }
174
176
  async function askPeerRole(zh, q, defaultValue) {
177
+ console.log(` ${tr(zh, "Memory scope — none (default): viking://user/<user_id>/memories, shared across all conversations; " +
178
+ "assistant: viking://user/<user_id>/peers/<assistant_id>/memories; " +
179
+ "sender: viking://user/<user_id>/peers/<sender_id>/memories (legacy alias: person).", "记忆归属 —— none(默认):viking://user/<user_id>/memories,所有对话共享;" +
180
+ "assistant:viking://user/<user_id>/peers/<assistant_id>/memories;" +
181
+ "sender:viking://user/<user_id>/peers/<sender_id>/memories(兼容旧值 person)。")}`);
175
182
  while (true) {
176
- const value = await q(tr(zh, "Peer Role (none/assistant/person)", "Peer Role(none/assistant/person)"), defaultValue);
183
+ const value = await q(tr(zh, "Memory scope (none/assistant/sender)", "记忆归属(none/assistant/sender)"), defaultValue);
177
184
  const role = normalizePeerRole(value);
178
185
  if (role)
179
186
  return role;
180
- console.log(` ✗ ${tr(zh, 'Peer Role must be "none", "assistant", or "person".', 'Peer Role 必须是 "none"、"assistant" 或 "person"。')}`);
187
+ console.log(` ✗ ${tr(zh, 'Memory scope must be "none", "assistant", or "sender" (legacy "person" is also accepted).', '记忆归属必须是 "none"、"assistant" 或 "sender"(也兼容旧值 "person")。')}`);
181
188
  }
182
189
  }
183
190
  async function askPeerPrefix(zh, q, defaultValue) {
@@ -473,7 +480,7 @@ export function registerSetupCli(api) {
473
480
  .option("--zh", "Chinese prompts")
474
481
  .option("--base-url <url>", "OpenViking server URL (enables non-interactive mode)")
475
482
  .option("--api-key <key>", "API key for authentication")
476
- .option("--peer-role <role>", "Peer ID role: none, assistant, or person")
483
+ .option("--peer-role <role>", "Memory scope: none (shared), assistant (per assistant), or sender (per sender); person is a legacy alias")
477
484
  .option("--peer-prefix <prefix>", "Prefix for assistant peer_id values")
478
485
  .option("--account-id <id>", "Account ID (required for root API keys)")
479
486
  .option("--user-id <id>", "User ID (required for root API keys)")
@@ -952,6 +959,8 @@ async function setupRemote(zh, configPath, existing, q) {
952
959
  export const __test__ = {
953
960
  isLegacyLocalMode,
954
961
  isValidPeerPrefixInput,
962
+ normalizePeerRole,
963
+ resolveSetupPeerRole,
955
964
  normalizeSetupRecallTargetTypes,
956
965
  activateContextEngineSlot,
957
966
  isContextEngineSlotActive,
package/dist/config.js CHANGED
@@ -6,7 +6,9 @@ const DEFAULT_TARGET_URI = "viking://~/memories";
6
6
  const DEFAULT_TIMEOUT_MS = 15000;
7
7
  const DEFAULT_CAPTURE_MODE = "semantic";
8
8
  const DEFAULT_CAPTURE_MAX_LENGTH = 24000;
9
- const DEFAULT_AUTO_RECALL_TIMEOUT_MS = 5000;
9
+ // Session-aware context search may spend up to 5 seconds on query expansion
10
+ // before returning its server-side fallback, so leave headroom for retrieval.
11
+ const DEFAULT_AUTO_RECALL_TIMEOUT_MS = 15000;
10
12
  const DEFAULT_RECALL_LIMIT = 6;
11
13
  const DEFAULT_RECALL_SCORE_THRESHOLD = 0.15;
12
14
  const DEFAULT_RECALL_MAX_CONTENT_CHARS = 5000;
@@ -16,7 +18,7 @@ const DEFAULT_COMMIT_TOKEN_THRESHOLD_RATIO = 0.5;
16
18
  const DEFAULT_COMMIT_KEEP_RECENT_COUNT = 10;
17
19
  const DEFAULT_BYPASS_SESSION_PATTERNS = [];
18
20
  const DEFAULT_EMIT_STANDARD_DIAGNOSTICS = false;
19
- const DEFAULT_PEER_ROLE = "assistant";
21
+ const DEFAULT_PEER_ROLE = "none";
20
22
  const DEFAULT_PEER_PREFIX = "";
21
23
  const DEFAULT_TRACE_RECALL_DIR = "~/.openclaw/openviking/recall-traces";
22
24
  const DEFAULT_TRACE_RECALL_RETENTION_DAYS = 14;
@@ -142,13 +144,16 @@ function resolveSecret(value, label) {
142
144
  function resolvePeerRole(configured) {
143
145
  if (typeof configured === "string") {
144
146
  const role = configured.trim().toLowerCase();
145
- if (role === "none" || role === "assistant" || role === "person") {
147
+ if (role === "none" || role === "assistant" || role === "sender") {
146
148
  return role;
147
149
  }
148
- throw new Error(`openviking peer_role must be "none", "assistant", or "person"`);
150
+ if (role === "person") {
151
+ return "sender";
152
+ }
153
+ throw new Error(`openviking peer_role must be "none", "assistant", or "sender" (legacy alias: "person")`);
149
154
  }
150
155
  if (configured !== undefined) {
151
- throw new Error(`openviking peer_role must be "none", "assistant", or "person"`);
156
+ throw new Error(`openviking peer_role must be "none", "assistant", or "sender" (legacy alias: "person")`);
152
157
  }
153
158
  return DEFAULT_PEER_ROLE;
154
159
  }
@@ -479,14 +484,16 @@ export const memoryOpenVikingConfigSchema = {
479
484
  help: "HTTP URL when mode is remote (or use ${OPENVIKING_BASE_URL})",
480
485
  },
481
486
  peer_role: {
482
- label: "Peer Role",
487
+ label: "Memory Scope (peer_role)",
483
488
  placeholder: DEFAULT_PEER_ROLE,
484
- help: 'Controls which session messages get peer_id: "none", "assistant", or "person".',
489
+ help: 'Where peer-scoped memories are stored. "none" (default): viking://user/<user_id>/memories. ' +
490
+ '"assistant": viking://user/<user_id>/peers/<assistant_id>/memories. ' +
491
+ '"sender": viking://user/<user_id>/peers/<sender_id>/memories. Legacy "person" is accepted as "sender".',
485
492
  },
486
493
  peer_prefix: {
487
494
  label: "Peer Prefix",
488
495
  placeholder: "optional-prefix",
489
- help: "Optional prefix applied to assistant peer_id values derived from OpenClaw runtime agent IDs.",
496
+ help: 'Only used when Memory Scope is "assistant". Prefix added to the peer id derived from the OpenClaw agent id.',
490
497
  },
491
498
  apiKey: {
492
499
  label: "OpenViking API Key",
@@ -547,7 +554,7 @@ export const memoryOpenVikingConfigSchema = {
547
554
  label: "Auto-Recall Timeout (ms)",
548
555
  placeholder: String(DEFAULT_AUTO_RECALL_TIMEOUT_MS),
549
556
  advanced: true,
550
- help: "Outer time budget for the whole auto-recall flow, including search, ranking, and memory reads.",
557
+ help: "Outer time budget for the server-assembled context search, including session-aware query expansion.",
551
558
  },
552
559
  recallResources: {
553
560
  label: "Recall Resources",
@@ -574,7 +581,7 @@ export const memoryOpenVikingConfigSchema = {
574
581
  label: "Recall Max Injected Chars",
575
582
  placeholder: String(DEFAULT_RECALL_MAX_INJECTED_CHARS),
576
583
  advanced: true,
577
- help: "Maximum total characters for auto-recall memory injection. Complete memories that do not fit are skipped, not truncated.",
584
+ help: "Legacy auto-recall character budget. It is converted to the server context token budget at four characters per token.",
578
585
  },
579
586
  recallMaxContentChars: {
580
587
  label: "Deprecated Recall Max Content Chars",
@@ -585,7 +592,7 @@ export const memoryOpenVikingConfigSchema = {
585
592
  recallPreferAbstract: {
586
593
  label: "Recall Prefer Abstract",
587
594
  advanced: true,
588
- help: "Use memory abstract instead of fetching full content when abstract is available. Reduces token usage.",
595
+ help: "Pin server-assembled auto-recall entries to abstract detail. When disabled, the server chooses each category's default detail tier.",
589
596
  },
590
597
  recallTokenBudget: {
591
598
  label: "Deprecated Recall Token Budget",
@@ -45,7 +45,7 @@ export function registerOpenVikingMemoryTools(deps) {
45
45
  const peerId = resolveOpenVikingMessagePeerId({
46
46
  peerRole: deps.peerRole,
47
47
  role,
48
- personPeerId: session.actorPeerId,
48
+ senderPeerId: session.actorPeerId,
49
49
  assistantPeerId: session.actorPeerId,
50
50
  });
51
51
  await client.addSessionMessage(sessionId, role, [{ type: "text", text }], undefined, undefined, peerId);
@@ -1,6 +1,6 @@
1
1
  import { createSessionAgentResolver, openClawSessionToOvStorageId, resolveOpenVikingActorPeerId, sanitizeOpenVikingPeerId, } from "../routing/identity-routing.js";
2
2
  export function createOpenVikingSessionRoutingRuntime(options) {
3
- const peerRole = options.peerRole ?? "assistant";
3
+ const peerRole = options.peerRole ?? "none";
4
4
  const sessionAgentResolver = createSessionAgentResolver(options.peerPrefix);
5
5
  const rememberSessionAgentId = (ctx) => {
6
6
  sessionAgentResolver.remember(ctx);
@@ -48,7 +48,7 @@ export function createOpenVikingSessionRoutingRuntime(options) {
48
48
  agentId,
49
49
  actorPeerId: resolveOpenVikingActorPeerId({
50
50
  peerRole,
51
- personPeerId: sanitizeOpenVikingPeerId(ctx?.requesterSenderId ?? ctx?.senderId),
51
+ senderPeerId: sanitizeOpenVikingPeerId(ctx?.requesterSenderId ?? ctx?.senderId),
52
52
  assistantPeerId: agentId,
53
53
  }),
54
54
  };
@@ -12,8 +12,8 @@ export function sanitizeOpenVikingPeerId(raw) {
12
12
  return normalized || undefined;
13
13
  }
14
14
  export function resolveOpenVikingMessagePeerId(params) {
15
- if (params.peerRole === "person" && params.role === "user") {
16
- return params.personPeerId;
15
+ if (params.peerRole === "sender" && params.role === "user") {
16
+ return params.senderPeerId;
17
17
  }
18
18
  if (params.peerRole === "assistant" && params.role === "assistant") {
19
19
  return params.assistantPeerId;
@@ -23,10 +23,10 @@ export function resolveOpenVikingMessagePeerId(params) {
23
23
  export function resolveOpenVikingActorPeerId(params) {
24
24
  const actorPeerId = resolveOpenVikingMessagePeerId({
25
25
  ...params,
26
- role: params.peerRole === "person" ? "user" : "assistant",
26
+ role: params.peerRole === "sender" ? "user" : "assistant",
27
27
  });
28
- if (params.peerRole === "person" && !actorPeerId) {
29
- throw new Error("openviking: peer_role=person requires a sender identity");
28
+ if (params.peerRole === "sender" && !actorPeerId) {
29
+ throw new Error("openviking: peer_role=sender requires a sender identity");
30
30
  }
31
31
  return actorPeerId;
32
32
  }
@@ -243,8 +243,8 @@ export async function assembleOpenVikingSession({ sessionId, sessionKey, message
243
243
  const routingRef = sessionId ?? sessionKey ?? ovSessionId;
244
244
  const agentId = resolveAgentId(routingRef, sessionKey, ovSessionId);
245
245
  const actorPeerId = resolveOpenVikingActorPeerId({
246
- peerRole: cfg.peer_role ?? "assistant",
247
- personPeerId: sanitizeOpenVikingPeerId(sender.senderId),
246
+ peerRole: cfg.peer_role ?? "none",
247
+ senderPeerId: sanitizeOpenVikingPeerId(sender.senderId),
248
248
  assistantPeerId: agentId,
249
249
  });
250
250
  const queryConfig = await queryConfigStore?.getEffective({
@@ -595,9 +595,9 @@ export async function afterTurnOpenVikingSession({ sessionId, sessionKey, messag
595
595
  });
596
596
  if (ovParts.length > 0) {
597
597
  await client.addSessionMessage(ovSessionId, msg.role, ovParts, undefined, createdAt, resolveOpenVikingMessagePeerId({
598
- peerRole: cfg.peer_role ?? "assistant",
598
+ peerRole: cfg.peer_role ?? "none",
599
599
  role: msg.role,
600
- personPeerId: senderRoleId,
600
+ senderPeerId: senderRoleId,
601
601
  assistantPeerId: agentId,
602
602
  }));
603
603
  }
@@ -0,0 +1,188 @@
1
+ // GENERATED FROM examples/memory-plugin-shared/lib. DO NOT EDIT.
2
+ import { createHash } from "node:crypto";
3
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
4
+ import { dirname } from "node:path";
5
+ export const NO_RELEVANT_MEMORY = "NO_RELEVANT_MEMORY";
6
+ export const DIGEST_HEADER = "OpenViking memory digest:";
7
+ const COMPRESS_OK = "ok";
8
+ const COMPRESS_EMPTY = "empty";
9
+ const COMPRESS_FAILED = "failed";
10
+ // Medium-constraint prompt: state the goal and two structural floors, but no hard
11
+ // bullet-length contract. Hard per-bullet limits pin the digest to headline
12
+ // density; leaving it unconstrained lets small models rewrite long URIs into dead
13
+ // links, which the URI repair below cleans up.
14
+ export function buildRecallCompressionPrompt({ query, rendered, maxBullets = 6 }) {
15
+ return `You are a memory relevance compressor utility.
16
+ Do not use any tools. Do not investigate. Only transform the given text.
17
+
18
+ User query:
19
+ ${query}
20
+
21
+ Retrieved OpenViking context fragments:
22
+ ${rendered}
23
+
24
+ Write a memory digest for a coding agent about to answer that query. Keep the
25
+ concrete facts (paths, identifiers, decisions, constraints); drop pleasantries
26
+ and conversational filler.
27
+
28
+ Format rules:
29
+ - Group related facts by topic, one bullet per topic, at most ${maxBullets} bullets.
30
+ - Start every bullet with "- ".
31
+ - End every bullet with its source, copied verbatim from the fragments above:
32
+ "来源:viking://..." or "source: viking://...". Never edit, shorten, or invent a URI.
33
+ - Output the digest body only. No preamble, no closing remark.
34
+
35
+ If nothing above is relevant to the query, output exactly: ${NO_RELEVANT_MEMORY}`;
36
+ }
37
+ function editDistance(a, b) {
38
+ if (a === b)
39
+ return 0;
40
+ const rows = a.length + 1;
41
+ const cols = b.length + 1;
42
+ let prev = Array.from({ length: cols }, (_, i) => i);
43
+ for (let i = 1; i < rows; i += 1) {
44
+ const cur = [i];
45
+ for (let j = 1; j < cols; j += 1) {
46
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
47
+ cur[j] = Math.min(cur[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
48
+ }
49
+ prev = cur;
50
+ }
51
+ return prev[cols - 1];
52
+ }
53
+ function nearestUri(candidate, validUris) {
54
+ let best = "";
55
+ let bestDistance = Infinity;
56
+ for (const uri of validUris) {
57
+ const distance = editDistance(candidate, uri);
58
+ if (distance < bestDistance) {
59
+ best = uri;
60
+ bestDistance = distance;
61
+ }
62
+ }
63
+ // Only repair near-misses; an unrelated hallucination is dropped instead.
64
+ const tolerance = Math.max(4, Math.floor(candidate.length * 0.25));
65
+ return bestDistance <= tolerance ? best : "";
66
+ }
67
+ /**
68
+ * Small models occasionally mangle long URIs. Snap every cited URI back onto the
69
+ * set the server actually returned, and drop bullets whose citation cannot be
70
+ * recovered so the digest never carries a dead link.
71
+ */
72
+ export function repairDigestUris(digest, validUris = []) {
73
+ const text = String(digest || "");
74
+ if (!text)
75
+ return "";
76
+ const valid = validUris.map((uri) => String(uri || "").trim()).filter(Boolean);
77
+ if (!valid.length)
78
+ return text;
79
+ const validSet = new Set(valid);
80
+ const lines = [];
81
+ for (const line of text.split(/\r?\n/)) {
82
+ if (!line.trim().startsWith("- ")) {
83
+ lines.push(line);
84
+ continue;
85
+ }
86
+ let dropped = false;
87
+ const repaired = line.replace(/viking:\/\/[^\s<>"')\]]+/g, (uri) => {
88
+ if (validSet.has(uri))
89
+ return uri;
90
+ const nearest = nearestUri(uri, valid);
91
+ if (nearest)
92
+ return nearest;
93
+ dropped = true;
94
+ return uri;
95
+ });
96
+ if (!dropped)
97
+ lines.push(repaired);
98
+ }
99
+ return lines.join("\n").trim();
100
+ }
101
+ export function normalizeCompressedContext(raw, maxChars = 4000, maxBullets = 6) {
102
+ const text = String(raw || "").trim();
103
+ if (!text)
104
+ return null;
105
+ if (text.toUpperCase() === NO_RELEVANT_MEMORY)
106
+ return "";
107
+ const bullets = text.split(/\r?\n/)
108
+ .map((line) => line.trim())
109
+ .filter((line) => /^[-*]\s+/.test(line) && line.includes("viking://"))
110
+ .slice(0, Math.max(1, maxBullets))
111
+ .map((line) => `- ${line.replace(/^[-*]\s+/, "").slice(0, 500).trim()}`);
112
+ if (!bullets.length)
113
+ return null;
114
+ return (`${DIGEST_HEADER}\n${bullets.join("\n")}`).slice(0, Math.max(100, maxChars));
115
+ }
116
+ export function recallDigestCacheKey({ query = "", rendered = "", entries = [], maxInputChars = 18000, maxBullets = 6, } = {}) {
117
+ const uris = entries.map((entry) => String(entry?.uri || "").trim()).filter(Boolean).sort();
118
+ const source = JSON.stringify({
119
+ version: 2,
120
+ query: String(query),
121
+ rendered: String(rendered).slice(0, maxInputChars),
122
+ uris,
123
+ maxInputChars,
124
+ maxBullets,
125
+ });
126
+ return createHash("sha256").update(source).digest("hex");
127
+ }
128
+ async function readCache(path) {
129
+ if (!path)
130
+ return null;
131
+ try {
132
+ return JSON.parse(await readFile(path, "utf8"));
133
+ }
134
+ catch {
135
+ return null;
136
+ }
137
+ }
138
+ async function writeCache(path, value) {
139
+ if (!path)
140
+ return;
141
+ try {
142
+ await mkdir(dirname(path), { recursive: true });
143
+ const tmp = `${path}.tmp`;
144
+ await writeFile(tmp, JSON.stringify(value));
145
+ await rename(tmp, path);
146
+ }
147
+ catch { /* best effort */ }
148
+ }
149
+ export async function compressRecallContext({ query, rendered, entries = [], cfg = {}, runCompressor, cachePath = "", now = 0, }) {
150
+ const input = String(rendered || "").trim();
151
+ if (!input)
152
+ return { status: COMPRESS_EMPTY, context: "" };
153
+ const minChars = Math.max(0, Number(cfg.recallCompressMinInputChars ?? 1500));
154
+ if (input.length < minChars)
155
+ return { status: COMPRESS_OK, context: input };
156
+ const maxInputChars = Math.max(1000, Number(cfg.recallCompressMaxInputChars || 18000));
157
+ const maxBullets = Math.max(1, Number(cfg.recallCompressMaxBullets || 6));
158
+ const key = recallDigestCacheKey({
159
+ query,
160
+ rendered: input,
161
+ entries,
162
+ maxInputChars,
163
+ maxBullets,
164
+ });
165
+ const cached = await readCache(cachePath);
166
+ if (cached?.key === key && typeof cached.digest === "string") {
167
+ return { status: COMPRESS_OK, context: cached.digest };
168
+ }
169
+ const prompt = buildRecallCompressionPrompt({
170
+ query,
171
+ rendered: input.slice(0, maxInputChars),
172
+ maxBullets,
173
+ });
174
+ const raw = await runCompressor(prompt);
175
+ const normalized = normalizeCompressedContext(raw, 4000, maxBullets);
176
+ if (normalized === null)
177
+ return { status: COMPRESS_FAILED, context: "" };
178
+ if (!normalized)
179
+ return { status: COMPRESS_EMPTY, context: "" };
180
+ const validUris = entries.map((entry) => entry?.uri).filter(Boolean);
181
+ const digest = repairDigestUris(normalized, validUris.length
182
+ ? validUris
183
+ : (input.match(/viking:\/\/[^\s<>"']+/g) || []));
184
+ if (!digest)
185
+ return { status: COMPRESS_FAILED, context: "" };
186
+ await writeCache(cachePath, { key, digest, updatedAt: now || 0 });
187
+ return { status: COMPRESS_OK, context: digest };
188
+ }