@openviking/dsh-memory-plugin 0.5.3 → 0.5.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/capture.mjs +9 -14
- package/config.mjs +1 -1
- package/mcp-env.mjs +1 -0
- package/package.json +1 -1
- package/servers/mcp-proxy.mjs +4 -5
- package/shared/capture-utils.mjs +59 -15
package/capture.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
2
|
import {
|
|
3
|
-
extractPartsFromPayload,
|
|
4
3
|
extractTextFromPayload,
|
|
5
|
-
|
|
4
|
+
shapeCapturePayload,
|
|
6
5
|
} from "./shared/capture-utils.mjs";
|
|
7
6
|
|
|
8
7
|
export const OPENVIKING_PLUGIN_SOURCE = "openviking-memory";
|
|
@@ -74,27 +73,23 @@ function captureMessage(event, message, config, toolNames) {
|
|
|
74
73
|
|
|
75
74
|
const role = message.role === "assistant" ? "assistant" : "user";
|
|
76
75
|
const toolNameById = Object.fromEntries(toolNames);
|
|
77
|
-
const
|
|
78
|
-
toolMaxChars: config.captureToolMaxChars,
|
|
79
|
-
});
|
|
80
|
-
const parts = extractPartsFromPayload(message, {
|
|
81
|
-
toolMaxChars: config.captureToolMaxChars,
|
|
76
|
+
const shaped = shapeCapturePayload(message, role, config, {
|
|
82
77
|
toolNameById,
|
|
83
78
|
});
|
|
84
|
-
|
|
85
|
-
const structuredParts = parts.filter(part => part?.type !== "text");
|
|
86
|
-
if (!
|
|
79
|
+
if (shaped.dropped) return null;
|
|
80
|
+
const structuredParts = shaped.parts.filter(part => part?.type !== "text");
|
|
81
|
+
if (!shaped.text && structuredParts.length === 0) return null;
|
|
87
82
|
|
|
88
|
-
const hasTextPart = parts.some(part => part?.type === "text");
|
|
83
|
+
const hasTextPart = shaped.parts.some(part => part?.type === "text");
|
|
89
84
|
const bodyParts = [
|
|
90
|
-
...(hasTextPart &&
|
|
91
|
-
? [{ type: "text", text:
|
|
85
|
+
...(hasTextPart && shaped.text
|
|
86
|
+
? [{ type: "text", text: shaped.text }]
|
|
92
87
|
: []),
|
|
93
88
|
...structuredParts,
|
|
94
89
|
];
|
|
95
90
|
const payload = bodyParts.length > 0
|
|
96
91
|
? { role, parts: bodyParts }
|
|
97
|
-
: { role, content:
|
|
92
|
+
: { role, content: shaped.text };
|
|
98
93
|
const createdAt = eventCreatedAt(event);
|
|
99
94
|
if (createdAt) payload.created_at = createdAt;
|
|
100
95
|
if (config.peerId) payload.peer_id = config.peerId;
|
package/config.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { buildPluginConfig } from "./shared/plugin-config.mjs";
|
|
2
2
|
import { loadCredentialFiles } from "./shared/credentials.mjs";
|
|
3
3
|
|
|
4
|
-
export const PLUGIN_VERSION = "0.5.
|
|
4
|
+
export const PLUGIN_VERSION = "0.5.4";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Namespace for the bridged OpenViking MCP tools. DSH publishes every MCP tool
|
package/mcp-env.mjs
CHANGED
|
@@ -21,6 +21,7 @@ export function buildMcpConfig(config) {
|
|
|
21
21
|
// standalone Node binary. This tells Electron to run the proxy script as
|
|
22
22
|
// Node instead of attempting to launch a second Desktop instance.
|
|
23
23
|
const env = { ELECTRON_RUN_AS_NODE: "1", ...forwardConnectionEnv(config) };
|
|
24
|
+
if (config.recallPeerScope) env.OPENVIKING_RECALL_PEER_SCOPE = config.recallPeerScope;
|
|
24
25
|
if (config.timeoutMs) env.OPENVIKING_TIMEOUT_MS = String(config.timeoutMs);
|
|
25
26
|
return {
|
|
26
27
|
transport: "stdio",
|
package/package.json
CHANGED
package/servers/mcp-proxy.mjs
CHANGED
|
@@ -21,11 +21,10 @@ export function readProxyConfig(env = process.env, cwd = process.cwd()) {
|
|
|
21
21
|
const cfg = resolveConfig({}, env, cwd);
|
|
22
22
|
return toMcpProxyConfig(cfg, {
|
|
23
23
|
env,
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
peerId: String(env.OPENVIKING_PEER_ID || "").trim(),
|
|
24
|
+
// The child env carries the runtime's resolved peer, but the shared mapper
|
|
25
|
+
// still decides whether the proxy may send it. In broad recall mode the
|
|
26
|
+
// actor header must stay unset; otherwise a process launched in workspace A
|
|
27
|
+
// narrows MCP searches for a session whose runtime is serving workspace B.
|
|
29
28
|
debug: Boolean(env.OV_DEBUG_LOG),
|
|
30
29
|
debugLogPath: env.OV_DEBUG_LOG || "",
|
|
31
30
|
});
|
package/shared/capture-utils.mjs
CHANGED
|
@@ -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);
|