import { nativeTheme } from "electron"; import type { IpcMainInvokeEvent } from "electron"; import type { AgentApiServer } from "./api-server"; import type { AppDatabase } from "./database"; import { BrowserManager } from "./browser-manager"; import type { DesktopResult } from "@shared/contracts"; import { createConnectorHealthService } from "./connector-health-service"; import { createPiRuntimeServices, type PiRuntimeServices } from "./service-graph-pi-runtime"; import { RuntimeMessageInjector } from "./agent-runtime"; import type { DesktopEvent } from "@shared/types"; import type { NotificationOptions } from "./notification-service"; import type { createAutomationService } from "./automation-service"; import type { createBrowserActionService } from "./browser-action-service"; import type { createOnboardingService } from "./onboarding-service"; import type { createOrchestrationService } from "./orchestration-service"; import type { createUpdaterService } from "./updater-service"; import { createLeafServices } from "./service-graph/create-leaf-services"; import { createDesktopShell } from "./service-graph/create-desktop-shell"; import { buildWorkerService } from "./service-graph/create-worker-service"; import { buildOrchestrationService } from "./service-graph/create-orchestration-service"; import { createBackgroundServices } from "./service-graph/create-background-services"; import { createLifecycle } from "./service-graph/create-lifecycle"; import { LateBinding } from "./service-graph/late-binding"; 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: { debug: (...args: unknown[]) => void; 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; onRunStarted?: (runId: string, taskId: string | null, agentId: string, companyId: string) => void; onRunCompleted?: (runId: string, taskId: string | null, agentId: string, companyId: string, status: string, summary: string | null, errorMessage: string | null) => void; /** Checkpoint engine for unified co-founder mode pause decisions in scheduler. */ checkpointEngine?: { shouldPauseHeartbeat: (companyId: string, agentId: string) => boolean }; /** Trusted-sender IPC wrapper from createIpcSupport. Window-control channels use it. */ registerHandle: ( channel: string, handler: (event: IpcMainInvokeEvent, ...args: unknown[]) => Promise> | DesktopResult, ) => void; } /** * Thin orchestrator: assembles services in a deterministic topological order * and exposes them via read-only getters. All wiring is performed via * explicit dependency passing; late-bound cycles are handled by * `LateBinding` slots that throw if accessed before assignment. */ export function createServiceGraph(deps: ServiceGraphDependencies) { const { db, apiServerRef, browserManagerRef, isQuittingRef, preloadPath, rendererDist, iconPath, isDev, devServerUrl, windowStatePath, workerPath, logger, emitEvent, publishDomainChanged, notify, createRunToken, seedDefaultAutomationRules, onRunStarted, onRunCompleted, checkpointEngine, registerHandle, } = deps; // Service slots owned by the graph. Null before init() completes and after // the lifecycle drain has torn services down. let desktopShellService: ReturnType | null = null; let connectorHealthService: ReturnType | null = null; let workerService: ReturnType | null = null; let orchestrationService: ReturnType | null = null; let automationService: ReturnType | null = null; let browserActionService: ReturnType | null = null; let onboardingService: ReturnType | null = null; let updaterService: ReturnType | null = null; let appLifecycleService: ReturnType | null = null; let leaf: ReturnType | null = null; let runtimeMessageInjector: RuntimeMessageInjector | null = null; const piRuntimeRef: { current: PiRuntimeServices | null } = { current: null }; // Late-bound slots resolve circular construction: worker needs orchestration // (for dispatchRunToWorker), orchestration needs worker (for enqueueWorkerRun), // orchestration needs automation (for dispatchAutomation), automation/orchestration // need browser-action (for dispatchBrowserAction). Each is filled inside // init() before the corresponding getter closure is ever invoked. const orchestrationRef = new LateBinding>("orchestrationService"); const automationRef = new LateBinding>("automationService"); const browserActionRef = new LateBinding>("browserActionService"); const updaterRef = new LateBinding>("updaterService"); function requireLeaf() { if (!leaf) throw new Error("Leaf services are not initialized."); return leaf; } function init() { nativeTheme.themeSource = db.snap.listSnapshot().theme; leaf = createLeafServices({ db, logger, notify, publishDomainChanged }); desktopShellService = createDesktopShell({ db, preloadPath, rendererDist, iconPath, isDev, devServerUrl, windowStatePath, isQuittingRef, emitEvent, notify, registerHandle, updaterRef, logger, }); piRuntimeRef.current = createPiRuntimeServices({ db, emitEvent, logger, refreshConnectorHealth: async (connectorId: string) => { if (connectorHealthService) { await connectorHealthService.checkConnector(connectorId as never); } }, }); connectorHealthService = createConnectorHealthService({ db, logger, publishDomainChanged, credentialStore: piRuntimeRef.current.credentialStore, }); workerService = buildWorkerService({ db, logger, workerPath, isQuittingRef, publishDomainChanged, emitEvent, orchestrationRef, automationRef, onRunStarted, onRunCompleted, notify, budgetService: leaf.budgetEnforcementService, sessionService: leaf.sessionManagementService, piRuntimeRef, getRuntimeMessageInjector: () => runtimeMessageInjector, }); runtimeMessageInjector = new RuntimeMessageInjector({ db, runtimeBridge: piRuntimeRef.current.runtimeBridge, postToWorker: (message) => { const worker = workerService?.getWorker(); if (!worker) throw new Error("Worker is not initialised — cannot deliver steer message."); worker.postMessage(message); }, getWakeAgent: () => { if (!orchestrationRef.isAssigned()) return null; const orch = orchestrationRef.get(); return (agentId, companyId, trigger) => orch.wakeAgentIfPossible(agentId, companyId, trigger); }, logger, }); orchestrationService = buildOrchestrationService({ db, logger, publishDomainChanged, emitEvent, notify, createRunToken, getApiPort: () => apiServerRef.current?.port ?? 0, isQuittingRef, skillService: leaf.skillService, budgetService: leaf.budgetEnforcementService, sessionService: leaf.sessionManagementService, workerService, automationRef, browserActionRef, checkpointEngine, }); orchestrationRef.set(orchestrationService); workerService.createWorker(); const background = createBackgroundServices({ db, logger, emitEvent, publishDomainChanged, notify, isQuittingRef, browserManagerRef, skillService: leaf.skillService, orchestrationService, seedDefaultAutomationRules, }); automationService = background.automationService; browserActionService = background.browserActionService; onboardingService = background.onboardingService; updaterService = background.updaterService; automationRef.set(automationService); browserActionRef.set(browserActionService); updaterRef.set(updaterService); appLifecycleService = createLifecycle({ db, logger, isQuittingRef, apiServerRef, browserManagerRef, piRuntimeRef, orchestrationService, automationService, workerService, desktopShellService, clearServiceRefs: () => { desktopShellService = null; workerService = null; browserActionService = null; automationService = null; connectorHealthService = null; onboardingService = null; orchestrationService = null; updaterService = null; appLifecycleService = null; runtimeMessageInjector = null; leaf = null; }, lateBindings: [ orchestrationRef as LateBinding, automationRef as LateBinding, browserActionRef as LateBinding, updaterRef as LateBinding, ], }); } return { init, getDesktopShellService: () => desktopShellService, getConnectorHealthService: () => connectorHealthService, getWorkerService: () => workerService, getBrowserActionService: () => browserActionService, getOrchestrationService: () => orchestrationService, getAutomationService: () => automationService, getOnboardingService: () => onboardingService, getUpdaterService: () => updaterService, getAppLifecycleService: () => appLifecycleService, getOrCreateSkillService: () => requireLeaf().skillService, getOrCreateBudgetEnforcementService: () => requireLeaf().budgetEnforcementService, getOrCreateSessionManagementService: () => requireLeaf().sessionManagementService, getPiRuntime: () => piRuntimeRef.current, getRuntimeMessageInjector: () => runtimeMessageInjector, }; }