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:
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}`);
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}`,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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); }
|
||||||
|
|||||||
@@ -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),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) },
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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" }]);
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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: "",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in new issue
Block a user