LearnNewsExamplesServices
Frontmatter
id16016
titleNo code path fetches past 50 comments / 20 replies per discussion — a long thread's tail has never been in the corpus
stateClosed
labels
bugaibuild
assigneesneo-gpt
createdAtJul 26, 2026, 8:11 PM
updatedAtJul 28, 2026, 6:50 PM
githubUrlhttps://github.com/neomjs/neo/issues/16016
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 28, 2026, 6:50 PM

No code path fetches past 50 comments / 20 replies per discussion — a long thread's tail has never been in the corpus

neo-opus-ada
neo-opus-ada commented on Jul 26, 2026, 8:11 PM

Context

Split out of #16001, where I mis-filed it. #16001 is titled for the fetch-cost defect — the delta cutoff never engaged, so every run re-paged the whole discussion history. This ticket is data completeness, a different failure mode that happens to live in the same query. Appending it to #16001 would have let PR #16015 auto-close a ticket with an undelivered AC, which is the close-target defect @neo-gpt-emmy flagged on PR #15999 earlier the same day.

Operator constraint, verbatim: "we MUST GET ALL COMMENTS … NEVER EVER EVER EVER drop content."

The Problem

FETCH_DISCUSSIONS_FOR_SYNC and FETCH_SINGLE_DISCUSSION_FOR_SYNC both request comments(first: $maxComments) × replies(first: $maxReplies), and both call sites pass 50 / 20 (DiscussionSyncer.mjs:422-423 and :609-610). There is no cursor anywhere:

  • All five exported queries in ai/services/github-workflow/queries/discussionQueries.mjs contain zero comment-level or reply-level cursors. Grep for commentCursor|commentsAfter|after: $comment|replyCursor|$after → no hits.
  • DiscussionSyncer.mjs has exactly one while (hasNextPage) loop, at the discussions level. The comment/reply hasNextPage values are read at :281, :285, :290, :292 only to compute a complete boolean and the conversationCommentCountObserved / …Total metadata — they never drive a fetch.
  • The recovery path passes the same caps, so a force-refetch of a truncated discussion re-truncates it.

Net: a discussion with more than 50 comments has never had its 51st comment in the corpus, and no code path can retrieve it. The syncer records that this happened (complete: false, plus observed-vs-total counts) and moves on. Same for replies past 20 on any single comment.

complete: false is not health telemetry — it is a record of content already dropped. Treating it as an acceptable field is what let a truncating sync look like a working one.

Why completeness is affordable

Measured on the live API via rateLimit(dryRun:true): 15 points per 30 discussions at the current shape, so a full traversal of ~210 discussions is ~105 points against a 5,000/hour ceiling — about 2%. Inner pagination adds requests for the minority of discussions that exceed a page, and each is cheap.

So there is no cost/completeness trade to make here. Any fix that drops content to save budget is optimising a constraint that is not binding. (The binding limit was the per-query cost ceiling, and #16001 addresses why that was reached at all.)

The Fix

Add comment and reply cursors to the sync queries and drive them from the hasNextPage values already being read for the completeness flag. The flag then stops being a record of loss and becomes an invariant: complete: false should be unreachable at the end of a successful sync.

Acceptance Criteria

  • No discussion in the corpus ends a successful sync with complete: false. This is the whole ticket.
  • Unit witness on a discussion whose comments.totalCount exceeds the per-page value: every comment appears in the rendered Markdown, and reply chains under them are complete. RED before the change.
  • Same witness for replies.totalCount exceeding the per-reply page size on a single comment.
  • The recovery path (refetchDiscussionsByNumber) reaches completeness too — asserted separately, because it carries its own caps and would otherwise re-truncate what the main path just fixed.
  • Point cost of a full pass measured before and after via rateLimit and recorded, so "affordable" is a number rather than a claim.
  • No change to the persisted corpus shape — this is a fetch-completeness fix, not a schema change.

Out of Scope

  • #16001 — the delta cutoff and the cache-merge matched pair. Landing separately as PR #16015; it reduces how often the full query runs and does not touch what a single discussion fetches.
  • #16002 / #16010 — facet isolation and within-facet resume.
  • Reducing the discussion page size. With #16001's delta engaged, a normal run fetches a handful.

Avoided Traps

  • Reading complete: false as telemetry. It records content already lost.
  • Assuming the recovery path recovers. It passes identical caps; verified at :609-610, not assumed.
  • Trading completeness for budget. Measured at ~2% of the hourly ceiling — there is nothing to trade.
  • Leaving this on #16001. Its title is the cost defect; keeping this AC there would auto-close a half-done ticket on PR #16015.

Related

#16001 / PR #16015 (delta cutoff + cache merge) · #15154 (Discussion/reply reconciliation semantics — related, different question) · #16002 / #16010 · #15972.

Live latest-open sweep at 2026-07-26T18:12Z: adjacent set is #16001, #15154, #16002, #15977; none covers inner-connection pagination as a closable leaf.

Authored by Ada (@neo-opus-ada, Claude Opus 5, Claude Code).

tobiu unassigned from @neo-opus-ada on Jul 27, 2026, 12:24 AM
tobiu referenced in commit c955a0b - "fix(github-workflow): exhaust discussion conversations (#16016) (#16101)" on Jul 28, 2026, 6:50 PM
tobiu closed this issue on Jul 28, 2026, 6:50 PM