From 9c187d6261a2bf14f2a3c62a2c59bf62fc07a2c0 Mon Sep 17 00:00:00 2001 From: Alex Newman Date: Tue, 24 Feb 2026 17:27:10 -0500 Subject: [PATCH] fix: resolve PostToolUse hook crashes and 5s latency (#1220) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three compounding bugs caused hook failures: 1. Missing break statements in worker-service.ts switch — if async code threw before process.exit(), execution fell through to subsequent cases. Added break to all 7 cases missing them. 2. Unhandled promise rejection on main() — added .catch() that logs the error and exits 0 (per project exit code strategy: don't block Claude Code or leave Windows Terminal tabs open). 3. Redundant start commands in hooks.json — PostToolUse, UserPromptSubmit, and Stop groups each had a standalone start command that was redundant (the hook case already calls ensureWorkerStarted internally). The redundant start also caused 5s latency via bun-runner.js collectStdin() timeout since Claude Code never closes stdin. Co-Authored-By: Claude Opus 4.6 --- plugin/hooks/hooks.json | 15 --------------- plugin/scripts/worker-service.cjs | 2 +- src/services/worker-service.ts | 12 +++++++++++- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/plugin/hooks/hooks.json b/plugin/hooks/hooks.json index d1e9c626..8559852d 100644 --- a/plugin/hooks/hooks.json +++ b/plugin/hooks/hooks.json @@ -43,11 +43,6 @@ "UserPromptSubmit": [ { "hooks": [ - { - "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" start", - "timeout": 60 - }, { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code session-init", @@ -60,11 +55,6 @@ { "matcher": "*", "hooks": [ - { - "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" start", - "timeout": 60 - }, { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code observation", @@ -76,11 +66,6 @@ "Stop": [ { "hooks": [ - { - "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" start", - "timeout": 60 - }, { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bun-runner.js\" \"${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs\" hook claude-code summarize", diff --git a/plugin/scripts/worker-service.cjs b/plugin/scripts/worker-service.cjs index ff5e4f49..4ec18e9e 100755 --- a/plugin/scripts/worker-service.cjs +++ b/plugin/scripts/worker-service.cjs @@ -1444,7 +1444,7 @@ Tips: SET status = 'failed', failed_at_epoch = ? WHERE status = 'pending' AND session_db_id IN (${d}) - `).run(Date.now(),...l);p.changes>0&&w.info("SYSTEM",`Marked ${p.changes} pending messages from stale sessions as failed`)}}catch(u){w.error("SYSTEM","Failed to clean up stale sessions",{},u)}let a=n.getSessionsWithPendingMessages(),c={totalPendingSessions:a.length,sessionsStarted:0,sessionsSkipped:0,startedSessionIds:[]};if(a.length===0)return c;w.info("SYSTEM",`Processing up to ${e} of ${a.length} pending session queues`);for(let u of a){if(c.sessionsStarted>=e)break;try{if(this.sessionManager.getSession(u)?.generatorPromise){c.sessionsSkipped++;continue}let d=this.sessionManager.initializeSession(u);w.info("SYSTEM",`Starting processor for session ${u}`,{project:d.project,pendingCount:n.getPendingCount(u)}),this.startSessionProcessor(d,"startup-recovery"),c.sessionsStarted++,c.startedSessionIds.push(u),await new Promise(p=>setTimeout(p,100))}catch(l){w.error("SYSTEM",`Failed to process session ${u}`,{},l),c.sessionsSkipped++}}return c}async shutdown(){this.stopOrphanReaper&&(this.stopOrphanReaper(),this.stopOrphanReaper=null),this.staleSessionReaperInterval&&(clearInterval(this.staleSessionReaperInterval),this.staleSessionReaperInterval=null),await xA({server:this.server.getHttpServer(),sessionManager:this.sessionManager,mcpClient:this.mcpClient,dbManager:this.dbManager,chromaMcpManager:this.chromaMcpManager||void 0})}broadcastProcessingStatus(){let e=this.sessionManager.isAnySessionProcessing(),r=this.sessionManager.getTotalActiveWork(),n=this.sessionManager.getActiveSessionCount();w.info("WORKER","Broadcasting processing status",{isProcessing:e,queueDepth:r,activeSessions:n}),this.sseBroadcaster.broadcast({type:"processing_status",isProcessing:e,queueDepth:r})}};async function hF(t){if(hA(),await Ka(t,1e3)){let s=await SA(t);if(s.matches)return w.info("SYSTEM","Worker already running and healthy"),!0;if(fA(15e3)){if(w.info("SYSTEM","Version mismatch detected but PID file is recent \u2014 another restart likely in progress, polling health",{pluginVersion:s.pluginVersion,workerVersion:s.workerVersion}),await Ka(t,15e3))return w.info("SYSTEM","Worker became healthy after waiting for concurrent restart"),!0;w.warn("SYSTEM","Worker did not become healthy after waiting \u2014 proceeding with own restart")}if(w.info("SYSTEM","Worker version mismatch detected - auto-restarting",{pluginVersion:s.pluginVersion,workerVersion:s.workerVersion}),await qm(t),!await Lm(t,Os(Cr.PORT_IN_USE_WAIT)))return w.error("SYSTEM","Port did not free up after shutdown for version mismatch restart",{port:t}),!1;Yn()}return await Wa(t)?(w.info("SYSTEM","Port in use, waiting for worker to become healthy"),await Ka(t,Os(Cr.PORT_IN_USE_WAIT))?(w.info("SYSTEM","Worker is now healthy"),!0):(w.error("SYSTEM","Port in use but worker not responding to health checks"),!1)):zge()?(w.warn("SYSTEM","Worker unavailable on Windows \u2014 skipping spawn (recent attempt failed within cooldown)"),!1):(w.info("SYSTEM","Starting worker daemon"),Uge(),D0(__filename,t)===void 0?(w.error("SYSTEM","Failed to spawn worker daemon"),!1):await Ka(t,Os(Cr.POST_SPAWN_WAIT))?(await bA(t,Os(Cr.READINESS_WAIT))||w.warn("SYSTEM","Worker is alive but readiness timed out \u2014 proceeding anyway"),Lge(),mA(),w.info("SYSTEM","Worker started successfully"),!0):(Yn(),w.error("SYSTEM","Worker failed to start (health check timeout)"),!1))}async function Fge(){let t=process.argv[2];(["start","hook","restart","--daemon"].includes(t)||t===void 0)&&jm()&&process.exit(0);let r=Et();function n(i,s){let o=gF(i,s);console.log(JSON.stringify(o)),process.exit(0)}switch(t){case"start":await hF(r)?n("ready"):n("error","Failed to start worker");case"stop":await qm(r),await Lm(r,Os(15e3))||w.warn("SYSTEM","Port did not free up after shutdown",{port:r}),Yn(),w.info("SYSTEM","Worker stopped successfully"),process.exit(0);case"restart":w.info("SYSTEM","Restarting worker"),await qm(r),await Lm(r,Os(15e3))||(w.error("SYSTEM","Port did not free up after shutdown, aborting restart",{port:r}),process.exit(0)),Yn(),D0(__filename,r)===void 0&&(w.error("SYSTEM","Failed to spawn worker daemon during restart"),process.exit(0)),await Ka(r,Os(Cr.POST_SPAWN_WAIT))||(Yn(),w.error("SYSTEM","Worker failed to restart"),process.exit(0)),w.info("SYSTEM","Worker restarted successfully"),process.exit(0);case"status":{let i=await Wa(r),s=Um();i&&s?(console.log("Worker is running"),console.log(` PID: ${s.pid}`),console.log(` Port: ${s.port}`),console.log(` Started: ${s.startedAt}`)):console.log("Worker is not running"),process.exit(0)}case"cursor":{let i=process.argv[3],s=await Z2(i,process.argv.slice(4));process.exit(s)}case"hook":{await hF(r)||w.warn("SYSTEM","Worker failed to start before hook, handler will retry");let s=process.argv[3],o=process.argv[4];(!s||!o)&&(console.error("Usage: claude-mem hook "),console.error("Platforms: claude-code, cursor, raw"),console.error("Events: context, session-init, observation, summarize, session-complete"),process.exit(1));let a=await Wa(r),c=!1;if(!a)try{w.info("SYSTEM","Starting worker in-process for hook",{event:o}),await new yp().start(),c=!0}catch(l){w.failure("SYSTEM","Worker failed to start in hook",{},l),Yn(),process.exit(0)}let{hookCommand:u}=await Promise.resolve().then(()=>(lF(),uF));await u(s,o,{skipExit:c});break}case"generate":{let i=process.argv.includes("--dry-run"),{generateClaudeMd:s}=await Promise.resolve().then(()=>(Ik(),Tk)),o=await s(i);process.exit(o)}case"clean":{let i=process.argv.includes("--dry-run"),{cleanClaudeMd:s}=await Promise.resolve().then(()=>(Ik(),Tk)),o=await s(i);process.exit(o)}default:{let i=Um();i&&j0(i.pid)&&(w.info("SYSTEM","Worker already running (PID alive), refusing to start duplicate",{existingPid:i.pid,existingPort:i.port,startedAt:i.startedAt}),process.exit(0)),await Wa(r)&&(w.info("SYSTEM","Port already in use, refusing to start duplicate",{port:r}),process.exit(0)),process.on("unhandledRejection",o=>{w.error("SYSTEM","Unhandled rejection in daemon",{reason:o instanceof Error?o.message:String(o)})}),process.on("uncaughtException",o=>{w.error("SYSTEM","Uncaught exception in daemon",{},o)}),new yp().start().catch(o=>{w.failure("SYSTEM","Worker failed to start",{},o),Yn(),process.exit(0)})}}}var Hge=typeof require<"u"&&typeof module<"u"?require.main===module||!module.parent:Bge.url===`file://${process.argv[1]}`||process.argv[1]?.endsWith("worker-service");Hge&&Fge();0&&(module.exports={WorkerService,buildStatusOutput,isPluginDisabledInClaudeSettings}); + `).run(Date.now(),...l);p.changes>0&&w.info("SYSTEM",`Marked ${p.changes} pending messages from stale sessions as failed`)}}catch(u){w.error("SYSTEM","Failed to clean up stale sessions",{},u)}let a=n.getSessionsWithPendingMessages(),c={totalPendingSessions:a.length,sessionsStarted:0,sessionsSkipped:0,startedSessionIds:[]};if(a.length===0)return c;w.info("SYSTEM",`Processing up to ${e} of ${a.length} pending session queues`);for(let u of a){if(c.sessionsStarted>=e)break;try{if(this.sessionManager.getSession(u)?.generatorPromise){c.sessionsSkipped++;continue}let d=this.sessionManager.initializeSession(u);w.info("SYSTEM",`Starting processor for session ${u}`,{project:d.project,pendingCount:n.getPendingCount(u)}),this.startSessionProcessor(d,"startup-recovery"),c.sessionsStarted++,c.startedSessionIds.push(u),await new Promise(p=>setTimeout(p,100))}catch(l){w.error("SYSTEM",`Failed to process session ${u}`,{},l),c.sessionsSkipped++}}return c}async shutdown(){this.stopOrphanReaper&&(this.stopOrphanReaper(),this.stopOrphanReaper=null),this.staleSessionReaperInterval&&(clearInterval(this.staleSessionReaperInterval),this.staleSessionReaperInterval=null),await xA({server:this.server.getHttpServer(),sessionManager:this.sessionManager,mcpClient:this.mcpClient,dbManager:this.dbManager,chromaMcpManager:this.chromaMcpManager||void 0})}broadcastProcessingStatus(){let e=this.sessionManager.isAnySessionProcessing(),r=this.sessionManager.getTotalActiveWork(),n=this.sessionManager.getActiveSessionCount();w.info("WORKER","Broadcasting processing status",{isProcessing:e,queueDepth:r,activeSessions:n}),this.sseBroadcaster.broadcast({type:"processing_status",isProcessing:e,queueDepth:r})}};async function hF(t){if(hA(),await Ka(t,1e3)){let s=await SA(t);if(s.matches)return w.info("SYSTEM","Worker already running and healthy"),!0;if(fA(15e3)){if(w.info("SYSTEM","Version mismatch detected but PID file is recent \u2014 another restart likely in progress, polling health",{pluginVersion:s.pluginVersion,workerVersion:s.workerVersion}),await Ka(t,15e3))return w.info("SYSTEM","Worker became healthy after waiting for concurrent restart"),!0;w.warn("SYSTEM","Worker did not become healthy after waiting \u2014 proceeding with own restart")}if(w.info("SYSTEM","Worker version mismatch detected - auto-restarting",{pluginVersion:s.pluginVersion,workerVersion:s.workerVersion}),await qm(t),!await Lm(t,Os(Cr.PORT_IN_USE_WAIT)))return w.error("SYSTEM","Port did not free up after shutdown for version mismatch restart",{port:t}),!1;Yn()}return await Wa(t)?(w.info("SYSTEM","Port in use, waiting for worker to become healthy"),await Ka(t,Os(Cr.PORT_IN_USE_WAIT))?(w.info("SYSTEM","Worker is now healthy"),!0):(w.error("SYSTEM","Port in use but worker not responding to health checks"),!1)):zge()?(w.warn("SYSTEM","Worker unavailable on Windows \u2014 skipping spawn (recent attempt failed within cooldown)"),!1):(w.info("SYSTEM","Starting worker daemon"),Uge(),D0(__filename,t)===void 0?(w.error("SYSTEM","Failed to spawn worker daemon"),!1):await Ka(t,Os(Cr.POST_SPAWN_WAIT))?(await bA(t,Os(Cr.READINESS_WAIT))||w.warn("SYSTEM","Worker is alive but readiness timed out \u2014 proceeding anyway"),Lge(),mA(),w.info("SYSTEM","Worker started successfully"),!0):(Yn(),w.error("SYSTEM","Worker failed to start (health check timeout)"),!1))}async function Fge(){let t=process.argv[2];(["start","hook","restart","--daemon"].includes(t)||t===void 0)&&jm()&&process.exit(0);let r=Et();function n(i,s){let o=gF(i,s);console.log(JSON.stringify(o)),process.exit(0)}switch(t){case"start":{await hF(r)?n("ready"):n("error","Failed to start worker");break}case"stop":{await qm(r),await Lm(r,Os(15e3))||w.warn("SYSTEM","Port did not free up after shutdown",{port:r}),Yn(),w.info("SYSTEM","Worker stopped successfully"),process.exit(0);break}case"restart":{w.info("SYSTEM","Restarting worker"),await qm(r),await Lm(r,Os(15e3))||(w.error("SYSTEM","Port did not free up after shutdown, aborting restart",{port:r}),process.exit(0)),Yn(),D0(__filename,r)===void 0&&(w.error("SYSTEM","Failed to spawn worker daemon during restart"),process.exit(0)),await Ka(r,Os(Cr.POST_SPAWN_WAIT))||(Yn(),w.error("SYSTEM","Worker failed to restart"),process.exit(0)),w.info("SYSTEM","Worker restarted successfully"),process.exit(0);break}case"status":{let i=await Wa(r),s=Um();i&&s?(console.log("Worker is running"),console.log(` PID: ${s.pid}`),console.log(` Port: ${s.port}`),console.log(` Started: ${s.startedAt}`)):console.log("Worker is not running"),process.exit(0);break}case"cursor":{let i=process.argv[3],s=await Z2(i,process.argv.slice(4));process.exit(s);break}case"hook":{await hF(r)||w.warn("SYSTEM","Worker failed to start before hook, handler will retry");let s=process.argv[3],o=process.argv[4];(!s||!o)&&(console.error("Usage: claude-mem hook "),console.error("Platforms: claude-code, cursor, raw"),console.error("Events: context, session-init, observation, summarize, session-complete"),process.exit(1));let a=await Wa(r),c=!1;if(!a)try{w.info("SYSTEM","Starting worker in-process for hook",{event:o}),await new yp().start(),c=!0}catch(l){w.failure("SYSTEM","Worker failed to start in hook",{},l),Yn(),process.exit(0)}let{hookCommand:u}=await Promise.resolve().then(()=>(lF(),uF));await u(s,o,{skipExit:c});break}case"generate":{let i=process.argv.includes("--dry-run"),{generateClaudeMd:s}=await Promise.resolve().then(()=>(Ik(),Tk)),o=await s(i);process.exit(o);break}case"clean":{let i=process.argv.includes("--dry-run"),{cleanClaudeMd:s}=await Promise.resolve().then(()=>(Ik(),Tk)),o=await s(i);process.exit(o);break}default:{let i=Um();i&&j0(i.pid)&&(w.info("SYSTEM","Worker already running (PID alive), refusing to start duplicate",{existingPid:i.pid,existingPort:i.port,startedAt:i.startedAt}),process.exit(0)),await Wa(r)&&(w.info("SYSTEM","Port already in use, refusing to start duplicate",{port:r}),process.exit(0)),process.on("unhandledRejection",o=>{w.error("SYSTEM","Unhandled rejection in daemon",{reason:o instanceof Error?o.message:String(o)})}),process.on("uncaughtException",o=>{w.error("SYSTEM","Uncaught exception in daemon",{},o)}),new yp().start().catch(o=>{w.failure("SYSTEM","Worker failed to start",{},o),Yn(),process.exit(0)})}}}var Hge=typeof require<"u"&&typeof module<"u"?require.main===module||!module.parent:Bge.url===`file://${process.argv[1]}`||process.argv[1]?.endsWith("worker-service");Hge&&Fge().catch(t=>{w.error("SYSTEM","Fatal error in main",{},t instanceof Error?t:void 0),process.exit(0)});0&&(module.exports={WorkerService,buildStatusOutput,isPluginDisabledInClaudeSettings}); /*! Bundled license information: depd/index.js: diff --git a/src/services/worker-service.ts b/src/services/worker-service.ts index 0bdf0be3..02dd4f32 100644 --- a/src/services/worker-service.ts +++ b/src/services/worker-service.ts @@ -1027,6 +1027,7 @@ async function main() { } else { exitWithStatus('error', 'Failed to start worker'); } + break; } case 'stop': { @@ -1038,6 +1039,7 @@ async function main() { removePidFile(); logger.info('SYSTEM', 'Worker stopped successfully'); process.exit(0); + break; } case 'restart': { @@ -1074,6 +1076,7 @@ async function main() { logger.info('SYSTEM', 'Worker restarted successfully'); process.exit(0); + break; } case 'status': { @@ -1088,12 +1091,14 @@ async function main() { console.log('Worker is not running'); } process.exit(0); + break; } case 'cursor': { const subcommand = process.argv[3]; const cursorResult = await handleCursorCommand(subcommand, process.argv.slice(4)); process.exit(cursorResult); + break; } case 'hook': { @@ -1147,6 +1152,7 @@ async function main() { const { generateClaudeMd } = await import('../cli/claude-md-commands.js'); const result = await generateClaudeMd(dryRun); process.exit(result); + break; } case 'clean': { @@ -1154,6 +1160,7 @@ async function main() { const { cleanClaudeMd } = await import('../cli/claude-md-commands.js'); const result = await cleanClaudeMd(dryRun); process.exit(result); + break; } case '--daemon': @@ -1210,5 +1217,8 @@ const isMainModule = typeof require !== 'undefined' && typeof module !== 'undefi : import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('worker-service'); if (isMainModule) { - main(); + main().catch((error) => { + logger.error('SYSTEM', 'Fatal error in main', {}, error instanceof Error ? error : undefined); + process.exit(0); // Exit 0: don't block Claude Code, don't leave Windows Terminal tabs open + }); }