feat(session_compact): add threshold check and auto-save compaction file

Improvements:
1. threshold parameter - skip compaction if context below threshold %
2. Auto-saves compaction file to memory/compactions/ with timestamp
3. Returns file path in response so agent knows where to read

This enables fully autonomous context management without manual steps.
This commit is contained in:
sasheenmusic 2026-01-29 10:50:02 -08:00
parent efd6209bc8
commit 30262f9ffc

View File

@ -1,4 +1,6 @@
import { Type } from "@sinclair/typebox"; import { Type } from "@sinclair/typebox";
import * as fs from "node:fs";
import * as path from "node:path";
import { import {
abortEmbeddedPiRun, abortEmbeddedPiRun,
compactEmbeddedPiSession, compactEmbeddedPiSession,
@ -11,18 +13,12 @@ import {
loadSessionStore, loadSessionStore,
resolveSessionFilePath, resolveSessionFilePath,
resolveStorePath, resolveStorePath,
updateSessionStore,
} from "../../config/sessions.js"; } from "../../config/sessions.js";
import { resolveAgentIdFromSessionKey, DEFAULT_AGENT_ID } from "../../routing/session-key.js"; import { resolveAgentIdFromSessionKey, DEFAULT_AGENT_ID } from "../../routing/session-key.js";
import { formatContextUsageShort, formatTokenCount } from "../../auto-reply/status.js"; import { formatContextUsageShort, formatTokenCount } from "../../auto-reply/status.js";
import { incrementCompactionCount } from "../../auto-reply/reply/session-updates.js"; import { incrementCompactionCount } from "../../auto-reply/reply/session-updates.js";
import type { AnyAgentTool } from "./common.js"; import type { AnyAgentTool } from "./common.js";
import { readStringParam } from "./common.js"; import { resolveInternalSessionKey, resolveMainSessionAlias } from "./sessions-helpers.js";
import {
resolveInternalSessionKey,
resolveMainSessionAlias,
createAgentToAgentPolicy,
} from "./sessions-helpers.js";
import { resolveDefaultModelForAgent } from "../../agents/model-selection.js"; import { resolveDefaultModelForAgent } from "../../agents/model-selection.js";
const SessionCompactToolSchema = Type.Object({ const SessionCompactToolSchema = Type.Object({
@ -32,6 +28,14 @@ const SessionCompactToolSchema = Type.Object({
"Optional instructions for what to focus on during compaction (e.g., 'Focus on decisions and open tasks')", "Optional instructions for what to focus on during compaction (e.g., 'Focus on decisions and open tasks')",
}), }),
), ),
threshold: Type.Optional(
Type.Number({
description:
"Only compact if context usage exceeds this percentage (default: 0, meaning always compact). Set to 60 to skip compaction when context is below 60%.",
minimum: 0,
maximum: 100,
}),
),
}); });
interface SessionCompactToolOpts { interface SessionCompactToolOpts {
@ -41,15 +45,77 @@ interface SessionCompactToolOpts {
thinkLevel?: string; thinkLevel?: string;
} }
function formatTimestamp(): string {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
const hours = String(now.getHours()).padStart(2, "0");
const minutes = String(now.getMinutes()).padStart(2, "0");
const seconds = String(now.getSeconds()).padStart(2, "0");
return `${year}-${month}-${day}-${hours}${minutes}${seconds}`;
}
function writeCompactionFile(params: {
workspaceDir: string;
tokensBefore?: number;
tokensAfter?: number;
contextBefore?: number;
contextAfter?: number;
instructions?: string;
}): string | null {
try {
const compactionsDir = path.join(params.workspaceDir, "memory", "compactions");
if (!fs.existsSync(compactionsDir)) {
fs.mkdirSync(compactionsDir, { recursive: true });
}
const timestamp = formatTimestamp();
const filename = `${timestamp}.md`;
const filepath = path.join(compactionsDir, filename);
const content = `# Context Compaction - ${new Date().toLocaleString("en-US", {
timeZone: "America/Los_Angeles",
dateStyle: "full",
timeStyle: "short",
})}
## Compaction Summary
- **Tokens before:** ${params.tokensBefore ? formatTokenCount(params.tokensBefore) : "unknown"}
- **Tokens after:** ${params.tokensAfter ? formatTokenCount(params.tokensAfter) : "unknown"}
- **Context before:** ${params.contextBefore ? `${params.contextBefore}%` : "unknown"}
- **Context after:** ${params.contextAfter ? `${params.contextAfter}%` : "unknown"}
${params.instructions ? `- **Focus:** ${params.instructions}` : ""}
## Instructions
Read this file after compaction to restore context. Add your working state below.
## Active Task
<!-- What were you working on? -->
## Key Decisions
<!-- Important decisions made this session -->
## Next Steps
<!-- What needs to happen next? -->
`;
fs.writeFileSync(filepath, content, "utf-8");
return filepath;
} catch {
return null;
}
}
export function createSessionCompactTool(opts?: SessionCompactToolOpts): AnyAgentTool { export function createSessionCompactTool(opts?: SessionCompactToolOpts): AnyAgentTool {
return { return {
label: "Session Compact", label: "Session Compact",
name: "session_compact", name: "session_compact",
description: description:
"Compact the current session's context to free up token space. Use when context is above 60% to proactively manage memory. The compaction summarizes older conversation history while preserving recent messages. After compaction, read your latest compaction file from memory/compactions/ to restore state.", "Compact the current session's context to free up token space. Use when context is above 60% to proactively manage memory. The compaction summarizes older conversation history while preserving recent messages. Automatically saves a compaction file to memory/compactions/ and returns the path.",
parameters: SessionCompactToolSchema, parameters: SessionCompactToolSchema,
execute: async (_toolCallId, args) => { execute: async (_toolCallId, args) => {
const params = args as { instructions?: string }; const params = args as { instructions?: string; threshold?: number };
const cfg = opts?.config ?? loadConfig(); const cfg = opts?.config ?? loadConfig();
const { mainKey, alias } = resolveMainSessionAlias(cfg); const { mainKey, alias } = resolveMainSessionAlias(cfg);
@ -77,6 +143,31 @@ export function createSessionCompactTool(opts?: SessionCompactToolOpts): AnyAgen
}; };
} }
// Check threshold - skip if context is below threshold
const threshold = params.threshold ?? 0;
const contextTokens = entry.contextTokens ?? 200_000; // Default to 200k if unknown
const totalTokens = entry.totalTokens ?? (entry.inputTokens ?? 0) + (entry.outputTokens ?? 0);
const currentContextPercent =
contextTokens > 0 ? Math.round((totalTokens / contextTokens) * 100) : 0;
if (threshold > 0 && currentContextPercent < threshold) {
return {
content: [
{
type: "text",
text: `⏭️ Compaction skipped: context at ${currentContextPercent}% is below ${threshold}% threshold.`,
},
],
details: {
ok: true,
compacted: false,
skipped: true,
reason: `context ${currentContextPercent}% < threshold ${threshold}%`,
currentContextPercent,
},
};
}
const sessionId = entry.sessionId; const sessionId = entry.sessionId;
// Abort any active run before compacting // Abort any active run before compacting
@ -131,26 +222,49 @@ export function createSessionCompactTool(opts?: SessionCompactToolOpts): AnyAgen
}); });
} }
// Calculate context percentages for the compaction file
const tokensAfterCompaction = result.result?.tokensAfter; const tokensAfterCompaction = result.result?.tokensAfter;
const totalTokens = const contextAfterPercent =
contextTokens > 0 && tokensAfterCompaction
? Math.round((tokensAfterCompaction / contextTokens) * 100)
: undefined;
// Auto-save compaction file
let compactionFilePath: string | null = null;
if (result.ok && result.compacted && workspaceDir) {
compactionFilePath = writeCompactionFile({
workspaceDir,
tokensBefore: result.result?.tokensBefore,
tokensAfter: result.result?.tokensAfter,
contextBefore: currentContextPercent,
contextAfter: contextAfterPercent,
instructions: params.instructions,
});
}
const newTotalTokens =
tokensAfterCompaction ?? tokensAfterCompaction ??
entry.totalTokens ?? entry.totalTokens ??
(entry.inputTokens ?? 0) + (entry.outputTokens ?? 0); (entry.inputTokens ?? 0) + (entry.outputTokens ?? 0);
const contextSummary = formatContextUsageShort( const contextSummary = formatContextUsageShort(
totalTokens > 0 ? totalTokens : null, newTotalTokens > 0 ? newTotalTokens : null,
entry.contextTokens ?? null, entry.contextTokens ?? null,
); );
const reason = result.reason?.trim(); const reason = result.reason?.trim();
const line = reason const statusLine = reason
? `${compactLabel}: ${reason}${contextSummary}` ? `${compactLabel}: ${reason}${contextSummary}`
: `${compactLabel}${contextSummary}`; : `${compactLabel}${contextSummary}`;
const fileNote = compactionFilePath
? `\n\n📁 Compaction file saved: \`${compactionFilePath}\`\nRead this file to restore your working context.`
: "\n\nNext: Read your latest file from memory/compactions/ to restore context state.";
return { return {
content: [ content: [
{ {
type: "text", type: "text",
text: `🧹 ${line}\n\nNext: Read your latest file from memory/compactions/ to restore context state.`, text: `🧹 ${statusLine}${fileNote}`,
}, },
], ],
details: { details: {
@ -158,6 +272,9 @@ export function createSessionCompactTool(opts?: SessionCompactToolOpts): AnyAgen
compacted: result.compacted, compacted: result.compacted,
tokensBefore: result.result?.tokensBefore, tokensBefore: result.result?.tokensBefore,
tokensAfter: result.result?.tokensAfter, tokensAfter: result.result?.tokensAfter,
contextBefore: currentContextPercent,
contextAfter: contextAfterPercent,
compactionFile: compactionFilePath,
reason: result.reason, reason: result.reason,
}, },
}; };