Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/openclaw/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,16 @@ questions:
- What changed in the MemWal OpenClaw plugin changelog?
- When was the MemWal OpenClaw plugin first released?
answer: >-
Version 0.0.8 bounds and expires idle capture-session state, watermarks auto-capture per session and preserves message timestamps for occurredAt, aborts timed-out analyze requests without blind retries, distinguishes accepted async remember jobs from completed storage in memory_store, updates the MemWal SDK dependency to 0.1.10, and detects letter-spaced prompt injection keywords.
Version 0.0.9 skips automatic capture on conversational opt-out turns (including trailing assistant replies) and inline credentials while preserving ordinary developer statements.
---

## 0.0.9

### Fixed

- Skip automatic capture for conversational opt-out directives (`do not remember`, `don't save`, `off the record`, `forget this`, `[no-save]`, `#nosave`) and suppress trailing assistant replies on opted-out user turns so echoed topics never reach `analyze()`.
- Exclude turns containing inline credentials, vendor API keys, PEM private keys, labelled 64-hex keys, seed phrases, and `key=value` or `key: value` secret assignments from automatic capture while keeping ordinary developer prose (`We never log request bodies in production`, `Our api_key is configured via Railway`) capturable.

## 0.0.8

### Fixed
Expand Down
7 changes: 7 additions & 0 deletions packages/openclaw-memory-memwal/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# @mysten-incubation/oc-memwal

## 0.0.9

### Fixed

- Skip automatic capture for conversational opt-out directives (`do not remember`, `don't save`, `off the record`, `forget this`, `[no-save]`, `#nosave`) and suppress trailing assistant replies on opted-out user turns so echoed topics never reach `analyze()`.
- Exclude turns containing inline credentials, vendor API keys, PEM private keys, labelled 64-hex keys, seed phrases, and `key=value` or `key: value` secret assignments from automatic capture while keeping ordinary developer prose (`We never log request bodies in production`, `Our api_key is configured via Railway`) capturable.

## 0.0.8

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion packages/openclaw-memory-memwal/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mysten-incubation/oc-memwal",
"version": "0.0.8",
"version": "0.0.9",
"type": "module",
"description": "NemoClaw/OpenClaw memory plugin — encrypted, decentralized long-term memory via Walrus Memory",
"license": "Apache-2.0",
Expand Down
69 changes: 69 additions & 0 deletions packages/openclaw-memory-memwal/src/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,54 @@ const ADDRESSES_MODEL = [
/^(ign[o0a]re|disregard|f[o0]rget|override|bypass|run|execute|call|invoke|reveal|print|output)\b/i,
];

const CLAUSE_IMPERATIVE_PREFIX =
"(?:^|[.!?;:,(\\[\\n]\\s*|\\b(?:please|just)\\s+|\\byou\\s+(?:must\\s+|should\\s+|can\\s+)?)";

const IMPERATIVE_NEGATION_PREFIX =
"(?:^|[.!?;:,(\\[\\n]\\s*|\\b(?:and|but|please|just)\\s+|\\byou\\s+(?:must\\s+|should\\s+|can\\s+)?)";

/** Conversational opt-out patterns that instruct the assistant not to persist the turn. */
const OPT_OUT_PATTERNS = [
new RegExp(
`${CLAUSE_IMPERATIVE_PREFIX}(?:do\\s+not|don['\\u2019]?t|never)\\s+(?:ever\\s+)?(?:remember|memorize)(?:\\s+or\\s+(?:save|store|record|capture|keep))?\\b`,
"i",
),
new RegExp(
`${IMPERATIVE_NEGATION_PREFIX}(?:do\\s+not|don['\\u2019]?t|never)\\s+(?:ever\\s+)?(?:remember|memorize)(?:\\s+or\\s+(?:save|store|record|capture|keep)\\b|\\s+(?:any(?:\\s+of)?(?:\\s+(?:this|that|these|those|it))?|anything(?:\\s+(?:here|from\\s+this))?|this|that|these|those|it|the\\s+following\\b|(?:in|to)\\s+memory|the\\s+(?:conversation|chat|message|turn|discussion|note|notes|details))\\b)`,
"i",
),
new RegExp(
`${IMPERATIVE_NEGATION_PREFIX}(?:do\\s+not|don['\\u2019]?t|never)\\s+(?:ever\\s+)?(?:save|store|record|capture|keep)(?:\\s+or\\s+(?:remember|memorize|save|store|record|capture|keep))?(?:\\s+(?:any(?:\\s+of)?(?:\\s+(?:this|that|these|those|it))?|anything(?:\\s+(?:here|from\\s+this))?|this|that|these|those|it|what\\s+i|where\\s+i|who\\s+i|how\\s+i|my\\b|our\\b|the\\s+following\\b|(?:in|to)\\s+memory|the\\s+(?:conversation|chat|message|turn|discussion|note|notes|details))\\b|(?=\\s*(?:$|[.!?:;,)\\]])))`,
"i",
),
new RegExp(
`${IMPERATIVE_NEGATION_PREFIX}(?:do\\s+not|don['\\u2019]?t|never)\\s+(?:ever\\s+)?log\\s+(?:this(?:\\s+(?:conversation|message|chat|turn|session))?|anything\\s+(?:here|from\\s+this)|our\\s+(?:conversation|chat))\\b`,
"i",
),
/\b(?:off\s+the\s+record|(?:do\s+not|don['\u2019]?t|never)\s+keep\s+(?:this\s+|that\s+|it\s+)?in\s+memory|(?:please\s+)?forget\s+(?:this(?:\s+(?:conversation|message|chat|turn))?|what\s+i\s+just\s+said))\b/i,
/(?:^|\s)(?:\[\s*no[-_\s]?save\s*\]|#no[-_]?save\b)/i,
];

/** High-confidence inline credential and secret patterns excluded from automatic capture. */
const SECRET_PATTERNS = [
/\bsuiprivkey1[02-9ac-hj-np-z]{50,}\b/i,
/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/,
/\bsk-(?:proj-|ant-(?:api\d+-)?|live-|test-)?[A-Za-z0-9_-]{20,}\b/,
/\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/,
/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/,
/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/,
/\bAIza[0-9A-Za-z_-]{35}\b/,
/\b(?:glpat-[A-Za-z0-9_-]{16,}|npm_[A-Za-z0-9]{36}|[sprk]k_(?:live|test)_[A-Za-z0-9]{16,}|hf_[A-Za-z0-9]{30,})\b/,
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/,
/[A-Za-z][A-Za-z0-9+.-]{0,30}:\/\/[^\s/@:?=&]+:[^\s/@?=&]+@/,
/\b(?:(?:proxy-)?authorization\s*[:=]\s*(?:bearer|basic|token|digest)\s+\S+|bearer\s+[A-Za-z0-9._~+/=-]{20,})/i,
/(?:\b|(?<=[a-z0-9_-]))(?:passwords?|passwd|pwd|db[_-]?pass(?:word)?s?|passphrases?|api[_-\s]?keys?|apikeys?|secret[_-\s]?keys?|access[_-\s]?keys?|encryption[_-\s]?keys?|client[_-\s]?secrets?|secrets?|access[_-\s]?tokens?|refresh[_-\s]?tokens?|auth[_-\s]?tokens?|bearer[_-\s]?tokens?|session[_-\s]?tokens?|api[_-\s]?tokens?|private[_-\s]?keys?|priv[_-\s]?keys?|delegate[_-\s]?(?:private[_-\s]*)?keys?)["'`]?\s*[:=]\s*(?:["'`](?!\s*["'`])[^"'`\n]{4,}["'`]|(?!(?:string|number|boolean|null|undefined|none|true|false|required|optional)\b)[^\s'",;}{)\]]{6,})/i,
/\btoken["'`]?\s*[:=]\s*(?:["'`](?!\s*["'`])[^"'`\n]{8,}["'`]|(?!(?:string|number|boolean|null|undefined|none|true|false|required|optional)\b)[^\s'",;}{)\]]{12,})/i,
/(?:private[\s_-]*key|secret[\s_-]*key|delegate[\s_-]*(?:private[\s_-]*)?key|signing[\s_-]*key|api[\s_-]*key|seed)\b[^.\n]{0,32}\b(?:0x)?[0-9a-fA-F]{64}\b/i,
/\b(?:0x)?[0-9a-fA-F]{64}\b[^A-Za-z0-9\n]{0,4}(?:is|was)?[^A-Za-z0-9\n]{0,4}(?:my|the|our)?[^A-Za-z0-9\n]{0,4}(?:delegate[\s_-]*)?(?:private|secret|signing)[\s_-]*key\b/i,
/(?:\b|(?<=[a-z]))(?:(?:seed|recovery|secret|mnemonic)[\s_-]*(?:phrase|words)|mnemonic)[^A-Za-z0-9\n]{0,4}(?:is|are)?[^A-Za-z0-9\n]{0,4}(?:[a-z]{3,8}[ \t]+){11,23}[a-z]{3,8}\b/i,
];

/** Memory trigger patterns — always capture if matched. */
const TRIGGER_PATTERNS = [
/remember|prefer|radši|zapamatuj/i,
Expand Down Expand Up @@ -117,6 +165,25 @@ export function looksLikeInjection(text: string): boolean {
);
}

/** Check if text contains an explicit conversational opt-out directive. */
export function hasCaptureOptOut(text: string): boolean {
if (!text) return false;
if (OPT_OUT_PATTERNS.some((p) => p.test(text))) return true;
const normalized = normalizeForInjectionCheck(text);
return Boolean(normalized && OPT_OUT_PATTERNS.some((p) => p.test(normalized)));
}

/** Alias for `hasCaptureOptOut` kept for callers checking opt-out predicates. */
export function looksLikeOptOut(text: string): boolean {
return hasCaptureOptOut(text);
}

/** Check if text contains high-confidence inline credentials or secret keys. */
export function looksLikeSecret(text: string): boolean {
if (!text) return false;
return SECRET_PATTERNS.some((p) => p.test(text));
}

/** Determine whether a conversation turn is worth capturing. */
export function shouldCapture(text: string): boolean {
if (text.length < MIN_CAPTURE_LENGTH) return false;
Expand All @@ -127,6 +194,8 @@ export function shouldCapture(text: string): boolean {
if (emojiCount > MAX_EMOJI_COUNT) return false;

if (looksLikeInjection(text)) return false;
if (hasCaptureOptOut(text)) return false;
if (looksLikeSecret(text)) return false;
if (TRIGGER_PATTERNS.some((p) => p.test(text))) return true;

return true;
Expand Down
43 changes: 41 additions & 2 deletions packages/openclaw-memory-memwal/src/hooks/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@

import type { MemWal } from "@mysten-incubation/memwal";
import { resolveAgent } from "../config.js";
import { shouldCapture } from "../capture.js";
import { hasCaptureOptOut, shouldCapture } from "../capture.js";
import {
extractMessages,
parseMessageTimestamp,
stripMemoryTags,
withRetry,
withTimeout,
type ExtractedMessage,
Expand Down Expand Up @@ -50,6 +51,44 @@ function rememberBoundedKey<T>(setOrMap: Set<string> | Map<string, T>, key: stri
}
}

function rawMessageText(msg: any): string {
let text = "";
if (typeof msg?.content === "string") {
text = msg.content;
} else if (Array.isArray(msg?.content)) {
for (const block of msg.content) {
if (block?.type === "text" && typeof block.text === "string") {
text += block.text + "\n";
}
}
}
return stripMemoryTags(text).trim();
}

/**
* Drop opted-out user turns and their trailing non-user replies before
* individual message filtering so assistant acknowledgements echoing an
* opted-out topic are never forwarded to `analyze()`.
*/
function extractNonOptedOutMessages(messages: any[], maxCount: number): ExtractedMessage[] {
const windowStart = Math.max(0, messages.length - maxCount);
let optOutActive = false;
const allowed: any[] = [];

for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
if (!msg || typeof msg !== "object") continue;
if (msg.role === "user") {
optOutActive = hasCaptureOptOut(rawMessageText(msg));
}
if (i >= windowStart && !optOutActive) {
allowed.push(msg);
}
}

return extractMessages(allowed, maxCount);
}

/** Register the agent_end hook for auto-capture. */
export function registerCaptureHook(api: any, client: MemWal, config: PluginConfig): void {
const stateByScope = new Map<string, ScopeCaptureState>();
Expand Down Expand Up @@ -115,7 +154,7 @@ export function registerCaptureHook(api: any, client: MemWal, config: PluginConf
// Extract both user and assistant messages — the server LLM on analyze()
// decides what's worth keeping. Assistant messages can contain user
// commitments, decisions, and summaries that are valuable as memories.
const extracted = extractMessages(
const extracted = extractNonOptedOutMessages(
event.messages,
config.captureMaxMessages,
);
Expand Down
Loading
Loading