@mclawnet/agent 0.6.29 → 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-Y4J44CKF.js → chunk-LXSWV3VS.js} +79 -11
- package/dist/chunk-LXSWV3VS.js.map +1 -0
- 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-Y4J44CKF.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) {
|
|
@@ -1236,12 +1249,15 @@ var HubConnection = class {
|
|
|
1236
1249
|
scheduleReconnect() {
|
|
1237
1250
|
if (this.destroyed) return;
|
|
1238
1251
|
if (this.reconnectTimer) return;
|
|
1239
|
-
|
|
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...");
|
|
1240
1256
|
this.reconnectTimer = setTimeout(() => {
|
|
1241
1257
|
this.reconnectTimer = null;
|
|
1242
1258
|
this.connect();
|
|
1243
|
-
},
|
|
1244
|
-
this.reconnectDelay = Math.min(
|
|
1259
|
+
}, delay);
|
|
1260
|
+
this.reconnectDelay = Math.min(cap * 2, this.maxReconnectDelay);
|
|
1245
1261
|
}
|
|
1246
1262
|
cleanup() {
|
|
1247
1263
|
this.cleanupConnection();
|
|
@@ -2071,6 +2087,13 @@ var SessionManager = class {
|
|
|
2071
2087
|
// correctly routed to the "spawn new + --resume" branch instead of trying
|
|
2072
2088
|
// to write to a process we are about to kill.
|
|
2073
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();
|
|
2074
2097
|
idleSweepTimer = null;
|
|
2075
2098
|
// PR-A: effective sweeper config. Initialized from env at construct time
|
|
2076
2099
|
// and overridable per-instance via startIdleSweeper(overrides) — the
|
|
@@ -2106,6 +2129,14 @@ var SessionManager = class {
|
|
|
2106
2129
|
onSessionError;
|
|
2107
2130
|
onSessionStarted;
|
|
2108
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;
|
|
2109
2140
|
// PR-A: classifies a sessionId as 'chat' or 'swarm-role'. Injected by
|
|
2110
2141
|
// start.ts via SwarmCoordinator.isSwarmSession to keep SessionManager
|
|
2111
2142
|
// independent of the swarm package. Defaults to 'chat' if absent — safe
|
|
@@ -2119,6 +2150,7 @@ var SessionManager = class {
|
|
|
2119
2150
|
this.onSessionError = options.onSessionError;
|
|
2120
2151
|
this.onSessionStarted = options.onSessionStarted;
|
|
2121
2152
|
this.onBeforeClose = options.onBeforeClose;
|
|
2153
|
+
this.onSessionExit = options.onSessionExit;
|
|
2122
2154
|
this.classify = options.classify ?? (() => "chat");
|
|
2123
2155
|
this.checkpointPath = options.checkpointPath ?? null;
|
|
2124
2156
|
this.checkpointDebounceMs = options.checkpointDebounceMs ?? 5e3;
|
|
@@ -2257,13 +2289,37 @@ ${notice.text}`;
|
|
|
2257
2289
|
}
|
|
2258
2290
|
this.adapter.onExit?.(process2, (code) => {
|
|
2259
2291
|
if (this.sessions.get(options.sessionId) === process2) {
|
|
2292
|
+
const expected = this.expectedExits.delete(options.sessionId);
|
|
2260
2293
|
this.sessions.delete(options.sessionId);
|
|
2261
2294
|
this.sessionMeta.delete(options.sessionId);
|
|
2262
2295
|
this.activelyExecuting.delete(options.sessionId);
|
|
2263
2296
|
this.conversationBuffer.delete(options.sessionId);
|
|
2264
2297
|
this.scheduleCheckpoint();
|
|
2265
|
-
|
|
2266
|
-
|
|
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
|
+
}
|
|
2267
2323
|
}
|
|
2268
2324
|
});
|
|
2269
2325
|
return process2.id;
|
|
@@ -2292,6 +2348,7 @@ ${notice.text}`;
|
|
|
2292
2348
|
const process2 = this.sessions.get(sessionId);
|
|
2293
2349
|
if (!process2) return;
|
|
2294
2350
|
this.aborting.add(sessionId);
|
|
2351
|
+
this.expectedExits.add(sessionId);
|
|
2295
2352
|
this.conversationBuffer.delete(sessionId);
|
|
2296
2353
|
this.sessions.delete(sessionId);
|
|
2297
2354
|
this.sessionMeta.delete(sessionId);
|
|
@@ -2312,6 +2369,7 @@ ${notice.text}`;
|
|
|
2312
2369
|
});
|
|
2313
2370
|
}
|
|
2314
2371
|
this.conversationBuffer.delete(sessionId);
|
|
2372
|
+
this.expectedExits.add(sessionId);
|
|
2315
2373
|
this.sessions.delete(sessionId);
|
|
2316
2374
|
this.sessionMeta.delete(sessionId);
|
|
2317
2375
|
this.activelyExecuting.delete(sessionId);
|
|
@@ -2328,6 +2386,7 @@ ${notice.text}`;
|
|
|
2328
2386
|
});
|
|
2329
2387
|
}
|
|
2330
2388
|
this.conversationBuffer.delete(sessionId);
|
|
2389
|
+
this.expectedExits.add(sessionId);
|
|
2331
2390
|
this.sessions.delete(sessionId);
|
|
2332
2391
|
this.sessionMeta.delete(sessionId);
|
|
2333
2392
|
this.activelyExecuting.delete(sessionId);
|
|
@@ -3140,6 +3199,15 @@ async function startAgent(options) {
|
|
|
3140
3199
|
error
|
|
3141
3200
|
});
|
|
3142
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
|
+
},
|
|
3143
3211
|
onBeforeClose,
|
|
3144
3212
|
// PR-A: classify session kind for the idle sweeper. SessionManager stays
|
|
3145
3213
|
// independent of the swarm package; we hand it a closure that defers to
|
|
@@ -3197,4 +3265,4 @@ export {
|
|
|
3197
3265
|
FsBridge,
|
|
3198
3266
|
startAgent
|
|
3199
3267
|
};
|
|
3200
|
-
//# sourceMappingURL=chunk-
|
|
3268
|
+
//# sourceMappingURL=chunk-LXSWV3VS.js.map
|