agent-ecosystem/src/main/services/team/taskLogs/exact/BoardTaskExactLogSummarySelector.ts
2026-04-13 17:00:49 +03:00

226 lines
7 KiB
TypeScript

import { describeBoardTaskActivityLabel } from '@shared/utils/boardTaskActivityLabels';
import { createLogger } from '@shared/utils/logger';
import { createHash } from 'crypto';
import type { BoardTaskActivityRecord } from '../activity/BoardTaskActivityRecord';
import type {
BoardTaskExactLogAnchor,
BoardTaskExactLogBundleCandidate,
BoardTaskExactLogFileVersion,
} from './BoardTaskExactLogTypes';
const logger = createLogger('Service:BoardTaskExactLogSummarySelector');
function noteExactDiagnostic(
event: string,
details: Record<string, string | number | undefined> = {}
): void {
const suffix = Object.entries(details)
.filter(([, value]) => value !== undefined)
.map(([key, value]) => `${key}=${String(value)}`)
.join(' ');
logger.debug(`[board_task_exact_logs.${event}]${suffix ? ` ${suffix}` : ''}`);
}
function compareCandidateTimestamps(
left: BoardTaskActivityRecord,
right: BoardTaskActivityRecord
): number {
const leftTs = Date.parse(left.timestamp);
const rightTs = Date.parse(right.timestamp);
if (Number.isFinite(leftTs) && Number.isFinite(rightTs) && leftTs !== rightTs) {
return leftTs - rightTs;
}
if (left.source.sourceOrder !== right.source.sourceOrder) {
return left.source.sourceOrder - right.source.sourceOrder;
}
return left.id.localeCompare(right.id);
}
function buildMessageGroupKey(record: BoardTaskActivityRecord): string {
return `${record.source.filePath}:${record.source.messageUuid}`;
}
function buildToolAnchor(
filePath: string,
messageUuid: string,
toolUseId: string
): BoardTaskExactLogAnchor {
return {
kind: 'tool',
filePath,
messageUuid,
toolUseId,
};
}
function buildMessageAnchor(filePath: string, messageUuid: string): BoardTaskExactLogAnchor {
return {
kind: 'message',
filePath,
messageUuid,
};
}
function anchorId(anchor: BoardTaskExactLogAnchor): string {
return anchor.kind === 'tool'
? `tool:${anchor.filePath}:${anchor.toolUseId ?? ''}`
: `message:${anchor.filePath}:${anchor.messageUuid}`;
}
function sourceGenerationFor(
anchor: BoardTaskExactLogAnchor,
version: BoardTaskExactLogFileVersion | undefined
): string | null {
if (!version) return null;
const hash = createHash('sha256');
hash.update(anchor.filePath);
hash.update('\0');
hash.update(String(version.size));
hash.update('\0');
hash.update(String(version.mtimeMs));
return hash.digest('hex');
}
function chooseSummaryRecord(
records: BoardTaskActivityRecord[],
anchor: BoardTaskExactLogAnchor
): BoardTaskActivityRecord | null {
if (records.length === 0) {
return null;
}
const anchoredRecords =
anchor.kind === 'tool' && anchor.toolUseId
? records.filter(
(record) =>
record.source.toolUseId === anchor.toolUseId ||
record.action?.toolUseId === anchor.toolUseId
)
: records;
const candidates = anchoredRecords.length > 0 ? anchoredRecords : records;
return (
candidates.find((record) => record.action?.canonicalToolName) ??
candidates.find((record) => record.linkKind !== 'execution' && record.action) ??
candidates[0] ??
null
);
}
export class BoardTaskExactLogSummarySelector {
selectSummaries(args: {
records: BoardTaskActivityRecord[];
fileVersionsByPath: Map<string, BoardTaskExactLogFileVersion>;
}): BoardTaskExactLogBundleCandidate[] {
const byMessage = new Map<string, BoardTaskActivityRecord[]>();
for (const record of args.records) {
const key = buildMessageGroupKey(record);
const bucket = byMessage.get(key) ?? [];
bucket.push(record);
byMessage.set(key, bucket);
}
const groups = new Map<
string,
{ anchor: BoardTaskExactLogAnchor; records: BoardTaskActivityRecord[] }
>();
for (const messageRecords of byMessage.values()) {
const sortedMessageRecords = [...messageRecords].sort(compareCandidateTimestamps);
const toolUseIds = [
...new Set(sortedMessageRecords.map((record) => record.source.toolUseId).filter(Boolean)),
] as string[];
const singleToolUseId = toolUseIds.length === 1 ? toolUseIds[0] : null;
for (const record of sortedMessageRecords) {
let anchor: BoardTaskExactLogAnchor;
if (record.source.toolUseId) {
anchor = buildToolAnchor(
record.source.filePath,
record.source.messageUuid,
record.source.toolUseId
);
} else if (singleToolUseId) {
anchor = buildToolAnchor(
record.source.filePath,
record.source.messageUuid,
singleToolUseId
);
} else {
anchor = buildMessageAnchor(record.source.filePath, record.source.messageUuid);
}
const key = anchorId(anchor);
const existing = groups.get(key);
if (existing) {
existing.records.push(record);
} else {
groups.set(key, { anchor, records: [record] });
}
}
}
const candidates: BoardTaskExactLogBundleCandidate[] = [];
for (const [key, group] of groups) {
const sortedRecords = [...group.records].sort(compareCandidateTimestamps);
const primaryRecord = sortedRecords[0];
if (!primaryRecord) {
continue;
}
const linkKinds = [...new Set(sortedRecords.map((record) => record.linkKind))];
const targetRoles = [...new Set(sortedRecords.map((record) => record.targetRole))];
const fileVersion = args.fileVersionsByPath.get(primaryRecord.source.filePath);
const sourceGeneration = sourceGenerationFor(group.anchor, fileVersion);
const summaryRecord = chooseSummaryRecord(sortedRecords, group.anchor) ?? primaryRecord;
const actionLabel = describeBoardTaskActivityLabel(summaryRecord);
const baseCandidate = {
id: key,
timestamp: primaryRecord.timestamp,
actor: primaryRecord.actor,
source: {
filePath: primaryRecord.source.filePath,
messageUuid: primaryRecord.source.messageUuid,
...(group.anchor.kind === 'tool' && group.anchor.toolUseId
? { toolUseId: group.anchor.toolUseId }
: {}),
sourceOrder: primaryRecord.source.sourceOrder,
},
records: sortedRecords,
anchor: group.anchor,
actionLabel,
...(summaryRecord.action?.category
? { actionCategory: summaryRecord.action.category }
: {}),
...(summaryRecord.action?.canonicalToolName
? { canonicalToolName: summaryRecord.action.canonicalToolName }
: {}),
linkKinds,
targetRoles,
};
if (sourceGeneration) {
candidates.push({
...baseCandidate,
canLoadDetail: true,
sourceGeneration,
});
} else {
noteExactDiagnostic('non_expandable_summary', {
filePath: primaryRecord.source.filePath,
toolUseId: group.anchor.toolUseId,
});
candidates.push({
...baseCandidate,
canLoadDetail: false,
});
}
}
return candidates;
}
}