diff --git a/src/main/services/team/TeamMemberLogsFinder.ts b/src/main/services/team/TeamMemberLogsFinder.ts index 88a36a75..885a4c65 100644 --- a/src/main/services/team/TeamMemberLogsFinder.ts +++ b/src/main/services/team/TeamMemberLogsFinder.ts @@ -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 => { + 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; diff --git a/test/main/services/team/TeamMemberLogsFinder.test.ts b/test/main/services/team/TeamMemberLogsFinder.test.ts index 78f17bc2..fdd87fc6 100644 --- a/test/main/services/team/TeamMemberLogsFinder.test.ts +++ b/test/main/services/team/TeamMemberLogsFinder.test.ts @@ -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]); + }); });