From 827f06f358662af2d177f75c677aca2ffcb1f64e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 20 Apr 2026 13:37:40 -0400 Subject: [PATCH] Migrate 26 high-value catch sites to AppError taxonomy (C6 wave 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Classify scheduler, orchestration, IPC read, and automation catch blocks via logAppError + classifyUnknownError so each failure surfaces a stable `module.subdomain.condition` code plus a kind. No behaviour change — returns, throws, and fall-through are preserved; only log prefixes and the taxonomy metadata are new. Wave 1 coverage (26 sites, all non-database): - orchestration/scheduler-lock-recovery (3) - orchestration/scheduler-housekeeping (5) - orchestration/scheduler-idle-automation (2) - orchestration/scheduler-heartbeat (4) - orchestration/turn-context (2) - ipc-core-read-handlers (6) - automation/automation-handlers (4) Relax the Logger interface so `error` is optional, letting callers whose deps expose only `{info, warn}` (scheduler contexts) pass in directly without wrapping. The helper falls back to warn when error is absent. --- src/main/automation/automation-handlers.ts | 29 +++++++++++-- src/main/errors/handlers.ts | 6 ++- src/main/ipc-core-read-handlers.ts | 42 ++++++++++++++++--- src/main/orchestration/scheduler-heartbeat.ts | 32 ++++++++++---- .../orchestration/scheduler-housekeeping.ts | 33 ++++++++++++--- .../scheduler-idle-automation.ts | 14 ++++++- .../orchestration/scheduler-lock-recovery.ts | 19 +++++++-- src/main/orchestration/turn-context.ts | 15 ++++++- 8 files changed, 158 insertions(+), 32 deletions(-) diff --git a/src/main/automation/automation-handlers.ts b/src/main/automation/automation-handlers.ts index 51abea72..abcd08f3 100644 --- a/src/main/automation/automation-handlers.ts +++ b/src/main/automation/automation-handlers.ts @@ -1,6 +1,7 @@ import type { AppDatabase } from "../database"; import type { NotificationOptions } from "../notification-service"; import type { AutomationEventContext } from "./types"; +import { logAppError } from "../errors/index.js"; export interface HandlerDeps { db: AppDatabase; @@ -45,7 +46,12 @@ export function handleTaskStatusChange( try { task = db.entityCrud.getTask(taskId); } catch (error) { - deps.logger.warn(`[automation] handleTaskStatusChange: task ${taskId} not found:`, error); + logAppError(deps.logger, error, { + kind: "recoverable", + code: "automation.task_status_change.task_not_found", + message: `handleTaskStatusChange: task ${taskId} disappeared before handler could read it`, + context: { taskId, companyId }, + }, "warn"); return; } if (!task) return; @@ -72,7 +78,12 @@ export function handleTaskStatusChange( context.department = agent.department ?? undefined; } } catch (error) { - deps.logger.warn(`[automation] Agent ${task.assigneeAgentId} deleted after task update — continuing without department context:`, error); + logAppError(deps.logger, error, { + kind: "recoverable", + code: "automation.task_status_change.assignee_deleted", + message: `Agent ${task.assigneeAgentId} deleted after task update; continuing without department context`, + context: { taskId, agentId: task.assigneeAgentId, companyId }, + }, "warn"); } dispatchAutomation("task_status_changed", context); @@ -103,7 +114,12 @@ export function handleTaskStatusChange( } } } catch (err) { - deps.logger.warn(`[automation] Parent task ${task.parentId} lookup failed during completion cascade:`, err instanceof Error ? err.message : String(err)); + logAppError(deps.logger, err, { + kind: "recoverable", + code: "automation.task_status_change.parent_lookup_failed", + message: `Parent task ${task.parentId} lookup failed during completion cascade`, + context: { taskId, parentTaskId: task.parentId, companyId }, + }, "warn"); } } } @@ -122,7 +138,12 @@ export function handleTaskCreated( try { task = db.entityCrud.getTask(taskId); } catch (error) { - deps.logger.warn(`[automation] handleTaskCreated: task ${taskId} not found:`, error); + logAppError(deps.logger, error, { + kind: "recoverable", + code: "automation.task_created.task_not_found", + message: `handleTaskCreated: task ${taskId} disappeared before handler could read it`, + context: { taskId, companyId }, + }, "warn"); return; } if (!task) return; diff --git a/src/main/errors/handlers.ts b/src/main/errors/handlers.ts index 1521a16b..d0c4b668 100644 --- a/src/main/errors/handlers.ts +++ b/src/main/errors/handlers.ts @@ -9,7 +9,7 @@ import { classifyUnknownError, type AppError, type AppErrorMeta } from "./taxonomy.js"; export interface Logger { - error: (...args: unknown[]) => void; + error?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void; } @@ -41,8 +41,10 @@ export function logAppError( if (causeDetail) parts.push(`cause=${causeDetail}`); if (level === "warn" && typeof logger.warn === "function") { logger.warn(...parts); - } else { + } else if (typeof logger.error === "function") { logger.error(...parts); + } else if (typeof logger.warn === "function") { + logger.warn(...parts); } return err; } diff --git a/src/main/ipc-core-read-handlers.ts b/src/main/ipc-core-read-handlers.ts index 593ba7d7..e57a44ab 100644 --- a/src/main/ipc-core-read-handlers.ts +++ b/src/main/ipc-core-read-handlers.ts @@ -10,6 +10,7 @@ import { settingsSchema, } from "@shared/contracts"; import { fail, ok, type RegisterCoreHandlersDependencies } from "./ipc-core-common"; +import { logAppError } from "./errors/index.js"; export function registerCoreReadHandlers({ registerHandle, @@ -130,7 +131,12 @@ export function registerCoreReadHandlers({ const parsed = connectorInputSchema.shape.id.parse(connectorId); return ok(await listConnectorModels(parsed)); } catch (err) { - logger.warn(`[ipc] listConnectorModels failed for connector ${String(connectorId)}:`, err instanceof Error ? err.message : String(err)); + logAppError(logger, err, { + kind: "upstream", + code: "ipc.list_connector_models.failed", + message: `listConnectorModels failed for connector ${String(connectorId)}`, + context: { connectorId }, + }, "warn"); return ok([]); } }); @@ -140,7 +146,11 @@ export function registerCoreReadHandlers({ const parsed = connectorModelPoolQuerySchema.parse(payload); return ok(db.settings.getDisabledModels(parsed.companyId, parsed.connectorId)); } catch (err) { - logger.warn(`[ipc] getConnectorDisabledModels failed:`, err instanceof Error ? err.message : String(err)); + logAppError(logger, err, { + kind: "user_input", + code: "ipc.get_connector_disabled_models.invalid_payload", + message: "getConnectorDisabledModels rejected payload (schema parse failed)", + }, "warn"); return ok([]); } }); @@ -170,7 +180,12 @@ export function registerCoreReadHandlers({ if (task.companyId !== parsed.companyId) return fail("TASK_NOT_FOUND", "Task not found in current company."); return ok(task); } catch (err) { - logger.warn(`[ipc] getTask failed for id ${parsed.id}:`, err instanceof Error ? err.message : String(err)); + logAppError(logger, err, { + kind: "recoverable", + code: "ipc.get_task.lookup_failed", + message: `getTask failed for id ${parsed.id}`, + context: { taskId: parsed.id, companyId: parsed.companyId }, + }, "warn"); return fail("TASK_NOT_FOUND", "Task not found."); } }); @@ -184,7 +199,12 @@ export function registerCoreReadHandlers({ if (agent.companyId !== parsed.companyId) return fail("AGENT_NOT_FOUND", "Agent not found in current company."); return ok(agent); } catch (err) { - logger.warn(`[ipc] getAgent failed for id ${parsed.id}:`, err instanceof Error ? err.message : String(err)); + logAppError(logger, err, { + kind: "recoverable", + code: "ipc.get_agent.lookup_failed", + message: `getAgent failed for id ${parsed.id}`, + context: { agentId: parsed.id, companyId: parsed.companyId }, + }, "warn"); return fail("AGENT_NOT_FOUND", "Agent not found."); } }); @@ -198,7 +218,12 @@ export function registerCoreReadHandlers({ if (run.companyId !== parsed.companyId) return fail("RUN_NOT_FOUND", "Run not found in current company."); return ok(run); } catch (err) { - logger.warn(`[ipc] getRun failed for id ${parsed.id}:`, err instanceof Error ? err.message : String(err)); + logAppError(logger, err, { + kind: "recoverable", + code: "ipc.get_run.lookup_failed", + message: `getRun failed for id ${parsed.id}`, + context: { runId: parsed.id, companyId: parsed.companyId }, + }, "warn"); return fail("RUN_NOT_FOUND", "Run not found."); } }); @@ -212,7 +237,12 @@ export function registerCoreReadHandlers({ if (approval.companyId !== parsed.companyId) return fail("APPROVAL_NOT_FOUND", "Approval not found in current company."); return ok(approval); } catch (err) { - logger.warn(`[ipc] getApprovalRecord failed for id ${parsed.id}:`, err instanceof Error ? err.message : String(err)); + logAppError(logger, err, { + kind: "recoverable", + code: "ipc.get_approval.lookup_failed", + message: `getApprovalRecord failed for id ${parsed.id}`, + context: { approvalId: parsed.id, companyId: parsed.companyId }, + }, "warn"); return fail("APPROVAL_NOT_FOUND", "Approval not found."); } }); diff --git a/src/main/orchestration/scheduler-heartbeat.ts b/src/main/orchestration/scheduler-heartbeat.ts index 22cf6dc3..cc571004 100644 --- a/src/main/orchestration/scheduler-heartbeat.ts +++ b/src/main/orchestration/scheduler-heartbeat.ts @@ -1,3 +1,4 @@ +import { logAppError } from "../errors/index.js"; import type { HeartbeatStartResult } from "./heartbeat"; import type { OrchestrationDependencies } from "./types"; @@ -29,9 +30,12 @@ export function createHeartbeatScheduler( heartbeatSkip: { reason, at: new Date().toISOString() }, }); } catch (error) { - logger.warn( - `[heartbeat] Failed to persist skip reason for agent ${agentId}: ${error instanceof Error ? error.message : String(error)}`, - ); + logAppError(logger, error, { + kind: "recoverable", + code: "scheduler.heartbeat.persist_skip_failed", + message: `Failed to persist heartbeat skip reason for agent ${agentId}`, + context: { agentId, companyId }, + }, "warn"); } } @@ -43,9 +47,12 @@ export function createHeartbeatScheduler( delete next.heartbeatSkip; db.agents.setRuntimeState(agentId, companyId, next); } catch (error) { - logger.warn( - `[heartbeat] Failed to clear skip reason for agent ${agentId}: ${error instanceof Error ? error.message : String(error)}`, - ); + logAppError(logger, error, { + kind: "recoverable", + code: "scheduler.heartbeat.clear_skip_failed", + message: `Failed to clear heartbeat skip reason for agent ${agentId}`, + context: { agentId, companyId }, + }, "warn"); } } @@ -123,11 +130,20 @@ export function createHeartbeatScheduler( scheduleLastFiredMap.set(scheduleKey, now); } } catch (error) { - logger.error(`[heartbeat] Error processing company ${company.id}: ${error instanceof Error ? error.message : String(error)}`); + logAppError(logger, error, { + kind: "recoverable", + code: "scheduler.heartbeat.company_tick_failed", + message: `Heartbeat tick failed for company ${company.id}`, + context: { companyId: company.id }, + }); } } } catch (error) { - logger.error(`[heartbeat] Scheduler error: ${error instanceof Error ? error.message : String(error)}`); + logAppError(logger, error, { + kind: "recoverable", + code: "scheduler.heartbeat.tick_failed", + message: "Heartbeat scheduler tick raised; next interval will retry", + }); } }, 15_000); } diff --git a/src/main/orchestration/scheduler-housekeeping.ts b/src/main/orchestration/scheduler-housekeeping.ts index 38afe206..d9f847c1 100644 --- a/src/main/orchestration/scheduler-housekeeping.ts +++ b/src/main/orchestration/scheduler-housekeeping.ts @@ -3,6 +3,7 @@ import { sweepStaleReviews } from "../task-quality-gates"; import type { SweepStaleReviewsDeps } from "../task-quality-gates"; import { resolveApprovalDecision } from "../approval-policy"; import { dispatchAgentMessage } from "../messaging/dispatch"; +import { logAppError } from "../errors/index.js"; export function createHousekeepingScheduler( deps: OrchestrationDependencies, @@ -73,10 +74,20 @@ export function createHousekeepingScheduler( publishDomainChanged(); } } catch (error) { - logger.error(`[housekeeping] Stale review sweep failed: ${error instanceof Error ? error.message : String(error)}`); + logAppError(logger, error, { + kind: "recoverable", + code: "scheduler.housekeeping.stale_review_sweep_failed", + message: `Stale review sweep failed for company ${company.id}`, + context: { companyId: company.id }, + }); } } catch (error) { - logger.error(`[housekeeping] Error processing company ${company.id}: ${error instanceof Error ? error.message : String(error)}`); + logAppError(logger, error, { + kind: "recoverable", + code: "scheduler.housekeeping.company_tick_failed", + message: `Housekeeping tick failed for company ${company.id}`, + context: { companyId: company.id }, + }); } } @@ -86,7 +97,11 @@ export function createHousekeepingScheduler( logger.info(`[housekeeping] Purged ${staleWakeups} expired wakeup request(s)`); } } catch (error) { - logger.error(`[housekeeping] Wakeup purge failed: ${error instanceof Error ? error.message : String(error)}`); + logAppError(logger, error, { + kind: "recoverable", + code: "scheduler.housekeeping.wakeup_purge_failed", + message: "Expired wakeup request purge failed; next tick will retry", + }); } try { @@ -102,10 +117,18 @@ export function createHousekeepingScheduler( logger.info(`[housekeeping] Purged ${purgedProjections} orphan inbox projection ledger row(s)`); } } catch (error) { - logger.error(`[housekeeping] Inbox projection purge failed: ${error instanceof Error ? error.message : String(error)}`); + logAppError(logger, error, { + kind: "recoverable", + code: "scheduler.housekeeping.inbox_projection_purge_failed", + message: "Orphan inbox projection purge failed; next tick will retry", + }); } } catch (error) { - logger.error(`[housekeeping] Scheduler error: ${error instanceof Error ? error.message : String(error)}`); + logAppError(logger, error, { + kind: "recoverable", + code: "scheduler.housekeeping.tick_failed", + message: "Housekeeping scheduler tick raised; next interval will retry", + }); } }, 300_000); } diff --git a/src/main/orchestration/scheduler-idle-automation.ts b/src/main/orchestration/scheduler-idle-automation.ts index 35edd6b6..4cb4db83 100644 --- a/src/main/orchestration/scheduler-idle-automation.ts +++ b/src/main/orchestration/scheduler-idle-automation.ts @@ -1,3 +1,4 @@ +import { logAppError } from "../errors/index.js"; import type { OrchestrationDependencies } from "./types"; export function createIdleAutomationScheduler(deps: OrchestrationDependencies) { @@ -32,11 +33,20 @@ export function createIdleAutomationScheduler(deps: OrchestrationDependencies) { } } } catch (error) { - logger.error(`[idle-automation] Error processing company ${company.id}: ${error instanceof Error ? error.message : String(error)}`); + logAppError(logger, error, { + kind: "recoverable", + code: "scheduler.idle_automation.company_tick_failed", + message: `Idle automation tick failed for company ${company.id}`, + context: { companyId: company.id }, + }); } } } catch (error) { - logger.error(`[idle-automation] Scheduler error: ${error instanceof Error ? error.message : String(error)}`); + logAppError(logger, error, { + kind: "recoverable", + code: "scheduler.idle_automation.tick_failed", + message: "Idle automation scheduler tick raised; next interval will retry", + }); } }, 60_000); } diff --git a/src/main/orchestration/scheduler-lock-recovery.ts b/src/main/orchestration/scheduler-lock-recovery.ts index 527be271..7c7b2296 100644 --- a/src/main/orchestration/scheduler-lock-recovery.ts +++ b/src/main/orchestration/scheduler-lock-recovery.ts @@ -1,3 +1,4 @@ +import { logAppError } from "../errors/index.js"; import type { OrchestrationDependencies } from "./types"; export function createLockRecoveryScheduler(deps: OrchestrationDependencies) { @@ -27,7 +28,11 @@ export function createLockRecoveryScheduler(deps: OrchestrationDependencies) { logger.info(`[lock-recovery] Recovered stale lock on task ${task.taskId}: "${task.title}"`); } } catch (err) { - logger.error(`[lock-recovery] Stale lock recovery failed: ${err instanceof Error ? err.message : String(err)}`); + logAppError(logger, err, { + kind: "recoverable", + code: "scheduler.lock_recovery.stale_lock_sweep_failed", + message: "Stale task lock recovery sweep failed; next tick will retry", + }); } try { @@ -37,10 +42,18 @@ export function createLockRecoveryScheduler(deps: OrchestrationDependencies) { publishDomainChanged(); } } catch (error) { - logger.error(`[lock-recovery] Stale workspace lock cleanup failed: ${error instanceof Error ? error.message : String(error)}`); + logAppError(logger, error, { + kind: "recoverable", + code: "scheduler.lock_recovery.workspace_lock_sweep_failed", + message: "Stale workspace lock cleanup failed; next tick will retry", + }); } } catch (error) { - logger.error(`[lock-recovery] Scheduler error: ${error instanceof Error ? error.message : String(error)}`); + logAppError(logger, error, { + kind: "recoverable", + code: "scheduler.lock_recovery.tick_failed", + message: "Lock recovery scheduler tick raised; next interval will retry", + }); } }, 60_000); } diff --git a/src/main/orchestration/turn-context.ts b/src/main/orchestration/turn-context.ts index 6e4583a2..b0fc73bd 100644 --- a/src/main/orchestration/turn-context.ts +++ b/src/main/orchestration/turn-context.ts @@ -14,6 +14,7 @@ import type { WorkspaceLockMode, EnqueueRunPayload } from "./types"; import { normalizeCommentsForPrompt } from "../messaging/normalize"; import { getConnectorModelPolicy, validateConnectorModelSelection } from "../connector-model-policy"; import { resolveInitialUserTurn } from "./initial-user-turn"; +import { logAppError } from "../errors/index.js"; export interface RunDispatchParams { runId: string; @@ -111,7 +112,12 @@ function resolveSessionHandoff( try { return deps.getSessionHandoff(agentId); } catch (err) { - deps.logger.warn(`[run] Failed to load session handoff for agent ${agentId}: ${err instanceof Error ? err.message : String(err)}`); + logAppError(deps.logger, err, { + kind: "recoverable", + code: "run.session_handoff.load_failed", + message: `Failed to load session handoff for agent ${agentId}; continuing without handoff`, + context: { agentId }, + }, "warn"); return null; } } @@ -126,7 +132,12 @@ function resolveSkills( try { return deps.listRuntimeSkillEntries(companyId, agentId, connectorId); } catch (err) { - deps.logger.warn(`[run] Failed to fetch managed skills for agent ${agentName}: ${err instanceof Error ? err.message : String(err)}`); + logAppError(deps.logger, err, { + kind: "recoverable", + code: "run.skills.fetch_failed", + message: `Failed to fetch managed skills for agent ${agentName}; proceeding with empty skill set`, + context: { agentId, agentName, connectorId, companyId }, + }, "warn"); return []; } }