Auto-compact not triggering on resumed sessions with Opus 4.6 (1M context)

Status Closed — not planned
Reported on v2.1.80
Maintainer reply None cached
Activity 12 comments · opened Mar 20, 2026 · closed Jun 29, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Description
Auto-compact never triggered during an extended heavy-usage session on Opus 4.6 (1M context). The conversation was resumed from a prior session that ran out of context — the resume injected a large summary at the top of the conversation. After resuming, auto-compact never triggered again despite hours of intensive work.
Environment
Model: Opus 4.6 (1M context)
Platform: Windows 11
Claude Code version: latest
Expected Behavior
Auto-compact should trigger as context approaches the 1M window limit, compressing prior messages to reduce per-message token cost. The system prompt even states: "The system will automatically compress prior messages in your conversation as it approaches context limits."
Actual Behavior
After resuming a compacted session, auto-compact never triggered. The context grew unchecked, and every message sent the full conversation history (500K+ tokens) back to the API. Combined with 8 agent spawns (~580K tokens total in agent calls alone), the entire usage allocation was consumed in approximately 1 hour.
Impact
Full usage allocation consumed in ~1 hour of work
Normal heavy sessions last 4-8+ hours before hitting limits
The resumed session appears to have blocked the auto-compaction mechanism
Hypothesis
The session resume mechanism may interfere with auto-compact tracking. The compaction system may not correctly track context size or compaction thresholds when a conversation is continued from a prior compacted session via the summary injection pattern.

What Should Happen?

claude code resume should not block auto-compact

Error Messages/Logs

Steps to Reproduce

Start a conversation with heavy tool usage (MCP server development, multiple agent spawns, large file reads/writes)
Session runs out of context and compacts / provides a summary for continuation
Resume the session ("continue from where we left off")
Continue heavy work — building an MCP server with 121 tools, spawning 8+ research agents, dozens of build/deploy/test cycles
Auto-compact never triggers again after the resume
Entire usage allocation drains in ~1 hour due to massive per-message token cost

Claude Model

Opus

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

2.1.80 (Claude Code)

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

PowerShell

Additional Information

_No response_

View original on GitHub ↗

13 Comments

yurukusa · 5 months ago

Auto-compact may not trigger on resumed sessions because the compaction threshold calculation uses the original session's token count, not the resumed context size.

/compact Keep working on the current task.

Run this periodically (~every 30 minutes or when you notice slowdown) on resumed sessions.

/context

If this shows high usage (>80%) but auto-compact hasn't fired, run /compact manually.

COUNTER="/tmp/claude-prompts-${CLAUDE_SESSION_ID:-$$}"
COUNT=$(cat "$COUNTER" 2>/dev/null || echo 0)
COUNT=$((COUNT + 1))
echo "$COUNT" > "$COUNTER"
if [ $((COUNT % 30)) -eq 0 ]; then
  echo "📊 $COUNT prompts in this session. Consider /compact if performance is degrading." >&2
fi
exit 0
kimmeyh · 4 months ago

Happened again today, 4/14/26 8:36am EST, while Claude Code was reporting context of 294,544 tokens (not anywhere close to 1M)
Claude Code version 2.1.204 (per Claude Code, this is the latest version as of this moment )

claude update

Current version: 2.1.104
Checking for updates to latest version...
Claude Code is up to date (2.1.104)

kimmeyh · 4 months ago

Happened again today

<img width="1355" height="182" alt="Image" src="https://github.com/user-attachments/assets/3d6f0ebf-6fd4-440d-b914-f3d012dd9462" />

xxxshipas-commits · 4 months ago

Still broken in 2.1.112 (macOS, Apple M1, VS Code)
Updated to 2.1.112 (released ~30 min ago as of writing) — issue persists.
Environment:

Claude Code: 2.1.112
Platform: macOS, Apple M1 (MacBook)
Interface: VS Code extension (anthropic.claude-code)

What's broken:

Auto-compact does not trigger when context fills up
Manual /compact command also fails with "Prompt is too long" error — so there is no workaround at all
Parallel subagents hang indefinitely

Regression started: ~April 10, 2026 (was working fine before that)
Impact: Cannot run multi-step agentic workflows. Even single long sessions are unusable because manual compact is also broken.
Please prioritize — this makes Claude Code non-functional for any project with large context.

kimmeyh · 4 months ago

Not sure if fixed in 2.1.113, but auto-compact has happened twice since start of 04/18/26. Noting that both time it occurred Tokens was being reported at < 500K. However, prefer auto-compaction early or token count being incorrect than complete failure to do auto-compaction.
Failed to auto-compact today at 513851 tokens, version 2.1.113
<img width="1144" height="156" alt="Image" src="https://github.com/user-attachments/assets/96718329-1cc9-40d5-95a2-a1f98120ed9b" />
Upgraded to 2.1.114 04/20/26

JeremyFriesen · 4 months ago

Adding a data point that differs slightly from the resume-session hypothesis:

The issue also occurs in fresh (non-resumed) sessions. On Windows 10 / Claude Code 2.1.113 / Max (5x) plan, auto-compact consistently fails to fire during heavy development sessions regardless of whether the session was resumed. The session usage page (claude.ai/settings/usage) showed only ~15% of the session allocation consumed when the error occurred — so the failure is not simply "context too large."

Critical UX impact not yet captured in this thread: When the limit is hit and auto-compact has not fired, manual /compact also fails with the same error. On the Max plan, the only way out of this stuck state is:

  1. Enable "extra usage" (paid overage) on the account — this draws from the account balance
  2. Run /compact with extra usage active
  3. Disable extra usage and continue

This means the bug has a direct financial cost: users are forced to spend money from their account balance simply to recover from a compaction failure. There is no free workaround.

This behavior is reproducible across multiple sessions on this machine. Auto-compact works correctly on Linux (same account) and macOS (different account) with identical Claude Code version and configuration.

See also: #51207

zh4ngx · 4 months ago

Same issue confirmed on Opus 4.7 (1M context):

  • Cumulative cached context grew to 868,123 tokens in a single session
  • compact_20260112 configured with trigger.type: "input_tokens", value: 400000
  • Beta header anthropic-beta: compact-2026-01-12 confirmed sent
  • 0 auto-compact events across the session (verified via subtype:compact_boundary count in session JSONL)
  • Manual /compact worked correctly when invoked: preTokens=868123, postTokens=3628, durationMs=1532

Mechanism: usage.input_tokens reports the per-request fresh/uncached portion under prompt caching (typically 1-6 tokens per turn), so cumulative cached context can grow to ~870K while the trigger metric never crosses any reasonable threshold.

Suggested fix: have the trigger evaluate input_tokens + cache_read_input_tokens + cache_creation_input_tokens (cumulative cached context) rather than per-request fresh input_tokens.

Note for others hitting this: client-side workarounds (UserPromptSubmit hooks, sub-agents) cannot trigger /compact programmatically — only the user-typed slash command works. Manual /compact or /exit + restart is the canonical mitigation today.

tovamerika-ux · 4 months ago
have the trigger evaluate input_tokens + cache_read_input_tokens + cache_creation_input_tokens (cumulative cached context) rather than per-request fresh input_tokens.

@zh4ngx Your diagnosis explains exactly what I've been seeing. The usage.input_tokens value reports only the fresh/uncached portion (1–6 tokens per turn under prompt caching), so any client-side trigger built on top of it will silently undercount and never fire.

While we wait for the fix you proposed (input_tokens + cache_read_input_tokens + cache_creation_input_tokens), I've been running a UserPromptSubmit hook that bypasses the API metric entirely and estimates context size from the transcript JSONL byte count instead. In long sessions (300K+ tokens) it has been noticeably more reliable than the built-in indicator, because the JSONL contains the full message history regardless of cache state.

Two key details that took some calibration:

  1. Bytes-per-token ratio: ~7.5 for JSONL. Empirically measured on a session that hit 927K real tokens at 7.0 MB transcript size. JSONL has more overhead than plain markdown — JSON envelope, tool_use blocks, role metadata, timestamps — so the standard 4 bytes/token rule for English prose is wrong here. Important caveat: this session was conducted in Russian. Cyrillic characters take 2 bytes each in UTF-8 (vs 1 byte for ASCII/Latin) and tokenize differently from English. For English-dominated, code-heavy, or mixed-language sessions the right ratio will likely differ. Recalibrate against your own usage by dividing the JSONL byte size by the total token count shown in /context after a long session.
  1. Handling /compact correctly. After a compact, the JSONL keeps growing but the live API context only contains the compact_summary plus messages after it. Counting full file size gives a 3–4× false-high estimate. Solution: scan the JSONL for the last isCompactSummary: true marker and count bytes only from that offset to EOF.

The hook injects a single-line indicator into the model's additionalContext via hookSpecificOutput, with three thresholds: [OK] (<75%), [HIGH] (>75%), [CRITICAL] (>89%). At [CRITICAL] it tells the model to suggest the user wraps up the session manually before auto-compact does so destructively.

The 89% CRITICAL threshold is user-specific and needs calibration for each individual workflow. It is set so that there's enough headroom for a graceful session-close procedure to run before the hard limit is hit. My own session-close protocol (writing state, decisions, and follow-ups to local disk) typically consumes ~20K tokens, occasionally up to 40K when there is a large amount of information to persist — so 89% gives me a comfortable buffer. If your wrap-up procedure is heavier (say, 50K tokens), drop the threshold accordingly so there's room for it to run before auto-compact takes over. If you don't have a structured wrap-up at all, the threshold doesn't matter much and you can set it close to the auto-compact trigger.

A note on the LIMIT_AUTO_COMPACT = 955_000 constant in the script: I once observed the bottom-right indicator showing 955K at the moment auto-compact kicked in, while the real cumulative cached context at that point was ~964K. I set 955K as a conservative value with a small buffer; if longer observations confirm my JSONL-based estimate tracks closer to cumulative cached context, I'll raise it to 964K. So treat this constant as my own empirical anchor, not a documented Anthropic threshold.

Bonus tip: I recommend disabling auto-compact entirely

After hitting this bug a few times I disabled auto-compact for good. In ~/.claude/settings.json:

{
  "env": {
    "DISABLE_AUTO_COMPACT": "true"
  }
}

With auto-compact off, the hard wall sits at ~997K (the actual Messages budget you see in /context, i.e. 1M minus ~3.3% from 1M reserved by Anthropic for system overhead). That's noticeably more usable budget per session than what's available when auto-compact triggers earlier.

In this configuration I bump THRESHOLD_RED to 960K (warning to wrap up) and try to hit /clear manually around 985K rather than letting anything else clean up.

Why prefer manual /clear over auto-compact, even with a healthy threshold:

  • Auto-compact itself consumes ~33K tokens of context just to run (the compaction prompt and its internal scaffolding).
  • After auto-compact, the resulting "compressed summary" injected into the new context window typically takes another 45–55K tokens — most of which is generic recap, not useful working state.
  • Total dead-weight: 78–85K tokens per auto-compact event.
  • For my workflow this overhead is pointless: my structured wrap-up (written to local disk) plus a fresh /clear reload (300–500K of project memory files I deliberately load) gives the model far more relevant context than any auto-compact summary can. Compared to that, the 50K of auto-compact recap is noise.

So the path I've settled on:

  1. DISABLE_AUTO_COMPACT=true.
  2. Watch the JSONL-based hook indicator.
  3. At [HIGH] (75%) start finishing what I'm on; at [CRITICAL] (~960K with this setup) trigger structured wrap-up; manually /clear around 985K.
  4. New session reads memory files from disk — model gets exactly the context I want, nothing implicit.

This isn't going to fit everyone — if your workflow doesn't have a structured wrap-up phase, auto-compact at least gives you something rather than a hard crash. But for anyone investing in their own session-close protocol, disabling auto-compact and steering manually wins on both context quality and effective token budget.

Full hook script

Python, stdlib only:

#!/usr/bin/env python
"""
context-monitor.py — UserPromptSubmit hook.
Estimates context size from the .jsonl transcript and injects an indicator
into the model context via hookSpecificOutput.additionalContext.
"""
from __future__ import annotations
import json
import os
import sys
import time
from pathlib import Path

CACHE_DIR = Path.home() / '.claude' / 'cache'
CACHE_TTL = 5            # seconds

# Bytes-per-token calibration. The value below was measured on a
# Russian-language session (Cyrillic = 2 bytes per char in UTF-8, different
# tokenization from Latin). For English / code-heavy / mixed sessions the
# right ratio will likely differ — recalibrate by dividing your transcript
# .jsonl byte size by the total token count from /context after a long session.
BYTES_PER_TOKEN = 7.5    # calibrated on a 927K-token Russian-language session
LIMIT_TOTAL = 1_000_000

# Empirical anchor, not a documented Anthropic threshold:
# I once saw the bottom-right indicator show 955K at the moment auto-compact
# kicked in, while the real cumulative cached context was ~964K. 955K is a
# conservative value with a small buffer. May raise to 964K after more
# observations.
LIMIT_AUTO_COMPACT = 955_000

# THRESHOLD_RED is user-specific: leave enough headroom for your own
# session-close procedure to run before the hard limit is hit.
# My wrap-up protocol costs ~20K tokens (occasionally up to 40K) for
# persisting session state to disk, so 890K (89%) gives a comfortable buffer.
# If you also set DISABLE_AUTO_COMPACT=true, the hard wall moves to ~997K
# (the Messages budget shown by /context), so THRESHOLD_RED can be raised
# to ~960K. Tune this value for your own workflow.
THRESHOLD_RED = 890_000      # > 89% — critical
THRESHOLD_YELLOW = 750_000   # > 75% — warning


def find_last_compact_offset(transcript_path: str) -> int:
    """Returns byte-offset of the last compact marker
    (a line with isCompactSummary=true). 0 if none found."""
    last_offset = 0
    try:
        with open(transcript_path, 'rb') as f:
            offset = 0
            for line in f:
                if b'isCompactSummary' in line:
                    try:
                        obj = json.loads(line.decode('utf-8'))
                        if obj.get('isCompactSummary') is True:
                            last_offset = offset
                    except Exception:
                        pass
                offset += len(line)
    except Exception:
        return 0
    return last_offset


def emit(text: str) -> None:
    out = {
        "hookSpecificOutput": {
            "hookEventName": "UserPromptSubmit",
            "additionalContext": text,
        }
    }
    sys.stdout.write(json.dumps(out, ensure_ascii=False))
    sys.exit(0)


def main() -> None:
    try:
        payload = json.loads(sys.stdin.read())
    except Exception:
        sys.exit(0)

    transcript_path = payload.get('transcript_path') or ''
    session_id = payload.get('session_id') or 'unknown'

    if not transcript_path or not os.path.isfile(transcript_path):
        sys.exit(0)

    CACHE_DIR.mkdir(parents=True, exist_ok=True)
    cache_file = CACHE_DIR / f'context-monitor-{session_id}.json'

    transcript_mtime = os.path.getmtime(transcript_path)
    transcript_size = os.path.getsize(transcript_path)

    if cache_file.exists():
        try:
            cached = json.loads(cache_file.read_text(encoding='utf-8'))
            if (cached.get('transcript_mtime') == transcript_mtime
                    and cached.get('transcript_size') == transcript_size
                    and time.time() - cached.get('cached_at', 0) < CACHE_TTL):
                emit(cached['text'])
        except Exception:
            pass

    compact_offset = find_last_compact_offset(transcript_path)
    effective_size = transcript_size - compact_offset
    tokens = int(effective_size / BYTES_PER_TOKEN)
    percent = int(tokens * 100 / LIMIT_TOTAL)

    if tokens > THRESHOLD_RED:
        status = '[CRITICAL]'
        suggestion = (
            f'Context approaching auto-compact ({LIMIT_AUTO_COMPACT // 1000}K). '
            f'Suggest the user wraps up the session manually now to preserve state '
            f'before auto-compact runs (which will discard fine-grained details).'
        )
    elif tokens > THRESHOLD_YELLOW:
        status = '[HIGH]'
        suggestion = 'Context >75% full. Consider winding down the current task.'
    else:
        status = '[OK]'
        suggestion = ''

    compact_note = (
        f' (post-compact tail: {effective_size // 1024} KB of {transcript_size // 1024} KB total jsonl)'
        if compact_offset > 0 else ''
    )
    text = (
        f'Context size (estimate from transcript .jsonl, {BYTES_PER_TOKEN} bytes/token): '
        f'~{tokens // 1000}K of {LIMIT_TOTAL // 1000}K tokens ({percent}%){compact_note}. '
        f'Status {status}. Auto-compact at {LIMIT_AUTO_COMPACT // 1000}K. '
        + suggestion
    )

    try:
        cache_file.write_text(
            json.dumps({
                'transcript_mtime': transcript_mtime,
                'transcript_size': transcript_size,
                'cached_at': time.time(),
                'text': text,
            }, ensure_ascii=False),
            encoding='utf-8',
        )
    except Exception:
        pass

    emit(text)


if __name__ == '__main__':
    main()

Install via ~/.claude/settings.json

{
  "env": {
    "DISABLE_AUTO_COMPACT": "true"
  },
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          { "type": "command", "command": "python ~/.claude/hooks/context-monitor.py" }
        ]
      }
    ]
  }
}

(The DISABLE_AUTO_COMPACT env var is optional — see the bonus tip section above.)

Caveats

  • The 7.5 bytes/token ratio was calibrated on a Russian-language session. Cyrillic UTF-8 bytes and Russian tokenization differ significantly from English/code, so for other language mixes the right value will be different. To recalibrate: divide your .jsonl byte size by the total token count shown by /context after a long session, and update BYTES_PER_TOKEN.
  • Depends on the transcript JSONL format being stable across Claude Code versions. The isCompactSummary field name in particular is what I use to detect compact boundaries — if Anthropic renames it, the post-compact handling silently breaks (the script just falls back to counting full file size).
  • This is purely client-side and doesn't fix the underlying trigger logic — the proper fix is still what you proposed (have auto-compact evaluate cumulative cached context).

Hope this is useful as a stopgap until the official fix lands. Happy to iterate on the calibration if anyone runs it on different model/language mixes.

Also available as a gist: https://gist.github.com/tovamerika-ux/f1c539ea5baa255b6c090b36984aea51

tovamerika-ux · 4 months ago
The session usage page (claude.ai/settings/usage) showed only ~15% of the session allocation consumed when the error occurred — so the failure is not simply "context too large."

@JeremyFriesen Yes, I just hit a related case worth adding here as another data point.

I ran a stress test in a fresh, empty session — Opus 4.7 1M, Medium effort, with DISABLE_AUTO_COMPACT=true set — and asked the model to read every file in the project non-stop. The session crashed with Prompt is too long while the GUI dashboard's Context window header still showed 761.2k / 1.0M (76%).

But look at the breakdown the same dashboard shows side-by-side:
<img width="876" height="531" alt="Image" src="https://github.com/user-attachments/assets/a0ead313-9cce-488f-80a6-1dd70006c456" />

Context window     761.2k / 1.0M (76%)
  Messages         997.1k        99.7%   ← !!
  System tools      14.6k         1.5%
  Compact buffer     3.0k         0.3%
  Skills             2.1k         0.2%
  MCP tools          1.2k         0.1%
  System tools (deferred)  20.2k  2.0%
  MCP tools (deferred)     18.0k  1.8%
  Free space             0        0.0%

The header says 76% used, but the Messages line shows 99.7% (≈997K) and Free space = 0. Those two numbers describe the same session and they disagree by 236K. Adding the breakdown lines together gives 1,056K — already over the 1M model limit on paper.

So to your point about the GUI being unreliable:
the same dashboard can both under-report (the header / input_tokens-based metric) and accurately report (the Messages line + Free space = 0) at the same moment, and the header is the one most people look at.

I think this is a direct manifestation of @zh4ngx's diagnosis above. Under prompt caching, the dashboard's headline number tracks usage.input_tokens (the fresh/uncached portion, 1–6 tokens per turn), so it crawls slowly and "looks fine" — while the real cumulative cached context, which is what the API actually checks against the model limit, is already at the wall. The breakdown's Messages 99.7% and Free space 0 lines tell the truth, but they're easy to miss next to the big "76%" at the top.

In my case the failure mode is different from yours (you saw failures while the session-allocation page reported only 15% consumed; I saw the in-session header at 76% while the actual context was full), but the underlying problem looks identical: Claude Code's main visible token metric is decoupled from the metric the API uses to enforce the limit, and the gap is systematic, not random.

Would be useful if Anthropic could make the dashboard header use the same metric zh4ngx proposed for the auto-compact trigger (input_tokens + cache_read_input_tokens + cache_creation_input_tokens). That would unify what the user sees with what the API enforces — and the reading-files stress test I ran would have surfaced as "97% used" instead of the misleading "76%".

For the moment, I've found that the most reliable way to know how full a session really is, is to look at /context and watch the Messages percentage and Free space line — not the headline number. Or, even simpler, run a hook that estimates context size directly from the transcript JSONL byte count, which sidesteps the API metric entirely (I posted one earlier in this thread).

zh4ngx · 4 months ago

@tovamerika-ux — great hook, thanks for building this. Replying with the English/code recalibration data you asked for.

My primary session jsonl (Opus 4.7, long coding session, pre-compact): 30,376,135 bytes. Divide by /context total tokens for exact BPT — I will compute and post the ratio once I have the token count from a long session. Rough expectation: English/code should land around 3-4 bytes/token (compared to your 7.5 for Cyrillic Russian), since English/code UTF-8 is mostly 1-byte ASCII with occasional 2-4 byte sequences for emoji/special chars.

For anyone calibrating their own: the simplest method is to paste /context into a file, grab the total token count, then wc -c <session.jsonl> and divide. No need to guess.

Two minor additions based on our experimentation:

  1. DISABLE_AUTO_COMPACT=true is essential — the auto-compact trigger is irreparably decoupled from real context fullness. Manual /clear at ~985K gives you control over when compression happens.
  2. The hook's find_last_compact_offset is a good idea, but note that /clear (manual compact) and auto-compact write different markers. Watching for isCompactSummary: true covers both, which is correct.
junaidtitan · 3 months ago

The root issue here is that once auto-compact stops tracking after a resume, nothing is bounding the JSONL, so you pay full freight (500K+ tokens) on every turn. Until the resume/compaction-tracking bug is fixed, you can put a hard ceiling on session size yourself with cozempic (uvx cozempic, or pip install cozempic). It prunes the session JSONL directly — stale file reads, aged tool outputs, redundant metadata — which restores a big chunk of effective context, and its guard daemon re-prunes at configurable thresholds even when Claude Code's own auto-compact never fires. On heavy resumed sessions that's typically where the per-turn token cost is coming from. Repo: github.com/Ruya-AI/cozempic.

jasoncbraatz · 3 months ago

Love the manual workarounds especially by @tovamerika-ux - but I must admit I don't remember hitting this until recently, though now that I read about the dead payload, it makes perfect sense to have a hand-off prompt and do a fresh session. Could anyone suggest a way to automatically generate a handoff prompt to be used as a preamble for a new session? It saves tokens and yet (in a meaningful way) does a better job than auto-compact anyway since the HITL gets to modify the information before blindly pasting in the hand-off .. some things aren't worth moving into a new conversation with, which has always been a problem with auto-compact (and no doubt with every LLM on the planet, this isn't just Claude's problem, it's a gotcha on how MLPs work at this stage in the game).

github-actions[bot] · 2 months ago

Closing for now — inactive for too long. Please open a new issue if this is still relevant.