@lensmcp/cluster 1.17.4 → 1.18.1

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.
@@ -44,6 +44,12 @@ export interface LensChildren {
44
44
  * everything"): spawn each app's vite (+ capture) in the workspace's OWN root + event bus, on a
45
45
  * collision-safe port, and point its route target at that port. Returns a reaper for unregister. */
46
46
  hostWorkspaceApps(fragment: WorkspaceFragment): Promise<() => void>;
47
+ /** ONE liveness round over every tracked lens-frontend vite: TCP-probe its pinned port, emit the
48
+ * RECOVERY edge (`service-up`) the first time it accepts, and — past the spawn grace and with the
49
+ * probe enabled — the wedge verdict + SIGKILL. The sweeper calls this on a timer; it is exposed
50
+ * (like `ServiceLayer.scanNow`) so the transitions are testable without a 15-second interval.
51
+ * Concurrency-guarded: a call while a round is in flight returns immediately. */
52
+ probeLensFrontends(): Promise<void>;
47
53
  /** Reap all managed children (called from `stop()`): SIGTERM, a grace window, then SIGKILL any survivor
48
54
  * (vite ignores SIGTERM → would orphan when the daemon exits). Async so `stop()` awaits the escalation. */
49
55
  reapAll(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"lens-children.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lens-children.ts"],"names":[],"mappings":"AASA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,kCAAkC,CAAC;AAI1C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAuB,MAAM,SAAS,CAAC;AAiB1F,OAAO,EAA0B,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AA0B7E,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAalE;AAyBD;;;;;;mDAMmD;AACnD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,SAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAG1F;AAMD;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GAClF,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED;;;iFAGiF;AACjF,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAQvE;AAED,MAAM,WAAW,YAAY;IAC3B,iBAAiB,EAAE,UAAU,CAAC;IAC9B;;gFAE4E;IAC5E,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC;;6GAEyG;IACzG,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC;IACvF;;yGAEqG;IACrG,iBAAiB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC;IACpE;gHAC4G;IAC5G,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,cAAc,EAClB,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,qBAAqB,EAE9B,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,GACzD,YAAY,CA4bd"}
1
+ {"version":3,"file":"lens-children.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lens-children.ts"],"names":[],"mappings":"AASA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,kCAAkC,CAAC;AAI1C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAuB,MAAM,SAAS,CAAC;AAiB1F,OAAO,EAA0B,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AA0B7E,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAalE;AAyBD;;;;;;mDAMmD;AACnD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,SAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAG1F;AAMD;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GAClF,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED;;;iFAGiF;AACjF,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAQvE;AAED,MAAM,WAAW,YAAY;IAC3B,iBAAiB,EAAE,UAAU,CAAC;IAC9B;;gFAE4E;IAC5E,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC;;6GAEyG;IACzG,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC;IACvF;;yGAEqG;IACrG,iBAAiB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC;IACpE;;;;sFAIkF;IAClF,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC;gHAC4G;IAC5G,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,cAAc,EAClB,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,qBAAqB,EAE9B,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,GACzD,YAAY,CAwfd"}
@@ -145,11 +145,6 @@ deps) {
145
145
  // `portConflicts` counts CONSECUTIVE lost bind races (EADDRINUSE) — forgiven rather than charged to
146
146
  // `attempts`, but capped so a permanently-squatted port still falls back to backoff.
147
147
  const lensHeal = new Map();
148
- // Zombie recycle + LIVENESS probe: the live `lens:true` app VITE children (label 'vite') + when each
149
- // spawned, its pinned `port`, and its run of consecutive probe failures. `port`/`probeFails` drive the
150
- // wedge probe (alive but NOT listening); `spawnedAt` also drives the stale-source-set recycle. Both reset
151
- // naturally on a respawn, which registers a FRESH handle. The capture sidecar + the dashboard/MCP
152
- // singletons are NOT tracked (they don't serve app source).
153
148
  const viteChildren = new Map();
154
149
  // Per-project last-recycle time — the cooldown survives the kill→auto-heal→respawn (a new child handle).
155
150
  const viteRecycleAt = new Map();
@@ -210,8 +205,20 @@ deps) {
210
205
  // The port THIS child owns (pinned `--port <n> --strictPort`): what the liveness probe checks and what
211
206
  // the respawn must wait to be free. `undefined` for the dashboard/MCP singletons + an unpinned vite.
212
207
  const ownPort = isVite ? viteArgPort(args) : undefined;
213
- if (isVite)
214
- viteChildren.set(child, { project: env?.['LENSMCP_PROJECT'] ?? 'app', spawnedAt, port: ownPort, probeFails: 0 });
208
+ if (isVite) {
209
+ // `spawnLensFrontend` always stamps the OWNING workspace's bus into the child env, so this is
210
+ // the authoritative "whose app is this" — a registered workspace's lifecycle events go to its
211
+ // own `.lensmcp/events.jsonl`, exactly as the service layer's `emitSvc` routes pod events.
212
+ const childBus = typeof env?.['LENSMCP_EVENT_FILE'] === 'string' ? env['LENSMCP_EVENT_FILE'] : undefined;
213
+ viteChildren.set(child, {
214
+ project: env?.['LENSMCP_PROJECT'] ?? 'app',
215
+ spawnedAt,
216
+ port: ownPort,
217
+ probeFails: 0,
218
+ listening: false,
219
+ ...(childBus && childBus !== rt.eventFile ? { bus: childBus } : {}),
220
+ });
221
+ }
215
222
  child.on('exit', (code, sig) => {
216
223
  const idx = lensChildren.indexOf(child);
217
224
  if (idx >= 0)
@@ -269,7 +276,7 @@ deps) {
269
276
  // 502ing forever. The kill lands on the proven exit→auto-heal path, which respawns after waiting the port
270
277
  // out. `probeInFlight` keeps a slow round from overlapping the next tick.
271
278
  let probeInFlight = false;
272
- const probeLensChildren = async (cfg) => {
279
+ const probeLensChildren = async (cfg, killWedged) => {
273
280
  if (probeInFlight)
274
281
  return;
275
282
  probeInFlight = true;
@@ -281,22 +288,39 @@ deps) {
281
288
  continue; // unpinned port ⇒ nothing to probe
282
289
  if (child.exitCode !== null || child.signalCode !== null)
283
290
  continue; // already dying — the exit heal owns it
284
- if (Date.now() - meta.spawnedAt < cfg.graceMs) {
285
- meta.probeFails = 0;
286
- continue;
287
- } // still booting
291
+ // The probe now runs DURING the spawn grace too. It costs one extra TCP connect per child
292
+ // for the first `graceMs`, and it buys the RECOVERY edge below at first-listen instead of
293
+ // 45s later — the grace exists to withhold the KILL verdict from a booting vite, not to
294
+ // withhold the observation that it came up.
288
295
  if (await probePortListening(meta.port, types_1.LENS_PROBE_TIMEOUT_MS)) {
289
296
  meta.probeFails = 0;
297
+ // RECOVERY EDGE. A wedged/zombie-recycled vite is SIGKILLed and auto-healed by the exit
298
+ // handler, which announces nothing — so `pod-recycle` (→ `wedged` in the lens fold) had no
299
+ // counterpart and a lens app stayed "wedged" for the rest of the session while serving
300
+ // every request (foodguard 2026-07-31: `wedged: ["authentication","dashboard"]`, `up: []`,
301
+ // zero 502s). An accepting listener is the same evidence the wedge verdict is the absence
302
+ // of, so it is what clears it. Once per generation: a fresh child starts `listening: false`.
303
+ if (!meta.listening) {
304
+ meta.listening = true;
305
+ emit('info', `lens app up: ${meta.project} (vite :${meta.port} listening)`, `cluster-lens-app:${meta.project}`, { kind: 'service-up', project: meta.project, port: meta.port }, undefined, meta.bus);
306
+ }
290
307
  continue;
291
308
  }
309
+ if (Date.now() - meta.spawnedAt < cfg.graceMs) {
310
+ meta.probeFails = 0;
311
+ continue;
312
+ } // still booting
313
+ if (!killWedged)
314
+ continue; // liveness still OBSERVED above; only the kill verdict is switched off
292
315
  meta.probeFails += 1;
293
316
  const reason = (0, lifecycle_1.lensChildWedged)(meta, viteRecycleAt.get(meta.project) ?? 0, Date.now(), cfg);
294
317
  if (!reason)
295
318
  continue; // failing, but not yet N-in-a-row / still in cooldown
296
319
  viteRecycleAt.set(meta.project, Date.now());
297
320
  meta.probeFails = 0;
321
+ meta.listening = false; // re-arm the recovery edge, in case the SIGKILL below doesn't take
298
322
  console.error(`[gateway] wedge recycle: lens app ${meta.project} is ALIVE but NOT LISTENING on :${meta.port} after ${Math.max(2, cfg.failureThreshold)} probes — SIGKILL; a fresh vite is coming.`);
299
- emit('warning', `wedge recycle: ${meta.project} (${reason})`, `cluster-lens-app:${meta.project}`, { kind: 'pod-recycle', project: meta.project, reason, port: meta.port });
323
+ emit('warning', `wedge recycle: ${meta.project} (${reason})`, `cluster-lens-app:${meta.project}`, { kind: 'pod-recycle', project: meta.project, reason, port: meta.port }, undefined, meta.bus);
300
324
  try {
301
325
  child.kill('SIGKILL');
302
326
  }
@@ -321,14 +345,15 @@ deps) {
321
345
  if (lensSweeper || (!types_1.POD_RECYCLE_ENABLED && !types_1.LENS_PROBE_ENABLED))
322
346
  return;
323
347
  const cfg = { graceMs: types_1.POD_STALE_GRACE_MS, settleMs: types_1.POD_RECYCLE_SETTLE_MS, cooldownMs: types_1.POD_STALE_RECYCLE_COOLDOWN_MS };
324
- const probeCfg = { graceMs: types_1.LENS_PROBE_GRACE_MS, failureThreshold: types_1.LENS_PROBE_FAILS, cooldownMs: types_1.LENS_PROBE_COOLDOWN_MS };
325
348
  lensSweeper = setInterval(() => {
326
349
  if (rt.stopped() || viteChildren.size === 0)
327
350
  return;
328
351
  // The liveness probe is INDEPENDENT of the source-set clock — a wedge has nothing to do with the file
329
- // set, so it must run before (and regardless of) the staleness early-return below.
330
- if (types_1.LENS_PROBE_ENABLED)
331
- void probeLensChildren(probeCfg);
352
+ // set, so it must run before (and regardless of) the staleness early-return below. It runs whenever
353
+ // the sweeper does: `LENS_PROBE_ENABLED` gates the KILL verdict, NOT the observation, because the
354
+ // probe is also the only RECOVERY signal a lens frontend has — with the staleness recycle on and the
355
+ // probe off, a zombie-recycled app would otherwise stay `wedged` in the fold forever.
356
+ void probeLensFrontends();
332
357
  if (!types_1.POD_RECYCLE_ENABLED)
333
358
  return;
334
359
  const wideChangedAt = rt.sourceSetChangedAt ?? 0;
@@ -343,8 +368,9 @@ deps) {
343
368
  if (!(0, lifecycle_1.lensFrontendStale)(meta.spawnedAt, last, changedAt, now, cfg))
344
369
  continue;
345
370
  viteRecycleAt.set(meta.project, now);
371
+ meta.listening = false; // re-arm the recovery edge — the respawned vite must announce itself
346
372
  console.log(`[gateway] zombie recycle: lens app ${meta.project} (stale-source-set) — vite missed a file add; restarting it`);
347
- emit('warning', `zombie recycle: ${meta.project} (stale-source-set)`, `cluster-lens-app:${meta.project}`, { kind: 'pod-recycle', project: meta.project, reason: 'stale-source-set' });
373
+ emit('warning', `zombie recycle: ${meta.project} (stale-source-set)`, `cluster-lens-app:${meta.project}`, { kind: 'pod-recycle', project: meta.project, reason: 'stale-source-set' }, undefined, meta.bus);
348
374
  try {
349
375
  child.kill('SIGTERM');
350
376
  }
@@ -413,6 +439,13 @@ deps) {
413
439
  LENSMCP_EVENT_FILE: rt.eventFile,
414
440
  LENSMCP_TRANSPORT: process.env['LENSMCP_TRANSPORT'] ?? 'http',
415
441
  LENSMCP_PORT: String(mcpPort),
442
+ // Which workspace this server serves — it publishes this on its identity
443
+ // route so a shim can REFUSE it when two workspaces derive one port
444
+ // (rather than silently reading the other project's events). Passed
445
+ // explicitly because the child's cwd is ambient and inheritable, while
446
+ // this is the workspace the gateway actually resolved the port from.
447
+ LENSMCP_WORKSPACE_KEY: scope.key,
448
+ LENSMCP_WORKSPACE_ROOT: rt.root,
416
449
  });
417
450
  }
418
451
  }
@@ -562,7 +595,10 @@ deps) {
562
595
  labelPrefix,
563
596
  log: (l) => console.log(l),
564
597
  }, spawnInRoot);
565
- emit('info', `lens app started: ${fragment.wsKey}/${project} (vite :${port})`, `cluster-lens-app:${fragment.wsKey}:${project}`, { kind: 'lens-app-up', project, port });
598
+ // Onto the REGISTERED workspace's OWN bus (like the service layer's `emitSvc`): its cluster
599
+ // view must show its apps, and the daemon's must not — two workspaces can both own a
600
+ // project called `dashboard`, so a shared bus makes the fold ambiguous as well as wrong.
601
+ emit('info', `lens app started: ${fragment.wsKey}/${project} (vite :${port})`, `cluster-lens-app:${fragment.wsKey}:${project}`, { kind: 'lens-app-up', project, port }, undefined, eventFile);
566
602
  }
567
603
  catch (e) {
568
604
  console.error(`[gateway] lens app ${fragment.wsKey}/${project}: ${e.message}`);
@@ -610,5 +646,6 @@ deps) {
610
646
  }
611
647
  }
612
648
  };
613
- return { spawnManagedChild, bootSingletonsAndApps, hostWorkspaceDashboard, hostWorkspaceApps, reapAll };
649
+ const probeLensFrontends = () => probeLensChildren({ 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);
650
+ return { spawnManagedChild, bootSingletonsAndApps, hostWorkspaceDashboard, hostWorkspaceApps, probeLensFrontends, reapAll };
614
651
  }
@@ -116,7 +116,13 @@ export interface ServiceLayer {
116
116
  spawnService(svc: ServiceCtl): void;
117
117
  ensureUp(svc: ServiceCtl): Promise<boolean>;
118
118
  scaleUp(svc: ServiceCtl): void;
119
- killSpawned(svc: ServiceCtl, why: string): void;
119
+ killSpawned(svc: ServiceCtl, why: string, opts?: {
120
+ announce?: boolean;
121
+ }): void;
122
+ /** The RECOVERY edge: record + announce that this service is serving again.
123
+ * Idempotent (a no-op once `state === 'up'`), so the hot path can call it per
124
+ * request. Returns true only on the transition. See the implementation. */
125
+ markUp(svc: ServiceCtl, how: 'adopted' | 'observed'): boolean;
120
126
  /** Start the idle sweeper; returns the interval (already `.unref()`'d). */
121
127
  startSweeper(): NodeJS.Timeout;
122
128
  }
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lifecycle.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lifecycle.ts"],"names":[],"mappings":"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"}
@@ -452,7 +452,7 @@ function createServiceLayer(rt, obs) {
452
452
  setTimeout(() => {
453
453
  if (rt.stopped() || !svc.lockBlockedAt)
454
454
  return;
455
- killSpawned(svc, 'parked on a foreign nx task lock — recycling');
455
+ killSpawned(svc, 'parked on a foreign nx task lock — recycling', { announce: false }); // a recycle: `nx-lock-break` above is the report
456
456
  reapForeignRunners(svc, target);
457
457
  setTimeout(() => {
458
458
  if (!rt.stopped() && !svc.proc && svc.state === 'down')
@@ -498,12 +498,34 @@ function createServiceLayer(rt, obs) {
498
498
  });
499
499
  svc.proc = proc;
500
500
  };
501
+ /**
502
+ * The RECOVERY edge — the counterpart to `service-down`/`service-recycle`/`pod-recycle`.
503
+ *
504
+ * The gateway used to announce a service going UP from exactly two places, both inside
505
+ * `ensureUp`: the adopt fast-path and the cold-start wait loop. But the proxy only calls
506
+ * `ensureUp` when `pickSock` came back EMPTY — so a service that self-healed on its own (the
507
+ * exit auto-heal respawn, a recycled pod re-binding, an externally-started devserver) was never
508
+ * announced. Its `svc.state` stayed `'starting'` and the lens fold behind `process://cluster`
509
+ * latched on the last DEGRADATION forever: `agent://current-status` reported `failing` while the
510
+ * app served every request (foodguard 2026-07-31 — four services `starting`, `up: []`, 20×200,
511
+ * 0×502).
512
+ *
513
+ * So the transition lives here, callable from every place that observes a live pod, and the
514
+ * evidence is the SAME socket scan the wedge detector uses (`live = scanNow(pool) > 0`) — the
515
+ * detector that clears `wedgedSince` must also be the one that records `up`, or the two disagree.
516
+ * Idempotent, so the per-request call costs one property compare.
517
+ */
518
+ const markUp = (svc, how) => {
519
+ if (svc.state === 'up')
520
+ return false;
521
+ svc.state = 'up';
522
+ const pods = svc.pool.socks.length;
523
+ emitSvc(svc, 'info', `service up: ${svc.project} (${pods} pods, ${how})`, `cluster-service:${svc.project}`, { kind: 'service-up', project: svc.project, pods, adopted: how === 'adopted' });
524
+ return true;
525
+ };
501
526
  const ensureUp = async (svc) => {
502
527
  if (scanNow(svc.pool) > 0) {
503
- if (svc.state !== 'up') {
504
- emitSvc(svc, 'info', `service up: ${svc.project} (${svc.pool.socks.length} pods, adopted)`, `cluster-service:${svc.project}`, { kind: 'service-up', project: svc.project, pods: svc.pool.socks.length, adopted: true });
505
- }
506
- svc.state = 'up';
528
+ markUp(svc, 'adopted');
507
529
  return true;
508
530
  }
509
531
  if (rt.stopped())
@@ -549,11 +571,21 @@ function createServiceLayer(rt, obs) {
549
571
  emitSvc(svc, 'info', `autoscale: ${svc.project} ${cur} → ${cur + 1} pods`, `cluster-service:${svc.project}`, { kind: 'autoscale', project: svc.project, from: cur, to: cur + 1, inflight: svc.inflight });
550
572
  };
551
573
  // Idle sweeper: kill gateway-spawned lazy services after idleKillSec.
552
- const killSpawned = (svc, why) => {
574
+ //
575
+ // `announce: false` for a caller that is TEARING THE TREE DOWN AS PART OF A RECYCLE. This function
576
+ // is the shared teardown for two different intents — "park this service, it's idle" and "kill this
577
+ // service, it's broken" — and the `idle-kill` event only describes the first. A recycle caller has
578
+ // ALREADY announced the real reason (`service-recycle` / `pod-recycle` / `service-crash-loop`), and
579
+ // an `idle-kill` on top would OVERWRITE that fault in the lens fold with a benign `idle` — the
580
+ // crash-loop and wedge reports would vanish milliseconds after they were raised. It also read wrong
581
+ // in the log ("idle kill: auth (wedged: 0 live pods — recycling)").
582
+ const killSpawned = (svc, why, opts) => {
553
583
  if (!svc.proc)
554
584
  return;
555
585
  console.log(`[gateway] ${svc.project} ${why} — scaling to zero.`);
556
- emitSvc(svc, 'info', `idle kill: ${svc.project} (${why})`, `cluster-service:${svc.project}`, { kind: 'idle-kill', project: svc.project, why });
586
+ if (opts?.announce !== false) {
587
+ emitSvc(svc, 'info', `idle kill: ${svc.project} (${why})`, `cluster-service:${svc.project}`, { kind: 'idle-kill', project: svc.project, why });
588
+ }
557
589
  const dying = svc.proc;
558
590
  // Detach the corpse's handlers BEFORE killing: a dying webpack/nx flushes
559
591
  // buffered stdout/stderr as it exits, and those `sniff` callbacks would
@@ -623,13 +655,13 @@ function createServiceLayer(rt, obs) {
623
655
  svc.restarts = (svc.restarts ?? []).filter((t) => now - t < types_1.SERVICE_RESTART_WINDOW_MS);
624
656
  if (svc.restarts.length >= types_1.SERVICE_MAX_RESTARTS) {
625
657
  emitSvc(svc, 'error', `service crash-looping: ${svc.project} — wedged ${types_1.SERVICE_MAX_RESTARTS}+×/min, not auto-recycling (the next request retries)`, `cluster-service:${svc.project}`, { kind: 'service-crash-loop', project: svc.project });
626
- killSpawned(svc, 'wedged (crash-loop cap) — leaving down for the next request'); // svc.proc cleared → scale-from-zero can retry
658
+ killSpawned(svc, 'wedged (crash-loop cap) — leaving down for the next request', { announce: false }); // svc.proc cleared → scale-from-zero can retry
627
659
  return;
628
660
  }
629
661
  svc.restarts.push(now);
630
662
  console.log(`[gateway] self-heal: recycling WEDGED ${svc.project} (live parent, 0 live pods; ${svc.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`);
631
663
  emitSvc(svc, 'warning', `self-heal: recycling wedged ${svc.project} (0 live pods)`, `cluster-service:${svc.project}`, { kind: 'service-recycle', project: svc.project, attempt: svc.restarts.length });
632
- killSpawned(svc, 'wedged: 0 live pods — recycling'); // kills the tree + unlinks stale socks (+1200ms) + svc.proc=undefined + state='down'
664
+ killSpawned(svc, 'wedged: 0 live pods — recycling', { announce: false }); // kills the tree + unlinks stale socks (+1200ms) + svc.proc=undefined + state='down'
633
665
  // A wedge is unexpected — bring it straight back (don't wait for a request). Respawn AFTER killSpawned's
634
666
  // deferred sock cleanup (1200ms) so the fresh child binds a clean socket path.
635
667
  setTimeout(() => {
@@ -748,7 +780,7 @@ function createServiceLayer(rt, obs) {
748
780
  console.log(`[gateway] zombie recycle: ${svc.project} (${reason}) — stale fs view; fresh pod incoming`);
749
781
  emitSvc(svc, 'warning', `zombie recycle: ${svc.project} (${reason})`, `cluster-service:${svc.project}`, { kind: 'pod-recycle', project: svc.project, reason });
750
782
  const eager = !!svc.decl.eager;
751
- killSpawned(svc, `${reason} — recycling stale pod`); // clears proc, state='down', unlinks socks (+1200ms)
783
+ killSpawned(svc, `${reason} — recycling stale pod`, { announce: false }); // clears proc, state='down', unlinks socks (+1200ms)
752
784
  if (eager)
753
785
  setTimeout(() => { if (!rt.stopped() && !svc.proc && svc.state === 'down')
754
786
  spawnService(svc); }, 1500).unref?.();
@@ -776,6 +808,13 @@ function createServiceLayer(rt, obs) {
776
808
  const live = scanNow(svc.pool) > 0;
777
809
  if (live || coldStarting) {
778
810
  svc.wedgedSince = undefined;
811
+ // RECOVERY EDGE. `live` is the exact evidence that just cleared `wedgedSince`, so it must
812
+ // also record the service as UP — otherwise the gateway silently forgives a wedge while
813
+ // the lens fold stays latched on it. This is the path that heals a service which came
814
+ // back WITHOUT a request (the exit auto-heal respawn), since nothing else calls `ensureUp`
815
+ // once a pod socket exists. Idempotent: after the first tick it is a property compare.
816
+ if (live)
817
+ markUp(svc, 'observed');
779
818
  // ZOMBIE recycle: a live pod (live parent + live socket, answers requests) whose fs view went
780
819
  // stale — neither exit-respawn nor wedge-recycle sees it. Idle-gated in podRecycleReason so a busy
781
820
  // pod is never interrupted.
@@ -799,5 +838,5 @@ function createServiceLayer(rt, obs) {
799
838
  sweeper.unref();
800
839
  return sweeper;
801
840
  };
802
- return { scanNow, spawnService, ensureUp, scaleUp, killSpawned, startSweeper };
841
+ return { scanNow, spawnService, ensureUp, scaleUp, killSpawned, markUp, startSweeper };
803
842
  }
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/proxy.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,KAAK,KAAK,MAAM,MAAM,aAAa,CAAC;AAG3C,OAAO,KAAK,EAAE,cAAc,EAAW,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA8BhD,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IACxH,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IACnI,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,KAAK,IAAI,GAAG,IAAI,CAAC;IAC/H;kGAC8F;IAC9F,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,KAAK,IAAI,GAAG,IAAI,CAAC;IAC7H,KAAK,CAAC,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1F,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC,KAAK,GAAG,OAAO,YAAY,EAAE,KAAK,CAAC;IACzE,aAAa,IAAI,IAAI,CAAC;IACtB,UAAU,IAAI,IAAI,CAAC;CACpB;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,GAAG,UAAU,CAuYtG"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/proxy.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,KAAK,KAAK,MAAM,MAAM,aAAa,CAAC;AAG3C,OAAO,KAAK,EAAE,cAAc,EAAW,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA8BhD,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IACxH,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IACnI,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,KAAK,IAAI,GAAG,IAAI,CAAC;IAC/H;kGAC8F;IAC9F,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,KAAK,IAAI,GAAG,IAAI,CAAC;IAC7H,KAAK,CAAC,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1F,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC,KAAK,GAAG,OAAO,YAAY,EAAE,KAAK,CAAC;IACzE,aAAa,IAAI,IAAI,CAAC;IACtB,UAAU,IAAI,IAAI,CAAC;CACpB;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,GAAG,UAAU,CA8YtG"}
@@ -256,6 +256,12 @@ function createProxy(rt, obs, svcLayer) {
256
256
  if (await svcLayer.ensureUp(ctl))
257
257
  sock = (0, discovery_1.pickSock)(route.pool, rt.scanTtlMs);
258
258
  }
259
+ // A ROUTABLE pod is the recovery edge (see `markUp` in lifecycle.ts): `ensureUp` — which used
260
+ // to be the only announcer of a service going up — is SKIPPED whenever `pickSock` already
261
+ // found a socket, so a service that self-healed between requests was never announced and the
262
+ // lens fold latched on its last degradation. Idempotent ⇒ a property compare per request.
263
+ if (ctl && sock)
264
+ svcLayer.markUp(ctl, 'observed');
259
265
  if (ctl)
260
266
  svcLayer.scaleUp(ctl);
261
267
  if (!sock) {
@@ -358,6 +364,9 @@ function createProxy(rt, obs, svcLayer) {
358
364
  if (await svcLayer.ensureUp(ctl))
359
365
  sock = (0, discovery_1.pickSock)(route.pool, rt.scanTtlMs);
360
366
  }
367
+ // The recovery edge — see the twin comment in `forwardNative` above.
368
+ if (ctl && sock)
369
+ svcLayer.markUp(ctl, 'observed');
361
370
  if (ctl)
362
371
  svcLayer.scaleUp(ctl);
363
372
  if (!sock) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/cluster",
3
- "version": "1.17.4",
3
+ "version": "1.18.1",
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,8 +65,8 @@
65
65
  }
66
66
  },
67
67
  "dependencies": {
68
- "@lensmcp/node-instrumentation": "1.17.4",
69
- "@lensmcp/nx-plugin": "1.17.4",
68
+ "@lensmcp/node-instrumentation": "1.18.1",
69
+ "@lensmcp/nx-plugin": "1.18.1",
70
70
  "@typescript/native-preview": "^7.0.0-dev.20260707.2",
71
71
  "fork-ts-checker-webpack-plugin": "^9.0.0",
72
72
  "glob": "^11.0.0",