本章总览
/compact 与 /memory 分别管理「对话上下文生命周期」与「跨 session 持久记忆」:compact 是 local 命令,调用 services/compact/ 压缩 transcript;memory 是 local-jsx 命令,打开 MemoryFileSelector 编辑 CLAUDE.md 系列文件。compact 在 BRIDGE_SAFE_COMMANDS 白名单内;memory 需本地编辑器。本章对比两条路径的 call 签名、与 compact 服务的边界、以及 session memory 优先策略。
学完本章你应该能
- 说明 compact index.ts 的 isEnabled 与 supportsNonInteractive
- 追踪 compact.ts call 的三段 fallback:session memory → reactive → legacy
- 解释 getMessagesAfterCompactBoundary 与 snip 消息投影
- 描述 memory.tsx 的文件创建与 editFileInEditor 流程
- 理解 compact 结果如何经 processSlashCommand 变为新 messages
核心概念(先读懂这些)
compact 命令 vs services/compact 服务
commands/compact/compact.ts 是薄 orchestration:调 trySessionMemoryCompaction、microcompactMessages、compactConversation、reactiveCompact。services/compact/compact.ts 含 fork 摘要核心。读 autocompact 阈值应去 services 章;读 /compact 用户参数与 displayText 读本章。
custom instructions 跳过 session memory 路径
trySessionMemoryCompaction 不支持 custom instructions;用户 /compact focus on API changes 直接走 traditional/reactive 路径。无参 /compact 优先尝试廉价 session memory compact。
memory 文件与 getMemoryFiles 缓存
call 入口 clearMemoryFileCaches + await getMemoryFiles() 避免 Suspense fallback flash。Memory 路径来自 utils/claudemd 扫描 project + user + local 层级 CLAUDE.md。
建议学习步骤
- 阅读源码块 A:compact index.ts
- 阅读源码块 B:compact call 主流程
- 阅读源码块 C:compactViaReactive
- 阅读源码块 D:buildDisplayText 与 getCacheSharingParams
- 阅读源码块 E:memory index 与 call
- 阅读源码块 F:MemoryCommand 文件编辑
- 对照 processSlashCommand compact 分支
常见误区
注意
DISABLE_COMPACT env 使 isEnabled false,命令从池移除
注意
abort 时统一 "Compaction canceled." 文案
注意
memory writeFile wx flag:EEXIST 忽略,保留已有内容
在架构中的位置
两条命令解决不同痛点:
| 命令 | 问题 | 写入目标 |
|---|---|---|
| /compact | context window 将满 | 替换 messages[],插入 compact boundary |
| /memory | 跨 session 记住偏好 | ~/.claude/CLAUDE.md、项目 CLAUDE.md 等 |
/compact [instructions]
→ processSlashCommand local 分支
→ compact.ts call(args, context)
→ { type:'compact', compactionResult, displayText }
→ buildPostCompactMessages → setMessages
/memory
→ local-jsx → MemoryFileSelector → editFileInEditor
→ onDone system 消息(路径 + $EDITOR 提示)
Autocompact(services)与 manual /compact 共用 compactConversation;manual 可带 customInstructions。
/compact:index.ts 元数据
commands/compact/index.ts:
type: 'local'— 非 JSX,直接 callsupportsNonInteractive: true— SDK/headless 可调用- isEnabled:
!isEnvTruthy(process.env.DISABLE_COMPACT) argumentHint: '<optional custom summarization instructions>'- description 说明保留 summary 清除 history
DISABLE_COMPACT 与 DISABLE_AUTO_COMPACT 不同:前者关掉命令注册(含 manual),后者仅关 autocompact(services 层)。
compact 在 BRIDGE_SAFE_COMMANDS — mobile 可 mid-session 压缩。
源码引用: src/commands/compact/index.ts · 第 1–15 行(共 16 行)
1| import type { Command } from '../../commands.js'
2| import { isEnvTruthy } from '../../utils/envUtils.js'
3|
4| const compact = {
5| type: 'local',
6| name: 'compact',
7| description:
8| 'Clear conversation history but keep a summary in context. Optional: /compact [instructions for summarization]',
9| isEnabled: () => !isEnvTruthy(process.env.DISABLE_COMPACT),
10| supportsNonInteractive: true,
11| argumentHint: '<optional custom summarization instructions>',
12| load: () => import('./compact.js'),
13| } satisfies Command
14|
15| export default compact
/compact:call 主流程
compact.ts export call 步骤:
- messages = getMessagesAfterCompactBoundary(messages) — REPL snip 保留 UI scrollback 但 compact 模型不应看到已 snip 内容
- 空 messages → throw 'No messages to compact'
- customInstructions = args.trim()
- 若无 customInstructions → trySessionMemoryCompaction 成功则:
- clear getUserContext cache
- runPostCompactCleanup
- notifyCompaction(PROMPT_CACHE_BREAK_DETECTION)
- markPostCompaction + suppressCompactWarning
- return compact result + buildDisplayText
- reactiveCompact?.isReactiveOnlyMode() → compactViaReactive
- 否则 legacy:microcompactMessages → compactConversation(fork 摘要)
- setLastSummarizedMessageId(undefined) — legacy 替换全部 UUID
- catch:abort → canceled;精确匹配 NOT_ENOUGH / INCOMPLETE 错误码
返回 LocalCommandResult type:'compact',processSlashCommand 负责 apply。
源码引用: src/commands/compact/compact.ts · 第 40–137 行(共 288 行)
40| export 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| }
compactViaReactive 与 hooks
REACTIVE_COMPACT feature 下 compactViaReactive:
并发 executePreCompactHooks(trigger:'manual')与 getCacheSharingParams — 注释:hooks 子进程与 system prompt 构建互不依赖。
流程:
- setSDKStatus('compacting')、onCompactProgress 事件
- reactiveCompactOnPromptTooLong 带 mergedInstructions
- 失败 reason 映射到标准 ERROR_MESSAGE_*
- 成功:setLastSummarizedMessageId(undefined)、runPostCompactCleanup、suppressCompactWarning
- 合并 pre/ post hook userDisplayMessage
finally 恢复 streamMode、compact_end、SDKStatus null。
与 autocompact reactive 路径共享 outcome 语义,但 manual /compact 显式 trigger:'manual'。
源码引用: src/commands/compact/compact.ts · 第 139–228 行(共 288 行)
139| async 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| }
displayText 与 cache 参数
buildDisplayText 组装用户可见 dim 文本:
- 前缀 "Compacted"
- 非 verbose 时提示 ctrl+o(toggleTranscript)展开完整摘要
- 附加 hook userDisplayMessage
- getUpgradeMessage('tip') 上下文升级提示(1M 等)
getCacheSharingParams 为 compact fork 构建与主 thread 一致的 cache 上下文:
- getSystemPrompt + buildEffectiveSystemPrompt
- getUserContext / getSystemContext parallel
- forkContextMessages 传入摘要 Agent
这保证 compact 子 Agent 看到与主 loop 对齐的 tool 列表与 system prompt,摘要质量稳定。
源码引用: src/commands/compact/compact.ts · 第 230–287 行(共 288 行)
230| function 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|
250| async 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| }
/memory:index 与 call
commands/memory/index.ts 最小定义:
const memory: Command = {
type: 'local-jsx',
name: 'memory',
description: 'Edit Claude memory files',
load: () => import('./memory.js'),
}
无 immediate、无 argumentHint — 纯菜单交互。
memory.tsx call:
- clearMemoryFileCaches()
- await getMemoryFiles() — prime 缓存
- return <MemoryCommand onDone={onDone} />
Suspense 包裹 MemoryFileSelector;await getMemoryFiles 避免首次打开 fallback flash。
源码引用: src/commands/memory/index.ts · 第 1–10 行(共 11 行)
1| import type { Command } from '../../commands.js'
2|
3| const memory: Command = {
4| type: 'local-jsx',
5| name: 'memory',
6| description: 'Edit Claude memory files',
7| load: () => import('./memory.js'),
8| }
9|
10| export default memory
源码引用: src/commands/memory/memory.tsx · 第 83–89 行(共 103 行)
83| />
84| </React.Suspense>
85|
86| <Box marginTop={1}>
87| <Text dimColor>
88| Learn more: <Link url="https://code.claude.com/docs/en/memory" />
89| </Text>
MemoryCommand:编辑流程
handleSelectMemoryFile(memoryPath) 异步:
- 若路径在 claude config home 下 → mkdir recursive
- writeFile(memoryPath, '', { flag: 'wx' }) 创建空文件;EEXIST 忽略
- editFileInEditor(memoryPath) — spawn $VISUAL/$EDITOR
- 构建 editorHint(显示使用的 env var)
- onDone
Opened memory file at ${relativePath}+ hint,display:'system'
Dialog title="Memory" color="remember";底部 Link 指向 docs memory 页。
handleCancel → 'Cancelled memory editing' system 消息。
依赖 utils/claudemd getMemoryFiles、components/memory/MemoryFileSelector 列表 UI。
源码引用: src/commands/memory/memory.tsx · 第 14–82 行(共 103 行)
14|
15| function MemoryCommand({
16| onDone,
17| }: {
18| onDone: (
19| result?: string,
20| options?: { display?: CommandResultDisplay },
21| ) => void
22| }): React.ReactNode {
23| const handleSelectMemoryFile = async (memoryPath: string) => {
24| try {
25| // Create claude directory if it doesn't exist (idempotent with recursive)
26| if (memoryPath.includes(getClaudeConfigHomeDir())) {
27| await mkdir(getClaudeConfigHomeDir(), { recursive: true })
28| }
29|
30| // Create file if it doesn't exist (wx flag fails if file exists,
31| // which we catch to preserve existing content)
32| try {
33| await writeFile(memoryPath, '', { encoding: 'utf8', flag: 'wx' })
34| } catch (e: unknown) {
35| if (getErrnoCode(e) !== 'EEXIST') {
36| throw e
37| }
38| }
39|
40| await editFileInEditor(memoryPath)
41|
42| // Determine which environment variable controls the editor
43| let editorSource = 'default'
44| let editorValue = ''
45| if (process.env.VISUAL) {
46| editorSource = '$VISUAL'
47| editorValue = process.env.VISUAL
48| } else if (process.env.EDITOR) {
49| editorSource = '$EDITOR'
50| editorValue = process.env.EDITOR
51| }
52|
53| const editorInfo =
54| editorSource !== 'default'
55| ? `Using ${editorSource}="${editorValue}".`
56| : ''
57|
58| const editorHint = editorInfo
59| ? `> ${editorInfo} To change editor, set $EDITOR or $VISUAL environment variable.`
60| : `> To use a different editor, set the $EDITOR or $VISUAL environment variable.`
61|
62| onDone(
63| `Opened memory file at ${getRelativeMemoryPath(memoryPath)}\n\n${editorHint}`,
64| { display: 'system' },
65| )
66| } catch (error) {
67| logError(error)
68| onDone(`Error opening memory file: ${error}`)
69| }
70| }
71|
72| const handleCancel = () => {
73| onDone('Cancelled memory editing', { display: 'system' })
74| }
75|
76| return (
77| <Dialog title="Memory" onCancel={handleCancel} color="remember">
78| <Box flexDirection="column">
79| <React.Suspense fallback={null}>
80| <MemoryFileSelector
81| onSelect={handleSelectMemoryFile}
82| onCancel={handleCancel}
processSlashCommand 如何应用 compact
local 命令 compact 返回后,processSlashCommand:
- 识别 result.type === 'compact'
- buildPostCompactMessages(compactionResult, ...) 构造新 transcript
- resetMicrocompactState — processSlashCommand 统一调用(reactive 路径注释说明)
- setMessages 替换;可能 enqueue 后续 query
Compact boundary 消息类型 isCompactBoundaryMessage 供 session JSONL loader 修复 parentUuid。
/memory 不修改 messages 结构,仅 system 消息记录操作;真正内容在 CLAUDE.md,下轮 query getUserContext 读取注入 system。
与 services/SessionMemory/ 交叉:session memory compact 写结构化记忆;/memory 编辑源文件——二者互补。
源码引用: src/utils/processUserInput/processSlashCommand.tsx · 第 14–16 行(共 1263 行)
14| getCommandName,
15| hasCommand,
16| type PromptCommand,
本章小结与延伸
/compact 缩上下文,/memory 扩持久知识。回到 command-registry 查命令池如何注册二者。 继续学习: