import { app } from "electron"; import type { AgentApiServer } from "./api-server"; import type { AppDatabase } from "./database"; import { BrowserManager } from "./browser-manager"; import { createAutomationService, autoAssignSkillsToAgent } from "./automation-service"; import { createBrowserActionService } from "./browser-action-service"; import { createConnectorHealthService } from "./connector-health-service"; import { createDesktopShellService } from "./desktop-shell-service"; import { createOnboardingService } from "./onboarding-service"; import { createOrchestrationService } from "./orchestration-service"; import { createSkillService } from "./skill-service"; import { createUpdaterService } from "./updater-service"; import { createWorkerService } from "./worker-service"; import { createAppLifecycleService } from "./app-lifecycle-service"; import { createBudgetEnforcementService } from "./budget-enforcement-service"; import { createSessionManagementService } from "./session-management-service"; import type { DesktopEvent, RunRecord } from "@shared/types"; import type { AutomationEventContext } from "./automation-service"; import type { NotificationOptions } from "./notification-service"; interface ServiceGraphDependencies { db: AppDatabase; apiServerRef: { current: AgentApiServer | null }; browserManagerRef: { current: BrowserManager | null }; isQuittingRef: { current: boolean }; preloadPath: string; rendererDist: string; iconPath: string; isDev: boolean; devServerUrl?: string; windowStatePath: string; workerPath: string; logger: { info: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void; }; emitEvent: (event: DesktopEvent) => void; publishDomainChanged: () => void; notify: (options: NotificationOptions) => void; createRunToken: (agentId: string, companyId: string, runId: string) => string; seedDefaultAutomationRules: (companyId: string) => void; } export function createServiceGraph({ db, apiServerRef, browserManagerRef, isQuittingRef, preloadPath, rendererDist, iconPath, isDev, devServerUrl, windowStatePath, workerPath, logger, emitEvent, publishDomainChanged, notify, createRunToken, seedDefaultAutomationRules, }: ServiceGraphDependencies) { let automationService: ReturnType | null = null; let browserActionService: ReturnType | null = null; let connectorHealthService: ReturnType | null = null; let onboardingService: ReturnType | null = null; let desktopShellService: ReturnType | null = null; let orchestrationService: ReturnType | null = null; let workerService: ReturnType | null = null; let updaterService: ReturnType | null = null; let appLifecycleService: ReturnType | null = null; let skillService: ReturnType | null = null; let budgetEnforcementService: ReturnType | null = null; let sessionManagementService: ReturnType | null = null; function getOrCreateSkillService() { if (!skillService) { skillService = createSkillService({ db, profileDir: db.profileDir, logger, }); } return skillService; } function getOrCreateBudgetEnforcementService() { if (!budgetEnforcementService) { budgetEnforcementService = createBudgetEnforcementService({ db, logger, notify, publishDomainChanged, }); } return budgetEnforcementService; } function getOrCreateSessionManagementService() { if (!sessionManagementService) { sessionManagementService = createSessionManagementService({ db, logger, }); } return sessionManagementService; } const wakeAgentIfPossible =(agentId: string | null | undefined, companyId: string, trigger: string) => { if (!orchestrationService) { throw new Error("Orchestration service is not initialized."); } return orchestrationService.wakeAgentIfPossible(agentId, companyId, trigger); }; const dispatchBrowserAction = async (actionId: string, companyId: string) => { if (!browserActionService) { throw new Error("Browser action service is not initialized"); } return browserActionService.dispatchBrowserAction(actionId, companyId); }; const dispatchAutomation = (trigger: string, ctx: AutomationEventContext) => { if (!automationService) { throw new Error("Automation service is not initialized."); } automationService.dispatchAutomation(trigger, ctx); }; const processRunCompletion = (run: RunRecord, event: Parameters["processRunCompletion"]>[1]) => { if (!automationService) { throw new Error("Automation service is not initialized."); } automationService.processRunCompletion(run, event); }; const queueTaskRun = (taskId: string, trigger: string, actor: string) => { if (!orchestrationService) { throw new Error("Orchestration service is not initialized."); } return orchestrationService.queueTaskRun(taskId, trigger, actor); }; const dispatchRunToWorker = (params: Parameters["dispatchRunToWorker"]>[0]) => { if (!orchestrationService) { throw new Error("Orchestration service is not initialized."); } return orchestrationService.dispatchRunToWorker(params); }; function init() { desktopShellService = createDesktopShellService({ preloadPath, rendererDist, iconPath, isDev, devServerUrl, windowStatePath, shouldQuit: () => isQuittingRef.current, onRequestQuit: () => { isQuittingRef.current = true; }, onCheckForUpdates: async () => { if (!updaterService) throw new Error("Updater service is not initialized."); await updaterService.checkForUpdates(); }, onBackupProfile: async () => { const target = await import("electron").then(({ dialog }) => dialog.showOpenDialog({ properties: ["openDirectory", "createDirectory"], })); if (target.canceled || target.filePaths.length === 0) return; await db.exportBackup(`${target.filePaths[0]!}/agentcompany-backup-${Date.now()}`); }, onShortcut: (action) => emitEvent({ type: "shortcut", action }), notify, getActiveRunCount: () => { try { const snapshot = db.listSnapshot(); return snapshot.runs.filter((r) => r.status === "running" || r.status === "queued").length; } catch { return 0; } }, }); desktopShellService.initialize(); connectorHealthService = createConnectorHealthService({ db, logger, publishDomainChanged, }); workerService = createWorkerService({ db, logger, workerPath, isQuitting: () => isQuittingRef.current, publishDomainChanged, emitEvent, wakeAgentIfPossible, dispatchRunToWorker, processRunCompletion: (run, event) => processRunCompletion(run, event), notify, budgetService: getOrCreateBudgetEnforcementService(), sessionService: getOrCreateSessionManagementService(), }); orchestrationService = createOrchestrationService({ db, logger, publishDomainChanged, emitEvent, notify, createRunToken, getApiPort: () => apiServerRef.current?.getPort() ?? 0, dispatchAutomation, hasWorkerReady: () => Boolean(workerService?.getWorker()), enqueueWorkerRun: (payload) => { workerService?.getWorker()?.postMessage({ type: "enqueue-run", payload }); }, isQuitting: () => isQuittingRef.current, listRuntimeSkillEntries: (() => { let cachedSkillService: ReturnType | null = null; return (companyId: string, agentId: string, connectorId: string) => { try { if (!cachedSkillService) { cachedSkillService = createSkillService({ db, profileDir: db.profileDir, logger }); } return cachedSkillService.listRuntimeSkillEntries(companyId, agentId, connectorId); } catch { return []; } }; })(), budgetService: getOrCreateBudgetEnforcementService(), getSessionHandoff: (taskId: string) => { try { return getOrCreateSessionManagementService().getHandoffContext(taskId); } catch { return null; } }, evaluatePreRunSessionRotation: (taskId: string, connectorId: string) => { try { const sessionSvc = getOrCreateSessionManagementService(); const activeSession = db.getActiveTaskSession(taskId); if (!activeSession) return { rotated: false, handoff: null }; const result = sessionSvc.evaluateRotation(activeSession, connectorId); if (result.shouldRotate) { sessionSvc.rotateSession(activeSession); const handoff = sessionSvc.getHandoffContext(taskId); return { rotated: true, handoff }; } return { rotated: false, handoff: null }; } catch { return { rotated: false, handoff: null }; } }, }); workerService.createWorker(); automationService = createAutomationService({ db, logger, emitEvent, publishDomainChanged, notify, wakeAgentIfPossible, dispatchBrowserAction, isQuitting: () => isQuittingRef.current, }); onboardingService = createOnboardingService({ db, seedDefaultAutomationRules, queueTaskRun, publishDomainChanged, scanAndAssignSkills: async (companyId: string, agentId: string) => { const svc = getOrCreateSkillService(); await svc.seedBuiltinSkills(companyId); svc.assignRoleSkillsToAgent(companyId, agentId, "CEO"); await svc.scanWorkspaceSkills(companyId); autoAssignSkillsToAgent(db, logger, companyId, agentId); }, }); browserActionService = createBrowserActionService({ db, browserManagerRef, publishDomainChanged, }); browserActionService.resetBrowserManager(); updaterService = createUpdaterService({ db, logger, notify, }); updaterService.attachListeners(); appLifecycleService = createAppLifecycleService({ getMainWindow: () => desktopShellService?.getMainWindow() ?? null, ensureWindow: async () => desktopShellService?.ensureWindow() ?? null, shouldQuit: () => isQuittingRef.current, onBeforeQuit: () => { isQuittingRef.current = true; orchestrationService?.stopHeartbeatScheduler(); automationService?.destroy(); apiServerRef.current?.stop(); apiServerRef.current = null; browserManagerRef.current?.shutdown(); browserManagerRef.current = null; desktopShellService?.destroy(); desktopShellService = null; workerService?.destroy(); workerService = null; browserActionService = null; automationService = null; connectorHealthService = null; onboardingService = null; orchestrationService = null; updaterService = null; appLifecycleService = null; skillService = null; budgetEnforcementService = null; sessionManagementService = null; db.close(); // Force exit after grace period — covers all platforms (macOS Dock, Windows tray, Linux) // Worker child processes (claude/codex CLI) may keep the event loop alive indefinitely setTimeout(() => { app.exit(0); }, 3000); }, }); } return { init, getDesktopShellService: () => desktopShellService, getConnectorHealthService: () => connectorHealthService, getWorkerService: () => workerService, getBrowserActionService: () => browserActionService, getOrchestrationService: () => orchestrationService, getAutomationService: () => automationService, getOnboardingService: () => onboardingService, getUpdaterService: () => updaterService, getAppLifecycleService: () => appLifecycleService, getOrCreateSkillService, getOrCreateBudgetEnforcementService, getOrCreateSessionManagementService, }; }