@lensmcp/cluster 1.23.8 → 1.23.9

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,3 +1,3 @@
1
- "use strict";var ge=Object.defineProperty;var v=(t,n)=>ge(t,"name",{value:n,configurable:!0});var fe=Object.defineProperty,i=v((t,n)=>fe(t,"name",{value:n,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isTestSourcePath=isTestSourcePath,exports.sourceSetSnapshot=sourceSetSnapshot,exports.sourceSetSignature=sourceSetSignature,exports.diffSourceSets=diffSourceSets,exports.sourceChangeWindowMs=sourceChangeWindowMs,exports.sourceScopeDirs=sourceScopeDirs,exports.podRecycleReason=podRecycleReason,exports.lensFrontendStale=lensFrontendStale,exports.lensWatcherVerdict=lensWatcherVerdict,exports.podWatcherVerdict=podWatcherVerdict,exports.lensChildWedged=lensChildWedged,exports.isPortConflictExit=isPortConflictExit,exports.createServiceLayer=createServiceLayer,exports.chooseStalePodKillTarget=chooseStalePodKillTarget;const tslib_1=require("tslib"),node_child_process_1=require("node:child_process"),node_crypto_1=require("node:crypto"),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"),pod_probe_1=require("./pod-probe"),service_keys_1=require("./service-keys"),workspace_log_1=require("./workspace-log"),pod_output_1=require("./pod-output"),types_1=require("./types"),exit_reason_1=require("../../../exit-reason"),process_name_1=require("../../../process-name");function eastWestEnv(t,n){const a={},l=t.serviceKeys[(0,service_keys_1.serviceKeyId)(n.wsKey,n.project)];if(l&&(a.LENSMCP_SERVICE_KEY=l),t.https){const f=require("../../../basic-ssl").caCertPath();fs.existsSync(f)&&(a.NODE_EXTRA_CA_CERTS=f)}return a}v(eastWestEnv,"eastWestEnv"),i(eastWestEnv,"eastWestEnv");const sleep=i(t=>new Promise(n=>setTimeout(n,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 n=2166136261;for(let a=0;a<t.length;a+=1)n^=t.charCodeAt(a),n=Math.imul(n,16777619);return n>>>0}v(fnv1a,"fnv1a"),i(fnv1a,"fnv1a");const execFileP=(0,node_util_1.promisify)(node_child_process_1.execFile);async function gitIgnoredPaths(t){try{const{stdout:n}=await execFileP("git",["ls-files","--others","--directory","--ignored","--exclude-standard","-z"],{cwd:t,maxBuffer:33554432,timeout:1e4,windowsHide:!0});return new Set(n.split("\0").filter(Boolean))}catch{return new Set}}v(gitIgnoredPaths,"gitIgnoredPaths"),i(gitIgnoredPaths,"gitIgnoredPaths");const TEST_FILE_RE=/\.(spec|test|e2e|e2e-spec|stories)\.[cm]?[jt]sx?$/,TEST_DIR_SEGMENTS=new Set(["__tests__","__mocks__","__snapshots__"]);function isTestSourcePath(t){const n=t.split("/"),a=n[n.length-1]??"";if(TEST_FILE_RE.test(a))return!0;for(let l=0;l<n.length-1;l+=1)if(TEST_DIR_SEGMENTS.has(n[l]))return!0;return!1}v(isTestSourcePath,"isTestSourcePath"),i(isTestSourcePath,"isTestSourcePath");async function sourceSetSnapshot(t,n,a,l){const f=l?.ignoreTests??!0;let S=0,y=0,_=!1;const g=new Set,L=await gitIgnoredPaths(t),h=i(R=>path.relative(t,R).split(path.sep).join("/"),"rel"),T=i(async(R,X=!0)=>{if(S>=SCAN_FILE_CAP){_=!0;return}let D;try{D=await fs.promises.readdir(R,{withFileTypes:!0})}catch{return}for(const K of D){if(S>=SCAN_FILE_CAP){_=!0;return}const x=K.name,q=path.join(R,x);if(K.isDirectory()){if(!X||x.startsWith(".")||SRC_EXCLUDE.has(x)||a?.has(x)||x.endsWith("-devserver")||L.has(`${h(q)}/`))continue;await T(q)}else if(K.isFile()){const Y=x.lastIndexOf(".");if(Y<=0||!SRC_EXT.has(x.slice(Y)))continue;const P=h(q);if(L.has(P)||f&&isTestSourcePath(P)||g.has(P))continue;g.add(P),S+=1,y^=fnv1a(P)}}},"walk");if(n&&n.length>0){await T(t,!1);for(const R of n)await T(R)}else await T(t);return{signature:`${S}:${(y>>>0).toString(36)}`,paths:g,truncated:_}}v(sourceSetSnapshot,"sourceSetSnapshot"),i(sourceSetSnapshot,"sourceSetSnapshot");async function sourceSetSignature(t,n,a,l){return(await sourceSetSnapshot(t,n,a,l)).signature}v(sourceSetSignature,"sourceSetSignature"),i(sourceSetSignature,"sourceSetSignature");const SOURCE_DIFF_PATH_CAP=128;function diffSourceSets(t,n,a,l){if(t.paths.size===n.paths.size){let _=!0;for(const g of t.paths)if(!n.paths.has(g)){_=!1;break}if(_)return null}const f=[],S=[];let y=t.truncated||n.truncated;for(const _ of n.paths)t.paths.has(_)||(f.length+S.length<SOURCE_DIFF_PATH_CAP?f.push(_):y=!0);for(const _ of t.paths)n.paths.has(_)||(f.length+S.length<SOURCE_DIFF_PATH_CAP?S.push(_):y=!0);return{changedAt:a,added:f,removed:S,truncated:y,...l!==void 0?{windowMs:l}:{}}}v(diffSourceSets,"diffSourceSets"),i(diffSourceSets,"diffSourceSets");function sourceChangeWindowMs(t,n,a,l){const f=n&&n>0?t-n:a;return Math.max(f,a)+l}v(sourceChangeWindowMs,"sourceChangeWindowMs"),i(sourceChangeWindowMs,"sourceChangeWindowMs");function sourceScopeDirs(t,n,a,l,f){const S=i(g=>{const L=path.relative(t,g).split(path.sep).filter(Boolean);return L.length===0||L[0].startsWith("..")?"":L[0]},"bucketOf")(n);if(!S)return[];const y=new Set(a.filter(Boolean)),_=new Set([S]);for(const g of l)g.startsWith(".")||SRC_EXCLUDE.has(g)||f?.has(g)||g.endsWith("-devserver")||y.has(g)||_.add(g);return[..._].sort().map(g=>path.join(t,g))}v(sourceScopeDirs,"sourceScopeDirs"),i(sourceScopeDirs,"sourceScopeDirs");function podRecycleReason(t,n,a,l){return!t.proc||t.state!=="up"||t.spawnAt==null||t.inflight>0||(t.activeWork??0)>0||n-t.lastUsed<l.settleMs||t.lastStaleRecycleAt!=null&&n-t.lastStaleRecycleAt<l.cooldownMs?null:a>0&&a-t.spawnAt>l.graceMs?"stale-source-set":l.maxAgeMs>0&&n-t.spawnAt>l.maxAgeMs?"max-age":null}v(podRecycleReason,"podRecycleReason"),i(podRecycleReason,"podRecycleReason");function lensFrontendStale(t,n,a,l,f){return!(l-n<f.cooldownMs||a<=0||a-t<=f.graceMs||l-a<f.settleMs)}v(lensFrontendStale,"lensFrontendStale"),i(lensFrontendStale,"lensFrontendStale");function lensWatcherVerdict(t,n,a,l=[]){if(!t)return"unknown";if("schemaVersion"in t&&t.schemaVersion>=2&&l.length>0){const f=n-a,S=new Set(t.recentEvents.filter(y=>y.at>=f).map(y=>path.resolve(y.path)));return l.every(y=>S.has(path.resolve(y)))?"heard":"missed"}return t.lastFsSetEventAt>=n-a?"heard":"missed"}v(lensWatcherVerdict,"lensWatcherVerdict"),i(lensWatcherVerdict,"lensWatcherVerdict");function podWatcherVerdict(t,n,a){if(!t)return"unknown";if(t.degraded)return"degraded";const l=n-a;return t.lastWatchRunAt>=l||t.lastBuildAt>=l?"heard":"silent"}v(podWatcherVerdict,"podWatcherVerdict"),i(podWatcherVerdict,"podWatcherVerdict");function lensChildWedged(t,n,a,l){const f=t.hasListened?l.graceMs:Math.max(l.graceMs,l.neverListenedGraceMs);return a-t.spawnedAt<f||a-n<l.cooldownMs||t.probeFails<Math.max(2,l.failureThreshold)?null:"wedged-no-listener"}v(lensChildWedged,"lensChildWedged"),i(lensChildWedged,"lensChildWedged");function isPortConflictExit(t,n){return t.signal||t.code===null||t.code===0||t.ranMs>n.fastFailMs?!1:t.portHeld}v(isPortConflictExit,"isPortConflictExit"),i(isPortConflictExit,"isPortConflictExit");function createServiceLayer(t,n,a={}){const l=a.spawnProcess??node_child_process_1.spawn,f=a.probeAlive??pod_probe_1.probePodAlive,S=a.killTree??((e,o="SIGTERM")=>{try{process.kill(-e.pid,o)}catch{try{e.kill(o)}catch{}}}),y=a.rollReadyMs??types_1.POD_ROLL_READY_MS,_=a.rollPollMs??750,{emit:g}=n,L=i(e=>e.wsKey===t.wsKey?void 0:path.join(e.root,".lensmcp","events.jsonl"),"serviceBus"),h=i((e,o,s,r,c)=>g(o,s,r,{wsKey:e.wsKey,...c},void 0,L(e)),"emitSvc"),T=i(e=>(e.scannedAt=0,(0,discovery_1.pickSock)(e,t.scanTtlMs)?e.socks.length:0),"scanNow"),R=i((e,o)=>{let s;try{s=(0,node_child_process_1.execFileSync)("ps",["ax","-ww","-o","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3})}catch{return 0}const r=e.proc?.pid;let c=0;for(const d of s.split(`
2
- `)){const u=d.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);if(!u)continue;const j=Number(u[1]),b=Number(u[2]),$=u[3];if($.includes(`run ${o}`)&&/\bnx(\.js)?\b/.test($)&&!($.includes("/node_modules/nx/")&&!$.includes(e.root))&&!(j===process.pid||j===r)&&!(r!==void 0&&b===r)){try{process.kill(-b,"SIGKILL")}catch{try{process.kill(j,"SIGKILL")}catch{}}c+=1}}return c>0&&(console.log(`[gateway] ${o}: reaped ${c} stale runner tree(s) \u2014 a foreign generation held the nx task lock`),h(e,"warning",`stale-runner reap: ${e.project} (${c} foreign tree(s))`,`cluster-service:${e.project}`,{kind:"stale-runner-reap",project:e.project,reaped:c})),c},"reapForeignRunners"),X=i((e,o,s)=>{const r=`${e.project}:${e.decl.serveTarget??"serve-hmr"}`,c=Math.max(1,e.decl.minPods??1),d=path.join(e.root,"node_modules","nx","dist","bin","nx.js"),u=(0,process_name_1.namedExecPath)((0,process_name_1.processName)(e.wsKey||path.basename(e.root),e.project,"nx")),[j,b]=fs.existsSync(d)?[u,[d,"run",r,`--childCount=${c}`]]:["yarn",["nx","run",r,`--childCount=${c}`]],$=l(j,b,{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),LENSMCP_POD_GENERATION:String(o),...eastWestEnv(t,e)},stdio:["ignore","pipe","pipe"]});s==="active"&&(e.lastErrors=[],e.buildFailed=!1);const C=i(w=>{const E=w.toString();for(const F of E.split(`
3
- `)){const{clean:p,broken:A,healthy:m}=(0,pod_output_1.classifyPodLine)(F),M=p;if(A){e.lastErrors.push(p),e.lastErrors.length>12&&e.lastErrors.shift(),e.buildFailed=!0;const G=Date.now();(p!==e.lastErrorEmitSig||G-(e.lastErrorEmitAt??0)>=types_1.SERVICE_ERROR_THROTTLE_MS)&&(e.lastErrorEmitSig=p,e.lastErrorEmitAt=G,h(e,"error",`service error: ${e.project}`,`cluster-service:${e.project}`,{kind:"service-error",project:e.project,line:p}))}m&&(e.buildFailed=!1),!e.lockBlockedAt&&/Waiting for .+ in another nx process/.test(M)&&(e.lockBlockedAt=Date.now(),console.log(`[gateway] ${r}: spawn blocked by a stale nx runner \u2014 breaking the lock + respawning`),h(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||(O(e,"parked on a foreign nx task lock \u2014 recycling",{announce:!1}),R(e,r),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&D(e)},1500).unref?.())},250).unref?.())}},"sniff"),k=e.wsKey&&e.wsKey!==t.wsKey&&e.root?(0,workspace_log_1.workspaceLogWriter)(e.root):void 0;return $.stdout?.on("data",w=>{if(t.stopped())return;C(w);const E=`[${e.project}] ${w}`;process.stdout.write(E),k?.(E)}),$.stderr?.on("data",w=>{if(t.stopped())return;C(w);const E=`[${e.project}] ${w}`;process.stderr.write(E),k?.(E)}),$.on("exit",(w,E)=>{if(e.proc!==$||(e.proc=void 0,e.state="down",t.stopped()))return;const F=(0,exit_reason_1.describeExit)(w,E);if(console.log(`[gateway] ${r} exited (${w??"no code"}${E?`, ${E}`:""}${e.inflight?`, ${e.inflight} request(s) in flight`:""})${F?` \u2014 ${F}`:""}`),h(e,w===0?"info":"warning",`service down: ${e.project} (exit ${w})`,`cluster-service:${e.project}`,{kind:"service-down",project:e.project,code:w,...E?{signal:E}:{}}),w===0||e.buildFailed)return;const p=Date.now();if(e.restarts=(e.restarts??[]).filter(m=>p-m<types_1.SERVICE_RESTART_WINDOW_MS),e.restarts.length>=types_1.SERVICE_MAX_RESTARTS){h(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(p);const A=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})`),h(e,"info",`self-heal: respawning ${e.project}`,`cluster-service:${e.project}`,{kind:"service-respawn",project:e.project,attempt:e.restarts.length}),D(e))},A)}),$},"launchServiceProcess"),D=i(e=>{if(t.stopped())return;const o=`${e.project}:${e.decl.serveTarget??"serve-hmr"}`,s=Math.max(1,e.decl.minPods??1);a.reapForeign!==!1&&R(e,o),e.lockBlockedAt=void 0,console.log(`[gateway] starting ${o} (pods=${s})\u2026`),h(e,"info",`service starting: ${e.project} (${s} pod${s>1?"s":""})`,`cluster-service:${e.project}`,{kind:"service-starting",project:e.project,pods:s}),e.state="starting",e.spawnAt=Date.now(),e.generation=(e.generation??0)+1,e.proc=X(e,e.generation,"active")},"spawnService"),K=i(async e=>{const o=e.pool.socks[0];if(!o)return;const s=await(0,pod_probe_1.probePodAlive)(o,void 0,t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)]);s&&(e.activeWork=s.activeWork,s.watcher&&(e.watcher=s.watcher))},"refreshActiveWork"),x=i(e=>{try{const o=Number(fs.readFileSync(`${e}.owner`,"utf8").trim());return Number.isInteger(o)&&o>1?o:void 0}catch{return}},"readOwnerSidecar"),q=i(async e=>{const o=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)];if(!o)return[];const s=(0,node_crypto_1.createHash)("sha256").update(o).digest("hex").slice(0,8),r=[];for(const c of e.pool.socks){const d=await(0,pod_probe_1.probePodAlive)(c,void 0,o);d&&(!d.keyFingerprint||d.keyFingerprint!==s)&&r.push({sock:c,pid:d.pid})}return r},"staleAdoptedSocks"),Y=i((e,o)=>{const s=o.map(r=>r.sock);for(const{sock:r,pid:c}of o){const d=chooseStalePodKillTarget(c,x(r),process.pid);if(d!==void 0)try{process.kill(d,"SIGTERM")}catch{}try{fs.unlinkSync(r)}catch{}try{fs.unlinkSync(`${r}.owner`)}catch{}}e.pool.socks=e.pool.socks.filter(r=>!s.includes(r)),e.pool.idx=-1,e.pool.scannedAt=0},"evictStaleAdopted"),P=i((e,o)=>{if(e.state==="up")return!1;e.state="up";const s=e.pool.socks.length;return h(e,"info",`service up: ${e.project} (${s} pods, ${o})`,`cluster-service:${e.project}`,{kind:"service-up",project:e.project,pods:s,adopted:o==="adopted"}),!0},"markUp"),re=i(async e=>{if(T(e.pool)>0){const r=await q(e);if(r.length>0&&(console.error(`[gateway] ${e.project}: ${r.length} adopted pod(s) hold a foreign east-west key \u2014 evicting instead of adopting.`),h(e,"warning",`stale adoption: ${e.project} (foreign east-west key)`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:"stale-adoption",pods:r.length}),e.proc&&O(e,"stale adoption (foreign east-west key)"),Y(e,r)),e.pool.socks.length>0)return P(e,"adopted"),!0}if(t.stopped())return!1;e.state!=="starting"&&!e.proc&&D(e);const o=Date.now()+t.startupTimeoutMs,s=Date.now()+12e3;for(;!t.stopped()&&Date.now()<o;){if(await sleep(Math.min(700,t.startupTimeoutMs/4)),e.buildFailed&&Date.now()>s)return!1;if(T(e.pool)>0){e.state="up",e.buildFailed=!1;const r=e.spawnAt?Date.now()-e.spawnAt:void 0;return console.log(`[gateway] ${e.project} up (${e.pool.socks.length} pods${r?`, cold start ${r}ms`:""})`),h(e,"info",`cold start: ${e.project} (${e.pool.socks.length} pods, ${r??"?"}ms)`,`cluster-service:${e.project}`,{kind:"cold-start",project:e.project,pods:e.pool.socks.length,durationMs:r}),!0}if(e.state==="down")return!1}return!1},"ensureUp"),se=i(e=>{const o=e.decl.maxPods??e.decl.minPods??1,s=e.pool.socks.length,r=e.decl.adminUrl??(e.decl.port?`http://localhost:${e.decl.port}`:void 0);if(!r||s===0||s>=o||e.inflight<=s||Date.now()-e.lastScaleAt<5e3)return;e.lastScaleAt=Date.now();const c=(r.startsWith("https")?require("node:https"):http).request(`${r}/webpack/scale`,{method:"POST",rejectUnauthorized:!1},d=>d.resume());c.on("error",()=>{}),c.end(JSON.stringify({pods:s+1})),console.log(`[gateway] autoscale ${e.project}: ${s} \u2192 ${s+1} pods (inflight ${e.inflight})`),h(e,"info",`autoscale: ${e.project} ${s} \u2192 ${s+1} pods`,`cluster-service:${e.project}`,{kind:"autoscale",project:e.project,from:s,to:s+1,inflight:e.inflight})},"scaleUp"),O=i((e,o,s)=>{if(!e.proc)return;console.log(`[gateway] ${e.project} ${o} \u2014 scaling to zero.`),s?.announce!==!1&&h(e,"info",`idle kill: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"idle-kill",project:e.project,why:o});const r=e.proc;try{r.stdout?.removeAllListeners("data"),r.stderr?.removeAllListeners("data"),r.removeAllListeners("exit")}catch{}S(r,"SIGTERM"),e.proc=void 0,e.state="down",setTimeout(()=>{z(e),e.pool.socks=[],e.pool.scannedAt=0},1200).unref?.()},"killSpawned"),z=i(e=>{try{for(const o of fs.readdirSync(e.pool.dir)){if(!o.endsWith(".sock"))continue;const s=path.join(e.pool.dir,o);try{const r=Number(fs.readFileSync(`${s}.owner`,"utf8").trim());if(Number.isFinite(r)&&r>0)try{process.kill(r,0);continue}catch{}}catch{}try{fs.unlinkSync(s)}catch{}try{fs.unlinkSync(`${s}.owner`)}catch{}}}catch{}},"sweepDeadOwnerSocks"),te=i((e,o)=>{if(e.rolling)return e.rolling;const s=e.proc;if(!s||t.stopped())return Promise.resolve(!1);const r=e.generation??0,c=r+1,d=Date.now();e.lastStaleRecycleAt=d,console.log(`[gateway] pod handoff start: ${e.project} g${r}\u2192g${c} reason=${o} \u2014 standby spawning, old generation keeps serving.`),h(e,"info",`pod handoff starting: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"pod-handoff-start",project:e.project,reason:o,generation:c,fromGeneration:r});const u=X(e,c,"standby"),j=(async()=>{const b=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)],$=d+y;let C;for(;!t.stopped()&&Date.now()<$&&!(u.exitCode!==null||u.signalCode!==null);){T(e.pool);for(const k of e.pool.socks)if((await f(k,void 0,b))?.podGeneration===c){C=k;break}if(C)break;await sleep(_)}if(!C||t.stopped()){const k=t.stopped()?"gateway stopping":u.exitCode!==null||u.signalCode!==null?`standby exited (${u.exitCode??u.signalCode})`:`standby not ready within ${Math.round(y/1e3)}s`;try{u.stdout?.removeAllListeners("data"),u.stderr?.removeAllListeners("data")}catch{}return S(u,"SIGTERM"),console.error(`[gateway] pod handoff failed: ${e.project} g${c}: ${k} \u2014 old generation g${r} kept serving.`),h(e,"warning",`pod handoff failed: ${e.project}`,`cluster-service:${e.project}`,{kind:"pod-handoff-failed",project:e.project,reason:o,generation:c,error:k,durationMs:Date.now()-d}),setTimeout(()=>z(e),1200).unref?.(),!1}if(e.proc!==s)return S(u,"SIGTERM"),!1;e.proc=u,e.generation=c,e.spawnAt=d,e.state="up",e.watcher=void 0,e.staleVerifiedAt=void 0,e.staleKeptAt=void 0,e.buildFailed=!1,e.lastErrors=[];try{s.stdout?.removeAllListeners("data"),s.stderr?.removeAllListeners("data"),s.removeAllListeners("exit")}catch{}return S(s,"SIGTERM"),setTimeout(()=>{z(e),e.pool.scannedAt=0},1200).unref?.(),console.log(`[gateway] pod handoff ready: ${e.project} g${c} answered on ${path.basename(C)} in ${Date.now()-d}ms; retiring g${r}.`),h(e,"info",`pod handoff complete: ${e.project}`,`cluster-service:${e.project}`,{kind:"pod-handoff-complete",project:e.project,reason:o,generation:c,fromGeneration:r,durationMs:Date.now()-d}),!0})().finally(()=>{e.rolling=void 0});return e.rolling=j,j},"rollPod"),ne=i(e=>{const o=Date.now();if(e.restarts=(e.restarts??[]).filter(s=>o-s<types_1.SERVICE_RESTART_WINDOW_MS),e.restarts.length>=types_1.SERVICE_MAX_RESTARTS){h(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}),O(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}; ${e.inflight??0} request(s) in flight \u2014 jobs held by the dead pod keep their locks until they expire)`),h(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}),O(e,"wedged: 0 live pods \u2014 recycling",{announce:!1}),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&D(e)},1500).unref?.()},"recycleWedged"),ce={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 J;const V=new Map,oe=new Map;let Q=!1,Z=0,H=0;const ie=i(()=>{const e=t.projectRoots??{},o=new Set(t.services.map(r=>r.project));for(const r of t.routes)r.lens&&o.add(r.lens.project);const s=new Set;for(const r of o){const c=e[r];if(!c)continue;const d=path.relative(t.root,c).split(path.sep).filter(Boolean)[0];d&&!d.startsWith("..")&&s.add(d)}return[...s]},"managedBuckets"),ae=i((e,o,s,r)=>{const c=t.projectSourceRoots?.[e];if(c?.length)return c;const d=t.projectRoots?.[e];if(!d)return;const u=sourceScopeDirs(t.root,d,s,o,r);return u.length>0?u:void 0},"scopeFor"),le=i(()=>{if(Q||Date.now()-Z<types_1.POD_STALE_SCAN_MS)return;Q=!0,Z=Date.now();const e=Z;(async()=>{const o=(0,scope_1.readSourceSetOptions)(t.root),s=o.exclude,r={ignoreTests:o.ignoreTests},c=await sourceSetSnapshot(t.root,void 0,s,r),d=Date.now(),u=sourceChangeWindowMs(d,H,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS),j=J?diffSourceSets(J,c,d,u):null;if(j&&(t.sourceSetChangedAt=d,t.sourceSetChange=j,g("info","source file set changed \u2014 stale pods recycle when idle","cluster-gateway",{kind:"source-set-changed",added:j.added,removed:j.removed,truncated:j.truncated})),J=c,!t.projectRoots){H=e;return}let b;try{b=(await fs.promises.readdir(t.root,{withFileTypes:!0})).filter(p=>p.isDirectory()).map(p=>p.name)}catch{return}const $=ie(),C=new Set(t.services.map(p=>p.project));for(const p of t.routes)p.lens&&C.add(p.lens.project);const k=new Map;for(const p of C){const A=ae(p,b,$,s);if(!A)continue;const m=A.join("|"),M=k.get(m)??{dirs:A,projects:[]};M.projects.push(p),k.set(m,M)}const w=Date.now(),E=sourceChangeWindowMs(w,H,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS);H=e;const F=i((p,A,m)=>{t.sourceSetChangedFor??=new Map,t.sourceSetChangesFor??=new Map;for(const M of p)t.sourceSetChangedFor.set(M,A),t.sourceSetChangesFor.set(M,m)},"stampScoped");for(const[p,{dirs:A,projects:m}]of k){const M=await sourceSetSnapshot(t.root,A,s,r),G=V.get(p);V.set(p,M);const ee=new Map;for(const N of m){const I=oe.get(N);if(I===void 0||I===p)continue;const W=ee.get(I)??[];W.push(N),ee.set(I,W)}for(const N of m)oe.set(N,p);for(const[N,I]of ee){const W=V.get(N),U=W?diffSourceSets(W,M,w,E):j;U&&(F(I,W?w:d,U),g("info",`source file set changed \u2014 ${I.join(", ")}'s watch scope was re-keyed`,"cluster-gateway",{kind:"source-set-changed",scoped:!0,rescoped:!0,projects:I,added:U.added,removed:U.removed,truncated:U.truncated}))}if(!G)continue;const B=diffSourceSets(G,M,w,E);B&&(F(m,w,B),g("info",`source file set changed in ${m.join(", ")}'s scope`,"cluster-gateway",{kind:"source-set-changed",scoped:!0,projects:m,added:B.added,removed:B.removed,truncated:B.truncated}))}for(const p of[...V.keys()])k.has(p)||V.delete(p)})().catch(()=>{}).finally(()=>{Q=!1})},"maybeScanSourceSet"),de=i((e,o)=>{const s=(t.sourceSetChangesFor?.get(e.project)??t.sourceSetChange)?.windowMs??types_1.POD_STALE_SCAN_MS+types_1.POD_RECYCLE_SETTLE_MS,r=podWatcherVerdict(e.watcher,o,s),c=i(d=>(e.staleKeptAt!==o&&(e.staleKeptAt=o,console.log(`[gateway] stale source set: pod ${e.project} ${d} \u2014 no recycle.`)),!1),"keep");return r==="heard"?(e.staleVerifiedAt=o,h(e,"info",`stale check: ${e.project} watcher confirmed \u2014 no recycle`,`cluster-service:${e.project}`,{kind:"pod-stale-verified",project:e.project,watcher:r}),c(`watcher CONFIRMED alive across the change (last build ${new Date(e.watcher?.lastBuildAt??0).toISOString()})`)):r==="degraded"?(console.log(`[gateway] stale source set: pod ${e.project} watcher DEGRADED (${e.watcher?.degraded}) \u2014 recycling.`),!0):r==="silent"?types_1.POD_WATCHER_SILENT==="keep"?c("watcher alive but quiet across the change \u2014 keeping it (LENSMCP_POD_WATCHER_SILENT=keep; a live watcher cannot miss an add it will need)"):!0:types_1.POD_WATCHER_UNVERIFIED==="keep"?c("unverifiable (no watcher testimony \u2014 older devserver bundle?) \u2014 keeping it (LENSMCP_POD_WATCHER_UNVERIFIED=keep)"):!0},"podStaleRecycleAllowed"),pe=i((e,o)=>{if(e.lastStaleRecycleAt=Date.now(),types_1.POD_RECYCLE_MODE==="rolling"&&e.proc){te(e,o);return}console.log(`[gateway] zombie recycle: ${e.project} (${o}) \u2014 stale fs view; fresh pod incoming`),h(e,"warning",`zombie recycle: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:o});const s=!!e.decl.eager;O(e,`${o} \u2014 recycling stale pod`,{announce:!1}),s&&setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&D(e)},1500).unref?.()},"recyclePod"),ue=i(()=>{const e=setInterval(()=>{he().catch(o=>console.error("[gateway] sweep error:",o))},t.sweepMs);return e.unref(),e},"startSweeper"),he=i(async()=>{{if(t.stopped())return;types_1.POD_RECYCLE_ENABLED&&le();for(const e of t.services){if(t.stopped()||!e.proc){e.wedgedSince=void 0;continue}const o=e.decl.idleKillSec;if(o&&e.inflight===0&&Date.now()-e.lastUsed>o*1e3){O(e,`idle ${o}s`),e.wedgedSince=void 0;continue}const s=e.state==="starting"&&e.spawnAt!==void 0&&Date.now()-e.spawnAt<t.startupTimeoutMs,r=T(e.pool)>0;if(r||s){if(e.wedgedSince=void 0,r&&P(e,"observed"),r&&!s&&types_1.POD_RECYCLE_ENABLED){const c=t.sourceSetChangedFor?.get(e.project)??t.sourceSetChangedAt??0;await K(e);const d=(e.staleVerifiedAt??0)>=c&&c>0,u=podRecycleReason(e,Date.now(),d?0:c,ce);if(u==="stale-source-set"&&!de(e,c))continue;u&&pe(e,u)}continue}e.wedgedSince??=Date.now(),Date.now()-e.wedgedSince>types_1.WEDGE_RECYCLE_GRACE_MS&&(e.wedgedSince=void 0,ne(e))}}},"sweepOnce");return{scanNow:T,spawnService:D,ensureUp:re,scaleUp:se,killSpawned:O,markUp:P,startSweeper:ue,rollPod:te}}v(createServiceLayer,"createServiceLayer"),i(createServiceLayer,"createServiceLayer");function chooseStalePodKillTarget(t,n,a){if(!(t===void 0||!Number.isInteger(t)||t<=1)&&!(n!==void 0&&n!==t)&&t!==a)return t}v(chooseStalePodKillTarget,"chooseStalePodKillTarget"),i(chooseStalePodKillTarget,"chooseStalePodKillTarget");
1
+ "use strict";var ge=Object.defineProperty;var v=(t,n)=>ge(t,"name",{value:n,configurable:!0});var fe=Object.defineProperty,i=v((t,n)=>fe(t,"name",{value:n,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isTestSourcePath=isTestSourcePath,exports.sourceSetSnapshot=sourceSetSnapshot,exports.sourceSetSignature=sourceSetSignature,exports.diffSourceSets=diffSourceSets,exports.sourceChangeWindowMs=sourceChangeWindowMs,exports.sourceScopeDirs=sourceScopeDirs,exports.podRecycleReason=podRecycleReason,exports.lensFrontendStale=lensFrontendStale,exports.lensWatcherVerdict=lensWatcherVerdict,exports.podWatcherVerdict=podWatcherVerdict,exports.lensChildWedged=lensChildWedged,exports.isPortConflictExit=isPortConflictExit,exports.createServiceLayer=createServiceLayer,exports.chooseStalePodKillTarget=chooseStalePodKillTarget;const tslib_1=require("tslib"),node_child_process_1=require("node:child_process"),node_crypto_1=require("node:crypto"),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"),pod_probe_1=require("./pod-probe"),service_keys_1=require("./service-keys"),workspace_log_1=require("./workspace-log"),pod_output_1=require("./pod-output"),types_1=require("./types"),exit_reason_1=require("../../../exit-reason"),process_name_1=require("../../../process-name");function eastWestEnv(t,n){const a={},l=t.serviceKeys[(0,service_keys_1.serviceKeyId)(n.wsKey,n.project)];if(l&&(a.LENSMCP_SERVICE_KEY=l),t.https){const f=require("../../../basic-ssl").caCertPath();fs.existsSync(f)&&(a.NODE_EXTRA_CA_CERTS=f)}return a}v(eastWestEnv,"eastWestEnv"),i(eastWestEnv,"eastWestEnv");const sleep=i(t=>new Promise(n=>setTimeout(n,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 n=2166136261;for(let a=0;a<t.length;a+=1)n^=t.charCodeAt(a),n=Math.imul(n,16777619);return n>>>0}v(fnv1a,"fnv1a"),i(fnv1a,"fnv1a");const execFileP=(0,node_util_1.promisify)(node_child_process_1.execFile);async function gitIgnoredPaths(t){try{const{stdout:n}=await execFileP("git",["ls-files","--others","--directory","--ignored","--exclude-standard","-z"],{cwd:t,maxBuffer:33554432,timeout:1e4,windowsHide:!0});return new Set(n.split("\0").filter(Boolean))}catch{return new Set}}v(gitIgnoredPaths,"gitIgnoredPaths"),i(gitIgnoredPaths,"gitIgnoredPaths");const TEST_FILE_RE=/\.(spec|test|e2e|e2e-spec|stories)\.[cm]?[jt]sx?$/,TEST_DIR_SEGMENTS=new Set(["__tests__","__mocks__","__snapshots__"]);function isTestSourcePath(t){const n=t.split("/"),a=n[n.length-1]??"";if(TEST_FILE_RE.test(a))return!0;for(let l=0;l<n.length-1;l+=1)if(TEST_DIR_SEGMENTS.has(n[l]))return!0;return!1}v(isTestSourcePath,"isTestSourcePath"),i(isTestSourcePath,"isTestSourcePath");async function sourceSetSnapshot(t,n,a,l){const f=l?.ignoreTests??!0;let S=0,y=0,E=!1;const g=new Set,L=await gitIgnoredPaths(t),h=i(R=>path.relative(t,R).split(path.sep).join("/"),"rel"),T=i(async(R,X=!0)=>{if(S>=SCAN_FILE_CAP){E=!0;return}let D;try{D=await fs.promises.readdir(R,{withFileTypes:!0})}catch{return}for(const q of D){if(S>=SCAN_FILE_CAP){E=!0;return}const x=q.name,V=path.join(R,x);if(q.isDirectory()){if(!X||x.startsWith(".")||SRC_EXCLUDE.has(x)||a?.has(x)||x.endsWith("-devserver")||L.has(`${h(V)}/`))continue;await T(V)}else if(q.isFile()){const Y=x.lastIndexOf(".");if(Y<=0||!SRC_EXT.has(x.slice(Y)))continue;const P=h(V);if(L.has(P)||f&&isTestSourcePath(P)||g.has(P))continue;g.add(P),S+=1,y^=fnv1a(P)}}},"walk");if(n&&n.length>0){await T(t,!1);for(const R of n)await T(R)}else await T(t);return{signature:`${S}:${(y>>>0).toString(36)}`,paths:g,truncated:E}}v(sourceSetSnapshot,"sourceSetSnapshot"),i(sourceSetSnapshot,"sourceSetSnapshot");async function sourceSetSignature(t,n,a,l){return(await sourceSetSnapshot(t,n,a,l)).signature}v(sourceSetSignature,"sourceSetSignature"),i(sourceSetSignature,"sourceSetSignature");const SOURCE_DIFF_PATH_CAP=128;function diffSourceSets(t,n,a,l){if(t.paths.size===n.paths.size){let E=!0;for(const g of t.paths)if(!n.paths.has(g)){E=!1;break}if(E)return null}const f=[],S=[];let y=t.truncated||n.truncated;for(const E of n.paths)t.paths.has(E)||(f.length+S.length<SOURCE_DIFF_PATH_CAP?f.push(E):y=!0);for(const E of t.paths)n.paths.has(E)||(f.length+S.length<SOURCE_DIFF_PATH_CAP?S.push(E):y=!0);return{changedAt:a,added:f,removed:S,truncated:y,...l!==void 0?{windowMs:l}:{}}}v(diffSourceSets,"diffSourceSets"),i(diffSourceSets,"diffSourceSets");function sourceChangeWindowMs(t,n,a,l){const f=n&&n>0?t-n:a;return Math.max(f,a)+l}v(sourceChangeWindowMs,"sourceChangeWindowMs"),i(sourceChangeWindowMs,"sourceChangeWindowMs");function sourceScopeDirs(t,n,a,l,f){const S=i(g=>{const L=path.relative(t,g).split(path.sep).filter(Boolean);return L.length===0||L[0].startsWith("..")?"":L[0]},"bucketOf")(n);if(!S)return[];const y=new Set(a.filter(Boolean)),E=new Set([S]);for(const g of l)g.startsWith(".")||SRC_EXCLUDE.has(g)||f?.has(g)||g.endsWith("-devserver")||y.has(g)||E.add(g);return[...E].sort().map(g=>path.join(t,g))}v(sourceScopeDirs,"sourceScopeDirs"),i(sourceScopeDirs,"sourceScopeDirs");function podRecycleReason(t,n,a,l){return!t.proc||t.state!=="up"||t.spawnAt==null||t.inflight>0||(t.activeWork??0)>0||n-t.lastUsed<l.settleMs||t.lastStaleRecycleAt!=null&&n-t.lastStaleRecycleAt<l.cooldownMs?null:a>0&&a-t.spawnAt>l.graceMs?"stale-source-set":l.maxAgeMs>0&&n-t.spawnAt>l.maxAgeMs?"max-age":null}v(podRecycleReason,"podRecycleReason"),i(podRecycleReason,"podRecycleReason");function lensFrontendStale(t,n,a,l,f){return!(l-n<f.cooldownMs||a<=0||a-t<=f.graceMs||l-a<f.settleMs)}v(lensFrontendStale,"lensFrontendStale"),i(lensFrontendStale,"lensFrontendStale");function lensWatcherVerdict(t,n,a,l=[]){if(!t)return"unknown";if("schemaVersion"in t&&t.schemaVersion>=2&&l.length>0){const f=n-a,S=new Set(t.recentEvents.filter(y=>y.at>=f).map(y=>path.resolve(y.path)));return l.every(y=>S.has(path.resolve(y)))?"heard":"missed"}return t.lastFsSetEventAt>=n-a?"heard":"missed"}v(lensWatcherVerdict,"lensWatcherVerdict"),i(lensWatcherVerdict,"lensWatcherVerdict");function podWatcherVerdict(t,n,a){if(!t)return"unknown";if(t.degraded)return"degraded";const l=n-a;return t.lastWatchRunAt>=l||t.lastBuildAt>=l?"heard":"silent"}v(podWatcherVerdict,"podWatcherVerdict"),i(podWatcherVerdict,"podWatcherVerdict");function lensChildWedged(t,n,a,l){const f=t.hasListened?l.graceMs:Math.max(l.graceMs,l.neverListenedGraceMs);return a-t.spawnedAt<f||a-n<l.cooldownMs||t.probeFails<Math.max(2,l.failureThreshold)?null:"wedged-no-listener"}v(lensChildWedged,"lensChildWedged"),i(lensChildWedged,"lensChildWedged");function isPortConflictExit(t,n){return t.signal||t.code===null||t.code===0||t.ranMs>n.fastFailMs?!1:t.portHeld}v(isPortConflictExit,"isPortConflictExit"),i(isPortConflictExit,"isPortConflictExit");function createServiceLayer(t,n,a={}){const l=a.spawnProcess??node_child_process_1.spawn,f=a.probeAlive??pod_probe_1.probePodAlive,S=a.killTree??((e,o="SIGTERM")=>{try{process.kill(-e.pid,o)}catch{try{e.kill(o)}catch{}}}),y=a.rollReadyMs??types_1.POD_ROLL_READY_MS,E=a.rollPollMs??750,{emit:g}=n,L=i(e=>e.wsKey===t.wsKey?void 0:path.join(e.root,".lensmcp","events.jsonl"),"serviceBus"),h=i((e,o,s,r,c)=>g(o,s,r,{wsKey:e.wsKey,...c},void 0,L(e)),"emitSvc"),T=i(e=>(e.scannedAt=0,(0,discovery_1.pickSock)(e,t.scanTtlMs)?e.socks.length:0),"scanNow"),R=i((e,o)=>{let s;try{s=(0,node_child_process_1.execFileSync)("ps",["ax","-ww","-o","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3})}catch{return 0}const r=e.proc?.pid;let c=0;for(const d of s.split(`
2
+ `)){const p=d.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);if(!p)continue;const $=Number(p[1]),b=Number(p[2]),m=p[3];if(m.includes(`run ${o}`)&&/\bnx(\.js)?\b/.test(m)&&!(m.includes("/node_modules/nx/")&&!m.includes(e.root))&&!($===process.pid||$===r)&&!(r!==void 0&&b===r)){try{process.kill(-b,"SIGKILL")}catch{try{process.kill($,"SIGKILL")}catch{}}c+=1}}return c>0&&(console.log(`[gateway] ${o}: reaped ${c} stale runner tree(s) \u2014 a foreign generation held the nx task lock`),h(e,"warning",`stale-runner reap: ${e.project} (${c} foreign tree(s))`,`cluster-service:${e.project}`,{kind:"stale-runner-reap",project:e.project,reaped:c})),c},"reapForeignRunners"),X=i((e,o,s)=>{const r=`${e.project}:${e.decl.serveTarget??"serve-hmr"}`,c=Math.max(1,e.decl.minPods??1),d=path.join(e.root,"node_modules","nx","dist","bin","nx.js"),p=(0,process_name_1.namedExecPath)((0,process_name_1.processName)(e.wsKey||path.basename(e.root),e.project,"nx")),[$,b]=fs.existsSync(d)?[p,[d,"run",r,`--childCount=${c}`]]:["yarn",["nx","run",r,`--childCount=${c}`]],m=l($,b,{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),LENSMCP_POD_GENERATION:String(o),...eastWestEnv(t,e)},stdio:["ignore","pipe","pipe"]});s==="active"&&(e.lastErrors=[],e.buildFailed=!1,e.faults=new pod_output_1.PodFaults);const C=i(w=>{const _=w.toString();for(const O of _.split(`
3
+ `)){const u=(0,pod_output_1.classifyPodLine)(O),{clean:j,broken:k,healthy:M}=u,z=j;if(k){e.lastErrors.push(j),e.lastErrors.length>12&&e.lastErrors.shift(),e.buildFailed=!0;const W=Date.now();(j!==e.lastErrorEmitSig||W-(e.lastErrorEmitAt??0)>=types_1.SERVICE_ERROR_THROTTLE_MS)&&(e.lastErrorEmitSig=j,e.lastErrorEmitAt=W,h(e,"error",`service error: ${e.project}`,`cluster-service:${e.project}`,{kind:"service-error",project:e.project,line:j}))}M&&(e.buildFailed=!1),(e.faults??=new pod_output_1.PodFaults).observe(u)&&(e.lastErrorEmitSig=void 0,h(e,"info",`service recovered: ${e.project}`,`cluster-service:${e.project}`,{kind:"service-recovered",project:e.project,line:j})),!e.lockBlockedAt&&/Waiting for .+ in another nx process/.test(z)&&(e.lockBlockedAt=Date.now(),console.log(`[gateway] ${r}: spawn blocked by a stale nx runner \u2014 breaking the lock + respawning`),h(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||(F(e,"parked on a foreign nx task lock \u2014 recycling",{announce:!1}),R(e,r),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&D(e)},1500).unref?.())},250).unref?.())}},"sniff"),A=e.wsKey&&e.wsKey!==t.wsKey&&e.root?(0,workspace_log_1.workspaceLogWriter)(e.root):void 0;return m.stdout?.on("data",w=>{if(t.stopped())return;C(w);const _=`[${e.project}] ${w}`;process.stdout.write(_),A?.(_)}),m.stderr?.on("data",w=>{if(t.stopped())return;C(w);const _=`[${e.project}] ${w}`;process.stderr.write(_),A?.(_)}),m.on("exit",(w,_)=>{if(e.proc!==m||(e.proc=void 0,e.state="down",t.stopped()))return;const O=(0,exit_reason_1.describeExit)(w,_);if(console.log(`[gateway] ${r} exited (${w??"no code"}${_?`, ${_}`:""}${e.inflight?`, ${e.inflight} request(s) in flight`:""})${O?` \u2014 ${O}`:""}`),h(e,w===0?"info":"warning",`service down: ${e.project} (exit ${w})`,`cluster-service:${e.project}`,{kind:"service-down",project:e.project,code:w,..._?{signal:_}:{}}),w===0||e.buildFailed)return;const u=Date.now();if(e.restarts=(e.restarts??[]).filter(k=>u-k<types_1.SERVICE_RESTART_WINDOW_MS),e.restarts.length>=types_1.SERVICE_MAX_RESTARTS){h(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(u);const j=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})`),h(e,"info",`self-heal: respawning ${e.project}`,`cluster-service:${e.project}`,{kind:"service-respawn",project:e.project,attempt:e.restarts.length}),D(e))},j)}),m},"launchServiceProcess"),D=i(e=>{if(t.stopped())return;const o=`${e.project}:${e.decl.serveTarget??"serve-hmr"}`,s=Math.max(1,e.decl.minPods??1);a.reapForeign!==!1&&R(e,o),e.lockBlockedAt=void 0,console.log(`[gateway] starting ${o} (pods=${s})\u2026`),h(e,"info",`service starting: ${e.project} (${s} pod${s>1?"s":""})`,`cluster-service:${e.project}`,{kind:"service-starting",project:e.project,pods:s}),e.state="starting",e.spawnAt=Date.now(),e.generation=(e.generation??0)+1,e.proc=X(e,e.generation,"active")},"spawnService"),q=i(async e=>{const o=e.pool.socks[0];if(!o)return;const s=await(0,pod_probe_1.probePodAlive)(o,void 0,t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)]);s&&(e.activeWork=s.activeWork,s.watcher&&(e.watcher=s.watcher))},"refreshActiveWork"),x=i(e=>{try{const o=Number(fs.readFileSync(`${e}.owner`,"utf8").trim());return Number.isInteger(o)&&o>1?o:void 0}catch{return}},"readOwnerSidecar"),V=i(async e=>{const o=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)];if(!o)return[];const s=(0,node_crypto_1.createHash)("sha256").update(o).digest("hex").slice(0,8),r=[];for(const c of e.pool.socks){const d=await(0,pod_probe_1.probePodAlive)(c,void 0,o);d&&(!d.keyFingerprint||d.keyFingerprint!==s)&&r.push({sock:c,pid:d.pid})}return r},"staleAdoptedSocks"),Y=i((e,o)=>{const s=o.map(r=>r.sock);for(const{sock:r,pid:c}of o){const d=chooseStalePodKillTarget(c,x(r),process.pid);if(d!==void 0)try{process.kill(d,"SIGTERM")}catch{}try{fs.unlinkSync(r)}catch{}try{fs.unlinkSync(`${r}.owner`)}catch{}}e.pool.socks=e.pool.socks.filter(r=>!s.includes(r)),e.pool.idx=-1,e.pool.scannedAt=0},"evictStaleAdopted"),P=i((e,o)=>{if(e.state==="up")return!1;e.state="up";const s=e.pool.socks.length;return h(e,"info",`service up: ${e.project} (${s} pods, ${o})`,`cluster-service:${e.project}`,{kind:"service-up",project:e.project,pods:s,adopted:o==="adopted"}),!0},"markUp"),re=i(async e=>{if(T(e.pool)>0){const r=await V(e);if(r.length>0&&(console.error(`[gateway] ${e.project}: ${r.length} adopted pod(s) hold a foreign east-west key \u2014 evicting instead of adopting.`),h(e,"warning",`stale adoption: ${e.project} (foreign east-west key)`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:"stale-adoption",pods:r.length}),e.proc&&F(e,"stale adoption (foreign east-west key)"),Y(e,r)),e.pool.socks.length>0)return P(e,"adopted"),!0}if(t.stopped())return!1;e.state!=="starting"&&!e.proc&&D(e);const o=Date.now()+t.startupTimeoutMs,s=Date.now()+12e3;for(;!t.stopped()&&Date.now()<o;){if(await sleep(Math.min(700,t.startupTimeoutMs/4)),e.buildFailed&&Date.now()>s)return!1;if(T(e.pool)>0){e.state="up",e.buildFailed=!1;const r=e.spawnAt?Date.now()-e.spawnAt:void 0;return console.log(`[gateway] ${e.project} up (${e.pool.socks.length} pods${r?`, cold start ${r}ms`:""})`),h(e,"info",`cold start: ${e.project} (${e.pool.socks.length} pods, ${r??"?"}ms)`,`cluster-service:${e.project}`,{kind:"cold-start",project:e.project,pods:e.pool.socks.length,durationMs:r}),!0}if(e.state==="down")return!1}return!1},"ensureUp"),se=i(e=>{const o=e.decl.maxPods??e.decl.minPods??1,s=e.pool.socks.length,r=e.decl.adminUrl??(e.decl.port?`http://localhost:${e.decl.port}`:void 0);if(!r||s===0||s>=o||e.inflight<=s||Date.now()-e.lastScaleAt<5e3)return;e.lastScaleAt=Date.now();const c=(r.startsWith("https")?require("node:https"):http).request(`${r}/webpack/scale`,{method:"POST",rejectUnauthorized:!1},d=>d.resume());c.on("error",()=>{}),c.end(JSON.stringify({pods:s+1})),console.log(`[gateway] autoscale ${e.project}: ${s} \u2192 ${s+1} pods (inflight ${e.inflight})`),h(e,"info",`autoscale: ${e.project} ${s} \u2192 ${s+1} pods`,`cluster-service:${e.project}`,{kind:"autoscale",project:e.project,from:s,to:s+1,inflight:e.inflight})},"scaleUp"),F=i((e,o,s)=>{if(!e.proc)return;console.log(`[gateway] ${e.project} ${o} \u2014 scaling to zero.`),s?.announce!==!1&&h(e,"info",`idle kill: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"idle-kill",project:e.project,why:o});const r=e.proc;try{r.stdout?.removeAllListeners("data"),r.stderr?.removeAllListeners("data"),r.removeAllListeners("exit")}catch{}S(r,"SIGTERM"),e.proc=void 0,e.state="down",setTimeout(()=>{J(e),e.pool.socks=[],e.pool.scannedAt=0},1200).unref?.()},"killSpawned"),J=i(e=>{try{for(const o of fs.readdirSync(e.pool.dir)){if(!o.endsWith(".sock"))continue;const s=path.join(e.pool.dir,o);try{const r=Number(fs.readFileSync(`${s}.owner`,"utf8").trim());if(Number.isFinite(r)&&r>0)try{process.kill(r,0);continue}catch{}}catch{}try{fs.unlinkSync(s)}catch{}try{fs.unlinkSync(`${s}.owner`)}catch{}}}catch{}},"sweepDeadOwnerSocks"),te=i((e,o)=>{if(e.rolling)return e.rolling;const s=e.proc;if(!s||t.stopped())return Promise.resolve(!1);const r=e.generation??0,c=r+1,d=Date.now();e.lastStaleRecycleAt=d,console.log(`[gateway] pod handoff start: ${e.project} g${r}\u2192g${c} reason=${o} \u2014 standby spawning, old generation keeps serving.`),h(e,"info",`pod handoff starting: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"pod-handoff-start",project:e.project,reason:o,generation:c,fromGeneration:r});const p=X(e,c,"standby"),$=(async()=>{const b=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)],m=d+y;let C;for(;!t.stopped()&&Date.now()<m&&!(p.exitCode!==null||p.signalCode!==null);){T(e.pool);for(const A of e.pool.socks)if((await f(A,void 0,b))?.podGeneration===c){C=A;break}if(C)break;await sleep(E)}if(!C||t.stopped()){const A=t.stopped()?"gateway stopping":p.exitCode!==null||p.signalCode!==null?`standby exited (${p.exitCode??p.signalCode})`:`standby not ready within ${Math.round(y/1e3)}s`;try{p.stdout?.removeAllListeners("data"),p.stderr?.removeAllListeners("data")}catch{}return S(p,"SIGTERM"),console.error(`[gateway] pod handoff failed: ${e.project} g${c}: ${A} \u2014 old generation g${r} kept serving.`),h(e,"warning",`pod handoff failed: ${e.project}`,`cluster-service:${e.project}`,{kind:"pod-handoff-failed",project:e.project,reason:o,generation:c,error:A,durationMs:Date.now()-d}),setTimeout(()=>J(e),1200).unref?.(),!1}if(e.proc!==s)return S(p,"SIGTERM"),!1;e.proc=p,e.generation=c,e.spawnAt=d,e.state="up",e.watcher=void 0,e.staleVerifiedAt=void 0,e.staleKeptAt=void 0,e.buildFailed=!1,e.lastErrors=[];try{s.stdout?.removeAllListeners("data"),s.stderr?.removeAllListeners("data"),s.removeAllListeners("exit")}catch{}return S(s,"SIGTERM"),setTimeout(()=>{J(e),e.pool.scannedAt=0},1200).unref?.(),console.log(`[gateway] pod handoff ready: ${e.project} g${c} answered on ${path.basename(C)} in ${Date.now()-d}ms; retiring g${r}.`),h(e,"info",`pod handoff complete: ${e.project}`,`cluster-service:${e.project}`,{kind:"pod-handoff-complete",project:e.project,reason:o,generation:c,fromGeneration:r,durationMs:Date.now()-d}),!0})().finally(()=>{e.rolling=void 0});return e.rolling=$,$},"rollPod"),ne=i(e=>{const o=Date.now();if(e.restarts=(e.restarts??[]).filter(s=>o-s<types_1.SERVICE_RESTART_WINDOW_MS),e.restarts.length>=types_1.SERVICE_MAX_RESTARTS){h(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}),F(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}; ${e.inflight??0} request(s) in flight \u2014 jobs held by the dead pod keep their locks until they expire)`),h(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}),F(e,"wedged: 0 live pods \u2014 recycling",{announce:!1}),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&D(e)},1500).unref?.()},"recycleWedged"),ce={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 Q;const G=new Map,oe=new Map;let Z=!1,ee=0,H=0;const ie=i(()=>{const e=t.projectRoots??{},o=new Set(t.services.map(r=>r.project));for(const r of t.routes)r.lens&&o.add(r.lens.project);const s=new Set;for(const r of o){const c=e[r];if(!c)continue;const d=path.relative(t.root,c).split(path.sep).filter(Boolean)[0];d&&!d.startsWith("..")&&s.add(d)}return[...s]},"managedBuckets"),ae=i((e,o,s,r)=>{const c=t.projectSourceRoots?.[e];if(c?.length)return c;const d=t.projectRoots?.[e];if(!d)return;const p=sourceScopeDirs(t.root,d,s,o,r);return p.length>0?p:void 0},"scopeFor"),le=i(()=>{if(Z||Date.now()-ee<types_1.POD_STALE_SCAN_MS)return;Z=!0,ee=Date.now();const e=ee;(async()=>{const o=(0,scope_1.readSourceSetOptions)(t.root),s=o.exclude,r={ignoreTests:o.ignoreTests},c=await sourceSetSnapshot(t.root,void 0,s,r),d=Date.now(),p=sourceChangeWindowMs(d,H,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS),$=Q?diffSourceSets(Q,c,d,p):null;if($&&(t.sourceSetChangedAt=d,t.sourceSetChange=$,g("info","source file set changed \u2014 stale pods recycle when idle","cluster-gateway",{kind:"source-set-changed",added:$.added,removed:$.removed,truncated:$.truncated})),Q=c,!t.projectRoots){H=e;return}let b;try{b=(await fs.promises.readdir(t.root,{withFileTypes:!0})).filter(u=>u.isDirectory()).map(u=>u.name)}catch{return}const m=ie(),C=new Set(t.services.map(u=>u.project));for(const u of t.routes)u.lens&&C.add(u.lens.project);const A=new Map;for(const u of C){const j=ae(u,b,m,s);if(!j)continue;const k=j.join("|"),M=A.get(k)??{dirs:j,projects:[]};M.projects.push(u),A.set(k,M)}const w=Date.now(),_=sourceChangeWindowMs(w,H,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS);H=e;const O=i((u,j,k)=>{t.sourceSetChangedFor??=new Map,t.sourceSetChangesFor??=new Map;for(const M of u)t.sourceSetChangedFor.set(M,j),t.sourceSetChangesFor.set(M,k)},"stampScoped");for(const[u,{dirs:j,projects:k}]of A){const M=await sourceSetSnapshot(t.root,j,s,r),z=G.get(u);G.set(u,M);const W=new Map;for(const N of k){const I=oe.get(N);if(I===void 0||I===u)continue;const K=W.get(I)??[];K.push(N),W.set(I,K)}for(const N of k)oe.set(N,u);for(const[N,I]of W){const K=G.get(N),U=K?diffSourceSets(K,M,w,_):$;U&&(O(I,K?w:d,U),g("info",`source file set changed \u2014 ${I.join(", ")}'s watch scope was re-keyed`,"cluster-gateway",{kind:"source-set-changed",scoped:!0,rescoped:!0,projects:I,added:U.added,removed:U.removed,truncated:U.truncated}))}if(!z)continue;const B=diffSourceSets(z,M,w,_);B&&(O(k,w,B),g("info",`source file set changed in ${k.join(", ")}'s scope`,"cluster-gateway",{kind:"source-set-changed",scoped:!0,projects:k,added:B.added,removed:B.removed,truncated:B.truncated}))}for(const u of[...G.keys()])A.has(u)||G.delete(u)})().catch(()=>{}).finally(()=>{Z=!1})},"maybeScanSourceSet"),de=i((e,o)=>{const s=(t.sourceSetChangesFor?.get(e.project)??t.sourceSetChange)?.windowMs??types_1.POD_STALE_SCAN_MS+types_1.POD_RECYCLE_SETTLE_MS,r=podWatcherVerdict(e.watcher,o,s),c=i(d=>(e.staleKeptAt!==o&&(e.staleKeptAt=o,console.log(`[gateway] stale source set: pod ${e.project} ${d} \u2014 no recycle.`)),!1),"keep");return r==="heard"?(e.staleVerifiedAt=o,h(e,"info",`stale check: ${e.project} watcher confirmed \u2014 no recycle`,`cluster-service:${e.project}`,{kind:"pod-stale-verified",project:e.project,watcher:r}),c(`watcher CONFIRMED alive across the change (last build ${new Date(e.watcher?.lastBuildAt??0).toISOString()})`)):r==="degraded"?(console.log(`[gateway] stale source set: pod ${e.project} watcher DEGRADED (${e.watcher?.degraded}) \u2014 recycling.`),!0):r==="silent"?types_1.POD_WATCHER_SILENT==="keep"?c("watcher alive but quiet across the change \u2014 keeping it (LENSMCP_POD_WATCHER_SILENT=keep; a live watcher cannot miss an add it will need)"):!0:types_1.POD_WATCHER_UNVERIFIED==="keep"?c("unverifiable (no watcher testimony \u2014 older devserver bundle?) \u2014 keeping it (LENSMCP_POD_WATCHER_UNVERIFIED=keep)"):!0},"podStaleRecycleAllowed"),pe=i((e,o)=>{if(e.lastStaleRecycleAt=Date.now(),types_1.POD_RECYCLE_MODE==="rolling"&&e.proc){te(e,o);return}console.log(`[gateway] zombie recycle: ${e.project} (${o}) \u2014 stale fs view; fresh pod incoming`),h(e,"warning",`zombie recycle: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:o});const s=!!e.decl.eager;F(e,`${o} \u2014 recycling stale pod`,{announce:!1}),s&&setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&D(e)},1500).unref?.()},"recyclePod"),ue=i(()=>{const e=setInterval(()=>{he().catch(o=>console.error("[gateway] sweep error:",o))},t.sweepMs);return e.unref(),e},"startSweeper"),he=i(async()=>{{if(t.stopped())return;types_1.POD_RECYCLE_ENABLED&&le();for(const e of t.services){if(t.stopped()||!e.proc){e.wedgedSince=void 0;continue}const o=e.decl.idleKillSec;if(o&&e.inflight===0&&Date.now()-e.lastUsed>o*1e3){F(e,`idle ${o}s`),e.wedgedSince=void 0;continue}const s=e.state==="starting"&&e.spawnAt!==void 0&&Date.now()-e.spawnAt<t.startupTimeoutMs,r=T(e.pool)>0;if(r||s){if(e.wedgedSince=void 0,r&&P(e,"observed"),r&&!s&&types_1.POD_RECYCLE_ENABLED){const c=t.sourceSetChangedFor?.get(e.project)??t.sourceSetChangedAt??0;await q(e);const d=(e.staleVerifiedAt??0)>=c&&c>0,p=podRecycleReason(e,Date.now(),d?0:c,ce);if(p==="stale-source-set"&&!de(e,c))continue;p&&pe(e,p)}continue}e.wedgedSince??=Date.now(),Date.now()-e.wedgedSince>types_1.WEDGE_RECYCLE_GRACE_MS&&(e.wedgedSince=void 0,ne(e))}}},"sweepOnce");return{scanNow:T,spawnService:D,ensureUp:re,scaleUp:se,killSpawned:F,markUp:P,startSweeper:ue,rollPod:te}}v(createServiceLayer,"createServiceLayer"),i(createServiceLayer,"createServiceLayer");function chooseStalePodKillTarget(t,n,a){if(!(t===void 0||!Number.isInteger(t)||t<=1)&&!(n!==void 0&&n!==t)&&t!==a)return t}v(chooseStalePodKillTarget,"chooseStalePodKillTarget"),i(chooseStalePodKillTarget,"chooseStalePodKillTarget");
@@ -15,13 +15,44 @@
15
15
  *
16
16
  * So the line is de-coloured FIRST and matched after, and the patterns are written against
17
17
  * what the tools actually print.
18
+ *
19
+ * ## A fault is closed by evidence about the same thing
20
+ *
21
+ * "Broken" was reported and "healthy again" never was: an error line became a `service-error`
22
+ * event, a healthy line lowered the gateway's own flag and told nobody. A pod that hot-swaps
23
+ * never restarts, so nothing else told the lens either. Measured (foodguard, 2026-10-06): a
24
+ * type error that existed for five minutes left six services reading "failed to build" an hour
25
+ * later, each with seven clean checks behind it.
26
+ *
27
+ * And the healthy lines are not interchangeable. In that same log the type error is followed,
28
+ * a line later, by the NEXT build's `webpack 5.108.4 compiled successfully in 102 ms` — true of
29
+ * the bundle, silent about the types. So each line names WHICH fault it speaks to, and
30
+ * {@link PodFaults} closes a fault only on a line about that fault.
18
31
  */
32
+ /** What a pod line can say is broken. */
33
+ export type PodFault = 'build' | 'types' | 'process';
19
34
  export interface PodLine {
20
35
  /** The line with colour codes removed and whitespace trimmed — what gets reported. */
21
36
  clean: string;
22
37
  /** The build or the process is broken. */
23
38
  broken: boolean;
24
- /** The build is healthy again. */
39
+ /** Something is healthy again — see `closes` for what. */
25
40
  healthy: boolean;
41
+ /** What this line says is broken. */
42
+ fault?: PodFault;
43
+ /** The faults this line is evidence AGAINST. */
44
+ closes: readonly PodFault[];
26
45
  }
27
46
  export declare function classifyPodLine(raw: string): PodLine;
47
+ /**
48
+ * The faults a pod has open, as REPORTED to the lens.
49
+ *
50
+ * Deliberately separate from the gateway's `buildFailed`: that flag decides whether a dead pod
51
+ * is respawned and how long a cold start waits, any healthy line lowers it, and an advisory
52
+ * type error must not hold it up. This one decides what the lens goes on believing.
53
+ */
54
+ export declare class PodFaults {
55
+ private readonly open;
56
+ /** Fold one line. True when it closed the LAST open fault — the edge worth announcing. */
57
+ observe(line: PodLine): boolean;
58
+ }
@@ -1 +1 @@
1
- "use strict";var s=Object.defineProperty;var n=(r,e)=>s(r,"name",{value:e,configurable:!0});var t=Object.defineProperty,o=n((r,e)=>t(r,"name",{value:e,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.classifyPodLine=classifyPodLine;const ANSI_SGR=/\x1b\[[0-9;]*m/g,BUILD_BROKEN=/ERROR in |Module not found|error TS\d+|Found \d+ errors?|Unhandled 'error' event|EADDRINUSE/,BUILD_HEALTHY=/No typescript errors found|compiled successfully|Child#\d+ ready/;function classifyPodLine(r){const e=r.replace(ANSI_SGR,"").trim();return{clean:e,broken:BUILD_BROKEN.test(e),healthy:BUILD_HEALTHY.test(e)}}n(classifyPodLine,"classifyPodLine"),o(classifyPodLine,"classifyPodLine");
1
+ "use strict";var l=Object.defineProperty;var n=(s,e)=>l(s,"name",{value:e,configurable:!0});var d=Object.defineProperty,r=n((s,e)=>d(s,"name",{value:e,configurable:!0}),"r");Object.defineProperty(exports,"__esModule",{value:!0}),exports.PodFaults=void 0,exports.classifyPodLine=classifyPodLine;const ANSI_SGR=/\x1b\[[0-9;]*m/g,OPENS=[["build",/ERROR in |Module not found/],["types",/error TS\d+|Found [1-9]\d* errors?/],["process",/Unhandled 'error' event|EADDRINUSE/]],CLOSES=[["build",/compiled successfully/],["types",/No typescript errors found|Found 0 errors|\[tsgo\] .+: types clean/],["process",/Child#\d+ (ready|hot-swapped)/]];function classifyPodLine(s){const e=s.replace(ANSI_SGR,"").trim(),t=OPENS.find(([,o])=>o.test(e))?.[0],i=t?[]:CLOSES.filter(([,o])=>o.test(e)).map(([o])=>o);return{clean:e,broken:t!==void 0,healthy:i.length>0,...t?{fault:t}:{},closes:i}}n(classifyPodLine,"classifyPodLine"),r(classifyPodLine,"classifyPodLine");class PodFaults{static{n(this,"PodFaults")}static{r(this,"PodFaults")}constructor(){this.open=new Set}observe(e){if(e.fault)return this.open.add(e.fault),!1;if(this.open.size===0)return!1;for(const t of e.closes)this.open.delete(t);return this.open.size===0}}exports.PodFaults=PodFaults;
@@ -13,6 +13,7 @@ import type { GatewayExecutorSchema } from '../schema';
13
13
  import type { AuthPolicy, Attrs } from '../manifest';
14
14
  import type { GatewayErrorReason } from '../gateway-errors';
15
15
  import type { RouteRegistry } from './route-registry';
16
+ import type { PodFaults } from './pod-output';
16
17
  export type { Attrs };
17
18
  export interface GatewayContext {
18
19
  root: string;
@@ -230,6 +231,9 @@ export interface ServiceCtl {
230
231
  spawnAt?: number;
231
232
  lastErrors?: string[];
232
233
  buildFailed?: boolean;
234
+ /** The faults REPORTED to the lens and not yet taken back — what decides when `service-recovered` is
235
+ * announced. Not `buildFailed`, which is the gateway's own control flag (pod-output.ts). */
236
+ faults?: PodFaults;
233
237
  /** Throttle state for the 'service error' event: the last-emitted error signature + when. Stops a
234
238
  * crash-looping pod (re-printing the same error line every restart) from flooding the event bus —
235
239
  * we re-emit only when the error line CHANGES or after {@link SERVICE_ERROR_THROTTLE_MS}. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/cluster",
3
- "version": "1.23.8",
3
+ "version": "1.23.9",
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",
@@ -57,9 +57,9 @@
57
57
  }
58
58
  },
59
59
  "dependencies": {
60
- "@lensmcp/core": "1.23.8",
61
- "@lensmcp/node-instrumentation": "1.23.8",
62
- "@lensmcp/nx-plugin": "1.23.8",
60
+ "@lensmcp/core": "1.23.9",
61
+ "@lensmcp/node-instrumentation": "1.23.9",
62
+ "@lensmcp/nx-plugin": "1.23.9",
63
63
  "fastify": "^5.8.5",
64
64
  "@fastify/reply-from": "^12.6.2",
65
65
  "@typescript/native-preview": "^7.0.0-dev.20260707.2",
@@ -11,6 +11,9 @@
11
11
  * lines to the executor's console, which the gateway sniffs (`error TS\d+`) into the lens
12
12
  * "service error" signal — the same wire the fork-ts-checker fed. Advisory like
13
13
  * `typeCheckOptions.async: true`: a type error never fails or blocks the webpack build.
14
+ * The CLEAN line is the other half of that contract: `[tsgo] <app>: types clean` is the only
15
+ * thing that takes the signal back (webpack's `compiled successfully` is about the bundle), so
16
+ * the gateway matches it too (`pod-output.ts`).
14
17
  *
15
18
  * SHIM tsconfig: tsgo is TS7 — `moduleResolution: node10` (what node-app tsconfigs often pin) is
16
19
  * REMOVED there, so the check runs against a generated shim extending the app tsconfig with