import type { CollectionSyncResult } from "../../ingestion"; import type { HttpGatewayOverrides } from "../../mcp/http-security"; import type { BackgroundRuntimeResult } from "../../serve/background-runtime"; import type { FindingsPassResult } from "../../serve/findings-pass"; import type { ResidentRuntime } from "../../serve/resident-runtime"; import type { SessionAutomationRunResult } from "../../sessions/types"; import { warnIfBunBelowFloor } from "../../app/bun-runtime"; import { enforceCollectionEgress, EGRESS_DENIED_MESSAGE, EgressDeniedError, } from "../../core/egress-enforcement"; import { markResidentShutdownHandlerInstalled } from "../../core/resident-shutdown-handler"; import { sighupHandlerAllowed } from "../../core/sighup-disposition"; import { DEFAULT_HTTP_GATEWAY_PORT, isHttpGatewayLoopbackBind, resolveHttpGatewayConfig, } from "../../mcp/http-security"; import { startBackgroundRuntime } from "../../serve/background-runtime"; import { refreshResidentConfig } from "../../serve/resident-request"; import { handleResidentStatus, handleStatus } from "../../serve/routes/api"; import { createMcpHttpGateway } from "../../serve/routes/mcp"; import { SessionsError } from "../../sessions/types"; export interface DaemonOptions extends HttpGatewayOverrides { configPath?: string; index?: string; offline?: boolean; verbose?: boolean; quiet?: boolean; noSyncOnStart?: boolean; signal?: AbortSignal; } export type DaemonResult = | { success: true } | { success: false; error: string }; type DaemonLogger = { log: (message: string) => void; error: (message: string) => void; }; type DaemonDeps = { startBackgroundRuntime?: typeof startBackgroundRuntime; createMcpHttpGateway?: typeof createMcpHttpGateway; serve?: typeof Bun.serve; logger?: DaemonLogger; }; /** Silent when clean: only failures, and non-empty writes when not quiet, reach the log. */ export function logFindingsPassResult( result: FindingsPassResult, logger: DaemonLogger, options: { quiet?: boolean; verbose?: boolean } ): void { if (result.outcome === "failed") { logger.error(`findings pass failed: ${result.error ?? "unknown error"}`); return; } if (result.outcome === "skipped_lease") { if (options.verbose) logger.log(`findings pass skipped: ${result.error}`); return; } const { counts } = result; const changed = counts.written + counts.reopened + counts.resolved + counts.deleted; if (changed === 0 || options.quiet) return; logger.log( `findings pass: ${counts.written} new, ${counts.reopened} reopened, ${counts.resolved} resolved, ${counts.deleted} expired (${counts.open} open)` ); } /** Content-free: counts and reason codes only; clean no-ops stay silent. */ export function logSessionAutomationResult( result: SessionAutomationRunResult, logger: DaemonLogger, options: { quiet?: boolean } ): void { if (!result.ran) return; if (result.outcome === "failed") { logger.error( `session automation ${result.profileId}: failed (${result.reason ?? "unknown"})` ); return; } if (result.outcome === "up_to_date" || options.quiet) return; let imported = 0; let updated = 0; for (const receipt of result.receipts) { imported += receipt.counts.imported; updated += receipt.counts.updated; } logger.log( `session automation ${result.profileId}: ${result.outcome}${result.reason ? ` (${result.reason})` : ""}; ${imported} threads imported, ${updated} updated` ); } function formatCollectionSyncSummary(result: CollectionSyncResult): string { const collectionErrors = result.errors .filter((error) => error.relPath === "") .map((error) => `; [${error.code}] ${error.message}`) .join(""); return `${result.collection}: ${result.filesAdded} added, ${result.filesUpdated} updated, ${result.filesUnchanged} unchanged, ${result.filesErrored} errors${collectionErrors}`; } export const authorizeDaemonStatus = async ( runtime: ResidentRuntime, gateway: Awaited>, request: Request, server: Parameters[1] ): Promise => { const authorization = await gateway.security.authorize(request, server); if (!authorization.ok) return authorization.response; const refreshFailure = await refreshResidentConfig(runtime); if (refreshFailure) return refreshFailure; try { enforceCollectionEgress({ collections: runtime.config.collections, action: "serve", destinationZone: authorization.value.peerClassification.zone, caller: { authenticated: authorization.value.authenticated, operationAuthorized: true, }, contentClass: "metadata", }); return handleResidentStatus(() => runtime.getStatus()); } catch (error) { if (!(error instanceof EgressDeniedError)) throw error; return Response.json( { error: "EGRESS_DENIED", message: EGRESS_DENIED_MESSAGE, reason: error.decision.reason, audit: error.decision.audit, }, { status: 403 } ); } }; export async function handleDaemonAppStatus( runtime: ResidentRuntime, host: string ): Promise { if (!isHttpGatewayLoopbackBind(host)) { return new Response(null, { status: 404 }); } const refreshFailure = await refreshResidentConfig(runtime); if (refreshFailure) return refreshFailure; return handleStatus(runtime.ctxHolder.current, { getResidentStatus: () => runtime.getStatus(), }); } function createSignalPromise( signal: AbortSignal | undefined, logger: DaemonLogger, quiet: boolean ): Promise { return new Promise((resolve) => { if (signal?.aborted) { resolve(); return; } const complete = (message?: string): void => { signal?.removeEventListener("abort", onAbort); process.off("SIGINT", onSigint); process.off("SIGTERM", onSigterm); process.off("SIGHUP", onSighup); if (message && !quiet) { logger.log(message); } resolve(); }; const onAbort = (): void => complete("Daemon stopped."); const onSigint = (): void => complete("Received SIGINT. Shutting down..."); const onSigterm = (): void => complete("Received SIGTERM. Shutting down..."); // A closed terminal or logout: shut down cleanly, as on SIGTERM, unless // SIGHUP was ignored at start (nohup), which must keep working. const handleSighup = sighupHandlerAllowed(); const onSighup = (): void => complete("Received SIGHUP. Shutting down..."); signal?.addEventListener("abort", onAbort, { once: true }); process.once("SIGINT", onSigint); process.once("SIGTERM", onSigterm); if (handleSighup) process.once("SIGHUP", onSighup); markResidentShutdownHandlerInstalled(); }); } export async function daemon( options: DaemonOptions = {}, deps: DaemonDeps = {} ): Promise { const logger = deps.logger ?? { log: (message: string) => { console.log(message); }, error: (message: string) => { console.error(message); }, }; warnIfBunBelowFloor("daemon", logger.error); const runtimeResult: BackgroundRuntimeResult = await ( deps.startBackgroundRuntime ?? startBackgroundRuntime )({ mode: "daemon", configPath: options.configPath, index: options.index, requireCollections: true, offline: options.offline, onFindingsResult: (result) => logFindingsPassResult(result, logger, { quiet: options.quiet, verbose: options.verbose, }), onSessionAutomationResult: (result) => logSessionAutomationResult(result, logger, { quiet: options.quiet }), onSessionAutomationError: (error) => { // Busy state (a run or state change in progress) is retried next tick. if (error instanceof SessionsError && error.code === "SESSIONS_BUSY") { if (options.verbose) { logger.log("session automation: run in progress; next tick retries"); } return; } logger.error( `session automation tick failed: ${error instanceof SessionsError ? error.code : "runtime error"}` ); }, watchCallbacks: { onSyncStart: ({ collection, relPaths }) => { if (!options.quiet) { logger.log( `watch sync started: ${collection} (${relPaths.length} path${relPaths.length === 1 ? "" : "s"})` ); } }, onSyncComplete: ({ result }) => { if (!options.quiet) { logger.log(formatCollectionSyncSummary(result)); } }, onSyncError: ({ collection, error }) => { logger.error( `watch sync failed: ${collection}: ${error instanceof Error ? error.message : String(error)}` ); }, }, }); if (!runtimeResult.success) { return { success: false, error: runtimeResult.error }; } const { runtime } = runtimeResult; const gatewayConfig = resolveHttpGatewayConfig(runtime.config.gateway, { host: options.host, port: options.port ?? DEFAULT_HTTP_GATEWAY_PORT, tokenFile: options.tokenFile, allowedHosts: options.allowedHosts, allowedOrigins: options.allowedOrigins, enableWrite: options.enableWrite, toolProfile: options.toolProfile, }); let gateway: Awaited> | undefined; let server: ReturnType | undefined; const stopSignals = new AbortController(); const shutdown = createSignalPromise( options.signal ? AbortSignal.any([options.signal, stopSignals.signal]) : stopSignals.signal, logger, options.quiet ?? false ); try { gateway = await (deps.createMcpHttpGateway ?? createMcpHttpGateway)( runtime as ResidentRuntime, gatewayConfig ); server = (deps.serve ?? Bun.serve)({ port: gatewayConfig.port, hostname: gatewayConfig.host, development: false, routes: { "/mcp": gateway.route, "/api/status": { GET: () => handleDaemonAppStatus( runtime as ResidentRuntime, gatewayConfig.host ), }, "/api/resident/status": { GET: (request, peerServer) => authorizeDaemonStatus( runtime as ResidentRuntime, gateway as Awaited>, request, peerServer ), }, }, }); (runtime as Partial).setListenerPort?.( server.port ?? gatewayConfig.port ); if (!options.quiet) { logger.log( `GNO daemon started for index "${options.index ?? "default"}" using ${runtime.config.collections.length} collection${runtime.config.collections.length === 1 ? "" : "s"}.` ); logger.log( `MCP gateway listening at http://${gatewayConfig.host}:${server.port}/mcp` ); const watchState = runtime.watchService.getState(); if (watchState.activeCollections.length > 0) { logger.log(`watching: ${watchState.activeCollections.join(", ")}`); } if (watchState.failedCollections.length > 0) { for (const failed of watchState.failedCollections) { logger.error(`watch failed: ${failed.collection}: ${failed.reason}`); } } const findings = (runtime as Partial).findingsScheduler; if (findings) { logger.log( `findings pass: every ${findings.state.cadence} into "${findings.state.collection}" (report-only)` ); } } if (!options.noSyncOnStart) { if (!options.quiet) { logger.log("Running initial sync..."); } const synced = await Promise.race([ runtime.syncAll({ runUpdateCmd: true, triggerEmbed: true }), shutdown.then(() => null), ]); if (!synced) return { success: true }; const { syncResult, embedResult } = synced; if (!options.quiet) { logger.log( `sync totals: ${syncResult.totalFilesAdded} added, ${syncResult.totalFilesUpdated} updated, ${syncResult.totalFilesErrored} errors, ${syncResult.totalFilesSkipped} skipped` ); } if (!options.quiet && embedResult) { logger.log( `embed: ${embedResult.embedded} embedded, ${embedResult.errors} errors` ); } } else if (!options.quiet) { logger.log("Skipping initial sync (--no-sync-on-start)."); } await shutdown; return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), }; } finally { stopSignals.abort(); const surfaceClose = Promise.all([ Promise.resolve().then(() => server?.stop(true)), Promise.resolve().then(() => gateway?.close()), ]); await runtime.dispose(() => surfaceClose); } }