Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions src/compaction-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,24 @@ export interface CompactionInputs {

/** Should the session trigger a compaction pass? */
export function shouldCompact(inputs: CompactionInputs): boolean {
const minMessages = inputs.minMessages ?? 6;
// Emergency / forced compaction bypasses BOTH the usage check and the
// minMessages guard. Without this short-circuit, a first-turn balloon
// (e.g. a single assistant turn with many tool calls that compound to
// 200k+ input tokens) silently skips compaction because the persisted
// messageCount is still 2 (user + assistant) — well below the default
// minMessages of 6. The same applies after assembleContext() truncates
// history; we need a way to opt back in regardless of message count.
//
// We still require at least 2 real messages to have something to
// summarise — compacting 1 message is a no-op and 0 is a crash.
if (inputs.force || inputs.contextWasTruncated) {
return inputs.realMessageCount >= 2;
}

const minMessages = inputs.minMessages ?? 6;
if (inputs.messageCount < minMessages) return false;
if (inputs.realMessageCount < minMessages) return false;

// Emergency / forced compaction bypasses the usage check.
if (inputs.force || inputs.contextWasTruncated) return true;

const tokenBudget = Math.floor(inputs.modelContextWindow * 0.8);
if (tokenBudget <= 0) return false;

Expand Down
44 changes: 44 additions & 0 deletions test/compaction-policy-unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,50 @@ describe("shouldCompact", () => {
});
expect(result).toBe(true);
});

// Regression: force used to be gated by the minMessages guard, which
// meant first-turn balloons (one assistant turn with many tool calls
// pushing input tokens past 200k while messageCount stayed at 2) silently
// skipped compaction. The force / contextWasTruncated short-circuit must
// bypass minMessages entirely. See memory/learnings/dodo-orchestrator-model-matters.md.
it("force=true bypasses the minMessages guard (2 messages is enough)", () => {
const result = shouldCompact({
messageCount: 2,
realMessageCount: 2,
estimatedTokens: 1,
modelContextWindow: 10_000,
thresholdRatio: 0.5,
force: true,
});
expect(result).toBe(true);
});

it("contextWasTruncated=true bypasses the minMessages guard", () => {
const result = shouldCompact({
messageCount: 3,
realMessageCount: 3,
estimatedTokens: 100,
modelContextWindow: 10_000,
thresholdRatio: 0.5,
contextWasTruncated: true,
});
expect(result).toBe(true);
});

// Edge case: force still needs SOMETHING to compact. Single-message
// sessions (just the user prompt, no assistant turn yet) have no real
// content to summarise.
it("force=true with only 1 real message still returns false (nothing to compact)", () => {
const result = shouldCompact({
messageCount: 1,
realMessageCount: 1,
estimatedTokens: 50_000,
modelContextWindow: 100_000,
thresholdRatio: 0.5,
force: true,
});
expect(result).toBe(false);
});
});

describe("pickCutoff", () => {
Expand Down
Loading