老钱庄的账房里,只有一张案台。先生办事,靠的全是这张台面:要核哪笔往来,就从架上抽出对应的票据、账册,平铺在台上比对;核完一笔,纸却往往不收,顺手压在一旁,接着抽下一沓。台子就那么大,纸越积越多。
他有个雷打不动的习惯:台面右下角那一块,无论多挤,都空着——那是留着最后誊写结单用的,占了它,这天的活就没法收尾。
平日里,他不等台子堆满才动手。每隔一阵,他只把最早抽出、早已比对完的那几沓旧票悄悄抽走归档,留下手边最近用的几样;台上其余的摆布丝毫不动——那套精心铺开的次序,他舍不得打乱,打乱了重铺又得费半天工夫。这样台面始终松动,他自己几乎都察觉不到在收拾。
赶上他出门办事、过了晌午才回来,情形就不同了:茶凉了,墨干了,台上那套铺排早没了热乎气,横竖要重新摊一遍。既然如此,他索性回来头一件事,就把旧票一次清掉大半,只留最近五沓,再重新铺台——反正都要重铺,不如先轻装。
最怕的是台子眼看要顶到那块留白。这时他不再修修补补,而是停下手里的活,提笔把至今所有的来龙去脉浓缩成一页节略,然后把满台散纸尽数收走,只留这页节略和手边正在算的那几笔,接着往下做。若是徒弟早已在旁边记着一本流水摘要,他连从头重读一遍都省了,直接拿摘要当那页节略用。
可有时账实在太大,一页节略也压不下——他试着收拾,腾出的地方还是不够;再试,还是不够。试到第三回仍旧塞不进,他便不再空耗,认了这台子今日就挤着办,先把手上的活做完要紧。
让这间账房几十年不乱的,从来不是先生记性好,而是这套规矩:先轻轻抽走几张旧纸而不动整体铺排,不行就趁台子凉透时清掉半台,再不行才把一切浓缩成一页,那块留白始终不许占,试三回不成就罢手。这,正是这一页要讲的——上下文的自动压缩与微压缩。
先猜揭晓前,先押一个猜测 · 你觉得这讲的是?
谜底 · The Concept
Context autocompaction and microcompaction — the layered, cost-graded strategy for keeping a finite window usable
Context Management · 上下文管理
中文速览 · Quick read
A context window is a finite desk. Claude Code keeps it usable with a layered optimization strategy: cheap, surgical microcompaction is applied first, and only if that fails does the expensive, summarize-everything autocompaction run. The query loop encodes exactly this order — microcompaction is attempted before autocompaction on every turn (claude-code/src/query.ts:414-468), and the comment there spells out why: if the cheap path frees enough room, the expensive path becomes a no-op.
Microcompaction itself fans out into three paths (claude-code/src/services/compact/microCompact.ts:253-293): a time-based trigger that content-clears old tool results when the prompt cache has gone cold, a cached path that uses the cache_edits API to surgically delete individual tool results without invalidating the cached prefix, or a no-op. Autocompaction is the heavier fallback that, near the window limit, condenses the whole conversation into a one-page summary (claude-code/src/services/compact/autoCompact.ts:241-351), preferring a pre-extracted session-memory summary over a fresh API call when one exists (claude-code/src/services/compact/autoCompact.ts:287-310).
How it works
microcompactMessages runs the time-based trigger first and short-circuits on it (claude-code/src/services/compact/microCompact.ts:253-293). The trigger fires only when the gap since the last assistant message exceeds a threshold — default 60 minutes, sourced from GrowthBook config tengu_slate_heron (claude-code/src/services/compact/timeBasedMCConfig.ts:18-28). The rationale is precise: 60 minutes guarantees the server's 1h cache TTL has expired, so the full prefix will be rewritten regardless; clearing old tool results now shrinks what gets re-sent. It content-clears all but the most recent N compactable tool results in place, swapping each for a sentinel string (claude-code/src/services/compact/microCompact.ts:446-530). Because this mutates prompt content and thus invalidates the server cache, it then resets the cached-microcompact module state so the next turn won't try to cache_edit tools whose server-side entries no longer exist (claude-code/src/services/compact/microCompact.ts:512-517).
If the time-based path didn't fire, the cached path runs — but only for the main thread, prefix-matched on repl_main_thread, to stop forked agents from registering their tool IDs in the shared module-level state (claude-code/src/services/compact/microCompact.ts:249-251). It registers compactable tool results, and when the count crosses the threshold it builds a cache_edits block instructing the API to delete specific tool_use IDs from the cached prefix — without modifying local message content, so the cached prefix stays valid and subsequent turns keep hitting cache (claude-code/src/services/compact/microCompact.ts:305-399). The deleted-IDs and boundary info ride back as a PendingCacheEdits (claude-code/src/services/compact/microCompact.ts:207-213) carried on MicrocompactResult (claude-code/src/services/compact/microCompact.ts:215-220); the query loop defers them until after the API response so it can use the real cache_deleted_input_tokens instead of a client-side estimate (claude-code/src/query.ts:414-468).
autoCompactIfNeeded checks the circuit breaker first: after 3 consecutive failures (MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES, claude-code/src/services/compact/autoCompact.ts:70) it bails out rather than hammering a doomed over-limit session (claude-code/src/services/compact/autoCompact.ts:260-265). The trigger is shouldAutoCompact: the threshold is the effective window minus a 13K buffer, where the effective window itself already reserves headroom for the summary's own output (claude-code/src/services/compact/autoCompact.ts:62-76). It is deliberately suppressed during reactive-compact and context-collapse modes, since collapse's 90%/95% commit/blocking flow owns the headroom problem and autocompact would race it (claude-code/src/services/compact/autoCompact.ts:189-223).
When it does fire, it tries session-memory compaction first (claude-code/src/services/compact/autoCompact.ts:287-310 → trySessionMemoryCompaction, claude-code/src/services/compact/sessionMemoryCompact.ts:514-598). That path computes a keep-index by expanding backward from the last-summarized message until both a token minimum and a text-block-message minimum are met or a hard max cap is hit, governed by SessionMemoryCompactConfig (claude-code/src/services/compact/sessionMemoryCompact.ts:47-54, claude-code/src/services/compact/sessionMemoryCompact.ts:324-397). Crucially it then adjusts that index to preserve API invariants — pulling in the assistant message that owns any kept tool_result, and any same-id message carrying a paired thinking block — so the surviving tail never has orphaned tool_results (claude-code/src/services/compact/sessionMemoryCompact.ts:232-314). It reuses the already-extracted session summary as the boundary text, so there is no compact API call at all (claude-code/src/services/compact/sessionMemoryCompact.ts:484-485).
If session memory is unavailable, compactConversation runs the heavy path (claude-code/src/services/compact/compact.ts:387-763): execute PreCompact hooks → streamCompactSummary, which prefers a forked agent that reuses the main conversation's prompt cache (gated by tengu_compact_cache_prefix) and falls back to a streaming path with optional retry (gated by tengu_compact_streaming_retry) (claude-code/src/services/compact/compact.ts:1136-1395, claude-code/src/services/compact/compact.ts:1155, claude-code/src/services/compact/compact.ts:1251-1254). If the compact request itself hits prompt-too-long, it truncates the oldest API-round groups and retries, falling back to a blunt ~20% drop when the token gap is unparseable (claude-code/src/services/compact/compact.ts:243-291). On success it clears readFileState and memory caches, then re-injects the most-recent files as attachments within a token budget, deduplicated against what's still visible in the preserved tail (claude-code/src/services/compact/compact.ts:1415-1464, claude-code/src/services/compact/compact.ts:1610-1655). Notably it does not re-inject the full skill listing — ~4K tokens of pure cache-creation with marginal benefit (claude-code/src/services/compact/compact.ts:524-529). The result is a CompactionResult (claude-code/src/services/compact/compact.ts:299-310).
The query loop resets tracking state — compacted, a fresh turnId, turnCounter and consecutiveFailures (claude-code/src/services/compact/autoCompact.ts:51-60) — to reflect the most recent compact (claude-code/src/query.ts:521-526), and runPostCompactCleanup resets microcompact state, context-collapse, and the file/memory caches so stale module-level state can't corrupt the next turn (claude-code/src/services/compact/postCompactCleanup.ts:31-77).
Why it matters
The layering is a deliberate cost/precision gradient. The cheapest move exploits a cache that is already cold — time-based clearing only fires past a TTL where the prefix would be rewritten anyway (claude-code/src/services/compact/timeBasedMCConfig.ts:18-28). The next cheapest, cached microcompaction, surgically removes individual tool results while leaving the cached prefix intact, so the model keeps paying cache-read prices on subsequent turns (claude-code/src/services/compact/microCompact.ts:305-399). Only when neither keeps the window under threshold does autocompaction pay for a full summarization — and even then it prefers an already-extracted session summary over a fresh API round-trip (claude-code/src/services/compact/autoCompact.ts:287-310). Running micro before auto (claude-code/src/query.ts:414-468) is what lets the expensive path frequently no-op.
The 13K buffer (claude-code/src/services/compact/autoCompact.ts:62-76) is the desk's reserved corner: it leaves room for the summary itself to fit, so compaction never fails for lack of space to write its own output. The circuit breaker (claude-code/src/services/compact/autoCompact.ts:70, claude-code/src/services/compact/autoCompact.ts:260-265) exists because real sessions had thousands of consecutive doomed retries wasting ~250K API calls/day globally — it accepts a cramped session rather than burning the budget. The invariant-preserving keep-index logic (claude-code/src/services/compact/sessionMemoryCompact.ts:232-314) guarantees the post-compact conversation stays API-valid, and post-compact cleanup (claude-code/src/services/compact/postCompactCleanup.ts:31-77) prevents the bookkeeping of one compaction from poisoning the next turn.
Read the source
- The order that makes it work — microcompaction before autocompaction in the query loop, with deferred cache_edits: claude-code/src/query.ts:414-468, claude-code/src/query.ts:521-526.
- The three microcompact paths — time-based, cached, no-op: claude-code/src/services/compact/microCompact.ts:253-293; the time-based content-clear: claude-code/src/services/compact/microCompact.ts:446-530; the cache_edits path: claude-code/src/services/compact/microCompact.ts:305-399.
- The autocompact threshold and circuit breaker: claude-code/src/services/compact/autoCompact.ts:62-76, claude-code/src/services/compact/autoCompact.ts:260-265; session-memory-first: claude-code/src/services/compact/autoCompact.ts:287-310.
- The full compaction pipeline and its PTL retry: claude-code/src/services/compact/compact.ts:387-763, claude-code/src/services/compact/compact.ts:243-291, claude-code/src/services/compact/compact.ts:1136-1395.
- Invariant-preserving keep-index and post-compact cleanup: claude-code/src/services/compact/sessionMemoryCompact.ts:232-314, claude-code/src/services/compact/sessionMemoryCompact.ts:324-397, claude-code/src/services/compact/postCompactCleanup.ts:31-77.
classDiagram
class queryLoop {
+microBeforeAuto()
}
class microcompactMessages {
+timeBased() cacheCold
+cached() cacheEdits
+noop()
}
class autoCompactIfNeeded {
+threshold effMinus13K
+breaker 3 fails
+sessionMemoryFirst()
}
class MicrocompactResult {
+pendingCacheEdits
}
class CompactionResult {
+summary
+preservedTail
}
queryLoop ..> microcompactMessages : first and cheap
queryLoop ..> autoCompactIfNeeded : fallback
microcompactMessages ..> MicrocompactResult : returns
autoCompactIfNeeded ..> CompactionResult : returns 读法:每轮先微压、后自动压(query.ts:414)。microcompactMessages 三条路:
缓存凉透(gap>60min)的 timeBased 清理(microCompact.ts:446),或 cache_edits 删个别
tool 结果而前缀仍热(:305),或 noop。autoCompactIfNeeded 阈值 eff−13K
(autoCompact.ts:62),连失败 3 次熔断(:260),且优先复用 session 摘要免 API(:287)。
台面就是模型有限的上下文窗口。每推进一轮就压上一沓 tool result。台子右端那块留白(−13K)永远不许占——留着誊写节略。 harness 永远先试最便宜的那一招:平日用 cache_edits 悄悄抽掉个别旧票而 不打乱缓存前缀;台子凉透(gap>60min)才清掉大半旧票;顶到留白才把一切 浓缩成一页。试着把台子推满。
台子是空的。点「推进一轮」开始堆票。
顺序是关键:每轮都先微压、后自动压(claude-code/src/query.ts:414-468)。微压有两条路—— cache_edits 删掉个别 tool 结果却不动缓存前缀(microCompact.ts:305-399),或缓存凉透时的 time-based 清理(microCompact.ts:446-530, 60 分钟来自 timeBasedMCConfig.ts:18-28)。顶到 eff−13K 才自动压(autoCompact.ts:62-76),且优先复用已抽好的 session 摘要、免一次 API (autoCompact.ts:287-310);连失败 3 次则熔断(autoCompact.ts:260-265)。
A context window is a finite desk. Claude Code keeps it usable with a layered optimization strategy: cheap, surgical microcompaction is applied first, and only if that fails does the expensive, summarize-everything autocompaction run. The query loop encodes exactly this order — microcompaction is attempted before autocompaction on every turn (claude-code/src/query.ts:414-468), and the comment there spells out why: if the cheap path frees enough room, the expensive path becomes a no-op.
Microcompaction itself fans out into three paths (claude-code/src/services/compact/microCompact.ts:253-293): a time-based trigger that content-clears old tool results when the prompt cache has gone cold, a cached path that uses the cache_edits API to surgically delete individual tool results without invalidating the cached prefix, or a no-op. Autocompaction is the heavier fallback that, near the window limit, condenses the whole conversation into a one-page summary (claude-code/src/services/compact/autoCompact.ts:241-351), preferring a pre-extracted session-memory summary over a fresh API call when one exists (claude-code/src/services/compact/autoCompact.ts:287-310).
How it works
Microcompaction entry. microcompactMessages runs the time-based trigger first and short-circuits on it (claude-code/src/services/compact/microCompact.ts:253-293). The trigger fires only when the gap since the last assistant message exceeds a threshold — default 60 minutes, sourced from GrowthBook config tengu_slate_heron (claude-code/src/services/compact/timeBasedMCConfig.ts:18-28). The rationale is precise: 60 minutes guarantees the server's 1h cache TTL has expired, so the full prefix will be rewritten regardless; clearing old tool results now shrinks what gets re-sent. It content-clears all but the most recent N compactable tool results in place, swapping each for a sentinel string (claude-code/src/services/compact/microCompact.ts:446-530). Because this mutates prompt content and thus invalidates the server cache, it then resets the cached-microcompact module state so the next turn won't try to cache_edit tools whose server-side entries no longer exist (claude-code/src/services/compact/microCompact.ts:512-517).
Cached microcompaction. If the time-based path didn't fire, the cached path runs — but only for the main thread, prefix-matched on repl_main_thread, to stop forked agents from registering their tool IDs in the shared module-level state (claude-code/src/services/compact/microCompact.ts:249-251). It registers compactable tool results, and when the count crosses the threshold it builds a cache_edits block instructing the API to delete specific tool_use IDs from the cached prefix — without modifying local message content, so the cached prefix stays valid and subsequent turns keep hitting cache (claude-code/src/services/compact/microCompact.ts:305-399). The deleted-IDs and boundary info ride back as a PendingCacheEdits (claude-code/src/services/compact/microCompact.ts:207-213) carried on MicrocompactResult (claude-code/src/services/compact/microCompact.ts:215-220); the query loop defers them until after the API response so it can use the real cache_deleted_input_tokens instead of a client-side estimate (claude-code/src/query.ts:414-468).
Autocompaction. autoCompactIfNeeded checks the circuit breaker first: after 3 consecutive failures (MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES, claude-code/src/services/compact/autoCompact.ts:70) it bails out rather than hammering a doomed over-limit session (claude-code/src/services/compact/autoCompact.ts:260-265). The trigger is shouldAutoCompact: the threshold is the effective window minus a 13K buffer, where the effective window itself already reserves headroom for the summary's own output (claude-code/src/services/compact/autoCompact.ts:62-76). It is deliberately suppressed during reactive-compact and context-collapse modes, since collapse's 90%/95% commit/blocking flow owns the headroom problem and autocompact would race it (claude-code/src/services/compact/autoCompact.ts:189-223).
When it does fire, it tries session-memory compaction first (claude-code/src/services/compact/autoCompact.ts:287-310 → trySessionMemoryCompaction, claude-code/src/services/compact/sessionMemoryCompact.ts:514-598). That path computes a keep-index by expanding backward from the last-summarized message until both a token minimum and a text-block-message minimum are met or a hard max cap is hit, governed by SessionMemoryCompactConfig (claude-code/src/services/compact/sessionMemoryCompact.ts:47-54, claude-code/src/services/compact/sessionMemoryCompact.ts:324-397). Crucially it then adjusts that index to preserve API invariants — pulling in the assistant message that owns any kept tool_result, and any same-id message carrying a paired thinking block — so the surviving tail never has orphaned tool_results (claude-code/src/services/compact/sessionMemoryCompact.ts:232-314). It reuses the already-extracted session summary as the boundary text, so there is no compact API call at all (claude-code/src/services/compact/sessionMemoryCompact.ts:484-485).
Full compaction. If session memory is unavailable, compactConversation runs the heavy path (claude-code/src/services/compact/compact.ts:387-763): execute PreCompact hooks → streamCompactSummary, which prefers a forked agent that reuses the main conversation's prompt cache (gated by tengu_compact_cache_prefix) and falls back to a streaming path with optional retry (gated by tengu_compact_streaming_retry) (claude-code/src/services/compact/compact.ts:1136-1395, claude-code/src/services/compact/compact.ts:1155, claude-code/src/services/compact/compact.ts:1251-1254). If the compact request itself hits prompt-too-long, it truncates the oldest API-round groups and retries, falling back to a blunt ~20% drop when the token gap is unparseable (claude-code/src/services/compact/compact.ts:243-291). On success it clears readFileState and memory caches, then re-injects the most-recent files as attachments within a token budget, deduplicated against what's still visible in the preserved tail (claude-code/src/services/compact/compact.ts:1415-1464, claude-code/src/services/compact/compact.ts:1610-1655). Notably it does not re-inject the full skill listing — ~4K tokens of pure cache-creation with marginal benefit (claude-code/src/services/compact/compact.ts:524-529). The result is a CompactionResult (claude-code/src/services/compact/compact.ts:299-310).
After compaction. The query loop resets tracking state — compacted, a fresh turnId, turnCounter and consecutiveFailures (claude-code/src/services/compact/autoCompact.ts:51-60) — to reflect the most recent compact (claude-code/src/query.ts:521-526), and runPostCompactCleanup resets microcompact state, context-collapse, and the file/memory caches so stale module-level state can't corrupt the next turn (claude-code/src/services/compact/postCompactCleanup.ts:31-77).
Why it matters
The layering is a deliberate cost/precision gradient. The cheapest move exploits a cache that is already cold — time-based clearing only fires past a TTL where the prefix would be rewritten anyway (claude-code/src/services/compact/timeBasedMCConfig.ts:18-28). The next cheapest, cached microcompaction, surgically removes individual tool results while leaving the cached prefix intact, so the model keeps paying cache-read prices on subsequent turns (claude-code/src/services/compact/microCompact.ts:305-399). Only when neither keeps the window under threshold does autocompaction pay for a full summarization — and even then it prefers an already-extracted session summary over a fresh API round-trip (claude-code/src/services/compact/autoCompact.ts:287-310). Running micro before auto (claude-code/src/query.ts:414-468) is what lets the expensive path frequently no-op.
The 13K buffer (claude-code/src/services/compact/autoCompact.ts:62-76) is the desk's reserved corner: it leaves room for the summary itself to fit, so compaction never fails for lack of space to write its own output. The circuit breaker (claude-code/src/services/compact/autoCompact.ts:70, claude-code/src/services/compact/autoCompact.ts:260-265) exists because real sessions had thousands of consecutive doomed retries wasting ~250K API calls/day globally — it accepts a cramped session rather than burning the budget. The invariant-preserving keep-index logic (claude-code/src/services/compact/sessionMemoryCompact.ts:232-314) guarantees the post-compact conversation stays API-valid, and post-compact cleanup (claude-code/src/services/compact/postCompactCleanup.ts:31-77) prevents the bookkeeping of one compaction from poisoning the next turn.
Read the source
- The order that makes it work — microcompaction before autocompaction in the query loop, with deferred cache_edits: claude-code/src/query.ts:414-468, claude-code/src/query.ts:521-526.
- The three microcompact paths — time-based, cached, no-op: claude-code/src/services/compact/microCompact.ts:253-293; the time-based content-clear: claude-code/src/services/compact/microCompact.ts:446-530; the cache_edits path: claude-code/src/services/compact/microCompact.ts:305-399.
- The autocompact threshold and circuit breaker: claude-code/src/services/compact/autoCompact.ts:62-76, claude-code/src/services/compact/autoCompact.ts:260-265; session-memory-first: claude-code/src/services/compact/autoCompact.ts:287-310.
- The full compaction pipeline and its PTL retry: claude-code/src/services/compact/compact.ts:387-763, claude-code/src/services/compact/compact.ts:243-291, claude-code/src/services/compact/compact.ts:1136-1395.
- Invariant-preserving keep-index and post-compact cleanup: claude-code/src/services/compact/sessionMemoryCompact.ts:232-314, claude-code/src/services/compact/sessionMemoryCompact.ts:324-397, claude-code/src/services/compact/postCompactCleanup.ts:31-77.
来源 · Source citations
- [1]
claude-code/src/query.ts:414-468 - [2]
claude-code/src/query.ts:521-526 - [3]
claude-code/src/services/compact/microCompact.ts:207-213 - [4]
claude-code/src/services/compact/microCompact.ts:215-220 - [5]
claude-code/src/services/compact/microCompact.ts:249-251 - [6]
claude-code/src/services/compact/microCompact.ts:253-293 - [7]
claude-code/src/services/compact/microCompact.ts:305-399 - [8]
claude-code/src/services/compact/microCompact.ts:446-530 - [9]
claude-code/src/services/compact/microCompact.ts:512-517 - [10]
claude-code/src/services/compact/timeBasedMCConfig.ts:18-28 - [11]
claude-code/src/services/compact/autoCompact.ts:51-60 - [12]
claude-code/src/services/compact/autoCompact.ts:62-76 - [13]
claude-code/src/services/compact/autoCompact.ts:70 - [14]
claude-code/src/services/compact/autoCompact.ts:189-223 - [15]
claude-code/src/services/compact/autoCompact.ts:241-351 - [16]
claude-code/src/services/compact/autoCompact.ts:260-265 - [17]
claude-code/src/services/compact/autoCompact.ts:287-310 - [18]
claude-code/src/services/compact/compact.ts:243-291 - [19]
claude-code/src/services/compact/compact.ts:299-310 - [20]
claude-code/src/services/compact/compact.ts:387-763 - [21]
claude-code/src/services/compact/compact.ts:524-529 - [22]
claude-code/src/services/compact/compact.ts:1136-1395 - [23]
claude-code/src/services/compact/compact.ts:1155 - [24]
claude-code/src/services/compact/compact.ts:1251-1254 - [25]
claude-code/src/services/compact/compact.ts:1415-1464 - [26]
claude-code/src/services/compact/compact.ts:1610-1655 - [27]
claude-code/src/services/compact/postCompactCleanup.ts:31-77 - [28]
claude-code/src/services/compact/sessionMemoryCompact.ts:47-54 - [29]
claude-code/src/services/compact/sessionMemoryCompact.ts:232-314 - [30]
claude-code/src/services/compact/sessionMemoryCompact.ts:324-397 - [31]
claude-code/src/services/compact/sessionMemoryCompact.ts:484-485 - [32]
claude-code/src/services/compact/sessionMemoryCompact.ts:514-598