@openviking/dsh-memory-plugin 0.5.25 → 0.5.27

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/config.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { buildPluginConfig } from "./shared/plugin-config.mjs";
2
- import { loadCredentialFiles } from "./shared/credentials.mjs";
2
+ import { loadCredentialFiles, readManifestVersion } from "./shared/credentials.mjs";
3
3
 
4
- export const PLUGIN_VERSION = "0.5.25";
4
+ export const PLUGIN_VERSION = readManifestVersion(new URL("./package.json", import.meta.url));
5
5
 
6
6
  /**
7
7
  * Namespace for the bridged OpenViking MCP tools. DSH publishes every MCP tool
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openviking/dsh-memory-plugin",
3
- "version": "0.5.25",
3
+ "version": "0.5.27",
4
4
  "description": "OpenViking memory and context bundle for DeepSeek Harness",
5
5
  "icon": "./icon.svg",
6
6
  "type": "module",
@@ -362,14 +362,41 @@ export function extractPartsFromPayload(payload, options = {}) {
362
362
  return parts;
363
363
  }
364
364
 
365
+ function substituteValue(value, rules, role) {
366
+ if (typeof value === "string") {
367
+ let current = value;
368
+ for (const rule of rules) {
369
+ if (rule.op !== "s" || (rule.scope && rule.scope !== role)) continue;
370
+ current = current.replace(rule.re, rule.replacement);
371
+ }
372
+ return current;
373
+ }
374
+ if (Array.isArray(value)) return value.map((item) => substituteValue(item, rules, role));
375
+ if (value && typeof value === "object") {
376
+ return Object.fromEntries(
377
+ Object.entries(value).map(([key, item]) => [key, substituteValue(item, rules, role)]),
378
+ );
379
+ }
380
+ return value;
381
+ }
382
+
383
+ function substituteToolPart(part, rules, role) {
384
+ if (part.type !== "tool") return part;
385
+ const out = { ...part };
386
+ if (part.tool_input !== undefined) out.tool_input = substituteValue(part.tool_input, rules, role);
387
+ if (part.tool_output !== undefined) out.tool_output = substituteValue(part.tool_output, rules, role);
388
+ return out;
389
+ }
390
+
365
391
  /**
366
392
  * Prune blank text parts and run `cfg.captureFilters` over the ones that remain.
367
393
  *
368
394
  * One drop verdict is taken per turn, on the aggregate of its text parts; the
369
395
  * individual parts are then rewritten with the substitutions only, so a rule can
370
- * never both keep and drop the same turn. Tool call/result parts are carried
371
- * through untouched — filters match conversation text, not tool payloads — but a
372
- * turn dropped on its text takes its tool parts with it.
396
+ * never both keep and drop the same turn. Tool call/result parts get the
397
+ * substitutions too, on their input values and output, so a redaction rule also
398
+ * covers a secret that reached a command or its output; only text parts decide
399
+ * the drop verdict, and a turn dropped on its text takes its tool parts with it.
373
400
  */
374
401
  export function filterCaptureParts(parts, role, cfg = {}) {
375
402
  const kept = [];
@@ -389,7 +416,9 @@ export function filterCaptureParts(parts, role, cfg = {}) {
389
416
  if (!compiled.rules.length || kept.length === 0) return blank;
390
417
 
391
418
  const textParts = kept.filter((part) => part.type === "text");
392
- if (textParts.length === 0) return blank;
419
+ if (textParts.length === 0) {
420
+ return { parts: kept.map((part) => substituteToolPart(part, compiled.rules, role)), dropped: false };
421
+ }
393
422
 
394
423
  const verdict = applyInputFilters(textParts.map((part) => part.text).join("\n\n"), compiled.rules, {
395
424
  role,
@@ -399,7 +428,7 @@ export function filterCaptureParts(parts, role, cfg = {}) {
399
428
  const out = [];
400
429
  for (const part of kept) {
401
430
  if (part.type !== "text") {
402
- out.push(part);
431
+ out.push(substituteToolPart(part, compiled.rules, role));
403
432
  continue;
404
433
  }
405
434
  const shaped = applyInputFilters(part.text, compiled.rules, { role, substituteOnly: true });
@@ -431,7 +460,7 @@ function faithfulCaptureDecision(text, cfg) {
431
460
  /**
432
461
  * Shape one host message before an adapter builds its session payload.
433
462
  * 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.
463
+ * will not be sent cannot satisfy a keep rule. Tool parts never decide keep or drop.
435
464
  */
436
465
  export function shapeCapturePayload(payload, role, cfg = {}, { toolNameById = {}, faithful = false } = {}) {
437
466
  const options = { toolMaxChars: cfg.captureToolMaxChars, toolNameById };
@@ -473,17 +473,22 @@ function looksLikeUnknownField(res) {
473
473
  return text.includes("extra") || text.includes("mode") || text.includes("unexpected");
474
474
  }
475
475
 
476
+ // Recalled text is the user's own history. Without a provenance boundary a model
477
+ // may cite an internal decision as external evidence or industry practice.
478
+ export const RECALL_CONTEXT_TEMPLATE = [
479
+ "<openviking-context>",
480
+ "Relevant memory from OpenViking. It is this user's own saved history (past conversations, meetings, notes) as recorded then: internal context, not an external source or established fact. Use the search/read MCP tools to expand URIs.",
481
+ "{context}",
482
+ "</openviking-context>",
483
+ ].join("\n");
484
+
476
485
  function wrapContext(body) {
477
486
  // Retrieved text cannot introduce a second capture delimiter inside ours.
478
487
  const text = String(body)
479
488
  .replace(/<\/?relevant-memor(?:y|ies)\b[^>]*>/gi, "legacy memory wrapper")
480
489
  .replace(/<\/?openviking-context\b[^>]*>/gi, "openviking context marker");
481
- return [
482
- "<openviking-context>",
483
- "Relevant memory from OpenViking. Use the search/read MCP tools to expand URIs.",
484
- text,
485
- "</openviking-context>",
486
- ].join("\n");
490
+ // Use a replacer so recalled dollar sequences stay literal.
491
+ return RECALL_CONTEXT_TEMPLATE.replace("{context}", () => text);
487
492
  }
488
493
 
489
494
  /**