instar 1.3.490 → 1.3.492

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 (121) hide show
  1. package/dashboard/index.html +30 -0
  2. package/dist/commands/init.d.ts.map +1 -1
  3. package/dist/commands/init.js +3 -0
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/commands/server.d.ts.map +1 -1
  6. package/dist/commands/server.js +199 -1
  7. package/dist/commands/server.js.map +1 -1
  8. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  9. package/dist/config/ConfigDefaults.js +11 -0
  10. package/dist/config/ConfigDefaults.js.map +1 -1
  11. package/dist/core/CartographerSweepEngine.d.ts +55 -11
  12. package/dist/core/CartographerSweepEngine.d.ts.map +1 -1
  13. package/dist/core/CartographerSweepEngine.js +276 -95
  14. package/dist/core/CartographerSweepEngine.js.map +1 -1
  15. package/dist/core/CartographerTree.d.ts +77 -1
  16. package/dist/core/CartographerTree.d.ts.map +1 -1
  17. package/dist/core/CartographerTree.js +257 -5
  18. package/dist/core/CartographerTree.js.map +1 -1
  19. package/dist/core/GuardPostureStore.d.ts +38 -0
  20. package/dist/core/GuardPostureStore.d.ts.map +1 -0
  21. package/dist/core/GuardPostureStore.js +87 -0
  22. package/dist/core/GuardPostureStore.js.map +1 -0
  23. package/dist/core/MachinePoolRegistry.d.ts +16 -0
  24. package/dist/core/MachinePoolRegistry.d.ts.map +1 -1
  25. package/dist/core/MachinePoolRegistry.js +24 -1
  26. package/dist/core/MachinePoolRegistry.js.map +1 -1
  27. package/dist/core/PeerPresencePuller.d.ts +9 -0
  28. package/dist/core/PeerPresencePuller.d.ts.map +1 -1
  29. package/dist/core/PeerPresencePuller.js +1 -1
  30. package/dist/core/PeerPresencePuller.js.map +1 -1
  31. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  32. package/dist/core/PostUpdateMigrator.js +43 -2
  33. package/dist/core/PostUpdateMigrator.js.map +1 -1
  34. package/dist/core/cartographerDetect.d.ts +133 -0
  35. package/dist/core/cartographerDetect.d.ts.map +1 -0
  36. package/dist/core/cartographerDetect.js +431 -0
  37. package/dist/core/cartographerDetect.js.map +1 -0
  38. package/dist/core/cartographerDetect.worker.d.ts +2 -0
  39. package/dist/core/cartographerDetect.worker.d.ts.map +1 -0
  40. package/dist/core/cartographerDetect.worker.js +37 -0
  41. package/dist/core/cartographerDetect.worker.js.map +1 -0
  42. package/dist/core/types.d.ts +29 -0
  43. package/dist/core/types.d.ts.map +1 -1
  44. package/dist/core/types.js.map +1 -1
  45. package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts +8 -0
  46. package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts.map +1 -1
  47. package/dist/monitoring/ActiveWorkSilenceSentinel.js +8 -0
  48. package/dist/monitoring/ActiveWorkSilenceSentinel.js.map +1 -1
  49. package/dist/monitoring/ContextWedgeSentinel.d.ts +8 -0
  50. package/dist/monitoring/ContextWedgeSentinel.d.ts.map +1 -1
  51. package/dist/monitoring/ContextWedgeSentinel.js +8 -0
  52. package/dist/monitoring/ContextWedgeSentinel.js.map +1 -1
  53. package/dist/monitoring/GuardPostureTripwire.d.ts +3 -27
  54. package/dist/monitoring/GuardPostureTripwire.d.ts.map +1 -1
  55. package/dist/monitoring/GuardPostureTripwire.js +8 -76
  56. package/dist/monitoring/GuardPostureTripwire.js.map +1 -1
  57. package/dist/monitoring/GuardRegistry.d.ts +48 -0
  58. package/dist/monitoring/GuardRegistry.d.ts.map +1 -0
  59. package/dist/monitoring/GuardRegistry.js +49 -0
  60. package/dist/monitoring/GuardRegistry.js.map +1 -0
  61. package/dist/monitoring/SessionReaper.d.ts +7 -0
  62. package/dist/monitoring/SessionReaper.d.ts.map +1 -1
  63. package/dist/monitoring/SessionReaper.js +9 -0
  64. package/dist/monitoring/SessionReaper.js.map +1 -1
  65. package/dist/monitoring/SessionWatchdog.d.ts +8 -0
  66. package/dist/monitoring/SessionWatchdog.d.ts.map +1 -1
  67. package/dist/monitoring/SessionWatchdog.js +8 -0
  68. package/dist/monitoring/SessionWatchdog.js.map +1 -1
  69. package/dist/monitoring/SocketDisconnectSentinel.d.ts +8 -0
  70. package/dist/monitoring/SocketDisconnectSentinel.d.ts.map +1 -1
  71. package/dist/monitoring/SocketDisconnectSentinel.js +8 -0
  72. package/dist/monitoring/SocketDisconnectSentinel.js.map +1 -1
  73. package/dist/monitoring/guardManifest.d.ts +67 -0
  74. package/dist/monitoring/guardManifest.d.ts.map +1 -0
  75. package/dist/monitoring/guardManifest.js +536 -0
  76. package/dist/monitoring/guardManifest.js.map +1 -0
  77. package/dist/monitoring/guardPosture.d.ts +77 -0
  78. package/dist/monitoring/guardPosture.d.ts.map +1 -0
  79. package/dist/monitoring/guardPosture.js +198 -0
  80. package/dist/monitoring/guardPosture.js.map +1 -0
  81. package/dist/monitoring/guardPostureView.d.ts +100 -0
  82. package/dist/monitoring/guardPostureView.d.ts.map +1 -0
  83. package/dist/monitoring/guardPostureView.js +294 -0
  84. package/dist/monitoring/guardPostureView.js.map +1 -0
  85. package/dist/monitoring/probes/GuardPostureProbe.d.ts +82 -0
  86. package/dist/monitoring/probes/GuardPostureProbe.d.ts.map +1 -0
  87. package/dist/monitoring/probes/GuardPostureProbe.js +384 -0
  88. package/dist/monitoring/probes/GuardPostureProbe.js.map +1 -0
  89. package/dist/scaffold/templates.d.ts.map +1 -1
  90. package/dist/scaffold/templates.js +9 -0
  91. package/dist/scaffold/templates.js.map +1 -1
  92. package/dist/scheduler/JobScheduler.d.ts +10 -0
  93. package/dist/scheduler/JobScheduler.d.ts.map +1 -1
  94. package/dist/scheduler/JobScheduler.js +12 -0
  95. package/dist/scheduler/JobScheduler.js.map +1 -1
  96. package/dist/server/AgentServer.d.ts +8 -0
  97. package/dist/server/AgentServer.d.ts.map +1 -1
  98. package/dist/server/AgentServer.js +2 -0
  99. package/dist/server/AgentServer.js.map +1 -1
  100. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  101. package/dist/server/CapabilityIndex.js +11 -0
  102. package/dist/server/CapabilityIndex.js.map +1 -1
  103. package/dist/server/peerUrlGuard.d.ts +35 -0
  104. package/dist/server/peerUrlGuard.d.ts.map +1 -0
  105. package/dist/server/peerUrlGuard.js +93 -0
  106. package/dist/server/peerUrlGuard.js.map +1 -0
  107. package/dist/server/routes.d.ts +14 -0
  108. package/dist/server/routes.d.ts.map +1 -1
  109. package/dist/server/routes.js +239 -25
  110. package/dist/server/routes.js.map +1 -1
  111. package/package.json +3 -2
  112. package/scripts/lint-guard-manifest.js +264 -0
  113. package/scripts/lint-no-direct-destructive.js +4 -0
  114. package/scripts/lint-no-mainthread-cartographer-walk.js +101 -0
  115. package/src/data/builtin-manifest.json +65 -65
  116. package/src/data/state-coherence-registry.json +54 -18
  117. package/src/scaffold/templates.ts +9 -0
  118. package/upgrades/1.3.491.md +37 -0
  119. package/upgrades/1.3.492.md +31 -0
  120. package/upgrades/side-effects/cartographer-sweep-eventloop-safety.md +110 -0
  121. package/upgrades/side-effects/guard-posture-endpoint.md +101 -0
@@ -65,6 +65,12 @@ import { QuotaNotifier } from '../monitoring/QuotaNotifier.js';
65
65
  import { QuotaManager } from '../monitoring/QuotaManager.js';
66
66
  import { classifySessionDeath, detectContextExhaustion } from '../monitoring/QuotaExhaustionDetector.js';
67
67
  import { SessionWatchdog } from '../monitoring/SessionWatchdog.js';
68
+ import { GuardRegistry } from '../monitoring/GuardRegistry.js';
69
+ import { resolveGuardConfigSnapshot, readGuardPostureBootSnapshot } from '../monitoring/guardPosture.js';
70
+ import { buildGuardInventory, buildHeartbeatPostureBlock } from '../monitoring/guardPostureView.js';
71
+ import { createGuardPostureProbes } from '../monitoring/probes/GuardPostureProbe.js';
72
+ import { GuardPostureStore } from '../core/GuardPostureStore.js';
73
+ import { isPeerUrlAllowedForCredentials } from '../server/peerUrlGuard.js';
68
74
  import { formatWatchdogUserMessage } from '../monitoring/watchdog-notifications.js';
69
75
  import { StallTriageNurse } from '../monitoring/StallTriageNurse.js';
70
76
  import { TriageOrchestrator } from '../monitoring/TriageOrchestrator.js';
@@ -372,6 +378,7 @@ let _resolveRouterUrl = null;
372
378
  /** Every OTHER active machine with a known URL — backs GET /sessions?scope=pool
373
379
  * (pool-wide session aggregation for the dashboard). Set in the same mesh block. */
374
380
  let _resolvePeerUrls = null;
381
+ let _listPoolMachines = null;
375
382
  /** Multi-Machine Session Pool §L4: per-topic placement pin store ("move this to <nickname>"). */
376
383
  let _topicPinStore = null;
377
384
  /** Cross-machine secret-sync (spec Phase 4): route-facing handle (push lever + read-only status). */
@@ -4029,9 +4036,15 @@ export async function startServer(options) {
4029
4036
  : 'detect-only';
4030
4037
  console.log(pc.green(` Prompt Gate: enabled (mode: ${mode})`));
4031
4038
  }
4039
+ // ── GuardRegistry (GUARD-POSTURE-ENDPOINT-SPEC §2.1) ──────────────
4040
+ // Constructed guard components self-register SYNC runtime getters at
4041
+ // their construction sites below; GET /guards reconciles registrations
4042
+ // against the declared manifest (missing = a state, never an omission).
4043
+ const guardRegistry = new GuardRegistry();
4032
4044
  let scheduler;
4033
4045
  if (config.scheduler.enabled && coordinator.isAwake) {
4034
4046
  scheduler = new JobScheduler(config.scheduler, sessionManager, state, config.stateDir);
4047
+ guardRegistry.register('scheduler.enabled', () => scheduler.guardStatus());
4035
4048
  // Wire machine identity for machine-scoped job filtering
4036
4049
  if (coordinator.identity) {
4037
4050
  scheduler.setMachineIdentity(coordinator.identity.machineId, coordinator.identity.name);
@@ -5805,6 +5818,7 @@ export async function startServer(options) {
5805
5818
  if (config.monitoring.watchdog?.enabled) {
5806
5819
  watchdog = new SessionWatchdog(config, sessionManager, state);
5807
5820
  watchdog.intelligence = sharedIntelligence ?? null;
5821
+ guardRegistry.register('monitoring.watchdog.enabled', () => watchdog.guardStatus());
5808
5822
  watchdog.on('intervention', (event) => {
5809
5823
  // Routine recovery (Ctrl+C, SIGTERM) stays as console diagnostics only.
5810
5824
  // The user only hears when we had to force-kill (SIGKILL / session-kill) —
@@ -6990,6 +7004,7 @@ export async function startServer(options) {
6990
7004
  socketSentinel.on('recovered', (n) => notifier.record('recovered', 'socket-disconnect', n));
6991
7005
  socketSentinel.on('recovery-error', (e) => notifier.record('recovery-error', 'socket-disconnect', e.sessionName, e.err instanceof Error ? e.err.message : String(e.err)));
6992
7006
  socketSentinel.start();
7007
+ guardRegistry.register('monitoring.socketDisconnectSentinel.enabled', () => socketSentinel.guardStatus());
6993
7008
  socketRecoveryActive = (s) => socketSentinel.isRecoveryActive(s);
6994
7009
  console.log(pc.green(' SocketDisconnectSentinel enabled (connection-drop recovery)'));
6995
7010
  }
@@ -7044,6 +7059,7 @@ export async function startServer(options) {
7044
7059
  silenceSentinel.on('recover-error', (e) => notifier.record('recovery-error', 'active-silence', e.sessionName, e.err instanceof Error ? e.err.message : String(e.err)));
7045
7060
  silenceSentinel.on('nudge-error', (e) => notifier.record('nudge-error', 'active-silence', e.sessionName, e.err instanceof Error ? e.err.message : String(e.err)));
7046
7061
  silenceSentinel.start();
7062
+ guardRegistry.register('monitoring.activeWorkSilenceSentinel.enabled', () => silenceSentinel.guardStatus());
7047
7063
  silenceRecoveryActive = (s) => silenceSentinel.isRecoveryActive(s);
7048
7064
  const silenceMode = silenceAutoRecover ? ' — auto-heal LIVE' : '';
7049
7065
  console.log(pc.green(telegramEscalation
@@ -7086,6 +7102,16 @@ export async function startServer(options) {
7086
7102
  wedgeSentinel.on('false-alarm', (e) => notifier.record('false-alarm', 'context-wedge', e.sessionName, 'signature scrolled out of tail'));
7087
7103
  wedgeSentinel.on('recovery-error', (e) => notifier.record('recovery-error', 'context-wedge', e.sessionName, e.err instanceof Error ? e.err.message : String(e.err)));
7088
7104
  wedgeSentinel.start();
7105
+ guardRegistry.register('monitoring.contextWedgeSentinel.enabled', () => wedgeSentinel.guardStatus());
7106
+ // Sub-guard row (spec §2.1): the destructive auto-recovery arm reports
7107
+ // its OWN posture so 'autoRecovery silently off inside an on-confirmed
7108
+ // sentinel' cannot hide.
7109
+ guardRegistry.register('monitoring.contextWedgeSentinel.autoRecovery.enabled', () => ({
7110
+ enabled: autoRecovery.enabled === true,
7111
+ dryRun: autoRecovery.dryRun !== false,
7112
+ // Deliberately no lastTickAt: the autoRecovery arm is config-derived
7113
+ // (no tick loop of its own); the parent sentinel row carries liveness.
7114
+ }));
7089
7115
  wedgeRecoveryActive = (s) => wedgeSentinel.isRecoveryActive(s);
7090
7116
  const mode = autoRecovery.enabled ? (autoRecovery.dryRun ? 'auto-recover dry-run' : 'auto-recover LIVE') : 'detect-only';
7091
7117
  console.log(pc.green(` ContextWedgeSentinel enabled (thinking-block-400 + aup-rejection wedges — ${mode})`));
@@ -8289,6 +8315,30 @@ export async function startServer(options) {
8289
8315
  : null;
8290
8316
  if (cartographer)
8291
8317
  console.log(pc.green(' Cartographer doc-tree enabled'));
8318
+ // fix instar#1069: build the structural index ONCE at boot, OFF the request path,
8319
+ // in chunks that yield the event loop — replacing the per-request lazy scaffold()
8320
+ // that froze /health. Fire-and-forget (boot is not blocked); P19 time ceiling so a
8321
+ // pathological tree can't run forever (next boot retries; routes serve not-built
8322
+ // meanwhile). Skipped when an index already exists.
8323
+ if (cartographer && !fs.existsSync(cartographer.indexFilePath())) {
8324
+ const scCfg = config.cartographer?.freshnessSweep;
8325
+ const chunkNodes = typeof scCfg?.scaffoldChunkNodes === 'number' ? scCfg.scaffoldChunkNodes : 500;
8326
+ const scaffoldStartedAt = Date.now();
8327
+ void cartographer.scaffoldChunked({
8328
+ chunkNodes,
8329
+ onYield: () => new Promise((r) => setImmediate(r)),
8330
+ shouldAbort: () => Date.now() - scaffoldStartedAt > 10 * 60_000, // P19: 10-min ceiling, retry next boot
8331
+ }).then(() => {
8332
+ console.log(pc.gray(' Cartographer index built (boot scaffold)'));
8333
+ }).catch((err) => {
8334
+ // @silent-fallback-ok — the boot scaffold is best-effort by design (fix
8335
+ // instar#1069 P19): a partial run never produces a readable index (atomic
8336
+ // tmp+rename), routes serve indexState:'not-built' honestly, and the next
8337
+ // boot retries. The yellow log line is the operator surface; failing the
8338
+ // boot over a map-build would invert the feature's priority.
8339
+ console.log(pc.yellow(` Cartographer boot scaffold incomplete (retries next boot): ${err instanceof Error ? err.message : String(err)}`));
8340
+ });
8341
+ }
8292
8342
  // Cartographer doc-freshness sweep (spec #2). In-process poller that authors
8293
8343
  // stale/never-authored node summaries on a LIGHT model routed OFF Claude.
8294
8344
  // Ships dark behind freshnessSweep.enabled ONLY — the redundant egressAcknowledged
@@ -8317,6 +8367,26 @@ export async function startServer(options) {
8317
8367
  const { CartographerSweepPoller } = await import('../monitoring/CartographerSweepPoller.js');
8318
8368
  const { sampleHostPressure: _sampleHostPressure } = await import('../monitoring/HostPressureSampler.js');
8319
8369
  const rcfg = config.monitoring?.sessionReaper;
8370
+ // Slice 3 (fix instar#1069): honor cartographer.freshnessSweep.framework as
8371
+ // the sweep's routing so BOTH the routing PROBE and the actual author call
8372
+ // resolve consistently (the router reads config.sessions.componentFrameworks
8373
+ // live). EXPLICIT-SET-ONLY: a pre-existing overrides.CartographerSweep OR an
8374
+ // explicitly-configured categories.job is never overridden (migration safety);
8375
+ // otherwise freshnessSweep.framework becomes the sweep's effective override.
8376
+ {
8377
+ const { resolveSweepFrameworkRouting } = await import('../core/CartographerSweepEngine.js');
8378
+ const cf = config.sessions?.componentFrameworks ?? {};
8379
+ const sweepFw = typeof fsCfg.framework === 'string' ? fsCfg.framework : undefined;
8380
+ const routed = resolveSweepFrameworkRouting(cf, sweepFw);
8381
+ if (routed.injectOverride && routed.framework) {
8382
+ // Inject so BOTH probeRouting and evaluate resolve consistently (router
8383
+ // reads config.sessions.componentFrameworks live). Explicit-set-only.
8384
+ const s = (config.sessions ??= {});
8385
+ const c = (s.componentFrameworks ??= {});
8386
+ (c.overrides ??= {}).CartographerSweep = routed.framework;
8387
+ }
8388
+ console.log(pc.gray(` Cartographer sweep routing: ${routed.framework ?? '(default)'} (source: ${routed.source})`));
8389
+ }
8320
8390
  const engine = new CartographerSweepEngine({
8321
8391
  tree: cartographer,
8322
8392
  router: routerLike,
@@ -8339,6 +8409,15 @@ export async function startServer(options) {
8339
8409
  maxDeferredPasses: num(fsCfg.maxDeferredPasses, 5),
8340
8410
  revalidateSamplePerPass: num(fsCfg.revalidateSamplePerPass, 2),
8341
8411
  minNodesUnderPressure: num(fsCfg.minNodesUnderPressure, 3),
8412
+ // fix instar#1069: off-event-loop detect knobs (worker by default).
8413
+ detectInWorker: fsCfg.detectInWorker !== false,
8414
+ detectTimeoutMs: num(fsCfg.detectTimeoutMs, 120000),
8415
+ detectWorkerHeapMb: num(fsCfg.detectWorkerHeapMb, 1536),
8416
+ maxIndexBytes: num(fsCfg.maxIndexBytes, 200 * 1024 * 1024),
8417
+ snapshotSampleMax: num(fsCfg.snapshotSampleMax, 500),
8418
+ gitMaxBuffer: num(fsCfg.gitMaxBuffer, 64 * 1024 * 1024),
8419
+ detectCandidateHeadroom: num(fsCfg.detectCandidateHeadroom, 4),
8420
+ detectGraceMs: num(fsCfg.cadenceMs, 600000) * 2,
8342
8421
  },
8343
8422
  stateDir: config.stateDir,
8344
8423
  log: (m) => console.log(pc.gray(m)),
@@ -9230,6 +9309,68 @@ export async function startServer(options) {
9230
9309
  lockFilePath: path.join(config.stateDir, 'lifeline.lock'),
9231
9310
  isEnabled: () => fs.existsSync(path.join(config.stateDir, 'lifeline.lock')),
9232
9311
  }),
9312
+ ...createGuardPostureProbes({
9313
+ // Same one-read inventory GET /guards serves; null on read failure.
9314
+ getLocalPosture: () => {
9315
+ try {
9316
+ const snap = resolveGuardConfigSnapshot(config.projectDir);
9317
+ if (snap.readError)
9318
+ return null;
9319
+ return buildGuardInventory({
9320
+ snapshot: snap,
9321
+ bootSnapshot: readGuardPostureBootSnapshot(config.stateDir),
9322
+ registry: guardRegistry,
9323
+ });
9324
+ }
9325
+ catch {
9326
+ return null; /* @silent-fallback-ok — probe degrades to no-local-posture for this tick; the route surfaces config errors loudly */
9327
+ }
9328
+ },
9329
+ // Heartbeat-sourced (durable last-known for offline peers) — never a
9330
+ // doomed fan-out for a dark peer (spec §2.4 data-source rule).
9331
+ getPeerPostures: () => {
9332
+ try {
9333
+ return (machinePoolRegistry?.getCapacities() ?? [])
9334
+ .filter((m) => m.machineId !== (_meshSelfId ?? ''))
9335
+ .map((m) => ({
9336
+ machineId: m.machineId,
9337
+ nickname: m.nickname,
9338
+ online: m.online,
9339
+ posture: m.guardPosture ?? null,
9340
+ postureAgeMs: m.guardPostureReceivedAt ? Date.now() - Date.parse(m.guardPostureReceivedAt) : null,
9341
+ }));
9342
+ }
9343
+ catch {
9344
+ return []; /* @silent-fallback-ok — pool not wired yet (boot order): peers none this tick, next tick reads the live registry */
9345
+ }
9346
+ },
9347
+ // Spec §2.4 deep-read fallback: ONLY for an ONLINE peer whose
9348
+ // heartbeat posture block is missing/stale — a plain GET /guards
9349
+ // (never ?scope=pool), token attached only past the URL guard.
9350
+ deepReadPeer: async (machineId) => {
9351
+ if (!_listPoolMachines)
9352
+ return null; // pool not wired on this host — explicit, not incidental
9353
+ const m = _listPoolMachines().find((x) => x.machineId === machineId);
9354
+ if (!m?.lastKnownUrl)
9355
+ return null;
9356
+ const extra = config.multiMachine?.peerUrlAllowlist;
9357
+ if (!isPeerUrlAllowedForCredentials(m.lastKnownUrl, extra).ok)
9358
+ return null;
9359
+ const r = await fetch(`${m.lastKnownUrl}/guards`, {
9360
+ headers: { Authorization: `Bearer ${config.authToken}` },
9361
+ signal: AbortSignal.timeout(5000),
9362
+ });
9363
+ if (!r.ok)
9364
+ return null;
9365
+ return r.json();
9366
+ },
9367
+ emitAttention: async (item) => {
9368
+ if (!telegram)
9369
+ return;
9370
+ await telegram.createAttentionItem(item);
9371
+ },
9372
+ stateDir: config.stateDir,
9373
+ }),
9233
9374
  ...createPlatformProbes({
9234
9375
  tmuxPath,
9235
9376
  }),
@@ -10755,6 +10896,7 @@ export async function startServer(options) {
10755
10896
  console.log(pc.green(' Unkillability backstop enabled (§P5 — signal-only, never auto-kills)'));
10756
10897
  }
10757
10898
  }
10899
+ guardRegistry.register('monitoring.sessionReaper.enabled', () => sessionReaper.guardStatus());
10758
10900
  if (config.monitoring?.sessionReaper?.enabled) {
10759
10901
  console.log(pc.green(config.monitoring.sessionReaper.dryRun === false
10760
10902
  ? ' SessionReaper enabled (idle-session reaper — LIVE)'
@@ -10936,6 +11078,9 @@ export async function startServer(options) {
10936
11078
  const failoverThresholdMs = (config.multiMachine?.failoverTimeoutMinutes ?? 15) * 60_000;
10937
11079
  const clockSkewToleranceMs = config.multiMachine?.sessionPool?.clockSkewToleranceMs ?? 300_000;
10938
11080
  machinePoolRegistry = new poolMod.MachinePoolRegistry({
11081
+ // Durable last-known posture (GUARD-POSTURE-ENDPOINT-SPEC §2.3(c)) —
11082
+ // survives local restarts so a dark peer renders with its real age.
11083
+ postureStore: new GuardPostureStore(config.stateDir),
10939
11084
  listMachines: () => poolIdMgr.getActiveMachines().map(({ machineId, entry }) => ({
10940
11085
  machineId,
10941
11086
  nickname: entry.nickname,
@@ -10978,6 +11123,47 @@ export async function startServer(options) {
10978
11123
  return undefined; /* unknown ≠ blocked */
10979
11124
  }
10980
11125
  };
11126
+ // Self guard-posture block riding the capacity heartbeat (spec §2.3).
11127
+ // Computed per beat from the same one-read snapshot GET /guards uses;
11128
+ // a failed compute omits the block (older-peer semantics), never throws.
11129
+ // The resolved-config snapshot is the expensive half (defaults clone
11130
+ // + deep merge); cache it keyed on config.json mtime so the 30s
11131
+ // beat pays one cheap fs.stat instead (perf review 2026-06-12 #1).
11132
+ // The INVENTORY still rebuilds every beat — runtime states
11133
+ // (lastTickAt staleness, self-reported enabled) must stay live.
11134
+ let _postureComputeWarned = false;
11135
+ let _postureSnapCache = null;
11136
+ const selfGuardPosture = () => {
11137
+ try {
11138
+ let mtimeMs = -1;
11139
+ try {
11140
+ mtimeMs = fs.statSync(path.join(config.stateDir, 'config.json')).mtimeMs;
11141
+ }
11142
+ catch { /* @silent-fallback-ok — absent config file: mtime -1 still caches the defaults-only snapshot */ }
11143
+ if (!_postureSnapCache || _postureSnapCache.mtimeMs !== mtimeMs) {
11144
+ _postureSnapCache = { mtimeMs, snap: resolveGuardConfigSnapshot(config.projectDir) };
11145
+ }
11146
+ const snap = _postureSnapCache.snap;
11147
+ if (snap.readError)
11148
+ return undefined;
11149
+ const inv = buildGuardInventory({
11150
+ snapshot: snap,
11151
+ bootSnapshot: readGuardPostureBootSnapshot(config.stateDir),
11152
+ registry: guardRegistry,
11153
+ });
11154
+ return buildHeartbeatPostureBlock(inv, new Date().toISOString());
11155
+ }
11156
+ catch (err) {
11157
+ // @silent-fallback-ok — posture is optional on a beat (the pool
11158
+ // renders "unknown" honestly), and a PERSISTENT compute failure
11159
+ // is not invisible: the first occurrence logs below.
11160
+ if (!_postureComputeWarned) {
11161
+ _postureComputeWarned = true;
11162
+ console.log(pc.yellow(` [guards] heartbeat posture compute failed (beats will omit posture until it recovers): ${err instanceof Error ? err.message : String(err)}`));
11163
+ }
11164
+ return undefined;
11165
+ }
11166
+ };
10981
11167
  const refreshPool = () => {
10982
11168
  try {
10983
11169
  machinePoolRegistry.recordHeartbeat({
@@ -10985,6 +11171,7 @@ export async function startServer(options) {
10985
11171
  selfReportedLastSeen: new Date().toISOString(),
10986
11172
  loadAvg: osMod.loadavg()[0],
10987
11173
  quotaState: selfQuotaState(),
11174
+ guardPosture: selfGuardPosture(),
10988
11175
  });
10989
11176
  const hbApi = machineHeartbeat?.api;
10990
11177
  if (hbApi) {
@@ -11427,6 +11614,14 @@ export async function startServer(options) {
11427
11614
  .getActiveMachines()
11428
11615
  .filter((m) => m.machineId !== meshSelfId && !!m.entry.lastKnownUrl)
11429
11616
  .map((m) => ({ machineId: m.machineId, url: m.entry.lastKnownUrl }));
11617
+ // EVERY registered non-revoked machine — URL or not — so the /guards
11618
+ // pool view can account for each by name (no-known-url is a NAMED row,
11619
+ // never a silent omission — GUARD-POSTURE-ENDPOINT-SPEC §2.3).
11620
+ _listPoolMachines = () => meshIdMgr.getActiveMachines().map((m) => ({
11621
+ machineId: m.machineId,
11622
+ nickname: m.entry.nickname,
11623
+ lastKnownUrl: m.entry.lastKnownUrl ?? null,
11624
+ }));
11430
11625
  // Pool Dashboard Streaming requesting side (§2.2): build the connector
11431
11626
  // the WebSocketManager uses to open an upstream /pool-stream to a peer.
11432
11627
  // connect() is synchronous (PeerStreamProxy contract) but the mint +
@@ -12203,6 +12398,9 @@ export async function startServer(options) {
12203
12398
  journalAdvert,
12204
12399
  ...(cap.commitmentsAdvert ? { commitmentsAdvert: cap.commitmentsAdvert } : {}),
12205
12400
  ...(cap.quotaState ? { quotaState: cap.quotaState } : {}),
12401
+ // Guard posture rides the same pass-through (the A2 narrowing
12402
+ // lesson): dropping it here would blind the pool to peers' posture.
12403
+ ...(cap.guardPosture ? { guardPosture: cap.guardPosture } : {}),
12206
12404
  };
12207
12405
  }
12208
12406
  return null;
@@ -12496,7 +12694,7 @@ export async function startServer(options) {
12496
12694
  carrier: _topicProfileCarrier,
12497
12695
  }
12498
12696
  : null;
12499
- const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, feedbackAnomalyDetector, dispatches, updateChecker, autoUpdater, autoDispatcher, quotaTracker, quotaManager, publisher, viewer, tunnel, evolution, watchdog, topicMemory, triageNurse, projectMapper, cartographer: cartographer ?? undefined, coherenceGate: scopeVerifier, contextHierarchy, canonicalState, operationGate, sentinel, adaptiveTrust, memoryMonitor, orphanReaper, coherenceMonitor, commitmentTracker, subscriptionPool, quotaPoller, quotaAwareScheduler: _quotaAwareScheduler ?? undefined, proactiveSwapMonitor: _proactiveSwapMonitor ?? undefined, inUseAccountResolver, enrollmentWizard, semanticMemory, activitySentinel, rateLimitSentinel, releaseReadinessSentinel: releaseReadinessSentinel ?? undefined, messageRouter, summarySentinel, spawnManager, systemReviewer, capabilityMapper, selfKnowledgeTree, coverageAuditor, topicResumeMap: _topicResumeMap ?? undefined, topicProfile: _topicProfileCtx ?? undefined, sessionRefresh: _sessionRefresh ?? undefined, autonomyManager, trustElevationTracker, autonomousEvolution, coordinator: coordinator.enabled ? coordinator : undefined, localSigningKeyPem, leaseTransport, onLeasePullRequest: () => leaseCoordinatorRef?.currentLease() ?? null, liveTailReceiver, handoffWireTransport, onHandoffBegin, onHandoffInitiate: handoffInitiate, handoffInProgress: handoffSentinelInProgress, messageLedger, currentInboundByTopic, replyMarkerTransport, onReplyMarker: messageLedger ? (marker) => { const m = marker; messageLedger.applyRemoteReplyMarker(m.dedupeKey, { platform: m.platform, replyIdempotencyKey: m.replyIdempotencyKey, epoch: m.epoch, topic: m.topic ?? null }); } : undefined, whatsapp: whatsappAdapter, slack: slackAdapter, imessage: imessageAdapter, whatsappBusinessBackend, messageBridge, hookEventReceiver, worktreeMonitor, subagentTracker, instructionsVerifier, handshakeManager: threadlineHandshake, threadlineRouter, conversationStore, warrantsReplyGate, collaborationSurfacer, threadResumeMap, topicLinkageHandler: topicLinkageHandler ?? undefined, threadlineRelayClient, threadlineReplyWaiters, listenerManager: listenerManager ?? undefined, a2aDeliveryTracker: a2aDeliveryTracker ?? undefined, responseReviewGate, messagingToneGate, outboundDedupGate, telemetryHeartbeat, pasteManager, featureRegistry, discoveryEvaluator, completionEvaluator, unifiedTrust, liveConfig, sharedStateLedger, ledgerSessionRegistry, worktreeManager, oidcEnrolledRepos: parallelDevConfig?.oidcEnrolledRepos, initiativeTracker, projectRoundRunner, projectDriftChecker, machineHeartbeat, machinePoolRegistry, meshRpcDispatcher, workingSetPullCoordinator, commitmentReplicaStore, forwardCommitmentMutate, sessionOwnershipRegistry, topicPinStore: _topicPinStore ?? undefined, streamTicketStore: _streamTicketStore ?? undefined, poolStreamAllowRemoteInput: config.dashboard?.poolStream?.allowRemoteInput ?? false, poolStreamConnector: _poolStreamConnector ?? undefined, secretSync: _secretSyncHandle ?? undefined, meshSelfId: _meshSelfId ?? undefined, resolveRouterUrl: _resolveRouterUrl ?? undefined, resolvePeerUrls: _resolvePeerUrls ?? undefined, sessionPoolE2EResultStore, proxyCoordinator, topicIntentStore, topicIntentArcCheck, usherSignalStore, intelligence: sharedIntelligence ?? undefined, telegramBridgeConfig, telegramBridge: telegramBridge ?? undefined, threadlineObservability, briefDeps, workingMemory, taskFlowRegistry, threadlineFlowBridge, sessionReaper, agentWorktreeReaper, mcpProcessReaper, geminiLoopRunner, sleepController, agentActivityState, reapLog, sleepWakeDetector, unjustifiedStopGate, stopGateDb, stopNotifier });
12697
+ const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, feedbackAnomalyDetector, dispatches, updateChecker, autoUpdater, autoDispatcher, quotaTracker, quotaManager, publisher, viewer, tunnel, evolution, watchdog, topicMemory, triageNurse, projectMapper, cartographer: cartographer ?? undefined, coherenceGate: scopeVerifier, contextHierarchy, canonicalState, operationGate, sentinel, adaptiveTrust, memoryMonitor, orphanReaper, coherenceMonitor, commitmentTracker, subscriptionPool, quotaPoller, quotaAwareScheduler: _quotaAwareScheduler ?? undefined, proactiveSwapMonitor: _proactiveSwapMonitor ?? undefined, inUseAccountResolver, enrollmentWizard, semanticMemory, activitySentinel, rateLimitSentinel, releaseReadinessSentinel: releaseReadinessSentinel ?? undefined, messageRouter, summarySentinel, spawnManager, systemReviewer, capabilityMapper, selfKnowledgeTree, coverageAuditor, topicResumeMap: _topicResumeMap ?? undefined, topicProfile: _topicProfileCtx ?? undefined, sessionRefresh: _sessionRefresh ?? undefined, autonomyManager, trustElevationTracker, autonomousEvolution, coordinator: coordinator.enabled ? coordinator : undefined, localSigningKeyPem, leaseTransport, onLeasePullRequest: () => leaseCoordinatorRef?.currentLease() ?? null, liveTailReceiver, handoffWireTransport, onHandoffBegin, onHandoffInitiate: handoffInitiate, handoffInProgress: handoffSentinelInProgress, messageLedger, currentInboundByTopic, replyMarkerTransport, onReplyMarker: messageLedger ? (marker) => { const m = marker; messageLedger.applyRemoteReplyMarker(m.dedupeKey, { platform: m.platform, replyIdempotencyKey: m.replyIdempotencyKey, epoch: m.epoch, topic: m.topic ?? null }); } : undefined, whatsapp: whatsappAdapter, slack: slackAdapter, imessage: imessageAdapter, whatsappBusinessBackend, messageBridge, hookEventReceiver, worktreeMonitor, subagentTracker, instructionsVerifier, handshakeManager: threadlineHandshake, threadlineRouter, conversationStore, warrantsReplyGate, collaborationSurfacer, threadResumeMap, topicLinkageHandler: topicLinkageHandler ?? undefined, threadlineRelayClient, threadlineReplyWaiters, listenerManager: listenerManager ?? undefined, a2aDeliveryTracker: a2aDeliveryTracker ?? undefined, responseReviewGate, messagingToneGate, outboundDedupGate, telemetryHeartbeat, pasteManager, featureRegistry, discoveryEvaluator, completionEvaluator, unifiedTrust, liveConfig, sharedStateLedger, ledgerSessionRegistry, worktreeManager, oidcEnrolledRepos: parallelDevConfig?.oidcEnrolledRepos, initiativeTracker, projectRoundRunner, projectDriftChecker, machineHeartbeat, machinePoolRegistry, meshRpcDispatcher, workingSetPullCoordinator, commitmentReplicaStore, forwardCommitmentMutate, sessionOwnershipRegistry, topicPinStore: _topicPinStore ?? undefined, streamTicketStore: _streamTicketStore ?? undefined, poolStreamAllowRemoteInput: config.dashboard?.poolStream?.allowRemoteInput ?? false, poolStreamConnector: _poolStreamConnector ?? undefined, secretSync: _secretSyncHandle ?? undefined, meshSelfId: _meshSelfId ?? undefined, resolveRouterUrl: _resolveRouterUrl ?? undefined, resolvePeerUrls: _resolvePeerUrls ?? undefined, guardRegistry, listPoolMachines: _listPoolMachines ?? undefined, sessionPoolE2EResultStore, proxyCoordinator, topicIntentStore, topicIntentArcCheck, usherSignalStore, intelligence: sharedIntelligence ?? undefined, telegramBridgeConfig, telegramBridge: telegramBridge ?? undefined, threadlineObservability, briefDeps, workingMemory, taskFlowRegistry, threadlineFlowBridge, sessionReaper, agentWorktreeReaper, mcpProcessReaper, geminiLoopRunner, sleepController, agentActivityState, reapLog, sleepWakeDetector, unjustifiedStopGate, stopGateDb, stopNotifier });
12500
12698
  // Resolve the late-bound topic-operator getter (increment 2e): routing was
12501
12699
  // wired before the server existed; from here on inbound binds use the
12502
12700
  // server's own store instance.