Codemod db pass-through calls to domain modules: tasks + agents (C1 part 1/3)

Rewrite ~525 call sites from `db.<method>()` to `db.<module>.<method>()`
for tasks and agents domains (including `getAgent`/`findAgent`/`getTask`
which live on the `entityCrud` field). Delete the corresponding
pass-through methods on AppDatabaseBase. Structural types that narrowed
`db` to a bag of flat methods (CapabilityMatchDb, EscalationDeps,
SweepStaleReviewsDeps) are reshaped to match the new namespace. Internal
callback references inside database.ts/database-base.ts now invoke the
owning module directly (e.g. `this.agents.saveAgent`) rather than the
deleted pass-through.

The codemod lives at scripts/codemod/remove-appdatabase-passthrough.mjs
and filters by owning module + optional explicit method list. It is
idempotent and does not re-rewrite already-namespaced calls.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-20 13:17:01 -04:00
1 parent 411294afa6
commit 5e9b7acbc6
95 files changed
+1123 -793

No files matched your search

@@ -0,0 +1,412 @@
#!/usr/bin/env node
/**
* Codemod: rewrite `db.<passthroughMethod>(` → `db.<module>.<passthroughMethod>(`
* 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.<method>(...)` with a
* word-boundary before `db`; never touches `db.<submodule>.<method>(...)`.
*
* 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 `<optional qualifier>db.<method>(`. 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}`);
}
+8 -8
View File
@@ -6,7 +6,7 @@ function safeGetAgent(db: AppDatabase, agentId: string | null | undefined) {
return null; return null;
} }
try { try {
return db.getAgent(agentId); return db.entityCrud.getAgent(agentId);
} catch (error) { } catch (error) {
if (error instanceof Error && error.message.startsWith("Agent not found:")) { if (error instanceof Error && error.message.startsWith("Agent not found:")) {
return null; return null;
@@ -46,14 +46,14 @@ export function hasManagementAuthority(agent: { role: string; title: string; dep
} }
function hasOrganizationPermission(db: AppDatabase, agentId: string) { 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 { function managesAgent(db: AppDatabase, managerId: string, targetAgentId: string): boolean {
if (managerId === targetAgentId) { if (managerId === targetAgentId) {
return true; 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 { 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)) { if (canManageOrganization(db, companyId, agentId)) {
return true; return true;
} }
if (db.getDirectReports(agentId, companyId).length > 0) { if (db.agents.getDirectReports(agentId, companyId).length > 0) {
return true; return true;
} }
return hasManagementAuthority(agent); return hasManagementAuthority(agent);
@@ -128,9 +128,9 @@ function getDepartmentLeadCandidateId(
.filter((candidate) => candidate.id !== excludeAgentId) .filter((candidate) => candidate.id !== excludeAgentId)
.map((candidate) => ({ .map((candidate) => ({
id: candidate.id, 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, leadershipScore: hasManagementAuthority(candidate) ? 1 : 0,
chainDepth: db.getChainOfCommand(candidate.id).length, chainDepth: db.agents.getChainOfCommand(candidate.id).length,
createdAt: candidate.createdAt, createdAt: candidate.createdAt,
})) }))
.filter((candidate) => candidate.hasReports || candidate.leadershipScore > 0) .filter((candidate) => candidate.hasReports || candidate.leadershipScore > 0)
@@ -161,7 +161,7 @@ export function resolveHireReportsTo(
} }
if (!department) { 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)) { if (requester && (requester.department === department || !requester.reportsTo)) {
@@ -177,7 +177,7 @@ export function resolveHireReportsTo(
return requester.id; return requester.id;
} }
return db.getCeoForCompany(companyId)?.id ?? null; return db.agents.getCeoForCompany(companyId)?.id ?? null;
} }
function normalizeComparableText(value: string): string { function normalizeComparableText(value: string): string {
+2 -2
View File
@@ -266,7 +266,7 @@ export class RuntimeBridge {
*/ */
private resolveAgentName(agentId: string): string { private resolveAgentName(agentId: string): string {
try { try {
const name = this.deps.db.findAgentName(agentId); const name = this.deps.db.agents.findAgentName(agentId);
if (name && name.length > 0) return name; if (name && name.length > 0) return name;
} catch (error) { } catch (error) {
this.deps.logger.warn( this.deps.logger.warn(
@@ -384,7 +384,7 @@ export class RuntimeBridge {
) { ) {
if (envelope.taskId) { if (envelope.taskId) {
try { 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); return ensureTaskThread(this.deps.db, envelope.taskId, envelope.companyId, task.title);
} catch (error) { } catch (error) {
this.deps.logger.warn( this.deps.logger.warn(
+5 -5
View File
@@ -12,7 +12,7 @@ export function registerAgentControlRoutes(server: AgentApiServer) {
const targetAgentId = requireParam(params, "agentId"); const targetAgentId = requireParam(params, "agentId");
const targetAgent = (() => { const targetAgent = (() => {
try { try {
return db.getAgent(targetAgentId); return db.entityCrud.getAgent(targetAgentId);
} catch (err) { } catch (err) {
log.warn(`[api/agent-control] getAgent failed for ${targetAgentId}:`, err instanceof Error ? err.message : String(err)); log.warn(`[api/agent-control] getAgent failed for ${targetAgentId}:`, err instanceof Error ? err.message : String(err));
return null; return null;
@@ -52,7 +52,7 @@ export function registerAgentControlRoutes(server: AgentApiServer) {
const targetAgentId = requireParam(params, "agentId"); const targetAgentId = requireParam(params, "agentId");
let targetAgent; let targetAgent;
try { try {
targetAgent = db.getAgent(targetAgentId); targetAgent = db.entityCrud.getAgent(targetAgentId);
} catch (err) { } catch (err) {
log.warn(`[api/agent-control] getAgent failed for heartbeat ${targetAgentId}:`, err instanceof Error ? err.message : String(err)); log.warn(`[api/agent-control] getAgent failed for heartbeat ${targetAgentId}:`, err instanceof Error ? err.message : String(err));
targetAgent = null; targetAgent = null;
@@ -86,7 +86,7 @@ export function registerAgentControlRoutes(server: AgentApiServer) {
const agentId = requireParam(params, "agentId"); const agentId = requireParam(params, "agentId");
let agent; let agent;
try { try {
agent = db.getAgent(agentId); agent = db.entityCrud.getAgent(agentId);
} catch (error) { } catch (error) {
log.warn(`[api/agent-skills:GET] getAgent(${agentId}) failed: ${error instanceof Error ? error.message : String(error)}`); log.warn(`[api/agent-skills:GET] getAgent(${agentId}) failed: ${error instanceof Error ? error.message : String(error)}`);
agent = null; agent = null;
@@ -118,7 +118,7 @@ export function registerAgentControlRoutes(server: AgentApiServer) {
} }
let agent; let agent;
try { try {
agent = db.getAgent(agentId); agent = db.entityCrud.getAgent(agentId);
} catch (error) { } catch (error) {
log.warn(`[api/agent-skills:POST] getAgent(${agentId}) failed: ${error instanceof Error ? error.message : String(error)}`); log.warn(`[api/agent-skills:POST] getAgent(${agentId}) failed: ${error instanceof Error ? error.message : String(error)}`);
agent = null; agent = null;
@@ -142,7 +142,7 @@ export function registerAgentControlRoutes(server: AgentApiServer) {
const skillId = requireParam(params, "skillId"); const skillId = requireParam(params, "skillId");
let agent; let agent;
try { try {
agent = db.getAgent(agentId); agent = db.entityCrud.getAgent(agentId);
} catch (error) { } catch (error) {
log.warn(`[api/agent-skills:DELETE] getAgent(${agentId}) failed: ${error instanceof Error ? error.message : String(error)}`); log.warn(`[api/agent-skills:DELETE] getAgent(${agentId}) failed: ${error instanceof Error ? error.message : String(error)}`);
agent = null; agent = null;
+6 -6
View File
@@ -3,23 +3,23 @@ import type { AgentApiServer } from "./server-core";
export function registerAgentStandupRoute(server: AgentApiServer): void { export function registerAgentStandupRoute(server: AgentApiServer): void {
const db = server.db; const db = server.db;
server.addRoute("GET", "/api/agents/me/standup", (_req, res, _params, token) => { 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) { if (!agent || agent.companyId !== token.companyId) {
server.json(res, 404, { error: "Agent not found" }); server.json(res, 404, { error: "Agent not found" });
return; 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, token.companyId,
directReports.map((r) => r.id), directReports.map((r) => r.id),
); );
const reportStatusRows = directReports.length > 0 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"); 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 unreadInbox = db.listAgentInboxUnread(token.companyId, token.agentId);
const senderIds = new Set(unreadInbox.map((m) => m.fromAgentId)); const senderIds = new Set(unreadInbox.map((m) => m.fromAgentId));
const senderNames = senderIds.size > 0 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<string, string>(); : new Map<string, string>();
// Recent outputs the AGENT itself produced — closes the // Recent outputs the AGENT itself produced — closes the
+11 -11
View File
@@ -17,9 +17,9 @@ export function registerAgentRoutes(server: AgentApiServer) {
const callbacks = server.callbacks; const callbacks = server.callbacks;
const onDomainChanged = server.onDomainChanged; const onDomainChanged = server.onDomainChanged;
server.addRoute("GET", "/api/agents/me", (_req, res, _params, token) => { server.addRoute("GET", "/api/agents/me", (_req, res, _params, token) => {
const agent = db.getAgent(token.agentId); const agent = db.entityCrud.getAgent(token.agentId);
const chainOfCommand = db.getChainOfCommand(token.agentId); const chainOfCommand = db.agents.getChainOfCommand(token.agentId);
const directReports = db.getDirectReports(token.agentId, token.companyId); const directReports = db.agents.getDirectReports(token.agentId, token.companyId);
server.json(res, 200, { agent, chainOfCommand, directReports }); 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) => { server.addRoute("GET", "/api/agents/:agentId/metrics", (_req, res, params, token) => {
const agentId = requireParam(params, "agentId"); const agentId = requireParam(params, "agentId");
const agent = db.getAgent(agentId); const agent = db.entityCrud.getAgent(agentId);
if (!agent || agent.companyId !== token.companyId) { if (!agent || agent.companyId !== token.companyId) {
server.json(res, 404, { error: "Agent not found" }); server.json(res, 404, { error: "Agent not found" });
return; return;
@@ -57,7 +57,7 @@ export function registerAgentRoutes(server: AgentApiServer) {
server.addRoute("GET", "/api/agents/:agentId", (_req, res, params, token) => { server.addRoute("GET", "/api/agents/:agentId", (_req, res, params, token) => {
let agent; let agent;
try { try {
agent = db.getAgent(requireParam(params, "agentId")); agent = db.entityCrud.getAgent(requireParam(params, "agentId"));
} catch (err) { } catch (err) {
log.warn(`[api/agents] getAgent failed:`, err instanceof Error ? err.message : String(err)); log.warn(`[api/agents] getAgent failed:`, err instanceof Error ? err.message : String(err));
server.json(res, 404, { error: "Agent not found" }); 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" }); server.json(res, 404, { error: "Agent not found" });
return; return;
} }
const directReports = db.getDirectReports(agent.id, token.companyId); const directReports = db.agents.getDirectReports(agent.id, token.companyId);
const reportsTo = agent.reportsTo; const reportsTo = agent.reportsTo;
const manager = reportsTo const manager = reportsTo
? (() => { ? (() => {
try { try {
return db.getAgent(reportsTo); return db.entityCrud.getAgent(reportsTo);
} catch (error) { } catch (error) {
log.warn(`[api/agents] manager lookup failed for ${reportsTo}: ${error instanceof Error ? error.message : String(error)}`); log.warn(`[api/agents] manager lookup failed for ${reportsTo}: ${error instanceof Error ? error.message : String(error)}`);
return null; return null;
@@ -91,7 +91,7 @@ export function registerAgentRoutes(server: AgentApiServer) {
server.json(res, 403, { error: "Company mismatch" }); server.json(res, 403, { error: "Company mismatch" });
return; return;
} }
const agents = db.listAgentsForApi(params.companyId); const agents = db.agents.listAgentsForApi(params.companyId);
server.json(res, 200, agents); server.json(res, 200, agents);
}); });
@@ -157,7 +157,7 @@ export function registerAgentRoutes(server: AgentApiServer) {
resolvedReportsTo && resolvedReportsTo &&
resolvedReportsTo !== token.agentId && resolvedReportsTo !== token.agentId &&
!canManageOrganization(db, companyId, 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." }); server.json(res, 403, { error: "Requested hire must report to you or a manager in your reporting chain." });
return; 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 // 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( const duplicateByName = existingAgents.find(
(a) => a.name.toLowerCase() === input.name.toLowerCase(), (a) => a.name.toLowerCase() === input.name.toLowerCase(),
); );
@@ -208,7 +208,7 @@ export function registerAgentRoutes(server: AgentApiServer) {
const managerLabel = resolvedReportsTo const managerLabel = resolvedReportsTo
? (() => { ? (() => {
try { try {
return db.getAgent(resolvedReportsTo).name; return db.entityCrud.getAgent(resolvedReportsTo).name;
} catch (err) { } catch (err) {
log.warn(`[api/agents] getAgent failed for manager ${resolvedReportsTo}:`, err instanceof Error ? err.message : String(err)); log.warn(`[api/agents] getAgent failed for manager ${resolvedReportsTo}:`, err instanceof Error ? err.message : String(err));
return resolvedReportsTo; return resolvedReportsTo;
+1 -1
View File
@@ -183,7 +183,7 @@ export function registerApprovalRoutes(server: AgentApiServer) {
body: commentBody, body: commentBody,
}); });
// Wake mentioned agents in comment // 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) { for (const mentionedId of mentionedIds) {
if (mentionedId !== token.agentId) { if (mentionedId !== token.agentId) {
onWakeAgent(mentionedId, approval.companyId, "approval_comment_mentioned"); onWakeAgent(mentionedId, approval.companyId, "approval_comment_mentioned");
+5 -5
View File
@@ -228,7 +228,7 @@ export function registerFileRoutes(server: AgentApiServer) {
server.addRoute("GET", "/api/issues/:issueId/work-products", (_req, res, params, token) => { server.addRoute("GET", "/api/issues/:issueId/work-products", (_req, res, params, token) => {
try { 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; } if (task.companyId !== token.companyId) { server.json(res, 404, { error: "Task not found" }); return; }
const products = db.listWorkProductsForTask(task.id, task.companyId); const products = db.listWorkProductsForTask(task.id, task.companyId);
server.json(res, 200, products); 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) => { server.addRoute("POST", "/api/issues/:issueId/work-products", (_req, res, params, token, body) => {
try { 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; } if (task.companyId !== token.companyId) { server.json(res, 404, { error: "Task not found" }); return; }
const parsed = createWorkProductSchema.safeParse(body); const parsed = createWorkProductSchema.safeParse(body);
if (!parsed.success) { server.json(res, 400, { error: "Invalid input", details: parsed.error.flatten() }); return; } 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) => { server.addRoute("GET", "/api/issues/:issueId/documents", (_req, res, params, token) => {
try { 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; } if (task.companyId !== token.companyId) { server.json(res, 404, { error: "Task not found" }); return; }
const links = db.listDocumentsForTask(task.id, task.companyId); const links = db.listDocumentsForTask(task.id, task.companyId);
server.json(res, 200, links); 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) => { server.addRoute("PUT", "/api/issues/:issueId/documents/:key", (_req, res, params, token, body) => {
try { 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; } if (task.companyId !== token.companyId) { server.json(res, 404, { error: "Task not found" }); return; }
const key = String(params.key ?? "").trim().toLowerCase(); const key = String(params.key ?? "").trim().toLowerCase();
if (!key || !/^[a-z0-9][a-z0-9_-]*$/.test(key) || key.length > 64) { 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) => { server.addRoute("DELETE", "/api/issues/:issueId/documents/:key", (_req, res, params, token) => {
try { 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; } if (task.companyId !== token.companyId) { server.json(res, 404, { error: "Task not found" }); return; }
const key = String(params.key ?? "").trim().toLowerCase(); const key = String(params.key ?? "").trim().toLowerCase();
db.unlinkDocumentFromTask(task.id, key, task.companyId); db.unlinkDocumentFromTask(task.id, key, task.companyId);
+1 -1
View File
@@ -133,7 +133,7 @@ export function registerMeetingRoutes(server: AgentApiServer) {
); );
continue; continue;
} }
db.saveTask({ db.tasks.saveTask({
companyId: token.companyId, companyId: token.companyId,
title: item.title, title: item.title,
description: item.description ?? `Action item from meeting: ${data.title ?? existing.title}`, description: item.description ?? `Action item from meeting: ${data.title ?? existing.title}`,
+5 -5
View File
@@ -20,12 +20,12 @@ export function registerMessageRoutes(server: AgentApiServer) {
return; return;
} }
try { try {
const task = db.getTask(requireParam(params, "issueId")); const task = db.entityCrud.getTask(requireParam(params, "issueId"));
if (task.companyId !== token.companyId) { if (task.companyId !== token.companyId) {
server.json(res, 404, { error: "Task not found" }); server.json(res, 404, { error: "Task not found" });
return; return;
} }
const agent = db.getAgent(token.agentId); const agent = db.entityCrud.getAgent(token.agentId);
const comment = db.addComment({ const comment = db.addComment({
companyId: task.companyId, companyId: task.companyId,
taskId: task.id, taskId: task.id,
@@ -44,14 +44,14 @@ export function registerMessageRoutes(server: AgentApiServer) {
wokenForComment.add(task.assigneeAgentId); 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)) { if (chain.length > 0 && chain[0].id !== token.agentId && !wokenForComment.has(chain[0].id)) {
onWakeAgent(chain[0].id, task.companyId, "comment"); onWakeAgent(chain[0].id, task.companyId, "comment");
wokenForComment.add(chain[0].id); wokenForComment.add(chain[0].id);
} }
// @mention detection: wake any mentioned agents not already woken // @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) { for (const mentionedId of mentionedIds) {
if (mentionedId !== token.agentId && !wokenForComment.has(mentionedId)) { if (mentionedId !== token.agentId && !wokenForComment.has(mentionedId)) {
onWakeAgent(mentionedId, task.companyId, "comment_mentioned"); 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) => { server.addRoute("GET", "/api/issues/:issueId/comments", (_req, res, params, token) => {
try { try {
const task = db.getTask(requireParam(params, "issueId")); const task = db.entityCrud.getTask(requireParam(params, "issueId"));
if (task.companyId !== token.companyId) { if (task.companyId !== token.companyId) {
server.json(res, 404, { error: "Task not found" }); server.json(res, 404, { error: "Task not found" });
return; return;
+20 -20
View File
@@ -38,17 +38,17 @@ export function registerTaskRoutes(server: AgentApiServer) {
const statuses = statusFilter ? statusFilter.split(",") : ["todo", "in_progress", "blocked"]; const statuses = statusFilter ? statusFilter.split(",") : ["todo", "in_progress", "blocked"];
if (assigneeAgentId) { if (assigneeAgentId) {
const tasks = db.listAgentTasks(assigneeAgentId, companyId, statuses); const tasks = db.tasks.listAgentTasks(assigneeAgentId, companyId, statuses);
server.json(res, 200, tasks); server.json(res, 200, tasks);
} else { } else {
const tasks = db.listTasksByStatuses(companyId, statuses); const tasks = db.tasks.listTasksByStatuses(companyId, statuses);
server.json(res, 200, tasks); server.json(res, 200, tasks);
} }
}); });
server.addRoute("GET", "/api/issues/:issueId", (_req, res, params, token) => { server.addRoute("GET", "/api/issues/:issueId", (_req, res, params, token) => {
try { try {
const task = db.getTask(requireParam(params, "issueId")); const task = db.entityCrud.getTask(requireParam(params, "issueId"));
if (task.companyId !== token.companyId) { if (task.companyId !== token.companyId) {
server.json(res, 404, { error: "Task not found" }); server.json(res, 404, { error: "Task not found" });
return; return;
@@ -73,7 +73,7 @@ export function registerTaskRoutes(server: AgentApiServer) {
} }
let task; let task;
try { try {
task = db.getTask(requireParam(params, "issueId")); task = db.entityCrud.getTask(requireParam(params, "issueId"));
} catch (err) { } catch (err) {
log.warn(`[api/tasks] getTask failed for checkout:`, err instanceof Error ? err.message : String(err)); log.warn(`[api/tasks] getTask failed for checkout:`, err instanceof Error ? err.message : String(err));
server.json(res, 404, { error: "Task not found" }); server.json(res, 404, { error: "Task not found" });
@@ -88,7 +88,7 @@ export function registerTaskRoutes(server: AgentApiServer) {
return; return;
} }
const previousStatus = task.status; const previousStatus = task.status;
const success = db.checkoutTask(task.id, token.runId); const success = db.tasks.checkoutTask(task.id, token.runId);
if (!success) { if (!success) {
server.json(res, 409, { error: "Task cannot be checked out — already assigned or not in valid status" }); server.json(res, 409, { error: "Task cannot be checked out — already assigned or not in valid status" });
return; return;
@@ -108,7 +108,7 @@ export function registerTaskRoutes(server: AgentApiServer) {
const update = parsedUpdate.data; const update = parsedUpdate.data;
try { try {
const task = db.getTask(requireParam(params, "issueId")); const task = db.entityCrud.getTask(requireParam(params, "issueId"));
if (task.companyId !== token.companyId) { if (task.companyId !== token.companyId) {
server.json(res, 404, { error: "Task not found" }); server.json(res, 404, { error: "Task not found" });
return; return;
@@ -134,7 +134,7 @@ export function registerTaskRoutes(server: AgentApiServer) {
taskId: task.id, taskId: task.id,
limit: 10, limit: 10,
}); });
const reviewerAgent = db.getAgent(token.agentId); const reviewerAgent = db.entityCrud.getAgent(token.agentId);
const docTitles = docs.map((d) => d.title).join(", "); const docTitles = docs.map((d) => d.title).join(", ");
db.requestApproval({ db.requestApproval({
companyId: task.companyId, 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), impactSummary: `${reviewerAgent.name} reviewed and approved. Awaiting your final review.`.slice(0, 400),
payloadJson: JSON.stringify({ documentIds: docs.map((d) => d.id), documents: docs }), 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) { if (task.assigneeAgentId) {
const prodAgent = db.getAgent(task.assigneeAgentId); const prodAgent = db.entityCrud.getAgent(task.assigneeAgentId);
if (prodAgent.heartbeatEnabled) { 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." }); 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, // advanceTaskStatus checks previousStatus !== newStatus internally,
// so it must run before saveTask writes the new status. // so it must run before saveTask writes the new status.
if (update.status && update.status !== task.status) { if (update.status && update.status !== task.status) {
db.advanceTaskStatus(task.id, update.status); db.tasks.advanceTaskStatus(task.id, update.status);
} }
const input: Record<string, unknown> = { const input: Record<string, unknown> = {
@@ -185,10 +185,10 @@ export function registerTaskRoutes(server: AgentApiServer) {
assigneeAgentId: newAssignee, assigneeAgentId: newAssignee,
workspaceId: task.workspaceId, workspaceId: task.workspaceId,
}; };
db.saveTask(input as Parameters<typeof db.saveTask>[0]); db.tasks.saveTask(input as Parameters<typeof db.tasks.saveTask>[0]);
if (update.comment) { if (update.comment) {
const agent = db.getAgent(token.agentId); const agent = db.entityCrud.getAgent(token.agentId);
db.addComment({ db.addComment({
companyId: task.companyId, companyId: task.companyId,
taskId: task.id, taskId: task.id,
@@ -219,7 +219,7 @@ export function registerTaskRoutes(server: AgentApiServer) {
if (update.status && ["done", "in_review"].includes(update.status)) { if (update.status && ["done", "in_review"].includes(update.status)) {
if (task.parentId) { if (task.parentId) {
try { try {
const parentTask = db.getTask(task.parentId); const parentTask = db.entityCrud.getTask(task.parentId);
if (parentTask.assigneeAgentId && parentTask.assigneeAgentId !== token.agentId) { if (parentTask.assigneeAgentId && parentTask.assigneeAgentId !== token.agentId) {
onWakeAgent(parentTask.assigneeAgentId, task.companyId, "subtask_completed"); 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 // This is not a subtask — use a distinct wake reason so the
// reviewer prompt does not misframe the work as "a subtask // reviewer prompt does not misframe the work as "a subtask
// you delegated". // you delegated".
const chain = db.getChainOfCommand(token.agentId); const chain = db.agents.getChainOfCommand(token.agentId);
if (chain.length > 0) { if (chain.length > 0) {
onWakeAgent(chain[0].id, task.companyId, "task_review_requested"); onWakeAgent(chain[0].id, task.companyId, "task_review_requested");
} }
@@ -241,7 +241,7 @@ export function registerTaskRoutes(server: AgentApiServer) {
} }
if (update.status === "blocked") { if (update.status === "blocked") {
const chain = db.getChainOfCommand(token.agentId); const chain = db.agents.getChainOfCommand(token.agentId);
if (chain.length > 0) { if (chain.length > 0) {
onWakeAgent(chain[0].id, task.companyId, "report_blocked"); onWakeAgent(chain[0].id, task.companyId, "report_blocked");
} }
@@ -261,7 +261,7 @@ export function registerTaskRoutes(server: AgentApiServer) {
onWakeAgent(task.assigneeAgentId, task.companyId, "comment"); onWakeAgent(task.assigneeAgentId, task.companyId, "comment");
wokenForComment.add(task.assigneeAgentId); 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)) { if (chain.length > 0 && chain[0].id !== token.agentId && !wokenForComment.has(chain[0].id)) {
onWakeAgent(chain[0].id, task.companyId, "comment"); onWakeAgent(chain[0].id, task.companyId, "comment");
} }
@@ -333,7 +333,7 @@ export function registerTaskRoutes(server: AgentApiServer) {
let id: string; let id: string;
try { try {
id = db.saveTask({ id = db.tasks.saveTask({
companyId, companyId,
title: input.title, title: input.title,
description: input.description ?? "", description: input.description ?? "",
@@ -352,9 +352,9 @@ export function registerTaskRoutes(server: AgentApiServer) {
// Auto-infer task_type from assignee's department // Auto-infer task_type from assignee's department
if (input.assigneeAgentId) { if (input.assigneeAgentId) {
const assignee = db.getAgent(input.assigneeAgentId); const assignee = db.entityCrud.getAgent(input.assigneeAgentId);
const inferredType = inferTaskType(assignee.department); const inferredType = inferTaskType(assignee.department);
db.setTaskType(id, inferredType); db.tasks.setTaskType(id, inferredType);
} }
db.addActivity({ db.addActivity({
+13 -13
View File
@@ -76,7 +76,7 @@ export function executeAutomationAction(
switch (action) { switch (action) {
case "assign_task": { case "assign_task": {
if (ctx.taskId) { if (ctx.taskId) {
const assignedId = db.autoAssignTask(ctx.taskId, ctx.companyId); const assignedId = db.tasks.autoAssignTask(ctx.taskId, ctx.companyId);
if (assignedId) { if (assignedId) {
wakeAgentIfPossible(assignedId, ctx.companyId, "assignment"); wakeAgentIfPossible(assignedId, ctx.companyId, "assignment");
} }
@@ -90,7 +90,7 @@ export function executeAutomationAction(
}); });
if (unassigned.length > 0) { if (unassigned.length > 0) {
const task = unassigned[0]!; const task = unassigned[0]!;
db.saveTask({ db.tasks.saveTask({
id: task.id, id: task.id,
companyId: task.companyId, companyId: task.companyId,
projectId: task.projectId, projectId: task.projectId,
@@ -120,7 +120,7 @@ export function executeAutomationAction(
logger.warn(`[automation] Skipping create_task — validation failed: ${taskValidation.errors.join("; ")}`); logger.warn(`[automation] Skipping create_task — validation failed: ${taskValidation.errors.join("; ")}`);
break; break;
} }
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId: ctx.companyId, companyId: ctx.companyId,
projectId: ctx.projectId ?? cfg.projectId ?? null, projectId: ctx.projectId ?? cfg.projectId ?? null,
title, title,
@@ -147,7 +147,7 @@ export function executeAutomationAction(
let channelTargetId: string | null = cfg.channelTargetId ?? cfg.department ?? ctx.department ?? null; let channelTargetId: string | null = cfg.channelTargetId ?? cfg.department ?? ctx.department ?? null;
if (channel === "department" && !channelTargetId && fromAgentId) { if (channel === "department" && !channelTargetId && fromAgentId) {
try { try {
const sender = db.getAgent(fromAgentId); const sender = db.entityCrud.getAgent(fromAgentId);
channelTargetId = sender.department ?? null; channelTargetId = sender.department ?? null;
} catch (err) { } catch (err) {
logger.warn(`[automation] getAgent failed for sender ${fromAgentId}; dropping department target:`, err instanceof Error ? err.message : String(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) { if (!targetAgentId && ctx.agentId) {
const triggerType = rule.trigger ?? ""; const triggerType = rule.trigger ?? "";
if (triggerType === "run_completed" || triggerType === "run_failed") { 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; targetAgentId = manager?.id ?? ctx.agentId;
} else { } else {
targetAgentId = ctx.agentId; targetAgentId = ctx.agentId;
@@ -221,8 +221,8 @@ export function executeAutomationAction(
case "update_task_status": { case "update_task_status": {
if (!ctx.taskId) break; if (!ctx.taskId) break;
const newStatus = (config.status as string) ?? "todo"; const newStatus = (config.status as string) ?? "todo";
const previousTask = db.getTask(ctx.taskId); const previousTask = db.entityCrud.getTask(ctx.taskId);
db.advanceTaskStatus(ctx.taskId, newStatus, "Status changed by automation rule"); db.tasks.advanceTaskStatus(ctx.taskId, newStatus, "Status changed by automation rule");
publishDomainChanged(); publishDomainChanged();
if (previousTask.status !== newStatus && rule.trigger !== "task_status_changed") { if (previousTask.status !== newStatus && rule.trigger !== "task_status_changed") {
handleTaskStatusChange(ctx.taskId, ctx.companyId, previousTask.status, newStatus); handleTaskStatusChange(ctx.taskId, ctx.companyId, previousTask.status, newStatus);
@@ -234,17 +234,17 @@ export function executeAutomationAction(
if (!targetAgentId) break; if (!targetAgentId) break;
let agentName = "an agent"; let agentName = "an agent";
try { try {
agentName = db.getAgent(targetAgentId).name; agentName = db.entityCrud.getAgent(targetAgentId).name;
} catch (err) { } catch (err) {
logger.warn(`[automation] getAgent failed for escalation target ${targetAgentId}; using generic label:`, err instanceof Error ? err.message : String(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"; let taskTitle = ctx.taskId ?? "unknown";
try { try {
if (ctx.taskId) taskTitle = db.getTask(ctx.taskId).title; if (ctx.taskId) taskTitle = db.entityCrud.getTask(ctx.taskId).title;
} catch (err) { } catch (err) {
logger.warn(`[automation] getTask failed for escalation task ${ctx.taskId}; using id as title:`, err instanceof Error ? err.message : String(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 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); const wakeTrigger = deriveEscalationWakeTrigger(ctx.newStatus ?? null, rule.trigger ?? null);
if (manager) { if (manager) {
@@ -277,7 +277,7 @@ export function executeAutomationAction(
case "cross_department_notify": { case "cross_department_notify": {
const targetDepartment = rule.targetDepartment ?? (config.department as string); const targetDepartment = rule.targetDepartment ?? (config.department as string);
if (!targetDepartment) break; 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; const senderAgentId = ctx.agentId ?? departmentAgents[0]?.id;
if (senderAgentId) { if (senderAgentId) {
const dispatchResult = dispatchAgentMessage( const dispatchResult = dispatchAgentMessage(
@@ -352,7 +352,7 @@ export function executeAutomationAction(
} }
case "reassign_task": { case "reassign_task": {
if (!ctx.taskId) break; if (!ctx.taskId) break;
const newAssignee = db.autoAssignTask(ctx.taskId, ctx.companyId); const newAssignee = db.tasks.autoAssignTask(ctx.taskId, ctx.companyId);
if (newAssignee) { if (newAssignee) {
wakeAgentIfPossible(newAssignee, ctx.companyId, "assignment"); wakeAgentIfPossible(newAssignee, ctx.companyId, "assignment");
} }
@@ -361,7 +361,7 @@ export function executeAutomationAction(
case "run_task": { case "run_task": {
const targetTaskId = (config.taskId as string) ?? ctx.taskId; const targetTaskId = (config.taskId as string) ?? ctx.taskId;
if (!targetTaskId) break; if (!targetTaskId) break;
const task = db.getTask(targetTaskId); const task = db.entityCrud.getTask(targetTaskId);
if (!task.assigneeAgentId) break; if (!task.assigneeAgentId) break;
wakeAgentIfPossible(task.assigneeAgentId, ctx.companyId, "assignment"); wakeAgentIfPossible(task.assigneeAgentId, ctx.companyId, "assignment");
break; break;
@@ -30,7 +30,7 @@ function recordReviewFeedbackKnowledge(
if (!approval.relatedTaskId) return; if (!approval.relatedTaskId) return;
if (decision === "approved") 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 feedback = approval.decisionNote?.trim() || "Board requested revisions before approval.";
const topic = `Review feedback: ${task.title}`; const topic = `Review feedback: ${task.title}`;
const content = [ const content = [
@@ -95,7 +95,7 @@ export function handleApprovalResolved(
}); });
if (approval.type === "hire_agent" && decision === "approved" && approval.relatedAgentId) { 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") { if (agent.status === "idle") {
// Deterministic default skill kit. The CEO's operating prompt // Deterministic default skill kit. The CEO's operating prompt
@@ -136,7 +136,7 @@ export function handleApprovalResolved(
}); });
wakeAgentIfPossible(agent.id, companyId, "assignment"); wakeAgentIfPossible(agent.id, companyId, "assignment");
const ceo = db.getCeoForCompany(companyId); const ceo = db.agents.getCeoForCompany(companyId);
if (ceo && ceo.id !== agent.id) { if (ceo && ceo.id !== agent.id) {
wakeAgentIfPossible(ceo.id, companyId, "assignment"); wakeAgentIfPossible(ceo.id, companyId, "assignment");
} }
@@ -174,27 +174,27 @@ export function handleApprovalResolved(
} }
if (approval.relatedTaskId) { if (approval.relatedTaskId) {
const task = db.getTask(approval.relatedTaskId); const task = db.entityCrud.getTask(approval.relatedTaskId);
if (task.assigneeAgentId) { if (task.assigneeAgentId) {
const agent = db.getAgent(task.assigneeAgentId); const agent = db.entityCrud.getAgent(task.assigneeAgentId);
if (!agent.heartbeatEnabled) { 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}.`); logger.info(`[deliverable-review] Resumed heartbeat for ${agent.name} after user ${decision}.`);
} }
} }
if (decision === "approved") { 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"); 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." }); db.addActivity({ companyId, actor: "board", action: "deliverable.approved", entityType: "task", entityId: task.id, detail: approval.decisionNote || "Deliverable approved by board." });
} else if (decision === "rejected") { } else if (decision === "rejected") {
recordReviewFeedbackKnowledge(db, approval, decision); 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"); handleTaskStatusChange(task.id, companyId, task.status, "in_progress");
if (task.assigneeAgentId) { if (task.assigneeAgentId) {
wakeAgentIfPossible(task.assigneeAgentId, companyId, "revision_needed"); wakeAgentIfPossible(task.assigneeAgentId, companyId, "revision_needed");
} }
} else if (decision === "revision_requested") { } 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"); handleTaskStatusChange(task.id, companyId, task.status, "in_progress");
recordReviewFeedbackKnowledge(db, approval, decision); recordReviewFeedbackKnowledge(db, approval, decision);
if (task.assigneeAgentId) { if (task.assigneeAgentId) {
@@ -202,9 +202,9 @@ export function handleApprovalResolved(
} }
} }
} else if (approval.requestedByAgentId) { } else if (approval.requestedByAgentId) {
const agent = db.getAgent(approval.requestedByAgentId); const agent = db.entityCrud.getAgent(approval.requestedByAgentId);
if (!agent.heartbeatEnabled) { 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}.`); logger.info(`[deliverable-review] Resumed heartbeat for ${agent.name} after user ${decision}.`);
} }
if (decision !== "approved") { if (decision !== "approved") {
+2 -2
View File
@@ -23,7 +23,7 @@ export function matchesEventConfig(
if (conditions.previousStatus && ctx.previousStatus && conditions.previousStatus !== ctx.previousStatus) return false; if (conditions.previousStatus && ctx.previousStatus && conditions.previousStatus !== ctx.previousStatus) return false;
if (conditions.priority) { if (conditions.priority) {
try { 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 && task.priority !== conditions.priority) return false;
// If task was deleted, skip this condition instead of rejecting the rule. // If task was deleted, skip this condition instead of rejecting the rule.
} catch (err) { } catch (err) {
@@ -32,7 +32,7 @@ export function matchesEventConfig(
} }
if (typeof conditions.budgetThresholdPct === "number" && ctx.agentId) { if (typeof conditions.budgetThresholdPct === "number" && ctx.agentId) {
try { try {
const agent = db.getAgent(ctx.agentId); const agent = db.entityCrud.getAgent(ctx.agentId);
if (agent.budgetMonthlyUsd > 0) { if (agent.budgetMonthlyUsd > 0) {
const utilization = agent.spentMonthlyUsd / agent.budgetMonthlyUsd; const utilization = agent.spentMonthlyUsd / agent.budgetMonthlyUsd;
if (utilization < (conditions.budgetThresholdPct as number) / 100) return false; if (utilization < (conditions.budgetThresholdPct as number) / 100) return false;
+10 -10
View File
@@ -41,9 +41,9 @@ export function handleTaskStatusChange(
const { db, wakeAgentIfPossible } = deps; const { db, wakeAgentIfPossible } = deps;
const { dispatchAutomation } = callbacks; const { dispatchAutomation } = callbacks;
let task: ReturnType<typeof db.getTask> | null = null; let task: ReturnType<typeof db.entityCrud.getTask> | null = null;
try { try {
task = db.getTask(taskId); task = db.entityCrud.getTask(taskId);
} catch (error) { } catch (error) {
deps.logger.warn(`[automation] handleTaskStatusChange: task ${taskId} not found:`, error); deps.logger.warn(`[automation] handleTaskStatusChange: task ${taskId} not found:`, error);
return; return;
@@ -68,7 +68,7 @@ export function handleTaskStatusChange(
}; };
try { try {
if (task.assigneeAgentId) { if (task.assigneeAgentId) {
const agent = db.getAgent(task.assigneeAgentId); const agent = db.entityCrud.getAgent(task.assigneeAgentId);
context.department = agent.department ?? undefined; context.department = agent.department ?? undefined;
} }
} catch (error) { } catch (error) {
@@ -80,7 +80,7 @@ export function handleTaskStatusChange(
if (newStatus === "blocked") { if (newStatus === "blocked") {
dispatchAutomation("task_blocked", context); dispatchAutomation("task_blocked", context);
if (task.assigneeAgentId) { if (task.assigneeAgentId) {
const manager = db.getManagerForAgent(task.assigneeAgentId); const manager = db.agents.getManagerForAgent(task.assigneeAgentId);
if (manager) { if (manager) {
wakeAgentIfPossible(manager.id, companyId, "report_blocked"); wakeAgentIfPossible(manager.id, companyId, "report_blocked");
} }
@@ -89,14 +89,14 @@ export function handleTaskStatusChange(
if (newStatus === "done" && task.parentId) { if (newStatus === "done" && task.parentId) {
try { 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"); const allDone = siblings.every((entry) => entry.id === taskId ? true : entry.status === "done");
if (allDone) { if (allDone) {
const parent = db.getTask(task.parentId); const parent = db.entityCrud.getTask(task.parentId);
if (parent.assigneeAgentId) { if (parent.assigneeAgentId) {
wakeAgentIfPossible(parent.assigneeAgentId, companyId, "subtask_completed"); wakeAgentIfPossible(parent.assigneeAgentId, companyId, "subtask_completed");
} else { } else {
const assignedId = db.autoAssignTask(parent.id, companyId); const assignedId = db.tasks.autoAssignTask(parent.id, companyId);
if (assignedId) { if (assignedId) {
wakeAgentIfPossible(assignedId, companyId, "subtask_completed"); wakeAgentIfPossible(assignedId, companyId, "subtask_completed");
} }
@@ -118,9 +118,9 @@ export function handleTaskCreated(
const { db, wakeAgentIfPossible } = deps; const { db, wakeAgentIfPossible } = deps;
const { dispatchAutomation } = callbacks; const { dispatchAutomation } = callbacks;
let task: ReturnType<typeof db.getTask> | null = null; let task: ReturnType<typeof db.entityCrud.getTask> | null = null;
try { try {
task = db.getTask(taskId); task = db.entityCrud.getTask(taskId);
} catch (error) { } catch (error) {
deps.logger.warn(`[automation] handleTaskCreated: task ${taskId} not found:`, error); deps.logger.warn(`[automation] handleTaskCreated: task ${taskId} not found:`, error);
return; return;
@@ -134,7 +134,7 @@ export function handleTaskCreated(
}); });
if (!task.assigneeAgentId) { if (!task.assigneeAgentId) {
const assignedId = db.autoAssignTask(taskId, companyId); const assignedId = db.tasks.autoAssignTask(taskId, companyId);
if (assignedId) { if (assignedId) {
wakeAgentIfPossible(assignedId, companyId, "assignment"); wakeAgentIfPossible(assignedId, companyId, "assignment");
} }
@@ -190,10 +190,10 @@ function createDeliverableReviewApproval(
impactSummary: reviewerNote ?? "Agent completed work with important deliverables. Awaiting your final review.", impactSummary: reviewerNote ?? "Agent completed work with important deliverables. Awaiting your final review.",
payloadJson: JSON.stringify({ documentIds: docs.map((d) => d.id), documents: docs }), payloadJson: JSON.stringify({ documentIds: docs.map((d) => d.id), documents: docs }),
}); });
db.setTaskRequiresUserReview(taskId, false); db.tasks.setTaskRequiresUserReview(taskId, false);
const agent = db.getAgent(agentId); const agent = db.entityCrud.getAgent(agentId);
if (agent.heartbeatEnabled) { 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.`); logger.info(`[deliverable-review] Paused heartbeat for ${agent.name} — awaiting user review.`);
} }
notify({ notify({
@@ -228,7 +228,7 @@ function createDeliverableReviewApproval(
*/ */
function decideTerminalStatus( function decideTerminalStatus(
db: RunCompletionDeps["db"], db: RunCompletionDeps["db"],
agent: ReturnType<RunCompletionDeps["db"]["getAgent"]>, agent: ReturnType<RunCompletionDeps["db"]["entityCrud"]["getAgent"]>,
companyId: string, companyId: string,
): "in_review" | "done" { ): "in_review" | "done" {
if (agent.reportsTo !== null) return "in_review"; if (agent.reportsTo !== null) return "in_review";
@@ -247,7 +247,7 @@ function advanceToDone(
statusMessage: string, statusMessage: string,
): void { ): void {
const { db, publishDomainChanged } = deps; const { db, publishDomainChanged } = deps;
db.advanceTaskStatus(taskId, "done", statusMessage); db.tasks.advanceTaskStatus(taskId, "done", statusMessage);
callbacks.handleTaskStatusChange(taskId, run.companyId, "in_progress", "done"); callbacks.handleTaskStatusChange(taskId, run.companyId, "in_progress", "done");
publishDomainChanged(); publishDomainChanged();
} }
@@ -268,22 +268,22 @@ function advanceToInReview(
// expected (`ai_cofounder` mode or `reviewDeliverables = true`), so // expected (`ai_cofounder` mode or `reviewDeliverables = true`), so
// pre-flagging here is the right semantics — the human IS the // pre-flagging here is the right semantics — the human IS the
// reviewer in those configurations. // reviewer in those configurations.
const agent = db.getAgent(run.agentId); const agent = db.entityCrud.getAgent(run.agentId);
if (agent.reportsTo === null) { 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"); callbacks.handleTaskStatusChange(taskId, run.companyId, "in_progress", "in_review");
const company = db.listSnapshot().companies.find((c) => c.id === run.companyId); const company = db.listSnapshot().companies.find((c) => c.id === run.companyId);
if (company?.reviewDeliverables) { if (company?.reviewDeliverables) {
const importantDocs = findImportantDocsForRun(db, run.id, run.companyId); const importantDocs = findImportantDocsForRun(db, run.id, run.companyId);
if (importantDocs.length > 0) { if (importantDocs.length > 0) {
db.setTaskRequiresUserReview(taskId, true); db.tasks.setTaskRequiresUserReview(taskId, true);
if (agent.heartbeatEnabled) { 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.`); 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) { if (chain.length === 0) {
createDeliverableReviewApproval(deps, run.companyId, taskId, run.agentId, importantDocs); createDeliverableReviewApproval(deps, run.companyId, taskId, run.agentId, importantDocs);
} }
@@ -305,7 +305,7 @@ function advanceSuccessfulRun(
taskId: string, taskId: string,
statusMessage: string, statusMessage: string,
): "in_review" | "done" { ): "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); const target = decideTerminalStatus(deps.db, agent, run.companyId);
if (target === "done") { if (target === "done") {
advanceToDone(deps, callbacks, run, taskId, statusMessage); advanceToDone(deps, callbacks, run, taskId, statusMessage);
@@ -327,9 +327,9 @@ export function processRunCompletion(
const { dispatchAutomation, handleTaskStatusChange } = callbacks; const { dispatchAutomation, handleTaskStatusChange } = callbacks;
if (!run.taskId) return; if (!run.taskId) return;
let task: ReturnType<typeof db.getTask> | null = null; let task: ReturnType<typeof db.entityCrud.getTask> | null = null;
try { try {
task = db.getTask(run.taskId); task = db.entityCrud.getTask(run.taskId);
} catch (error) { } catch (error) {
deps.logger.warn(`[automation] processRunCompletion: task ${run.taskId} not found for run ${run.id}:`, error); deps.logger.warn(`[automation] processRunCompletion: task ${run.taskId} not found for run ${run.id}:`, error);
return; return;
@@ -363,7 +363,7 @@ export function processRunCompletion(
// the task in the manager's blocker queue with the diagnostic // the task in the manager's blocker queue with the diagnostic
// comment attached; the manager patches it back to `todo` (or // comment attached; the manager patches it back to `todo` (or
// `cancelled`) after deciding how to proceed. // `cancelled`) after deciding how to proceed.
db.advanceTaskStatus( db.tasks.advanceTaskStatus(
task.id, task.id,
"blocked", "blocked",
`${taskType} task run completed but no deliverable artifact was detected. Flagging for manager review.`, `${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"); handleTaskStatusChange(task.id, run.companyId, "in_progress", "blocked");
// Wake manager for review // Wake manager for review
try { try {
const agent = db.getAgent(run.agentId); const agent = db.entityCrud.getAgent(run.agentId);
const chain = db.getChainOfCommand(agent.id); const chain = db.agents.getChainOfCommand(agent.id);
if (chain.length > 0) { if (chain.length > 0) {
wakeAgentIfPossible(chain[0].id, run.companyId, "review_needed"); wakeAgentIfPossible(chain[0].id, run.companyId, "review_needed");
} }
@@ -414,7 +414,7 @@ export function processRunCompletion(
runId: run.id, runId: run.id,
}; };
try { try {
const agent = db.getAgent(run.agentId); const agent = db.entityCrud.getAgent(run.agentId);
automationContext.department = agent.department ?? undefined; automationContext.department = agent.department ?? undefined;
} catch (err) { } 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)); 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); dispatchAutomation("run_completed", automationContext);
if (task.parentId) { if (task.parentId) {
try { 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"); const allSiblingsDoneOrReview = siblings.every((entry) => entry.status === "done" || entry.status === "in_review");
if (allSiblingsDoneOrReview) { if (allSiblingsDoneOrReview) {
const parent = db.getTask(task.parentId); const parent = db.entityCrud.getTask(task.parentId);
if (parent.assigneeAgentId) { if (parent.assigneeAgentId) {
wakeAgentIfPossible(parent.assigneeAgentId, run.companyId, "subtask_completed"); wakeAgentIfPossible(parent.assigneeAgentId, run.companyId, "subtask_completed");
} else { } else {
const assignedId = db.autoAssignTask(parent.id, run.companyId); const assignedId = db.tasks.autoAssignTask(parent.id, run.companyId);
if (assignedId) { if (assignedId) {
wakeAgentIfPossible(assignedId, run.companyId, "subtask_completed"); 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%. // just read the post-run spend and fire if it crosses 80%.
if (isSuccess) { if (isSuccess) {
try { try {
const agent = db.getAgent(run.agentId); const agent = db.entityCrud.getAgent(run.agentId);
if (agent.budgetMonthlyUsd > 0) { if (agent.budgetMonthlyUsd > 0) {
const utilization = agent.spentMonthlyUsd / agent.budgetMonthlyUsd; const utilization = agent.spentMonthlyUsd / agent.budgetMonthlyUsd;
if (utilization >= 0.8) { if (utilization >= 0.8) {
+3 -3
View File
@@ -59,7 +59,7 @@ export function resolveWorkflowStepActor(
companyId: string, companyId: string,
): AgentRecord | null { ): AgentRecord | null {
if (step.assigneeAgentId) { if (step.assigneeAgentId) {
const agent = db.getAgent(step.assigneeAgentId); const agent = db.entityCrud.getAgent(step.assigneeAgentId);
if (agent.companyId !== companyId) { if (agent.companyId !== companyId) {
throw new Error(`Workflow step assignee ${step.assigneeAgentId} does not belong to company ${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) { if (step.assigneeDepartment) {
const idleAgents = db.getIdleAgentsForDepartment(companyId, step.assigneeDepartment); const idleAgents = db.agents.getIdleAgentsForDepartment(companyId, step.assigneeDepartment);
if (idleAgents.length > 0) { if (idleAgents.length > 0) {
return db.getAgent(idleAgents[0]!.id); return db.entityCrud.getAgent(idleAgents[0]!.id);
} }
} }
@@ -49,10 +49,10 @@ export function recoverShutdownInterruptedTasks(deps: RecoverInterruptedTasksDep
for (const run of shutdownRuns) { for (const run of shutdownRuns) {
try { try {
if (run.taskId) { 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") { if (task.status === "in_progress" || task.status === "blocked") {
deps.db.advanceTaskStatus(run.taskId, "todo", "Auto-resumed after app restart."); deps.db.tasks.advanceTaskStatus(run.taskId, "todo", "Auto-resumed after app restart.");
deps.db.releaseTaskLock(run.taskId, run.id); deps.db.tasks.releaseTaskLock(run.taskId, run.id);
} }
} }
if (!resumedAgents.has(run.agentId)) { if (!resumedAgents.has(run.agentId)) {
+2 -2
View File
@@ -75,7 +75,7 @@ export function wireApiServer(deps: WireApiServerDeps): AgentApiServer {
deps.threadIntegration.onTaskStatusChanged(taskId, companyId, prev, next); deps.threadIntegration.onTaskStatusChanged(taskId, companyId, prev, next);
if (prev === "todo" && next === "in_progress") { if (prev === "todo" && next === "in_progress") {
try { try {
const task = deps.db.getTask(taskId); const task = deps.db.entityCrud.getTask(taskId);
deps.checkpointEngine.onTaskStarted(taskId, companyId, task.assigneeAgentId); deps.checkpointEngine.onTaskStarted(taskId, companyId, task.assigneeAgentId);
} catch (error) { } catch (error) {
log.warn( log.warn(
@@ -118,7 +118,7 @@ export function wireApiServer(deps: WireApiServerDeps): AgentApiServer {
}, },
input, input,
), ),
onAutoAssignTask: (taskId, companyId) => deps.db.autoAssignTask(taskId, companyId), onAutoAssignTask: (taskId, companyId) => deps.db.tasks.autoAssignTask(taskId, companyId),
onHumanMessage: (companyId, toAgentId, threadId, waitStateId) => { onHumanMessage: (companyId, toAgentId, threadId, waitStateId) => {
deps.threadIntegration.onHumanMessage(companyId, toAgentId, threadId, waitStateId); deps.threadIntegration.onHumanMessage(companyId, toAgentId, threadId, waitStateId);
deps.publishDomainChanged(); deps.publishDomainChanged();
+1 -1
View File
@@ -31,7 +31,7 @@ function getScopeLabel(
): string { ): string {
if (policy.scopeType === "agent") { if (policy.scopeType === "agent") {
try { try {
const agent = db.getAgent(policy.scopeId); const agent = db.entityCrud.getAgent(policy.scopeId);
return `Agent "${agent.name}"`; return `Agent "${agent.name}"`;
} catch (err) { } catch (err) {
logger.warn(`[budget] getAgent failed for scope ${policy.scopeId}:`, err instanceof Error ? err.message : String(err)); logger.warn(`[budget] getAgent failed for scope ${policy.scopeId}:`, err instanceof Error ? err.message : String(err));
+2 -2
View File
@@ -48,8 +48,8 @@ export function createCheckpointEngine(deps: CheckpointDeps) {
const mode = getCompanyExecutionMode(db, companyId); const mode = getCompanyExecutionMode(db, companyId);
if (mode !== "ai_cofounder") return null; if (mode !== "ai_cofounder") return null;
const task = db.getTask(taskId); const task = db.entityCrud.getTask(taskId);
const taskType = db.getTaskType(taskId); const taskType = db.tasks.getTaskType(taskId);
// Only create plan checkpoints for non-code task types // Only create plan checkpoints for non-code task types
if (!taskType || !CHECKPOINT_TASK_TYPES.has(taskType)) return null; if (!taskType || !CHECKPOINT_TASK_TYPES.has(taskType)) return null;
+3 -83
View File
@@ -123,7 +123,7 @@ export class AppDatabaseBase {
addComment: (input) => this.addComment(input), addComment: (input) => this.addComment(input),
}); });
this.agents = buildAgentMethods(getDb, ra, tx, { this.agents = buildAgentMethods(getDb, ra, tx, {
getAgent: (id) => this.getAgent(id), getAgent: (id) => this.entityCrud.getAgent(id),
wouldCreateAgentCycle: (agentId, reportsTo) => this.wouldCreateAgentCycle(agentId, reportsTo), wouldCreateAgentCycle: (agentId, reportsTo) => this.wouldCreateAgentCycle(agentId, reportsTo),
recordConfigRevision: (input) => this.recordConfigRevision(input), recordConfigRevision: (input) => this.recordConfigRevision(input),
cancelRunForDeletion: (runId, taskId, workspaceId, summary) => this.runs.cancelRunForDeletion(runId, taskId, workspaceId, summary), cancelRunForDeletion: (runId, taskId, workspaceId, summary) => this.runs.cancelRunForDeletion(runId, taskId, workspaceId, summary),
@@ -134,14 +134,14 @@ export class AppDatabaseBase {
addActivity: (input) => this.addActivity(input), addActivity: (input) => this.addActivity(input),
}); });
this.messages = buildMessageMethods(getDb, ra, { this.messages = buildMessageMethods(getDb, ra, {
getAgent: (id) => this.getAgent(id), getAgent: (id) => this.entityCrud.getAgent(id),
addActivity: (input) => this.addActivity(input), addActivity: (input) => this.addActivity(input),
transaction: tx, transaction: tx,
}); });
this.runs = buildRunMethods(getDb, ra, tx, { logsDir: this.logsDir }); this.runs = buildRunMethods(getDb, ra, tx, { logsDir: this.logsDir });
this.workspacesMod = buildWorkspaceMethods(getDb, ra, tx); this.workspacesMod = buildWorkspaceMethods(getDb, ra, tx);
this.approvalsMod = buildApprovalMethods(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.social = buildSocialMethods(getDb, ra, tx);
this.skills = buildSkillMethods(getDb, ra); this.skills = buildSkillMethods(getDb, ra);
@@ -270,86 +270,6 @@ export class AppDatabaseBase {
getConnector(id: ConnectorId) { return this.connectorsCrud.getConnector(id); } getConnector(id: ConnectorId) { return this.connectorsCrud.getConnector(id); }
findConnector(id: ConnectorId) { return this.connectorsCrud.findConnector(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<string, unknown>) { 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 ────────────────────────────────────────────────────────────────── // ─── Runs ──────────────────────────────────────────────────────────────────
getRun(id: string) { return this.runs.getRun(id); } getRun(id: string) { return this.runs.getRun(id); }
+5 -5
View File
@@ -77,8 +77,8 @@ export class AppDatabase extends AppDatabaseBase {
countActiveGoalsForCompany(companyId: string) { return this.entityCrud.countActiveGoalsForCompany(companyId); } 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 }) { 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, { return this.entityCrud.createHireRequest(input, {
saveAgent: (agentInput) => this.saveAgent(agentInput), saveAgent: (agentInput) => this.agents.saveAgent(agentInput),
requestApproval: (approvalInput) => this.requestApproval(approvalInput), 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); } getConfigRevision(revisionId: string, companyId: string) { return this.documentsMod.getConfigRevision(revisionId, companyId); }
rollbackConfigRevision(agentId: string, revisionId: string, companyId: string) { rollbackConfigRevision(agentId: string, revisionId: string, companyId: string) {
return this.documentsMod.rollbackConfigRevision(agentId, revisionId, companyId, { return this.documentsMod.rollbackConfigRevision(agentId, revisionId, companyId, {
getConfigRevision: (rid, cid) => this.getConfigRevision(rid, cid), getConfigRevision: (rid, cid) => this.documentsMod.getConfigRevision(rid, cid),
getAgent: (id) => this.getAgent(id), getAgent: (id) => this.entityCrud.getAgent(id),
recordConfigRevision: (input) => this.recordConfigRevision(input), recordConfigRevision: (input) => this.documentsMod.recordConfigRevision(input),
}); });
} }
+3 -3
View File
@@ -190,7 +190,7 @@ function maybeRaiseDeliverableReviewApproval(
const companySettings = deps.db.getCompanyApprovalSettings(input.companyId); const companySettings = deps.db.getCompanyApprovalSettings(input.companyId);
if (!companySettings?.reviewDeliverables) return null; if (!companySettings?.reviewDeliverables) return null;
if (deps.db.getCompanyExecutionMode(input.companyId) === "ai_cofounder") 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({ const approvalId = deps.db.requestApproval({
companyId: input.companyId, companyId: input.companyId,
@@ -207,9 +207,9 @@ function maybeRaiseDeliverableReviewApproval(
if (input.authorAgentId) { if (input.authorAgentId) {
try { try {
const agent = deps.db.getAgent(input.authorAgentId); const agent = deps.db.entityCrud.getAgent(input.authorAgentId);
if (agent.heartbeatEnabled) { if (agent.heartbeatEnabled) {
deps.db.setHeartbeat(input.authorAgentId, false, agent.heartbeatIntervalSec || 120); deps.db.agents.setHeartbeat(input.authorAgentId, false, agent.heartbeatIntervalSec || 120);
} }
} catch (error) { } catch (error) {
deps.logger.warn( deps.logger.warn(
+5 -5
View File
@@ -32,7 +32,7 @@ function collectImpactedAgentIdsFromWaitStates(db: AppDatabase, companyId: strin
const thread = db.getThread(waitState.threadId, companyId); const thread = db.getThread(waitState.threadId, companyId);
if (!thread || thread.relatedEntityType !== "task" || !thread.relatedEntityId) continue; if (!thread || thread.relatedEntityType !== "task" || !thread.relatedEntityId) continue;
try { try {
const task = db.getTask(thread.relatedEntityId); const task = db.entityCrud.getTask(thread.relatedEntityId);
if (task.assigneeAgentId) { if (task.assigneeAgentId) {
agentIds.add(task.assigneeAgentId); agentIds.add(task.assigneeAgentId);
} }
@@ -69,7 +69,7 @@ function collectImpactedAgentIdsFromReviewState(db: AppDatabase, companyId: stri
} }
if (!approval.relatedTaskId) continue; if (!approval.relatedTaskId) continue;
try { try {
const task = db.getTask(approval.relatedTaskId); const task = db.entityCrud.getTask(approval.relatedTaskId);
if (task.assigneeAgentId) { if (task.assigneeAgentId) {
agentIds.add(task.assigneeAgentId); agentIds.add(task.assigneeAgentId);
} }
@@ -84,11 +84,11 @@ function collectImpactedAgentIdsFromReviewState(db: AppDatabase, companyId: stri
function resumeAgents(db: AppDatabase, companyId: string, agentIds: Iterable<string>): void { function resumeAgents(db: AppDatabase, companyId: string, agentIds: Iterable<string>): void {
for (const agentId of agentIds) { for (const agentId of agentIds) {
try { try {
const agent = db.getAgent(agentId); const agent = db.entityCrud.getAgent(agentId);
if (agent.companyId !== companyId) continue; if (agent.companyId !== companyId) continue;
if (["terminated", "pending_approval"].includes(agent.status)) continue; if (["terminated", "pending_approval"].includes(agent.status)) continue;
if (!agent.heartbeatEnabled) { if (!agent.heartbeatEnabled) {
db.setHeartbeat(agent.id, true, agent.heartbeatIntervalSec || 120); db.agents.setHeartbeat(agent.id, true, agent.heartbeatIntervalSec || 120);
} }
} catch (err) { } catch (err) {
log.warn(`[execution-mode-reconciler] resumeAgents failed for ${agentId}:`, err instanceof Error ? err.message : String(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, companyId,
); );
for (const task of tasks) { for (const task of tasks) {
db.setTaskRequiresUserReview(task.id, false); db.tasks.setTaskRequiresUserReview(task.id, false);
} }
} }
+1 -1
View File
@@ -36,7 +36,7 @@ export async function notifyHireApproved(
try { try {
let agent; let agent;
try { try {
agent = db.getAgent(input.agentId); agent = db.entityCrud.getAgent(input.agentId);
} catch (err) { } catch (err) {
const classified = classifyUnknownError(err, { const classified = classifyUnknownError(err, {
kind: "recoverable", kind: "recoverable",
+5 -5
View File
@@ -64,7 +64,7 @@ export function registerCoreAgentGraphHandlers({
"Create new agents through the hiring workflow so approval, onboarding, and org state stay synchronized.", "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) { if (!existingAgent || existingAgent.companyId !== parsed.companyId) {
return fail("AGENT_NOT_FOUND", "Agent not found in the current company."); return fail("AGENT_NOT_FOUND", "Agent not found in the current company.");
} }
@@ -101,7 +101,7 @@ export function registerCoreAgentGraphHandlers({
} }
let id: string; let id: string;
try { try {
id = db.saveAgent({ id = db.agents.saveAgent({
id: parsed.id ?? null, id: parsed.id ?? null,
companyId: parsed.companyId, companyId: parsed.companyId,
name: parsed.name, name: parsed.name,
@@ -188,7 +188,7 @@ export function registerCoreAgentGraphHandlers({
const managerLabel = resolvedReportsTo const managerLabel = resolvedReportsTo
? (() => { ? (() => {
try { try {
return db.getAgent(resolvedReportsTo).name; return db.entityCrud.getAgent(resolvedReportsTo).name;
} catch (err) { } catch (err) {
logger.warn(`[ipc/agent-graph] getAgent failed for manager ${resolvedReportsTo}:`, err instanceof Error ? err.message : String(err)); logger.warn(`[ipc/agent-graph] getAgent failed for manager ${resolvedReportsTo}:`, err instanceof Error ? err.message : String(err));
return resolvedReportsTo; return resolvedReportsTo;
@@ -271,14 +271,14 @@ export function registerCoreAgentGraphHandlers({
if (!db.belongsToCompany("agents", parsed.id, parsed.companyId)) { if (!db.belongsToCompany("agents", parsed.id, parsed.companyId)) {
return fail("AGENT_NOT_FOUND", "Agent not found in the current company."); 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") { if (agent.status === "pending_approval") {
return fail( return fail(
"AGENT_APPROVAL_REQUIRED", "AGENT_APPROVAL_REQUIRED",
"Pending hires cannot be deleted from the agent editor. Reject the hire request from Hiring or Approvals instead.", "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({ db.addActivity({
companyId: parsed.companyId, companyId: parsed.companyId,
actor: "board", actor: "board",
+2 -2
View File
@@ -189,7 +189,7 @@ export function registerCoreOperationHandlers({
emitEvent({ type: "agent-message", taskId: parsed.taskId, comment }); emitEvent({ type: "agent-message", taskId: parsed.taskId, comment });
emitEvent({ type: "task-updated", taskId: parsed.taskId, action: "updated" }); emitEvent({ type: "task-updated", taskId: parsed.taskId, action: "updated" });
try { try {
const task = db.getTask(parsed.taskId); const task = db.entityCrud.getTask(parsed.taskId);
if (task.assigneeAgentId) { if (task.assigneeAgentId) {
wakeAgentIfPossible(task.assigneeAgentId, task.companyId, "comment"); wakeAgentIfPossible(task.assigneeAgentId, task.companyId, "comment");
} }
@@ -211,7 +211,7 @@ export function registerCoreOperationHandlers({
if (!db.belongsToCompany("agents", parsed.agentId, parsed.companyId)) { if (!db.belongsToCompany("agents", parsed.agentId, parsed.companyId)) {
return fail("AGENT_NOT_FOUND", "Agent not found in the current company."); 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({ db.addActivity({
companyId: parsed.companyId, companyId: parsed.companyId,
actor: "board", actor: "board",
+2 -2
View File
@@ -166,7 +166,7 @@ export function registerCoreReadHandlers({
const companyError = ensureCompanyExists(parsed.companyId); const companyError = ensureCompanyExists(parsed.companyId);
if (companyError) return companyError; if (companyError) return companyError;
try { 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."); if (task.companyId !== parsed.companyId) return fail("TASK_NOT_FOUND", "Task not found in current company.");
return ok(task); return ok(task);
} catch (err) { } catch (err) {
@@ -180,7 +180,7 @@ export function registerCoreReadHandlers({
const companyError = ensureCompanyExists(parsed.companyId); const companyError = ensureCompanyExists(parsed.companyId);
if (companyError) return companyError; if (companyError) return companyError;
try { 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."); if (agent.companyId !== parsed.companyId) return fail("AGENT_NOT_FOUND", "Agent not found in current company.");
return ok(agent); return ok(agent);
} catch (err) { } catch (err) {
+4 -4
View File
@@ -113,7 +113,7 @@ export function registerCoreTaskGraphHandlers({
const taskId = parsed.id; const taskId = parsed.id;
if (!taskId) return fail("INVALID_TASK_STATE", "Task ID required for updates."); if (!taskId) return fail("INVALID_TASK_STATE", "Task ID required for updates.");
try { try {
const existingTask = db.getTask(taskId); const existingTask = db.entityCrud.getTask(taskId);
previousStatus = existingTask.status; previousStatus = existingTask.status;
previousAssigneeAgentId = existingTask.assigneeAgentId; previousAssigneeAgentId = existingTask.assigneeAgentId;
} catch (err) { } catch (err) {
@@ -124,7 +124,7 @@ export function registerCoreTaskGraphHandlers({
} }
let id: string; let id: string;
try { try {
id = db.saveTask(parsed); id = db.tasks.saveTask(parsed);
} catch (error) { } catch (error) {
return fail("INVALID_TASK_STATE", error instanceof Error ? error.message : "Task could not be saved."); 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 // User (board) manually advancing a flagged task to done: clear the review flag
if (parsed.status === "done") { if (parsed.status === "done") {
try { try {
db.setTaskRequiresUserReview(id, false); db.tasks.setTaskRequiresUserReview(id, false);
} catch (error) { } catch (error) {
logger.warn( logger.warn(
`[ipc-task] setTaskRequiresUserReview(false) failed for task ${id}: ${error instanceof Error ? error.message : String(error)}`, `[ipc-task] setTaskRequiresUserReview(false) failed for task ${id}: ${error instanceof Error ? error.message : String(error)}`,
@@ -175,7 +175,7 @@ export function registerCoreTaskGraphHandlers({
}); });
const deleteHandlers = [ 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.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.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) }, { channel: IPC_CHANNELS.deleteWorkspace, table: "workspaces", entity: "workspace", deleteFn: (id: string, companyId: string) => db.deleteWorkspace(id, companyId) },
+2 -2
View File
@@ -36,7 +36,7 @@ export function registerRecoveryHandlers({
if (!db.belongsToCompany("tasks", parsed.taskId, parsed.companyId)) { if (!db.belongsToCompany("tasks", parsed.taskId, parsed.companyId)) {
return fail("TASK_NOT_FOUND", "Task not found in the current company."); 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 { try {
wakeAgentIfPossible(parsed.agentId, parsed.companyId, "retry_interrupted"); wakeAgentIfPossible(parsed.agentId, parsed.companyId, "retry_interrupted");
} catch (err) { } catch (err) {
@@ -53,7 +53,7 @@ export function registerRecoveryHandlers({
if (!db.belongsToCompany("tasks", parsed.taskId, parsed.companyId)) { if (!db.belongsToCompany("tasks", parsed.taskId, parsed.companyId)) {
return fail("TASK_NOT_FOUND", "Task not found in the current company."); return fail("TASK_NOT_FOUND", "Task not found in the current company.");
} }
db.markInterruptedTaskFailed(parsed.taskId); db.tasks.markInterruptedTaskFailed(parsed.taskId);
publishDomainChanged(); publishDomainChanged();
return ok(null); return ok(null);
}); });
+9 -9
View File
@@ -204,14 +204,14 @@ function resolveProjectWakeTargets(
const project = db.getProjectForUpdate(input.channelTargetId, input.companyId); const project = db.getProjectForUpdate(input.channelTargetId, input.companyId);
const participantIds = new Set<string>(); const participantIds = new Set<string>();
if (project?.leadAgentId) participantIds.add(project.leadAgentId); 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); participantIds.add(assigneeId);
} }
if (participantIds.size === 0) return []; if (participantIds.size === 0) return [];
const ids = Array.from(participantIds).filter((id) => id !== input.fromAgentId); const ids = Array.from(participantIds).filter((id) => id !== input.fromAgentId);
if (ids.length === 0) return []; 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 /** Resolve the agents mentioned in a message body, stripped of the
@@ -222,7 +222,7 @@ export function resolveMentionedAgentIds(
db: AppDatabase, db: AppDatabase,
input: Pick<NormalizedAgentMessageInput, "companyId" | "fromAgentId" | "body">, input: Pick<NormalizedAgentMessageInput, "companyId" | "fromAgentId" | "body">,
): string[] { ): string[] {
const mentioned = db.findMentionedAgentIds(input.companyId, input.body); const mentioned = db.agents.findMentionedAgentIds(input.companyId, input.body);
const filtered: string[] = []; const filtered: string[] = [];
for (const agentId of mentioned) { for (const agentId of mentioned) {
if (!agentId || agentId === input.fromAgentId || agentId === HUMAN_SENDER_ID) continue; if (!agentId || agentId === input.fromAgentId || agentId === HUMAN_SENDER_ID) continue;
@@ -255,7 +255,7 @@ export function resolveMessageWakeTargets(
} }
if (input.channel === "department" && input.channelTargetId) { 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); 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 // workspace is a listener. Board-only fan-outs use the dedicated
// `incident` channel; the previous top-level-only fan-out made the // `incident` channel; the previous top-level-only fan-out made the
// "company" label a misnomer that silently dropped ICs. // "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); if (agentId !== input.fromAgentId) wakeTargets.add(agentId);
} }
} }
@@ -277,7 +277,7 @@ export function resolveMessageWakeTargets(
} }
if (input.channel === "incident") { 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); 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 // 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 // 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 // which throws "Agent not found" and spams the log. Filter once, at
// the point of resolution, so every downstream consumer is clean. // the point of resolution, so every downstream consumer is clean.
wakeTargets.delete(HUMAN_SENDER_ID); wakeTargets.delete(HUMAN_SENDER_ID);
@@ -330,7 +330,7 @@ export function dispatchAgentMessage(
!normalized.fromHumanUser && !normalized.fromHumanUser &&
(message.channel !== "direct" || message.toAgentId === HUMAN_SENDER_ID) (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({ deps.notify({
title: truncateForNotification(`${senderName} · ${message.subject}`, 80), title: truncateForNotification(`${senderName} · ${message.subject}`, 80),
body: truncateForNotification(message.body, 140), body: truncateForNotification(message.body, 140),
@@ -350,7 +350,7 @@ export function dispatchAgentMessage(
const mentionResults: MentionResult[] = []; const mentionResults: MentionResult[] = [];
if (deps.wakeAgentIfPossible) { 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])); const agentNameMap = new Map(agentNameRows.map((a) => [a.id, a.name]));
for (const wakeTarget of wakeTargets) { for (const wakeTarget of wakeTargets) {
const agentName = agentNameMap.get(wakeTarget) ?? wakeTarget; const agentName = agentNameMap.get(wakeTarget) ?? wakeTarget;
+5 -5
View File
@@ -85,9 +85,9 @@ export function createOnboardingService({
}: OnboardingDependencies) { }: OnboardingDependencies) {
function updateAgentWorkspaceBinding(agentId: string, workspaceId: string | null) { function updateAgentWorkspaceBinding(agentId: string, workspaceId: string | null) {
if (!workspaceId) return; if (!workspaceId) return;
const agent = db.getAgent(agentId); const agent = db.entityCrud.getAgent(agentId);
if (agent.workspaceId === workspaceId) return; if (agent.workspaceId === workspaceId) return;
db.saveAgent({ db.agents.saveAgent({
id: agent.id, id: agent.id,
companyId: agent.companyId, companyId: agent.companyId,
name: agent.name, 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.`, `Review the current company state, create the first real tasks, delegate initial work, and report progress through tasks, approvals, and messages.`,
].join("\n\n"); ].join("\n\n");
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
projectId, projectId,
goalId: activeGoal?.id ?? null, goalId: activeGoal?.id ?? null,
@@ -260,7 +260,7 @@ export function createOnboardingService({
} }
const ceoLocale = getLocale(); const ceoLocale = getLocale();
const ceoId = db.saveAgent({ const ceoId = db.agents.saveAgent({
companyId, companyId,
name: "CEO", name: "CEO",
role: "CEO", role: "CEO",
@@ -279,7 +279,7 @@ export function createOnboardingService({
modelStrategy: "explicit", modelStrategy: "explicit",
roleLevel: "c_suite", roleLevel: "c_suite",
}); });
db.setHeartbeat(ceoId, true, 120); db.agents.setHeartbeat(ceoId, true, 120);
db.setCurrentCompany(companyId); db.setCurrentCompany(companyId);
seedDefaultAutomationRules(companyId); seedDefaultAutomationRules(companyId);
+16 -16
View File
@@ -55,9 +55,9 @@ export function createHeartbeat(
} = deps; } = deps;
function ensureAgentHeartbeatEnabled(agentId: string) { function ensureAgentHeartbeatEnabled(agentId: string) {
const agent = db.getAgent(agentId); const agent = db.entityCrud.getAgent(agentId);
if (!agent.heartbeatEnabled) { 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"); if (!hasWorkerReady()) return SKIPPED("worker_not_ready");
const companyStatus = db.getCompanyStatus(companyId); const companyStatus = db.getCompanyStatus(companyId);
if (companyStatus !== "active") return SKIPPED("company_inactive"); if (companyStatus !== "active") return SKIPPED("company_inactive");
const agent = db.getAgent(agentId); const agent = db.entityCrud.getAgent(agentId);
if (agent.companyId !== companyId) { if (agent.companyId !== companyId) {
logger.warn(`[heartbeat] Refusing cross-company run for agent ${agentId}: ${agent.companyId} !== ${companyId}`); logger.warn(`[heartbeat] Refusing cross-company run for agent ${agentId}: ${agent.companyId} !== ${companyId}`);
return SKIPPED("cross_company"); return SKIPPED("cross_company");
@@ -112,10 +112,10 @@ export function createHeartbeat(
// Let getHeartbeatPolicy throw — a missing row signals data corruption // Let getHeartbeatPolicy throw — a missing row signals data corruption
// and silently falling back hides the gate that decides whether the // and silently falling back hides the gate that decides whether the
// agent runs at all. // 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 // 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"); if (activeRunCount >= heartbeatPolicy.maxConcurrentRuns) return SKIPPED("max_concurrent_runs");
// Cooldown check: ensure enough time has passed since last heartbeat // Cooldown check: ensure enough time has passed since last heartbeat
@@ -129,7 +129,7 @@ export function createHeartbeat(
if (!heartbeatPolicy.wakeOnDemand && trigger !== "timer" && trigger !== "bootstrap") { if (!heartbeatPolicy.wakeOnDemand && trigger !== "timer" && trigger !== "bootstrap") {
return SKIPPED("wake_on_demand_disabled"); 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 companyMode = db.getCompanyExecutionMode(companyId);
const approvalSettings = db.getCompanyApprovalSettings(companyId); const approvalSettings = db.getCompanyApprovalSettings(companyId);
@@ -140,7 +140,7 @@ export function createHeartbeat(
if ( if (
shouldEnforceHumanReview && shouldEnforceHumanReview &&
!isMessageWake && !isMessageWake &&
(db.hasAgentPendingDeliverableReview(agentId, companyId) || db.hasAgentTaskAwaitingReview(agentId, companyId)) (db.tasks.hasAgentPendingDeliverableReview(agentId, companyId) || db.tasks.hasAgentTaskAwaitingReview(agentId, companyId))
) { ) {
return SKIPPED("awaiting_user_review"); return SKIPPED("awaiting_user_review");
} }
@@ -150,23 +150,23 @@ export function createHeartbeat(
shouldEnforceHumanReview && shouldEnforceHumanReview &&
!isMessageWake && !isMessageWake &&
!agent.reportsTo && !agent.reportsTo &&
db.hasPendingDeliverableReviews(companyId) db.tasks.hasPendingDeliverableReviews(companyId)
) { ) {
return SKIPPED("pending_deliverable_review"); return SKIPPED("pending_deliverable_review");
} }
const assignedTasks = isCommunicationWake 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 isTimer = trigger === "timer";
const candidateTasks = assignedTasks; const candidateTasks = assignedTasks;
const candidateTask = candidateTasks.find((entry) => !entry.activeRunId) ?? null; const candidateTask = candidateTasks.find((entry) => !entry.activeRunId) ?? null;
const runId = randomUUID(); const runId = randomUUID();
let task: ReturnType<AppDatabase["getTask"]> | null = null; let task: ReturnType<AppDatabase["entityCrud"]["getTask"]> | null = null;
if (candidateTask) { if (candidateTask) {
const checkoutSuccess = db.checkoutTask(candidateTask.id, runId); const checkoutSuccess = db.tasks.checkoutTask(candidateTask.id, runId);
if (!checkoutSuccess) { if (!checkoutSuccess) {
db.addActivity({ db.addActivity({
companyId, companyId,
@@ -178,7 +178,7 @@ export function createHeartbeat(
}); });
return SKIPPED("checkout_race_lost"); return SKIPPED("checkout_race_lost");
} }
task = db.getTask(candidateTask.id); task = db.entityCrud.getTask(candidateTask.id);
} else { } else {
const hasActiveAssignedRun = candidateTasks.some( const hasActiveAssignedRun = candidateTasks.some(
(entry) => entry.activeRunId && hasActiveRun({ taskId: entry.id, companyId }), (entry) => entry.activeRunId && hasActiveRun({ taskId: entry.id, companyId }),
@@ -186,9 +186,9 @@ export function createHeartbeat(
if (hasActiveAssignedRun) return SKIPPED("max_concurrent_runs"); if (hasActiveAssignedRun) return SKIPPED("max_concurrent_runs");
const isCeo = !agent.reportsTo; 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 goalCount = db.countActiveGoalsForCompany(companyId);
const taskCount = db.countActiveTasksForCompany(companyId); const taskCount = db.tasks.countActiveTasksForCompany(companyId);
const companyNeedsBootstrap = goalCount > 0 && taskCount < goalCount; const companyNeedsBootstrap = goalCount > 0 && taskCount < goalCount;
if (isTimer && !isCeo && !isManager && !companyNeedsBootstrap) { if (isTimer && !isCeo && !isManager && !companyNeedsBootstrap) {
@@ -232,7 +232,7 @@ export function createHeartbeat(
resolvedModel: assignment.modelId, resolvedModel: assignment.modelId,
}); });
db.markHeartbeatAt(agentId); db.agents.markHeartbeatAt(agentId);
// Actor audit: record what triggered this run // Actor audit: record what triggered this run
try { try {
@@ -266,7 +266,7 @@ export function createHeartbeat(
// raw) from spamming the log with "Agent not found: __board__". // raw) from spamming the log with "Agent not found: __board__".
if (agentId === HUMAN_SENDER_ID) return null; if (agentId === HUMAN_SENDER_ID) return null;
try { try {
const agent = db.getAgent(agentId); const agent = db.entityCrud.getAgent(agentId);
if (agent.companyId !== companyId) { if (agent.companyId !== companyId) {
logger.warn(`[wake] Refusing cross-company wake for agent ${agentId}: ${agent.companyId} !== ${companyId}`); logger.warn(`[wake] Refusing cross-company wake for agent ${agentId}: ${agent.companyId} !== ${companyId}`);
return null; return null;
+4 -4
View File
@@ -30,7 +30,7 @@ const SUBJECT_PREVIEW_LEN = 80;
export function resolveInitialUserTurn( export function resolveInitialUserTurn(
params: { params: {
trigger: string | null; trigger: string | null;
task: ReturnType<AppDatabase["getTask"]> | null; task: ReturnType<AppDatabase["entityCrud"]["getTask"]> | null;
companyId: string; companyId: string;
agentId: string; agentId: string;
}, },
@@ -61,9 +61,9 @@ export function resolveInitialUserTurn(
// mode prompt. // mode prompt.
if (trigger === "timer" || trigger === "bootstrap" || trigger === "app_restart_resume") { if (trigger === "timer" || trigger === "bootstrap" || trigger === "app_restart_resume") {
try { try {
const agent = deps.db.getAgent(agentId); const agent = deps.db.entityCrud.getAgent(agentId);
if (!agent.reportsTo) { if (!agent.reportsTo) {
const activeReports = deps.db const activeReports = deps.db.agents
.getDirectReports(agentId, companyId) .getDirectReports(agentId, companyId)
.filter((r) => r.status !== "pending_approval" && r.status !== "terminated"); .filter((r) => r.status !== "pending_approval" && r.status !== "terminated");
if (activeReports.length === 0) { if (activeReports.length === 0) {
@@ -145,7 +145,7 @@ function buildGenericWakeTurn(trigger: string | null): string {
function resolveAgentLabel(db: AppDatabase, agentId: string): string { function resolveAgentLabel(db: AppDatabase, agentId: string): string {
try { try {
const name = db.findAgentName(agentId); const name = db.agents.findAgentName(agentId);
if (name && name.length > 0) return name; if (name && name.length > 0) return name;
} catch { } catch {
// fall through // fall through
@@ -23,8 +23,8 @@ export function createHeartbeatScheduler(
function persistSkip(agentId: string, companyId: string, reason: string) { function persistSkip(agentId: string, companyId: string, reason: string) {
try { try {
const existing = db.getRuntimeState(agentId) as Record<string, unknown>; const existing = db.agents.getRuntimeState(agentId) as Record<string, unknown>;
db.setRuntimeState(agentId, companyId, { db.agents.setRuntimeState(agentId, companyId, {
...existing, ...existing,
heartbeatSkip: { reason, at: new Date().toISOString() }, heartbeatSkip: { reason, at: new Date().toISOString() },
}); });
@@ -37,11 +37,11 @@ export function createHeartbeatScheduler(
function clearSkip(agentId: string, companyId: string) { function clearSkip(agentId: string, companyId: string) {
try { try {
const existing = db.getRuntimeState(agentId) as Record<string, unknown>; const existing = db.agents.getRuntimeState(agentId) as Record<string, unknown>;
if (!("heartbeatSkip" in existing)) return; if (!("heartbeatSkip" in existing)) return;
const next = { ...existing }; const next = { ...existing };
delete next.heartbeatSkip; delete next.heartbeatSkip;
db.setRuntimeState(agentId, companyId, next); db.agents.setRuntimeState(agentId, companyId, next);
} catch (error) { } catch (error) {
logger.warn( logger.warn(
`[heartbeat] Failed to clear skip reason for agent ${agentId}: ${error instanceof Error ? error.message : String(error)}`, `[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) { for (const company of activeCompanies) {
try { try {
const eligibleAgents = db.listHeartbeatEligibleAgents(company.id); const eligibleAgents = db.agents.listHeartbeatEligibleAgents(company.id);
for (const agent of eligibleAgents) { for (const agent of eligibleAgents) {
if (hasActiveRun({ agentId: agent.id, companyId: company.id })) continue; if (hasActiveRun({ agentId: agent.id, companyId: company.id })) continue;
@@ -103,9 +103,9 @@ export function createHeartbeatScheduler(
// regular timer interval. countAdvancingTasksForCompany // regular timer interval. countAdvancingTasksForCompany
// counts only todo + in_progress so bootstrapping still // counts only todo + in_progress so bootstrapping still
// fires once the team stalls. // fires once the team stalls.
const advancingCount = db.countAdvancingTasksForCompany(company.id); const advancingCount = db.tasks.countAdvancingTasksForCompany(company.id);
if (goalCount > 0 && advancingCount === 0) { 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 ceoLastWake = ceo?.lastHeartbeatAt ? new Date(ceo.lastHeartbeatAt).getTime() : 0;
const ceoLastAttempt = ceo ? attemptedAtByAgent.get(ceo.id) ?? 0 : 0; const ceoLastAttempt = ceo ? attemptedAtByAgent.get(ceo.id) ?? 0 : 0;
const ceoLastTouch = Math.max(ceoLastWake, ceoLastAttempt); const ceoLastTouch = Math.max(ceoLastWake, ceoLastAttempt);
@@ -34,7 +34,7 @@ export function createHousekeepingScheduler(
const currentMonth = new Date().toISOString().slice(0, 7); const currentMonth = new Date().toISOString().slice(0, 7);
const lastResetMonth = db.getLastBudgetResetMonth(company.id) ?? ""; const lastResetMonth = db.getLastBudgetResetMonth(company.id) ?? "";
if (currentMonth !== lastResetMonth) { if (currentMonth !== lastResetMonth) {
db.resetMonthlyBudgets(company.id); db.agents.resetMonthlyBudgets(company.id);
db.setLastBudgetResetMonth(company.id, currentMonth); db.setLastBudgetResetMonth(company.id, currentMonth);
logger.info(`[housekeeping] Reset monthly budgets for company ${company.name} (${currentMonth})`); logger.info(`[housekeeping] Reset monthly budgets for company ${company.name} (${currentMonth})`);
} }
@@ -17,9 +17,9 @@ export function createIdleAutomationScheduler(deps: OrchestrationDependencies) {
for (const company of activeCompanies) { for (const company of activeCompanies) {
try { try {
const companyAgents = db.listActiveAgentsForCompany(company.id); const companyAgents = db.agents.listActiveAgentsForCompany(company.id);
for (const agent of companyAgents) { 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 lastActive = agent.lastHeartbeatAt ? new Date(agent.lastHeartbeatAt).getTime() : 0;
const idleThreshold = 20 * 60 * 1000; const idleThreshold = 20 * 60 * 1000;
if (now - lastActive > idleThreshold) { if (now - lastActive > idleThreshold) {
+6 -6
View File
@@ -37,13 +37,13 @@ export function createTaskRunner(
logger.warn(`[run] Worker not ready — cannot queue run for task ${taskId} (trigger: ${trigger})`); logger.warn(`[run] Worker not ready — cannot queue run for task ${taskId} (trigger: ${trigger})`);
return fail("WORKER_UNAVAILABLE", "Orchestration worker is not ready."); 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); const companyStatus = db.getCompanyStatus(task.companyId);
if (companyStatus !== "active") { if (companyStatus !== "active") {
return fail("COMPANY_NOT_ACTIVE", "Runs can only start while the company is 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."); 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) { if (agent.companyId !== task.companyId) {
return fail("AGENT_COMPANY_MISMATCH", "The assigned agent does not belong to the task company."); 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 })) { if (hasActiveRun({ agentId: agent.id, companyId: task.companyId })) {
return fail("AGENT_ALREADY_RUNNING", "The assigned agent already has an active run."); 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."); 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); 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) // CAS checkout — atomic claim (EXEC-01, EXEC-02)
const runId = randomUUID(); const runId = randomUUID();
const checkoutSuccess = db.checkoutTask(task.id, runId); const checkoutSuccess = db.tasks.checkoutTask(task.id, runId);
if (!checkoutSuccess) { if (!checkoutSuccess) {
return fail("CHECKOUT_RACE_LOST", "Another run claimed this task. Refresh and try again."); return fail("CHECKOUT_RACE_LOST", "Another run claimed this task. Refresh and try again.");
} }
@@ -180,8 +180,8 @@ export function createTaskRunner(
} }
try { try {
const task = run.taskId ? db.getTask(run.taskId) : null; const task = run.taskId ? db.entityCrud.getTask(run.taskId) : null;
const agent = db.getAgent(run.agentId); const agent = db.entityCrud.getAgent(run.agentId);
const connector = db.getConnector(run.connectorId); const connector = db.getConnector(run.connectorId);
const eligibilityFailure = getRunEligibilityFailure(agent, connector, task?.projectId ?? null); const eligibilityFailure = getRunEligibilityFailure(agent, connector, task?.projectId ?? null);
if (eligibilityFailure) { if (eligibilityFailure) {
+9 -9
View File
@@ -18,11 +18,11 @@ import { resolveInitialUserTurn } from "./initial-user-turn";
export interface RunDispatchParams { export interface RunDispatchParams {
runId: string; runId: string;
companyId: string; companyId: string;
agent: ReturnType<AppDatabase["getAgent"]>; agent: ReturnType<AppDatabase["entityCrud"]["getAgent"]>;
// `task` is null for heartbeat runs (agent wake-ups with no assigned task). // `task` is null for heartbeat runs (agent wake-ups with no assigned task).
// Downstream code (prompt builder, worker payload, session pointer) // Downstream code (prompt builder, worker payload, session pointer)
// branches on this — when null, skip task-scoped state. // branches on this — when null, skip task-scoped state.
task: ReturnType<AppDatabase["getTask"]> | null; task: ReturnType<AppDatabase["entityCrud"]["getTask"]> | null;
connector: ReturnType<AppDatabase["getConnector"]>; connector: ReturnType<AppDatabase["getConnector"]>;
workspace: ReturnType<AppDatabase["getWorkspace"]>; workspace: ReturnType<AppDatabase["getWorkspace"]>;
workspaceMode: WorkspaceLockMode; workspaceMode: WorkspaceLockMode;
@@ -81,8 +81,8 @@ function assembleApiEnv(
} }
function assembleRecentComments( function assembleRecentComments(
assignedTasks: ReturnType<AppDatabase["listAgentTasks"]>, assignedTasks: ReturnType<AppDatabase["tasks"]["listAgentTasks"]>,
reviewTasks: ReturnType<AppDatabase["listAgentTasks"]>, reviewTasks: ReturnType<AppDatabase["tasks"]["listAgentTasks"]>,
snapshot: RunDispatchParams["snapshot"], snapshot: RunDispatchParams["snapshot"],
) { ) {
const relevantTaskIds = new Set([ const relevantTaskIds = new Set([
@@ -150,7 +150,7 @@ function resolveSession(
// Per-agent session: one ongoing transcript per agent; switching tasks // Per-agent session: one ongoing transcript per agent; switching tasks
// (or running taskless heartbeats) becomes a "now work on X" user turn // (or running taskless heartbeats) becomes a "now work on X" user turn
// on the ongoing transcript rather than starting a fresh session. // on the ongoing transcript rather than starting a fresh session.
const agentSessionId = db.ensureAgentSession({ const agentSessionId = db.tasks.ensureAgentSession({
agentId: agent.id, agentId: agent.id,
companyId, companyId,
adapterType: connectorId, 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); const { env, apiUrl, apiKey } = assembleApiEnv(db, connector, companyId, agent.id, runId, trigger, deps);
env.AGENT_COMPANY_TASK_ID = task?.id ?? ""; env.AGENT_COMPANY_TASK_ID = task?.id ?? "";
const assignedTasks = db.listAgentTasks(agent.id, companyId, ["todo", "in_progress", "blocked"]); const assignedTasks = db.tasks.listAgentTasks(agent.id, companyId, ["todo", "in_progress", "blocked"]);
const reviewTasks = db.listReviewTasksForManager(agent.id, companyId); const reviewTasks = db.tasks.listReviewTasksForManager(agent.id, companyId);
const recentComments = assembleRecentComments(assignedTasks, reviewTasks, snapshot); const recentComments = assembleRecentComments(assignedTasks, reviewTasks, snapshot);
const initialHandoff = resolveSessionHandoff(agent.id, deps); const initialHandoff = resolveSessionHandoff(agent.id, deps);
const { taskSessionId, sessionHandoff } = resolveSession(db, agent, companyId, connector.id, initialHandoff, 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, executionMode: db.getCompanyExecutionMode(companyId) as ExecutionMode,
goals: snapshot.goals.filter((goal) => goal.companyId === companyId), goals: snapshot.goals.filter((goal) => goal.companyId === companyId),
projects: snapshot.projects.filter((project) => project.companyId === companyId), projects: snapshot.projects.filter((project) => project.companyId === companyId),
directReports: db.getDirectReports(agent.id, companyId), directReports: db.agents.getDirectReports(agent.id, companyId),
chainOfCommand: db.getChainOfCommand(agent.id), chainOfCommand: db.agents.getChainOfCommand(agent.id),
assignedTasks, assignedTasks,
allAgents: snapshot.agents.filter((entry) => entry.companyId === companyId), allAgents: snapshot.agents.filter((entry) => entry.companyId === companyId),
pendingApprovals: filterPendingApprovals(snapshot.approvals, companyId, agent.id), pendingApprovals: filterPendingApprovals(snapshot.approvals, companyId, agent.id),
+2 -2
View File
@@ -54,7 +54,7 @@ function shouldProduceKnowledge(
); );
} }
try { try {
const agent = db.getAgent(agentId); const agent = db.entityCrud.getAgent(agentId);
return Boolean(agent.department && KNOWLEDGE_PRODUCING_DEPARTMENTS.has(agent.department)); return Boolean(agent.department && KNOWLEDGE_PRODUCING_DEPARTMENTS.has(agent.department));
} catch (error) { } catch (error) {
logger.warn( logger.warn(
@@ -97,7 +97,7 @@ export function createRunHousekeeping({ db, logger, threadIntegration }: RunHous
try { try {
let topic = `Run ${runId.slice(0, 8)}`; let topic = `Run ${runId.slice(0, 8)}`;
try { try {
const task = db.getTask(taskId); const task = db.entityCrud.getTask(taskId);
topic = task.title; topic = task.title;
} catch (error) { } catch (error) {
logger.warn( logger.warn(
@@ -93,9 +93,9 @@ export function buildOrchestrationService({
}, },
evaluatePreRunSessionRotation: (agentId, connectorId) => { evaluatePreRunSessionRotation: (agentId, connectorId) => {
try { try {
const agent = db.getAgent(agentId); const agent = db.entityCrud.getAgent(agentId);
if (!agent.sessionId) return { rotated: false, handoff: null, sessionId: null }; 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 }; if (!session) return { rotated: false, handoff: null, sessionId: agent.sessionId };
const result = sessionService.evaluateRotation(session); const result = sessionService.evaluateRotation(session);
if (result.shouldRotate) { if (result.shouldRotate) {
+4 -4
View File
@@ -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) // 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 { 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 // 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}`); 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, companyId: session.companyId,
agentId: session.agentId, agentId: session.agentId,
adapterType: session.adapterType, 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}`); logger.info(`[session] New session ${newSessionId} created for agent ${session.agentId}`);
return newSessionId; return newSessionId;
+8 -6
View File
@@ -25,7 +25,9 @@ interface CapabilityMatchResult {
} }
interface CapabilityMatchDb { interface CapabilityMatchDb {
getAgent(id: string): { name: string; capabilities: string }; entityCrud: {
getAgent(id: string): { name: string; capabilities: string };
};
listAgentSkillAssignments(id: string): string[]; listAgentSkillAssignments(id: string): string[];
getCompanySkill(companyId: string, skillId: string): { name: string; description: string | null; slug: string } | null; 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." }; 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 capWords = tokenize(agent.capabilities);
const capabilityOverlap = taskWords.filter((w) => capWords.includes(w)).length; const capabilityOverlap = taskWords.filter((w) => capWords.includes(w)).length;
@@ -197,7 +199,7 @@ export function sweepStaleReviews(
const isCofounderMode = deps.getCompanyExecutionMode(companyId) === "ai_cofounder"; const isCofounderMode = deps.getCompanyExecutionMode(companyId) === "ai_cofounder";
let escalatedCount = 0; let escalatedCount = 0;
const reviewTasks = deps.db.listReviewTasksForSweep(companyId); const reviewTasks = deps.db.tasks.listReviewTasksForSweep(companyId);
for (const task of reviewTasks) { for (const task of reviewTasks) {
const reviewAge = now - new Date(task.updatedAt).getTime(); const reviewAge = now - new Date(task.updatedAt).getTime();
@@ -205,11 +207,11 @@ export function sweepStaleReviews(
if (!task.assigneeAgentId) continue; if (!task.assigneeAgentId) continue;
// The reviewer is the assignee's direct manager // 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 if (!reviewer) continue; // No manager = CEO-assigned task, skip escalation
// D-09: Escalate to the reviewer's manager (manager's manager) // 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 escalationTarget = chain.length > 0 ? chain[0] : null;
const pendingApproval = deps.db.findPendingApprovalForTask(companyId, task.id); 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 // D-10: Advance task status from in_review to done
try { 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"); deps.handleTaskStatusChange(task.id, companyId, "in_review", "done");
} catch (err) { } catch (err) {
log.warn(`[task-quality-gates] advanceTaskStatus failed for task ${task.id}:`, err instanceof Error ? err.message : String(err)); log.warn(`[task-quality-gates] advanceTaskStatus failed for task ${task.id}:`, err instanceof Error ? err.message : String(err));
+6 -6
View File
@@ -20,7 +20,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh
*/ */
onTaskCreated(taskId: string, companyId: string): void { onTaskCreated(taskId: string, companyId: string): void {
try { try {
const task = db.getTask(taskId); const task = db.entityCrud.getTask(taskId);
if (task.companyId !== companyId) return; if (task.companyId !== companyId) return;
const thread = ensureTaskThread(db, taskId, companyId, task.title); 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 { onTaskStatusChanged(taskId: string, companyId: string, previousStatus: string, newStatus: string): void {
try { try {
const task = db.getTask(taskId); const task = db.entityCrud.getTask(taskId);
if (task.companyId !== companyId) return; if (task.companyId !== companyId) return;
const thread = ensureTaskThread(db, taskId, companyId, task.title); 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 { onApprovalCreated(approvalId: string, companyId: string, taskId: string | null, approvalType: string, summary: string): void {
try { try {
const approval = db.getApprovalRecord(approvalId); const approval = db.getApprovalRecord(approvalId);
const task = taskId ? db.getTask(taskId) : null; const task = taskId ? db.entityCrud.getTask(taskId) : null;
const thread = task const thread = task
? ensureTaskThread(db, task.id, companyId, task.title) ? ensureTaskThread(db, task.id, companyId, task.title)
: ensureApprovalThread(db, approvalId, companyId, summary || approval.payloadSummary); : ensureApprovalThread(db, approvalId, companyId, summary || approval.payloadSummary);
@@ -181,7 +181,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh
try { try {
if (!taskId) return; 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); const thread = findTaskThread(db, taskId, companyId);
if (!thread) return; if (!thread) return;
@@ -249,7 +249,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh
if (status === "succeeded") { if (status === "succeeded") {
const mode = db.getCompanyExecutionMode(companyId) as ExecutionMode; const mode = db.getCompanyExecutionMode(companyId) as ExecutionMode;
if (mode === "ai_cofounder") { 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.`; const completionReason = `@Board review needed: Agent completed work on "${task.title}" — awaiting your review.`;
db.setThreadWaitState({ db.setThreadWaitState({
threadId: thread.id, threadId: thread.id,
@@ -304,7 +304,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh
const thread = db.getThread(ws.threadId, companyId); const thread = db.getThread(ws.threadId, companyId);
if (!thread || thread.relatedEntityType !== "task" || !thread.relatedEntityId) continue; if (!thread || thread.relatedEntityType !== "task" || !thread.relatedEntityId) continue;
try { try {
const task = db.getTask(thread.relatedEntityId); const task = db.entityCrud.getTask(thread.relatedEntityId);
if (task.assigneeAgentId !== toAgentId) continue; if (task.assigneeAgentId !== toAgentId) continue;
} catch (err) { } catch (err) {
logger.warn(`[thread-integration] getTask failed for thread ${ws.threadId}:`, err instanceof Error ? err.message : String(err)); logger.warn(`[thread-integration] getTask failed for thread ${ws.threadId}:`, err instanceof Error ? err.message : String(err));
@@ -104,7 +104,7 @@ function resolveApprovalAnnouncementChannel(
): { channel: "department" | "project" | "company"; channelTargetId: string | null } | null { ): { channel: "department" | "project" | "company"; channelTargetId: string | null } | null {
if (requestedByAgentId) { if (requestedByAgentId) {
try { try {
const requester = db.getAgent(requestedByAgentId); const requester = db.entityCrud.getAgent(requestedByAgentId);
if (requester.companyId === companyId && requester.department) { if (requester.companyId === companyId && requester.department) {
return { channel: "department", channelTargetId: requester.department }; return { channel: "department", channelTargetId: requester.department };
} }
@@ -115,7 +115,7 @@ function resolveApprovalAnnouncementChannel(
if (taskId) { if (taskId) {
try { try {
const task = db.getTask(taskId); const task = db.entityCrud.getTask(taskId);
if (task.companyId === companyId && task.projectId) { if (task.companyId === companyId && task.projectId) {
return { channel: "project", channelTargetId: task.projectId }; return { channel: "project", channelTargetId: task.projectId };
} }
+1 -1
View File
@@ -197,7 +197,7 @@ export function createWorkerService({
db.releaseWorkspaceLock(run.workspaceId, run.id); db.releaseWorkspaceLock(run.workspaceId, run.id);
} }
if (run.taskId) { if (run.taskId) {
db.releaseTaskLock(run.taskId, run.id); db.tasks.releaseTaskLock(run.taskId, run.id);
} }
} }
} }
+4 -4
View File
@@ -48,12 +48,12 @@ export function handleWorkerExit(
} }
// EXEC-02: Release execution lock on crash recovery // EXEC-02: Release execution lock on crash recovery
if (run.taskId) { if (run.taskId) {
db.releaseTaskLock(run.taskId, run.id); db.tasks.releaseTaskLock(run.taskId, run.id);
} }
affectedAgentIds.add(run.agentId); affectedAgentIds.add(run.agentId);
affectedCompanyIds.set(run.agentId, run.companyId); affectedCompanyIds.set(run.agentId, run.companyId);
const chain = db.getChainOfCommand(run.agentId); const chain = db.agents.getChainOfCommand(run.agentId);
if (chain.length > 0) { if (chain.length > 0) {
wakeAgentIfPossible(chain[0].id, run.companyId, "report_failed"); wakeAgentIfPossible(chain[0].id, run.companyId, "report_failed");
} }
@@ -98,8 +98,8 @@ export function handleWorkerExit(
if (candidate.workspaceId) { if (candidate.workspaceId) {
db.releaseWorkspaceLock(candidate.workspaceId, candidate.id); db.releaseWorkspaceLock(candidate.workspaceId, candidate.id);
} }
const task = candidate.taskId ? db.getTask(candidate.taskId) : null; const task = candidate.taskId ? db.entityCrud.getTask(candidate.taskId) : null;
const agent = db.getAgent(candidate.agentId); const agent = db.entityCrud.getAgent(candidate.agentId);
const connector = db.getConnector(agent.connectorId); const connector = db.getConnector(agent.connectorId);
const { workspace, lockMode } = resolveWorkspaceWithLockMode(db, task, agent, candidate.companyId); const { workspace, lockMode } = resolveWorkspaceWithLockMode(db, task, agent, candidate.companyId);
const recoveryCompany = recoverySnapshot.companies.find((company) => company.id === candidate.companyId); const recoveryCompany = recoverySnapshot.companies.find((company) => company.id === candidate.companyId);
+2 -2
View File
@@ -85,8 +85,8 @@ export interface WorkerServiceDependencies {
dispatchRunToWorker: (params: { dispatchRunToWorker: (params: {
runId: string; runId: string;
companyId: string; companyId: string;
agent: ReturnType<AppDatabase["getAgent"]>; agent: ReturnType<AppDatabase["entityCrud"]["getAgent"]>;
task: ReturnType<AppDatabase["getTask"]> | null; task: ReturnType<AppDatabase["entityCrud"]["getTask"]> | null;
connector: ReturnType<AppDatabase["getConnector"]>; connector: ReturnType<AppDatabase["getConnector"]>;
workspace: ReturnType<AppDatabase["getWorkspace"]>; workspace: ReturnType<AppDatabase["getWorkspace"]>;
workspaceMode: "exclusive" | "shared" | "none"; workspaceMode: "exclusive" | "shared" | "none";
@@ -7,7 +7,7 @@ import type { HandlerContext, HandlerDeps, WorkerMessageEvent } from "./handler-
type RunFinishedEvent = Extract<WorkerMessageEvent, { type: "run-finished" }>; type RunFinishedEvent = Extract<WorkerMessageEvent, { type: "run-finished" }>;
type RunRecord = ReturnType<HandlerDeps["db"]["getRun"]>; type RunRecord = ReturnType<HandlerDeps["db"]["getRun"]>;
type TaskRecord = ReturnType<HandlerDeps["db"]["getTask"]>; type TaskRecord = ReturnType<HandlerDeps["db"]["entityCrud"]["getTask"]>;
export interface RunFinishedContext { export interface RunFinishedContext {
run: RunRecord; run: RunRecord;
@@ -56,7 +56,7 @@ export function finalizeRun(
let task: TaskRecord | null = null; let task: TaskRecord | null = null;
if (run.taskId) { if (run.taskId) {
try { try {
task = db.getTask(run.taskId); task = db.entityCrud.getTask(run.taskId);
} catch (error) { } catch (error) {
logger.warn( logger.warn(
`[finalizeRun] getTask(${run.taskId}) failed; task may have been deleted: ${error instanceof Error ? error.message : String(error)}`, `[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 // heartbeat run — the exact failure mode that lets a taskless transcript
// grow past the model's context window. Resolve the session through the // grow past the model's context window. Resolve the session through the
// agent, never through the task. // agent, never through the task.
let agentRecord: ReturnType<HandlerDeps["db"]["getAgent"]>; let agentRecord: ReturnType<HandlerDeps["db"]["entityCrud"]["getAgent"]>;
try { try {
agentRecord = db.getAgent(run.agentId); agentRecord = db.entityCrud.getAgent(run.agentId);
} catch (error) { } catch (error) {
logger.warn( logger.warn(
`[session] accumulateAndRotateSession: getAgent(${run.agentId}) failed; skipping: ${error instanceof Error ? error.message : String(error)}`, `[session] accumulateAndRotateSession: getAgent(${run.agentId}) failed; skipping: ${error instanceof Error ? error.message : String(error)}`,
@@ -107,7 +107,7 @@ export function accumulateAndRotateSession(
return; return;
} }
if (!agentRecord.sessionId) return; if (!agentRecord.sessionId) return;
const session = db.getSessionById(agentRecord.sessionId); const session = db.tasks.getSessionById(agentRecord.sessionId);
if (!session) return; if (!session) return;
sessionService.accumulateTokenUsage( sessionService.accumulateTokenUsage(
@@ -120,7 +120,7 @@ export function accumulateAndRotateSession(
// that include this run, otherwise a run that just crossed the // that include this run, otherwise a run that just crossed the
// threshold waits a full additional cycle to rotate — the exact // threshold waits a full additional cycle to rotate — the exact
// failure mode that let transcripts balloon past the context window. // 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; if (!updatedSession) return;
const rotationResult = sessionService.evaluateRotation(updatedSession); const rotationResult = sessionService.evaluateRotation(updatedSession);
if (rotationResult.shouldRotate) { if (rotationResult.shouldRotate) {
@@ -154,9 +154,9 @@ export function emitAndNotifyCompletion(
}); });
} }
let updatedAgent: ReturnType<HandlerDeps["db"]["getAgent"]> | null = null; let updatedAgent: ReturnType<HandlerDeps["db"]["entityCrud"]["getAgent"]> | null = null;
try { try {
updatedAgent = db.getAgent(run.agentId); updatedAgent = db.entityCrud.getAgent(run.agentId);
} catch (error) { } catch (error) {
logger.warn( logger.warn(
`[emitAndNotifyCompletion] getAgent(${run.agentId}) failed; skipping budget-paused notification: ${error instanceof Error ? error.message : String(error)}`, `[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; if (hadDeferredWakes || isQuitting() || destroyed) return;
try { 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); const hasWorkToContinue = actionableTasks.length > 0 || hasUnfinishedTeamBuilding(db, run.agentId, run.companyId);
if (!hasWorkToContinue) return; if (!hasWorkToContinue) return;
@@ -305,7 +305,7 @@ export function triggerSelfWake(
); );
// Escalate to manager since agent is stuck // Escalate to manager since agent is stuck
try { try {
const chain = db.getChainOfCommand(run.agentId); const chain = db.agents.getChainOfCommand(run.agentId);
if (chain.length > 0) { if (chain.length > 0) {
wakeAgentIfPossible(chain[0].id, run.companyId, "report_stuck"); wakeAgentIfPossible(chain[0].id, run.companyId, "report_stuck");
} }
@@ -342,9 +342,9 @@ function hasUnfinishedTeamBuilding(
companyId: string, companyId: string,
): boolean { ): boolean {
try { try {
const agent = db.getAgent(agentId); const agent = db.entityCrud.getAgent(agentId);
if (agent.reportsTo !== null) return false; if (agent.reportsTo !== null) return false;
const activeReports = db const activeReports = db.agents
.getDirectReports(agentId, companyId) .getDirectReports(agentId, companyId)
.filter((r) => r.status !== "pending_approval" && r.status !== "terminated"); .filter((r) => r.status !== "pending_approval" && r.status !== "terminated");
if (activeReports.length > 0) return false; if (activeReports.length > 0) return false;
+1 -1
View File
@@ -38,7 +38,7 @@ export async function handleRunLog(
try { try {
const run = db.getRun(event.runId); const run = db.getRun(event.runId);
if (run.taskId) { if (run.taskId) {
db.renewTaskLease(run.taskId, run.id); db.tasks.renewTaskLease(run.taskId, run.id);
} }
leaseRenewalMap.set(event.runId, now); leaseRenewalMap.set(event.runId, now);
} catch (err) { } catch (err) {
@@ -32,7 +32,7 @@ export function handleRunStatus(
try { try {
const run = db.getRun(event.runId); const run = db.getRun(event.runId);
if (run.taskId) { 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); onRunStarted?.(run.id, run.taskId, run.agentId, run.companyId);
} catch (err) { } catch (err) {
@@ -11,12 +11,16 @@
export interface EscalationDeps { export interface EscalationDeps {
db: { db: {
getTaskRetryCount: (taskId: string) => number; tasks: {
incrementTaskRetryCount: (taskId: string) => void; getTaskRetryCount: (taskId: string) => number;
resetTaskRetryCount: (taskId: string) => void; incrementTaskRetryCount: (taskId: string) => void;
getChainOfCommand: (agentId: string) => Array<{ id: string; name: string }>; resetTaskRetryCount: (taskId: string) => void;
getCeoForCompany: (companyId: string) => { id: string; name: string } | null; advanceTaskStatus: (taskId: string, status: string, note: string) => unknown;
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; wakeAgentIfPossible: (agentId: string, companyId: string, trigger: string) => unknown;
notify: (title: string, body: string) => void; notify: (title: string, body: string) => void;
@@ -42,11 +46,11 @@ export function handleFailureEscalation(
scheduleRetry: (fn: () => void, delayMs: number) => void, scheduleRetry: (fn: () => void, delayMs: number) => void,
): EscalationResult { ): EscalationResult {
const { db, wakeAgentIfPossible, notify, logger } = deps; const { db, wakeAgentIfPossible, notify, logger } = deps;
const retryCount = db.getTaskRetryCount(run.taskId); const retryCount = db.tasks.getTaskRetryCount(run.taskId);
if (retryCount < maxRetryAttempts) { if (retryCount < maxRetryAttempts) {
db.incrementTaskRetryCount(run.taskId); db.tasks.incrementTaskRetryCount(run.taskId);
db.advanceTaskStatus( db.tasks.advanceTaskStatus(
run.taskId, run.taskId,
"todo", "todo",
`Run ${eventStatus} (attempt ${retryCount + 1}/${maxRetryAttempts}). Auto-retrying after ${retryCooldownMs / 1000}s cooldown.`, `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 // Retry limit reached — escalate up the chain
db.resetTaskRetryCount(run.taskId); db.tasks.resetTaskRetryCount(run.taskId);
logger.warn( logger.warn(
`[escalation] Task ${run.taskId} failed ${maxRetryAttempts} times. Escalating to manager.`, `[escalation] Task ${run.taskId} failed ${maxRetryAttempts} times. Escalating to manager.`,
); );
let escalated = false; let escalated = false;
try { try {
const chain = db.getChainOfCommand(run.agentId); const chain = db.agents.getChainOfCommand(run.agentId);
if (chain.length > 0) { if (chain.length > 0) {
wakeAgentIfPossible(chain[0].id, run.companyId, "report_failed"); wakeAgentIfPossible(chain[0].id, run.companyId, "report_failed");
escalated = true; escalated = true;
@@ -84,7 +88,7 @@ export function handleFailureEscalation(
if (!escalated) { if (!escalated) {
try { try {
const ceo = db.getCeoForCompany(run.companyId); const ceo = db.agents.getCeoForCompany(run.companyId);
if (ceo) { if (ceo) {
wakeAgentIfPossible(ceo.id, run.companyId, "report_failed"); wakeAgentIfPossible(ceo.id, run.companyId, "report_failed");
escalated = true; escalated = true;
@@ -120,7 +124,7 @@ export function handleSuccessReset(
logger?: EscalationDeps["logger"], logger?: EscalationDeps["logger"],
): void { ): void {
try { try {
db.resetTaskRetryCount(taskId); db.tasks.resetTaskRetryCount(taskId);
} catch (error) { } catch (error) {
logger?.warn( logger?.warn(
`[escalation] resetTaskRetryCount failed for task ${taskId}: ${error instanceof Error ? error.message : String(error)}`, `[escalation] resetTaskRetryCount failed for task ${taskId}: ${error instanceof Error ? error.message : String(error)}`,
@@ -91,7 +91,7 @@ describe("agent model switching", () => {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "Engineer", name: "Engineer",
role: "Engineer", role: "Engineer",
@@ -107,7 +107,7 @@ describe("agent model switching", () => {
const { enqueueWorkerRun, taskRunner } = createHarness(db); const { enqueueWorkerRun, taskRunner } = createHarness(db);
const firstTaskId = db.saveTask({ const firstTaskId = db.tasks.saveTask({
companyId, companyId,
title: "Initial model task", title: "Initial model task",
description: "Use the first model for this run.", description: "Use the first model for this run.",
@@ -137,7 +137,7 @@ describe("agent model switching", () => {
unattributedCost: true, unattributedCost: true,
}); });
db.saveAgent({ db.agents.saveAgent({
id: agentId, id: agentId,
companyId, companyId,
name: "Engineer", name: "Engineer",
@@ -152,7 +152,7 @@ describe("agent model switching", () => {
budgetMonthlyUsd: 200, budgetMonthlyUsd: 200,
}); });
const secondTaskId = db.saveTask({ const secondTaskId = db.tasks.saveTask({
companyId, companyId,
title: "Updated model task", title: "Updated model task",
description: "Use the new model for this run.", description: "Use the new model for this run.",
@@ -45,7 +45,7 @@ async function createHarness() {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const ceoId = db.saveAgent({ const ceoId = db.agents.saveAgent({
companyId, companyId,
name: "CEO", name: "CEO",
role: "CEO", role: "CEO",
@@ -58,7 +58,7 @@ async function createHarness() {
capabilities: "Lead the company", capabilities: "Lead the company",
budgetMonthlyUsd: 250, budgetMonthlyUsd: 250,
}); });
const ctoId = db.saveAgent({ const ctoId = db.agents.saveAgent({
companyId, companyId,
name: "CTO", name: "CTO",
role: "CTO", role: "CTO",
@@ -72,7 +72,7 @@ async function createHarness() {
budgetMonthlyUsd: 220, budgetMonthlyUsd: 220,
department: "engineering", department: "engineering",
}); });
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
title: "Build the org", title: "Build the org",
description: "CTO should hire the first IC under engineering.", 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 }; const body = await response.json() as { agentId: string; approvalId: string; autoApproved: boolean };
expect(body.autoApproved).toBe(false); 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.status).toBe("pending_approval");
expect(pendingHire.reportsTo).toBe(harness.ctoId); expect(pendingHire.reportsTo).toBe(harness.ctoId);
@@ -141,11 +141,11 @@ describe("recursive org execution", () => {
decisionNote: "Approved by board", 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(approvedHire.status).toBe("idle");
expect(harness.db.getDirectReports(harness.ctoId, harness.companyId).map((agent) => agent.id)).toContain(body.agentId); expect(harness.db.agents.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.agents.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.getChainOfCommand(body.agentId).map((agent) => agent.id)).toEqual([harness.ctoId, harness.ceoId]);
const agentResponse = await fetch(`${harness.baseUrl}/api/agents/${body.agentId}`, { const agentResponse = await fetch(`${harness.baseUrl}/api/agents/${body.agentId}`, {
headers: harness.headers, 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 () => { it("routes cross-department hires to the relevant department lead when reportsTo is omitted", async () => {
const harness = await createHarness(); const harness = await createHarness();
try { try {
const hrId = harness.db.saveAgent({ const hrId = harness.db.agents.saveAgent({
companyId: harness.companyId, companyId: harness.companyId,
name: "HR Director", name: "HR Director",
role: "HR Director", role: "HR Director",
@@ -183,7 +183,7 @@ describe("recursive org execution", () => {
budgetMonthlyUsd: 180, budgetMonthlyUsd: 180,
department: "hr", department: "hr",
}); });
const hrTaskId = harness.db.saveTask({ const hrTaskId = harness.db.tasks.saveTask({
companyId: harness.companyId, companyId: harness.companyId,
title: "Fill engineering gap", title: "Fill engineering gap",
description: "Request a backend engineer for the engineering team.", description: "Request a backend engineer for the engineering team.",
@@ -223,7 +223,7 @@ describe("recursive org execution", () => {
expect(response.status).toBe(201); expect(response.status).toBe(201);
const body = await response.json() as { agentId: string }; 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.department).toBe("engineering");
expect(pendingHire.reportsTo).toBe(harness.ctoId); expect(pendingHire.reportsTo).toBe(harness.ctoId);
} finally { } finally {
@@ -263,7 +263,7 @@ describe("recursive org execution", () => {
it("manager cannot route a hire outside their reporting branch", async () => { it("manager cannot route a hire outside their reporting branch", async () => {
const harness = await createHarness(); const harness = await createHarness();
try { try {
const cmoId = harness.db.saveAgent({ const cmoId = harness.db.agents.saveAgent({
companyId: harness.companyId, companyId: harness.companyId,
name: "CMO", name: "CMO",
role: "CMO", role: "CMO",
+2 -2
View File
@@ -33,7 +33,7 @@ async function createHarness() {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "Helios", name: "Helios",
role: "CEO", role: "CEO",
@@ -46,7 +46,7 @@ async function createHarness() {
capabilities: "Operate the company", capabilities: "Operate the company",
budgetMonthlyUsd: 100, budgetMonthlyUsd: 100,
}); });
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
title: "Ship the API", title: "Ship the API",
description: "Capture structured run events.", description: "Capture structured run events.",
@@ -29,7 +29,7 @@ describe("Step 4: Hire approval + decideApproval", () => {
decisionNote: "Approved by board", decisionNote: "Approved by board",
}); });
const updatedAgent = shared.db.getAgent(hireId); const updatedAgent = shared.db.entityCrud.getAgent(hireId);
expect(updatedAgent.status).toBe("idle"); expect(updatedAgent.status).toBe("idle");
}); });
@@ -52,7 +52,7 @@ describe("Step 4: Hire approval + decideApproval", () => {
decisionNote: "Not needed", decisionNote: "Not needed",
}); });
const updatedAgent = shared.db.getAgent(hireId); const updatedAgent = shared.db.entityCrud.getAgent(hireId);
expect(updatedAgent.status).toBe("terminated"); expect(updatedAgent.status).toBe("terminated");
}); });
@@ -89,13 +89,13 @@ describe("Agent Messages", () => {
let agentBId: string; let agentBId: string;
beforeEach(() => { beforeEach(() => {
agentAId = shared.db.saveAgent({ agentAId = shared.db.agents.saveAgent({
companyId: shared.companyId, name: "Alice", role: "Engineer", title: "Senior Engineer", companyId: shared.companyId, name: "Alice", role: "Engineer", title: "Senior Engineer",
department: "engineering", connectorId: "claude-pro", status: "active", department: "engineering", connectorId: "claude-pro", status: "active",
capabilities: "Backend development, API design, code review", capabilities: "Backend development, API design, code review",
budgetMonthlyUsd: 100, budgetMonthlyUsd: 100,
}); });
agentBId = shared.db.saveAgent({ agentBId = shared.db.agents.saveAgent({
companyId: shared.companyId, name: "Bob", role: "Designer", title: "UX Designer", companyId: shared.companyId, name: "Bob", role: "Designer", title: "UX Designer",
department: "design", connectorId: "claude-pro", status: "active", department: "design", connectorId: "claude-pro", status: "active",
capabilities: "UI design, wireframing, user research", capabilities: "UI design, wireframing, user research",
@@ -178,7 +178,7 @@ describe("Agent Messages", () => {
leadAgentId: engineeringLeadId, leadAgentId: engineeringLeadId,
status: "active", status: "active",
}); });
shared.db.saveTask({ shared.db.tasks.saveTask({
companyId: shared.companyId, companyId: shared.companyId,
title: "Project task", title: "Project task",
description: "Do work", description: "Do work",
@@ -240,13 +240,13 @@ describe("Auto Task Assignment", () => {
let frontendId: string; let frontendId: string;
beforeEach(() => { beforeEach(() => {
backendId = shared.db.saveAgent({ backendId = shared.db.agents.saveAgent({
companyId: shared.companyId, name: "Backend Dev", role: "Backend Engineer", title: "Senior Backend", companyId: shared.companyId, name: "Backend Dev", role: "Backend Engineer", title: "Senior Backend",
department: "engineering", connectorId: "openai-codex", status: "active", department: "engineering", connectorId: "openai-codex", status: "active",
capabilities: "API development, database design, Node.js, PostgreSQL, backend testing", capabilities: "API development, database design, Node.js, PostgreSQL, backend testing",
budgetMonthlyUsd: 200, budgetMonthlyUsd: 200,
}); });
frontendId = shared.db.saveAgent({ frontendId = shared.db.agents.saveAgent({
companyId: shared.companyId, name: "Frontend Dev", role: "Frontend Engineer", title: "Senior Frontend", companyId: shared.companyId, name: "Frontend Dev", role: "Frontend Engineer", title: "Senior Frontend",
department: "engineering", connectorId: "openai-codex", status: "active", department: "engineering", connectorId: "openai-codex", status: "active",
capabilities: "React, TypeScript, CSS, Tailwind, responsive design, frontend testing, UI components", 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", () => { 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", companyId: shared.companyId, title: "Build React component for user dashboard",
description: "Create a responsive React component with Tailwind CSS styling", description: "Create a responsive React component with Tailwind CSS styling",
priority: "medium", status: "todo", 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 expect(assignedId).toBe(frontendId); // "React", "responsive", "Tailwind" match frontend
}); });
it("assigns backend task to backend agent", () => { 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", companyId: shared.companyId, title: "Design database schema for orders",
description: "PostgreSQL database design with API endpoints for order management", description: "PostgreSQL database design with API endpoints for order management",
priority: "high", status: "todo", 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 expect(assignedId).toBe(backendId); // "database", "PostgreSQL", "API" match backend
}); });
it("returns existing assignee if task already assigned", () => { 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", companyId: shared.companyId, title: "Already assigned task",
description: "This task has an assignee", description: "This task has an assignee",
assigneeAgentId: frontendId, assigneeAgentId: frontendId,
priority: "medium", status: "todo", 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); expect(result).toBe(frontendId);
}); });
it("returns null if all agents have exhausted their budget", () => { it("returns null if all agents have exhausted their budget", () => {
// Set positive budgets that are fully spent // 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.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.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: 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) // 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(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); (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 taskId = shared.db.tasks.saveTask({ companyId: shared.companyId, title: "No budget task", description: "Test", priority: "low", status: "todo" });
const result = shared.db.autoAssignTask(taskId, shared.companyId); const result = shared.db.tasks.autoAssignTask(taskId, shared.companyId);
expect(result).toBeNull(); expect(result).toBeNull();
}); });
it("penalizes agents with many active tasks", () => { it("penalizes agents with many active tasks", () => {
// Give backend agent 5 active tasks // Give backend agent 5 active tasks
for (let i = 0; i < 5; i++) { 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 // 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 taskId = shared.db.tasks.saveTask({ companyId: shared.companyId, title: "Implement feature", description: "Build the feature", 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); // Frontend has 0 active tasks vs backend's 5 expect(assignedId).toBe(frontendId); // Frontend has 0 active tasks vs backend's 5
}); });
}); });
@@ -40,7 +40,7 @@ describe("Step 2: CEO phased prompts", () => {
const ceoId = createAgent({ name: "CEO", role: "CEO" }); const ceoId = createAgent({ name: "CEO", role: "CEO" });
const hireId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "pending_approval" as "idle" }); 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({ const ctx = buildCtx({
agentId: ceoId, 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", () => { it("Phase 2: shows normal executive instructions when active reports and no pending org approval", () => {
const ceoId = createAgent({ name: "CEO", role: "CEO" }); const ceoId = createAgent({ name: "CEO", role: "CEO" });
const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "idle" }); 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({ const ctx = buildCtx({
agentId: ceoId, agentId: ceoId,
@@ -109,7 +109,7 @@ describe("Step 2: CEO phased prompts", () => {
it("includes pending approvals section in prompt", () => { it("includes pending approvals section in prompt", () => {
const ceoId = createAgent({ name: "CEO", role: "CEO" }); const ceoId = createAgent({ name: "CEO", role: "CEO" });
const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "idle" }); 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({ const ctx = buildCtx({
agentId: ceoId, agentId: ceoId,
@@ -139,10 +139,10 @@ describe("Step 2: CEO phased prompts", () => {
it("Manager prompt includes credential escalation guidance", () => { it("Manager prompt includes credential escalation guidance", () => {
const ceoId = createAgent({ name: "CEO", role: "CEO" }); 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 managerId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId });
const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: managerId }); 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({ const ctx = buildCtx({
agentId: managerId, agentId: managerId,
@@ -156,7 +156,7 @@ describe("Step 2: CEO phased prompts", () => {
it("IC prompt includes credential escalation guidance", () => { it("IC prompt includes credential escalation guidance", () => {
const ceoId = createAgent({ name: "CEO", role: "CEO" }); 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 icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ceoId });
const ctx = buildCtx({ const ctx = buildCtx({
@@ -13,7 +13,7 @@ const { createAgent } = shared;
describe("Step 7: Worker crash recovery (database layer)", () => { describe("Step 7: Worker crash recovery (database layer)", () => {
it("interruptStaleRuns clears running and queued runs on init", async () => { it("interruptStaleRuns clears running and queued runs on init", async () => {
const agentId = createAgent({ name: "Agent" }); const agentId = createAgent({ name: "Agent" });
const taskId = shared.db.saveTask({ const taskId = shared.db.tasks.saveTask({
companyId: shared.companyId, companyId: shared.companyId,
title: "In-progress task", title: "In-progress task",
description: "Running when crash happened", 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", () => { it("finishRun can mark interrupted runs from crash recovery", () => {
const agentId = createAgent({ name: "Agent" }); const agentId = createAgent({ name: "Agent" });
const taskId = shared.db.saveTask({ const taskId = shared.db.tasks.saveTask({
companyId: shared.companyId, companyId: shared.companyId,
title: "Crash recovery task", title: "Crash recovery task",
description: "Test", description: "Test",
@@ -40,8 +40,8 @@ describe("E2E: Full autonomous CEO cold start flow", () => {
}); });
// CEO now in Phase 1 // CEO now in Phase 1
const ctoRecord = shared.db.getAgent(ctoId); const ctoRecord = shared.db.entityCrud.getAgent(ctoId);
const cmoRecord = shared.db.getAgent(cmoId); const cmoRecord = shared.db.entityCrud.getAgent(cmoId);
const phase1Ctx = buildCtx({ const phase1Ctx = buildCtx({
agentId: ceoId, agentId: ceoId,
directReports: [ctoRecord, cmoRecord], directReports: [ctoRecord, cmoRecord],
@@ -85,15 +85,15 @@ describe("E2E: Full autonomous CEO cold start flow", () => {
shared.db.decideApproval({ approvalId: hireApprovalCMO, state: "approved", decisionNote: "Approved" }); shared.db.decideApproval({ approvalId: hireApprovalCMO, state: "approved", decisionNote: "Approved" });
// Agents now idle // Agents now idle
expect(shared.db.getAgent(ctoId).status).toBe("idle"); expect(shared.db.entityCrud.getAgent(ctoId).status).toBe("idle");
expect(shared.db.getAgent(cmoId).status).toBe("idle"); expect(shared.db.entityCrud.getAgent(cmoId).status).toBe("idle");
// 5. Board approves org strategy // 5. Board approves org strategy
shared.db.decideApproval({ approvalId: orgApprovalId, state: "approved", decisionNote: "Go ahead" }); shared.db.decideApproval({ approvalId: orgApprovalId, state: "approved", decisionNote: "Go ahead" });
// 6. CEO now in Phase 2 // 6. CEO now in Phase 2
const updatedCto = shared.db.getAgent(ctoId); const updatedCto = shared.db.entityCrud.getAgent(ctoId);
const updatedCmo = shared.db.getAgent(cmoId); const updatedCmo = shared.db.entityCrud.getAgent(cmoId);
const phase2Ctx = buildCtx({ const phase2Ctx = buildCtx({
agentId: ceoId, agentId: ceoId,
directReports: [updatedCto, updatedCmo], directReports: [updatedCto, updatedCmo],
@@ -106,7 +106,7 @@ describe("E2E: Full autonomous CEO cold start flow", () => {
expect(phase2Prompt).toContain("Delegation"); expect(phase2Prompt).toContain("Delegation");
// 7. Verify hierarchical chain // 7. Verify hierarchical chain
const ctoChain = shared.db.getChainOfCommand(ctoId); const ctoChain = shared.db.agents.getChainOfCommand(ctoId);
expect(ctoChain).toHaveLength(1); expect(ctoChain).toHaveLength(1);
expect(ctoChain[0].id).toBe(ceoId); expect(ctoChain[0].id).toBe(ceoId);
@@ -120,7 +120,7 @@ describe("E2E: Full autonomous CEO cold start flow", () => {
status: "in_progress", status: "in_progress",
}); });
const parentTaskId = shared.db.saveTask({ const parentTaskId = shared.db.tasks.saveTask({
companyId: shared.companyId, companyId: shared.companyId,
title: "Build backend", title: "Build backend",
description: "Implement API", 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) // CTO creates subtask for IC (would be done via API)
const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ctoId, status: "idle" }); 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, companyId: shared.companyId,
title: "Implement auth", title: "Implement auth",
description: "JWT auth", description: "JWT auth",
@@ -146,7 +146,7 @@ describe("E2E: Full autonomous CEO cold start flow", () => {
}); });
// Verify parent-child relationship // Verify parent-child relationship
const subtask = shared.db.getTask(subtaskId); const subtask = shared.db.entityCrud.getTask(subtaskId);
expect(subtask.parentId).toBe(parentTaskId); expect(subtask.parentId).toBe(parentTaskId);
// 9. Wakeup request test: enqueue wake while IC is busy // 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({ const icPromptCtx = buildCtx({
agentId: icId, agentId: icId,
task: subtask, task: subtask,
chainOfCommand: [shared.db.getAgent(ctoId), shared.db.getAgent(ceoId)], chainOfCommand: [shared.db.entityCrud.getAgent(ctoId), shared.db.entityCrud.getAgent(ceoId)],
wakeReason: "assignment", wakeReason: "assignment",
}); });
const icPrompt = buildTaskPrompt(icPromptCtx); const icPrompt = buildTaskPrompt(icPromptCtx);
@@ -202,11 +202,11 @@ describe("E2E: Blocked escalation through org hierarchy", () => {
const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ctoId }); const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ctoId });
// When IC marks task blocked, we need to find its manager // 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 expect(chain[0].id).toBe(ctoId); // immediate manager gets woken
// When CTO needs to escalate further // 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); 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" }); const ceoId = createAgent({ name: "CEO", role: "CEO" });
// Hires submitted but not yet approved // Hires submitted but not yet approved
const hireId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "pending_approval" as "idle" }); 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 // Org strategy already approved (not in pendingApprovals), but hires still pending
const ctx = buildCtx({ 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)", () => { it("moves to Phase 2 once hires are approved (no pending org approval)", () => {
const ceoId = createAgent({ name: "CEO", role: "CEO" }); const ceoId = createAgent({ name: "CEO", role: "CEO" });
const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId, status: "idle" }); 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({ const ctx = buildCtx({
agentId: ceoId, agentId: ceoId,
@@ -315,7 +315,7 @@ describe("Review Fix: Root task completion notification", () => {
const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId }); const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId });
// CTO has a root-level task (no parentId) // CTO has a root-level task (no parentId)
const taskId = shared.db.saveTask({ const taskId = shared.db.tasks.saveTask({
companyId: shared.companyId, companyId: shared.companyId,
title: "Standalone task from CEO", title: "Standalone task from CEO",
description: "No parent", description: "No parent",
@@ -325,11 +325,11 @@ describe("Review Fix: Root task completion notification", () => {
status: "done", status: "done",
}); });
const task = shared.db.getTask(taskId); const task = shared.db.entityCrud.getTask(taskId);
expect(task.parentId).toBeNull(); expect(task.parentId).toBeNull();
// CTO's chain of command includes CEO — so api-server can wake CEO on completion // 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).toHaveLength(1);
expect(chain[0].id).toBe(ceoId); expect(chain[0].id).toBe(ceoId);
}); });
@@ -31,9 +31,9 @@ describe("Step 1: CEO role detection", () => {
it("identifies manager when has reports and has chain of command", () => { it("identifies manager when has reports and has chain of command", () => {
const ceoId = createAgent({ name: "CEO", role: "CEO" }); const ceoId = createAgent({ name: "CEO", role: "CEO" });
const managerId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId }); 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 icId = createAgent({ name: "IC", role: "Engineer", reportsTo: managerId });
const ic = shared.db.getAgent(icId); const ic = shared.db.entityCrud.getAgent(icId);
const ctx = buildCtx({ const ctx = buildCtx({
agentId: managerId, agentId: managerId,
@@ -49,7 +49,7 @@ describe("Step 1: CEO role detection", () => {
it("identifies IC when has chain of command and no direct reports", () => { it("identifies IC when has chain of command and no direct reports", () => {
const ceoId = createAgent({ name: "CEO", role: "CEO" }); 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 icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ceoId });
const ctx = buildCtx({ const ctx = buildCtx({
@@ -12,7 +12,7 @@ const { createAgent } = shared;
describe("Step 6: Run failure escalation (database layer)", () => { describe("Step 6: Run failure escalation (database layer)", () => {
it("finishRun marks run as failed and returns outcome", () => { it("finishRun marks run as failed and returns outcome", () => {
const agentId = createAgent({ name: "IC", role: "Engineer" }); const agentId = createAgent({ name: "IC", role: "Engineer" });
const taskId = shared.db.saveTask({ const taskId = shared.db.tasks.saveTask({
companyId: shared.companyId, companyId: shared.companyId,
title: "Failing task", title: "Failing task",
description: "Will fail", 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 ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId });
const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ctoId }); 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) // First in chain is the immediate manager (CTO)
expect(chain.length).toBeGreaterThan(0); expect(chain.length).toBeGreaterThan(0);
expect(chain[0].name).toBe("CTO"); expect(chain[0].name).toBe("CTO");
@@ -30,7 +30,7 @@ export interface SharedHandle {
set db(value: AppDatabase); set db(value: AppDatabase);
get companyId(): string; get companyId(): string;
get workspaceId(): string; get workspaceId(): string;
createAgent(overrides?: Partial<Parameters<AppDatabase["saveAgent"]>[0]>): string; createAgent(overrides?: Partial<Parameters<AppDatabase["agents"]["saveAgent"]>[0]>): string;
buildCtx(overrides?: Record<string, unknown>): Parameters<typeof buildSystemPrompt>[0]; buildCtx(overrides?: Record<string, unknown>): Parameters<typeof buildSystemPrompt>[0];
tempDirs: string[]; tempDirs: string[];
} }
@@ -80,8 +80,8 @@ export function installSharedSetup(): SharedHandle {
return state.db; return state.db;
} }
function createAgent(overrides: Partial<Parameters<AppDatabase["saveAgent"]>[0]> = {}): string { function createAgent(overrides: Partial<Parameters<AppDatabase["agents"]["saveAgent"]>[0]> = {}): string {
return currentDb().saveAgent({ return currentDb().agents.saveAgent({
companyId: state.companyId, companyId: state.companyId,
name: "TestAgent", name: "TestAgent",
role: "CEO", role: "CEO",
@@ -98,7 +98,7 @@ export function installSharedSetup(): SharedHandle {
} }
function buildCtx(overrides: Record<string, unknown> = {}): Parameters<typeof buildSystemPrompt>[0] { function buildCtx(overrides: Record<string, unknown> = {}): Parameters<typeof buildSystemPrompt>[0] {
const agent = currentDb().getAgent(overrides.agentId as string ?? createAgent()); const agent = currentDb().entityCrud.getAgent(overrides.agentId as string ?? createAgent());
return { return {
agent, agent,
apiUrl: "http://127.0.0.1:3000", apiUrl: "http://127.0.0.1:3000",
@@ -15,7 +15,7 @@ describe("Step 5: Task status + comment wake propagation", () => {
const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId }); const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId });
const icId = createAgent({ name: "Dev", role: "Engineer", reportsTo: ctoId }); 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).toHaveLength(2);
expect(chain[0].id).toBe(ctoId); // immediate manager expect(chain[0].id).toBe(ctoId); // immediate manager
expect(chain[1].id).toBe(ceoId); // top level 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) // Create a potential cycle (shouldn't happen in practice, but guard against it)
const agent1 = createAgent({ name: "Agent1" }); const agent1 = createAgent({ name: "Agent1" });
// agent1 reports to no one (CEO), so chain should be empty // 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); expect(chain).toHaveLength(0);
}); });
@@ -33,7 +33,7 @@ describe("Step 5: Task status + comment wake propagation", () => {
const ceoId = createAgent({ name: "CEO", role: "CEO" }); const ceoId = createAgent({ name: "CEO", role: "CEO" });
const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId }); const ctoId = createAgent({ name: "CTO", role: "CTO", reportsTo: ceoId });
const parentTaskId = shared.db.saveTask({ const parentTaskId = shared.db.tasks.saveTask({
companyId: shared.companyId, companyId: shared.companyId,
title: "Build feature X", title: "Build feature X",
description: "Top-level task", description: "Top-level task",
@@ -43,7 +43,7 @@ describe("Step 5: Task status + comment wake propagation", () => {
status: "in_progress", status: "in_progress",
}); });
const childTaskId = shared.db.saveTask({ const childTaskId = shared.db.tasks.saveTask({
companyId: shared.companyId, companyId: shared.companyId,
title: "Implement module A", title: "Implement module A",
description: "Subtask", description: "Subtask",
@@ -54,10 +54,10 @@ describe("Step 5: Task status + comment wake propagation", () => {
status: "todo", status: "todo",
}); });
const childTask = shared.db.getTask(childTaskId); const childTask = shared.db.entityCrud.getTask(childTaskId);
expect(childTask.parentId).toBe(parentTaskId); expect(childTask.parentId).toBe(parentTaskId);
const parentTask = shared.db.getTask(parentTaskId); const parentTask = shared.db.entityCrud.getTask(parentTaskId);
expect(parentTask.assigneeAgentId).toBe(ctoId); expect(parentTask.assigneeAgentId).toBe(ctoId);
}); });
}); });
@@ -14,7 +14,7 @@ describe("Step 8: Wake reason injection", () => {
it("includes wake reason section for non-timer triggers", () => { it("includes wake reason section for non-timer triggers", () => {
const agentId = createAgent({ name: "CTO", role: "CTO" }); const agentId = createAgent({ name: "CTO", role: "CTO" });
const ceoId = createAgent({ name: "CEO", role: "CEO" }); const ceoId = createAgent({ name: "CEO", role: "CEO" });
const ceo = shared.db.getAgent(ceoId); const ceo = shared.db.entityCrud.getAgent(ceoId);
const ctx = buildCtx({ const ctx = buildCtx({
agentId, agentId,
@@ -92,7 +92,7 @@ describe("Step 8: Wake reason injection", () => {
it("includes wake reason before task description", () => { it("includes wake reason before task description", () => {
const agentId = createAgent({ name: "Agent" }); const agentId = createAgent({ name: "Agent" });
const taskId = shared.db.saveTask({ const taskId = shared.db.tasks.saveTask({
companyId: shared.companyId, companyId: shared.companyId,
title: "Do the thing", title: "Do the thing",
description: "Test task", description: "Test task",
@@ -101,7 +101,7 @@ describe("Step 8: Wake reason injection", () => {
priority: "medium", priority: "medium",
status: "todo", status: "todo",
}); });
const task = shared.db.getTask(taskId); const task = shared.db.entityCrud.getTask(taskId);
const ctx = buildCtx({ const ctx = buildCtx({
agentId, agentId,
@@ -90,7 +90,7 @@ describe("connector model enforcement", () => {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "CEO", name: "CEO",
role: "CEO", role: "CEO",
@@ -103,7 +103,7 @@ describe("connector model enforcement", () => {
capabilities: "Lead the company", capabilities: "Lead the company",
budgetMonthlyUsd: 250, budgetMonthlyUsd: 250,
}); });
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
title: "Run with invalid model", title: "Run with invalid model",
description: "This task should be blocked because the agent model is disabled.", description: "This task should be blocked because the agent model is disabled.",
@@ -154,7 +154,7 @@ describe("connector model enforcement", () => {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "CEO", name: "CEO",
role: "CEO", role: "CEO",
@@ -167,7 +167,7 @@ describe("connector model enforcement", () => {
capabilities: "Lead the company", capabilities: "Lead the company",
budgetMonthlyUsd: 250, budgetMonthlyUsd: 250,
}); });
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
title: "Recover queued run", title: "Recover queued run",
description: "This queued run should fail on recovery because the model is disabled.", description: "This queued run should fail on recovery because the model is disabled.",
@@ -184,7 +184,7 @@ describe("connector model enforcement", () => {
workspaceId, workspaceId,
connectorId: "openai-codex", connectorId: "openai-codex",
}); });
const checkoutSuccess = db.checkoutTask(taskId, run.id); const checkoutSuccess = db.tasks.checkoutTask(taskId, run.id);
expect(checkoutSuccess).toBe(true); expect(checkoutSuccess).toBe(true);
db.setDisabledModels(companyId, "openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]); db.setDisabledModels(companyId, "openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]);
+19 -19
View File
@@ -79,7 +79,7 @@ function scaffold(db: AppDatabase, budgetAmountCents = 500): ScaffoldResult {
isPrimary: true, isPrimary: true,
}); });
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "PipelineAgent", name: "PipelineAgent",
role: "IC", role: "IC",
@@ -175,7 +175,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
expect(validation.errors).toHaveLength(0); expect(validation.errors).toHaveLength(0);
// Create the task in the DB // Create the task in the DB
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId: s.companyId, companyId: s.companyId,
title: "Build login page", title: "Build login page",
description: description:
@@ -194,7 +194,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
workspaceId: s.workspaceId, workspaceId: s.workspaceId,
connectorId: s.connectorId, connectorId: s.connectorId,
}); });
const checkoutResult = db.checkoutTask(taskId, runId); const checkoutResult = db.tasks.checkoutTask(taskId, runId);
expect(checkoutResult).toBe(true); expect(checkoutResult).toBe(true);
// 3. Budget gate: verify run is allowed // 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 // 5. Session management: create session and track usage
const sessionId = db.createNewSession({ const sessionId = db.tasks.createNewSession({
companyId: s.companyId, companyId: s.companyId,
agentId: s.agentId, agentId: s.agentId,
adapterType: "gemini-cli", adapterType: "gemini-cli",
@@ -232,10 +232,10 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
const run = db.getRun(runId); const run = db.getRun(runId);
expect(run.status).toBe("succeeded"); expect(run.status).toBe("succeeded");
const task = db.getTask(taskId); const task = db.entityCrud.getTask(taskId);
expect(task.costUsd).toBeGreaterThan(0); expect(task.costUsd).toBeGreaterThan(0);
const session = db.getSessionById(sessionId); const session = db.tasks.getSessionById(sessionId);
expect(session).not.toBeNull(); expect(session).not.toBeNull();
expect(session!.totalInputTokens).toBe(5000); expect(session!.totalInputTokens).toBe(5000);
expect(session!.totalOutputTokens).toBe(2000); expect(session!.totalOutputTokens).toBe(2000);
@@ -254,7 +254,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
it("CAS prevents double checkout within pipeline", () => { it("CAS prevents double checkout within pipeline", () => {
const s = scaffold(db); const s = scaffold(db);
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId: s.companyId, companyId: s.companyId,
title: "Concurrent Task", title: "Concurrent Task",
description: description:
@@ -273,10 +273,10 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
workspaceId: s.workspaceId, workspaceId: s.workspaceId,
connectorId: s.connectorId, 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 // Second agent creates a run but cannot check out
const agent2Id = db.saveAgent({ const agent2Id = db.agents.saveAgent({
companyId: s.companyId, companyId: s.companyId,
name: "Agent2", name: "Agent2",
role: "IC", role: "IC",
@@ -296,7 +296,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
workspaceId: s.workspaceId, workspaceId: s.workspaceId,
connectorId: s.connectorId, 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", () => { 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 // Create and complete a run that exceeds the budget
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId: s.companyId, companyId: s.companyId,
title: "Expensive Task", title: "Expensive Task",
description: description:
@@ -330,7 +330,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
connectorId: s.connectorId, connectorId: s.connectorId,
}); });
db.checkoutTask(taskId, runId); db.tasks.checkoutTask(taskId, runId);
db.markRunStatus(runId, "running", "Starting"); db.markRunStatus(runId, "running", "Starting");
db.recordCostEntry({ runId, companyId: s.companyId, agentId: s.agentId, taskId, connectorId: s.connectorId, amountUsd: 1.5, unattributed: false }); db.recordCostEntry({ runId, companyId: s.companyId, agentId: s.agentId, taskId, connectorId: s.connectorId, amountUsd: 1.5, unattributed: false });
db.finishRun({ db.finishRun({
@@ -363,7 +363,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
logger: mockLogger, logger: mockLogger,
}); });
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId: s.companyId, companyId: s.companyId,
title: "Multi-Run Task", title: "Multi-Run Task",
description: description:
@@ -375,7 +375,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
}); });
// Create session // Create session
const sessionId = db.createNewSession({ const sessionId = db.tasks.createNewSession({
companyId: s.companyId, companyId: s.companyId,
agentId: s.agentId, agentId: s.agentId,
adapterType: "gemini-cli", adapterType: "gemini-cli",
@@ -402,7 +402,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
sessionService.accumulateTokenUsage(sessionId, 3000, 1000, runId2); sessionService.accumulateTokenUsage(sessionId, 3000, 1000, runId2);
// Verify accumulated session state // Verify accumulated session state
const session = db.getSessionById(sessionId); const session = db.tasks.getSessionById(sessionId);
expect(session).not.toBeNull(); expect(session).not.toBeNull();
expect(session!.runCount).toBe(2); expect(session!.runCount).toBe(2);
expect(session!.totalInputTokens).toBe(8000); expect(session!.totalInputTokens).toBe(8000);
@@ -421,7 +421,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
}); });
// --- First run: costs $3, budget allows --- // --- First run: costs $3, budget allows ---
const taskId1 = db.saveTask({ const taskId1 = db.tasks.saveTask({
companyId: s.companyId, companyId: s.companyId,
title: "First Pipeline Task", title: "First Pipeline Task",
description: description:
@@ -439,7 +439,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
workspaceId: s.workspaceId, workspaceId: s.workspaceId,
connectorId: s.connectorId, connectorId: s.connectorId,
}); });
db.checkoutTask(taskId1, runId1); db.tasks.checkoutTask(taskId1, runId1);
db.markRunStatus(runId1, "running", "Starting"); 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.recordCostEntry({ runId: runId1, companyId: s.companyId, agentId: s.agentId, taskId: taskId1, connectorId: s.connectorId, amountUsd: 3.0, unattributed: false });
db.finishRun({ db.finishRun({
@@ -462,7 +462,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
expect(afterFirst.allowed).toBe(true); expect(afterFirst.allowed).toBe(true);
// --- Second run: costs $3, now total = $6 > $5 budget --- // --- Second run: costs $3, now total = $6 > $5 budget ---
const taskId2 = db.saveTask({ const taskId2 = db.tasks.saveTask({
companyId: s.companyId, companyId: s.companyId,
title: "Second Pipeline Task", title: "Second Pipeline Task",
description: description:
@@ -480,7 +480,7 @@ describe("Cross-Subsystem Pipeline (D-04)", () => {
workspaceId: s.workspaceId, workspaceId: s.workspaceId,
connectorId: s.connectorId, connectorId: s.connectorId,
}); });
db.checkoutTask(taskId2, runId2); db.tasks.checkoutTask(taskId2, runId2);
db.markRunStatus(runId2, "running", "Starting"); 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.recordCostEntry({ runId: runId2, companyId: s.companyId, agentId: s.agentId, taskId: taskId2, connectorId: s.connectorId, amountUsd: 3.0, unattributed: false });
db.finishRun({ db.finishRun({
+6 -6
View File
@@ -52,7 +52,7 @@ describe("AppDatabase", () => {
isPrimary: true, isPrimary: true,
}); });
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "Helios", name: "Helios",
role: "CEO", role: "CEO",
@@ -83,7 +83,7 @@ describe("AppDatabase", () => {
status: "in_progress", status: "in_progress",
}); });
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
projectId, projectId,
goalId, goalId,
@@ -194,7 +194,7 @@ describe("AppDatabase", () => {
isPrimary: true, isPrimary: true,
}); });
const alphaAgentId = db.saveAgent({ const alphaAgentId = db.agents.saveAgent({
companyId: alphaCompanyId, companyId: alphaCompanyId,
name: "Alpha CEO", name: "Alpha CEO",
role: "CEO", role: "CEO",
@@ -208,7 +208,7 @@ describe("AppDatabase", () => {
budgetMonthlyUsd: 100, budgetMonthlyUsd: 100,
}); });
db.saveAgent({ db.agents.saveAgent({
companyId: betaCompanyId, companyId: betaCompanyId,
name: "Beta CEO", name: "Beta CEO",
role: "CEO", role: "CEO",
@@ -222,7 +222,7 @@ describe("AppDatabase", () => {
budgetMonthlyUsd: 100, budgetMonthlyUsd: 100,
}); });
db.saveTask({ db.tasks.saveTask({
companyId: alphaCompanyId, companyId: alphaCompanyId,
title: "Alpha task", title: "Alpha task",
description: "Alpha work", description: "Alpha work",
@@ -232,7 +232,7 @@ describe("AppDatabase", () => {
status: "todo", status: "todo",
}); });
db.saveTask({ db.tasks.saveTask({
companyId: betaCompanyId, companyId: betaCompanyId,
title: "Beta task", title: "Beta task",
description: "Beta work", description: "Beta work",
@@ -47,7 +47,7 @@ async function createHarness() {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "Research Lead", name: "Research Lead",
role: "Researcher", role: "Researcher",
@@ -60,7 +60,7 @@ async function createHarness() {
capabilities: "Research and synthesis", capabilities: "Research and synthesis",
budgetMonthlyUsd: 180, budgetMonthlyUsd: 180,
}); });
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
title: "Refresh the market report", title: "Refresh the market report",
description: "Update the report with new findings and submit it for review.", description: "Update the report with new findings and submit it for review.",
@@ -69,7 +69,7 @@ async function createHarness() {
priority: "high", priority: "high",
status: "in_progress", status: "in_progress",
}); });
db.setTaskType(taskId, "research"); db.tasks.setTaskType(taskId, "research");
const existingDocumentId = db.saveDocument({ const existingDocumentId = db.saveDocument({
companyId, companyId,
@@ -172,7 +172,7 @@ describe("document run provenance", () => {
// ai_ceo + top-level CEO without `reviewDeliverables` advances // ai_ceo + top-level CEO without `reviewDeliverables` advances
// straight to `done` — there is no reviewer in that configuration // straight to `done` — there is no reviewer in that configuration
// so the prior `in_review` state was a dead letter. // 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"); expect(task.status).toBe("done");
} finally { } finally {
harness.server.stop(); harness.server.stop();
@@ -52,8 +52,8 @@ function buildKickoffPayload(db: AppDatabase, summary: { companyId: string; lead
throw new Error("Missing lead agent or task in onboarding summary."); throw new Error("Missing lead agent or task in onboarding summary.");
} }
const agent = db.getAgent(summary.leadAgentId); const agent = db.entityCrud.getAgent(summary.leadAgentId);
const task = db.getTask(summary.taskId); const task = db.entityCrud.getTask(summary.taskId);
const connector = db.getConnector(agent.connectorId); const connector = db.getConnector(agent.connectorId);
const workspace = summary.workspaceId ? db.getWorkspace(summary.workspaceId) : null; const workspace = summary.workspaceId ? db.getWorkspace(summary.workspaceId) : null;
const company = db.listSnapshot().companies.find((entry) => entry.id === summary.companyId); 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."); 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.connectorId).toBe("openai-codex");
expect(agent.model).toBe(enabledModel); expect(agent.model).toBe(enabledModel);
expect(db.getDisabledModels(summary.companyId, "openai-codex")).toEqual([{ id: disabledModel, label: disabledModel }]); 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."); 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(); expect(agent.model).toBeNull();
const payload = buildKickoffPayload(db, summary); const payload = buildKickoffPayload(db, summary);
+19 -19
View File
@@ -79,7 +79,7 @@ function scaffold(db: AppDatabase): ScaffoldResult {
}); });
// Agent // Agent
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "RecoveryAgent", name: "RecoveryAgent",
role: "IC", role: "IC",
@@ -145,7 +145,7 @@ function createAndStartRun(
): { runId: string; taskId: string } { ): { runId: string; taskId: string } {
const taskId = const taskId =
opts.taskId ?? opts.taskId ??
db.saveTask({ db.tasks.saveTask({
companyId: opts.companyId, companyId: opts.companyId,
title: opts.taskTitle ?? "Test Task", title: opts.taskTitle ?? "Test Task",
description: description:
@@ -165,7 +165,7 @@ function createAndStartRun(
connectorId: opts.connectorId, connectorId: opts.connectorId,
}); });
db.checkoutTask(taskId, runId); db.tasks.checkoutTask(taskId, runId);
db.markRunStatus(runId, "running", "Starting execution"); db.markRunStatus(runId, "running", "Starting execution");
if (opts.claimWorkspace !== false) { if (opts.claimWorkspace !== false) {
@@ -243,7 +243,7 @@ describe("Process Recovery -- Worker Crash Detection", () => {
const s = scaffold(db); const s = scaffold(db);
// Create a second agent for a second run // Create a second agent for a second run
const agent2Id = db.saveAgent({ const agent2Id = db.agents.saveAgent({
companyId: s.companyId, companyId: s.companyId,
name: "Agent2", name: "Agent2",
role: "IC", role: "IC",
@@ -306,7 +306,7 @@ describe("Process Recovery -- Worker Crash Detection", () => {
// However, after crash, the task is set to "blocked" and a subsequent // However, after crash, the task is set to "blocked" and a subsequent
// stale lock recovery pass handles the checkout_run_id. // stale lock recovery pass handles the checkout_run_id.
// For this test, verify the task status is blocked (the recovery mechanism). // 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"); expect(task.status).toBe("blocked");
}); });
@@ -320,11 +320,11 @@ describe("Process Recovery -- Worker Crash Detection", () => {
}); });
// Verify agent is running before crash // 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); 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", () => { it("releases workspace locks after crash", () => {
@@ -356,7 +356,7 @@ describe("Process Recovery -- Worker Crash Detection", () => {
expect(wsAfter.lock_holder_run_id).toBeNull(); expect(wsAfter.lock_holder_run_id).toBeNull();
// Verify we can re-claim the workspace (lock is truly released) // Verify we can re-claim the workspace (lock is truly released)
const agent2Id = db.saveAgent({ const agent2Id = db.agents.saveAgent({
companyId: s.companyId, companyId: s.companyId,
name: "Agent2", name: "Agent2",
role: "IC", role: "IC",
@@ -369,7 +369,7 @@ describe("Process Recovery -- Worker Crash Detection", () => {
capabilities: "Testing", capabilities: "Testing",
budgetMonthlyUsd: 100, budgetMonthlyUsd: 100,
}); });
const taskId2 = db.saveTask({ const taskId2 = db.tasks.saveTask({
companyId: s.companyId, companyId: s.companyId,
title: "Recovery Task", title: "Recovery Task",
description: "A recovery task after crash to verify workspace lock is released properly.", 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 // 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); simulateWorkerCrash(db);
// Task should be blocked (prevents heartbeat re-queuing) // 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 // Verify assignment before crash
expect(db.getTask(taskId).assigneeAgentId).toBe(s.agentId); expect(db.entityCrud.getTask(taskId).assigneeAgentId).toBe(s.agentId);
simulateWorkerCrash(db); simulateWorkerCrash(db);
// Agent assignment must be preserved for retry context // 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", () => { 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) // Create a task session (simulating what happens during normal execution)
const sessionId = db.createNewSession({ const sessionId = db.tasks.createNewSession({
companyId: s.companyId, companyId: s.companyId,
agentId: s.agentId, agentId: s.agentId,
adapterType: "gemini-cli", 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. // Drive the session past the maxRuns=20 threshold so rotation fires.
for (let i = 0; i < 20; i++) { 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({ const sessionService = createSessionManagementService({
@@ -456,7 +456,7 @@ describe("Process Recovery -- Context Preservation (D-02)", () => {
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, 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(); expect(session).not.toBeNull();
const rotationResult = sessionService.evaluateRotation(session!); const rotationResult = sessionService.evaluateRotation(session!);
@@ -499,7 +499,7 @@ describe("Process Recovery -- Multiple Concurrent Runs", () => {
const agents = [s.agentId]; const agents = [s.agentId];
for (let i = 2; i <= 3; i++) { for (let i = 2; i <= 3; i++) {
agents.push( agents.push(
db.saveAgent({ db.agents.saveAgent({
companyId: s.companyId, companyId: s.companyId,
name: `Agent${i}`, name: `Agent${i}`,
role: "IC", role: "IC",
@@ -540,7 +540,7 @@ describe("Process Recovery -- Multiple Concurrent Runs", () => {
const agents = [s.agentId]; const agents = [s.agentId];
for (let i = 2; i <= 3; i++) { for (let i = 2; i <= 3; i++) {
agents.push( agents.push(
db.saveAgent({ db.agents.saveAgent({
companyId: s.companyId, companyId: s.companyId,
name: `Agent${i}`, name: `Agent${i}`,
role: "IC", role: "IC",
@@ -571,7 +571,7 @@ describe("Process Recovery -- Multiple Concurrent Runs", () => {
// All tasks should be blocked (crash recovery sets them to blocked) // All tasks should be blocked (crash recovery sets them to blocked)
for (const run of runs) { for (const run of runs) {
const task = db.getTask(run.taskId); const task = db.entityCrud.getTask(run.taskId);
expect(task.status).toBe("blocked"); expect(task.status).toBe("blocked");
} }
@@ -46,7 +46,7 @@ describe("skill-aware capability registry", () => {
isPrimary: true, isPrimary: true,
}); });
db.saveAgent({ db.agents.saveAgent({
companyId, companyId,
name: "Generic Engineer", name: "Generic Engineer",
role: "Engineer", role: "Engineer",
@@ -59,7 +59,7 @@ describe("skill-aware capability registry", () => {
capabilities: "General software delivery", capabilities: "General software delivery",
budgetMonthlyUsd: 200, budgetMonthlyUsd: 200,
}); });
const styledEngineerId = db.saveAgent({ const styledEngineerId = db.agents.saveAgent({
companyId, companyId,
name: "Frontend Specialist", name: "Frontend Specialist",
role: "Engineer", role: "Engineer",
@@ -94,7 +94,7 @@ describe("skill-aware capability registry", () => {
}); });
db.assignSkillToAgent(styledEngineerId, skillId); db.assignSkillToAgent(styledEngineerId, skillId);
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
title: "Build React dashboard widget", title: "Build React dashboard widget",
description: "Create an accessible React component with hooks and Tailwind styling.", description: "Create an accessible React component with hooks and Tailwind styling.",
@@ -103,7 +103,7 @@ describe("skill-aware capability registry", () => {
status: "todo", status: "todo",
}); });
const assignedId = db.autoAssignTask(taskId, companyId); const assignedId = db.tasks.autoAssignTask(taskId, companyId);
expect(assignedId).toBe(styledEngineerId); expect(assignedId).toBe(styledEngineerId);
db.close(); db.close();
@@ -135,7 +135,7 @@ describe("skill-aware capability registry", () => {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "CEO", name: "CEO",
role: "CEO", role: "CEO",
@@ -171,7 +171,7 @@ describe("skill-aware capability registry", () => {
const snapshot = db.listSnapshot(); const snapshot = db.listSnapshot();
const prompt = buildTaskPrompt({ const prompt = buildTaskPrompt({
agent: db.getAgent(agentId), agent: db.entityCrud.getAgent(agentId),
apiUrl: "http://127.0.0.1:3210", apiUrl: "http://127.0.0.1:3210",
apiKey: "token", apiKey: "token",
runId: "run-1", runId: "run-1",
@@ -50,7 +50,7 @@ function createScaffold(db: AppDatabase) {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const ceoId = db.saveAgent({ const ceoId = db.agents.saveAgent({
companyId, companyId,
name: "CEO", name: "CEO",
role: "CEO", role: "CEO",
@@ -63,7 +63,7 @@ function createScaffold(db: AppDatabase) {
capabilities: "Lead the company", capabilities: "Lead the company",
budgetMonthlyUsd: 250, budgetMonthlyUsd: 250,
}); });
const engineerId = db.saveAgent({ const engineerId = db.agents.saveAgent({
companyId, companyId,
name: "Engineer", name: "Engineer",
role: "Engineer", role: "Engineer",
@@ -76,7 +76,7 @@ function createScaffold(db: AppDatabase) {
capabilities: "Ship features", capabilities: "Ship features",
budgetMonthlyUsd: 180, budgetMonthlyUsd: 180,
}); });
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
title: "Ship the feature", title: "Ship the feature",
description: "Implement the approved deliverable and wait for co-founder review.", 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(waitStates[0]?.waitingFor).toBe("user_reply");
expect(checkpointEngine.shouldPauseHeartbeat(companyId, engineerId)).toBe(true); expect(checkpointEngine.shouldPauseHeartbeat(companyId, engineerId)).toBe(true);
const otherAgentId = db.saveAgent({ const otherAgentId = db.agents.saveAgent({
companyId, companyId,
name: "Marketer", name: "Marketer",
role: "CMO", role: "CMO",
@@ -141,7 +141,7 @@ describe("thread wait + heartbeat integration", () => {
status: "idle", status: "idle",
reportsTo: null, reportsTo: null,
connectorId: "openai-codex", connectorId: "openai-codex",
workspaceId: db.getTask(taskId).workspaceId ?? null, workspaceId: db.entityCrud.getTask(taskId).workspaceId ?? null,
model: null, model: null,
capabilities: "Drive marketing work", capabilities: "Drive marketing work",
budgetMonthlyUsd: 180, budgetMonthlyUsd: 180,
@@ -66,7 +66,7 @@ async function createHarness(): Promise<Harness> {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "Artifact Engineer", name: "Artifact Engineer",
role: "Engineer", role: "Engineer",
@@ -79,7 +79,7 @@ async function createHarness(): Promise<Harness> {
capabilities: "Writes files", capabilities: "Writes files",
budgetMonthlyUsd: 100, budgetMonthlyUsd: 100,
}); });
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
title: "Produce artifact", title: "Produce artifact",
description: "", description: "",
+2 -2
View File
@@ -42,7 +42,7 @@ describe("agent budget policy synchronization", () => {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "Engineer", name: "Engineer",
role: "Engineer", role: "Engineer",
@@ -70,7 +70,7 @@ describe("agent budget policy synchronization", () => {
isActive: true, isActive: true,
}); });
db.saveAgent({ db.agents.saveAgent({
id: agentId, id: agentId,
companyId, companyId,
name: "Engineer", name: "Engineer",
+3 -3
View File
@@ -43,7 +43,7 @@ describe("hire routing guards", () => {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const ceoId = db.saveAgent({ const ceoId = db.agents.saveAgent({
companyId, companyId,
name: "CEO", name: "CEO",
role: "CEO", role: "CEO",
@@ -57,7 +57,7 @@ describe("hire routing guards", () => {
budgetMonthlyUsd: 250, budgetMonthlyUsd: 250,
department: "executive", department: "executive",
}); });
const hrId = db.saveAgent({ const hrId = db.agents.saveAgent({
companyId, companyId,
name: "HR Lead", name: "HR Lead",
role: "HR Lead", role: "HR Lead",
@@ -71,7 +71,7 @@ describe("hire routing guards", () => {
budgetMonthlyUsd: 180, budgetMonthlyUsd: 180,
department: "hr", department: "hr",
}); });
const ctoId = db.saveAgent({ const ctoId = db.agents.saveAgent({
companyId, companyId,
name: "CTO", name: "CTO",
role: "CTO", role: "CTO",
+12 -12
View File
@@ -44,7 +44,7 @@ describe("canManageOrganization", () => {
isPrimary: true, isPrimary: true,
}); });
const ceoId = db.saveAgent({ const ceoId = db.agents.saveAgent({
companyId, companyId,
name: "CEO", name: "CEO",
role: "CEO", role: "CEO",
@@ -57,7 +57,7 @@ describe("canManageOrganization", () => {
capabilities: "Lead the company", capabilities: "Lead the company",
budgetMonthlyUsd: 250, budgetMonthlyUsd: 250,
}); });
const ctoId = db.saveAgent({ const ctoId = db.agents.saveAgent({
companyId, companyId,
name: "CTO", name: "CTO",
role: "CTO", role: "CTO",
@@ -71,7 +71,7 @@ describe("canManageOrganization", () => {
budgetMonthlyUsd: 220, budgetMonthlyUsd: 220,
department: "engineering", department: "engineering",
}); });
const engineerId = db.saveAgent({ const engineerId = db.agents.saveAgent({
companyId, companyId,
name: "Engineer", name: "Engineer",
role: "Engineer", role: "Engineer",
@@ -113,7 +113,7 @@ describe("canManageOrganization", () => {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const ceoId = db.saveAgent({ const ceoId = db.agents.saveAgent({
companyId, companyId,
name: "CEO", name: "CEO",
role: "CEO", role: "CEO",
@@ -126,7 +126,7 @@ describe("canManageOrganization", () => {
capabilities: "Lead the company", capabilities: "Lead the company",
budgetMonthlyUsd: 250, budgetMonthlyUsd: 250,
}); });
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "Operations Lead", name: "Operations Lead",
role: "Specialist", role: "Specialist",
@@ -142,7 +142,7 @@ describe("canManageOrganization", () => {
}); });
expect(canManageOrganization(db, companyId, agentId)).toBe(false); 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(canManageOrganization(db, companyId, agentId)).toBe(true);
expect(canRequestHire(db, companyId, agentId)).toBe(true); expect(canRequestHire(db, companyId, agentId)).toBe(true);
@@ -167,7 +167,7 @@ describe("canManageOrganization", () => {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const ceoId = db.saveAgent({ const ceoId = db.agents.saveAgent({
companyId, companyId,
name: "CEO", name: "CEO",
role: "CEO", role: "CEO",
@@ -180,7 +180,7 @@ describe("canManageOrganization", () => {
capabilities: "Lead the company", capabilities: "Lead the company",
budgetMonthlyUsd: 250, budgetMonthlyUsd: 250,
}); });
const ctoId = db.saveAgent({ const ctoId = db.agents.saveAgent({
companyId, companyId,
name: "CTO", name: "CTO",
role: "CTO", role: "CTO",
@@ -194,7 +194,7 @@ describe("canManageOrganization", () => {
budgetMonthlyUsd: 220, budgetMonthlyUsd: 220,
department: "engineering", department: "engineering",
}); });
const cmoId = db.saveAgent({ const cmoId = db.agents.saveAgent({
companyId, companyId,
name: "CMO", name: "CMO",
role: "CMO", role: "CMO",
@@ -208,7 +208,7 @@ describe("canManageOrganization", () => {
budgetMonthlyUsd: 220, budgetMonthlyUsd: 220,
department: "marketing", department: "marketing",
}); });
const ctoReportId = db.saveAgent({ const ctoReportId = db.agents.saveAgent({
companyId, companyId,
name: "Engineer", name: "Engineer",
role: "Engineer", role: "Engineer",
@@ -223,7 +223,7 @@ describe("canManageOrganization", () => {
department: "engineering", department: "engineering",
}); });
const cmoTaskId = db.saveTask({ const cmoTaskId = db.tasks.saveTask({
companyId, companyId,
title: "Launch campaign", title: "Launch campaign",
description: "Owned by the marketing branch", description: "Owned by the marketing branch",
@@ -242,7 +242,7 @@ describe("canManageOrganization", () => {
expect(canManageProject(db, companyId, ctoId, cmoId)).toBe(false); expect(canManageProject(db, companyId, ctoId, cmoId)).toBe(false);
expect(canManageGoal(db, companyId, ceoId, cmoId)).toBe(true); expect(canManageGoal(db, companyId, ceoId, cmoId)).toBe(true);
expect(canManageProject(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(); db.close();
}); });
+3 -3
View File
@@ -44,7 +44,7 @@ describe("hire approval routing", () => {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const ceoId = db.saveAgent({ const ceoId = db.agents.saveAgent({
companyId, companyId,
name: "CEO", name: "CEO",
role: "CEO", role: "CEO",
@@ -57,7 +57,7 @@ describe("hire approval routing", () => {
capabilities: "Lead the company", capabilities: "Lead the company",
budgetMonthlyUsd: 250, budgetMonthlyUsd: 250,
}); });
const ctoId = db.saveAgent({ const ctoId = db.agents.saveAgent({
companyId, companyId,
name: "CTO", name: "CTO",
role: "CTO", role: "CTO",
@@ -116,7 +116,7 @@ describe("hire approval routing", () => {
trackTimer: vi.fn(), trackTimer: vi.fn(),
}, approvalId, companyId, "approved"); }, 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("approval_resolved", expect.objectContaining({ companyId, agentId: ctoId }));
expect(dispatchAutomation).toHaveBeenCalledWith("hire_approved", expect.objectContaining({ companyId, agentId: hireId })); expect(dispatchAutomation).toHaveBeenCalledWith("hire_approved", expect.objectContaining({ companyId, agentId: hireId }));
expect(wakeAgentIfPossible).toHaveBeenCalledWith(hireId, companyId, "assignment"); expect(wakeAgentIfPossible).toHaveBeenCalledWith(hireId, companyId, "assignment");
@@ -44,7 +44,7 @@ describe("deliverable review feedback writeback", () => {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "Research Lead", name: "Research Lead",
role: "Researcher", role: "Researcher",
@@ -58,7 +58,7 @@ describe("deliverable review feedback writeback", () => {
budgetMonthlyUsd: 180, budgetMonthlyUsd: 180,
department: "research", department: "research",
}); });
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
title: "Draft market report", title: "Draft market report",
description: "Produce a market report with concrete evidence and business insight.", 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("revision_requested");
expect(knowledgeEntries[0]?.content).toContain("stronger competitor evidence"); expect(knowledgeEntries[0]?.content).toContain("stronger competitor evidence");
expect(wakeAgentIfPossible).toHaveBeenCalledWith(agentId, companyId, "revision_needed"); 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); const document = db.listSnapshot().documents.find((entry) => entry.id === documentId);
expect(document?.status).toBe("draft"); expect(document?.status).toBe("draft");
+29 -29
View File
@@ -56,7 +56,7 @@ function createTestWorkspace(db: AppDatabase, companyId: string): string {
} }
function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string, name = "Agent"): string { function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string, name = "Agent"): string {
return db.saveAgent({ return db.agents.saveAgent({
companyId, companyId,
name, name,
role: "IC", 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 { function createTestTask(db: AppDatabase, companyId: string, agentId: string | null, workspaceId: string): string {
return db.saveTask({ return db.tasks.saveTask({
companyId, companyId,
title: "Test Task", title: "Test Task",
description: "A test task for checkout", description: "A test task for checkout",
@@ -117,10 +117,10 @@ describe("CAS checkout", () => {
connectorId: "openai-codex", connectorId: "openai-codex",
}); });
const result = db.checkoutTask(taskId, runId); const result = db.tasks.checkoutTask(taskId, runId);
expect(result).toBe(true); expect(result).toBe(true);
const task = db.getTask(taskId); const task = db.entityCrud.getTask(taskId);
expect(task.status).toBe("in_progress"); expect(task.status).toBe("in_progress");
}); });
@@ -146,8 +146,8 @@ describe("CAS checkout", () => {
connectorId: "openai-codex", connectorId: "openai-codex",
}); });
expect(db.checkoutTask(taskId, runId1)).toBe(true); expect(db.tasks.checkoutTask(taskId, runId1)).toBe(true);
expect(db.checkoutTask(taskId, runId2)).toBe(false); expect(db.tasks.checkoutTask(taskId, runId2)).toBe(false);
}); });
it("CAS checkout rejects already-in-progress tasks", () => { it("CAS checkout rejects already-in-progress tasks", () => {
@@ -168,7 +168,7 @@ describe("CAS checkout", () => {
connectorId: "openai-codex", 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", () => { it("lock release in finishRun clears checkout fields", () => {
@@ -186,7 +186,7 @@ describe("CAS checkout", () => {
connectorId: "openai-codex", connectorId: "openai-codex",
}); });
db.checkoutTask(taskId, runId); db.tasks.checkoutTask(taskId, runId);
db.markRunStatus(runId, "running", ""); db.markRunStatus(runId, "running", "");
db.finishRun({ db.finishRun({
runId, runId,
@@ -222,11 +222,11 @@ describe("CAS checkout", () => {
connectorId: "openai-codex", connectorId: "openai-codex",
}); });
db.checkoutTask(taskId, runId); db.tasks.checkoutTask(taskId, runId);
db.markRunStatus(runId, "running", ""); db.markRunStatus(runId, "running", "");
// Deleting the agent triggers cancelRunForDeletion for active runs // 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 const row = (db as unknown as RawDb).db
.prepare("SELECT checkout_run_id, execution_locked_at FROM tasks WHERE id = ?") .prepare("SELECT checkout_run_id, execution_locked_at FROM tasks WHERE id = ?")
@@ -251,7 +251,7 @@ describe("CAS checkout", () => {
connectorId: "openai-codex", connectorId: "openai-codex",
}); });
db.checkoutTask(taskId, runId); db.tasks.checkoutTask(taskId, runId);
// Backdate execution_locked_at to 20 minutes ago // Backdate execution_locked_at to 20 minutes ago
const backdated = new Date(Date.now() - 20 * 60 * 1000).toISOString(); 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].taskId).toBe(taskId);
expect(recovered[0].companyId).toBe(companyId); expect(recovered[0].companyId).toBe(companyId);
const task = db.getTask(taskId); const task = db.entityCrud.getTask(taskId);
expect(task.status).toBe("todo"); expect(task.status).toBe("todo");
const row = (db as unknown as RawDb).db const row = (db as unknown as RawDb).db
@@ -291,12 +291,12 @@ describe("CAS checkout", () => {
connectorId: "openai-codex", connectorId: "openai-codex",
}); });
db.checkoutTask(taskId, runId); db.tasks.checkoutTask(taskId, runId);
const recovered = db.recoverStaleLocks(); const recovered = db.recoverStaleLocks();
expect(recovered.length).toBe(0); expect(recovered.length).toBe(0);
const task = db.getTask(taskId); const task = db.entityCrud.getTask(taskId);
expect(task.status).toBe("in_progress"); expect(task.status).toBe("in_progress");
}); });
@@ -323,8 +323,8 @@ describe("CAS checkout", () => {
workspaceId, workspaceId,
connectorId: "openai-codex", connectorId: "openai-codex",
}); });
db.checkoutTask(taskId, runId); db.tasks.checkoutTask(taskId, runId);
expect(db.getTask(taskId).status).toBe("in_progress"); expect(db.entityCrud.getTask(taskId).status).toBe("in_progress");
// Never call markRunStatus('running', ...) — this simulates the // Never call markRunStatus('running', ...) — this simulates the
// "workspace already locked" bail-out inside dispatchRunToWorker. // "workspace already locked" bail-out inside dispatchRunToWorker.
@@ -339,7 +339,7 @@ describe("CAS checkout", () => {
unattributedCost: true, 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 // And the next CAS checkout must succeed — proving the heartbeat
// retry path is unblocked. // retry path is unblocked.
@@ -350,7 +350,7 @@ describe("CAS checkout", () => {
workspaceId, workspaceId,
connectorId: "openai-codex", 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'", () => { it("cancelled run that never started also rolls task back to 'todo'", () => {
@@ -371,7 +371,7 @@ describe("CAS checkout", () => {
workspaceId, workspaceId,
connectorId: "openai-codex", connectorId: "openai-codex",
}); });
db.checkoutTask(taskId, runId); db.tasks.checkoutTask(taskId, runId);
db.finishRun({ db.finishRun({
runId, runId,
status: "cancelled", status: "cancelled",
@@ -383,7 +383,7 @@ describe("CAS checkout", () => {
unattributedCost: true, 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)", () => { it("failed run that did start stays 'in_progress' (human retry path)", () => {
@@ -404,7 +404,7 @@ describe("CAS checkout", () => {
workspaceId, workspaceId,
connectorId: "openai-codex", connectorId: "openai-codex",
}); });
db.checkoutTask(taskId, runId); db.tasks.checkoutTask(taskId, runId);
db.markRunStatus(runId, "running", ""); db.markRunStatus(runId, "running", "");
db.finishRun({ db.finishRun({
runId, runId,
@@ -417,7 +417,7 @@ describe("CAS checkout", () => {
unattributedCost: false, 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", () => { 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 ceoId = createTestAgent(db, companyId, workspaceId, "CEO");
const taskId = createTestTask(db, companyId, ceoId, workspaceId); 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. // 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", () => { 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 workspaceId = createTestWorkspace(db, companyId);
const ceoId = createTestAgent(db, companyId, workspaceId, "CEO"); const ceoId = createTestAgent(db, companyId, workspaceId, "CEO");
const taskId = createTestTask(db, companyId, ceoId, workspaceId); const taskId = createTestTask(db, companyId, ceoId, workspaceId);
db.setTaskRequiresUserReview(taskId, true); db.tasks.setTaskRequiresUserReview(taskId, true);
db.advanceTaskStatus(taskId, "in_review"); db.tasks.advanceTaskStatus(taskId, "in_review");
expect(db.getTask(taskId).status).toBe("in_review"); expect(db.entityCrud.getTask(taskId).status).toBe("in_review");
}); });
it("interruptStaleRuns surfaces shutdown-era running runs without startup task rewrites", () => { it("interruptStaleRuns surfaces shutdown-era running runs without startup task rewrites", () => {
@@ -472,7 +472,7 @@ describe("CAS checkout", () => {
workspaceId, workspaceId,
connectorId: "openai-codex", connectorId: "openai-codex",
}); });
db.checkoutTask(taskId, liveRunId); db.tasks.checkoutTask(taskId, liveRunId);
db.markRunStatus(liveRunId, "running", ""); db.markRunStatus(liveRunId, "running", "");
db.close(); db.close();
@@ -480,6 +480,6 @@ describe("CAS checkout", () => {
db.init(); db.init();
expect(db.lastInterruptedCount).toBe(1); expect(db.lastInterruptedCount).toBe(1);
expect(db.getTask(taskId).status).toBe("blocked"); expect(db.entityCrud.getTask(taskId).status).toBe("blocked");
}); });
}); });
+6 -6
View File
@@ -56,7 +56,7 @@ describe("company mode switching", () => {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const ceoId = db.saveAgent({ const ceoId = db.agents.saveAgent({
companyId, companyId,
name: "CEO", name: "CEO",
role: "CEO", role: "CEO",
@@ -69,7 +69,7 @@ describe("company mode switching", () => {
capabilities: "Lead the company", capabilities: "Lead the company",
budgetMonthlyUsd: 250, budgetMonthlyUsd: 250,
}); });
const engineerId = db.saveAgent({ const engineerId = db.agents.saveAgent({
companyId, companyId,
name: "Engineer", name: "Engineer",
role: "Engineer", role: "Engineer",
@@ -83,7 +83,7 @@ describe("company mode switching", () => {
budgetMonthlyUsd: 180, budgetMonthlyUsd: 180,
department: "engineering", department: "engineering",
}); });
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
title: "Ship feature", title: "Ship feature",
description: "Waits for cofounder review.", description: "Waits for cofounder review.",
@@ -100,7 +100,7 @@ describe("company mode switching", () => {
threadIntegration.onTaskCreated(taskId, companyId); threadIntegration.onTaskCreated(taskId, companyId);
threadIntegration.onRunFinished("run-1", taskId, engineerId, companyId, "succeeded", null); threadIntegration.onRunFinished("run-1", taskId, engineerId, companyId, "succeeded", null);
db.setTaskRequiresUserReview(taskId, true); db.tasks.setTaskRequiresUserReview(taskId, true);
const approvalId = db.requestApproval({ const approvalId = db.requestApproval({
companyId, companyId,
relatedTaskId: taskId, relatedTaskId: taskId,
@@ -113,7 +113,7 @@ describe("company mode switching", () => {
threadIntegration.onApprovalCreated(approvalId, companyId, taskId, "deliverable_review", "Review deliverable"); threadIntegration.onApprovalCreated(approvalId, companyId, taskId, "deliverable_review", "Review deliverable");
expect(db.listThreadWaitStates(companyId).length).toBeGreaterThan(0); 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"); expect(db.getApprovalRecord(approvalId).state).toBe("pending");
const handlers = new Map<string, IpcHandler>(); const handlers = new Map<string, IpcHandler>();
@@ -149,7 +149,7 @@ describe("company mode switching", () => {
expect(result.ok).toBe(true); expect(result.ok).toBe(true);
expect(db.getCompanyExecutionMode(companyId)).toBe("ai_ceo"); expect(db.getCompanyExecutionMode(companyId)).toBe("ai_ceo");
expect(db.listThreadWaitStates(companyId)).toHaveLength(0); 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"); expect(db.getApprovalRecord(approvalId).state).toBe("approved");
db.close(); db.close();
+22 -22
View File
@@ -47,7 +47,7 @@ describe("AppDatabase", () => {
repoRef: "main", repoRef: "main",
isPrimary: true, isPrimary: true,
}); });
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "Chief Operator", name: "Chief Operator",
role: "CEO", role: "CEO",
@@ -76,7 +76,7 @@ describe("AppDatabase", () => {
status: "in_progress", status: "in_progress",
targetDate: null, targetDate: null,
}); });
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
projectId, projectId,
goalId, goalId,
@@ -181,7 +181,7 @@ describe("AppDatabase", () => {
repoRef: "main", repoRef: "main",
isPrimary: true, isPrimary: true,
}); });
const leadId = db.saveAgent({ const leadId = db.agents.saveAgent({
companyId, companyId,
name: "Lead", name: "Lead",
role: "CEO", role: "CEO",
@@ -200,7 +200,7 @@ describe("AppDatabase", () => {
envBindingText: "", envBindingText: "",
notes: "Test connector", notes: "Test connector",
}); });
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
title: "Claim me", title: "Claim me",
description: "First agent wins the checkout.", description: "First agent wins the checkout.",
@@ -225,10 +225,10 @@ describe("AppDatabase", () => {
connectorId: "openai-codex", connectorId: "openai-codex",
}); });
expect(db.checkoutTask(taskId, runId1)).toBe(true); expect(db.tasks.checkoutTask(taskId, runId1)).toBe(true);
expect(db.checkoutTask(taskId, runId2)).toBe(false); 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"); expect(task.status).toBe("in_progress");
}); });
@@ -246,7 +246,7 @@ describe("AppDatabase", () => {
repoRef: "main", repoRef: "main",
isPrimary: true, isPrimary: true,
}); });
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "Budget Owner", name: "Budget Owner",
role: "CTO", role: "CTO",
@@ -259,7 +259,7 @@ describe("AppDatabase", () => {
capabilities: "Ships work", capabilities: "Ships work",
budgetMonthlyUsd: 10, budgetMonthlyUsd: 10,
}); });
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
title: "Spend budget", title: "Spend budget",
description: "One run should exhaust the allowance.", description: "One run should exhaust the allowance.",
@@ -300,8 +300,8 @@ describe("AppDatabase", () => {
}); });
expect(outcome.budgetHardStopped).toBe(true); expect(outcome.budgetHardStopped).toBe(true);
expect(db.getAgent(agentId).status).toBe("paused"); expect(db.entityCrud.getAgent(agentId).status).toBe("paused");
expect(db.getAgent(agentId).spentMonthlyUsd).toBe(12); expect(db.entityCrud.getAgent(agentId).spentMonthlyUsd).toBe(12);
}); });
it("activates pending hires when the corresponding approval is granted", () => { it("activates pending hires when the corresponding approval is granted", () => {
@@ -310,7 +310,7 @@ describe("AppDatabase", () => {
description: "Approval-driven hiring", description: "Approval-driven hiring",
status: "active", status: "active",
}); });
const managerId = db.saveAgent({ const managerId = db.agents.saveAgent({
companyId, companyId,
name: "Manager", name: "Manager",
role: "CEO", role: "CEO",
@@ -323,7 +323,7 @@ describe("AppDatabase", () => {
capabilities: "Manages", capabilities: "Manages",
budgetMonthlyUsd: 100, budgetMonthlyUsd: 100,
}); });
const pendingHireId = db.saveAgent({ const pendingHireId = db.agents.saveAgent({
companyId, companyId,
name: "Pending Hire", name: "Pending Hire",
role: "IC", role: "IC",
@@ -353,7 +353,7 @@ describe("AppDatabase", () => {
decisionNote: "Approved", decisionNote: "Approved",
}); });
expect(db.getAgent(pendingHireId).status).toBe("idle"); expect(db.entityCrud.getAgent(pendingHireId).status).toBe("idle");
}); });
it("detects pending-approval agents via isAgentPendingApproval", () => { it("detects pending-approval agents via isAgentPendingApproval", () => {
@@ -362,7 +362,7 @@ describe("AppDatabase", () => {
description: "Approval gate validation", description: "Approval gate validation",
status: "active", status: "active",
}); });
const activeAgentId = db.saveAgent({ const activeAgentId = db.agents.saveAgent({
companyId, companyId,
name: "Active Agent", name: "Active Agent",
role: "CTO", role: "CTO",
@@ -375,7 +375,7 @@ describe("AppDatabase", () => {
capabilities: "Ships", capabilities: "Ships",
budgetMonthlyUsd: 100, budgetMonthlyUsd: 100,
}); });
const pendingAgentId = db.saveAgent({ const pendingAgentId = db.agents.saveAgent({
companyId, companyId,
name: "Pending Agent", name: "Pending Agent",
role: "IC", role: "IC",
@@ -389,8 +389,8 @@ describe("AppDatabase", () => {
budgetMonthlyUsd: 50, budgetMonthlyUsd: 50,
}); });
expect(db.isAgentPendingApproval(activeAgentId)).toBe(false); expect(db.agents.isAgentPendingApproval(activeAgentId)).toBe(false);
expect(db.isAgentPendingApproval(pendingAgentId)).toBe(true); expect(db.agents.isAgentPendingApproval(pendingAgentId)).toBe(true);
}); });
it("runs forward-only migrations and creates schema_migrations table", () => { it("runs forward-only migrations and creates schema_migrations table", () => {
@@ -479,7 +479,7 @@ describe("AppDatabase", () => {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "Purge Agent", name: "Purge Agent",
role: "IC", role: "IC",
@@ -492,7 +492,7 @@ describe("AppDatabase", () => {
capabilities: "test", capabilities: "test",
budgetMonthlyUsd: 100, budgetMonthlyUsd: 100,
}); });
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
title: "Purge task", title: "Purge task",
description: "test", description: "test",
@@ -542,7 +542,7 @@ describe("AppDatabase", () => {
description: "test", description: "test",
status: "active", status: "active",
}); });
const managerId = db.saveAgent({ const managerId = db.agents.saveAgent({
companyId, companyId,
name: "Manager", name: "Manager",
role: "CEO", role: "CEO",
@@ -555,7 +555,7 @@ describe("AppDatabase", () => {
capabilities: "manage", capabilities: "manage",
budgetMonthlyUsd: 100, budgetMonthlyUsd: 100,
}); });
const hireId = db.saveAgent({ const hireId = db.agents.saveAgent({
companyId, companyId,
name: "New Hire", name: "New Hire",
role: "IC", role: "IC",
+10 -10
View File
@@ -39,7 +39,7 @@ function scaffold(db: AppDatabase) {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "Worker Agent", name: "Worker Agent",
role: "developer", role: "developer",
@@ -68,7 +68,7 @@ function scaffold(db: AppDatabase) {
status: "in_progress", status: "in_progress",
targetDate: null, targetDate: null,
}); });
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
projectId, projectId,
goalId, goalId,
@@ -135,11 +135,11 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
workspaceId, workspaceId,
connectorId: "gemini-cli", connectorId: "gemini-cli",
}); });
db.checkoutTask(taskId, run1.id); db.tasks.checkoutTask(taskId, run1.id);
db.markRunStatus(run1.id, "running", ""); db.markRunStatus(run1.id, "running", "");
// Create a second task and run that exhausts budget WHILE Run-1 is in-flight // 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, companyId,
title: "Other Task", title: "Other Task",
description: "Another task that uses up budget", description: "Another task that uses up budget",
@@ -155,7 +155,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
workspaceId, workspaceId,
connectorId: "gemini-cli", connectorId: "gemini-cli",
}); });
db.checkoutTask(taskId2, run0.id); db.tasks.checkoutTask(taskId2, run0.id);
db.markRunStatus(run0.id, "running", ""); db.markRunStatus(run0.id, "running", "");
// Complete Run-0 with $6 cost (exceeds $5 budget) // 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 }); 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, workspaceId,
connectorId: "gemini-cli", connectorId: "gemini-cli",
}); });
db.checkoutTask(taskId, run.id); db.tasks.checkoutTask(taskId, run.id);
db.markRunStatus(run.id, "running", ""); db.markRunStatus(run.id, "running", "");
db.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 6.0, unattributed: false }); db.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 6.0, unattributed: false });
db.finishRun({ db.finishRun({
@@ -269,7 +269,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
workspaceId, workspaceId,
connectorId: "gemini-cli", connectorId: "gemini-cli",
}); });
db.checkoutTask(taskId, run.id); db.tasks.checkoutTask(taskId, run.id);
db.markRunStatus(run.id, "running", ""); db.markRunStatus(run.id, "running", "");
db.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 6.0, unattributed: false }); db.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 6.0, unattributed: false });
db.finishRun({ db.finishRun({
@@ -326,7 +326,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
workspaceId, workspaceId,
connectorId: "gemini-cli", connectorId: "gemini-cli",
}); });
db.checkoutTask(taskId, run.id); db.tasks.checkoutTask(taskId, run.id);
db.markRunStatus(run.id, "running", ""); db.markRunStatus(run.id, "running", "");
db.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 1.5, unattributed: false }); db.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 1.5, unattributed: false });
db.finishRun({ db.finishRun({
@@ -378,7 +378,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
workspaceId, workspaceId,
connectorId: "gemini-cli", connectorId: "gemini-cli",
}); });
db.checkoutTask(taskId, run.id); db.tasks.checkoutTask(taskId, run.id);
db.markRunStatus(run.id, "running", ""); db.markRunStatus(run.id, "running", "");
db.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 4.0, unattributed: false }); db.recordCostEntry({ runId: run.id, companyId, agentId, taskId, connectorId: "gemini-cli", amountUsd: 4.0, unattributed: false });
db.finishRun({ db.finishRun({
@@ -424,7 +424,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => {
workspaceId, workspaceId,
connectorId: "gemini-cli", connectorId: "gemini-cli",
}); });
db.checkoutTask(taskId, run.id); db.tasks.checkoutTask(taskId, run.id);
db.markRunStatus(run.id, "running", ""); db.markRunStatus(run.id, "running", "");
// Complete with cost that exceeds budget -- cost entry is still recorded // Complete with cost that exceeds budget -- cost entry is still recorded
+20 -20
View File
@@ -56,7 +56,7 @@ function createTestWorkspace(db: AppDatabase, companyId: string): string {
} }
function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string, name = "Agent"): string { function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string, name = "Agent"): string {
return db.saveAgent({ return db.agents.saveAgent({
companyId, companyId,
name, name,
role: "IC", role: "IC",
@@ -72,7 +72,7 @@ function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string
} }
function createTestTask(db: AppDatabase, companyId: string, workspaceId: string): string { function createTestTask(db: AppDatabase, companyId: string, workspaceId: string): string {
return db.saveTask({ return db.tasks.saveTask({
companyId, companyId,
title: "Concurrent Test Task", title: "Concurrent Test Task",
description: "A task for testing concurrent checkout edge cases", 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 // Agent-A claims first
const resultA = db.checkoutTask(taskId, runIdA); const resultA = db.tasks.checkoutTask(taskId, runIdA);
expect(resultA).toBe(true); expect(resultA).toBe(true);
// Agent-B tries to claim the same task // 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); expect(resultB).toBe(false);
// Task status must be in_progress (set by first successful checkout) // 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"); expect(task.status).toBe("in_progress");
}); });
@@ -162,9 +162,9 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
}); });
// Agent-A wins first task // Agent-A wins first task
expect(db.checkoutTask(taskId1, runIdA)).toBe(true); expect(db.tasks.checkoutTask(taskId1, runIdA)).toBe(true);
// Agent-B loses // 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 // Agent-B can still checkout a DIFFERENT task
const taskId2 = createTestTask(db, companyId, workspaceId); const taskId2 = createTestTask(db, companyId, workspaceId);
@@ -175,11 +175,11 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
workspaceId, workspaceId,
connectorId: "gemini-cli", connectorId: "gemini-cli",
}); });
const resultB2 = db.checkoutTask(taskId2, runIdB2); const resultB2 = db.tasks.checkoutTask(taskId2, runIdB2);
expect(resultB2).toBe(true); expect(resultB2).toBe(true);
// Verify second task is now in_progress // Verify second task is now in_progress
const task2 = db.getTask(taskId2); const task2 = db.entityCrud.getTask(taskId2);
expect(task2.status).toBe("in_progress"); expect(task2.status).toBe("in_progress");
}); });
@@ -199,7 +199,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
workspaceId, workspaceId,
connectorId: "gemini-cli", 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 // Task is now in_progress. A new run from Agent-B tries to checkout
const { id: runIdB } = db.createRun({ const { id: runIdB } = db.createRun({
@@ -209,11 +209,11 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
workspaceId, workspaceId,
connectorId: "gemini-cli", connectorId: "gemini-cli",
}); });
const result = db.checkoutTask(taskId, runIdB); const result = db.tasks.checkoutTask(taskId, runIdB);
expect(result).toBe(false); expect(result).toBe(false);
// Confirm status is still in_progress // 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", () => { it("after lock release, CAS guard still checks task status", () => {
@@ -232,10 +232,10 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
workspaceId, workspaceId,
connectorId: "gemini-cli", 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) // Release the lock (sets checkout_run_id = NULL)
db.releaseTaskLock(taskId, runIdA); db.tasks.releaseTaskLock(taskId, runIdA);
// Verify lock fields are cleared // Verify lock fields are cleared
const row = (db as unknown as RawDb).db const row = (db as unknown as RawDb).db
@@ -253,7 +253,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
workspaceId, workspaceId,
connectorId: "gemini-cli", connectorId: "gemini-cli",
}); });
const result = db.checkoutTask(taskId, runIdB); const result = db.tasks.checkoutTask(taskId, runIdB);
expect(result).toBe(false); expect(result).toBe(false);
}); });
@@ -271,7 +271,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
workspaceId, workspaceId,
connectorId: "gemini-cli", 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 // Backdate execution_locked_at to well past the stale timeout
(db as unknown as RawDb).db (db as unknown as RawDb).db
@@ -307,7 +307,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
workspaceId, workspaceId,
connectorId: "gemini-cli", connectorId: "gemini-cli",
}); });
expect(db.checkoutTask(taskId, runIdA)).toBe(true); expect(db.tasks.checkoutTask(taskId, runIdA)).toBe(true);
// Backdate to make stale // Backdate to make stale
(db as unknown as RawDb).db (db as unknown as RawDb).db
@@ -319,7 +319,7 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
expect(recovered.length).toBe(1); expect(recovered.length).toBe(1);
// Verify task is back to "todo" // 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 // Agent-B can now checkout the recovered task
const { id: runIdB } = db.createRun({ const { id: runIdB } = db.createRun({
@@ -329,9 +329,9 @@ describe("Edge Case: Concurrent Task Checkout (D-05.1)", () => {
workspaceId, workspaceId,
connectorId: "gemini-cli", connectorId: "gemini-cli",
}); });
const result = db.checkoutTask(taskId, runIdB); const result = db.tasks.checkoutTask(taskId, runIdB);
expect(result).toBe(true); 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", () => { it("STALE_LOCK_TIMEOUT_MS matches expected 15 minute value", () => {
+26 -26
View File
@@ -43,7 +43,7 @@ function scaffold(db: AppDatabase, connectorId: ConnectorId = "gemini-cli") {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "Worker Agent", name: "Worker Agent",
role: "developer", role: "developer",
@@ -72,7 +72,7 @@ function scaffold(db: AppDatabase, connectorId: ConnectorId = "gemini-cli") {
status: "in_progress", status: "in_progress",
targetDate: null, targetDate: null,
}); });
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
projectId, projectId,
goalId, goalId,
@@ -114,7 +114,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
const sessionService = createSessionManagementService({ db, logger: mockLogger }); const sessionService = createSessionManagementService({ db, logger: mockLogger });
// Create a session // Create a session
const sessionId = db.createNewSession({ const sessionId = db.tasks.createNewSession({
companyId, companyId,
agentId, agentId,
adapterType: "gemini-cli", 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) // Simulate runs until we reach the maxRuns threshold (20)
for (let i = 0; i < 20; i++) { 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 // Get the updated session from DB
const session = db.getSessionById(sessionId); const session = db.tasks.getSessionById(sessionId);
expect(session).not.toBeNull(); expect(session).not.toBeNull();
expect(session!.runCount).toBe(20); // matches maxRuns threshold in session-management-service 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 { companyId, agentId, taskId } = scaffold(db);
const sessionService = createSessionManagementService({ db, logger: mockLogger }); const sessionService = createSessionManagementService({ db, logger: mockLogger });
const sessionId = db.createNewSession({ const sessionId = db.tasks.createNewSession({
companyId, companyId,
agentId, agentId,
adapterType: "gemini-cli", adapterType: "gemini-cli",
}); });
// Accumulate tokens past the 500_000 maxTokens threshold in one big call // 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(); expect(session).not.toBeNull();
const totalTokens = session!.totalInputTokens + session!.totalOutputTokens; const totalTokens = session!.totalInputTokens + session!.totalOutputTokens;
expect(totalTokens).toBe(550000); 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 { companyId, agentId, taskId, workspaceId } = scaffold(db);
const sessionService = createSessionManagementService({ db, logger: mockLogger }); const sessionService = createSessionManagementService({ db, logger: mockLogger });
const sessionId = db.createNewSession({ const sessionId = db.tasks.createNewSession({
companyId, companyId,
agentId, agentId,
adapterType: "gemini-cli", adapterType: "gemini-cli",
@@ -171,7 +171,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
// Accumulate enough to trigger rotation // Accumulate enough to trigger rotation
for (let i = 0; i < 20; i++) { 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) // 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, unattributedCost: false,
}); });
const session = db.getSessionById(sessionId)!; const session = db.tasks.getSessionById(sessionId)!;
// generateHandoff is agent-scoped: it walks the agent's runs across // generateHandoff is agent-scoped: it walks the agent's runs across
// all tasks + taskless heartbeats, not a single task. // all tasks + taskless heartbeats, not a single task.
const handoff = sessionService.generateHandoff(session); 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"); expect(handoff).toContain("## Recent Run History");
// Archive the old session with handoff // 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 // Verify archived session has handoff stored
const archivedRows = db.queryAll<{ handoffMarkdown: string | null }>( 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); const { companyId, agentId, taskId } = scaffold(db);
// Create initial session // Create initial session
const oldSessionId = db.createNewSession({ const oldSessionId = db.tasks.createNewSession({
companyId, companyId,
agentId, agentId,
adapterType: "gemini-cli", adapterType: "gemini-cli",
}); });
// Add usage to the old session // 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 // 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 // Create new session
const newSessionId = db.createNewSession({ const newSessionId = db.tasks.createNewSession({
companyId, companyId,
agentId, agentId,
adapterType: "gemini-cli", adapterType: "gemini-cli",
}); });
// Get the new active session // Get the new active session
const newSession = db.getSessionById(newSessionId); const newSession = db.tasks.getSessionById(newSessionId);
expect(newSession).not.toBeNull(); expect(newSession).not.toBeNull();
expect(newSession!.id).toBe(newSessionId); expect(newSession!.id).toBe(newSessionId);
expect(newSession!.id).not.toBe(oldSessionId); 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 { companyId, agentId, taskId } = scaffold(db, "gemini-cli");
const sessionService = createSessionManagementService({ db, logger: mockLogger }); const sessionService = createSessionManagementService({ db, logger: mockLogger });
const sessionId = db.createNewSession({ const sessionId = db.tasks.createNewSession({
companyId, companyId,
agentId, agentId,
adapterType: "claude-pro", adapterType: "claude-pro",
@@ -260,10 +260,10 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
// Accumulate way past all thresholds // Accumulate way past all thresholds
for (let i = 0; i < 25; i++) { 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. // Pi-mono delegates compaction to pi-agent-core; AgentCompany still applies rotation uniformly.
const result = sessionService.evaluateRotation(session); const result = sessionService.evaluateRotation(session);
expect(result.shouldRotate).toBe(true); 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 { companyId, agentId, taskId } = scaffold(db, "gemini-cli");
const sessionService = createSessionManagementService({ db, logger: mockLogger }); const sessionService = createSessionManagementService({ db, logger: mockLogger });
const sessionId = db.createNewSession({ const sessionId = db.tasks.createNewSession({
companyId, companyId,
agentId, agentId,
adapterType: "openai-codex", adapterType: "openai-codex",
@@ -282,10 +282,10 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => {
// Accumulate way past all thresholds // Accumulate way past all thresholds
for (let i = 0; i < 25; i++) { 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. // Pi-mono delegates compaction to pi-agent-core; AgentCompany still applies rotation uniformly.
const result = sessionService.evaluateRotation(session); const result = sessionService.evaluateRotation(session);
expect(result.shouldRotate).toBe(true); 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 }); const sessionService = createSessionManagementService({ db, logger: mockLogger });
// Create session and add usage // Create session and add usage
const sessionId = db.createNewSession({ const sessionId = db.tasks.createNewSession({
companyId, companyId,
agentId, agentId,
adapterType: "gemini-cli", 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 // Create a completed run for run history
const run = db.createRun({ 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) // Rotate the session (archives old, creates new)
const oldSession = db.getSessionById(sessionId)!; const oldSession = db.tasks.getSessionById(sessionId)!;
sessionService.rotateSession(oldSession); sessionService.rotateSession(oldSession);
// After rotation, the handoff should be retrievable for the next run // After rotation, the handoff should be retrievable for the next run
+40 -36
View File
@@ -22,12 +22,16 @@ const RETRY_COOLDOWN_MS = 30_000;
function createMockDeps(): EscalationDeps { function createMockDeps(): EscalationDeps {
return { return {
db: { db: {
getTaskRetryCount: vi.fn().mockReturnValue(0), tasks: {
incrementTaskRetryCount: vi.fn(), getTaskRetryCount: vi.fn().mockReturnValue(0),
resetTaskRetryCount: vi.fn(), incrementTaskRetryCount: vi.fn(),
getChainOfCommand: vi.fn().mockReturnValue([]), resetTaskRetryCount: vi.fn(),
getCeoForCompany: vi.fn().mockReturnValue(null), advanceTaskStatus: vi.fn().mockReturnValue({ previousStatus: "in_progress" }),
advanceTaskStatus: vi.fn().mockReturnValue({ previousStatus: "in_progress" }), },
agents: {
getChainOfCommand: vi.fn().mockReturnValue([]),
getCeoForCompany: vi.fn().mockReturnValue(null),
},
}, },
wakeAgentIfPossible: vi.fn().mockReturnValue("wake-id"), wakeAgentIfPossible: vi.fn().mockReturnValue("wake-id"),
notify: vi.fn(), notify: vi.fn(),
@@ -58,15 +62,15 @@ describe("Failure Escalation Pipeline", () => {
describe("Retry path (count < MAX_RETRY_ATTEMPTS)", () => { describe("Retry path (count < MAX_RETRY_ATTEMPTS)", () => {
it("retries on first failure (retry_count=0)", () => { it("retries on first failure (retry_count=0)", () => {
(deps.db.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(0); (deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(0);
const result = handleFailureEscalation( const result = handleFailureEscalation(
deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry, deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry,
); );
expect(result.action).toBe("retry"); expect(result.action).toBe("retry");
expect(deps.db.incrementTaskRetryCount).toHaveBeenCalledWith("task-1"); expect(deps.db.tasks.incrementTaskRetryCount).toHaveBeenCalledWith("task-1");
expect(deps.db.advanceTaskStatus).toHaveBeenCalledWith( expect(deps.db.tasks.advanceTaskStatus).toHaveBeenCalledWith(
"task-1", "task-1",
"todo", "todo",
expect.stringContaining("attempt 1/2"), expect.stringContaining("attempt 1/2"),
@@ -75,15 +79,15 @@ describe("Failure Escalation Pipeline", () => {
}); });
it("retries on second failure (retry_count=1)", () => { it("retries on second failure (retry_count=1)", () => {
(deps.db.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(1); (deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(1);
const result = handleFailureEscalation( const result = handleFailureEscalation(
deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry, deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry,
); );
expect(result.action).toBe("retry"); expect(result.action).toBe("retry");
expect(deps.db.incrementTaskRetryCount).toHaveBeenCalledWith("task-1"); expect(deps.db.tasks.incrementTaskRetryCount).toHaveBeenCalledWith("task-1");
expect(deps.db.advanceTaskStatus).toHaveBeenCalledWith( expect(deps.db.tasks.advanceTaskStatus).toHaveBeenCalledWith(
"task-1", "task-1",
"todo", "todo",
expect.stringContaining("attempt 2/2"), expect.stringContaining("attempt 2/2"),
@@ -92,7 +96,7 @@ describe("Failure Escalation Pipeline", () => {
}); });
it("calls wakeAgentIfPossible via scheduleRetry callback", () => { it("calls wakeAgentIfPossible via scheduleRetry callback", () => {
(deps.db.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(0); (deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(0);
handleFailureEscalation( handleFailureEscalation(
deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry, 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)", () => { describe("Escalation path (count >= MAX_RETRY_ATTEMPTS)", () => {
it("escalates to chain[0] manager after max retries", () => { it("escalates to chain[0] manager after max retries", () => {
(deps.db.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2); (deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2);
(deps.db.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([ (deps.db.agents.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([
{ id: "manager-1", name: "Manager Alpha" }, { id: "manager-1", name: "Manager Alpha" },
]); ]);
@@ -117,32 +121,32 @@ describe("Failure Escalation Pipeline", () => {
); );
expect(result.action).toBe("escalated"); 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.wakeAgentIfPossible).toHaveBeenCalledWith("manager-1", "company-1", "report_failed");
expect(deps.notify).not.toHaveBeenCalled(); expect(deps.notify).not.toHaveBeenCalled();
}); });
it("falls through to CEO when chain is empty", () => { it("falls through to CEO when chain is empty", () => {
(deps.db.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2); (deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2);
(deps.db.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([]); (deps.db.agents.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([]);
(deps.db.getCeoForCompany as ReturnType<typeof vi.fn>).mockReturnValue({ id: "ceo-1", name: "CEO Boss" }); (deps.db.agents.getCeoForCompany as ReturnType<typeof vi.fn>).mockReturnValue({ id: "ceo-1", name: "CEO Boss" });
const result = handleFailureEscalation( const result = handleFailureEscalation(
deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry, deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry,
); );
expect(result.action).toBe("escalated"); 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.wakeAgentIfPossible).toHaveBeenCalledWith("ceo-1", "company-1", "report_failed");
expect(deps.notify).not.toHaveBeenCalled(); expect(deps.notify).not.toHaveBeenCalled();
}); });
it("falls through to CEO when chain lookup throws", () => { it("falls through to CEO when chain lookup throws", () => {
(deps.db.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2); (deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2);
(deps.db.getChainOfCommand as ReturnType<typeof vi.fn>).mockImplementation(() => { (deps.db.agents.getChainOfCommand as ReturnType<typeof vi.fn>).mockImplementation(() => {
throw new Error("Agent deleted"); throw new Error("Agent deleted");
}); });
(deps.db.getCeoForCompany as ReturnType<typeof vi.fn>).mockReturnValue({ id: "ceo-1", name: "CEO Boss" }); (deps.db.agents.getCeoForCompany as ReturnType<typeof vi.fn>).mockReturnValue({ id: "ceo-1", name: "CEO Boss" });
const result = handleFailureEscalation( const result = handleFailureEscalation(
deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry, 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", () => { it("notifies user when both chain and CEO are unavailable", () => {
(deps.db.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2); (deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2);
(deps.db.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([]); (deps.db.agents.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([]);
(deps.db.getCeoForCompany as ReturnType<typeof vi.fn>).mockReturnValue(null); (deps.db.agents.getCeoForCompany as ReturnType<typeof vi.fn>).mockReturnValue(null);
const result = handleFailureEscalation( const result = handleFailureEscalation(
deps, RUN, TASK, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry, 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", () => { it("uses taskId as fallback when task is null", () => {
(deps.db.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2); (deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2);
(deps.db.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([]); (deps.db.agents.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([]);
(deps.db.getCeoForCompany as ReturnType<typeof vi.fn>).mockReturnValue(null); (deps.db.agents.getCeoForCompany as ReturnType<typeof vi.fn>).mockReturnValue(null);
const result = handleFailureEscalation( const result = handleFailureEscalation(
deps, RUN, null, "failed", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry, 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", () => { describe("Timed out runs follow same path", () => {
it("retries timed_out runs the same as failed runs", () => { it("retries timed_out runs the same as failed runs", () => {
(deps.db.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(0); (deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(0);
const result = handleFailureEscalation( const result = handleFailureEscalation(
deps, RUN, TASK, "timed_out", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry, deps, RUN, TASK, "timed_out", MAX_RETRY_ATTEMPTS, RETRY_COOLDOWN_MS, scheduleRetry,
); );
expect(result.action).toBe("retry"); 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", () => { it("escalates timed_out runs after max retries", () => {
(deps.db.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2); (deps.db.tasks.getTaskRetryCount as ReturnType<typeof vi.fn>).mockReturnValue(2);
(deps.db.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([ (deps.db.agents.getChainOfCommand as ReturnType<typeof vi.fn>).mockReturnValue([
{ id: "manager-1", name: "Manager Alpha" }, { id: "manager-1", name: "Manager Alpha" },
]); ]);
@@ -212,18 +216,18 @@ describe("Failure Escalation Pipeline", () => {
); );
expect(result.action).toBe("escalated"); 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", () => { describe("Success resets retry count", () => {
it("resets retry_count on successful completion", () => { it("resets retry_count on successful completion", () => {
handleSuccessReset(deps.db, "task-1"); 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", () => { it("handles reset errors gracefully", () => {
(deps.db.resetTaskRetryCount as ReturnType<typeof vi.fn>).mockImplementation(() => { (deps.db.tasks.resetTaskRetryCount as ReturnType<typeof vi.fn>).mockImplementation(() => {
throw new Error("DB error"); throw new Error("DB error");
}); });
+1 -1
View File
@@ -52,7 +52,7 @@ describe("ipc-core-agent-graph-handlers model enforcement", () => {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "CEO", name: "CEO",
role: "CEO", role: "CEO",
+6 -6
View File
@@ -41,7 +41,7 @@ function createScaffold(db: AppDatabase) {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const ceoId = db.saveAgent({ const ceoId = db.agents.saveAgent({
companyId, companyId,
name: "CEO", name: "CEO",
role: "CEO", role: "CEO",
@@ -54,7 +54,7 @@ function createScaffold(db: AppDatabase) {
capabilities: "Lead the company", capabilities: "Lead the company",
budgetMonthlyUsd: 250, budgetMonthlyUsd: 250,
}); });
const ctoId = db.saveAgent({ const ctoId = db.agents.saveAgent({
companyId, companyId,
name: "CTO", name: "CTO",
role: "CTO", role: "CTO",
@@ -68,7 +68,7 @@ function createScaffold(db: AppDatabase) {
budgetMonthlyUsd: 200, budgetMonthlyUsd: 200,
department: "engineering", department: "engineering",
}); });
const cmoId = db.saveAgent({ const cmoId = db.agents.saveAgent({
companyId, companyId,
name: "CMO", name: "CMO",
role: "CMO", role: "CMO",
@@ -82,7 +82,7 @@ function createScaffold(db: AppDatabase) {
budgetMonthlyUsd: 200, budgetMonthlyUsd: 200,
department: "marketing", department: "marketing",
}); });
const pendingId = db.saveAgent({ const pendingId = db.agents.saveAgent({
companyId, companyId,
name: "Pending Recruit", name: "Pending Recruit",
role: "IC", role: "IC",
@@ -96,7 +96,7 @@ function createScaffold(db: AppDatabase) {
budgetMonthlyUsd: 100, budgetMonthlyUsd: 100,
department: "engineering", department: "engineering",
}); });
const terminatedId = db.saveAgent({ const terminatedId = db.agents.saveAgent({
companyId, companyId,
name: "Former COO", name: "Former COO",
role: "COO", role: "COO",
@@ -111,7 +111,7 @@ function createScaffold(db: AppDatabase) {
department: "operations", department: "operations",
}); });
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
title: "Review architecture plan", title: "Review architecture plan",
description: "Need board feedback in the thread.", description: "Need board feedback in the thread.",
+2 -2
View File
@@ -32,7 +32,7 @@ describe("AppDatabase.searchMessages", () => {
status: "active", status: "active",
}); });
agentA = db.saveAgent({ agentA = db.agents.saveAgent({
companyId, companyId,
name: "Agent Alpha", name: "Agent Alpha",
role: "Engineer", role: "Engineer",
@@ -46,7 +46,7 @@ describe("AppDatabase.searchMessages", () => {
budgetMonthlyUsd: 100, budgetMonthlyUsd: 100,
}); });
agentB = db.saveAgent({ agentB = db.agents.saveAgent({
companyId, companyId,
name: "Agent Beta", name: "Agent Beta",
role: "Designer", role: "Designer",
+32 -32
View File
@@ -34,7 +34,7 @@ function scaffold(db: AppDatabase) {
repoRef: "", repoRef: "",
isPrimary: true, isPrimary: true,
}); });
const agentId = db.saveAgent({ const agentId = db.agents.saveAgent({
companyId, companyId,
name: "Worker Agent", name: "Worker Agent",
role: "developer", role: "developer",
@@ -63,7 +63,7 @@ function scaffold(db: AppDatabase) {
status: "in_progress", status: "in_progress",
targetDate: null, targetDate: null,
}); });
const taskId = db.saveTask({ const taskId = db.tasks.saveTask({
companyId, companyId,
projectId, projectId,
goalId, goalId,
@@ -96,19 +96,19 @@ describe("task_sessions CRUD", () => {
}); });
it("getSessionById returns null for an unknown id", () => { it("getSessionById returns null for an unknown id", () => {
const session = db.getSessionById("nonexistent"); const session = db.tasks.getSessionById("nonexistent");
expect(session).toBeNull(); expect(session).toBeNull();
}); });
it("getSessionById returns TaskSessionRecord when valid session exists", () => { it("getSessionById returns TaskSessionRecord when valid session exists", () => {
const { companyId, agentId } = scaffold(db); const { companyId, agentId } = scaffold(db);
const sessionId = db.createNewSession({ const sessionId = db.tasks.createNewSession({
companyId, companyId,
agentId, agentId,
adapterType: "claude-pro", adapterType: "claude-pro",
}); });
const session = db.getSessionById(sessionId); const session = db.tasks.getSessionById(sessionId);
expect(session).not.toBeNull(); expect(session).not.toBeNull();
expect(session!.id).toBe(sessionId); expect(session!.id).toBe(sessionId);
expect(session!.companyId).toBe(companyId); expect(session!.companyId).toBe(companyId);
@@ -124,23 +124,23 @@ describe("task_sessions CRUD", () => {
it("incrementSessionUsage atomically updates counters", () => { it("incrementSessionUsage atomically updates counters", () => {
const { companyId, agentId } = scaffold(db); const { companyId, agentId } = scaffold(db);
const sessionId = db.createNewSession({ const sessionId = db.tasks.createNewSession({
companyId, companyId,
agentId, agentId,
adapterType: "claude-pro", 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!.runCount).toBe(1);
expect(session!.totalInputTokens).toBe(1000); expect(session!.totalInputTokens).toBe(1000);
expect(session!.totalOutputTokens).toBe(500); expect(session!.totalOutputTokens).toBe(500);
expect(session!.lastRunId).toBe("run-001"); expect(session!.lastRunId).toBe("run-001");
// Second increment accumulates // Second increment accumulates
db.incrementSessionUsage(sessionId, 2000, 800, "run-002"); db.tasks.incrementSessionUsage(sessionId, 2000, 800, "run-002");
const session2 = db.getSessionById(sessionId); const session2 = db.tasks.getSessionById(sessionId);
expect(session2!.runCount).toBe(2); expect(session2!.runCount).toBe(2);
expect(session2!.totalInputTokens).toBe(3000); expect(session2!.totalInputTokens).toBe(3000);
expect(session2!.totalOutputTokens).toBe(1300); expect(session2!.totalOutputTokens).toBe(1300);
@@ -149,15 +149,15 @@ describe("task_sessions CRUD", () => {
it("archiveSession sets rotated_at, rotation_reason, handoff_markdown", () => { it("archiveSession sets rotated_at, rotation_reason, handoff_markdown", () => {
const { companyId, agentId } = scaffold(db); const { companyId, agentId } = scaffold(db);
const sessionId = db.createNewSession({ const sessionId = db.tasks.createNewSession({
companyId, companyId,
agentId, agentId,
adapterType: "claude-pro", 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!.rotatedAt).toBeTruthy();
expect(session!.rotationReason).toBe("max_runs"); expect(session!.rotationReason).toBe("max_runs");
expect(session!.handoffMarkdown).toBe("# Handoff\nSession reached 20 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", () => { it("createNewSession returns an id; adapter type persists", () => {
const { companyId, agentId } = scaffold(db); const { companyId, agentId } = scaffold(db);
const sessionId = db.createNewSession({ const sessionId = db.tasks.createNewSession({
companyId, companyId,
agentId, agentId,
adapterType: "gemini-cli", adapterType: "gemini-cli",
@@ -175,7 +175,7 @@ describe("task_sessions CRUD", () => {
expect(typeof sessionId).toBe("string"); expect(typeof sessionId).toBe("string");
expect(sessionId.length).toBeGreaterThan(0); expect(sessionId.length).toBeGreaterThan(0);
const session = db.getSessionById(sessionId); const session = db.tasks.getSessionById(sessionId);
expect(session).not.toBeNull(); expect(session).not.toBeNull();
expect(session!.id).toBe(sessionId); expect(session!.id).toBe(sessionId);
expect(session!.adapterType).toBe("gemini-cli"); expect(session!.adapterType).toBe("gemini-cli");
@@ -209,7 +209,7 @@ describe("task_sessions CRUD", () => {
unattributedCost: false, unattributedCost: false,
}); });
const runs = db.getRecentRunsForTask(taskId, 5); const runs = db.tasks.getRecentRunsForTask(taskId, 5);
expect(runs.length).toBe(2); expect(runs.length).toBe(2);
// Most recent first // Most recent first
expect(runs[0].id).toBe(run2.id); expect(runs[0].id).toBe(run2.id);
@@ -401,7 +401,7 @@ describe("session management service", () => {
describe("token accumulation", () => { describe("token accumulation", () => {
it("accumulateTokenUsage calls db.incrementSessionUsage with correct args", () => { it("accumulateTokenUsage calls db.incrementSessionUsage with correct args", () => {
const { taskId, companyId, agentId } = scaffold(db); const { taskId, companyId, agentId } = scaffold(db);
const sessionId = db.createNewSession({ const sessionId = db.tasks.createNewSession({
companyId, companyId,
agentId, agentId,
adapterType: "claude-pro", adapterType: "claude-pro",
@@ -410,7 +410,7 @@ describe("session management service", () => {
svc.accumulateTokenUsage(sessionId, 5000, 2000, "run-abc"); 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!.runCount).toBe(1);
expect(session!.totalInputTokens).toBe(5000); expect(session!.totalInputTokens).toBe(5000);
expect(session!.totalOutputTokens).toBe(2000); expect(session!.totalOutputTokens).toBe(2000);
@@ -424,7 +424,7 @@ describe("session management service", () => {
describe("handoff generation", () => { describe("handoff generation", () => {
it("generateHandoff produces markdown with task title and run history", () => { it("generateHandoff produces markdown with task title and run history", () => {
const { taskId, companyId, agentId, workspaceId } = scaffold(db); const { taskId, companyId, agentId, workspaceId } = scaffold(db);
const sessionId = db.createNewSession({ const sessionId = db.tasks.createNewSession({
companyId, companyId,
agentId, agentId,
adapterType: "gemini-cli", adapterType: "gemini-cli",
@@ -444,9 +444,9 @@ describe("session management service", () => {
}); });
// Increment session usage to have stats // 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 }); const svc = createSessionManagementService({ db, logger: mockLogger });
// Handoff is now agent-scoped: it walks the agent's recent runs // Handoff is now agent-scoped: it walks the agent's recent runs
// (across tasks + heartbeats) rather than a single task. The task // (across tasks + heartbeats) rather than a single task. The task
@@ -468,16 +468,16 @@ describe("session management service", () => {
describe("session rotation", () => { describe("session rotation", () => {
it("rotateSession archives old session, creates new, copies session_params", () => { it("rotateSession archives old session, creates new, copies session_params", () => {
const { taskId, companyId, agentId } = scaffold(db); const { taskId, companyId, agentId } = scaffold(db);
const oldSessionId = db.createNewSession({ const oldSessionId = db.tasks.createNewSession({
companyId, companyId,
agentId, agentId,
adapterType: "gemini-cli", adapterType: "gemini-cli",
}); });
// Add some usage // 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 svc = createSessionManagementService({ db, logger: mockLogger });
const newSessionId = svc.rotateSession(oldSession); const newSessionId = svc.rotateSession(oldSession);
@@ -493,7 +493,7 @@ describe("session management service", () => {
expect(archivedRows[0].handoffMarkdown).toContain("# Session Handoff"); expect(archivedRows[0].handoffMarkdown).toContain("# Session Handoff");
// New session is a distinct row with fresh counters. // 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.id).toBe(newSessionId);
expect(newSession.runCount).toBe(0); expect(newSession.runCount).toBe(0);
expect(newSession.totalInputTokens).toBe(0); expect(newSession.totalInputTokens).toBe(0);
@@ -510,13 +510,13 @@ describe("session management service", () => {
const svc = createSessionManagementService({ db, logger: mockLogger }); const svc = createSessionManagementService({ db, logger: mockLogger });
// Create and rotate a session // Create and rotate a session
const session1Id = db.createNewSession({ const session1Id = db.tasks.createNewSession({
companyId, companyId,
agentId, agentId,
adapterType: "gemini-cli", adapterType: "gemini-cli",
}); });
db.incrementSessionUsage(session1Id, 100000, 50000, "run-1"); db.tasks.incrementSessionUsage(session1Id, 100000, 50000, "run-1");
const session1 = db.getSessionById(session1Id)!; const session1 = db.tasks.getSessionById(session1Id)!;
svc.rotateSession(session1); svc.rotateSession(session1);
const handoff = svc.getHandoffContext(taskId); const handoff = svc.getHandoffContext(taskId);
@@ -526,7 +526,7 @@ describe("session management service", () => {
it("getHandoffContext returns null when no rotated sessions exist", () => { it("getHandoffContext returns null when no rotated sessions exist", () => {
const { taskId, companyId, agentId } = scaffold(db); const { taskId, companyId, agentId } = scaffold(db);
db.createNewSession({ db.tasks.createNewSession({
companyId, companyId,
agentId, agentId,
adapterType: "gemini-cli", adapterType: "gemini-cli",
@@ -544,16 +544,16 @@ describe("session management service", () => {
describe("usage delta", () => { describe("usage delta", () => {
it("getUsageDelta returns token difference between snapshot and current totals", () => { it("getUsageDelta returns token difference between snapshot and current totals", () => {
const { taskId, companyId, agentId } = scaffold(db); const { taskId, companyId, agentId } = scaffold(db);
const sessionId = db.createNewSession({ const sessionId = db.tasks.createNewSession({
companyId, companyId,
agentId, agentId,
adapterType: "gemini-cli", adapterType: "gemini-cli",
}); });
// Pre-populate some usage // 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 }); const svc = createSessionManagementService({ db, logger: mockLogger });
// Snapshot current state // Snapshot current state
+50 -64
View File
@@ -14,11 +14,15 @@ describe("sweepStaleReviews", () => {
function makeSweepDeps(overrides: Partial<SweepStaleReviewsDeps> = {}): SweepStaleReviewsDeps { function makeSweepDeps(overrides: Partial<SweepStaleReviewsDeps> = {}): SweepStaleReviewsDeps {
return { return {
db: { 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), 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, } as unknown as AppDatabase,
getCompanyExecutionMode: vi.fn().mockReturnValue("ai_ceo"), getCompanyExecutionMode: vi.fn().mockReturnValue("ai_ceo"),
dispatchAgentMessage: vi.fn().mockReturnValue({ ok: true }), 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", () => { type SweepDbMock = {
const deps = makeSweepDeps(); tasks: {
const dbMock = deps.db as unknown as {
listReviewTasksForSweep: ReturnType<typeof vi.fn>; listReviewTasksForSweep: ReturnType<typeof vi.fn>;
findPendingApprovalForTask: ReturnType<typeof vi.fn>; advanceTaskStatus: ReturnType<typeof vi.fn>;
};
agents: {
getManagerForAgent: ReturnType<typeof vi.fn>; getManagerForAgent: ReturnType<typeof vi.fn>;
getChainOfCommand: ReturnType<typeof vi.fn>; getChainOfCommand: ReturnType<typeof vi.fn>;
}; };
findPendingApprovalForTask: ReturnType<typeof vi.fn>;
};
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" }, { 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.agents.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager One", companyId: "company-1" });
dbMock.getChainOfCommand.mockReturnValueOnce([{ id: "higher-manager-1", name: "Higher Manager" }]); dbMock.agents.getChainOfCommand.mockReturnValueOnce([{ id: "higher-manager-1", name: "Higher Manager" }]);
const count = sweepStaleReviews("company-1", deps, NOW); const count = sweepStaleReviews("company-1", deps, NOW);
expect(count).toBe(1); expect(count).toBe(1);
@@ -66,9 +77,9 @@ describe("sweepStaleReviews", () => {
it("does not escalate when task in_review for only 25 minutes", () => { it("does not escalate when task in_review for only 25 minutes", () => {
const deps = makeSweepDeps(); const deps = makeSweepDeps();
const dbMock = deps.db as unknown as { listReviewTasksForSweep: ReturnType<typeof vi.fn> }; 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" }, { 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", () => { it("notifies the human when the reviewer has no higher manager", () => {
const deps = makeSweepDeps(); const deps = makeSweepDeps();
const dbMock = deps.db as unknown as { const dbMock = deps.db as unknown as SweepDbMock;
listReviewTasksForSweep: ReturnType<typeof vi.fn>;
findPendingApprovalForTask: ReturnType<typeof vi.fn>;
getManagerForAgent: ReturnType<typeof vi.fn>;
getChainOfCommand: ReturnType<typeof vi.fn>;
};
dbMock.listReviewTasksForSweep.mockReturnValueOnce([ dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" }, { id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" },
]); ]);
dbMock.findPendingApprovalForTask.mockReturnValueOnce(null); dbMock.findPendingApprovalForTask.mockReturnValueOnce(null);
dbMock.getManagerForAgent.mockReturnValueOnce({ id: "ceo-1", name: "CEO", companyId: "company-1" }); dbMock.agents.getManagerForAgent.mockReturnValueOnce({ id: "ceo-1", name: "CEO", companyId: "company-1" });
dbMock.getChainOfCommand.mockReturnValueOnce([]); dbMock.agents.getChainOfCommand.mockReturnValueOnce([]);
const count = sweepStaleReviews("company-1", deps, NOW); const count = sweepStaleReviews("company-1", deps, NOW);
expect(count).toBe(1); expect(count).toBe(1);
@@ -109,18 +115,13 @@ describe("sweepStaleReviews", () => {
it("auto-approves pending approval linked to a stale review task", () => { it("auto-approves pending approval linked to a stale review task", () => {
const deps = makeSweepDeps(); const deps = makeSweepDeps();
const dbMock = deps.db as unknown as { const dbMock = deps.db as unknown as SweepDbMock;
listReviewTasksForSweep: ReturnType<typeof vi.fn>;
findPendingApprovalForTask: ReturnType<typeof vi.fn>;
getManagerForAgent: ReturnType<typeof vi.fn>;
getChainOfCommand: ReturnType<typeof vi.fn>;
};
dbMock.listReviewTasksForSweep.mockReturnValueOnce([ dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" }, { 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.agents.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager", companyId: "company-1" });
dbMock.getChainOfCommand.mockReturnValueOnce([{ id: "higher-1", name: "Higher" }]); dbMock.agents.getChainOfCommand.mockReturnValueOnce([{ id: "higher-1", name: "Higher" }]);
dbMock.findPendingApprovalForTask.mockReturnValueOnce({ id: "approval-1", type: "deliverable_review" }); dbMock.findPendingApprovalForTask.mockReturnValueOnce({ id: "approval-1", type: "deliverable_review" });
sweepStaleReviews("company-1", deps, NOW); sweepStaleReviews("company-1", deps, NOW);
@@ -139,18 +140,13 @@ describe("sweepStaleReviews", () => {
it("swallows resolveApprovalDecision races gracefully", () => { it("swallows resolveApprovalDecision races gracefully", () => {
const deps = makeSweepDeps(); const deps = makeSweepDeps();
const dbMock = deps.db as unknown as { const dbMock = deps.db as unknown as SweepDbMock;
listReviewTasksForSweep: ReturnType<typeof vi.fn>;
findPendingApprovalForTask: ReturnType<typeof vi.fn>;
getManagerForAgent: ReturnType<typeof vi.fn>;
getChainOfCommand: ReturnType<typeof vi.fn>;
};
dbMock.listReviewTasksForSweep.mockReturnValueOnce([ dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" }, { 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.agents.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager", companyId: "company-1" });
dbMock.getChainOfCommand.mockReturnValueOnce([{ id: "higher-1", name: "Higher" }]); dbMock.agents.getChainOfCommand.mockReturnValueOnce([{ id: "higher-1", name: "Higher" }]);
dbMock.findPendingApprovalForTask.mockReturnValueOnce({ id: "approval-1", type: "deliverable_review" }); dbMock.findPendingApprovalForTask.mockReturnValueOnce({ id: "approval-1", type: "deliverable_review" });
(deps.resolveApprovalDecision as ReturnType<typeof vi.fn>).mockImplementationOnce(() => { (deps.resolveApprovalDecision as ReturnType<typeof vi.fn>).mockImplementationOnce(() => {
throw new Error("Approval already decided"); throw new Error("Approval already decided");
@@ -161,24 +157,18 @@ describe("sweepStaleReviews", () => {
it("advances task status to done after escalation", () => { it("advances task status to done after escalation", () => {
const deps = makeSweepDeps(); const deps = makeSweepDeps();
const dbMock = deps.db as unknown as { const dbMock = deps.db as unknown as SweepDbMock;
listReviewTasksForSweep: ReturnType<typeof vi.fn>;
findPendingApprovalForTask: ReturnType<typeof vi.fn>;
getManagerForAgent: ReturnType<typeof vi.fn>;
getChainOfCommand: ReturnType<typeof vi.fn>;
advanceTaskStatus: ReturnType<typeof vi.fn>;
};
dbMock.listReviewTasksForSweep.mockReturnValueOnce([ dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Build login" }, { 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.agents.getManagerForAgent.mockReturnValueOnce({ id: "manager-1", name: "Manager", companyId: "company-1" });
dbMock.getChainOfCommand.mockReturnValueOnce([{ id: "higher-1", name: "Higher" }]); dbMock.agents.getChainOfCommand.mockReturnValueOnce([{ id: "higher-1", name: "Higher" }]);
dbMock.findPendingApprovalForTask.mockReturnValueOnce(null); dbMock.findPendingApprovalForTask.mockReturnValueOnce(null);
sweepStaleReviews("company-1", deps, NOW); sweepStaleReviews("company-1", deps, NOW);
expect(dbMock.advanceTaskStatus).toHaveBeenCalledWith( expect(dbMock.tasks.advanceTaskStatus).toHaveBeenCalledWith(
"task-1", "task-1",
"done", "done",
"Auto-advanced: review timeout exceeded 30 minutes.", "Auto-advanced: review timeout exceeded 30 minutes.",
@@ -188,9 +178,9 @@ describe("sweepStaleReviews", () => {
it("skips tasks without an assignee", () => { it("skips tasks without an assignee", () => {
const deps = makeSweepDeps(); const deps = makeSweepDeps();
const dbMock = deps.db as unknown as { listReviewTasksForSweep: ReturnType<typeof vi.fn> }; 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" }, { id: "task-1", assigneeAgentId: null, updatedAt: THIRTY_FIVE_MIN_AGO, title: "Orphan task" },
]); ]);
@@ -208,9 +198,9 @@ describe("sweepStaleReviews", () => {
const deps = makeSweepDeps({ const deps = makeSweepDeps({
getCompanyExecutionMode: vi.fn().mockReturnValue("ai_cofounder"), getCompanyExecutionMode: vi.fn().mockReturnValue("ai_cofounder"),
}); });
const dbMock = deps.db as unknown as { listReviewTasksForSweep: ReturnType<typeof vi.fn> }; 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" }, { 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.notify).not.toHaveBeenCalled();
expect(deps.resolveApprovalDecision).not.toHaveBeenCalled(); expect(deps.resolveApprovalDecision).not.toHaveBeenCalled();
expect(deps.dispatchAgentMessage).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", () => { it("sends a critical cofounder review reminder without auto-approving", () => {
const deps = makeSweepDeps({ const deps = makeSweepDeps({
getCompanyExecutionMode: vi.fn().mockReturnValue("ai_cofounder"), getCompanyExecutionMode: vi.fn().mockReturnValue("ai_cofounder"),
}); });
const dbMock = deps.db as unknown as { const dbMock = deps.db as unknown as SweepDbMock;
listReviewTasksForSweep: ReturnType<typeof vi.fn>;
findPendingApprovalForTask: ReturnType<typeof vi.fn>;
getManagerForAgent: ReturnType<typeof vi.fn>;
};
dbMock.listReviewTasksForSweep.mockReturnValueOnce([ dbMock.tasks.listReviewTasksForSweep.mockReturnValueOnce([
{ id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Review me" }, { id: "task-1", assigneeAgentId: "agent-1", updatedAt: THIRTY_FIVE_MIN_AGO, title: "Review me" },
]); ]);
dbMock.findPendingApprovalForTask.mockReturnValueOnce({ id: "approval-1", type: "deliverable_review" }); 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); const count = sweepStaleReviews("company-1", deps, NOW);
expect(count).toBe(0); expect(count).toBe(0);
@@ -250,6 +236,6 @@ describe("sweepStaleReviews", () => {
); );
expect(deps.resolveApprovalDecision).not.toHaveBeenCalled(); expect(deps.resolveApprovalDecision).not.toHaveBeenCalled();
expect(deps.dispatchAgentMessage).not.toHaveBeenCalled(); expect(deps.dispatchAgentMessage).not.toHaveBeenCalled();
expect(deps.db.advanceTaskStatus).not.toHaveBeenCalled(); expect(dbMock.tasks.advanceTaskStatus).not.toHaveBeenCalled();
}); });
}); });
+3 -1
View File
@@ -151,7 +151,9 @@ describe("ACCEPTANCE_CRITERIA_KEYWORDS", () => {
describe("checkCapabilityMatch", () => { describe("checkCapabilityMatch", () => {
function makeMockDb(agent: { name: string; capabilities: string }, skillAssignments: string[] = [], skills: Record<string, { name: string; description: string | null; slug: string }> = {}) { function makeMockDb(agent: { name: string; capabilities: string }, skillAssignments: string[] = [], skills: Record<string, { name: string; description: string | null; slug: string }> = {}) {
return { return {
getAgent: (_id: string) => agent, entityCrud: {
getAgent: (_id: string) => agent,
},
listAgentSkillAssignments: (_id: string) => skillAssignments, listAgentSkillAssignments: (_id: string) => skillAssignments,
getCompanySkill: (_companyId: string, skillId: string) => skills[skillId] ?? null, getCompanySkill: (_companyId: string, skillId: string) => skills[skillId] ?? null,
}; };
+2 -2
View File
@@ -55,7 +55,7 @@ function createTestWorkspace(db: AppDatabase, companyId: string): string {
} }
function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string, name = "Agent"): string { function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string, name = "Agent"): string {
return db.saveAgent({ return db.agents.saveAgent({
companyId, companyId,
name, name,
role: "IC", role: "IC",
@@ -76,7 +76,7 @@ function createTestTask(
agentId: string, agentId: string,
workspaceId: string, workspaceId: string,
): string { ): string {
return db.saveTask({ return db.tasks.saveTask({
companyId, companyId,
title: "Phase helper task", title: "Phase helper task",
description: "dual-write seed", description: "dual-write seed",
+1 -1
View File
@@ -47,7 +47,7 @@ function createTestWorkspace(db: AppDatabase, companyId: string): string {
} }
function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string, name = "Agent"): string { function createTestAgent(db: AppDatabase, companyId: string, workspaceId: string, name = "Agent"): string {
return db.saveAgent({ return db.agents.saveAgent({
companyId, companyId,
name, name,
role: "IC", role: "IC",