update
This commit is contained in:
1 parent
806200b3d1
commit
5547c29491
216 files changed
+5043
-3352
No files matched your search
@@ -0,0 +1,95 @@
|
||||
import type { AppDatabase } from "../database";
|
||||
import { BrowserManager } from "../browser-manager";
|
||||
import type { DesktopEvent } from "@shared/types";
|
||||
import { createAutomationService } from "../automation-service";
|
||||
import { createBrowserActionService } from "../browser-action-service";
|
||||
import { createOnboardingService } from "../onboarding-service";
|
||||
import { createUpdaterService } from "../updater-service";
|
||||
import type { createOrchestrationService } from "../orchestration-service";
|
||||
import type { createSkillService } from "../skill-service";
|
||||
import type { NotificationOptions } from "../notification-service";
|
||||
|
||||
export interface BackgroundServicesDeps {
|
||||
db: AppDatabase;
|
||||
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;
|
||||
isQuittingRef: { current: boolean };
|
||||
browserManagerRef: { current: BrowserManager | null };
|
||||
skillService: ReturnType<typeof createSkillService>;
|
||||
orchestrationService: ReturnType<typeof createOrchestrationService>;
|
||||
seedDefaultAutomationRules: (companyId: string) => void;
|
||||
}
|
||||
|
||||
export interface BackgroundServices {
|
||||
automationService: ReturnType<typeof createAutomationService>;
|
||||
browserActionService: ReturnType<typeof createBrowserActionService>;
|
||||
onboardingService: ReturnType<typeof createOnboardingService>;
|
||||
updaterService: ReturnType<typeof createUpdaterService>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build background services that run off the main dispatch loop: automation
|
||||
* triggers, browser-action dispatch, onboarding hire flow, and the updater.
|
||||
* Orchestration is fully assembled at this point, so all dependencies are
|
||||
* concrete references rather than lazy slots.
|
||||
*/
|
||||
export function createBackgroundServices({
|
||||
db,
|
||||
logger,
|
||||
emitEvent,
|
||||
publishDomainChanged,
|
||||
notify,
|
||||
isQuittingRef,
|
||||
browserManagerRef,
|
||||
skillService,
|
||||
orchestrationService,
|
||||
seedDefaultAutomationRules,
|
||||
}: BackgroundServicesDeps): BackgroundServices {
|
||||
const browserActionService = createBrowserActionService({
|
||||
db,
|
||||
browserManagerRef,
|
||||
publishDomainChanged,
|
||||
});
|
||||
browserActionService.resetBrowserManager();
|
||||
|
||||
const dispatchBrowserAction = async (actionId: string, companyId: string) =>
|
||||
browserActionService.dispatchBrowserAction(actionId, companyId);
|
||||
|
||||
const automationService = createAutomationService({
|
||||
db,
|
||||
logger,
|
||||
emitEvent,
|
||||
publishDomainChanged,
|
||||
notify,
|
||||
wakeAgentIfPossible: (agentId, companyId, trigger) =>
|
||||
orchestrationService.wakeAgentIfPossible(agentId, companyId, trigger),
|
||||
dispatchBrowserAction,
|
||||
isQuitting: () => isQuittingRef.current,
|
||||
});
|
||||
|
||||
const onboardingService = createOnboardingService({
|
||||
db,
|
||||
seedDefaultAutomationRules,
|
||||
queueTaskRun: (taskId, trigger, actor) =>
|
||||
orchestrationService.queueTaskRun(taskId, trigger, actor),
|
||||
publishDomainChanged,
|
||||
scanAndAssignSkills: async (companyId: string, _agentId: string) => {
|
||||
// All company skills are visible to every agent via progressive disclosure.
|
||||
// Seed the builtin catalog + any workspace-local SKILL.md bundles.
|
||||
await skillService.seedBuiltinSkills(companyId);
|
||||
await skillService.scanWorkspaceSkills(companyId);
|
||||
},
|
||||
});
|
||||
|
||||
const updaterService = createUpdaterService({ db, logger, notify });
|
||||
updaterService.attachListeners();
|
||||
|
||||
return { automationService, browserActionService, onboardingService, updaterService };
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { IpcMainInvokeEvent } from "electron";
|
||||
import type { AppDatabase } from "../database";
|
||||
import type { DesktopEvent } from "@shared/types";
|
||||
import type { DesktopResult } from "@shared/contracts";
|
||||
import { createDesktopShellService } from "../desktop-shell-service";
|
||||
import type { NotificationOptions } from "../notification-service";
|
||||
import type { LateBinding } from "./late-binding";
|
||||
import type { createUpdaterService } from "../updater-service";
|
||||
|
||||
export interface DesktopShellDeps {
|
||||
db: AppDatabase;
|
||||
preloadPath: string;
|
||||
rendererDist: string;
|
||||
iconPath: string;
|
||||
isDev: boolean;
|
||||
devServerUrl?: string;
|
||||
windowStatePath: string;
|
||||
isQuittingRef: { current: boolean };
|
||||
emitEvent: (event: DesktopEvent) => void;
|
||||
notify: (options: NotificationOptions) => void;
|
||||
registerHandle: (
|
||||
channel: string,
|
||||
handler: (event: IpcMainInvokeEvent, ...args: unknown[]) => Promise<DesktopResult<unknown>> | DesktopResult<unknown>,
|
||||
) => void;
|
||||
updaterRef: LateBinding<ReturnType<typeof createUpdaterService>>;
|
||||
logger: { warn: (...args: unknown[]) => void };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the desktop shell service. `updaterRef` is late-bound because the
|
||||
* updater is constructed after the shell; the menu's "Check for updates"
|
||||
* handler resolves it at call time.
|
||||
*/
|
||||
export function createDesktopShell({
|
||||
db,
|
||||
preloadPath,
|
||||
rendererDist,
|
||||
iconPath,
|
||||
isDev,
|
||||
devServerUrl,
|
||||
windowStatePath,
|
||||
isQuittingRef,
|
||||
emitEvent,
|
||||
notify,
|
||||
registerHandle,
|
||||
updaterRef,
|
||||
logger,
|
||||
}: DesktopShellDeps) {
|
||||
const desktopShellService = createDesktopShellService({
|
||||
preloadPath,
|
||||
rendererDist,
|
||||
iconPath,
|
||||
isDev,
|
||||
devServerUrl,
|
||||
windowStatePath,
|
||||
registerHandle,
|
||||
shouldQuit: () => isQuittingRef.current,
|
||||
onRequestQuit: () => {
|
||||
isQuittingRef.current = true;
|
||||
},
|
||||
onCheckForUpdates: async () => {
|
||||
await updaterRef.get().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 (error) {
|
||||
logger.warn("[service-graph] getActiveRunCount failed:", error);
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
});
|
||||
desktopShellService.initialize();
|
||||
return desktopShellService;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { AppDatabase } from "../database";
|
||||
import { createBudgetEnforcementService } from "../budget-enforcement-service";
|
||||
import { createSessionManagementService } from "../session-management-service";
|
||||
import { createSkillService } from "../skill-service";
|
||||
import type { NotificationOptions } from "../notification-service";
|
||||
|
||||
export interface LeafServicesDeps {
|
||||
db: AppDatabase;
|
||||
logger: {
|
||||
debug: (...args: unknown[]) => void;
|
||||
info: (...args: unknown[]) => void;
|
||||
warn: (...args: unknown[]) => void;
|
||||
error: (...args: unknown[]) => void;
|
||||
};
|
||||
notify: (options: NotificationOptions) => void;
|
||||
publishDomainChanged: () => void;
|
||||
}
|
||||
|
||||
export interface LeafServices {
|
||||
skillService: ReturnType<typeof createSkillService>;
|
||||
budgetEnforcementService: ReturnType<typeof createBudgetEnforcementService>;
|
||||
sessionManagementService: ReturnType<typeof createSessionManagementService>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build leaf services that no other service depends on except through this
|
||||
* bundle. Constructed first so downstream factories can receive concrete
|
||||
* references without null-checks.
|
||||
*/
|
||||
export function createLeafServices({
|
||||
db,
|
||||
logger,
|
||||
notify,
|
||||
publishDomainChanged,
|
||||
}: LeafServicesDeps): LeafServices {
|
||||
const skillService = createSkillService({ db, profileDir: db.profileDir, logger });
|
||||
const budgetEnforcementService = createBudgetEnforcementService({
|
||||
db,
|
||||
logger,
|
||||
notify,
|
||||
publishDomainChanged,
|
||||
});
|
||||
const sessionManagementService = createSessionManagementService({ db, logger });
|
||||
|
||||
return { skillService, budgetEnforcementService, sessionManagementService };
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { app } from "electron";
|
||||
import type { AgentApiServer } from "../api-server";
|
||||
import type { AppDatabase } from "../database";
|
||||
import { BrowserManager } from "../browser-manager";
|
||||
import { createAppLifecycleService } from "../app-lifecycle-service";
|
||||
import type { createAutomationService } from "../automation-service";
|
||||
import type { createDesktopShellService } from "../desktop-shell-service";
|
||||
import type { createOrchestrationService } from "../orchestration-service";
|
||||
import type { createWorkerService } from "../worker-service";
|
||||
import type { PiRuntimeServices } from "../service-graph-pi-runtime";
|
||||
import type { LateBinding } from "./late-binding";
|
||||
|
||||
/** Overall budget for awaited shutdown drain (worker exit + API close). */
|
||||
const SHUTDOWN_BUDGET_MS = 5_000;
|
||||
|
||||
export interface LifecycleDeps {
|
||||
db: AppDatabase;
|
||||
logger: {
|
||||
debug: (...args: unknown[]) => void;
|
||||
info: (...args: unknown[]) => void;
|
||||
warn: (...args: unknown[]) => void;
|
||||
error: (...args: unknown[]) => void;
|
||||
};
|
||||
isQuittingRef: { current: boolean };
|
||||
apiServerRef: { current: AgentApiServer | null };
|
||||
browserManagerRef: { current: BrowserManager | null };
|
||||
piRuntimeRef: { current: PiRuntimeServices | null };
|
||||
orchestrationService: ReturnType<typeof createOrchestrationService>;
|
||||
automationService: ReturnType<typeof createAutomationService>;
|
||||
workerService: ReturnType<typeof createWorkerService>;
|
||||
desktopShellService: ReturnType<typeof createDesktopShellService>;
|
||||
/**
|
||||
* Clears graph-owned service refs (so getters return null) after the drain
|
||||
* completes. Mirrors the original synchronous null-out at the tail of the
|
||||
* old onBeforeQuit.
|
||||
*/
|
||||
clearServiceRefs: () => void;
|
||||
/**
|
||||
* Late bindings owned by the graph; cleared after drain so any stray
|
||||
* post-quit access fails loudly rather than returning stale data.
|
||||
*/
|
||||
lateBindings: Array<LateBinding<unknown>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the app lifecycle service and wire a deterministic shutdown drain
|
||||
* that awaits the utility-process worker's exit (and API server close)
|
||||
* before calling app.exit(). On 5s timeout we log the stuck run ids and
|
||||
* exit(1) so operators see an unclean shutdown rather than a silent
|
||||
* force-kill.
|
||||
*/
|
||||
export function createLifecycle(deps: LifecycleDeps) {
|
||||
const {
|
||||
db,
|
||||
logger,
|
||||
isQuittingRef,
|
||||
apiServerRef,
|
||||
browserManagerRef,
|
||||
piRuntimeRef,
|
||||
orchestrationService,
|
||||
automationService,
|
||||
workerService,
|
||||
desktopShellService,
|
||||
clearServiceRefs,
|
||||
lateBindings,
|
||||
} = deps;
|
||||
|
||||
let drainPromise: Promise<number> | null = null;
|
||||
|
||||
function listActiveRunIds(): string[] {
|
||||
try {
|
||||
return db.listSnapshot().runs
|
||||
.filter((r) => r.status === "running" || r.status === "queued")
|
||||
.map((r) => r.id);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function drain(): Promise<number> {
|
||||
// Stop accepting new scheduled work first; cancel in-flight runs in the
|
||||
// worker via the utility-process shutdown postMessage inside destroy().
|
||||
try { orchestrationService.stopAllSchedulers(); } catch (err) { logger.warn("[lifecycle] stopAllSchedulers failed:", err); }
|
||||
try { automationService.destroy(); } catch (err) { logger.warn("[lifecycle] automationService.destroy failed:", err); }
|
||||
try { apiServerRef.current?.stop(); } catch (err) { logger.warn("[lifecycle] apiServer.stop failed:", err); }
|
||||
apiServerRef.current = null;
|
||||
try { browserManagerRef.current?.shutdown(); } catch (err) { logger.warn("[lifecycle] browserManager.shutdown failed:", err); }
|
||||
browserManagerRef.current = null;
|
||||
|
||||
// workerService.destroy() sends "shutdown" to the utility process and has
|
||||
// its own 2s SIGKILL safety net. Awaiting the worker's exit event closes
|
||||
// the remaining event-loop holder (the utilityProcess handle).
|
||||
const workerExit = new Promise<void>((resolve) => {
|
||||
const worker = workerService.getWorker();
|
||||
if (!worker) { resolve(); return; }
|
||||
worker.once("exit", () => resolve());
|
||||
});
|
||||
workerService.destroy();
|
||||
|
||||
const timeout = new Promise<"timeout">((resolve) => {
|
||||
const handle = setTimeout(() => resolve("timeout"), SHUTDOWN_BUDGET_MS);
|
||||
if (typeof handle.unref === "function") handle.unref();
|
||||
});
|
||||
const outcome = await Promise.race([
|
||||
workerExit.then(() => "drained" as const),
|
||||
timeout,
|
||||
]);
|
||||
|
||||
try { desktopShellService.destroy(); } catch (err) { logger.warn("[lifecycle] desktopShell.destroy failed:", err); }
|
||||
try { piRuntimeRef.current?.release(); } catch (err) { logger.warn("[lifecycle] piRuntime.release failed:", err); }
|
||||
piRuntimeRef.current = null;
|
||||
try { db.close(); } catch (err) { logger.warn("[lifecycle] db.close failed:", err); }
|
||||
|
||||
clearServiceRefs();
|
||||
for (const binding of lateBindings) binding.clear();
|
||||
|
||||
if (outcome === "timeout") {
|
||||
const stuck = listActiveRunIds();
|
||||
logger.error(
|
||||
`[lifecycle] Drain exceeded ${SHUTDOWN_BUDGET_MS}ms; stuck runs: ${stuck.length ? stuck.join(", ") : "<none>"}`,
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const appLifecycleService = createAppLifecycleService({
|
||||
getMainWindow: () => desktopShellService.getMainWindow(),
|
||||
ensureWindow: async () => desktopShellService.ensureWindow(),
|
||||
shouldQuit: () => isQuittingRef.current,
|
||||
onBeforeQuit: () => {
|
||||
if (drainPromise) return;
|
||||
isQuittingRef.current = true;
|
||||
drainPromise = drain().catch((err) => {
|
||||
logger.error("[lifecycle] drain threw unexpectedly:", err);
|
||||
return 1;
|
||||
});
|
||||
drainPromise.then((code) => {
|
||||
app.exit(code);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return appLifecycleService;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { AppDatabase } from "../database";
|
||||
import type { DesktopEvent } from "@shared/types";
|
||||
import { createOrchestrationService } from "../orchestration-service";
|
||||
import type { createBudgetEnforcementService } from "../budget-enforcement-service";
|
||||
import type { createSessionManagementService } from "../session-management-service";
|
||||
import type { createSkillService } from "../skill-service";
|
||||
import type { createWorkerService } from "../worker-service";
|
||||
import type { createAutomationService, AutomationEventContext } from "../automation-service";
|
||||
import type { createBrowserActionService } from "../browser-action-service";
|
||||
import type { NotificationOptions } from "../notification-service";
|
||||
import type { LateBinding } from "./late-binding";
|
||||
|
||||
export interface OrchestrationServiceDeps {
|
||||
db: AppDatabase;
|
||||
logger: {
|
||||
debug: (...args: unknown[]) => void;
|
||||
info: (...args: unknown[]) => void;
|
||||
warn: (...args: unknown[]) => void;
|
||||
error: (...args: unknown[]) => void;
|
||||
};
|
||||
publishDomainChanged: () => void;
|
||||
emitEvent: (event: DesktopEvent) => void;
|
||||
notify: (options: NotificationOptions) => void;
|
||||
createRunToken: (agentId: string, companyId: string, runId: string) => string;
|
||||
getApiPort: () => number;
|
||||
isQuittingRef: { current: boolean };
|
||||
skillService: ReturnType<typeof createSkillService>;
|
||||
budgetService: ReturnType<typeof createBudgetEnforcementService>;
|
||||
sessionService: ReturnType<typeof createSessionManagementService>;
|
||||
workerService: ReturnType<typeof createWorkerService>;
|
||||
automationRef: LateBinding<ReturnType<typeof createAutomationService>>;
|
||||
browserActionRef: LateBinding<ReturnType<typeof createBrowserActionService>>;
|
||||
checkpointEngine?: { shouldPauseHeartbeat: (companyId: string, agentId: string) => boolean };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the orchestration service with resolved dependencies. Late bindings
|
||||
* for automation and browser-action are required because orchestration's
|
||||
* dispatchAutomation closure is invoked long after assembly — the slots are
|
||||
* filled in the same init() pass, before any scheduler tick.
|
||||
*/
|
||||
export function buildOrchestrationService({
|
||||
db,
|
||||
logger,
|
||||
publishDomainChanged,
|
||||
emitEvent,
|
||||
notify,
|
||||
createRunToken,
|
||||
getApiPort,
|
||||
isQuittingRef,
|
||||
skillService,
|
||||
budgetService,
|
||||
sessionService,
|
||||
workerService,
|
||||
automationRef,
|
||||
checkpointEngine,
|
||||
}: OrchestrationServiceDeps) {
|
||||
const dispatchAutomation = (trigger: string, ctx: AutomationEventContext) => {
|
||||
automationRef.get().dispatchAutomation(trigger, ctx);
|
||||
};
|
||||
|
||||
return createOrchestrationService({
|
||||
db,
|
||||
logger,
|
||||
publishDomainChanged,
|
||||
emitEvent,
|
||||
notify,
|
||||
createRunToken,
|
||||
getApiPort,
|
||||
dispatchAutomation,
|
||||
hasWorkerReady: () => Boolean(workerService.getWorker()),
|
||||
enqueueWorkerRun: (payload) => {
|
||||
workerService.getWorker()?.postMessage({ type: "enqueue-run", payload });
|
||||
},
|
||||
isQuitting: () => isQuittingRef.current,
|
||||
listRuntimeSkillEntries: (companyId, agentId, connectorId) => {
|
||||
try {
|
||||
return skillService.listRuntimeSkillEntries(companyId, agentId, connectorId);
|
||||
} catch (error) {
|
||||
logger.warn(`[service-graph] listRuntimeSkillEntries failed for agent ${agentId}:`, error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
budgetService,
|
||||
checkpointEngine,
|
||||
getSessionHandoff: (taskId) => {
|
||||
try {
|
||||
return sessionService.getHandoffContext(taskId);
|
||||
} catch (error) {
|
||||
logger.warn(`[service-graph] getSessionHandoff failed for task ${taskId}:`, error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
evaluatePreRunSessionRotation: (taskId, connectorId) => {
|
||||
try {
|
||||
const activeSession = db.getActiveTaskSession(taskId);
|
||||
if (!activeSession) return { rotated: false, handoff: null };
|
||||
const result = sessionService.evaluateRotation(activeSession, connectorId);
|
||||
if (result.shouldRotate) {
|
||||
sessionService.rotateSession(activeSession);
|
||||
const handoff = sessionService.getHandoffContext(taskId);
|
||||
return { rotated: true, handoff };
|
||||
}
|
||||
return { rotated: false, handoff: null };
|
||||
} catch (error) {
|
||||
logger.warn(`[service-graph] evaluatePreRunSessionRotation failed for task ${taskId}:`, error);
|
||||
return { rotated: false, handoff: null };
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { AppDatabase } from "../database";
|
||||
import type { DesktopEvent, RunRecord } from "@shared/types";
|
||||
import { createWorkerService } from "../worker-service";
|
||||
import type { createOrchestrationService } from "../orchestration-service";
|
||||
import type { createAutomationService } from "../automation-service";
|
||||
import type { createBudgetEnforcementService } from "../budget-enforcement-service";
|
||||
import type { createSessionManagementService } from "../session-management-service";
|
||||
import type { PiRuntimeServices } from "../service-graph-pi-runtime";
|
||||
import type { NotificationOptions } from "../notification-service";
|
||||
import type { LateBinding } from "./late-binding";
|
||||
|
||||
export interface WorkerServiceDeps {
|
||||
db: AppDatabase;
|
||||
logger: {
|
||||
debug: (...args: unknown[]) => void;
|
||||
info: (...args: unknown[]) => void;
|
||||
warn: (...args: unknown[]) => void;
|
||||
error: (...args: unknown[]) => void;
|
||||
};
|
||||
workerPath: string;
|
||||
isQuittingRef: { current: boolean };
|
||||
publishDomainChanged: () => void;
|
||||
emitEvent: (event: DesktopEvent) => void;
|
||||
orchestrationRef: LateBinding<ReturnType<typeof createOrchestrationService>>;
|
||||
automationRef: LateBinding<ReturnType<typeof createAutomationService>>;
|
||||
onRunStarted?: (runId: string, taskId: string, agentId: string, companyId: string) => void;
|
||||
onRunCompleted?: (runId: string, taskId: string, agentId: string, companyId: string, status: string, summary: string | null, errorMessage: string | null) => void;
|
||||
notify: (options: NotificationOptions) => void;
|
||||
budgetService: ReturnType<typeof createBudgetEnforcementService>;
|
||||
sessionService: ReturnType<typeof createSessionManagementService>;
|
||||
piRuntimeRef: { current: PiRuntimeServices | null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the worker service. Orchestration and automation are late-bound via
|
||||
* LateBinding slots because they are constructed after the worker but close
|
||||
* over the worker's dispatch functions.
|
||||
*/
|
||||
export function buildWorkerService(deps: WorkerServiceDeps) {
|
||||
const {
|
||||
db,
|
||||
logger,
|
||||
workerPath,
|
||||
isQuittingRef,
|
||||
publishDomainChanged,
|
||||
emitEvent,
|
||||
orchestrationRef,
|
||||
automationRef,
|
||||
onRunStarted,
|
||||
onRunCompleted,
|
||||
notify,
|
||||
budgetService,
|
||||
sessionService,
|
||||
piRuntimeRef,
|
||||
} = deps;
|
||||
|
||||
const wakeAgentIfPossible = (agentId: string | null | undefined, companyId: string, trigger: string) =>
|
||||
orchestrationRef.get().wakeAgentIfPossible(agentId, companyId, trigger);
|
||||
|
||||
const dispatchRunToWorker: Parameters<typeof createWorkerService>[0]["dispatchRunToWorker"] = (params) =>
|
||||
orchestrationRef.get().dispatchRunToWorker(params);
|
||||
|
||||
const processRunCompletion = (
|
||||
run: RunRecord,
|
||||
event: Parameters<ReturnType<typeof createAutomationService>["processRunCompletion"]>[1],
|
||||
) => {
|
||||
automationRef.get().processRunCompletion(run, event);
|
||||
};
|
||||
|
||||
return createWorkerService({
|
||||
db,
|
||||
logger,
|
||||
workerPath,
|
||||
isQuitting: () => isQuittingRef.current,
|
||||
publishDomainChanged,
|
||||
emitEvent,
|
||||
wakeAgentIfPossible,
|
||||
dispatchRunToWorker,
|
||||
processRunCompletion,
|
||||
onRunStarted,
|
||||
onRunCompleted,
|
||||
notify,
|
||||
budgetService,
|
||||
sessionService,
|
||||
getRuntimeBridge: () => piRuntimeRef.current?.runtimeBridge ?? null,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* One-time settable slot for services that participate in a cycle during
|
||||
* assembly. `set()` may be called exactly once; `get()` throws until the
|
||||
* slot has been set. Eliminates the lazy-null `let` pattern that service-graph
|
||||
* previously used to wire `workerService.dispatchRunToWorker` against
|
||||
* `orchestrationService` before the latter exists.
|
||||
*/
|
||||
export class LateBinding<T> {
|
||||
private value: T | null = null;
|
||||
private assigned = false;
|
||||
|
||||
constructor(private readonly label: string) {}
|
||||
|
||||
set(value: T): void {
|
||||
if (this.assigned) {
|
||||
throw new Error(`LateBinding<${this.label}> is already assigned.`);
|
||||
}
|
||||
this.value = value;
|
||||
this.assigned = true;
|
||||
}
|
||||
|
||||
get(): T {
|
||||
if (!this.assigned || this.value === null) {
|
||||
throw new Error(`LateBinding<${this.label}> accessed before assignment.`);
|
||||
}
|
||||
return this.value;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.value = null;
|
||||
this.assigned = false;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user