From 35b76f13548a906716811a1e18130b9488961554 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sat, 30 May 2026 01:05:54 +0300 Subject: [PATCH] perf: share bootstrap transcript tail parse across members During launch, the bootstrap-wait loop polls each member and, per member, re-read and re-JSON.parsed the same growing transcript tail (readRecentBootstrapTranscriptOutcome was the top main-thread JS hotspot at ~21% during bootstrap, ~40% with its helpers). The same file was parsed once per member per poll. Memoize the parsed tail by (filePath, mtime, size) in a shared cache so the file is read + parsed once per change and reused across all members. The per-member filter and failure/success scan is byte-for-byte the same logic; only the redundant read + JSON.parse is removed. Cache is bounded (LRU, same cap as the outcome cache) and invalidated on mtime/size change, matching the existing outcome cache semantics. Adds a test asserting the tail is parsed once and shared while per-member outcome detection is unchanged. --- .../services/team/TeamProvisioningService.ts | 123 +++++++++++++----- .../team/TeamProvisioningService.test.ts | 41 ++++++ 2 files changed, 135 insertions(+), 29 deletions(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 2cb284ae..f66bf020 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -704,6 +704,19 @@ interface BootstrapTranscriptOutcomeCandidate { parsedAgentName: string | null; } +interface ParsedBootstrapTranscriptTailLine { + rawTimestamp: string | null; + timestampMs: number; + text: string | null; + parsedAgentName: string | null; +} + +interface ParsedBootstrapTranscriptTailCacheEntry { + mtimeMs: number; + size: number; + lines: ParsedBootstrapTranscriptTailLine[]; +} + import type { ActiveToolCall, AgentActionMode, @@ -3375,6 +3388,13 @@ export class TeamProvisioningService { string, BootstrapTranscriptOutcomeCacheEntry >(); + // Shared parsed-tail cache keyed by filePath (validated by mtime+size) so the + // same growing transcript is read + JSON.parsed ONCE per change instead of once + // per member per poll. The per-member outcome scan below is unchanged. + private readonly parsedBootstrapTranscriptTailCache = new Map< + string, + ParsedBootstrapTranscriptTailCacheEntry + >(); private readonly bootstrapTranscriptOutcomeLookupCache = new Map< string, BootstrapTranscriptOutcomeLookupCacheEntry @@ -30303,44 +30323,24 @@ export class TeamProvisioningService { if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) { return cached.outcome; } - const start = Math.max(0, stat.size - TeamProvisioningService.BOOTSTRAP_FAILURE_TAIL_BYTES); - const buffer = Buffer.alloc(stat.size - start); - if (buffer.length === 0) { - return null; - } - await handle.read(buffer, 0, buffer.length, start); - const lines = buffer.toString('utf8').split('\n'); - if (start > 0) { - lines.shift(); - } + // Parse the transcript tail once per (filePath, mtime, size) and share it + // across members. The per-member filter/scan below is byte-for-byte the same + // logic as before; only the redundant read + JSON.parse is now memoized. + const parsedLines = await this.getParsedBootstrapTranscriptTail(handle, filePath, stat); const shouldCollectBootstrapContext = options.allowAnonymousFailure !== true; const bootstrapContextMembers = new Set(); const candidates: BootstrapTranscriptOutcomeCandidate[] = []; - for (const rawLine of lines) { - const line = rawLine?.trim(); - if (!line) continue; - let parsed: { timestamp?: unknown } | null = null; - try { - parsed = JSON.parse(line) as { timestamp?: unknown }; - } catch { - continue; - } - const timestampMs = - typeof parsed.timestamp === 'string' ? Date.parse(parsed.timestamp) : Number.NaN; + for (const parsedLine of parsedLines) { + const { timestampMs, parsedAgentName, text, rawTimestamp } = parsedLine; if (sinceMs != null && (!Number.isFinite(timestampMs) || timestampMs < sinceMs)) { continue; } - const parsedAgentName = - typeof (parsed as { agentName?: unknown }).agentName === 'string' - ? (parsed as { agentName?: string }).agentName?.trim().toLowerCase() || null - : null; if ( parsedAgentName && !matchesObservedMemberNameForExpected(parsedAgentName, normalizedMemberName) ) { continue; } - const text = extractTranscriptMessageText(parsed); if (!text) { continue; } @@ -30354,9 +30354,7 @@ export class TeamProvisioningService { candidates.push({ text, observedAt: - typeof parsed.timestamp === 'string' && parsed.timestamp.trim().length > 0 - ? parsed.timestamp.trim() - : new Date().toISOString(), + rawTimestamp && rawTimestamp.length > 0 ? rawTimestamp : new Date().toISOString(), parsedAgentName, }); } @@ -30428,6 +30426,73 @@ export class TeamProvisioningService { ].join('\0'); } + private async getParsedBootstrapTranscriptTail( + handle: fs.promises.FileHandle, + filePath: string, + stat: { mtimeMs: number; size: number } + ): Promise { + const cached = this.parsedBootstrapTranscriptTailCache.get(filePath); + if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) { + return cached.lines; + } + const lines: ParsedBootstrapTranscriptTailLine[] = []; + const start = Math.max(0, stat.size - TeamProvisioningService.BOOTSTRAP_FAILURE_TAIL_BYTES); + const length = stat.size - start; + if (length > 0) { + const buffer = Buffer.alloc(length); + await handle.read(buffer, 0, length, start); + const rawLines = buffer.toString('utf8').split('\n'); + if (start > 0) { + rawLines.shift(); + } + for (const rawLine of rawLines) { + const line = rawLine?.trim(); + if (!line) continue; + let parsed: { timestamp?: unknown; agentName?: unknown } | null = null; + try { + parsed = JSON.parse(line) as { timestamp?: unknown; agentName?: unknown }; + } catch { + continue; + } + const rawTimestamp = + typeof parsed.timestamp === 'string' && parsed.timestamp.trim().length > 0 + ? parsed.timestamp.trim() + : null; + const timestampMs = + typeof parsed.timestamp === 'string' ? Date.parse(parsed.timestamp) : Number.NaN; + const parsedAgentName = + typeof parsed.agentName === 'string' + ? parsed.agentName.trim().toLowerCase() || null + : null; + const text = extractTranscriptMessageText(parsed); + lines.push({ rawTimestamp, timestampMs, text, parsedAgentName }); + } + } + this.setParsedBootstrapTranscriptTailCacheEntry(filePath, { + mtimeMs: stat.mtimeMs, + size: stat.size, + lines, + }); + return lines; + } + + private setParsedBootstrapTranscriptTailCacheEntry( + filePath: string, + entry: ParsedBootstrapTranscriptTailCacheEntry + ): void { + if ( + !this.parsedBootstrapTranscriptTailCache.has(filePath) && + this.parsedBootstrapTranscriptTailCache.size >= + TeamProvisioningService.BOOTSTRAP_TRANSCRIPT_OUTCOME_CACHE_MAX_ENTRIES + ) { + const oldestKey = this.parsedBootstrapTranscriptTailCache.keys().next().value; + if (oldestKey) { + this.parsedBootstrapTranscriptTailCache.delete(oldestKey); + } + } + this.parsedBootstrapTranscriptTailCache.set(filePath, entry); + } + private setBootstrapTranscriptOutcomeCacheEntry( cacheKey: string, entry: BootstrapTranscriptOutcomeCacheEntry diff --git a/test/main/services/team/TeamProvisioningService.test.ts b/test/main/services/team/TeamProvisioningService.test.ts index 3ad5868f..a4ef0ce4 100644 --- a/test/main/services/team/TeamProvisioningService.test.ts +++ b/test/main/services/team/TeamProvisioningService.test.ts @@ -21167,6 +21167,47 @@ describe('TeamProvisioningService', () => { }); }); + it('parses a bootstrap transcript tail once and shares it across members', async () => { + const teamName = 'zz-unit-bootstrap-transcript-shared-parse'; + const transcriptPath = path.join(tempProjectsBase, 'bootstrap-shared-parse.jsonl'); + await fsPromises.writeFile( + transcriptPath, + `${JSON.stringify({ + timestamp: '2026-05-24T09:25:42.904Z', + agentName: 'alice', + text: `member briefing for alice on team "${teamName}" (${teamName}). Ready.`, + })}\n`, + 'utf8' + ); + const updatedAt = new Date(Date.now() + 5_000); + await fsPromises.utimes(transcriptPath, updatedAt, updatedAt); + + const svc = new TeamProvisioningService(); + + const aliceOutcome = await privateHarness(svc).readRecentBootstrapTranscriptOutcome( + transcriptPath, + null, + 'alice', + teamName + ); + const bobOutcome = await privateHarness(svc).readRecentBootstrapTranscriptOutcome( + transcriptPath, + null, + 'bob', + teamName + ); + + // Per-member detection is unchanged: alice's briefing is a success, the same + // line is not attributed to bob. + expect(aliceOutcome).toMatchObject({ kind: 'success', source: 'member_briefing' }); + expect(bobOutcome).toBeNull(); + // The transcript tail is parsed once and shared: a single cache entry for the + // file rather than one parse per member. + expect((svc as unknown as Record>).parsedBootstrapTranscriptTailCache.size).toBe( + 1 + ); + }); + it('caches persisted bootstrap transcript outcome lookup between close polling reads', async () => { vi.useFakeTimers(); vi.setSystemTime(new Date('2026-05-03T12:00:00.000Z'));