Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
9.3 kB · 251 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252/** * Conversation clearing utility. * This module has heavier dependencies and should be lazy-loaded when possible. */import { feature } from 'bun:bundle'import { randomUUID, type UUID } from 'crypto'import { getLastMainRequestId, getOriginalCwd, getSessionId, regenerateSessionId,} from '../../bootstrap/state.js'import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent,} from '../../services/analytics/index.js'import type { AppState } from '../../state/AppState.js'import { isInProcessTeammateTask } from '../../tasks/InProcessTeammateTask/types.js'import { isLocalAgentTask, type LocalAgentTaskState,} from '../../tasks/LocalAgentTask/LocalAgentTask.js'import { isLocalShellTask } from '../../tasks/LocalShellTask/guards.js'import { asAgentId } from '../../types/ids.js'import type { Message } from '../../types/message.js'import { createEmptyAttributionState } from '../../utils/commitAttribution.js'import type { FileStateCache } from '../../utils/fileStateCache.js'import { executeSessionEndHooks, getSessionEndHookTimeoutMs,} from '../../utils/hooks.js'import { logError } from '../../utils/log.js'import { clearAllPlanSlugs } from '../../utils/plans.js'import { setCwd } from '../../utils/Shell.js'import { processSessionStartHooks } from '../../utils/sessionStart.js'import { clearSessionMetadata, getAgentTranscriptPath, resetSessionFilePointer, saveWorktreeState,} from '../../utils/sessionStorage.js'import { evictTaskOutput, initTaskOutputAsSymlink,} from '../../utils/task/diskOutput.js'import { getCurrentWorktreeSession } from '../../utils/worktree.js'import { clearSessionCaches } from './caches.js'
export async function clearConversation({ setMessages, readFileState, discoveredSkillNames, loadedNestedMemoryPaths, getAppState, setAppState, setConversationId,}: { setMessages: (updater: (prev: Message[]) => Message[]) => void readFileState: FileStateCache discoveredSkillNames?: Set<string> loadedNestedMemoryPaths?: Set<string> getAppState?: () => AppState setAppState?: (f: (prev: AppState) => AppState) => void setConversationId?: (id: UUID) => void}): Promise<void> { // Execute SessionEnd hooks before clearing (bounded by // CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS, default 1.5s) const sessionEndTimeoutMs = getSessionEndHookTimeoutMs() await executeSessionEndHooks('clear', { getAppState, setAppState, signal: AbortSignal.timeout(sessionEndTimeoutMs), timeoutMs: sessionEndTimeoutMs, })
// Signal to inference that this conversation's cache can be evicted. const lastRequestId = getLastMainRequestId() if (lastRequestId) { logEvent('tengu_cache_eviction_hint', { scope: 'conversation_clear' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, last_request_id: lastRequestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }) }
// Compute preserved tasks up front so their per-agent state survives the // cache wipe below. A task is preserved unless it explicitly has // isBackgrounded === false. Main-session tasks (Ctrl+B) are preserved — // they write to an isolated per-task transcript and run under an agent // context, so they're safe across session ID regeneration. See // LocalMainSessionTask.ts startBackgroundSession. const preservedAgentIds = new Set<string>() const preservedLocalAgents: LocalAgentTaskState[] = [] const shouldKillTask = (task: AppState['tasks'][string]): boolean => 'isBackgrounded' in task && task.isBackgrounded === false if (getAppState) { for (const task of Object.values(getAppState().tasks)) { if (shouldKillTask(task)) continue if (isLocalAgentTask(task)) { preservedAgentIds.add(task.agentId) preservedLocalAgents.push(task) } else if (isInProcessTeammateTask(task)) { preservedAgentIds.add(task.identity.agentId) } } }
setMessages(() => [])
// Clear context-blocked flag so proactive ticks resume after /clear if (feature('PROACTIVE') || feature('KAIROS')) { /* eslint-disable @typescript-eslint/no-require-imports */ const { setContextBlocked } = require('../../proactive/index.js') /* eslint-enable @typescript-eslint/no-require-imports */ setContextBlocked(false) }
// Force logo re-render by updating conversationId if (setConversationId) { setConversationId(randomUUID()) }
// Clear all session-related caches. Per-agent state for preserved background // tasks (invoked skills, pending permission callbacks, dump state, cache-break // tracking) is retained so those agents keep functioning. clearSessionCaches(preservedAgentIds)
setCwd(getOriginalCwd()) readFileState.clear() discoveredSkillNames?.clear() loadedNestedMemoryPaths?.clear()
// Clean out necessary items from App State if (setAppState) { setAppState(prev => { // Partition tasks using the same predicate computed above: // kill+remove foreground tasks, preserve everything else. const nextTasks: AppState['tasks'] = {} for (const [taskId, task] of Object.entries(prev.tasks)) { if (!shouldKillTask(task)) { nextTasks[taskId] = task continue } // Foreground task: kill it and drop from state try { if (task.status === 'running') { if (isLocalShellTask(task)) { task.shellCommand?.kill() task.shellCommand?.cleanup() if (task.cleanupTimeoutId) { clearTimeout(task.cleanupTimeoutId) } } if ('abortController' in task) { task.abortController?.abort() } if ('unregisterCleanup' in task) { task.unregisterCleanup?.() } } } catch (error) { logError(error) } void evictTaskOutput(taskId) }
return { ...prev, tasks: nextTasks, attribution: createEmptyAttributionState(), // Clear standalone agent context (name/color set by /rename, /color) // so the new session doesn't display the old session's identity badge standaloneAgentContext: undefined, fileHistory: { snapshots: [], trackedFiles: new Set(), snapshotSequence: 0, }, // Reset MCP state to default to trigger re-initialization. // Preserve pluginReconnectKey so /clear doesn't cause a no-op // (it's only bumped by /reload-plugins). mcp: { clients: [], tools: [], commands: [], resources: {}, pluginReconnectKey: prev.mcp.pluginReconnectKey, }, } }) }
// Clear plan slug cache so a new plan file is used after /clear clearAllPlanSlugs()
// Clear cached session metadata (title, tag, agent name/color) // so the new session doesn't inherit the previous session's identity clearSessionMetadata()
// Generate new session ID to provide fresh state // Set the old session as parent for analytics lineage tracking regenerateSessionId({ setCurrentAsParent: true }) // Update the environment variable so subprocesses use the new session ID if (process.env.USER_TYPE === 'ant' && process.env.CLAUDE_CODE_SESSION_ID) { process.env.CLAUDE_CODE_SESSION_ID = getSessionId() } await resetSessionFilePointer()
// Preserved local_agent tasks had their TaskOutput symlink baked against the // old session ID at spawn time, but post-clear transcript writes land under // the new session directory (appendEntry re-reads getSessionId()). Re-point // the symlinks so TaskOutput reads the live file instead of a frozen pre-clear // snapshot. Only re-point running tasks — finished tasks will never write // again, so re-pointing would replace a valid symlink with a dangling one. // Main-session tasks use the same per-agent path (they write via // recordSidechainTranscript to getAgentTranscriptPath), so no special case. for (const task of preservedLocalAgents) { if (task.status !== 'running') continue void initTaskOutputAsSymlink( task.id, getAgentTranscriptPath(asAgentId(task.agentId)), ) }
// Re-persist mode and worktree state after the clear so future --resume // knows what the new post-clear session was in. clearSessionMetadata // wiped both from the cache, but the process is still in the same mode // and (if applicable) the same worktree directory. if (feature('COORDINATOR_MODE')) { /* eslint-disable @typescript-eslint/no-require-imports */ const { saveMode } = require('../../utils/sessionStorage.js') const { isCoordinatorMode, } = require('../../coordinator/coordinatorMode.js') /* eslint-enable @typescript-eslint/no-require-imports */ saveMode(isCoordinatorMode() ? 'coordinator' : 'normal') } const worktreeSession = getCurrentWorktreeSession() if (worktreeSession) { saveWorktreeState(worktreeSession) }
// Execute SessionStart hooks after clearing const hookMessages = await processSessionStartHooks('clear')
// Update messages with hook results if (hookMessages.length > 0) { setMessages(() => hookMessages) }}