@openviking/opencode-plugin 2026.9.21-1 → 2026.9.21-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.
|
@@ -98,7 +98,7 @@ export const KNOBS = [
|
|
|
98
98
|
// off/client/server/auto through `normalizeRewriteMode`; Codex reads the same
|
|
99
99
|
// key as on/off. Both spellings stay declared so neither harness's users are
|
|
100
100
|
// told their config is a typo.
|
|
101
|
-
{ name: "recallCompress", type: "string", default: "auto", env: "OPENVIKING_RECALL_COMPRESS", aliases: ["recallRewrite"], capability: "recall" },
|
|
101
|
+
{ name: "recallCompress", type: "string", default: "off", harness: { claude_code: "auto", codex: "auto" }, env: "OPENVIKING_RECALL_COMPRESS", aliases: ["recallRewrite"], capability: "recall" },
|
|
102
102
|
{ name: "recallCompressModel", type: "string", default: "", env: "OPENVIKING_RECALL_COMPRESS_MODEL", capability: "recall" },
|
|
103
103
|
{ name: "recallCompressBaseUrl", type: "string", default: "", env: "OPENVIKING_RECALL_COMPRESS_BASE_URL", capability: "recall" },
|
|
104
104
|
{ name: "recallCompressThinking", type: "string", default: "", env: "OPENVIKING_RECALL_COMPRESS_THINKING", aliases: ["recallCompressReasoningEffort"], capability: "recall" },
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
import { homedir } from "node:os";
|
|
30
30
|
import { join } from "node:path";
|
|
31
31
|
|
|
32
|
-
import { HARNESS_KEYS, KNOBS, harnessKey, resolveKnobs } from "./config-schema.mjs";
|
|
32
|
+
import { HARNESS_KEYS, KNOBS, harnessKey, knobDefault, resolveKnobs } from "./config-schema.mjs";
|
|
33
33
|
import {
|
|
34
34
|
buildUserAgent,
|
|
35
35
|
loadCredentialFiles,
|
|
@@ -224,9 +224,15 @@ export function buildPluginConfig(harness, {
|
|
|
224
224
|
env,
|
|
225
225
|
});
|
|
226
226
|
|
|
227
|
+
const recallRewrite = normalizeRewriteMode(
|
|
228
|
+
env.OPENVIKING_RECALL_COMPRESS ?? env.OPENVIKING_RECALL_REWRITE ?? settings.recallCompress,
|
|
229
|
+
knobDefault(KNOBS.find((knob) => knob.name === "recallCompress"), harnessKey(name)),
|
|
230
|
+
);
|
|
227
231
|
const config = {
|
|
228
232
|
...settings,
|
|
229
233
|
harness: name,
|
|
234
|
+
recallCompress: recallRewrite,
|
|
235
|
+
recallRewrite,
|
|
230
236
|
clientVersion,
|
|
231
237
|
userAgent: buildUserAgent(name, clientVersion),
|
|
232
238
|
|
|
@@ -309,21 +309,46 @@ async function resolveTargetUri(fetchJSON, targetUri, actorPeerId = "") {
|
|
|
309
309
|
return `viking://user/${space}/${parts.join("/")}`;
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
async function searchOneSource(fetchJSON, query, source, limit,
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
312
|
+
async function searchOneSource(fetchJSON, cfg, query, source, limit, options) {
|
|
313
|
+
const actorPeerId = options.actorPeerId || "";
|
|
314
|
+
const home = await resolveTargetUri(fetchJSON, source.uri, actorPeerId);
|
|
315
|
+
const targets = [...new Set(cfg.user
|
|
316
|
+
? [`viking://user/${cfg.user}/${source.bucket}`, home] : [home])];
|
|
317
|
+
const sessionId = String(options.sessionId || "").trim();
|
|
318
|
+
const search = async (target, session) => {
|
|
319
|
+
const body = { query, target_uri: target, limit, score_threshold: 0 };
|
|
320
|
+
if (session) body.session_id = session;
|
|
321
|
+
const init = { method: "POST", body: JSON.stringify(body) };
|
|
322
|
+
// Session-aware retrieval may decide no context is needed. Only after all
|
|
323
|
+
// targets are empty do we retry without the session. Old servers still
|
|
324
|
+
// support find; an unsupported search route can fall back to it.
|
|
325
|
+
let res = await fetchJSON(sessionId ? "/api/v1/search/search" : "/api/v1/search/find", init, { actorPeerId });
|
|
326
|
+
if (sessionId && !res.ok && (
|
|
327
|
+
!res.status || res.status === 404 || res.status === 405 || res.status >= 500
|
|
328
|
+
|| ((res.status === 400 || res.status === 422) && looksLikeUnknownField(res))
|
|
329
|
+
)) {
|
|
330
|
+
const { session_id, ...findBody } = body;
|
|
331
|
+
res = await fetchJSON("/api/v1/search/find", { method: "POST", body: JSON.stringify(findBody) }, { actorPeerId });
|
|
332
|
+
}
|
|
333
|
+
const items = res.ok && Array.isArray(res.result?.[source.bucket]) ? res.result[source.bucket] : [];
|
|
334
|
+
return items.map((item) => ({ ...item, _sourceType: source.type }));
|
|
335
|
+
};
|
|
336
|
+
for (const target of targets) {
|
|
337
|
+
const items = await search(target, sessionId);
|
|
338
|
+
if (items.length) return items;
|
|
339
|
+
}
|
|
340
|
+
if (sessionId) {
|
|
341
|
+
for (const target of targets) {
|
|
342
|
+
const items = await search(target, "");
|
|
343
|
+
if (items.length) return items;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return [];
|
|
322
347
|
}
|
|
323
348
|
|
|
324
|
-
async function searchAllSources(fetchJSON, query, perSourceLimit,
|
|
349
|
+
async function searchAllSources(fetchJSON, cfg, query, perSourceLimit, options, log = () => {}) {
|
|
325
350
|
const results = await Promise.all(
|
|
326
|
-
SOURCES.map((src) => searchOneSource(fetchJSON, query, src, perSourceLimit,
|
|
351
|
+
SOURCES.map((src) => searchOneSource(fetchJSON, cfg, query, src, perSourceLimit, options)),
|
|
327
352
|
);
|
|
328
353
|
const all = results.flat();
|
|
329
354
|
log("recall_search_summary", {
|
|
@@ -359,50 +384,30 @@ async function resolveItemContent(fetchJSON, item, cfg, actorPeerId = "") {
|
|
|
359
384
|
return content;
|
|
360
385
|
}
|
|
361
386
|
|
|
362
|
-
|
|
387
|
+
function formatFallback(items, cfg, log = () => {}) {
|
|
363
388
|
const budgetTotal = Math.max(200, Number(cfg.recallTokenBudget || 2000));
|
|
364
|
-
|
|
365
|
-
const lines = [
|
|
366
|
-
"<openviking-context>",
|
|
367
|
-
"Relevant context from OpenViking. Use the read MCP tool to expand URIs.",
|
|
368
|
-
];
|
|
389
|
+
const lines = [];
|
|
369
390
|
let contentCount = 0;
|
|
370
391
|
let hintCount = 0;
|
|
371
|
-
|
|
392
|
+
const maxChars = Math.max(50, Number(cfg.recallMaxContentChars || 500));
|
|
372
393
|
for (const item of items) {
|
|
373
394
|
const score = (clampScore(item.score) * 100).toFixed(0);
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
hintCount++;
|
|
384
|
-
} else {
|
|
385
|
-
lines.push(contentLine);
|
|
386
|
-
budgetRemaining -= lineTokens;
|
|
387
|
-
contentCount++;
|
|
388
|
-
}
|
|
389
|
-
} else {
|
|
395
|
+
const kind = item._sourceType || item.category || "memory";
|
|
396
|
+
const uriLine = `- [${kind} ${score}%] ${item.uri}`;
|
|
397
|
+
const text = String(item.text || item.uri);
|
|
398
|
+
const content = text.length > maxChars ? `${text.slice(0, maxChars)}...` : text;
|
|
399
|
+
const contentLine = `- [${kind} ${score}%] ${content}`;
|
|
400
|
+
if (estimateTokens(wrapContext([...lines, uriLine, contentLine].join("\n"))) <= budgetTotal) {
|
|
401
|
+
lines.push(uriLine, contentLine);
|
|
402
|
+
contentCount += 1;
|
|
403
|
+
} else if (estimateTokens(wrapContext([...lines, uriLine].join("\n"))) <= budgetTotal) {
|
|
390
404
|
lines.push(uriLine);
|
|
391
|
-
hintCount
|
|
405
|
+
hintCount += 1;
|
|
392
406
|
}
|
|
393
407
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const budgetUsed = budgetTotal - budgetRemaining;
|
|
398
|
-
log("recall_injection_built", {
|
|
399
|
-
contentItems: contentCount,
|
|
400
|
-
hintItems: hintCount,
|
|
401
|
-
budgetUsed,
|
|
402
|
-
budgetTotal,
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
return { block: lines.join("\n"), contentCount, hintCount, budgetUsed, stage: "ranked" };
|
|
408
|
+
const budgetUsed = lines.length ? estimateTokens(wrapContext(lines.join("\n"))) : 0;
|
|
409
|
+
log("recall_injection_built", { contentItems: contentCount, hintItems: hintCount, budgetUsed, budgetTotal });
|
|
410
|
+
return { rendered: lines.join("\n"), contentCount, hintCount, budgetUsed };
|
|
406
411
|
}
|
|
407
412
|
|
|
408
413
|
const LEGACY_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
@@ -469,10 +474,14 @@ function looksLikeUnknownField(res) {
|
|
|
469
474
|
}
|
|
470
475
|
|
|
471
476
|
function wrapContext(body) {
|
|
477
|
+
// Retrieved text cannot introduce a second capture delimiter inside ours.
|
|
478
|
+
const text = String(body)
|
|
479
|
+
.replace(/<\/?relevant-memor(?:y|ies)\b[^>]*>/gi, "legacy memory wrapper")
|
|
480
|
+
.replace(/<\/?openviking-context\b[^>]*>/gi, "openviking context marker");
|
|
472
481
|
return [
|
|
473
482
|
"<openviking-context>",
|
|
474
483
|
"Relevant memory from OpenViking. Use the search/read MCP tools to expand URIs.",
|
|
475
|
-
|
|
484
|
+
text,
|
|
476
485
|
"</openviking-context>",
|
|
477
486
|
].join("\n");
|
|
478
487
|
}
|
|
@@ -488,7 +497,7 @@ export async function buildServerAssembledBlock(fetchJSON, cfg, query, options =
|
|
|
488
497
|
|
|
489
498
|
const block = await recallViaContextFace(fetchJSON, cfg, query, { ...options, actorPeerId }, log);
|
|
490
499
|
if (block !== null) return block;
|
|
491
|
-
return recallViaEndpoint(fetchJSON, cfg, query, actorPeerId, log);
|
|
500
|
+
return recallViaEndpoint(fetchJSON, cfg, query, { ...options, actorPeerId }, log);
|
|
492
501
|
}
|
|
493
502
|
|
|
494
503
|
/**
|
|
@@ -552,53 +561,65 @@ export function normalizeContextEntry(entry = {}) {
|
|
|
552
561
|
};
|
|
553
562
|
}
|
|
554
563
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
if (
|
|
564
|
+
/** Server relevance decisions and digest precedence are shared by every host. */
|
|
565
|
+
export function selectRecallContent(result = {}) {
|
|
566
|
+
if (String(result.stats?.rewrite || "").toLowerCase() === "no_relevant") return "";
|
|
567
|
+
return String(result.digest || "").trim() || String(result.rendered || "").trim();
|
|
568
|
+
}
|
|
558
569
|
|
|
559
|
-
|
|
560
|
-
let digest = assembled.digest;
|
|
570
|
+
function wantsLocalCompression(cfg, options) {
|
|
561
571
|
const mode = String(cfg.recallRewrite || "off").toLowerCase();
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
572
|
+
return (mode === "client" || mode === "auto")
|
|
573
|
+
&& options.localCompressorAvailable !== false
|
|
574
|
+
&& typeof options.runCompressor === "function";
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Context, legacy recall, and raw retrieval share the same result policy.
|
|
578
|
+
// The host callback only runs its model; it never chooses a fallback or digest.
|
|
579
|
+
async function finalizeRecall(result, cfg, query, options, log, shortContext) {
|
|
580
|
+
const selected = selectRecallContent(result);
|
|
581
|
+
if (String(result.stats?.rewrite || "").toLowerCase() === "no_relevant") return "";
|
|
582
|
+
const entries = (result.entries || []).map(normalizeContextEntry).filter((entry) => entry.uri);
|
|
583
|
+
const rendered = String(result.rendered || "").trim();
|
|
584
|
+
if (String(result.digest || "").trim() || !wantsLocalCompression(cfg, options)) return selected;
|
|
585
|
+
const fallback = shortContext ?? (entries.length ? formatFallback(entries, cfg).rendered : rendered);
|
|
586
|
+
const input = rendered || entries.map((entry) => `${entry.uri}\n${entry.text}`).join("\n");
|
|
587
|
+
if (!input) return "";
|
|
588
|
+
try {
|
|
589
|
+
const compression = await compressRecallContext({
|
|
590
|
+
query, rendered: input, shortContext: shortContext ?? (rendered || fallback),
|
|
591
|
+
entries, cfg, runCompressor: options.runCompressor,
|
|
592
|
+
cachePath: options.digestCachePath || stateFile("recall-digest.json"), now: Date.now(),
|
|
593
|
+
});
|
|
594
|
+
log("recall_local_compression", { status: compression.status });
|
|
595
|
+
if (compression.status === "ok") return compression.context;
|
|
596
|
+
if (compression.status === "empty") return "";
|
|
597
|
+
} catch (err) {
|
|
598
|
+
log("recall_local_compression_failed", { error: String(err?.message || err) });
|
|
584
599
|
}
|
|
600
|
+
return fallback;
|
|
601
|
+
}
|
|
585
602
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
603
|
+
async function recallViaContextFace(fetchJSON, cfg, query, options, log) {
|
|
604
|
+
const assembled = await fetchAssembledContext(fetchJSON, cfg, query, { ...options, log });
|
|
605
|
+
if (assembled === null) return null;
|
|
606
|
+
const text = await finalizeRecall(assembled, cfg, query, options, log);
|
|
607
|
+
return text ? wrapContext(text) : "";
|
|
589
608
|
}
|
|
590
609
|
|
|
591
|
-
async function recallViaEndpoint(fetchJSON, cfg, query,
|
|
610
|
+
async function recallViaEndpoint(fetchJSON, cfg, query, options, log) {
|
|
592
611
|
const body = buildRecallEndpointBody(cfg);
|
|
593
612
|
body.query = query;
|
|
594
|
-
|
|
613
|
+
if (wantsLocalCompression(cfg, options)) {
|
|
614
|
+
body.max_chars = Math.max(1000, Number(cfg.recallCompressMaxInputChars || 18000));
|
|
615
|
+
}
|
|
616
|
+
const res = await postRecall(fetchJSON, body, { actorPeerId: options.actorPeerId, log });
|
|
595
617
|
if (!res.ok) {
|
|
596
618
|
log("recall_endpoint_fallback", { status: res.status || 0 });
|
|
597
619
|
return null;
|
|
598
620
|
}
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
return wrapContext(rendered);
|
|
621
|
+
const text = await finalizeRecall(res.result || {}, cfg, query, options, log);
|
|
622
|
+
return text ? wrapContext(text) : "";
|
|
602
623
|
}
|
|
603
624
|
|
|
604
625
|
export async function postRecall(fetchJSON, body, opts = {}) {
|
|
@@ -711,7 +732,7 @@ async function recallForPeer(fetchJSON, cfg, query, options = {}) {
|
|
|
711
732
|
|
|
712
733
|
const recallLimit = Math.max(1, Number(cfg.recallLimit || DEFAULT_CONTEXT_LIMIT));
|
|
713
734
|
const perSourceLimit = Math.max(recallLimit * 2, 8);
|
|
714
|
-
const raw = await searchAllSources(fetchJSON, trimmed, perSourceLimit, actorPeerId, log);
|
|
735
|
+
const raw = await searchAllSources(fetchJSON, cfg, trimmed, perSourceLimit, { ...options, actorPeerId }, log);
|
|
715
736
|
if (raw.length === 0) return emptyRecall("no_results");
|
|
716
737
|
|
|
717
738
|
const profile = buildQueryProfile(trimmed);
|
|
@@ -727,5 +748,25 @@ async function recallForPeer(fetchJSON, cfg, query, options = {}) {
|
|
|
727
748
|
});
|
|
728
749
|
|
|
729
750
|
if (picked.length === 0) return emptyRecall("filtered_out");
|
|
730
|
-
|
|
751
|
+
const local = wantsLocalCompression(cfg, options);
|
|
752
|
+
const inputLimit = Math.max(1000, Number(cfg.recallCompressMaxInputChars || 18000));
|
|
753
|
+
const contentCfg = local ? { ...cfg, recallPreferAbstract: false, recallMaxContentChars: inputLimit } : cfg;
|
|
754
|
+
const entries = await Promise.all(picked.map(async (item) => ({
|
|
755
|
+
...item, text: await resolveItemContent(fetchJSON, item, contentCfg, actorPeerId),
|
|
756
|
+
})));
|
|
757
|
+
const fallback = formatFallback(entries, cfg, log);
|
|
758
|
+
let text = await finalizeRecall({
|
|
759
|
+
rendered: local ? entries.map((item) => `${item.uri}\n${item.text}`).join("\n").slice(0, inputLimit) : fallback.rendered,
|
|
760
|
+
entries,
|
|
761
|
+
}, cfg, trimmed, options, log, fallback.rendered);
|
|
762
|
+
if (estimateTokens(wrapContext(text)) > Math.max(200, Number(cfg.recallTokenBudget || 2000))) {
|
|
763
|
+
text = fallback.rendered;
|
|
764
|
+
}
|
|
765
|
+
if (!text) return emptyRecall("no_results");
|
|
766
|
+
const unchanged = text === fallback.rendered;
|
|
767
|
+
return {
|
|
768
|
+
block: wrapContext(text), contentCount: unchanged ? fallback.contentCount : 1,
|
|
769
|
+
hintCount: unchanged ? fallback.hintCount : 0,
|
|
770
|
+
budgetUsed: estimateTokens(wrapContext(text)), stage: "ranked",
|
|
771
|
+
};
|
|
731
772
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openviking/opencode-plugin",
|
|
3
|
-
"version": "2026.9.21-
|
|
3
|
+
"version": "2026.9.21-2",
|
|
4
4
|
"author": "@openviking",
|
|
5
5
|
"description": "Unified OpenCode plugin for OpenViking repository retrieval and long-term memory",
|
|
6
6
|
"type": "module",
|
|
@@ -45,5 +45,5 @@
|
|
|
45
45
|
"code-search"
|
|
46
46
|
],
|
|
47
47
|
"license": "Apache-2.0",
|
|
48
|
-
"openvikingSourceCommit": "
|
|
48
|
+
"openvikingSourceCommit": "e44ea6e11add1c7b3d4accdbfaf16e900a6049df"
|
|
49
49
|
}
|