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.
This commit is contained in:
777genius 2026-05-30 14:34:57 +03:00
parent 2dcd5cb6a8
commit 5c1d2e8d92

View file

@ -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 &&