compact.ts
commands/compact/compact.ts
288
Lines
10079
Bytes
1
Exports
22
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file lives in the command layer. It likely turns a user action into concrete program behavior.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with commands, compaction. It contains 288 lines, 22 detected imports, and 1 detected exports.
Important relationships
Detected exports
call
Keywords
contextcompactmessagescompactionservicescustominstructionstoolusecontextthrowuserdisplaymessageoptions
Detected imports
bun:bundlechalksrc/bootstrap/state.js../../constants/prompts.js../../context.js../../keybindings/shortcutFormat.js../../services/api/promptCacheBreakDetection.js../../services/compact/compact.js../../services/compact/compactWarningState.js../../services/compact/microCompact.js../../services/compact/postCompactCleanup.js../../services/compact/sessionMemoryCompact.js../../services/SessionMemory/sessionMemoryUtils.js../../Tool.js../../types/command.js../../types/message.js../../utils/errors.js../../utils/hooks.js../../utils/log.js../../utils/messages.js../../utils/model/contextWindowUpgradeCheck.js../../utils/systemPrompt.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
import { 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,
}
}