From 24221ea7493bf038a159649363cf4adb98026b31 Mon Sep 17 00:00:00 2001 From: dtzp555 Date: Mon, 16 Mar 2026 17:25:04 +1000 Subject: [PATCH] fix: prevent trivial conversations from overwriting meaningful state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Short conversations (< 2 real user messages) no longer overwrite existing meaningful state. This prevents the "recovery death spiral" where: 1. Secret told → state saved ✅ 2. /new → state injected → model ignores it → "I don't remember" 3. agent_end overwrites good state with "I don't remember" ❌ 4. All subsequent recoveries fail permanently Now step 3 is blocked: a 1-message "what was my secret?" conversation won't overwrite the previously saved secret. Also strips channel metadata and /new-style short commands from the real message count. Co-Authored-By: Claude Opus 4.6 --- index.js | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 8d53c6a..e08a2cb 100644 --- a/index.js +++ b/index.js @@ -238,23 +238,48 @@ const plugin = { const statePath = resolveStatePath(ws); if (!statePath) return; - // Always extract from the latest conversation — the whole point is to - // capture what happened *this* session so the next session can recover. - const newState = extractStateFromMessages(event?.messages); + const messages = event?.messages; + if (!messages || messages.length === 0) { + log.info?.("[memory-continuity] No messages in session, skipping"); + return; + } + + // Count real user messages (exclude system, metadata-only, short commands) + const realUserMsgs = messages.filter(m => { + if (m?.role !== "user") return false; + const text = typeof m?.content === "string" ? m.content + : Array.isArray(m?.content) ? m.content.filter(b => b?.type === "text").map(b => b.text).join("\n") + : ""; + const cleaned = text + .replace(/^Conversation info \(untrusted metadata\):[\s\S]*?\n\n/m, "") + .replace(/^Sender \(untrusted metadata\):[\s\S]*?\n\n/m, "") + .trim(); + // Skip very short messages like "/new", "/status", single-word queries + return cleaned.length > 10; + }); + + // Don't overwrite meaningful state with trivial conversations + // (e.g., "/new" then "what was my secret?" — only 1 real message) + const existing = readFile(statePath); + if (existing && buildSnapshot(existing) && realUserMsgs.length < 2) { + log.info?.("[memory-continuity] Conversation too short to overwrite existing state (" + realUserMsgs.length + " real msgs)"); + return; + } + + const newState = extractStateFromMessages(messages); if (!newState) { log.info?.("[memory-continuity] No extractable state from conversation"); return; } // Archive previous state if it exists - const existing = readFile(statePath); if (existing) { const archivePath = statePath.replace(/CURRENT_STATE\.md$/, `STATE_ARCHIVE_${Date.now()}.md`); writeFile(archivePath, existing); } writeFile(statePath, newState); - log.info?.("[memory-continuity] Updated state from conversation"); + log.info?.("[memory-continuity] Updated state from conversation (" + realUserMsgs.length + " real msgs)"); }, { priority: 90 }); // low priority, run after other hooks // ------------------------------------------------------------------