[BUG] 2.1.286 idle compaction silently discards working context in long-running sessions; no opt-out, logged as "manual"
Summary
Since 2.1.286, idle sessions are compacted automatically before the prompt cache expires ("Compacted while idle, before the prompt cache expired"). There is no opt-out and no warning. For long-running working sessions this discards the grounding the session was built on. I'd like to suggest that compaction stay user-initiated, or at least opt-in or announced in advance, except in the genuine emergency of hitting the context limit.
Environment
- Claude Code 2.1.286, native install, macOS
- Not seen on 2.1.285 or earlier
What happened
I keep roughly 20 Claude Code sessions open in parallel on long-running projects. Each one accumulates detailed working context over hours: the exact instructions I've given, decisions and their reasons, the methods we settled on, and in-flight state.
Overnight after updating to 2.1.286, 7 of those sessions were compacted while idle. None was close to the context limit. When I came back, each had a summary in place of its working context: my exact instructions, the reasoning behind decisions, and the methods we'd agreed on were gone from what the session actually sees. I had to rebuild the grounding by hand from the transcripts on disk.
I understand the cost logic (compacting before the cache expires avoids paying to reprocess a long uncached context on the next turn), and I can see it was requested in #66115. But for this way of working, the trade is backwards: the context is the valuable part, and a cache re-read is a cost I'd gladly pay to keep it.
Expected behavior
- Compaction should never happen silently while the user is away, unless the session is at the hard context limit and has no other option.
- If the cost-saving behavior stays, it should be opt-in, or at least announced (for example "this session will compact in N minutes unless you interact") with a one-keystroke way to decline.
Additional issues found
- Mislabelled trigger. These idle compactions are recorded in the transcript as
compactMetadata.trigger: "manual", although I never ran/compact. That makes them indistinguishable from user-initiated compactions, which makes the behavior hard to diagnose. - No documented hook. I couldn't find documentation confirming that
PreCompactfires for idle compaction, which would let users save state first.
Suggested fixes
- A setting to disable idle compaction, for example
"idleCompaction": falsein settings.json, or an env var. - A distinct trigger value (for example
"idle") incompactMetadata. - Documented
PreCompactandSessionStart(compact) hook support for idle compaction, so state can be saved before it runs and restored after.
Related: #66115 (the feature request), #71254, #91836.
Thanks. The full transcripts remained on disk, which made recovery possible, and I appreciate that.
23 Comments
+1, I'm seeing the same behaviour on 2.1.287 (macOS). I left a long-running working session idle over lunch and came back to "Compacted while idle, before the prompt cache expired". I never ran
/compactand was nowhere near my compaction threshold, yet the transcript records it astrigger: "manual", exactly as described above.I can understand why some users would want this to save on cache re-reads. But discarding a session's working context is a significant, invasive action, and it shouldn't happen silently while the user is away. Please make it opt-in, or at the very least provide a setting to disable it.
I had the same experience on Windows 11 with Claude 2.1.287. Having context history disappear may feel to some like a violation of an unwritten contract, especially if it's not clear how to recover it (Claude was able to recover my session by rebuilding the transcript, but it had to write a python script and the auto-mode classifier rightly blocked it for attempting to rewrite history).
Appreciate the feedback, setting is coming. Currently it's doing this only for larger contexts (200k+). Curious if there's a limit where you think this may be useful.
Also we need to get to the place where a compaction is less disruptive.
@blois Thanks, glad to hear a setting is coming.
Suggest making this user-configurable, like the existing
autoCompactWindowsetting. There's likely no "one-size-fits-all" value that everyone would agree on.The hook question has a direct answer, tested on 2.1.287: idle compaction does fire PreCompact, and it fires as
trigger: "manual". PostCompact gets"manual"too. That is the same missing value behind your finding 1, not a second bug.Mechanism: when the idle timer decides to compact, it calls the same routine a
/compactuses and passes no trigger, and that routine defaults the trigger tomanualbefore it builds the transcript boundary and the hook input. Confirmed in a local 2.1.287 run of that routine: PreCompact arrived as{"trigger":"manual","custom_instructions":null}, SessionStart fired with"source":"compact", PostCompact arrived with"trigger":"manual"and the full summary, and the boundary landed with"compactMetadata":{"trigger":"manual","preTokens":21280,...}. So a PreCompact hook written today will see idle compactions. It cannot tell them apart from your own/compact.The hooks reference defines the PreCompact matchers as
manual=/compactandauto= auto-compact window. Idle compaction matches neither definition and usesmanual. So ask 3 is smaller than it looks: the hooks do fire, and the transcript boundary, the hook payloads, and the metadata are all wrong in the same direction.On when it fires, from the 2.1.287 binary: the idle timer only arms when the session's prompt cache TTL is 1 hour, and it fires at 90 percent of the way from your last activity to the cache expiry, so roughly 54 minutes of quiet. It considers contexts of 200k tokens or more (the server can lower that, floor is 100k). It skips when the fire is more than 60 seconds late, when the cache went cold, when a newer request landed, when another process is holding the session, or when rate-limit state is not allowed. It ships behind a server-side gate with off, log and compact modes, which is why there is no release note and no setting yet.
Save and restore, run end to end on 2.1.287. In
.claude/settings.json:.claude/hooks/save-working-context.sh(needsjq):.claude/hooks/restore-working-context.sh:What I measured: PreCompact wrote the snapshot, the compaction ran, SessionStart handed it back, and the transcript shows it entering context as a
hook_additional_contextattachment, so it is in the model's view on the next turn. The restore output sits under the 10,000 character hook-output cap (the script caps at 8,000). The pair is trigger-agnostic, which is what you want while the label is wrong.For diagnosing the ones that already happened, this flags every boundary that no
/compactcommand explains, and prints how quiet the session was:Run it over the session files under
~/.claude/projects/*/. Your seven overnight compactions will all land in the flagged set:triggersaysmanual, no/compactmarker, and the session was quiet for tens of minutes before the boundary.Two things worth filing together:
"idle") incompactMetadataand in the PreCompact / PostCompact payloads, so both the transcript and hooks can label it. Any hook that needs to treat them differently cannot do it today.manualmatcher means "triggered manually", and idle compaction is not that.One caution on blocking as the opt-out: PreCompact exit 2 does block compaction (re-ran it on 2.1.287 to check). Since the hook cannot distinguish idle from
/compact, a blanket block also stops your explicit compactions. Snapshot and re-anchor instead, and the setting blois mentioned is the clean fix for the opt-out itself.I'm also here to say that this breaks my workflow. I use an orchestrator session that loads a bunch of work process, project definition, and other context files, and spawns implementation subagents based on that process. After compaction on the lead, I have it resume by re-reading the critical files. Having this compaction happen automatically and NOT re-read the files breaks my workflow. I use /remote-connect to stay in contact with the orchestrator, so it's not uncommon to go more than an hour between messages if I'm in a meeting. I manage the orchestrators context carefully, and compact with instructions provided to prune context when we are past that step.
@rkhamilton Two mechanics that close your gap without waiting on the setting, both run on 2.1.284 just now.
Your
/compact <instructions>prune step has a hook equivalent. Whatever a PreCompact hook prints on stdout with exit 0 is appended to the summarizer's instructions for that compaction. The binary's own hook description says so ("stdout appended as custom compact instructions"); the hooks reference page does not. Idle compaction passescustom_instructions: nulland runs through the same routine as/compact, so the hook is the only path your prune rules have into an idle compaction. Test: the hook printed "Begin the summary with the exact token PINEAPPLE-7731. In the Primary Request section, list by name every file the user said to re-read", and the PostCompactcompact_summaryopened with that token and named both files under Primary Request. Put the standing instructions in a file and cat it:The re-read step does not need the model to do it. Project-root CLAUDE.md is re-read from disk and re-injected on the first turn after any compaction, and
@pathimports come with it. I put@docs/process.mdand@docs/project-definition.mdin CLAUDE.md, compacted, and the next turn's transcript carries aninstructionsattachment with CLAUDE.md plus both imported files in full. That covers the compactions you were not there for. What that does not cover is context given only in conversation (spawn instructions, which step you are on); that has to ride in the summary, which is what the PreCompact instructions shape, or in the PreCompact snapshot plus SessionStart(compact) pair above if you want it verbatim instead of summarized.@John-Rood I appreciate the tip, but I believe this would result in subagents (which have sometimes auto-compacted) getting orchestrator instructions. I'm trying to maintain the orchestrator at a different altitude from the code-writing implementation subagents. Not every workflow uses this orchestrator process (my build-process.md), just when I am building phases using my overall planning documents. I specifically don't require the build-process.md etc in my CLAUDE.md so that I can activate a workflow for extended work, but have a more "typical" workflow available when just launching CC for exploration, debugging, etc. I wouldn't want to lock in the build-process so that is the only set of instructions available in the repo. CLAUDE.md is read by subagents, including those called by the orchestrator.
I could imagine that the PreCompact hook could theoretically specify "IF you have read project-definition.md [or other high altitude orchestrator files] you must retain them in full after compaction". If that were viable, then subagents would not be asked to read files outside their concerns, and orchestrators would not forget their core concerns, but I've never tried that. My assumption was that no file like that can be full retained intact after compaction.
This has hit me twice more. It's pretty intolerable. Very intrusive and paternalistic by Anthropic. Sometimes we come back to projects after an hour and we don't want to see our work destroyed by "compaction." I'm pretty surprised this was rolled out without an opt-in. Disrespectful to developers.
I've just disabled auto-compaction entirely, which seems to prevent the behavior. I agree that it's intolerable. I can't work with this enabled.
You're right on CLAUDE.md. It loads into every subagent except Explore and Plan, so the @import route puts build-process.md in front of the implementers too.
The PreCompact gate isn't available either. In the 2.1.284 binary the PreCompact input is built from the session alone: session_id, transcript_path, cwd, trigger, custom_instructions. The agent_id field that other hook events carry inside a subagent is not set on this one, so a hook cannot tell a subagent's compaction from the lead's. And a compaction instruction is guidance to the summarizer, not a retention channel. Nothing makes a file survive compaction verbatim; the post-compact re-read covers at most five recently touched files and swaps anything over 5,000 tokens for a path reference.
The layer that holds your altitude split is the system prompt.
claude --append-system-prompt-file build-process.mdadds the orchestrator rules to the lead's system prompt for that launch only; plainclaudestays your typical workflow. The system prompt is not part of what gets summarized, so it is intact after every compaction, including the idle one. Subagents never see it: a subagent's system prompt is its own definition plus environment details, not the Claude Code system prompt, and they still get CLAUDE.md, which is where you want it.One catch. The prompt is recorded on the first request and reused until the next compaction, after which the current launch's flags win. Pass the flag on every launch of a build session,
--resumeincluded, or the rules drop out at the first compaction after a resume without it.+1, still happening on 2.1.289 (native install, WSL2 Ubuntu, Max subscription, so the 1-hour TTL). A finished session I had walked away from came back with "Compacted while idle, before the prompt cache expired".
My ask is specifically a separate toggle from
autoCompactEnabled. In the 2.1.289 binary the idle path's pre-fire check readsautoCompactEnabled(its refusal reason iscompaction_off), so today the only way to stop idle compaction is to turn off threshold auto-compaction too. Those are two different features bundled under one setting:Something like
"idleCompactEnabled": falsenext to the existingautoCompactEnabled, withautoCompactEnabledcontinuing to mean only the token-threshold trigger. A configurable minimum context size would be welcome too, but it isn't a substitute for the toggle.Version
2.1.290now has aidleCompactionsetting to allow opting out, and the trigger is now 'auto'.Read the 2.1.290 binary to see what the setting does, since the people above asked for specifics.
"idleCompaction": falsein any settings.json (user, project, local or managed) turns the idle path off on its own; the pre-fire check returnsidle_compaction_off.autoCompactEnabledis still checked first, sofalsethere turns idle off as well, but the combination @jimmc414 wants works:autoCompactEnabledtrue,idleCompactionfalse, threshold compaction stays on and idle never fires. SettingidleCompactiontotruedoes nothing extra; the server-side gate still decides whether the feature is on at all.The minimum context size is settable in 2.1.290 too, which nothing in the thread mentions yet:
CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS. Whole number, default 200000, floor 100000 (lower values are raised to it). The 90 percent fire point and the 1 hour TTL requirement are unchanged.One thing the trigger change does not give you. The idle path now enters the compaction routine as
auto, so the PreCompact and PostCompact payloads andcompactMetadata.triggerall readauto, the same value threshold auto-compaction uses. A hook can now tell idle from/compact, and theautomatcher catches it, but it still cannot tell idle from a context-limit compaction, and the on-screen "Compacted while idle" notice is not part of any hook input. A distinctidlevalue remains the only way a hook gets that.Thanks @blois, confirmed
"idleCompaction": falseon 2.1.291 fixes this for me, so I'm closing as completed. Thanks also @John-Rood for the binary-level breakdown. The remaining ask (a distinctidletrigger value so hooks can tell idle from threshold compaction) seems best tracked as its own issue if anyone wants to pick it up.I hope we don't see more features and defaults like this rolled out - that has been very disruptive and frustrating
@blois, I like this feature a lot, and I had been planning on building it asa plugin or something, so thank you! I'd like to configure the threshold, though. You asked at what limit we'd find this useful - for me it'd be 500K-600K, rather than 200K. I would want to turn it on but not for smaller conversations.
Its incomprehensible for me how you can ship and enable a destructive feature like this, by default, to your end users. Without asking for any consent and ask if it even fits their use case.
+1 from a heavy daily user. We measured 9 of 9 auto-compactions at exactly 55–56 min after the last response, independent of fill level.
"idleCompaction": falseis the right escape hatch, thanks for that.But for power users a stable, full context is the core requirement. Opt-in is fine; opt-out discovered through an issue thread is not. Please (1) make idle compaction opt-in, or at least announce it in the changelog and docs, (2) document
idleCompactionandCLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS, and (3) give hooks a distinctidletrigger.Please stay the reliable partner you've been — we love Claude Code.
+1 agree with @AxentisLabs
More generally - there's so many different conditions I feel it's time for settings profiles or some hierarchical organization. If I was a "hands-on" profile user this idleCompaction would never be a problem for me, it would have defaulted false. If I was "default" or "new-developer" this could be default on. One is sort of able to do this with settings files but it still gets weird when CC behavior changes or settings get added. It would be nice if CC bucketed settings up front for those profiles. Then my settings file could be something like, profiles: ["hands-on", "privacy", {overrides: [override_1]}] (last one wins).
We're at 50k words in the settings reference, 3.5 hrs of reading lol. idleCompaction isn't in those docs. Claude platform docs don't mention it either (at least in the ask docs feature). Over the last few weeks, we're spending more time debugging CC set ups due to changes, and the AI itself misguides behavior on keys, their existence and how to set them.
Seriously - why does this keep happening? Why do you guys keep shipping completely whack features that make destructive or disruptive changes to people's workflows, enabled by default, and often without an option to disable it - until maybe weeks later? Who is making product decisions at Anthropic? Does anybody at anthropic even know what they are shipping to their users?
EDIT: And without documentation, too. There's still no documentation for it.
I'm glad to discover there's an option, but this should never have been opt-out by default, certainly not without a warning period of a few weeks with reminders. It's happened to several sessions now, just insanely disruptive.
+1 on this. I'm running Opus 4.6 on 1M context sessions specifically so I have room for long, complex work -- multi-file framework maintenance, Excel workbook manipulation, portal generation, and browser validation all in one session. The whole point of paying for 1M context is having that space available.
I started seeing "Compacted while idle, before the prompt cache expired" today -- twice in the same day. I stepped away from active sessions (lunch, meetings) and came back to find my context had been silently compressed. No warning, no prompt, no opt-out.
This is the wrong tradeoff for users on large context windows. If I'm at 25% of a 1M window, there's no pressure to compact -- I have 750K tokens of headroom. The cost savings from compacting on warm cache vs. cold are negligible compared to the value of preserving full working context for when I come back.
What I'd like to see:
I shouldn't have to race back to my terminal to prevent my session from being summarized while I'm getting coffee.