@mclawnet/agent 0.6.27 → 0.6.28

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.
@@ -474,10 +474,19 @@ var log2 = createLogger2({ module: "agent" });
474
474
  var HubConnection = class {
475
475
  ws = null;
476
476
  heartbeatTimer = null;
477
+ wakeWatchTimer = null;
477
478
  reconnectTimer = null;
478
479
  reconnectDelay;
479
480
  destroyed = false;
480
481
  authState = "pending";
482
+ /** Liveness tracking — if hub stops acking, the socket is dead even when
483
+ * the OS hasn't yet noticed (very common after a Mac/laptop wakes from
484
+ * sleep: the kernel believes the socket is up, but the peer / NAT box
485
+ * dropped it minutes ago). We probe by counting acks. */
486
+ lastAckAt = 0;
487
+ /** Wall-clock checkpoint for the wake watcher. A jump larger than the
488
+ * interval signals the process was suspended (sleep / Power Nap). */
489
+ lastWakeTickAt = 0;
481
490
  hubUrl;
482
491
  token;
483
492
  hostname;
@@ -514,6 +523,7 @@ var HubConnection = class {
514
523
  this.onConnectCb = opts.onConnect;
515
524
  this.onDisconnect = opts.onDisconnect;
516
525
  this.onError = opts.onError;
526
+ this.startWakeWatch();
517
527
  }
518
528
  setSessionManager(manager) {
519
529
  this.sessionManager = manager;
@@ -549,6 +559,7 @@ var HubConnection = class {
549
559
  }
550
560
  connect() {
551
561
  if (this.destroyed) return;
562
+ if (this.ws || this.reconnectTimer) return;
552
563
  this.cleanupConnection();
553
564
  this.authState = "pending";
554
565
  this.ws = new WebSocket(this.hubUrl);
@@ -585,6 +596,7 @@ var HubConnection = class {
585
596
  return;
586
597
  }
587
598
  if (this.authState === "authenticated") {
599
+ this.lastAckAt = Date.now();
588
600
  if (this.handleSessionMessage(data)) return;
589
601
  this.onMessage?.(data);
590
602
  }
@@ -592,6 +604,7 @@ var HubConnection = class {
592
604
  this.ws.on("close", (code, reason) => {
593
605
  log2.warn({ code, reason: reason.toString() }, "disconnected from hub");
594
606
  this.stopHeartbeat();
607
+ this.ws = null;
595
608
  this.authState = "pending";
596
609
  this.onDisconnect?.(code, reason.toString());
597
610
  if (code === WS_CLOSE_INVALID_TOKEN) {
@@ -1123,7 +1136,17 @@ var HubConnection = class {
1123
1136
  }
1124
1137
  startHeartbeat() {
1125
1138
  this.stopHeartbeat();
1139
+ this.lastAckAt = Date.now();
1126
1140
  this.heartbeatTimer = setInterval(() => {
1141
+ const sinceAck = Date.now() - this.lastAckAt;
1142
+ if (sinceAck > this.heartbeatInterval * 2.5) {
1143
+ log2.warn(
1144
+ { sinceAckMs: sinceAck, heartbeatIntervalMs: this.heartbeatInterval },
1145
+ "no ack from hub \u2014 terminating dead socket"
1146
+ );
1147
+ this.terminateAndReconnect("liveness_timeout");
1148
+ return;
1149
+ }
1127
1150
  this.send({ type: "heartbeat", ts: Date.now() });
1128
1151
  }, this.heartbeatInterval);
1129
1152
  }
@@ -1133,16 +1156,84 @@ var HubConnection = class {
1133
1156
  this.heartbeatTimer = null;
1134
1157
  }
1135
1158
  }
1159
+ /**
1160
+ * Detect machine wake-ups (sleep / Power Nap) by watching wall-clock jumps.
1161
+ * Node timers are paused while the process is suspended, so a tick that
1162
+ * fires "late" by more than the watch interval signals the host just woke.
1163
+ * On wake we proactively terminate the socket — it is almost certainly
1164
+ * half-open at this point — instead of waiting up to 2× heartbeat for the
1165
+ * liveness probe above to notice.
1166
+ */
1167
+ startWakeWatch() {
1168
+ this.stopWakeWatch();
1169
+ const intervalMs = 1e3;
1170
+ const jumpThresholdMs = 5e3;
1171
+ this.lastWakeTickAt = Date.now();
1172
+ this.wakeWatchTimer = setInterval(() => {
1173
+ const now = Date.now();
1174
+ const drift = now - this.lastWakeTickAt - intervalMs;
1175
+ this.lastWakeTickAt = now;
1176
+ if (drift <= jumpThresholdMs) return;
1177
+ log2.warn(
1178
+ { driftMs: drift, hasSocket: !!this.ws },
1179
+ "wake detected (clock jump) \u2014 forcing reconnect"
1180
+ );
1181
+ this.reconnectDelay = DEFAULT_RECONNECT_MS;
1182
+ if (this.ws) {
1183
+ this.terminateAndReconnect("wake_detected");
1184
+ } else {
1185
+ if (this.reconnectTimer) {
1186
+ clearTimeout(this.reconnectTimer);
1187
+ this.reconnectTimer = null;
1188
+ }
1189
+ this.scheduleReconnect();
1190
+ }
1191
+ }, intervalMs);
1192
+ this.wakeWatchTimer.unref();
1193
+ }
1194
+ stopWakeWatch() {
1195
+ if (this.wakeWatchTimer) {
1196
+ clearInterval(this.wakeWatchTimer);
1197
+ this.wakeWatchTimer = null;
1198
+ }
1199
+ }
1200
+ /**
1201
+ * Hard-kill the socket (RST, no four-way handshake) and immediately schedule
1202
+ * a reconnect. Used when we know the connection is dead but the OS hasn't
1203
+ * surfaced it yet — calling `close()` here would block on a FIN-ACK that
1204
+ * will never arrive.
1205
+ *
1206
+ * We `removeAllListeners()` before terminating so the close handler can't
1207
+ * also call `scheduleReconnect()` — single-entry reconnect is easier to
1208
+ * reason about than relying on the timer-slot idempotency check.
1209
+ */
1210
+ terminateAndReconnect(reason) {
1211
+ this.stopHeartbeat();
1212
+ if (this.ws) {
1213
+ this.ws.removeAllListeners();
1214
+ try {
1215
+ this.ws.terminate();
1216
+ } catch {
1217
+ }
1218
+ this.ws = null;
1219
+ }
1220
+ log2.warn({ reason }, "forced reconnect");
1221
+ this.authState = "pending";
1222
+ this.scheduleReconnect();
1223
+ }
1136
1224
  scheduleReconnect() {
1137
1225
  if (this.destroyed) return;
1226
+ if (this.reconnectTimer) return;
1138
1227
  log2.warn({ delayMs: this.reconnectDelay }, "reconnecting to hub...");
1139
1228
  this.reconnectTimer = setTimeout(() => {
1229
+ this.reconnectTimer = null;
1140
1230
  this.connect();
1141
1231
  }, this.reconnectDelay);
1142
1232
  this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
1143
1233
  }
1144
1234
  cleanup() {
1145
1235
  this.cleanupConnection();
1236
+ this.stopWakeWatch();
1146
1237
  this.sessionManager?.closeAll().catch(() => {
1147
1238
  });
1148
1239
  }
@@ -3094,4 +3185,4 @@ export {
3094
3185
  FsBridge,
3095
3186
  startAgent
3096
3187
  };
3097
- //# sourceMappingURL=chunk-K6CQFAGN.js.map
3188
+ //# sourceMappingURL=chunk-NQ3WFEXY.js.map