@openviking/opencode-plugin 2026.9.24-3 → 2026.9.24-4
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/index.mjs +12 -3
- package/lib/memory-session.mjs +12 -14
- package/lib/shared/capture-utils.mjs +59 -15
- package/package.json +2 -2
package/index.mjs
CHANGED
|
@@ -56,9 +56,18 @@ function v1Hooks(runtime) {
|
|
|
56
56
|
|
|
57
57
|
"chat.message": async (input, output) => {
|
|
58
58
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
// opencode awaits this hook before persisting/broadcasting the user
|
|
60
|
+
// message, so serial awaits here stack remote latency onto message
|
|
61
|
+
// display (#5148). Session inject and recall are independent after
|
|
62
|
+
// entry; overlap them the way the dsh plugin does (#4643).
|
|
63
|
+
if (!isRecallEnabled(config)) {
|
|
64
|
+
await sessionInject.injectSessionContext(input, output)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
await Promise.all([
|
|
68
|
+
sessionInject.injectSessionContext(input, output),
|
|
69
|
+
recall.injectRelevantMemories(input, output),
|
|
70
|
+
])
|
|
62
71
|
} catch (error) {
|
|
63
72
|
log("WARN", "recall", "Auto recall failed", { error: error?.message ?? String(error) })
|
|
64
73
|
}
|
package/lib/memory-session.mjs
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import fs from "fs"
|
|
2
2
|
import path from "path"
|
|
3
3
|
import {
|
|
4
|
-
extractPartsFromPayload,
|
|
5
4
|
extractTextFromPayload,
|
|
6
5
|
isCaptureEnabled,
|
|
7
|
-
|
|
6
|
+
shapeCapturePayload,
|
|
8
7
|
} from "./shared/capture-utils.mjs"
|
|
9
8
|
import {
|
|
10
9
|
deriveHarnessSessionId,
|
|
@@ -383,18 +382,17 @@ export function createMemorySessionManager({ config, pluginRoot }) {
|
|
|
383
382
|
if (!role) return null
|
|
384
383
|
if (role === "assistant" && !config.captureAssistantTurns) return null
|
|
385
384
|
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
: { role, content: decision.text }
|
|
385
|
+
const shaped = shapeCapturePayload({ role, content: partsRaw }, role, config)
|
|
386
|
+
const toolParts = shaped.parts.filter((part) => part.type !== "text")
|
|
387
|
+
if (shaped.dropped || (!shaped.text && toolParts.length === 0)) return null
|
|
388
|
+
const body = toolParts.length > 0
|
|
389
|
+
? { role, parts: [
|
|
390
|
+
...(shaped.text && shaped.parts.some((part) => part.type === "text")
|
|
391
|
+
? [{ type: "text", text: shaped.text }]
|
|
392
|
+
: []),
|
|
393
|
+
...toolParts,
|
|
394
|
+
] }
|
|
395
|
+
: { role, content: shaped.text }
|
|
398
396
|
const peerId = effectivePeerId(config)
|
|
399
397
|
if (peerId) body.peer_id = peerId
|
|
400
398
|
return body
|
|
@@ -409,6 +409,61 @@ export function filterCaptureParts(parts, role, cfg = {}) {
|
|
|
409
409
|
return { parts: out, dropped: out.length === 0 };
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
+
/** Sanitize already-extracted message parts before applying capture rules. */
|
|
413
|
+
export function shapeCaptureParts(parts, role, cfg = {}) {
|
|
414
|
+
const sanitized = (parts || []).map((part) => part?.type === "text"
|
|
415
|
+
? { ...part, text: sanitizeCapturedText(part.text) }
|
|
416
|
+
: part);
|
|
417
|
+
return filterCaptureParts(sanitized, role, cfg);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function faithfulCaptureDecision(text, cfg) {
|
|
421
|
+
const sanitized = sanitizeCapturedText(text);
|
|
422
|
+
if (!sanitized) return { shouldCapture: false, text: "" };
|
|
423
|
+
const capped = truncateCaptureText(sanitized, cfg.captureMaxLength || 24000);
|
|
424
|
+
const compact = oneLine(capped);
|
|
425
|
+
if (/^\[openviking-memory\]/i.test(compact) || SLASH_COMMAND_RE.test(compact)) {
|
|
426
|
+
return { shouldCapture: false, text: "" };
|
|
427
|
+
}
|
|
428
|
+
return { shouldCapture: true, text: capped };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Shape one host message before an adapter builds its session payload.
|
|
433
|
+
* Sanitize injected context before evaluating keep/drop rules, so text that
|
|
434
|
+
* will not be sent cannot satisfy a keep rule. Tool parts are never filtered.
|
|
435
|
+
*/
|
|
436
|
+
export function shapeCapturePayload(payload, role, cfg = {}, { toolNameById = {}, faithful = false } = {}) {
|
|
437
|
+
const options = { toolMaxChars: cfg.captureToolMaxChars, toolNameById };
|
|
438
|
+
const rawText = extractTextFromPayload(payload, options);
|
|
439
|
+
const sourceParts = extractPartsFromPayload(payload, options);
|
|
440
|
+
const hasTextPart = sourceParts.some((part) => part?.type === "text");
|
|
441
|
+
const sanitizedText = sanitizeCapturedText(rawText);
|
|
442
|
+
// Some hosts supply an array of plain strings. It has text but no structured
|
|
443
|
+
// parts, so use a temporary part for the same filter verdict.
|
|
444
|
+
const fallback = sourceParts.length === 0 && sanitizedText
|
|
445
|
+
? [{ type: "text", text: rawText }]
|
|
446
|
+
: [];
|
|
447
|
+
const shaped = shapeCaptureParts(sourceParts.length ? sourceParts : fallback, role, cfg);
|
|
448
|
+
if (shaped.dropped) return { parts: [], text: "", signalText: "", dropped: true };
|
|
449
|
+
|
|
450
|
+
const parts = sourceParts.length ? shaped.parts : [];
|
|
451
|
+
const decisionText = hasTextPart || fallback.length
|
|
452
|
+
? shaped.parts.filter((part) => part.type === "text").map((part) => part.text).join("\n\n")
|
|
453
|
+
: sanitizedText;
|
|
454
|
+
const decision = faithful
|
|
455
|
+
? faithfulCaptureDecision(decisionText, cfg)
|
|
456
|
+
: shouldCaptureText(decisionText, role, cfg, { filters: false });
|
|
457
|
+
// Codex scans turn.text for explicit-memory keywords; keep that scan based
|
|
458
|
+
// on the user's original words while the filtered parts go on the wire.
|
|
459
|
+
const signalText = decision.shouldCapture && parts.length
|
|
460
|
+
? (faithful
|
|
461
|
+
? faithfulCaptureDecision(sanitizedText, cfg)
|
|
462
|
+
: shouldCaptureText(sanitizedText, role, cfg, { filters: false })).text
|
|
463
|
+
: decision.text;
|
|
464
|
+
return { parts, text: decision.shouldCapture ? decision.text : "", signalText: signalText || "", dropped: false };
|
|
465
|
+
}
|
|
466
|
+
|
|
412
467
|
export function extractCaptureTurns(rolloutEntries, cfg = {}) {
|
|
413
468
|
const toolNameById = collectToolNamesByIdFromEntries(rolloutEntries);
|
|
414
469
|
const turns = [];
|
|
@@ -421,20 +476,9 @@ export function extractCaptureTurns(rolloutEntries, cfg = {}) {
|
|
|
421
476
|
if (!role) continue;
|
|
422
477
|
if (isAssistantSideCaptureRole(rawRole) && !cfg.captureAssistantTurns) continue;
|
|
423
478
|
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
toolNameById,
|
|
428
|
-
});
|
|
429
|
-
const shaped = filterCaptureParts(parts, role, cfg);
|
|
430
|
-
if (shaped.dropped) continue;
|
|
431
|
-
// With parts on the wire the drop decision was already taken above, so the
|
|
432
|
-
// text path only runs the filters for the `content` fallback. That also
|
|
433
|
-
// keeps `turn.text` faithful for callers that scan it for trigger words.
|
|
434
|
-
const decision = shouldCaptureText(rawText, role, cfg, { filters: shaped.parts.length === 0 });
|
|
435
|
-
if (!decision.shouldCapture && shaped.parts.length === 0) continue;
|
|
436
|
-
const text = decision.shouldCapture ? decision.text : "";
|
|
437
|
-
turns.push({ role, text, parts: shaped.parts });
|
|
479
|
+
const shaped = shapeCapturePayload(payload, role, cfg, { toolNameById });
|
|
480
|
+
if (shaped.dropped || (!shaped.text && shaped.parts.length === 0)) continue;
|
|
481
|
+
turns.push({ role, text: shaped.parts.length ? shaped.signalText : shaped.text, parts: shaped.parts });
|
|
438
482
|
}
|
|
439
483
|
return turns;
|
|
440
484
|
}
|
|
@@ -555,7 +599,7 @@ export function sanitizeCapturedText(text) {
|
|
|
555
599
|
.replace(/^[ \t]*\[Subagent Context\][^\n]*$/gim, " ")
|
|
556
600
|
.replace(/^\s*Sender\s*\([^)]+\)\s*```[\s\S]*?```\s*/gim, " ")
|
|
557
601
|
.replace(/^\s*Conversation (?:metadata|info):\s*```[\s\S]*?```\s*/gim, " ")
|
|
558
|
-
.replace(
|
|
602
|
+
.replace(/^[ \t]*\[\d{4}-\d{2}-\d{2}[T ][-+0-9:.Z]{5,35}\][ \t]*/gm, "")
|
|
559
603
|
.replace(/^\s*\d{10,13}\s+/gm, "");
|
|
560
604
|
value = stripMetadataFences(value);
|
|
561
605
|
value = stripInjectedDigestBlocks(value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openviking/opencode-plugin",
|
|
3
|
-
"version": "2026.9.24-
|
|
3
|
+
"version": "2026.9.24-4",
|
|
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": "92dcf00f837fa4f0c58401ba4349011e64c0b3bf"
|
|
49
49
|
}
|