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.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-16 23:35:14 -04:00
1 parent 0f1b7c0b33
commit 2bd47a601a
1 file changed
+18 -35
+18 -35
View File
@@ -78,40 +78,23 @@ export function createServiceGraph({
let workerService: ReturnType<typeof createWorkerService> | null = null;
let updaterService: ReturnType<typeof createUpdaterService> | null = null;
let appLifecycleService: ReturnType<typeof createAppLifecycleService> | null = null;
// Eagerly constructed inside init() and resolved via getOrCreate* getters.
let skillService: ReturnType<typeof createSkillService> | null = null;
let budgetEnforcementService: ReturnType<typeof createBudgetEnforcementService> | null = null;
let sessionManagementService: ReturnType<typeof createSessionManagementService> | 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<typeof createSkillService> | 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) => {