The root cause of the compression spiral (101 failures, 2 hours):
resolveAnchorTimestamp returned Infinity when no message followed the
compression range. Infinity corrupted JSON serialization (became null),
and null timestamps in JS overlap checks coerced to 0, making every
range appear to overlap the ghost block b7.
Fixes:
- resolveAnchorTimestamp returns endTimestamp + 1 instead of Infinity
- Validate all timestamps are finite before creating a block (fail fast)
- Skip blocks with non-finite timestamps in overlap checks and compression
- Include existing block range in overlap error messages (diagnostic)
- Filter out corrupted blocks on session restore
- Guard synthetic message timestamp creation against non-finite values
- Add regression tests for Infinity anchor and null-timestamp blocks
Backward and forward expansion now skip PI-internal passthrough roles
(compaction, branch_summary, custom_message) when scanning for paired
assistant↔toolResult messages, ensuring atomic removal. Added a
post-compression repair safety net and deep-cloning to prevent content
mutation across context events.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bug 1 (400 API error): applyCompressionBlocks could remove toolResult
messages while leaving their paired assistant(tool_use) message intact.
This produced invalid API sequences that Claude rejected with:
messages.N: tool_use ids found without tool_result blocks immediately after
The fix adds two boundary expansions before the splice:
- Expand lo backward: if messages[lo-1] is an assistant whose toolCall
ids appear as toolResult.toolCallId values inside [lo..hi], pull the
assistant into the range
- Expand hi forward: for assistants inside [lo..hi], extend hi to include
any consecutive toolResult messages that immediately follow hi
This ensures tool_use/tool_result pairs are always removed together.
Bug 2 (autocomplete crash): harden getArgumentCompletions to return an
explicitly typed AutocompleteItem[] | null, filter on value (not label,
matching pi-tui's internal contract), and add a typeof guard ensuring no
item with a non-string value can reach getBestAutocompleteMatchIndex.