agent-ecosystem/src/main/services/team/teamLogSourceWatchScope.ts

179 lines
5.4 KiB
TypeScript

import * as path from 'path';
import type { PersistedTeamLaunchMemberState, PersistedTeamLaunchSnapshot } from '@shared/types';
export const BOARD_TASK_LOG_FRESHNESS_DIRNAME = '.board-task-log-freshness';
export const TEAM_TASK_LOG_FRESHNESS_DIRNAME = 'task-log-freshness';
export const BOARD_TASK_CHANGE_FRESHNESS_DIRNAME = '.board-task-change-freshness';
export const BOARD_TASK_CHANGES_DIRNAME = '.board-task-changes';
export const BOARD_TASK_LOG_FRESHNESS_FILE_SUFFIX = '.json';
export const MAX_LOG_SOURCE_WATCH_SESSION_IDS = 24;
export const MAX_PENDING_UNKNOWN_ROOT_SESSIONS = 16;
export const PENDING_UNKNOWN_ROOT_SESSION_TTL_MS = 15_000;
export const MAX_PENDING_UNKNOWN_ROOT_REFRESH_ATTEMPTS = 4;
const SAFE_SESSION_ID_RE = /^[A-Za-z0-9_-]{1,200}$/;
const AGENT_TRANSCRIPT_RE = /^agent-(?!acompact).*\.jsonl$/;
export type WatcherEventName = 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir';
export type LogSourceWatcherAction =
| { kind: 'ignore' }
| { kind: 'task-freshness' }
| { kind: 'scoped-recompute' }
| { kind: 'context-refresh'; candidateSessionId?: string };
export interface TeamLogWatchSessionInput {
configLeadSessionId?: unknown;
launchLeadSessionId?: unknown;
sessionHistory?: unknown[];
launchRuntimeSessionIds?: unknown[];
bootstrapRuntimeSessionIds?: unknown[];
}
export interface ClassifyLogSourceWatcherEventInput {
projectDir: string;
changedPath: string;
eventName: WatcherEventName;
scopedSessionIds: ReadonlySet<string>;
pendingUnknownSessionIds: ReadonlySet<string>;
}
export function normalizeLogSourceSessionId(value: unknown): string | null {
if (typeof value !== 'string') {
return null;
}
const trimmed = value.trim();
if (!trimmed || !SAFE_SESSION_ID_RE.test(trimmed)) {
return null;
}
return trimmed;
}
function pushSessionId(ids: string[], seen: Set<string>, value: unknown, limit: number): void {
if (ids.length >= limit) {
return;
}
const normalized = normalizeLogSourceSessionId(value);
if (!normalized || seen.has(normalized)) {
return;
}
seen.add(normalized);
ids.push(normalized);
}
function shouldIncludeRuntimeMember(member: PersistedTeamLaunchMemberState): boolean {
return Boolean(
member.runtimeSessionId &&
!member.hardFailure &&
member.launchState !== 'skipped_for_launch' &&
member.launchState !== 'failed_to_start'
);
}
export function extractRuntimeSessionIds(
snapshot: PersistedTeamLaunchSnapshot | null | undefined
): string[] {
if (!snapshot?.members) {
return [];
}
const ids: string[] = [];
const seen = new Set<string>();
for (const member of Object.values(snapshot.members)) {
if (!shouldIncludeRuntimeMember(member)) {
continue;
}
pushSessionId(ids, seen, member.runtimeSessionId, MAX_LOG_SOURCE_WATCH_SESSION_IDS);
}
return ids;
}
export function buildTeamLogWatchSessionIds(
input: TeamLogWatchSessionInput,
limit = MAX_LOG_SOURCE_WATCH_SESSION_IDS
): string[] {
const ids: string[] = [];
const seen = new Set<string>();
pushSessionId(ids, seen, input.configLeadSessionId, limit);
pushSessionId(ids, seen, input.launchLeadSessionId, limit);
for (const sessionId of input.launchRuntimeSessionIds ?? []) {
pushSessionId(ids, seen, sessionId, limit);
}
for (const sessionId of input.bootstrapRuntimeSessionIds ?? []) {
pushSessionId(ids, seen, sessionId, limit);
}
const history = Array.isArray(input.sessionHistory) ? input.sessionHistory : [];
for (let index = history.length - 1; index >= 0; index -= 1) {
pushSessionId(ids, seen, history[index], limit);
}
return ids;
}
export function isAgentTranscriptFileName(fileName: string): boolean {
return AGENT_TRANSCRIPT_RE.test(fileName);
}
export function getRelativeLogSourceParts(projectDir: string, targetPath: string): string[] | null {
const relativePath = path.relative(projectDir, targetPath);
if (!relativePath || relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
return null;
}
return relativePath.split(/[/\\]+/).filter(Boolean);
}
export function classifyLogSourceWatcherEvent(
input: ClassifyLogSourceWatcherEventInput
): LogSourceWatcherAction {
const parts = getRelativeLogSourceParts(input.projectDir, input.changedPath);
if (!parts) {
return { kind: 'ignore' };
}
const first = parts[0];
if (first === BOARD_TASK_LOG_FRESHNESS_DIRNAME || first === BOARD_TASK_CHANGE_FRESHNESS_DIRNAME) {
return { kind: 'task-freshness' };
}
if (
first === BOARD_TASK_CHANGES_DIRNAME ||
parts.includes('tool-results') ||
parts.includes('memory')
) {
return { kind: 'ignore' };
}
if (parts.length === 1 && first.endsWith('.jsonl')) {
const sessionId = normalizeLogSourceSessionId(first.slice(0, -'.jsonl'.length));
if (!sessionId) {
return { kind: 'ignore' };
}
if (input.scopedSessionIds.has(sessionId)) {
return { kind: 'scoped-recompute' };
}
if (input.eventName === 'add' || input.pendingUnknownSessionIds.has(sessionId)) {
return { kind: 'context-refresh', candidateSessionId: sessionId };
}
return { kind: 'ignore' };
}
if (input.scopedSessionIds.has(first)) {
if (parts.length === 1) {
return { kind: 'scoped-recompute' };
}
if (parts[1] === 'subagents') {
if (parts.length === 2) {
return { kind: 'context-refresh' };
}
if (parts.length === 3 && isAgentTranscriptFileName(parts[2])) {
return { kind: 'scoped-recompute' };
}
}
}
return { kind: 'ignore' };
}