@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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=hub-connection-reconnect.test.d.ts.map
@@ -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.ws || this.reconnectTimer) return;
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
- log2.warn({ delayMs: this.reconnectDelay }, "reconnecting to hub...");
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
- }, this.reconnectDelay);
1244
- this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
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
- log5.warn({ sessionId: options.sessionId, exitCode: code }, "backend process exited unexpectedly, evicted from session map");
2266
- this.onSessionError(options.sessionId, `backend process exited (code=${code ?? "null"})`);
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-Y4J44CKF.js.map
3268
+ //# sourceMappingURL=chunk-LXSWV3VS.js.map