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.
This commit is contained in:
1 parent
a69fbb053b
commit
118e3d8795
120 files changed
+838
-933
No files matched your search
@@ -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/<module>.ts` and invoke via `db.<module>.<method>(...)`. 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.
|
||||
//
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<typeof s> => 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}` });
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
try {
|
||||
const id = db.saveSprint({
|
||||
const id = db.knowledgeMod.saveSprint({
|
||||
companyId,
|
||||
name: String(data.name ?? "Sprint"),
|
||||
goal: String(data.goal ?? ""),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -26,7 +26,7 @@ export function resolveApprovalDecision({
|
||||
state,
|
||||
decisionNote: note,
|
||||
});
|
||||
db.addActivity({
|
||||
db.activityMod.addActivity({
|
||||
companyId,
|
||||
actor,
|
||||
action: `approval.${state}`,
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -50,7 +50,7 @@ export function handleTaskStatusChange(
|
||||
}
|
||||
if (!task) return;
|
||||
|
||||
db.addActivity({
|
||||
db.activityMod.addActivity({
|
||||
companyId,
|
||||
actor: task.assigneeAgentId ?? "board",
|
||||
action: "task.status_changed",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<void> {
|
||||
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",
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<typeof deps.db.belongsToCompany>[0], id, companyId),
|
||||
deps.db.settings.belongsToCompany(table as Parameters<typeof deps.db.settings.belongsToCompany>[0], id, companyId),
|
||||
}),
|
||||
notificationService,
|
||||
});
|
||||
|
||||
@@ -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."),
|
||||
|
||||
@@ -15,9 +15,9 @@ export interface RecoverScheduledPostsDeps {
|
||||
export function recoverScheduledPosts(deps: RecoverScheduledPostsDeps): Set<ReturnType<typeof setTimeout>> {
|
||||
const timers = new Set<ReturnType<typeof setTimeout>>();
|
||||
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 {
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function runStartupHealthChecks(
|
||||
logger: Logger,
|
||||
): Promise<void> {
|
||||
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)),
|
||||
);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
+18
-59
@@ -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<string, unknown>; configAfter: Record<string, unknown>; source?: string; rolledBackFromRevisionId?: string | null; createdByAgentId?: string | null; createdByUserId?: string | null }) { return this.documentsMod.recordConfigRevision(input); }
|
||||
}
|
||||
+34
-162
@@ -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<typeof this.budget.getSpentInWindow>[0]) { return this.budget.getSpentInWindow(policy); }
|
||||
upsertBudgetIncident(policy: Parameters<typeof this.budget.upsertBudgetIncident>[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<AppDatabaseBase["entityCrud"]["createHireRequest"]>[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<ReturnType<typeof import("./database/documents").buildDocumentMethods>["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<ReturnType<typeof import("./database/documents").buildDocumentMethods>["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<string, unknown>) { 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<ReturnType<typeof buildThreadMethods>["createThread"]>[0]) { return this.threads.createThread(input); }
|
||||
listThreads(options: Parameters<ReturnType<typeof buildThreadMethods>["listThreads"]>[0]) { return this.threads.listThreads(options); }
|
||||
getThread(threadId: string, companyId: string) { return this.threads.getThread(threadId, companyId); }
|
||||
addThreadEvent(input: Parameters<ReturnType<typeof buildThreadMethods>["addThreadEvent"]>[0]) { return this.threads.addThreadEvent(input); }
|
||||
listThreadEvents(threadId: string, companyId: string, options?: Parameters<ReturnType<typeof buildThreadMethods>["listThreadEvents"]>[2]) { return this.threads.listThreadEvents(threadId, companyId, options); }
|
||||
setThreadWaitState(input: Parameters<ReturnType<typeof buildThreadMethods>["setThreadWaitState"]>[0]) { return this.threads.setThreadWaitState(input); }
|
||||
resolveThreadWaitState(waitStateId: string, companyId: string) { return this.threads.resolveThreadWaitState(waitStateId, companyId); }
|
||||
listThreadWaitStates(companyId: string, options?: Parameters<ReturnType<typeof buildThreadMethods>["listThreadWaitStates"]>[1]) { return this.threads.listThreadWaitStates(companyId, options); }
|
||||
hasUnresolvedWaitStateForAgent(companyId: string, agentId: string) { return this.threads.hasUnresolvedWaitStateForAgent(companyId, agentId); }
|
||||
addParticipant(input: Parameters<ReturnType<typeof buildThreadMethods>["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<ReturnType<typeof buildThreadMethods>["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/<module>.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/<module>.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<T>(sql: string, ...params: SQLInputValue[]): T[] { return this.readAll<T>(sql, ...params); }
|
||||
}
|
||||
@@ -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<string, string>;
|
||||
},
|
||||
) {
|
||||
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<string, string>): Record<string, string> {
|
||||
resolveSecretEnv(bindingText: string, companyId: string): Record<string, string> {
|
||||
const invalidKeys = new Set(listInvalidEnvBindingKeys(bindingText));
|
||||
const bindings = parseEnvBindingText(bindingText);
|
||||
const result: Record<string, string> = {};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -27,9 +27,9 @@ const REVIEW_WAIT_STATE_TYPES = new Set(["user_reply", "user_review", "user_appr
|
||||
|
||||
function collectImpactedAgentIdsFromWaitStates(db: AppDatabase, companyId: string): Set<string> {
|
||||
const agentIds = new Set<string>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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([]);
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string>();
|
||||
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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<AppDatabase["getWorkspace"]>; lockMode: "exclusive" | "shared" | "none"; source: string },
|
||||
resolveWorkspace: (task: { workspaceId: string | null; projectId?: string | null } | null, agent: { workspaceId: string | null }, companyId: string) => { workspace: ReturnType<AppDatabase["workspacesMod"]["getWorkspace"]>; 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,28 +31,28 @@ export function resolveWorkspaceWithLockMode(
|
||||
agent: { workspaceId: string | null },
|
||||
companyId: string,
|
||||
): {
|
||||
workspace: ReturnType<AppDatabase["getWorkspace"]>;
|
||||
workspace: ReturnType<AppDatabase["workspacesMod"]["getWorkspace"]>;
|
||||
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" };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 —
|
||||
|
||||
@@ -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)`);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<AppDatabase["getWorkspace"]>; lockMode: "exclusive" | "shared" | "none"; source: string },
|
||||
resolveWorkspace: (task: { workspaceId: string | null; projectId?: string | null } | null, agent: { workspaceId: string | null }, companyId: string) => { workspace: ReturnType<AppDatabase["workspacesMod"]["getWorkspace"]>; 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",
|
||||
|
||||
@@ -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<AppDatabase["entityCrud"]["getTask"]> | null;
|
||||
connector: ReturnType<AppDatabase["getConnector"]>;
|
||||
workspace: ReturnType<AppDatabase["getWorkspace"]>;
|
||||
connector: ReturnType<AppDatabase["connectorsCrud"]["getConnector"]>;
|
||||
workspace: ReturnType<AppDatabase["workspacesMod"]["getWorkspace"]>;
|
||||
workspaceMode: WorkspaceLockMode;
|
||||
companyName: string;
|
||||
companyDescription: string;
|
||||
snapshot: ReturnType<AppDatabase["listSnapshot"]>;
|
||||
snapshot: ReturnType<AppDatabase["snap"]["listSnapshot"]>;
|
||||
trigger: string;
|
||||
/**
|
||||
* Model id resolved by `pickModelForAgent` at dispatch time. When the
|
||||
@@ -63,7 +63,7 @@ function assembleApiEnv(
|
||||
trigger: string,
|
||||
deps: Pick<TurnContextDeps, "getApiPort" | "createRunToken">,
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<SkillRecord> {
|
||||
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<DesktopResult<void>> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<DesktopResult<SkillUpdateStatus>> {
|
||||
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") {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<string>();
|
||||
const affectedCompanyIds = new Map<string, string>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,12 +87,12 @@ export interface WorkerServiceDependencies {
|
||||
companyId: string;
|
||||
agent: ReturnType<AppDatabase["entityCrud"]["getAgent"]>;
|
||||
task: ReturnType<AppDatabase["entityCrud"]["getTask"]> | null;
|
||||
connector: ReturnType<AppDatabase["getConnector"]>;
|
||||
workspace: ReturnType<AppDatabase["getWorkspace"]>;
|
||||
connector: ReturnType<AppDatabase["connectorsCrud"]["getConnector"]>;
|
||||
workspace: ReturnType<AppDatabase["workspacesMod"]["getWorkspace"]>;
|
||||
workspaceMode: "exclusive" | "shared" | "none";
|
||||
companyName: string;
|
||||
companyDescription: string;
|
||||
snapshot: ReturnType<AppDatabase["listSnapshot"]>;
|
||||
snapshot: ReturnType<AppDatabase["snap"]["listSnapshot"]>;
|
||||
trigger: string;
|
||||
}) => void;
|
||||
processRunCompletion: (run: RunRecord, event: z.infer<typeof workerMessageSchema> & { type: "run-finished" }) => void;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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.");
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ async function createHarness(): Promise<Harness> {
|
||||
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<Harness> {
|
||||
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<Harness> {
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Loaded 100 of 120 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user