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:
ZacharyZhang-NY committed 2026-04-20 13:37:40 -04:00
1 parent 118e3d8795
commit 827f06f358
8 files changed
+158 -32

No files matched your search

+24 -8
View File
@@ -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);
}
+13 -2
View File
@@ -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 [];
}
}