perf: cache comment router issue comments#50
Conversation
ds4psb-ai
left a comment
There was a problem hiding this comment.
Independent code review
Posted on behalf of @ds4psb-ai (read-only contributor). Maintainer judgment owns merge.
Summary verdict
LGTM after addressing one P1 — the cache itself is well-shaped and the test is good, but the refactor inadvertently downgrades prehydrateCommandLookups from concurrent async fetches to serialized sync subprocess spawns.
Findings
P0 / blocker
- (none)
P1 / should fix before merge
src/repair/comment-router.ts:131-136+src/repair/comment-router.ts:307-312— the newcachedIssueCommentsis wired through syncghPaged(line 132 in this PR). Pre-PR,prehydrateCommandLookupscalledfetchIssueCommentsAsync(number)which usedghPagedAsync(ghPagedWithRetryAsync) and so achieved real parallelism undermapLimit(issueNumbers, lookupConcurrency, ...). Post-PR, the mapper bodycachedIssueComments(number)contains noawait, so all NmapLimitworkers serialize their gh subprocess spawns through the event loop. For a repair run with many open issue comments, this is a measurable wall-clock regression that runs counter to the perf goal of the PR.- Suggested fix: keep
createCachedIssueCommentsLookupfor the sync hot-path consumers (hasExistingResponse,hasExistingModeStatusResponse,issueCommentsFor), but introduce a parallel async-aware variant — e.g.createCachedIssueCommentsLookupAsyncthat takesfetchComments: (n) => Promise<T[]>, populates the same shared map, and is whatprehydrateCommandLookupscalls. The sync getter then opportunistically returns the warm copy for downstream readers. Alternatively, accept a dual fetcher{ sync, async }in one factory.
- Suggested fix: keep
P2 / nice to have / followup
src/repair/comment-router-core.ts:117-131— the cache silently swallowsfetchCommentsreturning a non-array (safeComments = Array.isArray(comments) ? comments : []) AND caches the empty result. That hides upstream parser regressions: a future change that breaks the gh JSON shape would be cached as "this issue has no comments" for the rest of the run. Either (a) don't cache non-array fetches, or (b) log the shape mismatch once. Mirrors a patterncreateCachedLabelNumberLookupdoesn't have (it relies onuniquePositiveIntegersto pre-validate).src/repair/comment-router-core.ts:121-122— whenNumber.isInteger(key) && key > 0is false you return[]without callingfetchComments. Good — but worth a one-line code comment so a maintainer doesn't "fix" the early return later thinking it skips a legitimate fetch.CHANGELOG.md:40-41— the entry is meaningful (per-run cache scope is observable behavior for anyone reading the bot's runtime budget), so the +2 lines are warranted.
Test coverage
test/repair/comment-router-core.test.ts:209-225 covers:
- cold fetch + cached miss → fetch invoked once for key 12 (
calls === [12, 13]) - copied-array semantics (mutating the returned array doesn't pollute the next read)
- string-vs-number key normalization (
lookup("12")returns the cached entry) - invalid key short-circuit (
lookup(0))
Solid for the core. Gaps worth one followup test (P2):
- Concurrent calls for the same key — important now that prehydrate is the primary cache populator. Two
lookup(12)issued before the first resolves should NOT result in two underlying fetches. The current sync implementation makes this safe by accident; an async variant (per the P1 above) would need an in-flightMap<number, Promise<T[]>>to remain race-free. - Empty-array fetch result is cached and returns a fresh
[]on each subsequent call (sanity for the spread of an empty array; trivial but locks behavior).
Risks I considered and dismissed
- Per-run lifetime correctness:
cachedIssueCommentsis module-scoped, same as the oldissueCommentsCache. The router process is per-run by AGENTS.md operating model, so the cache scope is unchanged. - Stale comments mid-run: pre-PR behavior was already "first fetch wins" via
Map.get(...) ?? ghPaged(...); post-PR is the same. - Loss of
hasExistingResponse/hasExistingModeStatusResponsecorrectness: both still see the same comments as before becauseprehydrateCommandLookupspopulates the map (sequentially now, but populated nonetheless) beforeclassifyCommandruns. - Removed import
ghPagedWithRetryAsync as ghPagedAsync: searched the file — it was only used byfetchIssueCommentsAsync, which the PR also removes. Clean removal.
Nice consolidation pattern overall — once the prehydrate parallelism is restored this is a strict win.
|
Addressed the P1 prehydrate regression. What changed:
Local proof:
Note: |
steipete
left a comment
There was a problem hiding this comment.
Reviewed comment-router comment cache fix; CI green and targeted local router tests passed.
Summary
Validation