Skip to content

fix: double compaction persists after #9282 — cache-ttl entry still bypasses prepareCompaction() guard #28491

@neomatrixyzy

Description

@neomatrixyzy

Summary

Double compaction still occurs on builds containing the #9282 fix (PR #13514). The fix moved appendCacheTtlTimestamp() from before prompt to after prompt+compaction, but this only changed timing, not the fundamental issue: the cache-ttl custom entry still inserts between the compaction entry and the next guard check, allowing prepareCompaction() to trigger a second compaction.

Environment

Reproduction

Observed on a live session (d2160dd5) with 6 compaction entries = 3 double pairs. Pattern is 100% consistent:

Evidence: 3 double compaction pairs (all on 2026-02-27, post-fix build)

Pair 1:
  L217: COMPACTION  05:03:22.667  tokensBefore=180,013  summary=7,810 chars
  L218: CACHE-TTL   05:03:22.668  (+1ms, type: "custom")
  L219: COMPACTION  05:05:32.714  tokensBefore=1,953   summary=7,935 chars (+125)

Pair 2:
  L544: COMPACTION  06:58:44.362  tokensBefore=180,650  summary=19,060 chars
  L545: CACHE-TTL   06:58:44.392  (+30ms, type: "custom")
  L546: COMPACTION  06:58:44.502  tokensBefore=4,765   summary=19,185 chars (+125)

Pair 3:
  L661: COMPACTION  07:40:15.449  tokensBefore=181,521  summary=15,190 chars
  L662: CACHE-TTL   07:40:15.490  (+41ms, type: "custom")
  L663: COMPACTION  07:42:22.825  tokensBefore=3,798   summary=15,315 chars (+125)

Key observations

  1. All second compactions fire on tiny contexts (1,953 / 4,765 / 3,798 tokens) — completely unnecessary. First compactions are legitimate (~180K tokens near context limit).

  2. The +125 chars delta is identical every time — it's <workspace-critical-rules> from compaction-safeguard.ts L349-351 being appended a second time:

    \n\n<workspace-critical-rules>\n## Session Startup\n...\n## Red Lines\n...\n</workspace-critical-rules>
    
  3. Pair 2 completed in 140ms — the LLM "summarized" 4,765 tokens of mostly-existing-summary in a trivial pass.

  4. Cache-TTL entry is always the sole entry between compaction pairs — the type: "custom" entry breaks the guard every time.

Root cause analysis

The #9282 fix (PR #13514) moved appendCacheTtlTimestamp() from before the prompt to after prompt+compaction completes. But the guard logic in prepareCompaction() checks whether the last session entry has type === "compaction":

Before fix:  cache-ttl → prompt → compaction → [next cycle: last=compaction ✅, but cache-ttl was BEFORE]
After fix:   prompt → compaction → cache-ttl → [next cycle: last=custom ❌ → double compaction]

The fix changed when cache-ttl is inserted but didn't change the fundamental issue: any type: "custom" entry between two compaction checks allows prepareCompaction() to fire again.

Impact

Impact Severity
Wasted LLM API call per compaction (~2 min, real tokens billed) Medium
compactionCount inflated 2x (6 recorded = 3 real) High — affects memoryFlushCompactionCount guard
workspace-critical-rules duplicated in summary Low
Compaction on <5K tokens may degrade summary quality Medium

Suggested fix directions

Option A (minimal): In the guard check, look at the last N entries (e.g., 2-3) instead of only the last entry. If any recent entry is type: "compaction", skip re-compaction.

Option B (structural): Don't emit cache-ttl as a standalone session entry. Instead, embed the timestamp in the compaction entry's metadata or in the next assistant message's metadata.

Option C (safety net): Add a tokensBefore floor check — if tokensBefore < threshold (e.g., 10% of context window), skip compaction entirely since there's nothing meaningful to summarize.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions