4df9f61347
* fix: stop generating empty CLAUDE.md files - Return empty string instead of "No recent activity" when no observations exist - Skip writing CLAUDE.md files when formatted content is empty - Remove redundant "auto-generated by claude-mem" HTML comment - Clean up 98 existing empty CLAUDE.md files across the codebase - Update tests to expect empty string for empty input Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * build assets * refactor: implement in-process worker architecture for hooks Replaces spawn-based worker startup with in-process architecture: - Hook processes now become the worker when port 37777 is free - Eliminates Windows spawn issues (NO SPAWN rule) - SessionStart chains: smart-install && stop && context Key changes: - worker-service.ts: hook case starts WorkerService in-process - hook-command.ts: skipExit option prevents process.exit() when hosting worker - hooks.json: single chained command replaces separate start/hook commands - worker-utils.ts: ensureWorkerRunning() returns boolean, doesn't block - handlers: graceful fallback when worker unavailable All 761 tests pass. Manual verification confirms hook stays alive as worker. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * context * a * MAESTRO: Mark PR #722 test verification task complete All 797 tests passed (3 skipped, 0 failed) after merge conflict resolution. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * MAESTRO: Mark PR #722 build verification task complete * MAESTRO: Mark PR #722 code review task complete Code review verified: - worker-service.ts hook case starts WorkerService in-process - hook-command.ts has skipExit option - hooks.json uses single chained command - worker-utils.ts ensureWorkerRunning() returns boolean Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * MAESTRO: Mark PR #722 conflict resolution push task complete Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
136 lines
4.5 KiB
TypeScript
136 lines
4.5 KiB
TypeScript
import path from "path";
|
|
import { homedir } from "os";
|
|
import { readFileSync } from "fs";
|
|
import { logger } from "../utils/logger.js";
|
|
import { HOOK_TIMEOUTS, getTimeout } from "./hook-constants.js";
|
|
import { SettingsDefaultsManager } from "./SettingsDefaultsManager.js";
|
|
|
|
const MARKETPLACE_ROOT = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
|
|
|
|
// Named constants for health checks
|
|
const HEALTH_CHECK_TIMEOUT_MS = getTimeout(HOOK_TIMEOUTS.HEALTH_CHECK);
|
|
|
|
// Cache to avoid repeated settings file reads
|
|
let cachedPort: number | null = null;
|
|
let cachedHost: string | null = null;
|
|
|
|
/**
|
|
* Get the worker port number from settings
|
|
* Uses CLAUDE_MEM_WORKER_PORT from settings file or default (37777)
|
|
* Caches the port value to avoid repeated file reads
|
|
*/
|
|
export function getWorkerPort(): number {
|
|
if (cachedPort !== null) {
|
|
return cachedPort;
|
|
}
|
|
|
|
const settingsPath = path.join(SettingsDefaultsManager.get('CLAUDE_MEM_DATA_DIR'), 'settings.json');
|
|
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
|
|
cachedPort = parseInt(settings.CLAUDE_MEM_WORKER_PORT, 10);
|
|
return cachedPort;
|
|
}
|
|
|
|
/**
|
|
* Get the worker host address
|
|
* Uses CLAUDE_MEM_WORKER_HOST from settings file or default (127.0.0.1)
|
|
* Caches the host value to avoid repeated file reads
|
|
*/
|
|
export function getWorkerHost(): string {
|
|
if (cachedHost !== null) {
|
|
return cachedHost;
|
|
}
|
|
|
|
const settingsPath = path.join(SettingsDefaultsManager.get('CLAUDE_MEM_DATA_DIR'), 'settings.json');
|
|
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
|
|
cachedHost = settings.CLAUDE_MEM_WORKER_HOST;
|
|
return cachedHost;
|
|
}
|
|
|
|
/**
|
|
* Clear the cached port and host values
|
|
* Call this when settings are updated to force re-reading from file
|
|
*/
|
|
export function clearPortCache(): void {
|
|
cachedPort = null;
|
|
cachedHost = null;
|
|
}
|
|
|
|
/**
|
|
* Check if worker is responsive and fully initialized by trying the readiness endpoint
|
|
* Changed from /health to /api/readiness to ensure MCP initialization is complete
|
|
*/
|
|
async function isWorkerHealthy(): Promise<boolean> {
|
|
const port = getWorkerPort();
|
|
// Note: Removed AbortSignal.timeout to avoid Windows Bun cleanup issue (libuv assertion)
|
|
const response = await fetch(`http://127.0.0.1:${port}/api/readiness`);
|
|
return response.ok;
|
|
}
|
|
|
|
/**
|
|
* Get the current plugin version from package.json
|
|
*/
|
|
function getPluginVersion(): string {
|
|
const packageJsonPath = path.join(MARKETPLACE_ROOT, 'package.json');
|
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
return packageJson.version;
|
|
}
|
|
|
|
/**
|
|
* Get the running worker's version from the API
|
|
*/
|
|
async function getWorkerVersion(): Promise<string> {
|
|
const port = getWorkerPort();
|
|
// Note: Removed AbortSignal.timeout to avoid Windows Bun cleanup issue (libuv assertion)
|
|
const response = await fetch(`http://127.0.0.1:${port}/api/version`);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to get worker version: ${response.status}`);
|
|
}
|
|
const data = await response.json() as { version: string };
|
|
return data.version;
|
|
}
|
|
|
|
/**
|
|
* Check if worker version matches plugin version
|
|
* Note: Auto-restart on version mismatch is now handled in worker-service.ts start command (issue #484)
|
|
* This function logs for informational purposes only
|
|
*/
|
|
async function checkWorkerVersion(): Promise<void> {
|
|
const pluginVersion = getPluginVersion();
|
|
const workerVersion = await getWorkerVersion();
|
|
|
|
if (pluginVersion !== workerVersion) {
|
|
// Just log debug info - auto-restart handles the mismatch in worker-service.ts
|
|
logger.debug('SYSTEM', 'Version check', {
|
|
pluginVersion,
|
|
workerVersion,
|
|
note: 'Mismatch will be auto-restarted by worker-service start command'
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Ensure worker service is running
|
|
* Quick health check - returns false if worker not healthy (doesn't block)
|
|
* Port might be in use by another process, or worker might not be started yet
|
|
*/
|
|
export async function ensureWorkerRunning(): Promise<boolean> {
|
|
// Quick health check (single attempt, no polling)
|
|
try {
|
|
if (await isWorkerHealthy()) {
|
|
await checkWorkerVersion(); // logs warning on mismatch, doesn't restart
|
|
return true; // Worker healthy
|
|
}
|
|
} catch (e) {
|
|
// Not healthy - log for debugging
|
|
logger.debug('SYSTEM', 'Worker health check failed', {
|
|
error: e instanceof Error ? e.message : String(e)
|
|
});
|
|
}
|
|
|
|
// Port might be in use by something else, or worker not started
|
|
// Return false but don't throw - let caller decide how to handle
|
|
logger.warn('SYSTEM', 'Worker not healthy, hook will proceed gracefully');
|
|
return false;
|
|
}
|