feat: smart tail protection during compaction (v3.1)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-31 14:33:13 +10:00
co-authored by Claude Sonnet 4.6
parent a0cd296ec7
commit e9046829b5
2 changed files with 90 additions and 1 deletions
+81 -1
View File
@@ -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 });
+9
View File
@@ -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"
} }
} }
} }