Refactor hooks and worker service for improved error handling and initialization
- Removed try-catch blocks in new-hook, save-hook, and summary-hook for cleaner flow. - Enhanced error handling in save and summary hooks to throw errors instead of logging and returning. - Introduced ensureWorkerRunning utility to manage worker service lifecycle and health checks. - Replaced dynamic port allocation with a fixed port for the worker service. - Simplified path management and removed unused port allocator utility. - Added database schema initialization for fresh installations and improved migration handling.
This commit is contained in:
+1
-58
@@ -1,8 +1,6 @@
|
||||
import path from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { spawn } from 'child_process';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { getWorkerPortFilePath, getPackageRoot } from '../shared/paths.js';
|
||||
import { ensureWorkerRunning } from '../shared/worker-utils.js';
|
||||
|
||||
export interface SessionStartInput {
|
||||
session_id?: string;
|
||||
@@ -13,61 +11,6 @@ export interface SessionStartInput {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure worker service is running
|
||||
* Auto-starts worker if not running (v4.0.0 feature)
|
||||
*/
|
||||
function ensureWorkerRunning(): void {
|
||||
try {
|
||||
const portFile = getWorkerPortFilePath();
|
||||
|
||||
// Check if worker is already running
|
||||
if (existsSync(portFile)) {
|
||||
// Worker appears to be running (port file exists)
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('[claude-mem] Worker not running, starting...');
|
||||
|
||||
// Find worker service path
|
||||
const packageRoot = getPackageRoot();
|
||||
const workerPath = path.join(packageRoot, 'dist', 'worker-service.cjs');
|
||||
|
||||
if (!existsSync(workerPath)) {
|
||||
console.error(`[claude-mem] Worker service not found at ${workerPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to start with PM2 first (preferred for production)
|
||||
const ecosystemPath = path.join(packageRoot, 'ecosystem.config.cjs');
|
||||
if (existsSync(ecosystemPath)) {
|
||||
try {
|
||||
spawn('pm2', ['start', ecosystemPath], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
cwd: packageRoot
|
||||
}).unref();
|
||||
console.error('[claude-mem] Worker started with PM2');
|
||||
return;
|
||||
} catch (pm2Error) {
|
||||
console.error('[claude-mem] PM2 not available, using direct spawn');
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: spawn worker directly
|
||||
spawn('node', [workerPath], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
env: { ...process.env, NODE_ENV: 'production' }
|
||||
}).unref();
|
||||
console.error('[claude-mem] Worker started in background');
|
||||
|
||||
} catch (error: any) {
|
||||
// Don't fail the hook if worker start fails
|
||||
console.error(`[claude-mem] Failed to start worker: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Context Hook - SessionStart
|
||||
* Shows user what happened in recent sessions
|
||||
|
||||
+10
-33
@@ -1,7 +1,7 @@
|
||||
import path from 'path';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
import { getWorkerPortFilePath } from '../shared/paths.js';
|
||||
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
||||
|
||||
export interface UserPromptSubmitInput {
|
||||
session_id: string;
|
||||
@@ -10,26 +10,6 @@ export interface UserPromptSubmitInput {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get worker service port from file
|
||||
*/
|
||||
async function getWorkerPort(): Promise<number | null> {
|
||||
const { readFileSync, existsSync } = await import('fs');
|
||||
|
||||
const portFile = getWorkerPortFilePath();
|
||||
|
||||
if (!existsSync(portFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const portStr = readFileSync(portFile, 'utf8').trim();
|
||||
return parseInt(portStr, 10);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* New Hook - UserPromptSubmit
|
||||
* Initializes SDK memory session via HTTP POST to worker service
|
||||
@@ -74,14 +54,15 @@ export async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// Find worker service port
|
||||
const port = await getWorkerPort();
|
||||
if (!port) {
|
||||
console.error('[new-hook] Worker service not running. Start with: npm run worker:start');
|
||||
console.log(createHookResponse('UserPromptSubmit', true)); // Don't block Claude
|
||||
return;
|
||||
// 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();
|
||||
|
||||
// Only initialize worker on new sessions
|
||||
if (isNewSession) {
|
||||
// Initialize session via HTTP
|
||||
@@ -93,16 +74,12 @@ export async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[new-hook] Failed to init session:', await response.text());
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to initialize session: ${response.status} ${errorText}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(createHookResponse('UserPromptSubmit', true));
|
||||
} catch (error: any) {
|
||||
console.error('[new-hook] FATAL ERROR:', error.message);
|
||||
console.error('[new-hook] Stack:', error.stack);
|
||||
console.error('[new-hook] Full error:', JSON.stringify(error, Object.getOwnPropertyNames(error)));
|
||||
console.log(createHookResponse('UserPromptSubmit', true)); // Don't block Claude
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
|
||||
+27
-32
@@ -44,8 +44,7 @@ export async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
if (!session.worker_port) {
|
||||
db.close();
|
||||
logger.error('HOOK', 'No worker port for session', { sessionId: session.id });
|
||||
console.log(createHookResponse('PostToolUse', true));
|
||||
return;
|
||||
throw new Error('No worker port for session - session may not be properly initialized');
|
||||
}
|
||||
|
||||
// Get current prompt number for this session
|
||||
@@ -54,36 +53,32 @@ export async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
|
||||
const toolStr = logger.formatTool(tool_name, tool_input);
|
||||
|
||||
try {
|
||||
logger.dataIn('HOOK', `PostToolUse: ${toolStr}`, {
|
||||
logger.dataIn('HOOK', `PostToolUse: ${toolStr}`, {
|
||||
sessionId: session.id,
|
||||
workerPort: session.worker_port
|
||||
});
|
||||
|
||||
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}/observations`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tool_name,
|
||||
tool_input: tool_input !== undefined ? JSON.stringify(tool_input) : '{}',
|
||||
tool_output: tool_output !== undefined ? JSON.stringify(tool_output) : '{}',
|
||||
prompt_number: promptNumber
|
||||
}),
|
||||
signal: AbortSignal.timeout(2000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
logger.failure('HOOK', 'Failed to send observation', {
|
||||
sessionId: session.id,
|
||||
workerPort: session.worker_port
|
||||
});
|
||||
|
||||
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}/observations`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tool_name,
|
||||
tool_input: tool_input !== undefined ? JSON.stringify(tool_input) : '{}',
|
||||
tool_output: tool_output !== undefined ? JSON.stringify(tool_output) : '{}',
|
||||
prompt_number: promptNumber
|
||||
}),
|
||||
signal: AbortSignal.timeout(2000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
logger.failure('HOOK', 'Failed to send observation', {
|
||||
sessionId: session.id,
|
||||
status: response.status
|
||||
}, errorText);
|
||||
} else {
|
||||
logger.debug('HOOK', 'Observation sent successfully', { sessionId: session.id, toolName: tool_name });
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.failure('HOOK', 'Error sending observation', { sessionId: session.id }, error);
|
||||
} finally {
|
||||
console.log(createHookResponse('PostToolUse', true));
|
||||
status: response.status
|
||||
}, errorText);
|
||||
throw new Error(`Failed to send observation to worker: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
logger.debug('HOOK', 'Observation sent successfully', { sessionId: session.id, toolName: tool_name });
|
||||
console.log(createHookResponse('PostToolUse', true));
|
||||
}
|
||||
|
||||
+23
-28
@@ -30,40 +30,35 @@ export async function summaryHook(input?: StopInput): Promise<void> {
|
||||
if (!session.worker_port) {
|
||||
db.close();
|
||||
logger.error('HOOK', 'No worker port for session', { sessionId: session.id });
|
||||
console.log(createHookResponse('Stop', true));
|
||||
return;
|
||||
throw new Error('No worker port for session - session may not be properly initialized');
|
||||
}
|
||||
|
||||
// Get current prompt number
|
||||
const promptNumber = db.getPromptCounter(session.id);
|
||||
db.close();
|
||||
|
||||
try {
|
||||
logger.dataIn('HOOK', 'Stop: Requesting summary', {
|
||||
logger.dataIn('HOOK', 'Stop: Requesting summary', {
|
||||
sessionId: session.id,
|
||||
workerPort: session.worker_port,
|
||||
promptNumber
|
||||
});
|
||||
|
||||
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}/summarize`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ prompt_number: promptNumber }),
|
||||
signal: AbortSignal.timeout(2000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
logger.failure('HOOK', 'Failed to generate summary', {
|
||||
sessionId: session.id,
|
||||
workerPort: session.worker_port,
|
||||
promptNumber
|
||||
});
|
||||
|
||||
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}/summarize`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ prompt_number: promptNumber }),
|
||||
signal: AbortSignal.timeout(2000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
logger.failure('HOOK', 'Failed to generate summary', {
|
||||
sessionId: session.id,
|
||||
status: response.status
|
||||
}, errorText);
|
||||
} else {
|
||||
logger.debug('HOOK', 'Summary request sent successfully', { sessionId: session.id });
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.failure('HOOK', 'Error requesting summary', { sessionId: session.id }, error);
|
||||
} finally {
|
||||
console.log(createHookResponse('Stop', true));
|
||||
status: response.status
|
||||
}, errorText);
|
||||
throw new Error(`Failed to request summary from worker: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
logger.debug('HOOK', 'Summary request sent successfully', { sessionId: session.id });
|
||||
console.log(createHookResponse('Stop', true));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user