From 2bd47a601a28b3de456f915f40bbf4323ddad7f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 16 Apr 2026 23:35:14 -0400 Subject: [PATCH] refactor(service-graph): eagerly construct leaf services in init() Moves skill, budget-enforcement, and session-management service construction to the top of service-graph init(), so the three getOrCreate* getters are pure readers that throw if called before init. This also fixes a duplicate-allocation bug in the orchestration listRuntimeSkillEntries closure, which was building a second SkillService instance instead of reusing the shared one. The outer getOrCreate* API surface stays intact for main-runtime-support.ts callers. The teardown setTimeout(app.exit, 3000) is left in place because worker CLI child processes keep the event loop alive; replacing it needs a full Disposable chain refactor that conflicts with in-progress pi-runtime integration work. --- src/main/service-graph.ts | 53 +++++++++++++-------------------------- 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/src/main/service-graph.ts b/src/main/service-graph.ts index 9637aaf3..a9f249b1 100644 --- a/src/main/service-graph.ts +++ b/src/main/service-graph.ts @@ -78,40 +78,23 @@ export function createServiceGraph({ let workerService: ReturnType | null = null; let updaterService: ReturnType | null = null; let appLifecycleService: ReturnType | null = null; + // Eagerly constructed inside init() and resolved via getOrCreate* getters. 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, - }); - } + if (!skillService) throw new Error("Skill service is not initialized."); return skillService; } function getOrCreateBudgetEnforcementService() { - if (!budgetEnforcementService) { - budgetEnforcementService = createBudgetEnforcementService({ - db, - logger, - notify, - publishDomainChanged, - }); - } + if (!budgetEnforcementService) throw new Error("Budget enforcement service is not initialized."); return budgetEnforcementService; } function getOrCreateSessionManagementService() { - if (!sessionManagementService) { - sessionManagementService = createSessionManagementService({ - db, - logger, - }); - } + if (!sessionManagementService) throw new Error("Session management service is not initialized."); return sessionManagementService; } @@ -159,6 +142,12 @@ export function createServiceGraph({ function init() { nativeTheme.themeSource = db.listSnapshot().theme; + // Eagerly construct leaf services that other services depend on. Keeps + // the graph deterministic and lets getOrCreate* getters stay pure readers. + skillService = createSkillService({ db, profileDir: db.profileDir, logger }); + budgetEnforcementService = createBudgetEnforcementService({ db, logger, notify, publishDomainChanged }); + sessionManagementService = createSessionManagementService({ db, logger }); + desktopShellService = createDesktopShellService({ preloadPath, rendererDist, @@ -232,20 +221,14 @@ export function createServiceGraph({ 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 (error) { - logger.warn(`[service-graph] listRuntimeSkillEntries failed for agent ${agentId}:`, error); - return []; - } - }; - })(), + listRuntimeSkillEntries: (companyId: string, agentId: string, connectorId: string) => { + try { + return getOrCreateSkillService().listRuntimeSkillEntries(companyId, agentId, connectorId); + } catch (error) { + logger.warn(`[service-graph] listRuntimeSkillEntries failed for agent ${agentId}:`, error); + return []; + } + }, budgetService: getOrCreateBudgetEnforcementService(), checkpointEngine, getSessionHandoff: (taskId: string) => {