diff --git a/plugin/scripts/worker-cli.js b/plugin/scripts/worker-cli.js index 55ae4040..8b56c6ae 100755 --- a/plugin/scripts/worker-cli.js +++ b/plugin/scripts/worker-cli.js @@ -1,19 +1,19 @@ #!/usr/bin/env bun -import{existsSync as D,readFileSync as et,writeFileSync as rt,unlinkSync as nt,mkdirSync as v}from"fs";import{createWriteStream as st}from"fs";import{join as h}from"path";import{spawn as ot,spawnSync as it}from"child_process";import{homedir as at}from"os";import{join as u,dirname as J,basename as bt}from"path";import{homedir as q}from"os";import{fileURLToPath as z}from"url";import{readFileSync as B,writeFileSync as j,existsSync as G}from"fs";import{join as Y}from"path";import{homedir as X}from"os";var V=["bugfix","feature","refactor","discovery","decision","change"],K=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var R=V.join(","),b=K.join(",");var O=(s=>(s[s.DEBUG=0]="DEBUG",s[s.INFO=1]="INFO",s[s.WARN=2]="WARN",s[s.ERROR=3]="ERROR",s[s.SILENT=4]="SILENT",s))(O||{}),C=class{level=null;useColor;constructor(){this.useColor=process.stdout.isTTY??!1}getLevel(){if(this.level===null){let t=p.get("CLAUDE_MEM_LOG_LEVEL").toUpperCase();this.level=O[t]??1}return this.level}correlationId(t,e){return`obs-${t}-${e}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message} -${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let e=Object.keys(t);return e.length===0?"{}":e.length<=3?JSON.stringify(t):`{${e.length} keys: ${e.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,e){if(!e)return t;try{let r=typeof e=="string"?JSON.parse(e):e;if(t==="Bash"&&r.command){let n=r.command.length>50?r.command.substring(0,50)+"...":r.command;return`${t}(${n})`}if(t==="Read"&&r.file_path){let n=r.file_path.split("/").pop()||r.file_path;return`${t}(${n})`}if(t==="Edit"&&r.file_path){let n=r.file_path.split("/").pop()||r.file_path;return`${t}(${n})`}if(t==="Write"&&r.file_path){let n=r.file_path.split("/").pop()||r.file_path;return`${t}(${n})`}return t}catch{return t}}formatTimestamp(t){let e=t.getFullYear(),r=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),s=String(t.getHours()).padStart(2,"0"),o=String(t.getMinutes()).padStart(2,"0"),a=String(t.getSeconds()).padStart(2,"0"),c=String(t.getMilliseconds()).padStart(3,"0");return`${e}-${r}-${n} ${s}:${o}:${a}.${c}`}log(t,e,r,n,s){if(t0&&(m=` {${Object.entries(P).map(([F,H])=>`${F}=${H}`).join(", ")}}`)}let d=`[${o}] [${a}] [${c}] ${g}${r}${m}${E}`;t===3?console.error(d):console.log(d)}debug(t,e,r,n){this.log(0,t,e,r,n)}info(t,e,r,n){this.log(1,t,e,r,n)}warn(t,e,r,n){this.log(2,t,e,r,n)}error(t,e,r,n){this.log(3,t,e,r,n)}dataIn(t,e,r,n){this.info(t,`\u2192 ${e}`,r,n)}dataOut(t,e,r,n){this.info(t,`\u2190 ${e}`,r,n)}success(t,e,r,n){this.info(t,`\u2713 ${e}`,r,n)}failure(t,e,r,n){this.error(t,`\u2717 ${e}`,r,n)}timing(t,e,r,n){this.info(t,`\u23F1 ${e}`,n,{duration:`${r}ms`})}happyPathError(t,e,r,n,s=""){let g=((new Error().stack||"").split(` -`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),E=g?`${g[1].split("/").pop()}:${g[2]}`:"unknown",m={...r,location:E};return this.warn(t,`[HAPPY-PATH] ${e}`,m,n),s}},S=new C;var p=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_DATA_DIR:Y(X(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:R,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:b,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let e=this.get(t);return parseInt(e,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!G(t))return this.getAllDefaults();let e=B(t,"utf-8"),r=JSON.parse(e),n=r;if(r.env&&typeof r.env=="object"){n=r.env;try{j(t,JSON.stringify(n,null,2),"utf-8"),S.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(o){S.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},o)}}let s={...this.DEFAULTS};for(let o of Object.keys(this.DEFAULTS))n[o]!==void 0&&(s[o]=n[o]);return s}};function Q(){return typeof __dirname<"u"?__dirname:J(z(import.meta.url))}var Ut=Q(),l=p.get("CLAUDE_MEM_DATA_DIR"),A=process.env.CLAUDE_CONFIG_DIR||u(q(),".claude"),Nt=u(l,"archives"),xt=u(l,"logs"),$t=u(l,"trash"),Wt=u(l,"backups"),Ft=u(l,"settings.json"),Ht=u(l,"claude-mem.db"),Vt=u(l,"vector-db"),Kt=u(A,"settings.json"),Bt=u(A,"commands"),jt=u(A,"CLAUDE.md");import{spawnSync as Z}from"child_process";import{existsSync as tt}from"fs";import{join as I}from"path";import{homedir as k}from"os";function M(){let i=process.platform==="win32";try{if(Z("bun",["--version"],{encoding:"utf-8",stdio:["pipe","pipe","pipe"],shell:!1}).status===0)return"bun"}catch{}let t=i?[I(k(),".bun","bin","bun.exe")]:[I(k(),".bun","bin","bun"),"/usr/local/bin/bun","/opt/homebrew/bin/bun","/home/linuxbrew/.linuxbrew/bin/bun"];for(let e of t)if(tt(e))return e;return null}function y(){return M()!==null}var T=h(l,"worker.pid"),U=h(l,"logs"),w=h(at(),".claude","plugins","marketplaces","thedotmack"),ct=5e3,ut=1e4,lt=200,pt=1e3,gt=100,f=class{static async start(t){if(isNaN(t)||t<1024||t>65535)return{success:!1,error:`Invalid port ${t}. Must be between 1024 and 65535`};if(await this.isRunning())return{success:!0,pid:this.getPidInfo()?.pid};v(U,{recursive:!0});let e=process.platform==="win32"?"worker-wrapper.cjs":"worker-service.cjs",r=h(w,"plugin","scripts",e);if(!D(r))return{success:!1,error:`Worker script not found at ${r}`};let n=this.getLogFilePath();return this.startWithBun(r,n,t)}static isBunAvailable(){return y()}static escapePowerShellString(t){return t.replace(/'/g,"''")}static async startWithBun(t,e,r){let n=M();if(!n)return{success:!1,error:"Bun is required but not found in PATH or common installation paths. Install from https://bun.sh"};try{if(process.platform==="win32"){let o=this.escapePowerShellString(n),a=this.escapePowerShellString(t),c=this.escapePowerShellString(w),E=`${`$env:CLAUDE_MEM_WORKER_PORT='${r}'`}; Start-Process -FilePath '${o}' -ArgumentList '${a}' -WorkingDirectory '${c}' -WindowStyle Hidden -PassThru | Select-Object -ExpandProperty Id`,m=it("powershell",["-Command",E],{stdio:"pipe",timeout:1e4,windowsHide:!0});if(m.status!==0)return{success:!1,error:`PowerShell spawn failed: ${m.stderr?.toString()||"unknown error"}`};let d=parseInt(m.stdout.toString().trim(),10);return isNaN(d)?{success:!1,error:"Failed to get PID from PowerShell"}:(this.writePidFile({pid:d,port:r,startedAt:new Date().toISOString(),version:process.env.npm_package_version||"unknown"}),this.waitForHealth(d,r))}else{let o=ot(n,[t],{detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,CLAUDE_MEM_WORKER_PORT:String(r)},cwd:w}),a=st(e,{flags:"a"});return o.stdout?.pipe(a),o.stderr?.pipe(a),o.unref(),o.pid?(this.writePidFile({pid:o.pid,port:r,startedAt:new Date().toISOString(),version:process.env.npm_package_version||"unknown"}),this.waitForHealth(o.pid,r)):{success:!1,error:"Failed to get PID from spawned process"}}}catch(s){return{success:!1,error:s instanceof Error?s.message:String(s)}}}static async stop(t=ct){let e=this.getPidInfo();if(!e)return!0;try{if(process.platform==="win32"){let{execSync:r}=await import("child_process");try{r(`taskkill /PID ${e.pid} /T /F`,{timeout:1e4,stdio:"ignore"})}catch{}}else process.kill(e.pid,"SIGTERM"),await this.waitForExit(e.pid,t)}catch{try{process.kill(e.pid,"SIGKILL")}catch{}}return this.removePidFile(),!0}static async restart(t){return await this.stop(),this.start(t)}static async status(){let t=this.getPidInfo();if(!t)return{running:!1};let e=this.isProcessAlive(t.pid);return{running:e,pid:e?t.pid:void 0,port:e?t.port:void 0,uptime:e?this.formatUptime(t.startedAt):void 0}}static async isRunning(){let t=this.getPidInfo();if(!t)return!1;let e=this.isProcessAlive(t.pid);return e||this.removePidFile(),e}static getPidInfo(){try{if(!D(T))return null;let t=et(T,"utf-8"),e=JSON.parse(t);return typeof e.pid!="number"||typeof e.port!="number"?null:e}catch{return null}}static writePidFile(t){v(l,{recursive:!0}),rt(T,JSON.stringify(t,null,2))}static removePidFile(){try{D(T)&&nt(T)}catch{}}static isProcessAlive(t){try{return process.kill(t,0),!0}catch{return!1}}static async waitForHealth(t,e,r=ut){let n=Date.now(),s=process.platform==="win32",o=s?r*2:r;for(;Date.now()-n(s[s.DEBUG=0]="DEBUG",s[s.INFO=1]="INFO",s[s.WARN=2]="WARN",s[s.ERROR=3]="ERROR",s[s.SILENT=4]="SILENT",s))(A||{}),C=class{level=null;useColor;constructor(){this.useColor=process.stdout.isTTY??!1}getLevel(){if(this.level===null){let t=u.get("CLAUDE_MEM_LOG_LEVEL").toUpperCase();this.level=A[t]??1}return this.level}correlationId(t,e){return`obs-${t}-${e}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message} +${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let e=Object.keys(t);return e.length===0?"{}":e.length<=3?JSON.stringify(t):`{${e.length} keys: ${e.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,e){if(!e)return t;try{let r=typeof e=="string"?JSON.parse(e):e;if(t==="Bash"&&r.command){let n=r.command.length>50?r.command.substring(0,50)+"...":r.command;return`${t}(${n})`}if(t==="Read"&&r.file_path){let n=r.file_path.split("/").pop()||r.file_path;return`${t}(${n})`}if(t==="Edit"&&r.file_path){let n=r.file_path.split("/").pop()||r.file_path;return`${t}(${n})`}if(t==="Write"&&r.file_path){let n=r.file_path.split("/").pop()||r.file_path;return`${t}(${n})`}return t}catch{return t}}formatTimestamp(t){let e=t.getFullYear(),r=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),s=String(t.getHours()).padStart(2,"0"),o=String(t.getMinutes()).padStart(2,"0"),a=String(t.getSeconds()).padStart(2,"0"),c=String(t.getMilliseconds()).padStart(3,"0");return`${e}-${r}-${n} ${s}:${o}:${a}.${c}`}log(t,e,r,n,s){if(t0&&(E=` {${Object.entries(R).map(([V,B])=>`${V}=${B}`).join(", ")}}`)}let d=`[${o}] [${a}] [${c}] ${g}${r}${E}${f}`;t===3?console.error(d):console.log(d)}debug(t,e,r,n){this.log(0,t,e,r,n)}info(t,e,r,n){this.log(1,t,e,r,n)}warn(t,e,r,n){this.log(2,t,e,r,n)}error(t,e,r,n){this.log(3,t,e,r,n)}dataIn(t,e,r,n){this.info(t,`\u2192 ${e}`,r,n)}dataOut(t,e,r,n){this.info(t,`\u2190 ${e}`,r,n)}success(t,e,r,n){this.info(t,`\u2713 ${e}`,r,n)}failure(t,e,r,n){this.error(t,`\u2717 ${e}`,r,n)}timing(t,e,r,n){this.info(t,`\u23F1 ${e}`,n,{duration:`${r}ms`})}happyPathError(t,e,r,n,s=""){let g=((new Error().stack||"").split(` +`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),f=g?`${g[1].split("/").pop()}:${g[2]}`:"unknown",E={...r,location:f};return this.warn(t,`[HAPPY-PATH] ${e}`,E,n),s}},T=new C;var u=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_DATA_DIR:q(z(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:b,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:I,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let e=this.get(t);return parseInt(e,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!J(t))return this.getAllDefaults();let e=Y(t,"utf-8"),r=JSON.parse(e),n=r;if(r.env&&typeof r.env=="object"){n=r.env;try{X(t,JSON.stringify(n,null,2),"utf-8"),T.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(o){T.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},o)}}let s={...this.DEFAULTS};for(let o of Object.keys(this.DEFAULTS))n[o]!==void 0&&(s[o]=n[o]);return s}};function et(){return typeof __dirname<"u"?__dirname:Q(tt(import.meta.url))}var Nt=et(),l=u.get("CLAUDE_MEM_DATA_DIR"),w=process.env.CLAUDE_CONFIG_DIR||p(Z(),".claude"),$t=p(l,"archives"),xt=p(l,"logs"),Wt=p(l,"trash"),Ft=p(l,"backups"),Ht=p(l,"settings.json"),Kt=p(l,"claude-mem.db"),Vt=p(l,"vector-db"),Bt=p(w,"settings.json"),jt=p(w,"commands"),Gt=p(w,"CLAUDE.md");import{spawnSync as rt}from"child_process";import{existsSync as nt}from"fs";import{join as k}from"path";import{homedir as y}from"os";function M(){let i=process.platform==="win32";try{if(rt("bun",["--version"],{encoding:"utf-8",stdio:["pipe","pipe","pipe"],shell:!1}).status===0)return"bun"}catch{}let t=i?[k(y(),".bun","bin","bun.exe")]:[k(y(),".bun","bin","bun"),"/usr/local/bin/bun","/opt/homebrew/bin/bun","/home/linuxbrew/.linuxbrew/bin/bun"];for(let e of t)if(nt(e))return e;return null}function v(){return M()!==null}var h=_(l,"worker.pid"),N=_(l,"logs"),P=_(lt(),".claude","plugins","marketplaces","thedotmack"),$=5e3,pt=1e4,gt=200,mt=1e3,x=100,ft=2e3,m=class{static async start(t){if(isNaN(t)||t<1024||t>65535)return{success:!1,error:`Invalid port ${t}. Must be between 1024 and 65535`};if(await this.isRunning())return{success:!0,pid:this.getPidInfo()?.pid};U(N,{recursive:!0});let e=process.platform==="win32"?"worker-wrapper.cjs":"worker-service.cjs",r=_(P,"plugin","scripts",e);if(!D(r))return{success:!1,error:`Worker script not found at ${r}`};let n=this.getLogFilePath();return this.startWithBun(r,n,t)}static isBunAvailable(){return v()}static escapePowerShellString(t){return t.replace(/'/g,"''")}static async startWithBun(t,e,r){let n=M();if(!n)return{success:!1,error:"Bun is required but not found in PATH or common installation paths. Install from https://bun.sh"};try{if(process.platform==="win32"){let o=this.escapePowerShellString(n),a=this.escapePowerShellString(t),c=this.escapePowerShellString(P),g=this.escapePowerShellString(e),E=`${`$env:CLAUDE_MEM_WORKER_PORT='${r}'`}; Start-Process -FilePath '${o}' -ArgumentList '${a}' -WorkingDirectory '${c}' -WindowStyle Hidden -RedirectStandardOutput '${g}' -RedirectStandardError '${g}.err' -PassThru | Select-Object -ExpandProperty Id`,d=ut("powershell",["-Command",E],{stdio:"pipe",timeout:1e4,windowsHide:!0});if(d.status!==0)return{success:!1,error:`PowerShell spawn failed: ${d.stderr?.toString()||"unknown error"}`};let O=parseInt(d.stdout.toString().trim(),10);return isNaN(O)?{success:!1,error:"Failed to get PID from PowerShell"}:(this.writePidFile({pid:O,port:r,startedAt:new Date().toISOString(),version:process.env.npm_package_version||"unknown"}),this.waitForHealth(O,r))}else{let o=ct(n,[t],{detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,CLAUDE_MEM_WORKER_PORT:String(r)},cwd:P}),a=at(e,{flags:"a"});return o.stdout?.pipe(a),o.stderr?.pipe(a),o.unref(),o.pid?(this.writePidFile({pid:o.pid,port:r,startedAt:new Date().toISOString(),version:process.env.npm_package_version||"unknown"}),this.waitForHealth(o.pid,r)):{success:!1,error:"Failed to get PID from spawned process"}}}catch(s){return{success:!1,error:s instanceof Error?s.message:String(s)}}}static async stop(t=$){let e=this.getPidInfo();if(process.platform==="win32"){let r=e?.port??this.getPortFromSettings();if(await this.tryHttpShutdown(r))return this.removePidFile(),!0;if(!e)return!0;let{execSync:s}=await import("child_process");try{s(`taskkill /PID ${e.pid} /T /F`,{timeout:1e4,stdio:"ignore"})}catch{}try{await this.waitForExit(e.pid,t)}catch{}return this.isProcessAlive(e.pid)||this.removePidFile(),!0}else{if(!e)return!0;try{process.kill(e.pid,"SIGTERM"),await this.waitForExit(e.pid,t)}catch{try{process.kill(e.pid,"SIGKILL")}catch{}}return this.removePidFile(),!0}}static async restart(t){return await this.stop(),this.start(t)}static async status(){let t=this.getPidInfo();if(!t)return{running:!1};let e=this.isProcessAlive(t.pid);return{running:e,pid:e?t.pid:void 0,port:e?t.port:void 0,uptime:e?this.formatUptime(t.startedAt):void 0}}static async isRunning(){let t=this.getPidInfo();if(!t)return!1;let e=this.isProcessAlive(t.pid);return e||this.removePidFile(),e}static getPortFromSettings(){try{let t=_(l,"settings.json"),e=u.loadFromFile(t);return parseInt(e.CLAUDE_MEM_WORKER_PORT,10)}catch{return parseInt(u.get("CLAUDE_MEM_WORKER_PORT"),10)}}static async tryHttpShutdown(t){try{return(await fetch(`http://127.0.0.1:${t}/api/admin/shutdown`,{method:"POST",signal:AbortSignal.timeout(ft)})).ok?await this.waitForWorkerDown(t,$):!1}catch{return!1}}static async waitForWorkerDown(t,e){let r=Date.now();for(;Date.now()-rsetTimeout(n,x))}catch{return!0}return!1}static getPidInfo(){try{if(!D(h))return null;let t=st(h,"utf-8"),e=JSON.parse(t);return typeof e.pid!="number"||typeof e.port!="number"?null:e}catch{return null}}static writePidFile(t){U(l,{recursive:!0}),ot(h,JSON.stringify(t,null,2))}static removePidFile(){try{D(h)&&it(h)}catch{}}static isProcessAlive(t){try{return process.kill(t,0),!0}catch{return!1}}static async waitForHealth(t,e,r=pt){let n=Date.now(),s=process.platform==="win32",o=s?r*2:r;for(;Date.now()-nsetTimeout(c,lt))}return{success:!1,error:s?`Worker failed to start on Windows (readiness check timed out after ${o}ms) +5. Docs: https://docs.claude-mem.ai/troubleshooting/windows-issues`:"Process died during startup"};try{if((await fetch(`http://127.0.0.1:${e}/api/readiness`,{signal:AbortSignal.timeout(mt)})).ok)return{success:!0,pid:t}}catch{}await new Promise(c=>setTimeout(c,gt))}return{success:!1,error:s?`Worker failed to start on Windows (readiness check timed out after ${o}ms) Troubleshooting: 1. Check Task Manager for zombie 'bun.exe' or 'node.exe' processes 2. Verify port ${e} is not in use: netstat -ano | findstr ${e} 3. Check worker logs in ~/.claude-mem/logs/ 4. See GitHub issues: #363, #367, #371, #373 -5. Docs: https://docs.claude-mem.ai/troubleshooting/windows-issues`:`Readiness check timed out after ${o}ms`}}static async waitForExit(t,e){let r=Date.now();for(;Date.now()-rsetTimeout(n,gt))}throw new Error("Process did not exit within timeout")}static getLogFilePath(){let t=new Date().toISOString().slice(0,10);return h(U,`worker-${t}.log`)}static formatUptime(t){let e=new Date(t).getTime(),n=Date.now()-e,s=Math.floor(n/1e3),o=Math.floor(s/60),a=Math.floor(o/60),c=Math.floor(a/24);return c>0?`${c}d ${a%24}h`:a>0?`${a}h ${o%60}m`:o>0?`${o}m ${s%60}s`:`${s}s`}};import x from"path";import{homedir as mt}from"os";var L={DEFAULT:5e3,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function N(i){return process.platform==="win32"?Math.round(i*L.WINDOWS_MULTIPLIER):i}var de=x.join(mt(),".claude","plugins","marketplaces","thedotmack"),_e=N(L.HEALTH_CHECK),_=null;function $(){if(_!==null)return _;try{let i=x.join(p.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=p.loadFromFile(i);return _=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),_}catch(i){return S.debug("SYSTEM","Failed to load port from settings, using default",{error:i}),_=parseInt(p.get("CLAUDE_MEM_WORKER_PORT"),10),_}}var ft=process.argv[2],W=$();async function Et(){switch(ft){case"start":{let i=await f.start(W);if(i.success){console.log(`Worker started (PID: ${i.pid})`);let t=new Date().toISOString().slice(0,10);console.log(`Logs: ~/.claude-mem/logs/worker-${t}.log`),process.exit(0)}else console.error(`Failed to start: ${i.error}`),process.exit(1);break}case"stop":await f.stop(),console.log("Worker stopped"),process.exit(0);case"restart":{let i=await f.restart(W);i.success?(console.log(`Worker restarted (PID: ${i.pid})`),process.exit(0)):(console.error(`Failed to restart: ${i.error}`),process.exit(1));break}case"status":{let i=await f.status();i.running?(console.log("Worker is running"),console.log(` PID: ${i.pid}`),console.log(` Port: ${i.port}`),console.log(` Uptime: ${i.uptime}`)):console.log("Worker is not running"),process.exit(0)}default:console.log("Usage: worker-cli.js "),process.exit(1)}}Et().catch(i=>{console.error(i),process.exit(1)}); +5. Docs: https://docs.claude-mem.ai/troubleshooting/windows-issues`:`Readiness check timed out after ${o}ms`}}static async waitForExit(t,e){let r=Date.now();for(;Date.now()-rsetTimeout(n,x))}throw new Error("Process did not exit within timeout")}static getLogFilePath(){let t=new Date().toISOString().slice(0,10);return _(N,`worker-${t}.log`)}static formatUptime(t){let e=new Date(t).getTime(),n=Date.now()-e,s=Math.floor(n/1e3),o=Math.floor(s/60),a=Math.floor(o/60),c=Math.floor(a/24);return c>0?`${c}d ${a%24}h`:a>0?`${a}h ${o%60}m`:o>0?`${o}m ${s%60}s`:`${s}s`}};import F from"path";import{homedir as Et}from"os";var L={DEFAULT:5e3,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function W(i){return process.platform==="win32"?Math.round(i*L.WINDOWS_MULTIPLIER):i}var Se=F.join(Et(),".claude","plugins","marketplaces","thedotmack"),Te=W(L.HEALTH_CHECK),S=null;function H(){if(S!==null)return S;try{let i=F.join(u.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=u.loadFromFile(i);return S=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),S}catch(i){return T.debug("SYSTEM","Failed to load port from settings, using default",{error:i}),S=parseInt(u.get("CLAUDE_MEM_WORKER_PORT"),10),S}}var dt=process.argv[2],K=H();async function _t(){switch(dt){case"start":{let i=await m.start(K);if(i.success){console.log(`Worker started (PID: ${i.pid})`);let t=new Date().toISOString().slice(0,10);console.log(`Logs: ~/.claude-mem/logs/worker-${t}.log`),process.exit(0)}else console.error(`Failed to start: ${i.error}`),process.exit(1);break}case"stop":await m.stop(),console.log("Worker stopped"),process.exit(0);case"restart":{let i=await m.restart(K);i.success?(console.log(`Worker restarted (PID: ${i.pid})`),process.exit(0)):(console.error(`Failed to restart: ${i.error}`),process.exit(1));break}case"status":{let i=await m.status();i.running?(console.log("Worker is running"),console.log(` PID: ${i.pid}`),console.log(` Port: ${i.port}`),console.log(` Uptime: ${i.uptime}`)):console.log("Worker is not running"),process.exit(0)}default:console.log("Usage: worker-cli.js "),process.exit(1)}}_t().catch(i=>{console.error(i),process.exit(1)}); diff --git a/plugin/scripts/worker-wrapper.cjs b/plugin/scripts/worker-wrapper.cjs index 74d200da..e1fec0eb 100755 --- a/plugin/scripts/worker-wrapper.cjs +++ b/plugin/scripts/worker-wrapper.cjs @@ -1,2 +1,2 @@ #!/usr/bin/env bun -"use strict";var u=Object.create;var w=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var f=Object.getOwnPropertyNames;var g=Object.getPrototypeOf,k=Object.prototype.hasOwnProperty;var y=(e,i,t,o)=>{if(i&&typeof i=="object"||typeof i=="function")for(let s of f(i))!k.call(e,s)&&s!==t&&w(e,s,{get:()=>i[s],enumerable:!(o=I(i,s))||o.enumerable});return e};var P=(e,i,t)=>(t=e!=null?u(g(e)):{},y(i||!e||!e.__esModule?w(t,"default",{value:e,enumerable:!0}):t,e));var c=require("child_process"),p=P(require("path"),1),h=process.platform==="win32",x=__dirname,l=p.default.join(x,"worker-service.cjs"),n=null,a=!1;function r(e){let i=new Date().toISOString();console.log(`[${i}] [wrapper] ${e}`)}function m(){r(`Spawning inner worker: ${l}`),n=(0,c.spawn)(process.execPath,[l],{stdio:["inherit","inherit","inherit","ipc"],env:{...process.env,CLAUDE_MEM_MANAGED:"true"},cwd:p.default.dirname(l)}),n.on("message",async e=>{(e.type==="restart"||e.type==="shutdown")&&(r(`${e.type} requested by inner`),a=!0,await d(),r("Exiting wrapper"),process.exit(0))}),n.on("exit",(e,i)=>{r(`Inner exited with code=${e}, signal=${i}`),n=null,!a&&e!==0&&(r("Inner crashed, respawning in 1 second..."),setTimeout(()=>m(),1e3))}),n.on("error",e=>{r(`Inner error: ${e.message}`)})}async function d(){if(!n||!n.pid){r("No inner process to kill");return}let e=n.pid;if(r(`Killing inner process tree (pid=${e})`),h)try{(0,c.execSync)(`taskkill /PID ${e} /T /F`,{timeout:1e4,stdio:"ignore"}),r(`taskkill completed for pid=${e}`)}catch(i){r(`taskkill failed (process may be dead): ${i}`)}else{n.kill("SIGTERM");let i=new Promise(o=>{if(!n){o();return}n.on("exit",()=>o())}),t=new Promise(o=>setTimeout(()=>o(),5e3));await Promise.race([i,t]),n&&!n.killed&&(r("Inner did not exit gracefully, force killing"),n.kill("SIGKILL"))}await S(e,5e3),n=null,r("Inner process terminated")}async function S(e,i){let t=Date.now();for(;Date.now()-tsetTimeout(o,100))}catch{return}r(`Timeout waiting for process ${e} to exit`)}process.on("SIGTERM",async()=>{r("Wrapper received SIGTERM"),a=!0,await d(),process.exit(0)});process.on("SIGINT",async()=>{r("Wrapper received SIGINT"),a=!0,await d(),process.exit(0)});r("Wrapper starting");m(); +"use strict";var m=Object.create;var w=Object.defineProperty;var u=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var f=Object.getPrototypeOf,x=Object.prototype.hasOwnProperty;var g=(e,i,n,o)=>{if(i&&typeof i=="object"||typeof i=="function")for(let s of I(i))!x.call(e,s)&&s!==n&&w(e,s,{get:()=>i[s],enumerable:!(o=u(i,s))||o.enumerable});return e};var k=(e,i,n)=>(n=e!=null?m(f(e)):{},g(i||!e||!e.__esModule?w(n,"default",{value:e,enumerable:!0}):n,e));var c=require("child_process"),p=k(require("path"),1),y=process.platform==="win32",P=__dirname,l=p.default.join(P,"worker-service.cjs"),t=null,a=!1;function r(e){let i=new Date().toISOString();console.log(`[${i}] [wrapper] ${e}`)}function h(){r(`Spawning inner worker: ${l}`),t=(0,c.spawn)(process.execPath,[l],{stdio:["inherit","inherit","inherit","ipc"],env:{...process.env,CLAUDE_MEM_MANAGED:"true"},cwd:p.default.dirname(l)}),t.on("message",async e=>{(e.type==="restart"||e.type==="shutdown")&&(r(`${e.type} requested by inner`),a=!0,await d(),r("Exiting wrapper"),process.exit(0))}),t.on("exit",(e,i)=>{r(`Inner exited with code=${e}, signal=${i}`),t=null,a||(r("Inner exited unexpectedly, wrapper exiting (hooks will restart if needed)"),process.exit(e??1))}),t.on("error",e=>{r(`Inner error: ${e.message}`)})}async function d(){if(!t||!t.pid){r("No inner process to kill");return}let e=t.pid;if(r(`Killing inner process tree (pid=${e})`),y)try{(0,c.execSync)(`taskkill /PID ${e} /T /F`,{timeout:1e4,stdio:"ignore"}),r(`taskkill completed for pid=${e}`)}catch(i){r(`taskkill failed (process may be dead): ${i}`)}else{t.kill("SIGTERM");let i=new Promise(o=>{if(!t){o();return}t.on("exit",()=>o())}),n=new Promise(o=>setTimeout(()=>o(),5e3));await Promise.race([i,n]),t&&!t.killed&&(r("Inner did not exit gracefully, force killing"),t.kill("SIGKILL"))}await S(e,5e3),t=null,r("Inner process terminated")}async function S(e,i){let n=Date.now();for(;Date.now()-nsetTimeout(o,100))}catch{return}r(`Timeout waiting for process ${e} to exit`)}process.on("SIGTERM",async()=>{r("Wrapper received SIGTERM"),a=!0,await d(),process.exit(0)});process.on("SIGINT",async()=>{r("Wrapper received SIGINT"),a=!0,await d(),process.exit(0)});r("Wrapper starting");h(); diff --git a/src/services/process/ProcessManager.ts b/src/services/process/ProcessManager.ts index e77d88aa..51c1db32 100644 --- a/src/services/process/ProcessManager.ts +++ b/src/services/process/ProcessManager.ts @@ -5,6 +5,7 @@ import { spawn, spawnSync } from 'child_process'; import { homedir } from 'os'; import { DATA_DIR } from '../../shared/paths.js'; import { getBunPath, isBunAvailable } from '../../utils/bun-path.js'; +import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js'; const PID_FILE = join(DATA_DIR, 'worker.pid'); const LOG_DIR = join(DATA_DIR, 'logs'); @@ -16,6 +17,7 @@ const HEALTH_CHECK_TIMEOUT_MS = 10000; const HEALTH_CHECK_INTERVAL_MS = 200; const HEALTH_CHECK_FETCH_TIMEOUT_MS = 1000; const PROCESS_EXIT_CHECK_INTERVAL_MS = 100; +const HTTP_SHUTDOWN_TIMEOUT_MS = 2000; interface PidInfo { pid: number; @@ -99,8 +101,9 @@ export class ProcessManager { const escapedBunPath = this.escapePowerShellString(bunPath); const escapedScript = this.escapePowerShellString(script); const escapedWorkDir = this.escapePowerShellString(MARKETPLACE_ROOT); + const escapedLogFile = this.escapePowerShellString(logFile); const envVars = `$env:CLAUDE_MEM_WORKER_PORT='${port}'`; - const psCommand = `${envVars}; Start-Process -FilePath '${escapedBunPath}' -ArgumentList '${escapedScript}' -WorkingDirectory '${escapedWorkDir}' -WindowStyle Hidden -PassThru | Select-Object -ExpandProperty Id`; + const psCommand = `${envVars}; Start-Process -FilePath '${escapedBunPath}' -ArgumentList '${escapedScript}' -WorkingDirectory '${escapedWorkDir}' -WindowStyle Hidden -RedirectStandardOutput '${escapedLogFile}' -RedirectStandardError '${escapedLogFile}.err' -PassThru | Select-Object -ExpandProperty Id`; const result = spawnSync('powershell', ['-Command', psCommand], { stdio: 'pipe', @@ -171,34 +174,65 @@ export class ProcessManager { static async stop(timeout: number = PROCESS_STOP_TIMEOUT_MS): Promise { const info = this.getPidInfo(); - if (!info) return true; - try { - if (process.platform === 'win32') { - // On Windows, use taskkill /T /F to kill entire process tree + if (process.platform === 'win32') { + // Windows: Try graceful HTTP shutdown first - this works regardless of PID file state + // because the worker shuts itself down from the inside (via wrapper IPC) + const port = info?.port ?? this.getPortFromSettings(); + const httpShutdownSucceeded = await this.tryHttpShutdown(port); + + if (httpShutdownSucceeded) { + // HTTP shutdown succeeded - worker confirmed down, safe to remove PID file + this.removePidFile(); + return true; + } + + // HTTP shutdown failed (worker not responding), fall back to taskkill + if (!info) { + // No PID file and HTTP failed - nothing more we can do + return true; + } + + const { execSync } = await import('child_process'); + try { + // Use taskkill /T /F to kill entire process tree // This ensures the wrapper AND all its children (inner worker, MCP, ChromaSync) are killed // which is necessary to properly release the socket and avoid zombie ports - const { execSync } = await import('child_process'); - try { - execSync(`taskkill /PID ${info.pid} /T /F`, { timeout: 10000, stdio: 'ignore' }); - } catch { - // Process may already be dead - } - } else { - // On Unix, use signals + execSync(`taskkill /PID ${info.pid} /T /F`, { timeout: 10000, stdio: 'ignore' }); + } catch { + // Process may already be dead + } + + // Wait for process to actually exit before removing PID file + try { + await this.waitForExit(info.pid, timeout); + } catch { + // Timeout waiting - process may still be alive + } + + // Only remove PID file if process is confirmed dead + if (!this.isProcessAlive(info.pid)) { + this.removePidFile(); + } + return true; + } else { + // Unix: Use signals (unchanged behavior) + if (!info) return true; + + try { process.kill(info.pid, 'SIGTERM'); await this.waitForExit(info.pid, timeout); - } - } catch { - try { - process.kill(info.pid, 'SIGKILL'); } catch { - // Process already dead + try { + process.kill(info.pid, 'SIGKILL'); + } catch { + // Process already dead + } } - } - this.removePidFile(); - return true; + this.removePidFile(); + return true; + } } static async restart(port: number): Promise<{ success: boolean; pid?: number; error?: string }> { @@ -229,6 +263,66 @@ export class ProcessManager { return alive; } + /** + * Get worker port from settings file + */ + private static getPortFromSettings(): number { + try { + const settingsPath = join(DATA_DIR, 'settings.json'); + const settings = SettingsDefaultsManager.loadFromFile(settingsPath); + return parseInt(settings.CLAUDE_MEM_WORKER_PORT, 10); + } catch { + return parseInt(SettingsDefaultsManager.get('CLAUDE_MEM_WORKER_PORT'), 10); + } + } + + /** + * Try to shut down the worker via HTTP endpoint + * Returns true if shutdown succeeded, false if worker not responding + */ + private static async tryHttpShutdown(port: number): Promise { + try { + // Send shutdown request + const response = await fetch(`http://127.0.0.1:${port}/api/admin/shutdown`, { + method: 'POST', + signal: AbortSignal.timeout(HTTP_SHUTDOWN_TIMEOUT_MS) + }); + + if (!response.ok) { + return false; + } + + // Wait for worker to actually stop responding + return await this.waitForWorkerDown(port, PROCESS_STOP_TIMEOUT_MS); + } catch { + // Worker not responding to HTTP - it may be dead or hung + return false; + } + } + + /** + * Wait for worker to stop responding on the given port + */ + private static async waitForWorkerDown(port: number, timeout: number): Promise { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + try { + await fetch(`http://127.0.0.1:${port}/api/health`, { + signal: AbortSignal.timeout(500) + }); + // Still responding, wait and retry + await new Promise(resolve => setTimeout(resolve, PROCESS_EXIT_CHECK_INTERVAL_MS)); + } catch { + // Worker stopped responding - success + return true; + } + } + + // Timeout - worker still responding + return false; + } + // Helper methods private static getPidInfo(): PidInfo | null { try { diff --git a/src/services/worker-wrapper.ts b/src/services/worker-wrapper.ts index 7835cece..fca9dee5 100644 --- a/src/services/worker-wrapper.ts +++ b/src/services/worker-wrapper.ts @@ -3,11 +3,15 @@ * * This wrapper exists to solve the Windows zombie port problem. * The wrapper spawns the actual worker as a child process. - * When restart/shutdown is requested, the wrapper kills the child - * and respawns it (or exits), ensuring clean socket cleanup. + * When shutdown is requested, the wrapper kills the child and exits. + * The hooks will start a fresh wrapper+worker if needed. * * The wrapper itself has no sockets, so Bun's socket cleanup bug * doesn't affect it. + * + * NOTE: The wrapper does NOT auto-restart the worker on crash. + * This is intentional - the hooks handle startup via ensureWorkerRunning(). + * Auto-restart would cause PID file mismatches and potential infinite loops. */ import { spawn, ChildProcess, execSync } from 'child_process'; @@ -51,10 +55,11 @@ function spawnInner() { log(`Inner exited with code=${code}, signal=${signal}`); inner = null; - // If inner crashed unexpectedly (not during shutdown), respawn it - if (!isShuttingDown && code !== 0) { - log('Inner crashed, respawning in 1 second...'); - setTimeout(() => spawnInner(), 1000); + // Don't auto-restart - let hooks handle it via ensureWorkerRunning() + // Auto-restart causes PID file mismatches and potential infinite loops + if (!isShuttingDown) { + log('Inner exited unexpectedly, wrapper exiting (hooks will restart if needed)'); + process.exit(code ?? 1); } });