fix(fetcher): paginate GraphQL connections + MAX_FETCHED_* caps (closes #66) - #95
Conversation
#66) Every PR/issue connection (comments, reviews, nested review comments, changed files) now selects pageInfo and walks the cursors via octokit.graphql.paginate, instead of hard-capping at first:100 and silently dropping the remainder. A new REVIEW_COMMENTS_QUERY follows the per-review nested overflow that paginate cannot reach in a single call. MAX_FETCHED_COMMENTS / _REVIEWS / _REVIEW_COMMENTS / _FILES (default 500) cap the merged result and surface a structured log.warn({ connection, fetched, cap }) plus a FetchedData.truncated.* flag whenever a cap fires. buildPrompt prepends a "WARNING: pre-fetched context is incomplete" banner when any flag is set so the agent knows to reach for the GitHub CLI for the missing context. filterByTriggerTime continues to run AFTER the paginate merge, preserving TOCTOU semantics. Documents the new env vars in configuration.md and adds a "Data fetching safety caps" section to observability.md per the CLAUDE.md doc-sync rule. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
bot workflow review pipeline execution failed — see server logs for details. |
|
bot workflow 🔍 Code review complete — 9 files, +708/-213. Review — PR #95 (
|
|
bot workflow 🔎 Resolve passed — no failing checks, no open review comments. Resolve report — PR #95 (
|
| Check | Status | Duration |
|---|---|---|
| Analyze (actions) | pass | 55s |
| Analyze (javascript-typescript) | pass | 1m6s |
| CodeQL | pass | 2s |
| Gitleaks (secrets-scan) | pass | 6s |
| Gitleaks (push-triggered) | pass | 8s |
| Label PR based on title | pass | 11s |
| Lint & Test | pass | 1m38s |
| build | pass | 26s |
Merge state: CLEAN, MERGEABLE. Zero fix attempts consumed against the FIX_ATTEMPTS_CAP=3 budget.
Review comments
No formal review threads were ever opened on this PR (reviewThreads GraphQL query returned 0 nodes). The blocker findings came from a top-level review comment posted by the bot earlier; nothing to mark RESOLVED via the threads API. The findings are addressed in code as documented above.
Commits pushed
| SHA | Subject |
|---|---|
fbcbd2f |
fix(fetcher): satisfy paginate-graphql plugin contract for PR/issue queries |
Outstanding
None. PR is ready for human review / merge.
— @chrisleekr-bot
cost: $7.5779 · turns: 81 · duration: 1199s
…ueries Address blockers from prior review on PR #95: - Rename all cursor variables to $cursor: the @octokit/plugin-paginate-graphql plugin hard-codes the cursor parameter name (iterator.js:24), so $afterX variants are never advanced and pagination silently runs once. - Split monolithic PR_QUERY into PR_FIRST_QUERY (top-level + files), PR_COMMENTS_QUERY, and PR_REVIEWS_QUERY. The plugin only walks the first pageInfo it discovers (DFS first-match in object-helpers.js:13-32), so a single query with multiple connections leaves all but one un-paginated. Top-level PR fields ride along on PR_FIRST_QUERY since mergeResponses preserves page-1 scalars. - applyCap now slice(-cap) instead of slice(0, cap): GitHub returns connections in ascending order, so trimming the head was dropping the newest items and keeping ancient comments — the opposite of intent. - Parallelize the per-review overflow fetches with Promise.all + flatMap rather than serializing them inside a for-loop. - Update src/config.ts JSDoc on MAX_FETCHED_* to accurately describe cap semantics ("walks every page, then trims to the most recent N"). - Add a contract test that wires the real paginate-graphql plugin against a stubbed graphql() with per-page sequences, proving the cursor parameter actually advances and the queries are plugin-compatible. - Strengthen the cap-truncation test to assert which items survived (newest) and which were dropped (oldest), pinning slice direction. Co-authored-by: chrisleekr-bot[bot] <chrisleekr-bot[bot]@users.noreply.github.com>
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR implements pagination for the GraphQL fetcher to eliminate silent truncation of PR/issue context beyond 100 items. The changes add configurable safety caps via ChangesGraphQL Pagination + Safety Caps
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 👉 Get your free trial and get 200 agent minutes per Slack user (a $50 value). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Fixes silent truncation of PR/issue GraphQL context by paginating connections (comments/reviews/review-comments/files), adding safety caps (MAX_FETCHED_*) plus a FetchedData.truncated signal, and surfacing that truncation in the generated agent prompt.
Changes:
- Paginate GraphQL connections (and nested review inline comments via a follow-up query) instead of single-page
first: 100fetches. - Add configurable per-connection caps (
MAX_FETCHED_*, default 500) with structured warn logs andFetchedData.truncatedflags. - Inject a prompt warning banner when any connection was truncated; add/extend unit tests and docs.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
src/core/fetcher.ts |
Adds GraphQL pagination, safety caps, truncation flags, and nested review-comment follow-up pagination. |
src/config.ts |
Adds MAX_FETCHED_* env vars (Zod schema + wiring). |
src/types.ts |
Extends FetchedData with truncated flags. |
src/core/prompt-builder.ts |
Adds truncation banner and injects it into the prompt instructions. |
test/core/fetcher.test.ts |
Adds tests for pagination merge, TOCTOU ordering, caps/logging, nested review-comment pagination, and banner presence. |
test/factories.ts |
Extends Octokit test double to support graphql.paginate + optional real plugin contract tests. |
docs/operate/configuration.md |
Documents new MAX_FETCHED_* env vars. |
docs/operate/observability.md |
Documents cap warn log shape and prompt banner behavior. |
IMPLEMENT.md |
Updates implementation summary for issue #66. |
| pageInfo { hasNextPage endCursor } | ||
| } | ||
| } | ||
| pageInfo { hasNextPage endCursor } | ||
| } |
There was a problem hiding this comment.
Status: ❌ Not addressed — disagree on the premise
Summary: This claim doesn't match the plugin's actual behaviour. The nested comments.pageInfo block under reviews.nodes[] is unreachable to the plugin's DFS, so it cannot interfere with the outer reviews.pageInfo. Verified against plugin source.
Details: @octokit/plugin-paginate-graphql@^6.0.0 finds the paginated connection by depth-first search for the first node carrying a pageInfo property. Its DFS uses an isObject guard that explicitly excludes arrays:
// node_modules/@octokit/plugin-paginate-graphql/dist-src/object-helpers.js:2,17-18
const isObject = (value) => Object.prototype.toString.call(value) === "[object Object]";
// …
if (isObject(currentValue)) {
if (currentValue.hasOwnProperty(searchProp)) {
return currentPath;
}reviews.nodes is an Array, so Object.prototype.toString.call(nodes) returns "[object Array]" and the recursion never descends into per-review objects. The DFS therefore visits repository → pullRequest → reviews and finds reviews.pageInfo first — exactly what we want.
The nested comments.pageInfo is selected only as a per-review overflow signal so we know whether to fire the follow-up REVIEW_COMMENTS_QUERY (see src/core/fetcher.ts:517); it is never used as a paginate-target for the plugin.
The suggested alternative (comments.totalCount vs nodes.length) would also work, but it's not strictly an improvement — both signals require the same fetch and the current pageInfo.hasNextPage is the canonical GitHub GraphQL paginate signal. Leaving as-is.
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@IMPLEMENT.md`:
- Around line 11-12: The documentation still references legacy cursor variable
names ($afterFiles, $afterComments, $afterReviews) while the implementation
standardizes on $cursor and the fetcher (src/core/fetcher.ts) treats the exact
variable name as part of the plugin contract; update the IMPLEMENT.md text to
replace those legacy names with $cursor, mention that all paginated queries must
expose a single $cursor variable, and note the exception for per-review nested
pagination handled by REVIEW_COMMENTS_QUERY so readers understand why a separate
request is used.
In `@src/core/fetcher.ts`:
- Around line 456-460: The current Promise.all that fans out
fetchRemainingReviewComments over overflowReviews can spawn too many concurrent
GraphQL calls and fail-fast on a single transient error; change the logic that
produces overflowResults to run the per-review fetches through a bounded
concurrency pool (limit concurrent fetchRemainingReviewComments calls to a small
number, e.g., 3–5) and wrap each call with the retry helper from
src/utils/retry.ts (use validated maxAttempts, initialDelayMs, maxDelayMs,
backoffFactor per the module’s validation rules) so each fetch is retried on
transient errors and failures degrade per-review instead of rejecting the whole
batch; keep references to overflowReviews, fetchRemainingReviewComments, and the
retry utility so reviewers can locate the change.
- Around line 439-490: The cap is being applied before TOCTOU filtering, which
can drop pre-trigger items; instead, run filterByTriggerTime (and remove
minimized items) on the raw sets first, then apply applyCap to those filtered
results and set truncated flags from the resulting capped objects; also compute
overflowReviews from the filtered reviews (not from cappedReviews.items) so
fetchRemainingReviewComments only paginates review comment threads that are
pre-trigger, and rebuild cappedReviewComments from the filtered+merged review
comments before applying applyCap.
In `@test/core/fetcher.test.ts`:
- Around line 656-706: The test currently asserts a hard-coded cap of 500;
instead read the actual fetcher cap used at runtime and use it for assertions.
Update the test around fetchGitHubData(ctx) to compute const cap =
Number(process.env.MAX_FETCHED_COMMENTS) || 500 (or import the fetcher
config/constant if available) and then replace the literal 500 in
expect(result.comments.length).toBe(500) and
expect(capWarn?.[0]["cap"]).toBe(500) with cap, and adjust any comments that
reference 500 so expectations reflect the configured MAX_FETCHED_COMMENTS value.
This ensures the test uses the same cap as the fetcher (symbols to touch:
fetchGitHubData, result.comments, log.warn mock and
MAX_FETCHED_COMMENTS/process.env).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 74632cec-4779-425c-91df-98adf31654a0
📒 Files selected for processing (9)
IMPLEMENT.mddocs/operate/configuration.mddocs/operate/observability.mdsrc/config.tssrc/core/fetcher.tssrc/core/prompt-builder.tssrc/types.tstest/core/fetcher.test.tstest/factories.ts
|
bot workflow 🔎 Resolve iteration complete — 0 failing checks, 12 open comment threads (some may already be resolved). Resolve: PR #95 — paginate GraphQL fetcher + MAX_FETCHED_* capsSummary12 review comments from Copilot and CodeRabbit triaged on PR #95 (branch
The branch was already up-to-date with CI statusHead SHA:
Review comments✅ Addressed (Valid) — 10 threads
✅ Partially Addressed — 1 thread
❌ Invalid — 1 thread (replied, NOT resolved)
Commits
Replies posted12 inline replies posted via Threads resolved11 threads resolved via the Outstanding
Verification
FIX_ATTEMPTS_CAP=3 budget: 1 attempt used ( cost: $10.2145 · turns: 132 · duration: 1206s |
…ore cap Address review feedback on PR #95: - Pass a fresh `{ owner, repo, number }` object to each parallel `octokit.graphql.paginate` call. The plugin mutates `parameters.cursor` in place between pages; sharing one object across the three concurrent PR queries produced a cursor race. - Wrap `fetchRemainingReviewComments` in `retryWithBackoff` and bound the per-review overflow fan-out to `REVIEW_OVERFLOW_CONCURRENCY = 5` via a small `pMap` helper. A PR with hundreds of overflowing reviews no longer storms the GitHub API in parallel, and a single review's pagination failure no longer aborts the whole fetch. - Filter `isMinimized` + `filterByTriggerTime` BEFORE `applyCap` on both the PR and issue paths so the cap reflects items that actually reach the prompt; minimized / post-trigger items can no longer crowd out legitimate context. - Add `@octokit/plugin-paginate-graphql` to `devDependencies` so the test factory's type imports resolve from a declared dep rather than via the transitive `octokit` chain. - IMPLEMENT.md + docs/operate/observability.md: clarify that caps bound the merged result reaching the prompt (not fetch-time memory) and document the `$cursor` / single-`pageInfo` plugin contract. - Pin the safety-cap test to `config.maxFetchedComments` rather than the literal 500 so the assertion tracks any future default change. Co-authored-by: chrisleekr-bot[bot] <chrisleekr-bot[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
# [1.9.0](v1.8.0...v1.9.0) (2026-05-03) ### Bug Fixes * **checkout:** fetch PR base branch so origin/<baseBranch> resolves (closes [#74](#74)) ([#96](#96)) ([71f83a6](71f83a6)) * **fetcher:** paginate GraphQL connections + MAX_FETCHED_* caps (closes [#66](#66)) ([#95](#95)) ([f728ecd](f728ecd)) * **triage:** accept note-only evidence; raise research max-turns to 200 ([#97](#97)) ([3b6036c](3b6036c)) * **workflow:** fix release.yml ([#98](#98)) ([cb43d69](cb43d69)) ### Features * **workflows:** publish SLSA provenance + SBOM attestations on every release tag (closes [#58](#58)) ([#94](#94)) ([95856bc](95856bc))
|
🎉 This PR is included in version 1.9.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
Closes #66 (
fix(pipeline): GraphQL fetcher silently truncates PR/issue context past 100 items).src/core/fetcher.tspreviously issued single-page GraphQL requests withfirst: 100on every connection (issue/PR comments, reviews, the inline comments nested under each review, and changed files); anything past the first 100 items was silently dropped before the data ever reached the agent. This PR threadspageInfo { hasNextPage endCursor }through every connection, walks the cursors viaoctokit.graphql.paginate(...), caps the merged result with newMAX_FETCHED_*env vars (default500), and surfaces atruncatedflag to the prompt so the agent knows when its context is incomplete.Changes
pageInfoon every connection.fetchGitHubDataswitches fromoctokit.graphql<T>()→octokit.graphql.paginate<T>(). A newREVIEW_COMMENTS_QUERYwalks each review's overflow comments via the review node ID —graphql.paginateonly follows onepageInfoper call, so the nested per-review pagination needs its own request.MAX_FETCHED_COMMENTS/_REVIEWS/_REVIEW_COMMENTS/_FILES(default500each) clamp the merged result via a smallapplyCap()helper that emitslog.warn({ connection, fetched, cap })and setsFetchedData.truncated.<connection> = truewhenever it fires.buildPromptreadsdata.truncatedand prepends aWARNING: pre-fetched context is incomplete…line naming the affected connections, so the agent can fall back to the GitHub CLI when full context matters.filterByTriggerTimeruns AFTER the paginate merge.docs/operate/configuration.mdlists the new env vars;docs/operate/observability.mddocuments the warn log shape and prompt banner under a new "Data fetching safety caps" heading (CLAUDE.md doc-sync rule).Files changed
src/core/fetcher.ts· pagination + REVIEW_COMMENTS_QUERY +applyCap+ structured warn log +truncatedflag wiring.src/types.ts·FetchedData.truncated?: { comments?, reviews?, reviewComments?, changedFiles? }.src/config.ts· 4 zod fields + env wiring (MAX_FETCHED_*).src/core/prompt-builder.ts·buildTruncationBanner+ injection into the prompt.test/core/fetcher.test.ts· 5 new tests (paginate merge / TOCTOU after merge / cap fire / nested review-comment pagination / banner).test/factories.ts·makeOctokitnow acceptsgraphqlPaginateResponses(substring-keyed routing on the paginate fn).docs/operate/configuration.md· 4 new rows.docs/operate/observability.md· new "Data fetching safety caps" section.IMPLEMENT.md· per-task summary written by the bot per the implement workflow.Commits
54e7465·fix(fetcher): paginate GraphQL connections + MAX_FETCHED_* caps (closes #66)Tests run
bun run typecheck· clean.NODE_OPTIONS='--max-old-space-size=4096' bunx eslint .· 0 errors / 291 warnings (all pre-existing, in unrelated test files).bun run format· clean.bun test test/core/fetcher.test.ts· 30 pass / 0 fail / 64 expect calls.bun run scripts/check-docs-citations.ts· clean.bun run scripts/check-docs-versions.ts· clean.bun run docs:build· skipped locally (mkdocsnot installed in workspace); enforced in CI via.github/workflows/docs.yml.bun run test(isolated runner) · 78 files passed; 25 files have all-skipped suites because Postgres/Valkey aren't running in this workspace (pre-existing infra dependency, none of the skipped files were touched by this PR).Verification
cap=500and assertsresult.comments.length === 500,truncated.comments === true, andlog.warn.mock.calls[0][0]carries{ connection: "comments", fetched: 600, cap: 500 }.triggerTimestampto comment-240 of 600 fixture comments and asserts comment-239 (newest pre-trigger) survives while 240+ are dropped.Plan deviations (intentional)
octokit ^5.0.5already bundles@octokit/plugin-paginate-graphqland exposesoctokit.graphql.paginateon every existing instance. Threading a shared factory through 11 instantiation sites would have been pure churn. Documented in IMPLEMENT.md.Related Issues
Test plan
bun run typecheckcleanbun run lintno new errors (0 errors, 291 pre-existing warnings)Summary by CodeRabbit
New Features
MAX_FETCHED_COMMENTS,MAX_FETCHED_REVIEWS,MAX_FETCHED_REVIEW_COMMENTS,MAX_FETCHED_FILES) with 500-item defaults to prevent excessive data fetching.Documentation
Tests