refactor: Complete rewrite of worker-utils.ts and cleanup of worker-service.ts

- Removed fragile PM2 string parsing and replaced with direct PM2 restart logic.
- Eliminated silent error handling in worker-utils.ts for better error visibility.
- Extracted duplicated session auto-creation logic into a new helper method getOrCreateSession() in worker-service.ts.
- Centralized configuration values and replaced magic numbers with named constants.
- Updated health check logic to ensure worker is restarted if unhealthy.
- Removed unnecessary getWorkerPort() wrapper function.
- Improved overall code quality and maintainability by applying DRY and YAGNI principles.
This commit is contained in:
Alex Newman
2025-11-06 22:00:07 -05:00
parent f8dc7f940f
commit 3030f518b5
18 changed files with 2454 additions and 346 deletions
+40 -68
View File
@@ -1,16 +1,40 @@
import path from "path";
import { spawn } from "child_process";
import { homedir } from "os";
import { existsSync, readFileSync } from "fs";
import { execSync } from "child_process";
import { getPackageRoot } from "./paths.js";
const FIXED_PORT = parseInt(process.env.CLAUDE_MEM_WORKER_PORT || "37777", 10);
// Named constants for health checks
const HEALTH_CHECK_TIMEOUT_MS = 100;
const HEALTH_CHECK_POLL_INTERVAL_MS = 100;
const HEALTH_CHECK_MAX_WAIT_MS = 10000;
/**
* Get the worker port number
* Priority: ~/.claude-mem/settings.json > env var > default
*/
export function getWorkerPort(): number {
try {
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
if (existsSync(settingsPath)) {
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
const port = parseInt(settings.env?.CLAUDE_MEM_WORKER_PORT, 10);
if (!isNaN(port)) return port;
}
} catch {
// Fall through to env var or default
}
return parseInt(process.env.CLAUDE_MEM_WORKER_PORT || '37777', 10);
}
/**
* Check if worker is responsive by trying the health endpoint
*/
async function isWorkerHealthy(timeoutMs: number = 100): Promise<boolean> {
async function isWorkerHealthy(): Promise<boolean> {
try {
const response = await fetch(`http://127.0.0.1:${FIXED_PORT}/health`, {
signal: AbortSignal.timeout(timeoutMs)
const port = getWorkerPort();
const response = await fetch(`http://127.0.0.1:${port}/health`, {
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS)
});
return response.ok;
} catch {
@@ -21,89 +45,37 @@ async function isWorkerHealthy(timeoutMs: number = 100): Promise<boolean> {
/**
* Wait for worker to become healthy
*/
async function waitForWorkerHealth(maxWaitMs: number = 10000): Promise<boolean> {
async function waitForWorkerHealth(): Promise<boolean> {
const start = Date.now();
const checkInterval = 100; // Check every 100ms
while (Date.now() - start < maxWaitMs) {
if (await isWorkerHealthy(1000)) {
while (Date.now() - start < HEALTH_CHECK_MAX_WAIT_MS) {
if (await isWorkerHealthy()) {
return true;
}
// Wait before next check
await new Promise(resolve => setTimeout(resolve, checkInterval));
await new Promise(resolve => setTimeout(resolve, HEALTH_CHECK_POLL_INTERVAL_MS));
}
return false;
}
/**
* Ensure worker service is running
* Checks if worker is already running before attempting to start
* This prevents unnecessary restarts that could interrupt mid-action processing
* If unhealthy, restarts PM2 and waits for health
*/
export async function ensureWorkerRunning(): Promise<void> {
// First, check if worker is already healthy
if (await isWorkerHealthy()) {
return; // Worker is already running and responsive
return;
}
const packageRoot = getPackageRoot();
const pm2Path = path.join(packageRoot, "node_modules", ".bin", "pm2");
const ecosystemPath = path.join(packageRoot, "ecosystem.config.cjs");
// Check PM2 status to see if worker process exists
const checkProcess = spawn(pm2Path, ["list", "--no-color"], {
execSync(`"${pm2Path}" restart "${ecosystemPath}"`, {
cwd: packageRoot,
stdio: ["ignore", "pipe", "ignore"],
stdio: 'pipe'
});
let output = "";
checkProcess.stdout?.on("data", (data) => {
output += data.toString();
});
// Wait for PM2 list to complete
await new Promise<void>((resolve, reject) => {
checkProcess.on("error", (error) => reject(error));
checkProcess.on("close", (code) => {
// PM2 list can fail, but we should still continue - just assume worker isn't running
// This handles cases where PM2 isn't installed yet
resolve();
});
});
// Check if 'claude-mem-worker' is in the PM2 list output and is 'online'
const isRunning = output.includes("claude-mem-worker") && output.includes("online");
if (!isRunning) {
// Start the worker
const startProcess = spawn(pm2Path, ["start", ecosystemPath], {
cwd: packageRoot,
stdio: "ignore",
});
// Wait for PM2 start command to complete
await new Promise<void>((resolve, reject) => {
startProcess.on("error", (error) => reject(error));
startProcess.on("close", (code) => {
if (code !== 0 && code !== null) {
reject(new Error(`PM2 start command failed with exit code ${code}`));
} else {
resolve();
}
});
});
}
// Wait for worker to become healthy (either just started or was starting)
const healthy = await waitForWorkerHealth(10000);
if (!healthy) {
throw new Error("Worker failed to become healthy after starting");
if (!await waitForWorkerHealth()) {
throw new Error("Worker failed to become healthy after restart");
}
}
/**
* Get the worker port number (fixed port)
*/
export function getWorkerPort(): number {
return FIXED_PORT;
}