fix: Move ensureWorkerRunning to start of hooks to prevent race condition

All hooks now call ensureWorkerRunning() BEFORE querying the database. This
ensures the worker's orphaned session cleanup runs before hooks check for
active sessions, preventing 404 errors when hooks try to use sessions that
don't exist in worker memory after a restart.

Hook order now:
1. ensureWorkerRunning() - starts worker, runs cleanup
2. Query DB - cleanup already marked orphaned sessions as failed
3. Use session - only valid sessions are processed

Fixed in:
- new-hook: Line 26, before DB queries
- save-hook: Line 37, before DB queries
- summary-hook: Line 24, before DB queries
- cleanup-hook: Line 50, before DB queries

This prevents the race condition where hooks would read session status before
cleanup ran, then get 404 from worker after cleanup marked sessions failed.
This commit is contained in:
Alex Newman
2025-10-19 02:01:11 -04:00
parent daf368e343
commit f849a69506
8 changed files with 54 additions and 52 deletions
+16 -17
View File
@@ -46,6 +46,12 @@ export async function cleanupHook(input?: SessionEndInput): Promise<void> {
const { session_id, reason } = input;
console.error('[claude-mem cleanup] Searching for active SDK session', { session_id, reason });
// Ensure worker is running first (runs cleanup if restarting)
const workerReady = await ensureWorkerRunning();
if (!workerReady) {
console.error('[claude-mem cleanup] Worker not available - skipping HTTP cleanup');
}
// Find active SDK session
const db = new SessionStore();
const session = db.findActiveSDKSession(session_id);
@@ -66,30 +72,23 @@ export async function cleanupHook(input?: SessionEndInput): Promise<void> {
});
// 1. Delete session via HTTP
if (session.worker_port) {
if (session.worker_port && workerReady) {
try {
// Ensure worker is running before sending cleanup request
const workerReady = await ensureWorkerRunning();
if (!workerReady) {
console.error('[claude-mem cleanup] Worker not available - skipping HTTP cleanup');
// Continue with local cleanup below
} else {
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}`, {
method: 'DELETE',
signal: AbortSignal.timeout(5000)
});
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}`, {
method: 'DELETE',
signal: AbortSignal.timeout(5000)
});
if (response.ok) {
console.error('[claude-mem cleanup] Session deleted successfully via HTTP');
} else {
console.error('[claude-mem cleanup] Failed to delete session:', await response.text());
}
if (response.ok) {
console.error('[claude-mem cleanup] Session deleted successfully via HTTP');
} else {
console.error('[claude-mem cleanup] Failed to delete session:', await response.text());
}
} catch (error: any) {
console.error('[claude-mem cleanup] HTTP DELETE error:', error.message);
}
} else {
console.error('[claude-mem cleanup] No worker port, cannot send DELETE request');
console.error('[claude-mem cleanup] No worker available or no worker port, skipping HTTP cleanup');
}
// 2. Mark session as failed in DB (if not already completed)
+8 -6
View File
@@ -21,9 +21,17 @@ export async function newHook(input?: UserPromptSubmitInput): Promise<void> {
const { session_id, cwd, prompt } = input;
const project = path.basename(cwd);
// Ensure worker is running first (runs cleanup if restarting)
const workerReady = await ensureWorkerRunning();
if (!workerReady) {
throw new Error('Worker service failed to start or become healthy');
}
const db = new SessionStore();
try {
// Check for any existing session (active, failed, or completed)
let existing = db.findActiveSDKSession(session_id);
let sessionDbId: number;
@@ -54,12 +62,6 @@ export async function newHook(input?: UserPromptSubmitInput): Promise<void> {
}
}
// Ensure worker service is running (v4.0.0 auto-start)
const workerReady = await ensureWorkerRunning();
if (!workerReady) {
throw new Error('Worker service failed to start or become healthy');
}
// Get fixed port
const port = getWorkerPort();
+6 -6
View File
@@ -33,6 +33,12 @@ export async function saveHook(input?: PostToolUseInput): Promise<void> {
return;
}
// Ensure worker is running first (runs cleanup if restarting)
const workerReady = await ensureWorkerRunning();
if (!workerReady) {
throw new Error('Worker service failed to start or become healthy');
}
const db = new SessionStore();
const session = db.findActiveSDKSession(session_id);
@@ -52,12 +58,6 @@ export async function saveHook(input?: PostToolUseInput): Promise<void> {
const promptNumber = db.getPromptCounter(session.id);
db.close();
// Ensure worker is running before sending observation
const workerReady = await ensureWorkerRunning();
if (!workerReady) {
throw new Error('Worker service failed to start or become healthy');
}
const toolStr = logger.formatTool(tool_name, tool_input);
logger.dataIn('HOOK', `PostToolUse: ${toolStr}`, {
+7 -6
View File
@@ -19,6 +19,13 @@ export async function summaryHook(input?: StopInput): Promise<void> {
}
const { session_id } = input;
// Ensure worker is running first (runs cleanup if restarting)
const workerReady = await ensureWorkerRunning();
if (!workerReady) {
throw new Error('Worker service failed to start or become healthy');
}
const db = new SessionStore();
const session = db.findActiveSDKSession(session_id);
@@ -38,12 +45,6 @@ export async function summaryHook(input?: StopInput): Promise<void> {
const promptNumber = db.getPromptCounter(session.id);
db.close();
// Ensure worker is running before requesting summary
const workerReady = await ensureWorkerRunning();
if (!workerReady) {
throw new Error('Worker service failed to start or become healthy');
}
logger.dataIn('HOOK', 'Stop: Requesting summary', {
sessionId: session.id,
workerPort: session.worker_port,