From 5c1d2e8d92962c01ba0fbbf30e67ae920a029406 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sat, 30 May 2026 14:34:57 +0300 Subject: [PATCH] perf: memoize bootstrap failure-reason per parsed line across members readRecentBootstrapTranscriptOutcome ran extractBootstrapFailureReason (regex + optional JSON.parse + ~40 substring scans per call) for every candidate line, once per team member: an anonymous transcript line (no agentName) passes the per-member filter for ALL members, so an N-member team re-extracted the same line's failure reason N times per scan -- a top main-thread cost in the warm launch profile (~5.4% with isBootstrapInstructionPrompt). Memoize the result on the shared parsed-tail line (parsedBootstrapTranscriptTailCache already reuses the same line objects across members and re-scans while the file is unchanged). The compute stays LAZY inside the newest-first candidate loop, so each line is extracted at most once across all members AND lines past the first match are never extracted -- preserving the early-break that an eager precompute would lose. The memo is keyed implicitly by the line's cache entry (filePath + mtime + size); a file change re-parses into fresh line objects, so it cannot drift from the line text. Pure-function memoization: byte-identical input (candidate.text === parsedLine.text) -> identical output, so failure/success/null outcomes and reasons are unchanged. Bootstrap-transcript outcome tests pass (306), no behavior change. --- .../services/team/TeamProvisioningService.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 5f3dbc8c..95f91fc5 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -706,6 +706,10 @@ interface BootstrapTranscriptOutcomeCandidate { normalizedText: string; observedAt: string; parsedAgentName: string | null; + // The shared parsed-tail line this candidate was built from. Used to memoize the + // pure extractBootstrapFailureReason() result on the line itself so an N-member + // team extracts each line's failure reason at most once instead of once per member. + parsedLine: ParsedBootstrapTranscriptTailLine; } interface ParsedBootstrapTranscriptTailLine { @@ -714,6 +718,11 @@ interface ParsedBootstrapTranscriptTailLine { text: string | null; normalizedText: string | null; parsedAgentName: string | null; + // Memoized extractBootstrapFailureReason(text): undefined = not computed yet, + // null = computed/no failure reason, string = the failure reason. Lives as long as + // this line's parse-cache entry (filePath + mtime + size); a file change re-parses + // into fresh line objects, so the memo cannot drift from the line's text. + bootstrapFailureReason?: string | null; } interface ParsedBootstrapTranscriptTailCacheEntry { @@ -30439,6 +30448,7 @@ export class TeamProvisioningService { observedAt: rawTimestamp && rawTimestamp.length > 0 ? rawTimestamp : new Date().toISOString(), parsedAgentName, + parsedLine, }); } const hasUnambiguousMatchingBootstrapContext = @@ -30449,7 +30459,14 @@ export class TeamProvisioningService { for (let index = candidates.length - 1; index >= 0; index -= 1) { const candidate = candidates[index]; if (!candidate) continue; - const reason = extractBootstrapFailureReason(candidate.text); + // Lazy + memoized on the shared parsed line: computed at most once per line + // across all members and re-scans, and only for lines this newest-first loop + // actually reaches (lines past the first match are never extracted). + const cachedLine = candidate.parsedLine; + if (cachedLine.bootstrapFailureReason === undefined) { + cachedLine.bootstrapFailureReason = extractBootstrapFailureReason(candidate.text); + } + const reason = cachedLine.bootstrapFailureReason; if (reason) { if ( !candidate.parsedAgentName &&