feat: add admin endpoints for process management and improve error handling

- Introduced `/api/admin/restart` and `/api/admin/shutdown` endpoints in WorkerService for restarting and shutting down the service.
- Updated error message in hook-error-handler to provide clearer instructions for restarting the worker.
- Refactored worker-utils to remove PM2 dependency and implement ProcessManager for starting the worker service.
- Cleaned up legacy PM2 references and provided new manual start instructions.
This commit is contained in:
Alex Newman
2025-12-10 23:46:17 -05:00
parent 8bf22b3dc5
commit 83b0f9551b
11 changed files with 159 additions and 247 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+17
View File
@@ -132,6 +132,23 @@ export class WorkerService {
res.status(200).json({ status: 'ok' }); res.status(200).json({ status: 'ok' });
}); });
// Admin endpoints for process management
this.app.post('/api/admin/restart', async (_req, res) => {
res.json({ status: 'restarting' });
setTimeout(async () => {
await this.shutdown();
process.exit(0);
}, 100);
});
this.app.post('/api/admin/shutdown', async (_req, res) => {
res.json({ status: 'shutting_down' });
setTimeout(async () => {
await this.shutdown();
process.exit(0);
}, 100);
});
this.viewerRoutes.setupRoutes(this.app); this.viewerRoutes.setupRoutes(this.app);
this.sessionRoutes.setupRoutes(this.app); this.sessionRoutes.setupRoutes(this.app);
this.dataRoutes.setupRoutes(this.app); this.dataRoutes.setupRoutes(this.app);
+1 -1
View File
@@ -7,7 +7,7 @@ export function handleWorkerError(error: any): never {
error.name === 'TimeoutError' || error.name === 'TimeoutError' ||
error.message?.includes('fetch failed')) { error.message?.includes('fetch failed')) {
throw new Error( throw new Error(
"There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue" "There's a problem with the worker. Try: npm run worker:restart"
); );
} }
throw error; throw error;
+23 -98
View File
@@ -1,29 +1,25 @@
import path from "path"; import path from "path";
import { existsSync } from "fs";
import { homedir } from "os"; import { homedir } from "os";
import { spawnSync } from "child_process"; import { spawnSync } from "child_process";
import { SettingsDefaultsManager } from "./SettingsDefaultsManager.js";
import { logger } from "../utils/logger.js"; import { logger } from "../utils/logger.js";
import { HOOK_TIMEOUTS, getTimeout } from "./hook-constants.js"; import { HOOK_TIMEOUTS, getTimeout } from "./hook-constants.js";
import { ProcessManager } from "../services/process/ProcessManager.js";
// CRITICAL: Always use marketplace directory for PM2/ecosystem
// This ensures cross-platform compatibility and avoids cache directory confusion
const MARKETPLACE_ROOT = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack'); const MARKETPLACE_ROOT = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
// Named constants for health checks // Named constants for health checks
// Windows needs longer timeouts due to startup overhead
const HEALTH_CHECK_TIMEOUT_MS = getTimeout(HOOK_TIMEOUTS.HEALTH_CHECK); const HEALTH_CHECK_TIMEOUT_MS = getTimeout(HOOK_TIMEOUTS.HEALTH_CHECK);
const WORKER_STARTUP_WAIT_MS = HOOK_TIMEOUTS.WORKER_STARTUP_WAIT;
const WORKER_STARTUP_RETRIES = HOOK_TIMEOUTS.WORKER_STARTUP_RETRIES; // MIGRATION: Hardcoded port to avoid conflicts with PM2 worker on 37777
// TODO: Switch to settings-based port after PM2 is fully removed
const MIGRATION_PORT = 38888;
/** /**
* Get the worker port number * Get the worker port number
* Priority: ~/.claude-mem/settings.json > env var > default * Currently hardcoded for migration; will use settings after PM2 removal
*/ */
export function getWorkerPort(): number { export function getWorkerPort(): number {
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json'); return MIGRATION_PORT;
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
return parseInt(settings.CLAUDE_MEM_WORKER_PORT, 10);
} }
/** /**
@@ -46,101 +42,32 @@ async function isWorkerHealthy(): Promise<boolean> {
} }
/** /**
* Start the worker service * Start the worker service using ProcessManager
* On Windows: Uses PowerShell Start-Process with hidden window to avoid console flash * Handles both Unix (Bun) and Windows (compiled exe) platforms
* On Unix: Uses PM2 for process management
*/ */
async function startWorker(): Promise<boolean> { async function startWorker(): Promise<boolean> {
// Clean up legacy PM2 (one-time migration)
if (process.platform !== 'win32') {
try { try {
const workerScript = path.join(MARKETPLACE_ROOT, 'plugin', 'scripts', 'worker-service.cjs'); spawnSync('pm2', ['delete', 'claude-mem-worker'], { stdio: 'ignore' });
} catch {
if (!existsSync(workerScript)) { // PM2 not installed or process doesn't exist - ignore
throw new Error(`Worker script not found at ${workerScript}`);
}
if (process.platform === 'win32') {
// On Windows, use PowerShell Start-Process with -WindowStyle Hidden
// This avoids visible console windows that PM2 creates on Windows
// Escape single quotes for PowerShell by doubling them
const escapedScript = workerScript.replace(/'/g, "''");
const escapedWorkingDir = MARKETPLACE_ROOT.replace(/'/g, "''");
const result = spawnSync('powershell.exe', [
'-NoProfile',
'-NonInteractive',
'-Command',
`Start-Process -FilePath 'node' -ArgumentList '${escapedScript}' -WorkingDirectory '${escapedWorkingDir}' -WindowStyle Hidden`
], {
cwd: MARKETPLACE_ROOT,
stdio: 'pipe',
encoding: 'utf-8',
windowsHide: true
});
if (result.status !== 0) {
throw new Error(result.stderr || 'PowerShell Start-Process failed');
}
} else {
// On Unix, use PM2 for process management
const ecosystemPath = path.join(MARKETPLACE_ROOT, 'ecosystem.config.cjs');
if (!existsSync(ecosystemPath)) {
throw new Error(`Ecosystem config not found at ${ecosystemPath}`);
}
const localPm2Base = path.join(MARKETPLACE_ROOT, 'node_modules', '.bin', 'pm2');
let pm2Command: string;
if (existsSync(localPm2Base)) {
pm2Command = localPm2Base;
} else {
// Check if global pm2 exists
const globalPm2Check = spawnSync('which', ['pm2'], {
encoding: 'utf-8',
stdio: 'pipe'
});
if (globalPm2Check.status !== 0) {
throw new Error(
'PM2 not found. Install it locally with:\n' +
` cd ${MARKETPLACE_ROOT}\n` +
' npm install\n\n' +
'Or install globally with: npm install -g pm2'
);
}
pm2Command = 'pm2';
}
const result = spawnSync(pm2Command, ['start', ecosystemPath], {
cwd: MARKETPLACE_ROOT,
stdio: 'pipe',
encoding: 'utf-8'
});
if (result.status !== 0) {
throw new Error(result.stderr || 'PM2 start failed');
} }
} }
// Wait for worker to become healthy const port = getWorkerPort();
for (let i = 0; i < WORKER_STARTUP_RETRIES; i++) { const result = await ProcessManager.start(port);
await new Promise(resolve => setTimeout(resolve, WORKER_STARTUP_WAIT_MS));
if (await isWorkerHealthy()) {
return true;
}
}
return false; if (!result.success) {
} catch (error) {
logger.error('SYSTEM', 'Failed to start worker', { logger.error('SYSTEM', 'Failed to start worker', {
platform: process.platform, platform: process.platform,
workerScript: path.join(MARKETPLACE_ROOT, 'plugin', 'scripts', 'worker-service.cjs'), port,
error: error instanceof Error ? error.message : String(error), error: result.error,
marketplaceRoot: MARKETPLACE_ROOT marketplaceRoot: MARKETPLACE_ROOT
}); });
return false;
} }
return result.success;
} }
/** /**
@@ -166,10 +93,8 @@ export async function ensureWorkerRunning(): Promise<void> {
const port = getWorkerPort(); const port = getWorkerPort();
throw new Error( throw new Error(
`Worker service failed to start on port ${port}.\n\n` + `Worker service failed to start on port ${port}.\n\n` +
`To start manually, run:\n` + `To start manually, run: npm run worker:start\n` +
` cd ${MARKETPLACE_ROOT}\n` + `If already running, try: npm run worker:restart`
` npx pm2 start ecosystem.config.cjs\n\n` +
`If already running, try: npx pm2 restart claude-mem-worker`
); );
} }
} }