Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
191 lines
5.3 KiB
JavaScript
191 lines
5.3 KiB
JavaScript
"use strict";
|
|
|
|
const { CAPABILITY_SURFACES } = require("./constants.cjs");
|
|
const { resolveRpcTimeoutMs } = require("./rpcTimeouts.cjs");
|
|
|
|
function createTaggedError(code, message) {
|
|
const error = new Error(message);
|
|
error.code = code;
|
|
return error;
|
|
}
|
|
|
|
function createUnavailableError(message) {
|
|
return createTaggedError("RPC_UNAVAILABLE", message);
|
|
}
|
|
|
|
function createRpcTimeoutError(method, timeoutMs, createError = createTaggedError) {
|
|
return createError(
|
|
"RPC_TIMEOUT",
|
|
`Timed out waiting for RPC response to "${method}" after ${timeoutMs}ms.`,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create a newline-delimited JSON-RPC client over an existing TCP socket.
|
|
*/
|
|
function createNdjsonRpcClient({
|
|
socket,
|
|
surface = CAPABILITY_SURFACES.BUILTIN,
|
|
setTimeoutImpl = setTimeout,
|
|
clearTimeoutImpl = clearTimeout,
|
|
onBridgeStatus,
|
|
createError = createTaggedError,
|
|
messages = {},
|
|
}) {
|
|
if (!socket) {
|
|
throw new Error("socket is required");
|
|
}
|
|
|
|
const connectionClosedMessage = messages.connectionClosed
|
|
|| "RPC connection closed.";
|
|
const connectionClosedWhileCallMessage = messages.connectionClosedWhileCall
|
|
|| connectionClosedMessage;
|
|
const connectionErrorMessage = messages.connectionError
|
|
|| ((error) => `RPC connection failed: ${error?.message || error}`);
|
|
const rpcTimeoutMessage = messages.rpcTimeout
|
|
|| ((method, timeoutMs) => `Timed out waiting for RPC response to "${method}" after ${timeoutMs}ms.`);
|
|
const writeFailedMessage = messages.writeFailed
|
|
|| ((method, error) => `Failed to send RPC "${method}": ${error?.message || error}`);
|
|
|
|
let nextRpcId = 1;
|
|
let buffer = "";
|
|
const pending = new Map();
|
|
let bridgeCommandTimeoutMs = null;
|
|
let bridgePermissionMode = null;
|
|
let bridgeApprovalTimeoutMs = null;
|
|
|
|
function settle(id, resolve, reject, payload) {
|
|
pending.delete(id);
|
|
clearTimeoutImpl(payload.timeoutId);
|
|
if (payload.error) {
|
|
reject(payload.error);
|
|
return;
|
|
}
|
|
resolve(payload.result);
|
|
}
|
|
|
|
function rejectAll(error) {
|
|
for (const [id, entry] of pending) {
|
|
settle(id, entry.resolve, entry.reject, { timeoutId: entry.timeoutId, error });
|
|
}
|
|
}
|
|
|
|
socket.on("data", (chunk) => {
|
|
buffer += chunk;
|
|
let newlineIndex;
|
|
while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
|
|
const line = buffer.slice(0, newlineIndex);
|
|
buffer = buffer.slice(newlineIndex + 1);
|
|
if (!line.trim()) continue;
|
|
|
|
let message;
|
|
try {
|
|
message = JSON.parse(line);
|
|
} catch {
|
|
continue;
|
|
}
|
|
if (message?.id == null) continue;
|
|
const entry = pending.get(message.id);
|
|
if (!entry) continue;
|
|
|
|
if (message.error) {
|
|
settle(message.id, entry.resolve, entry.reject, {
|
|
timeoutId: entry.timeoutId,
|
|
error: createError(
|
|
"RPC_ERROR",
|
|
message.error.message || JSON.stringify(message.error),
|
|
),
|
|
});
|
|
} else {
|
|
settle(message.id, entry.resolve, entry.reject, {
|
|
timeoutId: entry.timeoutId,
|
|
result: message.result,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
socket.on("error", (error) => {
|
|
rejectAll(createError("CONNECTION_ERROR", connectionErrorMessage(error)));
|
|
});
|
|
|
|
socket.on("close", () => {
|
|
rejectAll(createError("CONNECTION_CLOSED", connectionClosedMessage));
|
|
});
|
|
|
|
async function call(method, params) {
|
|
if (socket.destroyed || !socket.writable) {
|
|
throw createError("CONNECTION_CLOSED", connectionClosedWhileCallMessage);
|
|
}
|
|
|
|
const id = nextRpcId++;
|
|
const timeoutMs = resolveRpcTimeoutMs(method, {
|
|
surface,
|
|
bridgeCommandTimeoutMs,
|
|
bridgePermissionMode,
|
|
bridgeApprovalTimeoutMs,
|
|
});
|
|
|
|
return await new Promise((resolve, reject) => {
|
|
const timeoutId = setTimeoutImpl(() => {
|
|
pending.delete(id);
|
|
reject(createError("RPC_TIMEOUT", rpcTimeoutMessage(method, timeoutMs)));
|
|
}, timeoutMs);
|
|
|
|
pending.set(id, { resolve, reject, timeoutId });
|
|
|
|
const payload = `${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`;
|
|
try {
|
|
socket.write(payload, (writeError) => {
|
|
if (!writeError) return;
|
|
settle(id, resolve, reject, {
|
|
timeoutId,
|
|
error: createError("WRITE_FAILED", writeFailedMessage(method, writeError)),
|
|
});
|
|
});
|
|
} catch (writeError) {
|
|
settle(id, resolve, reject, {
|
|
timeoutId,
|
|
error: createError("WRITE_FAILED", writeFailedMessage(method, writeError)),
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function ingestBridgeStatus(statusResult) {
|
|
if (Number.isFinite(statusResult?.commandTimeoutMs) && statusResult.commandTimeoutMs > 0) {
|
|
bridgeCommandTimeoutMs = statusResult.commandTimeoutMs;
|
|
}
|
|
if (typeof statusResult?.permissionMode === "string") {
|
|
bridgePermissionMode = statusResult.permissionMode;
|
|
}
|
|
if (Number.isFinite(statusResult?.approvalTimeoutMs) && statusResult.approvalTimeoutMs > 0) {
|
|
bridgeApprovalTimeoutMs = statusResult.approvalTimeoutMs;
|
|
}
|
|
onBridgeStatus?.({
|
|
bridgeCommandTimeoutMs,
|
|
bridgePermissionMode,
|
|
bridgeApprovalTimeoutMs,
|
|
});
|
|
}
|
|
|
|
return {
|
|
call,
|
|
ingestBridgeStatus,
|
|
close() {
|
|
try {
|
|
socket.end();
|
|
} catch {
|
|
// Ignore shutdown errors.
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
createTaggedError,
|
|
createUnavailableError,
|
|
createRpcTimeoutError,
|
|
createNdjsonRpcClient,
|
|
};
|