mirror of
https://github.com/dtzp555-max/memory-continuity.git
synced 2026-07-21 21:15:07 +00:00
fix: prevent trivial conversations from overwriting meaningful state
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 <noreply@anthropic.com>
This commit is contained in:
@@ -238,23 +238,48 @@ const plugin = {
|
|||||||
const statePath = resolveStatePath(ws);
|
const statePath = resolveStatePath(ws);
|
||||||
if (!statePath) return;
|
if (!statePath) return;
|
||||||
|
|
||||||
// Always extract from the latest conversation — the whole point is to
|
const messages = event?.messages;
|
||||||
// capture what happened *this* session so the next session can recover.
|
if (!messages || messages.length === 0) {
|
||||||
const newState = extractStateFromMessages(event?.messages);
|
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) {
|
if (!newState) {
|
||||||
log.info?.("[memory-continuity] No extractable state from conversation");
|
log.info?.("[memory-continuity] No extractable state from conversation");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Archive previous state if it exists
|
// Archive previous state if it exists
|
||||||
const existing = readFile(statePath);
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
const archivePath = statePath.replace(/CURRENT_STATE\.md$/, `STATE_ARCHIVE_${Date.now()}.md`);
|
const archivePath = statePath.replace(/CURRENT_STATE\.md$/, `STATE_ARCHIVE_${Date.now()}.md`);
|
||||||
writeFile(archivePath, existing);
|
writeFile(archivePath, existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFile(statePath, newState);
|
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
|
}, { priority: 90 }); // low priority, run after other hooks
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user