/** Single-process ownership boundary shared by serve and daemon surfaces. */ // node:path resolve/dirname/join have no Bun path utility equivalents. import { dirname, join, resolve } from "node:path"; import type { Config } from "../config/types"; import type { WriteLockHandle } from "../core/file-lock"; import type { SyncResult } from "../ingestion"; import type { ModelManager } from "../llm/nodeLlamaCpp/lifecycle"; import type { ToolContext } from "../mcp/context"; import type { HttpMcpTransportStatus } from "../mcp/http-transport"; import type { DocumentEventBus } from "./doc-events"; import type { EmbedResult, EmbedScheduler } from "./embed-scheduler"; import type { ContextHolder } from "./routes/api"; import type { ResidentStatus } from "./status-model"; import type { CollectionWatchCallbacks, CollectionWatchService, } from "./watch-service"; import { DEFAULT_INDEX_NAME, getIndexDbPath } from "../app/constants"; import { canonicalizeIndexName, INDEX_NAME_REQUIREMENTS, isValidIndexName, } from "../app/index-name"; import { ensureDirectories, formatConfigWarnings, getConfigPaths, isInitialized, loadConfig, } from "../config"; import { SavedCapsuleReverificationScheduler } from "../core/capsule-reverification-scheduler"; import { collectionEgressPolicyEpoch } from "../core/collection-egress-policy-service"; import { authorizeCurrentEgress } from "../core/egress-authorization"; import { acquireWriteLock } from "../core/file-lock"; import { deleteFindingsRunState, findingsRunStatePath, resolveFindingsSchedule, } from "../core/findings-run-state"; import { JobManager } from "../core/job-manager"; import { recordContentMutation } from "../core/mutation-generations"; import { shutdownDuration, SHUTDOWN_DRAIN_MS, SHUTDOWN_ABORT_MS, } from "../core/shutdown-budget"; import { defaultSyncService, withContentTypeRules } from "../ingestion"; import { withOwnedInferenceScope } from "../llm/inference-scope"; import { getActivePreset } from "../llm/registry"; import { createToolContext, Mutex } from "../mcp/context"; import { SqliteAdapter } from "../store/sqlite/adapter"; import { createServerContext, type CreateServerContextOptions, disposeServerContext, type ServerContext, } from "./context"; import { createEmbedScheduler } from "./embed-scheduler"; import { FindingsScheduler, type FindingsPassResult } from "./findings-pass"; import { AdmissionController, ReaderGate } from "./resident-admission"; import { ResidentBackgroundWork } from "./resident-background-work"; import { disposeResidentResources } from "./resident-shutdown"; import { createStandaloneResidentStatus, buildResidentStatusSnapshot, } from "./resident-status"; import { CollectionWatchService as DefaultCollectionWatchService } from "./watch-service"; const OWNER_LOCK_TIMEOUT_MS = 0; export type ResidentMode = "serve" | "daemon"; export interface ResidentRuntimeOptions { configPath?: string; index?: string; mode?: ResidentMode; requireCollections?: boolean; offline?: boolean; eventBus?: DocumentEventBus | null; watchCallbacks?: CollectionWatchCallbacks; /** Daemon mode only: observes every scheduled findings-pass attempt. */ onFindingsResult?: (result: FindingsPassResult) => void; readerLimit?: number; readerQueueLimit?: number; shutdownDeadlineMs?: number; shutdownAbortSettleMs?: number; } export interface ResidentGeneration { content: number; index: number; } export interface ResidentRequestHandle { authorizationEpoch?: string; id: string; signal: AbortSignal; isAuthorizationEpochCurrent?(): boolean; finish(): void; } export interface ResidentRuntime { readonly mode: ResidentMode; readonly store: SqliteAdapter; readonly config: Config; readonly actualConfigPath: string; readonly ctxHolder: ContextHolder; readonly scheduler: EmbedScheduler; readonly eventBus: DocumentEventBus | null; readonly watchService: CollectionWatchService; readonly toolMutex: Mutex; readonly readerGate: ReaderGate; readonly jobManager: JobManager; readonly capsuleReverificationScheduler: SavedCapsuleReverificationScheduler; /** Present only when daemon mode runs with `findings.enabled`. */ readonly findingsScheduler: FindingsScheduler | null; readonly modelManager: Pick< ModelManager, "acquireLease" | "getLifecycleStats" | "disposeAll" >; readonly mcpContext: ToolContext; readonly generations: ResidentGeneration; readonly activeRequests: number; readonly activeSessions: number; readonly authorizationEpoch: string; readonly isShuttingDown: boolean; getStatus(): ResidentStatus; setListenerPort(port: number | null): void; setTransportStatusProvider( provider: (() => HttpMcpTransportStatus) | null ): void; setPolicySessionInvalidator(invalidator: (() => Promise) | null): void; admitRequest(signal?: AbortSignal): ResidentRequestHandle | null; withModelLease(operation: () => Promise): Promise; markContentMutation(): void; markIndexMutation(): void; startBackgroundWork( operation: (signal: AbortSignal) => Promise ): boolean; openSession(): () => void; syncAll(options?: { gitPull?: boolean; runUpdateCmd?: boolean; triggerEmbed?: boolean; }): Promise<{ syncResult: SyncResult; embedResult: EmbedResult | null }>; dispose(closeSurface?: () => Promise): Promise; } export type ResidentRuntimeResult = | { success: true; runtime: ResidentRuntime } | { success: false; error: string }; export type ResidentRuntimeDeps = { isInitialized?: typeof isInitialized; loadConfig?: typeof loadConfig; getConfigPaths?: typeof getConfigPaths; ensureDirectories?: typeof ensureDirectories; acquireOwnerLock?: ( path: string, timeoutMs: number ) => Promise; storeFactory?: () => SqliteAdapter; createServerContext?: ( store: SqliteAdapter, config: Config, options?: CreateServerContextOptions ) => Promise; disposeServerContext?: (ctx: ServerContext) => Promise; createEmbedScheduler?: typeof createEmbedScheduler; syncAllService?: typeof defaultSyncService.syncAll; watchServiceFactory?: (options: { collections: Config["collections"]; store: SqliteAdapter; scheduler: EmbedScheduler | null; eventBus?: DocumentEventBus | null; callbacks?: CollectionWatchCallbacks; syncOptions?: Parameters[0]; }) => CollectionWatchService; modelManagerFactory?: (config: Config) => ModelManager; }; export async function startResidentRuntime( options: ResidentRuntimeOptions = {}, deps: ResidentRuntimeDeps = {} ): Promise { shutdownDuration(options.shutdownDeadlineMs, SHUTDOWN_DRAIN_MS); shutdownDuration(options.shutdownAbortSettleMs, SHUTDOWN_ABORT_MS); if (options.index !== undefined && !isValidIndexName(options.index)) { return { success: false, error: `Invalid index name: ${INDEX_NAME_REQUIREMENTS}.`, }; } const initialized = await (deps.isInitialized ?? isInitialized)( options.configPath ); if (!initialized) return { success: false, error: "GNO not initialized. Run: gno init" }; const configResult = await (deps.loadConfig ?? loadConfig)( options.configPath ); if (!configResult.ok) return { success: false, error: configResult.error.message }; for (const warning of formatConfigWarnings(configResult.warnings)) console.warn(warning); const initialConfig = configResult.value; if (options.requireCollections && initialConfig.collections.length === 0) { return { success: false, error: "No collections configured. Run: gno collection add ", }; } const mode: ResidentMode = options.mode ?? "serve"; const findingsResolution = mode === "daemon" ? resolveFindingsSchedule(initialConfig) : ({ ok: true, enabled: false } as const); if (!findingsResolution.ok) { return { success: false, error: findingsResolution.error }; } await (deps.ensureDirectories ?? ensureDirectories)(); const dbPath = getIndexDbPath(options.index); const ownerLockPath = join(dirname(dbPath), ".resident-owner.lock"); const ownerLock = await (deps.acquireOwnerLock ?? acquireWriteLock)( ownerLockPath, OWNER_LOCK_TIMEOUT_MS ); if (!ownerLock) { return { success: false, error: `Resident runtime already active for index "${canonicalizeIndexName(options.index ?? DEFAULT_INDEX_NAME)}". Stop the owning gno serve or gno daemon process and retry.`, }; } const store = deps.storeFactory?.() ?? new SqliteAdapter(); const paths = (deps.getConfigPaths ?? getConfigPaths)(); const actualConfigPath = resolve(options.configPath ?? paths.configFile); store.setConfigPath(actualConfigPath); const openResult = await store.open( dbPath, initialConfig.ftsTokenizer, initialConfig.busyTimeoutMs ); if (!openResult.ok) { await ownerLock.release(); return { success: false, error: openResult.error.message }; } const failStartup = async (error: string): Promise => { await Promise.allSettled([store.close(), ownerLock.release()]); return { success: false, error }; }; const syncCollections = await store.syncCollections( initialConfig.collections ); if (!syncCollections.ok) return failStartup(syncCollections.error.message); const syncContexts = await store.syncContexts(initialConfig.contexts ?? []); if (!syncContexts.ok) return failStartup(syncContexts.error.message); let ctx: ServerContext; try { ctx = await (deps.createServerContext ?? createServerContext)( store, initialConfig, { offline: options.offline ?? false, indexName: options.index, } ); } catch (error) { return failStartup(error instanceof Error ? error.message : String(error)); } const ctxHolder: ContextHolder = { current: ctx, config: initialConfig, actualConfigPath, scheduler: null, eventBus: options.eventBus ?? null, watchService: null, }; const modelManager = deps.modelManagerFactory?.(initialConfig) ?? { acquireLease: () => ctxHolder.current.llm?.acquireModelLease() ?? { release() {} }, getLifecycleStats: () => ctxHolder.current.llm?.getLifecycleStats() ?? createStandaloneResidentStatus("stdio").models, disposeAll: async () => { await ctxHolder.current.llm?.dispose(); }, }; const generations: ResidentGeneration = { content: 0, index: 0 }; ctxHolder.markContentMutation = () => { generations.content += 1; }; ctxHolder.markIndexMutation = () => { generations.index += 1; }; const scheduler = (deps.createEmbedScheduler ?? createEmbedScheduler)({ db: store.getRawDb(), getEmbedPort: () => ctxHolder.current.embedPort, getVectorIndex: () => ctxHolder.current.vectorIndex, getModelUri: () => getActivePreset(ctxHolder.config).embed, onEmbedded: () => { generations.index += 1; }, }); ctxHolder.scheduler = scheduler; ctxHolder.current.scheduler = scheduler; ctxHolder.current.eventBus = options.eventBus ?? null; let capsuleReverificationScheduler: SavedCapsuleReverificationScheduler | null = null; const watchService = ( deps.watchServiceFactory ?? ((watchOptions) => new DefaultCollectionWatchService(watchOptions)) )({ collections: initialConfig.collections, store, scheduler, eventBus: options.eventBus ?? null, callbacks: { ...options.watchCallbacks, onSyncComplete: (event) => { recordContentMutation(event.result, () => { generations.content += 1; }); options.watchCallbacks?.onSyncComplete?.(event); }, onSettled: () => { capsuleReverificationScheduler?.notifySyncSettled(); options.watchCallbacks?.onSettled?.(); }, }, syncOptions: withContentTypeRules({}, initialConfig), }); watchService.start(); ctxHolder.watchService = watchService; ctxHolder.current.watchService = watchService; const toolMutex = new Mutex(); const serverInstanceId = crypto.randomUUID(); const writeLockPath = join(dirname(dbPath), ".mcp-write.lock"); const jobManager = new JobManager({ lockPath: writeLockPath, serverInstanceId, toolMutex, }); let authorizationEpoch = collectionEgressPolicyEpoch(initialConfig); jobManager.setAuthorizationEpoch(authorizationEpoch); ctxHolder.jobManager = jobManager; const admission = new AdmissionController(); const readerGate = new ReaderGate( options.readerLimit, options.readerQueueLimit ); const startedAt = Date.now(); let listenerPort: number | null = null; let transportStatusProvider: (() => HttpMcpTransportStatus) | null = null; let policySessionInvalidator: (() => Promise) | null = null; let shutdownState: ResidentStatus["shutdown"]["state"] = "none"; let admissionState: ResidentStatus["admission"]["state"] = "accepting"; let disposed = false; let disposal: Promise | undefined; const backgroundWork = new ResidentBackgroundWork( () => !disposed && admission.accepting ); const findingsStatePath = findingsRunStatePath(dbPath); let findingsScheduler: FindingsScheduler | null = null; if (findingsResolution.enabled) { findingsScheduler = new FindingsScheduler({ deps: { store, getConfig: () => ctxHolder.config, schedule: findingsResolution.schedule, dbPath, indexName: canonicalizeIndexName(options.index ?? DEFAULT_INDEX_NAME), statePath: findingsStatePath, }, startBackgroundWork: (operation) => backgroundWork.start(operation), onResult: options.onFindingsResult, }); await findingsScheduler.start(); } else if (mode === "daemon") { // Findings are off: a state file from an earlier configuration would // otherwise keep reporting a schedule that no longer exists. await deleteFindingsRunState(findingsStatePath); } capsuleReverificationScheduler = new SavedCapsuleReverificationScheduler({ deps: { store, get config() { return ctxHolder.config; }, indexName: canonicalizeIndexName(options.index ?? DEFAULT_INDEX_NAME), notify: (event) => options.eventBus?.emit(event), }, startBackgroundWork: (operation) => backgroundWork.start(operation), }); const mcpContext = createToolContext({ store, getConfig: () => ctxHolder.config, setConfig: (config) => { ctxHolder.config = config; ctxHolder.current = { ...ctxHolder.current, config }; ctxHolder.watchService?.updateCollections( config.collections, withContentTypeRules({}, config) ); }, actualConfigPath, indexName: canonicalizeIndexName(options.index ?? DEFAULT_INDEX_NAME), toolMutex, jobManager, serverInstanceId, writeLockPath, enableWrite: false, isShuttingDown: () => disposed || !admission.accepting, getModelAdapter: (config) => config === ctxHolder.current.config ? ctxHolder.current.llm : undefined, markContentMutation: () => { generations.content += 1; }, markIndexMutation: () => { generations.index += 1; }, invalidateEgressPolicy: async () => (await ctxHolder.invalidateEgressPolicy?.()) ?? { policyEpoch: collectionEgressPolicyEpoch(ctxHolder.config), queuedJobsInvalidated: 0, sessionsInvalidated: 0, staleWorkMustRetry: true, }, }); mcpContext.authorizeTraceExport = async (lineage) => { const egress = mcpContext.getEgressContext?.(); return authorizeCurrentEgress({ store, config: ctxHolder.config, lineage, action: "export", destinationZone: egress?.destinationZone === "loopback" ? "local_process" : (egress?.destinationZone ?? "local_process"), caller: egress?.caller ?? { authenticated: true, operationAuthorized: true, }, contentClass: "retrieval_trace", }); }; const runtime: ResidentRuntime = { mode: options.mode ?? "serve", store, get config() { return ctxHolder.config; }, actualConfigPath, ctxHolder, scheduler, eventBus: options.eventBus ?? null, watchService, toolMutex, readerGate, jobManager, capsuleReverificationScheduler, findingsScheduler, modelManager, mcpContext, generations, get activeRequests() { return admission.active; }, get activeSessions() { return transportStatusProvider?.().activeSessions ?? 0; }, get authorizationEpoch() { return authorizationEpoch; }, get isShuttingDown() { return disposed || !admission.accepting; }, admitRequest: (signal) => { const admitted = admission.admit(signal); if (!admitted) return null; const requestEpoch = authorizationEpoch; return { ...admitted, authorizationEpoch: requestEpoch, isAuthorizationEpochCurrent: () => requestEpoch === authorizationEpoch, }; }, async withModelLease(operation: () => Promise): Promise { const lease = modelManager.acquireLease(); try { return await operation(); } finally { lease.release(); } }, markContentMutation() { generations.content += 1; }, markIndexMutation() { generations.index += 1; }, startBackgroundWork(operation) { return backgroundWork.start(operation); }, openSession() { return () => undefined; }, getStatus() { const transport = transportStatusProvider?.() ?? { activeRequests: 0, activeSessions: 0, queuedRequests: 0, maxConcurrentRequests: 0, maxQueuedRequests: 0, maxSessions: 0, }; const jobs = jobManager.listJobs(100); return buildResidentStatusSnapshot({ mode: options.mode ?? "serve", startedAt, listenerPort, admission: { state: admissionState, activeRequests: admission.active, }, shutdown: { state: shutdownState }, transport, readers: { active: readerGate.active, queued: readerGate.queued, limit: readerGate.limit, maxQueued: readerGate.maxQueued, }, models: modelManager.getLifecycleStats(), jobs: { active: jobs.active.length, recent: jobs.recent.length, failed: jobs.recent.filter((job) => job.status === "failed").length, }, generations: { ...generations }, }); }, setListenerPort(port) { listenerPort = port; }, setTransportStatusProvider(provider) { transportStatusProvider = provider; }, setPolicySessionInvalidator(invalidator) { policySessionInvalidator = invalidator; }, async syncAll(syncOptions = {}) { const request = admission.admit(); if (!request) throw new Error("Resident runtime is shutting down"); try { return await withOwnedInferenceScope( { signal: request.signal }, async () => { const config = ctxHolder.config; const syncAllService = deps.syncAllService ? (...args: Parameters) => deps.syncAllService!(...args) : defaultSyncService.syncAll.bind(defaultSyncService); const syncResult = await syncAllService( config.collections, store, withContentTypeRules( { gitPull: syncOptions.gitPull, runUpdateCmd: syncOptions.runUpdateCmd, }, config ) ); recordContentMutation(syncResult, () => { generations.content += 1; }); const embedResult = syncOptions.triggerEmbed === false ? null : await scheduler.triggerNow(); capsuleReverificationScheduler.notifySyncSettled(); return { syncResult, embedResult }; } ); } finally { request.finish(); } }, dispose(closeSurface) { if (disposal) return disposal; disposed = true; admission.stop(); jobManager.stop(); admissionState = "draining"; shutdownState = "graceful"; disposal = disposeResidentResources({ options, admission, backgroundWork, jobManager, scheduler, store, watchService, context: ctxHolder.current, mcpContext, modelManager, ownerLock, stopFindings: () => findingsScheduler?.dispose(), stopCapsules: () => capsuleReverificationScheduler.dispose(), closeEvents: () => options.eventBus?.close(), disposeContext: deps.disposeServerContext ?? disposeServerContext, closeSurface, onDeadline: () => { shutdownState = "deadline"; }, }).then(() => { admissionState = "closed"; transportStatusProvider = null; listenerPort = null; }); return disposal; }, }; ctxHolder.startBackgroundWork = (operation) => runtime.startBackgroundWork(operation); ctxHolder.invalidateEgressPolicy = async () => { const sessionsInvalidated = transportStatusProvider?.().activeSessions ?? 0; const policyEpoch = collectionEgressPolicyEpoch(ctxHolder.config); authorizationEpoch = policyEpoch; const queuedJobsInvalidated = jobManager.setAuthorizationEpoch(policyEpoch); await policySessionInvalidator?.(); return { policyEpoch, queuedJobsInvalidated, sessionsInvalidated, staleWorkMustRetry: true, }; }; mcpContext.getResidentStatus = () => runtime.getStatus(); return { success: true, runtime }; }