From 37150e08e60f049c96bcc264ffa314cc50698a87 Mon Sep 17 00:00:00 2001 From: saurabhhhcodes Date: Mon, 20 Jul 2026 16:39:22 +0530 Subject: [PATCH] fix: close summaryInFlight TOCTOU gap in summarizeTranscriptIfNeeded (#739) --- src/background.ts | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/background.ts b/src/background.ts index 3698d7b5..bcd562f8 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1059,8 +1059,13 @@ function mergeUniqueStrings(existing: string[], incoming: unknown, maxSize = 500 async function summarizeTranscriptIfNeeded() { if (!state.isActive || state.transcript.length === 0) return; - // Bail out immediately if another summarization is already running. + // Claim the in-flight slot synchronously *before* any `await`. The previous + // guard checked `summaryInFlight` here but only set it after several awaits, + // leaving a TOCTOU window where two concurrent triggers (e.g. a timer tick + // and a manual action) could both pass the guard and run in parallel, doubling + // API cost and racing the state writes (#739). Setting it now closes that gap. if (summaryInFlight) return; + summaryInFlight = true; const settings = await getSettings(); const requestedInterval = Number(settings.summarizationInterval); @@ -1071,10 +1076,16 @@ async function summarizeTranscriptIfNeeded() { if (intervalSeconds > 900) intervalSeconds = 900; const lastSum = state.lastSummarizedAt || 0; const elapsed = Math.floor((Date.now() - lastSum) / 1000); - if (lastSum > 0 && elapsed < intervalSeconds) return; + if (lastSum > 0 && elapsed < intervalSeconds) { + summaryInFlight = false; + return; + } const apiKey = await getApiKey(); - if (!apiKey) return; + if (!apiKey) { + summaryInFlight = false; + return; + } const transcriptWindow = state.transcript .slice(-TRANSCRIPT_WINDOW_SIZE) @@ -1084,10 +1095,10 @@ async function summarizeTranscriptIfNeeded() { return `[${chunkId}] [${timestampLabel}] ${sanitizePromptText(e.speaker)}: ${sanitizePromptText(e.text)}`; }) .join("\n"); - if (!transcriptWindow.trim()) return; - - // Claim the in-flight slot *after* all cheap pre-checks pass. - summaryInFlight = true; + if (!transcriptWindow.trim()) { + summaryInFlight = false; + return; + } try { const topicDetectionEnabled = isFeatureEnabled(settings, "topicDetection");