Files
AgentCompany/src/main/index.ts
T

1936 lines
70 KiB
TypeScript

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<typeof utilityProcess.fork> | null = null;
let apiServer: AgentApiServer | null = null;
let heartbeatTimer: ReturnType<typeof setInterval> | 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<T>(data: T): DesktopResult<T> {
return { ok: true, data };
}
function fail(code: string, message: string): DesktopResult<never> {
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<unknown>> | DesktopResult<unknown>,
) {
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 || "<empty>"}`);
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<string>();
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<typeof workerMessageSchema>;
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<typeof db.getTask> | 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<typeof db.getAgent> | 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<string | null> {
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<typeof db.getConnector>[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<string>((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();
}