Rewrite ~525 call sites from `db.<method>()` to `db.<module>.<method>()` for tasks and agents domains (including `getAgent`/`findAgent`/`getTask` which live on the `entityCrud` field). Delete the corresponding pass-through methods on AppDatabaseBase. Structural types that narrowed `db` to a bag of flat methods (CapabilityMatchDb, EscalationDeps, SweepStaleReviewsDeps) are reshaped to match the new namespace. Internal callback references inside database.ts/database-base.ts now invoke the owning module directly (e.g. `this.agents.saveAgent`) rather than the deleted pass-through. The codemod lives at scripts/codemod/remove-appdatabase-passthrough.mjs and filters by owning module + optional explicit method list. It is idempotent and does not re-rewrite already-namespaced calls.
243 lines
9.1 KiB
TypeScript
243 lines
9.1 KiB
TypeScript
import {
|
|
IPC_CHANNELS,
|
|
approvalDecisionSchema,
|
|
approvalInputSchema,
|
|
cancelRunSchema,
|
|
commentInputSchema,
|
|
heartbeatSettingsSchema,
|
|
runLogChunkSchema,
|
|
runLogSchema,
|
|
startRunSchema,
|
|
triggerHeartbeatSchema,
|
|
} from "@shared/contracts";
|
|
import { resolveApprovalDecision } from "./approval-policy";
|
|
import { shouldAutoApprove } from "./execution-policy";
|
|
import { fail, ok, type RegisterCoreHandlersDependencies } from "./ipc-core-common";
|
|
|
|
export function registerCoreOperationHandlers({
|
|
registerHandle,
|
|
db,
|
|
ensureCompanyExists,
|
|
publishDomainChanged,
|
|
emitEvent,
|
|
wakeAgentIfPossible,
|
|
handleApprovalCreated,
|
|
handleApprovalResolved,
|
|
dispatchAutomation,
|
|
queueTaskRun,
|
|
getWorkerProcess,
|
|
notify,
|
|
logger,
|
|
}: Pick<
|
|
RegisterCoreHandlersDependencies,
|
|
| "registerHandle"
|
|
| "db"
|
|
| "ensureCompanyExists"
|
|
| "publishDomainChanged"
|
|
| "emitEvent"
|
|
| "wakeAgentIfPossible"
|
|
| "handleApprovalCreated"
|
|
| "handleApprovalResolved"
|
|
| "dispatchAutomation"
|
|
| "queueTaskRun"
|
|
| "getWorkerProcess"
|
|
| "notify"
|
|
| "logger"
|
|
>) {
|
|
registerHandle(IPC_CHANNELS.requestApproval, async (_event, payload) => {
|
|
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);
|
|
if (invalidTask || invalidAgent || invalidRelatedAgent) {
|
|
return fail("INVALID_COMPANY_REFERENCE", "The approval references data outside the current company.");
|
|
}
|
|
let id: string;
|
|
try {
|
|
id = db.requestApproval(parsed);
|
|
} catch (error) {
|
|
return fail("APPROVAL_INVALID", error instanceof Error ? error.message : "Approval could not be created.");
|
|
}
|
|
db.addActivity({
|
|
companyId: parsed.companyId,
|
|
actor: parsed.requestedByAgentId ?? "board",
|
|
action: "approval.created",
|
|
entityType: "approval",
|
|
entityId: id,
|
|
detail: parsed.payloadSummary,
|
|
});
|
|
|
|
// Notify thread integration about the new approval
|
|
handleApprovalCreated(id, parsed.companyId, parsed.relatedTaskId ?? null, parsed.type, parsed.payloadSummary ?? "");
|
|
|
|
if (shouldAutoApprove(db, parsed.companyId, parsed.type)) {
|
|
resolveApprovalDecision({
|
|
db,
|
|
handleApprovalResolved,
|
|
companyId: parsed.companyId,
|
|
approvalId: id,
|
|
state: "approved",
|
|
decisionNote: "Auto-approved by company policy",
|
|
detail: "Auto-approved by company policy",
|
|
});
|
|
publishDomainChanged();
|
|
return ok(id);
|
|
}
|
|
// Approval was NOT auto-approved — wake decision-makers to review it
|
|
dispatchAutomation("approval_created", {
|
|
companyId: parsed.companyId,
|
|
agentId: parsed.requestedByAgentId ?? undefined,
|
|
taskId: parsed.relatedTaskId ?? undefined,
|
|
});
|
|
notify({
|
|
title: "Approval Required",
|
|
body: parsed.payloadSummary ?? `${parsed.type} approval pending`,
|
|
urgency: "critical",
|
|
navigation: { section: "approvals", entityId: id },
|
|
batchKey: "pending_approval",
|
|
});
|
|
publishDomainChanged();
|
|
return ok(id);
|
|
});
|
|
|
|
registerHandle(IPC_CHANNELS.decideApproval, async (_event, payload) => {
|
|
const parsed = approvalDecisionSchema.parse(payload);
|
|
const companyError = ensureCompanyExists(parsed.companyId);
|
|
if (companyError) return companyError;
|
|
if (!db.belongsToCompany("approvals", parsed.approvalId, parsed.companyId)) {
|
|
return fail("APPROVAL_NOT_FOUND", "Approval not found in the current company.");
|
|
}
|
|
const approval = db.getApprovalRecord(parsed.approvalId);
|
|
if (!approval || approval.companyId !== parsed.companyId) {
|
|
return fail("APPROVAL_NOT_FOUND", "Approval not found in the current company.");
|
|
}
|
|
resolveApprovalDecision({
|
|
db,
|
|
handleApprovalResolved,
|
|
companyId: approval.companyId,
|
|
approvalId: approval.id,
|
|
state: parsed.state,
|
|
decisionNote: parsed.decisionNote,
|
|
detail: parsed.decisionNote,
|
|
});
|
|
emitEvent({ type: "approval-updated", approvalId: approval.id, action: "decided" });
|
|
return ok(true);
|
|
});
|
|
|
|
registerHandle(IPC_CHANNELS.startTaskRun, async (_event, payload) => {
|
|
const parsed = startRunSchema.parse(payload);
|
|
const companyError = ensureCompanyExists(parsed.companyId);
|
|
if (companyError) return companyError;
|
|
if (!db.belongsToCompany("tasks", parsed.taskId, parsed.companyId)) {
|
|
return fail("TASK_NOT_FOUND", "Task not found in the current company.");
|
|
}
|
|
return queueTaskRun(parsed.taskId, "manual", "board");
|
|
});
|
|
|
|
registerHandle(IPC_CHANNELS.cancelRun, (_event, payload) => {
|
|
const parsed = cancelRunSchema.parse(payload);
|
|
const companyError = ensureCompanyExists(parsed.companyId);
|
|
if (companyError) return companyError;
|
|
if (!db.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 });
|
|
return ok(true);
|
|
});
|
|
|
|
registerHandle(IPC_CHANNELS.getRunLog, (_event, payload) => {
|
|
const parsed = runLogSchema.parse(payload);
|
|
const companyError = ensureCompanyExists(parsed.companyId);
|
|
if (companyError) return companyError;
|
|
if (!db.belongsToCompany("runs", parsed.runId, parsed.companyId)) {
|
|
return fail("RUN_NOT_FOUND", "Run not found in the current company.");
|
|
}
|
|
return ok(db.readRunLog(parsed.runId));
|
|
});
|
|
|
|
registerHandle(IPC_CHANNELS.getRunLogChunk, (_event, payload) => {
|
|
const parsed = runLogChunkSchema.parse(payload);
|
|
const companyError = ensureCompanyExists(parsed.companyId);
|
|
if (companyError) return companyError;
|
|
if (!db.belongsToCompany("runs", parsed.runId, parsed.companyId)) {
|
|
return fail("RUN_NOT_FOUND", "Run not found in the current company.");
|
|
}
|
|
return ok(db.readRunLogChunk(parsed.runId, parsed.offset, parsed.limit));
|
|
});
|
|
|
|
registerHandle(IPC_CHANNELS.addComment, async (_event, payload) => {
|
|
const parsed = commentInputSchema.parse(payload);
|
|
const companyError = ensureCompanyExists(parsed.companyId);
|
|
if (companyError) return companyError;
|
|
const comment = db.addComment({
|
|
companyId: parsed.companyId,
|
|
taskId: parsed.taskId,
|
|
authorAgentId: parsed.authorAgentId ?? null,
|
|
authorName: parsed.authorName,
|
|
body: parsed.body,
|
|
});
|
|
db.addActivity({
|
|
companyId: parsed.companyId,
|
|
actor: parsed.authorAgentId ?? "board",
|
|
action: "comment.created",
|
|
entityType: "task",
|
|
entityId: parsed.taskId,
|
|
detail: parsed.body.slice(0, 100),
|
|
});
|
|
emitEvent({ type: "agent-message", taskId: parsed.taskId, comment });
|
|
emitEvent({ type: "task-updated", taskId: parsed.taskId, action: "updated" });
|
|
try {
|
|
const task = db.entityCrud.getTask(parsed.taskId);
|
|
if (task.assigneeAgentId) {
|
|
wakeAgentIfPossible(task.assigneeAgentId, task.companyId, "comment");
|
|
}
|
|
dispatchAutomation("comment_posted", {
|
|
companyId: parsed.companyId,
|
|
taskId: parsed.taskId,
|
|
agentId: parsed.authorAgentId ?? undefined,
|
|
});
|
|
} catch (err) {
|
|
logger.warn(`[ipc/core-operations] Post-comment wake/dispatch failed for task ${parsed.taskId}:`, err instanceof Error ? err.message : String(err));
|
|
}
|
|
return ok(comment.id);
|
|
});
|
|
|
|
registerHandle(IPC_CHANNELS.setHeartbeat, async (_event, payload) => {
|
|
const parsed = heartbeatSettingsSchema.parse(payload);
|
|
const companyError = ensureCompanyExists(parsed.companyId);
|
|
if (companyError) return companyError;
|
|
if (!db.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({
|
|
companyId: parsed.companyId,
|
|
actor: "board",
|
|
action: parsed.enabled ? "heartbeat.enabled" : "heartbeat.disabled",
|
|
entityType: "agent",
|
|
entityId: parsed.agentId,
|
|
detail: `Interval: ${parsed.intervalSec}s`,
|
|
});
|
|
emitEvent({ type: "heartbeat-status", agentId: parsed.agentId, enabled: parsed.enabled, nextWakeAt: null });
|
|
publishDomainChanged();
|
|
return ok(true);
|
|
});
|
|
|
|
registerHandle(IPC_CHANNELS.triggerHeartbeat, async (_event, payload) => {
|
|
const parsed = triggerHeartbeatSchema.parse(payload);
|
|
const companyError = ensureCompanyExists(parsed.companyId);
|
|
if (companyError) return companyError;
|
|
if (!db.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);
|
|
if (!runId) {
|
|
return fail("HEARTBEAT_SKIPPED", "Agent cannot be woken right now (check connector status, budget, or active runs).");
|
|
}
|
|
return ok(runId);
|
|
});
|
|
|
|
}
|