diff --git a/CHANGELOG.md b/CHANGELOG.md index f869201..92dfd1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [1.0.7] - 2026-04-14 + +### Fixed + +- **Infinity anchorTimestamp ghost block spiral** — When a `compress` range extended to the end of the conversation, `resolveAnchorTimestamp` returned `Infinity`. `JSON.stringify(Infinity)` serialises to `null`, so on session restore the corrupted block's timestamps coerced to `0` in JS overlap checks, making every new range appear to overlap the ghost block and trapping the model in a compression spiral (101 failures over 2 hours). `resolveAnchorTimestamp` now returns `endTimestamp + 1` instead of `Infinity`. +- **Corrupted block propagation on session restore** — `index.ts` now filters out any persisted compression block whose `startTimestamp`, `endTimestamp`, or `anchorTimestamp` is non-finite before restoring state, preventing ghost blocks from surviving across sessions. +- **Non-finite timestamp guard** — All code paths that create or apply compression blocks now validate timestamps are finite before proceeding, failing fast rather than silently corrupting state. +- **Overlap error diagnostics** — Overlap error messages now include the existing block's timestamp range to aid debugging. +- **Prompt tag name mismatch** — The prompt tag was named `` but the code injected ``; tag name corrected to `` throughout `prompts.ts`. +- **Duplicate test** — Removed a duplicate test case from `pruner.test.ts`. + +### Added + +- **Regression tests** — New test cases for the `Infinity` anchor scenario, `null`-timestamp corrupted blocks, and corrupted-block resilience on session restore. + +Thanks to [@wassname](https://github.com/wassname) for diagnosing and fixing the compression spiral root cause in [#3](https://github.com/complexthings/pi-dynamic-context-pruning/pull/3). + ## [1.0.6] - 2026-04-09 ### Fixed diff --git a/README.md b/README.md index 83d147f..40769fd 100644 --- a/README.md +++ b/README.md @@ -148,3 +148,10 @@ npx tsc --noEmit # type-check without emitting ``` The extension is loaded by pi via [jiti](https://github.com/unjs/jiti) so TypeScript is executed directly — no build step required for normal use. + +## Contributors + +[![complexthings](https://github.com/complexthings.png?size=50)](https://github.com/complexthings) [@complexthings](https://github.com/complexthings) +[![wassname](https://github.com/wassname.png?size=50)](https://github.com/wassname) [@wassname](https://github.com/wassname) + +Full contributor list: https://github.com/complexthings/pi-dynamic-context-pruning/graphs/contributors diff --git a/index.ts b/index.ts index 440fd12..5da0f80 100644 --- a/index.ts +++ b/index.ts @@ -81,17 +81,30 @@ export default function (pi: ExtensionAPI) { const data = entry.data as any if (data?.compressionBlocks) { - // Filter out blocks with corrupted (null/NaN/Infinity) timestamps — - // these were caused by Infinity anchorTimestamp values that became - // null after JSON round-trip. - const validBlocks = data.compressionBlocks.filter( - (b: any) => - Number.isFinite(b.startTimestamp) && - Number.isFinite(b.endTimestamp) && - Number.isFinite(b.anchorTimestamp), - ) + // Filter out blocks with corrupted timestamps, then repair + // anchorTimestamp which is legitimately Infinity for blocks that + // extend to end-of-conversation (JSON round-trips Infinity as null). + const validBlocks = data.compressionBlocks + .filter( + (b: any) => + Number.isFinite(b.startTimestamp) && + Number.isFinite(b.endTimestamp), + ) + .map((b: any) => ({ + ...b, + // anchorTimestamp is Infinity when the block extends to the end + // of the conversation; JSON round-trips Infinity as null, so + // repair it here rather than discarding the block. + anchorTimestamp: Number.isFinite(b.anchorTimestamp) + ? b.anchorTimestamp + : Infinity, + })) state.compressionBlocks = validBlocks - state.nextBlockId = data.nextBlockId ?? state.compressionBlocks.length + state.nextBlockId = + data.nextBlockId ?? + (state.compressionBlocks.length > 0 + ? Math.max(0, ...state.compressionBlocks.map((b: any) => b.id)) + 1 + : 1) state.tokensSaved = data.tokensSaved ?? 0 state.totalPruneCount = data.totalPruneCount ?? 0 } diff --git a/package.json b/package.json index 3aff46f..5a72be6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@complexthings/pi-dynamic-context-pruning", - "version": "1.0.6", + "version": "1.0.7", "description": "PI coding agent extension — Dynamic Context Pruning (DCP)", "keywords": [ "pi-package",