instar 1.3.641 → 1.3.643

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.
Files changed (54) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +160 -6
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  5. package/dist/config/ConfigDefaults.js +29 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/core/InFlightSyncOpMarker.d.ts +29 -0
  8. package/dist/core/InFlightSyncOpMarker.d.ts.map +1 -0
  9. package/dist/core/InFlightSyncOpMarker.js +149 -0
  10. package/dist/core/InFlightSyncOpMarker.js.map +1 -0
  11. package/dist/core/PostUpdateMigrator.d.ts +16 -0
  12. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  13. package/dist/core/PostUpdateMigrator.js +91 -0
  14. package/dist/core/PostUpdateMigrator.js.map +1 -1
  15. package/dist/core/SessionManager.d.ts +146 -1
  16. package/dist/core/SessionManager.d.ts.map +1 -1
  17. package/dist/core/SessionManager.js +498 -144
  18. package/dist/core/SessionManager.js.map +1 -1
  19. package/dist/core/SleepWakeDetector.d.ts +33 -0
  20. package/dist/core/SleepWakeDetector.d.ts.map +1 -1
  21. package/dist/core/SleepWakeDetector.js +53 -0
  22. package/dist/core/SleepWakeDetector.js.map +1 -1
  23. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  24. package/dist/core/devGatedFeatures.js +23 -0
  25. package/dist/core/devGatedFeatures.js.map +1 -1
  26. package/dist/core/types.d.ts +52 -0
  27. package/dist/core/types.d.ts.map +1 -1
  28. package/dist/core/types.js.map +1 -1
  29. package/dist/lifeline/ServerSupervisor.d.ts +42 -0
  30. package/dist/lifeline/ServerSupervisor.d.ts.map +1 -1
  31. package/dist/lifeline/ServerSupervisor.js +79 -4
  32. package/dist/lifeline/ServerSupervisor.js.map +1 -1
  33. package/dist/lifeline/TelegramLifeline.d.ts.map +1 -1
  34. package/dist/lifeline/TelegramLifeline.js +8 -0
  35. package/dist/lifeline/TelegramLifeline.js.map +1 -1
  36. package/dist/monitoring/DegradedTmuxGuard.d.ts +161 -0
  37. package/dist/monitoring/DegradedTmuxGuard.d.ts.map +1 -0
  38. package/dist/monitoring/DegradedTmuxGuard.js +277 -0
  39. package/dist/monitoring/DegradedTmuxGuard.js.map +1 -0
  40. package/dist/monitoring/guardManifest.d.ts.map +1 -1
  41. package/dist/monitoring/guardManifest.js +18 -0
  42. package/dist/monitoring/guardManifest.js.map +1 -1
  43. package/dist/server/routes.d.ts.map +1 -1
  44. package/dist/server/routes.js +13 -5
  45. package/dist/server/routes.js.map +1 -1
  46. package/package.json +2 -2
  47. package/scripts/lint-no-direct-destructive.js +14 -0
  48. package/scripts/lint-sync-subprocess-chokepoint.js +208 -0
  49. package/scripts/sync-subprocess-chokepoint-baseline.json +148 -0
  50. package/src/data/builtin-manifest.json +65 -65
  51. package/upgrades/1.3.642.md +46 -0
  52. package/upgrades/1.3.643.md +66 -0
  53. package/upgrades/side-effects/false-excuse-deferral-stop-guard.md +75 -0
  54. package/upgrades/side-effects/tmux-event-loop-resilience.md +86 -0
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAoCH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAS3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAwBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAsH7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AA+EtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAy4CD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GACpD,IAAI,CA8eN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA+oftE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAwCH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAU3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAwBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAsH7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AA+EtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAy4CD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GACpD,IAAI,CA8eN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAmzftE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
@@ -7,13 +7,17 @@
7
7
  * When Telegram is configured, wires up message routing:
8
8
  * topic message → find/spawn session → inject message → session replies via [telegram:N]
9
9
  */
10
- import { execFileSync } from 'node:child_process';
10
+ import { execFile, execFileSync } from 'node:child_process';
11
11
  import fs from 'node:fs';
12
12
  import os from 'node:os';
13
13
  import path from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
+ import { promisify } from 'node:util';
15
16
  import pc from 'picocolors';
16
17
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+ // (A.5) wake-handler amplifier guard — the post-wake tmux re-validation is bounded
19
+ // async (9s + SIGKILL) instead of a sync execFileSync that can wedge the event loop.
20
+ const execFileAsync = promisify(execFile);
17
21
  import { loadConfig, ensureStateDir, detectTmuxPath, detectGeminiPath } from '../core/Config.js';
18
22
  import { handleProcessLevelError } from '../core/uncaughtExceptionPolicy.js';
19
23
  import { resolveDevAgentGate, resolveStateSyncStores } from '../core/devAgentGate.js';
@@ -30,6 +34,7 @@ import { gapBEligibleForTopic, recentUserMessageFromHistory, resolveGapBInjectio
30
34
  import { TopicProfileTransferCarrier, createTopicProfilePullHandler } from '../core/TopicProfileTransferCarrier.js';
31
35
  import { closeAllSqlite } from '../core/SqliteRegistry.js';
32
36
  import { SessionManager } from '../core/SessionManager.js';
37
+ import { configureSyncOpMarker, defaultInflightMarkerReader } from '../core/InFlightSyncOpMarker.js';
33
38
  import { StateManager } from '../core/StateManager.js';
34
39
  import { StuckInputSentinel } from '../core/StuckInputSentinel.js';
35
40
  import { SessionRecoveryChannel } from '../core/SessionRecoveryChannel.js';
@@ -4241,7 +4246,51 @@ export async function startServer(options) {
4241
4246
  ? { subscriptionReroutedLifetimeMinutes: subscriptionPathCfg.maxReroutedLifetimeMinutes }
4242
4247
  : {}),
4243
4248
  };
4244
- const sessionManager = new SessionManager(sessionManagerConfig, state);
4249
+ // ── (A)/(C) tmux event-loop resilience wiring (threaded through SessionManager opts) ──
4250
+ // SessionManager construction (here) PRECEDES the telegram/guardRegistry/sleepWakeDetector
4251
+ // wiring the (C) guard's notify path needs, and SessionManager takes the guard as a
4252
+ // readonly constructor arg — so the guard is built HERE with a LATE-BOUND raiseAttention
4253
+ // (a closure over a mutable holder), then the holder + the stall listener + the guard-
4254
+ // registry getter are wired later (the §C block before sleepWakeDetector.start()). When
4255
+ // the dev-gate resolves off, tmuxAsyncEnabled is false ⇒ SessionManager's tmux callsites
4256
+ // stay on today's sync byte-for-byte path (D6) and the guard never fires.
4257
+ const _tmuxAsyncEnabled = resolveDevAgentGate(config.monitoring?.tmuxResilience?.asyncHotPath?.enabled, config);
4258
+ const _degradedTmuxEnabled = resolveDevAgentGate(config.monitoring?.degradedTmuxGuard?.enabled, config);
4259
+ // Wire the cross-process marker MIRROR (the WRITER side, owned by this server process).
4260
+ // The stateDir here is byte-identical to the one TelegramLifeline passes to ServerSupervisor
4261
+ // (both resolve from loadConfig(projectDir).stateDir), so writer and cross-process reader
4262
+ // agree on <stateDir>/state/tmux-inflight-sync-op.json. This un-inerts BOTH amplifier #2
4263
+ // (ServerSupervisor.deferRestartForInflightSyncOp) AND the wake-handler marker short-circuit:
4264
+ // without it mirrorStateDir stays null, the mirror file is never written, and every
4265
+ // cross-process read returns null. callTimeoutMs keeps the marker stale-TTL tracking the
4266
+ // same per-call timeout SessionManager uses (so a tuned timeout doesn't make the marker
4267
+ // self-heal early). The mirror is signal-only and harmless when no flag reads it.
4268
+ configureSyncOpMarker({
4269
+ stateDir: config.stateDir,
4270
+ callTimeoutMs: config.monitoring?.tmuxResilience?.asyncHotPath?.timeoutMs,
4271
+ });
4272
+ // The late-bound notify sink — set in the §C wiring block once telegram exists. Until
4273
+ // then (and on a non-telegram install) it is a no-op, so an early episode is dropped
4274
+ // rather than thrown (signal-only).
4275
+ let _degradedTmuxRaise = null;
4276
+ const { DegradedTmuxGuard: _DegradedTmuxGuardCtor } = await import('../monitoring/DegradedTmuxGuard.js');
4277
+ // Cache the core count ONCE (stable for the process lifetime) — the guard's loadPerCore
4278
+ // provider is called on every (non-settle-window) evaluate() i.e. potentially N+ times per
4279
+ // monitor tick, and os.cpus() allocates a per-core array each call (a throughput smell on
4280
+ // the busy multi-agent host this guard targets). os.loadavg() is a cheap syscall; read only
4281
+ // loadavg[0]/cachedCores per call, matching sampleHostPressureInputs' loadPerCore exactly.
4282
+ const _degradedTmuxCores = os.cpus()?.length ?? 0;
4283
+ const degradedTmuxGuard = new _DegradedTmuxGuardCtor({ ...config.monitoring?.degradedTmuxGuard, enabled: _degradedTmuxEnabled }, {
4284
+ raiseAttention: (ep) => _degradedTmuxRaise?.(ep),
4285
+ loadPerCore: () => (_degradedTmuxCores > 0 ? os.loadavg()[0] / _degradedTmuxCores : 0),
4286
+ now: () => Date.now(),
4287
+ });
4288
+ const sessionManager = new SessionManager(sessionManagerConfig, state, {
4289
+ tmuxAsyncEnabled: _tmuxAsyncEnabled,
4290
+ degradedTmuxGuard,
4291
+ tmuxCallTimeoutMs: config.monitoring?.tmuxResilience?.asyncHotPath?.timeoutMs,
4292
+ tmuxMaxInFlight: config.monitoring?.tmuxResilience?.asyncHotPath?.maxInFlight,
4293
+ });
4245
4294
  // Wire the SAME TTL-cached SDK-credit reader PR1's routing policy uses, so
4246
4295
  // the reroute 'auto' decision and the intelligence-funnel routing share one
4247
4296
  // credit source and can't drift (june15-headless-spawn-reroute, PR2). Only
@@ -11286,6 +11335,11 @@ export async function startServer(options) {
11286
11335
  minWakeIntervalMs: sleepWakeCfg?.minWakeIntervalMs,
11287
11336
  recentDriftWindowMs: sleepWakeCfg?.recentDriftWindowMs,
11288
11337
  recentDriftLoadFloor: sleepWakeCfg?.recentDriftLoadFloor,
11338
+ // (B) in-flight-sync-op marker — when on, a ~0-CPU drift while a sync subprocess
11339
+ // op is in flight (depth>0, not stale) classifies as an event-loop BLOCK (emits
11340
+ // `stall`, suppresses the false wake) instead of a wake. developmentAgent gate:
11341
+ // LIVE on a dev agent / DARK on the fleet. Off ⇒ today's behavior byte-for-byte (D6).
11342
+ inFlightMarkerEnabled: resolveDevAgentGate(config.monitoring?.tmuxResilience?.inFlightMarker?.enabled, config),
11289
11343
  });
11290
11344
  // §P0 #9 (SE-8): give the scheduler's wake-reaper a cumulative-sleep view
11291
11345
  // (not the single last sleep event) so a job that spanned multiple sleeps
@@ -11293,8 +11347,33 @@ export async function startServer(options) {
11293
11347
  if (scheduler) {
11294
11348
  scheduler.setCumulativeSleepProvider((a, b) => sleepWakeDetector.getCumulativeSleepMsBetween(a, b));
11295
11349
  }
11350
+ // (A.5) wake-handler amplifier guard — resolved once. When LIVE, a spurious wake
11351
+ // produced by an in-flight sync subprocess op (an event-loop BLOCK, not a suspend)
11352
+ // is short-circuited at entry, and a low-confidence wake skips the heavy
11353
+ // tunnel/tmux recovery cascade — the recovery storm that piled more load on the host
11354
+ // and re-wedged it. developmentAgent gate: LIVE on a dev agent / DARK on the fleet;
11355
+ // when off the handler is byte-for-byte today's behavior (D6).
11356
+ const _tmuxAsyncHotPathWakeGuard = resolveDevAgentGate(config.monitoring?.tmuxResilience?.asyncHotPath?.enabled, config);
11296
11357
  sleepWakeDetector.on('wake', async (event) => {
11297
11358
  console.log(`[SleepWake] Wake detected after ~${event.sleepDurationSeconds}s sleep`);
11359
+ // (A.5) Marker short-circuit (residual guard): a sync subprocess op is in flight
11360
+ // RIGHT NOW (the cross-process mirror reports inFlight, not stale) → this "wake" is
11361
+ // an event-loop BLOCK, not a real suspend. (B) already classifies a depth>0 gap as a
11362
+ // `stall` (never a `wake`), so this is the belt-and-suspenders for the moderate-
11363
+ // confidence band: do the minimum and return before any recovery cascade.
11364
+ if (_tmuxAsyncHotPathWakeGuard) {
11365
+ try {
11366
+ const marker = defaultInflightMarkerReader(config.stateDir)();
11367
+ if (marker?.inFlight) {
11368
+ console.log(`[SleepWake] Wake fired while a sync subprocess op is in flight (ageMs=${marker.ageMs}) — ` +
11369
+ `event-loop BLOCK, not a suspend. Skipping recovery cascade.`);
11370
+ return;
11371
+ }
11372
+ }
11373
+ catch {
11374
+ /* @silent-fallback-ok — a marker-read failure falls through to the normal handler */
11375
+ }
11376
+ }
11298
11377
  // Checkpoint SQLite WAL files to flush stale locks from pre-sleep connections
11299
11378
  try {
11300
11379
  topicMemory?.checkpoint();
@@ -11311,13 +11390,30 @@ export async function startServer(options) {
11311
11390
  void _inboundQueue?.onWake(event.sleepDurationSeconds * 1000, event.lowConfidence ? 'low' : 'high');
11312
11391
  }
11313
11392
  catch { /* the backstop tick covers a missed wake trigger */ }
11314
- // Re-validate tmux sessions
11393
+ // Re-validate tmux sessions — bounded async (9s + SIGKILL) under the amplifier
11394
+ // guard so a wedged tmux can't block the event loop here; informational only, so a
11395
+ // timeout/reject is swallowed.
11396
+ // NOTE: a low-confidence-wake producer (which would let this skip the heavy
11397
+ // tunnel/tmux recovery cascade) is a FUTURE increment — Increment 1 covers the
11398
+ // event-loop-block case via the in-flight marker short-circuit above, so the dead
11399
+ // `event.lowConfidence` cascade-skip branch (no producer in SleepWakeDetector) was
11400
+ // removed rather than ship a guard that can never fire.
11315
11401
  try {
11316
11402
  const tmuxPath = detectTmuxPath();
11317
11403
  if (tmuxPath) {
11318
- const { execFileSync } = await import('child_process');
11319
- const result = execFileSync(tmuxPath, ['list-sessions'], { encoding: 'utf-8', timeout: 5000 }).trim();
11320
- console.log(`[SleepWake] tmux sessions after wake: ${result.split('\n').length}`);
11404
+ if (_tmuxAsyncHotPathWakeGuard) {
11405
+ const { stdout } = await execFileAsync(tmuxPath, ['list-sessions'], {
11406
+ encoding: 'utf-8',
11407
+ timeout: 9000,
11408
+ killSignal: 'SIGKILL',
11409
+ });
11410
+ console.log(`[SleepWake] tmux sessions after wake: ${stdout.trim().split('\n').length}`);
11411
+ }
11412
+ else {
11413
+ const { execFileSync } = await import('child_process');
11414
+ const result = execFileSync(tmuxPath, ['list-sessions'], { encoding: 'utf-8', timeout: 5000 }).trim();
11415
+ console.log(`[SleepWake] tmux sessions after wake: ${result.split('\n').length}`);
11416
+ }
11321
11417
  }
11322
11418
  }
11323
11419
  catch {
@@ -11403,6 +11499,64 @@ export async function startServer(options) {
11403
11499
  notify('DIGEST', 'system', `Machine woke up after ${mins > 60 ? `${Math.round(mins / 60)}h` : `${mins}m`} of sleep.${reapNote} Everything's reconnected and running.`);
11404
11500
  }
11405
11501
  });
11502
+ // ── (C) DegradedTmuxGuard — signal-only degraded-shared-tmux watcher (wiring) ──
11503
+ // The guard was CONSTRUCTED at the SessionManager site (it is a readonly constructor
11504
+ // dep of SessionManager, which precedes this block) with a late-bound raiseAttention.
11505
+ // Here we finish the wiring now that telegram/guardRegistry/sleepWakeDetector exist:
11506
+ // set the notify sink, attach the (B) `stall` listener, and register the runtime getter.
11507
+ // SIGNAL-ONLY: any actual tmux refresh is the operator's explicit Y/N; the guard NEVER
11508
+ // kills the shared socket (a kill-server would restart EVERY agent on the machine).
11509
+ // developmentAgent gate: LIVE on a dev agent / DARK on the fleet. Non-fatal wiring.
11510
+ try {
11511
+ // Register the runtime getter FIRST — before the notify-sink bind / stall-listener
11512
+ // attach — so a throw in that later wiring can't strip the getter and leave an
11513
+ // enabled-but-unregistered expectRuntime:true guard reporting phantom `missing` on
11514
+ // /guards. The getter only needs `degradedTmuxGuard`, which already exists from the
11515
+ // SessionManager-site construction.
11516
+ guardRegistry.register('monitoring.degradedTmuxGuard.enabled', () => degradedTmuxGuard.guardStatus());
11517
+ const _degradedTmuxMid = _meshSelfId ?? coordinator.identity?.machineId ?? 'local';
11518
+ // Bind the notify sink (was a no-op until telegram existed).
11519
+ _degradedTmuxRaise = telegram
11520
+ ? (ep) => {
11521
+ // Resolve the machine nickname from the pool registry (single-machine ⇒ the id).
11522
+ const nick = (_listPoolMachines?.() ?? []).find((m) => m.machineId === _degradedTmuxMid)?.nickname ??
11523
+ _degradedTmuxMid;
11524
+ const ageMin = Math.round(ep.ageMs / 60_000);
11525
+ void telegram.createAttentionItem({
11526
+ id: `degraded-tmux:${_degradedTmuxMid}:${ep.id}`,
11527
+ title: `Slow terminal server on "${nick}"`,
11528
+ summary: 'The shared terminal server has been slow to respond — sessions may feel sluggish. ' +
11529
+ 'Refreshing it (your call) usually clears it.',
11530
+ description: `The shared tmux server on ${nick} has been responding slowly ` +
11531
+ `(~${ep.ewmaMs}ms average over ${ep.consecutiveSlowCycles} corroborating cycles` +
11532
+ `${ageMin > 0 ? `, persisting ~${ageMin}m` : ''}). This can make every session feel sluggish. ` +
11533
+ 'Restarting the tmux server (an operator action) usually clears it — I never do that automatically ' +
11534
+ 'because it would restart every agent on this machine.',
11535
+ category: 'agent-health',
11536
+ priority: 'NORMAL',
11537
+ lane: 'agent-health',
11538
+ healthKey: `degraded-tmux:${_degradedTmuxMid}`,
11539
+ sourceContext: `degraded-tmux:${_degradedTmuxMid}`,
11540
+ });
11541
+ }
11542
+ : null;
11543
+ // (B) feed — a `stall` is one corroborating slow cycle. Attach BEFORE start() so no
11544
+ // emit is missed. Swallow any throw — a guard fault must never crash the detector.
11545
+ sleepWakeDetector.on('stall', (e) => {
11546
+ try {
11547
+ degradedTmuxGuard.onStall(e);
11548
+ }
11549
+ catch {
11550
+ /* @silent-fallback-ok — signal-only; a guard fault never crashes the detector */
11551
+ }
11552
+ });
11553
+ if (_degradedTmuxEnabled) {
11554
+ console.log(pc.green(' DegradedTmuxGuard enabled (degraded-shared-tmux watcher — signal-only)'));
11555
+ }
11556
+ }
11557
+ catch (e) {
11558
+ console.error(' DegradedTmuxGuard wiring failed (non-fatal):', e instanceof Error ? e.message : String(e));
11559
+ }
11406
11560
  sleepWakeDetector.start();
11407
11561
  // Project Map + Coherence Gate — spatial awareness and pre-action verification
11408
11562
  const projectMapper = new ProjectMapper({ projectDir: config.projectDir, stateDir: config.stateDir });