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
@@ -51,7 +51,7 @@ var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
51
51
  var pty__namespace = /*#__PURE__*/_interopNamespaceDefault(pty);
52
52
 
53
53
  var name = "very-happy-cli";
54
- var version = "0.2.76";
54
+ var version = "0.2.77";
55
55
  var description = "Remote multi-agent workspace CLI with deep Claude integration, native terminal streaming, and self-hosting.";
56
56
  var author = "Kirill Dubovitskiy";
57
57
  var contributors = [
@@ -164,12 +164,12 @@ var dependencies = {
164
164
  zod: "^4.0.0"
165
165
  };
166
166
  var optionalDependencies = {
167
- "very-happy-tools-arm64-darwin": "0.2.76",
168
- "very-happy-tools-x64-darwin": "0.2.76",
169
- "very-happy-tools-arm64-linux": "0.2.76",
170
- "very-happy-tools-x64-linux": "0.2.76",
171
- "very-happy-tools-arm64-win32": "0.2.76",
172
- "very-happy-tools-x64-win32": "0.2.76"
167
+ "very-happy-tools-arm64-darwin": "0.2.77",
168
+ "very-happy-tools-x64-darwin": "0.2.77",
169
+ "very-happy-tools-arm64-linux": "0.2.77",
170
+ "very-happy-tools-x64-linux": "0.2.77",
171
+ "very-happy-tools-arm64-win32": "0.2.77",
172
+ "very-happy-tools-x64-win32": "0.2.77"
173
173
  };
174
174
  var devDependencies = {
175
175
  "@slopus/happy-wire": "workspace:*",
@@ -918,6 +918,15 @@ z.z.object({
918
918
  region: z.z.string().min(1).max(64),
919
919
  version: z.z.string().min(1).max(64)
920
920
  });
921
+ const ReleaseSlotSchema = z.z.enum(["blue", "green"]);
922
+ const ReleaseDrainNoticeSchema = z.z.object({
923
+ epoch: z.z.string().regex(/^[A-Za-z0-9._-]{8,128}$/),
924
+ fromRelease: z.z.string().regex(/^[0-9a-f]{40}$/),
925
+ toRelease: z.z.string().regex(/^[0-9a-f]{40}$/),
926
+ candidateSlot: ReleaseSlotSchema,
927
+ deadline: z.z.number().int().positive(),
928
+ mode: z.z.literal("make-before-break")
929
+ });
921
930
 
922
931
  z.z.object({
923
932
  host: z.z.string(),
@@ -1381,6 +1390,56 @@ class RpcHandlerManager {
1381
1390
  socket.emit("rpc-register", { method: prefixedMethod });
1382
1391
  }
1383
1392
  }
1393
+ /**
1394
+ * Attach a candidate transport and wait until the server confirms every
1395
+ * method before a release handover closes the old transport.
1396
+ */
1397
+ async onSocketConnectAndWait(socket, timeoutMs = 1e4) {
1398
+ this.sockets.add(socket);
1399
+ const pending = new Set(this.handlers.keys());
1400
+ const acknowledged = /* @__PURE__ */ new Set();
1401
+ await new Promise((resolve, reject) => {
1402
+ let settleTimer = null;
1403
+ const timer = setTimeout(() => {
1404
+ cleanup();
1405
+ reject(new Error(`RPC registration timed out (${pending.size} pending)`));
1406
+ }, timeoutMs);
1407
+ timer.unref?.();
1408
+ const maybeSettle = () => {
1409
+ if (pending.size > 0 || settleTimer) return;
1410
+ settleTimer = setTimeout(() => {
1411
+ settleTimer = null;
1412
+ for (const prefixedMethod of this.handlers.keys()) {
1413
+ if (acknowledged.has(prefixedMethod) || pending.has(prefixedMethod)) continue;
1414
+ pending.add(prefixedMethod);
1415
+ socket.emit("rpc-register", { method: prefixedMethod });
1416
+ }
1417
+ if (pending.size === 0) {
1418
+ cleanup();
1419
+ resolve();
1420
+ }
1421
+ }, 0);
1422
+ settleTimer.unref?.();
1423
+ };
1424
+ const onRegistered = (data) => {
1425
+ if (typeof data?.method !== "string") return;
1426
+ acknowledged.add(data.method);
1427
+ pending.delete(data.method);
1428
+ maybeSettle();
1429
+ };
1430
+ const cleanup = () => {
1431
+ clearTimeout(timer);
1432
+ if (settleTimer) clearTimeout(settleTimer);
1433
+ socket.off("rpc-registered", onRegistered);
1434
+ };
1435
+ socket.on("rpc-registered", onRegistered);
1436
+ for (const prefixedMethod of pending) socket.emit("rpc-register", { method: prefixedMethod });
1437
+ maybeSettle();
1438
+ }).catch((error) => {
1439
+ this.sockets.delete(socket);
1440
+ throw error;
1441
+ });
1442
+ }
1384
1443
  onSocketDisconnect(socket) {
1385
1444
  if (socket) this.sockets.delete(socket);
1386
1445
  else this.sockets.clear();
@@ -1415,7 +1474,7 @@ class RpcHandlerManager {
1415
1474
  }
1416
1475
  }
1417
1476
 
1418
- const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-xqPsUU1d.cjs', document.baseURI).href))));
1477
+ const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-NwtcBgfS.cjs', document.baseURI).href))));
1419
1478
  function projectPath() {
1420
1479
  const path = path$1.resolve(__dirname$1, "..");
1421
1480
  return path;
@@ -2782,6 +2841,7 @@ class ApiSessionClient extends node_events.EventEmitter {
2782
2841
  receiveSync;
2783
2842
  directInboundLocalIds = /* @__PURE__ */ new Set();
2784
2843
  routedInboundLocalIds = /* @__PURE__ */ new Set();
2844
+ handoverInFlight = null;
2785
2845
  constructor(token, session) {
2786
2846
  super();
2787
2847
  this.token = token;
@@ -2801,26 +2861,15 @@ class ApiSessionClient extends node_events.EventEmitter {
2801
2861
  logger: (msg, data) => logger.debug(msg, data)
2802
2862
  });
2803
2863
  registerCommonHandlers(this.rpcHandlerManager, this.metadata.path);
2804
- this.socket = socket_ioClient.io(configuration.serverUrl, {
2805
- auth: {
2806
- token: this.token,
2807
- clientType: "session-scoped",
2808
- sessionId: this.sessionId,
2809
- happyClient: `cli-coding-session/${configuration.currentCliVersion}`
2810
- },
2811
- path: "/v1/updates",
2812
- reconnection: false,
2813
- transports: ["websocket"],
2814
- withCredentials: true,
2815
- autoConnect: false
2816
- });
2864
+ this.socket = this.createControlSocket();
2865
+ const controlSocket = this.socket;
2817
2866
  this.socket.on("connect", () => {
2818
2867
  logger.debug("Socket connected successfully");
2819
2868
  if (this.reconnectInterval) {
2820
2869
  clearInterval(this.reconnectInterval);
2821
2870
  this.reconnectInterval = null;
2822
2871
  }
2823
- this.rpcHandlerManager.onSocketConnect(this.socket);
2872
+ this.rpcHandlerManager.onSocketConnect(controlSocket);
2824
2873
  this.receiveSync.invalidate();
2825
2874
  void this.connectSessionRelay();
2826
2875
  });
@@ -2829,12 +2878,12 @@ class ApiSessionClient extends node_events.EventEmitter {
2829
2878
  });
2830
2879
  this.socket.on("disconnect", (reason) => {
2831
2880
  logger.debug(`[API] Socket disconnected: ${reason}`);
2832
- this.rpcHandlerManager.onSocketDisconnect();
2881
+ this.rpcHandlerManager.onSocketDisconnect(controlSocket);
2833
2882
  this.startSmartReconnect();
2834
2883
  });
2835
2884
  this.socket.on("connect_error", (error) => {
2836
2885
  logger.debug("[API] Socket connection error:", error);
2837
- this.rpcHandlerManager.onSocketDisconnect();
2886
+ this.rpcHandlerManager.onSocketDisconnect(controlSocket);
2838
2887
  if (error.message === "Session archived") {
2839
2888
  logger.debug("[API] Archived session rejected during reconnect; exiting");
2840
2889
  this.emit("archived");
@@ -2905,8 +2954,103 @@ class ApiSessionClient extends node_events.EventEmitter {
2905
2954
  this.socket.on("error", (error) => {
2906
2955
  logger.debug("[API] Socket error:", error);
2907
2956
  });
2957
+ this.socket.on("server-draining", (data) => {
2958
+ const parsed = ReleaseDrainNoticeSchema.safeParse(data);
2959
+ if (!parsed.success || parsed.data.deadline <= Date.now()) return;
2960
+ void this.startReleaseHandover(parsed.data);
2961
+ });
2908
2962
  this.socket.connect();
2909
2963
  }
2964
+ createControlSocket(handover) {
2965
+ return socket_ioClient.io(configuration.serverUrl, {
2966
+ auth: {
2967
+ token: this.token,
2968
+ clientType: "session-scoped",
2969
+ sessionId: this.sessionId,
2970
+ happyClient: `cli-coding-session/${configuration.currentCliVersion}`,
2971
+ ...handover ? { handoverEpoch: handover.epoch } : {}
2972
+ },
2973
+ ...handover ? { query: { vh_slot: handover.candidateSlot }, forceNew: true } : {},
2974
+ path: "/v1/updates",
2975
+ reconnection: false,
2976
+ transports: ["websocket"],
2977
+ withCredentials: true,
2978
+ autoConnect: false
2979
+ });
2980
+ }
2981
+ startReleaseHandover(notice) {
2982
+ if (this.handoverInFlight) return this.handoverInFlight;
2983
+ this.handoverInFlight = this.releaseHandover(notice).finally(() => {
2984
+ this.handoverInFlight = null;
2985
+ });
2986
+ return this.handoverInFlight;
2987
+ }
2988
+ async releaseHandover(notice) {
2989
+ const startedAt = Date.now();
2990
+ const previous = this.socket;
2991
+ const candidate = this.createControlSocket(notice);
2992
+ const transferredListeners = /* @__PURE__ */ new Map();
2993
+ for (const listener of previous.listeners("rpc-request")) candidate.on("rpc-request", listener);
2994
+ const timeoutMs = Math.max(1, Math.min(1e4, notice.deadline - Date.now()));
2995
+ try {
2996
+ await new Promise((resolve, reject) => {
2997
+ const timer = setTimeout(() => reject(new Error("release handover timeout")), timeoutMs);
2998
+ candidate.once("connect", () => {
2999
+ clearTimeout(timer);
3000
+ resolve();
3001
+ });
3002
+ candidate.once("connect_error", (error) => {
3003
+ clearTimeout(timer);
3004
+ reject(error);
3005
+ });
3006
+ candidate.connect();
3007
+ });
3008
+ await this.rpcHandlerManager.onSocketConnectAndWait(candidate, timeoutMs);
3009
+ if (this.socket !== previous) {
3010
+ this.rpcHandlerManager.onSocketDisconnect(candidate);
3011
+ candidate.close();
3012
+ return;
3013
+ }
3014
+ for (const event of ["session-archive", "update", "error", "server-draining"]) {
3015
+ const listeners = previous.listeners(event);
3016
+ previous.removeAllListeners(event);
3017
+ for (const listener of listeners) candidate.on(event, listener);
3018
+ transferredListeners.set(event, listeners);
3019
+ }
3020
+ this.socket = candidate;
3021
+ candidate.on("connect", () => {
3022
+ if (this.reconnectInterval) {
3023
+ clearInterval(this.reconnectInterval);
3024
+ this.reconnectInterval = null;
3025
+ }
3026
+ this.rpcHandlerManager.onSocketConnect(candidate);
3027
+ this.receiveSync.invalidate();
3028
+ });
3029
+ candidate.on("disconnect", () => {
3030
+ this.rpcHandlerManager.onSocketDisconnect(candidate);
3031
+ this.startSmartReconnect();
3032
+ });
3033
+ candidate.on("connect_error", () => {
3034
+ this.rpcHandlerManager.onSocketDisconnect(candidate);
3035
+ this.startSmartReconnect();
3036
+ });
3037
+ this.receiveSync.invalidate();
3038
+ candidate.emit("release-handover-result", { result: "success", durationMs: Date.now() - startedAt });
3039
+ this.rpcHandlerManager.onSocketDisconnect(previous);
3040
+ previous.removeAllListeners("disconnect");
3041
+ previous.close();
3042
+ } catch (error) {
3043
+ if (this.socket === candidate) this.socket = previous;
3044
+ for (const [event, listeners] of transferredListeners) {
3045
+ candidate.removeAllListeners(event);
3046
+ for (const listener of listeners) previous.on(event, listener);
3047
+ }
3048
+ this.rpcHandlerManager.onSocketDisconnect(candidate);
3049
+ candidate.close();
3050
+ if (previous.connected) previous.emit("release-handover-result", { result: "failed", durationMs: Date.now() - startedAt });
3051
+ logger.debug(`[API] Release handover failed: ${error instanceof Error ? error.message : error}`);
3052
+ }
3053
+ }
2910
3054
  onUserMessage(callback) {
2911
3055
  this.pendingMessageCallback = callback;
2912
3056
  while (this.pendingMessages.length > 0) {
@@ -13107,6 +13251,7 @@ class ApiMachineClient {
13107
13251
  stopSessionHandler = null;
13108
13252
  listTrackedSessionIds = null;
13109
13253
  reconnectInterval = null;
13254
+ handoverInFlight = null;
13110
13255
  // Terminals negotiated with `encStream` protect the live byte payload with
13111
13256
  // the per-machine key. This keeps it opaque to passive storage/forwarding
13112
13257
  // paths, but is not an operator trust boundary: the trusted relay can
@@ -13567,57 +13712,72 @@ class ApiMachineClient {
13567
13712
  connect() {
13568
13713
  const serverUrl = configuration.serverUrl.replace(/^http/, "ws");
13569
13714
  logger.debug(`[API MACHINE] Connecting to ${serverUrl}`);
13570
- this.socket = socket_ioClient.io(serverUrl, {
13715
+ this.socket = this.createControlSocket();
13716
+ const controlSocket = this.socket;
13717
+ this.bindRealtimeHandlers(controlSocket, "control");
13718
+ controlSocket.on("connect", () => {
13719
+ if (this.socket !== controlSocket) return;
13720
+ this.activateControlSocket(controlSocket);
13721
+ });
13722
+ controlSocket.on("disconnect", (reason) => {
13723
+ logger.debug(`[API MACHINE] Disconnected from server \u2014 reason: ${reason}`);
13724
+ this.rpcHandlerManager.onSocketDisconnect(controlSocket);
13725
+ if (!this.relaySocket?.connected) this.webTerminal.resetSubscribers();
13726
+ this.stopKeepAlive();
13727
+ if (this.socket === controlSocket) this.startSmartReconnect();
13728
+ });
13729
+ this.bindControlDataHandlers(controlSocket);
13730
+ }
13731
+ createControlSocket(handover) {
13732
+ const serverUrl = configuration.serverUrl.replace(/^http/, "ws");
13733
+ return socket_ioClient.io(serverUrl, {
13571
13734
  transports: ["websocket"],
13572
13735
  auth: {
13573
13736
  token: this.token,
13574
13737
  clientType: "machine-scoped",
13575
13738
  machineId: this.machine.id,
13576
- happyClient: `cli-daemon/${configuration.currentCliVersion}`
13739
+ happyClient: `cli-daemon/${configuration.currentCliVersion}`,
13740
+ ...handover ? { handoverEpoch: handover.epoch } : {}
13577
13741
  },
13742
+ ...handover ? { query: { vh_slot: handover.candidateSlot }, forceNew: true } : {},
13578
13743
  path: "/v1/updates",
13579
- reconnection: false
13580
- });
13581
- this.bindRealtimeHandlers(this.socket, "control");
13582
- this.socket.on("connect", () => {
13583
- logger.debug("[API MACHINE] Connected to server");
13584
- if (this.reconnectInterval) {
13585
- clearInterval(this.reconnectInterval);
13586
- this.reconnectInterval = null;
13587
- }
13588
- const initialTerminals = this.webTerminal.buildTerminalList();
13589
- this.webTerminal.primeListSignature(initialTerminals);
13590
- this.updateDaemonState((state) => {
13591
- const now = Date.now();
13592
- const currentState = withCurrentCliUpdateState(state, this.cliUpdateState);
13593
- return {
13594
- ...currentState,
13595
- status: "running",
13596
- pid: process.pid,
13597
- httpPort: this.machine.daemonState?.httpPort,
13598
- startedAt: now,
13599
- webTerminals: { updatedAt: now, terminals: initialTerminals },
13600
- // B-084: closed records survive daemon restarts (persisted
13601
- // in closed-terminals.json), so the connect snapshot ships
13602
- // them too — not just the incremental pushes.
13603
- closedTerminals: this.webTerminal.getClosedTerminals()
13604
- };
13605
- });
13606
- this.webTerminal.startListTracking((terminals) => this.pushTerminalList(terminals));
13607
- this.rpcHandlerManager.onSocketConnect(this.socket);
13608
- this.syncResumeSessionRpcRegistration();
13609
- this.startKeepAlive();
13610
- void this.reconcileArchivedSessions();
13611
- void this.refreshRelayConnection();
13744
+ reconnection: false,
13745
+ ...handover ? { autoConnect: false } : {}
13612
13746
  });
13613
- this.socket.on("disconnect", (reason) => {
13614
- logger.debug(`[API MACHINE] Disconnected from server \u2014 reason: ${reason}`);
13615
- this.rpcHandlerManager.onSocketDisconnect(this.socket);
13616
- if (!this.relaySocket?.connected) this.webTerminal.resetSubscribers();
13617
- this.stopKeepAlive();
13618
- this.startSmartReconnect();
13747
+ }
13748
+ activateControlSocket(socket, rpcAlreadyRegistered = false) {
13749
+ logger.debug("[API MACHINE] Connected to server");
13750
+ if (this.reconnectInterval) {
13751
+ clearInterval(this.reconnectInterval);
13752
+ this.reconnectInterval = null;
13753
+ }
13754
+ const initialTerminals = this.webTerminal.buildTerminalList();
13755
+ this.webTerminal.primeListSignature(initialTerminals);
13756
+ this.updateDaemonState((state) => {
13757
+ const now = Date.now();
13758
+ const currentState = withCurrentCliUpdateState(state, this.cliUpdateState);
13759
+ return {
13760
+ ...currentState,
13761
+ status: "running",
13762
+ pid: process.pid,
13763
+ httpPort: this.machine.daemonState?.httpPort,
13764
+ startedAt: now,
13765
+ webTerminals: { updatedAt: now, terminals: initialTerminals },
13766
+ // B-084: closed records survive daemon restarts (persisted
13767
+ // in closed-terminals.json), so the connect snapshot ships
13768
+ // them too — not just the incremental pushes.
13769
+ closedTerminals: this.webTerminal.getClosedTerminals()
13770
+ };
13619
13771
  });
13620
- this.socket.on("update", (data) => {
13772
+ this.webTerminal.startListTracking((terminals) => this.pushTerminalList(terminals));
13773
+ if (!rpcAlreadyRegistered) this.rpcHandlerManager.onSocketConnect(socket);
13774
+ this.syncResumeSessionRpcRegistration();
13775
+ this.startKeepAlive();
13776
+ void this.reconcileArchivedSessions();
13777
+ void this.refreshRelayConnection();
13778
+ }
13779
+ bindControlDataHandlers(socket) {
13780
+ socket.on("update", (data) => {
13621
13781
  if (data.body.t === "update-machine" && data.body.machineId === this.machine.id) {
13622
13782
  const update = data.body;
13623
13783
  if (update.metadata) {
@@ -13634,13 +13794,83 @@ class ApiMachineClient {
13634
13794
  logger.debug(`[API MACHINE] Received unknown update type: ${data.body.t}`);
13635
13795
  }
13636
13796
  });
13637
- this.socket.on("connect_error", (error) => {
13797
+ socket.on("connect_error", (error) => {
13638
13798
  logger.debug(`[API MACHINE] Connection error: ${error.message}`);
13639
- this.startSmartReconnect();
13799
+ if (this.socket === socket) this.startSmartReconnect();
13640
13800
  });
13641
- this.socket.io.on("error", (error) => {
13801
+ socket.io.on("error", (error) => {
13642
13802
  logger.debug("[API MACHINE] Socket error:", error);
13643
13803
  });
13804
+ socket.on("server-draining", (data) => {
13805
+ if (this.socket !== socket) return;
13806
+ const parsed = ReleaseDrainNoticeSchema.safeParse(data);
13807
+ if (!parsed.success || parsed.data.deadline <= Date.now()) return;
13808
+ void this.startReleaseHandover(parsed.data);
13809
+ });
13810
+ }
13811
+ startReleaseHandover(notice) {
13812
+ if (this.handoverInFlight) return this.handoverInFlight;
13813
+ this.handoverInFlight = this.releaseHandover(notice).finally(() => {
13814
+ this.handoverInFlight = null;
13815
+ });
13816
+ return this.handoverInFlight;
13817
+ }
13818
+ async releaseHandover(notice) {
13819
+ const startedAt = Date.now();
13820
+ const previous = this.socket;
13821
+ const candidate = this.createControlSocket(notice);
13822
+ let commandsTransferred = false;
13823
+ this.bindRpcRequestHandler(candidate, "control");
13824
+ const timeoutMs = Math.max(1, Math.min(1e4, notice.deadline - Date.now()));
13825
+ try {
13826
+ await new Promise((resolve, reject) => {
13827
+ const timer = setTimeout(() => reject(new Error("release handover timeout")), timeoutMs);
13828
+ candidate.once("connect", () => {
13829
+ clearTimeout(timer);
13830
+ resolve();
13831
+ });
13832
+ candidate.once("connect_error", (error) => {
13833
+ clearTimeout(timer);
13834
+ reject(error);
13835
+ });
13836
+ candidate.connect();
13837
+ });
13838
+ await this.rpcHandlerManager.onSocketConnectAndWait(candidate, timeoutMs);
13839
+ if (this.socket !== previous) {
13840
+ this.rpcHandlerManager.onSocketDisconnect(candidate);
13841
+ candidate.close();
13842
+ return;
13843
+ }
13844
+ this.unbindMachineCommandHandlers(previous);
13845
+ this.bindMachineCommandHandlers(candidate);
13846
+ commandsTransferred = true;
13847
+ this.bindControlDataHandlers(candidate);
13848
+ this.socket = candidate;
13849
+ candidate.on("connect", () => {
13850
+ if (this.socket === candidate) this.activateControlSocket(candidate);
13851
+ });
13852
+ candidate.on("disconnect", () => {
13853
+ this.rpcHandlerManager.onSocketDisconnect(candidate);
13854
+ if (!this.relaySocket?.connected) this.webTerminal.resetSubscribers();
13855
+ this.stopKeepAlive();
13856
+ if (this.socket === candidate) this.startSmartReconnect();
13857
+ });
13858
+ this.activateControlSocket(candidate, true);
13859
+ candidate.emit("release-handover-result", { result: "success", durationMs: Date.now() - startedAt });
13860
+ this.rpcHandlerManager.onSocketDisconnect(previous);
13861
+ previous.removeAllListeners("disconnect");
13862
+ previous.close();
13863
+ } catch (error) {
13864
+ if (this.socket === candidate) this.socket = previous;
13865
+ if (commandsTransferred) {
13866
+ this.unbindMachineCommandHandlers(candidate);
13867
+ this.bindMachineCommandHandlers(previous);
13868
+ }
13869
+ this.rpcHandlerManager.onSocketDisconnect(candidate);
13870
+ candidate.close();
13871
+ if (previous.connected) previous.emit("release-handover-result", { result: "failed", durationMs: Date.now() - startedAt });
13872
+ logger.debug(`[API MACHINE] Release handover failed: ${error instanceof Error ? error.message : error}`);
13873
+ }
13644
13874
  }
13645
13875
  startKeepAlive() {
13646
13876
  this.stopKeepAlive();
@@ -13674,11 +13904,13 @@ class ApiMachineClient {
13674
13904
  }, 2e4);
13675
13905
  logger.debug("[API MACHINE] Keep-alive started (20s interval)");
13676
13906
  }
13677
- bindRealtimeHandlers(socket, transport) {
13907
+ bindRpcRequestHandler(socket, transport) {
13678
13908
  socket.on("rpc-request", async (data, callback) => {
13679
13909
  logger.debugLargeJson(`[API MACHINE] Received RPC request via ${transport}:`, data);
13680
13910
  callback(await this.rpcHandlerManager.handleRequest(data));
13681
13911
  });
13912
+ }
13913
+ bindMachineCommandHandlers(socket) {
13682
13914
  socket.on("terminal-input", (data) => {
13683
13915
  let payload = data.data;
13684
13916
  if (data.enc) {
@@ -13696,6 +13928,16 @@ class ApiMachineClient {
13696
13928
  logger.debug(`[API MACHINE] Server archived session ${data.sessionId}; local process stopped=${stopped}`);
13697
13929
  });
13698
13930
  }
13931
+ unbindMachineCommandHandlers(socket) {
13932
+ socket.removeAllListeners("terminal-input");
13933
+ socket.removeAllListeners("terminal-resize");
13934
+ socket.removeAllListeners("terminal-close");
13935
+ socket.removeAllListeners("session-archive");
13936
+ }
13937
+ bindRealtimeHandlers(socket, transport) {
13938
+ this.bindRpcRequestHandler(socket, transport);
13939
+ this.bindMachineCommandHandlers(socket);
13940
+ }
13699
13941
  /** Reconcile commands missed while the daemon was offline. The server
13700
13942
  * returns only ids owned by this account and durably archived in the DB. */
13701
13943
  async reconcileArchivedSessions() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "very-happy-cli",
3
- "version": "0.2.76",
3
+ "version": "0.2.77",
4
4
  "description": "Remote multi-agent workspace CLI with deep Claude integration, native terminal streaming, and self-hosting.",
5
5
  "author": "Kirill Dubovitskiy",
6
6
  "contributors": [
@@ -113,12 +113,12 @@
113
113
  "zod": "^4.0.0"
114
114
  },
115
115
  "optionalDependencies": {
116
- "very-happy-tools-arm64-darwin": "0.2.76",
117
- "very-happy-tools-x64-darwin": "0.2.76",
118
- "very-happy-tools-arm64-linux": "0.2.76",
119
- "very-happy-tools-x64-linux": "0.2.76",
120
- "very-happy-tools-arm64-win32": "0.2.76",
121
- "very-happy-tools-x64-win32": "0.2.76"
116
+ "very-happy-tools-arm64-darwin": "0.2.77",
117
+ "very-happy-tools-x64-darwin": "0.2.77",
118
+ "very-happy-tools-arm64-linux": "0.2.77",
119
+ "very-happy-tools-x64-linux": "0.2.77",
120
+ "very-happy-tools-arm64-win32": "0.2.77",
121
+ "very-happy-tools-x64-win32": "0.2.77"
122
122
  },
123
123
  "devDependencies": {
124
124
  "@slopus/happy-wire": "workspace:*",