mirror of
https://github.com/dtzp555-max/memory-continuity.git
synced 2026-07-21 21:15:07 +00:00
feat: smart tail protection during compaction (v3.1)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -233,6 +233,74 @@ function writeSessionLog(workspaceDir, messages, config = {}) {
|
|||||||
fs.appendFileSync(logFile, prefix + entry, "utf8");
|
fs.appendFileSync(logFile, prefix + entry, "utf8");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the last N meaningful user/assistant exchange pairs from messages.
|
||||||
|
* Returns a formatted string for injection into compaction context.
|
||||||
|
*/
|
||||||
|
function extractTailMessages(messages, count = 3) {
|
||||||
|
if (!messages || messages.length === 0) return null;
|
||||||
|
|
||||||
|
// Walk backwards, collect up to `count` user+assistant pairs
|
||||||
|
const pairs = [];
|
||||||
|
let i = messages.length - 1;
|
||||||
|
|
||||||
|
while (i >= 0 && pairs.length < count) {
|
||||||
|
// Find assistant message
|
||||||
|
while (i >= 0 && messages[i]?.role !== "assistant") i--;
|
||||||
|
if (i < 0) break;
|
||||||
|
const assistantMsg = messages[i];
|
||||||
|
i--;
|
||||||
|
|
||||||
|
// Find preceding user message
|
||||||
|
while (i >= 0 && messages[i]?.role !== "user") i--;
|
||||||
|
if (i < 0) break;
|
||||||
|
const userMsg = messages[i];
|
||||||
|
i--;
|
||||||
|
|
||||||
|
const getText = (msg) => {
|
||||||
|
if (typeof msg?.content === "string") return msg.content;
|
||||||
|
if (Array.isArray(msg?.content)) {
|
||||||
|
return msg.content.filter(b => b?.type === "text").map(b => b.text).join("\n");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const userText = getText(userMsg).trim();
|
||||||
|
const assistantText = getText(assistantMsg).trim();
|
||||||
|
|
||||||
|
// Skip trivial exchanges
|
||||||
|
if (userText.length < 10 && assistantText.length < 20) continue;
|
||||||
|
|
||||||
|
// Token-aware truncation per message
|
||||||
|
const maxPerMsg = 150;
|
||||||
|
const truncMsg = (s) => {
|
||||||
|
if (estimateTokens(s) <= maxPerMsg) return s;
|
||||||
|
let lo = 0, hi = s.length;
|
||||||
|
while (lo < hi) {
|
||||||
|
const mid = (lo + hi + 1) >> 1;
|
||||||
|
if (estimateTokens(s.slice(0, mid)) <= maxPerMsg) lo = mid;
|
||||||
|
else hi = mid - 1;
|
||||||
|
}
|
||||||
|
// Avoid splitting surrogate pairs
|
||||||
|
while (lo > 0 && lo < s.length && s.charCodeAt(lo) >= 0xDC00 && s.charCodeAt(lo) <= 0xDFFF) lo--;
|
||||||
|
return s.slice(0, lo) + "...";
|
||||||
|
};
|
||||||
|
|
||||||
|
pairs.unshift({ user: truncMsg(userText), assistant: truncMsg(assistantText) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pairs.length === 0) return null;
|
||||||
|
|
||||||
|
const lines = ["=== RECENT EXCHANGE (protected) ==="];
|
||||||
|
for (const p of pairs) {
|
||||||
|
lines.push(`User: ${p.user}`);
|
||||||
|
lines.push(`Assistant: ${p.assistant}`);
|
||||||
|
lines.push("---");
|
||||||
|
}
|
||||||
|
lines.push("=== END RECENT EXCHANGE ===");
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
function extractStateFromMessages(messages) {
|
function extractStateFromMessages(messages) {
|
||||||
if (!messages || messages.length === 0) return null;
|
if (!messages || messages.length === 0) return null;
|
||||||
|
|
||||||
@@ -349,6 +417,7 @@ const plugin = {
|
|||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
api.on("before_compaction", async (_event, _ctx) => {
|
api.on("before_compaction", async (_event, _ctx) => {
|
||||||
const ws = _ctx?.workspaceDir;
|
const ws = _ctx?.workspaceDir;
|
||||||
|
const config = getConfig();
|
||||||
const statePath = resolveStatePath(ws);
|
const statePath = resolveStatePath(ws);
|
||||||
if (!statePath) return;
|
if (!statePath) return;
|
||||||
|
|
||||||
@@ -360,8 +429,19 @@ const plugin = {
|
|||||||
|
|
||||||
log.info?.("[memory-continuity] Injecting state before compaction");
|
log.info?.("[memory-continuity] Injecting state before compaction");
|
||||||
|
|
||||||
|
// Smart tail protection: also inject recent critical messages
|
||||||
|
const tailCount = config.tailProtectCount ?? 3;
|
||||||
|
const messages = _event?.messages;
|
||||||
|
const tail = tailCount > 0 ? extractTailMessages(messages, tailCount) : null;
|
||||||
|
|
||||||
|
const parts = [snapshot];
|
||||||
|
if (tail) {
|
||||||
|
parts.push(tail);
|
||||||
|
log.info?.("[memory-continuity] Tail protection: injected recent exchanges");
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
prependSystemContext: snapshot,
|
prependSystemContext: parts.join("\n\n"),
|
||||||
};
|
};
|
||||||
}, { priority: 10 });
|
}, { priority: 10 });
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,11 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true,
|
"default": true,
|
||||||
"description": "Write session summaries to memory/sessions/ daily logs"
|
"description": "Write session summaries to memory/sessions/ daily logs"
|
||||||
|
},
|
||||||
|
"tailProtectCount": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 3,
|
||||||
|
"description": "Number of recent critical message pairs to protect during compaction"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -64,6 +69,10 @@
|
|||||||
"sessionLogging": {
|
"sessionLogging": {
|
||||||
"label": "Session logging",
|
"label": "Session logging",
|
||||||
"help": "Append session summaries to daily markdown logs"
|
"help": "Append session summaries to daily markdown logs"
|
||||||
|
},
|
||||||
|
"tailProtectCount": {
|
||||||
|
"label": "Tail protect count",
|
||||||
|
"help": "Keep N recent user/assistant pairs visible after compaction"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user