very-happy-cli 0.2.76 → 0.2.77

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.
Files changed (31) hide show
  1. package/dist/{AcpBackend-KEFEf0fa.mjs → AcpBackend-BFFC77vV.mjs} +1 -1
  2. package/dist/{AcpBackend-a8B0Y7EF.cjs → AcpBackend-CsLnh1So.cjs} +1 -1
  3. package/dist/{AcpSessionManager-rkoOuyxF.cjs → AcpSessionManager--CixPN-p.cjs} +1 -1
  4. package/dist/{AcpSessionManager-DpiEroK5.mjs → AcpSessionManager-CU5vsxgF.mjs} +1 -1
  5. package/dist/{config-gCpPGROw.mjs → config-CRFTYuTN.mjs} +2 -2
  6. package/dist/{config-Bk2_O4iy.cjs → config-pv17N6Kg.cjs} +2 -2
  7. package/dist/{index-BpR0Noyc.mjs → index-BkvUwyIU.mjs} +5 -5
  8. package/dist/{index-KFtRhinT.cjs → index-C1OnwMx0.cjs} +4 -4
  9. package/dist/{index-DBO5ydFO.mjs → index-DOb57EGb.mjs} +12 -12
  10. package/dist/{index-Baq7aDcp.cjs → index-Dcv1RKe8.cjs} +15 -15
  11. package/dist/index.cjs +2 -2
  12. package/dist/index.mjs +2 -2
  13. package/dist/{installTerminalHooks-5MMeWip7.mjs → installTerminalHooks-IXvZMGJq.mjs} +2 -2
  14. package/dist/{installTerminalHooks-CPbpttk4.cjs → installTerminalHooks-YyT3fv6w.cjs} +2 -2
  15. package/dist/lib.cjs +1 -1
  16. package/dist/lib.d.cts +18 -0
  17. package/dist/lib.d.mts +18 -0
  18. package/dist/lib.mjs +1 -1
  19. package/dist/{mcp-DYaTASOA.mjs → mcp-B-GdhlKD.mjs} +2 -2
  20. package/dist/{mcp-C0ai1fwz.cjs → mcp-CsKnmeE5.cjs} +2 -2
  21. package/dist/{runGemini-DypVM7XW.mjs → runGemini-D0lddF43.mjs} +4 -4
  22. package/dist/{runGemini-BOhhFluL.cjs → runGemini-DuUdmlVZ.cjs} +4 -4
  23. package/dist/{runOpenClaw-BLJwhL9y.mjs → runOpenClaw-BiwwYnYf.mjs} +3 -3
  24. package/dist/{runOpenClaw-CzYqRkGb.cjs → runOpenClaw-DEMCYSoD.cjs} +3 -3
  25. package/dist/{send-BG-4q_Nb.mjs → send-DM8oqJFp.mjs} +2 -2
  26. package/dist/{send-B4dnhYKC.cjs → send-DijSXAMp.cjs} +2 -2
  27. package/dist/{spawn-BXj4rWNL.cjs → spawn-De3b5Hsu.cjs} +2 -2
  28. package/dist/{spawn-BEwPBgbi.mjs → spawn-nISXbZD5.mjs} +2 -2
  29. package/dist/{types-kLTNjWVl.mjs → types-D2qSKjsO.mjs} +311 -69
  30. package/dist/{types-xqPsUU1d.cjs → types-NwtcBgfS.cjs} +312 -70
  31. package/package.json +7 -7
@@ -29,7 +29,7 @@ import { SandboxManager } from '@anthropic-ai/sandbox-runtime';
29
29
  import { Expo } from 'expo-server-sdk';
30
30
 
31
31
  var name = "very-happy-cli";
32
- var version = "0.2.76";
32
+ var version = "0.2.77";
33
33
  var description = "Remote multi-agent workspace CLI with deep Claude integration, native terminal streaming, and self-hosting.";
34
34
  var author = "Kirill Dubovitskiy";
35
35
  var contributors = [
@@ -142,12 +142,12 @@ var dependencies = {
142
142
  zod: "^4.0.0"
143
143
  };
144
144
  var optionalDependencies = {
145
- "very-happy-tools-arm64-darwin": "0.2.76",
146
- "very-happy-tools-x64-darwin": "0.2.76",
147
- "very-happy-tools-arm64-linux": "0.2.76",
148
- "very-happy-tools-x64-linux": "0.2.76",
149
- "very-happy-tools-arm64-win32": "0.2.76",
150
- "very-happy-tools-x64-win32": "0.2.76"
145
+ "very-happy-tools-arm64-darwin": "0.2.77",
146
+ "very-happy-tools-x64-darwin": "0.2.77",
147
+ "very-happy-tools-arm64-linux": "0.2.77",
148
+ "very-happy-tools-x64-linux": "0.2.77",
149
+ "very-happy-tools-arm64-win32": "0.2.77",
150
+ "very-happy-tools-x64-win32": "0.2.77"
151
151
  };
152
152
  var devDependencies = {
153
153
  "@slopus/happy-wire": "workspace:*",
@@ -896,6 +896,15 @@ z$1.object({
896
896
  region: z$1.string().min(1).max(64),
897
897
  version: z$1.string().min(1).max(64)
898
898
  });
899
+ const ReleaseSlotSchema = z$1.enum(["blue", "green"]);
900
+ const ReleaseDrainNoticeSchema = z$1.object({
901
+ epoch: z$1.string().regex(/^[A-Za-z0-9._-]{8,128}$/),
902
+ fromRelease: z$1.string().regex(/^[0-9a-f]{40}$/),
903
+ toRelease: z$1.string().regex(/^[0-9a-f]{40}$/),
904
+ candidateSlot: ReleaseSlotSchema,
905
+ deadline: z$1.number().int().positive(),
906
+ mode: z$1.literal("make-before-break")
907
+ });
899
908
 
900
909
  z$1.object({
901
910
  host: z$1.string(),
@@ -1359,6 +1368,56 @@ class RpcHandlerManager {
1359
1368
  socket.emit("rpc-register", { method: prefixedMethod });
1360
1369
  }
1361
1370
  }
1371
+ /**
1372
+ * Attach a candidate transport and wait until the server confirms every
1373
+ * method before a release handover closes the old transport.
1374
+ */
1375
+ async onSocketConnectAndWait(socket, timeoutMs = 1e4) {
1376
+ this.sockets.add(socket);
1377
+ const pending = new Set(this.handlers.keys());
1378
+ const acknowledged = /* @__PURE__ */ new Set();
1379
+ await new Promise((resolve, reject) => {
1380
+ let settleTimer = null;
1381
+ const timer = setTimeout(() => {
1382
+ cleanup();
1383
+ reject(new Error(`RPC registration timed out (${pending.size} pending)`));
1384
+ }, timeoutMs);
1385
+ timer.unref?.();
1386
+ const maybeSettle = () => {
1387
+ if (pending.size > 0 || settleTimer) return;
1388
+ settleTimer = setTimeout(() => {
1389
+ settleTimer = null;
1390
+ for (const prefixedMethod of this.handlers.keys()) {
1391
+ if (acknowledged.has(prefixedMethod) || pending.has(prefixedMethod)) continue;
1392
+ pending.add(prefixedMethod);
1393
+ socket.emit("rpc-register", { method: prefixedMethod });
1394
+ }
1395
+ if (pending.size === 0) {
1396
+ cleanup();
1397
+ resolve();
1398
+ }
1399
+ }, 0);
1400
+ settleTimer.unref?.();
1401
+ };
1402
+ const onRegistered = (data) => {
1403
+ if (typeof data?.method !== "string") return;
1404
+ acknowledged.add(data.method);
1405
+ pending.delete(data.method);
1406
+ maybeSettle();
1407
+ };
1408
+ const cleanup = () => {
1409
+ clearTimeout(timer);
1410
+ if (settleTimer) clearTimeout(settleTimer);
1411
+ socket.off("rpc-registered", onRegistered);
1412
+ };
1413
+ socket.on("rpc-registered", onRegistered);
1414
+ for (const prefixedMethod of pending) socket.emit("rpc-register", { method: prefixedMethod });
1415
+ maybeSettle();
1416
+ }).catch((error) => {
1417
+ this.sockets.delete(socket);
1418
+ throw error;
1419
+ });
1420
+ }
1362
1421
  onSocketDisconnect(socket) {
1363
1422
  if (socket) this.sockets.delete(socket);
1364
1423
  else this.sockets.clear();
@@ -2760,6 +2819,7 @@ class ApiSessionClient extends EventEmitter {
2760
2819
  receiveSync;
2761
2820
  directInboundLocalIds = /* @__PURE__ */ new Set();
2762
2821
  routedInboundLocalIds = /* @__PURE__ */ new Set();
2822
+ handoverInFlight = null;
2763
2823
  constructor(token, session) {
2764
2824
  super();
2765
2825
  this.token = token;
@@ -2779,26 +2839,15 @@ class ApiSessionClient extends EventEmitter {
2779
2839
  logger: (msg, data) => logger.debug(msg, data)
2780
2840
  });
2781
2841
  registerCommonHandlers(this.rpcHandlerManager, this.metadata.path);
2782
- this.socket = io(configuration.serverUrl, {
2783
- auth: {
2784
- token: this.token,
2785
- clientType: "session-scoped",
2786
- sessionId: this.sessionId,
2787
- happyClient: `cli-coding-session/${configuration.currentCliVersion}`
2788
- },
2789
- path: "/v1/updates",
2790
- reconnection: false,
2791
- transports: ["websocket"],
2792
- withCredentials: true,
2793
- autoConnect: false
2794
- });
2842
+ this.socket = this.createControlSocket();
2843
+ const controlSocket = this.socket;
2795
2844
  this.socket.on("connect", () => {
2796
2845
  logger.debug("Socket connected successfully");
2797
2846
  if (this.reconnectInterval) {
2798
2847
  clearInterval(this.reconnectInterval);
2799
2848
  this.reconnectInterval = null;
2800
2849
  }
2801
- this.rpcHandlerManager.onSocketConnect(this.socket);
2850
+ this.rpcHandlerManager.onSocketConnect(controlSocket);
2802
2851
  this.receiveSync.invalidate();
2803
2852
  void this.connectSessionRelay();
2804
2853
  });
@@ -2807,12 +2856,12 @@ class ApiSessionClient extends EventEmitter {
2807
2856
  });
2808
2857
  this.socket.on("disconnect", (reason) => {
2809
2858
  logger.debug(`[API] Socket disconnected: ${reason}`);
2810
- this.rpcHandlerManager.onSocketDisconnect();
2859
+ this.rpcHandlerManager.onSocketDisconnect(controlSocket);
2811
2860
  this.startSmartReconnect();
2812
2861
  });
2813
2862
  this.socket.on("connect_error", (error) => {
2814
2863
  logger.debug("[API] Socket connection error:", error);
2815
- this.rpcHandlerManager.onSocketDisconnect();
2864
+ this.rpcHandlerManager.onSocketDisconnect(controlSocket);
2816
2865
  if (error.message === "Session archived") {
2817
2866
  logger.debug("[API] Archived session rejected during reconnect; exiting");
2818
2867
  this.emit("archived");
@@ -2883,8 +2932,103 @@ class ApiSessionClient extends EventEmitter {
2883
2932
  this.socket.on("error", (error) => {
2884
2933
  logger.debug("[API] Socket error:", error);
2885
2934
  });
2935
+ this.socket.on("server-draining", (data) => {
2936
+ const parsed = ReleaseDrainNoticeSchema.safeParse(data);
2937
+ if (!parsed.success || parsed.data.deadline <= Date.now()) return;
2938
+ void this.startReleaseHandover(parsed.data);
2939
+ });
2886
2940
  this.socket.connect();
2887
2941
  }
2942
+ createControlSocket(handover) {
2943
+ return io(configuration.serverUrl, {
2944
+ auth: {
2945
+ token: this.token,
2946
+ clientType: "session-scoped",
2947
+ sessionId: this.sessionId,
2948
+ happyClient: `cli-coding-session/${configuration.currentCliVersion}`,
2949
+ ...handover ? { handoverEpoch: handover.epoch } : {}
2950
+ },
2951
+ ...handover ? { query: { vh_slot: handover.candidateSlot }, forceNew: true } : {},
2952
+ path: "/v1/updates",
2953
+ reconnection: false,
2954
+ transports: ["websocket"],
2955
+ withCredentials: true,
2956
+ autoConnect: false
2957
+ });
2958
+ }
2959
+ startReleaseHandover(notice) {
2960
+ if (this.handoverInFlight) return this.handoverInFlight;
2961
+ this.handoverInFlight = this.releaseHandover(notice).finally(() => {
2962
+ this.handoverInFlight = null;
2963
+ });
2964
+ return this.handoverInFlight;
2965
+ }
2966
+ async releaseHandover(notice) {
2967
+ const startedAt = Date.now();
2968
+ const previous = this.socket;
2969
+ const candidate = this.createControlSocket(notice);
2970
+ const transferredListeners = /* @__PURE__ */ new Map();
2971
+ for (const listener of previous.listeners("rpc-request")) candidate.on("rpc-request", listener);
2972
+ const timeoutMs = Math.max(1, Math.min(1e4, notice.deadline - Date.now()));
2973
+ try {
2974
+ await new Promise((resolve, reject) => {
2975
+ const timer = setTimeout(() => reject(new Error("release handover timeout")), timeoutMs);
2976
+ candidate.once("connect", () => {
2977
+ clearTimeout(timer);
2978
+ resolve();
2979
+ });
2980
+ candidate.once("connect_error", (error) => {
2981
+ clearTimeout(timer);
2982
+ reject(error);
2983
+ });
2984
+ candidate.connect();
2985
+ });
2986
+ await this.rpcHandlerManager.onSocketConnectAndWait(candidate, timeoutMs);
2987
+ if (this.socket !== previous) {
2988
+ this.rpcHandlerManager.onSocketDisconnect(candidate);
2989
+ candidate.close();
2990
+ return;
2991
+ }
2992
+ for (const event of ["session-archive", "update", "error", "server-draining"]) {
2993
+ const listeners = previous.listeners(event);
2994
+ previous.removeAllListeners(event);
2995
+ for (const listener of listeners) candidate.on(event, listener);
2996
+ transferredListeners.set(event, listeners);
2997
+ }
2998
+ this.socket = candidate;
2999
+ candidate.on("connect", () => {
3000
+ if (this.reconnectInterval) {
3001
+ clearInterval(this.reconnectInterval);
3002
+ this.reconnectInterval = null;
3003
+ }
3004
+ this.rpcHandlerManager.onSocketConnect(candidate);
3005
+ this.receiveSync.invalidate();
3006
+ });
3007
+ candidate.on("disconnect", () => {
3008
+ this.rpcHandlerManager.onSocketDisconnect(candidate);
3009
+ this.startSmartReconnect();
3010
+ });
3011
+ candidate.on("connect_error", () => {
3012
+ this.rpcHandlerManager.onSocketDisconnect(candidate);
3013
+ this.startSmartReconnect();
3014
+ });
3015
+ this.receiveSync.invalidate();
3016
+ candidate.emit("release-handover-result", { result: "success", durationMs: Date.now() - startedAt });
3017
+ this.rpcHandlerManager.onSocketDisconnect(previous);
3018
+ previous.removeAllListeners("disconnect");
3019
+ previous.close();
3020
+ } catch (error) {
3021
+ if (this.socket === candidate) this.socket = previous;
3022
+ for (const [event, listeners] of transferredListeners) {
3023
+ candidate.removeAllListeners(event);
3024
+ for (const listener of listeners) previous.on(event, listener);
3025
+ }
3026
+ this.rpcHandlerManager.onSocketDisconnect(candidate);
3027
+ candidate.close();
3028
+ if (previous.connected) previous.emit("release-handover-result", { result: "failed", durationMs: Date.now() - startedAt });
3029
+ logger.debug(`[API] Release handover failed: ${error instanceof Error ? error.message : error}`);
3030
+ }
3031
+ }
2888
3032
  onUserMessage(callback) {
2889
3033
  this.pendingMessageCallback = callback;
2890
3034
  while (this.pendingMessages.length > 0) {
@@ -13085,6 +13229,7 @@ class ApiMachineClient {
13085
13229
  stopSessionHandler = null;
13086
13230
  listTrackedSessionIds = null;
13087
13231
  reconnectInterval = null;
13232
+ handoverInFlight = null;
13088
13233
  // Terminals negotiated with `encStream` protect the live byte payload with
13089
13234
  // the per-machine key. This keeps it opaque to passive storage/forwarding
13090
13235
  // paths, but is not an operator trust boundary: the trusted relay can
@@ -13545,57 +13690,72 @@ class ApiMachineClient {
13545
13690
  connect() {
13546
13691
  const serverUrl = configuration.serverUrl.replace(/^http/, "ws");
13547
13692
  logger.debug(`[API MACHINE] Connecting to ${serverUrl}`);
13548
- this.socket = io(serverUrl, {
13693
+ this.socket = this.createControlSocket();
13694
+ const controlSocket = this.socket;
13695
+ this.bindRealtimeHandlers(controlSocket, "control");
13696
+ controlSocket.on("connect", () => {
13697
+ if (this.socket !== controlSocket) return;
13698
+ this.activateControlSocket(controlSocket);
13699
+ });
13700
+ controlSocket.on("disconnect", (reason) => {
13701
+ logger.debug(`[API MACHINE] Disconnected from server \u2014 reason: ${reason}`);
13702
+ this.rpcHandlerManager.onSocketDisconnect(controlSocket);
13703
+ if (!this.relaySocket?.connected) this.webTerminal.resetSubscribers();
13704
+ this.stopKeepAlive();
13705
+ if (this.socket === controlSocket) this.startSmartReconnect();
13706
+ });
13707
+ this.bindControlDataHandlers(controlSocket);
13708
+ }
13709
+ createControlSocket(handover) {
13710
+ const serverUrl = configuration.serverUrl.replace(/^http/, "ws");
13711
+ return io(serverUrl, {
13549
13712
  transports: ["websocket"],
13550
13713
  auth: {
13551
13714
  token: this.token,
13552
13715
  clientType: "machine-scoped",
13553
13716
  machineId: this.machine.id,
13554
- happyClient: `cli-daemon/${configuration.currentCliVersion}`
13717
+ happyClient: `cli-daemon/${configuration.currentCliVersion}`,
13718
+ ...handover ? { handoverEpoch: handover.epoch } : {}
13555
13719
  },
13720
+ ...handover ? { query: { vh_slot: handover.candidateSlot }, forceNew: true } : {},
13556
13721
  path: "/v1/updates",
13557
- reconnection: false
13558
- });
13559
- this.bindRealtimeHandlers(this.socket, "control");
13560
- this.socket.on("connect", () => {
13561
- logger.debug("[API MACHINE] Connected to server");
13562
- if (this.reconnectInterval) {
13563
- clearInterval(this.reconnectInterval);
13564
- this.reconnectInterval = null;
13565
- }
13566
- const initialTerminals = this.webTerminal.buildTerminalList();
13567
- this.webTerminal.primeListSignature(initialTerminals);
13568
- this.updateDaemonState((state) => {
13569
- const now = Date.now();
13570
- const currentState = withCurrentCliUpdateState(state, this.cliUpdateState);
13571
- return {
13572
- ...currentState,
13573
- status: "running",
13574
- pid: process.pid,
13575
- httpPort: this.machine.daemonState?.httpPort,
13576
- startedAt: now,
13577
- webTerminals: { updatedAt: now, terminals: initialTerminals },
13578
- // B-084: closed records survive daemon restarts (persisted
13579
- // in closed-terminals.json), so the connect snapshot ships
13580
- // them too — not just the incremental pushes.
13581
- closedTerminals: this.webTerminal.getClosedTerminals()
13582
- };
13583
- });
13584
- this.webTerminal.startListTracking((terminals) => this.pushTerminalList(terminals));
13585
- this.rpcHandlerManager.onSocketConnect(this.socket);
13586
- this.syncResumeSessionRpcRegistration();
13587
- this.startKeepAlive();
13588
- void this.reconcileArchivedSessions();
13589
- void this.refreshRelayConnection();
13722
+ reconnection: false,
13723
+ ...handover ? { autoConnect: false } : {}
13590
13724
  });
13591
- this.socket.on("disconnect", (reason) => {
13592
- logger.debug(`[API MACHINE] Disconnected from server \u2014 reason: ${reason}`);
13593
- this.rpcHandlerManager.onSocketDisconnect(this.socket);
13594
- if (!this.relaySocket?.connected) this.webTerminal.resetSubscribers();
13595
- this.stopKeepAlive();
13596
- this.startSmartReconnect();
13725
+ }
13726
+ activateControlSocket(socket, rpcAlreadyRegistered = false) {
13727
+ logger.debug("[API MACHINE] Connected to server");
13728
+ if (this.reconnectInterval) {
13729
+ clearInterval(this.reconnectInterval);
13730
+ this.reconnectInterval = null;
13731
+ }
13732
+ const initialTerminals = this.webTerminal.buildTerminalList();
13733
+ this.webTerminal.primeListSignature(initialTerminals);
13734
+ this.updateDaemonState((state) => {
13735
+ const now = Date.now();
13736
+ const currentState = withCurrentCliUpdateState(state, this.cliUpdateState);
13737
+ return {
13738
+ ...currentState,
13739
+ status: "running",
13740
+ pid: process.pid,
13741
+ httpPort: this.machine.daemonState?.httpPort,
13742
+ startedAt: now,
13743
+ webTerminals: { updatedAt: now, terminals: initialTerminals },
13744
+ // B-084: closed records survive daemon restarts (persisted
13745
+ // in closed-terminals.json), so the connect snapshot ships
13746
+ // them too — not just the incremental pushes.
13747
+ closedTerminals: this.webTerminal.getClosedTerminals()
13748
+ };
13597
13749
  });
13598
- this.socket.on("update", (data) => {
13750
+ this.webTerminal.startListTracking((terminals) => this.pushTerminalList(terminals));
13751
+ if (!rpcAlreadyRegistered) this.rpcHandlerManager.onSocketConnect(socket);
13752
+ this.syncResumeSessionRpcRegistration();
13753
+ this.startKeepAlive();
13754
+ void this.reconcileArchivedSessions();
13755
+ void this.refreshRelayConnection();
13756
+ }
13757
+ bindControlDataHandlers(socket) {
13758
+ socket.on("update", (data) => {
13599
13759
  if (data.body.t === "update-machine" && data.body.machineId === this.machine.id) {
13600
13760
  const update = data.body;
13601
13761
  if (update.metadata) {
@@ -13612,13 +13772,83 @@ class ApiMachineClient {
13612
13772
  logger.debug(`[API MACHINE] Received unknown update type: ${data.body.t}`);
13613
13773
  }
13614
13774
  });
13615
- this.socket.on("connect_error", (error) => {
13775
+ socket.on("connect_error", (error) => {
13616
13776
  logger.debug(`[API MACHINE] Connection error: ${error.message}`);
13617
- this.startSmartReconnect();
13777
+ if (this.socket === socket) this.startSmartReconnect();
13618
13778
  });
13619
- this.socket.io.on("error", (error) => {
13779
+ socket.io.on("error", (error) => {
13620
13780
  logger.debug("[API MACHINE] Socket error:", error);
13621
13781
  });
13782
+ socket.on("server-draining", (data) => {
13783
+ if (this.socket !== socket) return;
13784
+ const parsed = ReleaseDrainNoticeSchema.safeParse(data);
13785
+ if (!parsed.success || parsed.data.deadline <= Date.now()) return;
13786
+ void this.startReleaseHandover(parsed.data);
13787
+ });
13788
+ }
13789
+ startReleaseHandover(notice) {
13790
+ if (this.handoverInFlight) return this.handoverInFlight;
13791
+ this.handoverInFlight = this.releaseHandover(notice).finally(() => {
13792
+ this.handoverInFlight = null;
13793
+ });
13794
+ return this.handoverInFlight;
13795
+ }
13796
+ async releaseHandover(notice) {
13797
+ const startedAt = Date.now();
13798
+ const previous = this.socket;
13799
+ const candidate = this.createControlSocket(notice);
13800
+ let commandsTransferred = false;
13801
+ this.bindRpcRequestHandler(candidate, "control");
13802
+ const timeoutMs = Math.max(1, Math.min(1e4, notice.deadline - Date.now()));
13803
+ try {
13804
+ await new Promise((resolve, reject) => {
13805
+ const timer = setTimeout(() => reject(new Error("release handover timeout")), timeoutMs);
13806
+ candidate.once("connect", () => {
13807
+ clearTimeout(timer);
13808
+ resolve();
13809
+ });
13810
+ candidate.once("connect_error", (error) => {
13811
+ clearTimeout(timer);
13812
+ reject(error);
13813
+ });
13814
+ candidate.connect();
13815
+ });
13816
+ await this.rpcHandlerManager.onSocketConnectAndWait(candidate, timeoutMs);
13817
+ if (this.socket !== previous) {
13818
+ this.rpcHandlerManager.onSocketDisconnect(candidate);
13819
+ candidate.close();
13820
+ return;
13821
+ }
13822
+ this.unbindMachineCommandHandlers(previous);
13823
+ this.bindMachineCommandHandlers(candidate);
13824
+ commandsTransferred = true;
13825
+ this.bindControlDataHandlers(candidate);
13826
+ this.socket = candidate;
13827
+ candidate.on("connect", () => {
13828
+ if (this.socket === candidate) this.activateControlSocket(candidate);
13829
+ });
13830
+ candidate.on("disconnect", () => {
13831
+ this.rpcHandlerManager.onSocketDisconnect(candidate);
13832
+ if (!this.relaySocket?.connected) this.webTerminal.resetSubscribers();
13833
+ this.stopKeepAlive();
13834
+ if (this.socket === candidate) this.startSmartReconnect();
13835
+ });
13836
+ this.activateControlSocket(candidate, true);
13837
+ candidate.emit("release-handover-result", { result: "success", durationMs: Date.now() - startedAt });
13838
+ this.rpcHandlerManager.onSocketDisconnect(previous);
13839
+ previous.removeAllListeners("disconnect");
13840
+ previous.close();
13841
+ } catch (error) {
13842
+ if (this.socket === candidate) this.socket = previous;
13843
+ if (commandsTransferred) {
13844
+ this.unbindMachineCommandHandlers(candidate);
13845
+ this.bindMachineCommandHandlers(previous);
13846
+ }
13847
+ this.rpcHandlerManager.onSocketDisconnect(candidate);
13848
+ candidate.close();
13849
+ if (previous.connected) previous.emit("release-handover-result", { result: "failed", durationMs: Date.now() - startedAt });
13850
+ logger.debug(`[API MACHINE] Release handover failed: ${error instanceof Error ? error.message : error}`);
13851
+ }
13622
13852
  }
13623
13853
  startKeepAlive() {
13624
13854
  this.stopKeepAlive();
@@ -13652,11 +13882,13 @@ class ApiMachineClient {
13652
13882
  }, 2e4);
13653
13883
  logger.debug("[API MACHINE] Keep-alive started (20s interval)");
13654
13884
  }
13655
- bindRealtimeHandlers(socket, transport) {
13885
+ bindRpcRequestHandler(socket, transport) {
13656
13886
  socket.on("rpc-request", async (data, callback) => {
13657
13887
  logger.debugLargeJson(`[API MACHINE] Received RPC request via ${transport}:`, data);
13658
13888
  callback(await this.rpcHandlerManager.handleRequest(data));
13659
13889
  });
13890
+ }
13891
+ bindMachineCommandHandlers(socket) {
13660
13892
  socket.on("terminal-input", (data) => {
13661
13893
  let payload = data.data;
13662
13894
  if (data.enc) {
@@ -13674,6 +13906,16 @@ class ApiMachineClient {
13674
13906
  logger.debug(`[API MACHINE] Server archived session ${data.sessionId}; local process stopped=${stopped}`);
13675
13907
  });
13676
13908
  }
13909
+ unbindMachineCommandHandlers(socket) {
13910
+ socket.removeAllListeners("terminal-input");
13911
+ socket.removeAllListeners("terminal-resize");
13912
+ socket.removeAllListeners("terminal-close");
13913
+ socket.removeAllListeners("session-archive");
13914
+ }
13915
+ bindRealtimeHandlers(socket, transport) {
13916
+ this.bindRpcRequestHandler(socket, transport);
13917
+ this.bindMachineCommandHandlers(socket);
13918
+ }
13677
13919
  /** Reconcile commands missed while the daemon was offline. The server
13678
13920
  * returns only ids owned by this account and durably archived in the DB. */
13679
13921
  async reconcileArchivedSessions() {