import { spawn } from "node:child_process"; import { existsSync } from "node:fs"; import { pathToFileURL } from "node:url"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { app, BrowserWindow, dialog, type IpcMainInvokeEvent, ipcMain, Menu, nativeImage, Notification, protocol, net, session, shell, utilityProcess, } from "electron"; import electronUpdater from "electron-updater"; import log from "electron-log/main.js"; import { z } from "zod"; import { IPC_CHANNELS, approvalDecisionSchema, approvalInputSchema, agentInputSchema, cancelRunSchema, commentInputSchema, companyInputSchema, connectorInputSchema, deleteEntitySchema, goalInputSchema, heartbeatSettingsSchema, listCommentsSchema, type DesktopResult, projectInputSchema, runLogSchema, secretInputSchema, settingsSchema, startRunSchema, taskInputSchema, triggerHeartbeatSchema, workspaceInputSchema, } from "@shared/contracts"; import type { DesktopEvent } from "@shared/types"; import { getConnectorDefinition } from "./connectors"; import { AppDatabase } from "./database"; import { AgentApiServer } from "./api-server"; import { createAgentToken } from "./agent-auth"; import { buildTaskPrompt } from "./prompt-builder"; app.name = "AgentCompany"; // ── Fix PATH for packaged macOS/Linux apps ────────────────────────────────── // Packaged Electron apps on macOS launched from Finder inherit a minimal PATH // (/usr/bin:/bin:/usr/sbin:/sbin) that doesn't include Homebrew, nvm, cargo, etc. // We prepend common installation directories so connectors like claude, codex, // gemini, opencode, kimi can be found. { const extra = [ "/usr/local/bin", "/opt/homebrew/bin", "/opt/homebrew/sbin", `${process.env.HOME}/.local/bin`, `${process.env.HOME}/.cargo/bin`, `${process.env.HOME}/.nvm/current/bin`, `${process.env.HOME}/.volta/bin`, `${process.env.HOME}/.bun/bin`, "/usr/local/sbin", // Common Linux snap/flatpak paths "/snap/bin", ].filter((p) => p && !p.includes("undefined")); const current = process.env.PATH ?? ""; const existing = new Set(current.split(":").filter(Boolean)); const toAdd = extra.filter((p) => !existing.has(p)); if (toAdd.length > 0) { process.env.PATH = [...toAdd, current].join(":"); } } protocol.registerSchemesAsPrivileged([ { scheme: "app", privileges: { secure: true, standard: true, supportFetchAPI: true, stream: true, }, }, ]); const __dirname = dirname(fileURLToPath(import.meta.url)); const PRELOAD_PATH = join(__dirname, "../preload/index.js"); const RENDERER_DIST = join(__dirname, "../renderer"); const WORKER_PATH = join(__dirname, "worker.js"); const ICON_PATH = resolve(__dirname, "../../assets/icon.png"); const isDev = Boolean(process.env.ELECTRON_RENDERER_URL); const devServerOrigin = process.env.ELECTRON_RENDERER_URL ? new URL(process.env.ELECTRON_RENDERER_URL).origin : null; const { autoUpdater } = electronUpdater; let mainWindow: BrowserWindow | null = null; let db: AppDatabase; let worker: ReturnType | null = null; let apiServer: AgentApiServer | null = null; let heartbeatTimer: ReturnType | null = null; let isQuitting = false; let backgroundCloseNoticeShown = false; const companyIdSchema = z.string().uuid(); const runStatusSchema = z.enum(["queued", "running", "succeeded", "failed", "cancelled", "timed_out", "interrupted"]); const workerMessageSchema = z.union([ z.object({ type: z.literal("run-log"), runId: z.string().uuid(), stream: z.enum(["stdout", "stderr"]), chunk: z.string(), }), z.object({ type: z.literal("run-status"), runId: z.string().uuid(), status: runStatusSchema, message: z.string(), }), z.object({ type: z.literal("run-finished"), runId: z.string().uuid(), status: runStatusSchema, summary: z.string().nullable(), errorMessage: z.string().nullable(), exitCode: z.number().int().nullable(), signal: z.string().nullable(), model: z.string().nullable(), sessionParams: z.record(z.string(), z.unknown()).nullable(), sessionDisplayId: z.string().nullable(), costUsd: z.number().nullable(), unattributedCost: z.boolean(), }), ]); type CompanyScopedTable = "workspaces" | "agents" | "goals" | "projects" | "tasks" | "approvals" | "runs" | "secrets"; function ok(data: T): DesktopResult { return { ok: true, data }; } function fail(code: string, message: string): DesktopResult { return { ok: false, error: { code, message } }; } function isTrustedRendererUrl(url: string) { if (!url) { return false; } if (isDev) { return devServerOrigin ? url.startsWith(devServerOrigin) : false; } return url.startsWith("app://-"); } function registerHandle( channel: string, handler: (event: IpcMainInvokeEvent, ...args: unknown[]) => Promise> | DesktopResult, ) { ipcMain.handle(channel, async (event, ...args) => { const senderUrl = event.senderFrame?.url ?? event.sender.getURL(); if (!isTrustedRendererUrl(senderUrl)) { log.error(`[ipc:${channel}] blocked untrusted sender ${senderUrl || ""}`); return fail("IPC_FORBIDDEN", "IPC sender is not allowed."); } try { return await handler(event, ...args); } catch (error) { const message = error instanceof Error ? error.message : String(error); log.error(`[ipc:${channel}] ${message}`); return fail("IPC_HANDLER_FAILED", message); } }); } function emitEvent(event: DesktopEvent) { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send(IPC_CHANNELS.rendererEvent, event); } } function ensureCompanyExists(companyId: string) { return db.hasCompany(companyId) ? null : fail("COMPANY_NOT_FOUND", "The selected company no longer exists."); } function findInvalidReference( companyId: string, refs: Array<{ id: string | null | undefined; table: CompanyScopedTable; label: string }>, ) { return refs.find((ref) => ref.id && !db.belongsToCompany(ref.table, ref.id, companyId)) ?? null; } function publishDomainChanged() { emitEvent({ type: "domain-changed" }); } function notify(title: string, body: string) { if (Notification.isSupported()) { new Notification({ title, body }).show(); } } function buildMenu() { const macAppMenu: Electron.MenuItemConstructorOptions = { label: app.name, submenu: [ { role: "about" }, { type: "separator" }, { role: "services" }, { type: "separator" }, { role: "hide" }, { role: "hideOthers" }, { role: "unhide" }, { type: "separator" }, { role: "quit" }, ], }; const template: Electron.MenuItemConstructorOptions[] = [ ...(process.platform === "darwin" ? [macAppMenu] : []), { label: "File", submenu: [ { label: "Open Workspace Folder", accelerator: "CmdOrCtrl+O", click: async () => { const result = await dialog.showOpenDialog({ properties: ["openDirectory"], }); if (result.canceled || result.filePaths.length === 0) return; shell.openPath(result.filePaths[0] ?? ""); }, }, { type: "separator" }, { label: "Backup Profile", accelerator: "CmdOrCtrl+Shift+B", click: async () => { const target = await dialog.showOpenDialog({ properties: ["openDirectory", "createDirectory"], }); if (target.canceled || target.filePaths.length === 0) return; await db.exportBackup(join(target.filePaths[0]!, `agentcompany-backup-${Date.now()}`)); }, }, ...(process.platform === "darwin" ? [] : [{ type: "separator" as const }, { role: "quit" as const }]), ], }, { label: "Edit", submenu: [ { role: "undo" }, { role: "redo" }, { type: "separator" }, { role: "cut" }, { role: "copy" }, { role: "paste" }, { role: "selectAll" }, ], }, { label: "View", submenu: [ { label: "Command Palette", accelerator: "CmdOrCtrl+K", click: () => emitEvent({ type: "shortcut", action: "toggle-command-palette" }), }, { label: "Toggle Console", accelerator: "CmdOrCtrl+`", click: () => emitEvent({ type: "shortcut", action: "toggle-console" }), }, { label: "Refresh Data", accelerator: "CmdOrCtrl+R", click: () => emitEvent({ type: "shortcut", action: "refresh" }), }, { type: "separator" }, { role: "toggleDevTools", visible: isDev }, ], }, { label: "Run", submenu: [ { label: "Check For Updates", click: async () => { await checkForUpdates(); }, }, ], }, ]; Menu.setApplicationMenu(Menu.buildFromTemplate(template)); } function installSecurityGuards() { session.defaultSession.setPermissionCheckHandler(() => false); session.defaultSession.setPermissionRequestHandler((_webContents, _permission, callback) => { callback(false); }); app.on("web-contents-created", (_event, contents) => { contents.setWindowOpenHandler(() => ({ action: "deny" })); contents.on("will-attach-webview", (event) => { event.preventDefault(); }); contents.on("will-navigate", (event) => { event.preventDefault(); }); }); } function registerProtocol() { protocol.handle("app", (request) => { const url = new URL(request.url); const relativePath = decodeURIComponent(url.pathname === "/" ? "index.html" : url.pathname.replace(/^\/+/, "")); const targetPath = resolve(RENDERER_DIST, relativePath); if (!targetPath.startsWith(RENDERER_DIST) || !existsSync(targetPath)) { return new Response("Not found", { status: 404 }); } return net.fetch(pathToFileURL(targetPath).toString()); }); } async function createWindow() { const windowIcon = nativeImage.createFromPath(ICON_PATH); mainWindow = new BrowserWindow({ title: "AgentCompany", icon: windowIcon.isEmpty() ? undefined : windowIcon, width: 1480, height: 980, minWidth: 1180, minHeight: 760, backgroundColor: "#13110f", titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default", webPreferences: { preload: PRELOAD_PATH, contextIsolation: true, nodeIntegration: false, sandbox: true, devTools: isDev, webSecurity: true, allowRunningInsecureContent: false, }, }); if (isDev) { await mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL!); } else { await mainWindow.loadURL("app://-/index.html"); } mainWindow.on("close", (event) => { if (process.platform === "darwin" || isQuitting) { return; } event.preventDefault(); mainWindow?.hide(); if (!backgroundCloseNoticeShown) { backgroundCloseNoticeShown = true; notify("AgentCompany is still running", "Automation continues in the background until you quit the app."); } }); mainWindow.on("closed", () => { mainWindow = null; }); } function createWorker() { worker = utilityProcess.fork(WORKER_PATH, [], { stdio: "pipe", serviceName: "AgentCompany Orchestrator", }); worker.stdout?.on("data", (chunk) => log.info(`[worker] ${String(chunk).trim()}`)); worker.stderr?.on("data", (chunk) => log.error(`[worker] ${String(chunk).trim()}`)); // Step 7: Worker crash recovery worker.on("exit", (code) => { if (isQuitting) return; log.warn(`[worker] Worker exited with code ${code}, recovering...`); // Separate running runs (truly executing — mark interrupted) from queued runs // (never started — eligible for re-enqueue after worker restarts). const snapshot = db.listSnapshot(); const affectedAgentIds = new Set(); const requeueCandidates: Array<{ id: string; taskId: string; agentId: string; companyId: string; workspaceId: string | null; connectorId: string }> = []; for (const run of snapshot.runs) { if (run.status === "running") { // Running runs were actually executing — mark interrupted db.finishRun({ runId: run.id, status: "interrupted", summary: "Worker process crashed.", errorMessage: "Worker process exited unexpectedly.", exitCode: code, signal: null, model: null, sessionDisplayId: null, costUsd: null, unattributedCost: true, }); if (run.workspaceId) { db.releaseWorkspaceLock(run.workspaceId, run.id); } affectedAgentIds.add(run.agentId); const chain = db.getChainOfCommand(run.agentId); if (chain.length > 0) { wakeAgentIfPossible(chain[0].id, run.companyId, "report_failed"); } } else if (run.status === "queued") { // Queued runs never started — collect for re-enqueue requeueCandidates.push({ id: run.id, taskId: run.taskId, agentId: run.agentId, companyId: run.companyId, workspaceId: run.workspaceId, connectorId: run.connectorId, }); } } // Drain deferred wakes for affected agents for (const agentId of affectedAgentIds) { const pendingWakes = db.consumePendingWakes(agentId); if (pendingWakes.length > 0) { const wake = pendingWakes[0]; wakeAgentIfPossible(agentId, wake.companyId, wake.trigger); } } publishDomainChanged(); // Restart the worker, then re-enqueue queued runs that never started createWorker(); if (requeueCandidates.length > 0 && worker) { log.info(`[worker] Re-enqueuing ${requeueCandidates.length} queued run(s) after worker restart.`); const recoverySnapshot = db.listSnapshot(); for (const candidate of requeueCandidates) { try { const task = db.getTask(candidate.taskId); const agent = db.getAgent(candidate.agentId); const connector = db.getConnector(agent.connectorId as import("@shared/types").ConnectorId); const workspace = db.getWorkspace(candidate.workspaceId); const env = db.resolveSecretEnv(connector.envBindingText, candidate.companyId); const session = db.getTaskSession(task.id); // Build systemPrompt exactly like the normal heartbeat path const recoveryApiPort = apiServer?.getPort() ?? 0; const recoveryApiUrl = recoveryApiPort ? `http://127.0.0.1:${recoveryApiPort}` : ""; const recoveryApiKey = recoveryApiUrl ? createAgentToken(agent.id, candidate.companyId, candidate.id) : ""; const recoveryPrompt = recoveryApiUrl ? buildTaskPrompt({ agent, apiUrl: recoveryApiUrl, apiKey: recoveryApiKey, runId: candidate.id, task, companyName: recoverySnapshot.companies.find((c) => c.id === candidate.companyId)?.name ?? "Company", goals: recoverySnapshot.goals.filter((g) => g.companyId === candidate.companyId), projects: recoverySnapshot.projects.filter((p) => p.companyId === candidate.companyId), directReports: db.getDirectReports(agent.id, candidate.companyId), chainOfCommand: db.getChainOfCommand(agent.id), assignedTasks: db.listAgentTasks(agent.id, candidate.companyId, ["todo", "in_progress", "blocked"]), allAgents: recoverySnapshot.agents.filter((a) => a.companyId === candidate.companyId), pendingApprovals: recoverySnapshot.approvals.filter( (a) => a.companyId === candidate.companyId && a.requestedByAgentId === agent.id && (a.state === "pending" || a.state === "revision_requested"), ), tasksAwaitingReview: db.listReviewTasksForManager(agent.id, candidate.companyId), wakeReason: "recovery", }) : null; // Inject API credentials exactly like the normal path if (recoveryApiUrl) { env.AGENT_COMPANY_API_URL = recoveryApiUrl; env.AGENT_COMPANY_API_KEY = recoveryApiKey; env.AGENT_COMPANY_AGENT_ID = agent.id; env.AGENT_COMPANY_COMPANY_ID = candidate.companyId; env.AGENT_COMPANY_RUN_ID = candidate.id; env.AGENT_COMPANY_TASK_ID = task.id; env.AGENT_COMPANY_WAKE_REASON = "recovery"; } // Parse thinkingEffort exactly like the normal path let recoveryThinkingEffort: string | null = null; try { const meta = JSON.parse(agent.metadataJson || "{}"); if (typeof meta.thinkingEffort === "string" && meta.thinkingEffort) { recoveryThinkingEffort = meta.thinkingEffort; } } catch { /* ignore malformed json */ } worker.postMessage({ type: "enqueue-run", payload: { runId: candidate.id, companyId: candidate.companyId, taskId: task.id, taskTitle: task.title, taskDescription: task.description, agentId: agent.id, agentName: agent.name, connectorId: agent.connectorId, workspaceId: workspace?.id ?? null, workspacePath: workspace?.localPath ?? null, command: connector.command, model: connector.model ?? agent.model, thinkingEffort: recoveryThinkingEffort, env, sessionParams: session.sessionParams, systemPrompt: recoveryPrompt, trigger: "recovery", }, }); } catch (error) { // Re-enqueue failed — mark interrupted AND release workspace lock log.warn(`[worker] Failed to re-enqueue run ${candidate.id}: ${error instanceof Error ? error.message : String(error)}`); db.finishRun({ runId: candidate.id, status: "interrupted", summary: "Could not re-enqueue after worker restart.", errorMessage: error instanceof Error ? error.message : String(error), exitCode: null, signal: null, model: null, sessionDisplayId: null, costUsd: null, unattributedCost: true, }); if (candidate.workspaceId) { db.releaseWorkspaceLock(candidate.workspaceId, candidate.id); } } } } }); worker.on("message", async (message) => { let event: z.infer; try { event = workerMessageSchema.parse(message); } catch (error) { log.error("[worker] invalid message payload", error); return; } if (event.type === "run-log") { await db.appendRunLog(event.runId, event.stream, event.chunk); emitEvent(event); return; } if (event.type === "run-status") { db.markRunStatus(event.runId, event.status, event.message); emitEvent(event); publishDomainChanged(); return; } const snapshot = db.listSnapshot(); const run = snapshot.runs.find((entry) => entry.id === event.runId); if (run) { const connector = getConnectorDefinition(run.connectorId); // Guard: task/agent may have been deleted while the run was active let task: ReturnType | null = null; try { task = db.getTask(run.taskId); } catch { /* task deleted mid-run */ } if (task) { if (event.sessionParams && connector.module.sessionCodec) { const serialized = connector.module.sessionCodec.serialize(event.sessionParams); db.updateTaskSession(task.id, serialized, connector.module.sessionCodec.getDisplayId?.(serialized ?? null) ?? event.sessionDisplayId ?? null); } if (!event.sessionParams) { db.updateTaskSession(task.id, null, null); } } const runOutcome = db.finishRun(event); // Release workspace lock now that the run is finished if (run.workspaceId) { db.releaseWorkspaceLock(run.workspaceId, run.id); } db.addActivity({ companyId: run.companyId, actor: run.agentId, action: `run.${event.status}`, entityType: "run", entityId: run.id, detail: event.summary ?? event.errorMessage ?? "", }); publishDomainChanged(); emitEvent({ type: "run-status", runId: run.id, status: event.status, message: event.summary ?? event.errorMessage ?? event.status, }); if ((event.status === "failed" || event.status === "timed_out" || event.status === "interrupted") && task) { notify("Run attention required", task.title); } // Guard: agent may have been deleted while the run was active let updatedAgent: ReturnType | null = null; try { updatedAgent = db.getAgent(run.agentId); } catch { /* agent deleted mid-run */ } if (updatedAgent && runOutcome.budgetHardStopped) { autoPauseBudgetExceededAgent(updatedAgent); } // Step 6: Run failure escalation — wake agent's manager via org hierarchy if (event.status === "failed" || event.status === "timed_out") { try { const chain = db.getChainOfCommand(run.agentId); if (chain.length > 0) { wakeAgentIfPossible(chain[0].id, run.companyId, "report_failed"); } } catch { /* agent deleted, skip escalation */ } } // Step 3c: Consume deferred wakes — re-wake agent for each pending trigger const pendingWakes = db.consumePendingWakes(run.agentId); if (pendingWakes.length > 0) { // Process the first wake immediately; additional wakes will be // re-enqueued by wakeAgentIfPossible if the agent is still busy. for (const wake of pendingWakes) { wakeAgentIfPossible(run.agentId, wake.companyId, wake.trigger); } } } }); } function parseVersion(command: string): Promise { return new Promise((resolve) => { const child = spawn(command, ["--version"], { env: process.env, shell: false, stdio: ["ignore", "pipe", "pipe"], }); let buffer = ""; child.stdout.on("data", (chunk) => { buffer += String(chunk); }); child.stderr.on("data", (chunk) => { buffer += String(chunk); }); child.on("error", () => resolve(null)); child.on("close", () => { const versionLine = buffer.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? null; resolve(versionLine); }); }); } async function checkConnector(id: Parameters[0]) { const connector = db.getConnector(id); const definition = getConnectorDefinition(id); // Set "detected" (checking) state immediately so the UI shows progress db.updateConnectorHealth({ id, status: "detected", authState: connector.authState, version: connector.version, lastError: null, }); publishDomainChanged(); const result = await definition.module.testEnvironment({ companyId: db.getCurrentCompanyId() ?? randomFallbackCompanyId(), adapterType: id, config: { command: connector.command, model: connector.model, cwd: process.cwd(), // These flags allow the health probe to run non-interactively. // Without them, CLIs like Codex/Gemini enter interactive approval mode // and the probe hangs or fails. dangerouslyBypassApprovalsAndSandbox: true, // Codex dangerouslySkipPermissions: true, // Claude approvalMode: "yolo", // Gemini sandbox: false, // Gemini yolo: true, // Gemini fallback extraArgs: id === "codex_local" ? ["--skip-git-repo-check"] : [], }, }); const authState = result.checks.some((check) => check.code.includes("auth") || check.code.includes("login")) ? result.status === "fail" ? "required" : "ok" : result.status === "fail" ? "error" : "ok"; const version = await parseVersion(connector.command); // Map adapter status to connector state: pass/warn → ready, fail → error/auth_required // "warn" means the connector works but has non-fatal issues (e.g. missing API key that // might be set at runtime) — still usable, so map to "ready" not "degraded" db.updateConnectorHealth({ id, status: result.status === "pass" || result.status === "warn" ? "ready" : authState === "required" ? "auth_required" : "error", authState, version, lastError: result.status === "fail" ? result.checks.map((check) => check.message).join(" | ") : null, }); db.addActivity({ companyId: db.getCurrentCompanyId() ?? randomFallbackCompanyId(), actor: "system", action: "connector.tested", entityType: "connector", entityId: id, detail: result.status, }); publishDomainChanged(); } function randomFallbackCompanyId() { const snapshot = db.listSnapshot(); return snapshot.companies[0]?.id ?? "00000000-0000-0000-0000-000000000000"; } function autoPauseBudgetExceededAgent(agent: { id: string; companyId: string; name: string; spentMonthlyUsd: number; budgetMonthlyUsd: number }) { if (!(agent.budgetMonthlyUsd > 0) || agent.spentMonthlyUsd < agent.budgetMonthlyUsd) { return false; } db.addActivity({ companyId: agent.companyId, actor: "system", action: "agent.auto_paused", entityType: "agent", entityId: agent.id, detail: `Budget exhausted at ${agent.spentMonthlyUsd.toFixed(2)} / ${agent.budgetMonthlyUsd.toFixed(2)} USD.`, }); notify("Agent paused for budget limit", agent.name); publishDomainChanged(); return true; } function ensureAgentHeartbeatEnabled(agentId: string) { const agent = db.getAgent(agentId); if (!agent.heartbeatEnabled) { db.setHeartbeat(agentId, true, agent.heartbeatIntervalSec || 120); } } function wakeAgentIfPossible(agentId: string | null | undefined, companyId: string, trigger: string) { if (!agentId) return null; try { ensureAgentHeartbeatEnabled(agentId); } catch { return null; } // Check if agent is specifically busy (has active run) before attempting wake. // Only enqueue deferred wakes for the "busy" case, not for other skip reasons // (inactive company, paused agent, budget exhaustion, connector not ready). const snapshot = db.listSnapshot(); const hasActiveRun = snapshot.runs.some( (run) => run.companyId === companyId && run.agentId === agentId && (run.status === "queued" || run.status === "running"), ); const runId = startAgentHeartbeatRun(agentId, companyId, trigger); if (!runId && hasActiveRun) { // Agent is busy with an active run — defer the wake so it's not lost db.enqueuePendingWake(companyId, agentId, trigger); } return runId; } function getRunEligibilityFailure( agent: { status: string; budgetMonthlyUsd: number; spentMonthlyUsd: number; name: string; connectorId: string; id: string; companyId: string }, connector: { id: string; status: string }, ) { if (["terminated", "paused", "pending_approval"].includes(agent.status)) { return "AGENT_NOT_RUNNABLE"; } if (agent.budgetMonthlyUsd > 0 && agent.spentMonthlyUsd >= agent.budgetMonthlyUsd) { autoPauseBudgetExceededAgent(agent); log.info(`[run] Agent ${agent.name} budget exhausted, skipping`); return "AGENT_BUDGET_EXHAUSTED"; } if (connector.status !== "ready" && connector.status !== "degraded") { log.info(`[run] Agent ${agent.name} connector ${connector.id} not ready (${connector.status})`); return "CONNECTOR_NOT_READY"; } return null; } function startAgentHeartbeatRun(agentId: string, companyId: string, trigger: string): string | null { if (!worker) return null; const snapshot = db.listSnapshot(); const company = snapshot.companies.find((entry) => entry.id === companyId) ?? null; if (!company || company.status !== "active") { log.info(`[heartbeat] Skipping ${agentId} because company ${companyId} is not active.`); return null; } const agent = db.getAgent(agentId); const connector = db.getConnector(agent.connectorId); if (getRunEligibilityFailure(agent, connector)) { return null; } if ( snapshot.runs.some( (run) => run.companyId === companyId && run.agentId === agent.id && (run.status === "queued" || run.status === "running"), ) ) { log.info(`[heartbeat] Skipping ${agent.name} because it already has an active run.`); return null; } // Phase 3: Approval hard gate — don't start heartbeat runs for pending-approval agents if (db.isAgentPendingApproval(agentId)) { log.info(`[heartbeat] Skipping ${agent.name} because agent is pending hire approval.`); return null; } // Find an existing assigned task to run against const assignedTasks = db.listAgentTasks(agentId, companyId, ["todo", "in_progress", "blocked"]); // For timer-triggered heartbeats, only pick up real user-created tasks (not old heartbeat tasks) const isTimer = trigger === "timer"; const candidateTasks = isTimer ? assignedTasks.filter((entry) => !entry.title.startsWith("[Heartbeat]")) : assignedTasks; let task = candidateTasks.find((entry) => !entry.activeRunId) ?? null; if (!task) { const hasActiveAssignedRun = candidateTasks.some( (entry) => entry.activeRunId && snapshot.runs.some( (run) => run.id === entry.activeRunId && run.companyId === companyId && (run.status === "queued" || run.status === "running"), ), ); if (hasActiveAssignedRun) { return null; } // Timer should never create new heartbeat tasks — only explicit triggers can if (isTimer) { log.info(`[heartbeat] Skipping ${agent.name} — no actionable tasks and trigger is timer.`); return null; } // Create a heartbeat check task (inherit workspace from agent) const taskId = db.saveTask({ companyId, title: `[Heartbeat] ${agent.name} — Check for work`, description: `Automated heartbeat run for ${agent.name}. Review assigned tasks, check goals, and take action as needed.`, assigneeAgentId: agentId, workspaceId: agent.workspaceId ?? undefined, status: "in_progress", priority: "medium", }); task = db.getTask(taskId); } if ( task.activeRunId || snapshot.runs.some( (run) => run.taskId === task.id && run.companyId === companyId && (run.status === "queued" || run.status === "running"), ) ) { log.info(`[heartbeat] Skipping ${agent.name} because task ${task.title} already has an active run.`); return null; } const workspace = db.getWorkspace(task.workspaceId) ?? db.getWorkspace(agent.workspaceId) ?? db.getProjectPrimaryWorkspace(companyId) ?? db.getAnyCompanyWorkspace(companyId); log.info( `[heartbeat] Workspace resolution for ${agent.name}: task.workspaceId=${task.workspaceId ?? "null"}, agent.workspaceId=${agent.workspaceId ?? "null"}, resolved=${workspace?.name ?? "NONE"}, localPath=${workspace?.localPath ?? "NONE"}`, ); // Attempt workspace lock BEFORE creating the run — if another run holds it, defer if (workspace && !db.acquireWorkspaceLock(workspace.id, `pre-check-${agent.id}`)) { log.info(`[heartbeat] Skipping ${agent.name} because workspace ${workspace.name} is locked by another run.`); // Release the pre-check placeholder db.releaseWorkspaceLock(workspace.id, `pre-check-${agent.id}`); return null; } // Release pre-check — the real lock will be acquired with the actual run ID below if (workspace) { db.releaseWorkspaceLock(workspace.id, `pre-check-${agent.id}`); } const run = db.createRun({ companyId, taskId: task.id, agentId: agent.id, workspaceId: workspace?.id ?? null, connectorId: connector.id, }); // Acquire workspace lock with the real run ID if (workspace) { const locked = db.acquireWorkspaceLock(workspace.id, run.id); if (!locked) { // Race condition: someone else grabbed the lock between pre-check and here log.warn(`[heartbeat] Workspace lock race for ${workspace.name}, aborting run ${run.id}`); db.finishRun({ runId: run.id, status: "cancelled", summary: "Workspace lock contention.", errorMessage: null, exitCode: null, signal: null, model: null, sessionDisplayId: null, costUsd: null, unattributedCost: true }); return null; } } const env = db.resolveSecretEnv(connector.envBindingText, companyId); // Build rich prompt with API context const apiPort = apiServer?.getPort() ?? 0; const apiUrl = apiPort ? `http://127.0.0.1:${apiPort}` : ""; const apiKey = apiUrl ? createAgentToken(agent.id, companyId, run.id) : ""; const systemPrompt = apiUrl ? buildTaskPrompt({ agent, apiUrl, apiKey, runId: run.id, task, companyName: company.name, goals: snapshot.goals.filter((g) => g.companyId === companyId), projects: snapshot.projects.filter((p) => p.companyId === companyId), directReports: db.getDirectReports(agent.id, companyId), chainOfCommand: db.getChainOfCommand(agent.id), assignedTasks: assignedTasks, allAgents: snapshot.agents.filter((a) => a.companyId === companyId), pendingApprovals: snapshot.approvals.filter( (a) => a.companyId === companyId && a.requestedByAgentId === agent.id && (a.state === "pending" || a.state === "revision_requested"), ), tasksAwaitingReview: db.listReviewTasksForManager(agent.id, companyId), wakeReason: trigger, }) : null; // Add API credentials to env if (apiUrl) { env.AGENT_COMPANY_API_URL = apiUrl; env.AGENT_COMPANY_API_KEY = apiKey; env.AGENT_COMPANY_AGENT_ID = agent.id; env.AGENT_COMPANY_COMPANY_ID = companyId; env.AGENT_COMPANY_RUN_ID = run.id; env.AGENT_COMPANY_TASK_ID = task.id; env.AGENT_COMPANY_WAKE_REASON = trigger; } const session = db.getTaskSession(task.id); let thinkingEffort: string | null = null; try { const meta = JSON.parse(agent.metadataJson || "{}"); if (typeof meta.thinkingEffort === "string" && meta.thinkingEffort) { thinkingEffort = meta.thinkingEffort; } } catch { /* ignore malformed json */ } worker.postMessage({ type: "enqueue-run", payload: { runId: run.id, companyId, taskId: task.id, taskTitle: task.title, taskDescription: task.description, agentId: agent.id, agentName: agent.name, connectorId: connector.id, workspaceId: workspace?.id ?? null, workspacePath: workspace?.localPath ?? null, command: connector.command, model: connector.model ?? agent.model, thinkingEffort, env, sessionParams: session.sessionParams, systemPrompt, trigger, }, }); db.markHeartbeatAt(agentId); db.addActivity({ companyId, actor: "system", action: "heartbeat.triggered", entityType: "agent", entityId: agent.id, detail: `${trigger} heartbeat for ${agent.name}`, }); publishDomainChanged(); return run.id; } function startHeartbeatScheduler() { if (heartbeatTimer) clearInterval(heartbeatTimer); heartbeatTimer = setInterval(() => { try { const snapshot = db.listSnapshot(); const now = Date.now(); for (const company of snapshot.companies.filter((entry) => entry.status === "active")) { try { const eligibleAgents = db.listHeartbeatEligibleAgents(company.id); for (const agent of eligibleAgents) { const hasActiveRun = snapshot.runs.some( (r) => r.agentId === agent.id && (r.status === "running" || r.status === "queued"), ); if (hasActiveRun) continue; const lastHeartbeat = agent.lastHeartbeatAt ? new Date(agent.lastHeartbeatAt).getTime() : 0; const intervalMs = agent.heartbeatIntervalSec * 1000; if (now - lastHeartbeat < intervalMs) continue; log.info(`[heartbeat] Waking agent ${agent.name} (interval: ${agent.heartbeatIntervalSec}s)`); startAgentHeartbeatRun(agent.id, company.id, "timer"); } } catch (companyError) { log.error(`[heartbeat] Error processing company ${company.id}: ${companyError instanceof Error ? companyError.message : String(companyError)}`); } } } catch (error) { log.error(`[heartbeat] Scheduler error: ${error instanceof Error ? error.message : String(error)}`); } }, 15_000); // Check every 15 seconds } function registerHandlers() { registerHandle(IPC_CHANNELS.loadSnapshot, async () => ok(db.listSnapshot())); registerHandle(IPC_CHANNELS.pickDirectory, async () => { const result = await dialog.showOpenDialog({ properties: ["openDirectory", "createDirectory"], }); return ok(result.canceled ? null : (result.filePaths[0] ?? null)); }); registerHandle(IPC_CHANNELS.openPath, async (_event, targetPath) => { if (typeof targetPath !== "string" || targetPath.length === 0) { return fail("INVALID_PATH", "A valid path is required."); } if (!db.canOpenPath(targetPath)) { return fail("OPEN_PATH_FORBIDDEN", "The requested path is outside the managed desktop workspace."); } const error = await shell.openPath(targetPath); return error ? fail("OPEN_PATH_FAILED", error) : ok(true); }); registerHandle(IPC_CHANNELS.saveCompany, async (_event, payload) => { const parsed = companyInputSchema.parse(payload); const id = db.saveCompany(parsed); db.addActivity({ companyId: id, actor: "board", action: parsed.id ? "company.updated" : "company.created", entityType: "company", entityId: id, detail: parsed.name, }); publishDomainChanged(); return ok(id); }); registerHandle(IPC_CHANNELS.setCurrentCompany, async (_event, companyId) => { let parsedCompanyId: string; try { parsedCompanyId = companyIdSchema.parse(companyId); } catch { return fail("INVALID_COMPANY_ID", "A valid company id is required."); } if (!db.hasCompany(parsedCompanyId)) { return fail("COMPANY_NOT_FOUND", "The selected company does not exist."); } db.setCurrentCompany(parsedCompanyId); publishDomainChanged(); return ok(true); }); registerHandle(IPC_CHANNELS.saveConnector, async (_event, payload) => { const parsed = connectorInputSchema.parse(payload); const definition = getConnectorDefinition(parsed.id); if (parsed.command !== definition.defaultCommand) { return fail("CONNECTOR_COMMAND_LOCKED", `Connector command is managed by AgentCompany and must remain "${definition.defaultCommand}".`); } db.saveConnector({ ...parsed, command: definition.defaultCommand, model: parsed.model ?? null, }); db.addActivity({ companyId: db.getCurrentCompanyId() ?? randomFallbackCompanyId(), actor: "board", action: "connector.updated", entityType: "connector", entityId: parsed.id, detail: definition.defaultCommand, }); publishDomainChanged(); return ok(true); }); registerHandle(IPC_CHANNELS.testConnector, async (_event, id) => { const parsed = connectorInputSchema.shape.id.parse(id); await checkConnector(parsed); return ok(true); }); registerHandle(IPC_CHANNELS.saveWorkspace, async (_event, payload) => { const parsed = workspaceInputSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; const invalidReference = findInvalidReference(parsed.companyId, [ { id: parsed.projectId, table: "projects", label: "project" }, ]); if (invalidReference) { return fail("INVALID_COMPANY_REFERENCE", `The selected ${invalidReference.label} does not belong to the current company.`); } const id = db.saveWorkspace(parsed); db.addActivity({ companyId: parsed.companyId, actor: "board", action: parsed.id ? "workspace.updated" : "workspace.created", entityType: "workspace", entityId: id, detail: parsed.localPath, }); publishDomainChanged(); return ok(id); }); registerHandle(IPC_CHANNELS.saveAgent, async (_event, payload) => { const parsed = agentInputSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; const invalidReference = findInvalidReference(parsed.companyId, [ { id: parsed.reportsTo, table: "agents", label: "manager" }, { id: parsed.workspaceId, table: "workspaces", label: "workspace" }, ]); if (invalidReference) { return fail("INVALID_COMPANY_REFERENCE", `The selected ${invalidReference.label} does not belong to the current company.`); } let id: string; try { id = db.saveAgent({ id: parsed.id ?? null, companyId: parsed.companyId, name: parsed.name, role: parsed.role, title: parsed.title, status: parsed.status, reportsTo: parsed.reportsTo ?? null, connectorId: parsed.connectorId, workspaceId: parsed.workspaceId ?? null, model: parsed.model ?? null, metadataJson: parsed.metadataJson, capabilities: parsed.capabilities, budgetMonthlyUsd: parsed.budgetMonthlyUsd, }); } catch (error) { return fail("INVALID_AGENT_HIERARCHY", error instanceof Error ? error.message : "Agent could not be saved."); } db.addActivity({ companyId: parsed.companyId, actor: "board", action: parsed.id ? "agent.updated" : "agent.created", entityType: "agent", entityId: id, detail: parsed.name, }); publishDomainChanged(); return ok(id); }); registerHandle(IPC_CHANNELS.saveGoal, async (_event, payload) => { const parsed = goalInputSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; const invalidReference = findInvalidReference(parsed.companyId, [ { id: parsed.parentId, table: "goals", label: "parent goal" }, { id: parsed.ownerAgentId, table: "agents", label: "owner agent" }, ]); if (invalidReference) { return fail("INVALID_COMPANY_REFERENCE", `The selected ${invalidReference.label} does not belong to the current company.`); } const id = db.saveGoal(parsed); db.addActivity({ companyId: parsed.companyId, actor: "board", action: parsed.id ? "goal.updated" : "goal.created", entityType: "goal", entityId: id, detail: parsed.title, }); publishDomainChanged(); // Auto-cascade: when a goal becomes active with an owner agent, enable heartbeat and wake immediately if (parsed.status === "active" && parsed.ownerAgentId) { wakeAgentIfPossible(parsed.ownerAgentId, parsed.companyId, "goal_activated"); } return ok(id); }); registerHandle(IPC_CHANNELS.saveProject, async (_event, payload) => { const parsed = projectInputSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; const invalidReference = findInvalidReference(parsed.companyId, [ { id: parsed.goalId, table: "goals", label: "goal" }, { id: parsed.leadAgentId, table: "agents", label: "lead agent" }, ]); if (invalidReference) { return fail("INVALID_COMPANY_REFERENCE", `The selected ${invalidReference.label} does not belong to the current company.`); } const id = db.saveProject(parsed); db.addActivity({ companyId: parsed.companyId, actor: "board", action: parsed.id ? "project.updated" : "project.created", entityType: "project", entityId: id, detail: parsed.name, }); if (parsed.leadAgentId) { wakeAgentIfPossible(parsed.leadAgentId, parsed.companyId, "assignment"); } publishDomainChanged(); return ok(id); }); registerHandle(IPC_CHANNELS.saveTask, async (_event, payload) => { const parsed = taskInputSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; const invalidReference = findInvalidReference(parsed.companyId, [ { id: parsed.projectId, table: "projects", label: "project" }, { id: parsed.goalId, table: "goals", label: "goal" }, { id: parsed.parentId, table: "tasks", label: "parent task" }, { id: parsed.assigneeAgentId, table: "agents", label: "assignee agent" }, { id: parsed.workspaceId, table: "workspaces", label: "workspace" }, ]); if (invalidReference) { return fail("INVALID_COMPANY_REFERENCE", `The selected ${invalidReference.label} does not belong to the current company.`); } let id: string; try { id = db.saveTask(parsed); } catch (error) { return fail("INVALID_TASK_STATE", error instanceof Error ? error.message : "Task could not be saved."); } db.addActivity({ companyId: parsed.companyId, actor: "board", action: parsed.id ? "task.updated" : "task.created", entityType: "task", entityId: id, detail: parsed.title, }); if (parsed.assigneeAgentId) { wakeAgentIfPossible(parsed.assigneeAgentId, parsed.companyId, "assignment"); } publishDomainChanged(); return ok(id); }); registerHandle(IPC_CHANNELS.requestApproval, async (_event, payload) => { const parsed = approvalInputSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; const invalidReference = findInvalidReference(parsed.companyId, [ { id: parsed.relatedTaskId, table: "tasks", label: "task" }, { id: parsed.requestedByAgentId, table: "agents", label: "requesting agent" }, ]); if (invalidReference) { return fail("INVALID_COMPANY_REFERENCE", `The selected ${invalidReference.label} does not belong to the current company.`); } const id = db.requestApproval(parsed); db.addActivity({ companyId: parsed.companyId, actor: parsed.requestedByAgentId ?? "board", action: "approval.created", entityType: "approval", entityId: id, detail: parsed.payloadSummary, }); notify("Approval requested", parsed.payloadSummary); publishDomainChanged(); return ok(id); }); registerHandle(IPC_CHANNELS.decideApproval, async (_event, payload) => { const parsed = approvalDecisionSchema.parse(payload); db.decideApproval(parsed); const snapshot = db.listSnapshot(); const approval = snapshot.approvals.find((entry) => entry.id === parsed.approvalId); if (approval) { db.addActivity({ companyId: approval.companyId, actor: "board", action: `approval.${parsed.state}`, entityType: "approval", entityId: approval.id, detail: parsed.decisionNote, }); if (parsed.state === "approved" || parsed.state === "revision_requested" || parsed.state === "rejected") { wakeAgentIfPossible(approval.requestedByAgentId, approval.companyId, "approval_resolved"); } // Step 4: Wake newly hired agent when hire is approved if (approval.type === "hire_agent" && parsed.state === "approved") { try { const fullApproval = db.getApprovalRecord(parsed.approvalId); const payload = JSON.parse(fullApproval.payloadJson || "{}") as { agentId?: string }; if (payload.agentId) { // Don't wake new hires if org-strategy is still pending approval const hasPendingOrgApproval = snapshot.approvals.some( (a) => a.companyId === approval.companyId && a.type === "approve_ceo_strategy" && (a.state === "pending" || a.state === "revision_requested"), ); if (!hasPendingOrgApproval) { wakeAgentIfPossible(payload.agentId, approval.companyId, "assignment"); } } } catch { /* ignore if approval payload is malformed */ } } } publishDomainChanged(); return ok(true); }); registerHandle(IPC_CHANNELS.saveSecret, async (_event, payload) => { const parsed = secretInputSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; const id = db.saveSecret(parsed); db.addActivity({ companyId: parsed.companyId, actor: "board", action: parsed.id ? "secret.updated" : "secret.created", entityType: "secret", entityId: id, detail: parsed.name, }); publishDomainChanged(); return ok(id); }); registerHandle(IPC_CHANNELS.startTaskRun, async (_event, payload) => { if (!worker) return fail("WORKER_UNAVAILABLE", "Orchestration worker is not ready."); const parsed = startRunSchema.parse(payload); const task = db.getTask(parsed.taskId); const snapshot = db.listSnapshot(); const company = snapshot.companies.find((entry) => entry.id === task.companyId) ?? null; if (!company || company.status !== "active") { return fail("COMPANY_NOT_ACTIVE", "Runs can only start while the company is active."); } if ( task.activeRunId || snapshot.runs.some( (run) => run.taskId === task.id && run.companyId === task.companyId && (run.status === "queued" || run.status === "running"), ) ) { return fail("TASK_ALREADY_RUNNING", "This task already has an active run."); } if (!task.assigneeAgentId) return fail("TASK_UNASSIGNED", "Assign an agent before starting a run."); const agent = db.getAgent(task.assigneeAgentId); if (agent.companyId !== task.companyId) { return fail("AGENT_COMPANY_MISMATCH", "The assigned agent does not belong to the task company."); } const connector = db.getConnector(agent.connectorId); const eligibilityFailure = getRunEligibilityFailure(agent, connector); if (eligibilityFailure === "AGENT_NOT_RUNNABLE") { return fail("AGENT_NOT_RUNNABLE", "The assigned agent is paused, pending approval, or terminated."); } if (eligibilityFailure === "AGENT_BUDGET_EXHAUSTED") { return fail("AGENT_BUDGET_EXHAUSTED", "The assigned agent has exhausted its monthly budget."); } if (eligibilityFailure === "CONNECTOR_NOT_READY") { return fail("CONNECTOR_NOT_READY", "The assigned agent connector is not ready."); } if ( snapshot.runs.some( (run) => run.companyId === task.companyId && run.agentId === agent.id && (run.status === "queued" || run.status === "running"), ) ) { return fail("AGENT_ALREADY_RUNNING", "The assigned agent already has an active run."); } // Phase 3: Approval hard gate — block runs when relevant approvals are pending if (db.isAgentPendingApproval(agent.id)) { return fail("AGENT_PENDING_APPROVAL", "This agent is pending hire approval and cannot run tasks."); } const blockingApproval = db.getBlockingApproval(task.companyId, agent.id, task.id); if (blockingApproval) { return fail( "APPROVAL_GATE_BLOCKED", `Run blocked by pending approval: ${blockingApproval.type} — "${blockingApproval.payloadSummary}". Approve or reject before starting.`, ); } const workspace = db.getWorkspace(task.workspaceId) ?? db.getWorkspace(agent.workspaceId) ?? db.getProjectPrimaryWorkspace(task.companyId) ?? db.getAnyCompanyWorkspace(task.companyId); log.info( `[run] Workspace resolution for ${agent.name}: task.workspaceId=${task.workspaceId ?? "null"}, agent.workspaceId=${agent.workspaceId ?? "null"}, resolved=${workspace?.name ?? "NONE"}, localPath=${workspace?.localPath ?? "NONE"}`, ); if (workspace && workspace.companyId !== task.companyId) { return fail("WORKSPACE_COMPANY_MISMATCH", "The selected workspace does not belong to the task company."); } // Check workspace lock BEFORE creating the run — fail if another run holds it if (workspace && !db.acquireWorkspaceLock(workspace.id, `pre-check-manual-${agent.id}`)) { return fail("WORKSPACE_LOCKED", `Workspace "${workspace.name}" is locked by another active run. Wait for it to finish or cancel it.`); } if (workspace) { db.releaseWorkspaceLock(workspace.id, `pre-check-manual-${agent.id}`); } const run = db.createRun({ companyId: task.companyId, taskId: task.id, agentId: agent.id, workspaceId: workspace?.id ?? null, connectorId: connector.id, }); // Acquire workspace lock with real run ID — if race condition, cancel the run if (workspace) { const locked = db.acquireWorkspaceLock(workspace.id, run.id); if (!locked) { db.finishRun({ runId: run.id, status: "cancelled", summary: "Workspace lock contention.", errorMessage: null, exitCode: null, signal: null, model: null, sessionDisplayId: null, costUsd: null, unattributedCost: true }); return fail("WORKSPACE_LOCKED", `Workspace "${workspace.name}" was locked by another run during startup.`); } } const env = db.resolveSecretEnv(connector.envBindingText, task.companyId); // Build rich prompt with API context const apiPort = apiServer?.getPort() ?? 0; const apiUrl = apiPort ? `http://127.0.0.1:${apiPort}` : ""; const apiKey = apiUrl ? createAgentToken(agent.id, task.companyId, run.id) : ""; const systemPrompt = apiUrl ? buildTaskPrompt({ agent, apiUrl, apiKey, runId: run.id, task, companyName: snapshot.companies.find((c) => c.id === task.companyId)?.name ?? "Company", goals: snapshot.goals.filter((g) => g.companyId === task.companyId), projects: snapshot.projects.filter((p) => p.companyId === task.companyId), directReports: db.getDirectReports(agent.id, task.companyId), chainOfCommand: db.getChainOfCommand(agent.id), assignedTasks: db.listAgentTasks(agent.id, task.companyId, ["todo", "in_progress", "blocked"]), allAgents: snapshot.agents.filter((a) => a.companyId === task.companyId), pendingApprovals: snapshot.approvals.filter( (a) => a.companyId === task.companyId && a.requestedByAgentId === agent.id && (a.state === "pending" || a.state === "revision_requested"), ), tasksAwaitingReview: db.listReviewTasksForManager(agent.id, task.companyId), wakeReason: "manual", }) : null; // Add API credentials to env if (apiUrl) { env.AGENT_COMPANY_API_URL = apiUrl; env.AGENT_COMPANY_API_KEY = apiKey; env.AGENT_COMPANY_AGENT_ID = agent.id; env.AGENT_COMPANY_COMPANY_ID = task.companyId; env.AGENT_COMPANY_RUN_ID = run.id; env.AGENT_COMPANY_TASK_ID = task.id; env.AGENT_COMPANY_WAKE_REASON = "manual"; } const session = db.getTaskSession(task.id); let thinkingEffort: string | null = null; try { const meta = JSON.parse(agent.metadataJson || "{}"); if (typeof meta.thinkingEffort === "string" && meta.thinkingEffort) { thinkingEffort = meta.thinkingEffort; } } catch { /* ignore malformed json */ } worker.postMessage({ type: "enqueue-run", payload: { runId: run.id, companyId: task.companyId, taskId: task.id, taskTitle: task.title, taskDescription: task.description, agentId: agent.id, agentName: agent.name, connectorId: connector.id, workspaceId: workspace?.id ?? null, workspacePath: workspace?.localPath ?? null, command: connector.command, model: connector.model ?? agent.model, thinkingEffort, env, sessionParams: session.sessionParams, systemPrompt, trigger: "manual", }, }); db.addActivity({ companyId: task.companyId, actor: "board", action: "run.queued", entityType: "run", entityId: run.id, detail: task.title, }); publishDomainChanged(); return ok(run.id); }); registerHandle(IPC_CHANNELS.cancelRun, async (_event, payload) => { const parsed = cancelRunSchema.parse(payload); worker?.postMessage({ type: "cancel-run", runId: parsed.runId }); return ok(true); }); registerHandle(IPC_CHANNELS.getRunLog, async (_event, payload) => { const parsed = runLogSchema.parse(payload); return ok(db.readRunLog(parsed.runId)); }); registerHandle(IPC_CHANNELS.backupProfile, async () => { const result = await dialog.showOpenDialog({ properties: ["openDirectory", "createDirectory"], }); if (result.canceled || result.filePaths.length === 0) return ok(null); const backupPath = join(result.filePaths[0]!, `agentcompany-backup-${Date.now()}`); await db.exportBackup(backupPath); return ok(backupPath); }); registerHandle(IPC_CHANNELS.restoreProfile, async () => { const result = await dialog.showOpenDialog({ properties: ["openDirectory"], }); if (result.canceled || result.filePaths.length === 0) return ok(false); await db.restoreFromBackup(result.filePaths[0]!); publishDomainChanged(); return ok(true); }); registerHandle(IPC_CHANNELS.updateSettings, async (_event, payload) => { const parsed = settingsSchema.parse(payload); db.updateSettings(parsed); publishDomainChanged(); return ok(true); }); registerHandle(IPC_CHANNELS.checkForUpdates, async () => { await checkForUpdates(); return ok(true); }); registerHandle(IPC_CHANNELS.deleteCompany, async (_event, companyId) => { if (typeof companyId !== "string" || companyId.length === 0) { return fail("INVALID_INPUT", "Company ID is required."); } if (!db.hasCompany(companyId)) { return fail("COMPANY_NOT_FOUND", "Company not found."); } db.deleteCompany(companyId); publishDomainChanged(); return ok(true); }); registerHandle(IPC_CHANNELS.deleteAgent, async (_event, payload) => { const parsed = deleteEntitySchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; if (!db.belongsToCompany("agents", parsed.id, parsed.companyId)) { return fail("AGENT_NOT_FOUND", "Agent not found in the current company."); } db.deleteAgent(parsed.id, parsed.companyId); db.addActivity({ companyId: parsed.companyId, actor: "board", action: "agent.deleted", entityType: "agent", entityId: parsed.id, detail: "" }); publishDomainChanged(); return ok(true); }); registerHandle(IPC_CHANNELS.deleteTask, async (_event, payload) => { const parsed = deleteEntitySchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; if (!db.belongsToCompany("tasks", parsed.id, parsed.companyId)) { return fail("TASK_NOT_FOUND", "Task not found in the current company."); } db.deleteTask(parsed.id, parsed.companyId); db.addActivity({ companyId: parsed.companyId, actor: "board", action: "task.deleted", entityType: "task", entityId: parsed.id, detail: "" }); publishDomainChanged(); return ok(true); }); registerHandle(IPC_CHANNELS.deleteGoal, async (_event, payload) => { const parsed = deleteEntitySchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; if (!db.belongsToCompany("goals", parsed.id, parsed.companyId)) { return fail("GOAL_NOT_FOUND", "Goal not found in the current company."); } db.deleteGoal(parsed.id, parsed.companyId); db.addActivity({ companyId: parsed.companyId, actor: "board", action: "goal.deleted", entityType: "goal", entityId: parsed.id, detail: "" }); publishDomainChanged(); return ok(true); }); registerHandle(IPC_CHANNELS.deleteProject, async (_event, payload) => { const parsed = deleteEntitySchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; if (!db.belongsToCompany("projects", parsed.id, parsed.companyId)) { return fail("PROJECT_NOT_FOUND", "Project not found in the current company."); } db.deleteProject(parsed.id, parsed.companyId); db.addActivity({ companyId: parsed.companyId, actor: "board", action: "project.deleted", entityType: "project", entityId: parsed.id, detail: "" }); publishDomainChanged(); return ok(true); }); registerHandle(IPC_CHANNELS.deleteWorkspace, async (_event, payload) => { const parsed = deleteEntitySchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; if (!db.belongsToCompany("workspaces", parsed.id, parsed.companyId)) { return fail("WORKSPACE_NOT_FOUND", "Workspace not found in the current company."); } db.deleteWorkspace(parsed.id, parsed.companyId); db.addActivity({ companyId: parsed.companyId, actor: "board", action: "workspace.deleted", entityType: "workspace", entityId: parsed.id, detail: "" }); publishDomainChanged(); return ok(true); }); registerHandle(IPC_CHANNELS.deleteApproval, async (_event, payload) => { const parsed = deleteEntitySchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; if (!db.belongsToCompany("approvals", parsed.id, parsed.companyId)) { return fail("APPROVAL_NOT_FOUND", "Approval not found in the current company."); } db.deleteApproval(parsed.id, parsed.companyId); db.addActivity({ companyId: parsed.companyId, actor: "board", action: "approval.deleted", entityType: "approval", entityId: parsed.id, detail: "" }); publishDomainChanged(); return ok(true); }); registerHandle(IPC_CHANNELS.deleteSecret, async (_event, payload) => { const parsed = deleteEntitySchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; if (!db.belongsToCompany("secrets", parsed.id, parsed.companyId)) { return fail("SECRET_NOT_FOUND", "Secret not found in the current company."); } db.deleteSecret(parsed.id, parsed.companyId); db.addActivity({ companyId: parsed.companyId, actor: "board", action: "secret.deleted", entityType: "secret", entityId: parsed.id, detail: "" }); publishDomainChanged(); return ok(true); }); registerHandle(IPC_CHANNELS.getSidebarBadges, async (_event, companyId) => { let parsedCompanyId: string; try { parsedCompanyId = companyIdSchema.parse(companyId); } catch { return fail("INVALID_COMPANY_ID", "A valid company id is required."); } return ok(db.getSidebarBadges(parsedCompanyId)); }); /* ─── Comments ────────────────────────────────────────────────────── */ registerHandle(IPC_CHANNELS.addComment, async (_event, payload) => { const parsed = commentInputSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; const comment = db.addComment({ companyId: parsed.companyId, taskId: parsed.taskId, authorAgentId: parsed.authorAgentId ?? null, authorName: parsed.authorName, body: parsed.body, }); db.addActivity({ companyId: parsed.companyId, actor: parsed.authorAgentId ?? "board", action: "comment.created", entityType: "task", entityId: parsed.taskId, detail: parsed.body.slice(0, 100), }); emitEvent({ type: "agent-message", taskId: parsed.taskId, comment }); publishDomainChanged(); // Step 5c: Wake task assignee when Board/UI posts a comment try { const task = db.getTask(parsed.taskId); if (task.assigneeAgentId) { wakeAgentIfPossible(task.assigneeAgentId, task.companyId, "comment"); } } catch { /* ignore if task not found */ } return ok(comment.id); }); registerHandle(IPC_CHANNELS.listComments, async (_event, payload) => { const parsed = listCommentsSchema.parse(payload); return ok(db.listComments(parsed.taskId)); }); /* ─── Heartbeat ──────────────────────────────────────────────────── */ registerHandle(IPC_CHANNELS.setHeartbeat, async (_event, payload) => { const parsed = heartbeatSettingsSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; if (!db.belongsToCompany("agents", parsed.agentId, parsed.companyId)) { return fail("AGENT_NOT_FOUND", "Agent not found in the current company."); } db.setHeartbeat(parsed.agentId, parsed.enabled, parsed.intervalSec); db.addActivity({ companyId: parsed.companyId, actor: "board", action: parsed.enabled ? "heartbeat.enabled" : "heartbeat.disabled", entityType: "agent", entityId: parsed.agentId, detail: `Interval: ${parsed.intervalSec}s`, }); emitEvent({ type: "heartbeat-status", agentId: parsed.agentId, enabled: parsed.enabled, nextWakeAt: null }); publishDomainChanged(); return ok(true); }); registerHandle(IPC_CHANNELS.triggerHeartbeat, async (_event, payload) => { const parsed = triggerHeartbeatSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; if (!db.belongsToCompany("agents", parsed.agentId, parsed.companyId)) { return fail("AGENT_NOT_FOUND", "Agent not found in the current company."); } const runId = startAgentHeartbeatRun(parsed.agentId, parsed.companyId, parsed.trigger); if (!runId) return fail("HEARTBEAT_SKIPPED", "Agent cannot be woken right now (check connector status, budget, or active runs)."); return ok(runId); }); registerHandle(IPC_CHANNELS.getApiPort, async () => { return ok(apiServer?.getPort() ?? 0); }); registerHandle(IPC_CHANNELS.listConnectorModels, async (_event, connectorId) => { try { const parsed = connectorInputSchema.shape.id.parse(connectorId); const definition = getConnectorDefinition(parsed); if (definition.listModels) { const models = await definition.listModels(); return ok(models); } return ok([]); } catch { return ok([]); } }); } async function checkForUpdates() { const snapshot = db.listSnapshot(); if (!snapshot.updatesEnabled || process.env.AGENTCOMPANY_DISABLE_UPDATES === "true") { return false; } try { autoUpdater.logger = log; autoUpdater.autoDownload = false; await autoUpdater.checkForUpdates(); return true; } catch (error) { log.error(error); return false; } } async function bootstrap() { await app.whenReady(); log.initialize(); // Resolve the user's full shell PATH for packaged apps. // The static PATH fix above covers common directories, but the login shell // may have additional paths from .zshrc, .bashrc, nvm, pyenv, etc. if (process.platform !== "win32") { try { const userShell = process.env.SHELL || "/bin/zsh"; const shellPath = await new Promise((resolve) => { const child = spawn(userShell, ["-ilc", "echo -n $PATH"], { env: { ...process.env }, stdio: ["ignore", "pipe", "ignore"], timeout: 5000, }); let output = ""; child.stdout?.on("data", (chunk: Buffer) => { output += chunk.toString(); }); child.on("close", () => resolve(output.trim())); child.on("error", () => resolve("")); }); if (shellPath && shellPath.length > 0) { const current = new Set((process.env.PATH ?? "").split(":").filter(Boolean)); const shellParts = shellPath.split(":").filter(Boolean); const toAdd = shellParts.filter((p) => !current.has(p)); if (toAdd.length > 0) { process.env.PATH = [process.env.PATH, ...toAdd].join(":"); log.info(`[path] Added ${toAdd.length} shell PATH entries.`); } } } catch { // Non-fatal — static PATH fix is already in place } } // Set app icon for dock/taskbar (applies even during dev) const appIcon = nativeImage.createFromPath(ICON_PATH); if (!appIcon.isEmpty()) { if (process.platform === "darwin" && app.dock) { app.dock.setIcon(appIcon); } } if (process.platform === "win32") { app.setAppUserModelId("com.agentcompany.desktop"); } installSecurityGuards(); registerProtocol(); db = new AppDatabase(process.env.AGENTCOMPANY_PROFILE_DIR ?? join(app.getPath("userData"), "profile")); db.init(); buildMenu(); createWorker(); registerHandlers(); // Start local API server for agent-to-system interaction apiServer = new AgentApiServer(db, publishDomainChanged, (agentId, companyId, trigger) => { wakeAgentIfPossible(agentId, companyId, trigger); }); await apiServer.start(); // Start heartbeat scheduler for continuous agent operation startHeartbeatScheduler(); await createWindow(); for (const connector of db.listSnapshot().connectors) { void checkConnector(connector.id); } app.on("activate", async () => { if (mainWindow && !mainWindow.isDestroyed()) { if (!mainWindow.isVisible()) { mainWindow.show(); } mainWindow.focus(); return; } if (BrowserWindow.getAllWindows().length === 0) { await createWindow(); } }); } const hasSingleInstanceLock = process.env.AGENTCOMPANY_DISABLE_SINGLE_INSTANCE === "true" ? true : app.requestSingleInstanceLock(); app.on("second-instance", () => { if (!hasSingleInstanceLock) { return; } if (!mainWindow) { return; } if (!mainWindow.isVisible()) { mainWindow.show(); } if (mainWindow.isMinimized()) { mainWindow.restore(); } mainWindow.focus(); }); app.on("window-all-closed", () => { // Keep the process alive so scheduled automation and background runs continue. }); app.on("before-quit", () => { isQuitting = true; if (heartbeatTimer) clearInterval(heartbeatTimer); heartbeatTimer = null; apiServer?.stop(); apiServer = null; worker?.kill(); worker = null; db?.close(); }); if (hasSingleInstanceLock) { void bootstrap(); } else { app.quit(); }