Migrate all remaining pass-through call sites (social, skills, budget,
entityCrud, documents, knowledge, automation, wakeup, backup, threads,
settings, snapshot, connectors, workspaces, activity) to their owning
domain modules. `AppDatabase` now carries only three methods:
- createHireRequest (multi-module composite: agents + approvalsMod)
- rollbackConfigRevision (multi-module composite: documents + entityCrud)
- queryAll (raw-SQL escape hatch, @internal)
The vault/env-binding infrastructure previously passed argument-by-
argument into `entityCrud.saveSecret`/`resolveSecretEnv` now binds at
module construction, letting callers invoke `db.entityCrud.saveSecret({...})`
without carrying vault plumbing.
Structural types that narrowed AppDatabase to a bag of flat methods
(`ConnectorModelPolicyDeps` callers, `CapabilityMatchDb`, test mocks in
budget-enforcement / budget-incidents / task-quality-gates /
failure-escalation / task-quality-gates-sweep) are reshaped to the new
namespace. `connectorPolicyDepsFromDb` replaces the old pattern of
handing the whole AppDatabase to `validateConnectorModelSelection`.
A new ESLint rule in eslint.config.mjs blocks new top-level methods on
AppDatabase so the pass-through layer cannot silently grow back.
`AppDatabase.ts` dropped from 193 lines to 58; `AppDatabaseBase` lost
the 270-line trailing pass-through block.
285 lines
11 KiB
TypeScript
285 lines
11 KiB
TypeScript
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<unknown>> | DesktopResult<unknown>,
|
|
) => 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<typeof createDesktopShell> | null = null;
|
|
let connectorHealthService: ReturnType<typeof createConnectorHealthService> | null = null;
|
|
let workerService: ReturnType<typeof buildWorkerService> | null = null;
|
|
let orchestrationService: ReturnType<typeof createOrchestrationService> | null = null;
|
|
let automationService: ReturnType<typeof createAutomationService> | null = null;
|
|
let browserActionService: ReturnType<typeof createBrowserActionService> | null = null;
|
|
let onboardingService: ReturnType<typeof createOnboardingService> | null = null;
|
|
let updaterService: ReturnType<typeof createUpdaterService> | null = null;
|
|
let appLifecycleService: ReturnType<typeof createLifecycle> | null = null;
|
|
let leaf: ReturnType<typeof createLeafServices> | 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<ReturnType<typeof createOrchestrationService>>("orchestrationService");
|
|
const automationRef = new LateBinding<ReturnType<typeof createAutomationService>>("automationService");
|
|
const browserActionRef = new LateBinding<ReturnType<typeof createBrowserActionService>>("browserActionService");
|
|
const updaterRef = new LateBinding<ReturnType<typeof createUpdaterService>>("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<unknown>,
|
|
automationRef as LateBinding<unknown>,
|
|
browserActionRef as LateBinding<unknown>,
|
|
updaterRef as LateBinding<unknown>,
|
|
],
|
|
});
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|