@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.
- package/dist/{chunk-K6CQFAGN.js → chunk-NQ3WFEXY.js} +92 -1
- package/dist/chunk-NQ3WFEXY.js.map +1 -0
- package/dist/hub-connection.d.ts +30 -0
- package/dist/hub-connection.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/start.js +1 -1
- package/package.json +6 -6
- package/dist/chunk-K6CQFAGN.js.map +0 -1
|
@@ -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-
|
|
3188
|
+
//# sourceMappingURL=chunk-NQ3WFEXY.js.map
|