418e38ee46
Reduce timeouts to eliminate 10-30s startup delay when worker is dead (common on WSL2 after hibernate). Add stale PID detection, graceful error handling across all handlers, and error classification that distinguishes worker unavailability from handler bugs. - HEALTH_CHECK 30s→3s, new POST_SPAWN_WAIT (5s), PORT_IN_USE_WAIT (3s) - isProcessAlive() with EPERM handling, cleanStalePidFile() - getPluginVersion() try-catch for shutdown race (#1042) - isWorkerUnavailableError: transport+5xx+429→exit 0, 4xx→exit 2 - No-op handler for unknown event types (#984) - Wrap all handler fetch calls in try-catch for graceful degradation - CLAUDE_MEM_HEALTH_TIMEOUT_MS env var override with validation
81 lines
3.2 KiB
TypeScript
81 lines
3.2 KiB
TypeScript
/**
|
|
* Observation Handler - PostToolUse
|
|
*
|
|
* Extracted from save-hook.ts - sends tool usage to worker for storage.
|
|
*/
|
|
|
|
import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js';
|
|
import { ensureWorkerRunning, getWorkerPort } from '../../shared/worker-utils.js';
|
|
import { logger } from '../../utils/logger.js';
|
|
import { HOOK_EXIT_CODES } from '../../shared/hook-constants.js';
|
|
import { isProjectExcluded } from '../../utils/project-filter.js';
|
|
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
|
|
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
|
|
|
|
export const observationHandler: EventHandler = {
|
|
async execute(input: NormalizedHookInput): Promise<HookResult> {
|
|
// Ensure worker is running before any other logic
|
|
const workerReady = await ensureWorkerRunning();
|
|
if (!workerReady) {
|
|
// Worker not available - skip observation gracefully
|
|
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
|
|
}
|
|
|
|
const { sessionId, cwd, toolName, toolInput, toolResponse } = input;
|
|
|
|
if (!toolName) {
|
|
throw new Error('observationHandler requires toolName');
|
|
}
|
|
|
|
const port = getWorkerPort();
|
|
|
|
const toolStr = logger.formatTool(toolName, toolInput);
|
|
|
|
logger.dataIn('HOOK', `PostToolUse: ${toolStr}`, {
|
|
workerPort: port
|
|
});
|
|
|
|
// Validate required fields before sending to worker
|
|
if (!cwd) {
|
|
throw new Error(`Missing cwd in PostToolUse hook input for session ${sessionId}, tool ${toolName}`);
|
|
}
|
|
|
|
// Check if project is excluded from tracking
|
|
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
|
|
if (isProjectExcluded(cwd, settings.CLAUDE_MEM_EXCLUDED_PROJECTS)) {
|
|
logger.debug('HOOK', 'Project excluded from tracking, skipping observation', { cwd, toolName });
|
|
return { continue: true, suppressOutput: true };
|
|
}
|
|
|
|
// Send to worker - worker handles privacy check and database operations
|
|
try {
|
|
const response = await fetch(`http://127.0.0.1:${port}/api/sessions/observations`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
contentSessionId: sessionId,
|
|
tool_name: toolName,
|
|
tool_input: toolInput,
|
|
tool_response: toolResponse,
|
|
cwd
|
|
})
|
|
// Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
// Log but don't throw — observation storage failure should not block tool use
|
|
logger.warn('HOOK', 'Observation storage failed, skipping', { status: response.status, toolName });
|
|
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
|
|
}
|
|
|
|
logger.debug('HOOK', 'Observation sent successfully', { toolName });
|
|
} catch (error) {
|
|
// Worker unreachable — skip observation gracefully
|
|
logger.warn('HOOK', 'Observation fetch error, skipping', { error: error instanceof Error ? error.message : String(error) });
|
|
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
|
|
}
|
|
|
|
return { continue: true, suppressOutput: true };
|
|
}
|
|
};
|