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
|
@@ -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.
|
|
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.
|
|
168
|
-
"very-happy-tools-x64-darwin": "0.2.
|
|
169
|
-
"very-happy-tools-arm64-linux": "0.2.
|
|
170
|
-
"very-happy-tools-x64-linux": "0.2.
|
|
171
|
-
"very-happy-tools-arm64-win32": "0.2.
|
|
172
|
-
"very-happy-tools-x64-win32": "0.2.
|
|
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-
|
|
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 =
|
|
2805
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
13614
|
-
|
|
13615
|
-
|
|
13616
|
-
|
|
13617
|
-
this.
|
|
13618
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
117
|
-
"very-happy-tools-x64-darwin": "0.2.
|
|
118
|
-
"very-happy-tools-arm64-linux": "0.2.
|
|
119
|
-
"very-happy-tools-x64-linux": "0.2.
|
|
120
|
-
"very-happy-tools-arm64-win32": "0.2.
|
|
121
|
-
"very-happy-tools-x64-win32": "0.2.
|
|
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:*",
|