diff --git a/apps/agor-daemon/config-lean.example.yaml b/apps/agor-daemon/config-lean.example.yaml new file mode 100644 index 000000000..038217db4 --- /dev/null +++ b/apps/agor-daemon/config-lean.example.yaml @@ -0,0 +1,47 @@ +# config-lean.example.yaml — Minimal headless daemon for k8s / cloud deployments +# +# Copy to ~/.agor/config.yaml (or pass via --config flag) to run the daemon +# with only the essential services needed for API/chatbot use cases. +# +# Services disabled in lean mode: +# scheduler — Cron-based session spawning (background timer + DB polling) +# terminals — Web terminal / xterm.js (node-pty, per-session PTY allocation) +# gateway — Slack/Discord/GitHub integrations (Socket Mode listeners, channel cache) +# boards — Spatial canvas boards, board-objects, board-comments (UI-only) +# cards — Kanban cards and card type definitions (UI-only) +# artifacts — Sandpack artifact previews (UI-only, bundler detection) +# file_browser — File/context browsing endpoints (UI file explorer) +# leaderboard — Usage analytics / cost tracking (UI dashboard) +# +# Services kept enabled: +# core — Sessions, tasks, messages (the prompt loop) +# worktrees — Worktree CRUD (required FK for sessions) +# repos — Repository management +# users — User accounts + authentication +# mcp_servers — MCP server configuration (needed for agent tool access) + +daemon: + port: 3030 + host: 0.0.0.0 + allowAnonymous: false + +services: + # Core services (cannot be disabled) + core: on + worktrees: on + repos: on + users: on + + # Keep MCP servers for agent tool integration + mcp_servers: on + + # Disable UI-only and heavyweight services + scheduler: off + terminals: off + gateway: off + boards: off + cards: off + artifacts: off + file_browser: off + leaderboard: off + static_files: off diff --git a/apps/agor-daemon/src/index.ts b/apps/agor-daemon/src/index.ts index 4be365d6a..d4907a284 100644 --- a/apps/agor-daemon/src/index.ts +++ b/apps/agor-daemon/src/index.ts @@ -104,24 +104,31 @@ export interface DaemonStartOptions { * or from main.ts with no args for direct execution. */ export async function startDaemon(options?: DaemonStartOptions): Promise { + const tBoot = performance.now(); + // Initialize Handlebars helpers for template rendering + let t0 = performance.now(); registerHandlebarsHelpers(); - console.log('✅ Handlebars helpers registered'); + console.log(`⏱️ [boot] Handlebars helpers: ${(performance.now() - t0).toFixed(0)}ms`); // Configure Git to fail fast instead of prompting for credentials process.env.GIT_TERMINAL_PROMPT = '0'; process.env.GIT_ASKPASS = 'echo'; // Load config: CLI-provided > configPath > default loadConfig() + t0 = performance.now(); const config: AgorConfig = options?.config ? options.config : options?.configPath ? await loadConfigFromFile(options.configPath) : await loadConfig(); + console.log(`⏱️ [boot] Config load: ${(performance.now() - t0).toFixed(0)}ms`); // Resolve service tier configuration (validate deps, auto-promote) + t0 = performance.now(); const servicesConfig = resolveServicesConfig(config.services); logServicesConfig(servicesConfig); + console.log(`⏱️ [boot] Service tier resolution: ${(performance.now() - t0).toFixed(0)}ms`); const svcTier = (group: string): ServiceTier => getServiceTier(servicesConfig, group as ServiceGroupName); @@ -342,7 +349,9 @@ export async function startDaemon(options?: DaemonStartOptions): Promise { configureChannels(app); configureSwagger(app, { version: DAEMON_VERSION, port: DAEMON_PORT }); + t0 = performance.now(); const { db } = await initializeDatabase(DB_PATH); + console.log(`⏱️ [boot] Database init: ${(performance.now() - t0).toFixed(0)}ms`); // -------------------------------------------------------------------------- // RBAC flags @@ -354,6 +363,7 @@ export async function startDaemon(options?: DaemonStartOptions): Promise { // -------------------------------------------------------------------------- // Phase 1: Register services // -------------------------------------------------------------------------- + t0 = performance.now(); const services = await registerServices({ db, app, @@ -368,10 +378,12 @@ export async function startDaemon(options?: DaemonStartOptions): Promise { allowSuperadmin, requireAuth, }); + console.log(`⏱️ [boot] Phase 1 — registerServices: ${(performance.now() - t0).toFixed(0)}ms`); // -------------------------------------------------------------------------- // Phase 2: Register hooks // -------------------------------------------------------------------------- + t0 = performance.now(); registerHooks({ db, app, @@ -390,10 +402,12 @@ export async function startDaemon(options?: DaemonStartOptions): Promise { usersRepository: services.usersRepository, sessionsRepository: services.sessionsRepository, }); + console.log(`⏱️ [boot] Phase 2 — registerHooks: ${(performance.now() - t0).toFixed(0)}ms`); // -------------------------------------------------------------------------- // Phase 3: Register routes (auth, REST, tier hooks, error handler) // -------------------------------------------------------------------------- + t0 = performance.now(); await registerRoutes({ db, app, @@ -419,6 +433,7 @@ export async function startDaemon(options?: DaemonStartOptions): Promise { sessionMCPServersService: services.sessionMCPServersService, terminalsService: services.terminalsService, }); + console.log(`⏱️ [boot] Phase 3 — registerRoutes: ${(performance.now() - t0).toFixed(0)}ms`); // -------------------------------------------------------------------------- // Phase 4: Startup (orphan cleanup, health, scheduler, listen, shutdown) @@ -434,5 +449,6 @@ export async function startDaemon(options?: DaemonStartOptions): Promise { getSocketServer: socketIOConfig.getSocketServer, sessionsService: services.sessionsService, terminalsService: services.terminalsService, + tBoot, }); } diff --git a/apps/agor-daemon/src/register-services.ts b/apps/agor-daemon/src/register-services.ts index 272e3d796..530a8e168 100644 --- a/apps/agor-daemon/src/register-services.ts +++ b/apps/agor-daemon/src/register-services.ts @@ -38,30 +38,16 @@ import { persistOAuthToken, saveOAuth21TokenToDB, } from './oauth-cache.js'; -import type { ArtifactsService } from './services/artifacts.js'; -import { createArtifactsService } from './services/artifacts.js'; -import { createBoardCommentsService } from './services/board-comments.js'; -import { createBoardObjectsService } from './services/board-objects.js'; -import { createBoardsService } from './services/boards.js'; -import { createCardTypesService } from './services/card-types.js'; -import { createCardsService } from './services/cards.js'; import { createConfigService } from './services/config.js'; import { createContextService } from './services/context.js'; import { createFileService } from './services/file.js'; import { createFilesService } from './services/files.js'; -import { createGatewayService } from './services/gateway.js'; -import { createGatewayChannelsService } from './services/gateway-channels.js'; -import { registerGitHubAppSetupRoutes } from './services/github-app-setup.js'; -import { createLeaderboardService } from './services/leaderboard.js'; -import { createMCPServersService } from './services/mcp-servers.js'; import { createMessagesService } from './services/messages.js'; import { performOAuthDisconnect } from './services/oauth-disconnect.js'; import { createReposService } from './services/repos.js'; import { createSessionMCPServersService } from './services/session-mcp-servers.js'; import { createSessionsService } from './services/sessions.js'; import { createTasksService } from './services/tasks.js'; -import { TerminalsService } from './services/terminals.js'; -import { createThreadSessionMapService } from './services/thread-session-map.js'; import { createUsersService } from './services/users.js'; import { setupWorktreeOwnersService } from './services/worktree-owners.js'; import { createWorktreesService } from './services/worktrees.js'; @@ -95,7 +81,7 @@ export interface RegisteredServices { usersRepository: import('@agor/core/db').UsersRepository; sessionsRepository: import('@agor/core/db').SessionRepository; sessionMCPServersService: ReturnType; - terminalsService: TerminalsService | null; + terminalsService: import('./services/terminals.js').TerminalsService | null; configService: ReturnType; boardCommentsService: unknown; } @@ -154,6 +140,7 @@ export async function registerServices(ctx: RegisterServicesContext): Promise { safeService, getSocketServer, terminalsService, + tBoot, } = ctx; + const tStartup = performance.now(); + // 1. Cleanup orphaned tasks/sessions from previous daemon instance + let t0 = performance.now(); await cleanupOrphans(ctx); + console.log(`⏱️ [boot] cleanupOrphans: ${(performance.now() - t0).toFixed(0)}ms`); // 2. Initialize Health Monitor for periodic environment health checks + t0 = performance.now(); const healthMonitor = await createHealthMonitor(app); + console.log(`⏱️ [boot] createHealthMonitor: ${(performance.now() - t0).toFixed(0)}ms`); // 3. Validate/generate master secret for API key encryption + t0 = performance.now(); await ensureMasterSecret(config); + console.log(`⏱️ [boot] ensureMasterSecret: ${(performance.now() - t0).toFixed(0)}ms`); // 4. Start server + t0 = performance.now(); const server = await app.listen(DAEMON_PORT, DAEMON_HOST); + console.log(`⏱️ [boot] app.listen: ${(performance.now() - t0).toFixed(0)}ms`); const displayHost = DAEMON_HOST === '0.0.0.0' ? 'localhost' : DAEMON_HOST; console.log( @@ -211,9 +223,12 @@ export async function startup(ctx: StartupContext): Promise { console.log(` - /context`); console.log(` - /users`); - // 5. Start scheduler service (background worker) - let schedulerService: SchedulerService | null = null; + // 5. Start scheduler service (background worker) — dynamically imported to avoid + // loading the module at all when scheduler is disabled (lean mode optimization) + let schedulerService: import('./services/scheduler.js').SchedulerService | null = null; if (svcEnabled('scheduler')) { + t0 = performance.now(); + const { SchedulerService } = await import('./services/scheduler.js'); schedulerService = new SchedulerService(db, app, { tickInterval: 30000, // 30 seconds gracePeriod: 120000, // 2 minutes @@ -221,17 +236,22 @@ export async function startup(ctx: StartupContext): Promise { unixUserMode: config.execution?.unix_user_mode ?? 'simple', }); schedulerService.start(); + console.log(`⏱️ [boot] scheduler init: ${(performance.now() - t0).toFixed(0)}ms`); console.log(`🔄 Scheduler started (tick interval: 30s)`); } // 6. Initialize gateway: refresh channel state cache, then start Socket Mode listeners const gatewayService = safeService('gateway') as unknown as GatewayService | undefined; if (gatewayService) { + const tGw = performance.now(); gatewayService .refreshChannelState() .then(() => { return gatewayService.startListeners(); }) + .then(() => { + console.log(`⏱️ [boot] gateway init (async): ${(performance.now() - tGw).toFixed(0)}ms`); + }) .catch((error: unknown) => { console.error('[gateway] Failed to start listeners:', error); }); @@ -308,4 +328,7 @@ export async function startup(ctx: StartupContext): Promise { process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); + + console.log(`⏱️ [boot] Phase 4 — startup: ${(performance.now() - tStartup).toFixed(0)}ms`); + console.log(`⏱️ [boot] Total boot time: ${(performance.now() - tBoot).toFixed(0)}ms`); } diff --git a/packages/core/src/types/config-services.ts b/packages/core/src/types/config-services.ts index 86822c57c..ba2cbcd70 100644 --- a/packages/core/src/types/config-services.ts +++ b/packages/core/src/types/config-services.ts @@ -23,6 +23,26 @@ export const SERVICE_TIER_RANK: Record = { /** * The 13 service groups configurable in config.yaml under `services:`. * All default to 'on' for backward compatibility. + * + * ## Lean / Headless Preset + * + * For minimal headless deployments (e.g., k8s executor pods), disable non-essential + * services to reduce boot time and memory footprint: + * + * ```yaml + * services: + * scheduler: off # no cron-based session spawning + * terminals: off # no xterm/pty (heavy native deps) + * gateway: off # no Slack/Discord SDK (heavy) + * boards: off # no spatial canvas + * cards: off # no kanban cards + * artifacts: off # no sandpack artifacts + * leaderboard: off # no usage analytics + * static_files: off # no UI bundle serving + * ``` + * + * This leaves core, worktrees, repos, users, file_browser, and mcp_servers active — + * the minimum needed for prompt execution and worktree management. */ export interface DaemonServicesConfig { /** sessions, tasks, messages — the prompt loop */