From 7637e069fb3915feae2f27fbb33421dfacce8109 Mon Sep 17 00:00:00 2001 From: stainlu Date: Tue, 5 May 2026 11:09:03 +0800 Subject: [PATCH] perf: cache comment router label lookups --- CHANGELOG.md | 2 ++ src/repair/comment-router-core.ts | 23 +++++++++++++++++++++++ src/repair/comment-router.ts | 12 +++++++----- test/repair/comment-router-core.test.ts | 18 ++++++++++++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37f9b8d8a7..3324292a17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ checkpoint, and status-only commits are intentionally omitted. - Fixed live worker scheduling to filter GitHub Actions runs through supported `workflowName` JSON fields instead of silently falling back to zero active workers when `gh run list --workflow` is unavailable. +- Cached comment-router open-label issue lookups per run so repair-loop comment + discovery and command synthesis do not repeat identical GitHub searches. - Retried Codex edit workers after TPM/rate-limit exits and collapsed JSONL failure transcripts into concise repair status reasons. - Added deterministic merged closing-PR provenance to issue close reports and public close comments when GitHub exposes a high-confidence closing PR. diff --git a/src/repair/comment-router-core.ts b/src/repair/comment-router-core.ts index 06e88ce357..2fac1ca548 100644 --- a/src/repair/comment-router-core.ts +++ b/src/repair/comment-router-core.ts @@ -104,6 +104,29 @@ export function issueImplementationJobPath(repo: string, issueNumber: JsonValue) return `jobs/${owner}/inbox/${issueImplementationClusterId(repo, issueNumber)}.md`; } +export function createCachedLabelNumberLookup(fetchNumbers: (label: string) => JsonValue[]) { + const cache = new Map(); + return (label: string) => { + const key = String(label ?? ""); + const cached = cache.get(key); + if (cached) return [...cached]; + const numbers = uniquePositiveIntegers(fetchNumbers(key)); + cache.set(key, numbers); + return [...numbers]; + }; +} + +function uniquePositiveIntegers(values: JsonValue): number[] { + if (!Array.isArray(values)) return []; + return [ + ...new Set( + values + .map((value: JsonValue) => Number(value)) + .filter((number: number) => Number.isInteger(number) && number > 0), + ), + ]; +} + export function renderAutomergeJob({ repo, issueNumber, diff --git a/src/repair/comment-router.ts b/src/repair/comment-router.ts index 63b61e9852..8d964f11cf 100644 --- a/src/repair/comment-router.ts +++ b/src/repair/comment-router.ts @@ -33,6 +33,7 @@ import { automergeTransientWaitConfig, buildAutomergeMergeArgs, commandHasAction, + createCachedLabelNumberLookup, hasCommandResponseMarker, commandStatusMarker, commandStatusMarkerPrefix, @@ -132,6 +133,11 @@ const collaboratorPermissionCache = new Map(); const activeRepairRunsByPrefix = new Map(); const liveTargetCache = new Map(); const issueCommentsCache = new Map(); +const openIssueNumbersByLabel = createCachedLabelNumberLookup((label) => + ghPaged( + `repos/${targetRepo}/issues?state=open&labels=${encodeURIComponent(label)}&per_page=100`, + ).map((issue: JsonValue) => issue.number), +); const comments = measure("list_candidate_comments", () => listCandidateComments()); const rawCommands: LooseRecord[] = []; @@ -2392,11 +2398,7 @@ function isGitHubNotFoundError(error: unknown) { } function listOpenIssueNumbersWithLabel(label: string) { - return ghPaged( - `repos/${targetRepo}/issues?state=open&labels=${encodeURIComponent(label)}&per_page=100`, - ) - .map((issue: JsonValue) => Number(issue.number)) - .filter((number) => Number.isInteger(number) && number > 0); + return openIssueNumbersByLabel(label); } function isClawSweeperReviewMarkerComment(comment: JsonValue) { diff --git a/test/repair/comment-router-core.test.ts b/test/repair/comment-router-core.test.ts index 960aa9ef15..325c1218e2 100644 --- a/test/repair/comment-router-core.test.ts +++ b/test/repair/comment-router-core.test.ts @@ -21,6 +21,7 @@ import { commandResponseMarker, commandResponseMarkerPrefix, commandStatusMarkerPrefix, + createCachedLabelNumberLookup, existingCommandStatusBlocksReplay, existingModeStatusBlocksReplay, hasCommandResponseMarker, @@ -187,6 +188,23 @@ test("parseCommand recognizes maintainer slash commands", () => { }); }); +test("cached label number lookup fetches each label once and returns stable copies", () => { + const calls: string[] = []; + const lookup = createCachedLabelNumberLookup((label) => { + calls.push(label); + return label === "clawsweeper:autofix" ? ["10", 10, 0, "bad", 11, 10] : [20]; + }); + + const first = lookup("clawsweeper:autofix"); + first.push(99); + + assert.deepEqual(first, [10, 11, 99]); + assert.deepEqual(lookup("clawsweeper:autofix"), [10, 11]); + assert.deepEqual(lookup("clawsweeper:automerge"), [20]); + assert.deepEqual(lookup("clawsweeper:autofix"), [10, 11]); + assert.deepEqual(calls, ["clawsweeper:autofix", "clawsweeper:automerge"]); +}); + test("autoclose reason parser preserves maintainer wording", () => { assert.equal( autocloseReasonFromCommand("autoclose We don't want this feature"),