From 118e3d8795fc36030927d5a1bf3f97c2559668f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 20 Apr 2026 13:31:39 -0400 Subject: [PATCH] Remove final AppDatabase pass-through methods with ESLint guard (C1 part 3/3) Migrate all remaining pass-through call sites (social, skills, budget, entityCrud, documents, knowledge, automation, wakeup, backup, threads, settings, snapshot, connectors, workspaces, activity) to their owning domain modules. `AppDatabase` now carries only three methods: - createHireRequest (multi-module composite: agents + approvalsMod) - rollbackConfigRevision (multi-module composite: documents + entityCrud) - queryAll (raw-SQL escape hatch, @internal) The vault/env-binding infrastructure previously passed argument-by- argument into `entityCrud.saveSecret`/`resolveSecretEnv` now binds at module construction, letting callers invoke `db.entityCrud.saveSecret({...})` without carrying vault plumbing. Structural types that narrowed AppDatabase to a bag of flat methods (`ConnectorModelPolicyDeps` callers, `CapabilityMatchDb`, test mocks in budget-enforcement / budget-incidents / task-quality-gates / failure-escalation / task-quality-gates-sweep) are reshaped to the new namespace. `connectorPolicyDepsFromDb` replaces the old pattern of handing the whole AppDatabase to `validateConnectorModelSelection`. A new ESLint rule in eslint.config.mjs blocks new top-level methods on AppDatabase so the pass-through layer cannot silently grow back. `AppDatabase.ts` dropped from 193 lines to 58; `AppDatabaseBase` lost the 270-line trailing pass-through block. --- eslint.config.mjs | 27 +++ .../remove-appdatabase-passthrough.mjs | 4 +- src/main/agent-permissions.ts | 2 +- src/main/agent-runtime/artifact-projector.ts | 8 +- src/main/agent-runtime/runtime-bridge.ts | 4 +- src/main/api-server/routes-agent-control.ts | 10 +- src/main/api-server/routes-agents-standup.ts | 8 +- src/main/api-server/routes-agents.ts | 4 +- src/main/api-server/routes-approvals.ts | 4 +- src/main/api-server/routes-files.ts | 42 ++-- src/main/api-server/routes-meetings.ts | 12 +- src/main/api-server/routes-messages.ts | 4 +- src/main/api-server/routes-misc.ts | 24 +-- src/main/api-server/routes-tasks.ts | 10 +- src/main/api-server/routes-work-graph.ts | 24 +-- src/main/api-server/server-core.ts | 2 +- src/main/approval-policy.ts | 2 +- src/main/automation/automation-actions.ts | 8 +- .../automation/automation-approval-handler.ts | 36 ++-- src/main/automation/automation-handlers.ts | 2 +- .../automation/automation-run-completion.ts | 6 +- src/main/automation/automation-workflow.ts | 20 +- src/main/automation/index.ts | 8 +- src/main/bootstrap/prepare-services.ts | 4 +- .../bootstrap/recover-interrupted-tasks.ts | 2 +- src/main/bootstrap/recover-scheduled-posts.ts | 4 +- .../bootstrap/run-startup-health-checks.ts | 2 +- src/main/browser-action-service.ts | 16 +- src/main/budget-enforcement-service.ts | 28 +-- src/main/connector-health-service.ts | 14 +- src/main/connector-model-policy.ts | 16 ++ src/main/database-base.ts | 77 ++----- src/main/database.ts | 196 +++--------------- src/main/database/entity-crud.ts | 8 +- src/main/document-lifecycle.ts | 2 +- src/main/execution-mode-reconciler.ts | 8 +- src/main/hire-hook.ts | 4 +- src/main/ipc-agent-coordination-handlers.ts | 2 +- .../ipc-automation-operations-handlers.ts | 20 +- src/main/ipc-browser-handlers.ts | 18 +- src/main/ipc-budget-handlers.ts | 10 +- src/main/ipc-content-operations-handlers.ts | 24 +-- src/main/ipc-core-admin-handlers.ts | 22 +- src/main/ipc-core-agent-graph-handlers.ts | 26 +-- src/main/ipc-core-operations-handlers.ts | 28 +-- src/main/ipc-core-read-handlers.ts | 18 +- src/main/ipc-core-task-graph-handlers.ts | 20 +- src/main/ipc-recovery-handlers.ts | 4 +- src/main/ipc-skill-handlers.ts | 4 +- src/main/ipc-thread-handlers.ts | 10 +- src/main/main-runtime-support.ts | 4 +- src/main/messaging/dispatch.ts | 10 +- src/main/onboarding-service.ts | 18 +- src/main/orchestration/heartbeat.ts | 24 +-- src/main/orchestration/resolve-workspace.ts | 10 +- src/main/orchestration/run-dispatch.ts | 12 +- src/main/orchestration/scheduler-heartbeat.ts | 2 +- .../orchestration/scheduler-housekeeping.ts | 6 +- .../orchestration/scheduler-lock-recovery.ts | 4 +- src/main/orchestration/task-runner.ts | 20 +- src/main/orchestration/turn-context.ts | 33 ++- src/main/run-housekeeping.ts | 4 +- src/main/service-graph.ts | 2 +- .../service-graph/create-desktop-shell.ts | 4 +- src/main/service-graph/create-lifecycle.ts | 2 +- src/main/skills/auto-assign-on-hire.ts | 6 +- src/main/skills/crud.ts | 16 +- src/main/skills/import-github.ts | 2 +- src/main/skills/import-local.ts | 4 +- src/main/skills/import-url.ts | 2 +- src/main/skills/runtime.ts | 10 +- src/main/task-quality-gates.ts | 10 +- src/main/thread-integration/index.ts | 58 +++--- src/main/thread-integration/thread-helpers.ts | 14 +- src/main/updater-service.ts | 2 +- src/main/worker-service.ts | 4 +- src/main/worker/entry/crash-recovery.ts | 16 +- src/main/worker/entry/types.ts | 6 +- .../worker/handlers/run-finished-helpers.ts | 12 +- .../integration/agent-model-switching.spec.ts | 4 +- .../api-recursive-org-execution.spec.ts | 4 +- tests/integration/api-run-events.spec.ts | 2 +- .../automation-messages.spec.ts | 36 ++-- .../autonomous-chain/crash-recovery.spec.ts | 6 +- .../autonomous-chain/e2e-flows.spec.ts | 16 +- .../autonomous-chain/run-failure.spec.ts | 2 +- .../autonomous-chain/shared-setup.ts | 2 +- .../autonomous-chain/wakeup-queue.spec.ts | 16 +- .../connector-model-enforcement.spec.ts | 16 +- tests/integration/cross-subsystem.spec.ts | 10 +- tests/integration/database.spec.ts | 30 +-- .../document-run-provenance.spec.ts | 10 +- .../integration/onboarding-model-pool.spec.ts | 22 +- tests/integration/pi-mono-migrations.spec.ts | 2 +- tests/integration/process-recovery.spec.ts | 12 +- .../skill-aware-assignment.spec.ts | 18 +- .../integration/thread-wait-heartbeat.spec.ts | 18 +- .../work-products-lifecycle.spec.ts | 40 ++-- tests/unit/agent-budget-policy-sync.test.ts | 12 +- tests/unit/agent-hire-routing.test.ts | 4 +- tests/unit/agent-permissions.test.ts | 6 +- tests/unit/approval-hire-routing.test.ts | 4 +- ...approval-review-feedback-writeback.test.ts | 10 +- tests/unit/atomic-checkout.test.ts | 4 +- tests/unit/budget-enforcement.test.ts | 40 ++-- tests/unit/budget-incidents.test.ts | 58 +++--- tests/unit/company-mode-switch.test.ts | 10 +- tests/unit/database.test.ts | 70 +++---- tests/unit/edge-case-budget.test.ts | 22 +- tests/unit/edge-case-concurrent.test.ts | 4 +- tests/unit/edge-case-session.test.ts | 8 +- tests/unit/ipc-agent-graph-handlers.test.ts | 8 +- tests/unit/ipc-core-admin-handlers.test.ts | 2 +- tests/unit/knowledge-provenance.test.ts | 4 +- tests/unit/message-dispatch.test.ts | 16 +- tests/unit/resolve-workspace.test.ts | 10 +- tests/unit/session-management.test.ts | 6 +- tests/unit/task-quality-gates.test.ts | 6 +- tests/unit/tasks-phase.test.ts | 4 +- tests/unit/wakeup-lifecycle.test.ts | 22 +- 120 files changed, 838 insertions(+), 933 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 4042d38b..999f5486 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -77,6 +77,33 @@ export default tseslint.config( }, }, }, + { + // C1 part 3/3 — block new top-level methods on AppDatabase. + // + // The whole point of the C1 codemod was to force callers through + // the sub-module surface (`db.tasks.saveTask(...)`) rather than a + // flat pass-through (`db.saveTask(...)`). Any new method added to + // `AppDatabase` reintroduces exactly the broken-window pattern we + // just killed. Allowed methods are limited to + // `createHireRequest`, `rollbackConfigRevision`, and `queryAll` + // (the documented multi-module composites + raw-SQL escape hatch). + // Adding a new method here must either use a different name + // (then the rule fires and you must justify it with a + // disable-next-line and a rationale) or go on an existing or new + // domain module. + files: ["src/main/database.ts"], + rules: { + "no-restricted-syntax": [ + "error", + { + selector: + "ClassBody > MethodDefinition[key.name!=/^(createHireRequest|rollbackConfigRevision|queryAll)$/]", + message: + "C1: do not add new methods to AppDatabase. Put the implementation on the owning `database/.ts` and invoke via `db..(...)`. Only `createHireRequest`, `rollbackConfigRevision`, and `queryAll` are grandfathered; adding another requires a proposal and an explicit eslint-disable.", + }, + ], + }, + }, { // B2 Phase 1 step 5/5 — block new raw status-writes on tasks. // diff --git a/scripts/codemod/remove-appdatabase-passthrough.mjs b/scripts/codemod/remove-appdatabase-passthrough.mjs index 83d281a8..670749b0 100644 --- a/scripts/codemod/remove-appdatabase-passthrough.mjs +++ b/scripts/codemod/remove-appdatabase-passthrough.mjs @@ -88,7 +88,9 @@ const METHOD_TO_MODULE = { deleteProject: "entityCrud", deleteSecret: "entityCrud", countActiveGoalsForCompany: "entityCrud", - createHireRequest: "entityCrud", + // createHireRequest is a multi-module composite that lives on + // AppDatabase itself (spans agents + approvalsMod). Do not rewrite. + // rollbackConfigRevision is the same (spans documents + entityCrud). // documentsMod saveDocument: "documentsMod", listImportantDocsForReview: "documentsMod", diff --git a/src/main/agent-permissions.ts b/src/main/agent-permissions.ts index 5c4c2226..855832ce 100644 --- a/src/main/agent-permissions.ts +++ b/src/main/agent-permissions.ts @@ -255,7 +255,7 @@ export function canAssignTask(db: AppDatabase, companyId: string, actorAgentId: if (!targetAgentId || targetAgentId === actorAgentId) { return true; } - if (!db.belongsToCompany("agents", targetAgentId, companyId)) { + if (!db.settings.belongsToCompany("agents", targetAgentId, companyId)) { return false; } if (canManageOrganization(db, companyId, actorAgentId)) { diff --git a/src/main/agent-runtime/artifact-projector.ts b/src/main/agent-runtime/artifact-projector.ts index f97849f6..d328fc26 100644 --- a/src/main/agent-runtime/artifact-projector.ts +++ b/src/main/agent-runtime/artifact-projector.ts @@ -323,7 +323,7 @@ function retireDeletedArtifacts( deleted: string[], ): ProjectionOutcome[] { if (deleted.length === 0) return []; - const archived = db.archiveWorkProductsByExternalPaths({ + const archived = db.documentsMod.archiveWorkProductsByExternalPaths({ companyId: ctx.companyId, workspaceId: ctx.workspaceId, provider: "filesystem", @@ -608,13 +608,13 @@ function upsertFilesystemArtifact( // Every touch refreshes `last_touched_by_run_id` so the deliverable // signal, the DocumentCenter preview, and the run-provenance readers // all see the latest run that wrote the file. - const existing = db.findWorkProductByExternalId(ctx.companyId, provider, relativePath, ctx.workspaceId); + const existing = db.documentsMod.findWorkProductByExternalId(ctx.companyId, provider, relativePath, ctx.workspaceId); if (existing) { // `status: "active"` flips a previously-archived row (earlier run // retired the path, this run recreated it) back into the visible // DocumentCenter set. `lastTouchedByRunId` keeps the provenance // timeline pointed at the run that just wrote the bytes. - db.updateWorkProduct(existing.id, ctx.companyId, { + db.documentsMod.updateWorkProduct(existing.id, ctx.companyId, { title, status: "active", lastTouchedByRunId: ctx.runId, @@ -622,7 +622,7 @@ function upsertFilesystemArtifact( }); return { kind: "updated", workProductId: existing.id, relativePath }; } - const workProductId = db.createWorkProduct({ + const workProductId = db.documentsMod.createWorkProduct({ companyId: ctx.companyId, taskId: ctx.taskId, workspaceId: ctx.workspaceId, diff --git a/src/main/agent-runtime/runtime-bridge.ts b/src/main/agent-runtime/runtime-bridge.ts index 14adf259..45783416 100644 --- a/src/main/agent-runtime/runtime-bridge.ts +++ b/src/main/agent-runtime/runtime-bridge.ts @@ -358,7 +358,7 @@ export class RuntimeBridge { try { const body = extractTextBody(message.content); const actor = this.resolveThreadEventActor(envelope, message); - this.deps.db.addThreadEvent({ + this.deps.db.threads.addThreadEvent({ threadId: thread.id, actorId: actor.actorId, actorType: actor.actorType, @@ -470,7 +470,7 @@ export class RuntimeBridge { : record.senderKind === "system" ? "system" : "agent"; - this.deps.db.addThreadEvent({ + this.deps.db.threads.addThreadEvent({ threadId: thread.id, actorId: record.fromAgentId, actorType, diff --git a/src/main/api-server/routes-agent-control.ts b/src/main/api-server/routes-agent-control.ts index 0a4e766e..694231e6 100644 --- a/src/main/api-server/routes-agent-control.ts +++ b/src/main/api-server/routes-agent-control.ts @@ -95,9 +95,9 @@ export function registerAgentControlRoutes(server: AgentApiServer) { server.json(res, 404, { error: "Agent not found in your company" }); return; } - const skillIds = db.listAgentSkillAssignments(agentId); + const skillIds = db.skills.listAgentSkillAssignments(agentId); const skills = skillIds - .map((sid) => db.getCompanySkill(token.companyId, sid)) + .map((sid) => db.skills.getCompanySkill(token.companyId, sid)) .filter((s): s is NonNullable => s != null); server.json(res, 200, skills.map((s) => ({ id: s.id, @@ -127,12 +127,12 @@ export function registerAgentControlRoutes(server: AgentApiServer) { server.json(res, 404, { error: "Agent not found in your company" }); return; } - const skill = db.getCompanySkill(token.companyId, parsed.data.skillId); + const skill = db.skills.getCompanySkill(token.companyId, parsed.data.skillId); if (!skill) { server.json(res, 404, { error: "Skill not found in your company" }); return; } - db.assignSkillToAgent(agentId, parsed.data.skillId); + db.skills.assignSkillToAgent(agentId, parsed.data.skillId); onDomainChanged(); server.json(res, 200, { ok: true, message: `Skill "${skill.name}" assigned to agent ${agent.name}` }); }); @@ -151,7 +151,7 @@ export function registerAgentControlRoutes(server: AgentApiServer) { server.json(res, 404, { error: "Agent not found in your company" }); return; } - db.removeSkillFromAgent(agentId, skillId); + db.skills.removeSkillFromAgent(agentId, skillId); onDomainChanged(); server.json(res, 200, { ok: true, message: `Skill removed from agent ${agent.name}` }); }); diff --git a/src/main/api-server/routes-agents-standup.ts b/src/main/api-server/routes-agents-standup.ts index 1e2f7e09..a6f63914 100644 --- a/src/main/api-server/routes-agents-standup.ts +++ b/src/main/api-server/routes-agents-standup.ts @@ -33,7 +33,7 @@ export function registerAgentStandupRoute(server: AgentApiServer): void { ...reportTasks.map((t) => t.id), ]); const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(); - const recentComments = db.listRecentTaskComments([...allRelevantTaskIds], twoHoursAgo, 20); + const recentComments = db.activityMod.listRecentTaskComments([...allRelevantTaskIds], twoHoursAgo, 20); const myPendingApprovals = db.approvalsMod.listAgentPendingApprovals(token.companyId, token.agentId); const myResolvedApprovals = db.approvalsMod.listAgentResolvedApprovals(token.companyId, token.agentId, 5); @@ -59,10 +59,10 @@ export function registerAgentStandupRoute(server: AgentApiServer): void { // surface keeps the briefing readable. const RECENT_OUTPUT_LIMIT = 20; const myTaskIds = new Set(myTasks.map((t) => t.id)); - const recentDocuments = db.listDocuments(token.companyId) + const recentDocuments = db.documentsMod.listDocuments(token.companyId) .filter((doc) => doc.authorAgentId === token.agentId) .slice(0, RECENT_OUTPUT_LIMIT); - const recentWorkProducts = db.listWorkProductsForCompany(token.companyId) + const recentWorkProducts = db.documentsMod.listWorkProductsForCompany(token.companyId) .filter((wp) => wp.taskId !== null && myTaskIds.has(wp.taskId)) .slice(0, RECENT_OUTPUT_LIMIT); @@ -71,7 +71,7 @@ export function registerAgentStandupRoute(server: AgentApiServer): void { ...myTasks.map<[string, string]>((t) => [t.id, t.title]), ...reportTasks.map<[string, string]>((t) => [t.id, t.title]), ]); - const socialAccounts = db.listSocialAccounts(token.companyId); + const socialAccounts = db.social.listSocialAccounts(token.companyId); server.json(res, 200, { agent: { id: agent.id, name: agent.name, role: agent.role, status: agent.status }, diff --git a/src/main/api-server/routes-agents.ts b/src/main/api-server/routes-agents.ts index 827d2672..faca5f9e 100644 --- a/src/main/api-server/routes-agents.ts +++ b/src/main/api-server/routes-agents.ts @@ -164,7 +164,7 @@ export function registerAgentRoutes(server: AgentApiServer) { } const connectorRecord = (() => { try { - return db.getConnector(input.connectorId); + return db.connectorsCrud.getConnector(input.connectorId); } catch (err) { log.warn(`[api/agents] getConnector failed for ${input.connectorId}:`, err instanceof Error ? err.message : String(err)); return null; @@ -257,7 +257,7 @@ export function registerAgentRoutes(server: AgentApiServer) { return; } - db.addActivity({ + db.activityMod.addActivity({ companyId, actor: token.agentId, action: "agent.hire_requested", diff --git a/src/main/api-server/routes-approvals.ts b/src/main/api-server/routes-approvals.ts index 5ef1c056..fa776b53 100644 --- a/src/main/api-server/routes-approvals.ts +++ b/src/main/api-server/routes-approvals.ts @@ -85,7 +85,7 @@ export function registerApprovalRoutes(server: AgentApiServer) { return; } - db.addActivity({ + db.activityMod.addActivity({ companyId, actor: token.agentId, action: "approval.created", @@ -228,7 +228,7 @@ export function registerApprovalRoutes(server: AgentApiServer) { const taskIds = Array.isArray(data.taskIds) ? data.taskIds.filter((id): id is string => typeof id === "string") : []; if (taskIds.length === 0) { server.json(res, 400, { error: "taskIds array is required" }); return; } for (const taskId of taskIds) { - if (!db.belongsToCompany("tasks", taskId, token.companyId)) { + if (!db.settings.belongsToCompany("tasks", taskId, token.companyId)) { server.json(res, 400, { error: `Task ${taskId} does not belong to this company` }); return; } } diff --git a/src/main/api-server/routes-files.ts b/src/main/api-server/routes-files.ts index cda33aee..64389f40 100644 --- a/src/main/api-server/routes-files.ts +++ b/src/main/api-server/routes-files.ts @@ -71,15 +71,15 @@ export function registerFileRoutes(server: AgentApiServer) { const reviewerAgentId = data.reviewerAgentId ?? null; const projectId = data.projectId ?? null; const goalId = data.goalId ?? null; - if (reviewerAgentId && !db.belongsToCompany("agents", reviewerAgentId, token.companyId)) { + if (reviewerAgentId && !db.settings.belongsToCompany("agents", reviewerAgentId, token.companyId)) { server.json(res, 400, { error: "Reviewer agent does not belong to the current company" }); return; } - if (projectId && !db.belongsToCompany("projects", projectId, token.companyId)) { + if (projectId && !db.settings.belongsToCompany("projects", projectId, token.companyId)) { server.json(res, 400, { error: "Project does not belong to the current company" }); return; } - if (goalId && !db.belongsToCompany("goals", goalId, token.companyId)) { + if (goalId && !db.settings.belongsToCompany("goals", goalId, token.companyId)) { server.json(res, 400, { error: "Goal does not belong to the current company" }); return; } @@ -114,7 +114,7 @@ export function registerFileRoutes(server: AgentApiServer) { server.json(res, 403, { error: "Company mismatch" }); return; } - const docs = db.listDocuments(token.companyId); + const docs = db.documentsMod.listDocuments(token.companyId); server.json(res, 200, docs); }); @@ -128,7 +128,7 @@ export function registerFileRoutes(server: AgentApiServer) { server.json(res, 400, { error: "Missing documentId" }); return; } - const doc = db.getDocumentForUpdate(documentId, token.companyId); + const doc = db.documentsMod.getDocumentForUpdate(documentId, token.companyId); if (!doc) { server.json(res, 404, { error: "Document not found in this company" }); return; @@ -146,7 +146,7 @@ export function registerFileRoutes(server: AgentApiServer) { server.json(res, 400, { error: "Missing documentId" }); return; } - const doc = db.getDocumentForUpdate(documentId, token.companyId); + const doc = db.documentsMod.getDocumentForUpdate(documentId, token.companyId); if (!doc) { server.json(res, 404, { error: "Document not found in this company" }); return; @@ -158,14 +158,14 @@ export function registerFileRoutes(server: AgentApiServer) { const reviewerAgentId = data.reviewerAgentId !== undefined ? (data.reviewerAgentId ?? null) : doc.reviewerAgentId; - if (data.reviewerAgentId !== undefined && reviewerAgentId && !db.belongsToCompany("agents", reviewerAgentId, token.companyId)) { + if (data.reviewerAgentId !== undefined && reviewerAgentId && !db.settings.belongsToCompany("agents", reviewerAgentId, token.companyId)) { server.json(res, 400, { error: "Reviewer agent does not belong to the current company" }); return; } // Conflict detection: if baseRevisionId provided, verify it matches current if (data.baseRevisionId !== undefined) { - const currentRevId = db.getDocumentLatestRevisionId(documentId); + const currentRevId = db.documentsMod.getDocumentLatestRevisionId(documentId); if (currentRevId && data.baseRevisionId !== currentRevId) { server.json(res, 409, { error: "Document was updated by someone else", currentRevisionId: currentRevId }); return; @@ -182,7 +182,7 @@ export function registerFileRoutes(server: AgentApiServer) { // Content update → create revision for audit trail let updatedVersion = doc.version; if (data.content !== undefined) { - const revision = db.createDocumentRevision({ + const revision = db.documentsMod.createDocumentRevision({ documentId, companyId: token.companyId, body: data.content, @@ -194,9 +194,9 @@ export function registerFileRoutes(server: AgentApiServer) { // Field updates (title, reviewer, tags, etc.) if (data.status !== undefined) { - db.updateDocumentStatus(documentId, token.companyId, data.status); + db.documentsMod.updateDocumentStatus(documentId, token.companyId, data.status); } - db.saveDocument({ + db.documentsMod.saveDocument({ id: documentId, companyId: token.companyId, type: data.type ?? doc.type, @@ -213,14 +213,14 @@ export function registerFileRoutes(server: AgentApiServer) { }); onDomainChanged(); - server.json(res, 200, { updated: true, latestRevisionId: db.getDocumentLatestRevisionId(documentId) }); + server.json(res, 200, { updated: true, latestRevisionId: db.documentsMod.getDocumentLatestRevisionId(documentId) }); }); server.addRoute("GET", "/api/companies/:companyId/documents/:documentId/revisions", (_req, res, params, token) => { if (params.companyId !== token.companyId) { server.json(res, 403, { error: "Company mismatch" }); return; } const documentId = params.documentId; if (!documentId) { server.json(res, 400, { error: "Missing documentId" }); return; } - const revisions = db.listDocumentRevisions(documentId, token.companyId); + const revisions = db.documentsMod.listDocumentRevisions(documentId, token.companyId); server.json(res, 200, revisions); }); @@ -230,7 +230,7 @@ export function registerFileRoutes(server: AgentApiServer) { try { const task = db.entityCrud.getTask(requireParam(params, "issueId")); if (task.companyId !== token.companyId) { server.json(res, 404, { error: "Task not found" }); return; } - const products = db.listWorkProductsForTask(task.id, task.companyId); + const products = db.documentsMod.listWorkProductsForTask(task.id, task.companyId); server.json(res, 200, products); } catch (error) { log.warn(`[api/work-products] list failed for issue ${params.issueId}: ${error instanceof Error ? error.message : String(error)}`); @@ -245,7 +245,7 @@ export function registerFileRoutes(server: AgentApiServer) { const parsed = createWorkProductSchema.safeParse(body); if (!parsed.success) { server.json(res, 400, { error: "Invalid input", details: parsed.error.flatten() }); return; } const data = parsed.data; - const id = db.createWorkProduct({ + const id = db.documentsMod.createWorkProduct({ companyId: task.companyId, taskId: task.id, projectId: data.projectId ?? task.projectId, @@ -275,7 +275,7 @@ export function registerFileRoutes(server: AgentApiServer) { if (!wpId) { server.json(res, 400, { error: "Missing workProductId" }); return; } const parsed = updateWorkProductSchema.safeParse(body); if (!parsed.success) { server.json(res, 400, { error: "Invalid input", details: parsed.error.flatten() }); return; } - const changes = db.updateWorkProduct(wpId, token.companyId, parsed.data); + const changes = db.documentsMod.updateWorkProduct(wpId, token.companyId, parsed.data); if (changes === 0) { server.json(res, 404, { error: "Work product not found" }); return; } onDomainChanged(); server.json(res, 200, { updated: true }); @@ -284,7 +284,7 @@ export function registerFileRoutes(server: AgentApiServer) { server.addRoute("DELETE", "/api/work-products/:workProductId", (_req, res, params, token) => { const wpId = params.workProductId; if (!wpId) { server.json(res, 400, { error: "Missing workProductId" }); return; } - const changes = db.deleteWorkProduct(wpId, token.companyId); + const changes = db.documentsMod.deleteWorkProduct(wpId, token.companyId); if (changes === 0) { server.json(res, 404, { error: "Work product not found" }); return; } onDomainChanged(); server.json(res, 200, { deleted: true }); @@ -296,7 +296,7 @@ export function registerFileRoutes(server: AgentApiServer) { try { const task = db.entityCrud.getTask(requireParam(params, "issueId")); if (task.companyId !== token.companyId) { server.json(res, 404, { error: "Task not found" }); return; } - const links = db.listDocumentsForTask(task.id, task.companyId); + const links = db.documentsMod.listDocumentsForTask(task.id, task.companyId); server.json(res, 200, links); } catch (error) { log.warn(`[api/task-documents] list failed for issue ${params.issueId}: ${error instanceof Error ? error.message : String(error)}`); @@ -315,10 +315,10 @@ export function registerFileRoutes(server: AgentApiServer) { const parsed = linkDocumentSchema.safeParse(body); if (!parsed.success) { server.json(res, 400, { error: "Invalid input", details: parsed.error.flatten() }); return; } const documentId = parsed.data.documentId; - if (!db.belongsToCompany("documents", documentId, token.companyId)) { + if (!db.settings.belongsToCompany("documents", documentId, token.companyId)) { server.json(res, 400, { error: "Document does not belong to this company" }); return; } - const linkId = db.linkDocumentToTask(task.id, documentId, task.companyId, key); + const linkId = db.documentsMod.linkDocumentToTask(task.id, documentId, task.companyId, key); onDomainChanged(); server.json(res, 200, { id: linkId, key }); } catch (error) { @@ -332,7 +332,7 @@ export function registerFileRoutes(server: AgentApiServer) { const task = db.entityCrud.getTask(requireParam(params, "issueId")); if (task.companyId !== token.companyId) { server.json(res, 404, { error: "Task not found" }); return; } const key = String(params.key ?? "").trim().toLowerCase(); - db.unlinkDocumentFromTask(task.id, key, task.companyId); + db.documentsMod.unlinkDocumentFromTask(task.id, key, task.companyId); onDomainChanged(); server.json(res, 200, { unlinked: true }); } catch (error) { diff --git a/src/main/api-server/routes-meetings.ts b/src/main/api-server/routes-meetings.ts index a48ed553..5a45aeb1 100644 --- a/src/main/api-server/routes-meetings.ts +++ b/src/main/api-server/routes-meetings.ts @@ -33,7 +33,7 @@ export function registerMeetingRoutes(server: AgentApiServer) { try { const parsedParticipants = JSON.parse(input.participantAgentIds) as string[]; for (const agentId of parsedParticipants) { - if (typeof agentId !== "string" || !db.belongsToCompany("agents", agentId, token.companyId)) { + if (typeof agentId !== "string" || !db.settings.belongsToCompany("agents", agentId, token.companyId)) { server.json(res, 400, { error: "Meeting participants must belong to the current company" }); return; } @@ -43,7 +43,7 @@ export function registerMeetingRoutes(server: AgentApiServer) { server.json(res, 400, { error: "participantAgentIds must be a JSON array of agent ids" }); return; } - const id = db.saveMeeting({ + const id = db.knowledgeMod.saveMeeting({ companyId: token.companyId, type: input.type, title: input.title, @@ -63,13 +63,13 @@ export function registerMeetingRoutes(server: AgentApiServer) { server.json(res, 403, { error: "Company mismatch" }); return; } - const meetings = db.listMeetings(token.companyId); + const meetings = db.knowledgeMod.listMeetings(token.companyId); server.json(res, 200, meetings); }); server.addRoute("PATCH", "/api/meetings/:meetingId", (_req, res, params, token, body) => { const meetingId = requireParam(params, "meetingId"); - const existing = db.getMeeting(meetingId, token.companyId); + const existing = db.knowledgeMod.getMeeting(meetingId, token.companyId); if (!existing) { server.json(res, 404, { error: "Meeting not found" }); return; @@ -102,13 +102,13 @@ export function registerMeetingRoutes(server: AgentApiServer) { return; } for (const item of actionItems) { - if (item.assigneeAgentId && !db.belongsToCompany("agents", item.assigneeAgentId, token.companyId)) { + if (item.assigneeAgentId && !db.settings.belongsToCompany("agents", item.assigneeAgentId, token.companyId)) { server.json(res, 400, { error: "Meeting action item assignees must belong to the current company" }); return; } } } - db.saveMeeting({ + db.knowledgeMod.saveMeeting({ id: meetingId, companyId: token.companyId, type: data.type ?? "department_sync", diff --git a/src/main/api-server/routes-messages.ts b/src/main/api-server/routes-messages.ts index 65485fcd..af00dce2 100644 --- a/src/main/api-server/routes-messages.ts +++ b/src/main/api-server/routes-messages.ts @@ -26,7 +26,7 @@ export function registerMessageRoutes(server: AgentApiServer) { return; } const agent = db.entityCrud.getAgent(token.agentId); - const comment = db.addComment({ + const comment = db.activityMod.addComment({ companyId: task.companyId, taskId: task.id, authorAgentId: token.agentId, @@ -78,7 +78,7 @@ export function registerMessageRoutes(server: AgentApiServer) { server.json(res, 404, { error: "Task not found" }); return; } - const comments = db.listComments(requireParam(params, "issueId")); + const comments = db.activityMod.listComments(requireParam(params, "issueId")); server.json(res, 200, comments); }); diff --git a/src/main/api-server/routes-misc.ts b/src/main/api-server/routes-misc.ts index 69ec6627..59b3672f 100644 --- a/src/main/api-server/routes-misc.ts +++ b/src/main/api-server/routes-misc.ts @@ -34,7 +34,7 @@ export function registerMiscRoutes(server: AgentApiServer) { const input = parsed.data; - const account = db.getSocialAccount(input.socialAccountId, companyId); + const account = db.social.getSocialAccount(input.socialAccountId, companyId); if (!account) { server.json(res, 404, { error: "Social account not found" }); return; @@ -51,7 +51,7 @@ export function registerMiscRoutes(server: AgentApiServer) { impactSummary: `Will ${input.actionType} on ${account.displayName || account.accountName}'s ${account.platform} account. Content will be publicly visible.`, }); - const actionId = db.saveBrowserAction({ + const actionId = db.social.saveBrowserAction({ companyId, socialAccountId: input.socialAccountId, agentId: token.agentId, @@ -61,7 +61,7 @@ export function registerMiscRoutes(server: AgentApiServer) { approvalId, }); - db.addActivity({ + db.activityMod.addActivity({ companyId, actor: token.agentId, action: "browser.action_pending_approval", @@ -75,7 +75,7 @@ export function registerMiscRoutes(server: AgentApiServer) { return; } - const actionId = db.saveBrowserAction({ + const actionId = db.social.saveBrowserAction({ companyId, socialAccountId: input.socialAccountId, agentId: token.agentId, @@ -84,7 +84,7 @@ export function registerMiscRoutes(server: AgentApiServer) { status: "queued", }); - db.addActivity({ + db.activityMod.addActivity({ companyId, actor: token.agentId, action: "browser.action_queued", @@ -108,7 +108,7 @@ export function registerMiscRoutes(server: AgentApiServer) { server.json(res, 403, { error: "Company mismatch" }); return; } - const accounts = db.listSocialAccounts(requireParam(params, "companyId")); + const accounts = db.social.listSocialAccounts(requireParam(params, "companyId")); const sanitized = accounts.map(a => ({ id: a.id, platform: a.platform, @@ -134,7 +134,7 @@ export function registerMiscRoutes(server: AgentApiServer) { return; } - const account = db.getSocialAccount(parsed.data.socialAccountId, companyId); + const account = db.social.getSocialAccount(parsed.data.socialAccountId, companyId); if (!account) { server.json(res, 404, { error: "Social account not found" }); return; @@ -171,7 +171,7 @@ export function registerMiscRoutes(server: AgentApiServer) { const status = url.searchParams.get("status") ?? undefined; const limit = Number(url.searchParams.get("limit") || 50); - const actions = db.listBrowserActions(requireParam(params, "companyId"), { socialAccountId, status, limit }); + const actions = db.social.listBrowserActions(requireParam(params, "companyId"), { socialAccountId, status, limit }); server.json(res, 200, actions); }); @@ -198,7 +198,7 @@ export function registerMiscRoutes(server: AgentApiServer) { } const input = parsed.data; - const id = db.saveKnowledgeEntry({ + const id = db.knowledgeMod.saveKnowledgeEntry({ companyId: token.companyId, category: input.category, topic: input.topic, @@ -219,7 +219,7 @@ export function registerMiscRoutes(server: AgentApiServer) { server.json(res, 403, { error: "Company mismatch" }); return; } - const entries = db.listKnowledgeEntries(token.companyId); + const entries = db.knowledgeMod.listKnowledgeEntries(token.companyId); server.json(res, 200, entries); }); @@ -231,7 +231,7 @@ export function registerMiscRoutes(server: AgentApiServer) { server.json(res, 403, { error: "Company mismatch" }); return; } - const skills = db.listCompanySkills(companyId); + const skills = db.skills.listCompanySkills(companyId); server.json(res, 200, skills.map((s) => ({ id: s.id, name: s.name, @@ -244,7 +244,7 @@ export function registerMiscRoutes(server: AgentApiServer) { }); server.addRoute("GET", "/api/skills/:skillId", (_req, res, params, token) => { - const skill = db.getCompanySkill(token.companyId, requireParam(params, "skillId")); + const skill = db.skills.getCompanySkill(token.companyId, requireParam(params, "skillId")); if (!skill) { server.json(res, 404, { error: "Skill not found" }); return; diff --git a/src/main/api-server/routes-tasks.ts b/src/main/api-server/routes-tasks.ts index 4be995b0..8dcd02d6 100644 --- a/src/main/api-server/routes-tasks.ts +++ b/src/main/api-server/routes-tasks.ts @@ -127,7 +127,7 @@ export function registerTaskRoutes(server: AgentApiServer) { // Deliverable review gate: intercept agent trying to advance a flagged task to "done". // This MUST succeed — silently skipping review would let unreviewed work advance. if (update.status === "done" && task.requiresUserReview) { - const docs = db.listImportantDocsForReview({ + const docs = db.documentsMod.listImportantDocsForReview({ companyId: task.companyId, types: IMPORTANT_DOC_TYPES, authorAgentId: task.assigneeAgentId ?? null, @@ -152,7 +152,7 @@ export function registerTaskRoutes(server: AgentApiServer) { 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.activityMod.addComment({ companyId: task.companyId, taskId: task.id, authorAgentId: null, authorName: "System", body: "Deliverable submitted for board review." }); update.status = "in_review"; } @@ -189,7 +189,7 @@ export function registerTaskRoutes(server: AgentApiServer) { if (update.comment) { const agent = db.entityCrud.getAgent(token.agentId); - db.addComment({ + db.activityMod.addComment({ companyId: task.companyId, taskId: task.id, authorAgentId: token.agentId, @@ -198,7 +198,7 @@ export function registerTaskRoutes(server: AgentApiServer) { }); } - db.addActivity({ + db.activityMod.addActivity({ companyId: task.companyId, actor: token.agentId, action: `task.${update.status ? "status_changed" : "updated"}`, @@ -357,7 +357,7 @@ export function registerTaskRoutes(server: AgentApiServer) { db.tasks.setTaskType(id, inferredType); } - db.addActivity({ + db.activityMod.addActivity({ companyId, actor: token.agentId, action: "task.created", diff --git a/src/main/api-server/routes-work-graph.ts b/src/main/api-server/routes-work-graph.ts index b20dcb67..a976b820 100644 --- a/src/main/api-server/routes-work-graph.ts +++ b/src/main/api-server/routes-work-graph.ts @@ -35,7 +35,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) { return; } - const id = db.saveProject({ + const id = db.entityCrud.saveProject({ companyId, name: input.name, description: input.description ?? "", @@ -45,7 +45,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) { targetDate: input.targetDate ?? null, }); - db.addActivity({ + db.activityMod.addActivity({ companyId, actor: token.agentId, action: "project.created", @@ -79,7 +79,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) { return; } - const existing = db.getProjectForUpdate(projectId, token.companyId); + const existing = db.entityCrud.getProjectForUpdate(projectId, token.companyId); if (!existing) { server.json(res, 404, { error: "Project not found" }); return; @@ -91,7 +91,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) { } try { - db.saveProject({ + db.entityCrud.saveProject({ id: projectId, companyId: token.companyId, name: parsed.data.name ?? existing.name, @@ -135,8 +135,8 @@ export function registerWorkGraphRoutes(server: AgentApiServer) { return; } - const id = db.saveGoal(input); - db.addActivity({ + const id = db.entityCrud.saveGoal(input); + db.activityMod.addActivity({ companyId, actor: token.agentId, action: "goal.created", @@ -167,7 +167,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) { return; } - const existing = db.getGoalForUpdate(goalId, token.companyId); + const existing = db.entityCrud.getGoalForUpdate(goalId, token.companyId); if (!existing) { server.json(res, 404, { error: "Goal not found" }); return; @@ -179,7 +179,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) { } try { - db.saveGoal({ + db.entityCrud.saveGoal({ id: goalId, companyId: token.companyId, title: parsed.data.title ?? existing.title, @@ -200,7 +200,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) { server.json(res, 403, { error: "Company mismatch" }); return; } - const goals = db.listGoals(params.companyId); + const goals = db.entityCrud.listGoals(params.companyId); server.json(res, 200, goals); }); @@ -209,7 +209,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) { server.json(res, 403, { error: "Company mismatch" }); return; } - const projects = db.listProjects(params.companyId); + const projects = db.entityCrud.listProjects(params.companyId); server.json(res, 200, projects); }); @@ -219,7 +219,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) { server.json(res, 403, { error: "Company mismatch" }); return; } - const sprints = db.listSprints(companyId); + const sprints = db.knowledgeMod.listSprints(companyId); server.json(res, 200, sprints); }); @@ -231,7 +231,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) { } const data = body as Record; try { - const id = db.saveSprint({ + const id = db.knowledgeMod.saveSprint({ companyId, name: String(data.name ?? "Sprint"), goal: String(data.goal ?? ""), diff --git a/src/main/api-server/server-core.ts b/src/main/api-server/server-core.ts index 8571217a..ccd47426 100644 --- a/src/main/api-server/server-core.ts +++ b/src/main/api-server/server-core.ts @@ -349,7 +349,7 @@ export class AgentApiServer { companyId: string, refs: Array<{ id: string | null | undefined; table: "agents" | "goals" | "projects" | "tasks" | "workspaces" | "approvals"; label: string }>, ) { - const invalid = refs.find((ref) => ref.id && !this._db.belongsToCompany(ref.table, ref.id, companyId)); + const invalid = refs.find((ref) => ref.id && !this._db.settings.belongsToCompany(ref.table, ref.id, companyId)); if (!invalid) return false; this.json(res, 422, { error: `${invalid.label} does not belong to the current company` }); return true; diff --git a/src/main/approval-policy.ts b/src/main/approval-policy.ts index 94584cad..f9a02433 100644 --- a/src/main/approval-policy.ts +++ b/src/main/approval-policy.ts @@ -26,7 +26,7 @@ export function resolveApprovalDecision({ state, decisionNote: note, }); - db.addActivity({ + db.activityMod.addActivity({ companyId, actor, action: `approval.${state}`, diff --git a/src/main/automation/automation-actions.ts b/src/main/automation/automation-actions.ts index c9169641..b8aa7661 100644 --- a/src/main/automation/automation-actions.ts +++ b/src/main/automation/automation-actions.ts @@ -81,7 +81,7 @@ export function executeAutomationAction( wakeAgentIfPossible(assignedId, ctx.companyId, "assignment"); } } else if (ctx.agentId) { - const snapshot = db.listSnapshot(); + const snapshot = db.snap.listSnapshot(); const unassigned = snapshot.tasks .filter((task) => task.companyId === ctx.companyId && !task.assigneeAgentId && ["backlog", "todo"].includes(task.status)) .sort((a, b) => { @@ -310,7 +310,7 @@ export function executeAutomationAction( // Default to +60min only when the rule specifies no time at all. const offsetMinutes = cfg.scheduleOffsetMinutes ?? 60; const scheduledAt = cfg.scheduledAt ?? new Date(Date.now() + offsetMinutes * 60_000).toISOString(); - db.saveMeeting({ + db.knowledgeMod.saveMeeting({ companyId: ctx.companyId, type: meetingType, title, @@ -324,7 +324,7 @@ export function executeAutomationAction( case "create_document": { const documentType = (config.documentType as string) ?? "status_report"; const title = (config.title as string) ?? "Auto-generated document"; - db.saveDocument({ + db.documentsMod.saveDocument({ companyId: ctx.companyId, type: documentType, title, @@ -335,7 +335,7 @@ export function executeAutomationAction( break; } case "auto_approve": { - const snapshot = db.listSnapshot(); + const snapshot = db.snap.listSnapshot(); const pendingApprovals = snapshot.approvals.filter( (approval) => approval.companyId === ctx.companyId && approval.state === "pending", ); diff --git a/src/main/automation/automation-approval-handler.ts b/src/main/automation/automation-approval-handler.ts index f0408b9c..b6849470 100644 --- a/src/main/automation/automation-approval-handler.ts +++ b/src/main/automation/automation-approval-handler.ts @@ -39,7 +39,7 @@ function recordReviewFeedbackKnowledge( "Use this review guidance to improve future deliverables for similar work.", ].join("\n"); - db.saveKnowledgeEntry({ + db.knowledgeMod.saveKnowledgeEntry({ companyId: approval.companyId, category: "lesson_learned", topic, @@ -84,7 +84,7 @@ export function handleApprovalResolved( const { db, logger, publishDomainChanged, notify, wakeAgentIfPossible, dispatchBrowserAction } = deps; const { dispatchAutomation, handleTaskStatusChange, runWorkflowPipeline, trackTimer } = callbacks; - const snapshot = db.listSnapshot(); + const snapshot = db.snap.listSnapshot(); const approval = snapshot.approvals.find((entry) => entry.id === approvalId); if (!approval) return; @@ -111,7 +111,7 @@ export function handleApprovalResolved( department: agent.department, }); if (assignedSkillIds.length > 0) { - db.addActivity({ + db.activityMod.addActivity({ companyId, actor: "system", action: "agent.skills_defaulted", @@ -121,7 +121,7 @@ export function handleApprovalResolved( }); } - db.addActivity({ + db.activityMod.addActivity({ companyId, actor: "system", action: "agent.hired", @@ -142,7 +142,7 @@ export function handleApprovalResolved( } publishDomainChanged(); - const hireLocale = db.listSnapshot().locale; + const hireLocale = db.snap.listSnapshot().locale; notify({ title: localizeNotification(hireLocale, "Agent Hired", "新员工入职"), body: localizeNotification(hireLocale, `${agent.name} joined as ${agent.role}`, `${agent.name} 已加入,职位:${agent.role}`), @@ -170,7 +170,7 @@ export function handleApprovalResolved( ? "draft" : "in_review"; for (const docId of documentIds) { - db.updateDocumentStatus(docId, companyId, docStatus); + db.documentsMod.updateDocumentStatus(docId, companyId, docStatus); } if (approval.relatedTaskId) { @@ -185,7 +185,7 @@ export function handleApprovalResolved( if (decision === "approved") { db.tasks.advanceTaskStatus(task.id, "done", "Board approved deliverable."); handleTaskStatusChange(task.id, companyId, task.status, "done"); - db.addActivity({ companyId, actor: "board", action: "deliverable.approved", entityType: "task", entityId: task.id, detail: approval.decisionNote || "Deliverable approved by board." }); + db.activityMod.addActivity({ companyId, actor: "board", action: "deliverable.approved", entityType: "task", entityId: task.id, detail: approval.decisionNote || "Deliverable approved by board." }); } else if (decision === "rejected") { recordReviewFeedbackKnowledge(db, approval, decision); db.tasks.advanceTaskStatus(task.id, "in_progress", `Board rejected deliverable: ${approval.decisionNote || "No reason given."}`); @@ -219,7 +219,7 @@ export function handleApprovalResolved( const documentIds = extractApprovalDocumentIds(approval.payloadJson); const docStatus = decision === "approved" ? "approved" : "draft"; for (const docId of documentIds) { - db.updateDocumentStatus(docId, companyId, docStatus); + db.documentsMod.updateDocumentStatus(docId, companyId, docStatus); } publishDomainChanged(); } @@ -235,8 +235,8 @@ export function handleApprovalResolved( }; if (payload.policyId) { // Resolve the budget incident that triggered this approval - db.resolveBudgetIncident(payload.policyId, companyId); - db.addActivity({ + db.budget.resolveBudgetIncident(payload.policyId, companyId); + db.activityMod.addActivity({ companyId, actor: "board", action: "budget.override_approved", @@ -250,10 +250,10 @@ export function handleApprovalResolved( } } - const browserAction = db.getBrowserActionByApprovalId(approvalId, companyId); + const browserAction = db.social.getBrowserActionByApprovalId(approvalId, companyId); if (browserAction) { if (decision === "approved") { - db.updateBrowserAction(browserAction.id, { + db.social.updateBrowserAction(browserAction.id, { status: "queued", resultSummary: "Approval granted. Dispatching browser action.", errorMessage: null, @@ -263,7 +263,7 @@ export function handleApprovalResolved( logger.error(`[browser] Failed to dispatch approved browser action ${browserAction.id}: ${error instanceof Error ? error.message : String(error)}`); }); } else if (decision === "rejected") { - db.updateBrowserAction(browserAction.id, { + db.social.updateBrowserAction(browserAction.id, { status: "cancelled", resultSummary: "Browser action was rejected during approval.", errorMessage: "Approval rejected by board.", @@ -271,7 +271,7 @@ export function handleApprovalResolved( }); publishDomainChanged(); } else if (decision === "revision_requested") { - db.updateBrowserAction(browserAction.id, { + db.social.updateBrowserAction(browserAction.id, { status: "approval_required", resultSummary: "Board requested revisions before execution.", errorMessage: "Approval requires revisions before the browser action can run.", @@ -291,7 +291,7 @@ export function handleApprovalResolved( }; const dispatchSocialAction = () => { - const actionId = db.saveBrowserAction({ + const actionId = db.social.saveBrowserAction({ companyId: approval.companyId, socialAccountId: draft.socialAccountId, agentId: approval.requestedByAgentId ?? "", @@ -333,14 +333,14 @@ export function handleApprovalResolved( const payload = workflowApprovalPayloadSchema.safeParse(JSON.parse(approval.payloadJson)); if (payload.success && payload.data.workflowId && payload.data.stepId) { const { workflowId, stepId } = payload.data; - const workflow = db.getWorkflowRecord(workflowId, companyId); + const workflow = db.automationMod.getWorkflowRecord(workflowId, companyId); if (workflow) { const steps = normalizeWorkflowSteps(JSON.parse(workflow.stepsJson || "[]")); const stepIndex = steps.findIndex((step) => step.id === stepId); if (stepIndex >= 0) { if (decision === "approved") { steps[stepIndex]!.status = "pending"; - db.updateWorkflowExecution({ + db.automationMod.updateWorkflowExecution({ id: workflow.id, companyId, status: "paused", @@ -354,7 +354,7 @@ export function handleApprovalResolved( }); } else { steps[stepIndex]!.status = decision === "revision_requested" ? "pending" : "failed"; - db.updateWorkflowExecution({ + db.automationMod.updateWorkflowExecution({ id: workflow.id, companyId, status: decision === "revision_requested" ? "paused" : "failed", diff --git a/src/main/automation/automation-handlers.ts b/src/main/automation/automation-handlers.ts index 6adae39c..51abea72 100644 --- a/src/main/automation/automation-handlers.ts +++ b/src/main/automation/automation-handlers.ts @@ -50,7 +50,7 @@ export function handleTaskStatusChange( } if (!task) return; - db.addActivity({ + db.activityMod.addActivity({ companyId, actor: task.assigneeAgentId ?? "board", action: "task.status_changed", diff --git a/src/main/automation/automation-run-completion.ts b/src/main/automation/automation-run-completion.ts index e9680a6e..207939a6 100644 --- a/src/main/automation/automation-run-completion.ts +++ b/src/main/automation/automation-run-completion.ts @@ -151,7 +151,7 @@ function checkRunProducedDeliverable( taskId: string, taskType: string, ): boolean { - const signals = db.collectRunDeliverableSignals(runId, companyId, taskId); + const signals = db.knowledgeMod.collectRunDeliverableSignals(runId, companyId, taskId); return hasValidDeliverableFor(taskType, signals); } @@ -274,7 +274,7 @@ function advanceToInReview( } db.tasks.advanceTaskStatus(taskId, "in_review", statusMessage); callbacks.handleTaskStatusChange(taskId, run.companyId, "in_progress", "in_review"); - const company = db.listSnapshot().companies.find((c) => c.id === run.companyId); + const company = db.snap.listSnapshot().companies.find((c) => c.id === run.companyId); if (company?.reviewDeliverables) { const importantDocs = findImportantDocsForRun(db, run.id, run.companyId); if (importantDocs.length > 0) { @@ -462,7 +462,7 @@ export function processRunCompletion( }); } } - const budgetLocale = db.listSnapshot().locale; + const budgetLocale = db.snap.listSnapshot().locale; checkBudgetThreshold( agent, notify, diff --git a/src/main/automation/automation-workflow.ts b/src/main/automation/automation-workflow.ts index 8b6d6546..2dc528c8 100644 --- a/src/main/automation/automation-workflow.ts +++ b/src/main/automation/automation-workflow.ts @@ -73,7 +73,7 @@ export function resolveWorkflowStepActor( } } - const snapshot = db.listSnapshot(); + const snapshot = db.snap.listSnapshot(); return snapshot.agents.find((agent) => agent.companyId === companyId && !agent.reportsTo && agent.status !== "terminated") ?? null; } @@ -112,7 +112,7 @@ export async function runWorkflowPipeline( ): Promise { const { db, publishDomainChanged } = deps; - const workflow = db.getWorkflowRecord(workflowId, companyId); + const workflow = db.automationMod.getWorkflowRecord(workflowId, companyId); if (!workflow) { throw new Error("Workflow not found."); } @@ -132,7 +132,7 @@ export async function runWorkflowPipeline( return step; }); - db.updateWorkflowExecution({ + db.automationMod.updateWorkflowExecution({ id: workflowId, companyId, status: "active", @@ -146,7 +146,7 @@ export async function runWorkflowPipeline( for (let index = resumeFrom; index < steps.length; index += 1) { const step = steps[index]!; step.status = "running"; - db.updateWorkflowExecution({ + db.automationMod.updateWorkflowExecution({ id: workflowId, companyId, status: "active", @@ -170,7 +170,7 @@ export async function runWorkflowPipeline( payloadJson: JSON.stringify({ workflowId, stepId: step.id, stepName: step.name }), }); step.status = "pending"; - db.updateWorkflowExecution({ + db.automationMod.updateWorkflowExecution({ id: workflowId, companyId, status: "paused", @@ -178,7 +178,7 @@ export async function runWorkflowPipeline( stepsJson: JSON.stringify(steps), lastRunAt: new Date().toISOString(), }); - db.addActivity({ + db.activityMod.addActivity({ companyId, actor: actor?.id ?? "system", action: "workflow.paused_for_approval", @@ -203,7 +203,7 @@ export async function runWorkflowPipeline( ctx, ); step.status = "completed"; - db.addActivity({ + db.activityMod.addActivity({ companyId, actor: actor?.id ?? "system", action: "workflow.step_completed", @@ -213,7 +213,7 @@ export async function runWorkflowPipeline( }); const isLastStep = index === steps.length - 1; const completionStatus = isLastStep ? "completed" : "active"; - db.updateWorkflowExecution({ + db.automationMod.updateWorkflowExecution({ id: workflowId, companyId, status: completionStatus, @@ -224,7 +224,7 @@ export async function runWorkflowPipeline( publishDomainChanged(); } catch (error) { step.status = "failed"; - db.updateWorkflowExecution({ + db.automationMod.updateWorkflowExecution({ id: workflowId, companyId, status: "failed", @@ -232,7 +232,7 @@ export async function runWorkflowPipeline( stepsJson: JSON.stringify(steps), lastRunAt: new Date().toISOString(), }); - db.addActivity({ + db.activityMod.addActivity({ companyId, actor: actor?.id ?? "system", action: "workflow.step_failed", diff --git a/src/main/automation/index.ts b/src/main/automation/index.ts index 69dd98d0..113bdaea 100644 --- a/src/main/automation/index.ts +++ b/src/main/automation/index.ts @@ -107,7 +107,7 @@ export function createAutomationService({ function evaluateAutomationRules(trigger: string, ctx: AutomationEventContext) { try { - const rules = db.listActiveAutomationRules(ctx.companyId, trigger); + const rules = db.automationMod.listActiveAutomationRules(ctx.companyId, trigger); for (const rule of rules) { if (!matchesConditions(db, rule, ctx, logger)) continue; const executionKey = JSON.stringify({ @@ -127,7 +127,7 @@ export function createAutomationService({ activeAutomationExecutions.add(executionKey); try { executeAutomationAction(actionDeps, actionCallbacks, rule, ctx); - db.recordAutomationExecution( + db.automationMod.recordAutomationExecution( rule.id, ctx.companyId, rule.name, @@ -145,7 +145,7 @@ export function createAutomationService({ logger.info(`[automation] Fired rule "${rule.name}" (${trigger} → ${rule.action})`); } catch (error) { logger.error(`[automation] Failed to execute action for rule "${rule.name}": ${error instanceof Error ? error.message : String(error)}`); - db.recordAutomationExecution( + db.automationMod.recordAutomationExecution( rule.id, ctx.companyId, rule.name, @@ -164,7 +164,7 @@ export function createAutomationService({ function evaluateWorkflowTriggers(trigger: string, ctx: AutomationEventContext) { try { - const workflows = db.listTriggerableWorkflows(ctx.companyId, trigger); + const workflows = db.automationMod.listTriggerableWorkflows(ctx.companyId, trigger); for (const workflow of workflows) { if (!matchesEventConfig(db, workflow.triggerConfigJson, null, ctx, logger)) continue; const executionKey = JSON.stringify({ diff --git a/src/main/bootstrap/prepare-services.ts b/src/main/bootstrap/prepare-services.ts index 3058bfef..a26362c2 100644 --- a/src/main/bootstrap/prepare-services.ts +++ b/src/main/bootstrap/prepare-services.ts @@ -121,9 +121,9 @@ export function prepareServices(deps: PrepareServicesDeps): ServiceGraphHandles logger: deps.logger, getMainWindow: () => serviceGraph?.getDesktopShellService()?.getMainWindow() ?? null, getDb: () => ({ - hasCompany: (companyId: string) => deps.db.hasCompany(companyId), + hasCompany: (companyId: string) => deps.db.settings.hasCompany(companyId), belongsToCompany: (table: string, id: string, companyId: string) => - deps.db.belongsToCompany(table as Parameters[0], id, companyId), + deps.db.settings.belongsToCompany(table as Parameters[0], id, companyId), }), notificationService, }); diff --git a/src/main/bootstrap/recover-interrupted-tasks.ts b/src/main/bootstrap/recover-interrupted-tasks.ts index bde89517..10c08da7 100644 --- a/src/main/bootstrap/recover-interrupted-tasks.ts +++ b/src/main/bootstrap/recover-interrupted-tasks.ts @@ -39,7 +39,7 @@ export function emitCrashRecoveryNotification(deps: RecoverInterruptedTasksDeps) */ export function recoverShutdownInterruptedTasks(deps: RecoverInterruptedTasksDeps): void { try { - const snapshot = deps.db.listSnapshot(); + const snapshot = deps.db.snap.listSnapshot(); const shutdownRuns = snapshot.runs.filter( (r) => r.status === "interrupted" && (r.summary === "Application shut down." || r.summary === "Worker process crashed."), diff --git a/src/main/bootstrap/recover-scheduled-posts.ts b/src/main/bootstrap/recover-scheduled-posts.ts index e193d7ef..cd983af7 100644 --- a/src/main/bootstrap/recover-scheduled-posts.ts +++ b/src/main/bootstrap/recover-scheduled-posts.ts @@ -15,9 +15,9 @@ export interface RecoverScheduledPostsDeps { export function recoverScheduledPosts(deps: RecoverScheduledPostsDeps): Set> { const timers = new Set>(); try { - const snapshot = deps.db.listSnapshot(); + const snapshot = deps.db.snap.listSnapshot(); for (const company of snapshot.companies) { - const queuedActions = deps.db.listBrowserActions(company.id, { status: "queued" }); + const queuedActions = deps.db.social.listBrowserActions(company.id, { status: "queued" }); for (const action of queuedActions) { if (!action.payloadJson) continue; try { diff --git a/src/main/bootstrap/run-startup-health-checks.ts b/src/main/bootstrap/run-startup-health-checks.ts index 6a63e28e..918ea487 100644 --- a/src/main/bootstrap/run-startup-health-checks.ts +++ b/src/main/bootstrap/run-startup-health-checks.ts @@ -14,7 +14,7 @@ export async function runStartupHealthChecks( logger: Logger, ): Promise { if (!connectorHealth) return; - const connectors = db.listSnapshot().connectors; + const connectors = db.snap.listSnapshot().connectors; const results = await Promise.allSettled( connectors.map((connector) => connectorHealth.checkConnector(connector.id)), ); diff --git a/src/main/browser-action-service.ts b/src/main/browser-action-service.ts index 9dc03253..f046ac9c 100644 --- a/src/main/browser-action-service.ts +++ b/src/main/browser-action-service.ts @@ -43,7 +43,7 @@ export function createBrowserActionService({ } async function dispatchBrowserAction(actionId: string, companyId: string) { - const action = db.getBrowserAction(actionId, companyId); + const action = db.social.getBrowserAction(actionId, companyId); if (!action) { throw new Error(`Browser action not found: ${actionId}`); } @@ -56,7 +56,7 @@ export function createBrowserActionService({ throw new Error(`Browser action approval ${action.approvalId} does not belong to company ${companyId}.`); } if (approval.state !== "approved") { - db.updateBrowserAction(actionId, { + db.social.updateBrowserAction(actionId, { status: "approval_required", resultSummary: "Browser action is still waiting for approval.", errorMessage: "Approval must be granted before execution.", @@ -66,9 +66,9 @@ export function createBrowserActionService({ } } - const account = db.getSocialAccount(action.socialAccountId, companyId); + const account = db.social.getSocialAccount(action.socialAccountId, companyId); if (!account) { - db.updateBrowserAction(actionId, { + db.social.updateBrowserAction(actionId, { status: "failed", errorMessage: "Social account no longer exists.", finishedAt: new Date().toISOString(), @@ -88,7 +88,7 @@ export function createBrowserActionService({ context: { actionId }, }); log.warn(`[${classified.code}]`, classified.message, classified.context); - db.updateBrowserAction(actionId, { + db.social.updateBrowserAction(actionId, { status: "failed", errorMessage: "Browser action payload is not valid JSON.", finishedAt: new Date().toISOString(), @@ -97,7 +97,7 @@ export function createBrowserActionService({ return; } - db.updateBrowserAction(actionId, { + db.social.updateBrowserAction(actionId, { status: "running", startedAt: new Date().toISOString(), errorMessage: null, @@ -113,7 +113,7 @@ export function createBrowserActionService({ payload, }); - db.updateBrowserAction(actionId, { + db.social.updateBrowserAction(actionId, { status: result.cancelled ? "cancelled" : result.success ? "succeeded" : "failed", resultSummary: result.summary, screenshotPath: result.screenshotPath, @@ -121,7 +121,7 @@ export function createBrowserActionService({ finishedAt: new Date().toISOString(), }); } catch (error) { - db.updateBrowserAction(actionId, { + db.social.updateBrowserAction(actionId, { status: "failed", errorMessage: error instanceof Error ? error.message : String(error), finishedAt: new Date().toISOString(), diff --git a/src/main/budget-enforcement-service.ts b/src/main/budget-enforcement-service.ts index e131ad28..7f601017 100644 --- a/src/main/budget-enforcement-service.ts +++ b/src/main/budget-enforcement-service.ts @@ -68,7 +68,7 @@ export function createBudgetEnforcementService({ agentId: string, projectId: string | null, ): BudgetGateResult { - const policies = db.getActivePoliciesForRun(companyId, agentId, projectId); + const policies = db.budget.getActivePoliciesForRun(companyId, agentId, projectId); if (policies.length === 0) { return { allowed: true, blockedBy: [] }; } @@ -76,7 +76,7 @@ export function createBudgetEnforcementService({ const blockedBy: BudgetGateResult["blockedBy"] = []; for (const policy of policies) { if (!policy.hardStopEnabled) continue; - const spent = db.getSpentInWindow(policy); + const spent = db.budget.getSpentInWindow(policy); if (spent >= policy.amount) { blockedBy.push({ policyId: policy.id, @@ -103,19 +103,19 @@ export function createBudgetEnforcementService({ agentId: string, projectId: string | null, ): void { - const policies = db.getActivePoliciesForRun(companyId, agentId, projectId); + const policies = db.budget.getActivePoliciesForRun(companyId, agentId, projectId); for (const policy of policies) { // amount <= 0 disables the policy (and would otherwise divide by zero below). if (policy.amount <= 0) continue; - const spent = db.getSpentInWindow(policy); + const spent = db.budget.getSpentInWindow(policy); const utilization = spent / policy.amount; const warnThreshold = policy.warnPercent / 100; const scopeLabel = getScopeLabel(db, policy, logger); if (utilization >= warnThreshold) { - db.upsertBudgetIncident(policy, "warn_threshold", spent); + db.budget.upsertBudgetIncident(policy, "warn_threshold", spent); if (policy.notifyEnabled) { notify({ title: "Budget Warning", @@ -128,7 +128,7 @@ export function createBudgetEnforcementService({ } if (utilization >= 1.0 && policy.hardStopEnabled) { - db.upsertBudgetIncident(policy, "hard_stop", spent); + db.budget.upsertBudgetIncident(policy, "hard_stop", spent); if (policy.notifyEnabled) { notify({ title: "Budget Exhausted", @@ -151,22 +151,22 @@ export function createBudgetEnforcementService({ * Per D-06: fires auto_resolved_limit_increase status. */ function checkAutoResolve(companyId: string, policyId: string): void { - const policy = db.getBudgetPolicy(policyId); + const policy = db.budget.getBudgetPolicy(policyId); if (!policy) return; - const spent = db.getSpentInWindow(policy); + const spent = db.budget.getSpentInWindow(policy); let resolved = false; // If spend is below hard-stop threshold, resolve all open incidents if (spent < policy.amount) { - const count = db.resolveIncidentsForPolicy(policyId, "auto_resolved_limit_increase"); + const count = db.budget.resolveIncidentsForPolicy(policyId, "auto_resolved_limit_increase"); if (count > 0) resolved = true; } // If spend is below warn threshold, resolve warn incidents specifically const warnAmount = Math.floor((policy.amount * policy.warnPercent) / 100); if (spent < warnAmount) { - const count = db.resolveWarnIncidentsForPolicy(policyId, "auto_resolved_limit_increase"); + const count = db.budget.resolveWarnIncidentsForPolicy(policyId, "auto_resolved_limit_increase"); if (count > 0) resolved = true; } @@ -181,7 +181,7 @@ export function createBudgetEnforcementService({ * Per D-06: window rollover cleanup. */ function autoCloseExpiredIncidents(companyId: string): number { - const count = db.autoCloseExpiredIncidents(companyId); + const count = db.budget.autoCloseExpiredIncidents(companyId); if (count > 0) { logger.info(`Budget auto-close: ${count} expired incident(s) for company ${companyId}`); publishDomainChanged(); @@ -193,12 +193,12 @@ export function createBudgetEnforcementService({ * Returns a summary of all budget policies and their current spend status. */ function getBudgetStatus(companyId: string): BudgetStatusSummary { - const policies = db.listBudgetPolicies(companyId); - const openIncidents = db.listBudgetIncidents(companyId, "open"); + const policies = db.budget.listBudgetPolicies(companyId); + const openIncidents = db.budget.listBudgetIncidents(companyId, "open"); return { policies: policies.map((policy) => { - const spent = db.getSpentInWindow(policy); + const spent = db.budget.getSpentInWindow(policy); const hasOpenIncident = openIncidents.some((i) => i.policyId === policy.id); return { policyId: policy.id, diff --git a/src/main/connector-health-service.ts b/src/main/connector-health-service.ts index e4add2f1..072383d2 100644 --- a/src/main/connector-health-service.ts +++ b/src/main/connector-health-service.ts @@ -27,7 +27,7 @@ export function createConnectorHealthService({ credentialStore, }: ConnectorHealthDependencies) { function randomFallbackCompanyId() { - const snapshot = db.listSnapshot(); + const snapshot = db.snap.listSnapshot(); return snapshot.companies[0]?.id ?? "00000000-0000-0000-0000-000000000000"; } @@ -103,12 +103,12 @@ export function createConnectorHealthService({ } async function checkConnector(id: ConnectorId) { - const companyId = db.getCurrentCompanyId() ?? randomFallbackCompanyId(); + const companyId = db.settings.getCurrentCompanyId() ?? randomFallbackCompanyId(); - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id, status: "detected", - authState: db.getConnector(id).authState, + authState: db.connectorsCrud.getConnector(id).authState, version: null, lastError: null, }); @@ -116,14 +116,14 @@ export function createConnectorHealthService({ try { const result = evaluateCredential(id); - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id, status: result.status, authState: result.authState, version: null, lastError: result.lastError, }); - db.addActivity({ + db.activityMod.addActivity({ companyId, actor: "system", action: "connector.tested", @@ -135,7 +135,7 @@ export function createConnectorHealthService({ } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`[connector] Health check failed for ${id}: ${errorMessage}`); - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id, status: "error", authState: "unknown", diff --git a/src/main/connector-model-policy.ts b/src/main/connector-model-policy.ts index 06f55de1..a943db1c 100644 --- a/src/main/connector-model-policy.ts +++ b/src/main/connector-model-policy.ts @@ -7,6 +7,22 @@ interface ConnectorModelPolicyDeps { getDisabledModels: (companyId: string, connectorId: string) => Array<{ id: string; label: string }>; } +/** + * Adapter over AppDatabase that returns the structural shape consumed by + * `getConnectorModelPolicy` / `validateConnectorModelSelection`. Callers + * pass an `AppDatabase`-ish object; the helper unwraps the two typed + * domain modules so we do not repeat the closure literal at every site. + */ +export function connectorPolicyDepsFromDb(db: { + connectorsCrud: { getConnector: (id: ConnectorId) => { id: ConnectorId; label?: string | null; model: string | null } }; + settings: { getDisabledModels: (companyId: string, connectorId: string) => Array<{ id: string; label: string }> }; +}): ConnectorModelPolicyDeps { + return { + getConnector: (id) => db.connectorsCrud.getConnector(id), + getDisabledModels: (cid, connId) => db.settings.getDisabledModels(cid, connId), + }; +} + export interface ConnectorModelPolicyResult { connectorId: ConnectorId; connectorLabel: string; diff --git a/src/main/database-base.ts b/src/main/database-base.ts index c9538118..0a2d3201 100644 --- a/src/main/database-base.ts +++ b/src/main/database-base.ts @@ -17,6 +17,7 @@ import { buildActivityMethods } from "./database/activity"; import { buildSkillMethods } from "./database/skills"; import { buildBudgetMethods } from "./database/budget"; import { buildEntityCrudMethods } from "./database/entity-crud"; +import { listInvalidEnvBindingKeys, parseEnvBindingText } from "./connectors"; import { buildDocumentMethods } from "./database/documents"; import { buildAutomationMethods } from "./database/automation"; import { buildKnowledgeMethods } from "./database/knowledge"; @@ -119,23 +120,23 @@ export class AppDatabaseBase { this.company = buildCompanyMethods(getDb, ra, tx, { getSetting: (key) => this.getSetting(key), setSetting: (key, value) => this.setSetting(key, value), - listSnapshot: (options) => this.listSnapshot(options), - addComment: (input) => this.addComment(input), + listSnapshot: (options) => this.snap.listSnapshot(options), + addComment: (input) => this.activityMod.addComment(input), }); this.agents = buildAgentMethods(getDb, ra, tx, { getAgent: (id) => this.entityCrud.getAgent(id), wouldCreateAgentCycle: (agentId, reportsTo) => this.wouldCreateAgentCycle(agentId, reportsTo), - recordConfigRevision: (input) => this.recordConfigRevision(input), + recordConfigRevision: (input) => this.documentsMod.recordConfigRevision(input), cancelRunForDeletion: (runId, taskId, workspaceId, summary) => this.runs.cancelRunForDeletion(runId, taskId, workspaceId, summary), - addActivity: (input) => this.addActivity(input), + addActivity: (input) => this.activityMod.addActivity(input), }); this.tasks = buildTaskMethods(getDb, ra, tx, { - addComment: (input) => this.addComment(input), - addActivity: (input) => this.addActivity(input), + addComment: (input) => this.activityMod.addComment(input), + addActivity: (input) => this.activityMod.addActivity(input), }); this.messages = buildMessageMethods(getDb, ra, { getAgent: (id) => this.entityCrud.getAgent(id), - addActivity: (input) => this.addActivity(input), + addActivity: (input) => this.activityMod.addActivity(input), transaction: tx, }); this.runs = buildRunMethods(getDb, ra, tx, { logsDir: this.logsDir }); @@ -147,11 +148,14 @@ export class AppDatabaseBase { this.skills = buildSkillMethods(getDb, ra); this.budget = buildBudgetMethods(getDb, ra, tx); this.entityCrud = buildEntityCrudMethods(getDb, ra, tx, { - addActivity: (input) => this.addActivity(input), + addActivity: (input) => this.activityMod.addActivity(input), + vault: this.vault, + listInvalidEnvBindingKeys, + parseEnvBindingText, }); this.documentsMod = buildDocumentMethods(getDb, ra, tx); this.automationMod = buildAutomationMethods(getDb, ra, { - addActivity: (input) => this.addActivity(input), + addActivity: (input) => this.activityMod.addActivity(input), }); this.knowledgeMod = buildKnowledgeMethods(getDb, ra); this.wakeup = buildWakeupMethods(getDb, ra, tx); @@ -229,49 +233,10 @@ export class AppDatabaseBase { close() { this.db.close(); } - // ─── Settings ────────────────────────────────────────────────────────────── - - getCurrentCompanyId() { return this.settings.getCurrentCompanyId(); } - setCurrentCompany(companyId: string) { this.settings.setCurrentCompany(companyId); } - hasCompany(companyId: string) { return this.settings.hasCompany(companyId); } - belongsToCompany(table: "workspaces" | "agents" | "goals" | "projects" | "tasks" | "approvals" | "runs" | "secrets" | "meetings" | "documents" | "knowledge_base" | "sprints" | "automation_rules" | "workflow_pipelines", id: string, companyId: string) { return this.settings.belongsToCompany(table, id, companyId); } - updateSettings(input: { theme?: "system" | "light" | "dark"; locale?: "en" | "zh"; updatesEnabled?: boolean; logRetentionDays?: number }) { this.settings.updateSettings(input); } - getLogRetentionDays() { return this.settings.getLogRetentionDays(); } - purgeExpiredRunLogs() { return this.settings.purgeExpiredRunLogs(); } - canOpenPath(targetPath: string) { return this.settings.canOpenPath(targetPath); } - getDisabledModels(companyId: string, connectorId: string) { return this.settings.getDisabledModels(companyId, connectorId); } - setDisabledModels(companyId: string, connectorId: string, models: Array<{ id: string; label: string }>) { this.settings.setDisabledModels(companyId, connectorId, models); } - hasModelPoolSettings(companyId: string, connectorId: string) { return this.settings.hasModelPoolSettings(companyId, connectorId); } - getLastBudgetResetMonth(companyId: string) { return this.settings.getLastBudgetResetMonth(companyId); } - setLastBudgetResetMonth(companyId: string, month: string) { this.settings.setLastBudgetResetMonth(companyId, month); } - - // ─── Snapshot ────────────────────────────────────────────────────────────── - - listSnapshot(options: { companyId?: string | null } = {}): ProfileSnapshot { return this.snap.listSnapshot(options); } - listRendererSnapshot(): ProfileSnapshot { return this.snap.listRendererSnapshot(); } - - // ─── Connectors ──────────────────────────────────────────────────────────── - - saveConnector(input: { id: ConnectorId; model: string | null; envBindingText: string; notes: string }) { this.connectorsCrud.saveConnector(input); } - updateConnectorHealth(input: { id: ConnectorId; status: string; authState: string; version: string | null; lastError: string | null }) { this.connectorsCrud.updateConnectorHealth(input); } - getConnector(id: ConnectorId) { return this.connectorsCrud.getConnector(id); } - findConnector(id: ConnectorId) { return this.connectorsCrud.findConnector(id); } - - // ─── Workspaces ──────────────────────────────────────────────────────────── - - saveWorkspace(input: { id?: string | null; companyId: string; name: string; localPath: string; repoUrl: string; repoRef: string; projectId?: string | null; isPrimary: boolean }) { return this.workspacesMod.saveWorkspace(input); } - getWorkspace(id: string | null | undefined) { return this.workspacesMod.getWorkspace(id); } - getProjectPrimaryWorkspace(projectId: string | null | undefined, companyId: string) { return this.workspacesMod.getProjectPrimaryWorkspace(projectId, companyId); } - listWorkspaceLocalPaths(companyId: string) { return this.workspacesMod.listWorkspaceLocalPaths(companyId); } - getAnyCompanyWorkspace(companyId: string) { return this.workspacesMod.getAnyCompanyWorkspace(companyId); } - deleteWorkspace(id: string, companyId: string) { this.workspacesMod.deleteWorkspace(id, companyId); } - acquireWorkspaceLock(workspaceId: string, runId: string) { return this.workspacesMod.acquireWorkspaceLock(workspaceId, runId); } - claimWorkspaceLock(workspaceId: string, runId: string) { return this.workspacesMod.claimWorkspaceLock(workspaceId, runId); } - releaseWorkspaceLock(workspaceId: string, runId: string) { this.workspacesMod.releaseWorkspaceLock(workspaceId, runId); } - releaseAllWorkspaceLocks() { this.workspacesMod.releaseAllWorkspaceLocks(); } - releaseStaleWorkspaceLocks() { return this.workspacesMod.releaseStaleWorkspaceLocks(); } - isWorkspaceLocked(workspaceId: string) { return this.workspacesMod.isWorkspaceLocked(workspaceId); } - + // ─── Internal helper: relock releaseAllWorkspaceLocks is used by + // crash-recovery below; callers should prefer + // `db.workspacesMod.releaseAllWorkspaceLocks()` directly. + // // ─── Internal infrastructure ─────────────────────────────────────────────── protected _lastInterruptedCount = 0; get lastInterruptedCount(): number { return this._lastInterruptedCount; } @@ -301,7 +266,7 @@ export class AppDatabaseBase { ) .run(ts); this.db.prepare("update agents set status = 'idle', updated_at = ? where status = 'running'").run(ts); - this.releaseAllWorkspaceLocks(); + this.workspacesMod.releaseAllWorkspaceLocks(); }); return interruptedCount; } @@ -389,10 +354,4 @@ export class AppDatabaseBase { return this.transaction(operation); } - // ─── Methods used by subclass constructor callbacks ──────────────────────── - - addActivity(input: { companyId: string; actor: string; action: string; entityType: string; entityId: string; detail: string }) { this.activityMod.addActivity(input); } - addComment(input: { companyId: string; taskId: string; authorAgentId: string | null; authorName: string; body: string }) { return this.activityMod.addComment(input); } - listRecentTaskComments(taskIds: string[], sinceIso: string, limit: number) { return this.activityMod.listRecentTaskComments(taskIds, sinceIso, limit); } - recordConfigRevision(input: { companyId: string; agentId: string; changedKeys: string[]; configBefore: Record; configAfter: Record; source?: string; rolledBackFromRevisionId?: string | null; createdByAgentId?: string | null; createdByUserId?: string | null }) { return this.documentsMod.recordConfigRevision(input); } } diff --git a/src/main/database.ts b/src/main/database.ts index 9f8444df..8a589277 100644 --- a/src/main/database.ts +++ b/src/main/database.ts @@ -1,104 +1,39 @@ import type { SQLInputValue } from "node:sqlite"; import { AppDatabaseBase } from "./database-base"; -import { listInvalidEnvBindingKeys, parseEnvBindingText } from "./connectors"; -import type { ConnectorId, WorkflowPipelineRecord } from "@shared/types"; -import type { buildThreadMethods } from "./database/threads"; export { STALE_LOCK_TIMEOUT_MS } from "./database-base"; +/** + * AppDatabase — concrete persistence surface wired to the twenty-six + * domain modules declared on AppDatabaseBase. New code must call the + * owning module directly (e.g. `db.tasks.saveTask(...)`, not + * `db.saveTask(...)`); the ESLint rule in .eslintrc enforces this. + * + * The only members living here are cross-cutting composites whose + * single-module home does not exist yet (createHireRequest, + * rollbackConfigRevision) and the raw-SQL escape hatch, and backups + * which need the open/close lifecycle plumbing owned by this class. + */ export class AppDatabase extends AppDatabaseBase { - - // ─── Activity & Comments ─────────────────────────────────────────────────── - - listComments(taskId: string) { return this.activityMod.listComments(taskId); } - - // ─── Social & Browser Actions ────────────────────────────────────────────── - - saveSocialAccount(input: { id?: string | null; companyId: string; platform: string; accountName: string; displayName?: string; profileUrl?: string; credentialSecretId?: string | null; status?: string; requireApproval?: boolean; metadataJson?: string }) { return this.social.saveSocialAccount(input); } - deleteSocialAccount(id: string, companyId: string) { this.social.deleteSocialAccount(id, companyId); } - listSocialAccounts(companyId: string) { return this.social.listSocialAccounts(companyId); } - getSocialAccount(id: string, companyId: string) { return this.social.getSocialAccount(id, companyId); } - saveBrowserAction(input: { companyId: string; socialAccountId: string; agentId: string; taskId?: string | null; actionType: string; payloadJson: string; status?: string; approvalId?: string | null }) { return this.social.saveBrowserAction(input); } - updateBrowserAction(id: string, updates: { status?: string; resultSummary?: string; screenshotPath?: string | null; errorMessage?: string | null; startedAt?: string; finishedAt?: string }) { this.social.updateBrowserAction(id, updates); } - listBrowserActions(companyId: string, filters?: { socialAccountId?: string; status?: string; limit?: number }) { return this.social.listBrowserActions(companyId, filters); } - getBrowserAction(id: string, companyId: string) { return this.social.getBrowserAction(id, companyId); } - getBrowserActionByApprovalId(approvalId: string, companyId: string) { return this.social.getBrowserActionByApprovalId(approvalId, companyId); } - - // ─── Skills ──────────────────────────────────────────────────────────────── - - upsertCompanySkill(skill: { id: string; companyId: string; key: string; slug: string; name: string; description: string | null; sourceType: string; sourceLocator: string | null; sourceRef: string | null; trustLevel: string; compatibility: string; fileInventory: string; metadata: string | null; createdAt: string; updatedAt: string }) { return this.skills.upsertCompanySkill(skill); } - listCompanySkills(companyId: string) { return this.skills.listCompanySkills(companyId); } - getCompanySkill(companyId: string, skillId: string) { return this.skills.getCompanySkill(companyId, skillId); } - deleteCompanySkill(companyId: string, skillId: string) { return this.skills.deleteCompanySkill(companyId, skillId); } - assignSkillToAgent(agentId: string, skillId: string) { this.skills.assignSkillToAgent(agentId, skillId); } - removeSkillFromAgent(agentId: string, skillId: string) { this.skills.removeSkillFromAgent(agentId, skillId); } - listAgentSkillAssignments(agentId: string) { return this.skills.listAgentSkillAssignments(agentId); } - listSkillAgentAssignments(skillId: string) { return this.skills.listSkillAgentAssignments(skillId); } - updateCompanySkillMetadata(companyId: string, skillId: string, metadataJson: string) { this.skills.updateCompanySkillMetadata(companyId, skillId, metadataJson); } - - // ─── Budget ──────────────────────────────────────────────────────────────── - - saveBudgetPolicy(input: { id?: string | null; companyId: string; scopeType: string; scopeId: string; metric?: string; windowKind?: string; amount: number; warnPercent?: number; hardStopEnabled?: boolean; notifyEnabled?: boolean; isActive?: boolean }) { return this.budget.saveBudgetPolicy(input); } - deleteBudgetPolicy(companyId: string, policyId: string) { this.budget.deleteBudgetPolicy(companyId, policyId); } - listBudgetPolicies(companyId: string) { return this.budget.listBudgetPolicies(companyId); } - getBudgetPolicy(policyId: string) { return this.budget.getBudgetPolicy(policyId); } - getActivePoliciesForRun(companyId: string, agentId: string, projectId: string | null) { return this.budget.getActivePoliciesForRun(companyId, agentId, projectId); } - getCalendarMonthWindow() { return this.budget.getCalendarMonthWindow(); } - getSpentInWindow(policy: Parameters[0]) { return this.budget.getSpentInWindow(policy); } - upsertBudgetIncident(policy: Parameters[0], thresholdType: "warn_threshold" | "hard_stop", amountObserved: number) { return this.budget.upsertBudgetIncident(policy, thresholdType, amountObserved); } - listBudgetIncidents(companyId: string, statusFilter: string = "all") { return this.budget.listBudgetIncidents(companyId, statusFilter); } - resolveBudgetIncident(incidentId: string, companyId: string) { this.budget.resolveBudgetIncident(incidentId, companyId); } - resolveIncidentsForPolicy(policyId: string, resolutionStatus: string) { return this.budget.resolveIncidentsForPolicy(policyId, resolutionStatus); } - resolveWarnIncidentsForPolicy(policyId: string, resolutionStatus: string) { return this.budget.resolveWarnIncidentsForPolicy(policyId, resolutionStatus); } - autoCloseExpiredIncidents(companyId: string) { return this.budget.autoCloseExpiredIncidents(companyId); } - - // ─── Entity CRUD ─────────────────────────────────────────────────────────── - - saveGoal(input: { id?: string | null; companyId: string; title: string; description: string; parentId?: string | null; ownerAgentId?: string | null; status: string }) { return this.entityCrud.saveGoal(input); } - listGoals(companyId: string) { return this.entityCrud.listGoals(companyId); } - getGoalForUpdate(goalId: string, companyId: string) { return this.entityCrud.getGoalForUpdate(goalId, companyId); } - saveProject(input: { id?: string | null; companyId: string; goalId?: string | null; name: string; description: string; leadAgentId?: string | null; targetDate?: string | null; status: string }) { return this.entityCrud.saveProject(input); } - listProjects(companyId: string) { return this.entityCrud.listProjects(companyId); } - getProjectForUpdate(projectId: string, companyId: string) { return this.entityCrud.getProjectForUpdate(projectId, companyId); } - saveSecret(input: { id?: string | null; companyId: string; name: string; description: string; value: string }) { return this.entityCrud.saveSecret(input, this.vault); } - resolveSecretEnv(bindingText: string, companyId: string) { return this.entityCrud.resolveSecretEnv(bindingText, companyId, this.vault, listInvalidEnvBindingKeys, parseEnvBindingText); } - deleteGoal(id: string, companyId: string) { this.entityCrud.deleteGoal(id, companyId); } - deleteProject(id: string, companyId: string) { this.entityCrud.deleteProject(id, companyId); } - deleteSecret(id: string, companyId: string) { this.entityCrud.deleteSecret(id, 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 }) { + /** + * Create a hire request by persisting the provisional agent row and + * its approval atomically. The composition spans `agents` and + * `approvalsMod`, so it lives here until a dedicated hiring module + * owns both sides. + */ + createHireRequest(input: Parameters[0]) { return this.entityCrud.createHireRequest(input, { saveAgent: (agentInput) => this.agents.saveAgent(agentInput), requestApproval: (approvalInput) => this.approvalsMod.requestApproval(approvalInput), }); } - // ─── Documents & Work Products ───────────────────────────────────────────── - - saveDocument(input: { id?: string | null; companyId: string; type: string; title: string; content?: string; authorAgentId?: string | null; reviewerAgentId?: string | null; projectId?: string | null; goalId?: string | null; parentDocId?: string | null; version?: number; status?: string; tagsJson?: string; sourceRunId?: string | null }) { return this.documentsMod.saveDocument(input); } - listImportantDocsForReview(input: { companyId: string; types: readonly string[]; authorAgentId: string | null; taskId: string; limit?: number }) { return this.documentsMod.listImportantDocsForReview(input); } - listDocuments(companyId: string) { return this.documentsMod.listDocuments(companyId); } - getDocumentForUpdate(documentId: string, companyId: string) { return this.documentsMod.getDocumentForUpdate(documentId, companyId); } - updateDocumentStatus(id: string, companyId: string, status: string) { this.documentsMod.updateDocumentStatus(id, companyId, status); } - deleteDocument(id: string, companyId: string) { this.documentsMod.deleteDocument(id, companyId); } - createDocumentRevision(input: { documentId: string; companyId: string; body: string; changeSummary?: string | null; createdByAgentId?: string | null; createdByUserId?: string | null }) { return this.documentsMod.createDocumentRevision(input); } - getDocumentLatestRevisionId(documentId: string) { return this.documentsMod.getDocumentLatestRevisionId(documentId); } - listDocumentRevisions(documentId: string, companyId: string) { return this.documentsMod.listDocumentRevisions(documentId, companyId); } - createWorkProduct(input: { companyId: string; taskId: string | null; projectId?: string | null; workspaceId?: string | null; type: string; provider: string; externalId?: string | null; title: string; url?: string | null; status?: string; reviewState?: string; isPrimary?: boolean; summary?: string | null; metadataJson?: string | null; createdByRunId?: string | null }) { return this.documentsMod.createWorkProduct(input); } - updateWorkProduct(id: string, companyId: string, patch: Parameters["updateWorkProduct"]>[2]) { return this.documentsMod.updateWorkProduct(id, companyId, patch); } - listWorkProductsForTask(taskId: string, companyId: string) { return this.documentsMod.listWorkProductsForTask(taskId, companyId); } - listWorkProductsForCompany(companyId: string, limit?: number) { return this.documentsMod.listWorkProductsForCompany(companyId, limit); } - findWorkProductByExternalId(companyId: string, provider: string, externalId: string, workspaceId: string | null) { return this.documentsMod.findWorkProductByExternalId(companyId, provider, externalId, workspaceId); } - resolveWorkProductTarget(workProductId: string, companyId: string) { return this.documentsMod.resolveWorkProductTarget(workProductId, companyId); } - deleteWorkProduct(id: string, companyId: string) { return this.documentsMod.deleteWorkProduct(id, companyId); } - archiveWorkProductsByExternalPaths(input: Parameters["archiveWorkProductsByExternalPaths"]>[0]) { - return this.documentsMod.archiveWorkProductsByExternalPaths(input); - } - linkDocumentToTask(taskId: string, documentId: string, companyId: string, key: string) { return this.documentsMod.linkDocumentToTask(taskId, documentId, companyId, key); } - unlinkDocumentFromTask(taskId: string, key: string, companyId: string) { this.documentsMod.unlinkDocumentFromTask(taskId, key, companyId); } - listDocumentsForTask(taskId: string, companyId: string) { return this.documentsMod.listDocumentsForTask(taskId, companyId); } - listConfigRevisions(agentId: string, companyId: string) { return this.documentsMod.listConfigRevisions(agentId, companyId); } - getConfigRevision(revisionId: string, companyId: string) { return this.documentsMod.getConfigRevision(revisionId, companyId); } + /** + * Roll back an agent configuration revision. Spans documents + * (revision store), entityCrud (agent lookup), and a recorded + * rollback entry — again a multi-module composite the subclass + * carries until a dedicated agent-config module owns the flow. + */ rollbackConfigRevision(agentId: string, revisionId: string, companyId: string) { return this.documentsMod.rollbackConfigRevision(agentId, revisionId, companyId, { getConfigRevision: (rid, cid) => this.documentsMod.getConfigRevision(rid, cid), @@ -107,78 +42,15 @@ export class AppDatabase extends AppDatabaseBase { }); } - // ─── Knowledge ───────────────────────────────────────────────────────────── - - saveMeeting(input: { id?: string | null; companyId: string; type: string; title: string; organizerAgentId?: string | null; participantAgentIds?: string; scheduledAt: string; durationMinutes?: number; agendaJson?: string; notesJson?: string; decisionsJson?: string; actionItemsJson?: string; status?: string }) { return this.knowledgeMod.saveMeeting(input); } - deleteMeeting(id: string, companyId: string) { this.knowledgeMod.deleteMeeting(id, companyId); } - listMeetings(companyId: string) { return this.knowledgeMod.listMeetings(companyId); } - getMeeting(meetingId: string, companyId: string) { return this.knowledgeMod.getMeeting(meetingId, companyId); } - saveKnowledgeEntry(input: { id?: string | null; companyId: string; category: string; topic: string; content: string; authorAgentId?: string | null; importance?: string; tagsJson?: string; referencedEntityType?: string | null; referencedEntityId?: string | null; sourceRunId?: string | null }) { return this.knowledgeMod.saveKnowledgeEntry(input); } - deleteKnowledgeEntry(id: string, companyId: string) { this.knowledgeMod.deleteKnowledgeEntry(id, companyId); } - listKnowledgeEntries(companyId: string) { return this.knowledgeMod.listKnowledgeEntries(companyId); } - saveSprint(input: { id?: string | null; companyId: string; name: string; goal?: string; startDate: string; endDate: string; status?: string; retrospectiveNotes?: string; velocityPoints?: number; completedPoints?: number }) { return this.knowledgeMod.saveSprint(input); } - deleteSprint(id: string, companyId: string) { this.knowledgeMod.deleteSprint(id, companyId); } - listSprints(companyId: string) { return this.knowledgeMod.listSprints(companyId); } - collectRunDeliverableSignals(runId: string, companyId: string, taskId: string) { - return this.knowledgeMod.collectRunDeliverableSignals(runId, companyId, taskId); - } - - // ─── Automation ──────────────────────────────────────────────────────────── - - saveAutomationRule(input: { id?: string | null; companyId: string; name: string; description?: string; trigger: string; conditionsJson?: string; action: string; actionConfigJson?: string; sourceDepartment?: string | null; targetDepartment?: string | null; priority?: number; status?: string }) { return this.automationMod.saveAutomationRule(input); } - deleteAutomationRule(id: string, companyId: string) { this.automationMod.deleteAutomationRule(id, companyId); } - listAutomationRuleNames(companyId: string) { return this.automationMod.listAutomationRuleNames(companyId); } - toggleAutomationRule(id: string, companyId: string, status: "active" | "paused") { this.automationMod.toggleAutomationRule(id, companyId, status); } - recordAutomationExecution(ruleId: string, companyId: string, ruleName: string, trigger: string, action: string, detail: string) { this.automationMod.recordAutomationExecution(ruleId, companyId, ruleName, trigger, action, detail); } - getAutomationLog(companyId: string) { return this.automationMod.getAutomationLog(companyId); } - listActiveAutomationRules(companyId: string, trigger: string) { return this.automationMod.listActiveAutomationRules(companyId, trigger); } - saveWorkflow(input: { id?: string | null; companyId: string; name: string; description?: string; stepsJson?: string; triggerType: string; triggerConfigJson?: string; status?: string }) { return this.automationMod.saveWorkflow(input); } - getWorkflowRecord(id: string, companyId: string) { return this.automationMod.getWorkflowRecord(id, companyId); } - updateWorkflowExecution(input: { id: string; companyId: string; status: string; currentStepIndex: number; stepsJson: string; incrementRunCount?: boolean; lastRunAt?: string | null }) { this.automationMod.updateWorkflowExecution(input); } - deleteWorkflow(id: string, companyId: string) { this.automationMod.deleteWorkflow(id, companyId); } - listTriggerableWorkflows(companyId: string, triggerType: string): WorkflowPipelineRecord[] { return this.automationMod.listTriggerableWorkflows(companyId, triggerType); } - - // ─── Wakeup ──────────────────────────────────────────────────────────────── - - enqueueWakeupRequest(companyId: string, agentId: string, trigger: string, detail: string, context?: Record) { this.wakeup.enqueueWakeupRequest(companyId, agentId, trigger, detail, context); } - promoteWakeupRequests(agentId: string, companyId: string) { return this.wakeup.promoteWakeupRequests(agentId, companyId); } - listWakeupRequests(companyId: string, filters?: { status?: string; agentId?: string; limit?: number }) { return this.wakeup.listWakeupRequests(companyId, filters); } - getPreferredWakeTrigger(agentId: string, companyId: string) { return this.wakeup.getPreferredWakeTrigger(agentId, companyId); } - completePendingWakeupRequests(agentId: string, companyId: string, runId: string) { return this.wakeup.completePendingWakeupRequests(agentId, companyId, runId); } - purgeExpiredWakeupRequests() { return this.wakeup.purgeExpiredWakeupRequests(); } - - // ─── Backup ──────────────────────────────────────────────────────────────── - - exportBackup(targetDir: string) { return this.backupMod.exportBackup(targetDir); } - createAutomaticBackup(reason: string) { return this.backupMod.createAutomaticBackup(reason); } - async restoreFromBackup(sourceDir: string) { return this.backupMod.restoreFromBackup(sourceDir); } - - // ─── Thread Runtime ──────────────────────────────────────────────────────── - - createThread(input: Parameters["createThread"]>[0]) { return this.threads.createThread(input); } - listThreads(options: Parameters["listThreads"]>[0]) { return this.threads.listThreads(options); } - getThread(threadId: string, companyId: string) { return this.threads.getThread(threadId, companyId); } - addThreadEvent(input: Parameters["addThreadEvent"]>[0]) { return this.threads.addThreadEvent(input); } - listThreadEvents(threadId: string, companyId: string, options?: Parameters["listThreadEvents"]>[2]) { return this.threads.listThreadEvents(threadId, companyId, options); } - setThreadWaitState(input: Parameters["setThreadWaitState"]>[0]) { return this.threads.setThreadWaitState(input); } - resolveThreadWaitState(waitStateId: string, companyId: string) { return this.threads.resolveThreadWaitState(waitStateId, companyId); } - listThreadWaitStates(companyId: string, options?: Parameters["listThreadWaitStates"]>[1]) { return this.threads.listThreadWaitStates(companyId, options); } - hasUnresolvedWaitStateForAgent(companyId: string, agentId: string) { return this.threads.hasUnresolvedWaitStateForAgent(companyId, agentId); } - addParticipant(input: Parameters["addParticipant"]>[0]) { return this.threads.addParticipant(input); } - listParticipants(threadId: string) { return this.threads.listParticipants(threadId); } - removeParticipant(threadId: string, participantId: string) { return this.threads.removeParticipant(threadId, participantId); } - subscribe(input: Parameters["subscribe"]>[0]) { return this.threads.subscribe(input); } - unsubscribe(threadId: string, subscriberId: string) { return this.threads.unsubscribe(threadId, subscriberId); } - listSubscriptions(threadId: string) { return this.threads.listSubscriptions(threadId); } - listSubscribedThreads(subscriberId: string, companyId: string) { return this.threads.listSubscribedThreads(subscriberId, companyId); } - - // ─── Internal raw-SQL escape hatch ──────────────────────────────────────── - // - // Not part of the public API. New callers MUST add a typed method on the - // appropriate `database/.ts` and route through there. The existing - // call sites remain only to avoid a repo-wide migration; see - // `.planning/review2/02-persistence-ipc.md` DB-001 for the migration list. - - /** @internal */ + /** + * Raw-SQL escape hatch. Not part of the public API. New callers MUST + * add a typed method on the appropriate `database/.ts` and + * route through there. Retained only for legacy automation + * bookkeeping that predates the module split; see + * `.planning/review2/02-persistence-ipc.md` DB-001 for the migration + * list. + * + * @internal + */ queryAll(sql: string, ...params: SQLInputValue[]): T[] { return this.readAll(sql, ...params); } } diff --git a/src/main/database/entity-crud.ts b/src/main/database/entity-crud.ts index 1c819fe1..8cdb70ad 100644 --- a/src/main/database/entity-crud.ts +++ b/src/main/database/entity-crud.ts @@ -43,8 +43,12 @@ export function buildEntityCrudMethods( transaction: Transaction, _deps: { addActivity: (input: { companyId: string; actor: string; action: string; entityType: string; entityId: string; detail: string }) => void; + vault: { encrypt: (v: string) => { backend: string; payload: Uint8Array }; decrypt: (backend: "safe_storage" | "vault", ciphertext: Buffer) => string }; + listInvalidEnvBindingKeys: (t: string) => string[]; + parseEnvBindingText: (t: string) => Record; }, ) { + const { vault, listInvalidEnvBindingKeys, parseEnvBindingText } = _deps; return { getTask(id: string): TaskRecord { const row = getDb() @@ -169,7 +173,7 @@ export function buildEntityCrudMethods( return id; }, - saveSecret(input: { id?: string | null; companyId: string; name: string; description: string; value: string }, vault: { encrypt: (v: string) => { backend: string; payload: Uint8Array } }) { + saveSecret(input: { id?: string | null; companyId: string; name: string; description: string; value: string }) { const id = ensureId(input.id ?? null); const encrypted = vault.encrypt(input.value); const ts = nowIso(); @@ -191,7 +195,7 @@ export function buildEntityCrudMethods( return id; }, - resolveSecretEnv(bindingText: string, companyId: string, vault: { decrypt: (backend: "safe_storage" | "vault", ciphertext: Buffer) => string }, listInvalidEnvBindingKeys: (t: string) => string[], parseEnvBindingText: (t: string) => Record): Record { + resolveSecretEnv(bindingText: string, companyId: string): Record { const invalidKeys = new Set(listInvalidEnvBindingKeys(bindingText)); const bindings = parseEnvBindingText(bindingText); const result: Record = {}; diff --git a/src/main/document-lifecycle.ts b/src/main/document-lifecycle.ts index f80e1da3..52e8feee 100644 --- a/src/main/document-lifecycle.ts +++ b/src/main/document-lifecycle.ts @@ -84,7 +84,7 @@ export function createDocumentWithLifecycle( ): DocumentLifecycleOutcome { const outcome = deps.db.withTransaction(() => { const isNew = !input.id; - const id = deps.db.saveDocument(input); + const id = deps.db.documentsMod.saveDocument(input); const taskId = deriveTaskIdFromRun(deps.db, input.sourceRunId, input.companyId, deps.logger); let deliverableReviewApprovalId: string | null = null; diff --git a/src/main/execution-mode-reconciler.ts b/src/main/execution-mode-reconciler.ts index 2ba86f77..c6fb5118 100644 --- a/src/main/execution-mode-reconciler.ts +++ b/src/main/execution-mode-reconciler.ts @@ -27,9 +27,9 @@ const REVIEW_WAIT_STATE_TYPES = new Set(["user_reply", "user_review", "user_appr function collectImpactedAgentIdsFromWaitStates(db: AppDatabase, companyId: string): Set { const agentIds = new Set(); - const waitStates = db.listThreadWaitStates(companyId); + const waitStates = db.threads.listThreadWaitStates(companyId); for (const waitState of waitStates) { - const thread = db.getThread(waitState.threadId, companyId); + const thread = db.threads.getThread(waitState.threadId, companyId); if (!thread || thread.relatedEntityType !== "task" || !thread.relatedEntityId) continue; try { const task = db.entityCrud.getTask(thread.relatedEntityId); @@ -143,10 +143,10 @@ export function reconcileExecutionModeChange({ }); } - const waitStates = db.listThreadWaitStates(companyId); + const waitStates = db.threads.listThreadWaitStates(companyId); for (const waitState of waitStates) { if (!REVIEW_WAIT_STATE_TYPES.has(waitState.waitingFor)) continue; - db.resolveThreadWaitState(waitState.id, companyId); + db.threads.resolveThreadWaitState(waitState.id, companyId); } } diff --git a/src/main/hire-hook.ts b/src/main/hire-hook.ts index 541fbd41..064e8bdf 100644 --- a/src/main/hire-hook.ts +++ b/src/main/hire-hook.ts @@ -77,7 +77,7 @@ export async function notifyHireApproved( }; log.info(`[hire-hook] Hire approved for agent ${agent.name} (${input.agentId}) on connector ${connectorDef.id}`); - db.addActivity({ + db.activityMod.addActivity({ companyId: input.companyId, actor: "system", action: "hire_hook.success", @@ -95,7 +95,7 @@ export async function notifyHireApproved( const message = error instanceof Error ? error.message : String(error); log.error(`[${classified.code}] ${classified.message}: ${message}`); try { - db.addActivity({ + db.activityMod.addActivity({ companyId: input.companyId, actor: "system", action: "hire_hook.exception", diff --git a/src/main/ipc-agent-coordination-handlers.ts b/src/main/ipc-agent-coordination-handlers.ts index a9ab1a6c..8b7e2824 100644 --- a/src/main/ipc-agent-coordination-handlers.ts +++ b/src/main/ipc-agent-coordination-handlers.ts @@ -60,7 +60,7 @@ export function registerAgentCoordinationHandlers({ if (!message) { return fail("MESSAGE_NOT_FOUND", "Message not found in the current company."); } - if (parsed.readerAgentId && !db.belongsToCompany("agents", parsed.readerAgentId, parsed.companyId)) { + if (parsed.readerAgentId && !db.settings.belongsToCompany("agents", parsed.readerAgentId, parsed.companyId)) { return fail("INVALID_COMPANY_REFERENCE", "The selected message reader does not belong to the current company."); } if (parsed.readerAgentId) { diff --git a/src/main/ipc-automation-operations-handlers.ts b/src/main/ipc-automation-operations-handlers.ts index 3c7f1988..f0ffe957 100644 --- a/src/main/ipc-automation-operations-handlers.ts +++ b/src/main/ipc-automation-operations-handlers.ts @@ -29,7 +29,7 @@ export function registerAutomationOperationsHandlers({ const parsed = automationRuleInputSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - const id = db.saveAutomationRule(parsed); + const id = db.automationMod.saveAutomationRule(parsed); publishDomainChanged(); return ok(id); }); @@ -38,10 +38,10 @@ export function registerAutomationOperationsHandlers({ const parsed = deleteEntitySchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("automation_rules", parsed.id, parsed.companyId)) { + if (!db.settings.belongsToCompany("automation_rules", parsed.id, parsed.companyId)) { return fail("ENTITY_NOT_FOUND", "Automation rule not found in the current company."); } - db.deleteAutomationRule(parsed.id, parsed.companyId); + db.automationMod.deleteAutomationRule(parsed.id, parsed.companyId); publishDomainChanged(); return ok(true); }); @@ -50,10 +50,10 @@ export function registerAutomationOperationsHandlers({ const parsed = toggleAutomationRuleSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("automation_rules", parsed.id, parsed.companyId)) { + if (!db.settings.belongsToCompany("automation_rules", parsed.id, parsed.companyId)) { return fail("ENTITY_NOT_FOUND", "Automation rule not found in the current company."); } - db.toggleAutomationRule(parsed.id, parsed.companyId, parsed.status); + db.automationMod.toggleAutomationRule(parsed.id, parsed.companyId, parsed.status); publishDomainChanged(); return ok(true); }); @@ -62,14 +62,14 @@ export function registerAutomationOperationsHandlers({ const parsed = companyIdSchema.parse(companyId); const companyError = ensureCompanyExists(parsed); if (companyError) return companyError; - return ok(db.getAutomationLog(parsed)); + return ok(db.automationMod.getAutomationLog(parsed)); }); registerHandle(IPC_CHANNELS.saveWorkflow, async (_event, payload) => { const parsed = workflowInputSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - const id = db.saveWorkflow(parsed); + const id = db.automationMod.saveWorkflow(parsed); publishDomainChanged(); return ok(id); }); @@ -78,10 +78,10 @@ export function registerAutomationOperationsHandlers({ const parsed = deleteEntitySchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("workflow_pipelines", parsed.id, parsed.companyId)) { + if (!db.settings.belongsToCompany("workflow_pipelines", parsed.id, parsed.companyId)) { return fail("ENTITY_NOT_FOUND", "Workflow not found in the current company."); } - db.deleteWorkflow(parsed.id, parsed.companyId); + db.automationMod.deleteWorkflow(parsed.id, parsed.companyId); publishDomainChanged(); return ok(true); }); @@ -90,7 +90,7 @@ export function registerAutomationOperationsHandlers({ const parsed = runWorkflowSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("workflow_pipelines", parsed.id, parsed.companyId)) { + if (!db.settings.belongsToCompany("workflow_pipelines", parsed.id, parsed.companyId)) { return fail("ENTITY_NOT_FOUND", "Workflow not found in the current company."); } await runWorkflowPipeline(parsed.id, parsed.companyId); diff --git a/src/main/ipc-browser-handlers.ts b/src/main/ipc-browser-handlers.ts index b54c0148..d028acad 100644 --- a/src/main/ipc-browser-handlers.ts +++ b/src/main/ipc-browser-handlers.ts @@ -30,7 +30,7 @@ export function registerBrowserHandlers({ const input = socialAccountInputSchema.parse(payload); const err = ensureCompanyExists(input.companyId); if (err) return err; - const id = db.saveSocialAccount(input); + const id = db.social.saveSocialAccount(input); publishDomainChanged(); return ok(id); }); @@ -39,12 +39,12 @@ export function registerBrowserHandlers({ const input = deleteEntitySchema.parse(payload); const err = ensureCompanyExists(input.companyId); if (err) return err; - const browserActions = db.listBrowserActions(input.companyId, { socialAccountId: input.id, limit: 500 }); + const browserActions = db.social.listBrowserActions(input.companyId, { socialAccountId: input.id, limit: 500 }); await getBrowserActions()?.clearSocialAccountData( input.id, browserActions.flatMap((action: { screenshotPath: string | null }) => (action.screenshotPath ? [action.screenshotPath] : [])), ); - db.deleteSocialAccount(input.id, input.companyId); + db.social.deleteSocialAccount(input.id, input.companyId); publishDomainChanged(); return ok(true); }); @@ -53,7 +53,7 @@ export function registerBrowserHandlers({ const input = browserActionQuerySchema.parse(payload); const err = ensureCompanyExists(input.companyId); if (err) return err; - return ok(db.listBrowserActions(input.companyId, { + return ok(db.social.listBrowserActions(input.companyId, { socialAccountId: input.socialAccountId, status: input.status, limit: input.limit, @@ -62,12 +62,12 @@ export function registerBrowserHandlers({ registerHandle(IPC_CHANNELS.cancelBrowserAction, (_event, payload) => { const input = cancelBrowserActionSchema.parse(payload); - const action = db.getBrowserAction(input.actionId, input.companyId); + const action = db.social.getBrowserAction(input.actionId, input.companyId); if (!action) { return fail("BROWSER_ACTION_NOT_FOUND", "Browser action not found."); } getBrowserActions()?.cancelAction(input.actionId); - db.updateBrowserAction(input.actionId, { + db.social.updateBrowserAction(input.actionId, { status: "cancelled", resultSummary: "Browser action cancelled from the desktop control surface.", errorMessage: "Action cancelled.", @@ -86,7 +86,7 @@ export function registerBrowserHandlers({ return fail("BROWSER_NOT_READY", "Browser manager not initialized"); } - const account = db.getSocialAccount(input.socialAccountId, input.companyId); + const account = db.social.getSocialAccount(input.socialAccountId, input.companyId); if (!account) { return fail("ACCOUNT_NOT_FOUND", "Social account not found"); } @@ -97,7 +97,7 @@ export function registerBrowserHandlers({ inflightBrowserLogins.add(input.socialAccountId); browserActions.launchLoginBrowser(input.socialAccountId, account.platform).then(async (result) => { if (result.success) { - const latestAccount = db.getSocialAccount(input.socialAccountId, account.companyId); + const latestAccount = db.social.getSocialAccount(input.socialAccountId, account.companyId); if (!latestAccount) { return; } @@ -110,7 +110,7 @@ export function registerBrowserHandlers({ } catch (err) { logger.warn(`[ipc/browser] extractProfileFromSession failed for social account ${input.socialAccountId}:`, err instanceof Error ? err.message : String(err)); } - db.saveSocialAccount({ + db.social.saveSocialAccount({ id: latestAccount.id, companyId: latestAccount.companyId, platform: latestAccount.platform, diff --git a/src/main/ipc-budget-handlers.ts b/src/main/ipc-budget-handlers.ts index c3e6dc7c..4dcd7045 100644 --- a/src/main/ipc-budget-handlers.ts +++ b/src/main/ipc-budget-handlers.ts @@ -32,7 +32,7 @@ export function registerBudgetHandlers({ const parsed = budgetPolicyInputSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - const id = db.saveBudgetPolicy(parsed); + const id = db.budget.saveBudgetPolicy(parsed); // D-06: Auto-resolve incidents if limit was increased above current spend if (budgetService) { try { @@ -50,7 +50,7 @@ export function registerBudgetHandlers({ const parsed = deleteBudgetPolicySchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - db.deleteBudgetPolicy(parsed.companyId, parsed.policyId); + db.budget.deleteBudgetPolicy(parsed.companyId, parsed.policyId); publishDomainChanged(); return ok(true); }); @@ -60,7 +60,7 @@ export function registerBudgetHandlers({ const parsed = listBudgetPoliciesSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - return ok(db.listBudgetPolicies(parsed.companyId)); + return ok(db.budget.listBudgetPolicies(parsed.companyId)); }); // ---- List budget incidents ---- @@ -68,7 +68,7 @@ export function registerBudgetHandlers({ const parsed = listBudgetIncidentsSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - return ok(db.listBudgetIncidents(parsed.companyId, parsed.status)); + return ok(db.budget.listBudgetIncidents(parsed.companyId, parsed.status)); }); // ---- Resolve a budget incident manually ---- @@ -76,7 +76,7 @@ export function registerBudgetHandlers({ const parsed = resolveBudgetIncidentSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - db.resolveBudgetIncident(parsed.incidentId, parsed.companyId); + db.budget.resolveBudgetIncident(parsed.incidentId, parsed.companyId); publishDomainChanged(); return ok(true); }); diff --git a/src/main/ipc-content-operations-handlers.ts b/src/main/ipc-content-operations-handlers.ts index 13e2942c..f235742d 100644 --- a/src/main/ipc-content-operations-handlers.ts +++ b/src/main/ipc-content-operations-handlers.ts @@ -70,7 +70,7 @@ export function registerContentOperationsHandlers({ if (invalidReference) { return fail("INVALID_COMPANY_REFERENCE", `The selected ${invalidReference.label} does not belong to the current company.`); } - const id = db.saveMeeting(parsed); + const id = db.knowledgeMod.saveMeeting(parsed); publishDomainChanged(); return ok(id); }); @@ -79,10 +79,10 @@ export function registerContentOperationsHandlers({ const parsed = deleteEntitySchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("meetings", parsed.id, parsed.companyId)) { + if (!db.settings.belongsToCompany("meetings", parsed.id, parsed.companyId)) { return fail("ENTITY_NOT_FOUND", "Meeting not found in the current company."); } - db.deleteMeeting(parsed.id, parsed.companyId); + db.knowledgeMod.deleteMeeting(parsed.id, parsed.companyId); publishDomainChanged(); return ok(true); }); @@ -134,10 +134,10 @@ export function registerContentOperationsHandlers({ const parsed = deleteEntitySchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("documents", parsed.id, parsed.companyId)) { + if (!db.settings.belongsToCompany("documents", parsed.id, parsed.companyId)) { return fail("ENTITY_NOT_FOUND", "Document not found in the current company."); } - db.deleteDocument(parsed.id, parsed.companyId); + db.documentsMod.deleteDocument(parsed.id, parsed.companyId); publishDomainChanged(); return ok(true); }); @@ -159,7 +159,7 @@ export function registerContentOperationsHandlers({ if (invalidReference) { return fail("INVALID_COMPANY_REFERENCE", `The selected ${invalidReference.label} does not belong to the current company.`); } - const id = db.saveKnowledgeEntry(parsed); + const id = db.knowledgeMod.saveKnowledgeEntry(parsed); publishDomainChanged(); return ok(id); }); @@ -168,10 +168,10 @@ export function registerContentOperationsHandlers({ const parsed = deleteEntitySchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("knowledge_base", parsed.id, parsed.companyId)) { + if (!db.settings.belongsToCompany("knowledge_base", parsed.id, parsed.companyId)) { return fail("ENTITY_NOT_FOUND", "Knowledge entry not found in the current company."); } - db.deleteKnowledgeEntry(parsed.id, parsed.companyId); + db.knowledgeMod.deleteKnowledgeEntry(parsed.id, parsed.companyId); publishDomainChanged(); return ok(true); }); @@ -183,13 +183,13 @@ export function registerContentOperationsHandlers({ let previousSprintStatus: string | null = null; if (parsed.id) { try { - previousSprintStatus = db.listSnapshot().sprints.find((s: { id: string }) => s.id === parsed.id)?.status ?? null; + previousSprintStatus = db.snap.listSnapshot().sprints.find((s: { id: string }) => s.id === parsed.id)?.status ?? null; } catch (err) { logger.warn(`[ipc/content] listSnapshot failed while resolving sprint ${parsed.id} previous status:`, err instanceof Error ? err.message : String(err)); previousSprintStatus = null; } } - const id = db.saveSprint(parsed); + const id = db.knowledgeMod.saveSprint(parsed); if (parsed.status === "active" && previousSprintStatus !== "active") { dispatchAutomation("sprint_started", { companyId: parsed.companyId }); } @@ -204,10 +204,10 @@ export function registerContentOperationsHandlers({ const parsed = deleteEntitySchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("sprints", parsed.id, parsed.companyId)) { + if (!db.settings.belongsToCompany("sprints", parsed.id, parsed.companyId)) { return fail("ENTITY_NOT_FOUND", "Sprint not found in the current company."); } - db.deleteSprint(parsed.id, parsed.companyId); + db.knowledgeMod.deleteSprint(parsed.id, parsed.companyId); publishDomainChanged(); return ok(true); }); diff --git a/src/main/ipc-core-admin-handlers.ts b/src/main/ipc-core-admin-handlers.ts index ddb077e1..c64e8e2e 100644 --- a/src/main/ipc-core-admin-handlers.ts +++ b/src/main/ipc-core-admin-handlers.ts @@ -60,7 +60,7 @@ export function registerCoreAdminHandlers({ nextReviewDeliverables: parsed.reviewDeliverables, handleApprovalResolved, }); - db.addActivity({ + db.activityMod.addActivity({ companyId: id, actor: "board", action: parsed.id ? "company.updated" : "company.created", @@ -76,10 +76,10 @@ export function registerCoreAdminHandlers({ if (typeof companyId !== "string" || !companyId) { return fail("INVALID_COMPANY_ID", "A valid company id is required."); } - if (!db.hasCompany(companyId)) { + if (!db.settings.hasCompany(companyId)) { return fail("COMPANY_NOT_FOUND", "The selected company does not exist."); } - db.setCurrentCompany(companyId); + db.settings.setCurrentCompany(companyId); publishDomainChanged(); return ok(true); }); @@ -93,12 +93,12 @@ export function registerCoreAdminHandlers({ // The per-company disabled-models pool is not consulted here — // connector config is a global hint, and pool enforcement // happens at run dispatch time where the company is known. - db.saveConnector({ + db.connectorsCrud.saveConnector({ ...parsed, model: parsed.model ?? null, }); - db.addActivity({ - companyId: db.getCurrentCompanyId() ?? "00000000-0000-0000-0000-000000000000", + db.activityMod.addActivity({ + companyId: db.settings.getCurrentCompanyId() ?? "00000000-0000-0000-0000-000000000000", actor: "board", action: "connector.updated", entityType: "connector", @@ -127,7 +127,7 @@ export function registerCoreAdminHandlers({ registerHandle(IPC_CHANNELS.setConnectorDisabledModels, async (_event, payload) => { const parsed = connectorDisabledModelsSchema.parse(payload); - db.setDisabledModels(parsed.companyId, parsed.connectorId, parsed.disabledModels); + db.settings.setDisabledModels(parsed.companyId, parsed.connectorId, parsed.disabledModels); publishDomainChanged(); return ok(true); }); @@ -136,11 +136,11 @@ export function registerCoreAdminHandlers({ const parsed = deleteEntitySchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("secrets", parsed.id, parsed.companyId)) { + if (!db.settings.belongsToCompany("secrets", parsed.id, parsed.companyId)) { return fail("SECRET_NOT_FOUND", "Secret not found in the current company."); } - db.deleteSecret(parsed.id, parsed.companyId); - db.addActivity({ + db.entityCrud.deleteSecret(parsed.id, parsed.companyId); + db.activityMod.addActivity({ companyId: parsed.companyId, actor: "board", action: "secret.deleted", @@ -156,7 +156,7 @@ export function registerCoreAdminHandlers({ if (typeof companyId !== "string" || companyId.length === 0) { return fail("INVALID_INPUT", "Company ID is required."); } - if (!db.hasCompany(companyId)) { + if (!db.settings.hasCompany(companyId)) { return fail("COMPANY_NOT_FOUND", "Company not found."); } db.company.deleteCompany(companyId); diff --git a/src/main/ipc-core-agent-graph-handlers.ts b/src/main/ipc-core-agent-graph-handlers.ts index c0ea14e6..608335e4 100644 --- a/src/main/ipc-core-agent-graph-handlers.ts +++ b/src/main/ipc-core-agent-graph-handlers.ts @@ -9,7 +9,7 @@ import { getConnectorExecutionReadinessIssue } from "@shared/connector-policy"; import { resolveApprovalDecision } from "./approval-policy"; import { shouldAutoApprove } from "./execution-policy"; import { getHireNameValidationError, getHireRoutingValidationError, resolveHireReportsTo } from "./agent-permissions"; -import { validateConnectorModelSelection } from "./connector-model-policy"; +import { validateConnectorModelSelection, connectorPolicyDepsFromDb } from "./connector-model-policy"; import { fail, ok, type RegisterCoreHandlersDependencies } from "./ipc-core-common"; export function registerCoreAgentGraphHandlers({ @@ -40,8 +40,8 @@ export function registerCoreAgentGraphHandlers({ if (invalidReference) { return fail("INVALID_COMPANY_REFERENCE", `The selected ${invalidReference.label} does not belong to the current company.`); } - const id = db.saveWorkspace(parsed); - db.addActivity({ + const id = db.workspacesMod.saveWorkspace(parsed); + db.activityMod.addActivity({ companyId: parsed.companyId, actor: "board", action: parsed.id ? "workspace.updated" : "workspace.created", @@ -87,7 +87,7 @@ export function registerCoreAgentGraphHandlers({ if (invalidReference) { return fail("INVALID_COMPANY_REFERENCE", `The selected ${invalidReference.label} does not belong to the current company.`); } - const connectorRecord = db.findConnector(parsed.connectorId); + const connectorRecord = db.connectorsCrud.findConnector(parsed.connectorId); if (!connectorRecord) { return fail("CONNECTOR_NOT_FOUND", "The selected connector does not exist."); } @@ -95,7 +95,7 @@ export function registerCoreAgentGraphHandlers({ if (connectorIssue) { return fail("CONNECTOR_NOT_READY", connectorIssue); } - const modelSelection = validateConnectorModelSelection(db, parsed.companyId, parsed.connectorId, parsed.model ?? null); + const modelSelection = validateConnectorModelSelection(connectorPolicyDepsFromDb(db), parsed.companyId, parsed.connectorId, parsed.model ?? null); if (!modelSelection.ok) { return fail("CONNECTOR_MODEL_NOT_ALLOWED", modelSelection.error); } @@ -124,7 +124,7 @@ export function registerCoreAgentGraphHandlers({ } catch (error) { return fail("INVALID_AGENT_HIERARCHY", error instanceof Error ? error.message : "Agent could not be saved."); } - db.addActivity({ + db.activityMod.addActivity({ companyId: parsed.companyId, actor: "board", action: parsed.id ? "agent.updated" : "agent.created", @@ -155,7 +155,7 @@ export function registerCoreAgentGraphHandlers({ if (invalidReference) { return fail("INVALID_COMPANY_REFERENCE", `The selected ${invalidReference.label} does not belong to the current company.`); } - const connectorRecord = db.findConnector(parsed.connectorId); + const connectorRecord = db.connectorsCrud.findConnector(parsed.connectorId); if (!connectorRecord) { return fail("CONNECTOR_NOT_FOUND", "The selected connector does not exist."); } @@ -181,7 +181,7 @@ export function registerCoreAgentGraphHandlers({ if (routingError) { return fail("INVALID_HIRE_ROUTING", routingError); } - const modelSelection = validateConnectorModelSelection(db, parsed.companyId, parsed.connectorId, parsed.model ?? null); + const modelSelection = validateConnectorModelSelection(connectorPolicyDepsFromDb(db), parsed.companyId, parsed.connectorId, parsed.model ?? null); if (!modelSelection.ok) { return fail("CONNECTOR_MODEL_NOT_ALLOWED", modelSelection.error); } @@ -237,7 +237,7 @@ export function registerCoreAgentGraphHandlers({ return fail("HIRE_REQUEST_FAILED", error instanceof Error ? error.message : "Hire request could not be created."); } - db.addActivity({ + db.activityMod.addActivity({ companyId: parsed.companyId, actor: parsed.requestedByAgentId ?? "board", action: "agent.hire_requested", @@ -268,7 +268,7 @@ export function registerCoreAgentGraphHandlers({ const parsed = deleteEntitySchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("agents", parsed.id, parsed.companyId)) { + if (!db.settings.belongsToCompany("agents", parsed.id, parsed.companyId)) { return fail("AGENT_NOT_FOUND", "Agent not found in the current company."); } const agent = db.entityCrud.getAgent(parsed.id); @@ -279,7 +279,7 @@ export function registerCoreAgentGraphHandlers({ ); } db.agents.deleteAgent(parsed.id, parsed.companyId); - db.addActivity({ + db.activityMod.addActivity({ companyId: parsed.companyId, actor: "board", action: "agent.deleted", @@ -295,7 +295,7 @@ export function registerCoreAgentGraphHandlers({ const parsed = deleteEntitySchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("approvals", parsed.id, parsed.companyId)) { + if (!db.settings.belongsToCompany("approvals", parsed.id, parsed.companyId)) { return fail("APPROVAL_NOT_FOUND", "Approval not found in the current company."); } const approval = db.approvalsMod.getApprovalRecord(parsed.id); @@ -306,7 +306,7 @@ export function registerCoreAgentGraphHandlers({ ); } db.approvalsMod.deleteApproval(parsed.id, parsed.companyId); - db.addActivity({ + db.activityMod.addActivity({ companyId: parsed.companyId, actor: "board", action: "approval.deleted", diff --git a/src/main/ipc-core-operations-handlers.ts b/src/main/ipc-core-operations-handlers.ts index d4286818..d976283a 100644 --- a/src/main/ipc-core-operations-handlers.ts +++ b/src/main/ipc-core-operations-handlers.ts @@ -48,9 +48,9 @@ export function registerCoreOperationHandlers({ const parsed = approvalInputSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - const invalidTask = parsed.relatedTaskId && !db.belongsToCompany("tasks", parsed.relatedTaskId, parsed.companyId); - const invalidAgent = parsed.requestedByAgentId && !db.belongsToCompany("agents", parsed.requestedByAgentId, parsed.companyId); - const invalidRelatedAgent = parsed.relatedAgentId && !db.belongsToCompany("agents", parsed.relatedAgentId, parsed.companyId); + const invalidTask = parsed.relatedTaskId && !db.settings.belongsToCompany("tasks", parsed.relatedTaskId, parsed.companyId); + const invalidAgent = parsed.requestedByAgentId && !db.settings.belongsToCompany("agents", parsed.requestedByAgentId, parsed.companyId); + const invalidRelatedAgent = parsed.relatedAgentId && !db.settings.belongsToCompany("agents", parsed.relatedAgentId, parsed.companyId); if (invalidTask || invalidAgent || invalidRelatedAgent) { return fail("INVALID_COMPANY_REFERENCE", "The approval references data outside the current company."); } @@ -60,7 +60,7 @@ export function registerCoreOperationHandlers({ } catch (error) { return fail("APPROVAL_INVALID", error instanceof Error ? error.message : "Approval could not be created."); } - db.addActivity({ + db.activityMod.addActivity({ companyId: parsed.companyId, actor: parsed.requestedByAgentId ?? "board", action: "approval.created", @@ -106,7 +106,7 @@ export function registerCoreOperationHandlers({ const parsed = approvalDecisionSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("approvals", parsed.approvalId, parsed.companyId)) { + if (!db.settings.belongsToCompany("approvals", parsed.approvalId, parsed.companyId)) { return fail("APPROVAL_NOT_FOUND", "Approval not found in the current company."); } const approval = db.approvalsMod.getApprovalRecord(parsed.approvalId); @@ -130,7 +130,7 @@ export function registerCoreOperationHandlers({ const parsed = startRunSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("tasks", parsed.taskId, parsed.companyId)) { + if (!db.settings.belongsToCompany("tasks", parsed.taskId, parsed.companyId)) { return fail("TASK_NOT_FOUND", "Task not found in the current company."); } return queueTaskRun(parsed.taskId, "manual", "board"); @@ -140,7 +140,7 @@ export function registerCoreOperationHandlers({ const parsed = cancelRunSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("runs", parsed.runId, parsed.companyId)) { + if (!db.settings.belongsToCompany("runs", parsed.runId, parsed.companyId)) { return fail("RUN_NOT_FOUND", "Run not found in the current company."); } getWorkerProcess()?.postMessage({ type: "cancel-run", runId: parsed.runId }); @@ -151,7 +151,7 @@ export function registerCoreOperationHandlers({ const parsed = runLogSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("runs", parsed.runId, parsed.companyId)) { + if (!db.settings.belongsToCompany("runs", parsed.runId, parsed.companyId)) { return fail("RUN_NOT_FOUND", "Run not found in the current company."); } return ok(db.runs.readRunLog(parsed.runId)); @@ -161,7 +161,7 @@ export function registerCoreOperationHandlers({ const parsed = runLogChunkSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("runs", parsed.runId, parsed.companyId)) { + if (!db.settings.belongsToCompany("runs", parsed.runId, parsed.companyId)) { return fail("RUN_NOT_FOUND", "Run not found in the current company."); } return ok(db.runs.readRunLogChunk(parsed.runId, parsed.offset, parsed.limit)); @@ -171,14 +171,14 @@ export function registerCoreOperationHandlers({ const parsed = commentInputSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - const comment = db.addComment({ + const comment = db.activityMod.addComment({ companyId: parsed.companyId, taskId: parsed.taskId, authorAgentId: parsed.authorAgentId ?? null, authorName: parsed.authorName, body: parsed.body, }); - db.addActivity({ + db.activityMod.addActivity({ companyId: parsed.companyId, actor: parsed.authorAgentId ?? "board", action: "comment.created", @@ -208,11 +208,11 @@ export function registerCoreOperationHandlers({ const parsed = heartbeatSettingsSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("agents", parsed.agentId, parsed.companyId)) { + if (!db.settings.belongsToCompany("agents", parsed.agentId, parsed.companyId)) { return fail("AGENT_NOT_FOUND", "Agent not found in the current company."); } db.agents.setHeartbeat(parsed.agentId, parsed.enabled, parsed.intervalSec); - db.addActivity({ + db.activityMod.addActivity({ companyId: parsed.companyId, actor: "board", action: parsed.enabled ? "heartbeat.enabled" : "heartbeat.disabled", @@ -229,7 +229,7 @@ export function registerCoreOperationHandlers({ const parsed = triggerHeartbeatSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("agents", parsed.agentId, parsed.companyId)) { + if (!db.settings.belongsToCompany("agents", parsed.agentId, parsed.companyId)) { return fail("AGENT_NOT_FOUND", "Agent not found in the current company."); } const runId = wakeAgentIfPossible(parsed.agentId, parsed.companyId, parsed.trigger); diff --git a/src/main/ipc-core-read-handlers.ts b/src/main/ipc-core-read-handlers.ts index c7d4a29a..593ba7d7 100644 --- a/src/main/ipc-core-read-handlers.ts +++ b/src/main/ipc-core-read-handlers.ts @@ -31,7 +31,7 @@ export function registerCoreReadHandlers({ | "listConnectorModels" | "logger" >) { - registerHandle(IPC_CHANNELS.loadSnapshot, () => ok(db.listRendererSnapshot())); + registerHandle(IPC_CHANNELS.loadSnapshot, () => ok(db.snap.listRendererSnapshot())); registerHandle(IPC_CHANNELS.pickDirectory, async () => { const result = await dialog.showOpenDialog({ @@ -44,7 +44,7 @@ export function registerCoreReadHandlers({ if (typeof targetPath !== "string" || targetPath.length === 0) { return fail("INVALID_PATH", "A valid path is required."); } - if (!db.canOpenPath(targetPath)) { + if (!db.settings.canOpenPath(targetPath)) { return fail("OPEN_PATH_FORBIDDEN", "The requested path is outside the managed desktop workspace."); } const error = await shell.openPath(targetPath); @@ -67,13 +67,13 @@ export function registerCoreReadHandlers({ if (!parsed.success) return fail("INVALID_PAYLOAD", "openWorkProduct requires { companyId, id }."); const companyError = ensureCompanyExists(parsed.data.companyId); if (companyError) return companyError; - const target = db.resolveWorkProductTarget(parsed.data.id, parsed.data.companyId); + const target = db.documentsMod.resolveWorkProductTarget(parsed.data.id, parsed.data.companyId); if (!target) return fail("WORK_PRODUCT_NOT_FOUND", "Work product not found or its workspace is unresolved."); if (target.kind === "url") { const error = await shell.openExternal(target.value).then(() => "").catch((err: unknown) => err instanceof Error ? err.message : String(err)); return error ? fail("OPEN_PATH_FAILED", error) : ok(true); } - if (!db.canOpenPath(target.value)) { + if (!db.settings.canOpenPath(target.value)) { return fail("OPEN_PATH_FORBIDDEN", "The requested path is outside the managed desktop workspace."); } const error = await shell.openPath(target.value); @@ -88,12 +88,12 @@ export function registerCoreReadHandlers({ return ok(null); } const backupPath = join(result.filePaths[0]!, `agentcompany-backup-${Date.now()}`); - await db.exportBackup(backupPath); + await db.backupMod.exportBackup(backupPath); return ok(backupPath); }); registerHandle(IPC_CHANNELS.restoreProfile, async () => { - const hasActiveRuns = db.listSnapshot().runs.some((run: { status: string }) => run.status === "queued" || run.status === "running"); + const hasActiveRuns = db.snap.listSnapshot().runs.some((run: { status: string }) => run.status === "queued" || run.status === "running"); if (hasActiveRuns) { return fail("RESTORE_BLOCKED", "Stop all active runs before restoring a profile backup."); } @@ -103,7 +103,7 @@ export function registerCoreReadHandlers({ if (result.canceled || result.filePaths.length === 0) { return ok(false); } - await db.restoreFromBackup(result.filePaths[0]!); + await db.backupMod.restoreFromBackup(result.filePaths[0]!); resetAfterRestore(); publishDomainChanged(); return ok(true); @@ -111,7 +111,7 @@ export function registerCoreReadHandlers({ registerHandle(IPC_CHANNELS.updateSettings, async (_event, payload) => { const parsed = settingsSchema.parse(payload); - db.updateSettings(parsed); + db.settings.updateSettings(parsed); if (parsed.theme) { nativeTheme.themeSource = parsed.theme; } @@ -138,7 +138,7 @@ export function registerCoreReadHandlers({ registerHandle(IPC_CHANNELS.getConnectorDisabledModels, (_event, payload) => { try { const parsed = connectorModelPoolQuerySchema.parse(payload); - return ok(db.getDisabledModels(parsed.companyId, parsed.connectorId)); + return ok(db.settings.getDisabledModels(parsed.companyId, parsed.connectorId)); } catch (err) { logger.warn(`[ipc] getConnectorDisabledModels failed:`, err instanceof Error ? err.message : String(err)); return ok([]); diff --git a/src/main/ipc-core-task-graph-handlers.ts b/src/main/ipc-core-task-graph-handlers.ts index fdeb088a..fecf63fa 100644 --- a/src/main/ipc-core-task-graph-handlers.ts +++ b/src/main/ipc-core-task-graph-handlers.ts @@ -43,8 +43,8 @@ export function registerCoreTaskGraphHandlers({ if (invalidReference) { return fail("INVALID_COMPANY_REFERENCE", `The selected ${invalidReference.label} does not belong to the current company.`); } - const id = db.saveGoal(parsed); - db.addActivity({ + const id = db.entityCrud.saveGoal(parsed); + db.activityMod.addActivity({ companyId: parsed.companyId, actor: "board", action: parsed.id ? "goal.updated" : "goal.created", @@ -70,8 +70,8 @@ export function registerCoreTaskGraphHandlers({ if (invalidReference) { return fail("INVALID_COMPANY_REFERENCE", `The selected ${invalidReference.label} does not belong to the current company.`); } - const id = db.saveProject(parsed); - db.addActivity({ + const id = db.entityCrud.saveProject(parsed); + db.activityMod.addActivity({ companyId: parsed.companyId, actor: "board", action: parsed.id ? "project.updated" : "project.created", @@ -128,7 +128,7 @@ export function registerCoreTaskGraphHandlers({ } catch (error) { return fail("INVALID_TASK_STATE", error instanceof Error ? error.message : "Task could not be saved."); } - db.addActivity({ + db.activityMod.addActivity({ companyId: parsed.companyId, actor: "board", action: isNew ? "task.created" : "task.updated", @@ -176,9 +176,9 @@ export function registerCoreTaskGraphHandlers({ const deleteHandlers = [ { channel: IPC_CHANNELS.deleteTask, table: "tasks", entity: "task", deleteFn: (id: string, companyId: string) => db.tasks.deleteTask(id, companyId) }, - { channel: IPC_CHANNELS.deleteGoal, table: "goals", entity: "goal", deleteFn: (id: string, companyId: string) => db.deleteGoal(id, companyId) }, - { channel: IPC_CHANNELS.deleteProject, table: "projects", entity: "project", deleteFn: (id: string, companyId: string) => db.deleteProject(id, companyId) }, - { channel: IPC_CHANNELS.deleteWorkspace, table: "workspaces", entity: "workspace", deleteFn: (id: string, companyId: string) => db.deleteWorkspace(id, companyId) }, + { channel: IPC_CHANNELS.deleteGoal, table: "goals", entity: "goal", deleteFn: (id: string, companyId: string) => db.entityCrud.deleteGoal(id, companyId) }, + { channel: IPC_CHANNELS.deleteProject, table: "projects", entity: "project", deleteFn: (id: string, companyId: string) => db.entityCrud.deleteProject(id, companyId) }, + { channel: IPC_CHANNELS.deleteWorkspace, table: "workspaces", entity: "workspace", deleteFn: (id: string, companyId: string) => db.workspacesMod.deleteWorkspace(id, companyId) }, ] as const; for (const { channel, table, entity, deleteFn } of deleteHandlers) { @@ -186,11 +186,11 @@ export function registerCoreTaskGraphHandlers({ const parsed = deleteEntitySchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany(table, parsed.id, parsed.companyId)) { + if (!db.settings.belongsToCompany(table, parsed.id, parsed.companyId)) { return fail(`${entity.toUpperCase()}_NOT_FOUND`, `${entity.charAt(0).toUpperCase() + entity.slice(1)} not found in the current company.`); } deleteFn(parsed.id, parsed.companyId); - db.addActivity({ + db.activityMod.addActivity({ companyId: parsed.companyId, actor: "board", action: `${entity}.deleted`, diff --git a/src/main/ipc-recovery-handlers.ts b/src/main/ipc-recovery-handlers.ts index 5012e814..88c6b89b 100644 --- a/src/main/ipc-recovery-handlers.ts +++ b/src/main/ipc-recovery-handlers.ts @@ -33,7 +33,7 @@ export function registerRecoveryHandlers({ const parsed = recoveryRetrySchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("tasks", parsed.taskId, parsed.companyId)) { + if (!db.settings.belongsToCompany("tasks", parsed.taskId, parsed.companyId)) { return fail("TASK_NOT_FOUND", "Task not found in the current company."); } db.tasks.retryInterruptedTask(parsed.taskId, parsed.agentId, parsed.companyId); @@ -50,7 +50,7 @@ export function registerRecoveryHandlers({ const parsed = recoveryTaskSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - if (!db.belongsToCompany("tasks", parsed.taskId, parsed.companyId)) { + if (!db.settings.belongsToCompany("tasks", parsed.taskId, parsed.companyId)) { return fail("TASK_NOT_FOUND", "Task not found in the current company."); } db.tasks.markInterruptedTaskFailed(parsed.taskId); diff --git a/src/main/ipc-skill-handlers.ts b/src/main/ipc-skill-handlers.ts index 66f35afb..4e4f4a30 100644 --- a/src/main/ipc-skill-handlers.ts +++ b/src/main/ipc-skill-handlers.ts @@ -168,7 +168,7 @@ export function registerSkillHandlers({ const parsed = listSkillAgentsSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - return ok(db.listSkillAgentAssignments(parsed.skillId)); + return ok(db.skills.listSkillAgentAssignments(parsed.skillId)); }); // ---- List skill IDs assigned to an agent ---- @@ -176,6 +176,6 @@ export function registerSkillHandlers({ const parsed = listAgentSkillsSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - return ok(db.listAgentSkillAssignments(parsed.agentId)); + return ok(db.skills.listAgentSkillAssignments(parsed.agentId)); }); } diff --git a/src/main/ipc-thread-handlers.ts b/src/main/ipc-thread-handlers.ts index bc47e247..a6c2efd9 100644 --- a/src/main/ipc-thread-handlers.ts +++ b/src/main/ipc-thread-handlers.ts @@ -13,7 +13,7 @@ export function registerThreadHandlers({ registerHandle, db, ensureCompanyExists const input = threadGetterSchema.parse(payload); const companyError = ensureCompanyExists(input.companyId); if (companyError) return companyError; - const thread = db.getThread(input.threadId, input.companyId); + const thread = db.threads.getThread(input.threadId, input.companyId); if (!thread) return fail("THREAD_NOT_FOUND", "Thread not found"); return ok(thread); }); @@ -22,7 +22,7 @@ export function registerThreadHandlers({ registerHandle, db, ensureCompanyExists const input = listThreadEventsSchema.parse(payload); const companyError = ensureCompanyExists(input.companyId); if (companyError) return companyError; - const events = db.listThreadEvents(input.threadId, input.companyId, { + const events = db.threads.listThreadEvents(input.threadId, input.companyId, { limit: input.limit, offset: input.offset, }); @@ -33,7 +33,7 @@ export function registerThreadHandlers({ registerHandle, db, ensureCompanyExists const input = listThreadWaitStatesSchema.parse(payload); const companyError = ensureCompanyExists(input.companyId); if (companyError) return companyError; - const waitStates = db.listThreadWaitStates(input.companyId, { resolvedOnly: input.resolvedOnly }); + const waitStates = db.threads.listThreadWaitStates(input.companyId, { resolvedOnly: input.resolvedOnly }); return ok(waitStates); }); @@ -41,9 +41,9 @@ export function registerThreadHandlers({ registerHandle, db, ensureCompanyExists const input = listThreadParticipantsSchema.parse(payload); const companyError = ensureCompanyExists(input.companyId); if (companyError) return companyError; - const thread = db.getThread(input.threadId, input.companyId); + const thread = db.threads.getThread(input.threadId, input.companyId); if (!thread) return fail("THREAD_NOT_FOUND", "Thread not found"); - const participants = db.listParticipants(input.threadId); + const participants = db.threads.listParticipants(input.threadId); return ok(participants); }); } diff --git a/src/main/main-runtime-support.ts b/src/main/main-runtime-support.ts index 7bdbeee2..d3251bb0 100644 --- a/src/main/main-runtime-support.ts +++ b/src/main/main-runtime-support.ts @@ -140,8 +140,8 @@ export function createMainRuntimeSupport(state: MainRuntimeState) { function seedDefaultAutomationRules(companyId: string) { const db = requireDb(); seedDefaultAutomationRulesImpl(companyId, { - listAutomationRuleNames: (targetCompanyId) => db.listAutomationRuleNames(targetCompanyId), - saveAutomationRule: (input) => db.saveAutomationRule(input), + listAutomationRuleNames: (targetCompanyId) => db.automationMod.listAutomationRuleNames(targetCompanyId), + saveAutomationRule: (input) => db.automationMod.saveAutomationRule(input), }); } diff --git a/src/main/messaging/dispatch.ts b/src/main/messaging/dispatch.ts index ef24ae31..a606ccc0 100644 --- a/src/main/messaging/dispatch.ts +++ b/src/main/messaging/dispatch.ts @@ -134,7 +134,7 @@ export function validateAgentMessageInput( input: NormalizedAgentMessageInput, ): MessageValidationError | null { // Human sender (BOARD_READER_ID) is always a valid sender — skip agent ownership check. - if (!input.fromHumanUser && !db.belongsToCompany("agents", input.fromAgentId, input.companyId)) { + if (!input.fromHumanUser && !db.settings.belongsToCompany("agents", input.fromAgentId, input.companyId)) { return { code: "INVALID_COMPANY_REFERENCE", message: "The message sender does not belong to the current company.", @@ -154,7 +154,7 @@ export function validateAgentMessageInput( }; } - if (input.toAgentId && input.toAgentId !== HUMAN_SENDER_ID && !db.belongsToCompany("agents", input.toAgentId, input.companyId)) { + if (input.toAgentId && input.toAgentId !== HUMAN_SENDER_ID && !db.settings.belongsToCompany("agents", input.toAgentId, input.companyId)) { return { code: "INVALID_COMPANY_REFERENCE", message: "The message recipient does not belong to the current company.", @@ -168,7 +168,7 @@ export function validateAgentMessageInput( message: "Project messages require a project channel target.", }; } - if (!db.belongsToCompany("projects", input.channelTargetId, input.companyId)) { + if (!db.settings.belongsToCompany("projects", input.channelTargetId, input.companyId)) { return { code: "INVALID_COMPANY_REFERENCE", message: "The selected project channel does not belong to the current company.", @@ -201,7 +201,7 @@ function resolveProjectWakeTargets( input: NormalizedAgentMessageInput, ): string[] { if (!input.channelTargetId) return []; - const project = db.getProjectForUpdate(input.channelTargetId, input.companyId); + const project = db.entityCrud.getProjectForUpdate(input.channelTargetId, input.companyId); const participantIds = new Set(); if (project?.leadAgentId) participantIds.add(project.leadAgentId); for (const assigneeId of db.tasks.listProjectTaskAssigneeIds(input.channelTargetId, input.companyId)) { @@ -367,7 +367,7 @@ export function dispatchAgentMessage( continue; } const runId = deps.wakeAgentIfPossible(wakeTarget, normalized.companyId, wakeTrigger); - const hasQueuedWake = !runId && deps.db.listWakeupRequests(normalized.companyId, { + const hasQueuedWake = !runId && deps.db.wakeup.listWakeupRequests(normalized.companyId, { agentId: wakeTarget, limit: 10, }).some((request) => request.status === "queued" || request.status === "deferred"); diff --git a/src/main/onboarding-service.ts b/src/main/onboarding-service.ts index 6c49029f..ce802f80 100644 --- a/src/main/onboarding-service.ts +++ b/src/main/onboarding-service.ts @@ -1,7 +1,7 @@ import type { AppDatabase } from "./database"; import { getConnectorExecutionReadinessIssue, isConnectorExecutionReady } from "@shared/connector-policy"; import type { ConnectorId, ConnectorRecord } from "@shared/types"; -import { validateConnectorModelSelection } from "./connector-model-policy"; +import { validateConnectorModelSelection, connectorPolicyDepsFromDb } from "./connector-model-policy"; interface OnboardingBootstrapSummary { companyId: string; @@ -113,7 +113,7 @@ export function createOnboardingService({ const trimmedPath = workspacePath.trim(); if (!trimmedPath) return null; const wsLocale = getLocale(); - return db.saveWorkspace({ + return db.workspacesMod.saveWorkspace({ companyId, name: wsLocale === "zh" ? "默认工作区" : "Default Workspace", localPath: trimmedPath, @@ -158,7 +158,7 @@ export function createOnboardingService({ ? `Turn the founding goal "${activeGoal.title}" into an execution plan, delegation map, and the first wave of real work.` : "Establish the first operating plan, assign work, and start the company execution loop."; - const projectId = db.saveProject({ + const projectId = db.entityCrud.saveProject({ companyId, goalId: activeGoal?.id ?? null, name: kickoffProjectName, @@ -217,7 +217,7 @@ export function createOnboardingService({ } function bootstrapOnboarding(input: BootstrapInput): OnboardingBootstrapSummary { - const connectors = db.listSnapshot({ companyId: null }).connectors; + const connectors = db.snap.listSnapshot({ companyId: null }).connectors; const selectedConnectorId = resolveOnboardingConnector(connectors, input.connectorId); const companyId = db.company.saveCompany({ @@ -234,13 +234,13 @@ export function createOnboardingService({ // pre-commit; the pool settings arrive in the input payload and // land in the DB only once a company exists to scope them. for (const pool of input.connectorPoolSelections ?? []) { - db.setDisabledModels(companyId, pool.connectorId, pool.disabledModels); + db.settings.setDisabledModels(companyId, pool.connectorId, pool.disabledModels); } // Validate the CEO model selection against the now-persisted pool // scoped to this new company. Must run AFTER the pool is stored, // otherwise the validator sees an empty pool and accepts anything. - const modelSelection = validateConnectorModelSelection(db, companyId, selectedConnectorId, input.connectorModel ?? null); + const modelSelection = validateConnectorModelSelection(connectorPolicyDepsFromDb(db), companyId, selectedConnectorId, input.connectorModel ?? null); if (!modelSelection.ok) { throw new Error(modelSelection.error); } @@ -249,7 +249,7 @@ export function createOnboardingService({ const goalTitle = input.goalTitle.trim(); if (goalTitle) { - db.saveGoal({ + db.entityCrud.saveGoal({ companyId, title: goalTitle, description: input.goalDescription.trim() || `Achieve: ${goalTitle}`, @@ -280,7 +280,7 @@ export function createOnboardingService({ roleLevel: "c_suite", }); db.agents.setHeartbeat(ceoId, true, 120); - db.setCurrentCompany(companyId); + db.settings.setCurrentCompany(companyId); seedDefaultAutomationRules(companyId); if (scanAndAssignSkills) { @@ -292,7 +292,7 @@ export function createOnboardingService({ const kickoff = createInitialExecutionLoop(companyId, workspaceId); - db.addActivity({ + db.activityMod.addActivity({ companyId, actor: "board", action: "company.bootstrapped", diff --git a/src/main/orchestration/heartbeat.ts b/src/main/orchestration/heartbeat.ts index aea1ca7a..c5192c9b 100644 --- a/src/main/orchestration/heartbeat.ts +++ b/src/main/orchestration/heartbeat.ts @@ -41,7 +41,7 @@ const SKIPPED = (reason: HeartbeatSkipReason): HeartbeatStartResult => ({ export function createHeartbeat( deps: OrchestrationDependencies, - resolveWorkspace: (task: { workspaceId: string | null; projectId?: string | null } | null, agent: { workspaceId: string | null }, companyId: string) => { workspace: ReturnType; lockMode: "exclusive" | "shared" | "none"; source: string }, + resolveWorkspace: (task: { workspaceId: string | null; projectId?: string | null } | null, agent: { workspaceId: string | null }, companyId: string) => { workspace: ReturnType; lockMode: "exclusive" | "shared" | "none"; source: string }, getRunEligibilityFailure: (agent: AgentRecord, connector: ConnectorRecord, projectId?: string | null) => string | null, dispatchRunToWorker: (params: RunDispatchParams) => void, hasActiveRun: (filter: { taskId?: string; agentId?: string; companyId: string }) => boolean, @@ -74,14 +74,14 @@ export function createHeartbeat( return SKIPPED("cross_company"); } const pick = pickModelForAgent(agent, { - getConnector: (id) => db.getConnector(id), - getDisabledModels: (id) => db.getDisabledModels(agent.companyId, id), + getConnector: (id) => db.connectorsCrud.getConnector(id), + getDisabledModels: (id) => db.settings.getDisabledModels(agent.companyId, id), listModelCapabilities: () => db.modelCapabilities.listModelCapabilities(), readConnectors: () => db.connectorsCrud.readConnectors(), }); if (!pick.ok) { logger.warn(`[heartbeat] Model pick failed for agent ${agent.name}: ${pick.message}`); - db.addActivity({ + db.activityMod.addActivity({ companyId, actor: "system", action: "heartbeat.model_pick_failed", @@ -92,7 +92,7 @@ export function createHeartbeat( return SKIPPED("model_pick_failed"); } const assignment = pick.assignment; - const connector = db.getConnector(assignment.connectorId); + const connector = db.connectorsCrud.getConnector(assignment.connectorId); const effectiveAgentForEligibility: AgentRecord = { ...agent, model: assignment.modelId }; const eligibilityFailure = getRunEligibilityFailure(effectiveAgentForEligibility, connector, null); if (eligibilityFailure) { @@ -168,7 +168,7 @@ export function createHeartbeat( if (candidateTask) { const checkoutSuccess = db.tasks.checkoutTask(candidateTask.id, runId); if (!checkoutSuccess) { - db.addActivity({ + db.activityMod.addActivity({ companyId, actor: "system", action: "execution.checkout_rejected", @@ -187,7 +187,7 @@ export function createHeartbeat( const isCeo = !agent.reportsTo; const isManager = db.agents.getDirectReports(agent.id, companyId).length > 0; - const goalCount = db.countActiveGoalsForCompany(companyId); + const goalCount = db.entityCrud.countActiveGoalsForCompany(companyId); const taskCount = db.tasks.countActiveTasksForCompany(companyId); const companyNeedsBootstrap = goalCount > 0 && taskCount < goalCount; @@ -227,7 +227,7 @@ export function createHeartbeat( workspaceMode: workspaceResolution.lockMode, companyName: companyBasic?.name ?? "Unknown", companyDescription: companyBasic?.description ?? "", - snapshot: db.listSnapshot(), + snapshot: db.snap.listSnapshot(), trigger, resolvedModel: assignment.modelId, }); @@ -245,7 +245,7 @@ export function createHeartbeat( ); } - db.addActivity({ + db.activityMod.addActivity({ companyId, actor: "system", action: "heartbeat.triggered", @@ -284,16 +284,16 @@ export function createHeartbeat( const agentBusy = hasActiveRun({ agentId, companyId }); if (agentBusy) { - db.enqueueWakeupRequest(companyId, agentId, trigger, `Wake for agent: ${trigger}`, context); + db.wakeup.enqueueWakeupRequest(companyId, agentId, trigger, `Wake for agent: ${trigger}`, context); return null; } const result = startAgentHeartbeatRun(agentId, companyId, trigger); if (result.status === "skipped") { - db.enqueueWakeupRequest(companyId, agentId, trigger, `Wake for agent (deferred:${result.reason}): ${trigger}`, context); + db.wakeup.enqueueWakeupRequest(companyId, agentId, trigger, `Wake for agent (deferred:${result.reason}): ${trigger}`, context); return null; } - db.completePendingWakeupRequests(agentId, companyId, result.runId); + db.wakeup.completePendingWakeupRequests(agentId, companyId, result.runId); return result.runId; } diff --git a/src/main/orchestration/resolve-workspace.ts b/src/main/orchestration/resolve-workspace.ts index aa95f76f..58bb4271 100644 --- a/src/main/orchestration/resolve-workspace.ts +++ b/src/main/orchestration/resolve-workspace.ts @@ -31,28 +31,28 @@ export function resolveWorkspaceWithLockMode( agent: { workspaceId: string | null }, companyId: string, ): { - workspace: ReturnType; + workspace: ReturnType; lockMode: WorkspaceLockMode; source: "task" | "agent" | "project" | "company_fallback" | "none"; } { if (task) { - const taskWorkspace = db.getWorkspace(task.workspaceId); + const taskWorkspace = db.workspacesMod.getWorkspace(task.workspaceId); if (taskWorkspace) { return { workspace: taskWorkspace, lockMode: "exclusive", source: "task" }; } } - const agentWorkspace = db.getWorkspace(agent.workspaceId); + const agentWorkspace = db.workspacesMod.getWorkspace(agent.workspaceId); if (agentWorkspace) { return { workspace: agentWorkspace, lockMode: "exclusive", source: "agent" }; } - const projectWorkspace = db.getProjectPrimaryWorkspace(task?.projectId ?? null, companyId); + const projectWorkspace = db.workspacesMod.getProjectPrimaryWorkspace(task?.projectId ?? null, companyId); if (projectWorkspace) { return { workspace: projectWorkspace, lockMode: "exclusive", source: "project" }; } - const fallbackWorkspace = db.getAnyCompanyWorkspace(companyId); + const fallbackWorkspace = db.workspacesMod.getAnyCompanyWorkspace(companyId); if (fallbackWorkspace) { return { workspace: fallbackWorkspace, lockMode: "exclusive", source: "company_fallback" }; } diff --git a/src/main/orchestration/run-dispatch.ts b/src/main/orchestration/run-dispatch.ts index 0b8c3ef3..20a1fc3c 100644 --- a/src/main/orchestration/run-dispatch.ts +++ b/src/main/orchestration/run-dispatch.ts @@ -47,7 +47,15 @@ export function createRunDispatch(deps: OrchestrationDependencies) { logger.info(`[run] Agent ${agent.name} connector ${connector.id} blocked: ${issue}`); return "CONNECTOR_NOT_READY"; } - const modelSelection = validateConnectorModelSelection(db, agent.companyId, connector.id, agent.model ?? null); + const modelSelection = validateConnectorModelSelection( + { + getConnector: (id) => db.connectorsCrud.getConnector(id), + getDisabledModels: (cid, connId) => db.settings.getDisabledModels(cid, connId), + }, + agent.companyId, + connector.id, + agent.model ?? null, + ); if (!modelSelection.ok) { logger.info(`[run] Agent ${agent.name} connector model blocked: ${modelSelection.error}`); return "CONNECTOR_MODEL_NOT_ALLOWED"; @@ -81,7 +89,7 @@ export function createRunDispatch(deps: OrchestrationDependencies) { if ( params.workspaceMode === "exclusive" && params.workspace && - !db.claimWorkspaceLock(params.workspace.id, params.runId) + !db.workspacesMod.claimWorkspaceLock(params.workspace.id, params.runId) ) { db.runs.finishRun({ runId: params.runId, diff --git a/src/main/orchestration/scheduler-heartbeat.ts b/src/main/orchestration/scheduler-heartbeat.ts index 13e3c972..22cf6dc3 100644 --- a/src/main/orchestration/scheduler-heartbeat.ts +++ b/src/main/orchestration/scheduler-heartbeat.ts @@ -94,7 +94,7 @@ export function createHeartbeatScheduler( tryWake(agent.id, company.id, agent.name, "timer"); } - const goalCount = db.countActiveGoalsForCompany(company.id); + const goalCount = db.entityCrud.countActiveGoalsForCompany(company.id); // Bootstrap when the company has active goals but no work is // actually advancing. The previous gate ("taskCount === 0") // missed the case where every task is in_review or blocked — diff --git a/src/main/orchestration/scheduler-housekeeping.ts b/src/main/orchestration/scheduler-housekeeping.ts index 3f360db0..38afe206 100644 --- a/src/main/orchestration/scheduler-housekeeping.ts +++ b/src/main/orchestration/scheduler-housekeeping.ts @@ -32,10 +32,10 @@ export function createHousekeepingScheduler( for (const company of activeCompanies) { try { const currentMonth = new Date().toISOString().slice(0, 7); - const lastResetMonth = db.getLastBudgetResetMonth(company.id) ?? ""; + const lastResetMonth = db.settings.getLastBudgetResetMonth(company.id) ?? ""; if (currentMonth !== lastResetMonth) { db.agents.resetMonthlyBudgets(company.id); - db.setLastBudgetResetMonth(company.id, currentMonth); + db.settings.setLastBudgetResetMonth(company.id, currentMonth); logger.info(`[housekeeping] Reset monthly budgets for company ${company.name} (${currentMonth})`); } @@ -81,7 +81,7 @@ export function createHousekeepingScheduler( } try { - const staleWakeups = db.purgeExpiredWakeupRequests(); + const staleWakeups = db.wakeup.purgeExpiredWakeupRequests(); if (staleWakeups > 0) { logger.info(`[housekeeping] Purged ${staleWakeups} expired wakeup request(s)`); } diff --git a/src/main/orchestration/scheduler-lock-recovery.ts b/src/main/orchestration/scheduler-lock-recovery.ts index 5e977934..527be271 100644 --- a/src/main/orchestration/scheduler-lock-recovery.ts +++ b/src/main/orchestration/scheduler-lock-recovery.ts @@ -16,7 +16,7 @@ export function createLockRecoveryScheduler(deps: OrchestrationDependencies) { try { const recovered = db.recoverStaleLocks(); for (const task of recovered) { - db.addActivity({ + db.activityMod.addActivity({ companyId: task.companyId, actor: "system", action: "execution.lock_recovered", @@ -31,7 +31,7 @@ export function createLockRecoveryScheduler(deps: OrchestrationDependencies) { } try { - const staleReleased = db.releaseStaleWorkspaceLocks(); + const staleReleased = db.workspacesMod.releaseStaleWorkspaceLocks(); if (staleReleased > 0) { logger.info(`[lock-recovery] Released ${staleReleased} stale workspace lock(s)`); publishDomainChanged(); diff --git a/src/main/orchestration/task-runner.ts b/src/main/orchestration/task-runner.ts index 69aecce6..76fae0f8 100644 --- a/src/main/orchestration/task-runner.ts +++ b/src/main/orchestration/task-runner.ts @@ -8,7 +8,7 @@ import { pickModelForAgent } from "../model-dispatch"; export function createTaskRunner( deps: OrchestrationDependencies, - resolveWorkspace: (task: { workspaceId: string | null; projectId?: string | null } | null, agent: { workspaceId: string | null }, companyId: string) => { workspace: ReturnType; lockMode: "exclusive" | "shared" | "none"; source: string }, + resolveWorkspace: (task: { workspaceId: string | null; projectId?: string | null } | null, agent: { workspaceId: string | null }, companyId: string) => { workspace: ReturnType; lockMode: "exclusive" | "shared" | "none"; source: string }, getRunEligibilityFailure: (agent: AgentRecord, connector: ConnectorRecord, projectId?: string | null) => string | null, dispatchRunToWorker: (params: RunDispatchParams) => void, hasActiveRun: (filter: { taskId?: string; agentId?: string; companyId: string }) => boolean, @@ -48,8 +48,8 @@ export function createTaskRunner( return fail("AGENT_COMPANY_MISMATCH", "The assigned agent does not belong to the task company."); } const pick = pickModelForAgent(agent, { - getConnector: (id) => db.getConnector(id), - getDisabledModels: (id) => db.getDisabledModels(agent.companyId, id), + getConnector: (id) => db.connectorsCrud.getConnector(id), + getDisabledModels: (id) => db.settings.getDisabledModels(agent.companyId, id), listModelCapabilities: () => db.modelCapabilities.listModelCapabilities(), readConnectors: () => db.connectorsCrud.readConnectors(), }); @@ -57,7 +57,7 @@ export function createTaskRunner( return fail("MODEL_PICK_FAILED", pick.message); } const assignment = pick.assignment; - const connector = db.getConnector(assignment.connectorId); + const connector = db.connectorsCrud.getConnector(assignment.connectorId); const effectiveAgentForEligibility: AgentRecord = { ...agent, model: assignment.modelId }; const eligibilityFailure = getRunEligibilityFailure(effectiveAgentForEligibility, connector, task.projectId ?? null); if (eligibilityFailure === "AGENT_NOT_RUNNABLE") { @@ -119,11 +119,11 @@ export function createTaskRunner( workspaceMode: workspaceResolution.lockMode, companyName: companyBasic?.name ?? "Unknown", companyDescription: companyBasic?.description ?? "", - snapshot: db.listSnapshot(), + snapshot: db.snap.listSnapshot(), trigger, resolvedModel: assignment.modelId, }); - db.addActivity({ + db.activityMod.addActivity({ companyId: task.companyId, actor, action: "run.queued", @@ -140,7 +140,7 @@ export function createTaskRunner( try { const recovered = db.recoverStaleLocks(); for (const task of recovered) { - db.addActivity({ + db.activityMod.addActivity({ companyId: task.companyId, actor: "system", action: "execution.lock_recovered", @@ -160,7 +160,7 @@ export function createTaskRunner( if (isQuitting() || !hasWorkerReady()) { return; } - const snapshot = db.listSnapshot(); + const snapshot = db.snap.listSnapshot(); const companiesById = new Map(snapshot.companies.map((company) => [company.id, company])); for (const run of db.runs.listQueuedRuns()) { @@ -182,7 +182,7 @@ export function createTaskRunner( try { const task = run.taskId ? db.entityCrud.getTask(run.taskId) : null; const agent = db.entityCrud.getAgent(run.agentId); - const connector = db.getConnector(run.connectorId); + const connector = db.connectorsCrud.getConnector(run.connectorId); const eligibilityFailure = getRunEligibilityFailure(agent, connector, task?.projectId ?? null); if (eligibilityFailure) { if (eligibilityFailure === "CONNECTOR_MODEL_NOT_ALLOWED") { @@ -196,7 +196,7 @@ export function createTaskRunner( model: null, unattributedCost: true, }); - db.addActivity({ + db.activityMod.addActivity({ companyId: run.companyId, actor: "system", action: "run.recovery_blocked", diff --git a/src/main/orchestration/turn-context.ts b/src/main/orchestration/turn-context.ts index 35f3440c..6e4583a2 100644 --- a/src/main/orchestration/turn-context.ts +++ b/src/main/orchestration/turn-context.ts @@ -23,12 +23,12 @@ export interface RunDispatchParams { // Downstream code (prompt builder, worker payload, session pointer) // branches on this — when null, skip task-scoped state. task: ReturnType | null; - connector: ReturnType; - workspace: ReturnType; + connector: ReturnType; + workspace: ReturnType; workspaceMode: WorkspaceLockMode; companyName: string; companyDescription: string; - snapshot: ReturnType; + snapshot: ReturnType; trigger: string; /** * Model id resolved by `pickModelForAgent` at dispatch time. When the @@ -63,7 +63,7 @@ function assembleApiEnv( trigger: string, deps: Pick, ) { - const env = db.resolveSecretEnv(connector.envBindingText, companyId); + const env = db.entityCrud.resolveSecretEnv(connector.envBindingText, companyId); const apiPort = deps.getApiPort(); const apiUrl = apiPort ? `http://127.0.0.1:${apiPort}` : ""; const apiKey = apiUrl ? deps.createRunToken(agentId, companyId, runId) : ""; @@ -136,7 +136,14 @@ function resolveEnabledConnectorModels( companyId: string, connector: { id: string; model: string | null }, ): string[] { - return getConnectorModelPolicy(db, companyId, connector.id as ConnectorId).enabledModels.map((model) => model.id); + return getConnectorModelPolicy( + { + getConnector: (id) => db.connectorsCrud.getConnector(id), + getDisabledModels: (cid, connId) => db.settings.getDisabledModels(cid, connId), + }, + companyId, + connector.id as ConnectorId, + ).enabledModels.map((model) => model.id); } function resolveSession( @@ -252,7 +259,7 @@ function buildConnectorCatalog( ) { return connectors .filter((c) => c.status === "ready") - .filter((c) => db.hasModelPoolSettings(companyId, c.id)) + .filter((c) => db.settings.hasModelPoolSettings(companyId, c.id)) .map((c) => ({ id: c.id, label: c.label, @@ -301,7 +308,7 @@ export function buildRunPayload(params: RunDispatchParams, deps: TurnContextDeps allTasks: snapshot.tasks.filter((entry) => entry.companyId === companyId), recentComments: normalizeCommentsForPrompt(recentComments), companyDescription, - socialAccounts: db.listSocialAccounts(companyId).map((account) => ({ + socialAccounts: db.social.listSocialAccounts(companyId).map((account) => ({ id: account.id, platform: account.platform, accountName: account.accountName, @@ -315,7 +322,7 @@ export function buildRunPayload(params: RunDispatchParams, deps: TurnContextDeps allConnectors: buildConnectorCatalog(db, companyId, snapshot.connectors), sessionHandoff, skills, - companySkillCatalog: db.listCompanySkills(companyId).map((skill) => ({ + companySkillCatalog: db.skills.listCompanySkills(companyId).map((skill) => ({ name: skill.name, slug: skill.slug, description: skill.description, @@ -328,7 +335,15 @@ export function buildRunPayload(params: RunDispatchParams, deps: TurnContextDeps const thinkingEffort = extractThinkingEffort(agent.metadataJson, deps.logger, agent.id); const modelForRun = params.resolvedModel !== undefined ? params.resolvedModel : agent.model; - const modelSelection = validateConnectorModelSelection(db, companyId, connector.id, modelForRun); + const modelSelection = validateConnectorModelSelection( + { + getConnector: (id) => db.connectorsCrud.getConnector(id), + getDisabledModels: (cid, connId) => db.settings.getDisabledModels(cid, connId), + }, + companyId, + connector.id, + modelForRun, + ); if (!modelSelection.ok) { throw new Error(modelSelection.error); } diff --git a/src/main/run-housekeeping.ts b/src/main/run-housekeeping.ts index dcc3ee70..58b6a8ca 100644 --- a/src/main/run-housekeeping.ts +++ b/src/main/run-housekeeping.ts @@ -105,7 +105,7 @@ export function createRunHousekeeping({ db, logger, threadIntegration }: RunHous ); } - db.saveKnowledgeEntry({ + db.knowledgeMod.saveKnowledgeEntry({ companyId, category: "lesson_learned", topic, @@ -121,7 +121,7 @@ export function createRunHousekeeping({ db, logger, threadIntegration }: RunHous // 3. On failure, record an activity log entry if (status === "failed") { try { - db.addActivity({ + db.activityMod.addActivity({ companyId, actor: agentId, action: "run.failed", diff --git a/src/main/service-graph.ts b/src/main/service-graph.ts index e3a3c33a..8fc70429 100644 --- a/src/main/service-graph.ts +++ b/src/main/service-graph.ts @@ -118,7 +118,7 @@ export function createServiceGraph(deps: ServiceGraphDependencies) { } function init() { - nativeTheme.themeSource = db.listSnapshot().theme; + nativeTheme.themeSource = db.snap.listSnapshot().theme; leaf = createLeafServices({ db, logger, notify, publishDomainChanged }); diff --git a/src/main/service-graph/create-desktop-shell.ts b/src/main/service-graph/create-desktop-shell.ts index 5b6f84d9..699a96ee 100644 --- a/src/main/service-graph/create-desktop-shell.ts +++ b/src/main/service-graph/create-desktop-shell.ts @@ -66,13 +66,13 @@ export function createDesktopShell({ properties: ["openDirectory", "createDirectory"], })); if (target.canceled || target.filePaths.length === 0) return; - await db.exportBackup(`${target.filePaths[0]!}/agentcompany-backup-${Date.now()}`); + await db.backupMod.exportBackup(`${target.filePaths[0]!}/agentcompany-backup-${Date.now()}`); }, onShortcut: (action) => emitEvent({ type: "shortcut", action }), notify, getActiveRunCount: () => { try { - const snapshot = db.listSnapshot(); + const snapshot = db.snap.listSnapshot(); return snapshot.runs.filter((r) => r.status === "running" || r.status === "queued").length; } catch (error) { logger.warn("[service-graph] getActiveRunCount failed:", error); diff --git a/src/main/service-graph/create-lifecycle.ts b/src/main/service-graph/create-lifecycle.ts index cde1c849..930936df 100644 --- a/src/main/service-graph/create-lifecycle.ts +++ b/src/main/service-graph/create-lifecycle.ts @@ -69,7 +69,7 @@ export function createLifecycle(deps: LifecycleDeps) { function listActiveRunIds(): string[] { try { - return db.listSnapshot().runs + return db.snap.listSnapshot().runs .filter((r) => r.status === "running" || r.status === "queued") .map((r) => r.id); } catch (err) { diff --git a/src/main/skills/auto-assign-on-hire.ts b/src/main/skills/auto-assign-on-hire.ts index d4a2b13f..9801943f 100644 --- a/src/main/skills/auto-assign-on-hire.ts +++ b/src/main/skills/auto-assign-on-hire.ts @@ -72,16 +72,16 @@ export function autoAssignSkillsForHire( return { assignedSkillIds: [] }; } - const alreadyAssigned = new Set(db.listAgentSkillAssignments(input.agentId)); + const alreadyAssigned = new Set(db.skills.listAgentSkillAssignments(input.agentId)); const wanted = new Set(categories); - const catalogue = db.listCompanySkills(input.companyId); + const catalogue = db.skills.listCompanySkills(input.companyId); const assigned: string[] = []; for (const skill of catalogue) { if (alreadyAssigned.has(skill.id)) continue; const category = readBuiltinCategory(skill.metadata); if (!category || !wanted.has(category)) continue; - db.assignSkillToAgent(input.agentId, skill.id); + db.skills.assignSkillToAgent(input.agentId, skill.id); assigned.push(skill.id); } diff --git a/src/main/skills/crud.ts b/src/main/skills/crud.ts index ed89afb0..0fdf408d 100644 --- a/src/main/skills/crud.ts +++ b/src/main/skills/crud.ts @@ -23,11 +23,11 @@ export function createSkillCrud({ db, logger, cleanupSkillFiles, parseGitHubUrl } function listForCompany(companyId: string): SkillRecord[] { - return db.listCompanySkills(companyId); + return db.skills.listCompanySkills(companyId); } function getDetail(companyId: string, skillId: string): DesktopResult { - const skill = db.getCompanySkill(companyId, skillId); + const skill = db.skills.getCompanySkill(companyId, skillId); if (!skill) return fail("not_found", `Skill ${skillId} not found`); return ok(skill); } @@ -36,12 +36,12 @@ export function createSkillCrud({ db, logger, cleanupSkillFiles, parseGitHubUrl companyId: string, skillId: string, ): Promise> { - const skill = db.getCompanySkill(companyId, skillId); + const skill = db.skills.getCompanySkill(companyId, skillId); if (!skill) return fail("not_found", `Skill ${skillId} not found`); try { await cleanupSkillFiles(companyId, skill.slug); - db.deleteCompanySkill(companyId, skillId); + db.skills.deleteCompanySkill(companyId, skillId); logger.info(`Skill deleted: ${skill.key}`); return ok(undefined); } catch (err) { @@ -56,9 +56,9 @@ export function createSkillCrud({ db, logger, cleanupSkillFiles, parseGitHubUrl agentId: string, skillId: string, ): DesktopResult { - const skill = db.getCompanySkill(companyId, skillId); + const skill = db.skills.getCompanySkill(companyId, skillId); if (!skill) return fail("not_found", `Skill ${skillId} not found for company`); - db.assignSkillToAgent(agentId, skillId); + db.skills.assignSkillToAgent(agentId, skillId); return ok(undefined); } @@ -67,7 +67,7 @@ export function createSkillCrud({ db, logger, cleanupSkillFiles, parseGitHubUrl agentId: string, skillId: string, ): DesktopResult { - db.removeSkillFromAgent(agentId, skillId); + db.skills.removeSkillFromAgent(agentId, skillId); return ok(undefined); } @@ -75,7 +75,7 @@ export function createSkillCrud({ db, logger, cleanupSkillFiles, parseGitHubUrl companyId: string, skillId: string, ): Promise> { - const skill = db.getCompanySkill(companyId, skillId); + const skill = db.skills.getCompanySkill(companyId, skillId); if (!skill) return fail("not_found", `Skill ${skillId} not found`); if (skill.sourceType !== "github" && skill.sourceType !== "skills_sh") { diff --git a/src/main/skills/import-github.ts b/src/main/skills/import-github.ts index 026e3d93..b9e33ab9 100644 --- a/src/main/skills/import-github.ts +++ b/src/main/skills/import-github.ts @@ -155,7 +155,7 @@ export function createGitHubImporter({ db, logger, skillFilesDir }: GitHubImport await materializeSkillFiles(companyId, rawSlug, ghSkill.markdown, inventory); - const { isUpdate } = db.upsertCompanySkill({ + const { isUpdate } = db.skills.upsertCompanySkill({ id: skillId, companyId, key, diff --git a/src/main/skills/import-local.ts b/src/main/skills/import-local.ts index 39ecd7bd..f3759eec 100644 --- a/src/main/skills/import-local.ts +++ b/src/main/skills/import-local.ts @@ -69,7 +69,7 @@ export async function importFromLocal( const skillId = randomUUID(); await materializeSkillFiles(skillFilesDir(companyId, rawSlug), markdown, inventory); - const { isUpdate } = db.upsertCompanySkill({ + const { isUpdate } = db.skills.upsertCompanySkill({ id: skillId, companyId, key, @@ -121,7 +121,7 @@ export async function importFromLocal( const skillId = randomUUID(); await materializeSkillFiles(skillFilesDir(companyId, rawSlug), markdown, inventory); - const { isUpdate } = db.upsertCompanySkill({ + const { isUpdate } = db.skills.upsertCompanySkill({ id: skillId, companyId, key, diff --git a/src/main/skills/import-url.ts b/src/main/skills/import-url.ts index 41ced222..8fc94eca 100644 --- a/src/main/skills/import-url.ts +++ b/src/main/skills/import-url.ts @@ -59,7 +59,7 @@ export async function importFromUrl( const skillId = randomUUID(); await materializeSkillFiles(skillFilesDir(companyId, rawSlug), markdown, inventory); - const { isUpdate } = db.upsertCompanySkill({ + const { isUpdate } = db.skills.upsertCompanySkill({ id: skillId, companyId, key, diff --git a/src/main/skills/runtime.ts b/src/main/skills/runtime.ts index 8f6a651b..ab936d79 100644 --- a/src/main/skills/runtime.ts +++ b/src/main/skills/runtime.ts @@ -35,9 +35,9 @@ export function createSkillRuntime({ db, logger, skillsRoot, skillFilesDir, impo agentId: string, _connectorId: string, ): ProjectedSkill[] { - const assignedIds = new Set(db.listAgentSkillAssignments(agentId)); + const assignedIds = new Set(db.skills.listAgentSkillAssignments(agentId)); if (assignedIds.size === 0) return []; - const companySkills = db.listCompanySkills(companyId); + const companySkills = db.skills.listCompanySkills(companyId); const skillPaths = companySkills .filter((s) => assignedIds.has(s.id)) .map((s) => skillFilesDir(companyId, s.slug)); @@ -82,7 +82,7 @@ export function createSkillRuntime({ db, logger, skillsRoot, skillFilesDir, impo } } - const workspacePaths = db.listWorkspaceLocalPaths(companyId); + const workspacePaths = db.workspacesMod.listWorkspaceLocalPaths(companyId); for (const wsPath of workspacePaths) { const agentcompanySkillsDir = join(wsPath, ".agentcompany", "skills"); if (!existsSync(agentcompanySkillsDir)) continue; @@ -159,12 +159,12 @@ export function createSkillRuntime({ db, logger, skillsRoot, skillFilesDir, impo const results = await importFromLocal(companyId, categoryDir); for (const result of results) { try { - const skill = db.getCompanySkill(companyId, result.skillId); + const skill = db.skills.getCompanySkill(companyId, result.skillId); if (skill) { const existingMeta = skill.metadata ? JSON.parse(typeof skill.metadata === "string" ? skill.metadata : JSON.stringify(skill.metadata)) : {}; existingMeta.builtinCategory = category; existingMeta.sourceKind = "builtin"; - db.updateCompanySkillMetadata(companyId, result.skillId, JSON.stringify(existingMeta)); + db.skills.updateCompanySkillMetadata(companyId, result.skillId, JSON.stringify(existingMeta)); } } catch (error) { logger.warn( diff --git a/src/main/task-quality-gates.ts b/src/main/task-quality-gates.ts index ada6b892..8e72e6d5 100644 --- a/src/main/task-quality-gates.ts +++ b/src/main/task-quality-gates.ts @@ -28,8 +28,10 @@ interface CapabilityMatchDb { entityCrud: { getAgent(id: string): { name: string; capabilities: string }; }; - listAgentSkillAssignments(id: string): string[]; - getCompanySkill(companyId: string, skillId: string): { name: string; description: string | null; slug: string } | null; + skills: { + listAgentSkillAssignments(id: string): string[]; + getCompanySkill(companyId: string, skillId: string): { name: string; description: string | null; slug: string } | null; + }; } @@ -135,9 +137,9 @@ export function checkCapabilityMatch( const capabilityOverlap = taskWords.filter((w) => capWords.includes(w)).length; let skillOverlap = 0; - const skillIds = db.listAgentSkillAssignments(agentId); + const skillIds = db.skills.listAgentSkillAssignments(agentId); for (const skillId of skillIds) { - const skill = db.getCompanySkill(companyId, skillId); + const skill = db.skills.getCompanySkill(companyId, skillId); if (skill) { const skillWords = tokenize(`${skill.name} ${skill.description ?? ""} ${skill.slug}`); skillOverlap += taskWords.filter((w) => skillWords.includes(w)).length; diff --git a/src/main/thread-integration/index.ts b/src/main/thread-integration/index.ts index ad811ed8..b65754c5 100644 --- a/src/main/thread-integration/index.ts +++ b/src/main/thread-integration/index.ts @@ -25,7 +25,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh const thread = ensureTaskThread(db, taskId, companyId, task.title); - db.addThreadEvent({ + db.threads.addThreadEvent({ threadId: thread.id, actorId: task.assigneeAgentId ?? "system", actorType: task.assigneeAgentId ? "agent" : "system", @@ -36,11 +36,11 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh // Auto-enroll assignee and human as participants if (task.assigneeAgentId) { - db.addParticipant({ threadId: thread.id, participantId: task.assigneeAgentId, participantType: "agent", role: "owner" }); - db.subscribe({ threadId: thread.id, subscriberId: task.assigneeAgentId, subscriberType: "agent" }); + db.threads.addParticipant({ threadId: thread.id, participantId: task.assigneeAgentId, participantType: "agent", role: "owner" }); + db.threads.subscribe({ threadId: thread.id, subscriberId: task.assigneeAgentId, subscriberType: "agent" }); } - db.addParticipant({ threadId: thread.id, participantId: "user", participantType: "user", role: "observer" }); - db.subscribe({ threadId: thread.id, subscriberId: "user", subscriberType: "user" }); + db.threads.addParticipant({ threadId: thread.id, participantId: "user", participantType: "user", role: "observer" }); + db.threads.subscribe({ threadId: thread.id, subscriberId: "user", subscriberType: "user" }); } catch (err) { logger.error(`[thread-integration] onTaskCreated failed: ${err instanceof Error ? err.message : String(err)}`); } @@ -58,7 +58,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh const thread = ensureTaskThread(db, taskId, companyId, task.title); - db.addThreadEvent({ + db.threads.addThreadEvent({ threadId: thread.id, actorId: task.assigneeAgentId ?? "system", actorType: task.assigneeAgentId ? "agent" : "system", @@ -72,7 +72,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh const mode = db.company.getCompanyExecutionMode(companyId) as ExecutionMode; if (mode === "ai_cofounder") { const reason = `@Board review needed: Task "${task.title}" is ready for your review.`; - db.setThreadWaitState({ + db.threads.setThreadWaitState({ threadId: thread.id, companyId, waitingFor: "user_review", @@ -85,10 +85,10 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh // Resolve wait states when task moves past review if (previousStatus === "in_review" && (newStatus === "done" || newStatus === "in_progress")) { - const waitStates = db.listThreadWaitStates(companyId); + const waitStates = db.threads.listThreadWaitStates(companyId); for (const ws of waitStates) { if (ws.threadId === thread.id && ws.waitingFor === "user_review") { - db.resolveThreadWaitState(ws.id, companyId); + db.threads.resolveThreadWaitState(ws.id, companyId); } } } @@ -110,7 +110,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh ? ensureTaskThread(db, task.id, companyId, task.title) : ensureApprovalThread(db, approvalId, companyId, summary || approval.payloadSummary); - db.addThreadEvent({ + db.threads.addThreadEvent({ threadId: thread.id, actorId: "system", actorType: "system", @@ -120,17 +120,17 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh }); if (approval.requestedByAgentId) { - db.addParticipant({ threadId: thread.id, participantId: approval.requestedByAgentId, participantType: "agent", role: "owner" }); - db.subscribe({ threadId: thread.id, subscriberId: approval.requestedByAgentId, subscriberType: "agent" }); + db.threads.addParticipant({ threadId: thread.id, participantId: approval.requestedByAgentId, participantType: "agent", role: "owner" }); + db.threads.subscribe({ threadId: thread.id, subscriberId: approval.requestedByAgentId, subscriberType: "agent" }); } - db.addParticipant({ threadId: thread.id, participantId: "user", participantType: "user", role: "observer" }); - db.subscribe({ threadId: thread.id, subscriberId: "user", subscriberType: "user" }); + db.threads.addParticipant({ threadId: thread.id, participantId: "user", participantType: "user", role: "observer" }); + db.threads.subscribe({ threadId: thread.id, subscriberId: "user", subscriberType: "user" }); // In co-founder mode, set wait state for user approval const mode = db.company.getCompanyExecutionMode(companyId) as ExecutionMode; if (mode === "ai_cofounder") { const approvalReason = `@Board approval needed: ${approvalType}`; - db.setThreadWaitState({ + db.threads.setThreadWaitState({ threadId: thread.id, companyId, waitingFor: "user_approval", @@ -152,13 +152,13 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh onApprovalResolved(approvalId: string, companyId: string, decision: string): void { try { // Resolve any wait states linked to this approval - const waitStates = db.listThreadWaitStates(companyId); + const waitStates = db.threads.listThreadWaitStates(companyId); for (const ws of waitStates) { if (ws.relatedApprovalId === approvalId) { - db.resolveThreadWaitState(ws.id, companyId); + db.threads.resolveThreadWaitState(ws.id, companyId); // Add decision event to the thread - db.addThreadEvent({ + db.threads.addThreadEvent({ threadId: ws.threadId, actorId: "user", actorType: "user", @@ -185,7 +185,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh const thread = findTaskThread(db, taskId, companyId); if (!thread) return; - db.addThreadEvent({ + db.threads.addThreadEvent({ threadId: thread.id, actorId: authorAgentId ?? "system", actorType: authorAgentId ? "agent" : "system", @@ -208,7 +208,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh const thread = findTaskThread(db, taskId, companyId); if (!thread) return; - db.addThreadEvent({ + db.threads.addThreadEvent({ threadId: thread.id, actorId: agentId, actorType: "agent", @@ -235,7 +235,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh ? "Execution completed successfully" : `Execution ${status}${errorMessage ? `: ${errorMessage}` : ""}`; - db.addThreadEvent({ + db.threads.addThreadEvent({ threadId: thread.id, actorId: agentId, actorType: "agent", @@ -251,7 +251,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh if (mode === "ai_cofounder") { const task = db.entityCrud.getTask(taskId); const completionReason = `@Board review needed: Agent completed work on "${task.title}" — awaiting your review.`; - db.setThreadWaitState({ + db.threads.setThreadWaitState({ threadId: thread.id, companyId, waitingFor: "user_reply", @@ -280,7 +280,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh try { // Most precise: resolve a single identified wait state if (waitStateId) { - db.resolveThreadWaitState(waitStateId, companyId); + db.threads.resolveThreadWaitState(waitStateId, companyId); return; } @@ -288,7 +288,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh const SCOPED_WAIT_TYPES = new Set(["user_reply", "user_review"]); const isScoped = Boolean(threadId || toAgentId); - const waitStates = db.listThreadWaitStates(companyId); + const waitStates = db.threads.listThreadWaitStates(companyId); for (const ws of waitStates) { if (isScoped) { if (!SCOPED_WAIT_TYPES.has(ws.waitingFor)) continue; @@ -301,7 +301,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh if (threadId) { if (ws.threadId !== threadId) continue; } else if (toAgentId) { - const thread = db.getThread(ws.threadId, companyId); + const thread = db.threads.getThread(ws.threadId, companyId); if (!thread || thread.relatedEntityType !== "task" || !thread.relatedEntityId) continue; try { const task = db.entityCrud.getTask(thread.relatedEntityId); @@ -312,7 +312,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh } } - db.resolveThreadWaitState(ws.id, companyId); + db.threads.resolveThreadWaitState(ws.id, companyId); } } catch (err) { logger.error(`[thread-integration] onHumanMessage failed: ${err instanceof Error ? err.message : String(err)}`); @@ -321,7 +321,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh hasUnresolvedWaitStates(companyId: string): boolean { try { - const waitStates = db.listThreadWaitStates(companyId); + const waitStates = db.threads.listThreadWaitStates(companyId); return waitStates.length > 0; } catch (err) { logger.warn(`[thread-integration] hasUnresolvedWaitStates query failed for company ${companyId}:`, err instanceof Error ? err.message : String(err)); @@ -337,7 +337,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh */ hasUnresolvedWaitStatesForAgent(companyId: string, agentId: string): boolean { try { - return db.hasUnresolvedWaitStateForAgent(companyId, agentId); + return db.threads.hasUnresolvedWaitStateForAgent(companyId, agentId); } catch (err) { logger.warn(`[thread-integration] hasUnresolvedWaitStatesForAgent query failed (company=${companyId} agent=${agentId}):`, err instanceof Error ? err.message : String(err)); return false; @@ -350,7 +350,7 @@ export function createThreadIntegration({ db, logger, emitEvent, publishDomainCh */ countUnresolvedWaitStates(companyId: string): number { try { - return db.listThreadWaitStates(companyId).length; + return db.threads.listThreadWaitStates(companyId).length; } catch (err) { logger.warn(`[thread-integration] countUnresolvedWaitStates failed for company ${companyId}:`, err instanceof Error ? err.message : String(err)); return 0; diff --git a/src/main/thread-integration/thread-helpers.ts b/src/main/thread-integration/thread-helpers.ts index 4105bc4b..e4ab3342 100644 --- a/src/main/thread-integration/thread-helpers.ts +++ b/src/main/thread-integration/thread-helpers.ts @@ -12,7 +12,7 @@ export interface ThreadIntegrationDeps { } export function findTaskThread(db: AppDatabase, taskId: string, companyId: string) { - const existing = db.listThreads({ + const existing = db.threads.listThreads({ companyId, type: "task" as ThreadType, relatedEntityType: "task", @@ -26,7 +26,7 @@ export function ensureTaskThread(db: AppDatabase, taskId: string, companyId: str const existing = findTaskThread(db, taskId, companyId); if (existing) return existing; - return db.createThread({ + return db.threads.createThread({ companyId, title, type: "task" as ThreadType, @@ -36,7 +36,7 @@ export function ensureTaskThread(db: AppDatabase, taskId: string, companyId: str } export function findAgentCommunicationThread(db: AppDatabase, agentId: string, companyId: string) { - const existing = db.listThreads({ + const existing = db.threads.listThreads({ companyId, type: "general" as ThreadType, relatedEntityType: "agent", @@ -62,7 +62,7 @@ export function ensureAgentCommunicationThread( const existing = findAgentCommunicationThread(db, agentId, companyId); if (existing) return existing; - return db.createThread({ + return db.threads.createThread({ companyId, title: `${agentLabel} · communication`, type: "general" as ThreadType, @@ -72,7 +72,7 @@ export function ensureAgentCommunicationThread( } export function findApprovalThread(db: AppDatabase, approvalId: string, companyId: string) { - const existing = db.listThreads({ + const existing = db.threads.listThreads({ companyId, type: "approval" as ThreadType, relatedEntityType: "approval", @@ -86,7 +86,7 @@ export function ensureApprovalThread(db: AppDatabase, approvalId: string, compan const existing = findApprovalThread(db, approvalId, companyId); if (existing) return existing; - return db.createThread({ + return db.threads.createThread({ companyId, title, type: "approval" as ThreadType, @@ -176,7 +176,7 @@ export function notifyThreadSubscribers( notify?: (options: { title: string; body: string }) => void, ): void { if (!notify) return; - const subscriptions = db.listSubscriptions(threadId); + const subscriptions = db.threads.listSubscriptions(threadId); for (const sub of subscriptions) { if (sub.subscriberType !== "user") continue; if (sub.notifyOn === "none") continue; diff --git a/src/main/updater-service.ts b/src/main/updater-service.ts index e64d7580..5e0cfef2 100644 --- a/src/main/updater-service.ts +++ b/src/main/updater-service.ts @@ -107,7 +107,7 @@ export function createUpdaterService({ } async function checkForUpdates(options: { manual?: boolean } = {}) { - const snapshot = db.listSnapshot(); + const snapshot = db.snap.listSnapshot(); const updateUrl = process.env.AGENTCOMPANY_UPDATE_URL?.trim(); const skipReason = shouldSkipUpdateCheck({ isPackaged: app.isPackaged, diff --git a/src/main/worker-service.ts b/src/main/worker-service.ts index 089c5580..5198879c 100644 --- a/src/main/worker-service.ts +++ b/src/main/worker-service.ts @@ -180,7 +180,7 @@ export function createWorkerService({ // Mark all running/queued runs as interrupted so DB state is clean on restart try { - const snapshot = db.listSnapshot(); + const snapshot = db.snap.listSnapshot(); for (const run of snapshot.runs) { if (run.status === "running" || run.status === "queued") { db.runs.finishRun({ @@ -194,7 +194,7 @@ export function createWorkerService({ unattributedCost: true, }); if (run.workspaceId) { - db.releaseWorkspaceLock(run.workspaceId, run.id); + db.workspacesMod.releaseWorkspaceLock(run.workspaceId, run.id); } if (run.taskId) { db.tasks.releaseTaskLock(run.taskId, run.id); diff --git a/src/main/worker/entry/crash-recovery.ts b/src/main/worker/entry/crash-recovery.ts index 489909fd..4e768def 100644 --- a/src/main/worker/entry/crash-recovery.ts +++ b/src/main/worker/entry/crash-recovery.ts @@ -19,7 +19,7 @@ export function handleWorkerExit( logger.warn(`[worker] Worker exited with code ${code}, recovering...`); - const snapshot = db.listSnapshot(); + const snapshot = db.snap.listSnapshot(); const affectedAgentIds = new Set(); const affectedCompanyIds = new Map(); const requeueCandidates: Array<{ @@ -44,7 +44,7 @@ export function handleWorkerExit( unattributedCost: true, }); if (run.workspaceId) { - db.releaseWorkspaceLock(run.workspaceId, run.id); + db.workspacesMod.releaseWorkspaceLock(run.workspaceId, run.id); } // EXEC-02: Release execution lock on crash recovery if (run.taskId) { @@ -72,11 +72,11 @@ export function handleWorkerExit( for (const agentId of affectedAgentIds) { // EXEC-05: Promote deferred wakes after crash recovery const cid = affectedCompanyIds.get(agentId) ?? ""; - const promoted = db.promoteWakeupRequests(agentId, cid); + const promoted = db.wakeup.promoteWakeupRequests(agentId, cid); if (promoted > 0) { logger.info(`[worker] Promoted ${promoted} deferred wake(s) for agent ${agentId} after crash recovery`); } - const pendingWakeTrigger = db.getPreferredWakeTrigger(agentId, cid); + const pendingWakeTrigger = db.wakeup.getPreferredWakeTrigger(agentId, cid); if (pendingWakeTrigger) { wakeAgentIfPossible(agentId, cid, pendingWakeTrigger); } @@ -89,18 +89,18 @@ export function handleWorkerExit( const worker = getWorker(); if (requeueCandidates.length > 0 && worker) { logger.info(`[worker] Re-enqueuing ${requeueCandidates.length} queued run(s) after worker restart.`); - const recoverySnapshot = db.listSnapshot(); + const recoverySnapshot = db.snap.listSnapshot(); for (const candidate of requeueCandidates) { try { // The crashed run may still own a workspace lock acquired during the // original dispatch; release it before re-dispatch so the replay // does not self-collide with "Workspace is locked by another run". if (candidate.workspaceId) { - db.releaseWorkspaceLock(candidate.workspaceId, candidate.id); + db.workspacesMod.releaseWorkspaceLock(candidate.workspaceId, candidate.id); } const task = candidate.taskId ? db.entityCrud.getTask(candidate.taskId) : null; const agent = db.entityCrud.getAgent(candidate.agentId); - const connector = db.getConnector(agent.connectorId); + const connector = db.connectorsCrud.getConnector(agent.connectorId); const { workspace, lockMode } = resolveWorkspaceWithLockMode(db, task, agent, candidate.companyId); const recoveryCompany = recoverySnapshot.companies.find((company) => company.id === candidate.companyId); @@ -130,7 +130,7 @@ export function handleWorkerExit( unattributedCost: true, }); if (candidate.workspaceId) { - db.releaseWorkspaceLock(candidate.workspaceId, candidate.id); + db.workspacesMod.releaseWorkspaceLock(candidate.workspaceId, candidate.id); } } } diff --git a/src/main/worker/entry/types.ts b/src/main/worker/entry/types.ts index 3a6ea10d..ced960a4 100644 --- a/src/main/worker/entry/types.ts +++ b/src/main/worker/entry/types.ts @@ -87,12 +87,12 @@ export interface WorkerServiceDependencies { companyId: string; agent: ReturnType; task: ReturnType | null; - connector: ReturnType; - workspace: ReturnType; + connector: ReturnType; + workspace: ReturnType; workspaceMode: "exclusive" | "shared" | "none"; companyName: string; companyDescription: string; - snapshot: ReturnType; + snapshot: ReturnType; trigger: string; }) => void; processRunCompletion: (run: RunRecord, event: z.infer & { type: "run-finished" }) => void; diff --git a/src/main/worker/handlers/run-finished-helpers.ts b/src/main/worker/handlers/run-finished-helpers.ts index f0e4fec2..68dfde39 100644 --- a/src/main/worker/handlers/run-finished-helpers.ts +++ b/src/main/worker/handlers/run-finished-helpers.ts @@ -68,9 +68,9 @@ export function finalizeRun( leaseRenewalMap.delete(event.runId); const runOutcome = db.runs.finishRun(event); if (run.workspaceId) { - db.releaseWorkspaceLock(run.workspaceId, run.id); + db.workspacesMod.releaseWorkspaceLock(run.workspaceId, run.id); } - db.addActivity({ + db.activityMod.addActivity({ companyId: run.companyId, actor: run.agentId, action: `run.${event.status}`, @@ -165,7 +165,7 @@ export function emitAndNotifyCompletion( } if (updatedAgent && runOutcome.budgetHardStopped) { - db.addActivity({ + db.activityMod.addActivity({ companyId: updatedAgent.companyId, actor: "system", action: "agent.auto_paused", @@ -229,8 +229,8 @@ export function runPostCompletionAutomation( // EXEC-05: Promote deferred wakes after run completion let hadDeferredWakes = false; if (!isQuitting() && !destroyed) { - const promoted = db.promoteWakeupRequests(run.agentId, run.companyId); - const pendingWakeTrigger = db.getPreferredWakeTrigger(run.agentId, run.companyId); + const promoted = db.wakeup.promoteWakeupRequests(run.agentId, run.companyId); + const pendingWakeTrigger = db.wakeup.getPreferredWakeTrigger(run.agentId, run.companyId); if (promoted > 0) { logger.info(`[worker] Promoted ${promoted} deferred wake(s) for agent ${run.agentId}`); } @@ -348,7 +348,7 @@ function hasUnfinishedTeamBuilding( .getDirectReports(agentId, companyId) .filter((r) => r.status !== "pending_approval" && r.status !== "terminated"); if (activeReports.length > 0) return false; - const goalCount = db.countActiveGoalsForCompany(companyId); + const goalCount = db.entityCrud.countActiveGoalsForCompany(companyId); if (goalCount === 0) return false; const pendingHires = db.approvalsMod .listCompanyApprovalsByType(companyId, "hire_agent") diff --git a/tests/integration/agent-model-switching.spec.ts b/tests/integration/agent-model-switching.spec.ts index 395b4957..cb905326 100644 --- a/tests/integration/agent-model-switching.spec.ts +++ b/tests/integration/agent-model-switching.spec.ts @@ -70,7 +70,7 @@ describe("agent model switching", () => { const db = new AppDatabase(profileDir); db.init(); - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", @@ -83,7 +83,7 @@ describe("agent model switching", () => { description: "Tests post-hire model switching", status: "active", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: profileDir, diff --git a/tests/integration/api-recursive-org-execution.spec.ts b/tests/integration/api-recursive-org-execution.spec.ts index eebf0e78..4d816ae0 100644 --- a/tests/integration/api-recursive-org-execution.spec.ts +++ b/tests/integration/api-recursive-org-execution.spec.ts @@ -24,7 +24,7 @@ async function createHarness() { const db = new AppDatabase(profileDir); db.init(); - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", @@ -37,7 +37,7 @@ async function createHarness() { description: "Tests manager-driven recursive hires", status: "active", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: profileDir, diff --git a/tests/integration/api-run-events.spec.ts b/tests/integration/api-run-events.spec.ts index 58a1594f..5bce4c1c 100644 --- a/tests/integration/api-run-events.spec.ts +++ b/tests/integration/api-run-events.spec.ts @@ -25,7 +25,7 @@ async function createHarness() { db.init(); const companyId = db.company.saveCompany({ name: "Orbit Foundry", description: "API test", status: "active" }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: profileDir, diff --git a/tests/integration/autonomous-chain/automation-messages.spec.ts b/tests/integration/autonomous-chain/automation-messages.spec.ts index d61cb926..1e4ad7a3 100644 --- a/tests/integration/autonomous-chain/automation-messages.spec.ts +++ b/tests/integration/autonomous-chain/automation-messages.spec.ts @@ -11,7 +11,7 @@ const { createAgent } = shared; describe("Automation Rules CRUD", () => { it("creates, reads, toggles, and deletes an automation rule", () => { - const ruleId = shared.db.saveAutomationRule({ + const ruleId = shared.db.automationMod.saveAutomationRule({ companyId: shared.companyId, name: "Auto-escalate blocked tasks", description: "When a task is blocked, notify the manager", @@ -28,7 +28,7 @@ describe("Automation Rules CRUD", () => { expect(ruleId).toBeTruthy(); // Rule appears in snapshot - const snapshot = shared.db.listSnapshot(); + const snapshot = shared.db.snap.listSnapshot(); const rule = snapshot.automationRules.find(r => r.id === ruleId); expect(rule).toBeTruthy(); expect(rule!.name).toBe("Auto-escalate blocked tasks"); @@ -39,36 +39,36 @@ describe("Automation Rules CRUD", () => { expect(rule!.executionCount).toBe(0); // Toggle to paused - shared.db.toggleAutomationRule(ruleId, shared.companyId, "paused"); - const updated = shared.db.listSnapshot().automationRules.find(r => r.id === ruleId); + shared.db.automationMod.toggleAutomationRule(ruleId, shared.companyId, "paused"); + const updated = shared.db.snap.listSnapshot().automationRules.find(r => r.id === ruleId); expect(updated!.status).toBe("paused"); // Record execution - shared.db.recordAutomationExecution(ruleId, shared.companyId, "Auto-escalate blocked tasks", "task_blocked", "escalate_to_manager", "Escalated task-123"); - const afterExec = shared.db.listSnapshot().automationRules.find(r => r.id === ruleId); + shared.db.automationMod.recordAutomationExecution(ruleId, shared.companyId, "Auto-escalate blocked tasks", "task_blocked", "escalate_to_manager", "Escalated task-123"); + const afterExec = shared.db.snap.listSnapshot().automationRules.find(r => r.id === ruleId); expect(afterExec!.executionCount).toBe(1); expect(afterExec!.lastExecutedAt).toBeTruthy(); // Automation log - const log = shared.db.getAutomationLog(shared.companyId); + const log = shared.db.automationMod.getAutomationLog(shared.companyId); expect(log).toHaveLength(1); expect(log[0].ruleName).toBe("Auto-escalate blocked tasks"); // Delete - shared.db.deleteAutomationRule(ruleId, shared.companyId); - const afterDelete = shared.db.listSnapshot().automationRules.find(r => r.id === ruleId); + shared.db.automationMod.deleteAutomationRule(ruleId, shared.companyId); + const afterDelete = shared.db.snap.listSnapshot().automationRules.find(r => r.id === ruleId); expect(afterDelete).toBeUndefined(); }); it("updates an existing rule", () => { - const ruleId = shared.db.saveAutomationRule({ + const ruleId = shared.db.automationMod.saveAutomationRule({ companyId: shared.companyId, name: "Original name", trigger: "task_created", action: "assign_task", }); - shared.db.saveAutomationRule({ + shared.db.automationMod.saveAutomationRule({ id: ruleId, companyId: shared.companyId, name: "Updated name", @@ -77,7 +77,7 @@ describe("Automation Rules CRUD", () => { priority: 90, }); - const rule = shared.db.listSnapshot().automationRules.find(r => r.id === ruleId); + const rule = shared.db.snap.listSnapshot().automationRules.find(r => r.id === ruleId); expect(rule!.name).toBe("Updated name"); expect(rule!.action).toBe("notify_agent"); expect(rule!.priority).toBe(90); @@ -171,7 +171,7 @@ describe("Agent Messages", () => { const engineeringLeadId = createAgent({ name: "Engineering Lead", role: "Lead", department: "engineering" }); const productLeadId = createAgent({ name: "Product Lead", role: "Lead", department: "product" }); - const projectId = shared.db.saveProject({ + const projectId = shared.db.entityCrud.saveProject({ companyId: shared.companyId, name: "Cross-team Project", description: "Scope", @@ -229,7 +229,7 @@ describe("Agent Messages", () => { it("appears in snapshot", () => { shared.db.messages.sendAgentMessage({ companyId: shared.companyId, fromAgentId: agentAId, channel: "company", subject: "Snapshot test", body: "Hi" }); - const snapshot = shared.db.listSnapshot(); + const snapshot = shared.db.snap.listSnapshot(); expect(snapshot.agentMessages.length).toBeGreaterThanOrEqual(1); expect(snapshot.agentMessages[0].subject).toBe("Snapshot test"); }); @@ -316,7 +316,7 @@ describe("Auto Task Assignment", () => { describe("Workflow Pipelines", () => { it("creates, activates, and deletes a workflow", () => { - const wfId = shared.db.saveWorkflow({ + const wfId = shared.db.automationMod.saveWorkflow({ companyId: shared.companyId, name: "Code Review Pipeline", description: "Auto-review flow for PRs", @@ -330,14 +330,14 @@ describe("Workflow Pipelines", () => { }); expect(wfId).toBeTruthy(); - const snapshot = shared.db.listSnapshot(); + const snapshot = shared.db.snap.listSnapshot(); const wf = snapshot.workflows.find(w => w.id === wfId); expect(wf!.name).toBe("Code Review Pipeline"); expect(wf!.status).toBe("draft"); expect(wf!.runCount).toBe(0); // Delete - shared.db.deleteWorkflow(wfId, shared.companyId); - expect(shared.db.listSnapshot().workflows.find(w => w.id === wfId)).toBeUndefined(); + shared.db.automationMod.deleteWorkflow(wfId, shared.companyId); + expect(shared.db.snap.listSnapshot().workflows.find(w => w.id === wfId)).toBeUndefined(); }); }); diff --git a/tests/integration/autonomous-chain/crash-recovery.spec.ts b/tests/integration/autonomous-chain/crash-recovery.spec.ts index 185873af..008c6926 100644 --- a/tests/integration/autonomous-chain/crash-recovery.spec.ts +++ b/tests/integration/autonomous-chain/crash-recovery.spec.ts @@ -34,7 +34,7 @@ describe("Step 7: Worker crash recovery (database layer)", () => { shared.db.runs.markRunStatus(run.id, "running", "Started"); // Verify it's running - let snapshot = shared.db.listSnapshot(); + let snapshot = shared.db.snap.listSnapshot(); expect(snapshot.runs.find((r) => r.id === run.id)?.status).toBe("running"); // Simulate app restart — close and re-init @@ -44,7 +44,7 @@ describe("Step 7: Worker crash recovery (database layer)", () => { shared.db = new AppDatabase(profileDir); shared.db.init(); // This calls interruptStaleRuns() - snapshot = shared.db.listSnapshot(); + snapshot = shared.db.snap.listSnapshot(); const recoveredRun = snapshot.runs.find((r) => r.id === run.id); expect(recoveredRun?.status).toBe("interrupted"); }); @@ -83,7 +83,7 @@ describe("Step 7: Worker crash recovery (database layer)", () => { unattributedCost: true, }); - const snapshot = shared.db.listSnapshot(); + const snapshot = shared.db.snap.listSnapshot(); const recovered = snapshot.runs.find((r) => r.id === run.id); expect(recovered?.status).toBe("interrupted"); expect(recovered?.errorMessage).toBe("Worker process exited unexpectedly."); diff --git a/tests/integration/autonomous-chain/e2e-flows.spec.ts b/tests/integration/autonomous-chain/e2e-flows.spec.ts index 61a62397..163e2c7f 100644 --- a/tests/integration/autonomous-chain/e2e-flows.spec.ts +++ b/tests/integration/autonomous-chain/e2e-flows.spec.ts @@ -111,7 +111,7 @@ describe("E2E: Full autonomous CEO cold start flow", () => { expect(ctoChain[0].id).toBe(ceoId); // 8. Create task tree - const projectId = shared.db.saveProject({ + const projectId = shared.db.entityCrud.saveProject({ companyId: shared.companyId, name: "Launch MVP", description: "Build and launch MVP", @@ -150,8 +150,8 @@ describe("E2E: Full autonomous CEO cold start flow", () => { expect(subtask.parentId).toBe(parentTaskId); // 9. Wakeup request test: enqueue wake while IC is busy - shared.db.enqueueWakeupRequest(shared.companyId, icId, "comment", "comment"); - const queuedWakes = shared.db.listWakeupRequests(shared.companyId, { agentId: icId, status: "queued" }); + shared.db.wakeup.enqueueWakeupRequest(shared.companyId, icId, "comment", "comment"); + const queuedWakes = shared.db.wakeup.listWakeupRequests(shared.companyId, { agentId: icId, status: "queued" }); expect(queuedWakes).toHaveLength(1); expect(queuedWakes[0].source).toBe("comment"); @@ -175,12 +175,12 @@ describe("E2E: Wakeup request lifecycle with run lifecycle", () => { // Simulate: agent is running, wakes come in. All triggers are coalesced // into a single wakeup request with JSON array of trigger details. - shared.db.enqueueWakeupRequest(shared.companyId, agentId, "subtask_completed", "subtask_completed"); - shared.db.enqueueWakeupRequest(shared.companyId, agentId, "comment", "comment"); - shared.db.enqueueWakeupRequest(shared.companyId, agentId, "report_blocked", "report_blocked"); + shared.db.wakeup.enqueueWakeupRequest(shared.companyId, agentId, "subtask_completed", "subtask_completed"); + shared.db.wakeup.enqueueWakeupRequest(shared.companyId, agentId, "comment", "comment"); + shared.db.wakeup.enqueueWakeupRequest(shared.companyId, agentId, "report_blocked", "report_blocked"); // Verify coalesced into single row with all triggers - const wakes = shared.db.listWakeupRequests(shared.companyId, { agentId, status: "queued" }); + const wakes = shared.db.wakeup.listWakeupRequests(shared.companyId, { agentId, status: "queued" }); expect(wakes).toHaveLength(1); expect(wakes[0].coalescedCount).toBe(2); @@ -191,7 +191,7 @@ describe("E2E: Wakeup request lifecycle with run lifecycle", () => { expect(triggers[2].trigger).toBe("report_blocked"); // Wakes remain queued until consumed by the run lifecycle - expect(shared.db.listWakeupRequests(shared.companyId, { agentId, status: "queued" })).toHaveLength(1); + expect(shared.db.wakeup.listWakeupRequests(shared.companyId, { agentId, status: "queued" })).toHaveLength(1); }); }); diff --git a/tests/integration/autonomous-chain/run-failure.spec.ts b/tests/integration/autonomous-chain/run-failure.spec.ts index 0bf7e067..5b4e83d2 100644 --- a/tests/integration/autonomous-chain/run-failure.spec.ts +++ b/tests/integration/autonomous-chain/run-failure.spec.ts @@ -45,7 +45,7 @@ describe("Step 6: Run failure escalation (database layer)", () => { expect(outcome.budgetHardStopped).toBe(false); - const snapshot = shared.db.listSnapshot(); + const snapshot = shared.db.snap.listSnapshot(); const finishedRun = snapshot.runs.find((r) => r.id === run.id); expect(finishedRun?.status).toBe("failed"); expect(finishedRun?.errorMessage).toBe("Process crashed"); diff --git a/tests/integration/autonomous-chain/shared-setup.ts b/tests/integration/autonomous-chain/shared-setup.ts index 6e4e1fcf..52bd26aa 100644 --- a/tests/integration/autonomous-chain/shared-setup.ts +++ b/tests/integration/autonomous-chain/shared-setup.ts @@ -60,7 +60,7 @@ export function installSharedSetup(): SharedHandle { status: "active", }); - state.workspaceId = state.db.saveWorkspace({ + state.workspaceId = state.db.workspacesMod.saveWorkspace({ companyId: state.companyId, name: "Primary", localPath: profileDir, diff --git a/tests/integration/autonomous-chain/wakeup-queue.spec.ts b/tests/integration/autonomous-chain/wakeup-queue.spec.ts index 874b1330..62a28da8 100644 --- a/tests/integration/autonomous-chain/wakeup-queue.spec.ts +++ b/tests/integration/autonomous-chain/wakeup-queue.spec.ts @@ -15,11 +15,11 @@ describe("Step 3: Wakeup request lifecycle (database)", () => { // Enqueue two wakes for the same busy agent. Coalesced into one row // with trigger_detail JSON array containing both triggers. - shared.db.enqueueWakeupRequest(shared.companyId, agentId, "subtask_completed", "subtask_completed"); - shared.db.enqueueWakeupRequest(shared.companyId, agentId, "comment", "comment"); + shared.db.wakeup.enqueueWakeupRequest(shared.companyId, agentId, "subtask_completed", "subtask_completed"); + shared.db.wakeup.enqueueWakeupRequest(shared.companyId, agentId, "comment", "comment"); // List returns the coalesced wake - const wakes = shared.db.listWakeupRequests(shared.companyId, { agentId, status: "queued" }); + const wakes = shared.db.wakeup.listWakeupRequests(shared.companyId, { agentId, status: "queued" }); expect(wakes).toHaveLength(1); expect(wakes[0].companyId).toBe(shared.companyId); expect(wakes[0].coalescedCount).toBe(1); @@ -32,7 +32,7 @@ describe("Step 3: Wakeup request lifecycle (database)", () => { it("returns empty array when no wakeup requests exist", () => { const agentId = createAgent({ name: "TestAgent" }); - const wakes = shared.db.listWakeupRequests(shared.companyId, { agentId, status: "queued" }); + const wakes = shared.db.wakeup.listWakeupRequests(shared.companyId, { agentId, status: "queued" }); expect(wakes).toHaveLength(0); }); @@ -40,15 +40,15 @@ describe("Step 3: Wakeup request lifecycle (database)", () => { const agent1 = createAgent({ name: "Agent1" }); const agent2 = createAgent({ name: "Agent2" }); - shared.db.enqueueWakeupRequest(shared.companyId, agent1, "comment", "comment"); - shared.db.enqueueWakeupRequest(shared.companyId, agent2, "subtask_completed", "subtask_completed"); + shared.db.wakeup.enqueueWakeupRequest(shared.companyId, agent1, "comment", "comment"); + shared.db.wakeup.enqueueWakeupRequest(shared.companyId, agent2, "subtask_completed", "subtask_completed"); - const wakes1 = shared.db.listWakeupRequests(shared.companyId, { agentId: agent1, status: "queued" }); + const wakes1 = shared.db.wakeup.listWakeupRequests(shared.companyId, { agentId: agent1, status: "queued" }); expect(wakes1).toHaveLength(1); expect(wakes1[0].source).toBe("comment"); // Agent2's wake should still be there - const wakes2 = shared.db.listWakeupRequests(shared.companyId, { agentId: agent2, status: "queued" }); + const wakes2 = shared.db.wakeup.listWakeupRequests(shared.companyId, { agentId: agent2, status: "queued" }); expect(wakes2).toHaveLength(1); expect(wakes2[0].source).toBe("subtask_completed"); }); diff --git a/tests/integration/connector-model-enforcement.spec.ts b/tests/integration/connector-model-enforcement.spec.ts index 690265a4..c8176264 100644 --- a/tests/integration/connector-model-enforcement.spec.ts +++ b/tests/integration/connector-model-enforcement.spec.ts @@ -69,7 +69,7 @@ describe("connector model enforcement", () => { const db = new AppDatabase(profileDir); db.init(); - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", @@ -82,7 +82,7 @@ describe("connector model enforcement", () => { description: "Blocks runs when agent models are disabled", status: "active", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: profileDir, @@ -113,7 +113,7 @@ describe("connector model enforcement", () => { status: "todo", }); - db.setDisabledModels(companyId, "openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]); + db.settings.setDisabledModels(companyId, "openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]); const { taskRunner } = createOrchestrationHarness(db); const result = taskRunner.queueTaskRun(taskId, "manual", "board"); @@ -123,7 +123,7 @@ describe("connector model enforcement", () => { throw new Error("Expected queueTaskRun to reject the disabled model."); } expect(result.error.code).toBe("CONNECTOR_MODEL_NOT_ALLOWED"); - expect(db.listSnapshot().runs).toHaveLength(0); + expect(db.snap.listSnapshot().runs).toHaveLength(0); db.close(); }); @@ -133,7 +133,7 @@ describe("connector model enforcement", () => { const db = new AppDatabase(profileDir); db.init(); - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", @@ -146,7 +146,7 @@ describe("connector model enforcement", () => { description: "Rejects invalid queued runs during recovery", status: "active", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: profileDir, @@ -187,12 +187,12 @@ describe("connector model enforcement", () => { const checkoutSuccess = db.tasks.checkoutTask(taskId, run.id); expect(checkoutSuccess).toBe(true); - db.setDisabledModels(companyId, "openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]); + db.settings.setDisabledModels(companyId, "openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]); const { taskRunner } = createOrchestrationHarness(db); taskRunner.resumeQueuedRuns(); - const recoveredRun = db.listSnapshot().runs.find((entry) => entry.id === run.id); + const recoveredRun = db.snap.listSnapshot().runs.find((entry) => entry.id === run.id); expect(recoveredRun?.status).toBe("failed"); expect(recoveredRun?.errorMessage).toContain("Connector model is disabled or invalid"); diff --git a/tests/integration/cross-subsystem.spec.ts b/tests/integration/cross-subsystem.spec.ts index c0ccf974..1917fe64 100644 --- a/tests/integration/cross-subsystem.spec.ts +++ b/tests/integration/cross-subsystem.spec.ts @@ -57,7 +57,7 @@ const mockLogger = { function scaffold(db: AppDatabase, budgetAmountCents = 500): ScaffoldResult { const connectorId: ConnectorId = "gemini-cli"; - db.saveConnector({ + db.connectorsCrud.saveConnector({ id: connectorId, model: "gemini-2.5-pro", envBindingText: "", @@ -70,7 +70,7 @@ function scaffold(db: AppDatabase, budgetAmountCents = 500): ScaffoldResult { status: "active", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary Workspace", localPath: "/tmp/pipeline-test", @@ -93,7 +93,7 @@ function scaffold(db: AppDatabase, budgetAmountCents = 500): ScaffoldResult { budgetMonthlyUsd: 100, }); - const goalId = db.saveGoal({ + const goalId = db.entityCrud.saveGoal({ companyId, title: "Implement Authentication", description: "Build full authentication system", @@ -101,7 +101,7 @@ function scaffold(db: AppDatabase, budgetAmountCents = 500): ScaffoldResult { status: "active", }); - const projectId = db.saveProject({ + const projectId = db.entityCrud.saveProject({ companyId, goalId, name: "Auth Project", @@ -112,7 +112,7 @@ function scaffold(db: AppDatabase, budgetAmountCents = 500): ScaffoldResult { }); // Budget policy: company-level with hardStop - const policyId = db.saveBudgetPolicy({ + const policyId = db.budget.saveBudgetPolicy({ companyId, scopeType: "company", scopeId: companyId, diff --git a/tests/integration/database.spec.ts b/tests/integration/database.spec.ts index 06ac58c6..7b707daa 100644 --- a/tests/integration/database.spec.ts +++ b/tests/integration/database.spec.ts @@ -43,7 +43,7 @@ describe("AppDatabase", () => { status: "active", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: profileDir, @@ -66,7 +66,7 @@ describe("AppDatabase", () => { budgetMonthlyUsd: 200, }); - const goalId = db.saveGoal({ + const goalId = db.entityCrud.saveGoal({ companyId, title: "Ship the desktop app", description: "Reach packaged desktop parity.", @@ -74,7 +74,7 @@ describe("AppDatabase", () => { status: "active", }); - const projectId = db.saveProject({ + const projectId = db.entityCrud.saveProject({ companyId, goalId, name: "Desktop rebuild", @@ -95,21 +95,21 @@ describe("AppDatabase", () => { status: "todo", }); - db.saveSecret({ + db.entityCrud.saveSecret({ companyId, name: "vault-openai", description: "Test key", value: "sk-test-123", }); - db.saveConnector({ + db.connectorsCrud.saveConnector({ id: "openai-codex", model: null, envBindingText: "OPENAI_API_KEY=vault-openai", notes: "Connector note", }); - expect(db.resolveSecretEnv("OPENAI_API_KEY=vault-openai", companyId)).toEqual({ + expect(db.entityCrud.resolveSecretEnv("OPENAI_API_KEY=vault-openai", companyId)).toEqual({ OPENAI_API_KEY: "sk-test-123", }); @@ -136,22 +136,22 @@ describe("AppDatabase", () => { unattributedCost: false, }); - const snapshotBeforeBackup = db.listSnapshot(); + const snapshotBeforeBackup = db.snap.listSnapshot(); expect(snapshotBeforeBackup.companies).toHaveLength(1); expect(snapshotBeforeBackup.runs[0]?.status).toBe("succeeded"); expect(snapshotBeforeBackup.tasks[0]?.activeRunId).toBeNull(); - await db.exportBackup(backupDir); + await db.backupMod.exportBackup(backupDir); db.company.saveCompany({ name: "Temporary company", description: "Should disappear after restore", status: "active", }); - expect(db.listSnapshot().companies).toHaveLength(2); + expect(db.snap.listSnapshot().companies).toHaveLength(2); - await db.restoreFromBackup(backupDir); - const restoredSnapshot = db.listSnapshot(); + await db.backupMod.restoreFromBackup(backupDir); + const restoredSnapshot = db.snap.listSnapshot(); expect(restoredSnapshot.companies).toHaveLength(1); expect(restoredSnapshot.companies[0]?.name).toBe("Orbit Foundry"); expect(restoredSnapshot.runs[0]?.summary).toBe("Completed successfully"); @@ -176,7 +176,7 @@ describe("AppDatabase", () => { status: "active", }); - const alphaWorkspaceId = db.saveWorkspace({ + const alphaWorkspaceId = db.workspacesMod.saveWorkspace({ companyId: alphaCompanyId, name: "Alpha Workspace", localPath: profileDir, @@ -185,7 +185,7 @@ describe("AppDatabase", () => { isPrimary: true, }); - const betaWorkspaceId = db.saveWorkspace({ + const betaWorkspaceId = db.workspacesMod.saveWorkspace({ companyId: betaCompanyId, name: "Beta Workspace", localPath: profileDir, @@ -242,9 +242,9 @@ describe("AppDatabase", () => { status: "todo", }); - db.setCurrentCompany(alphaCompanyId); + db.settings.setCurrentCompany(alphaCompanyId); - const rendererSnapshot = db.listRendererSnapshot(); + const rendererSnapshot = db.snap.listRendererSnapshot(); expect(rendererSnapshot.currentCompanyId).toBe(alphaCompanyId); expect(rendererSnapshot.companies).toHaveLength(2); expect(rendererSnapshot.connectors.length).toBeGreaterThan(0); diff --git a/tests/integration/document-run-provenance.spec.ts b/tests/integration/document-run-provenance.spec.ts index 7d04cc17..4f34e836 100644 --- a/tests/integration/document-run-provenance.spec.ts +++ b/tests/integration/document-run-provenance.spec.ts @@ -26,7 +26,7 @@ async function createHarness() { const db = new AppDatabase(profileDir); db.init(); - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", @@ -39,7 +39,7 @@ async function createHarness() { description: "Tests document revision provenance through run completion", status: "active", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: profileDir, @@ -71,7 +71,7 @@ async function createHarness() { }); db.tasks.setTaskType(taskId, "research"); - const existingDocumentId = db.saveDocument({ + const existingDocumentId = db.documentsMod.saveDocument({ companyId, type: "knowledge_article", title: "Market report", @@ -149,7 +149,7 @@ describe("document run provenance", () => { )[0]; expect(updatedDocument?.sourceRunId).toBe(harness.run.id); - const persistedRun = harness.db.listSnapshot().runs.find((entry) => entry.id === harness.run.id); + const persistedRun = harness.db.snap.listSnapshot().runs.find((entry) => entry.id === harness.run.id); if (!persistedRun) { throw new Error("Expected persisted run record for provenance test."); } @@ -307,7 +307,7 @@ describe("document run provenance", () => { ); expect(createResponse.status).toBe(201); - const approvals = harness.db.listSnapshot().approvals.filter((approval) => approval.type === "deliverable_review"); + const approvals = harness.db.snap.listSnapshot().approvals.filter((approval) => approval.type === "deliverable_review"); expect(approvals).toHaveLength(1); expect(approvals[0]?.relatedTaskId).toBe(harness.taskId); } finally { diff --git a/tests/integration/onboarding-model-pool.spec.ts b/tests/integration/onboarding-model-pool.spec.ts index fc50a6b2..7cbc0af9 100644 --- a/tests/integration/onboarding-model-pool.spec.ts +++ b/tests/integration/onboarding-model-pool.spec.ts @@ -54,9 +54,9 @@ function buildKickoffPayload(db: AppDatabase, summary: { companyId: string; lead const agent = db.entityCrud.getAgent(summary.leadAgentId); const task = db.entityCrud.getTask(summary.taskId); - const connector = db.getConnector(agent.connectorId); - const workspace = summary.workspaceId ? db.getWorkspace(summary.workspaceId) : null; - const company = db.listSnapshot().companies.find((entry) => entry.id === summary.companyId); + const connector = db.connectorsCrud.getConnector(agent.connectorId); + const workspace = summary.workspaceId ? db.workspacesMod.getWorkspace(summary.workspaceId) : null; + const company = db.snap.listSnapshot().companies.find((entry) => entry.id === summary.companyId); if (!company) { throw new Error(`Company ${summary.companyId} not found in snapshot.`); @@ -72,7 +72,7 @@ function buildKickoffPayload(db: AppDatabase, summary: { companyId: string; lead workspaceMode: "exclusive", companyName: company.name, companyDescription: company.description, - snapshot: db.listSnapshot(), + snapshot: db.snap.listSnapshot(), trigger: "bootstrap", }, { db, @@ -98,7 +98,7 @@ describe("onboarding model pool integration", () => { throw new Error("openai-codex requires at least two models for this integration test."); } - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", @@ -128,8 +128,8 @@ describe("onboarding model pool integration", () => { const agent = db.entityCrud.getAgent(summary.leadAgentId); expect(agent.connectorId).toBe("openai-codex"); expect(agent.model).toBe(enabledModel); - expect(db.getDisabledModels(summary.companyId, "openai-codex")).toEqual([{ id: disabledModel, label: disabledModel }]); - expect(db.getCurrentCompanyId()).toBe(summary.companyId); + expect(db.settings.getDisabledModels(summary.companyId, "openai-codex")).toEqual([{ id: disabledModel, label: disabledModel }]); + expect(db.settings.getCurrentCompanyId()).toBe(summary.companyId); const payload = buildKickoffPayload(db, summary); expect(payload.model).toBe(enabledModel); @@ -161,7 +161,7 @@ describe("onboarding model pool integration", () => { throw new Error("anthropic provider must expose at least two models from pi-ai."); } - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "anthropic", status: "ready", authState: "authenticated", @@ -205,7 +205,7 @@ describe("onboarding model pool integration", () => { const db = new AppDatabase(profileDir); db.init(); - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", @@ -254,7 +254,7 @@ describe("onboarding model pool integration", () => { throw new Error("openai-codex requires a default and fallback model for reroute testing."); } - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", @@ -296,7 +296,7 @@ describe("onboarding model pool integration", () => { throw new Error("openai-codex requires a model to validate onboarding rejection."); } - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", diff --git a/tests/integration/pi-mono-migrations.spec.ts b/tests/integration/pi-mono-migrations.spec.ts index a49c2457..7b0f27fb 100644 --- a/tests/integration/pi-mono-migrations.spec.ts +++ b/tests/integration/pi-mono-migrations.spec.ts @@ -106,7 +106,7 @@ describe("pi-mono migrations 27 + 28", () => { const db = new AppDatabase(dir); db.init(); - const rows = db.listSnapshot().connectors.map((entry) => entry.id); + const rows = db.snap.listSnapshot().connectors.map((entry) => entry.id); expect(rows.length).toBe(26); expect(rows).toContain("claude-pro"); expect(rows).toContain("amazon-bedrock"); diff --git a/tests/integration/process-recovery.spec.ts b/tests/integration/process-recovery.spec.ts index e873bba7..845f38aa 100644 --- a/tests/integration/process-recovery.spec.ts +++ b/tests/integration/process-recovery.spec.ts @@ -54,7 +54,7 @@ function scaffold(db: AppDatabase): ScaffoldResult { const connectorId: ConnectorId = "gemini-cli"; // Connector - db.saveConnector({ + db.connectorsCrud.saveConnector({ id: connectorId, model: "gemini-2.5-pro", envBindingText: "", @@ -69,7 +69,7 @@ function scaffold(db: AppDatabase): ScaffoldResult { }); // Workspace - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary Workspace", localPath: "/tmp/recovery-test", @@ -94,7 +94,7 @@ function scaffold(db: AppDatabase): ScaffoldResult { }); // Goal - const goalId = db.saveGoal({ + const goalId = db.entityCrud.saveGoal({ companyId, title: "Build login page", description: "Implement login page with authentication", @@ -103,7 +103,7 @@ function scaffold(db: AppDatabase): ScaffoldResult { }); // Project - const projectId = db.saveProject({ + const projectId = db.entityCrud.saveProject({ companyId, goalId, name: "Auth Module", @@ -169,7 +169,7 @@ function createAndStartRun( db.runs.markRunStatus(runId, "running", "Starting execution"); if (opts.claimWorkspace !== false) { - db.claimWorkspaceLock(opts.workspaceId, runId); + db.workspacesMod.claimWorkspaceLock(opts.workspaceId, runId); } // Set agent status to "running" to simulate active execution @@ -385,7 +385,7 @@ describe("Process Recovery -- Worker Crash Detection", () => { workspaceId: s.workspaceId, connectorId: s.connectorId, }); - expect(db.claimWorkspaceLock(s.workspaceId, newRunId)).toBe(true); + expect(db.workspacesMod.claimWorkspaceLock(s.workspaceId, newRunId)).toBe(true); }); it("tasks set to blocked after crash", () => { diff --git a/tests/integration/skill-aware-assignment.spec.ts b/tests/integration/skill-aware-assignment.spec.ts index ddf1de20..31b07318 100644 --- a/tests/integration/skill-aware-assignment.spec.ts +++ b/tests/integration/skill-aware-assignment.spec.ts @@ -24,7 +24,7 @@ describe("skill-aware capability registry", () => { const db = new AppDatabase(profileDir); db.init(); - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", @@ -37,7 +37,7 @@ describe("skill-aware capability registry", () => { description: "Tests skill-aware assignment", status: "active", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: profileDir, @@ -75,7 +75,7 @@ describe("skill-aware capability registry", () => { const skillId = randomUUID(); const now = new Date().toISOString(); - db.upsertCompanySkill({ + db.skills.upsertCompanySkill({ id: skillId, companyId, key: "frontend/react-ui", @@ -92,7 +92,7 @@ describe("skill-aware capability registry", () => { createdAt: now, updatedAt: now, }); - db.assignSkillToAgent(styledEngineerId, skillId); + db.skills.assignSkillToAgent(styledEngineerId, skillId); const taskId = db.tasks.saveTask({ companyId, @@ -114,7 +114,7 @@ describe("skill-aware capability registry", () => { const db = new AppDatabase(profileDir); db.init(); - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", @@ -127,7 +127,7 @@ describe("skill-aware capability registry", () => { description: "Tests capability registry prompt injection", status: "active", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: profileDir, @@ -151,7 +151,7 @@ describe("skill-aware capability registry", () => { const skillId = randomUUID(); const now = new Date().toISOString(); - db.upsertCompanySkill({ + db.skills.upsertCompanySkill({ id: skillId, companyId, key: "frontend/react-ui", @@ -169,7 +169,7 @@ describe("skill-aware capability registry", () => { updatedAt: now, }); - const snapshot = db.listSnapshot(); + const snapshot = db.snap.listSnapshot(); const prompt = buildTaskPrompt({ agent: db.entityCrud.getAgent(agentId), apiUrl: "http://127.0.0.1:3210", @@ -193,7 +193,7 @@ describe("skill-aware capability registry", () => { allConnectors: snapshot.connectors .filter((connector) => connector.id === "openai-codex") .map((connector) => ({ id: connector.id, label: connector.label, status: connector.status, enabledModels: ["gpt-5.4"] })), - companySkillCatalog: db.listCompanySkills(companyId).map((skill) => ({ + companySkillCatalog: db.skills.listCompanySkills(companyId).map((skill) => ({ name: skill.name, slug: skill.slug, description: skill.description, diff --git a/tests/integration/thread-wait-heartbeat.spec.ts b/tests/integration/thread-wait-heartbeat.spec.ts index a9d73a56..9bb98c06 100644 --- a/tests/integration/thread-wait-heartbeat.spec.ts +++ b/tests/integration/thread-wait-heartbeat.spec.ts @@ -28,7 +28,7 @@ function createNoopLogger() { } function createScaffold(db: AppDatabase) { - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", @@ -42,7 +42,7 @@ function createScaffold(db: AppDatabase) { status: "active", executionMode: "ai_cofounder", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: "/tmp/thread-wait", @@ -116,7 +116,7 @@ describe("thread wait + heartbeat integration", () => { threadIntegration.onTaskCreated(taskId, companyId); threadIntegration.onRunFinished("run-1", taskId, engineerId, companyId, "succeeded", null); - const thread = db.listThreads({ + const thread = db.threads.listThreads({ companyId, type: "task", relatedEntityType: "task", @@ -128,7 +128,7 @@ describe("thread wait + heartbeat integration", () => { throw new Error("Expected a task thread to exist for the completed run."); } - let waitStates = db.listThreadWaitStates(companyId).filter((entry) => entry.threadId === thread.id); + let waitStates = db.threads.listThreadWaitStates(companyId).filter((entry) => entry.threadId === thread.id); expect(waitStates).toHaveLength(1); expect(waitStates[0]?.waitingFor).toBe("user_reply"); expect(checkpointEngine.shouldPauseHeartbeat(companyId, engineerId)).toBe(true); @@ -165,7 +165,7 @@ describe("thread wait + heartbeat integration", () => { body: "Board reviewed this deliverable. Continue.", }); - waitStates = db.listThreadWaitStates(companyId).filter((entry) => entry.threadId === thread.id); + waitStates = db.threads.listThreadWaitStates(companyId).filter((entry) => entry.threadId === thread.id); expect(waitStates).toHaveLength(0); expect(threadIntegration.hasUnresolvedWaitStatesForAgent(companyId, engineerId)).toBe(false); expect(checkpointEngine.shouldPauseHeartbeat(companyId, engineerId)).toBe(false); @@ -192,7 +192,7 @@ describe("thread wait + heartbeat integration", () => { }); threadIntegration.onTaskCreated(taskId, companyId); - const thread = db.listThreads({ + const thread = db.threads.listThreads({ companyId, type: "task", relatedEntityType: "task", @@ -215,7 +215,7 @@ describe("thread wait + heartbeat integration", () => { }); threadIntegration.onApprovalCreated(approvalId, companyId, taskId, "draft_review", "Draft review needed"); - let waitStates = db.listThreadWaitStates(companyId).filter((entry) => entry.relatedApprovalId === approvalId); + let waitStates = db.threads.listThreadWaitStates(companyId).filter((entry) => entry.relatedApprovalId === approvalId); expect(waitStates).toHaveLength(1); expect(waitStates[0]?.waitingFor).toBe("user_approval"); expect(checkpointEngine.shouldPauseHeartbeat(companyId, engineerId)).toBe(true); @@ -234,7 +234,7 @@ describe("thread wait + heartbeat integration", () => { body: "Board left a comment but did not decide approval yet.", }); - waitStates = db.listThreadWaitStates(companyId).filter((entry) => entry.relatedApprovalId === approvalId); + waitStates = db.threads.listThreadWaitStates(companyId).filter((entry) => entry.relatedApprovalId === approvalId); expect(waitStates).toHaveLength(1); expect(checkpointEngine.shouldPauseHeartbeat(companyId, engineerId)).toBe(true); @@ -244,7 +244,7 @@ describe("thread wait + heartbeat integration", () => { decisionNote: "Approved by board", }); threadIntegration.onApprovalResolved(approvalId, companyId, "approved"); - waitStates = db.listThreadWaitStates(companyId).filter((entry) => entry.relatedApprovalId === approvalId); + waitStates = db.threads.listThreadWaitStates(companyId).filter((entry) => entry.relatedApprovalId === approvalId); expect(waitStates).toHaveLength(0); expect(checkpointEngine.shouldPauseHeartbeat(companyId, engineerId)).toBe(false); diff --git a/tests/integration/work-products-lifecycle.spec.ts b/tests/integration/work-products-lifecycle.spec.ts index d84c7980..58456f1b 100644 --- a/tests/integration/work-products-lifecycle.spec.ts +++ b/tests/integration/work-products-lifecycle.spec.ts @@ -45,7 +45,7 @@ async function createHarness(): Promise { const db = new AppDatabase(profileDir); db.init(); - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", @@ -58,7 +58,7 @@ async function createHarness(): Promise { description: "Tests archived visibility, bulk retirement, deliverable signal", status: "active", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: workspacePath, @@ -107,7 +107,7 @@ async function createHarness(): Promise { } function registerArtifact(harness: Harness, relativePath: string): string { - return harness.db.createWorkProduct({ + return harness.db.documentsMod.createWorkProduct({ companyId: harness.companyId, taskId: harness.taskId, workspaceId: harness.workspaceId, @@ -126,12 +126,12 @@ describe("work_products archived visibility", () => { try { const activeId = registerArtifact(harness, "docs/active.md"); const archivedId = registerArtifact(harness, "docs/retired.md"); - harness.db.updateWorkProduct(archivedId, harness.companyId, { + harness.db.documentsMod.updateWorkProduct(archivedId, harness.companyId, { status: "archived", lastTouchedByRunId: harness.runId, }); - const snapshot = harness.db.listSnapshot(); + const snapshot = harness.db.snap.listSnapshot(); const visibleIds = snapshot.workProducts.map((wp) => wp.id); expect(visibleIds).toContain(activeId); expect(visibleIds).not.toContain(archivedId); @@ -145,12 +145,12 @@ describe("work_products archived visibility", () => { try { const activeId = registerArtifact(harness, "src/feature.ts"); const archivedId = registerArtifact(harness, "src/removed.ts"); - harness.db.updateWorkProduct(archivedId, harness.companyId, { + harness.db.documentsMod.updateWorkProduct(archivedId, harness.companyId, { status: "archived", lastTouchedByRunId: harness.runId, }); - const rows = harness.db.listWorkProductsForTask(harness.taskId, harness.companyId); + const rows = harness.db.documentsMod.listWorkProductsForTask(harness.taskId, harness.companyId); const ids = rows.map((r) => r.id); expect(ids).toContain(activeId); expect(ids).not.toContain(archivedId); @@ -164,16 +164,16 @@ describe("work_products archived visibility", () => { try { const id = registerArtifact(harness, "notes/ghost.md"); expect( - harness.db.resolveWorkProductTarget(id, harness.companyId), + harness.db.documentsMod.resolveWorkProductTarget(id, harness.companyId), ).not.toBeNull(); - harness.db.updateWorkProduct(id, harness.companyId, { + harness.db.documentsMod.updateWorkProduct(id, harness.companyId, { status: "archived", lastTouchedByRunId: harness.runId, }); expect( - harness.db.resolveWorkProductTarget(id, harness.companyId), + harness.db.documentsMod.resolveWorkProductTarget(id, harness.companyId), ).toBeNull(); } finally { harness.db.close(); @@ -193,7 +193,7 @@ describe("work_products bulk retirement", () => { ids.push(registerArtifact(harness, rel)); } - const archived = harness.db.archiveWorkProductsByExternalPaths({ + const archived = harness.db.documentsMod.archiveWorkProductsByExternalPaths({ companyId: harness.companyId, workspaceId: harness.workspaceId, provider: "filesystem", @@ -206,7 +206,7 @@ describe("work_products bulk retirement", () => { for (const id of ids) { expect(archivedIdSet.has(id)).toBe(true); } - const visible = harness.db.listSnapshot().workProducts; + const visible = harness.db.snap.listSnapshot().workProducts; expect(visible.length).toBe(0); } finally { harness.db.close(); @@ -217,7 +217,7 @@ describe("work_products bulk retirement", () => { const harness = await createHarness(); try { const id = registerArtifact(harness, "tmp/scratch.log"); - harness.db.archiveWorkProductsByExternalPaths({ + harness.db.documentsMod.archiveWorkProductsByExternalPaths({ companyId: harness.companyId, workspaceId: harness.workspaceId, provider: "filesystem", @@ -225,7 +225,7 @@ describe("work_products bulk retirement", () => { lastTouchedByRunId: harness.runId, }); - const second = harness.db.archiveWorkProductsByExternalPaths({ + const second = harness.db.documentsMod.archiveWorkProductsByExternalPaths({ companyId: harness.companyId, workspaceId: harness.workspaceId, provider: "filesystem", @@ -235,7 +235,7 @@ describe("work_products bulk retirement", () => { expect(second).toHaveLength(0); expect( - harness.db.resolveWorkProductTarget(id, harness.companyId), + harness.db.documentsMod.resolveWorkProductTarget(id, harness.companyId), ).toBeNull(); } finally { harness.db.close(); @@ -252,7 +252,7 @@ describe("work_products bulk retirement", () => { const workProductId = registerArtifact(harness, relPath); expect( - harness.db.resolveWorkProductTarget(workProductId, harness.companyId), + harness.db.documentsMod.resolveWorkProductTarget(workProductId, harness.companyId), ).not.toBeNull(); const baseline = { @@ -277,7 +277,7 @@ describe("work_products bulk retirement", () => { expect(retired.map((o) => (o as { relativePath: string }).relativePath)).toContain(relPath); expect( - harness.db.resolveWorkProductTarget(workProductId, harness.companyId), + harness.db.documentsMod.resolveWorkProductTarget(workProductId, harness.companyId), ).toBeNull(); } finally { harness.db.close(); @@ -290,19 +290,19 @@ describe("work_products deliverable signal", () => { const harness = await createHarness(); try { const id = registerArtifact(harness, "deliverables/final.md"); - const beforeArchive = harness.db.collectRunDeliverableSignals( + const beforeArchive = harness.db.knowledgeMod.collectRunDeliverableSignals( harness.runId, harness.companyId, harness.taskId, ); expect(beforeArchive.hasWorkProduct).toBe(true); - harness.db.updateWorkProduct(id, harness.companyId, { + harness.db.documentsMod.updateWorkProduct(id, harness.companyId, { status: "archived", lastTouchedByRunId: harness.runId, }); - const afterArchive = harness.db.collectRunDeliverableSignals( + const afterArchive = harness.db.knowledgeMod.collectRunDeliverableSignals( harness.runId, harness.companyId, harness.taskId, diff --git a/tests/unit/agent-budget-policy-sync.test.ts b/tests/unit/agent-budget-policy-sync.test.ts index 518b88b5..a966f942 100644 --- a/tests/unit/agent-budget-policy-sync.test.ts +++ b/tests/unit/agent-budget-policy-sync.test.ts @@ -27,14 +27,14 @@ describe("agent budget policy synchronization", () => { description: "Budget sync regression coverage", status: "active", }); - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", version: "test", lastError: null, }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: profileDir, @@ -57,7 +57,7 @@ describe("agent budget policy synchronization", () => { department: "engineering", }); - const firstPolicyId = db.saveBudgetPolicy({ + const firstPolicyId = db.budget.saveBudgetPolicy({ companyId, scopeType: "agent", scopeId: agentId, @@ -86,7 +86,7 @@ describe("agent budget policy synchronization", () => { department: "engineering", }); - const secondPolicyId = db.saveBudgetPolicy({ + const secondPolicyId = db.budget.saveBudgetPolicy({ companyId, scopeType: "agent", scopeId: agentId, @@ -99,13 +99,13 @@ describe("agent budget policy synchronization", () => { isActive: true, }); - const agentPolicies = db.listBudgetPolicies(companyId).filter( + const agentPolicies = db.budget.listBudgetPolicies(companyId).filter( (policy) => policy.scopeType === "agent" && policy.scopeId === agentId, ); expect(secondPolicyId).toBe(firstPolicyId); expect(agentPolicies).toHaveLength(1); expect(agentPolicies[0]?.amount).toBe(25_000); - expect(db.getActivePoliciesForRun(companyId, agentId, null).map((policy) => policy.amount)).toEqual([25_000]); + expect(db.budget.getActivePoliciesForRun(companyId, agentId, null).map((policy) => policy.amount)).toEqual([25_000]); db.close(); }); diff --git a/tests/unit/agent-hire-routing.test.ts b/tests/unit/agent-hire-routing.test.ts index e1fb5759..dd3a20ed 100644 --- a/tests/unit/agent-hire-routing.test.ts +++ b/tests/unit/agent-hire-routing.test.ts @@ -28,14 +28,14 @@ describe("hire routing guards", () => { description: "Hire routing regression coverage", status: "active", }); - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", version: "test", lastError: null, }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: profileDir, diff --git a/tests/unit/agent-permissions.test.ts b/tests/unit/agent-permissions.test.ts index 4da95caf..83e1064f 100644 --- a/tests/unit/agent-permissions.test.ts +++ b/tests/unit/agent-permissions.test.ts @@ -35,7 +35,7 @@ describe("canManageOrganization", () => { description: "Tests recursive org authority", status: "active", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: profileDir, @@ -105,7 +105,7 @@ describe("canManageOrganization", () => { description: "Tests permission-based org authority", status: "active", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: profileDir, @@ -159,7 +159,7 @@ describe("canManageOrganization", () => { description: "Tests that branch growth does not unlock org-wide admin powers", status: "active", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: profileDir, diff --git a/tests/unit/approval-hire-routing.test.ts b/tests/unit/approval-hire-routing.test.ts index d54ab3aa..859871a0 100644 --- a/tests/unit/approval-hire-routing.test.ts +++ b/tests/unit/approval-hire-routing.test.ts @@ -23,7 +23,7 @@ describe("hire approval routing", () => { const db = new AppDatabase(profileDir); db.init(); - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", @@ -36,7 +36,7 @@ describe("hire approval routing", () => { description: "Tests recursive hire wake routing", status: "active", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: profileDir, diff --git a/tests/unit/approval-review-feedback-writeback.test.ts b/tests/unit/approval-review-feedback-writeback.test.ts index dbf29cb9..13f3a9f8 100644 --- a/tests/unit/approval-review-feedback-writeback.test.ts +++ b/tests/unit/approval-review-feedback-writeback.test.ts @@ -23,7 +23,7 @@ describe("deliverable review feedback writeback", () => { const db = new AppDatabase(profileDir); db.init(); - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", @@ -36,7 +36,7 @@ describe("deliverable review feedback writeback", () => { description: "Tests review feedback knowledge writeback", status: "active", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: profileDir, @@ -67,7 +67,7 @@ describe("deliverable review feedback writeback", () => { priority: "high", status: "in_review", }); - const documentId = db.saveDocument({ + const documentId = db.documentsMod.saveDocument({ companyId, type: "knowledge_article", title: "Market report", @@ -107,7 +107,7 @@ describe("deliverable review feedback writeback", () => { trackTimer: vi.fn(), }, approvalId, companyId, "revision_requested"); - const knowledgeEntries = db.listSnapshot().knowledgeBase.filter((entry) => entry.companyId === companyId); + const knowledgeEntries = db.snap.listSnapshot().knowledgeBase.filter((entry) => entry.companyId === companyId); expect(knowledgeEntries).toHaveLength(1); expect(knowledgeEntries[0]?.category).toBe("lesson_learned"); expect(knowledgeEntries[0]?.importance).toBe("high"); @@ -117,7 +117,7 @@ describe("deliverable review feedback writeback", () => { expect(knowledgeEntries[0]?.content).toContain("stronger competitor evidence"); expect(wakeAgentIfPossible).toHaveBeenCalledWith(agentId, companyId, "revision_needed"); expect(db.entityCrud.getTask(taskId).status).toBe("in_progress"); - const document = db.listSnapshot().documents.find((entry) => entry.id === documentId); + const document = db.snap.listSnapshot().documents.find((entry) => entry.id === documentId); expect(document?.status).toBe("draft"); db.close(); diff --git a/tests/unit/atomic-checkout.test.ts b/tests/unit/atomic-checkout.test.ts index e62c3074..a6062053 100644 --- a/tests/unit/atomic-checkout.test.ts +++ b/tests/unit/atomic-checkout.test.ts @@ -36,7 +36,7 @@ function createTestCompany(db: AppDatabase): string { } function createTestConnector(db: AppDatabase): void { - db.saveConnector({ + db.connectorsCrud.saveConnector({ id: "openai-codex", model: "gpt-5", envBindingText: "", @@ -45,7 +45,7 @@ function createTestConnector(db: AppDatabase): void { } function createTestWorkspace(db: AppDatabase, companyId: string): string { - return db.saveWorkspace({ + return db.workspacesMod.saveWorkspace({ companyId, name: "Test Workspace", localPath: "/tmp/test-workspace", diff --git a/tests/unit/budget-enforcement.test.ts b/tests/unit/budget-enforcement.test.ts index aa5a0cba..8bfa3071 100644 --- a/tests/unit/budget-enforcement.test.ts +++ b/tests/unit/budget-enforcement.test.ts @@ -84,10 +84,7 @@ function createMockDb() { createdAt: string; }> = []; - return { - policies, - costEntries, - + const budget = { saveBudgetPolicy(input: Partial & { companyId: string; scopeType: string; scopeId: string; amount: number }) { const id = input.id ?? crypto.randomUUID(); const existing = policies.findIndex((p) => p.id === id); @@ -150,6 +147,7 @@ function createMockDb() { return computeSpentCents(filtered); }, }; + return { policies, costEntries, budget }; } // ====================================================================== @@ -164,7 +162,7 @@ describe("Budget Policy CRUD", () => { }); it("saveBudgetPolicy creates new policy and returns its ID", () => { - const id = db.saveBudgetPolicy({ + const id = db.budget.saveBudgetPolicy({ companyId: "c1", scopeType: "company", scopeId: "c1", @@ -177,13 +175,13 @@ describe("Budget Policy CRUD", () => { }); it("saveBudgetPolicy updates existing policy when ID provided", () => { - const id = db.saveBudgetPolicy({ + const id = db.budget.saveBudgetPolicy({ companyId: "c1", scopeType: "company", scopeId: "c1", amount: 10000, }); - db.saveBudgetPolicy({ + db.budget.saveBudgetPolicy({ id, companyId: "c1", scopeType: "company", @@ -195,14 +193,14 @@ describe("Budget Policy CRUD", () => { }); it("deleteBudgetPolicy removes policy by ID", () => { - const id = db.saveBudgetPolicy({ + const id = db.budget.saveBudgetPolicy({ companyId: "c1", scopeType: "company", scopeId: "c1", amount: 10000, }); expect(db.policies).toHaveLength(1); - db.deleteBudgetPolicy("c1", id); + db.budget.deleteBudgetPolicy("c1", id); expect(db.policies).toHaveLength(0); }); }); @@ -215,27 +213,27 @@ describe("getActivePoliciesForRun", () => { }); it("returns all active policies matching company+agent+project scopes", () => { - db.saveBudgetPolicy({ companyId: "c1", scopeType: "company", scopeId: "c1", amount: 50000 }); - db.saveBudgetPolicy({ companyId: "c1", scopeType: "agent", scopeId: "a1", amount: 10000 }); - db.saveBudgetPolicy({ companyId: "c1", scopeType: "project", scopeId: "p1", amount: 20000 }); + db.budget.saveBudgetPolicy({ companyId: "c1", scopeType: "company", scopeId: "c1", amount: 50000 }); + db.budget.saveBudgetPolicy({ companyId: "c1", scopeType: "agent", scopeId: "a1", amount: 10000 }); + db.budget.saveBudgetPolicy({ companyId: "c1", scopeType: "project", scopeId: "p1", amount: 20000 }); // Different company — should not match - db.saveBudgetPolicy({ companyId: "c2", scopeType: "company", scopeId: "c2", amount: 99999 }); + db.budget.saveBudgetPolicy({ companyId: "c2", scopeType: "company", scopeId: "c2", amount: 99999 }); - const result = db.getActivePoliciesForRun("c1", "a1", "p1"); + const result = db.budget.getActivePoliciesForRun("c1", "a1", "p1"); expect(result).toHaveLength(3); expect(result.map((p) => p.scopeType).sort()).toEqual(["agent", "company", "project"]); }); it("returns empty array when no policies exist (D-02: no policy = unlimited)", () => { - const result = db.getActivePoliciesForRun("c1", "a1", null); + const result = db.budget.getActivePoliciesForRun("c1", "a1", null); expect(result).toEqual([]); }); it("excludes project scope when projectId is null", () => { - db.saveBudgetPolicy({ companyId: "c1", scopeType: "company", scopeId: "c1", amount: 50000 }); - db.saveBudgetPolicy({ companyId: "c1", scopeType: "project", scopeId: "p1", amount: 20000 }); + db.budget.saveBudgetPolicy({ companyId: "c1", scopeType: "company", scopeId: "c1", amount: 50000 }); + db.budget.saveBudgetPolicy({ companyId: "c1", scopeType: "project", scopeId: "p1", amount: 20000 }); - const result = db.getActivePoliciesForRun("c1", "a1", null); + const result = db.budget.getActivePoliciesForRun("c1", "a1", null); expect(result).toHaveLength(1); expect(result[0]!.scopeType).toBe("company"); }); @@ -261,7 +259,7 @@ describe("getSpentInWindow", () => { amount: 100000, warnPercent: 80, hardStopEnabled: true, notifyEnabled: true, isActive: true, createdAt: now.toISOString(), updatedAt: now.toISOString(), }; - const spent = db.getSpentInWindow(policy); + const spent = db.budget.getSpentInWindow(policy); expect(spent).toBe(380); // (1.50 + 2.30) * 100 = 380 cents }); @@ -278,7 +276,7 @@ describe("getSpentInWindow", () => { amount: 100000, warnPercent: 80, hardStopEnabled: true, notifyEnabled: true, isActive: true, createdAt: now.toISOString(), updatedAt: now.toISOString(), }; - const spent = db.getSpentInWindow(policy); + const spent = db.budget.getSpentInWindow(policy); expect(spent).toBe(500); // Only a1's $5.00 = 500 cents }); @@ -295,7 +293,7 @@ describe("getSpentInWindow", () => { amount: 100000, warnPercent: 80, hardStopEnabled: true, notifyEnabled: true, isActive: true, createdAt: now.toISOString(), updatedAt: now.toISOString(), }; - const spent = db.getSpentInWindow(policy); + const spent = db.budget.getSpentInWindow(policy); expect(spent).toBe(750); // Only p1's $7.50 = 750 cents }); }); diff --git a/tests/unit/budget-incidents.test.ts b/tests/unit/budget-incidents.test.ts index 7f3640d4..59585c11 100644 --- a/tests/unit/budget-incidents.test.ts +++ b/tests/unit/budget-incidents.test.ts @@ -23,8 +23,7 @@ function createMockIncidentDb() { return { start, end }; } - return { - incidents, + const budget = { getCalendarMonthWindow, upsertBudgetIncident( @@ -132,6 +131,7 @@ function createMockIncidentDb() { return true; }, }; + return { incidents, budget }; } function makePolicy(overrides: Partial = {}): BudgetPolicyRecord { @@ -166,7 +166,7 @@ describe("upsertBudgetIncident", () => { it("creates new incident when none exists for policy+window+threshold_type", () => { const policy = makePolicy(); - const id = db.upsertBudgetIncident(policy, "warn_threshold", 8500); + const id = db.budget.upsertBudgetIncident(policy, "warn_threshold", 8500); expect(id).toBeTruthy(); expect(db.incidents).toHaveLength(1); expect(db.incidents[0]!.thresholdType).toBe("warn_threshold"); @@ -176,8 +176,8 @@ describe("upsertBudgetIncident", () => { it("updates amount_observed when open incident already exists (D-08 dedup)", () => { const policy = makePolicy(); - const id1 = db.upsertBudgetIncident(policy, "warn_threshold", 8500); - const id2 = db.upsertBudgetIncident(policy, "warn_threshold", 9200); + const id1 = db.budget.upsertBudgetIncident(policy, "warn_threshold", 8500); + const id2 = db.budget.upsertBudgetIncident(policy, "warn_threshold", 9200); expect(id1).toBe(id2); // Same incident, not a new one expect(db.incidents).toHaveLength(1); // Still just one expect(db.incidents[0]!.amountObserved).toBe(9200); // Updated @@ -185,8 +185,8 @@ describe("upsertBudgetIncident", () => { it("creates separate incidents for different threshold types", () => { const policy = makePolicy(); - db.upsertBudgetIncident(policy, "warn_threshold", 8500); - db.upsertBudgetIncident(policy, "hard_stop", 10500); + db.budget.upsertBudgetIncident(policy, "warn_threshold", 8500); + db.budget.upsertBudgetIncident(policy, "hard_stop", 10500); expect(db.incidents).toHaveLength(2); expect(db.incidents[0]!.thresholdType).toBe("warn_threshold"); expect(db.incidents[1]!.thresholdType).toBe("hard_stop"); @@ -202,10 +202,10 @@ describe("resolveIncidentsForPolicy", () => { it("sets status to provided resolution reason for all open incidents", () => { const policy = makePolicy(); - db.upsertBudgetIncident(policy, "warn_threshold", 8500); - db.upsertBudgetIncident(policy, "hard_stop", 10500); + db.budget.upsertBudgetIncident(policy, "warn_threshold", 8500); + db.budget.upsertBudgetIncident(policy, "hard_stop", 10500); - const count = db.resolveIncidentsForPolicy("pol-1", "auto_resolved_limit_increase"); + const count = db.budget.resolveIncidentsForPolicy("pol-1", "auto_resolved_limit_increase"); expect(count).toBe(2); expect(db.incidents[0]!.status).toBe("auto_resolved_limit_increase"); expect(db.incidents[1]!.status).toBe("auto_resolved_limit_increase"); @@ -214,10 +214,10 @@ describe("resolveIncidentsForPolicy", () => { it("does not affect already-resolved incidents", () => { const policy = makePolicy(); - db.upsertBudgetIncident(policy, "warn_threshold", 8500); - db.resolveIncidentsForPolicy("pol-1", "resolved"); + db.budget.upsertBudgetIncident(policy, "warn_threshold", 8500); + db.budget.resolveIncidentsForPolicy("pol-1", "resolved"); // Now try to resolve again - const count = db.resolveIncidentsForPolicy("pol-1", "auto_resolved_limit_increase"); + const count = db.budget.resolveIncidentsForPolicy("pol-1", "auto_resolved_limit_increase"); expect(count).toBe(0); expect(db.incidents[0]!.status).toBe("resolved"); // Unchanged }); @@ -252,7 +252,7 @@ describe("autoCloseExpiredIncidents", () => { updatedAt: "2025-01-15T00:00:00.000Z", }); - const count = db.autoCloseExpiredIncidents("c1"); + const count = db.budget.autoCloseExpiredIncidents("c1"); expect(count).toBe(1); expect(db.incidents[0]!.status).toBe("auto_closed"); expect(db.incidents[0]!.resolvedAt).toBeTruthy(); @@ -260,10 +260,10 @@ describe("autoCloseExpiredIncidents", () => { it("does not close incidents whose window_end is in the future", () => { const policy = makePolicy(); - db.upsertBudgetIncident(policy, "warn_threshold", 8500); + db.budget.upsertBudgetIncident(policy, "warn_threshold", 8500); // The auto-created incident has window_end in the future (end of current month) - const count = db.autoCloseExpiredIncidents("c1"); + const count = db.budget.autoCloseExpiredIncidents("c1"); expect(count).toBe(0); expect(db.incidents[0]!.status).toBe("open"); }); @@ -278,22 +278,22 @@ describe("listBudgetIncidents", () => { it("returns all incidents for a company when status is 'all'", () => { const policy = makePolicy(); - db.upsertBudgetIncident(policy, "warn_threshold", 8500); - db.upsertBudgetIncident(policy, "hard_stop", 10500); - db.resolveIncidentsForPolicy("pol-1", "resolved"); + db.budget.upsertBudgetIncident(policy, "warn_threshold", 8500); + db.budget.upsertBudgetIncident(policy, "hard_stop", 10500); + db.budget.resolveIncidentsForPolicy("pol-1", "resolved"); - const all = db.listBudgetIncidents("c1", "all"); + const all = db.budget.listBudgetIncidents("c1", "all"); expect(all).toHaveLength(2); }); it("filters by status when specified", () => { const policy = makePolicy(); - db.upsertBudgetIncident(policy, "warn_threshold", 8500); - db.upsertBudgetIncident(policy, "hard_stop", 10500); + db.budget.upsertBudgetIncident(policy, "warn_threshold", 8500); + db.budget.upsertBudgetIncident(policy, "hard_stop", 10500); // Resolve only warn - db.resolveWarnIncidentsForPolicy("pol-1", "resolved"); + db.budget.resolveWarnIncidentsForPolicy("pol-1", "resolved"); - const open = db.listBudgetIncidents("c1", "open"); + const open = db.budget.listBudgetIncidents("c1", "open"); expect(open).toHaveLength(1); expect(open[0]!.thresholdType).toBe("hard_stop"); }); @@ -308,7 +308,7 @@ describe("checkAutoResolve (D-06)", () => { it("auto-resolves incidents when policy limit increases above observed amount", () => { const policy = makePolicy({ amount: 10000 }); - db.upsertBudgetIncident(policy, "hard_stop", 10500); + db.budget.upsertBudgetIncident(policy, "hard_stop", 10500); expect(db.incidents[0]!.status).toBe("open"); // Simulate limit increase: now checking if spent < new limit @@ -316,7 +316,7 @@ describe("checkAutoResolve (D-06)", () => { const spent = 10500; const newAmount = 20000; if (spent < newAmount) { - db.resolveIncidentsForPolicy("pol-1", "auto_resolved_limit_increase"); + db.budget.resolveIncidentsForPolicy("pol-1", "auto_resolved_limit_increase"); } expect(db.incidents[0]!.status).toBe("auto_resolved_limit_increase"); @@ -324,14 +324,14 @@ describe("checkAutoResolve (D-06)", () => { it("auto-resolves only warn incidents when below warn threshold but above hard stop", () => { const policy = makePolicy({ amount: 10000, warnPercent: 80 }); - db.upsertBudgetIncident(policy, "warn_threshold", 8500); - db.upsertBudgetIncident(policy, "hard_stop", 10500); + db.budget.upsertBudgetIncident(policy, "warn_threshold", 8500); + db.budget.upsertBudgetIncident(policy, "hard_stop", 10500); // Spent is now 7500 (below warn threshold of 8000 = 10000*80/100) const spent = 7500; const _warnAmount = Math.floor((policy.amount * policy.warnPercent) / 100); if (spent < policy.amount) { - db.resolveIncidentsForPolicy("pol-1", "auto_resolved_limit_increase"); + db.budget.resolveIncidentsForPolicy("pol-1", "auto_resolved_limit_increase"); } // Both resolved since spent < policy.amount expect(db.incidents.every((i) => i.status === "auto_resolved_limit_increase")).toBe(true); diff --git a/tests/unit/company-mode-switch.test.ts b/tests/unit/company-mode-switch.test.ts index c2af425f..41208593 100644 --- a/tests/unit/company-mode-switch.test.ts +++ b/tests/unit/company-mode-switch.test.ts @@ -32,7 +32,7 @@ describe("company mode switching", () => { const db = new AppDatabase(profileDir); db.init(); - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", @@ -48,7 +48,7 @@ describe("company mode switching", () => { autoApproveHires: false, reviewDeliverables: true, }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: profileDir, @@ -112,7 +112,7 @@ describe("company mode switching", () => { }); threadIntegration.onApprovalCreated(approvalId, companyId, taskId, "deliverable_review", "Review deliverable"); - expect(db.listThreadWaitStates(companyId).length).toBeGreaterThan(0); + expect(db.threads.listThreadWaitStates(companyId).length).toBeGreaterThan(0); expect(db.entityCrud.getTask(taskId).requiresUserReview).toBe(true); expect(db.approvalsMod.getApprovalRecord(approvalId).state).toBe("pending"); @@ -122,7 +122,7 @@ describe("company mode switching", () => { handlers.set(channel, handler); }, db, - ensureCompanyExists: (id) => (db.hasCompany(id) ? null : { ok: false, error: { code: "COMPANY_NOT_FOUND", message: "missing" } }), + ensureCompanyExists: (id) => (db.settings.hasCompany(id) ? null : { ok: false, error: { code: "COMPANY_NOT_FOUND", message: "missing" } }), publishDomainChanged: vi.fn(), handleApprovalResolved: (resolvedApprovalId, resolvedCompanyId, decision) => { threadIntegration.onApprovalResolved(resolvedApprovalId, resolvedCompanyId, decision); @@ -148,7 +148,7 @@ describe("company mode switching", () => { expect(result.ok).toBe(true); expect(db.company.getCompanyExecutionMode(companyId)).toBe("ai_ceo"); - expect(db.listThreadWaitStates(companyId)).toHaveLength(0); + expect(db.threads.listThreadWaitStates(companyId)).toHaveLength(0); expect(db.entityCrud.getTask(taskId).requiresUserReview).toBe(false); expect(db.approvalsMod.getApprovalRecord(approvalId).state).toBe("approved"); diff --git a/tests/unit/database.test.ts b/tests/unit/database.test.ts index 5ac8fa91..f1738d91 100644 --- a/tests/unit/database.test.ts +++ b/tests/unit/database.test.ts @@ -39,7 +39,7 @@ describe("AppDatabase", () => { description: "Operator company", status: "active", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary Workspace", localPath: "/tmp/northline", @@ -60,14 +60,14 @@ describe("AppDatabase", () => { capabilities: "Directs execution", budgetMonthlyUsd: 250, }); - const goalId = db.saveGoal({ + const goalId = db.entityCrud.saveGoal({ companyId, title: "Ship the desktop product", description: "Stabilize the local operator console.", ownerAgentId: agentId, status: "active", }); - const projectId = db.saveProject({ + const projectId = db.entityCrud.saveProject({ companyId, goalId, name: "Desktop Rebuild", @@ -88,20 +88,20 @@ describe("AppDatabase", () => { status: "todo", }); - db.saveSecret({ + db.entityCrud.saveSecret({ companyId, name: "vault-openai", description: "OpenAI API token", value: "sk-test-secret", }); - db.saveConnector({ + db.connectorsCrud.saveConnector({ id: "openai-codex", model: "gpt-5", envBindingText: "OPENAI_API_KEY=vault-openai", notes: "Primary coding connector", }); - const snapshot = db.listSnapshot(); + const snapshot = db.snap.listSnapshot(); expect(snapshot.currentCompanyId).toBe(companyId); expect(snapshot.workspaces[0]?.id).toBe(workspaceId); @@ -109,7 +109,7 @@ describe("AppDatabase", () => { expect(snapshot.goals[0]?.id).toBe(goalId); expect(snapshot.projects[0]?.id).toBe(projectId); expect(snapshot.tasks[0]?.id).toBe(taskId); - expect(db.resolveSecretEnv("OPENAI_API_KEY=vault-openai", companyId)).toEqual({ + expect(db.entityCrud.resolveSecretEnv("OPENAI_API_KEY=vault-openai", companyId)).toEqual({ OPENAI_API_KEY: "sk-test-secret", }); }); @@ -122,7 +122,7 @@ describe("AppDatabase", () => { }); const backupDir = await mkdtemp(join(tmpdir(), "agentcompany-backup-")); - await db.exportBackup(backupDir); + await db.backupMod.exportBackup(backupDir); db.company.saveCompany({ id: companyId, name: "Mutated Company", @@ -130,9 +130,9 @@ describe("AppDatabase", () => { status: "paused", }); - await db.restoreFromBackup(backupDir); + await db.backupMod.restoreFromBackup(backupDir); - const snapshot = db.listSnapshot(); + const snapshot = db.snap.listSnapshot(); expect(snapshot.companies[0]?.name).toBe("Backup Test Co"); await rm(backupDir, { recursive: true, force: true }); @@ -150,19 +150,19 @@ describe("AppDatabase", () => { status: "active", }); - db.saveSecret({ + db.entityCrud.saveSecret({ companyId: alphaCompanyId, name: "shared-secret-name", description: "Alpha secret", value: "alpha-secret", }); - expect(db.resolveSecretEnv("API_KEY=shared-secret-name", alphaCompanyId)).toEqual({ + expect(db.entityCrud.resolveSecretEnv("API_KEY=shared-secret-name", alphaCompanyId)).toEqual({ API_KEY: "alpha-secret", }); // Beta company has no secret named "shared-secret-name", so resolveSecretEnv // falls back to using the raw value literally (allows direct env bindings). - expect(db.resolveSecretEnv("API_KEY=shared-secret-name", betaCompanyId)).toEqual({ + expect(db.entityCrud.resolveSecretEnv("API_KEY=shared-secret-name", betaCompanyId)).toEqual({ API_KEY: "shared-secret-name", }); }); @@ -173,7 +173,7 @@ describe("AppDatabase", () => { description: "Atomic checkout validation", status: "active", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Execution Workspace", localPath: "/tmp/checkout", @@ -194,7 +194,7 @@ describe("AppDatabase", () => { capabilities: "Leads", budgetMonthlyUsd: 100, }); - db.saveConnector({ + db.connectorsCrud.saveConnector({ id: "openai-codex", model: "gpt-5", envBindingText: "", @@ -238,7 +238,7 @@ describe("AppDatabase", () => { description: "Budget hard stop validation", status: "active", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Budget Workspace", localPath: "/tmp/budget", @@ -395,7 +395,7 @@ describe("AppDatabase", () => { it("runs forward-only migrations and creates schema_migrations table", () => { // The migration system runs during init() — verify the tracking table exists - const snapshot = db.listSnapshot(); + const snapshot = db.snap.listSnapshot(); expect(snapshot).toBeTruthy(); // Verify we can re-init without errors (migrations are idempotent) @@ -403,7 +403,7 @@ describe("AppDatabase", () => { db = new AppDatabase(tempDir); db.init(); - const snapshot2 = db.listSnapshot(); + const snapshot2 = db.snap.listSnapshot(); expect(snapshot2).toBeTruthy(); }); @@ -413,7 +413,7 @@ describe("AppDatabase", () => { description: "Workspace lock test", status: "active", }); - const wsId = db.saveWorkspace({ + const wsId = db.workspacesMod.saveWorkspace({ companyId, name: "Lockable WS", localPath: "/tmp/lock-test", @@ -423,24 +423,24 @@ describe("AppDatabase", () => { }); // Acquire lock for run-1 - expect(db.acquireWorkspaceLock(wsId, "run-1")).toBe(true); - let ws = db.getWorkspace(wsId); + expect(db.workspacesMod.acquireWorkspaceLock(wsId, "run-1")).toBe(true); + let ws = db.workspacesMod.getWorkspace(wsId); expect(ws?.state).toBe("busy"); // Same run can re-acquire (idempotent) - expect(db.acquireWorkspaceLock(wsId, "run-1")).toBe(true); + expect(db.workspacesMod.acquireWorkspaceLock(wsId, "run-1")).toBe(true); // Different run cannot acquire while run-1 holds the lock - expect(db.acquireWorkspaceLock(wsId, "run-2")).toBe(false); + expect(db.workspacesMod.acquireWorkspaceLock(wsId, "run-2")).toBe(false); // Release lock - db.releaseWorkspaceLock(wsId, "run-1"); - ws = db.getWorkspace(wsId); + db.workspacesMod.releaseWorkspaceLock(wsId, "run-1"); + ws = db.workspacesMod.getWorkspace(wsId); expect(ws?.state).toBe("ready"); // Now run-2 can acquire - expect(db.acquireWorkspaceLock(wsId, "run-2")).toBe(true); - db.releaseWorkspaceLock(wsId, "run-2"); + expect(db.workspacesMod.acquireWorkspaceLock(wsId, "run-2")).toBe(true); + db.workspacesMod.releaseWorkspaceLock(wsId, "run-2"); }); it("releases all workspace locks on crash recovery", () => { @@ -449,7 +449,7 @@ describe("AppDatabase", () => { description: "Lock recovery test", status: "active", }); - const wsId = db.saveWorkspace({ + const wsId = db.workspacesMod.saveWorkspace({ companyId, name: "Recovery WS", localPath: "/tmp/recovery-test", @@ -458,11 +458,11 @@ describe("AppDatabase", () => { isPrimary: true, }); - db.acquireWorkspaceLock(wsId, "stale-run"); - expect(db.getWorkspace(wsId)?.state).toBe("busy"); + db.workspacesMod.acquireWorkspaceLock(wsId, "stale-run"); + expect(db.workspacesMod.getWorkspace(wsId)?.state).toBe("busy"); - db.releaseAllWorkspaceLocks(); - expect(db.getWorkspace(wsId)?.state).toBe("ready"); + db.workspacesMod.releaseAllWorkspaceLocks(); + expect(db.workspacesMod.getWorkspace(wsId)?.state).toBe("ready"); }); it("purges expired run logs based on retention setting", async () => { @@ -471,7 +471,7 @@ describe("AppDatabase", () => { description: "Log purge test", status: "active", }); - const wsId = db.saveWorkspace({ + const wsId = db.workspacesMod.saveWorkspace({ companyId, name: "PurgeWS", localPath: "/tmp/purge", @@ -521,7 +521,7 @@ describe("AppDatabase", () => { expect(db.runs.readRunLog(run.id)).toContain("test log line"); // Set retention to 1 day and manually backdate the run's finished_at - db.updateSettings({ logRetentionDays: 1 }); + db.settings.updateSettings({ logRetentionDays: 1 }); // Backdate finished_at to 10 days ago const tenDaysAgo = new Date(Date.now() - 10 * 86_400_000).toISOString(); // Direct SQL update for test only @@ -529,7 +529,7 @@ describe("AppDatabase", () => { .prepare("update runs set finished_at = ? where id = ?") .run(tenDaysAgo, run.id); - const purged = db.purgeExpiredRunLogs(); + const purged = db.settings.purgeExpiredRunLogs(); expect(purged).toBe(1); // Log should be gone after purge diff --git a/tests/unit/edge-case-budget.test.ts b/tests/unit/edge-case-budget.test.ts index df2f56b7..92a4bb46 100644 --- a/tests/unit/edge-case-budget.test.ts +++ b/tests/unit/edge-case-budget.test.ts @@ -25,13 +25,13 @@ function scaffold(db: AppDatabase) { description: "Budget exhaustion mid-run edge case tests (D-05.2)", status: "active", }); - db.saveConnector({ + db.connectorsCrud.saveConnector({ id: "gemini-cli", model: "gemini-pro", envBindingText: "", notes: "Test connector", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: "/tmp/budget-edge-test", @@ -52,14 +52,14 @@ function scaffold(db: AppDatabase) { capabilities: "Executes tasks", budgetMonthlyUsd: 100, }); - const goalId = db.saveGoal({ + const goalId = db.entityCrud.saveGoal({ companyId, title: "Ship feature", description: "Build budget edge case test", ownerAgentId: agentId, status: "active", }); - const projectId = db.saveProject({ + const projectId = db.entityCrud.saveProject({ companyId, goalId, name: "Budget Project", @@ -119,7 +119,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { }); // Create company budget policy: 500 cents ($5), hard stop enabled - db.saveBudgetPolicy({ + db.budget.saveBudgetPolicy({ companyId, scopeType: "company", scopeId: companyId, @@ -205,7 +205,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { }); // Create company budget policy: 500 cents ($5) - db.saveBudgetPolicy({ + db.budget.saveBudgetPolicy({ companyId, scopeType: "company", scopeId: companyId, @@ -253,7 +253,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { }); // Create company budget policy with known ID - const policyId = db.saveBudgetPolicy({ + const policyId = db.budget.saveBudgetPolicy({ companyId, scopeType: "company", scopeId: companyId, @@ -301,7 +301,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { }); // Company-level: plenty of room (10000 cents = $100) - db.saveBudgetPolicy({ + db.budget.saveBudgetPolicy({ companyId, scopeType: "company", scopeId: companyId, @@ -310,7 +310,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { }); // Agent-level: tight budget (100 cents = $1) - db.saveBudgetPolicy({ + db.budget.saveBudgetPolicy({ companyId, scopeType: "agent", scopeId: agentId, @@ -358,7 +358,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { }); // Create policy with 300 cents ($3) budget - db.saveBudgetPolicy({ + db.budget.saveBudgetPolicy({ companyId, scopeType: "company", scopeId: companyId, @@ -408,7 +408,7 @@ describe("Edge Case: Budget Exhaustion Mid-Run (D-05.2)", () => { }); // Create tight budget - db.saveBudgetPolicy({ + db.budget.saveBudgetPolicy({ companyId, scopeType: "company", scopeId: companyId, diff --git a/tests/unit/edge-case-concurrent.test.ts b/tests/unit/edge-case-concurrent.test.ts index 26e0ca75..f578193e 100644 --- a/tests/unit/edge-case-concurrent.test.ts +++ b/tests/unit/edge-case-concurrent.test.ts @@ -36,7 +36,7 @@ function createTestCompany(db: AppDatabase): string { } function createTestConnector(db: AppDatabase): void { - db.saveConnector({ + db.connectorsCrud.saveConnector({ id: "gemini-cli", model: "gemini-pro", envBindingText: "", @@ -45,7 +45,7 @@ function createTestConnector(db: AppDatabase): void { } function createTestWorkspace(db: AppDatabase, companyId: string): string { - return db.saveWorkspace({ + return db.workspacesMod.saveWorkspace({ companyId, name: "Test Workspace", localPath: "/tmp/concurrent-test", diff --git a/tests/unit/edge-case-session.test.ts b/tests/unit/edge-case-session.test.ts index 603320c8..84f7eb5d 100644 --- a/tests/unit/edge-case-session.test.ts +++ b/tests/unit/edge-case-session.test.ts @@ -28,14 +28,14 @@ function scaffold(db: AppDatabase, connectorId: ConnectorId = "gemini-cli") { }); // Always save gemini-cli (needed for most tests) - db.saveConnector({ + db.connectorsCrud.saveConnector({ id: "gemini-cli", model: "gemini-pro", envBindingText: "", notes: "Test connector - no native context management", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: "/tmp/session-edge-test", @@ -56,14 +56,14 @@ function scaffold(db: AppDatabase, connectorId: ConnectorId = "gemini-cli") { capabilities: "Executes tasks", budgetMonthlyUsd: 100, }); - const goalId = db.saveGoal({ + const goalId = db.entityCrud.saveGoal({ companyId, title: "Ship feature", description: "Build session rotation edge case test", ownerAgentId: agentId, status: "active", }); - const projectId = db.saveProject({ + const projectId = db.entityCrud.saveProject({ companyId, goalId, name: "Session Project", diff --git a/tests/unit/ipc-agent-graph-handlers.test.ts b/tests/unit/ipc-agent-graph-handlers.test.ts index d056008d..6e21705e 100644 --- a/tests/unit/ipc-agent-graph-handlers.test.ts +++ b/tests/unit/ipc-agent-graph-handlers.test.ts @@ -31,7 +31,7 @@ describe("ipc-core-agent-graph-handlers model enforcement", () => { const db = new AppDatabase(profileDir); db.init(); - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", @@ -43,8 +43,8 @@ describe("ipc-core-agent-graph-handlers model enforcement", () => { description: "Tests saveAgent model validation", status: "active", }); - db.setDisabledModels(companyId, "openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]); - const workspaceId = db.saveWorkspace({ + db.settings.setDisabledModels(companyId, "openai-codex", [{ id: "gpt-5.4", label: "gpt-5.4" }]); + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: profileDir, @@ -72,7 +72,7 @@ describe("ipc-core-agent-graph-handlers model enforcement", () => { handlers.set(channel, handler); }, db, - ensureCompanyExists: (id) => (db.hasCompany(id) ? null : { ok: false, error: { code: "COMPANY_NOT_FOUND", message: "missing" } }), + ensureCompanyExists: (id) => (db.settings.hasCompany(id) ? null : { ok: false, error: { code: "COMPANY_NOT_FOUND", message: "missing" } }), findInvalidReference: () => null, publishDomainChanged: vi.fn(), handleApprovalResolved: vi.fn(), diff --git a/tests/unit/ipc-core-admin-handlers.test.ts b/tests/unit/ipc-core-admin-handlers.test.ts index 6a49dd8d..c4c4e343 100644 --- a/tests/unit/ipc-core-admin-handlers.test.ts +++ b/tests/unit/ipc-core-admin-handlers.test.ts @@ -63,7 +63,7 @@ describe("ipc-core-admin-handlers connector saves", () => { }) as { ok: boolean; error?: { code: string; message: string } }; expect(result.ok).toBe(true); - expect(db.getConnector("anthropic").model).toBe(anthropicModel); + expect(db.connectorsCrud.getConnector("anthropic").model).toBe(anthropicModel); db.close(); }); diff --git a/tests/unit/knowledge-provenance.test.ts b/tests/unit/knowledge-provenance.test.ts index bcab758a..043a7adb 100644 --- a/tests/unit/knowledge-provenance.test.ts +++ b/tests/unit/knowledge-provenance.test.ts @@ -28,7 +28,7 @@ describe("knowledge provenance", () => { status: "active", }); - const knowledgeId = db.saveKnowledgeEntry({ + const knowledgeId = db.knowledgeMod.saveKnowledgeEntry({ companyId, category: "lesson_learned", topic: "Original lesson", @@ -37,7 +37,7 @@ describe("knowledge provenance", () => { sourceRunId: null, }); - db.saveKnowledgeEntry({ + db.knowledgeMod.saveKnowledgeEntry({ id: knowledgeId, companyId, category: "lesson_learned", diff --git a/tests/unit/message-dispatch.test.ts b/tests/unit/message-dispatch.test.ts index ae9f6ae0..9db78551 100644 --- a/tests/unit/message-dispatch.test.ts +++ b/tests/unit/message-dispatch.test.ts @@ -19,7 +19,7 @@ afterEach(async () => { }); function createScaffold(db: AppDatabase) { - db.updateConnectorHealth({ + db.connectorsCrud.updateConnectorHealth({ id: "openai-codex", status: "ready", authState: "authenticated", @@ -33,7 +33,7 @@ function createScaffold(db: AppDatabase) { status: "active", executionMode: "ai_cofounder", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: "/tmp/message-dispatch", @@ -282,7 +282,7 @@ describe("message dispatch", () => { }); threadIntegration.onTaskCreated(taskId, companyId); - const thread = db.listThreads({ + const thread = db.threads.listThreads({ companyId, type: "task", relatedEntityType: "task", @@ -294,21 +294,21 @@ describe("message dispatch", () => { throw new Error("Expected task thread to exist."); } - const replyWait = db.setThreadWaitState({ + const replyWait = db.threads.setThreadWaitState({ threadId: thread.id, companyId, waitingFor: "user_reply", relatedApprovalId: null, reason: "Reply needed", }); - const reviewWait = db.setThreadWaitState({ + const reviewWait = db.threads.setThreadWaitState({ threadId: thread.id, companyId, waitingFor: "user_review", relatedApprovalId: null, reason: "Review needed", }); - const approvalWait = db.setThreadWaitState({ + const approvalWait = db.threads.setThreadWaitState({ threadId: thread.id, companyId, waitingFor: "user_approval", @@ -328,7 +328,7 @@ describe("message dispatch", () => { body: "Board-wide update", }); - let unresolved = db.listThreadWaitStates(companyId); + let unresolved = db.threads.listThreadWaitStates(companyId); expect(unresolved.some((entry) => entry.id === replyWait.id)).toBe(false); expect(unresolved.some((entry) => entry.id === reviewWait.id)).toBe(true); @@ -346,7 +346,7 @@ describe("message dispatch", () => { body: "Specific reply for the thread", }); - unresolved = db.listThreadWaitStates(companyId); + unresolved = db.threads.listThreadWaitStates(companyId); expect(unresolved.some((entry) => entry.id === reviewWait.id)).toBe(false); expect(unresolved.some((entry) => entry.id === approvalWait.id)).toBe(true); diff --git a/tests/unit/resolve-workspace.test.ts b/tests/unit/resolve-workspace.test.ts index aad855db..ba63a9cc 100644 --- a/tests/unit/resolve-workspace.test.ts +++ b/tests/unit/resolve-workspace.test.ts @@ -32,13 +32,13 @@ function seedCompany(db: AppDatabase) { description: "Regression fixture", status: "active", }); - db.saveConnector({ + db.connectorsCrud.saveConnector({ id: "openai-codex", model: "gpt-5", envBindingText: "", notes: "fixture", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Default", localPath: "/tmp/fixture", @@ -91,13 +91,13 @@ describe("resolveWorkspaceWithLockMode", () => { it("project-bound workspace → exclusive (single writer per repo)", () => { const { companyId } = seedCompany(db); - const projectId = db.saveProject({ + const projectId = db.entityCrud.saveProject({ companyId, name: "Platform", description: "", status: "active", }); - db.saveWorkspace({ + db.workspacesMod.saveWorkspace({ companyId, name: "Project repo", localPath: "/tmp/project-ws", @@ -131,7 +131,7 @@ describe("resolveWorkspaceWithLockMode", () => { it("task workspace wins over agent workspace even when both are set", () => { const { companyId, workspaceId: agentWsId } = seedCompany(db); - const taskWsId = db.saveWorkspace({ + const taskWsId = db.workspacesMod.saveWorkspace({ companyId, name: "Task-only", localPath: "/tmp/task-ws", diff --git a/tests/unit/session-management.test.ts b/tests/unit/session-management.test.ts index 4dd6ba98..99d4752c 100644 --- a/tests/unit/session-management.test.ts +++ b/tests/unit/session-management.test.ts @@ -26,7 +26,7 @@ function scaffold(db: AppDatabase) { description: "Testing session management", status: "active", }); - const workspaceId = db.saveWorkspace({ + const workspaceId = db.workspacesMod.saveWorkspace({ companyId, name: "Primary", localPath: "/tmp/session-test", @@ -47,14 +47,14 @@ function scaffold(db: AppDatabase) { capabilities: "Writes code", budgetMonthlyUsd: 100, }); - const goalId = db.saveGoal({ + const goalId = db.entityCrud.saveGoal({ companyId, title: "Ship feature", description: "Build session management", ownerAgentId: agentId, status: "active", }); - const projectId = db.saveProject({ + const projectId = db.entityCrud.saveProject({ companyId, goalId, name: "Session Project", diff --git a/tests/unit/task-quality-gates.test.ts b/tests/unit/task-quality-gates.test.ts index 074bd527..a585d74f 100644 --- a/tests/unit/task-quality-gates.test.ts +++ b/tests/unit/task-quality-gates.test.ts @@ -154,8 +154,10 @@ describe("checkCapabilityMatch", () => { entityCrud: { getAgent: (_id: string) => agent, }, - listAgentSkillAssignments: (_id: string) => skillAssignments, - getCompanySkill: (_companyId: string, skillId: string) => skills[skillId] ?? null, + skills: { + listAgentSkillAssignments: (_id: string) => skillAssignments, + getCompanySkill: (_companyId: string, skillId: string) => skills[skillId] ?? null, + }, }; } diff --git a/tests/unit/tasks-phase.test.ts b/tests/unit/tasks-phase.test.ts index cb4f9a8a..b11f8d21 100644 --- a/tests/unit/tasks-phase.test.ts +++ b/tests/unit/tasks-phase.test.ts @@ -35,7 +35,7 @@ function createTestCompany(db: AppDatabase): string { } function createTestConnector(db: AppDatabase): void { - db.saveConnector({ + db.connectorsCrud.saveConnector({ id: "openai-codex", model: "gpt-5", envBindingText: "", @@ -44,7 +44,7 @@ function createTestConnector(db: AppDatabase): void { } function createTestWorkspace(db: AppDatabase, companyId: string): string { - return db.saveWorkspace({ + return db.workspacesMod.saveWorkspace({ companyId, name: "Phase Workspace", localPath: "/tmp/phase-workspace", diff --git a/tests/unit/wakeup-lifecycle.test.ts b/tests/unit/wakeup-lifecycle.test.ts index 9712b395..b9c11d55 100644 --- a/tests/unit/wakeup-lifecycle.test.ts +++ b/tests/unit/wakeup-lifecycle.test.ts @@ -27,7 +27,7 @@ function createTestCompany(db: AppDatabase): string { } function createTestConnector(db: AppDatabase): void { - db.saveConnector({ + db.connectorsCrud.saveConnector({ id: "openai-codex", model: "gpt-5", envBindingText: "", @@ -36,7 +36,7 @@ function createTestConnector(db: AppDatabase): void { } function createTestWorkspace(db: AppDatabase, companyId: string): string { - return db.saveWorkspace({ + return db.workspacesMod.saveWorkspace({ companyId, name: "Wakeup Workspace", localPath: "/tmp/wakeup-workspace", @@ -87,9 +87,9 @@ describe("wakeup lifecycle", () => { const workspaceId = createTestWorkspace(db, companyId); const agentId = createTestAgent(db, companyId, workspaceId); - db.enqueueWakeupRequest(companyId, agentId, "task.created", "New task assigned"); + db.wakeup.enqueueWakeupRequest(companyId, agentId, "task.created", "New task assigned"); - const wakes = db.listWakeupRequests(companyId); + const wakes = db.wakeup.listWakeupRequests(companyId); expect(wakes.length).toBe(1); expect(wakes[0].agentId).toBe(agentId); expect(wakes[0].status).toBe("queued"); @@ -107,10 +107,10 @@ describe("wakeup lifecycle", () => { const workspaceId = createTestWorkspace(db, companyId); const agentId = createTestAgent(db, companyId, workspaceId); - db.enqueueWakeupRequest(companyId, agentId, "task.created", "First task"); - db.enqueueWakeupRequest(companyId, agentId, "task.status_changed", "Second trigger"); + db.wakeup.enqueueWakeupRequest(companyId, agentId, "task.created", "First task"); + db.wakeup.enqueueWakeupRequest(companyId, agentId, "task.status_changed", "Second trigger"); - const wakes = db.listWakeupRequests(companyId); + const wakes = db.wakeup.listWakeupRequests(companyId); // D-07: Per-agent scope -- only one wakeup row should exist expect(wakes.length).toBe(1); expect(wakes[0].coalescedCount).toBe(1); @@ -128,15 +128,15 @@ describe("wakeup lifecycle", () => { const agent1Id = createTestAgent(db, companyId, workspaceId, "Agent1"); const agent2Id = createTestAgent(db, companyId, workspaceId, "Agent2"); - db.enqueueWakeupRequest(companyId, agent1Id, "task.created", "Agent1 task"); - db.enqueueWakeupRequest(companyId, agent2Id, "task.created", "Agent2 task"); + db.wakeup.enqueueWakeupRequest(companyId, agent1Id, "task.created", "Agent1 task"); + db.wakeup.enqueueWakeupRequest(companyId, agent2Id, "task.created", "Agent2 task"); // List all for company - const allWakes = db.listWakeupRequests(companyId); + const allWakes = db.wakeup.listWakeupRequests(companyId); expect(allWakes.length).toBe(2); // Filter by agentId - const agent1Wakes = db.listWakeupRequests(companyId, { agentId: agent1Id }); + const agent1Wakes = db.wakeup.listWakeupRequests(companyId, { agentId: agent1Id }); expect(agent1Wakes.length).toBe(1); expect(agent1Wakes[0].agentId).toBe(agent1Id); });