@mclawnet/agent 0.6.28 → 0.6.30
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/__tests__/hub-connection-reconnect.test.d.ts +2 -0
- package/dist/__tests__/hub-connection-reconnect.test.d.ts.map +1 -0
- package/dist/{chunk-NQ3WFEXY.js → chunk-LXSWV3VS.js} +94 -14
- package/dist/chunk-LXSWV3VS.js.map +1 -0
- package/dist/hub-connection.d.ts +6 -1
- package/dist/hub-connection.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/session-manager.d.ts +14 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/start.d.ts.map +1 -1
- package/dist/start.js +1 -1
- package/package.json +6 -6
- package/dist/chunk-NQ3WFEXY.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hub-connection-reconnect.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/hub-connection-reconnect.test.ts"],"names":[],"mappings":""}
|
|
@@ -559,13 +559,13 @@ var HubConnection = class {
|
|
|
559
559
|
}
|
|
560
560
|
connect() {
|
|
561
561
|
if (this.destroyed) return;
|
|
562
|
-
if (this.
|
|
562
|
+
if (this.reconnectTimer) return;
|
|
563
|
+
if (this.ws && (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
563
566
|
this.cleanupConnection();
|
|
564
567
|
this.authState = "pending";
|
|
565
|
-
this.ws = new WebSocket(this.hubUrl);
|
|
566
|
-
this.ws.on("open", () => {
|
|
567
|
-
this.reconnectDelay = DEFAULT_RECONNECT_MS;
|
|
568
|
-
});
|
|
568
|
+
this.ws = new WebSocket(this.hubUrl, { handshakeTimeout: 1e4 });
|
|
569
569
|
this.ws.on("message", (raw) => {
|
|
570
570
|
let data;
|
|
571
571
|
try {
|
|
@@ -589,6 +589,7 @@ var HubConnection = class {
|
|
|
589
589
|
log2.info({ agentId: data.agentId }, "registered with hub");
|
|
590
590
|
this.authState = "authenticated";
|
|
591
591
|
this.agentId = data.agentId ?? null;
|
|
592
|
+
this.reconnectDelay = DEFAULT_RECONNECT_MS;
|
|
592
593
|
this.startHeartbeat();
|
|
593
594
|
this.onConnectCb?.(this.agentId);
|
|
594
595
|
this.tryRecoverSwarms();
|
|
@@ -616,6 +617,18 @@ var HubConnection = class {
|
|
|
616
617
|
this.ws.on("error", (err) => {
|
|
617
618
|
log2.error({ err }, "ws connection error");
|
|
618
619
|
this.onError?.(err);
|
|
620
|
+
if (this.ws) {
|
|
621
|
+
const dying = this.ws;
|
|
622
|
+
this.ws = null;
|
|
623
|
+
try {
|
|
624
|
+
dying.removeAllListeners();
|
|
625
|
+
dying.terminate();
|
|
626
|
+
} catch {
|
|
627
|
+
}
|
|
628
|
+
this.stopHeartbeat();
|
|
629
|
+
this.authState = "pending";
|
|
630
|
+
this.scheduleReconnect();
|
|
631
|
+
}
|
|
619
632
|
});
|
|
620
633
|
}
|
|
621
634
|
send(data) {
|
|
@@ -1205,14 +1218,26 @@ var HubConnection = class {
|
|
|
1205
1218
|
*
|
|
1206
1219
|
* We `removeAllListeners()` before terminating so the close handler can't
|
|
1207
1220
|
* also call `scheduleReconnect()` — single-entry reconnect is easier to
|
|
1208
|
-
* reason about than relying on the timer-slot idempotency check.
|
|
1221
|
+
* reason about than relying on the timer-slot idempotency check. We then
|
|
1222
|
+
* re-attach a no-op `error` listener: terminating a socket still in
|
|
1223
|
+
* CONNECTING state (very common right after a wake — the post-sleep
|
|
1224
|
+
* reconnect attempt may not have completed the handshake yet) emits an
|
|
1225
|
+
* asynchronous `error` event, and an unhandled `error` on a WebSocket
|
|
1226
|
+
* (which extends EventEmitter) crashes the whole process.
|
|
1209
1227
|
*/
|
|
1210
1228
|
terminateAndReconnect(reason) {
|
|
1211
1229
|
this.stopHeartbeat();
|
|
1212
1230
|
if (this.ws) {
|
|
1213
|
-
this.ws
|
|
1231
|
+
const ws = this.ws;
|
|
1232
|
+
ws.removeAllListeners();
|
|
1233
|
+
ws.on("error", () => {
|
|
1234
|
+
});
|
|
1214
1235
|
try {
|
|
1215
|
-
|
|
1236
|
+
if (ws.readyState === WebSocket.CONNECTING) {
|
|
1237
|
+
ws.close();
|
|
1238
|
+
} else {
|
|
1239
|
+
ws.terminate();
|
|
1240
|
+
}
|
|
1216
1241
|
} catch {
|
|
1217
1242
|
}
|
|
1218
1243
|
this.ws = null;
|
|
@@ -1224,12 +1249,15 @@ var HubConnection = class {
|
|
|
1224
1249
|
scheduleReconnect() {
|
|
1225
1250
|
if (this.destroyed) return;
|
|
1226
1251
|
if (this.reconnectTimer) return;
|
|
1227
|
-
|
|
1252
|
+
const cap = Math.min(this.reconnectDelay, this.maxReconnectDelay);
|
|
1253
|
+
const base = Math.min(DEFAULT_RECONNECT_MS, cap);
|
|
1254
|
+
const delay = Math.floor(base + Math.random() * Math.max(0, cap - base));
|
|
1255
|
+
log2.warn({ delayMs: delay, capMs: cap }, "reconnecting to hub...");
|
|
1228
1256
|
this.reconnectTimer = setTimeout(() => {
|
|
1229
1257
|
this.reconnectTimer = null;
|
|
1230
1258
|
this.connect();
|
|
1231
|
-
},
|
|
1232
|
-
this.reconnectDelay = Math.min(
|
|
1259
|
+
}, delay);
|
|
1260
|
+
this.reconnectDelay = Math.min(cap * 2, this.maxReconnectDelay);
|
|
1233
1261
|
}
|
|
1234
1262
|
cleanup() {
|
|
1235
1263
|
this.cleanupConnection();
|
|
@@ -2059,6 +2087,13 @@ var SessionManager = class {
|
|
|
2059
2087
|
// correctly routed to the "spawn new + --resume" branch instead of trying
|
|
2060
2088
|
// to write to a process we are about to kill.
|
|
2061
2089
|
aborting = /* @__PURE__ */ new Set();
|
|
2090
|
+
// Sessions whose exit was triggered by an explicit close/abort/closeAll —
|
|
2091
|
+
// used by the onExit hook to label the resulting exit event as `expected`
|
|
2092
|
+
// (so SwarmCoordinator can skip flipping the role to `crashed`). The set is
|
|
2093
|
+
// populated *before* `adapter.stop()` runs and drained inside the onExit
|
|
2094
|
+
// handler. A leftover entry would only matter if a session id were reused,
|
|
2095
|
+
// which createSession already forbids (line 193 throws on duplicate).
|
|
2096
|
+
expectedExits = /* @__PURE__ */ new Set();
|
|
2062
2097
|
idleSweepTimer = null;
|
|
2063
2098
|
// PR-A: effective sweeper config. Initialized from env at construct time
|
|
2064
2099
|
// and overridable per-instance via startIdleSweeper(overrides) — the
|
|
@@ -2094,6 +2129,14 @@ var SessionManager = class {
|
|
|
2094
2129
|
onSessionError;
|
|
2095
2130
|
onSessionStarted;
|
|
2096
2131
|
onBeforeClose;
|
|
2132
|
+
/**
|
|
2133
|
+
* Fires whenever the underlying child process exits — both expected
|
|
2134
|
+
* (closeSession/abortSession) and unexpected (crash, OOM, external SIGKILL).
|
|
2135
|
+
* `expected` lets the listener distinguish so a swarm coordinator can flip
|
|
2136
|
+
* the role to `crashed` only on unplanned exits. Wired in start.ts to route
|
|
2137
|
+
* swarm-role exits to SwarmCoordinator.handleRoleCrashed.
|
|
2138
|
+
*/
|
|
2139
|
+
onSessionExit;
|
|
2097
2140
|
// PR-A: classifies a sessionId as 'chat' or 'swarm-role'. Injected by
|
|
2098
2141
|
// start.ts via SwarmCoordinator.isSwarmSession to keep SessionManager
|
|
2099
2142
|
// independent of the swarm package. Defaults to 'chat' if absent — safe
|
|
@@ -2107,6 +2150,7 @@ var SessionManager = class {
|
|
|
2107
2150
|
this.onSessionError = options.onSessionError;
|
|
2108
2151
|
this.onSessionStarted = options.onSessionStarted;
|
|
2109
2152
|
this.onBeforeClose = options.onBeforeClose;
|
|
2153
|
+
this.onSessionExit = options.onSessionExit;
|
|
2110
2154
|
this.classify = options.classify ?? (() => "chat");
|
|
2111
2155
|
this.checkpointPath = options.checkpointPath ?? null;
|
|
2112
2156
|
this.checkpointDebounceMs = options.checkpointDebounceMs ?? 5e3;
|
|
@@ -2245,13 +2289,37 @@ ${notice.text}`;
|
|
|
2245
2289
|
}
|
|
2246
2290
|
this.adapter.onExit?.(process2, (code) => {
|
|
2247
2291
|
if (this.sessions.get(options.sessionId) === process2) {
|
|
2292
|
+
const expected = this.expectedExits.delete(options.sessionId);
|
|
2248
2293
|
this.sessions.delete(options.sessionId);
|
|
2249
2294
|
this.sessionMeta.delete(options.sessionId);
|
|
2250
2295
|
this.activelyExecuting.delete(options.sessionId);
|
|
2251
2296
|
this.conversationBuffer.delete(options.sessionId);
|
|
2252
2297
|
this.scheduleCheckpoint();
|
|
2253
|
-
|
|
2254
|
-
|
|
2298
|
+
if (!expected) {
|
|
2299
|
+
log5.warn({ sessionId: options.sessionId, exitCode: code }, "backend process exited unexpectedly, evicted from session map");
|
|
2300
|
+
this.onSessionError(options.sessionId, `backend process exited (code=${code ?? "null"})`);
|
|
2301
|
+
} else {
|
|
2302
|
+
log5.debug({ sessionId: options.sessionId, exitCode: code }, "backend process exited as expected");
|
|
2303
|
+
}
|
|
2304
|
+
try {
|
|
2305
|
+
this.onSessionExit?.(options.sessionId, {
|
|
2306
|
+
code: code ?? null,
|
|
2307
|
+
expected,
|
|
2308
|
+
...expected ? {} : { reason: `exit code=${code ?? "null"}` }
|
|
2309
|
+
});
|
|
2310
|
+
} catch (err) {
|
|
2311
|
+
log5.warn({ err, sessionId: options.sessionId }, "onSessionExit listener threw");
|
|
2312
|
+
}
|
|
2313
|
+
} else {
|
|
2314
|
+
const expected = this.expectedExits.delete(options.sessionId);
|
|
2315
|
+
try {
|
|
2316
|
+
this.onSessionExit?.(options.sessionId, {
|
|
2317
|
+
code: code ?? null,
|
|
2318
|
+
expected
|
|
2319
|
+
});
|
|
2320
|
+
} catch (err) {
|
|
2321
|
+
log5.warn({ err, sessionId: options.sessionId }, "onSessionExit listener threw");
|
|
2322
|
+
}
|
|
2255
2323
|
}
|
|
2256
2324
|
});
|
|
2257
2325
|
return process2.id;
|
|
@@ -2280,6 +2348,7 @@ ${notice.text}`;
|
|
|
2280
2348
|
const process2 = this.sessions.get(sessionId);
|
|
2281
2349
|
if (!process2) return;
|
|
2282
2350
|
this.aborting.add(sessionId);
|
|
2351
|
+
this.expectedExits.add(sessionId);
|
|
2283
2352
|
this.conversationBuffer.delete(sessionId);
|
|
2284
2353
|
this.sessions.delete(sessionId);
|
|
2285
2354
|
this.sessionMeta.delete(sessionId);
|
|
@@ -2300,6 +2369,7 @@ ${notice.text}`;
|
|
|
2300
2369
|
});
|
|
2301
2370
|
}
|
|
2302
2371
|
this.conversationBuffer.delete(sessionId);
|
|
2372
|
+
this.expectedExits.add(sessionId);
|
|
2303
2373
|
this.sessions.delete(sessionId);
|
|
2304
2374
|
this.sessionMeta.delete(sessionId);
|
|
2305
2375
|
this.activelyExecuting.delete(sessionId);
|
|
@@ -2316,6 +2386,7 @@ ${notice.text}`;
|
|
|
2316
2386
|
});
|
|
2317
2387
|
}
|
|
2318
2388
|
this.conversationBuffer.delete(sessionId);
|
|
2389
|
+
this.expectedExits.add(sessionId);
|
|
2319
2390
|
this.sessions.delete(sessionId);
|
|
2320
2391
|
this.sessionMeta.delete(sessionId);
|
|
2321
2392
|
this.activelyExecuting.delete(sessionId);
|
|
@@ -3128,6 +3199,15 @@ async function startAgent(options) {
|
|
|
3128
3199
|
error
|
|
3129
3200
|
});
|
|
3130
3201
|
},
|
|
3202
|
+
onSessionExit: (sessionId, info) => {
|
|
3203
|
+
if (info.expected) return;
|
|
3204
|
+
if (!swarmCoordinator?.isSwarmSession(sessionId)) return;
|
|
3205
|
+
try {
|
|
3206
|
+
swarmCoordinator.handleRoleCrashed(sessionId, info.reason ?? `exit code=${info.code ?? "null"}`);
|
|
3207
|
+
} catch (err) {
|
|
3208
|
+
log9.warn({ err, sessionId }, "swarmCoordinator.handleRoleCrashed threw");
|
|
3209
|
+
}
|
|
3210
|
+
},
|
|
3131
3211
|
onBeforeClose,
|
|
3132
3212
|
// PR-A: classify session kind for the idle sweeper. SessionManager stays
|
|
3133
3213
|
// independent of the swarm package; we hand it a closure that defers to
|
|
@@ -3185,4 +3265,4 @@ export {
|
|
|
3185
3265
|
FsBridge,
|
|
3186
3266
|
startAgent
|
|
3187
3267
|
};
|
|
3188
|
-
//# sourceMappingURL=chunk-
|
|
3268
|
+
//# sourceMappingURL=chunk-LXSWV3VS.js.map
|