agent-ecosystem/src/main/services/error/ErrorMessageBuilder.ts
iliya 2131ad32d0 feat: add review status, all-tasks-completed and cross-team message notifications
- Detect review state transitions (kanbanColumn → review) in status change notifications
- Add "All tasks completed" notification when every task in a team reaches completed/deleted
- Add "Cross-team message" notification with separate toggle (source: cross_team in inbox)
- All three enabled by default with UI toggles in settings
2026-03-16 14:32:03 +02:00

202 lines
6 KiB
TypeScript

/**
* ErrorMessageBuilder service - Builds error messages and DetectedError objects.
*
* Provides utilities for:
* - Extracting error messages from tool results
* - Finding tool names by ID
* - Creating DetectedError objects
* - Truncating messages for display
*/
import { type ContentBlock, type ParsedMessage } from '@main/types';
import { randomUUID } from 'crypto';
import { type ExtractedToolResult } from '../analysis/ToolResultExtractor';
import type { TriggerColor } from '@shared/constants/triggerColors';
// =============================================================================
// Types
// =============================================================================
/**
* Represents a detected error from a Claude Code session.
*/
export interface DetectedError {
/** UUID for unique identification */
id: string;
/** Unix timestamp when error was detected */
timestamp: number;
/** Session ID where error occurred */
sessionId: string;
/** Project ID (encoded directory name) */
projectId: string;
/** Path to the JSONL file */
filePath: string;
/** Source of the error - tool name or 'assistant' */
source: string;
/** Error message content */
message: string;
/** Line number in JSONL for deep linking */
lineNumber?: number;
/** Tool use ID for precise deep linking to the specific tool item */
toolUseId?: string;
/** Subagent ID when error originates from a subagent session */
subagentId?: string;
/** Trigger color key for notification dot and highlight */
triggerColor?: TriggerColor;
/** ID of the trigger that produced this notification */
triggerId?: string;
/** Human-readable name of the trigger that produced this notification */
triggerName?: string;
/** Notification domain: 'error' (default/undefined) or 'team' */
category?: 'error' | 'team';
/** For team notifications: specific event sub-type */
teamEventType?:
| 'rate_limit'
| 'lead_inbox'
| 'user_inbox'
| 'task_clarification'
| 'task_status_change'
| 'task_comment'
| 'task_created'
| 'all_tasks_completed'
| 'cross_team_message'
| 'schedule_completed'
| 'schedule_failed';
/** Explicit key for storage deduplication. Two notifications with the same dedupeKey won't be stored twice. */
dedupeKey?: string;
/** Additional context about the error */
context: {
/** Human-readable project name */
projectName: string;
/** Current working directory when error occurred */
cwd?: string;
};
}
/**
* Parameters for creating a DetectedError.
*/
export interface CreateDetectedErrorParams {
sessionId: string;
projectId: string;
filePath: string;
projectName: string;
lineNumber: number;
source: string;
message: string;
timestamp: Date;
cwd?: string;
toolUseId?: string;
subagentId?: string;
triggerColor?: TriggerColor;
triggerId?: string;
triggerName?: string;
}
// =============================================================================
// Error Message Extraction
// =============================================================================
/**
* Extracts error message from a tool result.
*/
export function extractErrorMessage(result: ExtractedToolResult): string {
if (typeof result.content === 'string') {
return result.content.trim() || 'Unknown error';
}
if (Array.isArray(result.content)) {
const texts: string[] = [];
for (const item of result.content) {
if (item && typeof item === 'object' && 'type' in item) {
const block = item as ContentBlock;
if (block.type === 'text' && 'text' in block) {
texts.push(block.text);
}
}
}
return texts.join('\n').trim() || 'Unknown error';
}
return 'Unknown error';
}
// =============================================================================
// Tool Name Lookup
// =============================================================================
/**
* Finds tool name from message's tool calls by tool use ID.
*/
function findToolName(message: ParsedMessage, toolUseId: string): string | null {
if (message.toolCalls) {
const toolCall = message.toolCalls.find((tc) => tc.id === toolUseId);
if (toolCall) {
return toolCall.name;
}
}
return null;
}
/**
* Finds tool name by searching tool_use_id in the message context.
*/
export function findToolNameByToolUseId(message: ParsedMessage, toolUseId: string): string | null {
// First check toolCalls
const fromToolCalls = findToolName(message, toolUseId);
if (fromToolCalls) return fromToolCalls;
// Check sourceToolUseID if this message is a tool result
if (message.sourceToolUseID === toolUseId && message.toolUseResult) {
if (typeof message.toolUseResult.toolName === 'string') {
return message.toolUseResult.toolName;
}
}
return null;
}
// =============================================================================
// Message Truncation
// =============================================================================
/**
* Truncates error message to a reasonable length for display.
*/
function truncateMessage(message: string, maxLength: number = 500): string {
if (message.length <= maxLength) {
return message;
}
return message.slice(0, maxLength) + '...';
}
// =============================================================================
// DetectedError Creation
// =============================================================================
/**
* Creates a DetectedError object with all required fields.
*/
export function createDetectedError(params: CreateDetectedErrorParams): DetectedError {
return {
id: randomUUID(),
timestamp: params.timestamp.getTime(),
sessionId: params.sessionId,
projectId: params.projectId,
filePath: params.filePath,
source: params.source,
message: truncateMessage(params.message),
lineNumber: params.lineNumber,
toolUseId: params.toolUseId,
subagentId: params.subagentId,
triggerColor: params.triggerColor,
triggerId: params.triggerId,
triggerName: params.triggerName,
context: {
projectName: params.projectName,
cwd: params.cwd,
},
};
}