The earlier sweep cleaned the hot path (worker/runtime/automation/api-
server/ipc core — 59 sites). This pass finishes the deferred long tail:
main/* services, database/* CRUD modules, api-server/* routes, ipc-*
handlers, thread-integration, automation helpers, browser, skills.
Default strategy: catch (error) { logger.warn("[module] context:", ...) }
— preserve current behavior, make the failure visible instead of
silently hidden. Widened 3 logger interface signatures to include warn
where callers only passed info/error previously (thread-helpers,
host-environment-service, ipc-domain-common).
Legitimate boundaries left intact: JSON.parse on untrusted input, DNS
resolver no-records paths, explicit safe* helpers (safeJson, parseJson,
parseTriggerDetail), URL-parse predicates, and `<unserialisable>`
fallbacks inside the prompt-builder summarisers. Each kept catch now
either has a reason in context or is a documented safe* helper.
Tests adjusted only where test-constructed no-op loggers needed a warn
stub (message-dispatch, company-mode-switch, ipc-agent-graph-handlers).
With the earlier pass, AgentCompany no longer has silently-swallowing
catch blocks in main-process or renderer business code.
165 lines
5.4 KiB
TypeScript
165 lines
5.4 KiB
TypeScript
import log from "electron-log/main.js";
|
|
import { resolveApprovalDecision } from "./approval-policy";
|
|
import type { AppDatabase } from "./database";
|
|
|
|
type ReconcileExecutionModeChangeInput = {
|
|
db: AppDatabase;
|
|
companyId: string;
|
|
previousExecutionMode: string;
|
|
nextExecutionMode: string;
|
|
previousReviewDeliverables: boolean;
|
|
nextReviewDeliverables: boolean;
|
|
handleApprovalResolved: (approvalId: string, companyId: string, decision: string) => void;
|
|
};
|
|
|
|
const COFOUNDER_TRANSITION_APPROVAL_TYPES = [
|
|
"deliverable_review",
|
|
"document_review",
|
|
"plan_review",
|
|
"scope_review",
|
|
"draft_review",
|
|
"strategy_review",
|
|
"exception_review",
|
|
"approve_ceo_strategy",
|
|
] as const;
|
|
|
|
const REVIEW_WAIT_STATE_TYPES = new Set(["user_reply", "user_review", "user_approval"]);
|
|
|
|
function collectImpactedAgentIdsFromWaitStates(db: AppDatabase, companyId: string): Set<string> {
|
|
const agentIds = new Set<string>();
|
|
const waitStates = db.listThreadWaitStates(companyId);
|
|
for (const waitState of waitStates) {
|
|
const thread = db.getThread(waitState.threadId, companyId);
|
|
if (!thread || thread.relatedEntityType !== "task" || !thread.relatedEntityId) continue;
|
|
try {
|
|
const task = db.getTask(thread.relatedEntityId);
|
|
if (task.assigneeAgentId) {
|
|
agentIds.add(task.assigneeAgentId);
|
|
}
|
|
} catch (err) {
|
|
log.warn(`[execution-mode-reconciler] getTask failed for ${thread.relatedEntityId}:`, err instanceof Error ? err.message : String(err));
|
|
continue;
|
|
}
|
|
}
|
|
return agentIds;
|
|
}
|
|
|
|
function collectImpactedAgentIdsFromReviewState(db: AppDatabase, companyId: string): Set<string> {
|
|
const agentIds = new Set<string>();
|
|
const flaggedTasks = db.queryAll<{ assigneeAgentId: string | null }>(
|
|
"select assignee_agent_id as assigneeAgentId from tasks where company_id = ? and requires_user_review = 1",
|
|
companyId,
|
|
);
|
|
for (const task of flaggedTasks) {
|
|
if (task.assigneeAgentId) {
|
|
agentIds.add(task.assigneeAgentId);
|
|
}
|
|
}
|
|
const pendingApprovals = db.queryAll<{ requestedByAgentId: string | null; relatedTaskId: string | null }>(
|
|
`select requested_by_agent_id as requestedByAgentId, related_task_id as relatedTaskId
|
|
from approvals
|
|
where company_id = ?
|
|
and state = 'pending'
|
|
and type = 'deliverable_review'`,
|
|
companyId,
|
|
);
|
|
for (const approval of pendingApprovals) {
|
|
if (approval.requestedByAgentId) {
|
|
agentIds.add(approval.requestedByAgentId);
|
|
}
|
|
if (!approval.relatedTaskId) continue;
|
|
try {
|
|
const task = db.getTask(approval.relatedTaskId);
|
|
if (task.assigneeAgentId) {
|
|
agentIds.add(task.assigneeAgentId);
|
|
}
|
|
} catch (err) {
|
|
log.warn(`[execution-mode-reconciler] getTask failed for approval ${approval.relatedTaskId}:`, err instanceof Error ? err.message : String(err));
|
|
continue;
|
|
}
|
|
}
|
|
return agentIds;
|
|
}
|
|
|
|
function resumeAgents(db: AppDatabase, companyId: string, agentIds: Iterable<string>): void {
|
|
for (const agentId of agentIds) {
|
|
try {
|
|
const agent = db.getAgent(agentId);
|
|
if (agent.companyId !== companyId) continue;
|
|
if (["terminated", "pending_approval"].includes(agent.status)) continue;
|
|
if (!agent.heartbeatEnabled) {
|
|
db.setHeartbeat(agent.id, true, agent.heartbeatIntervalSec || 120);
|
|
}
|
|
} catch (err) {
|
|
log.warn(`[execution-mode-reconciler] resumeAgents failed for ${agentId}:`, err instanceof Error ? err.message : String(err));
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function reconcileExecutionModeChange({
|
|
db,
|
|
companyId,
|
|
previousExecutionMode,
|
|
nextExecutionMode,
|
|
previousReviewDeliverables,
|
|
nextReviewDeliverables,
|
|
handleApprovalResolved,
|
|
}: ReconcileExecutionModeChangeInput): void {
|
|
const leavingCofounderMode =
|
|
previousExecutionMode === "ai_cofounder" && nextExecutionMode === "ai_ceo";
|
|
const disablingDeliverableReviews =
|
|
previousReviewDeliverables && !nextReviewDeliverables;
|
|
|
|
if (!leavingCofounderMode && !disablingDeliverableReviews) {
|
|
return;
|
|
}
|
|
|
|
const impactedAgents = new Set<string>([
|
|
...collectImpactedAgentIdsFromWaitStates(db, companyId),
|
|
...collectImpactedAgentIdsFromReviewState(db, companyId),
|
|
]);
|
|
|
|
if (leavingCofounderMode) {
|
|
const pendingApprovals = db.queryAll<{ id: string; type: string }>(
|
|
`select id, type
|
|
from approvals
|
|
where company_id = ?
|
|
and state = 'pending'
|
|
and type in (${COFOUNDER_TRANSITION_APPROVAL_TYPES.map(() => "?").join(", ")})`,
|
|
companyId,
|
|
...COFOUNDER_TRANSITION_APPROVAL_TYPES,
|
|
);
|
|
for (const approval of pendingApprovals) {
|
|
resolveApprovalDecision({
|
|
db,
|
|
handleApprovalResolved,
|
|
companyId,
|
|
approvalId: approval.id,
|
|
state: "approved",
|
|
decisionNote: "Auto-approved because the company switched to AI CEO mode.",
|
|
detail: `Mode transition auto-approved ${approval.type}.`,
|
|
actor: "system",
|
|
});
|
|
}
|
|
|
|
const waitStates = db.listThreadWaitStates(companyId);
|
|
for (const waitState of waitStates) {
|
|
if (!REVIEW_WAIT_STATE_TYPES.has(waitState.waitingFor)) continue;
|
|
db.resolveThreadWaitState(waitState.id, companyId);
|
|
}
|
|
}
|
|
|
|
if (disablingDeliverableReviews || leavingCofounderMode) {
|
|
const tasks = db.queryAll<{ id: string }>(
|
|
"select id from tasks where company_id = ? and requires_user_review = 1",
|
|
companyId,
|
|
);
|
|
for (const task of tasks) {
|
|
db.setTaskRequiresUserReview(task.id, false);
|
|
}
|
|
}
|
|
|
|
resumeAgents(db, companyId, impactedAgents);
|
|
}
|