multica/src/shared/errors.ts
Jiayuan f74ac430a9
feat(shared): add error handling infrastructure (#27)
- Add JakartaError base class with error codes and retry semantics
- Add typed error classes for network, session, compaction, process, channel, and gateway errors
- Add withRetry utility with exponential backoff, jitter, and abort support
- Add CancellationToken with hierarchical parent-child cancellation support

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 13:50:49 +08:00

353 lines
9.3 KiB
TypeScript

/**
* Error type hierarchy for Multica agent system.
* Provides typed errors with retry semantics and serialization support.
*/
/**
* Base error class for all Multica errors.
* Provides common functionality like error codes, retry semantics, and JSON serialization.
*/
export abstract class MulticaError extends Error {
/** Unique error code for programmatic handling */
abstract readonly code: string;
/** Whether this error type is generally retryable */
abstract readonly retryable: boolean;
/** Timestamp when the error occurred */
readonly timestamp = Date.now();
/** Additional context about the error */
readonly details: Record<string, unknown> | undefined;
constructor(message: string, details?: Record<string, unknown>) {
super(message);
this.name = this.constructor.name;
this.details = details;
// Maintains proper stack trace for where error was thrown (V8 only)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
/**
* Serialize error for logging or transmission
*/
toJSON(): Record<string, unknown> {
return {
name: this.name,
code: this.code,
message: this.message,
retryable: this.retryable,
timestamp: this.timestamp,
details: this.details,
stack: this.stack,
};
}
}
// =============================================================================
// Network / API Errors
// =============================================================================
/**
* General network connectivity error (DNS, TCP, TLS failures)
*/
export class NetworkError extends MulticaError {
readonly code = "NETWORK_ERROR" as const;
readonly retryable = true;
}
/**
* Streaming connection was unexpectedly disconnected
*/
export class StreamDisconnectedError extends MulticaError {
readonly code = "STREAM_DISCONNECTED" as const;
readonly retryable = true;
}
/**
* API rate limit exceeded
*/
export class RateLimitError extends MulticaError {
readonly code = "RATE_LIMIT" as const;
readonly retryable = true;
/** Seconds to wait before retrying (from Retry-After header) */
readonly retryAfter: number | undefined;
constructor(message: string, retryAfter?: number, details?: Record<string, unknown>) {
super(message, { ...details, retryAfter });
this.retryAfter = retryAfter;
}
}
/**
* API returned an error response
*/
export class APIError extends MulticaError {
readonly code = "API_ERROR" as const;
/** HTTP status code if available */
readonly statusCode: number | undefined;
/** Whether this specific API error is retryable */
readonly retryable: boolean;
constructor(
message: string,
statusCode?: number,
retryable = false,
details?: Record<string, unknown>,
) {
super(message, { ...details, statusCode });
this.statusCode = statusCode;
this.retryable = retryable;
}
}
// =============================================================================
// Session / Storage Errors
// =============================================================================
/**
* Failed to read or write session data
*/
export class SessionStorageError extends MulticaError {
readonly code = "SESSION_STORAGE_ERROR" as const;
readonly retryable = false;
/** The session ID that failed */
readonly sessionId: string | undefined;
constructor(message: string, sessionId?: string, details?: Record<string, unknown>) {
super(message, { ...details, sessionId });
this.sessionId = sessionId;
}
}
/**
* Session data is corrupted or partially unreadable
*/
export class SessionCorruptedError extends MulticaError {
readonly code = "SESSION_CORRUPTED" as const;
readonly retryable = false;
/** Number of entries that were successfully recovered */
readonly recoveredEntries: number | undefined;
/** Number of entries that were corrupted */
readonly corruptedEntries: number | undefined;
constructor(
message: string,
recoveredEntries?: number,
corruptedEntries?: number,
details?: Record<string, unknown>,
) {
super(message, { ...details, recoveredEntries, corruptedEntries });
this.recoveredEntries = recoveredEntries;
this.corruptedEntries = corruptedEntries;
}
}
// =============================================================================
// Compaction Errors
// =============================================================================
/**
* General compaction operation failure
*/
export class CompactionError extends MulticaError {
readonly code = "COMPACTION_ERROR" as const;
readonly retryable = true;
/** Compaction mode that failed */
readonly mode: string | undefined;
constructor(message: string, mode?: string, details?: Record<string, unknown>) {
super(message, { ...details, mode });
this.mode = mode;
}
}
/**
* Failed to generate conversation summary for compaction
*/
export class SummaryGenerationError extends MulticaError {
readonly code = "SUMMARY_GENERATION_ERROR" as const;
readonly retryable = true;
/** Number of messages that were being summarized */
readonly messagesCount: number | undefined;
constructor(message: string, messagesCount?: number, details?: Record<string, unknown>) {
super(message, { ...details, messagesCount });
this.messagesCount = messagesCount;
}
}
// =============================================================================
// Process Execution Errors
// =============================================================================
/**
* Process execution exceeded timeout
*/
export class ProcessTimeoutError extends MulticaError {
readonly code = "PROCESS_TIMEOUT" as const;
readonly retryable = false;
/** Timeout in milliseconds */
readonly timeoutMs: number | undefined;
/** Process ID if available */
readonly pid: number | undefined;
constructor(
message: string,
timeoutMs?: number,
pid?: number,
details?: Record<string, unknown>,
) {
super(message, { ...details, timeoutMs, pid });
this.timeoutMs = timeoutMs;
this.pid = pid;
}
}
/**
* Process was killed by signal
*/
export class ProcessKilledError extends MulticaError {
readonly code = "PROCESS_KILLED" as const;
readonly retryable = false;
/** Signal that killed the process */
readonly signal: string | undefined;
/** Process ID if available */
readonly pid: number | undefined;
constructor(
message: string,
signal?: string,
pid?: number,
details?: Record<string, unknown>,
) {
super(message, { ...details, signal, pid });
this.signal = signal;
this.pid = pid;
}
}
// =============================================================================
// Channel Errors
// =============================================================================
/**
* Attempted to use a closed channel
*/
export class ChannelClosedError extends MulticaError {
readonly code = "CHANNEL_CLOSED" as const;
readonly retryable = false;
}
// =============================================================================
// Hub / Gateway Errors
// =============================================================================
/**
* Failed to connect to gateway
*/
export class GatewayConnectionError extends MulticaError {
readonly code = "GATEWAY_CONNECTION_ERROR" as const;
readonly retryable = true;
/** Gateway URL that failed */
readonly url: string | undefined;
constructor(message: string, url?: string, details?: Record<string, unknown>) {
super(message, { ...details, url });
this.url = url;
}
}
/**
* Failed to deliver message through gateway
*/
export class MessageDeliveryError extends MulticaError {
readonly code = "MESSAGE_DELIVERY_ERROR" as const;
readonly retryable = true;
/** ID of the message that failed */
readonly messageId: string | undefined;
/** Target device ID */
readonly targetDeviceId: string | undefined;
constructor(
message: string,
messageId?: string,
targetDeviceId?: string,
details?: Record<string, unknown>,
) {
super(message, { ...details, messageId, targetDeviceId });
this.messageId = messageId;
this.targetDeviceId = targetDeviceId;
}
}
// =============================================================================
// Type Guards
// =============================================================================
/**
* Check if an error is a MulticaError
*/
export function isMulticaError(error: unknown): error is MulticaError {
return error instanceof MulticaError;
}
/**
* Check if an error is retryable
*/
export function isRetryableError(error: unknown): boolean {
if (error instanceof MulticaError) {
return error.retryable;
}
// Check for common transient error patterns
if (error instanceof Error) {
const message = error.message.toLowerCase();
return (
message.includes("network") ||
message.includes("timeout") ||
message.includes("econnreset") ||
message.includes("econnrefused") ||
message.includes("socket hang up") ||
message.includes("fetch failed")
);
}
return false;
}
/**
* All Jakarta error codes for type-safe handling
*/
export type MulticaErrorCode =
| "NETWORK_ERROR"
| "STREAM_DISCONNECTED"
| "RATE_LIMIT"
| "API_ERROR"
| "SESSION_STORAGE_ERROR"
| "SESSION_CORRUPTED"
| "COMPACTION_ERROR"
| "SUMMARY_GENERATION_ERROR"
| "PROCESS_TIMEOUT"
| "PROCESS_KILLED"
| "CHANNEL_CLOSED"
| "GATEWAY_CONNECTION_ERROR"
| "MESSAGE_DELIVERY_ERROR";