feat(compaction): add dynamic context pruning as recovery stage

Implements DCP-style pruning strategies inspired by opencode-dynamic-context-pruning plugin:

- Deduplication: removes duplicate tool calls (same tool + args)
- Supersede writes: prunes write inputs when file subsequently read
- Purge errors: removes old error tool inputs after N turns

Integration:
- Added as Stage 2.5 in compaction pipeline (after truncation, before summarize)
- Configurable via experimental.dynamic_context_pruning
- Opt-in by default (experimental feature)
- Protected tools list prevents pruning critical tools

Configuration:
- Turn protection (default: 3 turns)
- Per-strategy enable/disable
- Aggressive/conservative modes for supersede writes
- Configurable error purge threshold (default: 5 turns)
- Toast notifications (off/minimal/detailed)

Testing:
- Added unit tests for deduplication signature creation
- Type check passes
- Schema regenerated

Closes #271
This commit is contained in:
sisyphus-dev-ai
2025-12-27 09:20:42 +00:00
parent 07d3a3bf8b
commit 85a65bf4e5
11 changed files with 1042 additions and 0 deletions
@@ -0,0 +1,33 @@
import { describe, test, expect } from "bun:test"
import { createToolSignature } from "./pruning-deduplication"
describe("createToolSignature", () => {
test("creates consistent signature for same input", () => {
const input1 = { filePath: "/foo/bar.ts", content: "hello" }
const input2 = { content: "hello", filePath: "/foo/bar.ts" }
const sig1 = createToolSignature("read", input1)
const sig2 = createToolSignature("read", input2)
expect(sig1).toBe(sig2)
})
test("creates different signature for different input", () => {
const input1 = { filePath: "/foo/bar.ts" }
const input2 = { filePath: "/foo/baz.ts" }
const sig1 = createToolSignature("read", input1)
const sig2 = createToolSignature("read", input2)
expect(sig1).not.toBe(sig2)
})
test("includes tool name in signature", () => {
const input = { filePath: "/foo/bar.ts" }
const sig1 = createToolSignature("read", input)
const sig2 = createToolSignature("write", input)
expect(sig1).not.toBe(sig2)
})
})