Files
AgentCompany/src/main/database/company-crud.ts
T
2026-04-04 12:03:50 -04:00

310 lines
13 KiB
TypeScript

import { randomUUID } from "node:crypto";
import type { DatabaseSync } from "node:sqlite";
import type {
InboxItem,
ProfileSnapshot,
} from "@shared/types";
import {
type ReadAll,
type Transaction,
nowIso,
} from "./helpers";
export function buildCompanyCrudMethods(
getDb: () => DatabaseSync,
readAll: ReadAll,
transaction: Transaction,
deps: {
getSetting: (key: string) => string | null;
setSetting: (key: string, value: string) => void;
listSnapshot: (options: { companyId?: string | null }) => ProfileSnapshot;
addComment: (input: { companyId: string; taskId: string; authorAgentId: string | null; authorName: string; body: string }) => void;
},
) {
return {
saveCompany(input: {
id?: string | null;
name: string;
description: string;
status: string;
autoApproveHires?: boolean;
reviewDeliverables?: boolean;
executionMode?: string;
}): string {
const id = input.id && input.id.length > 0 ? input.id : randomUUID();
const ts = nowIso();
const autoApprove = input.autoApproveHires ? 1 : 0;
const reviewDeliverables = input.reviewDeliverables ? 1 : 0;
const executionMode = input.executionMode ?? "ai_ceo";
const existing = getDb()
.prepare("select id from companies where id = ?")
.get(id) as { id: string } | undefined;
if (existing) {
getDb()
.prepare(
"update companies set name = ?, description = ?, status = ?, auto_approve_hires = ?, review_deliverables = ?, execution_mode = ?, updated_at = ? where id = ?",
)
.run(input.name, input.description, input.status, autoApprove, reviewDeliverables, executionMode, ts, id);
} else {
getDb()
.prepare(
"insert into companies (id, name, description, status, auto_approve_hires, review_deliverables, execution_mode, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?)",
)
.run(id, input.name, input.description, input.status, autoApprove, reviewDeliverables, executionMode, ts, ts);
if (!deps.getSetting("current_company_id")) {
deps.setSetting("current_company_id", id);
}
}
return id;
},
deleteCompany(companyId: string): void {
transaction(() => {
try {
getDb()
.prepare(
"delete from agent_message_reads where message_id in (select id from agent_messages where company_id = ?)",
)
.run(companyId);
} catch (err) {
if (!(err instanceof Error) || !err.message.includes("no such table")) throw err;
}
// New table cleanup (budget_incidents before budget_policies due to FK)
getDb().prepare("delete from budget_incidents where company_id = ?").run(companyId);
getDb().prepare("delete from budget_policies where company_id = ?").run(companyId);
// agent_skill_assignments references company_skills -- delete before skills
try {
getDb()
.prepare(
"delete from agent_skill_assignments where skill_id in (select id from company_skills where company_id = ?)",
)
.run(companyId);
} catch (err) {
if (!(err instanceof Error) || !err.message.includes("no such table")) throw err;
}
getDb().prepare("delete from company_skills where company_id = ?").run(companyId);
getDb().prepare("delete from task_sessions where company_id = ?").run(companyId);
getDb().prepare("delete from wakeup_requests where company_id = ?").run(companyId);
// Thread runtime tables (events + participants cascade from threads via FK)
getDb().prepare("delete from thread_wait_states where company_id = ?").run(companyId);
getDb().prepare("delete from conversation_threads where company_id = ?").run(companyId);
getDb().prepare("delete from cost_entries where company_id = ?").run(companyId);
getDb().prepare("delete from activity where company_id = ?").run(companyId);
getDb().prepare("delete from task_comments where company_id = ?").run(companyId);
getDb().prepare("delete from runs where company_id = ?").run(companyId);
getDb().prepare("delete from approvals where company_id = ?").run(companyId);
getDb().prepare("delete from tasks where company_id = ?").run(companyId);
getDb().prepare("delete from projects where company_id = ?").run(companyId);
getDb().prepare("delete from goals where company_id = ?").run(companyId);
getDb().prepare("delete from agents where company_id = ?").run(companyId);
getDb().prepare("delete from secrets where company_id = ?").run(companyId);
getDb().prepare("delete from workspaces where company_id = ?").run(companyId);
for (const table of [
"automation_rules",
"automation_log",
"agent_messages",
"workflow_pipelines",
"meetings",
"documents",
"knowledge_base",
"sprints",
"browser_actions",
"social_accounts",
]) {
try {
getDb().prepare(`delete from ${table} where company_id = ?`).run(companyId);
} catch (err) {
// Only tolerate "no such table" — re-throw anything else (disk error, etc.)
if (!(err instanceof Error) || !err.message.includes("no such table")) throw err;
}
}
getDb().prepare("delete from companies where id = ?").run(companyId);
if (deps.getSetting("current_company_id") === companyId) {
const next = getDb()
.prepare("select id from companies order by created_at asc limit 1")
.get() as { id: string } | undefined;
if (next) {
deps.setSetting("current_company_id", next.id);
} else {
deps.setSetting("current_company_id", "");
}
}
});
},
getCompanyStatus(companyId: string): string | null {
const row = getDb()
.prepare("select status from companies where id = ?")
.get(companyId) as { status: string } | undefined;
return row?.status ?? null;
},
getCompanyBasic(companyId: string): { name: string; description: string } | null {
const row = getDb()
.prepare("select name, description from companies where id = ?")
.get(companyId) as { name: string; description: string } | undefined;
return row ?? null;
},
listActiveCompanyIds(): Array<{ id: string; name: string }> {
return readAll<{ id: string; name: string }>(
"select id, name from companies where status = 'active'",
);
},
getCompanyExecutionMode(companyId: string): string {
const row = getDb()
.prepare("SELECT execution_mode FROM companies WHERE id = ? LIMIT 1")
.get(companyId) as { execution_mode: string } | undefined;
return row?.execution_mode ?? "ai_ceo";
},
getCompanyApprovalSettings(companyId: string): { autoApproveHires: boolean; reviewDeliverables: boolean } | null {
const row = getDb()
.prepare("SELECT auto_approve_hires, review_deliverables FROM companies WHERE id = ? LIMIT 1")
.get(companyId) as { auto_approve_hires: number; review_deliverables: number } | undefined;
if (!row) return null;
return {
autoApproveHires: row.auto_approve_hires === 1,
reviewDeliverables: row.review_deliverables === 1,
};
},
getInbox(companyId: string): InboxItem[] {
const items: InboxItem[] = [];
const snapshot = deps.listSnapshot({ companyId });
const agents = snapshot.agents;
const agentMap = new Map(agents.map((a) => [a.id, a]));
// Pending approvals
for (const approval of snapshot.approvals.filter(
(a) => a.companyId === companyId && a.state === "pending",
)) {
const requester = agentMap.get(approval.requestedByAgentId ?? "");
items.push({
id: `approval-${approval.id}`,
type: "pending_approval",
title: approval.payloadSummary,
subtitle: `${approval.type.replaceAll("_", " ")} — requested by ${requester?.name ?? "Unknown"}`,
severity: approval.type === "dangerous_command" ? "critical" : "high",
entityType: "approval",
entityId: approval.id,
agentId: approval.requestedByAgentId,
agentName: requester?.name ?? null,
createdAt: approval.createdAt,
});
}
// Failed runs (last 24h)
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
for (const run of snapshot.runs.filter(
(r) =>
r.companyId === companyId &&
(r.status === "failed" || r.status === "timed_out") &&
r.createdAt > oneDayAgo,
)) {
const agent = agentMap.get(run.agentId);
items.push({
id: `run-${run.id}`,
type: "failed_run",
title: run.summary || "Run failed",
subtitle: `${agent?.name ?? "Unknown agent"} — ${run.errorMessage?.slice(0, 100) ?? "Unknown error"}`,
severity: "high",
entityType: "run",
entityId: run.id,
agentId: run.agentId,
agentName: agent?.name ?? null,
createdAt: run.createdAt,
});
}
// Budget warnings (agents at >80% budget)
for (const agent of agents.filter(
(a) => a.budgetMonthlyUsd > 0 && a.spentMonthlyUsd / a.budgetMonthlyUsd > 0.8,
)) {
const pct = Math.round((agent.spentMonthlyUsd / agent.budgetMonthlyUsd) * 100);
items.push({
id: `budget-${agent.id}`,
type: "budget_warning",
title: `${agent.name} at ${pct}% budget`,
subtitle: `$${agent.spentMonthlyUsd.toFixed(2)} of $${agent.budgetMonthlyUsd.toFixed(2)} monthly limit`,
severity: pct >= 100 ? "critical" : "high",
entityType: "agent",
entityId: agent.id,
agentId: agent.id,
agentName: agent.name,
createdAt: agent.updatedAt,
});
}
// Blocked tasks
for (const task of snapshot.tasks.filter(
(t) => t.companyId === companyId && t.status === "blocked",
)) {
const assignee = agentMap.get(task.assigneeAgentId ?? "");
items.push({
id: `blocked-${task.id}`,
type: "blocked_task",
title: task.title,
subtitle: `Assigned to ${assignee?.name ?? "Unassigned"} — blocked`,
severity: task.priority === "critical" ? "critical" : "medium",
entityType: "task",
entityId: task.id,
agentId: task.assigneeAgentId,
agentName: assignee?.name ?? null,
createdAt: task.updatedAt,
});
}
// Tasks needing review
for (const task of snapshot.tasks.filter(
(t) => t.companyId === companyId && t.status === "in_review",
)) {
const assignee = agentMap.get(task.assigneeAgentId ?? "");
items.push({
id: `review-${task.id}`,
type: "review_needed",
title: task.title,
subtitle: `Submitted by ${assignee?.name ?? "Unknown"} — awaiting review`,
severity: task.priority === "critical" ? "high" : "medium",
entityType: "task",
entityId: task.id,
agentId: task.assigneeAgentId,
agentName: assignee?.name ?? null,
createdAt: task.updatedAt,
});
}
// Unread urgent agent messages (company-wide or incident channels that the Board should see)
for (const msg of snapshot.agentMessages.filter(
(m) =>
m.companyId === companyId &&
!m.readAt &&
(m.channel === "company" || m.channel === "incident" || m.priority === "urgent"),
)) {
const sender = agentMap.get(msg.fromAgentId);
items.push({
id: `message-${msg.id}`,
type: "agent_message",
title: msg.subject,
subtitle: `${sender?.name ?? "Unknown"} — ${msg.channel === "incident" ? "INCIDENT" : msg.channel} ${msg.priority === "urgent" ? "(urgent)" : ""}`,
severity: msg.priority === "urgent" || msg.channel === "incident" ? "critical" : "medium",
entityType: "message",
entityId: msg.id,
agentId: msg.fromAgentId,
agentName: sender?.name ?? null,
createdAt: msg.createdAt,
});
}
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
items.sort(
(a, b) =>
severityOrder[a.severity] - severityOrder[b.severity] ||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);
return items;
},
};
}