Migrate 26 high-value catch sites to AppError taxonomy (C6 wave 1)
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.
This commit is contained in:
1 parent
118e3d8795
commit
827f06f358
8 files changed
+158
-32
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user