UNPKG

naisys

Version:

NAISYS - Autonomous AI agent runner with built-in context management and cost tracking

192 lines 7.09 kB
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