very-happy-cli 0.2.75 → 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-5CPQMP3H.mjs → AcpBackend-BFFC77vV.mjs} +5 -2
- package/dist/{AcpBackend-B0qCm-bY.cjs → AcpBackend-CsLnh1So.cjs} +5 -2
- package/dist/{AcpSessionManager-B4M0y5fI.cjs → AcpSessionManager--CixPN-p.cjs} +1 -1
- package/dist/{AcpSessionManager-BTmJyKDO.mjs → AcpSessionManager-CU5vsxgF.mjs} +1 -1
- package/dist/{config-DbY2R-H7.mjs → config-CRFTYuTN.mjs} +2 -2
- package/dist/{config-DBIfKaNJ.cjs → config-pv17N6Kg.cjs} +2 -2
- package/dist/{index-CJaqyq4z.mjs → index-BkvUwyIU.mjs} +8 -5
- package/dist/{index-CqxywXyf.cjs → index-C1OnwMx0.cjs} +7 -4
- package/dist/{index-Cq30c63l.mjs → index-DOb57EGb.mjs} +16 -12
- package/dist/{index-edEH95Mf.cjs → index-Dcv1RKe8.cjs} +19 -15
- package/dist/index.cjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{installTerminalHooks-8TUdSFCY.mjs → installTerminalHooks-IXvZMGJq.mjs} +2 -2
- package/dist/{installTerminalHooks-BvQbDFcE.cjs → installTerminalHooks-YyT3fv6w.cjs} +2 -2
- package/dist/lib.cjs +1 -1
- package/dist/lib.d.cts +23 -1
- package/dist/lib.d.mts +23 -1
- package/dist/lib.mjs +1 -1
- package/dist/{mcp-B4UxtBKQ.mjs → mcp-B-GdhlKD.mjs} +2 -2
- package/dist/{mcp-D12X7XKG.cjs → mcp-CsKnmeE5.cjs} +2 -2
- package/dist/{runGemini-BD8OGMQr.mjs → runGemini-D0lddF43.mjs} +5 -4
- package/dist/{runGemini-LIck5ard.cjs → runGemini-DuUdmlVZ.cjs} +5 -4
- package/dist/{runOpenClaw-ENAgQy1E.mjs → runOpenClaw-BiwwYnYf.mjs} +15 -3
- package/dist/{runOpenClaw-B5XNV1zZ.cjs → runOpenClaw-DEMCYSoD.cjs} +15 -3
- package/dist/{send-BVbiwtrQ.mjs → send-DM8oqJFp.mjs} +2 -2
- package/dist/{send-o4xljyoV.cjs → send-DijSXAMp.cjs} +2 -2
- package/dist/{spawn-DTA6f9TE.cjs → spawn-De3b5Hsu.cjs} +2 -2
- package/dist/{spawn-BmqqHUYI.mjs → spawn-nISXbZD5.mjs} +2 -2
- package/dist/{types-Dj9JEGDh.mjs → types-D2qSKjsO.mjs} +400 -79
- package/dist/{types-DEre2zxO.cjs → types-NwtcBgfS.cjs} +401 -80
- 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();
|
|
@@ -2664,6 +2723,45 @@ class InvalidateSync {
|
|
|
2664
2723
|
};
|
|
2665
2724
|
}
|
|
2666
2725
|
|
|
2726
|
+
function record(value) {
|
|
2727
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2728
|
+
}
|
|
2729
|
+
function finiteNonNegative(value) {
|
|
2730
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
2731
|
+
}
|
|
2732
|
+
function firstNumber(source, keys) {
|
|
2733
|
+
for (const key of keys) {
|
|
2734
|
+
const value = finiteNonNegative(source[key]);
|
|
2735
|
+
if (value !== void 0) return value;
|
|
2736
|
+
}
|
|
2737
|
+
return void 0;
|
|
2738
|
+
}
|
|
2739
|
+
function normalizeAgentUsage(raw) {
|
|
2740
|
+
const outer = record(raw);
|
|
2741
|
+
if (!outer) return null;
|
|
2742
|
+
const wrapped = record(outer.totalTokenUsage) ?? record(outer.total_token_usage) ?? record(outer.total) ?? record(outer.usage) ?? outer;
|
|
2743
|
+
const input = firstNumber(wrapped, ["inputTokens", "input_tokens", "input"]);
|
|
2744
|
+
const output = firstNumber(wrapped, ["outputTokens", "output_tokens", "output"]);
|
|
2745
|
+
const cacheRead = firstNumber(wrapped, ["cachedInputTokens", "cached_input_tokens", "cachedReadTokens", "cacheReadInputTokens", "cache_read_input_tokens", "cache_read"]);
|
|
2746
|
+
const cacheCreation = firstNumber(wrapped, ["cacheCreationInputTokens", "cache_creation_input_tokens", "cachedWriteTokens", "cacheWriteInputTokens", "cache_write_input_tokens", "cache_creation"]);
|
|
2747
|
+
const reasoning = firstNumber(wrapped, ["reasoningOutputTokens", "reasoning_output_tokens", "thoughtTokens", "reasoning"]);
|
|
2748
|
+
const explicitTotal = firstNumber(wrapped, ["totalTokens", "total_tokens", "total"]);
|
|
2749
|
+
const fallbackTotal = input !== void 0 || output !== void 0 ? (input ?? 0) + (output ?? 0) : void 0;
|
|
2750
|
+
const total = explicitTotal ?? fallbackTotal;
|
|
2751
|
+
if (total === void 0) return null;
|
|
2752
|
+
return {
|
|
2753
|
+
total,
|
|
2754
|
+
...input !== void 0 ? { input } : {},
|
|
2755
|
+
...output !== void 0 ? { output } : {},
|
|
2756
|
+
...cacheCreation !== void 0 ? { cache_creation: cacheCreation } : {},
|
|
2757
|
+
...cacheRead !== void 0 ? { cache_read: cacheRead } : {},
|
|
2758
|
+
...reasoning !== void 0 ? { reasoning } : {}
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
function usageAgentKey(agent) {
|
|
2762
|
+
return agent.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2667
2765
|
class ApiSessionClient extends EventEmitter {
|
|
2668
2766
|
token;
|
|
2669
2767
|
sessionId;
|
|
@@ -2704,12 +2802,24 @@ class ApiSessionClient extends EventEmitter {
|
|
|
2704
2802
|
startedSubagents: /* @__PURE__ */ new Set(),
|
|
2705
2803
|
activeSubagents: /* @__PURE__ */ new Set()
|
|
2706
2804
|
};
|
|
2805
|
+
seenClaudeUsageEvents = /* @__PURE__ */ new Set();
|
|
2806
|
+
claudeUsageTotals = {
|
|
2807
|
+
total: 0,
|
|
2808
|
+
input: 0,
|
|
2809
|
+
output: 0,
|
|
2810
|
+
cache_creation: 0,
|
|
2811
|
+
cache_read: 0,
|
|
2812
|
+
costTotal: 0,
|
|
2813
|
+
costInput: 0,
|
|
2814
|
+
costOutput: 0
|
|
2815
|
+
};
|
|
2707
2816
|
lastSeq = 0;
|
|
2708
2817
|
pendingOutbox = [];
|
|
2709
2818
|
sendSync;
|
|
2710
2819
|
receiveSync;
|
|
2711
2820
|
directInboundLocalIds = /* @__PURE__ */ new Set();
|
|
2712
2821
|
routedInboundLocalIds = /* @__PURE__ */ new Set();
|
|
2822
|
+
handoverInFlight = null;
|
|
2713
2823
|
constructor(token, session) {
|
|
2714
2824
|
super();
|
|
2715
2825
|
this.token = token;
|
|
@@ -2729,26 +2839,15 @@ class ApiSessionClient extends EventEmitter {
|
|
|
2729
2839
|
logger: (msg, data) => logger.debug(msg, data)
|
|
2730
2840
|
});
|
|
2731
2841
|
registerCommonHandlers(this.rpcHandlerManager, this.metadata.path);
|
|
2732
|
-
this.socket =
|
|
2733
|
-
|
|
2734
|
-
token: this.token,
|
|
2735
|
-
clientType: "session-scoped",
|
|
2736
|
-
sessionId: this.sessionId,
|
|
2737
|
-
happyClient: `cli-coding-session/${configuration.currentCliVersion}`
|
|
2738
|
-
},
|
|
2739
|
-
path: "/v1/updates",
|
|
2740
|
-
reconnection: false,
|
|
2741
|
-
transports: ["websocket"],
|
|
2742
|
-
withCredentials: true,
|
|
2743
|
-
autoConnect: false
|
|
2744
|
-
});
|
|
2842
|
+
this.socket = this.createControlSocket();
|
|
2843
|
+
const controlSocket = this.socket;
|
|
2745
2844
|
this.socket.on("connect", () => {
|
|
2746
2845
|
logger.debug("Socket connected successfully");
|
|
2747
2846
|
if (this.reconnectInterval) {
|
|
2748
2847
|
clearInterval(this.reconnectInterval);
|
|
2749
2848
|
this.reconnectInterval = null;
|
|
2750
2849
|
}
|
|
2751
|
-
this.rpcHandlerManager.onSocketConnect(
|
|
2850
|
+
this.rpcHandlerManager.onSocketConnect(controlSocket);
|
|
2752
2851
|
this.receiveSync.invalidate();
|
|
2753
2852
|
void this.connectSessionRelay();
|
|
2754
2853
|
});
|
|
@@ -2757,12 +2856,12 @@ class ApiSessionClient extends EventEmitter {
|
|
|
2757
2856
|
});
|
|
2758
2857
|
this.socket.on("disconnect", (reason) => {
|
|
2759
2858
|
logger.debug(`[API] Socket disconnected: ${reason}`);
|
|
2760
|
-
this.rpcHandlerManager.onSocketDisconnect();
|
|
2859
|
+
this.rpcHandlerManager.onSocketDisconnect(controlSocket);
|
|
2761
2860
|
this.startSmartReconnect();
|
|
2762
2861
|
});
|
|
2763
2862
|
this.socket.on("connect_error", (error) => {
|
|
2764
2863
|
logger.debug("[API] Socket connection error:", error);
|
|
2765
|
-
this.rpcHandlerManager.onSocketDisconnect();
|
|
2864
|
+
this.rpcHandlerManager.onSocketDisconnect(controlSocket);
|
|
2766
2865
|
if (error.message === "Session archived") {
|
|
2767
2866
|
logger.debug("[API] Archived session rejected during reconnect; exiting");
|
|
2768
2867
|
this.emit("archived");
|
|
@@ -2833,8 +2932,103 @@ class ApiSessionClient extends EventEmitter {
|
|
|
2833
2932
|
this.socket.on("error", (error) => {
|
|
2834
2933
|
logger.debug("[API] Socket error:", error);
|
|
2835
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
|
+
});
|
|
2836
2940
|
this.socket.connect();
|
|
2837
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
|
+
}
|
|
2838
3032
|
onUserMessage(callback) {
|
|
2839
3033
|
this.pendingMessageCallback = callback;
|
|
2840
3034
|
while (this.pendingMessages.length > 0) {
|
|
@@ -3166,7 +3360,8 @@ class ApiSessionClient extends EventEmitter {
|
|
|
3166
3360
|
}
|
|
3167
3361
|
if (body.type === "assistant" && body.message?.usage) {
|
|
3168
3362
|
try {
|
|
3169
|
-
|
|
3363
|
+
const eventId = typeof body.uuid === "string" ? body.uuid : typeof body.message.id === "string" ? body.message.id : void 0;
|
|
3364
|
+
this.sendUsageData(body.message.usage, body.message.model, eventId);
|
|
3170
3365
|
} catch (error) {
|
|
3171
3366
|
logger.debug("[SOCKET] Failed to send usage data:", error);
|
|
3172
3367
|
}
|
|
@@ -3317,28 +3512,56 @@ class ApiSessionClient extends EventEmitter {
|
|
|
3317
3512
|
/**
|
|
3318
3513
|
* Send usage data to the server
|
|
3319
3514
|
*/
|
|
3320
|
-
sendUsageData(usage, model) {
|
|
3515
|
+
sendUsageData(usage, model, eventId) {
|
|
3516
|
+
if (eventId && this.seenClaudeUsageEvents.has(eventId)) return;
|
|
3517
|
+
if (eventId) this.seenClaudeUsageEvents.add(eventId);
|
|
3321
3518
|
const totalTokens = usage.input_tokens + usage.output_tokens + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
|
|
3322
3519
|
const costs = calculateCost(usage, model);
|
|
3520
|
+
this.claudeUsageTotals.total += totalTokens;
|
|
3521
|
+
this.claudeUsageTotals.input += usage.input_tokens;
|
|
3522
|
+
this.claudeUsageTotals.output += usage.output_tokens;
|
|
3523
|
+
this.claudeUsageTotals.cache_creation += usage.cache_creation_input_tokens || 0;
|
|
3524
|
+
this.claudeUsageTotals.cache_read += usage.cache_read_input_tokens || 0;
|
|
3525
|
+
this.claudeUsageTotals.costTotal += costs.total;
|
|
3526
|
+
this.claudeUsageTotals.costInput += costs.input;
|
|
3527
|
+
this.claudeUsageTotals.costOutput += costs.output;
|
|
3323
3528
|
const usageReport = {
|
|
3529
|
+
// Keep the legacy key so upgrading replaces the old last-call row
|
|
3530
|
+
// instead of making old + new clients double-count one session.
|
|
3324
3531
|
key: "claude-session",
|
|
3325
3532
|
sessionId: this.sessionId,
|
|
3326
3533
|
tokens: {
|
|
3327
|
-
total:
|
|
3328
|
-
input:
|
|
3329
|
-
output:
|
|
3330
|
-
cache_creation:
|
|
3331
|
-
cache_read:
|
|
3534
|
+
total: this.claudeUsageTotals.total,
|
|
3535
|
+
input: this.claudeUsageTotals.input,
|
|
3536
|
+
output: this.claudeUsageTotals.output,
|
|
3537
|
+
cache_creation: this.claudeUsageTotals.cache_creation,
|
|
3538
|
+
cache_read: this.claudeUsageTotals.cache_read
|
|
3332
3539
|
},
|
|
3333
3540
|
cost: {
|
|
3334
|
-
total:
|
|
3335
|
-
input:
|
|
3336
|
-
output:
|
|
3541
|
+
total: this.claudeUsageTotals.costTotal,
|
|
3542
|
+
input: this.claudeUsageTotals.costInput,
|
|
3543
|
+
output: this.claudeUsageTotals.costOutput
|
|
3337
3544
|
}
|
|
3338
3545
|
};
|
|
3339
3546
|
logger.debugLargeJson("[SOCKET] Sending usage data:", usageReport);
|
|
3340
3547
|
this.socket.emit("usage-report", usageReport);
|
|
3341
3548
|
}
|
|
3549
|
+
/** Save a provider's cumulative session snapshot under one upsert key. */
|
|
3550
|
+
sendAgentUsageSnapshot(agent, rawUsage) {
|
|
3551
|
+
const tokens = normalizeAgentUsage(rawUsage);
|
|
3552
|
+
if (!tokens) return false;
|
|
3553
|
+
const usageReport = {
|
|
3554
|
+
key: `usage:${usageAgentKey(agent)}:session`,
|
|
3555
|
+
sessionId: this.sessionId,
|
|
3556
|
+
tokens,
|
|
3557
|
+
// Provider token-count events do not expose reliable billed cost.
|
|
3558
|
+
// The Web treats this sentinel as unknown, not as real $0.00.
|
|
3559
|
+
cost: { total: 0 }
|
|
3560
|
+
};
|
|
3561
|
+
logger.debugLargeJson("[SOCKET] Sending agent usage snapshot:", usageReport);
|
|
3562
|
+
this.socket.emit("usage-report", usageReport);
|
|
3563
|
+
return true;
|
|
3564
|
+
}
|
|
3342
3565
|
/**
|
|
3343
3566
|
* Returns the latest session metadata known to the client.
|
|
3344
3567
|
*/
|
|
@@ -13006,6 +13229,7 @@ class ApiMachineClient {
|
|
|
13006
13229
|
stopSessionHandler = null;
|
|
13007
13230
|
listTrackedSessionIds = null;
|
|
13008
13231
|
reconnectInterval = null;
|
|
13232
|
+
handoverInFlight = null;
|
|
13009
13233
|
// Terminals negotiated with `encStream` protect the live byte payload with
|
|
13010
13234
|
// the per-machine key. This keeps it opaque to passive storage/forwarding
|
|
13011
13235
|
// paths, but is not an operator trust boundary: the trusted relay can
|
|
@@ -13466,57 +13690,72 @@ class ApiMachineClient {
|
|
|
13466
13690
|
connect() {
|
|
13467
13691
|
const serverUrl = configuration.serverUrl.replace(/^http/, "ws");
|
|
13468
13692
|
logger.debug(`[API MACHINE] Connecting to ${serverUrl}`);
|
|
13469
|
-
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, {
|
|
13470
13712
|
transports: ["websocket"],
|
|
13471
13713
|
auth: {
|
|
13472
13714
|
token: this.token,
|
|
13473
13715
|
clientType: "machine-scoped",
|
|
13474
13716
|
machineId: this.machine.id,
|
|
13475
|
-
happyClient: `cli-daemon/${configuration.currentCliVersion}
|
|
13717
|
+
happyClient: `cli-daemon/${configuration.currentCliVersion}`,
|
|
13718
|
+
...handover ? { handoverEpoch: handover.epoch } : {}
|
|
13476
13719
|
},
|
|
13720
|
+
...handover ? { query: { vh_slot: handover.candidateSlot }, forceNew: true } : {},
|
|
13477
13721
|
path: "/v1/updates",
|
|
13478
|
-
reconnection: false
|
|
13479
|
-
|
|
13480
|
-
this.bindRealtimeHandlers(this.socket, "control");
|
|
13481
|
-
this.socket.on("connect", () => {
|
|
13482
|
-
logger.debug("[API MACHINE] Connected to server");
|
|
13483
|
-
if (this.reconnectInterval) {
|
|
13484
|
-
clearInterval(this.reconnectInterval);
|
|
13485
|
-
this.reconnectInterval = null;
|
|
13486
|
-
}
|
|
13487
|
-
const initialTerminals = this.webTerminal.buildTerminalList();
|
|
13488
|
-
this.webTerminal.primeListSignature(initialTerminals);
|
|
13489
|
-
this.updateDaemonState((state) => {
|
|
13490
|
-
const now = Date.now();
|
|
13491
|
-
const currentState = withCurrentCliUpdateState(state, this.cliUpdateState);
|
|
13492
|
-
return {
|
|
13493
|
-
...currentState,
|
|
13494
|
-
status: "running",
|
|
13495
|
-
pid: process.pid,
|
|
13496
|
-
httpPort: this.machine.daemonState?.httpPort,
|
|
13497
|
-
startedAt: now,
|
|
13498
|
-
webTerminals: { updatedAt: now, terminals: initialTerminals },
|
|
13499
|
-
// B-084: closed records survive daemon restarts (persisted
|
|
13500
|
-
// in closed-terminals.json), so the connect snapshot ships
|
|
13501
|
-
// them too — not just the incremental pushes.
|
|
13502
|
-
closedTerminals: this.webTerminal.getClosedTerminals()
|
|
13503
|
-
};
|
|
13504
|
-
});
|
|
13505
|
-
this.webTerminal.startListTracking((terminals) => this.pushTerminalList(terminals));
|
|
13506
|
-
this.rpcHandlerManager.onSocketConnect(this.socket);
|
|
13507
|
-
this.syncResumeSessionRpcRegistration();
|
|
13508
|
-
this.startKeepAlive();
|
|
13509
|
-
void this.reconcileArchivedSessions();
|
|
13510
|
-
void this.refreshRelayConnection();
|
|
13722
|
+
reconnection: false,
|
|
13723
|
+
...handover ? { autoConnect: false } : {}
|
|
13511
13724
|
});
|
|
13512
|
-
|
|
13513
|
-
|
|
13514
|
-
|
|
13515
|
-
|
|
13516
|
-
this.
|
|
13517
|
-
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
|
+
};
|
|
13518
13749
|
});
|
|
13519
|
-
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) => {
|
|
13520
13759
|
if (data.body.t === "update-machine" && data.body.machineId === this.machine.id) {
|
|
13521
13760
|
const update = data.body;
|
|
13522
13761
|
if (update.metadata) {
|
|
@@ -13533,13 +13772,83 @@ class ApiMachineClient {
|
|
|
13533
13772
|
logger.debug(`[API MACHINE] Received unknown update type: ${data.body.t}`);
|
|
13534
13773
|
}
|
|
13535
13774
|
});
|
|
13536
|
-
|
|
13775
|
+
socket.on("connect_error", (error) => {
|
|
13537
13776
|
logger.debug(`[API MACHINE] Connection error: ${error.message}`);
|
|
13538
|
-
this.startSmartReconnect();
|
|
13777
|
+
if (this.socket === socket) this.startSmartReconnect();
|
|
13539
13778
|
});
|
|
13540
|
-
|
|
13779
|
+
socket.io.on("error", (error) => {
|
|
13541
13780
|
logger.debug("[API MACHINE] Socket error:", error);
|
|
13542
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
|
+
}
|
|
13543
13852
|
}
|
|
13544
13853
|
startKeepAlive() {
|
|
13545
13854
|
this.stopKeepAlive();
|
|
@@ -13573,11 +13882,13 @@ class ApiMachineClient {
|
|
|
13573
13882
|
}, 2e4);
|
|
13574
13883
|
logger.debug("[API MACHINE] Keep-alive started (20s interval)");
|
|
13575
13884
|
}
|
|
13576
|
-
|
|
13885
|
+
bindRpcRequestHandler(socket, transport) {
|
|
13577
13886
|
socket.on("rpc-request", async (data, callback) => {
|
|
13578
13887
|
logger.debugLargeJson(`[API MACHINE] Received RPC request via ${transport}:`, data);
|
|
13579
13888
|
callback(await this.rpcHandlerManager.handleRequest(data));
|
|
13580
13889
|
});
|
|
13890
|
+
}
|
|
13891
|
+
bindMachineCommandHandlers(socket) {
|
|
13581
13892
|
socket.on("terminal-input", (data) => {
|
|
13582
13893
|
let payload = data.data;
|
|
13583
13894
|
if (data.enc) {
|
|
@@ -13595,6 +13906,16 @@ class ApiMachineClient {
|
|
|
13595
13906
|
logger.debug(`[API MACHINE] Server archived session ${data.sessionId}; local process stopped=${stopped}`);
|
|
13596
13907
|
});
|
|
13597
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
|
+
}
|
|
13598
13919
|
/** Reconcile commands missed while the daemon was offline. The server
|
|
13599
13920
|
* returns only ids owned by this account and durably archived in the DB. */
|
|
13600
13921
|
async reconcileArchivedSessions() {
|