init claude-code
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Command } from '../../commands.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
|
||||
const compact = {
|
||||
type: 'local',
|
||||
name: 'compact',
|
||||
description:
|
||||
'Clear conversation history but keep a summary in context. Optional: /compact [instructions for summarization]',
|
||||
isEnabled: () => !isEnvTruthy(process.env.DISABLE_COMPACT),
|
||||
supportsNonInteractive: true,
|
||||
argumentHint: '<optional custom summarization instructions>',
|
||||
load: () => import('./compact.js'),
|
||||
} satisfies Command
|
||||
|
||||
export default compact
|
||||
Reference in New Issue
Block a user