naisys
Version:
NAISYS - Autonomous AI agent runner with built-in context management and cost tracking
192 lines • 7.09 kB
JavaScript
import { hubCmd } from "../command/commandDefs.js";
import { createHubConnection } from "./hubConnection.js";
/** Upper bound on any request/response round-trip. Without this, a socket
* drop mid-request leaves the ack callback dangling and the promise hangs
* forever — the agent stalls instead of erroring out and retrying. */
const REQUEST_TIMEOUT_MS = 30_000;
export function createHubClient(hubClientConfig, hubClientLog, promptNotification) {
const hubUrl = hubClientConfig.hubUrl;
const apiUrlBase = hubClientConfig.apiUrlBase;
let activeConnection = null;
let hasConnectedOnce = false;
let disconnectNotified = false;
let connectErrorNotified = false;
let connectedHandler = null;
let connectErrorHandler = null;
// Generic event handlers registry - maps event name to set of handlers
const eventHandlers = new Map();
init();
function init() {
connect();
}
function connect() {
activeConnection = createHubConnection(hubClientConfig, hubClientLog, raiseEvent, handleConnected, handleDisconnected, handleConnectError);
activeConnection.connect();
}
function handleConnected() {
// Output to the console on startup that we're connecting, suppress further logs, we'll use the prompt service for that
hubClientLog.disableConsole();
connectedHandler?.();
if (hasConnectedOnce) {
promptNotification.notify({
wake: "always",
commentOutput: ["Hub connection re-established"],
});
}
hasConnectedOnce = true;
disconnectNotified = false;
connectErrorNotified = false;
}
function handleConnectError(error) {
connectErrorHandler?.(error);
if (error.data?.fatal) {
activeConnection?.disconnect();
}
// Surface the reason once per disconnected interval so the user knows why
// retries aren't succeeding (e.g. invalid access key after rotation).
// File logging always captures every attempt; this is for the prompt UI.
if (hasConnectedOnce && !connectErrorNotified) {
connectErrorNotified = true;
promptNotification.notify({
wake: "always",
errorOutput: [getUserFacingConnectError(error)],
});
}
}
function handleDisconnected() {
if (hasConnectedOnce && !disconnectNotified) {
disconnectNotified = true;
promptNotification.notify({
wake: "always",
errorOutput: ["Hub connection lost"],
});
}
}
function registerEvent(event, handler) {
if (!eventHandlers.has(event)) {
eventHandlers.set(event, new Set());
}
eventHandlers.get(event).add(handler);
}
/** Unregister an event handler */
function unregisterEvent(event, handler) {
const handlers = eventHandlers.get(event);
if (handlers) {
handlers.delete(handler);
}
}
/** Raise an event to all registered handlers */
function raiseEvent(event, ...args) {
const handlers = eventHandlers.get(event);
if (handlers) {
for (const handler of handlers) {
handler(...args);
}
}
}
function isConnected() {
return activeConnection?.isConnected() ?? false;
}
function sendMessage(event, payload) {
if (!activeConnection) {
hubClientLog.log("[NAISYS:HubClient] No active connection for sendMessage");
return false;
}
return activeConnection.sendMessage(event, payload);
}
function sendRequest(event, payload) {
if (!activeConnection) {
hubClientLog.log("[NAISYS:HubClient] No active connection for sendRequest");
return Promise.reject(new Error("No active hub connection"));
}
return new Promise((resolve, reject) => {
let settled = false;
const timeout = setTimeout(() => {
if (settled)
return;
settled = true;
reject(new Error(`Hub request '${event}' timed out after ${REQUEST_TIMEOUT_MS}ms`));
}, REQUEST_TIMEOUT_MS);
timeout.unref?.();
const sent = activeConnection.sendMessage(event, payload, (response) => {
if (settled)
return;
settled = true;
clearTimeout(timeout);
resolve(response);
});
if (!sent && !settled) {
settled = true;
clearTimeout(timeout);
reject(new Error("Failed to send request - not connected"));
}
});
}
/** Returns a promise that resolves once a hub connection is established,
* or rejects if the handshake fails with a fatal structured connect error */
function waitForConnection() {
if (isConnected()) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
function cleanup() {
connectedHandler = null;
connectErrorHandler = null;
}
connectedHandler = () => {
cleanup();
resolve();
};
connectErrorHandler = (error) => {
if (error.data?.fatal) {
cleanup();
reject(new Error(getUserFacingConnectError(error)));
}
};
});
}
function getUserFacingConnectError(error) {
switch (error.data?.code) {
case "superseded_by_newer_instance":
return "This NAISYS instance was superseded by a newer local process.";
case "invalid_access_key":
return "Host access key was rejected by the server.";
case "registration_failed":
return `Hub rejected host registration: ${error.message}`;
default:
return `Hub connection failed: ${error.message}`;
}
}
function getConnectionInfo() {
if (!activeConnection)
return null;
return {
url: activeConnection.getUrl(),
connected: activeConnection.isConnected(),
};
}
function cleanup() {
activeConnection?.disconnect();
activeConnection = null;
eventHandlers.clear();
connectedHandler = null;
connectErrorHandler = null;
}
return {
// RegistrableCommand
command: hubCmd,
handleCommand: () => `${hubUrl} - ${isConnected() ? "Connected" : "Disconnected"}`,
// HubClient API
getConnectionInfo,
getHubUrl: () => hubUrl,
getApiUrlBase: () => apiUrlBase,
isConnected,
waitForConnection,
registerEvent,
unregisterEvent,
sendMessage,
sendRequest,
cleanup,
};
}
//# sourceMappingURL=hubClient.js.map