@openviking/dsh-memory-plugin 0.5.2 → 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 CHANGED
@@ -1,8 +1,7 @@
1
1
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
2
2
  import {
3
- extractPartsFromPayload,
4
3
  extractTextFromPayload,
5
- shouldCaptureText,
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 rawText = extractTextFromPayload(message, {
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
- const decision = shouldCaptureText(rawText, role, config);
85
- const structuredParts = parts.filter(part => part?.type !== "text");
86
- if (!decision.shouldCapture && structuredParts.length === 0) return null;
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 && decision.shouldCapture && decision.text
91
- ? [{ type: "text", text: decision.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: decision.text };
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.2";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openviking/dsh-memory-plugin",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "OpenViking memory and context bundle for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
package/runtime.mjs CHANGED
@@ -139,6 +139,7 @@ export class OpenVikingRuntime {
139
139
  actorPeerId: state.config.peerId,
140
140
  legacyPeerId: state.config.legacyPeerId,
141
141
  sessionId: state.ovSessionId,
142
+ excludeUris: state.config.recallExcludeUris,
142
143
  log: (stage, data) => this.log(stage, data),
143
144
  },
144
145
  );
@@ -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
- // Not gated through `resolveMcpActorPeerId` like the other proxies: DSH's
25
- // parent process resolves the peer per session and hands it over in the
26
- // child env. Empty means it has none, and deriving one from wherever DSH
27
- // launched this process would send a peer the runtime does not.
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
  });
@@ -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 rawText = extractTextFromPayload(payload, { toolMaxChars: cfg.captureToolMaxChars });
425
- const parts = extractPartsFromPayload(payload, {
426
- toolMaxChars: cfg.captureToolMaxChars,
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(/^\s*\[?\d{4}-\d{2}-\d{2}[T ][^\]\n]{3,80}\]?\s*/gm, "")
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);
@@ -93,6 +93,17 @@ export const KNOBS = [
93
93
  { name: "logRankingDetails", type: "bool", default: false, env: "OPENVIKING_LOG_RANKING_DETAILS", capability: "recall" },
94
94
  { name: "recallLedger", type: "bool", default: true, env: "OPENVIKING_RECALL_LEDGER", capability: "recall" },
95
95
  { name: "recallQueryFilters", type: "list", default: [], env: "OPENVIKING_RECALL_QUERY_FILTERS", capability: "recall" },
96
+ // Subtree URIs the recall search must never return. Without this there is no
97
+ // way to keep generated directory files (viking://user/<space>/skills,
98
+ // viking://user/<space>/resources, viking://agent/skills) out of the hits:
99
+ // they match like ordinary content and carry only boilerplate text.
100
+ {
101
+ name: "recallExcludeUris",
102
+ type: "list",
103
+ default: [],
104
+ env: "OPENVIKING_RECALL_EXCLUDE_URIS",
105
+ capability: "recall",
106
+ },
96
107
 
97
108
  // Digest compression. Claude Code reads this as the tri-state
98
109
  // off/client/server/auto through `normalizeRewriteMode`; Codex reads the same