refactor: implement in-process worker architecture for hooks (#722)
* 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>
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
### Dec 10, 2025
|
||||
|
||||
| ID | Time | T | Title | Read |
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<claude-mem-context>
|
||||
# Recent Activity
|
||||
|
||||
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
|
||||
|
||||
*No recent activity*
|
||||
</claude-mem-context>
|
||||
@@ -8,11 +8,22 @@
|
||||
import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js';
|
||||
import { ensureWorkerRunning, getWorkerPort } from '../../shared/worker-utils.js';
|
||||
import { getProjectContext } from '../../utils/project-name.js';
|
||||
import { HOOK_EXIT_CODES } from '../../shared/hook-constants.js';
|
||||
|
||||
export const contextHandler: EventHandler = {
|
||||
async execute(input: NormalizedHookInput): Promise<HookResult> {
|
||||
// Ensure worker is running before any other logic
|
||||
await ensureWorkerRunning();
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
// Worker not available - return empty context gracefully
|
||||
return {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'SessionStart',
|
||||
additionalContext: ''
|
||||
},
|
||||
exitCode: HOOK_EXIT_CODES.SUCCESS
|
||||
};
|
||||
}
|
||||
|
||||
const cwd = input.cwd ?? process.cwd();
|
||||
const context = getProjectContext(cwd);
|
||||
|
||||
@@ -8,11 +8,16 @@
|
||||
import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js';
|
||||
import { ensureWorkerRunning, getWorkerPort } from '../../shared/worker-utils.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
import { HOOK_EXIT_CODES } from '../../shared/hook-constants.js';
|
||||
|
||||
export const fileEditHandler: EventHandler = {
|
||||
async execute(input: NormalizedHookInput): Promise<HookResult> {
|
||||
// Ensure worker is running before any other logic
|
||||
await ensureWorkerRunning();
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
// Worker not available - skip file edit observation gracefully
|
||||
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
|
||||
}
|
||||
|
||||
const { sessionId, cwd, filePath, edits } = input;
|
||||
|
||||
|
||||
@@ -7,11 +7,16 @@
|
||||
import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js';
|
||||
import { ensureWorkerRunning, getWorkerPort } from '../../shared/worker-utils.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
import { HOOK_EXIT_CODES } from '../../shared/hook-constants.js';
|
||||
|
||||
export const observationHandler: EventHandler = {
|
||||
async execute(input: NormalizedHookInput): Promise<HookResult> {
|
||||
// Ensure worker is running before any other logic
|
||||
await ensureWorkerRunning();
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
// Worker not available - skip observation gracefully
|
||||
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
|
||||
}
|
||||
|
||||
const { sessionId, cwd, toolName, toolInput, toolResponse } = input;
|
||||
|
||||
|
||||
@@ -8,11 +8,16 @@ import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js'
|
||||
import { ensureWorkerRunning, getWorkerPort } from '../../shared/worker-utils.js';
|
||||
import { getProjectName } from '../../utils/project-name.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
import { HOOK_EXIT_CODES } from '../../shared/hook-constants.js';
|
||||
|
||||
export const sessionInitHandler: EventHandler = {
|
||||
async execute(input: NormalizedHookInput): Promise<HookResult> {
|
||||
// Ensure worker is running before any other logic
|
||||
await ensureWorkerRunning();
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
// Worker not available - skip session init gracefully
|
||||
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
|
||||
}
|
||||
|
||||
const { sessionId, cwd, prompt } = input;
|
||||
|
||||
|
||||
@@ -10,11 +10,16 @@ import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js'
|
||||
import { ensureWorkerRunning, getWorkerPort } from '../../shared/worker-utils.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
import { extractLastMessage } from '../../shared/transcript-parser.js';
|
||||
import { HOOK_EXIT_CODES } from '../../shared/hook-constants.js';
|
||||
|
||||
export const summarizeHandler: EventHandler = {
|
||||
async execute(input: NormalizedHookInput): Promise<HookResult> {
|
||||
// Ensure worker is running before any other logic
|
||||
await ensureWorkerRunning();
|
||||
const workerReady = await ensureWorkerRunning();
|
||||
if (!workerReady) {
|
||||
// Worker not available - skip summary gracefully
|
||||
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
|
||||
}
|
||||
|
||||
const { sessionId, transcriptPath } = input;
|
||||
|
||||
|
||||
+15
-3
@@ -3,7 +3,12 @@ import { getPlatformAdapter } from './adapters/index.js';
|
||||
import { getEventHandler } from './handlers/index.js';
|
||||
import { HOOK_EXIT_CODES } from '../shared/hook-constants.js';
|
||||
|
||||
export async function hookCommand(platform: string, event: string): Promise<void> {
|
||||
export interface HookCommandOptions {
|
||||
/** If true, don't call process.exit() - let caller handle process lifecycle */
|
||||
skipExit?: boolean;
|
||||
}
|
||||
|
||||
export async function hookCommand(platform: string, event: string, options: HookCommandOptions = {}): Promise<number> {
|
||||
try {
|
||||
const adapter = getPlatformAdapter(platform);
|
||||
const handler = getEventHandler(event);
|
||||
@@ -15,11 +20,18 @@ export async function hookCommand(platform: string, event: string): Promise<void
|
||||
const output = adapter.formatOutput(result);
|
||||
|
||||
console.log(JSON.stringify(output));
|
||||
process.exit(result.exitCode ?? HOOK_EXIT_CODES.SUCCESS);
|
||||
const exitCode = result.exitCode ?? HOOK_EXIT_CODES.SUCCESS;
|
||||
if (!options.skipExit) {
|
||||
process.exit(exitCode);
|
||||
}
|
||||
return exitCode;
|
||||
} catch (error) {
|
||||
console.error(`Hook error: ${error}`);
|
||||
// Use exit code 2 (blocking error) so users see the error message
|
||||
// Exit code 1 only shows in verbose mode per Claude Code docs
|
||||
process.exit(HOOK_EXIT_CODES.BLOCKING_ERROR); // = 2
|
||||
if (!options.skipExit) {
|
||||
process.exit(HOOK_EXIT_CODES.BLOCKING_ERROR); // = 2
|
||||
}
|
||||
return HOOK_EXIT_CODES.BLOCKING_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user