@lensmcp/cluster 1.18.6 → 1.18.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- "use strict";var V=Object.defineProperty;var L=(t,c)=>V(t,"name",{value:c,configurable:!0});var G=Object.defineProperty,i=L((t,c)=>G(t,"name",{value:c,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.probePortFree=probePortFree,exports.probePortListening=probePortListening,exports.waitForPortFree=waitForPortFree,exports.viteArgPort=viteArgPort,exports.createLensChildren=createLensChildren;const tslib_1=require("tslib"),net=tslib_1.__importStar(require("node:net")),path=tslib_1.__importStar(require("node:path")),node_child_process_1=require("node:child_process"),lens_frontend_1=require("@lensmcp/nx-plugin/lens-frontend"),pod_env_1=require("./pod-env"),scope_1=require("./scope"),workspace_registry_1=require("./workspace-registry"),lifecycle_1=require("./lifecycle"),types_1=require("./types"),observability_1=require("./observability"),HEAL_BASE_MS=1500,HEAL_MAX_MS=3e4,HEAL_HEALTHY_MS=2e4;function tryBind(t,c){return new Promise(p=>{const h=net.createServer();h.unref(),h.once("error",E=>{const _=E.code??"";p(_==="EADDRINUSE"||_==="EACCES"?"in-use":"unavailable")}),h.listen(c===void 0?{port:t}:{port:t,host:c},()=>h.close(()=>p("free")))})}L(tryBind,"tryBind"),i(tryBind,"tryBind");async function probePortFree(t){for(const c of[void 0,"127.0.0.1","::1"])if(await tryBind(t,c)==="in-use")return!1;return!0}L(probePortFree,"probePortFree"),i(probePortFree,"probePortFree");function probeConnect(t,c,p){return new Promise(h=>{let E=!1;const _=net.createConnection({host:t,port:c});_.unref();const g=setTimeout(()=>b(!1),p);g.unref?.();function b(M){E||(E=!0,clearTimeout(g),_.destroy(),h(M))}L(b,"g"),i(b,"finish"),_.once("connect",()=>b(!0)),_.once("error",()=>b(!1))})}L(probeConnect,"probeConnect"),i(probeConnect,"probeConnect");async function probePortListening(t,c=1e3){return await probeConnect("127.0.0.1",t,c)?!0:probeConnect("::1",t,c)}L(probePortListening,"probePortListening"),i(probePortListening,"probePortListening");const sleepUnref=i(t=>new Promise(c=>{setTimeout(c,t).unref?.()}),"sleepUnref");async function waitForPortFree(t,c,p={}){const h=p.isPortFree??probePortFree,E=Math.max(1,p.intervalMs??types_1.PORT_FREE_POLL_MS),_=Date.now()+Math.max(0,c);for(;;){if(await h(t))return!0;const g=_-Date.now();if(g<=0)return!1;await sleepUnref(Math.min(E,g))}}L(waitForPortFree,"waitForPortFree"),i(waitForPortFree,"waitForPortFree");function viteArgPort(t){const c=i(p=>Number.isInteger(p)&&p>0&&p<65536?p:void 0,"valid");for(let p=0;p<t.length;p+=1){const h=t[p];if(h==="--port")return c(Number(t[p+1]));if(h.startsWith("--port="))return c(Number(h.slice(7)))}}L(viteArgPort,"viteArgPort"),i(viteArgPort,"viteArgPort");function createLensChildren(t,c,p,h){const{emit:E}=c,_=h?.isPortFree??probePortFree,g=[],b=new Map,M=new Map,A=new Map;let R;const v=new Map,y=new Set,j=new Set,D=new Set,m=i(async(o,a)=>{let s=o;for(;a.has(s)||!await _(s);)s+=1;return a.add(s),s!==o&&console.warn(`[gateway] port ${o} is unavailable (another process holds it \u2014 an orphan from a crashed gateway?) \u2014 using ${s} instead.`),s},"claimFreePort"),C=i((o,a,s,e,r=!1,n,l)=>{const d=Date.now(),u=(0,node_child_process_1.spawn)(a,s,{cwd:l??t.root,env:{...(0,pod_env_1.podBaseEnv)(process.env,t.root,l??t.root),...e??{},LENSMCP_PARENT_PID:String(process.pid)},stdio:"inherit"});g.push(u),v.set(o,u);const $=o==="vite"||o.endsWith(":vite"),w=$?viteArgPort(s):void 0;if($){const P=typeof e?.LENSMCP_EVENT_FILE=="string"?e.LENSMCP_EVENT_FILE:void 0;M.set(u,{project:e?.LENSMCP_PROJECT??"app",spawnedAt:d,port:w,probeFails:0,listening:!1,...P&&P!==t.eventFile?{bus:P}:{}})}return u.on("exit",(P,O)=>{const f=g.indexOf(u);if(f>=0&&g.splice(f,1),M.delete(u),t.stopped()||y.has(o))return;const T=Date.now()-d,S=b.get(o)??{attempts:0,portConflicts:0};(async()=>{const I=w!==void 0&&!await _(w);if((0,lifecycle_1.isPortConflictExit)({code:P,signal:O,ranMs:T,portHeld:I},{fastFailMs:types_1.CHILD_START_FAIL_MS})&&S.portConflicts<types_1.MAX_PORT_CONFLICT_RETRIES)S.portConflicts+=1,b.set(o,S),console.warn(`[gateway] lens child ${o} lost the :${w} bind race (EADDRINUSE, code=${P}); waiting for the port, then retrying (port-retry ${S.portConflicts}/${types_1.MAX_PORT_CONFLICT_RETRIES}; crash budget untouched).`);else{S.portConflicts=0,T>=HEAL_HEALTHY_MS&&(S.attempts=0);const N=Math.min(HEAL_MAX_MS,HEAL_BASE_MS*2**S.attempts);S.attempts+=1,b.set(o,S),console.warn(`[gateway] lens child ${o} exited (code=${P} sig=${O}); auto-healing in ${Math.round(N/1e3)}s (attempt ${S.attempts}).`),await sleepUnref(N)}t.stopped()||y.has(o)||(w!==void 0&&!await waitForPortFree(w,types_1.PORT_FREE_WAIT_MS,{isPortFree:_,intervalMs:types_1.PORT_FREE_POLL_MS})&&console.warn(`[gateway] lens child ${o}: :${w} still held after ${Math.round(types_1.PORT_FREE_WAIT_MS/1e3)}s \u2014 spawning anyway.`),!(t.stopped()||y.has(o))&&C(o,a,s,e,r,n,l))})().catch(()=>{})}),u},"spawnManagedChild");let F=!1;const B=i(async(o,a)=>{if(!F){F=!0;try{for(const[s,e]of[...M]){if(t.stopped())return;if(e.port===void 0||s.exitCode!==null||s.signalCode!==null)continue;if(await probePortListening(e.port,types_1.LENS_PROBE_TIMEOUT_MS)){e.probeFails=0,e.listening||(e.listening=!0,E("info",`lens app up: ${e.project} (vite :${e.port} listening)`,`cluster-lens-app:${e.project}`,{kind:"service-up",project:e.project,port:e.port},void 0,e.bus));continue}if(Date.now()-e.spawnedAt<o.graceMs){e.probeFails=0;continue}if(!a)continue;e.probeFails+=1;const r=(0,lifecycle_1.lensChildWedged)(e,A.get(e.project)??0,Date.now(),o);if(r){A.set(e.project,Date.now()),e.probeFails=0,e.listening=!1,console.error(`[gateway] wedge recycle: lens app ${e.project} is ALIVE but NOT LISTENING on :${e.port} after ${Math.max(2,o.failureThreshold)} probes \u2014 SIGKILL; a fresh vite is coming.`),E("warning",`wedge recycle: ${e.project} (${r})`,`cluster-lens-app:${e.project}`,{kind:"pod-recycle",project:e.project,reason:r,port:e.port},void 0,e.bus);try{s.kill("SIGKILL")}catch{}}}}finally{F=!1}}},"probeLensChildren"),x=i(()=>{if(R||!types_1.POD_RECYCLE_ENABLED&&!types_1.LENS_PROBE_ENABLED)return;const o={graceMs:types_1.POD_STALE_GRACE_MS,settleMs:types_1.POD_RECYCLE_SETTLE_MS,cooldownMs:types_1.POD_STALE_RECYCLE_COOLDOWN_MS};R=setInterval(()=>{if(t.stopped()||M.size===0||(k(),!types_1.POD_RECYCLE_ENABLED))return;const a=t.sourceSetChangedAt??0;if(a<=0)return;const s=Date.now();for(const[e,r]of M){const n=t.sourceSetChangedFor?.get(r.project)??a,l=A.get(r.project)??0;if((0,lifecycle_1.lensFrontendStale)(r.spawnedAt,l,n,s,o)){A.set(r.project,s),r.listening=!1,console.log(`[gateway] zombie recycle: lens app ${r.project} (stale-source-set) \u2014 vite missed a file add; restarting it`),E("warning",`zombie recycle: ${r.project} (stale-source-set)`,`cluster-lens-app:${r.project}`,{kind:"pod-recycle",project:r.project,reason:"stale-source-set"},void 0,r.bus);try{e.kill("SIGTERM")}catch{}}}},Math.min(types_1.POD_STALE_SCAN_MS,15e3)),R.unref?.()},"startLensSweeper"),K=i(async()=>{const o=[...new Map(t.routes.filter(e=>e.lens).map(e=>[e.project,e.lens])).values()],a=p.dashboard!==!1,s=p.mcp!==!1&&o.length>0;if(a){const e=(0,lens_frontend_1.findDashboardBundle)(t.root);if(!e)console.warn("[gateway] lens dashboard bundle not found \u2014 skipping. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/dashboard.js`), or `yarn nx build @lensmcp/lensmcp-mcp` in-repo.");else{const r=(0,scope_1.readLensScope)(t.root),n=await m(p.dashboardPort??r.dashboardPort,j),l=p.lensHost??"lensmcp.local",d=r.basePath;t.registry.appendRoutes(t.wsKey,[{host:l,prefix:d,project:observability_1.LENS_DASHBOARD_PROJECT,target:`http://localhost:${n}`}]),t.rebuildRoutes(),(0,workspace_registry_1.registerWorkspace)({key:r.key,root:t.root,base:d,pid:process.pid,procStamp:(0,workspace_registry_1.processStamp)(process.pid),updatedAt:new Date().toISOString()}),console.log(`[gateway] starting lens dashboard \u2192 http://localhost:${n} (routed at https://${l}${d}/)`),E("info",`lens dashboard up: ${l}${d} \u2192 :${n}`,"cluster-lens-dashboard",{kind:"lens-dashboard-up",host:l,port:n,base:d}),C("lens-dashboard",process.execPath,[e],{LENSMCP_EVENT_FILE:t.eventFile,LENSMCP_DASHBOARD_PORT:String(n),LENSMCP_DASHBOARD_BASE:d})}}if(s){const e=(0,lens_frontend_1.findMcpBundle)(t.root);if(!e)console.warn("[gateway] MCP server bundle not found \u2014 skipping (lens apps will still publish to the bus; an external `lensmcp mcp` can read it). Install: `npm i -D lensmcp`.");else{const r=(0,scope_1.readLensScope)(t.root),n=Number(process.env.LENSMCP_PORT??r.mcpHttpPort);console.log(`[gateway] starting lens MCP server on 127.0.0.1:${n} (${e})`),C("lens-mcp",process.execPath,[e],{LENSMCP_EVENT_FILE:t.eventFile,LENSMCP_TRANSPORT:process.env.LENSMCP_TRANSPORT??"http",LENSMCP_PORT:String(n),LENSMCP_WORKSPACE_KEY:r.key,LENSMCP_WORKSPACE_ROOT:t.root})}}for(const e of o){if(e.started)continue;e.started=!0;const r=await m(e.port,D);if(r!==e.port){for(const n of t.routes)n.lens?.project===e.project&&(n.lens.port=r,n.target=`http://localhost:${r}`);e.port=r}try{(0,lens_frontend_1.spawnLensFrontend)({projectRoot:e.projectRoot,workspaceRoot:t.root,project:e.project,eventFile:t.eventFile,port:e.port,chrome:!0,headless:!0,viteEnv:{LENSMCP_PROJECT:e.project},log:i(n=>console.log(n),"log")},C),E("info",`lens app started: ${e.project} (vite :${e.port})`,`cluster-lens-app:${e.project}`,{kind:"lens-app-up",project:e.project,port:e.port})}catch(n){console.error(`[gateway] lens app ${e.project}: ${n.message}`)}}x()},"bootSingletonsAndApps"),W=i(async o=>{const a=(0,lens_frontend_1.findDashboardBundle)(t.root);if(!a)return;const s=(0,scope_1.readLensScope)(o.root),e=p.lensHost??"lensmcp.local",r=s.basePath,n=await m(s.dashboardPort,j),l=`lens-dashboard:${o.wsKey}`,d=path.join(o.root,".lensmcp","events.jsonl");return y.delete(l),t.registry.appendRoutes(o.wsKey,[{host:e,prefix:r,project:observability_1.LENS_DASHBOARD_PROJECT,target:`http://localhost:${n}`}]),(0,workspace_registry_1.registerWorkspace)({key:o.wsKey,root:o.root,base:r,pid:process.pid,procStamp:(0,workspace_registry_1.processStamp)(process.pid),updatedAt:new Date().toISOString()}),console.log(`[gateway] hosting dashboard for workspace '${o.wsKey}' \u2192 http://localhost:${n} (routed at https://${e}${r}/)`),E("info",`lens dashboard up: ${e}${r} \u2192 :${n}`,"cluster-lens-dashboard",{kind:"lens-dashboard-up",host:e,port:n,base:r}),C(l,process.execPath,[a],{LENSMCP_EVENT_FILE:d,LENSMCP_DASHBOARD_PORT:String(n),LENSMCP_DASHBOARD_BASE:r,LENSMCP_WS_KEY:o.wsKey},!1,void 0,o.root),()=>{y.add(l),j.delete(n),(0,workspace_registry_1.unregisterWorkspace)(o.wsKey);try{v.get(l)?.kill("SIGTERM")}catch{}v.delete(l)}},"hostWorkspaceDashboard"),H=i(async o=>{const a=new Map;for(const n of o.routes){if(!n.lens)continue;const l=a.get(n.lens.project)??[];l.push(n),a.set(n.lens.project,l)}if(a.size===0)return()=>{};const s=path.join(o.root,".lensmcp","events.jsonl"),e=[],r=[];for(const[n,l]of a){const d=l[0].lens;if(d.started)continue;const u=await m(d.port,D);r.push(u);for(const f of l)f.lens.started=!0,f.lens.port=u,f.target=`http://localhost:${u}`;const $=`${o.wsKey}:${n}:`,w=`${$}vite`,P=`${$}browser-capture`;e.push(w,P),y.delete(w),y.delete(P);const O=i((f,T,S,I,N,Y)=>C(f,T,S,I,N,Y,o.root),"spawnInRoot");console.log(`[gateway] hosting lens app '${n}' of '${o.wsKey}' \u2192 vite :${u}`);try{(0,lens_frontend_1.spawnLensFrontend)({projectRoot:d.projectRoot,workspaceRoot:o.root,project:n,eventFile:s,port:u,chrome:!0,headless:!0,viteEnv:{LENSMCP_PROJECT:n},labelPrefix:$,log:i(f=>console.log(f),"log")},O),E("info",`lens app started: ${o.wsKey}/${n} (vite :${u})`,`cluster-lens-app:${o.wsKey}:${n}`,{kind:"lens-app-up",project:n,port:u},void 0,s)}catch(f){console.error(`[gateway] lens app ${o.wsKey}/${n}: ${f.message}`)}}return()=>{for(const n of e){y.add(n);try{v.get(n)?.kill("SIGTERM")}catch{}v.delete(n)}for(const n of r)D.delete(n)}},"hostWorkspaceApps"),q=i(async(o=2500)=>{R&&(clearInterval(R),R=void 0);const a=[...g];for(const r of a)try{r.kill("SIGTERM")}catch{}const s=i(r=>r.exitCode===null&&r.signalCode===null,"alive"),e=Date.now()+o;for(;Date.now()<e&&a.some(s);)await new Promise(r=>setTimeout(r,100));for(const r of a)if(s(r))try{r.kill("SIGKILL")}catch{}},"reapAll"),k=i(()=>B({graceMs:types_1.LENS_PROBE_GRACE_MS,failureThreshold:types_1.LENS_PROBE_FAILS,cooldownMs:types_1.LENS_PROBE_COOLDOWN_MS},types_1.LENS_PROBE_ENABLED),"probeLensFrontends");return{spawnManagedChild:C,bootSingletonsAndApps:K,hostWorkspaceDashboard:W,hostWorkspaceApps:H,probeLensFrontends:k,reapAll:q}}L(createLensChildren,"createLensChildren"),i(createLensChildren,"createLensChildren");
1
+ "use strict";var J=Object.defineProperty;var L=(r,p)=>J(r,"name",{value:p,configurable:!0});var z=Object.defineProperty,i=L((r,p)=>z(r,"name",{value:p,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.probePortFree=probePortFree,exports.probePortListening=probePortListening,exports.waitForPortFree=waitForPortFree,exports.viteArgPort=viteArgPort,exports.createLensChildren=createLensChildren;const tslib_1=require("tslib"),net=tslib_1.__importStar(require("node:net")),path=tslib_1.__importStar(require("node:path")),node_child_process_1=require("node:child_process"),lens_frontend_1=require("@lensmcp/nx-plugin/lens-frontend"),pod_env_1=require("./pod-env"),scope_1=require("./scope"),workspace_registry_1=require("./workspace-registry"),lifecycle_1=require("./lifecycle"),types_1=require("./types"),observability_1=require("./observability"),HEAL_BASE_MS=1500,HEAL_MAX_MS=3e4,HEAL_HEALTHY_MS=2e4;function tryBind(r,p){return new Promise(l=>{const E=net.createServer();E.unref(),E.once("error",d=>{const h=d.code??"";l(h==="EADDRINUSE"||h==="EACCES"?"in-use":"unavailable")}),E.listen(p===void 0?{port:r}:{port:r,host:p},()=>E.close(()=>l("free")))})}L(tryBind,"tryBind"),i(tryBind,"tryBind");async function probePortFree(r){for(const p of[void 0,"0.0.0.0","127.0.0.1","::1"])if(await tryBind(r,p)==="in-use")return!1;return!0}L(probePortFree,"probePortFree"),i(probePortFree,"probePortFree");function probeConnect(r,p,l){return new Promise(E=>{let d=!1;const h=net.createConnection({host:r,port:p});h.unref();const S=setTimeout(()=>M(!1),l);S.unref?.();function M(C){d||(d=!0,clearTimeout(S),h.destroy(),E(C))}L(M,"M"),i(M,"finish"),h.once("connect",()=>M(!0)),h.once("error",()=>M(!1))})}L(probeConnect,"probeConnect"),i(probeConnect,"probeConnect");async function probePortListening(r,p=1e3){return await probeConnect("127.0.0.1",r,p)?!0:probeConnect("::1",r,p)}L(probePortListening,"probePortListening"),i(probePortListening,"probePortListening");const sleepUnref=i(r=>new Promise(p=>{setTimeout(p,r).unref?.()}),"sleepUnref");async function waitForPortFree(r,p,l={}){const E=l.isPortFree??probePortFree,d=Math.max(1,l.intervalMs??types_1.PORT_FREE_POLL_MS),h=Date.now()+Math.max(0,p);for(;;){if(await E(r))return!0;const S=h-Date.now();if(S<=0)return!1;await sleepUnref(Math.min(d,S))}}L(waitForPortFree,"waitForPortFree"),i(waitForPortFree,"waitForPortFree");function viteArgPort(r){const p=i(l=>Number.isInteger(l)&&l>0&&l<65536?l:void 0,"valid");for(let l=0;l<r.length;l+=1){const E=r[l];if(E==="--port")return p(Number(r[l+1]));if(E.startsWith("--port="))return p(Number(E.slice(7)))}}L(viteArgPort,"viteArgPort"),i(viteArgPort,"viteArgPort");function createLensChildren(r,p,l,E){const{emit:d}=p,h=E?.isPortFree??probePortFree,S=[],M=new Map,C=new Map,m=new Map;let $;const v=new Map,y=new Set,j=new Set,F=new Set,O=new Set,B=i((t,a,s)=>{if(O.has(t))return;O.add(t);const e=setInterval(()=>{(async()=>{if(r.stopped()||y.has(t)){clearInterval(e),O.delete(t);return}await h(a)&&(clearInterval(e),O.delete(t),console.log(`[gateway] lens child ${t}: :${a} freed \u2014 respawning.`),s())})().catch(()=>{})},types_1.LENS_PARKED_PORT_REPROBE_MS);e.unref?.()},"parkUntilPortFree"),N=i(async(t,a)=>{let s=t;for(;a.has(s)||!await h(s);)s+=1;return a.add(s),s!==t&&console.warn(`[gateway] port ${t} is unavailable (another process holds it \u2014 an orphan from a crashed gateway?) \u2014 using ${s} instead.`),s},"claimFreePort"),b=i((t,a,s,e,n=!1,o,c,u)=>{const P=Date.now(),g=(0,node_child_process_1.spawn)(a,s,{cwd:c??r.root,env:{...(0,pod_env_1.podBaseEnv)(process.env,r.root,c??r.root),...e??{},LENSMCP_PARENT_PID:String(process.pid)},stdio:"inherit"});S.push(g),v.set(t,g);const A=t==="vite"||t.endsWith(":vite"),_=A?viteArgPort(s):u;if(A){const R=typeof e?.LENSMCP_EVENT_FILE=="string"?e.LENSMCP_EVENT_FILE:void 0;C.set(g,{project:e?.LENSMCP_PROJECT??"app",spawnedAt:P,port:_,probeFails:0,listening:!1,...R&&R!==r.eventFile?{bus:R}:{}})}return g.on("exit",(R,w)=>{const T=S.indexOf(g);if(T>=0&&S.splice(T,1),C.delete(g),r.stopped()||y.has(t))return;const I=Date.now()-P,f=M.get(t)??{attempts:0,portConflicts:0};(async()=>{const x=_!==void 0&&!await h(_),D=(0,lifecycle_1.isPortConflictExit)({code:R,signal:w,ranMs:I,portHeld:x},{fastFailMs:types_1.CHILD_START_FAIL_MS});if(D&&f.portConflicts<types_1.MAX_PORT_CONFLICT_RETRIES)f.portConflicts+=1,M.set(t,f),console.warn(`[gateway] lens child ${t} lost the :${_} bind race (EADDRINUSE, code=${R}); waiting for the port, then retrying (port-retry ${f.portConflicts}/${types_1.MAX_PORT_CONFLICT_RETRIES}; crash budget untouched).`);else if(D){f.portConflicts=0,M.set(t,f),console.warn(`[gateway] lens child ${t}: :${_} persistently held after ${types_1.MAX_PORT_CONFLICT_RETRIES} bind races \u2014 another server owns it (an external/shared daemon?). Parking; re-probing every ${Math.round(types_1.LENS_PARKED_PORT_REPROBE_MS/1e3)}s, will respawn if it frees.`),d("warning",`lens child parked: ${t} (:${_} held externally)`,`cluster-lens-child:${t}`,{kind:"lens-child-parked",label:t,port:_}),B(t,_,()=>b(t,a,s,e,n,o,c,u));return}else{f.portConflicts=0,I>=HEAL_HEALTHY_MS&&(f.attempts=0);const G=Math.min(HEAL_MAX_MS,HEAL_BASE_MS*2**f.attempts);f.attempts+=1,M.set(t,f),console.warn(`[gateway] lens child ${t} exited (code=${R} sig=${w}); auto-healing in ${Math.round(G/1e3)}s (attempt ${f.attempts}).`),await sleepUnref(G)}r.stopped()||y.has(t)||(_!==void 0&&!await waitForPortFree(_,types_1.PORT_FREE_WAIT_MS,{isPortFree:h,intervalMs:types_1.PORT_FREE_POLL_MS})&&console.warn(`[gateway] lens child ${t}: :${_} still held after ${Math.round(types_1.PORT_FREE_WAIT_MS/1e3)}s \u2014 spawning anyway.`),!(r.stopped()||y.has(t))&&b(t,a,s,e,n,o,c,u))})().catch(()=>{})}),g},"spawnManagedChild");let k=!1;const H=i(async(t,a)=>{if(!k){k=!0;try{for(const[s,e]of[...C]){if(r.stopped())return;if(e.port===void 0||s.exitCode!==null||s.signalCode!==null)continue;if(await probePortListening(e.port,types_1.LENS_PROBE_TIMEOUT_MS)){e.probeFails=0,e.listening||(e.listening=!0,d("info",`lens app up: ${e.project} (vite :${e.port} listening)`,`cluster-lens-app:${e.project}`,{kind:"service-up",project:e.project,port:e.port},void 0,e.bus));continue}const n=e.listening?t.graceMs:Math.max(t.graceMs,t.neverListenedGraceMs);if(Date.now()-e.spawnedAt<n){e.probeFails=0;continue}if(!a)continue;e.probeFails+=1;const o=(0,lifecycle_1.lensChildWedged)({spawnedAt:e.spawnedAt,probeFails:e.probeFails,hasListened:e.listening},m.get(e.project)??0,Date.now(),t);if(!o)continue;const c=e.listening?"its listener VANISHED (was up, now refusing)":`it NEVER bound within the ${Math.round(n/1e3)}s boot allowance`;m.set(e.project,Date.now()),e.probeFails=0,e.listening=!1,console.error(`[gateway] wedge recycle: lens app ${e.project} is ALIVE but NOT LISTENING on :${e.port} after ${Math.max(2,t.failureThreshold)} probes \u2014 ${c} \u2014 SIGKILL; a fresh vite is coming.`),d("warning",`wedge recycle: ${e.project} (${o})`,`cluster-lens-app:${e.project}`,{kind:"pod-recycle",project:e.project,reason:o,port:e.port},void 0,e.bus);try{s.kill("SIGKILL")}catch{}}}finally{k=!1}}},"probeLensChildren"),W=i(()=>{if($||!types_1.POD_RECYCLE_ENABLED&&!types_1.LENS_PROBE_ENABLED)return;const t={graceMs:types_1.POD_STALE_GRACE_MS,settleMs:types_1.POD_RECYCLE_SETTLE_MS,cooldownMs:types_1.POD_STALE_RECYCLE_COOLDOWN_MS};$=setInterval(()=>{if(r.stopped()||C.size===0||(K(),!types_1.POD_RECYCLE_ENABLED))return;const a=r.sourceSetChangedAt??0;if(a<=0)return;const s=Date.now();for(const[e,n]of C){const o=r.sourceSetChangedFor?.get(n.project)??a,c=m.get(n.project)??0;if((0,lifecycle_1.lensFrontendStale)(n.spawnedAt,c,o,s,t)){m.set(n.project,s),n.listening=!1,console.log(`[gateway] zombie recycle: lens app ${n.project} (stale-source-set) \u2014 vite missed a file add; restarting it`),d("warning",`zombie recycle: ${n.project} (stale-source-set)`,`cluster-lens-app:${n.project}`,{kind:"pod-recycle",project:n.project,reason:"stale-source-set"},void 0,n.bus);try{e.kill("SIGTERM")}catch{}setTimeout(()=>{if(e.exitCode===null&&e.signalCode===null){console.log(`[gateway] zombie recycle: lens app ${n.project} ignored SIGTERM for ${Math.round(types_1.LENS_RECYCLE_KILL_GRACE_MS/1e3)}s \u2014 escalating to SIGKILL.`);try{e.kill("SIGKILL")}catch{}}},types_1.LENS_RECYCLE_KILL_GRACE_MS).unref?.()}}},Math.min(types_1.POD_STALE_SCAN_MS,15e3)),$.unref?.()},"startLensSweeper"),q=i(async()=>{const t=[...new Map(r.routes.filter(e=>e.lens).map(e=>[e.project,e.lens])).values()],a=l.dashboard!==!1,s=l.mcp!==!1&&t.length>0;if(a){const e=(0,lens_frontend_1.findDashboardBundle)(r.root);if(!e)console.warn("[gateway] lens dashboard bundle not found \u2014 skipping. Install the self-contained CLI: `npm i -D lensmcp` (it ships `bundled/dashboard.js`), or `yarn nx build @lensmcp/lensmcp-mcp` in-repo.");else{const n=(0,scope_1.readLensScope)(r.root),o=await N(l.dashboardPort??n.dashboardPort,j),c=l.lensHost??"lensmcp.local",u=n.basePath;r.registry.appendRoutes(r.wsKey,[{host:c,prefix:u,project:observability_1.LENS_DASHBOARD_PROJECT,target:`http://localhost:${o}`}]),r.rebuildRoutes(),(0,workspace_registry_1.registerWorkspace)({key:n.key,root:r.root,base:u,pid:process.pid,procStamp:(0,workspace_registry_1.processStamp)(process.pid),updatedAt:new Date().toISOString()}),console.log(`[gateway] starting lens dashboard \u2192 http://localhost:${o} (routed at https://${c}${u}/)`),d("info",`lens dashboard up: ${c}${u} \u2192 :${o}`,"cluster-lens-dashboard",{kind:"lens-dashboard-up",host:c,port:o,base:u}),b("lens-dashboard",process.execPath,[e],{LENSMCP_EVENT_FILE:r.eventFile,LENSMCP_DASHBOARD_PORT:String(o),LENSMCP_DASHBOARD_BASE:u},!1,void 0,void 0,o)}}if(s){const e=(0,lens_frontend_1.findMcpBundle)(r.root);if(!e)console.warn("[gateway] MCP server bundle not found \u2014 skipping (lens apps will still publish to the bus; an external `lensmcp mcp` can read it). Install: `npm i -D lensmcp`.");else{const n=(0,scope_1.readLensScope)(r.root),o=Number(process.env.LENSMCP_PORT??n.mcpHttpPort),c=i(()=>{console.log(`[gateway] starting lens MCP server on 127.0.0.1:${o} (${e})`),b("lens-mcp",process.execPath,[e],{LENSMCP_EVENT_FILE:r.eventFile,LENSMCP_TRANSPORT:process.env.LENSMCP_TRANSPORT??"http",LENSMCP_PORT:String(o),LENSMCP_WORKSPACE_KEY:n.key,LENSMCP_WORKSPACE_ROOT:r.root},!1,void 0,void 0,o)},"spawnMcp");await h(o)?c():(console.warn(`[gateway] lens MCP port :${o} is already served \u2014 an external lensmcp server (a shared daemon?) owns this workspace's MCP; not spawning a second. Re-probing every ${Math.round(types_1.LENS_PARKED_PORT_REPROBE_MS/1e3)}s; will take over if it frees.`),d("info",`lens MCP served externally on :${o} \u2014 gateway parked its own`,"cluster-lens-child:lens-mcp",{kind:"lens-mcp-external",port:o}),B("lens-mcp",o,c))}}for(const e of t){if(e.started)continue;e.started=!0;const n=await N(e.port,F);if(n!==e.port){for(const o of r.routes)o.lens?.project===e.project&&(o.lens.port=n,o.target=`http://localhost:${n}`);e.port=n}try{(0,lens_frontend_1.spawnLensFrontend)({projectRoot:e.projectRoot,workspaceRoot:r.root,project:e.project,eventFile:r.eventFile,port:e.port,chrome:!0,headless:!0,viteEnv:{LENSMCP_PROJECT:e.project},log:i(o=>console.log(o),"log")},b),d("info",`lens app started: ${e.project} (vite :${e.port})`,`cluster-lens-app:${e.project}`,{kind:"lens-app-up",project:e.project,port:e.port})}catch(o){console.error(`[gateway] lens app ${e.project}: ${o.message}`)}}W()},"bootSingletonsAndApps"),Y=i(async t=>{const a=(0,lens_frontend_1.findDashboardBundle)(r.root);if(!a)return;const s=(0,scope_1.readLensScope)(t.root),e=l.lensHost??"lensmcp.local",n=s.basePath,o=await N(s.dashboardPort,j),c=`lens-dashboard:${t.wsKey}`,u=path.join(t.root,".lensmcp","events.jsonl");return y.delete(c),r.registry.appendRoutes(t.wsKey,[{host:e,prefix:n,project:observability_1.LENS_DASHBOARD_PROJECT,target:`http://localhost:${o}`}]),(0,workspace_registry_1.registerWorkspace)({key:t.wsKey,root:t.root,base:n,pid:process.pid,procStamp:(0,workspace_registry_1.processStamp)(process.pid),updatedAt:new Date().toISOString()}),console.log(`[gateway] hosting dashboard for workspace '${t.wsKey}' \u2192 http://localhost:${o} (routed at https://${e}${n}/)`),d("info",`lens dashboard up: ${e}${n} \u2192 :${o}`,"cluster-lens-dashboard",{kind:"lens-dashboard-up",host:e,port:o,base:n}),b(c,process.execPath,[a],{LENSMCP_EVENT_FILE:u,LENSMCP_DASHBOARD_PORT:String(o),LENSMCP_DASHBOARD_BASE:n,LENSMCP_WS_KEY:t.wsKey},!1,void 0,t.root,o),()=>{y.add(c),j.delete(o),(0,workspace_registry_1.unregisterWorkspace)(t.wsKey);try{v.get(c)?.kill("SIGTERM")}catch{}v.delete(c)}},"hostWorkspaceDashboard"),V=i(async t=>{const a=new Map;for(const o of t.routes){if(!o.lens)continue;const c=a.get(o.lens.project)??[];c.push(o),a.set(o.lens.project,c)}if(a.size===0)return()=>{};const s=path.join(t.root,".lensmcp","events.jsonl"),e=[],n=[];for(const[o,c]of a){const u=c[0].lens;if(u.started)continue;const P=await N(u.port,F);n.push(P);for(const w of c)w.lens.started=!0,w.lens.port=P,w.target=`http://localhost:${P}`;const g=`${t.wsKey}:${o}:`,A=`${g}vite`,_=`${g}browser-capture`;e.push(A,_),y.delete(A),y.delete(_);const R=i((w,T,I,f,x,D)=>b(w,T,I,f,x,D,t.root),"spawnInRoot");console.log(`[gateway] hosting lens app '${o}' of '${t.wsKey}' \u2192 vite :${P}`);try{(0,lens_frontend_1.spawnLensFrontend)({projectRoot:u.projectRoot,workspaceRoot:t.root,project:o,eventFile:s,port:P,chrome:!0,headless:!0,viteEnv:{LENSMCP_PROJECT:o},labelPrefix:g,log:i(w=>console.log(w),"log")},R),d("info",`lens app started: ${t.wsKey}/${o} (vite :${P})`,`cluster-lens-app:${t.wsKey}:${o}`,{kind:"lens-app-up",project:o,port:P},void 0,s)}catch(w){console.error(`[gateway] lens app ${t.wsKey}/${o}: ${w.message}`)}}return()=>{for(const o of e){y.add(o);try{v.get(o)?.kill("SIGTERM")}catch{}v.delete(o)}for(const o of n)F.delete(o)}},"hostWorkspaceApps"),U=i(async(t=2500)=>{$&&(clearInterval($),$=void 0);const a=[...S];for(const n of a)try{n.kill("SIGTERM")}catch{}const s=i(n=>n.exitCode===null&&n.signalCode===null,"alive"),e=Date.now()+t;for(;Date.now()<e&&a.some(s);)await new Promise(n=>setTimeout(n,100));for(const n of a)if(s(n))try{n.kill("SIGKILL")}catch{}},"reapAll"),K=i(()=>H({graceMs:types_1.LENS_PROBE_GRACE_MS,failureThreshold:types_1.LENS_PROBE_FAILS,cooldownMs:types_1.LENS_PROBE_COOLDOWN_MS,neverListenedGraceMs:types_1.LENS_PROBE_BOOT_GRACE_MS},types_1.LENS_PROBE_ENABLED),"probeLensFrontends");return{spawnManagedChild:b,bootSingletonsAndApps:q,hostWorkspaceDashboard:Y,hostWorkspaceApps:V,probeLensFrontends:K,reapAll:U}}L(createLensChildren,"createLensChildren"),i(createLensChildren,"createLensChildren");
@@ -66,6 +66,17 @@ export interface LensChildProbeConfig {
66
66
  failureThreshold: number;
67
67
  /** Per-project anti-thrash window after any recycle (shared with {@link lensFrontendStale}). */
68
68
  cooldownMs: number;
69
+ /**
70
+ * The kill allowance for a child that has NEVER LISTENED this generation. "Alive but not
71
+ * listening" is the boot signature as much as the wedge signature: a big-monorepo vite (nx
72
+ * graph + tsconfig-paths + several vites recycling at once after a source-set change) can take
73
+ * minutes to bind, and killing it at `graceMs` respawns it into the SAME slow boot — the
74
+ * kill→boot→kill loop that turned a routine stale-source-set recycle into a minutes-long 502
75
+ * window (foodguard 2026-08-06: every "wedge recycle: … ALIVE but NOT LISTENING" chased a
76
+ * zombie recycle's OWN respawn). A vite whose listener VANISHED (it listened, then stopped) is
77
+ * the precise wedge the probe was built for and keeps the short `graceMs` path.
78
+ */
79
+ neverListenedGraceMs: number;
69
80
  }
70
81
  /**
71
82
  * PURE decision: is this ALIVE lens-frontend child WEDGED — process running, port NOT listening?
@@ -77,8 +88,12 @@ export interface LensChildProbeConfig {
77
88
  * (foodguard 2026-07-29 — see {@link LENS_PROBE_ENABLED}).
78
89
  *
79
90
  * The caller owns the socket I/O (a cheap TCP connect per tick) and accumulates `probeFails`; this owns
80
- * WHEN that evidence justifies a kill. Three guards, each earning its place:
81
- * - **grace** — a still-booting vite has no listener YET; killing it would be the outage, not the fix.
91
+ * WHEN that evidence justifies a kill. The guards, each earning its place:
92
+ * - **grace, split by `hasListened`** — a still-booting vite has no listener YET; killing it would be
93
+ * the outage, not the fix. A child that LISTENED and then lost its socket is the precise wedge →
94
+ * the short `graceMs`. A child that has NEVER listened this generation is indistinguishable from a
95
+ * slow cold boot → the long `neverListenedGraceMs` (killing it early just respawns it into the same
96
+ * slow boot — the kill→boot→kill loop; foodguard 2026-08-06).
82
97
  * - **N consecutive failures** (≥2, enforced here so even a misconfigured `failureThreshold: 1` can't
83
98
  * single-blip recycle) — a momentary refusal is not a wedge.
84
99
  * - **cooldown** — the same per-project window the two recycle paths share, so probe + staleness can
@@ -88,6 +103,7 @@ export interface LensChildProbeConfig {
88
103
  export declare function lensChildWedged(child: {
89
104
  spawnedAt: number;
90
105
  probeFails: number;
106
+ hasListened?: boolean;
91
107
  }, lastRecycleAt: number, now: number, cfg: LensChildProbeConfig): 'wedged-no-listener' | null;
92
108
  /**
93
109
  * PURE decision: was this child's exit a lost BIND RACE (EADDRINUSE) rather than a crash?
@@ -1,3 +1,3 @@
1
- "use strict";var N=Object.defineProperty;var j=(t,c)=>N(t,"name",{value:c,configurable:!0});var I=Object.defineProperty,s=j((t,c)=>I(t,"name",{value:c,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.sourceSetSignature=sourceSetSignature,exports.sourceScopeDirs=sourceScopeDirs,exports.podRecycleReason=podRecycleReason,exports.lensFrontendStale=lensFrontendStale,exports.lensChildWedged=lensChildWedged,exports.isPortConflictExit=isPortConflictExit,exports.createServiceLayer=createServiceLayer;const tslib_1=require("tslib"),node_child_process_1=require("node:child_process"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),node_util_1=require("node:util"),discovery_1=require("./discovery"),scope_1=require("./scope"),pod_env_1=require("./pod-env"),types_1=require("./types");function eastWestEnv(t,c){const l={},p=t.serviceKeys[c];if(p&&(l.LENSMCP_SERVICE_KEY=p),t.https){const a=require("../../../basic-ssl").caCertPath();fs.existsSync(a)&&(l.NODE_EXTRA_CA_CERTS=a)}return l}j(eastWestEnv,"eastWestEnv"),s(eastWestEnv,"eastWestEnv");const sleep=s(t=>new Promise(c=>setTimeout(c,t)),"sleep"),SRC_EXCLUDE=new Set(["node_modules","dist","tmp","coverage","build","out","target"]),SRC_EXT=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".css",".scss",".sass",".less",".json",".html",".htm",".vue",".svelte",".mdx"]),SCAN_FILE_CAP=1e5;function fnv1a(t){let c=2166136261;for(let l=0;l<t.length;l+=1)c^=t.charCodeAt(l),c=Math.imul(c,16777619);return c>>>0}j(fnv1a,"fnv1a"),s(fnv1a,"fnv1a");const execFileP=(0,node_util_1.promisify)(node_child_process_1.execFile);async function gitIgnoredPaths(t){try{const{stdout:c}=await execFileP("git",["ls-files","--others","--directory","--ignored","--exclude-standard","-z"],{cwd:t,maxBuffer:33554432,timeout:1e4,windowsHide:!0});return new Set(c.split("\0").filter(Boolean))}catch{return new Set}}j(gitIgnoredPaths,"gitIgnoredPaths"),s(gitIgnoredPaths,"gitIgnoredPaths");async function sourceSetSignature(t,c,l){let p=0,a=0;const v=await gitIgnoredPaths(t),_=s(f=>path.relative(t,f).split(path.sep).join("/"),"rel"),S=s(async(f,k=!0)=>{if(p>SCAN_FILE_CAP)return;let M;try{M=await fs.promises.readdir(f,{withFileTypes:!0})}catch{return}for(const y of M){if(p>SCAN_FILE_CAP)return;const m=y.name,A=path.join(f,m);if(y.isDirectory()){if(!k||m.startsWith(".")||SRC_EXCLUDE.has(m)||l?.has(m)||m.endsWith("-devserver")||v.has(`${_(A)}/`))continue;await S(A)}else if(y.isFile()){const R=m.lastIndexOf(".");if(R<=0||!SRC_EXT.has(m.slice(R))||v.has(_(A)))continue;p+=1,a^=fnv1a(A)}}},"walk");if(c&&c.length>0){await S(t,!1);for(const f of c)await S(f)}else await S(t);return`${p}:${(a>>>0).toString(36)}`}j(sourceSetSignature,"sourceSetSignature"),s(sourceSetSignature,"sourceSetSignature");function sourceScopeDirs(t,c,l,p,a){const v=s(f=>{const k=path.relative(t,f).split(path.sep).filter(Boolean);return k.length===0||k[0].startsWith("..")?"":k[0]},"bucketOf")(c);if(!v)return[];const _=new Set(l.filter(Boolean)),S=new Set([v]);for(const f of p)f.startsWith(".")||SRC_EXCLUDE.has(f)||a?.has(f)||f.endsWith("-devserver")||_.has(f)||S.add(f);return[...S].sort().map(f=>path.join(t,f))}j(sourceScopeDirs,"sourceScopeDirs"),s(sourceScopeDirs,"sourceScopeDirs");function podRecycleReason(t,c,l,p){return!t.proc||t.state!=="up"||t.spawnAt==null||t.inflight>0||c-t.lastUsed<p.settleMs||t.lastStaleRecycleAt!=null&&c-t.lastStaleRecycleAt<p.cooldownMs?null:l>0&&l-t.spawnAt>p.graceMs?"stale-source-set":p.maxAgeMs>0&&c-t.spawnAt>p.maxAgeMs?"max-age":null}j(podRecycleReason,"podRecycleReason"),s(podRecycleReason,"podRecycleReason");function lensFrontendStale(t,c,l,p,a){return!(p-c<a.cooldownMs||l<=0||l-t<=a.graceMs||p-l<a.settleMs)}j(lensFrontendStale,"lensFrontendStale"),s(lensFrontendStale,"lensFrontendStale");function lensChildWedged(t,c,l,p){return l-t.spawnedAt<p.graceMs||l-c<p.cooldownMs||t.probeFails<Math.max(2,p.failureThreshold)?null:"wedged-no-listener"}j(lensChildWedged,"lensChildWedged"),s(lensChildWedged,"lensChildWedged");function isPortConflictExit(t,c){return t.signal||t.code===null||t.code===0||t.ranMs>c.fastFailMs?!1:t.portHeld}j(isPortConflictExit,"isPortConflictExit"),s(isPortConflictExit,"isPortConflictExit");function createServiceLayer(t,c){const{emit:l}=c,p=s(e=>e.wsKey===t.wsKey?void 0:path.join(e.root,".lensmcp","events.jsonl"),"serviceBus"),a=s((e,o,r,n,i)=>l(o,r,n,{wsKey:e.wsKey,...i},void 0,p(e)),"emitSvc"),v=s(e=>(e.scannedAt=0,(0,discovery_1.pickSock)(e,t.scanTtlMs)?e.socks.length:0),"scanNow"),_=s((e,o)=>{let r;try{r=(0,node_child_process_1.execFileSync)("ps",["ax","-ww","-o","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3})}catch{return 0}const n=e.proc?.pid;let i=0;for(const d of r.split(`
1
+ "use strict";var N=Object.defineProperty;var j=(t,c)=>N(t,"name",{value:c,configurable:!0});var I=Object.defineProperty,s=j((t,c)=>I(t,"name",{value:c,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.sourceSetSignature=sourceSetSignature,exports.sourceScopeDirs=sourceScopeDirs,exports.podRecycleReason=podRecycleReason,exports.lensFrontendStale=lensFrontendStale,exports.lensChildWedged=lensChildWedged,exports.isPortConflictExit=isPortConflictExit,exports.createServiceLayer=createServiceLayer;const tslib_1=require("tslib"),node_child_process_1=require("node:child_process"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),node_util_1=require("node:util"),discovery_1=require("./discovery"),scope_1=require("./scope"),pod_env_1=require("./pod-env"),types_1=require("./types");function eastWestEnv(t,c){const p={},l=t.serviceKeys[c];if(l&&(p.LENSMCP_SERVICE_KEY=l),t.https){const a=require("../../../basic-ssl").caCertPath();fs.existsSync(a)&&(p.NODE_EXTRA_CA_CERTS=a)}return p}j(eastWestEnv,"eastWestEnv"),s(eastWestEnv,"eastWestEnv");const sleep=s(t=>new Promise(c=>setTimeout(c,t)),"sleep"),SRC_EXCLUDE=new Set(["node_modules","dist","tmp","coverage","build","out","target"]),SRC_EXT=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".css",".scss",".sass",".less",".json",".html",".htm",".vue",".svelte",".mdx"]),SCAN_FILE_CAP=1e5;function fnv1a(t){let c=2166136261;for(let p=0;p<t.length;p+=1)c^=t.charCodeAt(p),c=Math.imul(c,16777619);return c>>>0}j(fnv1a,"fnv1a"),s(fnv1a,"fnv1a");const execFileP=(0,node_util_1.promisify)(node_child_process_1.execFile);async function gitIgnoredPaths(t){try{const{stdout:c}=await execFileP("git",["ls-files","--others","--directory","--ignored","--exclude-standard","-z"],{cwd:t,maxBuffer:33554432,timeout:1e4,windowsHide:!0});return new Set(c.split("\0").filter(Boolean))}catch{return new Set}}j(gitIgnoredPaths,"gitIgnoredPaths"),s(gitIgnoredPaths,"gitIgnoredPaths");async function sourceSetSignature(t,c,p){let l=0,a=0;const v=await gitIgnoredPaths(t),_=s(f=>path.relative(t,f).split(path.sep).join("/"),"rel"),S=s(async(f,k=!0)=>{if(l>SCAN_FILE_CAP)return;let M;try{M=await fs.promises.readdir(f,{withFileTypes:!0})}catch{return}for(const y of M){if(l>SCAN_FILE_CAP)return;const m=y.name,A=path.join(f,m);if(y.isDirectory()){if(!k||m.startsWith(".")||SRC_EXCLUDE.has(m)||p?.has(m)||m.endsWith("-devserver")||v.has(`${_(A)}/`))continue;await S(A)}else if(y.isFile()){const R=m.lastIndexOf(".");if(R<=0||!SRC_EXT.has(m.slice(R))||v.has(_(A)))continue;l+=1,a^=fnv1a(A)}}},"walk");if(c&&c.length>0){await S(t,!1);for(const f of c)await S(f)}else await S(t);return`${l}:${(a>>>0).toString(36)}`}j(sourceSetSignature,"sourceSetSignature"),s(sourceSetSignature,"sourceSetSignature");function sourceScopeDirs(t,c,p,l,a){const v=s(f=>{const k=path.relative(t,f).split(path.sep).filter(Boolean);return k.length===0||k[0].startsWith("..")?"":k[0]},"bucketOf")(c);if(!v)return[];const _=new Set(p.filter(Boolean)),S=new Set([v]);for(const f of l)f.startsWith(".")||SRC_EXCLUDE.has(f)||a?.has(f)||f.endsWith("-devserver")||_.has(f)||S.add(f);return[...S].sort().map(f=>path.join(t,f))}j(sourceScopeDirs,"sourceScopeDirs"),s(sourceScopeDirs,"sourceScopeDirs");function podRecycleReason(t,c,p,l){return!t.proc||t.state!=="up"||t.spawnAt==null||t.inflight>0||c-t.lastUsed<l.settleMs||t.lastStaleRecycleAt!=null&&c-t.lastStaleRecycleAt<l.cooldownMs?null:p>0&&p-t.spawnAt>l.graceMs?"stale-source-set":l.maxAgeMs>0&&c-t.spawnAt>l.maxAgeMs?"max-age":null}j(podRecycleReason,"podRecycleReason"),s(podRecycleReason,"podRecycleReason");function lensFrontendStale(t,c,p,l,a){return!(l-c<a.cooldownMs||p<=0||p-t<=a.graceMs||l-p<a.settleMs)}j(lensFrontendStale,"lensFrontendStale"),s(lensFrontendStale,"lensFrontendStale");function lensChildWedged(t,c,p,l){const a=t.hasListened?l.graceMs:Math.max(l.graceMs,l.neverListenedGraceMs);return p-t.spawnedAt<a||p-c<l.cooldownMs||t.probeFails<Math.max(2,l.failureThreshold)?null:"wedged-no-listener"}j(lensChildWedged,"lensChildWedged"),s(lensChildWedged,"lensChildWedged");function isPortConflictExit(t,c){return t.signal||t.code===null||t.code===0||t.ranMs>c.fastFailMs?!1:t.portHeld}j(isPortConflictExit,"isPortConflictExit"),s(isPortConflictExit,"isPortConflictExit");function createServiceLayer(t,c){const{emit:p}=c,l=s(e=>e.wsKey===t.wsKey?void 0:path.join(e.root,".lensmcp","events.jsonl"),"serviceBus"),a=s((e,o,r,n,i)=>p(o,r,n,{wsKey:e.wsKey,...i},void 0,l(e)),"emitSvc"),v=s(e=>(e.scannedAt=0,(0,discovery_1.pickSock)(e,t.scanTtlMs)?e.socks.length:0),"scanNow"),_=s((e,o)=>{let r;try{r=(0,node_child_process_1.execFileSync)("ps",["ax","-ww","-o","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3})}catch{return 0}const n=e.proc?.pid;let i=0;for(const d of r.split(`
2
2
  `)){const w=d.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);if(!w)continue;const g=Number(w[1]),u=Number(w[2]),h=w[3];if(h.includes(`run ${o}`)&&/\bnx(\.js)?\b/.test(h)&&!(h.includes("/node_modules/nx/")&&!h.includes(e.root))&&!(g===process.pid||g===n)&&!(n!==void 0&&u===n)){try{process.kill(-u,"SIGKILL")}catch{try{process.kill(g,"SIGKILL")}catch{}}i+=1}}return i>0&&(console.log(`[gateway] ${o}: reaped ${i} stale runner tree(s) \u2014 a foreign generation held the nx task lock`),a(e,"warning",`stale-runner reap: ${e.project} (${i} foreign tree(s))`,`cluster-service:${e.project}`,{kind:"stale-runner-reap",project:e.project,reaped:i})),i},"reapForeignRunners"),S=s(e=>{if(t.stopped())return;const o=`${e.project}:${e.decl.serveTarget??"serve-hmr"}`,r=Math.max(1,e.decl.minPods??1);_(e,o),e.lockBlockedAt=void 0,console.log(`[gateway] starting ${o} (pods=${r})\u2026`),a(e,"info",`service starting: ${e.project} (${r} pod${r>1?"s":""})`,`cluster-service:${e.project}`,{kind:"service-starting",project:e.project,pods:r}),e.state="starting",e.spawnAt=Date.now();const n=path.join(e.root,"node_modules","nx","dist","bin","nx.js"),[i,d]=fs.existsSync(n)?[process.execPath,[n,"run",o,`--childCount=${r}`]]:["yarn",["nx","run",o,`--childCount=${r}`]],w=(0,node_child_process_1.spawn)(i,d,{cwd:e.root,detached:!0,env:{...(0,pod_env_1.podBaseEnv)(process.env,t.root,e.root),NX_DAEMON:"false",NX_ISOLATE_PLUGINS:"false",...e.wsKey?{LENSMCP_WS_KEY:e.wsKey}:{},LENSMCP_EVENT_FILE:e.wsKey===t.wsKey?t.eventFile:path.join(e.root,".lensmcp","events.jsonl"),LENSMCP_GATEWAY_PID:String(process.pid),...eastWestEnv(t,e.project)},stdio:["ignore","pipe","pipe"]});e.lastErrors=[],e.buildFailed=!1;const g=s(u=>{const h=u.toString();for(const E of h.split(`
3
- `)){if(/ERROR in |error TS\d+|Found \d+ error|Unhandled 'error' event|EADDRINUSE/.test(E)){const $=E.replace(/\x1b\[[0-9;]*m/g,"").trim();e.lastErrors.push($),e.lastErrors.length>12&&e.lastErrors.shift(),e.buildFailed=!0;const x=Date.now();($!==e.lastErrorEmitSig||x-(e.lastErrorEmitAt??0)>=types_1.SERVICE_ERROR_THROTTLE_MS)&&(e.lastErrorEmitSig=$,e.lastErrorEmitAt=x,a(e,"error",`service error: ${e.project}`,`cluster-service:${e.project}`,{kind:"service-error",project:e.project,line:$}))}/No typescript errors found|webpack compiled successfully|Child#\d+ ready/.test(E)&&(e.buildFailed=!1),!e.lockBlockedAt&&/Waiting for .+ in another nx process/.test(E)&&(e.lockBlockedAt=Date.now(),console.log(`[gateway] ${o}: spawn blocked by a stale nx runner \u2014 breaking the lock + respawning`),a(e,"warning",`nx task lock: ${e.project} blocked by a stale runner \u2014 auto-breaking`,`cluster-service:${e.project}`,{kind:"nx-lock-break",project:e.project}),setTimeout(()=>{t.stopped()||!e.lockBlockedAt||(y(e,"parked on a foreign nx task lock \u2014 recycling",{announce:!1}),_(e,o),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&S(e)},1500).unref?.())},250).unref?.())}},"sniff");w.stdout?.on("data",u=>{t.stopped()||(g(u),process.stdout.write(`[${e.project}] ${u}`))}),w.stderr?.on("data",u=>{t.stopped()||(g(u),process.stderr.write(`[${e.project}] ${u}`))}),w.on("exit",u=>{if(e.proc=void 0,e.state="down",t.stopped()||(console.log(`[gateway] ${o} exited (${u})`),a(e,u===0?"info":"warning",`service down: ${e.project} (exit ${u})`,`cluster-service:${e.project}`,{kind:"service-down",project:e.project,code:u}),u===0||e.buildFailed))return;const h=Date.now();if(e.restarts=(e.restarts??[]).filter($=>h-$<types_1.SERVICE_RESTART_WINDOW_MS),e.restarts.length>=types_1.SERVICE_MAX_RESTARTS){a(e,"error",`service crash-looping: ${e.project} \u2014 ${types_1.SERVICE_MAX_RESTARTS}+ restarts/min, not auto-respawning (the next request retries)`,`cluster-service:${e.project}`,{kind:"service-crash-loop",project:e.project});return}e.restarts.push(h);const E=Math.min(8e3,500*2**(e.restarts.length-1));setTimeout(()=>{t.stopped()||e.proc||e.state!=="down"||(console.log(`[gateway] self-heal: respawning ${e.project} (restart ${e.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`),a(e,"info",`self-heal: respawning ${e.project}`,`cluster-service:${e.project}`,{kind:"service-respawn",project:e.project,attempt:e.restarts.length}),S(e))},E)}),e.proc=w},"spawnService"),f=s((e,o)=>{if(e.state==="up")return!1;e.state="up";const r=e.pool.socks.length;return a(e,"info",`service up: ${e.project} (${r} pods, ${o})`,`cluster-service:${e.project}`,{kind:"service-up",project:e.project,pods:r,adopted:o==="adopted"}),!0},"markUp"),k=s(async e=>{if(v(e.pool)>0)return f(e,"adopted"),!0;if(t.stopped())return!1;e.state!=="starting"&&!e.proc&&S(e);const o=Date.now()+t.startupTimeoutMs,r=Date.now()+12e3;for(;!t.stopped()&&Date.now()<o;){if(await sleep(Math.min(700,t.startupTimeoutMs/4)),e.buildFailed&&Date.now()>r)return!1;if(v(e.pool)>0){e.state="up",e.buildFailed=!1;const n=e.spawnAt?Date.now()-e.spawnAt:void 0;return console.log(`[gateway] ${e.project} up (${e.pool.socks.length} pods${n?`, cold start ${n}ms`:""})`),a(e,"info",`cold start: ${e.project} (${e.pool.socks.length} pods, ${n??"?"}ms)`,`cluster-service:${e.project}`,{kind:"cold-start",project:e.project,pods:e.pool.socks.length,durationMs:n}),!0}if(e.state==="down")return!1}return!1},"ensureUp"),M=s(e=>{const o=e.decl.maxPods??e.decl.minPods??1,r=e.pool.socks.length,n=e.decl.adminUrl??(e.decl.port?`http://localhost:${e.decl.port}`:void 0);if(!n||r===0||r>=o||e.inflight<=r||Date.now()-e.lastScaleAt<5e3)return;e.lastScaleAt=Date.now();const i=(n.startsWith("https")?require("node:https"):http).request(`${n}/webpack/scale`,{method:"POST",rejectUnauthorized:!1},d=>d.resume());i.on("error",()=>{}),i.end(JSON.stringify({pods:r+1})),console.log(`[gateway] autoscale ${e.project}: ${r} \u2192 ${r+1} pods (inflight ${e.inflight})`),a(e,"info",`autoscale: ${e.project} ${r} \u2192 ${r+1} pods`,`cluster-service:${e.project}`,{kind:"autoscale",project:e.project,from:r,to:r+1,inflight:e.inflight})},"scaleUp"),y=s((e,o,r)=>{if(!e.proc)return;console.log(`[gateway] ${e.project} ${o} \u2014 scaling to zero.`),r?.announce!==!1&&a(e,"info",`idle kill: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"idle-kill",project:e.project,why:o});const n=e.proc;try{n.stdout?.removeAllListeners("data"),n.stderr?.removeAllListeners("data"),n.removeAllListeners("exit")}catch{}try{process.kill(-n.pid,"SIGTERM")}catch{try{n.kill("SIGTERM")}catch{}}e.proc=void 0,e.state="down",setTimeout(()=>{try{for(const i of fs.readdirSync(e.pool.dir)){if(!i.endsWith(".sock"))continue;const d=path.join(e.pool.dir,i);try{const w=Number(fs.readFileSync(`${d}.owner`,"utf8").trim());if(Number.isFinite(w)&&w>0)try{process.kill(w,0);continue}catch{}}catch{}try{fs.unlinkSync(d)}catch{}try{fs.unlinkSync(`${d}.owner`)}catch{}}}catch{}e.pool.socks=[],e.pool.scannedAt=0},1200).unref?.()},"killSpawned"),m=s(e=>{const o=Date.now();if(e.restarts=(e.restarts??[]).filter(r=>o-r<types_1.SERVICE_RESTART_WINDOW_MS),e.restarts.length>=types_1.SERVICE_MAX_RESTARTS){a(e,"error",`service crash-looping: ${e.project} \u2014 wedged ${types_1.SERVICE_MAX_RESTARTS}+\xD7/min, not auto-recycling (the next request retries)`,`cluster-service:${e.project}`,{kind:"service-crash-loop",project:e.project}),y(e,"wedged (crash-loop cap) \u2014 leaving down for the next request",{announce:!1});return}e.restarts.push(o),console.log(`[gateway] self-heal: recycling WEDGED ${e.project} (live parent, 0 live pods; ${e.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`),a(e,"warning",`self-heal: recycling wedged ${e.project} (0 live pods)`,`cluster-service:${e.project}`,{kind:"service-recycle",project:e.project,attempt:e.restarts.length}),y(e,"wedged: 0 live pods \u2014 recycling",{announce:!1}),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&S(e)},1500).unref?.()},"recycleWedged"),A={maxAgeMs:types_1.POD_MAX_AGE_MS,graceMs:types_1.POD_STALE_GRACE_MS,settleMs:types_1.POD_RECYCLE_SETTLE_MS,cooldownMs:types_1.POD_STALE_RECYCLE_COOLDOWN_MS};let R;const D=new Map;let C=!1,T=0;const b=s(()=>{const e=t.projectRoots??{},o=new Set(t.services.map(n=>n.project));for(const n of t.routes)n.lens&&o.add(n.lens.project);const r=new Set;for(const n of o){const i=e[n];if(!i)continue;const d=path.relative(t.root,i).split(path.sep).filter(Boolean)[0];d&&!d.startsWith("..")&&r.add(d)}return[...r]},"managedBuckets"),L=s((e,o,r,n)=>{const i=t.projectRoots?.[e];if(!i)return;const d=sourceScopeDirs(t.root,i,r,o,n);return d.length>0?d:void 0},"scopeFor"),P=s(()=>{C||Date.now()-T<types_1.POD_STALE_SCAN_MS||(C=!0,T=Date.now(),(async()=>{const e=(0,scope_1.readSourceSetExclude)(t.root),o=await sourceSetSignature(t.root,void 0,e);if(R!==void 0&&o!==R&&(t.sourceSetChangedAt=Date.now(),l("info","source file set changed \u2014 stale pods recycle when idle","cluster-gateway",{kind:"source-set-changed"})),R=o,!t.projectRoots)return;let r;try{r=(await fs.promises.readdir(t.root,{withFileTypes:!0})).filter(g=>g.isDirectory()).map(g=>g.name)}catch{return}const n=b(),i=new Set(t.services.map(g=>g.project));for(const g of t.routes)g.lens&&i.add(g.lens.project);const d=new Map;for(const g of i){const u=L(g,r,n,e);if(!u)continue;const h=u.join("|"),E=d.get(h)??{dirs:u,projects:[]};E.projects.push(g),d.set(h,E)}const w=Date.now();for(const[g,{dirs:u,projects:h}]of d){const E=await sourceSetSignature(t.root,u,e),$=D.get(g);if(D.set(g,E),!($===void 0||E===$)){t.sourceSetChangedFor??=new Map;for(const x of h)t.sourceSetChangedFor.set(x,w);l("info",`source file set changed in ${h.join(", ")}'s scope`,"cluster-gateway",{kind:"source-set-changed",scoped:!0,projects:h})}}})().catch(()=>{}).finally(()=>{C=!1}))},"maybeScanSourceSet"),F=s((e,o)=>{e.lastStaleRecycleAt=Date.now(),console.log(`[gateway] zombie recycle: ${e.project} (${o}) \u2014 stale fs view; fresh pod incoming`),a(e,"warning",`zombie recycle: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:o});const r=!!e.decl.eager;y(e,`${o} \u2014 recycling stale pod`,{announce:!1}),r&&setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&S(e)},1500).unref?.()},"recyclePod");return{scanNow:v,spawnService:S,ensureUp:k,scaleUp:M,killSpawned:y,markUp:f,startSweeper:s(()=>{const e=setInterval(()=>{if(!t.stopped()){types_1.POD_RECYCLE_ENABLED&&P();for(const o of t.services){if(t.stopped()||!o.proc){o.wedgedSince=void 0;continue}const r=o.decl.idleKillSec;if(r&&o.inflight===0&&Date.now()-o.lastUsed>r*1e3){y(o,`idle ${r}s`),o.wedgedSince=void 0;continue}const n=o.state==="starting"&&o.spawnAt!==void 0&&Date.now()-o.spawnAt<t.startupTimeoutMs,i=v(o.pool)>0;if(i||n){if(o.wedgedSince=void 0,i&&f(o,"observed"),i&&!n&&types_1.POD_RECYCLE_ENABLED){const d=t.sourceSetChangedFor?.get(o.project)??t.sourceSetChangedAt??0,w=podRecycleReason(o,Date.now(),d,A);w&&F(o,w)}continue}o.wedgedSince??=Date.now(),Date.now()-o.wedgedSince>types_1.WEDGE_RECYCLE_GRACE_MS&&(o.wedgedSince=void 0,m(o))}}},t.sweepMs);return e.unref(),e},"startSweeper")}}j(createServiceLayer,"createServiceLayer"),s(createServiceLayer,"createServiceLayer");
3
+ `)){if(/ERROR in |error TS\d+|Found \d+ error|Unhandled 'error' event|EADDRINUSE/.test(E)){const $=E.replace(/\x1b\[[0-9;]*m/g,"").trim();e.lastErrors.push($),e.lastErrors.length>12&&e.lastErrors.shift(),e.buildFailed=!0;const x=Date.now();($!==e.lastErrorEmitSig||x-(e.lastErrorEmitAt??0)>=types_1.SERVICE_ERROR_THROTTLE_MS)&&(e.lastErrorEmitSig=$,e.lastErrorEmitAt=x,a(e,"error",`service error: ${e.project}`,`cluster-service:${e.project}`,{kind:"service-error",project:e.project,line:$}))}/No typescript errors found|webpack compiled successfully|Child#\d+ ready/.test(E)&&(e.buildFailed=!1),!e.lockBlockedAt&&/Waiting for .+ in another nx process/.test(E)&&(e.lockBlockedAt=Date.now(),console.log(`[gateway] ${o}: spawn blocked by a stale nx runner \u2014 breaking the lock + respawning`),a(e,"warning",`nx task lock: ${e.project} blocked by a stale runner \u2014 auto-breaking`,`cluster-service:${e.project}`,{kind:"nx-lock-break",project:e.project}),setTimeout(()=>{t.stopped()||!e.lockBlockedAt||(y(e,"parked on a foreign nx task lock \u2014 recycling",{announce:!1}),_(e,o),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&S(e)},1500).unref?.())},250).unref?.())}},"sniff");w.stdout?.on("data",u=>{t.stopped()||(g(u),process.stdout.write(`[${e.project}] ${u}`))}),w.stderr?.on("data",u=>{t.stopped()||(g(u),process.stderr.write(`[${e.project}] ${u}`))}),w.on("exit",u=>{if(e.proc=void 0,e.state="down",t.stopped()||(console.log(`[gateway] ${o} exited (${u})`),a(e,u===0?"info":"warning",`service down: ${e.project} (exit ${u})`,`cluster-service:${e.project}`,{kind:"service-down",project:e.project,code:u}),u===0||e.buildFailed))return;const h=Date.now();if(e.restarts=(e.restarts??[]).filter($=>h-$<types_1.SERVICE_RESTART_WINDOW_MS),e.restarts.length>=types_1.SERVICE_MAX_RESTARTS){a(e,"error",`service crash-looping: ${e.project} \u2014 ${types_1.SERVICE_MAX_RESTARTS}+ restarts/min, not auto-respawning (the next request retries)`,`cluster-service:${e.project}`,{kind:"service-crash-loop",project:e.project});return}e.restarts.push(h);const E=Math.min(8e3,500*2**(e.restarts.length-1));setTimeout(()=>{t.stopped()||e.proc||e.state!=="down"||(console.log(`[gateway] self-heal: respawning ${e.project} (restart ${e.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`),a(e,"info",`self-heal: respawning ${e.project}`,`cluster-service:${e.project}`,{kind:"service-respawn",project:e.project,attempt:e.restarts.length}),S(e))},E)}),e.proc=w},"spawnService"),f=s((e,o)=>{if(e.state==="up")return!1;e.state="up";const r=e.pool.socks.length;return a(e,"info",`service up: ${e.project} (${r} pods, ${o})`,`cluster-service:${e.project}`,{kind:"service-up",project:e.project,pods:r,adopted:o==="adopted"}),!0},"markUp"),k=s(async e=>{if(v(e.pool)>0)return f(e,"adopted"),!0;if(t.stopped())return!1;e.state!=="starting"&&!e.proc&&S(e);const o=Date.now()+t.startupTimeoutMs,r=Date.now()+12e3;for(;!t.stopped()&&Date.now()<o;){if(await sleep(Math.min(700,t.startupTimeoutMs/4)),e.buildFailed&&Date.now()>r)return!1;if(v(e.pool)>0){e.state="up",e.buildFailed=!1;const n=e.spawnAt?Date.now()-e.spawnAt:void 0;return console.log(`[gateway] ${e.project} up (${e.pool.socks.length} pods${n?`, cold start ${n}ms`:""})`),a(e,"info",`cold start: ${e.project} (${e.pool.socks.length} pods, ${n??"?"}ms)`,`cluster-service:${e.project}`,{kind:"cold-start",project:e.project,pods:e.pool.socks.length,durationMs:n}),!0}if(e.state==="down")return!1}return!1},"ensureUp"),M=s(e=>{const o=e.decl.maxPods??e.decl.minPods??1,r=e.pool.socks.length,n=e.decl.adminUrl??(e.decl.port?`http://localhost:${e.decl.port}`:void 0);if(!n||r===0||r>=o||e.inflight<=r||Date.now()-e.lastScaleAt<5e3)return;e.lastScaleAt=Date.now();const i=(n.startsWith("https")?require("node:https"):http).request(`${n}/webpack/scale`,{method:"POST",rejectUnauthorized:!1},d=>d.resume());i.on("error",()=>{}),i.end(JSON.stringify({pods:r+1})),console.log(`[gateway] autoscale ${e.project}: ${r} \u2192 ${r+1} pods (inflight ${e.inflight})`),a(e,"info",`autoscale: ${e.project} ${r} \u2192 ${r+1} pods`,`cluster-service:${e.project}`,{kind:"autoscale",project:e.project,from:r,to:r+1,inflight:e.inflight})},"scaleUp"),y=s((e,o,r)=>{if(!e.proc)return;console.log(`[gateway] ${e.project} ${o} \u2014 scaling to zero.`),r?.announce!==!1&&a(e,"info",`idle kill: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"idle-kill",project:e.project,why:o});const n=e.proc;try{n.stdout?.removeAllListeners("data"),n.stderr?.removeAllListeners("data"),n.removeAllListeners("exit")}catch{}try{process.kill(-n.pid,"SIGTERM")}catch{try{n.kill("SIGTERM")}catch{}}e.proc=void 0,e.state="down",setTimeout(()=>{try{for(const i of fs.readdirSync(e.pool.dir)){if(!i.endsWith(".sock"))continue;const d=path.join(e.pool.dir,i);try{const w=Number(fs.readFileSync(`${d}.owner`,"utf8").trim());if(Number.isFinite(w)&&w>0)try{process.kill(w,0);continue}catch{}}catch{}try{fs.unlinkSync(d)}catch{}try{fs.unlinkSync(`${d}.owner`)}catch{}}}catch{}e.pool.socks=[],e.pool.scannedAt=0},1200).unref?.()},"killSpawned"),m=s(e=>{const o=Date.now();if(e.restarts=(e.restarts??[]).filter(r=>o-r<types_1.SERVICE_RESTART_WINDOW_MS),e.restarts.length>=types_1.SERVICE_MAX_RESTARTS){a(e,"error",`service crash-looping: ${e.project} \u2014 wedged ${types_1.SERVICE_MAX_RESTARTS}+\xD7/min, not auto-recycling (the next request retries)`,`cluster-service:${e.project}`,{kind:"service-crash-loop",project:e.project}),y(e,"wedged (crash-loop cap) \u2014 leaving down for the next request",{announce:!1});return}e.restarts.push(o),console.log(`[gateway] self-heal: recycling WEDGED ${e.project} (live parent, 0 live pods; ${e.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`),a(e,"warning",`self-heal: recycling wedged ${e.project} (0 live pods)`,`cluster-service:${e.project}`,{kind:"service-recycle",project:e.project,attempt:e.restarts.length}),y(e,"wedged: 0 live pods \u2014 recycling",{announce:!1}),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&S(e)},1500).unref?.()},"recycleWedged"),A={maxAgeMs:types_1.POD_MAX_AGE_MS,graceMs:types_1.POD_STALE_GRACE_MS,settleMs:types_1.POD_RECYCLE_SETTLE_MS,cooldownMs:types_1.POD_STALE_RECYCLE_COOLDOWN_MS};let R;const D=new Map;let C=!1,T=0;const b=s(()=>{const e=t.projectRoots??{},o=new Set(t.services.map(n=>n.project));for(const n of t.routes)n.lens&&o.add(n.lens.project);const r=new Set;for(const n of o){const i=e[n];if(!i)continue;const d=path.relative(t.root,i).split(path.sep).filter(Boolean)[0];d&&!d.startsWith("..")&&r.add(d)}return[...r]},"managedBuckets"),L=s((e,o,r,n)=>{const i=t.projectRoots?.[e];if(!i)return;const d=sourceScopeDirs(t.root,i,r,o,n);return d.length>0?d:void 0},"scopeFor"),P=s(()=>{C||Date.now()-T<types_1.POD_STALE_SCAN_MS||(C=!0,T=Date.now(),(async()=>{const e=(0,scope_1.readSourceSetExclude)(t.root),o=await sourceSetSignature(t.root,void 0,e);if(R!==void 0&&o!==R&&(t.sourceSetChangedAt=Date.now(),p("info","source file set changed \u2014 stale pods recycle when idle","cluster-gateway",{kind:"source-set-changed"})),R=o,!t.projectRoots)return;let r;try{r=(await fs.promises.readdir(t.root,{withFileTypes:!0})).filter(g=>g.isDirectory()).map(g=>g.name)}catch{return}const n=b(),i=new Set(t.services.map(g=>g.project));for(const g of t.routes)g.lens&&i.add(g.lens.project);const d=new Map;for(const g of i){const u=L(g,r,n,e);if(!u)continue;const h=u.join("|"),E=d.get(h)??{dirs:u,projects:[]};E.projects.push(g),d.set(h,E)}const w=Date.now();for(const[g,{dirs:u,projects:h}]of d){const E=await sourceSetSignature(t.root,u,e),$=D.get(g);if(D.set(g,E),!($===void 0||E===$)){t.sourceSetChangedFor??=new Map;for(const x of h)t.sourceSetChangedFor.set(x,w);p("info",`source file set changed in ${h.join(", ")}'s scope`,"cluster-gateway",{kind:"source-set-changed",scoped:!0,projects:h})}}})().catch(()=>{}).finally(()=>{C=!1}))},"maybeScanSourceSet"),F=s((e,o)=>{e.lastStaleRecycleAt=Date.now(),console.log(`[gateway] zombie recycle: ${e.project} (${o}) \u2014 stale fs view; fresh pod incoming`),a(e,"warning",`zombie recycle: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:o});const r=!!e.decl.eager;y(e,`${o} \u2014 recycling stale pod`,{announce:!1}),r&&setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&S(e)},1500).unref?.()},"recyclePod");return{scanNow:v,spawnService:S,ensureUp:k,scaleUp:M,killSpawned:y,markUp:f,startSweeper:s(()=>{const e=setInterval(()=>{if(!t.stopped()){types_1.POD_RECYCLE_ENABLED&&P();for(const o of t.services){if(t.stopped()||!o.proc){o.wedgedSince=void 0;continue}const r=o.decl.idleKillSec;if(r&&o.inflight===0&&Date.now()-o.lastUsed>r*1e3){y(o,`idle ${r}s`),o.wedgedSince=void 0;continue}const n=o.state==="starting"&&o.spawnAt!==void 0&&Date.now()-o.spawnAt<t.startupTimeoutMs,i=v(o.pool)>0;if(i||n){if(o.wedgedSince=void 0,i&&f(o,"observed"),i&&!n&&types_1.POD_RECYCLE_ENABLED){const d=t.sourceSetChangedFor?.get(o.project)??t.sourceSetChangedAt??0,w=podRecycleReason(o,Date.now(),d,A);w&&F(o,w)}continue}o.wedgedSince??=Date.now(),Date.now()-o.wedgedSince>types_1.WEDGE_RECYCLE_GRACE_MS&&(o.wedgedSince=void 0,m(o))}}},t.sweepMs);return e.unref(),e},"startSweeper")}}j(createServiceLayer,"createServiceLayer"),s(createServiceLayer,"createServiceLayer");
@@ -258,6 +258,19 @@ export declare const LENS_PROBE_TIMEOUT_MS: number;
258
258
  * immediately on respawn backs off instead of spinning, short enough that a wedge is a blip, not an outage.
259
259
  */
260
260
  export declare const LENS_PROBE_COOLDOWN_MS: number;
261
+ /**
262
+ * The kill allowance for a child that has NEVER LISTENED this generation — the boot-vs-wedge split of
263
+ * {@link LENS_PROBE_GRACE_MS}. "Alive but not listening" is also the signature of a SLOW COLD BOOT (a
264
+ * big-monorepo vite loads the nx graph + tsconfig-paths, often with several vites recycling at once
265
+ * after a source-set change), and SIGKILLing a booting vite at the short grace respawns it into the
266
+ * SAME slow boot — the kill→boot→kill loop that turned a routine stale-source-set recycle into a
267
+ * minutes-long 502/RST window (foodguard 2026-08-06: every "wedge recycle: … ALIVE but NOT LISTENING"
268
+ * chased a zombie recycle's own respawn; the browser saw the in-flight streams die as a burst of
269
+ * ERR_HTTP2_PROTOCOL_ERROR). A child that DID listen and lost its socket keeps the short grace — that
270
+ * is the precise wedge the probe exists for. This long allowance is only the backstop for a child that
271
+ * truly never binds. Override `LENSMCP_LENS_PROBE_BOOT_GRACE_MS`. Default 4min.
272
+ */
273
+ export declare const LENS_PROBE_BOOT_GRACE_MS: number;
261
274
  /**
262
275
  * RESPAWN-INTO-A-HELD-PORT (the EADDRINUSE self-inflicted crash). The auto-heal respawned after a FIXED
263
276
  * backoff while the previous child still held the pinned port — vite's `--strictPort` then exits code=1 on
@@ -275,10 +288,26 @@ export declare const PORT_FREE_POLL_MS: number;
275
288
  /** A non-zero exit sooner than this is a START failure (it never bound); later means the child RAN and so
276
289
  * had the port, which cannot be a bind race. Override `LENSMCP_CHILD_START_FAIL_MS`. Default 10s. */
277
290
  export declare const CHILD_START_FAIL_MS: number;
278
- /** Ultimate backstop: after this many CONSECUTIVE bind-race exits for one label, stop forgiving them and
279
- * fall back to the normal exponential backoff (so a permanently-squatted port can't 10s-loop forever).
280
- * Override `LENSMCP_MAX_PORT_CONFLICT_RETRIES`. Default 5. */
291
+ /** Ultimate backstop: after this many CONSECUTIVE bind-race exits for one label, stop forgiving them
292
+ * a FIXED-port child (the MCP singleton, whose port is a shim contract and can never step up) then
293
+ * PARKS instead of falling to the 30s backoff loop: a permanently-held port made that loop respawn
294
+ * FOREVER (foodguard 2026-08-06: `lens child lens-mcp … auto-healing in 30s (attempt 10970)` — a
295
+ * shared daemon legitimately owned :4543 and the gateway EADDRINUSE-crash-looped against it for
296
+ * days). Override `LENSMCP_MAX_PORT_CONFLICT_RETRIES`. Default 5. */
281
297
  export declare const MAX_PORT_CONFLICT_RETRIES: number;
298
+ /** While a fixed-port child is PARKED (its port persistently held by another server — e.g. a shared
299
+ * lensmcp daemon already serving this workspace's MCP), how often to re-probe the port; the child
300
+ * respawns only when it actually frees. Override `LENSMCP_PARKED_PORT_REPROBE_MS`. Default 5min. */
301
+ export declare const LENS_PARKED_PORT_REPROBE_MS: number;
302
+ /** How long a zombie-recycled lens child gets to exit GRACEFULLY before the recycle escalates to
303
+ * SIGKILL. Vite often IGNORES SIGTERM (`reapAll` learned this first): it closes its listener yet
304
+ * keeps the process alive, so a recycle used to park in limbo until the WEDGE probe noticed the
305
+ * vanished listener — ~probe-grace + 2 ticks (≈75s) of dead air added to EVERY stale-source-set
306
+ * recycle (foodguard 2026-08-06: each "zombie recycle: lens app …" was followed a minute later by
307
+ * "wedge recycle: … its listener VANISHED"). Escalating locally cuts that to seconds; a vite that
308
+ * does honor SIGTERM exits first and the escalation no-ops. Override
309
+ * `LENSMCP_RECYCLE_KILL_GRACE_MS`. Default 5s. */
310
+ export declare const LENS_RECYCLE_KILL_GRACE_MS: number;
282
311
  /** Inbound keep-alive idle window for the front door. MUST exceed every client's pooled-socket idle timeout
283
312
  * so the SERVER never closes a keep-alive socket a client is about to reuse — otherwise both sides race the
284
313
  * same deadline and the truncated request surfaces as a spurious front-door `400` (see
@@ -1 +1 @@
1
- "use strict";var R=Object.defineProperty;var e=(_,E)=>R(_,"name",{value:E,configurable:!0});var O=Object.defineProperty,L=e((_,E)=>O(_,"name",{value:E,configurable:!0}),"L");Object.defineProperty(exports,"__esModule",{value:!0}),exports.GATEWAY_KEEPALIVE_TIMEOUT_MS=exports.MAX_PORT_CONFLICT_RETRIES=exports.CHILD_START_FAIL_MS=exports.PORT_FREE_POLL_MS=exports.PORT_FREE_WAIT_MS=exports.LENS_PROBE_COOLDOWN_MS=exports.LENS_PROBE_TIMEOUT_MS=exports.LENS_PROBE_FAILS=exports.LENS_PROBE_GRACE_MS=exports.LENS_PROBE_ENABLED=exports.POD_STALE_RECYCLE_COOLDOWN_MS=exports.POD_STALE_SCAN_MS=exports.POD_RECYCLE_SETTLE_MS=exports.POD_STALE_GRACE_MS=exports.POD_MAX_AGE_MS=exports.POD_RECYCLE_ENABLED=exports.WEDGE_RECYCLE_GRACE_MS=exports.POD_RESPONSE_TIMEOUT_MS=exports.UPSTREAM_HEAL_STEP_MS=exports.UPSTREAM_HEAL_WINDOW_MS=exports.SERVICE_RESTART_WINDOW_MS=exports.SERVICE_MAX_RESTARTS=exports.SERVICE_ERROR_THROTTLE_MS=void 0;function envInt(_,E,M=!1){const S=Number(process.env[_]);return!Number.isFinite(S)||S<0?E:S===0?M?0:E:S}e(envInt,"envInt"),L(envInt,"envInt"),exports.SERVICE_ERROR_THROTTLE_MS=5e3,exports.SERVICE_MAX_RESTARTS=5,exports.SERVICE_RESTART_WINDOW_MS=6e4,exports.UPSTREAM_HEAL_WINDOW_MS=8e3,exports.UPSTREAM_HEAL_STEP_MS=300,exports.POD_RESPONSE_TIMEOUT_MS=(()=>{const _=Number(process.env.LENSMCP_POD_RESPONSE_TIMEOUT_MS);return Number.isFinite(_)&&_>0?_:3e4})(),exports.WEDGE_RECYCLE_GRACE_MS=(()=>{const _=Number(process.env.LENSMCP_WEDGE_RECYCLE_GRACE_MS);return Number.isFinite(_)&&_>0?_:15e3})(),exports.POD_RECYCLE_ENABLED=process.env.LENSMCP_POD_RECYCLE!=="0",exports.POD_MAX_AGE_MS=envInt("LENSMCP_POD_MAX_AGE_MS",240*6e4,!0),exports.POD_STALE_GRACE_MS=envInt("LENSMCP_POD_STALE_GRACE_MS",1e4),exports.POD_RECYCLE_SETTLE_MS=envInt("LENSMCP_POD_RECYCLE_SETTLE_MS",8e3),exports.POD_STALE_SCAN_MS=envInt("LENSMCP_POD_STALE_SCAN_MS",3e4),exports.POD_STALE_RECYCLE_COOLDOWN_MS=envInt("LENSMCP_POD_STALE_RECYCLE_COOLDOWN_MS",6e4),exports.LENS_PROBE_ENABLED=process.env.LENSMCP_LENS_PROBE!=="0",exports.LENS_PROBE_GRACE_MS=envInt("LENSMCP_LENS_PROBE_GRACE_MS",45e3),exports.LENS_PROBE_FAILS=envInt("LENSMCP_LENS_PROBE_FAILS",2),exports.LENS_PROBE_TIMEOUT_MS=envInt("LENSMCP_LENS_PROBE_TIMEOUT_MS",1e3),exports.LENS_PROBE_COOLDOWN_MS=envInt("LENSMCP_LENS_PROBE_COOLDOWN_MS",3e4),exports.PORT_FREE_WAIT_MS=envInt("LENSMCP_PORT_FREE_WAIT_MS",1e4),exports.PORT_FREE_POLL_MS=envInt("LENSMCP_PORT_FREE_POLL_MS",250),exports.CHILD_START_FAIL_MS=envInt("LENSMCP_CHILD_START_FAIL_MS",1e4),exports.MAX_PORT_CONFLICT_RETRIES=envInt("LENSMCP_MAX_PORT_CONFLICT_RETRIES",5),exports.GATEWAY_KEEPALIVE_TIMEOUT_MS=envInt("LENSMCP_KEEPALIVE_TIMEOUT_MS",76e3);
1
+ "use strict";var M=Object.defineProperty;var R=(_,E)=>M(_,"name",{value:E,configurable:!0});var O=Object.defineProperty,L=R((_,E)=>O(_,"name",{value:E,configurable:!0}),"L");Object.defineProperty(exports,"__esModule",{value:!0}),exports.GATEWAY_KEEPALIVE_TIMEOUT_MS=exports.LENS_RECYCLE_KILL_GRACE_MS=exports.LENS_PARKED_PORT_REPROBE_MS=exports.MAX_PORT_CONFLICT_RETRIES=exports.CHILD_START_FAIL_MS=exports.PORT_FREE_POLL_MS=exports.PORT_FREE_WAIT_MS=exports.LENS_PROBE_BOOT_GRACE_MS=exports.LENS_PROBE_COOLDOWN_MS=exports.LENS_PROBE_TIMEOUT_MS=exports.LENS_PROBE_FAILS=exports.LENS_PROBE_GRACE_MS=exports.LENS_PROBE_ENABLED=exports.POD_STALE_RECYCLE_COOLDOWN_MS=exports.POD_STALE_SCAN_MS=exports.POD_RECYCLE_SETTLE_MS=exports.POD_STALE_GRACE_MS=exports.POD_MAX_AGE_MS=exports.POD_RECYCLE_ENABLED=exports.WEDGE_RECYCLE_GRACE_MS=exports.POD_RESPONSE_TIMEOUT_MS=exports.UPSTREAM_HEAL_STEP_MS=exports.UPSTREAM_HEAL_WINDOW_MS=exports.SERVICE_RESTART_WINDOW_MS=exports.SERVICE_MAX_RESTARTS=exports.SERVICE_ERROR_THROTTLE_MS=void 0;function envInt(_,E,e=!1){const S=Number(process.env[_]);return!Number.isFinite(S)||S<0?E:S===0?e?0:E:S}R(envInt,"envInt"),L(envInt,"envInt"),exports.SERVICE_ERROR_THROTTLE_MS=5e3,exports.SERVICE_MAX_RESTARTS=5,exports.SERVICE_RESTART_WINDOW_MS=6e4,exports.UPSTREAM_HEAL_WINDOW_MS=8e3,exports.UPSTREAM_HEAL_STEP_MS=300,exports.POD_RESPONSE_TIMEOUT_MS=(()=>{const _=Number(process.env.LENSMCP_POD_RESPONSE_TIMEOUT_MS);return Number.isFinite(_)&&_>0?_:3e4})(),exports.WEDGE_RECYCLE_GRACE_MS=(()=>{const _=Number(process.env.LENSMCP_WEDGE_RECYCLE_GRACE_MS);return Number.isFinite(_)&&_>0?_:15e3})(),exports.POD_RECYCLE_ENABLED=process.env.LENSMCP_POD_RECYCLE!=="0",exports.POD_MAX_AGE_MS=envInt("LENSMCP_POD_MAX_AGE_MS",240*6e4,!0),exports.POD_STALE_GRACE_MS=envInt("LENSMCP_POD_STALE_GRACE_MS",1e4),exports.POD_RECYCLE_SETTLE_MS=envInt("LENSMCP_POD_RECYCLE_SETTLE_MS",8e3),exports.POD_STALE_SCAN_MS=envInt("LENSMCP_POD_STALE_SCAN_MS",3e4),exports.POD_STALE_RECYCLE_COOLDOWN_MS=envInt("LENSMCP_POD_STALE_RECYCLE_COOLDOWN_MS",6e4),exports.LENS_PROBE_ENABLED=process.env.LENSMCP_LENS_PROBE!=="0",exports.LENS_PROBE_GRACE_MS=envInt("LENSMCP_LENS_PROBE_GRACE_MS",45e3),exports.LENS_PROBE_FAILS=envInt("LENSMCP_LENS_PROBE_FAILS",2),exports.LENS_PROBE_TIMEOUT_MS=envInt("LENSMCP_LENS_PROBE_TIMEOUT_MS",1e3),exports.LENS_PROBE_COOLDOWN_MS=envInt("LENSMCP_LENS_PROBE_COOLDOWN_MS",3e4),exports.LENS_PROBE_BOOT_GRACE_MS=envInt("LENSMCP_LENS_PROBE_BOOT_GRACE_MS",24e4),exports.PORT_FREE_WAIT_MS=envInt("LENSMCP_PORT_FREE_WAIT_MS",1e4),exports.PORT_FREE_POLL_MS=envInt("LENSMCP_PORT_FREE_POLL_MS",250),exports.CHILD_START_FAIL_MS=envInt("LENSMCP_CHILD_START_FAIL_MS",1e4),exports.MAX_PORT_CONFLICT_RETRIES=envInt("LENSMCP_MAX_PORT_CONFLICT_RETRIES",5),exports.LENS_PARKED_PORT_REPROBE_MS=envInt("LENSMCP_PARKED_PORT_REPROBE_MS",3e5),exports.LENS_RECYCLE_KILL_GRACE_MS=envInt("LENSMCP_RECYCLE_KILL_GRACE_MS",5e3),exports.GATEWAY_KEEPALIVE_TIMEOUT_MS=envInt("LENSMCP_KEEPALIVE_TIMEOUT_MS",76e3);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/cluster",
3
- "version": "1.18.6",
3
+ "version": "1.18.8",
4
4
  "description": "Run your Nx workspace as a local production cluster: pods on unix sockets with true HMR, one https gateway with per-project domains, scale-from-zero, autoscale, idle-kill, local-CA TLS — observed by the LensMCP lens.",
5
5
  "main": "./index.js",
6
6
  "types": "./index.d.ts",
@@ -65,9 +65,9 @@
65
65
  }
66
66
  },
67
67
  "dependencies": {
68
- "@lensmcp/core": "1.18.6",
69
- "@lensmcp/node-instrumentation": "1.18.6",
70
- "@lensmcp/nx-plugin": "1.18.6",
68
+ "@lensmcp/core": "1.18.8",
69
+ "@lensmcp/node-instrumentation": "1.18.8",
70
+ "@lensmcp/nx-plugin": "1.18.8",
71
71
  "@typescript/native-preview": "^7.0.0-dev.20260707.2",
72
72
  "fork-ts-checker-webpack-plugin": "^9.0.0",
73
73
  "glob": "^11.0.0",