import { getTaskDisplayId, taskMatchesRef } from '@shared/utils/taskIdentity'; import { TeamTaskReader } from '../../TeamTaskReader'; import { BoardTaskActivityRecordSource } from '../activity/BoardTaskActivityRecordSource'; import { TeamTranscriptSourceLocator } from '../discovery/TeamTranscriptSourceLocator'; import { BoardTaskExactLogStrictParser } from '../exact/BoardTaskExactLogStrictParser'; import { BoardTaskLogStreamService } from '../stream/BoardTaskLogStreamService'; import { HistoricalBoardMcpRawProbe } from '../stream/HistoricalBoardMcpRawProbe'; import { TaskLogTranscriptCandidateSelector } from '../stream/TaskLogTranscriptCandidateSelector'; import type { BoardTaskActivityRecord } from '../activity/BoardTaskActivityRecord'; import type { ParsedMessage } from '@main/types'; import type { TaskWorkInterval, TeamTask } from '@shared/types'; const BOARD_MCP_TOOL_PREFIXES = ['mcp__agent-teams__', 'mcp__agent_teams__'] as const; const MAX_EXAMPLES = 10; export interface BoardTaskLogDiagnosticExample { timestamp: string; filePath: string; messageUuid: string; toolUseId?: string; toolName: string; isSidechain: boolean; agentId?: string; } export interface BoardTaskLogDiagnosticsReport { teamName: string; requestedTaskRef: string; task: { taskId: string; displayId: string; subject: string; status: TeamTask['status']; owner?: string; workIntervals: TaskWorkInterval[]; }; transcript: { fileCount: number; files: string[]; parsedFileCount: number; candidateSelection?: { mode: 'activity_records' | 'historical_raw_probe' | 'none'; candidateFileCount: number; rawProbeScannedFileCount?: number; rawProbeHitCount?: number; }; }; explicitRecords: { total: number; execution: number; lifecycle: number; boardAction: number; participants: string[]; toolNames: string[]; }; intervalToolResults: { total: number; boardMcp: number; worker: { total: number; explicitLinked: number; missingExplicit: number; examples: BoardTaskLogDiagnosticExample[]; }; }; stream: { participants: string[]; defaultFilter: string; segmentCount: number; visibleToolNames: string[]; emptyPayloadExamples: BoardTaskLogDiagnosticExample[]; }; diagnosis: string[]; } function normalizeRequestedTaskRef(taskRef: string): string { return taskRef.trim().replace(/^#/, ''); } function isBoardMcpToolName(toolName: string | undefined): boolean { if (!toolName) return false; const normalized = toolName.trim().toLowerCase(); return BOARD_MCP_TOOL_PREFIXES.some((prefix) => normalized.startsWith(prefix)); } function isWithinWorkIntervals(timestamp: Date, intervals: TaskWorkInterval[]): boolean { if (!Number.isFinite(timestamp.getTime())) { return false; } if (intervals.length === 0) { return true; } const time = timestamp.getTime(); return intervals.some((interval) => { const startedAt = Date.parse(interval.startedAt); if (!Number.isFinite(startedAt) || time < startedAt) { return false; } if (interval.completedAt === undefined) { return true; } const completedAt = Date.parse(interval.completedAt); const endMs = Number.isFinite(completedAt) ? Math.max(completedAt, startedAt) : startedAt; return time <= endMs; }); } function pushUnique(values: string[], value: string | undefined): void { if (!value) return; if (!values.includes(value)) { values.push(value); } } function pushExample( examples: BoardTaskLogDiagnosticExample[], example: BoardTaskLogDiagnosticExample ): void { if (examples.length < MAX_EXAMPLES) { examples.push(example); } } function buildParticipantLabel(record: BoardTaskActivityRecord): string { if (record.actor.memberName) { return record.actor.memberName; } if (!record.actor.isSidechain || record.actor.role === 'lead') { return 'lead session'; } if (record.actor.agentId) { return `member ${record.actor.agentId.slice(0, 8)}`; } return `member session ${record.actor.sessionId.slice(0, 8)}`; } function extractVisibleToolNames( stream: Awaited> ): string[] { const toolNames: string[] = []; for (const segment of stream.segments) { for (const chunk of segment.chunks) { for (const message of chunk.rawMessages) { for (const toolCall of message.toolCalls) { pushUnique(toolNames, toolCall.name); } } } } return toolNames; } function buildStreamToolNameMap( stream: Awaited> ): Map { const toolNameByUseId = new Map(); for (const segment of stream.segments) { for (const chunk of segment.chunks) { for (const message of chunk.rawMessages) { for (const toolCall of message.toolCalls) { toolNameByUseId.set(toolCall.id, toolCall.name); } } } } return toolNameByUseId; } function isEmptyToolPayload(value: unknown): boolean { if (value == null) return true; if (typeof value === 'string') { return value.trim().length === 0; } if (Array.isArray(value)) { return value.length === 0; } return false; } function asObjectRecord(value: unknown): Record | null { return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : null; } function collectEmptyPayloadExamples( stream: Awaited> ): BoardTaskLogDiagnosticExample[] { const examples: BoardTaskLogDiagnosticExample[] = []; const toolNameByUseId = buildStreamToolNameMap(stream); for (const segment of stream.segments) { for (const chunk of segment.chunks) { for (const message of chunk.rawMessages) { for (const toolResult of message.toolResults) { if (!isEmptyToolPayload(toolResult.content)) { continue; } pushExample(examples, { timestamp: message.timestamp.toISOString(), filePath: 'stream', messageUuid: message.uuid, toolUseId: toolResult.toolUseId, toolName: toolNameByUseId.get(toolResult.toolUseId) ?? 'unknown tool', isSidechain: message.isSidechain, ...(message.agentId ? { agentId: message.agentId } : {}), }); } const toolUseResult = asObjectRecord(message.toolUseResult); if (!toolUseResult) { continue; } const toolUseId = typeof toolUseResult.toolUseId === 'string' ? toolUseResult.toolUseId : message.sourceToolUseID; const contentIsEmpty = (!('content' in toolUseResult) || isEmptyToolPayload(toolUseResult.content)) && (!('message' in toolUseResult) || isEmptyToolPayload(toolUseResult.message)); if (!contentIsEmpty) { continue; } pushExample(examples, { timestamp: message.timestamp.toISOString(), filePath: 'stream', messageUuid: message.uuid, ...(toolUseId ? { toolUseId } : {}), toolName: toolUseId ? (toolNameByUseId.get(toolUseId) ?? 'unknown tool') : 'unknown tool', isSidechain: message.isSidechain, ...(message.agentId ? { agentId: message.agentId } : {}), }); } } } return examples; } function buildToolNameMap(parsedMessagesByFile: Map): Map { const toolNameByUseId = new Map(); for (const messages of parsedMessagesByFile.values()) { for (const message of messages) { for (const toolCall of message.toolCalls) { toolNameByUseId.set(toolCall.id, toolCall.name); } } } return toolNameByUseId; } export class BoardTaskLogDiagnosticsService { constructor( private readonly taskReader: TeamTaskReader = new TeamTaskReader(), private readonly transcriptSourceLocator: TeamTranscriptSourceLocator = new TeamTranscriptSourceLocator(), private readonly recordSource: BoardTaskActivityRecordSource = new BoardTaskActivityRecordSource(), private readonly strictParser: BoardTaskExactLogStrictParser = new BoardTaskExactLogStrictParser(), private readonly streamService: BoardTaskLogStreamService = new BoardTaskLogStreamService(), private readonly transcriptCandidateSelector: TaskLogTranscriptCandidateSelector = new TaskLogTranscriptCandidateSelector(), private readonly historicalRawProbe: HistoricalBoardMcpRawProbe = new HistoricalBoardMcpRawProbe() ) {} private async getTranscriptContext(teamName: string): Promise<{ projectDir?: string; transcriptFiles: string[]; }> { const locator = this.transcriptSourceLocator as { getContext?: ( teamName: string ) => Promise<{ projectDir: string; transcriptFiles: string[] } | null>; listTranscriptFiles?: (teamName: string) => Promise; }; const context = await locator.getContext?.(teamName); if (context) { return { projectDir: context.projectDir, transcriptFiles: context.transcriptFiles, }; } return { transcriptFiles: (await locator.listTranscriptFiles?.(teamName)) ?? [], }; } private async parseDiagnosticCandidateFiles(args: { task: TeamTask; records: BoardTaskActivityRecord[]; projectDir?: string; transcriptFiles: string[]; }): Promise<{ parsedMessagesByFile: Map; candidateSelection: NonNullable< BoardTaskLogDiagnosticsReport['transcript']['candidateSelection'] >; }> { if (args.transcriptFiles.length === 0) { return { parsedMessagesByFile: new Map(), candidateSelection: { mode: 'none', candidateFileCount: 0, }, }; } if (args.records.length > 0) { const selection = this.transcriptCandidateSelector.selectInferredNativeTranscriptFiles({ records: args.records, transcriptFiles: args.transcriptFiles, projectDir: args.projectDir, }); return { parsedMessagesByFile: selection.filePaths.length > 0 ? await this.strictParser.parseFiles(selection.filePaths) : new Map(), candidateSelection: { mode: 'activity_records', candidateFileCount: selection.filePaths.length, }, }; } const rawProbe = await this.historicalRawProbe.findCandidateFiles({ task: args.task, transcriptFiles: args.transcriptFiles, }); return { parsedMessagesByFile: rawProbe.filePaths.length > 0 ? await this.strictParser.parseFiles(rawProbe.filePaths) : new Map(), candidateSelection: { mode: 'historical_raw_probe', candidateFileCount: rawProbe.filePaths.length, rawProbeScannedFileCount: rawProbe.scannedFileCount, rawProbeHitCount: rawProbe.hitCount, }, }; } async diagnose(teamName: string, taskRef: string): Promise { const normalizedRef = normalizeRequestedTaskRef(taskRef); const [activeTasks, deletedTasks, transcriptContext] = await Promise.all([ this.taskReader.getTasks(teamName), this.taskReader.getDeletedTasks(teamName), this.getTranscriptContext(teamName), ]); const tasks = [...activeTasks, ...deletedTasks]; const task = tasks.find((candidate) => taskMatchesRef(candidate, normalizedRef)); if (!task) { throw new Error(`Task "${taskRef}" was not found in team "${teamName}"`); } const records = await this.recordSource.getTaskRecords(teamName, task.id); const { parsedMessagesByFile, candidateSelection } = await this.parseDiagnosticCandidateFiles({ task, records, projectDir: transcriptContext.projectDir, transcriptFiles: transcriptContext.transcriptFiles, }); const stream = await this.streamService.getTaskLogStream(teamName, task.id); const toolNameByUseId = buildToolNameMap(parsedMessagesByFile); const explicitExecutionKeys = new Set( records .filter((record) => record.linkKind === 'execution') .map((record) => `${record.source.messageUuid}:${record.source.toolUseId ?? ''}`) ); const workIntervals = Array.isArray(task.workIntervals) ? task.workIntervals : []; const explicitParticipants: string[] = []; const explicitToolNames: string[] = []; for (const record of records) { pushUnique(explicitParticipants, buildParticipantLabel(record)); pushUnique(explicitToolNames, record.action?.canonicalToolName); } let intervalToolResultTotal = 0; let boardMcpToolResultTotal = 0; let workerToolResultTotal = 0; let explicitLinkedWorkerResultTotal = 0; let missingExplicitWorkerResultTotal = 0; const missingExplicitWorkerExamples: BoardTaskLogDiagnosticExample[] = []; for (const [filePath, messages] of parsedMessagesByFile.entries()) { for (const message of messages) { if (message.type !== 'user' || message.toolResults.length === 0) { continue; } if (!isWithinWorkIntervals(message.timestamp, workIntervals)) { continue; } for (const toolResult of message.toolResults) { intervalToolResultTotal += 1; const toolName = toolNameByUseId.get(toolResult.toolUseId) ?? 'unknown tool'; if (isBoardMcpToolName(toolName)) { boardMcpToolResultTotal += 1; continue; } workerToolResultTotal += 1; const explicitKey = `${message.uuid}:${toolResult.toolUseId}`; if (explicitExecutionKeys.has(explicitKey)) { explicitLinkedWorkerResultTotal += 1; continue; } missingExplicitWorkerResultTotal += 1; pushExample(missingExplicitWorkerExamples, { timestamp: message.timestamp.toISOString(), filePath, messageUuid: message.uuid, toolUseId: toolResult.toolUseId, toolName, isSidechain: message.isSidechain, ...(message.agentId ? { agentId: message.agentId } : {}), }); } } } const diagnosis: string[] = []; if (transcriptContext.transcriptFiles.length === 0) { diagnosis.push('No transcript files were found for this team.'); } if (records.length === 0) { diagnosis.push('No explicit task-linked activity records were found for this task.'); } if (missingExplicitWorkerResultTotal > 0) { diagnosis.push( `Only board MCP actions are explicit for part of this task history. Found ${missingExplicitWorkerResultTotal} worker tool result(s) inside task work intervals without boardTaskLinks, so Task Log Stream cannot safely include them.` ); } if ( missingExplicitWorkerResultTotal > 0 && extractVisibleToolNames(stream).every((toolName) => isBoardMcpToolName(toolName)) ) { diagnosis.push( 'Current stream visibility matches the data gap: the visible tools are MCP board actions, while worker tools exist in transcript but are unlinked.' ); } const emptyPayloadExamples = collectEmptyPayloadExamples(stream); if (emptyPayloadExamples.length > 0) { diagnosis.push( `Found ${emptyPayloadExamples.length} tool result payload(s) with empty rendered content in the current stream. This explains empty success/output blocks.` ); } if (diagnosis.length === 0) { diagnosis.push('No obvious task-log data gap was detected by diagnostics.'); } return { teamName, requestedTaskRef: taskRef, task: { taskId: task.id, displayId: getTaskDisplayId(task), subject: task.subject, status: task.status, ...(task.owner ? { owner: task.owner } : {}), workIntervals, }, transcript: { fileCount: transcriptContext.transcriptFiles.length, files: transcriptContext.transcriptFiles, parsedFileCount: parsedMessagesByFile.size, candidateSelection, }, explicitRecords: { total: records.length, execution: records.filter((record) => record.linkKind === 'execution').length, lifecycle: records.filter((record) => record.linkKind === 'lifecycle').length, boardAction: records.filter((record) => record.linkKind === 'board_action').length, participants: explicitParticipants, toolNames: explicitToolNames, }, intervalToolResults: { total: intervalToolResultTotal, boardMcp: boardMcpToolResultTotal, worker: { total: workerToolResultTotal, explicitLinked: explicitLinkedWorkerResultTotal, missingExplicit: missingExplicitWorkerResultTotal, examples: missingExplicitWorkerExamples, }, }, stream: { participants: stream.participants.map((participant) => participant.label), defaultFilter: stream.defaultFilter, segmentCount: stream.segments.length, visibleToolNames: extractVisibleToolNames(stream), emptyPayloadExamples, }, diagnosis, }; } }