source dump of claude code
at main 287 lines 10 kB view raw
1import { feature } from 'bun:bundle' 2import chalk from 'chalk' 3import { markPostCompaction } from 'src/bootstrap/state.js' 4import { getSystemPrompt } from '../../constants/prompts.js' 5import { getSystemContext, getUserContext } from '../../context.js' 6import { getShortcutDisplay } from '../../keybindings/shortcutFormat.js' 7import { notifyCompaction } from '../../services/api/promptCacheBreakDetection.js' 8import { 9 type CompactionResult, 10 compactConversation, 11 ERROR_MESSAGE_INCOMPLETE_RESPONSE, 12 ERROR_MESSAGE_NOT_ENOUGH_MESSAGES, 13 ERROR_MESSAGE_USER_ABORT, 14 mergeHookInstructions, 15} from '../../services/compact/compact.js' 16import { suppressCompactWarning } from '../../services/compact/compactWarningState.js' 17import { microcompactMessages } from '../../services/compact/microCompact.js' 18import { runPostCompactCleanup } from '../../services/compact/postCompactCleanup.js' 19import { trySessionMemoryCompaction } from '../../services/compact/sessionMemoryCompact.js' 20import { setLastSummarizedMessageId } from '../../services/SessionMemory/sessionMemoryUtils.js' 21import type { ToolUseContext } from '../../Tool.js' 22import type { LocalCommandCall } from '../../types/command.js' 23import type { Message } from '../../types/message.js' 24import { hasExactErrorMessage } from '../../utils/errors.js' 25import { executePreCompactHooks } from '../../utils/hooks.js' 26import { logError } from '../../utils/log.js' 27import { getMessagesAfterCompactBoundary } from '../../utils/messages.js' 28import { getUpgradeMessage } from '../../utils/model/contextWindowUpgradeCheck.js' 29import { 30 buildEffectiveSystemPrompt, 31 type SystemPrompt, 32} from '../../utils/systemPrompt.js' 33 34/* eslint-disable @typescript-eslint/no-require-imports */ 35const reactiveCompact = feature('REACTIVE_COMPACT') 36 ? (require('../../services/compact/reactiveCompact.js') as typeof import('../../services/compact/reactiveCompact.js')) 37 : null 38/* eslint-enable @typescript-eslint/no-require-imports */ 39 40export const call: LocalCommandCall = async (args, context) => { 41 const { abortController } = context 42 let { messages } = context 43 44 // REPL keeps snipped messages for UI scrollback — project so the compact 45 // model doesn't summarize content that was intentionally removed. 46 messages = getMessagesAfterCompactBoundary(messages) 47 48 if (messages.length === 0) { 49 throw new Error('No messages to compact') 50 } 51 52 const customInstructions = args.trim() 53 54 try { 55 // Try session memory compaction first if no custom instructions 56 // (session memory compaction doesn't support custom instructions) 57 if (!customInstructions) { 58 const sessionMemoryResult = await trySessionMemoryCompaction( 59 messages, 60 context.agentId, 61 ) 62 if (sessionMemoryResult) { 63 getUserContext.cache.clear?.() 64 runPostCompactCleanup() 65 // Reset cache read baseline so the post-compact drop isn't flagged 66 // as a break. compactConversation does this internally; SM-compact doesn't. 67 if (feature('PROMPT_CACHE_BREAK_DETECTION')) { 68 notifyCompaction( 69 context.options.querySource ?? 'compact', 70 context.agentId, 71 ) 72 } 73 markPostCompaction() 74 // Suppress warning immediately after successful compaction 75 suppressCompactWarning() 76 77 return { 78 type: 'compact', 79 compactionResult: sessionMemoryResult, 80 displayText: buildDisplayText(context), 81 } 82 } 83 } 84 85 // Reactive-only mode: route /compact through the reactive path. 86 // Checked after session-memory (that path is cheap and orthogonal). 87 if (reactiveCompact?.isReactiveOnlyMode()) { 88 return await compactViaReactive( 89 messages, 90 context, 91 customInstructions, 92 reactiveCompact, 93 ) 94 } 95 96 // Fall back to traditional compaction 97 // Run microcompact first to reduce tokens before summarization 98 const microcompactResult = await microcompactMessages(messages, context) 99 const messagesForCompact = microcompactResult.messages 100 101 const result = await compactConversation( 102 messagesForCompact, 103 context, 104 await getCacheSharingParams(context, messagesForCompact), 105 false, 106 customInstructions, 107 false, 108 ) 109 110 // Reset lastSummarizedMessageId since legacy compaction replaces all messages 111 // and the old message UUID will no longer exist in the new messages array 112 setLastSummarizedMessageId(undefined) 113 114 // Suppress the "Context left until auto-compact" warning after successful compaction 115 suppressCompactWarning() 116 117 getUserContext.cache.clear?.() 118 runPostCompactCleanup() 119 120 return { 121 type: 'compact', 122 compactionResult: result, 123 displayText: buildDisplayText(context, result.userDisplayMessage), 124 } 125 } catch (error) { 126 if (abortController.signal.aborted) { 127 throw new Error('Compaction canceled.') 128 } else if (hasExactErrorMessage(error, ERROR_MESSAGE_NOT_ENOUGH_MESSAGES)) { 129 throw new Error(ERROR_MESSAGE_NOT_ENOUGH_MESSAGES) 130 } else if (hasExactErrorMessage(error, ERROR_MESSAGE_INCOMPLETE_RESPONSE)) { 131 throw new Error(ERROR_MESSAGE_INCOMPLETE_RESPONSE) 132 } else { 133 logError(error) 134 throw new Error(`Error during compaction: ${error}`) 135 } 136 } 137} 138 139async function compactViaReactive( 140 messages: Message[], 141 context: ToolUseContext, 142 customInstructions: string, 143 reactive: NonNullable<typeof reactiveCompact>, 144): Promise<{ 145 type: 'compact' 146 compactionResult: CompactionResult 147 displayText: string 148}> { 149 context.onCompactProgress?.({ 150 type: 'hooks_start', 151 hookType: 'pre_compact', 152 }) 153 context.setSDKStatus?.('compacting') 154 155 try { 156 // Hooks and cache-param build are independent — run concurrently. 157 // getCacheSharingParams walks all tools to build the system prompt; 158 // pre-compact hooks spawn subprocesses. Neither depends on the other. 159 const [hookResult, cacheSafeParams] = await Promise.all([ 160 executePreCompactHooks( 161 { trigger: 'manual', customInstructions: customInstructions || null }, 162 context.abortController.signal, 163 ), 164 getCacheSharingParams(context, messages), 165 ]) 166 const mergedInstructions = mergeHookInstructions( 167 customInstructions, 168 hookResult.newCustomInstructions, 169 ) 170 171 context.setStreamMode?.('requesting') 172 context.setResponseLength?.(() => 0) 173 context.onCompactProgress?.({ type: 'compact_start' }) 174 175 const outcome = await reactive.reactiveCompactOnPromptTooLong( 176 messages, 177 cacheSafeParams, 178 { customInstructions: mergedInstructions, trigger: 'manual' }, 179 ) 180 181 if (!outcome.ok) { 182 // The outer catch in `call` translates these: aborted → "Compaction 183 // canceled." (via abortController.signal.aborted check), NOT_ENOUGH → 184 // re-thrown as-is, everything else → "Error during compaction: …". 185 switch (outcome.reason) { 186 case 'too_few_groups': 187 throw new Error(ERROR_MESSAGE_NOT_ENOUGH_MESSAGES) 188 case 'aborted': 189 throw new Error(ERROR_MESSAGE_USER_ABORT) 190 case 'exhausted': 191 case 'error': 192 case 'media_unstrippable': 193 throw new Error(ERROR_MESSAGE_INCOMPLETE_RESPONSE) 194 } 195 } 196 197 // Mirrors the post-success cleanup in tryReactiveCompact, minus 198 // resetMicrocompactState — processSlashCommand calls that for all 199 // type:'compact' results. 200 setLastSummarizedMessageId(undefined) 201 runPostCompactCleanup() 202 suppressCompactWarning() 203 getUserContext.cache.clear?.() 204 205 // reactiveCompactOnPromptTooLong runs PostCompact hooks but not PreCompact 206 // — both callers (here and tryReactiveCompact) run PreCompact outside so 207 // they can merge its userDisplayMessage with PostCompact's here. This 208 // caller additionally runs it concurrently with getCacheSharingParams. 209 const combinedMessage = 210 [hookResult.userDisplayMessage, outcome.result.userDisplayMessage] 211 .filter(Boolean) 212 .join('\n') || undefined 213 214 return { 215 type: 'compact', 216 compactionResult: { 217 ...outcome.result, 218 userDisplayMessage: combinedMessage, 219 }, 220 displayText: buildDisplayText(context, combinedMessage), 221 } 222 } finally { 223 context.setStreamMode?.('requesting') 224 context.setResponseLength?.(() => 0) 225 context.onCompactProgress?.({ type: 'compact_end' }) 226 context.setSDKStatus?.(null) 227 } 228} 229 230function buildDisplayText( 231 context: ToolUseContext, 232 userDisplayMessage?: string, 233): string { 234 const upgradeMessage = getUpgradeMessage('tip') 235 const expandShortcut = getShortcutDisplay( 236 'app:toggleTranscript', 237 'Global', 238 'ctrl+o', 239 ) 240 const dimmed = [ 241 ...(context.options.verbose 242 ? [] 243 : [`(${expandShortcut} to see full summary)`]), 244 ...(userDisplayMessage ? [userDisplayMessage] : []), 245 ...(upgradeMessage ? [upgradeMessage] : []), 246 ] 247 return chalk.dim('Compacted ' + dimmed.join('\n')) 248} 249 250async function getCacheSharingParams( 251 context: ToolUseContext, 252 forkContextMessages: Message[], 253): Promise<{ 254 systemPrompt: SystemPrompt 255 userContext: { [k: string]: string } 256 systemContext: { [k: string]: string } 257 toolUseContext: ToolUseContext 258 forkContextMessages: Message[] 259}> { 260 const appState = context.getAppState() 261 const defaultSysPrompt = await getSystemPrompt( 262 context.options.tools, 263 context.options.mainLoopModel, 264 Array.from( 265 appState.toolPermissionContext.additionalWorkingDirectories.keys(), 266 ), 267 context.options.mcpClients, 268 ) 269 const systemPrompt = buildEffectiveSystemPrompt({ 270 mainThreadAgentDefinition: undefined, 271 toolUseContext: context, 272 customSystemPrompt: context.options.customSystemPrompt, 273 defaultSystemPrompt: defaultSysPrompt, 274 appendSystemPrompt: context.options.appendSystemPrompt, 275 }) 276 const [userContext, systemContext] = await Promise.all([ 277 getUserContext(), 278 getSystemContext(), 279 ]) 280 return { 281 systemPrompt, 282 userContext, 283 systemContext, 284 toolUseContext: context, 285 forkContextMessages, 286 } 287}