diff --git a/scripts/codemod/remove-appdatabase-passthrough.mjs b/scripts/codemod/remove-appdatabase-passthrough.mjs new file mode 100644 index 00000000..83d281a8 --- /dev/null +++ b/scripts/codemod/remove-appdatabase-passthrough.mjs @@ -0,0 +1,412 @@ +#!/usr/bin/env node +/** + * Codemod: rewrite `db.(` → `db..(` + * for all AppDatabase pass-through methods removed by the C1 refactor. + * + * Regex-based (no ts-morph dep). Operates on a fixed method→module map + * derived mechanically from src/main/database.ts and + * src/main/database-base.ts. Only rewrites `db.(...)` with a + * word-boundary before `db`; never touches `db..(...)`. + * + * Usage: + * node scripts/codemod/remove-appdatabase-passthrough.mjs \ + * --modules=tasks,agents \ + * [--dry-run] + * + * The --modules flag restricts rewrites to passthroughs owned by those + * sub-module fields; omitting it rewrites every passthrough in the map. + */ + +import { readFileSync, writeFileSync } from "node:fs"; +import { globSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { execSync } from "node:child_process"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = join(__dirname, "..", ".."); + +// Canonical map: passthrough method name → AppDatabaseBase field name +// Source: src/main/database.ts + src/main/database-base.ts as of C1 start. +const METHOD_TO_MODULE = { + // activityMod + listComments: "activityMod", + addActivity: "activityMod", + addComment: "activityMod", + listRecentTaskComments: "activityMod", + // messages + sendAgentMessage: "messages", + getAgentMessage: "messages", + listAgentMessages: "messages", + searchMessages: "messages", + markMessageReadForReader: "messages", + listAgentInboxUnread: "messages", + // social + saveSocialAccount: "social", + deleteSocialAccount: "social", + listSocialAccounts: "social", + getSocialAccount: "social", + saveBrowserAction: "social", + updateBrowserAction: "social", + listBrowserActions: "social", + getBrowserAction: "social", + getBrowserActionByApprovalId: "social", + // skills + upsertCompanySkill: "skills", + listCompanySkills: "skills", + getCompanySkill: "skills", + deleteCompanySkill: "skills", + assignSkillToAgent: "skills", + removeSkillFromAgent: "skills", + listAgentSkillAssignments: "skills", + listSkillAgentAssignments: "skills", + updateCompanySkillMetadata: "skills", + // budget + saveBudgetPolicy: "budget", + deleteBudgetPolicy: "budget", + listBudgetPolicies: "budget", + getBudgetPolicy: "budget", + getActivePoliciesForRun: "budget", + getCalendarMonthWindow: "budget", + getSpentInWindow: "budget", + upsertBudgetIncident: "budget", + listBudgetIncidents: "budget", + resolveBudgetIncident: "budget", + resolveIncidentsForPolicy: "budget", + resolveWarnIncidentsForPolicy: "budget", + autoCloseExpiredIncidents: "budget", + // entityCrud + saveGoal: "entityCrud", + listGoals: "entityCrud", + getGoalForUpdate: "entityCrud", + saveProject: "entityCrud", + listProjects: "entityCrud", + getProjectForUpdate: "entityCrud", + saveSecret: "entityCrud", + resolveSecretEnv: "entityCrud", + deleteGoal: "entityCrud", + deleteProject: "entityCrud", + deleteSecret: "entityCrud", + countActiveGoalsForCompany: "entityCrud", + createHireRequest: "entityCrud", + // documentsMod + saveDocument: "documentsMod", + listImportantDocsForReview: "documentsMod", + listDocuments: "documentsMod", + getDocumentForUpdate: "documentsMod", + updateDocumentStatus: "documentsMod", + deleteDocument: "documentsMod", + createDocumentRevision: "documentsMod", + getDocumentLatestRevisionId: "documentsMod", + listDocumentRevisions: "documentsMod", + createWorkProduct: "documentsMod", + updateWorkProduct: "documentsMod", + listWorkProductsForTask: "documentsMod", + listWorkProductsForCompany: "documentsMod", + findWorkProductByExternalId: "documentsMod", + resolveWorkProductTarget: "documentsMod", + deleteWorkProduct: "documentsMod", + archiveWorkProductsByExternalPaths: "documentsMod", + linkDocumentToTask: "documentsMod", + unlinkDocumentFromTask: "documentsMod", + listDocumentsForTask: "documentsMod", + listConfigRevisions: "documentsMod", + getConfigRevision: "documentsMod", + rollbackConfigRevision: "documentsMod", + recordConfigRevision: "documentsMod", + // knowledgeMod + saveMeeting: "knowledgeMod", + deleteMeeting: "knowledgeMod", + listMeetings: "knowledgeMod", + getMeeting: "knowledgeMod", + saveKnowledgeEntry: "knowledgeMod", + deleteKnowledgeEntry: "knowledgeMod", + listKnowledgeEntries: "knowledgeMod", + saveSprint: "knowledgeMod", + deleteSprint: "knowledgeMod", + listSprints: "knowledgeMod", + collectRunDeliverableSignals: "knowledgeMod", + // automationMod + saveAutomationRule: "automationMod", + deleteAutomationRule: "automationMod", + listAutomationRuleNames: "automationMod", + toggleAutomationRule: "automationMod", + recordAutomationExecution: "automationMod", + getAutomationLog: "automationMod", + listActiveAutomationRules: "automationMod", + saveWorkflow: "automationMod", + getWorkflowRecord: "automationMod", + updateWorkflowExecution: "automationMod", + deleteWorkflow: "automationMod", + listTriggerableWorkflows: "automationMod", + // wakeup + enqueueWakeupRequest: "wakeup", + promoteWakeupRequests: "wakeup", + listWakeupRequests: "wakeup", + getPreferredWakeTrigger: "wakeup", + completePendingWakeupRequests: "wakeup", + purgeExpiredWakeupRequests: "wakeup", + // backupMod + exportBackup: "backupMod", + createAutomaticBackup: "backupMod", + restoreFromBackup: "backupMod", + // threads + createThread: "threads", + listThreads: "threads", + getThread: "threads", + addThreadEvent: "threads", + listThreadEvents: "threads", + setThreadWaitState: "threads", + resolveThreadWaitState: "threads", + listThreadWaitStates: "threads", + hasUnresolvedWaitStateForAgent: "threads", + addParticipant: "threads", + listParticipants: "threads", + removeParticipant: "threads", + subscribe: "threads", + unsubscribe: "threads", + listSubscriptions: "threads", + listSubscribedThreads: "threads", + // settings + getCurrentCompanyId: "settings", + setCurrentCompany: "settings", + hasCompany: "settings", + belongsToCompany: "settings", + updateSettings: "settings", + getLogRetentionDays: "settings", + purgeExpiredRunLogs: "settings", + canOpenPath: "settings", + getDisabledModels: "settings", + setDisabledModels: "settings", + hasModelPoolSettings: "settings", + getLastBudgetResetMonth: "settings", + setLastBudgetResetMonth: "settings", + // snap + listSnapshot: "snap", + listRendererSnapshot: "snap", + // company + saveCompany: "company", + deleteCompany: "company", + getCompanyStatus: "company", + getCompanyBasic: "company", + getCompanyExecutionMode: "company", + getCompanyApprovalSettings: "company", + listActiveCompanyIds: "company", + getCompanyMetrics: "company", + getStandupReport: "company", + getDashboardSnapshot: "company", + // connectorsCrud + saveConnector: "connectorsCrud", + updateConnectorHealth: "connectorsCrud", + getConnector: "connectorsCrud", + findConnector: "connectorsCrud", + // agents (note: getAgent/findAgent live on entityCrud) + saveAgent: "agents", + deleteAgent: "agents", + getAgent: "entityCrud", + findAgent: "entityCrud", + setHeartbeat: "agents", + markHeartbeatAt: "agents", + resetMonthlyBudgets: "agents", + listHeartbeatEligibleAgents: "agents", + getChainOfCommand: "agents", + getDirectReports: "agents", + getCeoForCompany: "agents", + listActiveAgentsForCompany: "agents", + listAgentsForApi: "agents", + listDirectReportsForStandup: "agents", + listReportStatusesForStandup: "agents", + listExistingAgentsForHireCheck: "agents", + hasAgentAssignedTasks: "agents", + updateAgentStatus: "agents", + isAgentPendingApproval: "agents", + findMentionedAgentIds: "agents", + getIdleAgentsForDepartment: "agents", + getManagerForAgent: "agents", + getAgentPermissions: "agents", + setAgentPermission: "agents", + hasAgentPermission: "agents", + getRuntimeState: "agents", + setRuntimeState: "agents", + getHeartbeatPolicy: "agents", + setHeartbeatPolicy: "agents", + countActiveRunsForAgent: "agents", + listDepartmentAgentIds: "agents", + listCompanyBroadcastAgentIds: "agents", + listIncidentPingableAgentIds: "agents", + findAgentName: "agents", + listAgentNamesByIds: "agents", + listActiveAgentIdsFromIds: "agents", + // tasks (note: getTask lives on entityCrud) + saveTask: "tasks", + getTask: "entityCrud", + deleteTask: "tasks", + getSessionById: "tasks", + incrementSessionUsage: "tasks", + archiveSession: "tasks", + createNewSession: "tasks", + ensureAgentSession: "tasks", + setAgentSessionId: "tasks", + getRecentRunsForTask: "tasks", + getRecentActivityForTask: "tasks", + checkoutTask: "tasks", + releaseTaskLock: "tasks", + renewTaskLease: "tasks", + listAgentTasks: "tasks", + listTasksByStatuses: "tasks", + listAssigneeTasksForStandup: "tasks", + listTasksForAssignees: "tasks", + listReviewTasksForManager: "tasks", + countActiveTasksForCompany: "tasks", + countAdvancingTasksForCompany: "tasks", + listChildTasks: "tasks", + advanceTaskStatus: "tasks", + setTaskRequiresUserReview: "tasks", + hasPendingDeliverableReviews: "tasks", + hasAgentPendingDeliverableReview: "tasks", + hasAgentTaskAwaitingReview: "tasks", + autoAssignTask: "tasks", + getTaskRetryCount: "tasks", + incrementTaskRetryCount: "tasks", + resetTaskRetryCount: "tasks", + retryInterruptedTask: "tasks", + dismissInterruptedTask: "tasks", + markInterruptedTaskFailed: "tasks", + setTaskType: "tasks", + listProjectTaskAssigneeIds: "tasks", + listReviewTasksForSweep: "tasks", + getTaskType: "tasks", + // runs + getRun: "runs", + getRunAuthState: "runs", + createRun: "runs", + appendRunLog: "runs", + readRunLog: "runs", + readRunLogChunk: "runs", + markRunStatus: "runs", + finishRun: "runs", + recordCostEntry: "runs", + hasActiveRun: "runs", + listQueuedRuns: "runs", + getInterruptedRunsForRecovery: "runs", + appendRunEvent: "runs", + listRunEvents: "runs", + listRunEventsAfter: "runs", + updateRunActor: "runs", + // approvalsMod + requestApproval: "approvalsMod", + decideApproval: "approvalsMod", + getApprovalRecord: "approvalsMod", + findApprovalById: "approvalsMod", + deleteApproval: "approvalsMod", + getBlockingApproval: "approvalsMod", + resubmitApproval: "approvalsMod", + addApprovalComment: "approvalsMod", + listApprovalComments: "approvalsMod", + linkTaskToApproval: "approvalsMod", + unlinkTaskFromApproval: "approvalsMod", + listApprovalsForTask: "approvalsMod", + listTasksForApproval: "approvalsMod", + linkManyTasksToApproval: "approvalsMod", + listAgentPendingApprovals: "approvalsMod", + listAgentResolvedApprovals: "approvalsMod", + listPendingHireApprovalPayloads: "approvalsMod", + listApprovalsForApi: "approvalsMod", + findPendingApprovalForTask: "approvalsMod", + hasPendingReviewApprovalForAgent: "approvalsMod", + listCompanyApprovalsByType: "approvalsMod", + // workspacesMod + saveWorkspace: "workspacesMod", + getWorkspace: "workspacesMod", + getProjectPrimaryWorkspace: "workspacesMod", + listWorkspaceLocalPaths: "workspacesMod", + getAnyCompanyWorkspace: "workspacesMod", + deleteWorkspace: "workspacesMod", + acquireWorkspaceLock: "workspacesMod", + claimWorkspaceLock: "workspacesMod", + releaseWorkspaceLock: "workspacesMod", + releaseAllWorkspaceLocks: "workspacesMod", + releaseStaleWorkspaceLocks: "workspacesMod", + isWorkspaceLocked: "workspacesMod", +}; + +const args = process.argv.slice(2); +const dryRun = args.includes("--dry-run"); +const modulesArg = args.find((a) => a.startsWith("--modules=")); +const methodsArg = args.find((a) => a.startsWith("--methods=")); +const includedModules = modulesArg + ? new Set(modulesArg.split("=")[1].split(",")) + : null; +const extraMethods = methodsArg + ? new Set(methodsArg.split("=")[1].split(",")) + : new Set(); + +const targetMethods = Object.entries(METHOD_TO_MODULE) + .filter(([name, mod]) => { + if (!includedModules) return true; + if (includedModules.has(mod)) return true; + if (extraMethods.has(name)) return true; + return false; + }) + .map(([m]) => m); + +if (targetMethods.length === 0) { + console.error("No methods matched module filter."); + process.exit(2); +} + +// Collect files via git ls-files (fast, respects .gitignore) +const filesOut = execSync("git ls-files 'src/**/*.ts' 'src/**/*.tsx' 'tests/**/*.ts'", { + cwd: REPO_ROOT, + encoding: "utf8", +}); +const files = filesOut + .split("\n") + .filter(Boolean) + .filter((f) => !f.startsWith("src/main/database.ts")) + .filter((f) => !f.startsWith("src/main/database-base.ts")) + .filter((f) => !f.startsWith("src/main/database/")); + +// Build one giant alternation regex, non-capturing, then use a back-reference +// group only for the method name so we can look up the module. +const methodAlt = targetMethods + .sort((a, b) => b.length - a.length) // longest-first to avoid partial shadow + .join("|"); + +// Match `db.(`. The qualifier must end with +// a `.` so `adb.saveTask(` (same token) is not matched. Examples that +// DO match: `db.saveTask(`, `this.db.saveTask(`, `shared.db.saveTask(`, +// `ctx.db.saveTask(`. The pre-anchor `(^|[^\w.])` keeps us from +// splitting an identifier token containing `db` somewhere in the middle. +const pattern = new RegExp( + `(^|[^\\w.])((?:[\\w$]+\\.)*)db\\.(${methodAlt})\\(`, + "g", +); + +let totalReplacements = 0; +let changedFiles = 0; +const perFile = {}; + +for (const relPath of files) { + const abs = join(REPO_ROOT, relPath); + const src = readFileSync(abs, "utf8"); + let count = 0; + const out = src.replace(pattern, (match, pre, qualifier, method) => { + const mod = METHOD_TO_MODULE[method]; + if (!mod) return match; + count += 1; + return `${pre}${qualifier}db.${mod}.${method}(`; + }); + if (count > 0) { + perFile[relPath] = count; + totalReplacements += count; + changedFiles += 1; + if (!dryRun) writeFileSync(abs, out); + } +} + +const hdr = dryRun ? "DRY RUN" : "APPLIED"; +console.log(`${hdr}: ${totalReplacements} rewrites across ${changedFiles} files`); +for (const [f, n] of Object.entries(perFile).sort((a, b) => b[1] - a[1])) { + console.log(` ${n}\t${f}`); +} diff --git a/src/main/agent-permissions.ts b/src/main/agent-permissions.ts index affb059c..5c4c2226 100644 --- a/src/main/agent-permissions.ts +++ b/src/main/agent-permissions.ts @@ -6,7 +6,7 @@ function safeGetAgent(db: AppDatabase, agentId: string | null | undefined) { return null; } try { - return db.getAgent(agentId); + return db.entityCrud.getAgent(agentId); } catch (error) { if (error instanceof Error && error.message.startsWith("Agent not found:")) { return null; @@ -46,14 +46,14 @@ export function hasManagementAuthority(agent: { role: string; title: string; dep } function hasOrganizationPermission(db: AppDatabase, agentId: string) { - return db.hasAgentPermission(agentId, "organization_manage"); + return db.agents.hasAgentPermission(agentId, "organization_manage"); } function managesAgent(db: AppDatabase, managerId: string, targetAgentId: string): boolean { if (managerId === targetAgentId) { return true; } - return db.getChainOfCommand(targetAgentId).some((agent) => agent.id === managerId); + return db.agents.getChainOfCommand(targetAgentId).some((agent) => agent.id === managerId); } export function canControlAgent(db: AppDatabase, companyId: string, actorAgentId: string, targetAgentId: string): boolean { @@ -96,7 +96,7 @@ export function canRequestHire(db: AppDatabase, companyId: string, agentId: stri if (canManageOrganization(db, companyId, agentId)) { return true; } - if (db.getDirectReports(agentId, companyId).length > 0) { + if (db.agents.getDirectReports(agentId, companyId).length > 0) { return true; } return hasManagementAuthority(agent); @@ -128,9 +128,9 @@ function getDepartmentLeadCandidateId( .filter((candidate) => candidate.id !== excludeAgentId) .map((candidate) => ({ id: candidate.id, - hasReports: db.getDirectReports(candidate.id, companyId).length > 0, + hasReports: db.agents.getDirectReports(candidate.id, companyId).length > 0, leadershipScore: hasManagementAuthority(candidate) ? 1 : 0, - chainDepth: db.getChainOfCommand(candidate.id).length, + chainDepth: db.agents.getChainOfCommand(candidate.id).length, createdAt: candidate.createdAt, })) .filter((candidate) => candidate.hasReports || candidate.leadershipScore > 0) @@ -161,7 +161,7 @@ export function resolveHireReportsTo( } if (!department) { - return requester?.id ?? db.getCeoForCompany(companyId)?.id ?? null; + return requester?.id ?? db.agents.getCeoForCompany(companyId)?.id ?? null; } if (requester && (requester.department === department || !requester.reportsTo)) { @@ -177,7 +177,7 @@ export function resolveHireReportsTo( return requester.id; } - return db.getCeoForCompany(companyId)?.id ?? null; + return db.agents.getCeoForCompany(companyId)?.id ?? null; } function normalizeComparableText(value: string): string { diff --git a/src/main/agent-runtime/runtime-bridge.ts b/src/main/agent-runtime/runtime-bridge.ts index 88087b30..6f10dade 100644 --- a/src/main/agent-runtime/runtime-bridge.ts +++ b/src/main/agent-runtime/runtime-bridge.ts @@ -266,7 +266,7 @@ export class RuntimeBridge { */ private resolveAgentName(agentId: string): string { try { - const name = this.deps.db.findAgentName(agentId); + const name = this.deps.db.agents.findAgentName(agentId); if (name && name.length > 0) return name; } catch (error) { this.deps.logger.warn( @@ -384,7 +384,7 @@ export class RuntimeBridge { ) { if (envelope.taskId) { try { - const task = this.deps.db.getTask(envelope.taskId); + const task = this.deps.db.entityCrud.getTask(envelope.taskId); return ensureTaskThread(this.deps.db, envelope.taskId, envelope.companyId, task.title); } catch (error) { this.deps.logger.warn( diff --git a/src/main/api-server/routes-agent-control.ts b/src/main/api-server/routes-agent-control.ts index e0e5b6d6..240cfe08 100644 --- a/src/main/api-server/routes-agent-control.ts +++ b/src/main/api-server/routes-agent-control.ts @@ -12,7 +12,7 @@ export function registerAgentControlRoutes(server: AgentApiServer) { const targetAgentId = requireParam(params, "agentId"); const targetAgent = (() => { try { - return db.getAgent(targetAgentId); + return db.entityCrud.getAgent(targetAgentId); } catch (err) { log.warn(`[api/agent-control] getAgent failed for ${targetAgentId}:`, err instanceof Error ? err.message : String(err)); return null; @@ -52,7 +52,7 @@ export function registerAgentControlRoutes(server: AgentApiServer) { const targetAgentId = requireParam(params, "agentId"); let targetAgent; try { - targetAgent = db.getAgent(targetAgentId); + targetAgent = db.entityCrud.getAgent(targetAgentId); } catch (err) { log.warn(`[api/agent-control] getAgent failed for heartbeat ${targetAgentId}:`, err instanceof Error ? err.message : String(err)); targetAgent = null; @@ -86,7 +86,7 @@ export function registerAgentControlRoutes(server: AgentApiServer) { const agentId = requireParam(params, "agentId"); let agent; try { - agent = db.getAgent(agentId); + agent = db.entityCrud.getAgent(agentId); } catch (error) { log.warn(`[api/agent-skills:GET] getAgent(${agentId}) failed: ${error instanceof Error ? error.message : String(error)}`); agent = null; @@ -118,7 +118,7 @@ export function registerAgentControlRoutes(server: AgentApiServer) { } let agent; try { - agent = db.getAgent(agentId); + agent = db.entityCrud.getAgent(agentId); } catch (error) { log.warn(`[api/agent-skills:POST] getAgent(${agentId}) failed: ${error instanceof Error ? error.message : String(error)}`); agent = null; @@ -142,7 +142,7 @@ export function registerAgentControlRoutes(server: AgentApiServer) { const skillId = requireParam(params, "skillId"); let agent; try { - agent = db.getAgent(agentId); + agent = db.entityCrud.getAgent(agentId); } catch (error) { log.warn(`[api/agent-skills:DELETE] getAgent(${agentId}) failed: ${error instanceof Error ? error.message : String(error)}`); agent = null; diff --git a/src/main/api-server/routes-agents-standup.ts b/src/main/api-server/routes-agents-standup.ts index cf0c8526..915904ff 100644 --- a/src/main/api-server/routes-agents-standup.ts +++ b/src/main/api-server/routes-agents-standup.ts @@ -3,23 +3,23 @@ import type { AgentApiServer } from "./server-core"; export function registerAgentStandupRoute(server: AgentApiServer): void { const db = server.db; server.addRoute("GET", "/api/agents/me/standup", (_req, res, _params, token) => { - const agent = db.getAgent(token.agentId); + const agent = db.entityCrud.getAgent(token.agentId); if (!agent || agent.companyId !== token.companyId) { server.json(res, 404, { error: "Agent not found" }); return; } - const directReports = db.listDirectReportsForStandup(token.companyId, token.agentId); + const directReports = db.agents.listDirectReportsForStandup(token.companyId, token.agentId); - const myTasks = db.listAssigneeTasksForStandup(token.companyId, token.agentId); + const myTasks = db.tasks.listAssigneeTasksForStandup(token.companyId, token.agentId); - const reportTasks = db.listTasksForAssignees( + const reportTasks = db.tasks.listTasksForAssignees( token.companyId, directReports.map((r) => r.id), ); const reportStatusRows = directReports.length > 0 - ? db.listReportStatusesForStandup(token.companyId, token.agentId) + ? db.agents.listReportStatusesForStandup(token.companyId, token.agentId) : []; const tasksAwaitingMyReview = reportTasks.filter((t) => t.status === "in_review"); @@ -46,7 +46,7 @@ export function registerAgentStandupRoute(server: AgentApiServer): void { const unreadInbox = db.listAgentInboxUnread(token.companyId, token.agentId); const senderIds = new Set(unreadInbox.map((m) => m.fromAgentId)); const senderNames = senderIds.size > 0 - ? new Map(db.listAgentNamesByIds(token.companyId, Array.from(senderIds)).map((row) => [row.id, row.name])) + ? new Map(db.agents.listAgentNamesByIds(token.companyId, Array.from(senderIds)).map((row) => [row.id, row.name])) : new Map(); // Recent outputs the AGENT itself produced — closes the diff --git a/src/main/api-server/routes-agents.ts b/src/main/api-server/routes-agents.ts index 17d5ca7c..95f4baf0 100644 --- a/src/main/api-server/routes-agents.ts +++ b/src/main/api-server/routes-agents.ts @@ -17,9 +17,9 @@ export function registerAgentRoutes(server: AgentApiServer) { const callbacks = server.callbacks; const onDomainChanged = server.onDomainChanged; server.addRoute("GET", "/api/agents/me", (_req, res, _params, token) => { - const agent = db.getAgent(token.agentId); - const chainOfCommand = db.getChainOfCommand(token.agentId); - const directReports = db.getDirectReports(token.agentId, token.companyId); + const agent = db.entityCrud.getAgent(token.agentId); + const chainOfCommand = db.agents.getChainOfCommand(token.agentId); + const directReports = db.agents.getDirectReports(token.agentId, token.companyId); server.json(res, 200, { agent, chainOfCommand, directReports }); }); @@ -40,7 +40,7 @@ export function registerAgentRoutes(server: AgentApiServer) { server.addRoute("GET", "/api/agents/:agentId/metrics", (_req, res, params, token) => { const agentId = requireParam(params, "agentId"); - const agent = db.getAgent(agentId); + const agent = db.entityCrud.getAgent(agentId); if (!agent || agent.companyId !== token.companyId) { server.json(res, 404, { error: "Agent not found" }); return; @@ -57,7 +57,7 @@ export function registerAgentRoutes(server: AgentApiServer) { server.addRoute("GET", "/api/agents/:agentId", (_req, res, params, token) => { let agent; try { - agent = db.getAgent(requireParam(params, "agentId")); + agent = db.entityCrud.getAgent(requireParam(params, "agentId")); } catch (err) { log.warn(`[api/agents] getAgent failed:`, err instanceof Error ? err.message : String(err)); server.json(res, 404, { error: "Agent not found" }); @@ -67,12 +67,12 @@ export function registerAgentRoutes(server: AgentApiServer) { server.json(res, 404, { error: "Agent not found" }); return; } - const directReports = db.getDirectReports(agent.id, token.companyId); + const directReports = db.agents.getDirectReports(agent.id, token.companyId); const reportsTo = agent.reportsTo; const manager = reportsTo ? (() => { try { - return db.getAgent(reportsTo); + return db.entityCrud.getAgent(reportsTo); } catch (error) { log.warn(`[api/agents] manager lookup failed for ${reportsTo}: ${error instanceof Error ? error.message : String(error)}`); return null; @@ -91,7 +91,7 @@ export function registerAgentRoutes(server: AgentApiServer) { server.json(res, 403, { error: "Company mismatch" }); return; } - const agents = db.listAgentsForApi(params.companyId); + const agents = db.agents.listAgentsForApi(params.companyId); server.json(res, 200, agents); }); @@ -157,7 +157,7 @@ export function registerAgentRoutes(server: AgentApiServer) { resolvedReportsTo && resolvedReportsTo !== token.agentId && !canManageOrganization(db, companyId, token.agentId) && - !db.getChainOfCommand(resolvedReportsTo).some((agent) => agent.id === token.agentId) + !db.agents.getChainOfCommand(resolvedReportsTo).some((agent) => agent.id === token.agentId) ) { server.json(res, 403, { error: "Requested hire must report to you or a manager in your reporting chain." }); return; @@ -181,7 +181,7 @@ export function registerAgentRoutes(server: AgentApiServer) { } // Prevent duplicate agents: reject if a non-terminated agent with the same name already exists - const existingAgents = db.listExistingAgentsForHireCheck(companyId); + const existingAgents = db.agents.listExistingAgentsForHireCheck(companyId); const duplicateByName = existingAgents.find( (a) => a.name.toLowerCase() === input.name.toLowerCase(), ); @@ -208,7 +208,7 @@ export function registerAgentRoutes(server: AgentApiServer) { const managerLabel = resolvedReportsTo ? (() => { try { - return db.getAgent(resolvedReportsTo).name; + return db.entityCrud.getAgent(resolvedReportsTo).name; } catch (err) { log.warn(`[api/agents] getAgent failed for manager ${resolvedReportsTo}:`, err instanceof Error ? err.message : String(err)); return resolvedReportsTo; diff --git a/src/main/api-server/routes-approvals.ts b/src/main/api-server/routes-approvals.ts index aaeebfb2..64af7c02 100644 --- a/src/main/api-server/routes-approvals.ts +++ b/src/main/api-server/routes-approvals.ts @@ -183,7 +183,7 @@ export function registerApprovalRoutes(server: AgentApiServer) { body: commentBody, }); // Wake mentioned agents in comment - const mentionedIds = db.findMentionedAgentIds(approval.companyId, commentBody); + const mentionedIds = db.agents.findMentionedAgentIds(approval.companyId, commentBody); for (const mentionedId of mentionedIds) { if (mentionedId !== token.agentId) { onWakeAgent(mentionedId, approval.companyId, "approval_comment_mentioned"); diff --git a/src/main/api-server/routes-files.ts b/src/main/api-server/routes-files.ts index a1b2fd7e..cda33aee 100644 --- a/src/main/api-server/routes-files.ts +++ b/src/main/api-server/routes-files.ts @@ -228,7 +228,7 @@ export function registerFileRoutes(server: AgentApiServer) { server.addRoute("GET", "/api/issues/:issueId/work-products", (_req, res, params, token) => { try { - const task = db.getTask(requireParam(params, "issueId")); + const task = db.entityCrud.getTask(requireParam(params, "issueId")); if (task.companyId !== token.companyId) { server.json(res, 404, { error: "Task not found" }); return; } const products = db.listWorkProductsForTask(task.id, task.companyId); server.json(res, 200, products); @@ -240,7 +240,7 @@ export function registerFileRoutes(server: AgentApiServer) { server.addRoute("POST", "/api/issues/:issueId/work-products", (_req, res, params, token, body) => { try { - const task = db.getTask(requireParam(params, "issueId")); + const task = db.entityCrud.getTask(requireParam(params, "issueId")); if (task.companyId !== token.companyId) { server.json(res, 404, { error: "Task not found" }); return; } const parsed = createWorkProductSchema.safeParse(body); if (!parsed.success) { server.json(res, 400, { error: "Invalid input", details: parsed.error.flatten() }); return; } @@ -294,7 +294,7 @@ export function registerFileRoutes(server: AgentApiServer) { server.addRoute("GET", "/api/issues/:issueId/documents", (_req, res, params, token) => { try { - const task = db.getTask(requireParam(params, "issueId")); + const task = db.entityCrud.getTask(requireParam(params, "issueId")); if (task.companyId !== token.companyId) { server.json(res, 404, { error: "Task not found" }); return; } const links = db.listDocumentsForTask(task.id, task.companyId); server.json(res, 200, links); @@ -306,7 +306,7 @@ export function registerFileRoutes(server: AgentApiServer) { server.addRoute("PUT", "/api/issues/:issueId/documents/:key", (_req, res, params, token, body) => { try { - const task = db.getTask(requireParam(params, "issueId")); + const task = db.entityCrud.getTask(requireParam(params, "issueId")); if (task.companyId !== token.companyId) { server.json(res, 404, { error: "Task not found" }); return; } const key = String(params.key ?? "").trim().toLowerCase(); if (!key || !/^[a-z0-9][a-z0-9_-]*$/.test(key) || key.length > 64) { @@ -329,7 +329,7 @@ export function registerFileRoutes(server: AgentApiServer) { server.addRoute("DELETE", "/api/issues/:issueId/documents/:key", (_req, res, params, token) => { try { - const task = db.getTask(requireParam(params, "issueId")); + const task = db.entityCrud.getTask(requireParam(params, "issueId")); if (task.companyId !== token.companyId) { server.json(res, 404, { error: "Task not found" }); return; } const key = String(params.key ?? "").trim().toLowerCase(); db.unlinkDocumentFromTask(task.id, key, task.companyId); diff --git a/src/main/api-server/routes-meetings.ts b/src/main/api-server/routes-meetings.ts index 9d6ee5a2..a48ed553 100644 --- a/src/main/api-server/routes-meetings.ts +++ b/src/main/api-server/routes-meetings.ts @@ -133,7 +133,7 @@ export function registerMeetingRoutes(server: AgentApiServer) { ); continue; } - db.saveTask({ + db.tasks.saveTask({ companyId: token.companyId, title: item.title, description: item.description ?? `Action item from meeting: ${data.title ?? existing.title}`, diff --git a/src/main/api-server/routes-messages.ts b/src/main/api-server/routes-messages.ts index 55bd7774..75e63b0d 100644 --- a/src/main/api-server/routes-messages.ts +++ b/src/main/api-server/routes-messages.ts @@ -20,12 +20,12 @@ export function registerMessageRoutes(server: AgentApiServer) { return; } try { - const task = db.getTask(requireParam(params, "issueId")); + const task = db.entityCrud.getTask(requireParam(params, "issueId")); if (task.companyId !== token.companyId) { server.json(res, 404, { error: "Task not found" }); return; } - const agent = db.getAgent(token.agentId); + const agent = db.entityCrud.getAgent(token.agentId); const comment = db.addComment({ companyId: task.companyId, taskId: task.id, @@ -44,14 +44,14 @@ export function registerMessageRoutes(server: AgentApiServer) { wokenForComment.add(task.assigneeAgentId); } - const chain = db.getChainOfCommand(token.agentId); + const chain = db.agents.getChainOfCommand(token.agentId); if (chain.length > 0 && chain[0].id !== token.agentId && !wokenForComment.has(chain[0].id)) { onWakeAgent(chain[0].id, task.companyId, "comment"); wokenForComment.add(chain[0].id); } // @mention detection: wake any mentioned agents not already woken - const mentionedIds = db.findMentionedAgentIds(task.companyId, input.body); + const mentionedIds = db.agents.findMentionedAgentIds(task.companyId, input.body); for (const mentionedId of mentionedIds) { if (mentionedId !== token.agentId && !wokenForComment.has(mentionedId)) { onWakeAgent(mentionedId, task.companyId, "comment_mentioned"); @@ -68,7 +68,7 @@ export function registerMessageRoutes(server: AgentApiServer) { server.addRoute("GET", "/api/issues/:issueId/comments", (_req, res, params, token) => { try { - const task = db.getTask(requireParam(params, "issueId")); + const task = db.entityCrud.getTask(requireParam(params, "issueId")); if (task.companyId !== token.companyId) { server.json(res, 404, { error: "Task not found" }); return; diff --git a/src/main/api-server/routes-tasks.ts b/src/main/api-server/routes-tasks.ts index ecdcadd1..6762c70e 100644 --- a/src/main/api-server/routes-tasks.ts +++ b/src/main/api-server/routes-tasks.ts @@ -38,17 +38,17 @@ export function registerTaskRoutes(server: AgentApiServer) { const statuses = statusFilter ? statusFilter.split(",") : ["todo", "in_progress", "blocked"]; if (assigneeAgentId) { - const tasks = db.listAgentTasks(assigneeAgentId, companyId, statuses); + const tasks = db.tasks.listAgentTasks(assigneeAgentId, companyId, statuses); server.json(res, 200, tasks); } else { - const tasks = db.listTasksByStatuses(companyId, statuses); + const tasks = db.tasks.listTasksByStatuses(companyId, statuses); server.json(res, 200, tasks); } }); server.addRoute("GET", "/api/issues/:issueId", (_req, res, params, token) => { try { - const task = db.getTask(requireParam(params, "issueId")); + const task = db.entityCrud.getTask(requireParam(params, "issueId")); if (task.companyId !== token.companyId) { server.json(res, 404, { error: "Task not found" }); return; @@ -73,7 +73,7 @@ export function registerTaskRoutes(server: AgentApiServer) { } let task; try { - task = db.getTask(requireParam(params, "issueId")); + task = db.entityCrud.getTask(requireParam(params, "issueId")); } catch (err) { log.warn(`[api/tasks] getTask failed for checkout:`, err instanceof Error ? err.message : String(err)); server.json(res, 404, { error: "Task not found" }); @@ -88,7 +88,7 @@ export function registerTaskRoutes(server: AgentApiServer) { return; } const previousStatus = task.status; - const success = db.checkoutTask(task.id, token.runId); + const success = db.tasks.checkoutTask(task.id, token.runId); if (!success) { server.json(res, 409, { error: "Task cannot be checked out — already assigned or not in valid status" }); return; @@ -108,7 +108,7 @@ export function registerTaskRoutes(server: AgentApiServer) { const update = parsedUpdate.data; try { - const task = db.getTask(requireParam(params, "issueId")); + const task = db.entityCrud.getTask(requireParam(params, "issueId")); if (task.companyId !== token.companyId) { server.json(res, 404, { error: "Task not found" }); return; @@ -134,7 +134,7 @@ export function registerTaskRoutes(server: AgentApiServer) { taskId: task.id, limit: 10, }); - const reviewerAgent = db.getAgent(token.agentId); + const reviewerAgent = db.entityCrud.getAgent(token.agentId); const docTitles = docs.map((d) => d.title).join(", "); db.requestApproval({ companyId: task.companyId, @@ -145,11 +145,11 @@ export function registerTaskRoutes(server: AgentApiServer) { impactSummary: `${reviewerAgent.name} reviewed and approved. Awaiting your final review.`.slice(0, 400), payloadJson: JSON.stringify({ documentIds: docs.map((d) => d.id), documents: docs }), }); - db.setTaskRequiresUserReview(task.id, false); + db.tasks.setTaskRequiresUserReview(task.id, false); if (task.assigneeAgentId) { - const prodAgent = db.getAgent(task.assigneeAgentId); + const prodAgent = db.entityCrud.getAgent(task.assigneeAgentId); if (prodAgent.heartbeatEnabled) { - db.setHeartbeat(task.assigneeAgentId, false, prodAgent.heartbeatIntervalSec || 120); + db.agents.setHeartbeat(task.assigneeAgentId, false, prodAgent.heartbeatIntervalSec || 120); } } db.addComment({ companyId: task.companyId, taskId: task.id, authorAgentId: null, authorName: "System", body: "Deliverable submitted for board review." }); @@ -169,7 +169,7 @@ export function registerTaskRoutes(server: AgentApiServer) { // advanceTaskStatus checks previousStatus !== newStatus internally, // so it must run before saveTask writes the new status. if (update.status && update.status !== task.status) { - db.advanceTaskStatus(task.id, update.status); + db.tasks.advanceTaskStatus(task.id, update.status); } const input: Record = { @@ -185,10 +185,10 @@ export function registerTaskRoutes(server: AgentApiServer) { assigneeAgentId: newAssignee, workspaceId: task.workspaceId, }; - db.saveTask(input as Parameters[0]); + db.tasks.saveTask(input as Parameters[0]); if (update.comment) { - const agent = db.getAgent(token.agentId); + const agent = db.entityCrud.getAgent(token.agentId); db.addComment({ companyId: task.companyId, taskId: task.id, @@ -219,7 +219,7 @@ export function registerTaskRoutes(server: AgentApiServer) { if (update.status && ["done", "in_review"].includes(update.status)) { if (task.parentId) { try { - const parentTask = db.getTask(task.parentId); + const parentTask = db.entityCrud.getTask(task.parentId); if (parentTask.assigneeAgentId && parentTask.assigneeAgentId !== token.agentId) { onWakeAgent(parentTask.assigneeAgentId, task.companyId, "subtask_completed"); } @@ -233,7 +233,7 @@ export function registerTaskRoutes(server: AgentApiServer) { // This is not a subtask — use a distinct wake reason so the // reviewer prompt does not misframe the work as "a subtask // you delegated". - const chain = db.getChainOfCommand(token.agentId); + const chain = db.agents.getChainOfCommand(token.agentId); if (chain.length > 0) { onWakeAgent(chain[0].id, task.companyId, "task_review_requested"); } @@ -241,7 +241,7 @@ export function registerTaskRoutes(server: AgentApiServer) { } if (update.status === "blocked") { - const chain = db.getChainOfCommand(token.agentId); + const chain = db.agents.getChainOfCommand(token.agentId); if (chain.length > 0) { onWakeAgent(chain[0].id, task.companyId, "report_blocked"); } @@ -261,7 +261,7 @@ export function registerTaskRoutes(server: AgentApiServer) { onWakeAgent(task.assigneeAgentId, task.companyId, "comment"); wokenForComment.add(task.assigneeAgentId); } - const chain = db.getChainOfCommand(token.agentId); + const chain = db.agents.getChainOfCommand(token.agentId); if (chain.length > 0 && chain[0].id !== token.agentId && !wokenForComment.has(chain[0].id)) { onWakeAgent(chain[0].id, task.companyId, "comment"); } @@ -333,7 +333,7 @@ export function registerTaskRoutes(server: AgentApiServer) { let id: string; try { - id = db.saveTask({ + id = db.tasks.saveTask({ companyId, title: input.title, description: input.description ?? "", @@ -352,9 +352,9 @@ export function registerTaskRoutes(server: AgentApiServer) { // Auto-infer task_type from assignee's department if (input.assigneeAgentId) { - const assignee = db.getAgent(input.assigneeAgentId); + const assignee = db.entityCrud.getAgent(input.assigneeAgentId); const inferredType = inferTaskType(assignee.department); - db.setTaskType(id, inferredType); + db.tasks.setTaskType(id, inferredType); } db.addActivity({ diff --git a/src/main/automation/automation-actions.ts b/src/main/automation/automation-actions.ts index d8bacf23..a2c9fabf 100644 --- a/src/main/automation/automation-actions.ts +++ b/src/main/automation/automation-actions.ts @@ -76,7 +76,7 @@ export function executeAutomationAction( switch (action) { case "assign_task": { if (ctx.taskId) { - const assignedId = db.autoAssignTask(ctx.taskId, ctx.companyId); + const assignedId = db.tasks.autoAssignTask(ctx.taskId, ctx.companyId); if (assignedId) { wakeAgentIfPossible(assignedId, ctx.companyId, "assignment"); } @@ -90,7 +90,7 @@ export function executeAutomationAction( }); if (unassigned.length > 0) { const task = unassigned[0]!; - db.saveTask({ + db.tasks.saveTask({ id: task.id, companyId: task.companyId, projectId: task.projectId, @@ -120,7 +120,7 @@ export function executeAutomationAction( logger.warn(`[automation] Skipping create_task — validation failed: ${taskValidation.errors.join("; ")}`); break; } - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId: ctx.companyId, projectId: ctx.projectId ?? cfg.projectId ?? null, title, @@ -147,7 +147,7 @@ export function executeAutomationAction( let channelTargetId: string | null = cfg.channelTargetId ?? cfg.department ?? ctx.department ?? null; if (channel === "department" && !channelTargetId && fromAgentId) { try { - const sender = db.getAgent(fromAgentId); + const sender = db.entityCrud.getAgent(fromAgentId); channelTargetId = sender.department ?? null; } catch (err) { logger.warn(`[automation] getAgent failed for sender ${fromAgentId}; dropping department target:`, err instanceof Error ? err.message : String(err)); @@ -195,7 +195,7 @@ export function executeAutomationAction( if (!targetAgentId && ctx.agentId) { const triggerType = rule.trigger ?? ""; if (triggerType === "run_completed" || triggerType === "run_failed") { - const manager = db.getManagerForAgent(ctx.agentId); + const manager = db.agents.getManagerForAgent(ctx.agentId); targetAgentId = manager?.id ?? ctx.agentId; } else { targetAgentId = ctx.agentId; @@ -221,8 +221,8 @@ export function executeAutomationAction( case "update_task_status": { if (!ctx.taskId) break; const newStatus = (config.status as string) ?? "todo"; - const previousTask = db.getTask(ctx.taskId); - db.advanceTaskStatus(ctx.taskId, newStatus, "Status changed by automation rule"); + const previousTask = db.entityCrud.getTask(ctx.taskId); + db.tasks.advanceTaskStatus(ctx.taskId, newStatus, "Status changed by automation rule"); publishDomainChanged(); if (previousTask.status !== newStatus && rule.trigger !== "task_status_changed") { handleTaskStatusChange(ctx.taskId, ctx.companyId, previousTask.status, newStatus); @@ -234,17 +234,17 @@ export function executeAutomationAction( if (!targetAgentId) break; let agentName = "an agent"; try { - agentName = db.getAgent(targetAgentId).name; + agentName = db.entityCrud.getAgent(targetAgentId).name; } catch (err) { logger.warn(`[automation] getAgent failed for escalation target ${targetAgentId}; using generic label:`, err instanceof Error ? err.message : String(err)); } let taskTitle = ctx.taskId ?? "unknown"; try { - if (ctx.taskId) taskTitle = db.getTask(ctx.taskId).title; + if (ctx.taskId) taskTitle = db.entityCrud.getTask(ctx.taskId).title; } catch (err) { logger.warn(`[automation] getTask failed for escalation task ${ctx.taskId}; using id as title:`, err instanceof Error ? err.message : String(err)); } - const manager = db.getManagerForAgent(targetAgentId); + const manager = db.agents.getManagerForAgent(targetAgentId); const escalationBody = (config.body as string) ?? `Task "${taskTitle}" needs your attention. ${agentName} has flagged this as requiring manager intervention.`; const wakeTrigger = deriveEscalationWakeTrigger(ctx.newStatus ?? null, rule.trigger ?? null); if (manager) { @@ -277,7 +277,7 @@ export function executeAutomationAction( case "cross_department_notify": { const targetDepartment = rule.targetDepartment ?? (config.department as string); if (!targetDepartment) break; - const departmentAgents = db.getIdleAgentsForDepartment(ctx.companyId, targetDepartment); + const departmentAgents = db.agents.getIdleAgentsForDepartment(ctx.companyId, targetDepartment); const senderAgentId = ctx.agentId ?? departmentAgents[0]?.id; if (senderAgentId) { const dispatchResult = dispatchAgentMessage( @@ -352,7 +352,7 @@ export function executeAutomationAction( } case "reassign_task": { if (!ctx.taskId) break; - const newAssignee = db.autoAssignTask(ctx.taskId, ctx.companyId); + const newAssignee = db.tasks.autoAssignTask(ctx.taskId, ctx.companyId); if (newAssignee) { wakeAgentIfPossible(newAssignee, ctx.companyId, "assignment"); } @@ -361,7 +361,7 @@ export function executeAutomationAction( case "run_task": { const targetTaskId = (config.taskId as string) ?? ctx.taskId; if (!targetTaskId) break; - const task = db.getTask(targetTaskId); + const task = db.entityCrud.getTask(targetTaskId); if (!task.assigneeAgentId) break; wakeAgentIfPossible(task.assigneeAgentId, ctx.companyId, "assignment"); break; diff --git a/src/main/automation/automation-approval-handler.ts b/src/main/automation/automation-approval-handler.ts index ee8e777b..f0408b9c 100644 --- a/src/main/automation/automation-approval-handler.ts +++ b/src/main/automation/automation-approval-handler.ts @@ -30,7 +30,7 @@ function recordReviewFeedbackKnowledge( if (!approval.relatedTaskId) return; if (decision === "approved") return; - const task = db.getTask(approval.relatedTaskId); + const task = db.entityCrud.getTask(approval.relatedTaskId); const feedback = approval.decisionNote?.trim() || "Board requested revisions before approval."; const topic = `Review feedback: ${task.title}`; const content = [ @@ -95,7 +95,7 @@ export function handleApprovalResolved( }); if (approval.type === "hire_agent" && decision === "approved" && approval.relatedAgentId) { - const agent = db.getAgent(approval.relatedAgentId); + const agent = db.entityCrud.getAgent(approval.relatedAgentId); if (agent.status === "idle") { // Deterministic default skill kit. The CEO's operating prompt @@ -136,7 +136,7 @@ export function handleApprovalResolved( }); wakeAgentIfPossible(agent.id, companyId, "assignment"); - const ceo = db.getCeoForCompany(companyId); + const ceo = db.agents.getCeoForCompany(companyId); if (ceo && ceo.id !== agent.id) { wakeAgentIfPossible(ceo.id, companyId, "assignment"); } @@ -174,27 +174,27 @@ export function handleApprovalResolved( } if (approval.relatedTaskId) { - const task = db.getTask(approval.relatedTaskId); + const task = db.entityCrud.getTask(approval.relatedTaskId); if (task.assigneeAgentId) { - const agent = db.getAgent(task.assigneeAgentId); + const agent = db.entityCrud.getAgent(task.assigneeAgentId); if (!agent.heartbeatEnabled) { - db.setHeartbeat(task.assigneeAgentId, true, agent.heartbeatIntervalSec || 120); + db.agents.setHeartbeat(task.assigneeAgentId, true, agent.heartbeatIntervalSec || 120); logger.info(`[deliverable-review] Resumed heartbeat for ${agent.name} after user ${decision}.`); } } if (decision === "approved") { - db.advanceTaskStatus(task.id, "done", "Board approved deliverable."); + db.tasks.advanceTaskStatus(task.id, "done", "Board approved deliverable."); handleTaskStatusChange(task.id, companyId, task.status, "done"); db.addActivity({ companyId, actor: "board", action: "deliverable.approved", entityType: "task", entityId: task.id, detail: approval.decisionNote || "Deliverable approved by board." }); } else if (decision === "rejected") { recordReviewFeedbackKnowledge(db, approval, decision); - db.advanceTaskStatus(task.id, "in_progress", `Board rejected deliverable: ${approval.decisionNote || "No reason given."}`); + db.tasks.advanceTaskStatus(task.id, "in_progress", `Board rejected deliverable: ${approval.decisionNote || "No reason given."}`); handleTaskStatusChange(task.id, companyId, task.status, "in_progress"); if (task.assigneeAgentId) { wakeAgentIfPossible(task.assigneeAgentId, companyId, "revision_needed"); } } else if (decision === "revision_requested") { - db.advanceTaskStatus(task.id, "in_progress", `Board requested revisions: ${approval.decisionNote || "No reason given."}`); + db.tasks.advanceTaskStatus(task.id, "in_progress", `Board requested revisions: ${approval.decisionNote || "No reason given."}`); handleTaskStatusChange(task.id, companyId, task.status, "in_progress"); recordReviewFeedbackKnowledge(db, approval, decision); if (task.assigneeAgentId) { @@ -202,9 +202,9 @@ export function handleApprovalResolved( } } } else if (approval.requestedByAgentId) { - const agent = db.getAgent(approval.requestedByAgentId); + const agent = db.entityCrud.getAgent(approval.requestedByAgentId); if (!agent.heartbeatEnabled) { - db.setHeartbeat(approval.requestedByAgentId, true, agent.heartbeatIntervalSec || 120); + db.agents.setHeartbeat(approval.requestedByAgentId, true, agent.heartbeatIntervalSec || 120); logger.info(`[deliverable-review] Resumed heartbeat for ${agent.name} after user ${decision}.`); } if (decision !== "approved") { diff --git a/src/main/automation/automation-conditions.ts b/src/main/automation/automation-conditions.ts index b10f17d9..319ab038 100644 --- a/src/main/automation/automation-conditions.ts +++ b/src/main/automation/automation-conditions.ts @@ -23,7 +23,7 @@ export function matchesEventConfig( if (conditions.previousStatus && ctx.previousStatus && conditions.previousStatus !== ctx.previousStatus) return false; if (conditions.priority) { try { - const task = ctx.taskId ? db.getTask(ctx.taskId) : null; + const task = ctx.taskId ? db.entityCrud.getTask(ctx.taskId) : null; if (task && task.priority !== conditions.priority) return false; // If task was deleted, skip this condition instead of rejecting the rule. } catch (err) { @@ -32,7 +32,7 @@ export function matchesEventConfig( } if (typeof conditions.budgetThresholdPct === "number" && ctx.agentId) { try { - const agent = db.getAgent(ctx.agentId); + const agent = db.entityCrud.getAgent(ctx.agentId); if (agent.budgetMonthlyUsd > 0) { const utilization = agent.spentMonthlyUsd / agent.budgetMonthlyUsd; if (utilization < (conditions.budgetThresholdPct as number) / 100) return false; diff --git a/src/main/automation/automation-handlers.ts b/src/main/automation/automation-handlers.ts index b579a7ef..6adae39c 100644 --- a/src/main/automation/automation-handlers.ts +++ b/src/main/automation/automation-handlers.ts @@ -41,9 +41,9 @@ export function handleTaskStatusChange( const { db, wakeAgentIfPossible } = deps; const { dispatchAutomation } = callbacks; - let task: ReturnType | null = null; + let task: ReturnType | null = null; try { - task = db.getTask(taskId); + task = db.entityCrud.getTask(taskId); } catch (error) { deps.logger.warn(`[automation] handleTaskStatusChange: task ${taskId} not found:`, error); return; @@ -68,7 +68,7 @@ export function handleTaskStatusChange( }; try { if (task.assigneeAgentId) { - const agent = db.getAgent(task.assigneeAgentId); + const agent = db.entityCrud.getAgent(task.assigneeAgentId); context.department = agent.department ?? undefined; } } catch (error) { @@ -80,7 +80,7 @@ export function handleTaskStatusChange( if (newStatus === "blocked") { dispatchAutomation("task_blocked", context); if (task.assigneeAgentId) { - const manager = db.getManagerForAgent(task.assigneeAgentId); + const manager = db.agents.getManagerForAgent(task.assigneeAgentId); if (manager) { wakeAgentIfPossible(manager.id, companyId, "report_blocked"); } @@ -89,14 +89,14 @@ export function handleTaskStatusChange( if (newStatus === "done" && task.parentId) { try { - const siblings = db.listChildTasks(task.parentId, companyId); + const siblings = db.tasks.listChildTasks(task.parentId, companyId); const allDone = siblings.every((entry) => entry.id === taskId ? true : entry.status === "done"); if (allDone) { - const parent = db.getTask(task.parentId); + const parent = db.entityCrud.getTask(task.parentId); if (parent.assigneeAgentId) { wakeAgentIfPossible(parent.assigneeAgentId, companyId, "subtask_completed"); } else { - const assignedId = db.autoAssignTask(parent.id, companyId); + const assignedId = db.tasks.autoAssignTask(parent.id, companyId); if (assignedId) { wakeAgentIfPossible(assignedId, companyId, "subtask_completed"); } @@ -118,9 +118,9 @@ export function handleTaskCreated( const { db, wakeAgentIfPossible } = deps; const { dispatchAutomation } = callbacks; - let task: ReturnType | null = null; + let task: ReturnType | null = null; try { - task = db.getTask(taskId); + task = db.entityCrud.getTask(taskId); } catch (error) { deps.logger.warn(`[automation] handleTaskCreated: task ${taskId} not found:`, error); return; @@ -134,7 +134,7 @@ export function handleTaskCreated( }); if (!task.assigneeAgentId) { - const assignedId = db.autoAssignTask(taskId, companyId); + const assignedId = db.tasks.autoAssignTask(taskId, companyId); if (assignedId) { wakeAgentIfPossible(assignedId, companyId, "assignment"); } diff --git a/src/main/automation/automation-run-completion.ts b/src/main/automation/automation-run-completion.ts index 012f5579..f487ab34 100644 --- a/src/main/automation/automation-run-completion.ts +++ b/src/main/automation/automation-run-completion.ts @@ -190,10 +190,10 @@ function createDeliverableReviewApproval( impactSummary: reviewerNote ?? "Agent completed work with important deliverables. Awaiting your final review.", payloadJson: JSON.stringify({ documentIds: docs.map((d) => d.id), documents: docs }), }); - db.setTaskRequiresUserReview(taskId, false); - const agent = db.getAgent(agentId); + db.tasks.setTaskRequiresUserReview(taskId, false); + const agent = db.entityCrud.getAgent(agentId); if (agent.heartbeatEnabled) { - db.setHeartbeat(agentId, false, agent.heartbeatIntervalSec || 120); + db.agents.setHeartbeat(agentId, false, agent.heartbeatIntervalSec || 120); logger.info(`[deliverable-review] Paused heartbeat for ${agent.name} — awaiting user review.`); } notify({ @@ -228,7 +228,7 @@ function createDeliverableReviewApproval( */ function decideTerminalStatus( db: RunCompletionDeps["db"], - agent: ReturnType, + agent: ReturnType, companyId: string, ): "in_review" | "done" { if (agent.reportsTo !== null) return "in_review"; @@ -247,7 +247,7 @@ function advanceToDone( statusMessage: string, ): void { const { db, publishDomainChanged } = deps; - db.advanceTaskStatus(taskId, "done", statusMessage); + db.tasks.advanceTaskStatus(taskId, "done", statusMessage); callbacks.handleTaskStatusChange(taskId, run.companyId, "in_progress", "done"); publishDomainChanged(); } @@ -268,22 +268,22 @@ function advanceToInReview( // expected (`ai_cofounder` mode or `reviewDeliverables = true`), so // pre-flagging here is the right semantics — the human IS the // reviewer in those configurations. - const agent = db.getAgent(run.agentId); + const agent = db.entityCrud.getAgent(run.agentId); if (agent.reportsTo === null) { - db.setTaskRequiresUserReview(taskId, true); + db.tasks.setTaskRequiresUserReview(taskId, true); } - db.advanceTaskStatus(taskId, "in_review", statusMessage); + db.tasks.advanceTaskStatus(taskId, "in_review", statusMessage); callbacks.handleTaskStatusChange(taskId, run.companyId, "in_progress", "in_review"); const company = db.listSnapshot().companies.find((c) => c.id === run.companyId); if (company?.reviewDeliverables) { const importantDocs = findImportantDocsForRun(db, run.id, run.companyId); if (importantDocs.length > 0) { - db.setTaskRequiresUserReview(taskId, true); + db.tasks.setTaskRequiresUserReview(taskId, true); if (agent.heartbeatEnabled) { - db.setHeartbeat(agent.id, false, agent.heartbeatIntervalSec || 120); + db.agents.setHeartbeat(agent.id, false, agent.heartbeatIntervalSec || 120); logger.info(`[deliverable-review] Paused heartbeat for ${agent.name} — deliverable flagged for review.`); } - const chain = db.getChainOfCommand(agent.id); + const chain = db.agents.getChainOfCommand(agent.id); if (chain.length === 0) { createDeliverableReviewApproval(deps, run.companyId, taskId, run.agentId, importantDocs); } @@ -305,7 +305,7 @@ function advanceSuccessfulRun( taskId: string, statusMessage: string, ): "in_review" | "done" { - const agent = deps.db.getAgent(run.agentId); + const agent = deps.db.entityCrud.getAgent(run.agentId); const target = decideTerminalStatus(deps.db, agent, run.companyId); if (target === "done") { advanceToDone(deps, callbacks, run, taskId, statusMessage); @@ -327,9 +327,9 @@ export function processRunCompletion( const { dispatchAutomation, handleTaskStatusChange } = callbacks; if (!run.taskId) return; - let task: ReturnType | null = null; + let task: ReturnType | null = null; try { - task = db.getTask(run.taskId); + task = db.entityCrud.getTask(run.taskId); } catch (error) { deps.logger.warn(`[automation] processRunCompletion: task ${run.taskId} not found for run ${run.id}:`, error); return; @@ -363,7 +363,7 @@ export function processRunCompletion( // the task in the manager's blocker queue with the diagnostic // comment attached; the manager patches it back to `todo` (or // `cancelled`) after deciding how to proceed. - db.advanceTaskStatus( + db.tasks.advanceTaskStatus( task.id, "blocked", `${taskType} task run completed but no deliverable artifact was detected. Flagging for manager review.`, @@ -371,8 +371,8 @@ export function processRunCompletion( handleTaskStatusChange(task.id, run.companyId, "in_progress", "blocked"); // Wake manager for review try { - const agent = db.getAgent(run.agentId); - const chain = db.getChainOfCommand(agent.id); + const agent = db.entityCrud.getAgent(run.agentId); + const chain = db.agents.getChainOfCommand(agent.id); if (chain.length > 0) { wakeAgentIfPossible(chain[0].id, run.companyId, "review_needed"); } @@ -414,7 +414,7 @@ export function processRunCompletion( runId: run.id, }; try { - const agent = db.getAgent(run.agentId); + const agent = db.entityCrud.getAgent(run.agentId); automationContext.department = agent.department ?? undefined; } catch (err) { deps.logger.warn(`[automation] getAgent failed for ${run.agentId} during run completion; dropping department from context:`, err instanceof Error ? err.message : String(err)); @@ -424,14 +424,14 @@ export function processRunCompletion( dispatchAutomation("run_completed", automationContext); if (task.parentId) { try { - const siblings = db.listChildTasks(task.parentId, run.companyId); + const siblings = db.tasks.listChildTasks(task.parentId, run.companyId); const allSiblingsDoneOrReview = siblings.every((entry) => entry.status === "done" || entry.status === "in_review"); if (allSiblingsDoneOrReview) { - const parent = db.getTask(task.parentId); + const parent = db.entityCrud.getTask(task.parentId); if (parent.assigneeAgentId) { wakeAgentIfPossible(parent.assigneeAgentId, run.companyId, "subtask_completed"); } else { - const assignedId = db.autoAssignTask(parent.id, run.companyId); + const assignedId = db.tasks.autoAssignTask(parent.id, run.companyId); if (assignedId) { wakeAgentIfPossible(assignedId, run.companyId, "subtask_completed"); } @@ -452,7 +452,7 @@ export function processRunCompletion( // just read the post-run spend and fire if it crosses 80%. if (isSuccess) { try { - const agent = db.getAgent(run.agentId); + const agent = db.entityCrud.getAgent(run.agentId); if (agent.budgetMonthlyUsd > 0) { const utilization = agent.spentMonthlyUsd / agent.budgetMonthlyUsd; if (utilization >= 0.8) { diff --git a/src/main/automation/automation-workflow.ts b/src/main/automation/automation-workflow.ts index b142fc0c..3dd71a28 100644 --- a/src/main/automation/automation-workflow.ts +++ b/src/main/automation/automation-workflow.ts @@ -59,7 +59,7 @@ export function resolveWorkflowStepActor( companyId: string, ): AgentRecord | null { if (step.assigneeAgentId) { - const agent = db.getAgent(step.assigneeAgentId); + const agent = db.entityCrud.getAgent(step.assigneeAgentId); if (agent.companyId !== companyId) { throw new Error(`Workflow step assignee ${step.assigneeAgentId} does not belong to company ${companyId}.`); } @@ -67,9 +67,9 @@ export function resolveWorkflowStepActor( } if (step.assigneeDepartment) { - const idleAgents = db.getIdleAgentsForDepartment(companyId, step.assigneeDepartment); + const idleAgents = db.agents.getIdleAgentsForDepartment(companyId, step.assigneeDepartment); if (idleAgents.length > 0) { - return db.getAgent(idleAgents[0]!.id); + return db.entityCrud.getAgent(idleAgents[0]!.id); } } diff --git a/src/main/bootstrap/recover-interrupted-tasks.ts b/src/main/bootstrap/recover-interrupted-tasks.ts index 6d21e9fe..cab1d714 100644 --- a/src/main/bootstrap/recover-interrupted-tasks.ts +++ b/src/main/bootstrap/recover-interrupted-tasks.ts @@ -49,10 +49,10 @@ export function recoverShutdownInterruptedTasks(deps: RecoverInterruptedTasksDep for (const run of shutdownRuns) { try { if (run.taskId) { - const task = deps.db.getTask(run.taskId); + const task = deps.db.entityCrud.getTask(run.taskId); if (task.status === "in_progress" || task.status === "blocked") { - deps.db.advanceTaskStatus(run.taskId, "todo", "Auto-resumed after app restart."); - deps.db.releaseTaskLock(run.taskId, run.id); + deps.db.tasks.advanceTaskStatus(run.taskId, "todo", "Auto-resumed after app restart."); + deps.db.tasks.releaseTaskLock(run.taskId, run.id); } } if (!resumedAgents.has(run.agentId)) { diff --git a/src/main/bootstrap/wire-api-server.ts b/src/main/bootstrap/wire-api-server.ts index 87401176..f8eec3cb 100644 --- a/src/main/bootstrap/wire-api-server.ts +++ b/src/main/bootstrap/wire-api-server.ts @@ -75,7 +75,7 @@ export function wireApiServer(deps: WireApiServerDeps): AgentApiServer { deps.threadIntegration.onTaskStatusChanged(taskId, companyId, prev, next); if (prev === "todo" && next === "in_progress") { try { - const task = deps.db.getTask(taskId); + const task = deps.db.entityCrud.getTask(taskId); deps.checkpointEngine.onTaskStarted(taskId, companyId, task.assigneeAgentId); } catch (error) { log.warn( @@ -118,7 +118,7 @@ export function wireApiServer(deps: WireApiServerDeps): AgentApiServer { }, input, ), - onAutoAssignTask: (taskId, companyId) => deps.db.autoAssignTask(taskId, companyId), + onAutoAssignTask: (taskId, companyId) => deps.db.tasks.autoAssignTask(taskId, companyId), onHumanMessage: (companyId, toAgentId, threadId, waitStateId) => { deps.threadIntegration.onHumanMessage(companyId, toAgentId, threadId, waitStateId); deps.publishDomainChanged(); diff --git a/src/main/budget-enforcement-service.ts b/src/main/budget-enforcement-service.ts index c8b20223..e131ad28 100644 --- a/src/main/budget-enforcement-service.ts +++ b/src/main/budget-enforcement-service.ts @@ -31,7 +31,7 @@ function getScopeLabel( ): string { if (policy.scopeType === "agent") { try { - const agent = db.getAgent(policy.scopeId); + const agent = db.entityCrud.getAgent(policy.scopeId); return `Agent "${agent.name}"`; } catch (err) { logger.warn(`[budget] getAgent failed for scope ${policy.scopeId}:`, err instanceof Error ? err.message : String(err)); diff --git a/src/main/checkpoint-engine.ts b/src/main/checkpoint-engine.ts index cca33809..db4e9f32 100644 --- a/src/main/checkpoint-engine.ts +++ b/src/main/checkpoint-engine.ts @@ -48,8 +48,8 @@ export function createCheckpointEngine(deps: CheckpointDeps) { const mode = getCompanyExecutionMode(db, companyId); if (mode !== "ai_cofounder") return null; - const task = db.getTask(taskId); - const taskType = db.getTaskType(taskId); + const task = db.entityCrud.getTask(taskId); + const taskType = db.tasks.getTaskType(taskId); // Only create plan checkpoints for non-code task types if (!taskType || !CHECKPOINT_TASK_TYPES.has(taskType)) return null; diff --git a/src/main/database-base.ts b/src/main/database-base.ts index 77e625a9..950d04db 100644 --- a/src/main/database-base.ts +++ b/src/main/database-base.ts @@ -123,7 +123,7 @@ export class AppDatabaseBase { addComment: (input) => this.addComment(input), }); this.agents = buildAgentMethods(getDb, ra, tx, { - getAgent: (id) => this.getAgent(id), + getAgent: (id) => this.entityCrud.getAgent(id), wouldCreateAgentCycle: (agentId, reportsTo) => this.wouldCreateAgentCycle(agentId, reportsTo), recordConfigRevision: (input) => this.recordConfigRevision(input), cancelRunForDeletion: (runId, taskId, workspaceId, summary) => this.runs.cancelRunForDeletion(runId, taskId, workspaceId, summary), @@ -134,14 +134,14 @@ export class AppDatabaseBase { addActivity: (input) => this.addActivity(input), }); this.messages = buildMessageMethods(getDb, ra, { - getAgent: (id) => this.getAgent(id), + getAgent: (id) => this.entityCrud.getAgent(id), addActivity: (input) => this.addActivity(input), transaction: tx, }); this.runs = buildRunMethods(getDb, ra, tx, { logsDir: this.logsDir }); this.workspacesMod = buildWorkspaceMethods(getDb, ra, tx); this.approvalsMod = buildApprovalMethods(getDb, ra, tx, { - getAgent: (id) => this.getAgent(id), + getAgent: (id) => this.entityCrud.getAgent(id), }); this.social = buildSocialMethods(getDb, ra, tx); this.skills = buildSkillMethods(getDb, ra); @@ -270,86 +270,6 @@ export class AppDatabaseBase { getConnector(id: ConnectorId) { return this.connectorsCrud.getConnector(id); } findConnector(id: ConnectorId) { return this.connectorsCrud.findConnector(id); } - // ─── Agents ──────────────────────────────────────────────────────────────── - - saveAgent(input: { id?: string | null; companyId: string; name: string; role: string; title: string; department?: string | null; status: AgentStatus; reportsTo?: string | null; connectorId: ConnectorId; workspaceId?: string | null; model?: string | null; metadataJson?: string; capabilities: string; budgetMonthlyUsd: number; roleLevel?: import("@shared/types").AgentRoleLevel; modelStrategy?: import("@shared/types").AgentModelStrategy }) { return this.agents.saveAgent(input); } - deleteAgent(id: string, companyId: string) { return this.agents.deleteAgent(id, companyId); } - getAgent(id: string) { return this.entityCrud.getAgent(id); } - findAgent(id: string) { return this.entityCrud.findAgent(id); } - setHeartbeat(agentId: string, enabled: boolean, intervalSec: number) { return this.agents.setHeartbeat(agentId, enabled, intervalSec); } - markHeartbeatAt(agentId: string) { return this.agents.markHeartbeatAt(agentId); } - resetMonthlyBudgets(companyId: string) { return this.agents.resetMonthlyBudgets(companyId); } - listHeartbeatEligibleAgents(companyId: string) { return this.agents.listHeartbeatEligibleAgents(companyId); } - getChainOfCommand(agentId: string) { return this.agents.getChainOfCommand(agentId); } - getDirectReports(agentId: string, companyId: string) { return this.agents.getDirectReports(agentId, companyId); } - getCeoForCompany(companyId: string) { return this.agents.getCeoForCompany(companyId); } - listActiveAgentsForCompany(companyId: string) { return this.agents.listActiveAgentsForCompany(companyId); } - listAgentsForApi(companyId: string) { return this.agents.listAgentsForApi(companyId); } - listDirectReportsForStandup(companyId: string, managerId: string) { return this.agents.listDirectReportsForStandup(companyId, managerId); } - listReportStatusesForStandup(companyId: string, managerId: string) { return this.agents.listReportStatusesForStandup(companyId, managerId); } - listExistingAgentsForHireCheck(companyId: string) { return this.agents.listExistingAgentsForHireCheck(companyId); } - hasAgentAssignedTasks(agentId: string, companyId: string) { return this.agents.hasAgentAssignedTasks(agentId, companyId); } - updateAgentStatus(agentId: string, status: string) { return this.agents.updateAgentStatus(agentId, status); } - isAgentPendingApproval(agentId: string) { return this.agents.isAgentPendingApproval(agentId); } - findMentionedAgentIds(companyId: string, text: string) { return this.agents.findMentionedAgentIds(companyId, text); } - getIdleAgentsForDepartment(companyId: string, department: string) { return this.agents.getIdleAgentsForDepartment(companyId, department); } - getManagerForAgent(agentId: string) { return this.agents.getManagerForAgent(agentId); } - getAgentPermissions(agentId: string) { return this.agents.getAgentPermissions(agentId); } - setAgentPermission(agentId: string, companyId: string, permissionKey: string, enabled: boolean) { return this.agents.setAgentPermission(agentId, companyId, permissionKey, enabled); } - hasAgentPermission(agentId: string, permissionKey: string) { return this.agents.hasAgentPermission(agentId, permissionKey); } - getRuntimeState(agentId: string) { return this.agents.getRuntimeState(agentId); } - setRuntimeState(agentId: string, companyId: string, state: Record) { return this.agents.setRuntimeState(agentId, companyId, state); } - getHeartbeatPolicy(agentId: string) { return this.agents.getHeartbeatPolicy(agentId); } - setHeartbeatPolicy(agentId: string, companyId: string, policy: { enabled?: boolean; intervalSec?: number; cooldownSec?: number; maxConcurrentRuns?: number; wakeOnDemand?: boolean }) { return this.agents.setHeartbeatPolicy(agentId, companyId, policy); } - countActiveRunsForAgent(agentId: string, companyId: string) { return this.agents.countActiveRunsForAgent(agentId, companyId); } - listDepartmentAgentIds(companyId: string, department: string) { return this.agents.listDepartmentAgentIds(companyId, department); } - listCompanyBroadcastAgentIds(companyId: string) { return this.agents.listCompanyBroadcastAgentIds(companyId); } - listIncidentPingableAgentIds(companyId: string) { return this.agents.listIncidentPingableAgentIds(companyId); } - findAgentName(agentId: string) { return this.agents.findAgentName(agentId); } - listAgentNamesByIds(companyId: string, agentIds: readonly string[]) { return this.agents.listAgentNamesByIds(companyId, agentIds); } - listActiveAgentIdsFromIds(companyId: string, agentIds: readonly string[]) { return this.agents.listActiveAgentIdsFromIds(companyId, agentIds); } - - // ─── Tasks ───────────────────────────────────────────────────────────────── - - saveTask(input: { id?: string | null; companyId: string; projectId?: string | null; goalId?: string | null; parentId?: string | null; title: string; description: string; assigneeAgentId?: string | null; workspaceId?: string | null; priority: string; status: string }) { return this.tasks.saveTask(input); } - getTask(id: string) { return this.entityCrud.getTask(id); } - deleteTask(id: string, companyId: string) { return this.tasks.deleteTask(id, companyId); } - getSessionById(sessionId: string) { return this.tasks.getSessionById(sessionId); } - incrementSessionUsage(sessionId: string, inputTokens: number, outputTokens: number, runId: string) { return this.tasks.incrementSessionUsage(sessionId, inputTokens, outputTokens, runId); } - archiveSession(sessionId: string, reason: string, handoffMarkdown: string) { return this.tasks.archiveSession(sessionId, reason, handoffMarkdown); } - createNewSession(input: { companyId: string; agentId: string; adapterType: string }) { return this.tasks.createNewSession(input); } - ensureAgentSession(input: { agentId: string; companyId: string; adapterType: string }) { return this.tasks.ensureAgentSession(input); } - setAgentSessionId(agentId: string, sessionId: string) { return this.tasks.setAgentSessionId(agentId, sessionId); } - getRecentRunsForTask(taskId: string, limit: number) { return this.tasks.getRecentRunsForTask(taskId, limit); } - getRecentActivityForTask(taskId: string, limit: number) { return this.tasks.getRecentActivityForTask(taskId, limit); } - checkoutTask(taskId: string, runId: string) { return this.tasks.checkoutTask(taskId, runId); } - releaseTaskLock(taskId: string, runId: string) { return this.tasks.releaseTaskLock(taskId, runId); } - renewTaskLease(taskId: string, runId: string) { return this.tasks.renewTaskLease(taskId, runId); } - listAgentTasks(agentId: string, companyId: string, statuses: string[]) { return this.tasks.listAgentTasks(agentId, companyId, statuses); } - listTasksByStatuses(companyId: string, statuses: string[]) { return this.tasks.listTasksByStatuses(companyId, statuses); } - listAssigneeTasksForStandup(companyId: string, agentId: string) { return this.tasks.listAssigneeTasksForStandup(companyId, agentId); } - listTasksForAssignees(companyId: string, assigneeIds: string[]) { return this.tasks.listTasksForAssignees(companyId, assigneeIds); } - listReviewTasksForManager(managerId: string, companyId: string) { return this.tasks.listReviewTasksForManager(managerId, companyId); } - countActiveTasksForCompany(companyId: string) { return this.tasks.countActiveTasksForCompany(companyId); } - countAdvancingTasksForCompany(companyId: string) { return this.tasks.countAdvancingTasksForCompany(companyId); } - listChildTasks(parentId: string, companyId: string) { return this.tasks.listChildTasks(parentId, companyId); } - advanceTaskStatus(taskId: string, newStatus: string, comment?: string) { return this.tasks.advanceTaskStatus(taskId, newStatus, comment); } - setTaskRequiresUserReview(taskId: string, value: boolean) { return this.tasks.setTaskRequiresUserReview(taskId, value); } - hasPendingDeliverableReviews(companyId: string) { return this.tasks.hasPendingDeliverableReviews(companyId); } - hasAgentPendingDeliverableReview(agentId: string, companyId: string) { return this.tasks.hasAgentPendingDeliverableReview(agentId, companyId); } - hasAgentTaskAwaitingReview(agentId: string, companyId: string) { return this.tasks.hasAgentTaskAwaitingReview(agentId, companyId); } - autoAssignTask(taskId: string, companyId: string) { return this.tasks.autoAssignTask(taskId, companyId); } - getTaskRetryCount(taskId: string) { return this.tasks.getTaskRetryCount(taskId); } - incrementTaskRetryCount(taskId: string) { return this.tasks.incrementTaskRetryCount(taskId); } - resetTaskRetryCount(taskId: string) { return this.tasks.resetTaskRetryCount(taskId); } - retryInterruptedTask(taskId: string, _agentId: string, _companyId: string) { return this.tasks.retryInterruptedTask(taskId, _agentId, _companyId); } - dismissInterruptedTask(taskId: string) { return this.tasks.dismissInterruptedTask(taskId); } - markInterruptedTaskFailed(taskId: string) { return this.tasks.markInterruptedTaskFailed(taskId); } - setTaskType(taskId: string, taskType: string) { return this.tasks.setTaskType(taskId, taskType); } - listProjectTaskAssigneeIds(projectId: string, companyId: string) { return this.tasks.listProjectTaskAssigneeIds(projectId, companyId); } - listReviewTasksForSweep(companyId: string) { return this.tasks.listReviewTasksForSweep(companyId); } - getTaskType(taskId: string) { return this.tasks.getTaskType(taskId); } - // ─── Runs ────────────────────────────────────────────────────────────────── getRun(id: string) { return this.runs.getRun(id); } diff --git a/src/main/database.ts b/src/main/database.ts index 7a43018c..709543d0 100644 --- a/src/main/database.ts +++ b/src/main/database.ts @@ -77,8 +77,8 @@ export class AppDatabase extends AppDatabaseBase { countActiveGoalsForCompany(companyId: string) { return this.entityCrud.countActiveGoalsForCompany(companyId); } createHireRequest(input: { companyId: string; requestedByAgentId?: string | null; name: string; role: string; title: string; department?: string | null; reportsTo?: string | null; connectorId: ConnectorId; workspaceId?: string | null; model?: string | null; metadataJson?: string; capabilities: string; budgetMonthlyUsd: number; payloadSummary: string; impactSummary: string; payloadJson?: string | null }) { return this.entityCrud.createHireRequest(input, { - saveAgent: (agentInput) => this.saveAgent(agentInput), - requestApproval: (approvalInput) => this.requestApproval(approvalInput), + saveAgent: (agentInput) => this.agents.saveAgent(agentInput), + requestApproval: (approvalInput) => this.approvalsMod.requestApproval(approvalInput), }); } @@ -110,9 +110,9 @@ export class AppDatabase extends AppDatabaseBase { getConfigRevision(revisionId: string, companyId: string) { return this.documentsMod.getConfigRevision(revisionId, companyId); } rollbackConfigRevision(agentId: string, revisionId: string, companyId: string) { return this.documentsMod.rollbackConfigRevision(agentId, revisionId, companyId, { - getConfigRevision: (rid, cid) => this.getConfigRevision(rid, cid), - getAgent: (id) => this.getAgent(id), - recordConfigRevision: (input) => this.recordConfigRevision(input), + getConfigRevision: (rid, cid) => this.documentsMod.getConfigRevision(rid, cid), + getAgent: (id) => this.entityCrud.getAgent(id), + recordConfigRevision: (input) => this.documentsMod.recordConfigRevision(input), }); } diff --git a/src/main/document-lifecycle.ts b/src/main/document-lifecycle.ts index 46bda1c9..89a18a5c 100644 --- a/src/main/document-lifecycle.ts +++ b/src/main/document-lifecycle.ts @@ -190,7 +190,7 @@ function maybeRaiseDeliverableReviewApproval( const companySettings = deps.db.getCompanyApprovalSettings(input.companyId); if (!companySettings?.reviewDeliverables) return null; if (deps.db.getCompanyExecutionMode(input.companyId) === "ai_cofounder") return null; - if (deps.db.hasPendingDeliverableReviews(input.companyId)) return null; + if (deps.db.tasks.hasPendingDeliverableReviews(input.companyId)) return null; const approvalId = deps.db.requestApproval({ companyId: input.companyId, @@ -207,9 +207,9 @@ function maybeRaiseDeliverableReviewApproval( if (input.authorAgentId) { try { - const agent = deps.db.getAgent(input.authorAgentId); + const agent = deps.db.entityCrud.getAgent(input.authorAgentId); if (agent.heartbeatEnabled) { - deps.db.setHeartbeat(input.authorAgentId, false, agent.heartbeatIntervalSec || 120); + deps.db.agents.setHeartbeat(input.authorAgentId, false, agent.heartbeatIntervalSec || 120); } } catch (error) { deps.logger.warn( diff --git a/src/main/execution-mode-reconciler.ts b/src/main/execution-mode-reconciler.ts index 59f80ff7..2ba86f77 100644 --- a/src/main/execution-mode-reconciler.ts +++ b/src/main/execution-mode-reconciler.ts @@ -32,7 +32,7 @@ function collectImpactedAgentIdsFromWaitStates(db: AppDatabase, companyId: strin const thread = db.getThread(waitState.threadId, companyId); if (!thread || thread.relatedEntityType !== "task" || !thread.relatedEntityId) continue; try { - const task = db.getTask(thread.relatedEntityId); + const task = db.entityCrud.getTask(thread.relatedEntityId); if (task.assigneeAgentId) { agentIds.add(task.assigneeAgentId); } @@ -69,7 +69,7 @@ function collectImpactedAgentIdsFromReviewState(db: AppDatabase, companyId: stri } if (!approval.relatedTaskId) continue; try { - const task = db.getTask(approval.relatedTaskId); + const task = db.entityCrud.getTask(approval.relatedTaskId); if (task.assigneeAgentId) { agentIds.add(task.assigneeAgentId); } @@ -84,11 +84,11 @@ function collectImpactedAgentIdsFromReviewState(db: AppDatabase, companyId: stri function resumeAgents(db: AppDatabase, companyId: string, agentIds: Iterable): void { for (const agentId of agentIds) { try { - const agent = db.getAgent(agentId); + const agent = db.entityCrud.getAgent(agentId); if (agent.companyId !== companyId) continue; if (["terminated", "pending_approval"].includes(agent.status)) continue; if (!agent.heartbeatEnabled) { - db.setHeartbeat(agent.id, true, agent.heartbeatIntervalSec || 120); + db.agents.setHeartbeat(agent.id, true, agent.heartbeatIntervalSec || 120); } } catch (err) { log.warn(`[execution-mode-reconciler] resumeAgents failed for ${agentId}:`, err instanceof Error ? err.message : String(err)); @@ -156,7 +156,7 @@ export function reconcileExecutionModeChange({ companyId, ); for (const task of tasks) { - db.setTaskRequiresUserReview(task.id, false); + db.tasks.setTaskRequiresUserReview(task.id, false); } } diff --git a/src/main/hire-hook.ts b/src/main/hire-hook.ts index 3badf23e..541fbd41 100644 --- a/src/main/hire-hook.ts +++ b/src/main/hire-hook.ts @@ -36,7 +36,7 @@ export async function notifyHireApproved( try { let agent; try { - agent = db.getAgent(input.agentId); + agent = db.entityCrud.getAgent(input.agentId); } catch (err) { const classified = classifyUnknownError(err, { kind: "recoverable", diff --git a/src/main/ipc-core-agent-graph-handlers.ts b/src/main/ipc-core-agent-graph-handlers.ts index 47dac543..9601c64e 100644 --- a/src/main/ipc-core-agent-graph-handlers.ts +++ b/src/main/ipc-core-agent-graph-handlers.ts @@ -64,7 +64,7 @@ export function registerCoreAgentGraphHandlers({ "Create new agents through the hiring workflow so approval, onboarding, and org state stay synchronized.", ); } - const existingAgent = parsed.id ? db.findAgent(parsed.id) : null; + const existingAgent = parsed.id ? db.entityCrud.findAgent(parsed.id) : null; if (!existingAgent || existingAgent.companyId !== parsed.companyId) { return fail("AGENT_NOT_FOUND", "Agent not found in the current company."); } @@ -101,7 +101,7 @@ export function registerCoreAgentGraphHandlers({ } let id: string; try { - id = db.saveAgent({ + id = db.agents.saveAgent({ id: parsed.id ?? null, companyId: parsed.companyId, name: parsed.name, @@ -188,7 +188,7 @@ export function registerCoreAgentGraphHandlers({ const managerLabel = resolvedReportsTo ? (() => { try { - return db.getAgent(resolvedReportsTo).name; + return db.entityCrud.getAgent(resolvedReportsTo).name; } catch (err) { logger.warn(`[ipc/agent-graph] getAgent failed for manager ${resolvedReportsTo}:`, err instanceof Error ? err.message : String(err)); return resolvedReportsTo; @@ -271,14 +271,14 @@ export function registerCoreAgentGraphHandlers({ if (!db.belongsToCompany("agents", parsed.id, parsed.companyId)) { return fail("AGENT_NOT_FOUND", "Agent not found in the current company."); } - const agent = db.getAgent(parsed.id); + const agent = db.entityCrud.getAgent(parsed.id); if (agent.status === "pending_approval") { return fail( "AGENT_APPROVAL_REQUIRED", "Pending hires cannot be deleted from the agent editor. Reject the hire request from Hiring or Approvals instead.", ); } - db.deleteAgent(parsed.id, parsed.companyId); + db.agents.deleteAgent(parsed.id, parsed.companyId); db.addActivity({ companyId: parsed.companyId, actor: "board", diff --git a/src/main/ipc-core-operations-handlers.ts b/src/main/ipc-core-operations-handlers.ts index 9e7b3f47..8761186f 100644 --- a/src/main/ipc-core-operations-handlers.ts +++ b/src/main/ipc-core-operations-handlers.ts @@ -189,7 +189,7 @@ export function registerCoreOperationHandlers({ emitEvent({ type: "agent-message", taskId: parsed.taskId, comment }); emitEvent({ type: "task-updated", taskId: parsed.taskId, action: "updated" }); try { - const task = db.getTask(parsed.taskId); + const task = db.entityCrud.getTask(parsed.taskId); if (task.assigneeAgentId) { wakeAgentIfPossible(task.assigneeAgentId, task.companyId, "comment"); } @@ -211,7 +211,7 @@ export function registerCoreOperationHandlers({ 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.agents.setHeartbeat(parsed.agentId, parsed.enabled, parsed.intervalSec); db.addActivity({ companyId: parsed.companyId, actor: "board", diff --git a/src/main/ipc-core-read-handlers.ts b/src/main/ipc-core-read-handlers.ts index a036c92c..f2ba01fb 100644 --- a/src/main/ipc-core-read-handlers.ts +++ b/src/main/ipc-core-read-handlers.ts @@ -166,7 +166,7 @@ export function registerCoreReadHandlers({ const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; try { - const task = db.getTask(parsed.id); + const task = db.entityCrud.getTask(parsed.id); if (task.companyId !== parsed.companyId) return fail("TASK_NOT_FOUND", "Task not found in current company."); return ok(task); } catch (err) { @@ -180,7 +180,7 @@ export function registerCoreReadHandlers({ const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; try { - const agent = db.getAgent(parsed.id); + const agent = db.entityCrud.getAgent(parsed.id); if (agent.companyId !== parsed.companyId) return fail("AGENT_NOT_FOUND", "Agent not found in current company."); return ok(agent); } catch (err) { diff --git a/src/main/ipc-core-task-graph-handlers.ts b/src/main/ipc-core-task-graph-handlers.ts index 06eb41d5..fdeb088a 100644 --- a/src/main/ipc-core-task-graph-handlers.ts +++ b/src/main/ipc-core-task-graph-handlers.ts @@ -113,7 +113,7 @@ export function registerCoreTaskGraphHandlers({ const taskId = parsed.id; if (!taskId) return fail("INVALID_TASK_STATE", "Task ID required for updates."); try { - const existingTask = db.getTask(taskId); + const existingTask = db.entityCrud.getTask(taskId); previousStatus = existingTask.status; previousAssigneeAgentId = existingTask.assigneeAgentId; } catch (err) { @@ -124,7 +124,7 @@ export function registerCoreTaskGraphHandlers({ } let id: string; try { - id = db.saveTask(parsed); + id = db.tasks.saveTask(parsed); } catch (error) { return fail("INVALID_TASK_STATE", error instanceof Error ? error.message : "Task could not be saved."); } @@ -145,7 +145,7 @@ export function registerCoreTaskGraphHandlers({ // User (board) manually advancing a flagged task to done: clear the review flag if (parsed.status === "done") { try { - db.setTaskRequiresUserReview(id, false); + db.tasks.setTaskRequiresUserReview(id, false); } catch (error) { logger.warn( `[ipc-task] setTaskRequiresUserReview(false) failed for task ${id}: ${error instanceof Error ? error.message : String(error)}`, @@ -175,7 +175,7 @@ export function registerCoreTaskGraphHandlers({ }); const deleteHandlers = [ - { channel: IPC_CHANNELS.deleteTask, table: "tasks", entity: "task", deleteFn: (id: string, companyId: string) => db.deleteTask(id, companyId) }, + { channel: IPC_CHANNELS.deleteTask, table: "tasks", entity: "task", deleteFn: (id: string, companyId: string) => db.tasks.deleteTask(id, companyId) }, { channel: IPC_CHANNELS.deleteGoal, table: "goals", entity: "goal", deleteFn: (id: string, companyId: string) => db.deleteGoal(id, companyId) }, { channel: IPC_CHANNELS.deleteProject, table: "projects", entity: "project", deleteFn: (id: string, companyId: string) => db.deleteProject(id, companyId) }, { channel: IPC_CHANNELS.deleteWorkspace, table: "workspaces", entity: "workspace", deleteFn: (id: string, companyId: string) => db.deleteWorkspace(id, companyId) }, diff --git a/src/main/ipc-recovery-handlers.ts b/src/main/ipc-recovery-handlers.ts index 14574d0d..5012e814 100644 --- a/src/main/ipc-recovery-handlers.ts +++ b/src/main/ipc-recovery-handlers.ts @@ -36,7 +36,7 @@ export function registerRecoveryHandlers({ if (!db.belongsToCompany("tasks", parsed.taskId, parsed.companyId)) { return fail("TASK_NOT_FOUND", "Task not found in the current company."); } - db.retryInterruptedTask(parsed.taskId, parsed.agentId, parsed.companyId); + db.tasks.retryInterruptedTask(parsed.taskId, parsed.agentId, parsed.companyId); try { wakeAgentIfPossible(parsed.agentId, parsed.companyId, "retry_interrupted"); } catch (err) { @@ -53,7 +53,7 @@ export function registerRecoveryHandlers({ if (!db.belongsToCompany("tasks", parsed.taskId, parsed.companyId)) { return fail("TASK_NOT_FOUND", "Task not found in the current company."); } - db.markInterruptedTaskFailed(parsed.taskId); + db.tasks.markInterruptedTaskFailed(parsed.taskId); publishDomainChanged(); return ok(null); }); diff --git a/src/main/messaging/dispatch.ts b/src/main/messaging/dispatch.ts index 7b585bde..9df282b8 100644 --- a/src/main/messaging/dispatch.ts +++ b/src/main/messaging/dispatch.ts @@ -204,14 +204,14 @@ function resolveProjectWakeTargets( const project = db.getProjectForUpdate(input.channelTargetId, input.companyId); const participantIds = new Set(); if (project?.leadAgentId) participantIds.add(project.leadAgentId); - for (const assigneeId of db.listProjectTaskAssigneeIds(input.channelTargetId, input.companyId)) { + for (const assigneeId of db.tasks.listProjectTaskAssigneeIds(input.channelTargetId, input.companyId)) { participantIds.add(assigneeId); } if (participantIds.size === 0) return []; const ids = Array.from(participantIds).filter((id) => id !== input.fromAgentId); if (ids.length === 0) return []; - return db.listActiveAgentIdsFromIds(input.companyId, ids); + return db.agents.listActiveAgentIdsFromIds(input.companyId, ids); } /** Resolve the agents mentioned in a message body, stripped of the @@ -222,7 +222,7 @@ export function resolveMentionedAgentIds( db: AppDatabase, input: Pick, ): string[] { - const mentioned = db.findMentionedAgentIds(input.companyId, input.body); + const mentioned = db.agents.findMentionedAgentIds(input.companyId, input.body); const filtered: string[] = []; for (const agentId of mentioned) { if (!agentId || agentId === input.fromAgentId || agentId === HUMAN_SENDER_ID) continue; @@ -255,7 +255,7 @@ export function resolveMessageWakeTargets( } if (input.channel === "department" && input.channelTargetId) { - for (const agentId of db.listDepartmentAgentIds(input.companyId, input.channelTargetId)) { + for (const agentId of db.agents.listDepartmentAgentIds(input.companyId, input.channelTargetId)) { if (agentId !== input.fromAgentId) wakeTargets.add(agentId); } } @@ -265,7 +265,7 @@ export function resolveMessageWakeTargets( // workspace is a listener. Board-only fan-outs use the dedicated // `incident` channel; the previous top-level-only fan-out made the // "company" label a misnomer that silently dropped ICs. - for (const agentId of db.listCompanyBroadcastAgentIds(input.companyId)) { + for (const agentId of db.agents.listCompanyBroadcastAgentIds(input.companyId)) { if (agentId !== input.fromAgentId) wakeTargets.add(agentId); } } @@ -277,7 +277,7 @@ export function resolveMessageWakeTargets( } if (input.channel === "incident") { - for (const agentId of db.listIncidentPingableAgentIds(input.companyId)) { + for (const agentId of db.agents.listIncidentPingableAgentIds(input.companyId)) { if (agentId !== input.fromAgentId) wakeTargets.add(agentId); } } @@ -288,7 +288,7 @@ export function resolveMessageWakeTargets( // The human participant (`__board__`) is a reader, not a wake-able // agent: it has no runtime, no heartbeat, no DB row. Leaving it in the - // set makes `wakeAgentIfPossible` call `db.getAgent("__board__")`, + // set makes `wakeAgentIfPossible` call `db.entityCrud.getAgent("__board__")`, // which throws "Agent not found" and spams the log. Filter once, at // the point of resolution, so every downstream consumer is clean. wakeTargets.delete(HUMAN_SENDER_ID); @@ -330,7 +330,7 @@ export function dispatchAgentMessage( !normalized.fromHumanUser && (message.channel !== "direct" || message.toAgentId === HUMAN_SENDER_ID) ) { - const senderName = deps.db.findAgentName(message.fromAgentId) ?? "Agent"; + const senderName = deps.db.agents.findAgentName(message.fromAgentId) ?? "Agent"; deps.notify({ title: truncateForNotification(`${senderName} · ${message.subject}`, 80), body: truncateForNotification(message.body, 140), @@ -350,7 +350,7 @@ export function dispatchAgentMessage( const mentionResults: MentionResult[] = []; if (deps.wakeAgentIfPossible) { - const agentNameRows = deps.db.listAgentNamesByIds(normalized.companyId, wakeTargets); + const agentNameRows = deps.db.agents.listAgentNamesByIds(normalized.companyId, wakeTargets); const agentNameMap = new Map(agentNameRows.map((a) => [a.id, a.name])); for (const wakeTarget of wakeTargets) { const agentName = agentNameMap.get(wakeTarget) ?? wakeTarget; diff --git a/src/main/onboarding-service.ts b/src/main/onboarding-service.ts index 94e3bc06..eb44048b 100644 --- a/src/main/onboarding-service.ts +++ b/src/main/onboarding-service.ts @@ -85,9 +85,9 @@ export function createOnboardingService({ }: OnboardingDependencies) { function updateAgentWorkspaceBinding(agentId: string, workspaceId: string | null) { if (!workspaceId) return; - const agent = db.getAgent(agentId); + const agent = db.entityCrud.getAgent(agentId); if (agent.workspaceId === workspaceId) return; - db.saveAgent({ + db.agents.saveAgent({ id: agent.id, companyId: agent.companyId, name: agent.name, @@ -187,7 +187,7 @@ export function createOnboardingService({ `Review the current company state, create the first real tasks, delegate initial work, and report progress through tasks, approvals, and messages.`, ].join("\n\n"); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, projectId, goalId: activeGoal?.id ?? null, @@ -260,7 +260,7 @@ export function createOnboardingService({ } const ceoLocale = getLocale(); - const ceoId = db.saveAgent({ + const ceoId = db.agents.saveAgent({ companyId, name: "CEO", role: "CEO", @@ -279,7 +279,7 @@ export function createOnboardingService({ modelStrategy: "explicit", roleLevel: "c_suite", }); - db.setHeartbeat(ceoId, true, 120); + db.agents.setHeartbeat(ceoId, true, 120); db.setCurrentCompany(companyId); seedDefaultAutomationRules(companyId); diff --git a/src/main/orchestration/heartbeat.ts b/src/main/orchestration/heartbeat.ts index d5f146ba..1ddd9e61 100644 --- a/src/main/orchestration/heartbeat.ts +++ b/src/main/orchestration/heartbeat.ts @@ -55,9 +55,9 @@ export function createHeartbeat( } = deps; function ensureAgentHeartbeatEnabled(agentId: string) { - const agent = db.getAgent(agentId); + const agent = db.entityCrud.getAgent(agentId); if (!agent.heartbeatEnabled) { - db.setHeartbeat(agentId, true, agent.heartbeatIntervalSec || 120); + db.agents.setHeartbeat(agentId, true, agent.heartbeatIntervalSec || 120); } } @@ -68,7 +68,7 @@ export function createHeartbeat( if (!hasWorkerReady()) return SKIPPED("worker_not_ready"); const companyStatus = db.getCompanyStatus(companyId); if (companyStatus !== "active") return SKIPPED("company_inactive"); - const agent = db.getAgent(agentId); + const agent = db.entityCrud.getAgent(agentId); if (agent.companyId !== companyId) { logger.warn(`[heartbeat] Refusing cross-company run for agent ${agentId}: ${agent.companyId} !== ${companyId}`); return SKIPPED("cross_company"); @@ -112,10 +112,10 @@ export function createHeartbeat( // Let getHeartbeatPolicy throw — a missing row signals data corruption // and silently falling back hides the gate that decides whether the // agent runs at all. - const heartbeatPolicy = db.getHeartbeatPolicy(agent.id); + const heartbeatPolicy = db.agents.getHeartbeatPolicy(agent.id); // maxConcurrentRuns check: count active runs for this agent - const activeRunCount = db.countActiveRunsForAgent(agent.id, companyId); + const activeRunCount = db.agents.countActiveRunsForAgent(agent.id, companyId); if (activeRunCount >= heartbeatPolicy.maxConcurrentRuns) return SKIPPED("max_concurrent_runs"); // Cooldown check: ensure enough time has passed since last heartbeat @@ -129,7 +129,7 @@ export function createHeartbeat( if (!heartbeatPolicy.wakeOnDemand && trigger !== "timer" && trigger !== "bootstrap") { return SKIPPED("wake_on_demand_disabled"); } - if (db.isAgentPendingApproval(agentId)) return SKIPPED("pending_approval"); + if (db.agents.isAgentPendingApproval(agentId)) return SKIPPED("pending_approval"); const companyMode = db.getCompanyExecutionMode(companyId); const approvalSettings = db.getCompanyApprovalSettings(companyId); @@ -140,7 +140,7 @@ export function createHeartbeat( if ( shouldEnforceHumanReview && !isMessageWake && - (db.hasAgentPendingDeliverableReview(agentId, companyId) || db.hasAgentTaskAwaitingReview(agentId, companyId)) + (db.tasks.hasAgentPendingDeliverableReview(agentId, companyId) || db.tasks.hasAgentTaskAwaitingReview(agentId, companyId)) ) { return SKIPPED("awaiting_user_review"); } @@ -150,23 +150,23 @@ export function createHeartbeat( shouldEnforceHumanReview && !isMessageWake && !agent.reportsTo && - db.hasPendingDeliverableReviews(companyId) + db.tasks.hasPendingDeliverableReviews(companyId) ) { return SKIPPED("pending_deliverable_review"); } const assignedTasks = isCommunicationWake ? [] - : db.listAgentTasks(agentId, companyId, ["todo", "in_progress", "blocked"]); + : db.tasks.listAgentTasks(agentId, companyId, ["todo", "in_progress", "blocked"]); const isTimer = trigger === "timer"; const candidateTasks = assignedTasks; const candidateTask = candidateTasks.find((entry) => !entry.activeRunId) ?? null; const runId = randomUUID(); - let task: ReturnType | null = null; + let task: ReturnType | null = null; if (candidateTask) { - const checkoutSuccess = db.checkoutTask(candidateTask.id, runId); + const checkoutSuccess = db.tasks.checkoutTask(candidateTask.id, runId); if (!checkoutSuccess) { db.addActivity({ companyId, @@ -178,7 +178,7 @@ export function createHeartbeat( }); return SKIPPED("checkout_race_lost"); } - task = db.getTask(candidateTask.id); + task = db.entityCrud.getTask(candidateTask.id); } else { const hasActiveAssignedRun = candidateTasks.some( (entry) => entry.activeRunId && hasActiveRun({ taskId: entry.id, companyId }), @@ -186,9 +186,9 @@ export function createHeartbeat( if (hasActiveAssignedRun) return SKIPPED("max_concurrent_runs"); const isCeo = !agent.reportsTo; - const isManager = db.getDirectReports(agent.id, companyId).length > 0; + const isManager = db.agents.getDirectReports(agent.id, companyId).length > 0; const goalCount = db.countActiveGoalsForCompany(companyId); - const taskCount = db.countActiveTasksForCompany(companyId); + const taskCount = db.tasks.countActiveTasksForCompany(companyId); const companyNeedsBootstrap = goalCount > 0 && taskCount < goalCount; if (isTimer && !isCeo && !isManager && !companyNeedsBootstrap) { @@ -232,7 +232,7 @@ export function createHeartbeat( resolvedModel: assignment.modelId, }); - db.markHeartbeatAt(agentId); + db.agents.markHeartbeatAt(agentId); // Actor audit: record what triggered this run try { @@ -266,7 +266,7 @@ export function createHeartbeat( // raw) from spamming the log with "Agent not found: __board__". if (agentId === HUMAN_SENDER_ID) return null; try { - const agent = db.getAgent(agentId); + const agent = db.entityCrud.getAgent(agentId); if (agent.companyId !== companyId) { logger.warn(`[wake] Refusing cross-company wake for agent ${agentId}: ${agent.companyId} !== ${companyId}`); return null; diff --git a/src/main/orchestration/initial-user-turn.ts b/src/main/orchestration/initial-user-turn.ts index e9e07806..e51f163c 100644 --- a/src/main/orchestration/initial-user-turn.ts +++ b/src/main/orchestration/initial-user-turn.ts @@ -30,7 +30,7 @@ const SUBJECT_PREVIEW_LEN = 80; export function resolveInitialUserTurn( params: { trigger: string | null; - task: ReturnType | null; + task: ReturnType | null; companyId: string; agentId: string; }, @@ -61,9 +61,9 @@ export function resolveInitialUserTurn( // mode prompt. if (trigger === "timer" || trigger === "bootstrap" || trigger === "app_restart_resume") { try { - const agent = deps.db.getAgent(agentId); + const agent = deps.db.entityCrud.getAgent(agentId); if (!agent.reportsTo) { - const activeReports = deps.db + const activeReports = deps.db.agents .getDirectReports(agentId, companyId) .filter((r) => r.status !== "pending_approval" && r.status !== "terminated"); if (activeReports.length === 0) { @@ -145,7 +145,7 @@ function buildGenericWakeTurn(trigger: string | null): string { function resolveAgentLabel(db: AppDatabase, agentId: string): string { try { - const name = db.findAgentName(agentId); + const name = db.agents.findAgentName(agentId); if (name && name.length > 0) return name; } catch { // fall through diff --git a/src/main/orchestration/scheduler-heartbeat.ts b/src/main/orchestration/scheduler-heartbeat.ts index 02a5a342..c89bdcc5 100644 --- a/src/main/orchestration/scheduler-heartbeat.ts +++ b/src/main/orchestration/scheduler-heartbeat.ts @@ -23,8 +23,8 @@ export function createHeartbeatScheduler( function persistSkip(agentId: string, companyId: string, reason: string) { try { - const existing = db.getRuntimeState(agentId) as Record; - db.setRuntimeState(agentId, companyId, { + const existing = db.agents.getRuntimeState(agentId) as Record; + db.agents.setRuntimeState(agentId, companyId, { ...existing, heartbeatSkip: { reason, at: new Date().toISOString() }, }); @@ -37,11 +37,11 @@ export function createHeartbeatScheduler( function clearSkip(agentId: string, companyId: string) { try { - const existing = db.getRuntimeState(agentId) as Record; + const existing = db.agents.getRuntimeState(agentId) as Record; if (!("heartbeatSkip" in existing)) return; const next = { ...existing }; delete next.heartbeatSkip; - db.setRuntimeState(agentId, companyId, next); + db.agents.setRuntimeState(agentId, companyId, next); } catch (error) { logger.warn( `[heartbeat] Failed to clear skip reason for agent ${agentId}: ${error instanceof Error ? error.message : String(error)}`, @@ -72,7 +72,7 @@ export function createHeartbeatScheduler( for (const company of activeCompanies) { try { - const eligibleAgents = db.listHeartbeatEligibleAgents(company.id); + const eligibleAgents = db.agents.listHeartbeatEligibleAgents(company.id); for (const agent of eligibleAgents) { if (hasActiveRun({ agentId: agent.id, companyId: company.id })) continue; @@ -103,9 +103,9 @@ export function createHeartbeatScheduler( // regular timer interval. countAdvancingTasksForCompany // counts only todo + in_progress so bootstrapping still // fires once the team stalls. - const advancingCount = db.countAdvancingTasksForCompany(company.id); + const advancingCount = db.tasks.countAdvancingTasksForCompany(company.id); if (goalCount > 0 && advancingCount === 0) { - const ceo = db.getCeoForCompany(company.id); + const ceo = db.agents.getCeoForCompany(company.id); const ceoLastWake = ceo?.lastHeartbeatAt ? new Date(ceo.lastHeartbeatAt).getTime() : 0; const ceoLastAttempt = ceo ? attemptedAtByAgent.get(ceo.id) ?? 0 : 0; const ceoLastTouch = Math.max(ceoLastWake, ceoLastAttempt); diff --git a/src/main/orchestration/scheduler-housekeeping.ts b/src/main/orchestration/scheduler-housekeeping.ts index 8c65bbed..47000f99 100644 --- a/src/main/orchestration/scheduler-housekeeping.ts +++ b/src/main/orchestration/scheduler-housekeeping.ts @@ -34,7 +34,7 @@ export function createHousekeepingScheduler( const currentMonth = new Date().toISOString().slice(0, 7); const lastResetMonth = db.getLastBudgetResetMonth(company.id) ?? ""; if (currentMonth !== lastResetMonth) { - db.resetMonthlyBudgets(company.id); + db.agents.resetMonthlyBudgets(company.id); db.setLastBudgetResetMonth(company.id, currentMonth); logger.info(`[housekeeping] Reset monthly budgets for company ${company.name} (${currentMonth})`); } diff --git a/src/main/orchestration/scheduler-idle-automation.ts b/src/main/orchestration/scheduler-idle-automation.ts index e58bc279..92b2af43 100644 --- a/src/main/orchestration/scheduler-idle-automation.ts +++ b/src/main/orchestration/scheduler-idle-automation.ts @@ -17,9 +17,9 @@ export function createIdleAutomationScheduler(deps: OrchestrationDependencies) { for (const company of activeCompanies) { try { - const companyAgents = db.listActiveAgentsForCompany(company.id); + const companyAgents = db.agents.listActiveAgentsForCompany(company.id); for (const agent of companyAgents) { - if (!db.hasAgentAssignedTasks(agent.id, company.id)) { + if (!db.agents.hasAgentAssignedTasks(agent.id, company.id)) { const lastActive = agent.lastHeartbeatAt ? new Date(agent.lastHeartbeatAt).getTime() : 0; const idleThreshold = 20 * 60 * 1000; if (now - lastActive > idleThreshold) { diff --git a/src/main/orchestration/task-runner.ts b/src/main/orchestration/task-runner.ts index df59db89..f99d3bf7 100644 --- a/src/main/orchestration/task-runner.ts +++ b/src/main/orchestration/task-runner.ts @@ -37,13 +37,13 @@ export function createTaskRunner( logger.warn(`[run] Worker not ready — cannot queue run for task ${taskId} (trigger: ${trigger})`); return fail("WORKER_UNAVAILABLE", "Orchestration worker is not ready."); } - const task = db.getTask(taskId); + const task = db.entityCrud.getTask(taskId); const companyStatus = db.getCompanyStatus(task.companyId); if (companyStatus !== "active") { return fail("COMPANY_NOT_ACTIVE", "Runs can only start while the company is active."); } if (!task.assigneeAgentId) return fail("TASK_UNASSIGNED", "Assign an agent before starting a run."); - const agent = db.getAgent(task.assigneeAgentId); + const agent = db.entityCrud.getAgent(task.assigneeAgentId); if (agent.companyId !== task.companyId) { return fail("AGENT_COMPANY_MISMATCH", "The assigned agent does not belong to the task company."); } @@ -75,7 +75,7 @@ export function createTaskRunner( if (hasActiveRun({ agentId: agent.id, companyId: task.companyId })) { return fail("AGENT_ALREADY_RUNNING", "The assigned agent already has an active run."); } - if (db.isAgentPendingApproval(agent.id)) { + if (db.agents.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); @@ -88,7 +88,7 @@ export function createTaskRunner( // CAS checkout — atomic claim (EXEC-01, EXEC-02) const runId = randomUUID(); - const checkoutSuccess = db.checkoutTask(task.id, runId); + const checkoutSuccess = db.tasks.checkoutTask(task.id, runId); if (!checkoutSuccess) { return fail("CHECKOUT_RACE_LOST", "Another run claimed this task. Refresh and try again."); } @@ -180,8 +180,8 @@ export function createTaskRunner( } try { - const task = run.taskId ? db.getTask(run.taskId) : null; - const agent = db.getAgent(run.agentId); + const task = run.taskId ? db.entityCrud.getTask(run.taskId) : null; + const agent = db.entityCrud.getAgent(run.agentId); const connector = db.getConnector(run.connectorId); const eligibilityFailure = getRunEligibilityFailure(agent, connector, task?.projectId ?? null); if (eligibilityFailure) { diff --git a/src/main/orchestration/turn-context.ts b/src/main/orchestration/turn-context.ts index 8a8f14e4..af44cb10 100644 --- a/src/main/orchestration/turn-context.ts +++ b/src/main/orchestration/turn-context.ts @@ -18,11 +18,11 @@ import { resolveInitialUserTurn } from "./initial-user-turn"; export interface RunDispatchParams { runId: string; companyId: string; - agent: ReturnType; + agent: ReturnType; // `task` is null for heartbeat runs (agent wake-ups with no assigned task). // Downstream code (prompt builder, worker payload, session pointer) // branches on this — when null, skip task-scoped state. - task: ReturnType | null; + task: ReturnType | null; connector: ReturnType; workspace: ReturnType; workspaceMode: WorkspaceLockMode; @@ -81,8 +81,8 @@ function assembleApiEnv( } function assembleRecentComments( - assignedTasks: ReturnType, - reviewTasks: ReturnType, + assignedTasks: ReturnType, + reviewTasks: ReturnType, snapshot: RunDispatchParams["snapshot"], ) { const relevantTaskIds = new Set([ @@ -150,7 +150,7 @@ function resolveSession( // Per-agent session: one ongoing transcript per agent; switching tasks // (or running taskless heartbeats) becomes a "now work on X" user turn // on the ongoing transcript rather than starting a fresh session. - const agentSessionId = db.ensureAgentSession({ + const agentSessionId = db.tasks.ensureAgentSession({ agentId: agent.id, companyId, adapterType: connectorId, @@ -273,8 +273,8 @@ export function buildRunPayload(params: RunDispatchParams, deps: TurnContextDeps const { env, apiUrl, apiKey } = assembleApiEnv(db, connector, companyId, agent.id, runId, trigger, deps); env.AGENT_COMPANY_TASK_ID = task?.id ?? ""; - const assignedTasks = db.listAgentTasks(agent.id, companyId, ["todo", "in_progress", "blocked"]); - const reviewTasks = db.listReviewTasksForManager(agent.id, companyId); + const assignedTasks = db.tasks.listAgentTasks(agent.id, companyId, ["todo", "in_progress", "blocked"]); + const reviewTasks = db.tasks.listReviewTasksForManager(agent.id, companyId); const recentComments = assembleRecentComments(assignedTasks, reviewTasks, snapshot); const initialHandoff = resolveSessionHandoff(agent.id, deps); const { taskSessionId, sessionHandoff } = resolveSession(db, agent, companyId, connector.id, initialHandoff, deps); @@ -291,8 +291,8 @@ export function buildRunPayload(params: RunDispatchParams, deps: TurnContextDeps executionMode: db.getCompanyExecutionMode(companyId) as ExecutionMode, goals: snapshot.goals.filter((goal) => goal.companyId === companyId), projects: snapshot.projects.filter((project) => project.companyId === companyId), - directReports: db.getDirectReports(agent.id, companyId), - chainOfCommand: db.getChainOfCommand(agent.id), + directReports: db.agents.getDirectReports(agent.id, companyId), + chainOfCommand: db.agents.getChainOfCommand(agent.id), assignedTasks, allAgents: snapshot.agents.filter((entry) => entry.companyId === companyId), pendingApprovals: filterPendingApprovals(snapshot.approvals, companyId, agent.id), diff --git a/src/main/run-housekeeping.ts b/src/main/run-housekeeping.ts index fe73ce56..dcc3ee70 100644 --- a/src/main/run-housekeeping.ts +++ b/src/main/run-housekeeping.ts @@ -54,7 +54,7 @@ function shouldProduceKnowledge( ); } try { - const agent = db.getAgent(agentId); + const agent = db.entityCrud.getAgent(agentId); return Boolean(agent.department && KNOWLEDGE_PRODUCING_DEPARTMENTS.has(agent.department)); } catch (error) { logger.warn( @@ -97,7 +97,7 @@ export function createRunHousekeeping({ db, logger, threadIntegration }: RunHous try { let topic = `Run ${runId.slice(0, 8)}`; try { - const task = db.getTask(taskId); + const task = db.entityCrud.getTask(taskId); topic = task.title; } catch (error) { logger.warn( diff --git a/src/main/service-graph/create-orchestration-service.ts b/src/main/service-graph/create-orchestration-service.ts index 5e5b518c..728677f7 100644 --- a/src/main/service-graph/create-orchestration-service.ts +++ b/src/main/service-graph/create-orchestration-service.ts @@ -93,9 +93,9 @@ export function buildOrchestrationService({ }, evaluatePreRunSessionRotation: (agentId, connectorId) => { try { - const agent = db.getAgent(agentId); + const agent = db.entityCrud.getAgent(agentId); if (!agent.sessionId) return { rotated: false, handoff: null, sessionId: null }; - const session = db.getSessionById(agent.sessionId); + const session = db.tasks.getSessionById(agent.sessionId); if (!session) return { rotated: false, handoff: null, sessionId: agent.sessionId }; const result = sessionService.evaluateRotation(session); if (result.shouldRotate) { diff --git a/src/main/session-management-service.ts b/src/main/session-management-service.ts index 45e24af9..ea84780c 100644 --- a/src/main/session-management-service.ts +++ b/src/main/session-management-service.ts @@ -25,7 +25,7 @@ export function createSessionManagementService({ db, logger }: SessionManagement // D-08: Accumulate token usage on task_sessions row (called for ALL adapters per D-05) function accumulateTokenUsage(sessionId: string, inputTokens: number, outputTokens: number, runId: string): void { - db.incrementSessionUsage(sessionId, inputTokens, outputTokens, runId); + db.tasks.incrementSessionUsage(sessionId, inputTokens, outputTokens, runId); } // Any-of thresholds — first trigger wins. Pi-mono delegates context @@ -133,15 +133,15 @@ export function createSessionManagementService({ db, logger }: SessionManagement logger.info(`[session] Rotating session ${session.id} for agent ${session.agentId}: ${reason}`); - db.archiveSession(session.id, reason, handoff); + db.tasks.archiveSession(session.id, reason, handoff); - const newSessionId = db.createNewSession({ + const newSessionId = db.tasks.createNewSession({ companyId: session.companyId, agentId: session.agentId, adapterType: session.adapterType, }); - db.setAgentSessionId(session.agentId, newSessionId); + db.tasks.setAgentSessionId(session.agentId, newSessionId); logger.info(`[session] New session ${newSessionId} created for agent ${session.agentId}`); return newSessionId; diff --git a/src/main/task-quality-gates.ts b/src/main/task-quality-gates.ts index 3e671258..0f8ebf01 100644 --- a/src/main/task-quality-gates.ts +++ b/src/main/task-quality-gates.ts @@ -25,7 +25,9 @@ interface CapabilityMatchResult { } interface CapabilityMatchDb { - getAgent(id: string): { name: string; capabilities: string }; + entityCrud: { + getAgent(id: string): { name: string; capabilities: string }; + }; listAgentSkillAssignments(id: string): string[]; getCompanySkill(companyId: string, skillId: string): { name: string; description: string | null; slug: string } | null; } @@ -128,7 +130,7 @@ export function checkCapabilityMatch( return { matched: true, warning: "Task description has no distinctive keywords for capability matching." }; } - const agent = db.getAgent(agentId); + const agent = db.entityCrud.getAgent(agentId); const capWords = tokenize(agent.capabilities); const capabilityOverlap = taskWords.filter((w) => capWords.includes(w)).length; @@ -197,7 +199,7 @@ export function sweepStaleReviews( const isCofounderMode = deps.getCompanyExecutionMode(companyId) === "ai_cofounder"; let escalatedCount = 0; - const reviewTasks = deps.db.listReviewTasksForSweep(companyId); + const reviewTasks = deps.db.tasks.listReviewTasksForSweep(companyId); for (const task of reviewTasks) { const reviewAge = now - new Date(task.updatedAt).getTime(); @@ -205,11 +207,11 @@ export function sweepStaleReviews( if (!task.assigneeAgentId) continue; // The reviewer is the assignee's direct manager - const reviewer = deps.db.getManagerForAgent(task.assigneeAgentId); + const reviewer = deps.db.agents.getManagerForAgent(task.assigneeAgentId); if (!reviewer) continue; // No manager = CEO-assigned task, skip escalation // D-09: Escalate to the reviewer's manager (manager's manager) - const chain = deps.db.getChainOfCommand(reviewer.id); + const chain = deps.db.agents.getChainOfCommand(reviewer.id); const escalationTarget = chain.length > 0 ? chain[0] : null; const pendingApproval = deps.db.findPendingApprovalForTask(companyId, task.id); @@ -281,7 +283,7 @@ export function sweepStaleReviews( // D-10: Advance task status from in_review to done try { - deps.db.advanceTaskStatus(task.id, "done", "Auto-advanced: review timeout exceeded 30 minutes."); + deps.db.tasks.advanceTaskStatus(task.id, "done", "Auto-advanced: review timeout exceeded 30 minutes."); deps.handleTaskStatusChange(task.id, companyId, "in_review", "done"); } catch (err) { log.warn(`[task-quality-gates] advanceTaskStatus failed for task ${task.id}:`, err instanceof Error ? err.message : String(err)); diff --git a/src/main/thread-integration/index.ts b/src/main/thread-integration/index.ts index 897b706b..83ed9816 100644 --- a/src/main/thread-integration/index.ts +++ b/src/main/thread-integration/index.ts @@ -20,7 +20,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh */ onTaskCreated(taskId: string, companyId: string): void { try { - const task = db.getTask(taskId); + const task = db.entityCrud.getTask(taskId); if (task.companyId !== companyId) return; const thread = ensureTaskThread(db, taskId, companyId, task.title); @@ -53,7 +53,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh */ onTaskStatusChanged(taskId: string, companyId: string, previousStatus: string, newStatus: string): void { try { - const task = db.getTask(taskId); + const task = db.entityCrud.getTask(taskId); if (task.companyId !== companyId) return; const thread = ensureTaskThread(db, taskId, companyId, task.title); @@ -105,7 +105,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh onApprovalCreated(approvalId: string, companyId: string, taskId: string | null, approvalType: string, summary: string): void { try { const approval = db.getApprovalRecord(approvalId); - const task = taskId ? db.getTask(taskId) : null; + const task = taskId ? db.entityCrud.getTask(taskId) : null; const thread = task ? ensureTaskThread(db, task.id, companyId, task.title) : ensureApprovalThread(db, approvalId, companyId, summary || approval.payloadSummary); @@ -181,7 +181,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh try { if (!taskId) return; - db.getTask(taskId); // validates task exists and belongs to company + db.entityCrud.getTask(taskId); // validates task exists and belongs to company const thread = findTaskThread(db, taskId, companyId); if (!thread) return; @@ -249,7 +249,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh if (status === "succeeded") { const mode = db.getCompanyExecutionMode(companyId) as ExecutionMode; if (mode === "ai_cofounder") { - const task = db.getTask(taskId); + const task = db.entityCrud.getTask(taskId); const completionReason = `@Board review needed: Agent completed work on "${task.title}" — awaiting your review.`; db.setThreadWaitState({ threadId: thread.id, @@ -304,7 +304,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh const thread = db.getThread(ws.threadId, companyId); if (!thread || thread.relatedEntityType !== "task" || !thread.relatedEntityId) continue; try { - const task = db.getTask(thread.relatedEntityId); + const task = db.entityCrud.getTask(thread.relatedEntityId); if (task.assigneeAgentId !== toAgentId) continue; } catch (err) { logger.warn(`[thread-integration] getTask failed for thread ${ws.threadId}:`, err instanceof Error ? err.message : String(err)); diff --git a/src/main/thread-integration/thread-helpers.ts b/src/main/thread-integration/thread-helpers.ts index efdb2763..95ef9d15 100644 --- a/src/main/thread-integration/thread-helpers.ts +++ b/src/main/thread-integration/thread-helpers.ts @@ -104,7 +104,7 @@ function resolveApprovalAnnouncementChannel( ): { channel: "department" | "project" | "company"; channelTargetId: string | null } | null { if (requestedByAgentId) { try { - const requester = db.getAgent(requestedByAgentId); + const requester = db.entityCrud.getAgent(requestedByAgentId); if (requester.companyId === companyId && requester.department) { return { channel: "department", channelTargetId: requester.department }; } @@ -115,7 +115,7 @@ function resolveApprovalAnnouncementChannel( if (taskId) { try { - const task = db.getTask(taskId); + const task = db.entityCrud.getTask(taskId); if (task.companyId === companyId && task.projectId) { return { channel: "project", channelTargetId: task.projectId }; } diff --git a/src/main/worker-service.ts b/src/main/worker-service.ts index be910591..2dd2e3cb 100644 --- a/src/main/worker-service.ts +++ b/src/main/worker-service.ts @@ -197,7 +197,7 @@ export function createWorkerService({ db.releaseWorkspaceLock(run.workspaceId, run.id); } if (run.taskId) { - db.releaseTaskLock(run.taskId, run.id); + db.tasks.releaseTaskLock(run.taskId, run.id); } } } diff --git a/src/main/worker/entry/crash-recovery.ts b/src/main/worker/entry/crash-recovery.ts index 459ff5f5..c8cdde4c 100644 --- a/src/main/worker/entry/crash-recovery.ts +++ b/src/main/worker/entry/crash-recovery.ts @@ -48,12 +48,12 @@ export function handleWorkerExit( } // EXEC-02: Release execution lock on crash recovery if (run.taskId) { - db.releaseTaskLock(run.taskId, run.id); + db.tasks.releaseTaskLock(run.taskId, run.id); } affectedAgentIds.add(run.agentId); affectedCompanyIds.set(run.agentId, run.companyId); - const chain = db.getChainOfCommand(run.agentId); + const chain = db.agents.getChainOfCommand(run.agentId); if (chain.length > 0) { wakeAgentIfPossible(chain[0].id, run.companyId, "report_failed"); } @@ -98,8 +98,8 @@ export function handleWorkerExit( if (candidate.workspaceId) { db.releaseWorkspaceLock(candidate.workspaceId, candidate.id); } - const task = candidate.taskId ? db.getTask(candidate.taskId) : null; - const agent = db.getAgent(candidate.agentId); + const task = candidate.taskId ? db.entityCrud.getTask(candidate.taskId) : null; + const agent = db.entityCrud.getAgent(candidate.agentId); const connector = db.getConnector(agent.connectorId); const { workspace, lockMode } = resolveWorkspaceWithLockMode(db, task, agent, candidate.companyId); const recoveryCompany = recoverySnapshot.companies.find((company) => company.id === candidate.companyId); diff --git a/src/main/worker/entry/types.ts b/src/main/worker/entry/types.ts index db256f73..3a6ea10d 100644 --- a/src/main/worker/entry/types.ts +++ b/src/main/worker/entry/types.ts @@ -85,8 +85,8 @@ export interface WorkerServiceDependencies { dispatchRunToWorker: (params: { runId: string; companyId: string; - agent: ReturnType; - task: ReturnType | null; + agent: ReturnType; + task: ReturnType | null; connector: ReturnType; workspace: ReturnType; workspaceMode: "exclusive" | "shared" | "none"; diff --git a/src/main/worker/handlers/run-finished-helpers.ts b/src/main/worker/handlers/run-finished-helpers.ts index 4d652008..53f09e7e 100644 --- a/src/main/worker/handlers/run-finished-helpers.ts +++ b/src/main/worker/handlers/run-finished-helpers.ts @@ -7,7 +7,7 @@ import type { HandlerContext, HandlerDeps, WorkerMessageEvent } from "./handler- type RunFinishedEvent = Extract; type RunRecord = ReturnType; -type TaskRecord = ReturnType; +type TaskRecord = ReturnType; export interface RunFinishedContext { run: RunRecord; @@ -56,7 +56,7 @@ export function finalizeRun( let task: TaskRecord | null = null; if (run.taskId) { try { - task = db.getTask(run.taskId); + task = db.entityCrud.getTask(run.taskId); } catch (error) { logger.warn( `[finalizeRun] getTask(${run.taskId}) failed; task may have been deleted: ${error instanceof Error ? error.message : String(error)}`, @@ -97,9 +97,9 @@ export function accumulateAndRotateSession( // heartbeat run — the exact failure mode that lets a taskless transcript // grow past the model's context window. Resolve the session through the // agent, never through the task. - let agentRecord: ReturnType; + let agentRecord: ReturnType; try { - agentRecord = db.getAgent(run.agentId); + agentRecord = db.entityCrud.getAgent(run.agentId); } catch (error) { logger.warn( `[session] accumulateAndRotateSession: getAgent(${run.agentId}) failed; skipping: ${error instanceof Error ? error.message : String(error)}`, @@ -107,7 +107,7 @@ export function accumulateAndRotateSession( return; } if (!agentRecord.sessionId) return; - const session = db.getSessionById(agentRecord.sessionId); + const session = db.tasks.getSessionById(agentRecord.sessionId); if (!session) return; sessionService.accumulateTokenUsage( @@ -120,7 +120,7 @@ export function accumulateAndRotateSession( // that include this run, otherwise a run that just crossed the // threshold waits a full additional cycle to rotate — the exact // failure mode that let transcripts balloon past the context window. - const updatedSession = db.getSessionById(agentRecord.sessionId); + const updatedSession = db.tasks.getSessionById(agentRecord.sessionId); if (!updatedSession) return; const rotationResult = sessionService.evaluateRotation(updatedSession); if (rotationResult.shouldRotate) { @@ -154,9 +154,9 @@ export function emitAndNotifyCompletion( }); } - let updatedAgent: ReturnType | null = null; + let updatedAgent: ReturnType | null = null; try { - updatedAgent = db.getAgent(run.agentId); + updatedAgent = db.entityCrud.getAgent(run.agentId); } catch (error) { logger.warn( `[emitAndNotifyCompletion] getAgent(${run.agentId}) failed; skipping budget-paused notification: ${error instanceof Error ? error.message : String(error)}`, @@ -294,7 +294,7 @@ export function triggerSelfWake( if (hadDeferredWakes || isQuitting() || destroyed) return; try { - const actionableTasks = db.listAgentTasks(run.agentId, run.companyId, ["todo", "in_progress"]); + const actionableTasks = db.tasks.listAgentTasks(run.agentId, run.companyId, ["todo", "in_progress"]); const hasWorkToContinue = actionableTasks.length > 0 || hasUnfinishedTeamBuilding(db, run.agentId, run.companyId); if (!hasWorkToContinue) return; @@ -305,7 +305,7 @@ export function triggerSelfWake( ); // Escalate to manager since agent is stuck try { - const chain = db.getChainOfCommand(run.agentId); + const chain = db.agents.getChainOfCommand(run.agentId); if (chain.length > 0) { wakeAgentIfPossible(chain[0].id, run.companyId, "report_stuck"); } @@ -342,9 +342,9 @@ function hasUnfinishedTeamBuilding( companyId: string, ): boolean { try { - const agent = db.getAgent(agentId); + const agent = db.entityCrud.getAgent(agentId); if (agent.reportsTo !== null) return false; - const activeReports = db + const activeReports = db.agents .getDirectReports(agentId, companyId) .filter((r) => r.status !== "pending_approval" && r.status !== "terminated"); if (activeReports.length > 0) return false; diff --git a/src/main/worker/handlers/run-log-handler.ts b/src/main/worker/handlers/run-log-handler.ts index 5559e9ac..9a166024 100644 --- a/src/main/worker/handlers/run-log-handler.ts +++ b/src/main/worker/handlers/run-log-handler.ts @@ -38,7 +38,7 @@ export async function handleRunLog( try { const run = db.getRun(event.runId); if (run.taskId) { - db.renewTaskLease(run.taskId, run.id); + db.tasks.renewTaskLease(run.taskId, run.id); } leaseRenewalMap.set(event.runId, now); } catch (err) { diff --git a/src/main/worker/handlers/run-status-handler.ts b/src/main/worker/handlers/run-status-handler.ts index df977731..19720180 100644 --- a/src/main/worker/handlers/run-status-handler.ts +++ b/src/main/worker/handlers/run-status-handler.ts @@ -32,7 +32,7 @@ export function handleRunStatus( try { const run = db.getRun(event.runId); if (run.taskId) { - db.renewTaskLease(run.taskId, run.id); + db.tasks.renewTaskLease(run.taskId, run.id); } onRunStarted?.(run.id, run.taskId, run.agentId, run.companyId); } catch (err) { diff --git a/src/main/worker/observability/failure-escalation.ts b/src/main/worker/observability/failure-escalation.ts index dd41b6bc..8d24b9dd 100644 --- a/src/main/worker/observability/failure-escalation.ts +++ b/src/main/worker/observability/failure-escalation.ts @@ -11,12 +11,16 @@ export interface EscalationDeps { db: { - getTaskRetryCount: (taskId: string) => number; - incrementTaskRetryCount: (taskId: string) => void; - resetTaskRetryCount: (taskId: string) => void; - getChainOfCommand: (agentId: string) => Array<{ id: string; name: string }>; - getCeoForCompany: (companyId: string) => { id: string; name: string } | null; - advanceTaskStatus: (taskId: string, status: string, note: string) => unknown; + tasks: { + getTaskRetryCount: (taskId: string) => number; + incrementTaskRetryCount: (taskId: string) => void; + resetTaskRetryCount: (taskId: string) => void; + advanceTaskStatus: (taskId: string, status: string, note: string) => unknown; + }; + agents: { + getChainOfCommand: (agentId: string) => Array<{ id: string; name: string }>; + getCeoForCompany: (companyId: string) => { id: string; name: string } | null; + }; }; wakeAgentIfPossible: (agentId: string, companyId: string, trigger: string) => unknown; notify: (title: string, body: string) => void; @@ -42,11 +46,11 @@ export function handleFailureEscalation( scheduleRetry: (fn: () => void, delayMs: number) => void, ): EscalationResult { const { db, wakeAgentIfPossible, notify, logger } = deps; - const retryCount = db.getTaskRetryCount(run.taskId); + const retryCount = db.tasks.getTaskRetryCount(run.taskId); if (retryCount < maxRetryAttempts) { - db.incrementTaskRetryCount(run.taskId); - db.advanceTaskStatus( + db.tasks.incrementTaskRetryCount(run.taskId); + db.tasks.advanceTaskStatus( run.taskId, "todo", `Run ${eventStatus} (attempt ${retryCount + 1}/${maxRetryAttempts}). Auto-retrying after ${retryCooldownMs / 1000}s cooldown.`, @@ -61,14 +65,14 @@ export function handleFailureEscalation( } // Retry limit reached — escalate up the chain - db.resetTaskRetryCount(run.taskId); + db.tasks.resetTaskRetryCount(run.taskId); logger.warn( `[escalation] Task ${run.taskId} failed ${maxRetryAttempts} times. Escalating to manager.`, ); let escalated = false; try { - const chain = db.getChainOfCommand(run.agentId); + const chain = db.agents.getChainOfCommand(run.agentId); if (chain.length > 0) { wakeAgentIfPossible(chain[0].id, run.companyId, "report_failed"); escalated = true; @@ -84,7 +88,7 @@ export function handleFailureEscalation( if (!escalated) { try { - const ceo = db.getCeoForCompany(run.companyId); + const ceo = db.agents.getCeoForCompany(run.companyId); if (ceo) { wakeAgentIfPossible(ceo.id, run.companyId, "report_failed"); escalated = true; @@ -120,7 +124,7 @@ export function handleSuccessReset( logger?: EscalationDeps["logger"], ): void { try { - db.resetTaskRetryCount(taskId); + db.tasks.resetTaskRetryCount(taskId); } catch (error) { logger?.warn( `[escalation] resetTaskRetryCount failed for task ${taskId}: ${error instanceof Error ? error.message : String(error)}`, diff --git a/tests/integration/agent-model-switching.spec.ts b/tests/integration/agent-model-switching.spec.ts index 901057a5..d3ccf14d 100644 --- a/tests/integration/agent-model-switching.spec.ts +++ b/tests/integration/agent-model-switching.spec.ts @@ -91,7 +91,7 @@ describe("agent model switching", () => { repoRef: "", isPrimary: true, }); - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "Engineer", role: "Engineer", @@ -107,7 +107,7 @@ describe("agent model switching", () => { const { enqueueWorkerRun, taskRunner } = createHarness(db); - const firstTaskId = db.saveTask({ + const firstTaskId = db.tasks.saveTask({ companyId, title: "Initial model task", description: "Use the first model for this run.", @@ -137,7 +137,7 @@ describe("agent model switching", () => { unattributedCost: true, }); - db.saveAgent({ + db.agents.saveAgent({ id: agentId, companyId, name: "Engineer", @@ -152,7 +152,7 @@ describe("agent model switching", () => { budgetMonthlyUsd: 200, }); - const secondTaskId = db.saveTask({ + const secondTaskId = db.tasks.saveTask({ companyId, title: "Updated model task", description: "Use the new model for this run.", diff --git a/tests/integration/api-recursive-org-execution.spec.ts b/tests/integration/api-recursive-org-execution.spec.ts index c90ed7ba..1c05c0bf 100644 --- a/tests/integration/api-recursive-org-execution.spec.ts +++ b/tests/integration/api-recursive-org-execution.spec.ts @@ -45,7 +45,7 @@ async function createHarness() { repoRef: "", isPrimary: true, }); - const ceoId = db.saveAgent({ + const ceoId = db.agents.saveAgent({ companyId, name: "CEO", role: "CEO", @@ -58,7 +58,7 @@ async function createHarness() { capabilities: "Lead the company", budgetMonthlyUsd: 250, }); - const ctoId = db.saveAgent({ + const ctoId = db.agents.saveAgent({ companyId, name: "CTO", role: "CTO", @@ -72,7 +72,7 @@ async function createHarness() { budgetMonthlyUsd: 220, department: "engineering", }); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, title: "Build the org", description: "CTO should hire the first IC under engineering.", @@ -126,7 +126,7 @@ describe("recursive org execution", () => { const body = await response.json() as { agentId: string; approvalId: string; autoApproved: boolean }; expect(body.autoApproved).toBe(false); - const pendingHire = harness.db.getAgent(body.agentId); + const pendingHire = harness.db.entityCrud.getAgent(body.agentId); expect(pendingHire.status).toBe("pending_approval"); expect(pendingHire.reportsTo).toBe(harness.ctoId); @@ -141,11 +141,11 @@ describe("recursive org execution", () => { decisionNote: "Approved by board", }); - const approvedHire = harness.db.getAgent(body.agentId); + const approvedHire = harness.db.entityCrud.getAgent(body.agentId); expect(approvedHire.status).toBe("idle"); - expect(harness.db.getDirectReports(harness.ctoId, harness.companyId).map((agent) => agent.id)).toContain(body.agentId); - expect(harness.db.getDirectReports(harness.ceoId, harness.companyId).map((agent) => agent.id)).not.toContain(body.agentId); - expect(harness.db.getChainOfCommand(body.agentId).map((agent) => agent.id)).toEqual([harness.ctoId, harness.ceoId]); + expect(harness.db.agents.getDirectReports(harness.ctoId, harness.companyId).map((agent) => agent.id)).toContain(body.agentId); + expect(harness.db.agents.getDirectReports(harness.ceoId, harness.companyId).map((agent) => agent.id)).not.toContain(body.agentId); + expect(harness.db.agents.getChainOfCommand(body.agentId).map((agent) => agent.id)).toEqual([harness.ctoId, harness.ceoId]); const agentResponse = await fetch(`${harness.baseUrl}/api/agents/${body.agentId}`, { headers: harness.headers, @@ -169,7 +169,7 @@ describe("recursive org execution", () => { it("routes cross-department hires to the relevant department lead when reportsTo is omitted", async () => { const harness = await createHarness(); try { - const hrId = harness.db.saveAgent({ + const hrId = harness.db.agents.saveAgent({ companyId: harness.companyId, name: "HR Director", role: "HR Director", @@ -183,7 +183,7 @@ describe("recursive org execution", () => { budgetMonthlyUsd: 180, department: "hr", }); - const hrTaskId = harness.db.saveTask({ + const hrTaskId = harness.db.tasks.saveTask({ companyId: harness.companyId, title: "Fill engineering gap", description: "Request a backend engineer for the engineering team.", @@ -223,7 +223,7 @@ describe("recursive org execution", () => { expect(response.status).toBe(201); const body = await response.json() as { agentId: string }; - const pendingHire = harness.db.getAgent(body.agentId); + const pendingHire = harness.db.entityCrud.getAgent(body.agentId); expect(pendingHire.department).toBe("engineering"); expect(pendingHire.reportsTo).toBe(harness.ctoId); } finally { @@ -263,7 +263,7 @@ describe("recursive org execution", () => { it("manager cannot route a hire outside their reporting branch", async () => { const harness = await createHarness(); try { - const cmoId = harness.db.saveAgent({ + const cmoId = harness.db.agents.saveAgent({ companyId: harness.companyId, name: "CMO", role: "CMO", diff --git a/tests/integration/api-run-events.spec.ts b/tests/integration/api-run-events.spec.ts index fc335d41..46b553c1 100644 --- a/tests/integration/api-run-events.spec.ts +++ b/tests/integration/api-run-events.spec.ts @@ -33,7 +33,7 @@ async function createHarness() { repoRef: "", isPrimary: true, }); - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "Helios", role: "CEO", @@ -46,7 +46,7 @@ async function createHarness() { capabilities: "Operate the company", budgetMonthlyUsd: 100, }); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, title: "Ship the API", description: "Capture structured run events.", diff --git a/tests/integration/autonomous-chain/approval-decisions.spec.ts b/tests/integration/autonomous-chain/approval-decisions.spec.ts index e42f2daf..420a526c 100644 --- a/tests/integration/autonomous-chain/approval-decisions.spec.ts +++ b/tests/integration/autonomous-chain/approval-decisions.spec.ts @@ -29,7 +29,7 @@ describe("Step 4: Hire approval + decideApproval", () => { decisionNote: "Approved by board", }); - const updatedAgent = shared.db.getAgent(hireId); + const updatedAgent = shared.db.entityCrud.getAgent(hireId); expect(updatedAgent.status).toBe("idle"); }); @@ -52,7 +52,7 @@ describe("Step 4: Hire approval + decideApproval", () => { decisionNote: "Not needed", }); - const updatedAgent = shared.db.getAgent(hireId); + const updatedAgent = shared.db.entityCrud.getAgent(hireId); expect(updatedAgent.status).toBe("terminated"); }); diff --git a/tests/integration/autonomous-chain/automation-messages.spec.ts b/tests/integration/autonomous-chain/automation-messages.spec.ts index 074d292c..d7d9e929 100644 --- a/tests/integration/autonomous-chain/automation-messages.spec.ts +++ b/tests/integration/autonomous-chain/automation-messages.spec.ts @@ -89,13 +89,13 @@ describe("Agent Messages", () => { let agentBId: string; beforeEach(() => { - agentAId = shared.db.saveAgent({ + agentAId = shared.db.agents.saveAgent({ companyId: shared.companyId, name: "Alice", role: "Engineer", title: "Senior Engineer", department: "engineering", connectorId: "claude-pro", status: "active", capabilities: "Backend development, API design, code review", budgetMonthlyUsd: 100, }); - agentBId = shared.db.saveAgent({ + agentBId = shared.db.agents.saveAgent({ companyId: shared.companyId, name: "Bob", role: "Designer", title: "UX Designer", department: "design", connectorId: "claude-pro", status: "active", capabilities: "UI design, wireframing, user research", @@ -178,7 +178,7 @@ describe("Agent Messages", () => { leadAgentId: engineeringLeadId, status: "active", }); - shared.db.saveTask({ + shared.db.tasks.saveTask({ companyId: shared.companyId, title: "Project task", description: "Do work", @@ -240,13 +240,13 @@ describe("Auto Task Assignment", () => { let frontendId: string; beforeEach(() => { - backendId = shared.db.saveAgent({ + backendId = shared.db.agents.saveAgent({ companyId: shared.companyId, name: "Backend Dev", role: "Backend Engineer", title: "Senior Backend", department: "engineering", connectorId: "openai-codex", status: "active", capabilities: "API development, database design, Node.js, PostgreSQL, backend testing", budgetMonthlyUsd: 200, }); - frontendId = shared.db.saveAgent({ + frontendId = shared.db.agents.saveAgent({ companyId: shared.companyId, name: "Frontend Dev", role: "Frontend Engineer", title: "Senior Frontend", department: "engineering", connectorId: "openai-codex", status: "active", capabilities: "React, TypeScript, CSS, Tailwind, responsive design, frontend testing, UI components", @@ -255,61 +255,61 @@ describe("Auto Task Assignment", () => { }); it("assigns to the best-matching agent by capability keywords", () => { - const taskId = shared.db.saveTask({ + const taskId = shared.db.tasks.saveTask({ companyId: shared.companyId, title: "Build React component for user dashboard", description: "Create a responsive React component with Tailwind CSS styling", priority: "medium", status: "todo", }); - const assignedId = shared.db.autoAssignTask(taskId, shared.companyId); + const assignedId = shared.db.tasks.autoAssignTask(taskId, shared.companyId); expect(assignedId).toBe(frontendId); // "React", "responsive", "Tailwind" match frontend }); it("assigns backend task to backend agent", () => { - const taskId = shared.db.saveTask({ + const taskId = shared.db.tasks.saveTask({ companyId: shared.companyId, title: "Design database schema for orders", description: "PostgreSQL database design with API endpoints for order management", priority: "high", status: "todo", }); - const assignedId = shared.db.autoAssignTask(taskId, shared.companyId); + const assignedId = shared.db.tasks.autoAssignTask(taskId, shared.companyId); expect(assignedId).toBe(backendId); // "database", "PostgreSQL", "API" match backend }); it("returns existing assignee if task already assigned", () => { - const taskId = shared.db.saveTask({ + const taskId = shared.db.tasks.saveTask({ companyId: shared.companyId, title: "Already assigned task", description: "This task has an assignee", assigneeAgentId: frontendId, priority: "medium", status: "todo", }); - const result = shared.db.autoAssignTask(taskId, shared.companyId); + const result = shared.db.tasks.autoAssignTask(taskId, shared.companyId); expect(result).toBe(frontendId); }); it("returns null if all agents have exhausted their budget", () => { // Set positive budgets that are fully spent - shared.db.saveAgent({ id: backendId, companyId: shared.companyId, name: "Backend Dev", role: "Backend", title: "Backend", connectorId: "openai-codex", status: "active", capabilities: "backend", budgetMonthlyUsd: 100 }); - shared.db.saveAgent({ id: frontendId, companyId: shared.companyId, name: "Frontend Dev", role: "Frontend", title: "Frontend", connectorId: "openai-codex", status: "active", capabilities: "frontend", budgetMonthlyUsd: 100 }); + shared.db.agents.saveAgent({ id: backendId, companyId: shared.companyId, name: "Backend Dev", role: "Backend", title: "Backend", connectorId: "openai-codex", status: "active", capabilities: "backend", budgetMonthlyUsd: 100 }); + shared.db.agents.saveAgent({ id: frontendId, companyId: shared.companyId, name: "Frontend Dev", role: "Frontend", title: "Frontend", connectorId: "openai-codex", status: "active", capabilities: "frontend", budgetMonthlyUsd: 100 }); // Exhaust budgets by recording spending via direct SQL (since finishRun would need a real run) (shared.db as unknown as { db: { prepare: (sql: string) => { run: (...args: unknown[]) => void } } }).db.prepare("update agents set spent_monthly_usd = 100 where id = ?").run(backendId); (shared.db as unknown as { db: { prepare: (sql: string) => { run: (...args: unknown[]) => void } } }).db.prepare("update agents set spent_monthly_usd = 100 where id = ?").run(frontendId); - const taskId = shared.db.saveTask({ companyId: shared.companyId, title: "No budget task", description: "Test", priority: "low", status: "todo" }); - const result = shared.db.autoAssignTask(taskId, shared.companyId); + const taskId = shared.db.tasks.saveTask({ companyId: shared.companyId, title: "No budget task", description: "Test", priority: "low", status: "todo" }); + const result = shared.db.tasks.autoAssignTask(taskId, shared.companyId); expect(result).toBeNull(); }); it("penalizes agents with many active tasks", () => { // Give backend agent 5 active tasks for (let i = 0; i < 5; i++) { - shared.db.saveTask({ companyId: shared.companyId, title: `Backend task ${i}`, description: "", assigneeAgentId: backendId, priority: "medium", status: "in_progress" }); + shared.db.tasks.saveTask({ companyId: shared.companyId, title: `Backend task ${i}`, description: "", assigneeAgentId: backendId, priority: "medium", status: "in_progress" }); } // Generic task with no strong keyword match — should go to less busy frontend agent - const taskId = shared.db.saveTask({ companyId: shared.companyId, title: "Implement feature", description: "Build the feature", priority: "medium", status: "todo" }); - const assignedId = shared.db.autoAssignTask(taskId, shared.companyId); + const taskId = shared.db.tasks.saveTask({ companyId: shared.companyId, title: "Implement feature", description: "Build the feature", priority: "medium", status: "todo" }); + const assignedId = shared.db.tasks.autoAssignTask(taskId, shared.companyId); expect(assignedId).toBe(frontendId); // Frontend has 0 active tasks vs backend's 5 }); }); diff --git a/tests/integration/autonomous-chain/ceo-phased-prompts.spec.ts b/tests/integration/autonomous-chain/ceo-phased-prompts.spec.ts index 307a80e9..ece1957d 100644 --- a/tests/integration/autonomous-chain/ceo-phased-prompts.spec.ts +++ b/tests/integration/autonomous-chain/ceo-phased-prompts.spec.ts @@ -40,7 +40,7 @@ describe("Step 2: CEO phased prompts", () => { const ceoId = createAgent({ name: "CEO", role: "CEO" }); const hireId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "pending_approval" as "idle" }); - const hire = shared.db.getAgent(hireId); + const hire = shared.db.entityCrud.getAgent(hireId); const ctx = buildCtx({ agentId: ceoId, @@ -88,7 +88,7 @@ describe("Step 2: CEO phased prompts", () => { it("Phase 2: shows normal executive instructions when active reports and no pending org approval", () => { const ceoId = createAgent({ name: "CEO", role: "CEO" }); const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "idle" }); - const cto = shared.db.getAgent(ctoId); + const cto = shared.db.entityCrud.getAgent(ctoId); const ctx = buildCtx({ agentId: ceoId, @@ -109,7 +109,7 @@ describe("Step 2: CEO phased prompts", () => { it("includes pending approvals section in prompt", () => { const ceoId = createAgent({ name: "CEO", role: "CEO" }); const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "idle" }); - const cto = shared.db.getAgent(ctoId); + const cto = shared.db.entityCrud.getAgent(ctoId); const ctx = buildCtx({ agentId: ceoId, @@ -139,10 +139,10 @@ describe("Step 2: CEO phased prompts", () => { it("Manager prompt includes credential escalation guidance", () => { const ceoId = createAgent({ name: "CEO", role: "CEO" }); - const ceo = shared.db.getAgent(ceoId); + const ceo = shared.db.entityCrud.getAgent(ceoId); const managerId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId }); const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: managerId }); - const ic = shared.db.getAgent(icId); + const ic = shared.db.entityCrud.getAgent(icId); const ctx = buildCtx({ agentId: managerId, @@ -156,7 +156,7 @@ describe("Step 2: CEO phased prompts", () => { it("IC prompt includes credential escalation guidance", () => { const ceoId = createAgent({ name: "CEO", role: "CEO" }); - const ceo = shared.db.getAgent(ceoId); + const ceo = shared.db.entityCrud.getAgent(ceoId); const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ceoId }); const ctx = buildCtx({ diff --git a/tests/integration/autonomous-chain/crash-recovery.spec.ts b/tests/integration/autonomous-chain/crash-recovery.spec.ts index 91173a7b..4f6f69b1 100644 --- a/tests/integration/autonomous-chain/crash-recovery.spec.ts +++ b/tests/integration/autonomous-chain/crash-recovery.spec.ts @@ -13,7 +13,7 @@ const { createAgent } = shared; describe("Step 7: Worker crash recovery (database layer)", () => { it("interruptStaleRuns clears running and queued runs on init", async () => { const agentId = createAgent({ name: "Agent" }); - const taskId = shared.db.saveTask({ + const taskId = shared.db.tasks.saveTask({ companyId: shared.companyId, title: "In-progress task", description: "Running when crash happened", @@ -51,7 +51,7 @@ describe("Step 7: Worker crash recovery (database layer)", () => { it("finishRun can mark interrupted runs from crash recovery", () => { const agentId = createAgent({ name: "Agent" }); - const taskId = shared.db.saveTask({ + const taskId = shared.db.tasks.saveTask({ companyId: shared.companyId, title: "Crash recovery task", description: "Test", diff --git a/tests/integration/autonomous-chain/e2e-flows.spec.ts b/tests/integration/autonomous-chain/e2e-flows.spec.ts index 1b44fbd5..d6d31d89 100644 --- a/tests/integration/autonomous-chain/e2e-flows.spec.ts +++ b/tests/integration/autonomous-chain/e2e-flows.spec.ts @@ -40,8 +40,8 @@ describe("E2E: Full autonomous CEO cold start flow", () => { }); // CEO now in Phase 1 - const ctoRecord = shared.db.getAgent(ctoId); - const cmoRecord = shared.db.getAgent(cmoId); + const ctoRecord = shared.db.entityCrud.getAgent(ctoId); + const cmoRecord = shared.db.entityCrud.getAgent(cmoId); const phase1Ctx = buildCtx({ agentId: ceoId, directReports: [ctoRecord, cmoRecord], @@ -85,15 +85,15 @@ describe("E2E: Full autonomous CEO cold start flow", () => { shared.db.decideApproval({ approvalId: hireApprovalCMO, state: "approved", decisionNote: "Approved" }); // Agents now idle - expect(shared.db.getAgent(ctoId).status).toBe("idle"); - expect(shared.db.getAgent(cmoId).status).toBe("idle"); + expect(shared.db.entityCrud.getAgent(ctoId).status).toBe("idle"); + expect(shared.db.entityCrud.getAgent(cmoId).status).toBe("idle"); // 5. Board approves org strategy shared.db.decideApproval({ approvalId: orgApprovalId, state: "approved", decisionNote: "Go ahead" }); // 6. CEO now in Phase 2 - const updatedCto = shared.db.getAgent(ctoId); - const updatedCmo = shared.db.getAgent(cmoId); + const updatedCto = shared.db.entityCrud.getAgent(ctoId); + const updatedCmo = shared.db.entityCrud.getAgent(cmoId); const phase2Ctx = buildCtx({ agentId: ceoId, directReports: [updatedCto, updatedCmo], @@ -106,7 +106,7 @@ describe("E2E: Full autonomous CEO cold start flow", () => { expect(phase2Prompt).toContain("Delegation"); // 7. Verify hierarchical chain - const ctoChain = shared.db.getChainOfCommand(ctoId); + const ctoChain = shared.db.agents.getChainOfCommand(ctoId); expect(ctoChain).toHaveLength(1); expect(ctoChain[0].id).toBe(ceoId); @@ -120,7 +120,7 @@ describe("E2E: Full autonomous CEO cold start flow", () => { status: "in_progress", }); - const parentTaskId = shared.db.saveTask({ + const parentTaskId = shared.db.tasks.saveTask({ companyId: shared.companyId, title: "Build backend", description: "Implement API", @@ -133,7 +133,7 @@ describe("E2E: Full autonomous CEO cold start flow", () => { // CTO creates subtask for IC (would be done via API) const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ctoId, status: "idle" }); - const subtaskId = shared.db.saveTask({ + const subtaskId = shared.db.tasks.saveTask({ companyId: shared.companyId, title: "Implement auth", description: "JWT auth", @@ -146,7 +146,7 @@ describe("E2E: Full autonomous CEO cold start flow", () => { }); // Verify parent-child relationship - const subtask = shared.db.getTask(subtaskId); + const subtask = shared.db.entityCrud.getTask(subtaskId); expect(subtask.parentId).toBe(parentTaskId); // 9. Wakeup request test: enqueue wake while IC is busy @@ -159,7 +159,7 @@ describe("E2E: Full autonomous CEO cold start flow", () => { const icPromptCtx = buildCtx({ agentId: icId, task: subtask, - chainOfCommand: [shared.db.getAgent(ctoId), shared.db.getAgent(ceoId)], + chainOfCommand: [shared.db.entityCrud.getAgent(ctoId), shared.db.entityCrud.getAgent(ceoId)], wakeReason: "assignment", }); const icPrompt = buildTaskPrompt(icPromptCtx); @@ -202,11 +202,11 @@ describe("E2E: Blocked escalation through org hierarchy", () => { const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ctoId }); // When IC marks task blocked, we need to find its manager - const chain = shared.db.getChainOfCommand(icId); + const chain = shared.db.agents.getChainOfCommand(icId); expect(chain[0].id).toBe(ctoId); // immediate manager gets woken // When CTO needs to escalate further - const ctoChain = shared.db.getChainOfCommand(ctoId); + const ctoChain = shared.db.agents.getChainOfCommand(ctoId); expect(ctoChain[0].id).toBe(ceoId); }); }); @@ -216,7 +216,7 @@ describe("Review Fix: CEO phase ordering edge case", () => { const ceoId = createAgent({ name: "CEO", role: "CEO" }); // Hires submitted but not yet approved const hireId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "pending_approval" as "idle" }); - const hire = shared.db.getAgent(hireId); + const hire = shared.db.entityCrud.getAgent(hireId); // Org strategy already approved (not in pendingApprovals), but hires still pending const ctx = buildCtx({ @@ -236,7 +236,7 @@ describe("Review Fix: CEO phase ordering edge case", () => { it("moves to Phase 2 once hires are approved (no pending org approval)", () => { const ceoId = createAgent({ name: "CEO", role: "CEO" }); const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "idle" }); - const cto = shared.db.getAgent(ctoId); + const cto = shared.db.entityCrud.getAgent(ctoId); const ctx = buildCtx({ agentId: ceoId, @@ -315,7 +315,7 @@ describe("Review Fix: Root task completion notification", () => { const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId }); // CTO has a root-level task (no parentId) - const taskId = shared.db.saveTask({ + const taskId = shared.db.tasks.saveTask({ companyId: shared.companyId, title: "Standalone task from CEO", description: "No parent", @@ -325,11 +325,11 @@ describe("Review Fix: Root task completion notification", () => { status: "done", }); - const task = shared.db.getTask(taskId); + const task = shared.db.entityCrud.getTask(taskId); expect(task.parentId).toBeNull(); // CTO's chain of command includes CEO — so api-server can wake CEO on completion - const chain = shared.db.getChainOfCommand(ctoId); + const chain = shared.db.agents.getChainOfCommand(ctoId); expect(chain).toHaveLength(1); expect(chain[0].id).toBe(ceoId); }); diff --git a/tests/integration/autonomous-chain/role-detection.spec.ts b/tests/integration/autonomous-chain/role-detection.spec.ts index 636ce555..96284223 100644 --- a/tests/integration/autonomous-chain/role-detection.spec.ts +++ b/tests/integration/autonomous-chain/role-detection.spec.ts @@ -31,9 +31,9 @@ describe("Step 1: CEO role detection", () => { it("identifies manager when has reports and has chain of command", () => { const ceoId = createAgent({ name: "CEO", role: "CEO" }); const managerId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId }); - const ceo = shared.db.getAgent(ceoId); + const ceo = shared.db.entityCrud.getAgent(ceoId); const icId = createAgent({ name: "IC", role: "Engineer", reportsTo: managerId }); - const ic = shared.db.getAgent(icId); + const ic = shared.db.entityCrud.getAgent(icId); const ctx = buildCtx({ agentId: managerId, @@ -49,7 +49,7 @@ describe("Step 1: CEO role detection", () => { it("identifies IC when has chain of command and no direct reports", () => { const ceoId = createAgent({ name: "CEO", role: "CEO" }); - const ceo = shared.db.getAgent(ceoId); + const ceo = shared.db.entityCrud.getAgent(ceoId); const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ceoId }); const ctx = buildCtx({ diff --git a/tests/integration/autonomous-chain/run-failure.spec.ts b/tests/integration/autonomous-chain/run-failure.spec.ts index 4bd6358b..c0938be5 100644 --- a/tests/integration/autonomous-chain/run-failure.spec.ts +++ b/tests/integration/autonomous-chain/run-failure.spec.ts @@ -12,7 +12,7 @@ const { createAgent } = shared; describe("Step 6: Run failure escalation (database layer)", () => { it("finishRun marks run as failed and returns outcome", () => { const agentId = createAgent({ name: "IC", role: "Engineer" }); - const taskId = shared.db.saveTask({ + const taskId = shared.db.tasks.saveTask({ companyId: shared.companyId, title: "Failing task", description: "Will fail", @@ -56,7 +56,7 @@ describe("Step 6: Run failure escalation (database layer)", () => { const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId }); const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ctoId }); - const chain = shared.db.getChainOfCommand(icId); + const chain = shared.db.agents.getChainOfCommand(icId); // First in chain is the immediate manager (CTO) expect(chain.length).toBeGreaterThan(0); expect(chain[0].name).toBe("CTO"); diff --git a/tests/integration/autonomous-chain/shared-setup.ts b/tests/integration/autonomous-chain/shared-setup.ts index 013e307c..d00fae8e 100644 --- a/tests/integration/autonomous-chain/shared-setup.ts +++ b/tests/integration/autonomous-chain/shared-setup.ts @@ -30,7 +30,7 @@ export interface SharedHandle { set db(value: AppDatabase); get companyId(): string; get workspaceId(): string; - createAgent(overrides?: Partial[0]>): string; + createAgent(overrides?: Partial[0]>): string; buildCtx(overrides?: Record): Parameters[0]; tempDirs: string[]; } @@ -80,8 +80,8 @@ export function installSharedSetup(): SharedHandle { return state.db; } - function createAgent(overrides: Partial[0]> = {}): string { - return currentDb().saveAgent({ + function createAgent(overrides: Partial[0]> = {}): string { + return currentDb().agents.saveAgent({ companyId: state.companyId, name: "TestAgent", role: "CEO", @@ -98,7 +98,7 @@ export function installSharedSetup(): SharedHandle { } function buildCtx(overrides: Record = {}): Parameters[0] { - const agent = currentDb().getAgent(overrides.agentId as string ?? createAgent()); + const agent = currentDb().entityCrud.getAgent(overrides.agentId as string ?? createAgent()); return { agent, apiUrl: "http://127.0.0.1:3000", diff --git a/tests/integration/autonomous-chain/task-propagation.spec.ts b/tests/integration/autonomous-chain/task-propagation.spec.ts index 333cee8b..2bdf625f 100644 --- a/tests/integration/autonomous-chain/task-propagation.spec.ts +++ b/tests/integration/autonomous-chain/task-propagation.spec.ts @@ -15,7 +15,7 @@ describe("Step 5: Task status + comment wake propagation", () => { const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId }); const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ctoId }); - const chain = shared.db.getChainOfCommand(icId); + const chain = shared.db.agents.getChainOfCommand(icId); expect(chain).toHaveLength(2); expect(chain[0].id).toBe(ctoId); // immediate manager expect(chain[1].id).toBe(ceoId); // top level @@ -25,7 +25,7 @@ describe("Step 5: Task status + comment wake propagation", () => { // Create a potential cycle (shouldn't happen in practice, but guard against it) const agent1 = createAgent({ name: "Agent1" }); // agent1 reports to no one (CEO), so chain should be empty - const chain = shared.db.getChainOfCommand(agent1); + const chain = shared.db.agents.getChainOfCommand(agent1); expect(chain).toHaveLength(0); }); @@ -33,7 +33,7 @@ describe("Step 5: Task status + comment wake propagation", () => { const ceoId = createAgent({ name: "CEO", role: "CEO" }); const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId }); - const parentTaskId = shared.db.saveTask({ + const parentTaskId = shared.db.tasks.saveTask({ companyId: shared.companyId, title: "Build feature X", description: "Top-level task", @@ -43,7 +43,7 @@ describe("Step 5: Task status + comment wake propagation", () => { status: "in_progress", }); - const childTaskId = shared.db.saveTask({ + const childTaskId = shared.db.tasks.saveTask({ companyId: shared.companyId, title: "Implement module A", description: "Subtask", @@ -54,10 +54,10 @@ describe("Step 5: Task status + comment wake propagation", () => { status: "todo", }); - const childTask = shared.db.getTask(childTaskId); + const childTask = shared.db.entityCrud.getTask(childTaskId); expect(childTask.parentId).toBe(parentTaskId); - const parentTask = shared.db.getTask(parentTaskId); + const parentTask = shared.db.entityCrud.getTask(parentTaskId); expect(parentTask.assigneeAgentId).toBe(ctoId); }); }); diff --git a/tests/integration/autonomous-chain/wake-reason.spec.ts b/tests/integration/autonomous-chain/wake-reason.spec.ts index 205c28cc..9f27a7aa 100644 --- a/tests/integration/autonomous-chain/wake-reason.spec.ts +++ b/tests/integration/autonomous-chain/wake-reason.spec.ts @@ -14,7 +14,7 @@ describe("Step 8: Wake reason injection", () => { it("includes wake reason section for non-timer triggers", () => { const agentId = createAgent({ name: "CTO", role: "CTO" }); const ceoId = createAgent({ name: "CEO", role: "CEO" }); - const ceo = shared.db.getAgent(ceoId); + const ceo = shared.db.entityCrud.getAgent(ceoId); const ctx = buildCtx({ agentId, @@ -92,7 +92,7 @@ describe("Step 8: Wake reason injection", () => { it("includes wake reason before task description", () => { const agentId = createAgent({ name: "Agent" }); - const taskId = shared.db.saveTask({ + const taskId = shared.db.tasks.saveTask({ companyId: shared.companyId, title: "Do the thing", description: "Test task", @@ -101,7 +101,7 @@ describe("Step 8: Wake reason injection", () => { priority: "medium", status: "todo", }); - const task = shared.db.getTask(taskId); + const task = shared.db.entityCrud.getTask(taskId); const ctx = buildCtx({ agentId, diff --git a/tests/integration/connector-model-enforcement.spec.ts b/tests/integration/connector-model-enforcement.spec.ts index 3399f71f..57ae246a 100644 --- a/tests/integration/connector-model-enforcement.spec.ts +++ b/tests/integration/connector-model-enforcement.spec.ts @@ -90,7 +90,7 @@ describe("connector model enforcement", () => { repoRef: "", isPrimary: true, }); - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "CEO", role: "CEO", @@ -103,7 +103,7 @@ describe("connector model enforcement", () => { capabilities: "Lead the company", budgetMonthlyUsd: 250, }); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, title: "Run with invalid model", description: "This task should be blocked because the agent model is disabled.", @@ -154,7 +154,7 @@ describe("connector model enforcement", () => { repoRef: "", isPrimary: true, }); - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "CEO", role: "CEO", @@ -167,7 +167,7 @@ describe("connector model enforcement", () => { capabilities: "Lead the company", budgetMonthlyUsd: 250, }); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, title: "Recover queued run", description: "This queued run should fail on recovery because the model is disabled.", @@ -184,7 +184,7 @@ describe("connector model enforcement", () => { workspaceId, connectorId: "openai-codex", }); - const checkoutSuccess = db.checkoutTask(taskId, run.id); + const checkoutSuccess = db.tasks.checkoutTask(taskId, run.id); expect(checkoutSuccess).toBe(true); db.setDisabledModels(companyId, "openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]); diff --git a/tests/integration/cross-subsystem.spec.ts b/tests/integration/cross-subsystem.spec.ts index 10c1cb79..75f9b1dc 100644 --- a/tests/integration/cross-subsystem.spec.ts +++ b/tests/integration/cross-subsystem.spec.ts @@ -79,7 +79,7 @@ function scaffold(db: AppDatabase, budgetAmountCents = 500): ScaffoldResult { isPrimary: true, }); - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "PipelineAgent", role: "IC", @@ -175,7 +175,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { expect(validation.errors).toHaveLength(0); // Create the task in the DB - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId: s.companyId, title: "Build login page", description: @@ -194,7 +194,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { workspaceId: s.workspaceId, connectorId: s.connectorId, }); - const checkoutResult = db.checkoutTask(taskId, runId); + const checkoutResult = db.tasks.checkoutTask(taskId, runId); expect(checkoutResult).toBe(true); // 3. Budget gate: verify run is allowed @@ -221,7 +221,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { }); // 5. Session management: create session and track usage - const sessionId = db.createNewSession({ + const sessionId = db.tasks.createNewSession({ companyId: s.companyId, agentId: s.agentId, adapterType: "gemini-cli", @@ -232,10 +232,10 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { const run = db.getRun(runId); expect(run.status).toBe("succeeded"); - const task = db.getTask(taskId); + const task = db.entityCrud.getTask(taskId); expect(task.costUsd).toBeGreaterThan(0); - const session = db.getSessionById(sessionId); + const session = db.tasks.getSessionById(sessionId); expect(session).not.toBeNull(); expect(session!.totalInputTokens).toBe(5000); expect(session!.totalOutputTokens).toBe(2000); @@ -254,7 +254,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { it("CAS prevents double checkout within pipeline", () => { const s = scaffold(db); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId: s.companyId, title: "Concurrent Task", description: @@ -273,10 +273,10 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { workspaceId: s.workspaceId, connectorId: s.connectorId, }); - expect(db.checkoutTask(taskId, runId1)).toBe(true); + expect(db.tasks.checkoutTask(taskId, runId1)).toBe(true); // Second agent creates a run but cannot check out - const agent2Id = db.saveAgent({ + const agent2Id = db.agents.saveAgent({ companyId: s.companyId, name: "Agent2", role: "IC", @@ -296,7 +296,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { workspaceId: s.workspaceId, connectorId: s.connectorId, }); - expect(db.checkoutTask(taskId, runId2)).toBe(false); + expect(db.tasks.checkoutTask(taskId, runId2)).toBe(false); }); it("budget gate blocks run when policy exhausted", () => { @@ -311,7 +311,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { }); // Create and complete a run that exceeds the budget - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId: s.companyId, title: "Expensive Task", description: @@ -330,7 +330,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { connectorId: s.connectorId, }); - db.checkoutTask(taskId, runId); + db.tasks.checkoutTask(taskId, runId); db.markRunStatus(runId, "running", "Starting"); db.recordCostEntry({ runId, companyId: s.companyId, agentId: s.agentId, taskId, connectorId: s.connectorId, amountUsd: 1.5, unattributed: false }); db.finishRun({ @@ -363,7 +363,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { logger: mockLogger, }); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId: s.companyId, title: "Multi-Run Task", description: @@ -375,7 +375,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { }); // Create session - const sessionId = db.createNewSession({ + const sessionId = db.tasks.createNewSession({ companyId: s.companyId, agentId: s.agentId, adapterType: "gemini-cli", @@ -402,7 +402,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { sessionService.accumulateTokenUsage(sessionId, 3000, 1000, runId2); // Verify accumulated session state - const session = db.getSessionById(sessionId); + const session = db.tasks.getSessionById(sessionId); expect(session).not.toBeNull(); expect(session!.runCount).toBe(2); expect(session!.totalInputTokens).toBe(8000); @@ -421,7 +421,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { }); // --- First run: costs $3, budget allows --- - const taskId1 = db.saveTask({ + const taskId1 = db.tasks.saveTask({ companyId: s.companyId, title: "First Pipeline Task", description: @@ -439,7 +439,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { workspaceId: s.workspaceId, connectorId: s.connectorId, }); - db.checkoutTask(taskId1, runId1); + db.tasks.checkoutTask(taskId1, runId1); db.markRunStatus(runId1, "running", "Starting"); db.recordCostEntry({ runId: runId1, companyId: s.companyId, agentId: s.agentId, taskId: taskId1, connectorId: s.connectorId, amountUsd: 3.0, unattributed: false }); db.finishRun({ @@ -462,7 +462,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { expect(afterFirst.allowed).toBe(true); // --- Second run: costs $3, now total = $6 > $5 budget --- - const taskId2 = db.saveTask({ + const taskId2 = db.tasks.saveTask({ companyId: s.companyId, title: "Second Pipeline Task", description: @@ -480,7 +480,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => { workspaceId: s.workspaceId, connectorId: s.connectorId, }); - db.checkoutTask(taskId2, runId2); + db.tasks.checkoutTask(taskId2, runId2); db.markRunStatus(runId2, "running", "Starting"); db.recordCostEntry({ runId: runId2, companyId: s.companyId, agentId: s.agentId, taskId: taskId2, connectorId: s.connectorId, amountUsd: 3.0, unattributed: false }); db.finishRun({ diff --git a/tests/integration/database.spec.ts b/tests/integration/database.spec.ts index a10d95a5..ec45e4bf 100644 --- a/tests/integration/database.spec.ts +++ b/tests/integration/database.spec.ts @@ -52,7 +52,7 @@ describe("AppDatabase", () => { isPrimary: true, }); - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "Helios", role: "CEO", @@ -83,7 +83,7 @@ describe("AppDatabase", () => { status: "in_progress", }); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, projectId, goalId, @@ -194,7 +194,7 @@ describe("AppDatabase", () => { isPrimary: true, }); - const alphaAgentId = db.saveAgent({ + const alphaAgentId = db.agents.saveAgent({ companyId: alphaCompanyId, name: "Alpha CEO", role: "CEO", @@ -208,7 +208,7 @@ describe("AppDatabase", () => { budgetMonthlyUsd: 100, }); - db.saveAgent({ + db.agents.saveAgent({ companyId: betaCompanyId, name: "Beta CEO", role: "CEO", @@ -222,7 +222,7 @@ describe("AppDatabase", () => { budgetMonthlyUsd: 100, }); - db.saveTask({ + db.tasks.saveTask({ companyId: alphaCompanyId, title: "Alpha task", description: "Alpha work", @@ -232,7 +232,7 @@ describe("AppDatabase", () => { status: "todo", }); - db.saveTask({ + db.tasks.saveTask({ companyId: betaCompanyId, title: "Beta task", description: "Beta work", diff --git a/tests/integration/document-run-provenance.spec.ts b/tests/integration/document-run-provenance.spec.ts index 8f853ef2..466c5784 100644 --- a/tests/integration/document-run-provenance.spec.ts +++ b/tests/integration/document-run-provenance.spec.ts @@ -47,7 +47,7 @@ async function createHarness() { repoRef: "", isPrimary: true, }); - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "Research Lead", role: "Researcher", @@ -60,7 +60,7 @@ async function createHarness() { capabilities: "Research and synthesis", budgetMonthlyUsd: 180, }); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, title: "Refresh the market report", description: "Update the report with new findings and submit it for review.", @@ -69,7 +69,7 @@ async function createHarness() { priority: "high", status: "in_progress", }); - db.setTaskType(taskId, "research"); + db.tasks.setTaskType(taskId, "research"); const existingDocumentId = db.saveDocument({ companyId, @@ -172,7 +172,7 @@ describe("document run provenance", () => { // ai_ceo + top-level CEO without `reviewDeliverables` advances // straight to `done` — there is no reviewer in that configuration // so the prior `in_review` state was a dead letter. - const task = harness.db.getTask(harness.taskId); + const task = harness.db.entityCrud.getTask(harness.taskId); expect(task.status).toBe("done"); } finally { harness.server.stop(); diff --git a/tests/integration/onboarding-model-pool.spec.ts b/tests/integration/onboarding-model-pool.spec.ts index 73601396..fc50a6b2 100644 --- a/tests/integration/onboarding-model-pool.spec.ts +++ b/tests/integration/onboarding-model-pool.spec.ts @@ -52,8 +52,8 @@ function buildKickoffPayload(db: AppDatabase, summary: { companyId: string; lead throw new Error("Missing lead agent or task in onboarding summary."); } - const agent = db.getAgent(summary.leadAgentId); - const task = db.getTask(summary.taskId); + const agent = db.entityCrud.getAgent(summary.leadAgentId); + const task = db.entityCrud.getTask(summary.taskId); const connector = db.getConnector(agent.connectorId); const workspace = summary.workspaceId ? db.getWorkspace(summary.workspaceId) : null; const company = db.listSnapshot().companies.find((entry) => entry.id === summary.companyId); @@ -125,7 +125,7 @@ describe("onboarding model pool integration", () => { throw new Error("Expected lead agent id from onboarding bootstrap."); } - const agent = db.getAgent(summary.leadAgentId); + const agent = db.entityCrud.getAgent(summary.leadAgentId); expect(agent.connectorId).toBe("openai-codex"); expect(agent.model).toBe(enabledModel); expect(db.getDisabledModels(summary.companyId, "openai-codex")).toEqual([{ id: disabledModel, label: disabledModel }]); @@ -230,7 +230,7 @@ describe("onboarding model pool integration", () => { throw new Error("Expected lead agent id from onboarding bootstrap."); } - const agent = db.getAgent(summary.leadAgentId); + const agent = db.entityCrud.getAgent(summary.leadAgentId); expect(agent.model).toBeNull(); const payload = buildKickoffPayload(db, summary); diff --git a/tests/integration/process-recovery.spec.ts b/tests/integration/process-recovery.spec.ts index 5e0a5f68..dddebf8f 100644 --- a/tests/integration/process-recovery.spec.ts +++ b/tests/integration/process-recovery.spec.ts @@ -79,7 +79,7 @@ function scaffold(db: AppDatabase): ScaffoldResult { }); // Agent - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "RecoveryAgent", role: "IC", @@ -145,7 +145,7 @@ function createAndStartRun( ): { runId: string; taskId: string } { const taskId = opts.taskId ?? - db.saveTask({ + db.tasks.saveTask({ companyId: opts.companyId, title: opts.taskTitle ?? "Test Task", description: @@ -165,7 +165,7 @@ function createAndStartRun( connectorId: opts.connectorId, }); - db.checkoutTask(taskId, runId); + db.tasks.checkoutTask(taskId, runId); db.markRunStatus(runId, "running", "Starting execution"); if (opts.claimWorkspace !== false) { @@ -243,7 +243,7 @@ describe("Process Recovery -- Worker Crash Detection", () => { const s = scaffold(db); // Create a second agent for a second run - const agent2Id = db.saveAgent({ + const agent2Id = db.agents.saveAgent({ companyId: s.companyId, name: "Agent2", role: "IC", @@ -306,7 +306,7 @@ describe("Process Recovery -- Worker Crash Detection", () => { // However, after crash, the task is set to "blocked" and a subsequent // stale lock recovery pass handles the checkout_run_id. // For this test, verify the task status is blocked (the recovery mechanism). - const task = db.getTask(taskId); + const task = db.entityCrud.getTask(taskId); expect(task.status).toBe("blocked"); }); @@ -320,11 +320,11 @@ describe("Process Recovery -- Worker Crash Detection", () => { }); // Verify agent is running before crash - expect(db.getAgent(s.agentId).status).toBe("running"); + expect(db.entityCrud.getAgent(s.agentId).status).toBe("running"); simulateWorkerCrash(db); - expect(db.getAgent(s.agentId).status).toBe("idle"); + expect(db.entityCrud.getAgent(s.agentId).status).toBe("idle"); }); it("releases workspace locks after crash", () => { @@ -356,7 +356,7 @@ describe("Process Recovery -- Worker Crash Detection", () => { expect(wsAfter.lock_holder_run_id).toBeNull(); // Verify we can re-claim the workspace (lock is truly released) - const agent2Id = db.saveAgent({ + const agent2Id = db.agents.saveAgent({ companyId: s.companyId, name: "Agent2", role: "IC", @@ -369,7 +369,7 @@ describe("Process Recovery -- Worker Crash Detection", () => { capabilities: "Testing", budgetMonthlyUsd: 100, }); - const taskId2 = db.saveTask({ + const taskId2 = db.tasks.saveTask({ companyId: s.companyId, title: "Recovery Task", description: "A recovery task after crash to verify workspace lock is released properly.", @@ -398,12 +398,12 @@ describe("Process Recovery -- Worker Crash Detection", () => { }); // Verify task is in_progress before crash - expect(db.getTask(taskId).status).toBe("in_progress"); + expect(db.entityCrud.getTask(taskId).status).toBe("in_progress"); simulateWorkerCrash(db); // Task should be blocked (prevents heartbeat re-queuing) - expect(db.getTask(taskId).status).toBe("blocked"); + expect(db.entityCrud.getTask(taskId).status).toBe("blocked"); }); }); @@ -422,12 +422,12 @@ describe("Process Recovery -- Context Preservation (D-02)", () => { }); // Verify assignment before crash - expect(db.getTask(taskId).assigneeAgentId).toBe(s.agentId); + expect(db.entityCrud.getTask(taskId).assigneeAgentId).toBe(s.agentId); simulateWorkerCrash(db); // Agent assignment must be preserved for retry context - expect(db.getTask(taskId).assigneeAgentId).toBe(s.agentId); + expect(db.entityCrud.getTask(taskId).assigneeAgentId).toBe(s.agentId); }); it("session handoff markdown generated on crash", () => { @@ -440,7 +440,7 @@ describe("Process Recovery -- Context Preservation (D-02)", () => { }); // Create a task session (simulating what happens during normal execution) - const sessionId = db.createNewSession({ + const sessionId = db.tasks.createNewSession({ companyId: s.companyId, agentId: s.agentId, adapterType: "gemini-cli", @@ -448,7 +448,7 @@ describe("Process Recovery -- Context Preservation (D-02)", () => { // Drive the session past the maxRuns=20 threshold so rotation fires. for (let i = 0; i < 20; i++) { - db.incrementSessionUsage(sessionId, 1000, 500, `${runId}-${i}`); + db.tasks.incrementSessionUsage(sessionId, 1000, 500, `${runId}-${i}`); } const sessionService = createSessionManagementService({ @@ -456,7 +456,7 @@ describe("Process Recovery -- Context Preservation (D-02)", () => { logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, }); - const session = db.getSessionById(sessionId); + const session = db.tasks.getSessionById(sessionId); expect(session).not.toBeNull(); const rotationResult = sessionService.evaluateRotation(session!); @@ -499,7 +499,7 @@ describe("Process Recovery -- Multiple Concurrent Runs", () => { const agents = [s.agentId]; for (let i = 2; i <= 3; i++) { agents.push( - db.saveAgent({ + db.agents.saveAgent({ companyId: s.companyId, name: `Agent${i}`, role: "IC", @@ -540,7 +540,7 @@ describe("Process Recovery -- Multiple Concurrent Runs", () => { const agents = [s.agentId]; for (let i = 2; i <= 3; i++) { agents.push( - db.saveAgent({ + db.agents.saveAgent({ companyId: s.companyId, name: `Agent${i}`, role: "IC", @@ -571,7 +571,7 @@ describe("Process Recovery -- Multiple Concurrent Runs", () => { // All tasks should be blocked (crash recovery sets them to blocked) for (const run of runs) { - const task = db.getTask(run.taskId); + const task = db.entityCrud.getTask(run.taskId); expect(task.status).toBe("blocked"); } diff --git a/tests/integration/skill-aware-assignment.spec.ts b/tests/integration/skill-aware-assignment.spec.ts index ea8f8303..bf74bd27 100644 --- a/tests/integration/skill-aware-assignment.spec.ts +++ b/tests/integration/skill-aware-assignment.spec.ts @@ -46,7 +46,7 @@ describe("skill-aware capability registry", () => { isPrimary: true, }); - db.saveAgent({ + db.agents.saveAgent({ companyId, name: "Generic Engineer", role: "Engineer", @@ -59,7 +59,7 @@ describe("skill-aware capability registry", () => { capabilities: "General software delivery", budgetMonthlyUsd: 200, }); - const styledEngineerId = db.saveAgent({ + const styledEngineerId = db.agents.saveAgent({ companyId, name: "Frontend Specialist", role: "Engineer", @@ -94,7 +94,7 @@ describe("skill-aware capability registry", () => { }); db.assignSkillToAgent(styledEngineerId, skillId); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, title: "Build React dashboard widget", description: "Create an accessible React component with hooks and Tailwind styling.", @@ -103,7 +103,7 @@ describe("skill-aware capability registry", () => { status: "todo", }); - const assignedId = db.autoAssignTask(taskId, companyId); + const assignedId = db.tasks.autoAssignTask(taskId, companyId); expect(assignedId).toBe(styledEngineerId); db.close(); @@ -135,7 +135,7 @@ describe("skill-aware capability registry", () => { repoRef: "", isPrimary: true, }); - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "CEO", role: "CEO", @@ -171,7 +171,7 @@ describe("skill-aware capability registry", () => { const snapshot = db.listSnapshot(); const prompt = buildTaskPrompt({ - agent: db.getAgent(agentId), + agent: db.entityCrud.getAgent(agentId), apiUrl: "http://127.0.0.1:3210", apiKey: "token", runId: "run-1", diff --git a/tests/integration/thread-wait-heartbeat.spec.ts b/tests/integration/thread-wait-heartbeat.spec.ts index bbf497a9..1777d16a 100644 --- a/tests/integration/thread-wait-heartbeat.spec.ts +++ b/tests/integration/thread-wait-heartbeat.spec.ts @@ -50,7 +50,7 @@ function createScaffold(db: AppDatabase) { repoRef: "", isPrimary: true, }); - const ceoId = db.saveAgent({ + const ceoId = db.agents.saveAgent({ companyId, name: "CEO", role: "CEO", @@ -63,7 +63,7 @@ function createScaffold(db: AppDatabase) { capabilities: "Lead the company", budgetMonthlyUsd: 250, }); - const engineerId = db.saveAgent({ + const engineerId = db.agents.saveAgent({ companyId, name: "Engineer", role: "Engineer", @@ -76,7 +76,7 @@ function createScaffold(db: AppDatabase) { capabilities: "Ship features", budgetMonthlyUsd: 180, }); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, title: "Ship the feature", description: "Implement the approved deliverable and wait for co-founder review.", @@ -133,7 +133,7 @@ describe("thread wait + heartbeat integration", () => { expect(waitStates[0]?.waitingFor).toBe("user_reply"); expect(checkpointEngine.shouldPauseHeartbeat(companyId, engineerId)).toBe(true); - const otherAgentId = db.saveAgent({ + const otherAgentId = db.agents.saveAgent({ companyId, name: "Marketer", role: "CMO", @@ -141,7 +141,7 @@ describe("thread wait + heartbeat integration", () => { status: "idle", reportsTo: null, connectorId: "openai-codex", - workspaceId: db.getTask(taskId).workspaceId ?? null, + workspaceId: db.entityCrud.getTask(taskId).workspaceId ?? null, model: null, capabilities: "Drive marketing work", budgetMonthlyUsd: 180, diff --git a/tests/integration/work-products-lifecycle.spec.ts b/tests/integration/work-products-lifecycle.spec.ts index af4783ce..d841cbe4 100644 --- a/tests/integration/work-products-lifecycle.spec.ts +++ b/tests/integration/work-products-lifecycle.spec.ts @@ -66,7 +66,7 @@ async function createHarness(): Promise { repoRef: "", isPrimary: true, }); - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "Artifact Engineer", role: "Engineer", @@ -79,7 +79,7 @@ async function createHarness(): Promise { capabilities: "Writes files", budgetMonthlyUsd: 100, }); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, title: "Produce artifact", description: "", diff --git a/tests/unit/agent-budget-policy-sync.test.ts b/tests/unit/agent-budget-policy-sync.test.ts index 23898bbc..7cc5a949 100644 --- a/tests/unit/agent-budget-policy-sync.test.ts +++ b/tests/unit/agent-budget-policy-sync.test.ts @@ -42,7 +42,7 @@ describe("agent budget policy synchronization", () => { repoRef: "", isPrimary: true, }); - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "Engineer", role: "Engineer", @@ -70,7 +70,7 @@ describe("agent budget policy synchronization", () => { isActive: true, }); - db.saveAgent({ + db.agents.saveAgent({ id: agentId, companyId, name: "Engineer", diff --git a/tests/unit/agent-hire-routing.test.ts b/tests/unit/agent-hire-routing.test.ts index dd6b44b6..92497da0 100644 --- a/tests/unit/agent-hire-routing.test.ts +++ b/tests/unit/agent-hire-routing.test.ts @@ -43,7 +43,7 @@ describe("hire routing guards", () => { repoRef: "", isPrimary: true, }); - const ceoId = db.saveAgent({ + const ceoId = db.agents.saveAgent({ companyId, name: "CEO", role: "CEO", @@ -57,7 +57,7 @@ describe("hire routing guards", () => { budgetMonthlyUsd: 250, department: "executive", }); - const hrId = db.saveAgent({ + const hrId = db.agents.saveAgent({ companyId, name: "HR Lead", role: "HR Lead", @@ -71,7 +71,7 @@ describe("hire routing guards", () => { budgetMonthlyUsd: 180, department: "hr", }); - const ctoId = db.saveAgent({ + const ctoId = db.agents.saveAgent({ companyId, name: "CTO", role: "CTO", diff --git a/tests/unit/agent-permissions.test.ts b/tests/unit/agent-permissions.test.ts index 59c9976b..d508ec54 100644 --- a/tests/unit/agent-permissions.test.ts +++ b/tests/unit/agent-permissions.test.ts @@ -44,7 +44,7 @@ describe("canManageOrganization", () => { isPrimary: true, }); - const ceoId = db.saveAgent({ + const ceoId = db.agents.saveAgent({ companyId, name: "CEO", role: "CEO", @@ -57,7 +57,7 @@ describe("canManageOrganization", () => { capabilities: "Lead the company", budgetMonthlyUsd: 250, }); - const ctoId = db.saveAgent({ + const ctoId = db.agents.saveAgent({ companyId, name: "CTO", role: "CTO", @@ -71,7 +71,7 @@ describe("canManageOrganization", () => { budgetMonthlyUsd: 220, department: "engineering", }); - const engineerId = db.saveAgent({ + const engineerId = db.agents.saveAgent({ companyId, name: "Engineer", role: "Engineer", @@ -113,7 +113,7 @@ describe("canManageOrganization", () => { repoRef: "", isPrimary: true, }); - const ceoId = db.saveAgent({ + const ceoId = db.agents.saveAgent({ companyId, name: "CEO", role: "CEO", @@ -126,7 +126,7 @@ describe("canManageOrganization", () => { capabilities: "Lead the company", budgetMonthlyUsd: 250, }); - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "Operations Lead", role: "Specialist", @@ -142,7 +142,7 @@ describe("canManageOrganization", () => { }); expect(canManageOrganization(db, companyId, agentId)).toBe(false); - db.setAgentPermission(agentId, companyId, "organization_manage", true); + db.agents.setAgentPermission(agentId, companyId, "organization_manage", true); expect(canManageOrganization(db, companyId, agentId)).toBe(true); expect(canRequestHire(db, companyId, agentId)).toBe(true); @@ -167,7 +167,7 @@ describe("canManageOrganization", () => { repoRef: "", isPrimary: true, }); - const ceoId = db.saveAgent({ + const ceoId = db.agents.saveAgent({ companyId, name: "CEO", role: "CEO", @@ -180,7 +180,7 @@ describe("canManageOrganization", () => { capabilities: "Lead the company", budgetMonthlyUsd: 250, }); - const ctoId = db.saveAgent({ + const ctoId = db.agents.saveAgent({ companyId, name: "CTO", role: "CTO", @@ -194,7 +194,7 @@ describe("canManageOrganization", () => { budgetMonthlyUsd: 220, department: "engineering", }); - const cmoId = db.saveAgent({ + const cmoId = db.agents.saveAgent({ companyId, name: "CMO", role: "CMO", @@ -208,7 +208,7 @@ describe("canManageOrganization", () => { budgetMonthlyUsd: 220, department: "marketing", }); - const ctoReportId = db.saveAgent({ + const ctoReportId = db.agents.saveAgent({ companyId, name: "Engineer", role: "Engineer", @@ -223,7 +223,7 @@ describe("canManageOrganization", () => { department: "engineering", }); - const cmoTaskId = db.saveTask({ + const cmoTaskId = db.tasks.saveTask({ companyId, title: "Launch campaign", description: "Owned by the marketing branch", @@ -242,7 +242,7 @@ describe("canManageOrganization", () => { expect(canManageProject(db, companyId, ctoId, cmoId)).toBe(false); expect(canManageGoal(db, companyId, ceoId, cmoId)).toBe(true); expect(canManageProject(db, companyId, ceoId, cmoId)).toBe(true); - expect(db.getTask(cmoTaskId).assigneeAgentId).toBe(cmoId); + expect(db.entityCrud.getTask(cmoTaskId).assigneeAgentId).toBe(cmoId); db.close(); }); diff --git a/tests/unit/approval-hire-routing.test.ts b/tests/unit/approval-hire-routing.test.ts index f7d7d19a..9a5ae78f 100644 --- a/tests/unit/approval-hire-routing.test.ts +++ b/tests/unit/approval-hire-routing.test.ts @@ -44,7 +44,7 @@ describe("hire approval routing", () => { repoRef: "", isPrimary: true, }); - const ceoId = db.saveAgent({ + const ceoId = db.agents.saveAgent({ companyId, name: "CEO", role: "CEO", @@ -57,7 +57,7 @@ describe("hire approval routing", () => { capabilities: "Lead the company", budgetMonthlyUsd: 250, }); - const ctoId = db.saveAgent({ + const ctoId = db.agents.saveAgent({ companyId, name: "CTO", role: "CTO", @@ -116,7 +116,7 @@ describe("hire approval routing", () => { trackTimer: vi.fn(), }, approvalId, companyId, "approved"); - expect(db.getAgent(hireId).status).toBe("idle"); + expect(db.entityCrud.getAgent(hireId).status).toBe("idle"); expect(dispatchAutomation).toHaveBeenCalledWith("approval_resolved", expect.objectContaining({ companyId, agentId: ctoId })); expect(dispatchAutomation).toHaveBeenCalledWith("hire_approved", expect.objectContaining({ companyId, agentId: hireId })); expect(wakeAgentIfPossible).toHaveBeenCalledWith(hireId, companyId, "assignment"); diff --git a/tests/unit/approval-review-feedback-writeback.test.ts b/tests/unit/approval-review-feedback-writeback.test.ts index 649f592e..b735f00e 100644 --- a/tests/unit/approval-review-feedback-writeback.test.ts +++ b/tests/unit/approval-review-feedback-writeback.test.ts @@ -44,7 +44,7 @@ describe("deliverable review feedback writeback", () => { repoRef: "", isPrimary: true, }); - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "Research Lead", role: "Researcher", @@ -58,7 +58,7 @@ describe("deliverable review feedback writeback", () => { budgetMonthlyUsd: 180, department: "research", }); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, title: "Draft market report", description: "Produce a market report with concrete evidence and business insight.", @@ -116,7 +116,7 @@ describe("deliverable review feedback writeback", () => { expect(knowledgeEntries[0]?.content).toContain("revision_requested"); expect(knowledgeEntries[0]?.content).toContain("stronger competitor evidence"); expect(wakeAgentIfPossible).toHaveBeenCalledWith(agentId, companyId, "revision_needed"); - expect(db.getTask(taskId).status).toBe("in_progress"); + expect(db.entityCrud.getTask(taskId).status).toBe("in_progress"); const document = db.listSnapshot().documents.find((entry) => entry.id === documentId); expect(document?.status).toBe("draft"); diff --git a/tests/unit/atomic-checkout.test.ts b/tests/unit/atomic-checkout.test.ts index 88101177..2430bea2 100644 --- a/tests/unit/atomic-checkout.test.ts +++ b/tests/unit/atomic-checkout.test.ts @@ -56,7 +56,7 @@ function createTestWorkspace(db: AppDatabase, companyId: string): string { } function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string, name = "Agent"): string { - return db.saveAgent({ + return db.agents.saveAgent({ companyId, name, role: "IC", @@ -72,7 +72,7 @@ function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string } function createTestTask(db: AppDatabase, companyId: string, agentId: string | null, workspaceId: string): string { - return db.saveTask({ + return db.tasks.saveTask({ companyId, title: "Test Task", description: "A test task for checkout", @@ -117,10 +117,10 @@ describe("CAS checkout", () => { connectorId: "openai-codex", }); - const result = db.checkoutTask(taskId, runId); + const result = db.tasks.checkoutTask(taskId, runId); expect(result).toBe(true); - const task = db.getTask(taskId); + const task = db.entityCrud.getTask(taskId); expect(task.status).toBe("in_progress"); }); @@ -146,8 +146,8 @@ describe("CAS checkout", () => { connectorId: "openai-codex", }); - expect(db.checkoutTask(taskId, runId1)).toBe(true); - expect(db.checkoutTask(taskId, runId2)).toBe(false); + expect(db.tasks.checkoutTask(taskId, runId1)).toBe(true); + expect(db.tasks.checkoutTask(taskId, runId2)).toBe(false); }); it("CAS checkout rejects already-in-progress tasks", () => { @@ -168,7 +168,7 @@ describe("CAS checkout", () => { connectorId: "openai-codex", }); - expect(db.checkoutTask(taskId, runId)).toBe(false); + expect(db.tasks.checkoutTask(taskId, runId)).toBe(false); }); it("lock release in finishRun clears checkout fields", () => { @@ -186,7 +186,7 @@ describe("CAS checkout", () => { connectorId: "openai-codex", }); - db.checkoutTask(taskId, runId); + db.tasks.checkoutTask(taskId, runId); db.markRunStatus(runId, "running", ""); db.finishRun({ runId, @@ -222,11 +222,11 @@ describe("CAS checkout", () => { connectorId: "openai-codex", }); - db.checkoutTask(taskId, runId); + db.tasks.checkoutTask(taskId, runId); db.markRunStatus(runId, "running", ""); // Deleting the agent triggers cancelRunForDeletion for active runs - db.deleteAgent(agentId, companyId); + db.agents.deleteAgent(agentId, companyId); const row = (db as unknown as RawDb).db .prepare("SELECT checkout_run_id, execution_locked_at FROM tasks WHERE id = ?") @@ -251,7 +251,7 @@ describe("CAS checkout", () => { connectorId: "openai-codex", }); - db.checkoutTask(taskId, runId); + db.tasks.checkoutTask(taskId, runId); // Backdate execution_locked_at to 20 minutes ago const backdated = new Date(Date.now() - 20 * 60 * 1000).toISOString(); @@ -265,7 +265,7 @@ describe("CAS checkout", () => { expect(recovered[0].taskId).toBe(taskId); expect(recovered[0].companyId).toBe(companyId); - const task = db.getTask(taskId); + const task = db.entityCrud.getTask(taskId); expect(task.status).toBe("todo"); const row = (db as unknown as RawDb).db @@ -291,12 +291,12 @@ describe("CAS checkout", () => { connectorId: "openai-codex", }); - db.checkoutTask(taskId, runId); + db.tasks.checkoutTask(taskId, runId); const recovered = db.recoverStaleLocks(); expect(recovered.length).toBe(0); - const task = db.getTask(taskId); + const task = db.entityCrud.getTask(taskId); expect(task.status).toBe("in_progress"); }); @@ -323,8 +323,8 @@ describe("CAS checkout", () => { workspaceId, connectorId: "openai-codex", }); - db.checkoutTask(taskId, runId); - expect(db.getTask(taskId).status).toBe("in_progress"); + db.tasks.checkoutTask(taskId, runId); + expect(db.entityCrud.getTask(taskId).status).toBe("in_progress"); // Never call markRunStatus('running', ...) — this simulates the // "workspace already locked" bail-out inside dispatchRunToWorker. @@ -339,7 +339,7 @@ describe("CAS checkout", () => { unattributedCost: true, }); - expect(db.getTask(taskId).status).toBe("todo"); + expect(db.entityCrud.getTask(taskId).status).toBe("todo"); // And the next CAS checkout must succeed — proving the heartbeat // retry path is unblocked. @@ -350,7 +350,7 @@ describe("CAS checkout", () => { workspaceId, connectorId: "openai-codex", }); - expect(db.checkoutTask(taskId, retryRunId)).toBe(true); + expect(db.tasks.checkoutTask(taskId, retryRunId)).toBe(true); }); it("cancelled run that never started also rolls task back to 'todo'", () => { @@ -371,7 +371,7 @@ describe("CAS checkout", () => { workspaceId, connectorId: "openai-codex", }); - db.checkoutTask(taskId, runId); + db.tasks.checkoutTask(taskId, runId); db.finishRun({ runId, status: "cancelled", @@ -383,7 +383,7 @@ describe("CAS checkout", () => { unattributedCost: true, }); - expect(db.getTask(taskId).status).toBe("todo"); + expect(db.entityCrud.getTask(taskId).status).toBe("todo"); }); it("failed run that did start stays 'in_progress' (human retry path)", () => { @@ -404,7 +404,7 @@ describe("CAS checkout", () => { workspaceId, connectorId: "openai-codex", }); - db.checkoutTask(taskId, runId); + db.tasks.checkoutTask(taskId, runId); db.markRunStatus(runId, "running", ""); db.finishRun({ runId, @@ -417,7 +417,7 @@ describe("CAS checkout", () => { unattributedCost: false, }); - expect(db.getTask(taskId).status).toBe("in_progress"); + expect(db.entityCrud.getTask(taskId).status).toBe("in_progress"); }); it("advanceTaskStatus('in_review') rejects a CEO self-review that has no reviewer", () => { @@ -433,9 +433,9 @@ describe("CAS checkout", () => { const ceoId = createTestAgent(db, companyId, workspaceId, "CEO"); const taskId = createTestTask(db, companyId, ceoId, workspaceId); - expect(() => db.advanceTaskStatus(taskId, "in_review")).toThrow(/no reviewer/); + expect(() => db.tasks.advanceTaskStatus(taskId, "in_review")).toThrow(/no reviewer/); // Task status must remain unchanged after the rejected transition. - expect(db.getTask(taskId).status).toBe("todo"); + expect(db.entityCrud.getTask(taskId).status).toBe("todo"); }); it("advanceTaskStatus('in_review') accepts a CEO assignee flagged requires_user_review=1", () => { @@ -445,10 +445,10 @@ describe("CAS checkout", () => { const workspaceId = createTestWorkspace(db, companyId); const ceoId = createTestAgent(db, companyId, workspaceId, "CEO"); const taskId = createTestTask(db, companyId, ceoId, workspaceId); - db.setTaskRequiresUserReview(taskId, true); + db.tasks.setTaskRequiresUserReview(taskId, true); - db.advanceTaskStatus(taskId, "in_review"); - expect(db.getTask(taskId).status).toBe("in_review"); + db.tasks.advanceTaskStatus(taskId, "in_review"); + expect(db.entityCrud.getTask(taskId).status).toBe("in_review"); }); it("interruptStaleRuns surfaces shutdown-era running runs without startup task rewrites", () => { @@ -472,7 +472,7 @@ describe("CAS checkout", () => { workspaceId, connectorId: "openai-codex", }); - db.checkoutTask(taskId, liveRunId); + db.tasks.checkoutTask(taskId, liveRunId); db.markRunStatus(liveRunId, "running", ""); db.close(); @@ -480,6 +480,6 @@ describe("CAS checkout", () => { db.init(); expect(db.lastInterruptedCount).toBe(1); - expect(db.getTask(taskId).status).toBe("blocked"); + expect(db.entityCrud.getTask(taskId).status).toBe("blocked"); }); }); diff --git a/tests/unit/company-mode-switch.test.ts b/tests/unit/company-mode-switch.test.ts index 2bbde445..7f879e6a 100644 --- a/tests/unit/company-mode-switch.test.ts +++ b/tests/unit/company-mode-switch.test.ts @@ -56,7 +56,7 @@ describe("company mode switching", () => { repoRef: "", isPrimary: true, }); - const ceoId = db.saveAgent({ + const ceoId = db.agents.saveAgent({ companyId, name: "CEO", role: "CEO", @@ -69,7 +69,7 @@ describe("company mode switching", () => { capabilities: "Lead the company", budgetMonthlyUsd: 250, }); - const engineerId = db.saveAgent({ + const engineerId = db.agents.saveAgent({ companyId, name: "Engineer", role: "Engineer", @@ -83,7 +83,7 @@ describe("company mode switching", () => { budgetMonthlyUsd: 180, department: "engineering", }); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, title: "Ship feature", description: "Waits for cofounder review.", @@ -100,7 +100,7 @@ describe("company mode switching", () => { threadIntegration.onTaskCreated(taskId, companyId); threadIntegration.onRunFinished("run-1", taskId, engineerId, companyId, "succeeded", null); - db.setTaskRequiresUserReview(taskId, true); + db.tasks.setTaskRequiresUserReview(taskId, true); const approvalId = db.requestApproval({ companyId, relatedTaskId: taskId, @@ -113,7 +113,7 @@ describe("company mode switching", () => { threadIntegration.onApprovalCreated(approvalId, companyId, taskId, "deliverable_review", "Review deliverable"); expect(db.listThreadWaitStates(companyId).length).toBeGreaterThan(0); - expect(db.getTask(taskId).requiresUserReview).toBe(true); + expect(db.entityCrud.getTask(taskId).requiresUserReview).toBe(true); expect(db.getApprovalRecord(approvalId).state).toBe("pending"); const handlers = new Map(); @@ -149,7 +149,7 @@ describe("company mode switching", () => { expect(result.ok).toBe(true); expect(db.getCompanyExecutionMode(companyId)).toBe("ai_ceo"); expect(db.listThreadWaitStates(companyId)).toHaveLength(0); - expect(db.getTask(taskId).requiresUserReview).toBe(false); + expect(db.entityCrud.getTask(taskId).requiresUserReview).toBe(false); expect(db.getApprovalRecord(approvalId).state).toBe("approved"); db.close(); diff --git a/tests/unit/database.test.ts b/tests/unit/database.test.ts index 8c0f9fa6..e12bd70d 100644 --- a/tests/unit/database.test.ts +++ b/tests/unit/database.test.ts @@ -47,7 +47,7 @@ describe("AppDatabase", () => { repoRef: "main", isPrimary: true, }); - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "Chief Operator", role: "CEO", @@ -76,7 +76,7 @@ describe("AppDatabase", () => { status: "in_progress", targetDate: null, }); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, projectId, goalId, @@ -181,7 +181,7 @@ describe("AppDatabase", () => { repoRef: "main", isPrimary: true, }); - const leadId = db.saveAgent({ + const leadId = db.agents.saveAgent({ companyId, name: "Lead", role: "CEO", @@ -200,7 +200,7 @@ describe("AppDatabase", () => { envBindingText: "", notes: "Test connector", }); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, title: "Claim me", description: "First agent wins the checkout.", @@ -225,10 +225,10 @@ describe("AppDatabase", () => { connectorId: "openai-codex", }); - expect(db.checkoutTask(taskId, runId1)).toBe(true); - expect(db.checkoutTask(taskId, runId2)).toBe(false); + expect(db.tasks.checkoutTask(taskId, runId1)).toBe(true); + expect(db.tasks.checkoutTask(taskId, runId2)).toBe(false); - const task = db.getTask(taskId); + const task = db.entityCrud.getTask(taskId); expect(task.status).toBe("in_progress"); }); @@ -246,7 +246,7 @@ describe("AppDatabase", () => { repoRef: "main", isPrimary: true, }); - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "Budget Owner", role: "CTO", @@ -259,7 +259,7 @@ describe("AppDatabase", () => { capabilities: "Ships work", budgetMonthlyUsd: 10, }); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, title: "Spend budget", description: "One run should exhaust the allowance.", @@ -300,8 +300,8 @@ describe("AppDatabase", () => { }); expect(outcome.budgetHardStopped).toBe(true); - expect(db.getAgent(agentId).status).toBe("paused"); - expect(db.getAgent(agentId).spentMonthlyUsd).toBe(12); + expect(db.entityCrud.getAgent(agentId).status).toBe("paused"); + expect(db.entityCrud.getAgent(agentId).spentMonthlyUsd).toBe(12); }); it("activates pending hires when the corresponding approval is granted", () => { @@ -310,7 +310,7 @@ describe("AppDatabase", () => { description: "Approval-driven hiring", status: "active", }); - const managerId = db.saveAgent({ + const managerId = db.agents.saveAgent({ companyId, name: "Manager", role: "CEO", @@ -323,7 +323,7 @@ describe("AppDatabase", () => { capabilities: "Manages", budgetMonthlyUsd: 100, }); - const pendingHireId = db.saveAgent({ + const pendingHireId = db.agents.saveAgent({ companyId, name: "Pending Hire", role: "IC", @@ -353,7 +353,7 @@ describe("AppDatabase", () => { decisionNote: "Approved", }); - expect(db.getAgent(pendingHireId).status).toBe("idle"); + expect(db.entityCrud.getAgent(pendingHireId).status).toBe("idle"); }); it("detects pending-approval agents via isAgentPendingApproval", () => { @@ -362,7 +362,7 @@ describe("AppDatabase", () => { description: "Approval gate validation", status: "active", }); - const activeAgentId = db.saveAgent({ + const activeAgentId = db.agents.saveAgent({ companyId, name: "Active Agent", role: "CTO", @@ -375,7 +375,7 @@ describe("AppDatabase", () => { capabilities: "Ships", budgetMonthlyUsd: 100, }); - const pendingAgentId = db.saveAgent({ + const pendingAgentId = db.agents.saveAgent({ companyId, name: "Pending Agent", role: "IC", @@ -389,8 +389,8 @@ describe("AppDatabase", () => { budgetMonthlyUsd: 50, }); - expect(db.isAgentPendingApproval(activeAgentId)).toBe(false); - expect(db.isAgentPendingApproval(pendingAgentId)).toBe(true); + expect(db.agents.isAgentPendingApproval(activeAgentId)).toBe(false); + expect(db.agents.isAgentPendingApproval(pendingAgentId)).toBe(true); }); it("runs forward-only migrations and creates schema_migrations table", () => { @@ -479,7 +479,7 @@ describe("AppDatabase", () => { repoRef: "", isPrimary: true, }); - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "Purge Agent", role: "IC", @@ -492,7 +492,7 @@ describe("AppDatabase", () => { capabilities: "test", budgetMonthlyUsd: 100, }); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, title: "Purge task", description: "test", @@ -542,7 +542,7 @@ describe("AppDatabase", () => { description: "test", status: "active", }); - const managerId = db.saveAgent({ + const managerId = db.agents.saveAgent({ companyId, name: "Manager", role: "CEO", @@ -555,7 +555,7 @@ describe("AppDatabase", () => { capabilities: "manage", budgetMonthlyUsd: 100, }); - const hireId = db.saveAgent({ + const hireId = db.agents.saveAgent({ companyId, name: "New Hire", role: "IC", diff --git a/tests/unit/edge-case-budget.test.ts b/tests/unit/edge-case-budget.test.ts index fbdc0368..a92e7ae7 100644 --- a/tests/unit/edge-case-budget.test.ts +++ b/tests/unit/edge-case-budget.test.ts @@ -39,7 +39,7 @@ function scaffold(db: AppDatabase) { repoRef: "", isPrimary: true, }); - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "Worker Agent", role: "developer", @@ -68,7 +68,7 @@ function scaffold(db: AppDatabase) { status: "in_progress", targetDate: null, }); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, projectId, goalId, @@ -135,11 +135,11 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { workspaceId, connectorId: "gemini-cli", }); - db.checkoutTask(taskId, run1.id); + db.tasks.checkoutTask(taskId, run1.id); db.markRunStatus(run1.id, "running", ""); // Create a second task and run that exhausts budget WHILE Run-1 is in-flight - const taskId2 = db.saveTask({ + const taskId2 = db.tasks.saveTask({ companyId, title: "Other Task", description: "Another task that uses up budget", @@ -155,7 +155,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { workspaceId, connectorId: "gemini-cli", }); - db.checkoutTask(taskId2, run0.id); + db.tasks.checkoutTask(taskId2, run0.id); db.markRunStatus(run0.id, "running", ""); // Complete Run-0 with $6 cost (exceeds $5 budget) db.recordCostEntry({ runId: run0.id, companyId, agentId, taskId: taskId2, connectorId: "gemini-cli", amountUsd: 6.0, unattributed: false }); @@ -221,7 +221,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { workspaceId, connectorId: "gemini-cli", }); - db.checkoutTask(taskId, run.id); + db.tasks.checkoutTask(taskId, run.id); db.markRunStatus(run.id, "running", ""); db.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 6.0, unattributed: false }); db.finishRun({ @@ -269,7 +269,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { workspaceId, connectorId: "gemini-cli", }); - db.checkoutTask(taskId, run.id); + db.tasks.checkoutTask(taskId, run.id); db.markRunStatus(run.id, "running", ""); db.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 6.0, unattributed: false }); db.finishRun({ @@ -326,7 +326,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { workspaceId, connectorId: "gemini-cli", }); - db.checkoutTask(taskId, run.id); + db.tasks.checkoutTask(taskId, run.id); db.markRunStatus(run.id, "running", ""); db.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 1.5, unattributed: false }); db.finishRun({ @@ -378,7 +378,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { workspaceId, connectorId: "gemini-cli", }); - db.checkoutTask(taskId, run.id); + db.tasks.checkoutTask(taskId, run.id); db.markRunStatus(run.id, "running", ""); db.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 4.0, unattributed: false }); db.finishRun({ @@ -424,7 +424,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { workspaceId, connectorId: "gemini-cli", }); - db.checkoutTask(taskId, run.id); + db.tasks.checkoutTask(taskId, run.id); db.markRunStatus(run.id, "running", ""); // Complete with cost that exceeds budget -- cost entry is still recorded diff --git a/tests/unit/edge-case-concurrent.test.ts b/tests/unit/edge-case-concurrent.test.ts index 0b98e830..631e7cfb 100644 --- a/tests/unit/edge-case-concurrent.test.ts +++ b/tests/unit/edge-case-concurrent.test.ts @@ -56,7 +56,7 @@ function createTestWorkspace(db: AppDatabase, companyId: string): string { } function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string, name = "Agent"): string { - return db.saveAgent({ + return db.agents.saveAgent({ companyId, name, role: "IC", @@ -72,7 +72,7 @@ function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string } function createTestTask(db: AppDatabase, companyId: string, workspaceId: string): string { - return db.saveTask({ + return db.tasks.saveTask({ companyId, title: "Concurrent Test Task", description: "A task for testing concurrent checkout edge cases", @@ -126,15 +126,15 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { }); // Agent-A claims first - const resultA = db.checkoutTask(taskId, runIdA); + const resultA = db.tasks.checkoutTask(taskId, runIdA); expect(resultA).toBe(true); // Agent-B tries to claim the same task - const resultB = db.checkoutTask(taskId, runIdB); + const resultB = db.tasks.checkoutTask(taskId, runIdB); expect(resultB).toBe(false); // Task status must be in_progress (set by first successful checkout) - const task = db.getTask(taskId); + const task = db.entityCrud.getTask(taskId); expect(task.status).toBe("in_progress"); }); @@ -162,9 +162,9 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { }); // Agent-A wins first task - expect(db.checkoutTask(taskId1, runIdA)).toBe(true); + expect(db.tasks.checkoutTask(taskId1, runIdA)).toBe(true); // Agent-B loses - expect(db.checkoutTask(taskId1, runIdB1)).toBe(false); + expect(db.tasks.checkoutTask(taskId1, runIdB1)).toBe(false); // Agent-B can still checkout a DIFFERENT task const taskId2 = createTestTask(db, companyId, workspaceId); @@ -175,11 +175,11 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { workspaceId, connectorId: "gemini-cli", }); - const resultB2 = db.checkoutTask(taskId2, runIdB2); + const resultB2 = db.tasks.checkoutTask(taskId2, runIdB2); expect(resultB2).toBe(true); // Verify second task is now in_progress - const task2 = db.getTask(taskId2); + const task2 = db.entityCrud.getTask(taskId2); expect(task2.status).toBe("in_progress"); }); @@ -199,7 +199,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { workspaceId, connectorId: "gemini-cli", }); - expect(db.checkoutTask(taskId, runIdA)).toBe(true); + expect(db.tasks.checkoutTask(taskId, runIdA)).toBe(true); // Task is now in_progress. A new run from Agent-B tries to checkout const { id: runIdB } = db.createRun({ @@ -209,11 +209,11 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { workspaceId, connectorId: "gemini-cli", }); - const result = db.checkoutTask(taskId, runIdB); + const result = db.tasks.checkoutTask(taskId, runIdB); expect(result).toBe(false); // Confirm status is still in_progress - expect(db.getTask(taskId).status).toBe("in_progress"); + expect(db.entityCrud.getTask(taskId).status).toBe("in_progress"); }); it("after lock release, CAS guard still checks task status", () => { @@ -232,10 +232,10 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { workspaceId, connectorId: "gemini-cli", }); - expect(db.checkoutTask(taskId, runIdA)).toBe(true); + expect(db.tasks.checkoutTask(taskId, runIdA)).toBe(true); // Release the lock (sets checkout_run_id = NULL) - db.releaseTaskLock(taskId, runIdA); + db.tasks.releaseTaskLock(taskId, runIdA); // Verify lock fields are cleared const row = (db as unknown as RawDb).db @@ -253,7 +253,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { workspaceId, connectorId: "gemini-cli", }); - const result = db.checkoutTask(taskId, runIdB); + const result = db.tasks.checkoutTask(taskId, runIdB); expect(result).toBe(false); }); @@ -271,7 +271,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { workspaceId, connectorId: "gemini-cli", }); - expect(db.checkoutTask(taskId, runIdA)).toBe(true); + expect(db.tasks.checkoutTask(taskId, runIdA)).toBe(true); // Backdate execution_locked_at to well past the stale timeout (db as unknown as RawDb).db @@ -307,7 +307,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { workspaceId, connectorId: "gemini-cli", }); - expect(db.checkoutTask(taskId, runIdA)).toBe(true); + expect(db.tasks.checkoutTask(taskId, runIdA)).toBe(true); // Backdate to make stale (db as unknown as RawDb).db @@ -319,7 +319,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { expect(recovered.length).toBe(1); // Verify task is back to "todo" - expect(db.getTask(taskId).status).toBe("todo"); + expect(db.entityCrud.getTask(taskId).status).toBe("todo"); // Agent-B can now checkout the recovered task const { id: runIdB } = db.createRun({ @@ -329,9 +329,9 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => { workspaceId, connectorId: "gemini-cli", }); - const result = db.checkoutTask(taskId, runIdB); + const result = db.tasks.checkoutTask(taskId, runIdB); expect(result).toBe(true); - expect(db.getTask(taskId).status).toBe("in_progress"); + expect(db.entityCrud.getTask(taskId).status).toBe("in_progress"); }); it("STALE_LOCK_TIMEOUT_MS matches expected 15 minute value", () => { diff --git a/tests/unit/edge-case-session.test.ts b/tests/unit/edge-case-session.test.ts index a4ef9bad..6ee66baa 100644 --- a/tests/unit/edge-case-session.test.ts +++ b/tests/unit/edge-case-session.test.ts @@ -43,7 +43,7 @@ function scaffold(db: AppDatabase, connectorId: ConnectorId = "gemini-cli") { repoRef: "", isPrimary: true, }); - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "Worker Agent", role: "developer", @@ -72,7 +72,7 @@ function scaffold(db: AppDatabase, connectorId: ConnectorId = "gemini-cli") { status: "in_progress", targetDate: null, }); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, projectId, goalId, @@ -114,7 +114,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { const sessionService = createSessionManagementService({ db, logger: mockLogger }); // Create a session - const sessionId = db.createNewSession({ + const sessionId = db.tasks.createNewSession({ companyId, agentId, adapterType: "gemini-cli", @@ -122,11 +122,11 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { // Simulate runs until we reach the maxRuns threshold (20) for (let i = 0; i < 20; i++) { - db.incrementSessionUsage(sessionId, 1000, 500, `run-${i}`); + db.tasks.incrementSessionUsage(sessionId, 1000, 500, `run-${i}`); } // Get the updated session from DB - const session = db.getSessionById(sessionId); + const session = db.tasks.getSessionById(sessionId); expect(session).not.toBeNull(); expect(session!.runCount).toBe(20); // matches maxRuns threshold in session-management-service @@ -140,16 +140,16 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { const { companyId, agentId, taskId } = scaffold(db); const sessionService = createSessionManagementService({ db, logger: mockLogger }); - const sessionId = db.createNewSession({ + const sessionId = db.tasks.createNewSession({ companyId, agentId, adapterType: "gemini-cli", }); // Accumulate tokens past the 500_000 maxTokens threshold in one big call - db.incrementSessionUsage(sessionId, 300000, 250000, "big-run"); + db.tasks.incrementSessionUsage(sessionId, 300000, 250000, "big-run"); - const session = db.getSessionById(sessionId); + const session = db.tasks.getSessionById(sessionId); expect(session).not.toBeNull(); const totalTokens = session!.totalInputTokens + session!.totalOutputTokens; expect(totalTokens).toBe(550000); @@ -163,7 +163,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { const { companyId, agentId, taskId, workspaceId } = scaffold(db); const sessionService = createSessionManagementService({ db, logger: mockLogger }); - const sessionId = db.createNewSession({ + const sessionId = db.tasks.createNewSession({ companyId, agentId, adapterType: "gemini-cli", @@ -171,7 +171,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { // Accumulate enough to trigger rotation for (let i = 0; i < 20; i++) { - db.incrementSessionUsage(sessionId, 1000, 500, `run-${i}`); + db.tasks.incrementSessionUsage(sessionId, 1000, 500, `run-${i}`); } // Create a completed run for history (so generateHandoff has run data) @@ -193,7 +193,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { unattributedCost: false, }); - const session = db.getSessionById(sessionId)!; + const session = db.tasks.getSessionById(sessionId)!; // generateHandoff is agent-scoped: it walks the agent's runs across // all tasks + taskless heartbeats, not a single task. const handoff = sessionService.generateHandoff(session); @@ -203,7 +203,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { expect(handoff).toContain("## Recent Run History"); // Archive the old session with handoff - db.archiveSession(sessionId, "max_runs", handoff); + db.tasks.archiveSession(sessionId, "max_runs", handoff); // Verify archived session has handoff stored const archivedRows = db.queryAll<{ handoffMarkdown: string | null }>( @@ -217,27 +217,27 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { const { companyId, agentId, taskId } = scaffold(db); // Create initial session - const oldSessionId = db.createNewSession({ + const oldSessionId = db.tasks.createNewSession({ companyId, agentId, adapterType: "gemini-cli", }); // Add usage to the old session - db.incrementSessionUsage(oldSessionId, 300000, 250000, "run-final"); + db.tasks.incrementSessionUsage(oldSessionId, 300000, 250000, "run-final"); // Archive the old session - db.archiveSession(oldSessionId, "max_tokens", "# Session Handoff\nPrevious work summary."); + db.tasks.archiveSession(oldSessionId, "max_tokens", "# Session Handoff\nPrevious work summary."); // Create new session - const newSessionId = db.createNewSession({ + const newSessionId = db.tasks.createNewSession({ companyId, agentId, adapterType: "gemini-cli", }); // Get the new active session - const newSession = db.getSessionById(newSessionId); + const newSession = db.tasks.getSessionById(newSessionId); expect(newSession).not.toBeNull(); expect(newSession!.id).toBe(newSessionId); expect(newSession!.id).not.toBe(oldSessionId); @@ -252,7 +252,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { const { companyId, agentId, taskId } = scaffold(db, "gemini-cli"); const sessionService = createSessionManagementService({ db, logger: mockLogger }); - const sessionId = db.createNewSession({ + const sessionId = db.tasks.createNewSession({ companyId, agentId, adapterType: "claude-pro", @@ -260,10 +260,10 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { // Accumulate way past all thresholds for (let i = 0; i < 25; i++) { - db.incrementSessionUsage(sessionId, 30000, 20000, `run-${i}`); + db.tasks.incrementSessionUsage(sessionId, 30000, 20000, `run-${i}`); } - const session = db.getSessionById(sessionId)!; + const session = db.tasks.getSessionById(sessionId)!; // Pi-mono delegates compaction to pi-agent-core; AgentCompany still applies rotation uniformly. const result = sessionService.evaluateRotation(session); expect(result.shouldRotate).toBe(true); @@ -274,7 +274,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { const { companyId, agentId, taskId } = scaffold(db, "gemini-cli"); const sessionService = createSessionManagementService({ db, logger: mockLogger }); - const sessionId = db.createNewSession({ + const sessionId = db.tasks.createNewSession({ companyId, agentId, adapterType: "openai-codex", @@ -282,10 +282,10 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { // Accumulate way past all thresholds for (let i = 0; i < 25; i++) { - db.incrementSessionUsage(sessionId, 30000, 20000, `run-${i}`); + db.tasks.incrementSessionUsage(sessionId, 30000, 20000, `run-${i}`); } - const session = db.getSessionById(sessionId)!; + const session = db.tasks.getSessionById(sessionId)!; // Pi-mono delegates compaction to pi-agent-core; AgentCompany still applies rotation uniformly. const result = sessionService.evaluateRotation(session); expect(result.shouldRotate).toBe(true); @@ -297,12 +297,12 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { const sessionService = createSessionManagementService({ db, logger: mockLogger }); // Create session and add usage - const sessionId = db.createNewSession({ + const sessionId = db.tasks.createNewSession({ companyId, agentId, adapterType: "gemini-cli", }); - db.incrementSessionUsage(sessionId, 300000, 200000, "run-1"); + db.tasks.incrementSessionUsage(sessionId, 300000, 200000, "run-1"); // Create a completed run for run history const run = db.createRun({ @@ -324,7 +324,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { }); // Rotate the session (archives old, creates new) - const oldSession = db.getSessionById(sessionId)!; + const oldSession = db.tasks.getSessionById(sessionId)!; sessionService.rotateSession(oldSession); // After rotation, the handoff should be retrievable for the next run diff --git a/tests/unit/failure-escalation.test.ts b/tests/unit/failure-escalation.test.ts index 6f039e56..ee6364f8 100644 --- a/tests/unit/failure-escalation.test.ts +++ b/tests/unit/failure-escalation.test.ts @@ -22,12 +22,16 @@ const RETRY_COOLDOWN_MS = 30_000; function createMockDeps(): EscalationDeps { return { db: { - getTaskRetryCount: vi.fn().mockReturnValue(0), - incrementTaskRetryCount: vi.fn(), - resetTaskRetryCount: vi.fn(), - getChainOfCommand: vi.fn().mockReturnValue([]), - getCeoForCompany: vi.fn().mockReturnValue(null), - advanceTaskStatus: vi.fn().mockReturnValue({ previousStatus: "in_progress" }), + tasks: { + getTaskRetryCount: vi.fn().mockReturnValue(0), + incrementTaskRetryCount: vi.fn(), + resetTaskRetryCount: vi.fn(), + advanceTaskStatus: vi.fn().mockReturnValue({ previousStatus: "in_progress" }), + }, + agents: { + getChainOfCommand: vi.fn().mockReturnValue([]), + getCeoForCompany: vi.fn().mockReturnValue(null), + }, }, wakeAgentIfPossible: vi.fn().mockReturnValue("wake-id"), notify: vi.fn(), @@ -58,15 +62,15 @@ describe("Failure Escalation Pipeline", () => { describe("Retry path (count < MAX_RETRY_ATTEMPTS)", () => { it("retries on first failure (retry_count=0)", () => { - (deps.db.getTaskRetryCount as ReturnType).mockReturnValue(0); + (deps.db.tasks.getTaskRetryCount as ReturnType).mockReturnValue(0); const result = handleFailureEscalation( deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry, ); expect(result.action).toBe("retry"); - expect(deps.db.incrementTaskRetryCount).toHaveBeenCalledWith("task-1"); - expect(deps.db.advanceTaskStatus).toHaveBeenCalledWith( + expect(deps.db.tasks.incrementTaskRetryCount).toHaveBeenCalledWith("task-1"); + expect(deps.db.tasks.advanceTaskStatus).toHaveBeenCalledWith( "task-1", "todo", expect.stringContaining("attempt 1/2"), @@ -75,15 +79,15 @@ describe("Failure Escalation Pipeline", () => { }); it("retries on second failure (retry_count=1)", () => { - (deps.db.getTaskRetryCount as ReturnType).mockReturnValue(1); + (deps.db.tasks.getTaskRetryCount as ReturnType).mockReturnValue(1); const result = handleFailureEscalation( deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry, ); expect(result.action).toBe("retry"); - expect(deps.db.incrementTaskRetryCount).toHaveBeenCalledWith("task-1"); - expect(deps.db.advanceTaskStatus).toHaveBeenCalledWith( + expect(deps.db.tasks.incrementTaskRetryCount).toHaveBeenCalledWith("task-1"); + expect(deps.db.tasks.advanceTaskStatus).toHaveBeenCalledWith( "task-1", "todo", expect.stringContaining("attempt 2/2"), @@ -92,7 +96,7 @@ describe("Failure Escalation Pipeline", () => { }); it("calls wakeAgentIfPossible via scheduleRetry callback", () => { - (deps.db.getTaskRetryCount as ReturnType).mockReturnValue(0); + (deps.db.tasks.getTaskRetryCount as ReturnType).mockReturnValue(0); handleFailureEscalation( deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry, @@ -107,8 +111,8 @@ describe("Failure Escalation Pipeline", () => { describe("Escalation path (count >= MAX_RETRY_ATTEMPTS)", () => { it("escalates to chain[0] manager after max retries", () => { - (deps.db.getTaskRetryCount as ReturnType).mockReturnValue(2); - (deps.db.getChainOfCommand as ReturnType).mockReturnValue([ + (deps.db.tasks.getTaskRetryCount as ReturnType).mockReturnValue(2); + (deps.db.agents.getChainOfCommand as ReturnType).mockReturnValue([ { id: "manager-1", name: "Manager Alpha" }, ]); @@ -117,32 +121,32 @@ describe("Failure Escalation Pipeline", () => { ); expect(result.action).toBe("escalated"); - expect(deps.db.resetTaskRetryCount).toHaveBeenCalledWith("task-1"); + expect(deps.db.tasks.resetTaskRetryCount).toHaveBeenCalledWith("task-1"); expect(deps.wakeAgentIfPossible).toHaveBeenCalledWith("manager-1", "company-1", "report_failed"); expect(deps.notify).not.toHaveBeenCalled(); }); it("falls through to CEO when chain is empty", () => { - (deps.db.getTaskRetryCount as ReturnType).mockReturnValue(2); - (deps.db.getChainOfCommand as ReturnType).mockReturnValue([]); - (deps.db.getCeoForCompany as ReturnType).mockReturnValue({ id: "ceo-1", name: "CEO Boss" }); + (deps.db.tasks.getTaskRetryCount as ReturnType).mockReturnValue(2); + (deps.db.agents.getChainOfCommand as ReturnType).mockReturnValue([]); + (deps.db.agents.getCeoForCompany as ReturnType).mockReturnValue({ id: "ceo-1", name: "CEO Boss" }); const result = handleFailureEscalation( deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry, ); expect(result.action).toBe("escalated"); - expect(deps.db.resetTaskRetryCount).toHaveBeenCalledWith("task-1"); + expect(deps.db.tasks.resetTaskRetryCount).toHaveBeenCalledWith("task-1"); expect(deps.wakeAgentIfPossible).toHaveBeenCalledWith("ceo-1", "company-1", "report_failed"); expect(deps.notify).not.toHaveBeenCalled(); }); it("falls through to CEO when chain lookup throws", () => { - (deps.db.getTaskRetryCount as ReturnType).mockReturnValue(2); - (deps.db.getChainOfCommand as ReturnType).mockImplementation(() => { + (deps.db.tasks.getTaskRetryCount as ReturnType).mockReturnValue(2); + (deps.db.agents.getChainOfCommand as ReturnType).mockImplementation(() => { throw new Error("Agent deleted"); }); - (deps.db.getCeoForCompany as ReturnType).mockReturnValue({ id: "ceo-1", name: "CEO Boss" }); + (deps.db.agents.getCeoForCompany as ReturnType).mockReturnValue({ id: "ceo-1", name: "CEO Boss" }); const result = handleFailureEscalation( deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry, @@ -153,9 +157,9 @@ describe("Failure Escalation Pipeline", () => { }); it("notifies user when both chain and CEO are unavailable", () => { - (deps.db.getTaskRetryCount as ReturnType).mockReturnValue(2); - (deps.db.getChainOfCommand as ReturnType).mockReturnValue([]); - (deps.db.getCeoForCompany as ReturnType).mockReturnValue(null); + (deps.db.tasks.getTaskRetryCount as ReturnType).mockReturnValue(2); + (deps.db.agents.getChainOfCommand as ReturnType).mockReturnValue([]); + (deps.db.agents.getCeoForCompany as ReturnType).mockReturnValue(null); const result = handleFailureEscalation( deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry, @@ -173,9 +177,9 @@ describe("Failure Escalation Pipeline", () => { }); it("uses taskId as fallback when task is null", () => { - (deps.db.getTaskRetryCount as ReturnType).mockReturnValue(2); - (deps.db.getChainOfCommand as ReturnType).mockReturnValue([]); - (deps.db.getCeoForCompany as ReturnType).mockReturnValue(null); + (deps.db.tasks.getTaskRetryCount as ReturnType).mockReturnValue(2); + (deps.db.agents.getChainOfCommand as ReturnType).mockReturnValue([]); + (deps.db.agents.getCeoForCompany as ReturnType).mockReturnValue(null); const result = handleFailureEscalation( deps, RUN, null, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry, @@ -191,19 +195,19 @@ describe("Failure Escalation Pipeline", () => { describe("Timed out runs follow same path", () => { it("retries timed_out runs the same as failed runs", () => { - (deps.db.getTaskRetryCount as ReturnType).mockReturnValue(0); + (deps.db.tasks.getTaskRetryCount as ReturnType).mockReturnValue(0); const result = handleFailureEscalation( deps, RUN, TASK, "timed_out", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry, ); expect(result.action).toBe("retry"); - expect(deps.db.incrementTaskRetryCount).toHaveBeenCalledWith("task-1"); + expect(deps.db.tasks.incrementTaskRetryCount).toHaveBeenCalledWith("task-1"); }); it("escalates timed_out runs after max retries", () => { - (deps.db.getTaskRetryCount as ReturnType).mockReturnValue(2); - (deps.db.getChainOfCommand as ReturnType).mockReturnValue([ + (deps.db.tasks.getTaskRetryCount as ReturnType).mockReturnValue(2); + (deps.db.agents.getChainOfCommand as ReturnType).mockReturnValue([ { id: "manager-1", name: "Manager Alpha" }, ]); @@ -212,18 +216,18 @@ describe("Failure Escalation Pipeline", () => { ); expect(result.action).toBe("escalated"); - expect(deps.db.resetTaskRetryCount).toHaveBeenCalledWith("task-1"); + expect(deps.db.tasks.resetTaskRetryCount).toHaveBeenCalledWith("task-1"); }); }); describe("Success resets retry count", () => { it("resets retry_count on successful completion", () => { handleSuccessReset(deps.db, "task-1"); - expect(deps.db.resetTaskRetryCount).toHaveBeenCalledWith("task-1"); + expect(deps.db.tasks.resetTaskRetryCount).toHaveBeenCalledWith("task-1"); }); it("handles reset errors gracefully", () => { - (deps.db.resetTaskRetryCount as ReturnType).mockImplementation(() => { + (deps.db.tasks.resetTaskRetryCount as ReturnType).mockImplementation(() => { throw new Error("DB error"); }); diff --git a/tests/unit/ipc-agent-graph-handlers.test.ts b/tests/unit/ipc-agent-graph-handlers.test.ts index 0b63fddf..b4183231 100644 --- a/tests/unit/ipc-agent-graph-handlers.test.ts +++ b/tests/unit/ipc-agent-graph-handlers.test.ts @@ -52,7 +52,7 @@ describe("ipc-core-agent-graph-handlers model enforcement", () => { repoRef: "", isPrimary: true, }); - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "CEO", role: "CEO", diff --git a/tests/unit/message-dispatch.test.ts b/tests/unit/message-dispatch.test.ts index 233a6b28..5ab6bba9 100644 --- a/tests/unit/message-dispatch.test.ts +++ b/tests/unit/message-dispatch.test.ts @@ -41,7 +41,7 @@ function createScaffold(db: AppDatabase) { repoRef: "", isPrimary: true, }); - const ceoId = db.saveAgent({ + const ceoId = db.agents.saveAgent({ companyId, name: "CEO", role: "CEO", @@ -54,7 +54,7 @@ function createScaffold(db: AppDatabase) { capabilities: "Lead the company", budgetMonthlyUsd: 250, }); - const ctoId = db.saveAgent({ + const ctoId = db.agents.saveAgent({ companyId, name: "CTO", role: "CTO", @@ -68,7 +68,7 @@ function createScaffold(db: AppDatabase) { budgetMonthlyUsd: 200, department: "engineering", }); - const cmoId = db.saveAgent({ + const cmoId = db.agents.saveAgent({ companyId, name: "CMO", role: "CMO", @@ -82,7 +82,7 @@ function createScaffold(db: AppDatabase) { budgetMonthlyUsd: 200, department: "marketing", }); - const pendingId = db.saveAgent({ + const pendingId = db.agents.saveAgent({ companyId, name: "Pending Recruit", role: "IC", @@ -96,7 +96,7 @@ function createScaffold(db: AppDatabase) { budgetMonthlyUsd: 100, department: "engineering", }); - const terminatedId = db.saveAgent({ + const terminatedId = db.agents.saveAgent({ companyId, name: "Former COO", role: "COO", @@ -111,7 +111,7 @@ function createScaffold(db: AppDatabase) { department: "operations", }); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, title: "Review architecture plan", description: "Need board feedback in the thread.", diff --git a/tests/unit/message-search.test.ts b/tests/unit/message-search.test.ts index 5a93a8b7..8fd4c604 100644 --- a/tests/unit/message-search.test.ts +++ b/tests/unit/message-search.test.ts @@ -32,7 +32,7 @@ describe("AppDatabase.searchMessages", () => { status: "active", }); - agentA = db.saveAgent({ + agentA = db.agents.saveAgent({ companyId, name: "Agent Alpha", role: "Engineer", @@ -46,7 +46,7 @@ describe("AppDatabase.searchMessages", () => { budgetMonthlyUsd: 100, }); - agentB = db.saveAgent({ + agentB = db.agents.saveAgent({ companyId, name: "Agent Beta", role: "Designer", diff --git a/tests/unit/session-management.test.ts b/tests/unit/session-management.test.ts index f5fd9012..e7d31005 100644 --- a/tests/unit/session-management.test.ts +++ b/tests/unit/session-management.test.ts @@ -34,7 +34,7 @@ function scaffold(db: AppDatabase) { repoRef: "", isPrimary: true, }); - const agentId = db.saveAgent({ + const agentId = db.agents.saveAgent({ companyId, name: "Worker Agent", role: "developer", @@ -63,7 +63,7 @@ function scaffold(db: AppDatabase) { status: "in_progress", targetDate: null, }); - const taskId = db.saveTask({ + const taskId = db.tasks.saveTask({ companyId, projectId, goalId, @@ -96,19 +96,19 @@ describe("task_sessions CRUD", () => { }); it("getSessionById returns null for an unknown id", () => { - const session = db.getSessionById("nonexistent"); + const session = db.tasks.getSessionById("nonexistent"); expect(session).toBeNull(); }); it("getSessionById returns TaskSessionRecord when valid session exists", () => { const { companyId, agentId } = scaffold(db); - const sessionId = db.createNewSession({ + const sessionId = db.tasks.createNewSession({ companyId, agentId, adapterType: "claude-pro", }); - const session = db.getSessionById(sessionId); + const session = db.tasks.getSessionById(sessionId); expect(session).not.toBeNull(); expect(session!.id).toBe(sessionId); expect(session!.companyId).toBe(companyId); @@ -124,23 +124,23 @@ describe("task_sessions CRUD", () => { it("incrementSessionUsage atomically updates counters", () => { const { companyId, agentId } = scaffold(db); - const sessionId = db.createNewSession({ + const sessionId = db.tasks.createNewSession({ companyId, agentId, adapterType: "claude-pro", }); - db.incrementSessionUsage(sessionId, 1000, 500, "run-001"); + db.tasks.incrementSessionUsage(sessionId, 1000, 500, "run-001"); - const session = db.getSessionById(sessionId); + const session = db.tasks.getSessionById(sessionId); expect(session!.runCount).toBe(1); expect(session!.totalInputTokens).toBe(1000); expect(session!.totalOutputTokens).toBe(500); expect(session!.lastRunId).toBe("run-001"); // Second increment accumulates - db.incrementSessionUsage(sessionId, 2000, 800, "run-002"); - const session2 = db.getSessionById(sessionId); + db.tasks.incrementSessionUsage(sessionId, 2000, 800, "run-002"); + const session2 = db.tasks.getSessionById(sessionId); expect(session2!.runCount).toBe(2); expect(session2!.totalInputTokens).toBe(3000); expect(session2!.totalOutputTokens).toBe(1300); @@ -149,15 +149,15 @@ describe("task_sessions CRUD", () => { it("archiveSession sets rotated_at, rotation_reason, handoff_markdown", () => { const { companyId, agentId } = scaffold(db); - const sessionId = db.createNewSession({ + const sessionId = db.tasks.createNewSession({ companyId, agentId, adapterType: "claude-pro", }); - db.archiveSession(sessionId, "max_runs", "# Handoff\nSession reached 20 runs."); + db.tasks.archiveSession(sessionId, "max_runs", "# Handoff\nSession reached 20 runs."); - const session = db.getSessionById(sessionId); + const session = db.tasks.getSessionById(sessionId); expect(session!.rotatedAt).toBeTruthy(); expect(session!.rotationReason).toBe("max_runs"); expect(session!.handoffMarkdown).toBe("# Handoff\nSession reached 20 runs."); @@ -166,7 +166,7 @@ describe("task_sessions CRUD", () => { it("createNewSession returns an id; adapter type persists", () => { const { companyId, agentId } = scaffold(db); - const sessionId = db.createNewSession({ + const sessionId = db.tasks.createNewSession({ companyId, agentId, adapterType: "gemini-cli", @@ -175,7 +175,7 @@ describe("task_sessions CRUD", () => { expect(typeof sessionId).toBe("string"); expect(sessionId.length).toBeGreaterThan(0); - const session = db.getSessionById(sessionId); + const session = db.tasks.getSessionById(sessionId); expect(session).not.toBeNull(); expect(session!.id).toBe(sessionId); expect(session!.adapterType).toBe("gemini-cli"); @@ -209,7 +209,7 @@ describe("task_sessions CRUD", () => { unattributedCost: false, }); - const runs = db.getRecentRunsForTask(taskId, 5); + const runs = db.tasks.getRecentRunsForTask(taskId, 5); expect(runs.length).toBe(2); // Most recent first expect(runs[0].id).toBe(run2.id); @@ -401,7 +401,7 @@ describe("session management service", () => { describe("token accumulation", () => { it("accumulateTokenUsage calls db.incrementSessionUsage with correct args", () => { const { taskId, companyId, agentId } = scaffold(db); - const sessionId = db.createNewSession({ + const sessionId = db.tasks.createNewSession({ companyId, agentId, adapterType: "claude-pro", @@ -410,7 +410,7 @@ describe("session management service", () => { svc.accumulateTokenUsage(sessionId, 5000, 2000, "run-abc"); - const session = db.getSessionById(sessionId); + const session = db.tasks.getSessionById(sessionId); expect(session!.runCount).toBe(1); expect(session!.totalInputTokens).toBe(5000); expect(session!.totalOutputTokens).toBe(2000); @@ -424,7 +424,7 @@ describe("session management service", () => { describe("handoff generation", () => { it("generateHandoff produces markdown with task title and run history", () => { const { taskId, companyId, agentId, workspaceId } = scaffold(db); - const sessionId = db.createNewSession({ + const sessionId = db.tasks.createNewSession({ companyId, agentId, adapterType: "gemini-cli", @@ -444,9 +444,9 @@ describe("session management service", () => { }); // Increment session usage to have stats - db.incrementSessionUsage(sessionId, 10000, 5000, run.id); + db.tasks.incrementSessionUsage(sessionId, 10000, 5000, run.id); - const session = db.getSessionById(sessionId)!; + const session = db.tasks.getSessionById(sessionId)!; const svc = createSessionManagementService({ db, logger: mockLogger }); // Handoff is now agent-scoped: it walks the agent's recent runs // (across tasks + heartbeats) rather than a single task. The task @@ -468,16 +468,16 @@ describe("session management service", () => { describe("session rotation", () => { it("rotateSession archives old session, creates new, copies session_params", () => { const { taskId, companyId, agentId } = scaffold(db); - const oldSessionId = db.createNewSession({ + const oldSessionId = db.tasks.createNewSession({ companyId, agentId, adapterType: "gemini-cli", }); // Add some usage - db.incrementSessionUsage(oldSessionId, 300000, 200000, "run-10"); + db.tasks.incrementSessionUsage(oldSessionId, 300000, 200000, "run-10"); - const oldSession = db.getSessionById(oldSessionId)!; + const oldSession = db.tasks.getSessionById(oldSessionId)!; const svc = createSessionManagementService({ db, logger: mockLogger }); const newSessionId = svc.rotateSession(oldSession); @@ -493,7 +493,7 @@ describe("session management service", () => { expect(archivedRows[0].handoffMarkdown).toContain("# Session Handoff"); // New session is a distinct row with fresh counters. - const newSession = db.getSessionById(newSessionId)!; + const newSession = db.tasks.getSessionById(newSessionId)!; expect(newSession.id).toBe(newSessionId); expect(newSession.runCount).toBe(0); expect(newSession.totalInputTokens).toBe(0); @@ -510,13 +510,13 @@ describe("session management service", () => { const svc = createSessionManagementService({ db, logger: mockLogger }); // Create and rotate a session - const session1Id = db.createNewSession({ + const session1Id = db.tasks.createNewSession({ companyId, agentId, adapterType: "gemini-cli", }); - db.incrementSessionUsage(session1Id, 100000, 50000, "run-1"); - const session1 = db.getSessionById(session1Id)!; + db.tasks.incrementSessionUsage(session1Id, 100000, 50000, "run-1"); + const session1 = db.tasks.getSessionById(session1Id)!; svc.rotateSession(session1); const handoff = svc.getHandoffContext(taskId); @@ -526,7 +526,7 @@ describe("session management service", () => { it("getHandoffContext returns null when no rotated sessions exist", () => { const { taskId, companyId, agentId } = scaffold(db); - db.createNewSession({ + db.tasks.createNewSession({ companyId, agentId, adapterType: "gemini-cli", @@ -544,16 +544,16 @@ describe("session management service", () => { describe("usage delta", () => { it("getUsageDelta returns token difference between snapshot and current totals", () => { const { taskId, companyId, agentId } = scaffold(db); - const sessionId = db.createNewSession({ + const sessionId = db.tasks.createNewSession({ companyId, agentId, adapterType: "gemini-cli", }); // Pre-populate some usage - db.incrementSessionUsage(sessionId, 1000, 500, "run-1"); + db.tasks.incrementSessionUsage(sessionId, 1000, 500, "run-1"); - const session = db.getSessionById(sessionId)!; + const session = db.tasks.getSessionById(sessionId)!; const svc = createSessionManagementService({ db, logger: mockLogger }); // Snapshot current state diff --git a/tests/unit/task-quality-gates-sweep.test.ts b/tests/unit/task-quality-gates-sweep.test.ts index 812742e5..27cb614c 100644 --- a/tests/unit/task-quality-gates-sweep.test.ts +++ b/tests/unit/task-quality-gates-sweep.test.ts @@ -14,11 +14,15 @@ describe("sweepStaleReviews", () => { function makeSweepDeps(overrides: Partial = {}): SweepStaleReviewsDeps { return { db: { - listReviewTasksForSweep: vi.fn().mockReturnValue([]), + tasks: { + listReviewTasksForSweep: vi.fn().mockReturnValue([]), + advanceTaskStatus: vi.fn().mockReturnValue({ previousStatus: "in_review" }), + }, + agents: { + getManagerForAgent: vi.fn().mockReturnValue(null), + getChainOfCommand: vi.fn().mockReturnValue([]), + }, findPendingApprovalForTask: vi.fn().mockReturnValue(null), - getManagerForAgent: vi.fn().mockReturnValue(null), - getChainOfCommand: vi.fn().mockReturnValue([]), - advanceTaskStatus: vi.fn().mockReturnValue({ previousStatus: "in_review" }), } as unknown as AppDatabase, getCompanyExecutionMode: vi.fn().mockReturnValue("ai_ceo"), dispatchAgentMessage: vi.fn().mockReturnValue({ ok: true }), @@ -34,20 +38,27 @@ describe("sweepStaleReviews", () => { }; } - it("escalates to manager's manager when task in_review for 35 minutes", () => { - const deps = makeSweepDeps(); - const dbMock = deps.db as unknown as { + type SweepDbMock = { + tasks: { listReviewTasksForSweep: ReturnType; - findPendingApprovalForTask: ReturnType; + advanceTaskStatus: ReturnType; + }; + agents: { getManagerForAgent: ReturnType; getChainOfCommand: ReturnType; }; + findPendingApprovalForTask: ReturnType; + }; - dbMock.listReviewTasksForSweep.mockReturnValueOnce([ + it("escalates to manager's manager when task in_review for 35 minutes", () => { + const deps = makeSweepDeps(); + const dbMock = deps.db as unknown as SweepDbMock; + + dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([ { id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" }, ]); - dbMock.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager One", companyId: "company-1" }); - dbMock.getChainOfCommand.mockReturnValueOnce([{ id: "higher-manager-1", name: "Higher Manager" }]); + dbMock.agents.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager One", companyId: "company-1" }); + dbMock.agents.getChainOfCommand.mockReturnValueOnce([{ id: "higher-manager-1", name: "Higher Manager" }]); const count = sweepStaleReviews("company-1", deps, NOW); expect(count).toBe(1); @@ -66,9 +77,9 @@ describe("sweepStaleReviews", () => { it("does not escalate when task in_review for only 25 minutes", () => { const deps = makeSweepDeps(); - const dbMock = deps.db as unknown as { listReviewTasksForSweep: ReturnType }; + const dbMock = deps.db as unknown as SweepDbMock; - dbMock.listReviewTasksForSweep.mockReturnValueOnce([ + dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([ { id: "task-1", assigneeAgentId: "agent-1", updatedAt: TWENTY_FIVE_MIN_AGO, title: "Build login" }, ]); @@ -80,19 +91,14 @@ describe("sweepStaleReviews", () => { it("notifies the human when the reviewer has no higher manager", () => { const deps = makeSweepDeps(); - const dbMock = deps.db as unknown as { - listReviewTasksForSweep: ReturnType; - findPendingApprovalForTask: ReturnType; - getManagerForAgent: ReturnType; - getChainOfCommand: ReturnType; - }; + const dbMock = deps.db as unknown as SweepDbMock; - dbMock.listReviewTasksForSweep.mockReturnValueOnce([ + dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([ { id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" }, ]); dbMock.findPendingApprovalForTask.mockReturnValueOnce(null); - dbMock.getManagerForAgent.mockReturnValueOnce({ id: "ceo-1", name: "CEO", companyId: "company-1" }); - dbMock.getChainOfCommand.mockReturnValueOnce([]); + dbMock.agents.getManagerForAgent.mockReturnValueOnce({ id: "ceo-1", name: "CEO", companyId: "company-1" }); + dbMock.agents.getChainOfCommand.mockReturnValueOnce([]); const count = sweepStaleReviews("company-1", deps, NOW); expect(count).toBe(1); @@ -109,18 +115,13 @@ describe("sweepStaleReviews", () => { it("auto-approves pending approval linked to a stale review task", () => { const deps = makeSweepDeps(); - const dbMock = deps.db as unknown as { - listReviewTasksForSweep: ReturnType; - findPendingApprovalForTask: ReturnType; - getManagerForAgent: ReturnType; - getChainOfCommand: ReturnType; - }; + const dbMock = deps.db as unknown as SweepDbMock; - dbMock.listReviewTasksForSweep.mockReturnValueOnce([ + dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([ { id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" }, ]); - dbMock.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager", companyId: "company-1" }); - dbMock.getChainOfCommand.mockReturnValueOnce([{ id: "higher-1", name: "Higher" }]); + dbMock.agents.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager", companyId: "company-1" }); + dbMock.agents.getChainOfCommand.mockReturnValueOnce([{ id: "higher-1", name: "Higher" }]); dbMock.findPendingApprovalForTask.mockReturnValueOnce({ id: "approval-1", type: "deliverable_review" }); sweepStaleReviews("company-1", deps, NOW); @@ -139,18 +140,13 @@ describe("sweepStaleReviews", () => { it("swallows resolveApprovalDecision races gracefully", () => { const deps = makeSweepDeps(); - const dbMock = deps.db as unknown as { - listReviewTasksForSweep: ReturnType; - findPendingApprovalForTask: ReturnType; - getManagerForAgent: ReturnType; - getChainOfCommand: ReturnType; - }; + const dbMock = deps.db as unknown as SweepDbMock; - dbMock.listReviewTasksForSweep.mockReturnValueOnce([ + dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([ { id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" }, ]); - dbMock.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager", companyId: "company-1" }); - dbMock.getChainOfCommand.mockReturnValueOnce([{ id: "higher-1", name: "Higher" }]); + dbMock.agents.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager", companyId: "company-1" }); + dbMock.agents.getChainOfCommand.mockReturnValueOnce([{ id: "higher-1", name: "Higher" }]); dbMock.findPendingApprovalForTask.mockReturnValueOnce({ id: "approval-1", type: "deliverable_review" }); (deps.resolveApprovalDecision as ReturnType).mockImplementationOnce(() => { throw new Error("Approval already decided"); @@ -161,24 +157,18 @@ describe("sweepStaleReviews", () => { it("advances task status to done after escalation", () => { const deps = makeSweepDeps(); - const dbMock = deps.db as unknown as { - listReviewTasksForSweep: ReturnType; - findPendingApprovalForTask: ReturnType; - getManagerForAgent: ReturnType; - getChainOfCommand: ReturnType; - advanceTaskStatus: ReturnType; - }; + const dbMock = deps.db as unknown as SweepDbMock; - dbMock.listReviewTasksForSweep.mockReturnValueOnce([ + dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([ { id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" }, ]); - dbMock.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager", companyId: "company-1" }); - dbMock.getChainOfCommand.mockReturnValueOnce([{ id: "higher-1", name: "Higher" }]); + dbMock.agents.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager", companyId: "company-1" }); + dbMock.agents.getChainOfCommand.mockReturnValueOnce([{ id: "higher-1", name: "Higher" }]); dbMock.findPendingApprovalForTask.mockReturnValueOnce(null); sweepStaleReviews("company-1", deps, NOW); - expect(dbMock.advanceTaskStatus).toHaveBeenCalledWith( + expect(dbMock.tasks.advanceTaskStatus).toHaveBeenCalledWith( "task-1", "done", "Auto-advanced: review timeout exceeded 30 minutes.", @@ -188,9 +178,9 @@ describe("sweepStaleReviews", () => { it("skips tasks without an assignee", () => { const deps = makeSweepDeps(); - const dbMock = deps.db as unknown as { listReviewTasksForSweep: ReturnType }; + const dbMock = deps.db as unknown as SweepDbMock; - dbMock.listReviewTasksForSweep.mockReturnValueOnce([ + dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([ { id: "task-1", assigneeAgentId: null, updatedAt: THIRTY_FIVE_MIN_AGO, title: "Orphan task" }, ]); @@ -208,9 +198,9 @@ describe("sweepStaleReviews", () => { const deps = makeSweepDeps({ getCompanyExecutionMode: vi.fn().mockReturnValue("ai_cofounder"), }); - const dbMock = deps.db as unknown as { listReviewTasksForSweep: ReturnType }; + const dbMock = deps.db as unknown as SweepDbMock; - dbMock.listReviewTasksForSweep.mockReturnValueOnce([ + dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([ { id: "task-1", assigneeAgentId: "agent-1", updatedAt: TWENTY_FIVE_MIN_AGO, title: "Review me" }, ]); @@ -219,24 +209,20 @@ describe("sweepStaleReviews", () => { expect(deps.notify).not.toHaveBeenCalled(); expect(deps.resolveApprovalDecision).not.toHaveBeenCalled(); expect(deps.dispatchAgentMessage).not.toHaveBeenCalled(); - expect(deps.db.advanceTaskStatus).not.toHaveBeenCalled(); + expect(dbMock.tasks.advanceTaskStatus).not.toHaveBeenCalled(); }); it("sends a critical cofounder review reminder without auto-approving", () => { const deps = makeSweepDeps({ getCompanyExecutionMode: vi.fn().mockReturnValue("ai_cofounder"), }); - const dbMock = deps.db as unknown as { - listReviewTasksForSweep: ReturnType; - findPendingApprovalForTask: ReturnType; - getManagerForAgent: ReturnType; - }; + const dbMock = deps.db as unknown as SweepDbMock; - dbMock.listReviewTasksForSweep.mockReturnValueOnce([ + dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([ { id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Review me" }, ]); dbMock.findPendingApprovalForTask.mockReturnValueOnce({ id: "approval-1", type: "deliverable_review" }); - dbMock.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager", companyId: "company-1" }); + dbMock.agents.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager", companyId: "company-1" }); const count = sweepStaleReviews("company-1", deps, NOW); expect(count).toBe(0); @@ -250,6 +236,6 @@ describe("sweepStaleReviews", () => { ); expect(deps.resolveApprovalDecision).not.toHaveBeenCalled(); expect(deps.dispatchAgentMessage).not.toHaveBeenCalled(); - expect(deps.db.advanceTaskStatus).not.toHaveBeenCalled(); + expect(dbMock.tasks.advanceTaskStatus).not.toHaveBeenCalled(); }); }); diff --git a/tests/unit/task-quality-gates.test.ts b/tests/unit/task-quality-gates.test.ts index a15daa58..074bd527 100644 --- a/tests/unit/task-quality-gates.test.ts +++ b/tests/unit/task-quality-gates.test.ts @@ -151,7 +151,9 @@ describe("ACCEPTANCE_CRITERIA_KEYWORDS", () => { describe("checkCapabilityMatch", () => { function makeMockDb(agent: { name: string; capabilities: string }, skillAssignments: string[] = [], skills: Record = {}) { return { - getAgent: (_id: string) => agent, + entityCrud: { + getAgent: (_id: string) => agent, + }, listAgentSkillAssignments: (_id: string) => skillAssignments, getCompanySkill: (_companyId: string, skillId: string) => skills[skillId] ?? null, }; diff --git a/tests/unit/tasks-phase.test.ts b/tests/unit/tasks-phase.test.ts index d4eadc95..9be638f0 100644 --- a/tests/unit/tasks-phase.test.ts +++ b/tests/unit/tasks-phase.test.ts @@ -55,7 +55,7 @@ function createTestWorkspace(db: AppDatabase, companyId: string): string { } function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string, name = "Agent"): string { - return db.saveAgent({ + return db.agents.saveAgent({ companyId, name, role: "IC", @@ -76,7 +76,7 @@ function createTestTask( agentId: string, workspaceId: string, ): string { - return db.saveTask({ + return db.tasks.saveTask({ companyId, title: "Phase helper task", description: "dual-write seed", diff --git a/tests/unit/wakeup-lifecycle.test.ts b/tests/unit/wakeup-lifecycle.test.ts index aaec8bf5..39642294 100644 --- a/tests/unit/wakeup-lifecycle.test.ts +++ b/tests/unit/wakeup-lifecycle.test.ts @@ -47,7 +47,7 @@ function createTestWorkspace(db: AppDatabase, companyId: string): string { } function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string, name = "Agent"): string { - return db.saveAgent({ + return db.agents.saveAgent({ companyId, name, role: "IC",