source dump of claude code
at main 251 lines 9.3 kB view raw
1/** 2 * Conversation clearing utility. 3 * This module has heavier dependencies and should be lazy-loaded when possible. 4 */ 5import { feature } from 'bun:bundle' 6import { randomUUID, type UUID } from 'crypto' 7import { 8 getLastMainRequestId, 9 getOriginalCwd, 10 getSessionId, 11 regenerateSessionId, 12} from '../../bootstrap/state.js' 13import { 14 type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 15 logEvent, 16} from '../../services/analytics/index.js' 17import type { AppState } from '../../state/AppState.js' 18import { isInProcessTeammateTask } from '../../tasks/InProcessTeammateTask/types.js' 19import { 20 isLocalAgentTask, 21 type LocalAgentTaskState, 22} from '../../tasks/LocalAgentTask/LocalAgentTask.js' 23import { isLocalShellTask } from '../../tasks/LocalShellTask/guards.js' 24import { asAgentId } from '../../types/ids.js' 25import type { Message } from '../../types/message.js' 26import { createEmptyAttributionState } from '../../utils/commitAttribution.js' 27import type { FileStateCache } from '../../utils/fileStateCache.js' 28import { 29 executeSessionEndHooks, 30 getSessionEndHookTimeoutMs, 31} from '../../utils/hooks.js' 32import { logError } from '../../utils/log.js' 33import { clearAllPlanSlugs } from '../../utils/plans.js' 34import { setCwd } from '../../utils/Shell.js' 35import { processSessionStartHooks } from '../../utils/sessionStart.js' 36import { 37 clearSessionMetadata, 38 getAgentTranscriptPath, 39 resetSessionFilePointer, 40 saveWorktreeState, 41} from '../../utils/sessionStorage.js' 42import { 43 evictTaskOutput, 44 initTaskOutputAsSymlink, 45} from '../../utils/task/diskOutput.js' 46import { getCurrentWorktreeSession } from '../../utils/worktree.js' 47import { clearSessionCaches } from './caches.js' 48 49export async function clearConversation({ 50 setMessages, 51 readFileState, 52 discoveredSkillNames, 53 loadedNestedMemoryPaths, 54 getAppState, 55 setAppState, 56 setConversationId, 57}: { 58 setMessages: (updater: (prev: Message[]) => Message[]) => void 59 readFileState: FileStateCache 60 discoveredSkillNames?: Set<string> 61 loadedNestedMemoryPaths?: Set<string> 62 getAppState?: () => AppState 63 setAppState?: (f: (prev: AppState) => AppState) => void 64 setConversationId?: (id: UUID) => void 65}): Promise<void> { 66 // Execute SessionEnd hooks before clearing (bounded by 67 // CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS, default 1.5s) 68 const sessionEndTimeoutMs = getSessionEndHookTimeoutMs() 69 await executeSessionEndHooks('clear', { 70 getAppState, 71 setAppState, 72 signal: AbortSignal.timeout(sessionEndTimeoutMs), 73 timeoutMs: sessionEndTimeoutMs, 74 }) 75 76 // Signal to inference that this conversation's cache can be evicted. 77 const lastRequestId = getLastMainRequestId() 78 if (lastRequestId) { 79 logEvent('tengu_cache_eviction_hint', { 80 scope: 81 'conversation_clear' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 82 last_request_id: 83 lastRequestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 84 }) 85 } 86 87 // Compute preserved tasks up front so their per-agent state survives the 88 // cache wipe below. A task is preserved unless it explicitly has 89 // isBackgrounded === false. Main-session tasks (Ctrl+B) are preserved — 90 // they write to an isolated per-task transcript and run under an agent 91 // context, so they're safe across session ID regeneration. See 92 // LocalMainSessionTask.ts startBackgroundSession. 93 const preservedAgentIds = new Set<string>() 94 const preservedLocalAgents: LocalAgentTaskState[] = [] 95 const shouldKillTask = (task: AppState['tasks'][string]): boolean => 96 'isBackgrounded' in task && task.isBackgrounded === false 97 if (getAppState) { 98 for (const task of Object.values(getAppState().tasks)) { 99 if (shouldKillTask(task)) continue 100 if (isLocalAgentTask(task)) { 101 preservedAgentIds.add(task.agentId) 102 preservedLocalAgents.push(task) 103 } else if (isInProcessTeammateTask(task)) { 104 preservedAgentIds.add(task.identity.agentId) 105 } 106 } 107 } 108 109 setMessages(() => []) 110 111 // Clear context-blocked flag so proactive ticks resume after /clear 112 if (feature('PROACTIVE') || feature('KAIROS')) { 113 /* eslint-disable @typescript-eslint/no-require-imports */ 114 const { setContextBlocked } = require('../../proactive/index.js') 115 /* eslint-enable @typescript-eslint/no-require-imports */ 116 setContextBlocked(false) 117 } 118 119 // Force logo re-render by updating conversationId 120 if (setConversationId) { 121 setConversationId(randomUUID()) 122 } 123 124 // Clear all session-related caches. Per-agent state for preserved background 125 // tasks (invoked skills, pending permission callbacks, dump state, cache-break 126 // tracking) is retained so those agents keep functioning. 127 clearSessionCaches(preservedAgentIds) 128 129 setCwd(getOriginalCwd()) 130 readFileState.clear() 131 discoveredSkillNames?.clear() 132 loadedNestedMemoryPaths?.clear() 133 134 // Clean out necessary items from App State 135 if (setAppState) { 136 setAppState(prev => { 137 // Partition tasks using the same predicate computed above: 138 // kill+remove foreground tasks, preserve everything else. 139 const nextTasks: AppState['tasks'] = {} 140 for (const [taskId, task] of Object.entries(prev.tasks)) { 141 if (!shouldKillTask(task)) { 142 nextTasks[taskId] = task 143 continue 144 } 145 // Foreground task: kill it and drop from state 146 try { 147 if (task.status === 'running') { 148 if (isLocalShellTask(task)) { 149 task.shellCommand?.kill() 150 task.shellCommand?.cleanup() 151 if (task.cleanupTimeoutId) { 152 clearTimeout(task.cleanupTimeoutId) 153 } 154 } 155 if ('abortController' in task) { 156 task.abortController?.abort() 157 } 158 if ('unregisterCleanup' in task) { 159 task.unregisterCleanup?.() 160 } 161 } 162 } catch (error) { 163 logError(error) 164 } 165 void evictTaskOutput(taskId) 166 } 167 168 return { 169 ...prev, 170 tasks: nextTasks, 171 attribution: createEmptyAttributionState(), 172 // Clear standalone agent context (name/color set by /rename, /color) 173 // so the new session doesn't display the old session's identity badge 174 standaloneAgentContext: undefined, 175 fileHistory: { 176 snapshots: [], 177 trackedFiles: new Set(), 178 snapshotSequence: 0, 179 }, 180 // Reset MCP state to default to trigger re-initialization. 181 // Preserve pluginReconnectKey so /clear doesn't cause a no-op 182 // (it's only bumped by /reload-plugins). 183 mcp: { 184 clients: [], 185 tools: [], 186 commands: [], 187 resources: {}, 188 pluginReconnectKey: prev.mcp.pluginReconnectKey, 189 }, 190 } 191 }) 192 } 193 194 // Clear plan slug cache so a new plan file is used after /clear 195 clearAllPlanSlugs() 196 197 // Clear cached session metadata (title, tag, agent name/color) 198 // so the new session doesn't inherit the previous session's identity 199 clearSessionMetadata() 200 201 // Generate new session ID to provide fresh state 202 // Set the old session as parent for analytics lineage tracking 203 regenerateSessionId({ setCurrentAsParent: true }) 204 // Update the environment variable so subprocesses use the new session ID 205 if (process.env.USER_TYPE === 'ant' && process.env.CLAUDE_CODE_SESSION_ID) { 206 process.env.CLAUDE_CODE_SESSION_ID = getSessionId() 207 } 208 await resetSessionFilePointer() 209 210 // Preserved local_agent tasks had their TaskOutput symlink baked against the 211 // old session ID at spawn time, but post-clear transcript writes land under 212 // the new session directory (appendEntry re-reads getSessionId()). Re-point 213 // the symlinks so TaskOutput reads the live file instead of a frozen pre-clear 214 // snapshot. Only re-point running tasks — finished tasks will never write 215 // again, so re-pointing would replace a valid symlink with a dangling one. 216 // Main-session tasks use the same per-agent path (they write via 217 // recordSidechainTranscript to getAgentTranscriptPath), so no special case. 218 for (const task of preservedLocalAgents) { 219 if (task.status !== 'running') continue 220 void initTaskOutputAsSymlink( 221 task.id, 222 getAgentTranscriptPath(asAgentId(task.agentId)), 223 ) 224 } 225 226 // Re-persist mode and worktree state after the clear so future --resume 227 // knows what the new post-clear session was in. clearSessionMetadata 228 // wiped both from the cache, but the process is still in the same mode 229 // and (if applicable) the same worktree directory. 230 if (feature('COORDINATOR_MODE')) { 231 /* eslint-disable @typescript-eslint/no-require-imports */ 232 const { saveMode } = require('../../utils/sessionStorage.js') 233 const { 234 isCoordinatorMode, 235 } = require('../../coordinator/coordinatorMode.js') 236 /* eslint-enable @typescript-eslint/no-require-imports */ 237 saveMode(isCoordinatorMode() ? 'coordinator' : 'normal') 238 } 239 const worktreeSession = getCurrentWorktreeSession() 240 if (worktreeSession) { 241 saveWorktreeState(worktreeSession) 242 } 243 244 // Execute SessionStart hooks after clearing 245 const hookMessages = await processSessionStartHooks('clear') 246 247 // Update messages with hook results 248 if (hookMessages.length > 0) { 249 setMessages(() => hookMessages) 250 } 251}