Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
10 kB · 287 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288import { feature } from 'bun:bundle'import chalk from 'chalk'import { markPostCompaction } from 'src/bootstrap/state.js'import { getSystemPrompt } from '../../constants/prompts.js'import { getSystemContext, getUserContext } from '../../context.js'import { getShortcutDisplay } from '../../keybindings/shortcutFormat.js'import { notifyCompaction } from '../../services/api/promptCacheBreakDetection.js'import { type CompactionResult, compactConversation, ERROR_MESSAGE_INCOMPLETE_RESPONSE, ERROR_MESSAGE_NOT_ENOUGH_MESSAGES, ERROR_MESSAGE_USER_ABORT, mergeHookInstructions,} from '../../services/compact/compact.js'import { suppressCompactWarning } from '../../services/compact/compactWarningState.js'import { microcompactMessages } from '../../services/compact/microCompact.js'import { runPostCompactCleanup } from '../../services/compact/postCompactCleanup.js'import { trySessionMemoryCompaction } from '../../services/compact/sessionMemoryCompact.js'import { setLastSummarizedMessageId } from '../../services/SessionMemory/sessionMemoryUtils.js'import type { ToolUseContext } from '../../Tool.js'import type { LocalCommandCall } from '../../types/command.js'import type { Message } from '../../types/message.js'import { hasExactErrorMessage } from '../../utils/errors.js'import { executePreCompactHooks } from '../../utils/hooks.js'import { logError } from '../../utils/log.js'import { getMessagesAfterCompactBoundary } from '../../utils/messages.js'import { getUpgradeMessage } from '../../utils/model/contextWindowUpgradeCheck.js'import { buildEffectiveSystemPrompt, type SystemPrompt,} from '../../utils/systemPrompt.js'
/* eslint-disable @typescript-eslint/no-require-imports */const reactiveCompact = feature('REACTIVE_COMPACT') ? (require('../../services/compact/reactiveCompact.js') as typeof import('../../services/compact/reactiveCompact.js')) : null/* eslint-enable @typescript-eslint/no-require-imports */
export const call: LocalCommandCall = async (args, context) => { const { abortController } = context let { messages } = context
// REPL keeps snipped messages for UI scrollback — project so the compact // model doesn't summarize content that was intentionally removed. messages = getMessagesAfterCompactBoundary(messages)
if (messages.length === 0) { throw new Error('No messages to compact') }
const customInstructions = args.trim()
try { // Try session memory compaction first if no custom instructions // (session memory compaction doesn't support custom instructions) if (!customInstructions) { const sessionMemoryResult = await trySessionMemoryCompaction( messages, context.agentId, ) if (sessionMemoryResult) { getUserContext.cache.clear?.() runPostCompactCleanup() // Reset cache read baseline so the post-compact drop isn't flagged // as a break. compactConversation does this internally; SM-compact doesn't. if (feature('PROMPT_CACHE_BREAK_DETECTION')) { notifyCompaction( context.options.querySource ?? 'compact', context.agentId, ) } markPostCompaction() // Suppress warning immediately after successful compaction suppressCompactWarning()
return { type: 'compact', compactionResult: sessionMemoryResult, displayText: buildDisplayText(context), } } }
// Reactive-only mode: route /compact through the reactive path. // Checked after session-memory (that path is cheap and orthogonal). if (reactiveCompact?.isReactiveOnlyMode()) { return await compactViaReactive( messages, context, customInstructions, reactiveCompact, ) }
// Fall back to traditional compaction // Run microcompact first to reduce tokens before summarization const microcompactResult = await microcompactMessages(messages, context) const messagesForCompact = microcompactResult.messages
const result = await compactConversation( messagesForCompact, context, await getCacheSharingParams(context, messagesForCompact), false, customInstructions, false, )
// Reset lastSummarizedMessageId since legacy compaction replaces all messages // and the old message UUID will no longer exist in the new messages array setLastSummarizedMessageId(undefined)
// Suppress the "Context left until auto-compact" warning after successful compaction suppressCompactWarning()
getUserContext.cache.clear?.() runPostCompactCleanup()
return { type: 'compact', compactionResult: result, displayText: buildDisplayText(context, result.userDisplayMessage), } } catch (error) { if (abortController.signal.aborted) { throw new Error('Compaction canceled.') } else if (hasExactErrorMessage(error, ERROR_MESSAGE_NOT_ENOUGH_MESSAGES)) { throw new Error(ERROR_MESSAGE_NOT_ENOUGH_MESSAGES) } else if (hasExactErrorMessage(error, ERROR_MESSAGE_INCOMPLETE_RESPONSE)) { throw new Error(ERROR_MESSAGE_INCOMPLETE_RESPONSE) } else { logError(error) throw new Error(`Error during compaction: ${error}`) } }}
async function compactViaReactive( messages: Message[], context: ToolUseContext, customInstructions: string, reactive: NonNullable<typeof reactiveCompact>,): Promise<{ type: 'compact' compactionResult: CompactionResult displayText: string}> { context.onCompactProgress?.({ type: 'hooks_start', hookType: 'pre_compact', }) context.setSDKStatus?.('compacting')
try { // Hooks and cache-param build are independent — run concurrently. // getCacheSharingParams walks all tools to build the system prompt; // pre-compact hooks spawn subprocesses. Neither depends on the other. const [hookResult, cacheSafeParams] = await Promise.all([ executePreCompactHooks( { trigger: 'manual', customInstructions: customInstructions || null }, context.abortController.signal, ), getCacheSharingParams(context, messages), ]) const mergedInstructions = mergeHookInstructions( customInstructions, hookResult.newCustomInstructions, )
context.setStreamMode?.('requesting') context.setResponseLength?.(() => 0) context.onCompactProgress?.({ type: 'compact_start' })
const outcome = await reactive.reactiveCompactOnPromptTooLong( messages, cacheSafeParams, { customInstructions: mergedInstructions, trigger: 'manual' }, )
if (!outcome.ok) { // The outer catch in `call` translates these: aborted → "Compaction // canceled." (via abortController.signal.aborted check), NOT_ENOUGH → // re-thrown as-is, everything else → "Error during compaction: …". switch (outcome.reason) { case 'too_few_groups': throw new Error(ERROR_MESSAGE_NOT_ENOUGH_MESSAGES) case 'aborted': throw new Error(ERROR_MESSAGE_USER_ABORT) case 'exhausted': case 'error': case 'media_unstrippable': throw new Error(ERROR_MESSAGE_INCOMPLETE_RESPONSE) } }
// Mirrors the post-success cleanup in tryReactiveCompact, minus // resetMicrocompactState — processSlashCommand calls that for all // type:'compact' results. setLastSummarizedMessageId(undefined) runPostCompactCleanup() suppressCompactWarning() getUserContext.cache.clear?.()
// reactiveCompactOnPromptTooLong runs PostCompact hooks but not PreCompact // — both callers (here and tryReactiveCompact) run PreCompact outside so // they can merge its userDisplayMessage with PostCompact's here. This // caller additionally runs it concurrently with getCacheSharingParams. const combinedMessage = [hookResult.userDisplayMessage, outcome.result.userDisplayMessage] .filter(Boolean) .join('\n') || undefined
return { type: 'compact', compactionResult: { ...outcome.result, userDisplayMessage: combinedMessage, }, displayText: buildDisplayText(context, combinedMessage), } } finally { context.setStreamMode?.('requesting') context.setResponseLength?.(() => 0) context.onCompactProgress?.({ type: 'compact_end' }) context.setSDKStatus?.(null) }}
function buildDisplayText( context: ToolUseContext, userDisplayMessage?: string,): string { const upgradeMessage = getUpgradeMessage('tip') const expandShortcut = getShortcutDisplay( 'app:toggleTranscript', 'Global', 'ctrl+o', ) const dimmed = [ ...(context.options.verbose ? [] : [`(${expandShortcut} to see full summary)`]), ...(userDisplayMessage ? [userDisplayMessage] : []), ...(upgradeMessage ? [upgradeMessage] : []), ] return chalk.dim('Compacted ' + dimmed.join('\n'))}
async function getCacheSharingParams( context: ToolUseContext, forkContextMessages: Message[],): Promise<{ systemPrompt: SystemPrompt userContext: { [k: string]: string } systemContext: { [k: string]: string } toolUseContext: ToolUseContext forkContextMessages: Message[]}> { const appState = context.getAppState() const defaultSysPrompt = await getSystemPrompt( context.options.tools, context.options.mainLoopModel, Array.from( appState.toolPermissionContext.additionalWorkingDirectories.keys(), ), context.options.mcpClients, ) const systemPrompt = buildEffectiveSystemPrompt({ mainThreadAgentDefinition: undefined, toolUseContext: context, customSystemPrompt: context.options.customSystemPrompt, defaultSystemPrompt: defaultSysPrompt, appendSystemPrompt: context.options.appendSystemPrompt, }) const [userContext, systemContext] = await Promise.all([ getUserContext(), getSystemContext(), ]) return { systemPrompt, userContext, systemContext, toolUseContext: context, forkContextMessages, }}