refactor: optimize TeamMemberLogsFinder for efficient subagent file processing

- Introduced a new method to collect subagent file candidates, streamlining the file scanning process.
- Implemented parallel processing of subagent files with a concurrency limit to enhance performance.
- Improved error handling during file parsing, ensuring that failures do not disrupt the overall scanning process.
- Added unit tests to verify that member logs are returned sorted by start time in descending order.
This commit is contained in:
iliya 2026-03-14 19:44:28 +02:00
parent 471d1871ec
commit 85fa86f1be
2 changed files with 122 additions and 56 deletions

View file

@ -108,31 +108,36 @@ export class TeamMemberLogsFinder {
}
}
for (const sessionId of sessionIds) {
const subagentsDir = path.join(projectDir, sessionId, 'subagents');
// ── Collect and parallel-scan subagent files ──
const candidates = await this.collectSubagentCandidates(projectDir, sessionIds);
const settled: (MemberSubagentLogSummary | null)[] = new Array(candidates.length).fill(null);
let nextIdx = 0;
let files: string[];
try {
files = await fs.readdir(subagentsDir);
} catch {
continue;
const scanWorker = async (): Promise<void> => {
while (nextIdx < candidates.length) {
const idx = nextIdx++;
const c = candidates[idx];
try {
const summary = await this.parseSubagentSummary(
c.filePath,
projectId,
c.sessionId,
c.fileName,
memberName,
knownMembers
);
if (summary) settled[idx] = summary;
} catch (err) {
logger.warn(`Failed to parse subagent summary: ${c.filePath}`, err);
}
}
};
for (const file of files) {
if (!file.startsWith('agent-') || !file.endsWith('.jsonl')) continue;
if (file.startsWith('agent-acompact')) continue;
const filePath = path.join(subagentsDir, file);
const summary = await this.parseSubagentSummary(
filePath,
projectId,
sessionId,
file,
memberName,
knownMembers
);
if (summary) results.push(summary);
}
await Promise.all(
Array.from({ length: Math.min(SCAN_CONCURRENCY, candidates.length) }, () => scanWorker())
);
for (const s of settled) {
if (s) results.push(s);
}
return results.sort(
@ -193,21 +198,7 @@ export class TeamMemberLogsFinder {
const tLead = performance.now();
// ── Collect all subagent file candidates ──
const candidates: { filePath: string; sessionId: string; fileName: string }[] = [];
for (const sessionId of sessionIds) {
const subagentsDir = path.join(projectDir, sessionId, 'subagents');
let dirFiles: string[];
try {
dirFiles = await fs.readdir(subagentsDir);
} catch {
continue;
}
for (const f of dirFiles) {
if (!f.startsWith('agent-') || !f.endsWith('.jsonl') || f.startsWith('agent-acompact'))
continue;
candidates.push({ filePath: path.join(subagentsDir, f), sessionId, fileName: f });
}
}
const candidates = await this.collectSubagentCandidates(projectDir, sessionIds);
// ── Parallel scan with concurrency limit ──
const settled: (MemberLogSummary | null)[] = new Array(candidates.length).fill(null);
@ -234,8 +225,8 @@ export class TeamMemberLogsFinder {
attribution
);
if (summary) settled[idx] = summary;
} catch {
// One file error must not break others
} catch (err) {
logger.warn(`Failed to scan subagent file: ${c.filePath}`, err);
}
}
};
@ -405,21 +396,7 @@ export class TeamMemberLogsFinder {
const tLead = performance.now();
// ── Collect all subagent file candidates ──
const candidates: { filePath: string; sessionId: string; fileName: string }[] = [];
for (const sessionId of sessionIds) {
const subagentsDir = path.join(projectDir, sessionId, 'subagents');
let dirFiles: string[];
try {
dirFiles = await fs.readdir(subagentsDir);
} catch {
continue;
}
for (const f of dirFiles) {
if (!f.startsWith('agent-') || !f.endsWith('.jsonl') || f.startsWith('agent-acompact'))
continue;
candidates.push({ filePath: path.join(subagentsDir, f), sessionId, fileName: f });
}
}
const candidates = await this.collectSubagentCandidates(projectDir, sessionIds);
// ── Parallel scan with concurrency limit ──
let nextIdx = 0;
@ -440,8 +417,8 @@ export class TeamMemberLogsFinder {
attribution.detectedMember,
await this.getSortTime(c.filePath, attribution.firstTimestamp)
);
} catch {
// One file error must not break others
} catch (err) {
logger.warn(`Failed to scan subagent file: ${c.filePath}`, err);
}
}
};
@ -762,6 +739,32 @@ export class TeamMemberLogsFinder {
return { ...discovery, isLeadMember };
}
/**
* Collect all subagent JSONL file candidates across session directories.
* Filters out non-agent files and compact files (agent-acompact*).
*/
private async collectSubagentCandidates(
projectDir: string,
sessionIds: string[]
): Promise<{ filePath: string; sessionId: string; fileName: string }[]> {
const candidates: { filePath: string; sessionId: string; fileName: string }[] = [];
for (const sessionId of sessionIds) {
const subagentsDir = path.join(projectDir, sessionId, 'subagents');
let dirFiles: string[];
try {
dirFiles = await fs.readdir(subagentsDir);
} catch {
continue;
}
for (const f of dirFiles) {
if (!f.startsWith('agent-') || !f.endsWith('.jsonl') || f.startsWith('agent-acompact'))
continue;
candidates.push({ filePath: path.join(subagentsDir, f), sessionId, fileName: f });
}
}
return candidates;
}
private deriveSinceMs(options?: {
intervals?: { startedAt: string; completedAt?: string }[];
since?: string;

View file

@ -1018,4 +1018,67 @@ describe('TeamMemberLogsFinder', () => {
expect(deduped).toHaveLength(1);
expect(deduped[0].memberName.toLowerCase()).toBe('dev');
});
it('findMemberLogs returns results sorted by startTime descending', async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-sort-'));
setClaudeBasePathOverride(tmpDir);
const teamName = 'sort-team';
const projectPath = '/Users/test/sort-proj';
const projectId = '-Users-test-sort-proj';
const sessionId = 'ss1';
await fs.mkdir(path.join(tmpDir, 'teams', teamName), { recursive: true });
await fs.writeFile(
path.join(tmpDir, 'teams', teamName, 'config.json'),
JSON.stringify({
name: teamName,
projectPath,
members: [
{ name: 'team-lead', agentType: 'team-lead' },
{ name: 'dev', agentType: 'general-purpose' },
],
}),
'utf8'
);
const projectRoot = path.join(tmpDir, 'projects', projectId);
await fs.mkdir(path.join(projectRoot, sessionId, 'subagents'), { recursive: true });
// 3 agent files with different startTimes: 00:03, 00:01, 00:02
for (const [id, ts] of [
['agent-s1.jsonl', '2026-01-01T00:00:03.000Z'],
['agent-s2.jsonl', '2026-01-01T00:00:01.000Z'],
['agent-s3.jsonl', '2026-01-01T00:00:02.000Z'],
] as const) {
await fs.writeFile(
path.join(projectRoot, sessionId, 'subagents', id),
[
JSON.stringify({
timestamp: ts,
type: 'user',
message: {
role: 'user',
content: `You are dev, a developer on team "${teamName}" (${teamName}).`,
},
}),
JSON.stringify({
timestamp: ts,
type: 'assistant',
message: { role: 'assistant', content: [{ type: 'text', text: 'OK' }] },
}),
].join('\n') + '\n',
'utf8'
);
}
const finder = new TeamMemberLogsFinder();
const logs = await finder.findMemberLogs(teamName, 'dev');
expect(logs).toHaveLength(3);
// Must be descending: 00:03, 00:02, 00:01
const times = logs.map((l) => new Date(l.startTime).getTime());
expect(times[0]).toBeGreaterThan(times[1]);
expect(times[1]).toBeGreaterThan(times[2]);
});
});