Files
AgentCompany/src/main/database/company-crud.ts
T
ZacharyZhang-NY 6abc1a823f refactor(inbox): derive from snapshot; delete getInbox IPC chain
Sibling refactor to the sidebar-badges cleanup. The inbox aggregates
five item categories (pending approvals, failed runs, budget warnings,
blocked/review tasks, unread company/incident/urgent messages) — every
row is already in `ProfileSnapshot`. The server-side `getInbox`
aggregation (180 lines of SQL + projection) was a redundant second
path.

New: `src/shared/inbox-items.ts::deriveInboxItems(snapshot, companyId)`
produces the same InboxItem[] shape with the same severity ordering
and 24h failed-run window. Pure function, no IPC, no state.

Deleted:
- database/company-crud.ts::getInbox (180-line aggregation)
- database-base.ts::getInbox pass-through
- ipc-core-read-handlers.ts: handler
- preload/index.ts: bridge binding
- shared/contracts/channels.ts: channel name
- shared/contracts/desktop-api/companies.ts: API signature
- state/slices/company.ts: `inboxItems` field, `setInboxItems` action
- useDataLoader.ts: both getInbox call sites (initial snapshot load
  and section-change effect)
- useCompanyController.ts: inbox reset on company switch

OperationsSections now computes `inboxItems` via `useMemo` off the
current snapshot + company id. The snapshot is already company-scoped
at the main/database/snapshot.ts layer when `currentCompanyId` is
non-null; the derivation re-filters defensively so the function works
correctly even for unscoped snapshots.

Tests: 603/603 green. No new tests added — the derivation is a pure
function whose shape is exercised end-to-end by the existing Inbox
component integration.
2026-04-18 21:42:58 -04:00

171 lines
7.4 KiB
TypeScript

import { randomUUID } from "node:crypto";
import type { DatabaseSync } from "node:sqlite";
import type { 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,
};
},
};
}