Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
8.9 kB · 309 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310import type { AsyncHookJSONOutput, HookEvent, SyncHookJSONOutput,} from 'src/entrypoints/agentSdkTypes.js'import { logForDebugging } from '../debug.js'import type { ShellCommand } from '../ShellCommand.js'import { invalidateSessionEnvCache } from '../sessionEnvironment.js'import { jsonParse, jsonStringify } from '../slowOperations.js'import { emitHookResponse, startHookProgressInterval } from './hookEvents.js'
export type PendingAsyncHook = { processId: string hookId: string hookName: string hookEvent: HookEvent | 'StatusLine' | 'FileSuggestion' toolName?: string pluginId?: string startTime: number timeout: number command: string responseAttachmentSent: boolean shellCommand?: ShellCommand stopProgressInterval: () => void}
// Global registry stateconst pendingHooks = new Map<string, PendingAsyncHook>()
export function registerPendingAsyncHook({ processId, hookId, asyncResponse, hookName, hookEvent, command, shellCommand, toolName, pluginId,}: { processId: string hookId: string asyncResponse: AsyncHookJSONOutput hookName: string hookEvent: HookEvent | 'StatusLine' | 'FileSuggestion' command: string shellCommand: ShellCommand toolName?: string pluginId?: string}): void { const timeout = asyncResponse.asyncTimeout || 15000 // Default 15s logForDebugging( `Hooks: Registering async hook ${processId} (${hookName}) with timeout ${timeout}ms`, ) const stopProgressInterval = startHookProgressInterval({ hookId, hookName, hookEvent, getOutput: async () => { const taskOutput = pendingHooks.get(processId)?.shellCommand?.taskOutput if (!taskOutput) { return { stdout: '', stderr: '', output: '' } } const stdout = await taskOutput.getStdout() const stderr = taskOutput.getStderr() return { stdout, stderr, output: stdout + stderr } }, }) pendingHooks.set(processId, { processId, hookId, hookName, hookEvent, toolName, pluginId, command, startTime: Date.now(), timeout, responseAttachmentSent: false, shellCommand, stopProgressInterval, })}
export function getPendingAsyncHooks(): PendingAsyncHook[] { return Array.from(pendingHooks.values()).filter( hook => !hook.responseAttachmentSent, )}
async function finalizeHook( hook: PendingAsyncHook, exitCode: number, outcome: 'success' | 'error' | 'cancelled',): Promise<void> { hook.stopProgressInterval() const taskOutput = hook.shellCommand?.taskOutput const stdout = taskOutput ? await taskOutput.getStdout() : '' const stderr = taskOutput?.getStderr() ?? '' hook.shellCommand?.cleanup() emitHookResponse({ hookId: hook.hookId, hookName: hook.hookName, hookEvent: hook.hookEvent, output: stdout + stderr, stdout, stderr, exitCode, outcome, })}
export async function checkForAsyncHookResponses(): Promise< Array<{ processId: string response: SyncHookJSONOutput hookName: string hookEvent: HookEvent | 'StatusLine' | 'FileSuggestion' toolName?: string pluginId?: string stdout: string stderr: string exitCode?: number }>> { const responses: { processId: string response: SyncHookJSONOutput hookName: string hookEvent: HookEvent | 'StatusLine' | 'FileSuggestion' toolName?: string pluginId?: string stdout: string stderr: string exitCode?: number }[] = []
const pendingCount = pendingHooks.size logForDebugging(`Hooks: Found ${pendingCount} total hooks in registry`)
// Snapshot hooks before processing — we'll mutate the map after. const hooks = Array.from(pendingHooks.values())
const settled = await Promise.allSettled( hooks.map(async hook => { const stdout = (await hook.shellCommand?.taskOutput.getStdout()) ?? '' const stderr = hook.shellCommand?.taskOutput.getStderr() ?? '' logForDebugging( `Hooks: Checking hook ${hook.processId} (${hook.hookName}) - attachmentSent: ${hook.responseAttachmentSent}, stdout length: ${stdout.length}`, )
if (!hook.shellCommand) { logForDebugging( `Hooks: Hook ${hook.processId} has no shell command, removing from registry`, ) hook.stopProgressInterval() return { type: 'remove' as const, processId: hook.processId } }
logForDebugging(`Hooks: Hook shell status ${hook.shellCommand.status}`)
if (hook.shellCommand.status === 'killed') { logForDebugging( `Hooks: Hook ${hook.processId} is ${hook.shellCommand.status}, removing from registry`, ) hook.stopProgressInterval() hook.shellCommand.cleanup() return { type: 'remove' as const, processId: hook.processId } }
if (hook.shellCommand.status !== 'completed') { return { type: 'skip' as const } }
if (hook.responseAttachmentSent || !stdout.trim()) { logForDebugging( `Hooks: Skipping hook ${hook.processId} - already delivered/sent or no stdout`, ) hook.stopProgressInterval() return { type: 'remove' as const, processId: hook.processId } }
const lines = stdout.split('\n') logForDebugging( `Hooks: Processing ${lines.length} lines of stdout for ${hook.processId}`, )
const execResult = await hook.shellCommand.result const exitCode = execResult.code
let response: SyncHookJSONOutput = {} for (const line of lines) { if (line.trim().startsWith('{')) { logForDebugging( `Hooks: Found JSON line: ${line.trim().substring(0, 100)}...`, ) try { const parsed = jsonParse(line.trim()) if (!('async' in parsed)) { logForDebugging( `Hooks: Found sync response from ${hook.processId}: ${jsonStringify(parsed)}`, ) response = parsed break } } catch { logForDebugging( `Hooks: Failed to parse JSON from ${hook.processId}: ${line.trim()}`, ) } } }
hook.responseAttachmentSent = true await finalizeHook(hook, exitCode, exitCode === 0 ? 'success' : 'error')
return { type: 'response' as const, processId: hook.processId, isSessionStart: hook.hookEvent === 'SessionStart', payload: { processId: hook.processId, response, hookName: hook.hookName, hookEvent: hook.hookEvent, toolName: hook.toolName, pluginId: hook.pluginId, stdout, stderr, exitCode, }, } }), )
// allSettled — isolate failures so one throwing callback doesn't orphan // already-applied side effects (responseAttachmentSent, finalizeHook) from others. let sessionStartCompleted = false for (const s of settled) { if (s.status !== 'fulfilled') { logForDebugging( `Hooks: checkForAsyncHookResponses callback rejected: ${s.reason}`, { level: 'error' }, ) continue } const r = s.value if (r.type === 'remove') { pendingHooks.delete(r.processId) } else if (r.type === 'response') { responses.push(r.payload) pendingHooks.delete(r.processId) if (r.isSessionStart) sessionStartCompleted = true } }
if (sessionStartCompleted) { logForDebugging( `Invalidating session env cache after SessionStart hook completed`, ) invalidateSessionEnvCache() }
logForDebugging( `Hooks: checkForNewResponses returning ${responses.length} responses`, ) return responses}
export function removeDeliveredAsyncHooks(processIds: string[]): void { for (const processId of processIds) { const hook = pendingHooks.get(processId) if (hook && hook.responseAttachmentSent) { logForDebugging(`Hooks: Removing delivered hook ${processId}`) hook.stopProgressInterval() pendingHooks.delete(processId) } }}
export async function finalizePendingAsyncHooks(): Promise<void> { const hooks = Array.from(pendingHooks.values()) await Promise.all( hooks.map(async hook => { if (hook.shellCommand?.status === 'completed') { const result = await hook.shellCommand.result await finalizeHook( hook, result.code, result.code === 0 ? 'success' : 'error', ) } else { if (hook.shellCommand && hook.shellCommand.status !== 'killed') { hook.shellCommand.kill() } await finalizeHook(hook, 1, 'cancelled') } }), ) pendingHooks.clear()}
// Test utility function to clear all hooksexport function clearAllAsyncHooks(): void { for (const hook of pendingHooks.values()) { hook.stopProgressInterval() } pendingHooks.clear()}