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.
- package/dist/{AcpBackend-KEFEf0fa.mjs → AcpBackend-BFFC77vV.mjs} +1 -1
- package/dist/{AcpBackend-a8B0Y7EF.cjs → AcpBackend-CsLnh1So.cjs} +1 -1
- package/dist/{AcpSessionManager-rkoOuyxF.cjs → AcpSessionManager--CixPN-p.cjs} +1 -1
- package/dist/{AcpSessionManager-DpiEroK5.mjs → AcpSessionManager-CU5vsxgF.mjs} +1 -1
- package/dist/{config-gCpPGROw.mjs → config-CRFTYuTN.mjs} +2 -2
- package/dist/{config-Bk2_O4iy.cjs → config-pv17N6Kg.cjs} +2 -2
- package/dist/{index-BpR0Noyc.mjs → index-BkvUwyIU.mjs} +5 -5
- package/dist/{index-KFtRhinT.cjs → index-C1OnwMx0.cjs} +4 -4
- package/dist/{index-DBO5ydFO.mjs → index-DOb57EGb.mjs} +12 -12
- package/dist/{index-Baq7aDcp.cjs → index-Dcv1RKe8.cjs} +15 -15
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{installTerminalHooks-5MMeWip7.mjs → installTerminalHooks-IXvZMGJq.mjs} +2 -2
- package/dist/{installTerminalHooks-CPbpttk4.cjs → installTerminalHooks-YyT3fv6w.cjs} +2 -2
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +18 -0
- package/dist/lib.d.mts +18 -0
- package/dist/lib.mjs +1 -1
- package/dist/{mcp-DYaTASOA.mjs → mcp-B-GdhlKD.mjs} +2 -2
- package/dist/{mcp-C0ai1fwz.cjs → mcp-CsKnmeE5.cjs} +2 -2
- package/dist/{runGemini-DypVM7XW.mjs → runGemini-D0lddF43.mjs} +4 -4
- package/dist/{runGemini-BOhhFluL.cjs → runGemini-DuUdmlVZ.cjs} +4 -4
- package/dist/{runOpenClaw-BLJwhL9y.mjs → runOpenClaw-BiwwYnYf.mjs} +3 -3
- package/dist/{runOpenClaw-CzYqRkGb.cjs → runOpenClaw-DEMCYSoD.cjs} +3 -3
- package/dist/{send-BG-4q_Nb.mjs → send-DM8oqJFp.mjs} +2 -2
- package/dist/{send-B4dnhYKC.cjs → send-DijSXAMp.cjs} +2 -2
- package/dist/{spawn-BXj4rWNL.cjs → spawn-De3b5Hsu.cjs} +2 -2
- package/dist/{spawn-BEwPBgbi.mjs → spawn-nISXbZD5.mjs} +2 -2
- package/dist/{types-kLTNjWVl.mjs → types-D2qSKjsO.mjs} +311 -69
- package/dist/{types-xqPsUU1d.cjs → types-NwtcBgfS.cjs} +312 -70
- 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.
|
|
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.
|
|
146
|
-
"very-happy-tools-x64-darwin": "0.2.
|
|
147
|
-
"very-happy-tools-arm64-linux": "0.2.
|
|
148
|
-
"very-happy-tools-x64-linux": "0.2.
|
|
149
|
-
"very-happy-tools-arm64-win32": "0.2.
|
|
150
|
-
"very-happy-tools-x64-win32": "0.2.
|
|
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 =
|
|
2783
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
13592
|
-
|
|
13593
|
-
|
|
13594
|
-
|
|
13595
|
-
this.
|
|
13596
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() {
|