mirror of
https://github.com/wassname/pi-dynamic-context-pruning.git
synced 2026-06-27 16:46:12 +08:00
fix: prevent Infinity anchorTimestamp ghost block spiral
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
This commit is contained in:
+29
-6
@@ -65,17 +65,20 @@ function resolveIdToTimestamp(
|
||||
* Determine the anchor timestamp for a compression block — the timestamp of
|
||||
* the first raw message that appears strictly after `endTimestamp`.
|
||||
*
|
||||
* Returns `Infinity` when the range extends to the very end of the visible
|
||||
* conversation (nothing comes after it).
|
||||
* Returns `endTimestamp + 1` when the range extends to the very end of the
|
||||
* visible conversation (nothing comes after it). We never use Infinity because
|
||||
* it corrupts JSON serialization (becomes null) and breaks numeric comparisons.
|
||||
*/
|
||||
function resolveAnchorTimestamp(endTimestamp: number, state: DcpState): number {
|
||||
let anchor = Infinity
|
||||
let anchor: number | null = null
|
||||
for (const ts of state.messageIdSnapshot.values()) {
|
||||
if (ts > endTimestamp && ts < anchor) {
|
||||
if (ts > endTimestamp && (anchor === null || ts < anchor)) {
|
||||
anchor = ts
|
||||
}
|
||||
}
|
||||
return anchor
|
||||
// Fall back to endTimestamp + 1 instead of Infinity to avoid JSON
|
||||
// serialization corruption (Infinity → null) and comparison breakage.
|
||||
return anchor ?? endTimestamp + 1
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -132,9 +135,27 @@ export function registerCompressTool(
|
||||
)
|
||||
}
|
||||
|
||||
// ── Validate timestamps are finite ──────────────────────────────
|
||||
if (!Number.isFinite(startTimestamp)) {
|
||||
throw new Error(
|
||||
`Start ID "${startId}" resolved to a non-finite timestamp (${startTimestamp}). ` +
|
||||
`This usually means the referenced message has a corrupted timestamp.`,
|
||||
)
|
||||
}
|
||||
if (!Number.isFinite(endTimestamp)) {
|
||||
throw new Error(
|
||||
`End ID "${endId}" resolved to a non-finite timestamp (${endTimestamp}). ` +
|
||||
`This usually means the referenced message has a corrupted timestamp.`,
|
||||
)
|
||||
}
|
||||
|
||||
// ── Overlap check against existing active blocks ─────────────────
|
||||
for (const existing of state.compressionBlocks) {
|
||||
if (!existing.active) continue
|
||||
// Skip blocks with corrupted timestamps
|
||||
if (!Number.isFinite(existing.startTimestamp) || !Number.isFinite(existing.endTimestamp)) {
|
||||
continue
|
||||
}
|
||||
const overlaps =
|
||||
startTimestamp <= existing.endTimestamp &&
|
||||
existing.startTimestamp <= endTimestamp
|
||||
@@ -142,7 +163,9 @@ export function registerCompressTool(
|
||||
throw new Error(
|
||||
`Overlapping compression ranges are not supported. ` +
|
||||
`New range (${startId}..${endId}) overlaps existing block ` +
|
||||
`b${existing.id} "${existing.topic}"`,
|
||||
`b${existing.id} "${existing.topic}" ` +
|
||||
`(b${existing.id} covers ${existing.startTimestamp}..${existing.endTimestamp}, ` +
|
||||
`new range covers ${startTimestamp}..${endTimestamp})`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user