diff --git a/CHANGELOG.md b/CHANGELOG.md
index edb9c220c..2dfd801be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
### Changes
- Cron: accept ISO timestamps for one-shot schedules (UTC) and allow optional delete-after-run; wired into CLI + macOS editor.
- Gateway: add Tailscale binary discovery, custom bind mode, and probe auth retry for password changes. (#740 — thanks @jeffersonwarrior)
+- Agents: add compaction mode config with optional safeguard summarization for long histories. (#700 — thanks @thewilloftheshadow)
## 2026.1.12-4
diff --git a/src/agents/pi-extensions/compaction-safeguard.ts b/src/agents/pi-extensions/compaction-safeguard.ts
index 198fb99ab..4b374c258 100644
--- a/src/agents/pi-extensions/compaction-safeguard.ts
+++ b/src/agents/pi-extensions/compaction-safeguard.ts
@@ -2,13 +2,9 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type {
ExtensionAPI,
ExtensionContext,
+ FileOperations,
} from "@mariozechner/pi-coding-agent";
-import {
- computeFileLists,
- formatFileOperations,
- generateSummary,
- estimateTokens,
-} from "@mariozechner/pi-coding-agent";
+import { estimateTokens, generateSummary } from "@mariozechner/pi-coding-agent";
import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js";
@@ -16,10 +12,40 @@ const MAX_CHUNK_RATIO = 0.4;
const FALLBACK_SUMMARY =
"Summary unavailable due to context limits. Older messages were truncated.";
const TURN_PREFIX_INSTRUCTIONS =
- "This summary covers the prefix of a split turn. Focus on the original request,"
- + " early progress, and any details needed to understand the retained suffix.";
+ "This summary covers the prefix of a split turn. Focus on the original request," +
+ " early progress, and any details needed to understand the retained suffix.";
-function chunkMessages(messages: AgentMessage[], maxTokens: number): AgentMessage[][] {
+function computeFileLists(fileOps: FileOperations): {
+ readFiles: string[];
+ modifiedFiles: string[];
+} {
+ const modified = new Set([...fileOps.edited, ...fileOps.written]);
+ const readFiles = [...fileOps.read].filter((f) => !modified.has(f)).sort();
+ const modifiedFiles = [...modified].sort();
+ return { readFiles, modifiedFiles };
+}
+
+function formatFileOperations(
+ readFiles: string[],
+ modifiedFiles: string[],
+): string {
+ const sections: string[] = [];
+ if (readFiles.length > 0) {
+ sections.push(`\n${readFiles.join("\n")}\n`);
+ }
+ if (modifiedFiles.length > 0) {
+ sections.push(
+ `\n${modifiedFiles.join("\n")}\n`,
+ );
+ }
+ if (sections.length === 0) return "";
+ return `\n\n${sections.join("\n\n")}`;
+}
+
+function chunkMessages(
+ messages: AgentMessage[],
+ maxTokens: number,
+): AgentMessage[][] {
if (messages.length === 0) return [];
const chunks: AgentMessage[][] = [];
@@ -28,10 +54,7 @@ function chunkMessages(messages: AgentMessage[], maxTokens: number): AgentMessag
for (const message of messages) {
const messageTokens = estimateTokens(message);
- if (
- currentChunk.length > 0 &&
- currentTokens + messageTokens > maxTokens
- ) {
+ if (currentChunk.length > 0 && currentTokens + messageTokens > maxTokens) {
chunks.push(currentChunk);
currentChunk = [];
currentTokens = 0;
@@ -144,7 +167,10 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
});
let summary = historySummary;
- if (preparation.isSplitTurn && preparation.turnPrefixMessages.length > 0) {
+ if (
+ preparation.isSplitTurn &&
+ preparation.turnPrefixMessages.length > 0
+ ) {
const prefixSummary = await summarizeChunks({
messages: preparation.turnPrefixMessages,
model,
diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts
index 1fb119124..cacd7fcfb 100644
--- a/src/config/zod-schema.ts
+++ b/src/config/zod-schema.ts
@@ -1210,7 +1210,9 @@ const AgentDefaultsSchema = z
.optional(),
compaction: z
.object({
- mode: z.union([z.literal("default"), z.literal("safeguard")]).optional(),
+ mode: z
+ .union([z.literal("default"), z.literal("safeguard")])
+ .optional(),
reserveTokensFloor: z.number().int().nonnegative().optional(),
memoryFlush: z
.object({