Skip to content

fix(provider-review): page the Bitbucket inbox with a signed continuation cursor - #8229

Open
iscekic wants to merge 13 commits into
mainfrom
drain/bound-bitbucket-inbox-paging
Open

iscekic wants to merge 13 commits into
mainfrom
drain/bound-bitbucket-inbox-paging

Conversation

@iscekic

@iscekic iscekic commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Evidence

Evidence: demonstrated — on this work VM, providerReview.listInbox ran end to end through the real Next.js server, real Postgres and the real tRPC router against one local stub answering api.bitbucket.org, with three cached repositories: acme/api spanning two provider pages, acme/widgets holding one older row, and acme/gone that the provider answers with 404. The first load served 50 rows and issued a continuation; the continuation served the remaining 12 and ended; all 62 refs were distinct; and the stub observed one provider request per repository per load — page=1 for all three repositories on the first load, then page=2 for acme/api and page=1 for acme/widgets under the continuation's updated_on ceiling. acme/gone was read once, was not requested again, and did not stop the load: the other repositories' rows were served in both loads.

Change

The Bitbucket review inbox pages with a signed continuation cursor and reads a bounded amount of provider work per load.

  • listInbox enumerates the integration's cached repositories (sorted) and reads at most one provider page per repository that still has rows.
  • The continuation carries a fixed provider ceiling: the newest updated_on the first load saw. Later loads ask for state="OPEN" AND updated_on <= <ceiling>, so every pull request that existed when the walk started stays inside the collection, and one updated afterwards appears on a fresh inbox load.
  • Per repository the continuation records the provider page it is on and how many leading rows of that page are served. Rows are emitted in the merged order, so a repository's served rows are always the leading rows of its current page: the next load re-reads that page, skips them, and moves to the next page once the page is fully served (two page reads at most in one load).
  • A repository with nothing left is dropped from later loads, so the provider work per load decreases as the walk proceeds.
  • A cursor carries one window of at most 300 repositories (two packed bytes each, inside the router's 2048-character receiving limit). A workspace whose cached set is larger is walked window by window: once every repository of the current window is served, the continuation advances to the next window, so every cached repository is served. Each window starts its own ceiling from its first load, because its rows are read for the first time when the walk reaches it; the previous window's newest timestamp never bounds them. A window that serves nothing — its repositories are gone or its pages are empty — hands the walk to the next window, so an empty slice never ends the inbox. A window with rows still to serve always issues a continuation: it carries the window's timestamp bound when the window has one, and none while the pages read so far carry no orderable row. Ordering inside a window is by updated_on; a workspace larger than one window is served window by window rather than in one global merge.
  • A provider row without a parseable updated_on is left out of the merged order: it has no comparable position in the newest-first order or in a continuation, and leaving it out keeps the ceiling defined for every load that serves rows.
  • A cached repository the provider no longer serves, and a provider page an earlier load was promised for a collection that has since shrunk, end that repository's walk for this load. The rest of the inbox still loads in the same request, and the repository stops appearing in later continuations.
  • nextCursor is issued only while some repository still has rows, and the response shape (items, nextCursor) is unchanged, so the web and mobile inboxes need no change.
  • The cursor is an HMAC-signed token carrying the workspace and caller identity, a fingerprint of the cached repository set, the ceiling, the repository window, and two packed bytes per repository of that window. It fits the provider-review router's pageCursor limit (2048 characters) even for a full window positioned past its first page; INBOX_CURSOR_MAX_CHARS names that bound and a test pins it.
  • Continuation state is bound to one workspace, one caller and one repository set, verified against its signature, and usable for one hour; a load without usable state starts from the first page.
  • Stated bound: a pull request that leaves the filtered collection mid-walk shifts the rows behind it, so a row that crosses a provider page boundary during one continuation is served by a fresh inbox load rather than by that walk. Continuations live one hour, and a fresh load starts at the first page.

Verification

All of the following ran on this VM at head 4e051d9df.

  • Unit: pnpm --filter web exec jest --runInBand src/lib/provider-review src/routers/provider-review-router.test.ts — 7 suites, 285 tests passed.
    • One repository with 100 distinct timestamps (two provider pages) plus a second repository whose single row is older than all of them: 101 rows served exactly once over three loads, and each load reads one page per repository that still has rows.
    • 60 open pull requests: the first load serves the newest 50 and stores page 2; the continuation asks for updated_on <= <ceiling> on page=2 and serves the remaining 10, then ends.
    • 51 rows sharing one millisecond and differing only below it: the continuation keeps sub-millisecond precision and serves the remaining row.
    • 12 repositories with five open pull requests each: the first load reads 12 provider pages and the continuation reads only the two repositories with rows left.
    • 301 cached repositories: the first load reads the first window, the walk serves all 301 rows exactly once (including the repository past the first window), and no repository is read past its first page.
    • 301 cached repositories where the repository in the second window holds the newest row of the workspace: the walk serves all 301 rows exactly once, and the request for the second window carries no updated_on filter.
    • 301 cached repositories where the first window answers 404 for every repository and the one behind it holds a row: the first load serves nothing and issues a continuation, the next load serves that row, and the walk ends.
    • Pages one and two carrying only rows without a timestamp while each promises another page, and page three holding an orderable row: the first load serves nothing and issues a continuation, the next load reads page three without a timestamp bound and serves that row.
    • 300 repositories positioned on page 2: 300 provider requests on page=2 and a returned cursor within the receiving limit.
    • Three repositories with 40 tied rows each over three inbox pages: 120 rows served exactly once, in repository-then-position order.
    • A cached repository the provider answers with 404: the load serves the other repositories' rows in full.
    • A repository whose promised continuation page no longer exists (its collection shrank between loads): that repository ends, the remaining repositories still page, the walk terminates, and the vanished page is requested exactly once.
    • A provider page carrying one row without a timestamp plus 60 orderable rows over two pages: the walk serves all 60 exactly once, leaves the unorderable row out, and ends.
    • A repository whose final page omits the next link: the walk ends there, and the recorded provider requests show no request past either repository's last page.
    • Continuation state that does not verify, belongs to another workspace or caller, has a mismatched signature, or is older than the lifetime resolves to the first page.
    • Router: a 2048-character signed cursor is accepted and forwarded to the read layer unchanged.
  • pnpm --filter web typecheck (tsgo) passed on the same tree.
  • Forge integration E2E (the forge-integration-e2e harness, run on this VM at head 4e051d9df): 31/31 checks passed, against the real cloud dev stack (nextjs on :3000, Postgres, tRPC), one stub serving the git-token-service broker, gitlab.com, api.bitbucket.org and api.github.com. Provider paths observed:
    • providerReview.listInbox (bitbucket) first load: 50 rows, each with its full ref; the stub saw GET /2.0/repositories/acme/api/pullrequests, GET /2.0/repositories/acme/gone/pullrequests and GET /2.0/repositories/acme/widgets/pullrequests, all with pagelen=50, page=1, q=state="OPEN", sort=-updated_on. acme/gone answered 404 and the load still returned rows.
    • The continuation, sent back through the router, returned the remaining 12 rows (the tail of the two-page repository and the other repository's row), all distinct from the first page, and nextCursor was null; the stub saw GET /2.0/repositories/acme/api/pullrequests with page=2 and q=state="OPEN" AND updated_on <= 2026-01-02T01:00:00Z, and GET /2.0/repositories/acme/widgets/pullrequests with page=1 and the same ceiling. acme/gone was not requested again.
    • Bitbucket pull-request read and comment write, GitLab merge-request read / files / comment write, GitHub pull-request read and issue-comment write.
    • POST /api/webhooks/{gitlab,bitbucket/<integrationId>,github} all answered 202; Postgres shows one cloud_agent_code_reviews row per provider (acme/api Security - Fix npm audit vulnerabilities #12, acme/widgets App Builder - Unit and Integration tests #7, group/sub/repo App Builder - Unit and Integration tests #7).

Boundary. The E2E proves this repository's own code and contracts against provider-shaped responses; it does not prove a real Bitbucket workspace accepts these requests, because no owned workspace credentials exist on this work VM. Review-job dispatch and model calls stay best-effort (CODE_REVIEW_WORKER_URL points at the stub), so no review is claimed to have run. Bitbucket write paths other than the top-level comment (merge, thread resolution, inline comments) were not exercised by this run. The change is server-side and has no visual surface, so there are no screenshots.

Production prerequisites

None.

Proxy answers

The owner's proxy gave these answers in the owner's place.

  1. Question: Pagination: How should the Bitbucket inbox stop multiplying provider work with page depth?
    Answer: Keyset + signed cursor (Recommended)
    Confidence: 0.70 · Precedent: Fix the root cause and the class of the issue: session 7f3470e0ba6f, "fix class of issue preventing progress". Keyset paging makes the work per call flat at any depth, keeps the global ordering correct, and causes no product regression.
  2. Question: Repo cap: how many repositories should one load fan out over?
    Answer: Keep 300 (Recommended)
    Confidence: 0.60 · Precedent: Do the least work possible with no drop in quality: session 7f3470e0ba6f, "with no drop in quality". The cost per call becomes fixed and bounded at today's page-1 cost, and no workspace loses coverage.
  3. Question: Bad cursor: What should an unverifiable cursor (forged, tampered, expired, or minted for another workspace) do?
    Answer: Serve the first page (Recommended)
    Confidence: 0.70 · Precedent: none
  4. Question: Cursor TTL: How long should a signed inbox cursor stay valid?
    Answer: 1 hour (Recommended)
    Confidence: 0.60 · Precedent: none
  5. Question: Depth cap: the previous inbox capped depth at 10 pages (500 rows). Keep a depth cap?
    Answer: Remove it (Recommended)
    Confidence: 0.75 · Precedent: Rip out obsolete limits instead of keeping them: session ad6a3f589364, "alongside? why not instead?". The depth cap no longer bounds any work under keyset paging.
  6. Question: Mid-walk shift: how should the inbox walk handle rows that leave the filtered collection mid-walk, which shifts the rows behind them up one position?
    Answer: Ship with the documented bound
    Confidence: 0.72 · Precedent: Speed and least work: choose the simplest design that reaches the goal — session a30dc8dc131a, owner typed "Simplest please" instead of the recommended durable managed delivery
  7. Question: Repo coverage: the cursor's wire limit bounds how many repositories one window carries; how should a workspace with a larger cached set be served?
    Answer: Rotate repository windows across pages
    Confidence: 0.70 · Precedent: Choose the simplest design that reaches the goal (session 1f0202ee8c71: "What's the simplest solution that gets us to our goal?"). The goal is bounded work per call with no loss of coverage.

…tion cursor

Each inbox load reads one provider page per enumerated repository and
continues from the `updated_on` boundary of the last row served. The
continuation cursor is an HMAC-signed token bound to the workspace, and a
load without usable continuation state starts from the first page.

The merge keeps a bounded newest-first window, and the repository fan-out is
capped by the same per-load request budget.
@iscekic iscekic added drain Work item from the findings drain janitor:trust Janitor finding: trust boundary hardening labels Oct 20, 2026
Comment thread apps/web/src/lib/provider-review/bitbucket-read.ts Outdated
@kilo-code-bot

kilo-code-bot Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The incremental commit always issues a continuation when hasMore is true, so a window whose pages carry no orderable rows no longer suppresses the cursor and the walk reaches the remaining provider pages; the new stub test covers the empty-then-orderable hand-off and the unbounded first continuation request.

Assumptions: [INFERENCE] Live Bitbucket behavior for a page that advertises next while all of its rows lack a parseable updated_on was not verified against a real workspace; the stub suite covers that response shape. The mobile inbox keeps its own 20-page (maxPages) client cap, so a walk that needs more loads is still bounded there.

Files Reviewed (2 files)
  • apps/web/src/lib/provider-review/bitbucket-read.ts
  • apps/web/src/lib/provider-review/bitbucket-read.test.ts
Previous Review Summaries (9 snapshots, latest commit 15e6cde)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 15e6cde)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The empty-window hand-off is fixed and covered by a test, but the same cursor suppression still silently truncates the walk when a repository page advertises next without serving any orderable row.

Assumptions: [INFERENCE] Whether Bitbucket ever returns a page with an empty values array while still advertising a next link was not verified against a live workspace; the stub suite does not exercise that response shape.

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
apps/web/src/lib/provider-review/bitbucket-read.ts 1132 hasMore can be true while ceiling is null (a page with no orderable rows that still advertises next), so the cursor is suppressed and the remaining rows and windows are dropped silently
Files Reviewed (2 files)
  • apps/web/src/lib/provider-review/bitbucket-read.ts - 1 issue
  • apps/web/src/lib/provider-review/bitbucket-read.test.ts - no issues (the new test covers the empty first window and the next-window hand-off)

Fix these issues in Kilo Cloud

Previous review (commit a74138a)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The incremental fix correctly gives each repository window its own ceiling, but the window transition is still gated on a non-null ceiling, so a workspace whose first window yields no orderable rows never reaches the remaining windows.

Assumptions: [INFERENCE] Whether a 300-repository window is ever completely empty in practice (no open PRs or only rows without a parseable updated_on) was not verified against a live Bitbucket workspace; the stub in this suite does not exercise an empty first window.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/lib/provider-review/bitbucket-read.ts 1131 The window advance remains inside if (ceiling !== null), so an empty finished window (inboxCeiling returns null) issues no cursor and later windows are never read
Files Reviewed (2 files)
  • apps/web/src/lib/provider-review/bitbucket-read.ts - 1 issue
  • apps/web/src/lib/provider-review/bitbucket-read.test.ts - no issues (the new test covers the inherited-ceiling case but not an empty first window)

Fix these issues in Kilo Cloud

Previous review (commit 82a35e8)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The new repository-window walk reuses the first window's updated_on ceiling for every later window, so newer PRs in repositories past the first 300 are filtered out, and the walk never advances at all when the first window has no rows.

Assumptions: [INFERENCE] Bitbucket's per-repository query applies state="OPEN" AND updated_on <= <ts> the way the test stub does (verified against the stub; no live workspace). The window-offset and cursor-validation paths were read in full and behave as documented.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/lib/provider-review/bitbucket-read.ts 1126 The window advance reuses a ceiling computed from the first window only, dropping newer rows in later windows and never advancing when the first window has no rows
Files Reviewed (2 files)
  • apps/web/src/lib/provider-review/bitbucket-read.ts - 1 issue
  • apps/web/src/lib/provider-review/bitbucket-read.test.ts - no issues (the new 301-repository test does not vary timestamps, so it does not cover the ceiling defect)

Fix these issues in Kilo Cloud

Previous review (commit 55ac218)

Status: 3 Issues Found | Recommendation: Address before merge

Executive Summary

The per-repository page-position redesign is logically sound, but the new 300-repository cap silently hides PRs from larger cached workspaces, and two smaller gaps remain around missing updated_on values and the next-link regression test.

Assumptions: [INFERENCE] Bitbucket's BBQL endpoint accepts the raw provider timestamp (including microseconds and +00:00) unquoted in updated_on <= <ts> and compares it at full precision; the E2E used a stub, so this is unverified. The model also assumes equal-timestamp rows keep their relative order between loads. The 300-repository cap is disclosed in the PR, but the "no workspace loses coverage" rationale does not hold: the cached set can hold up to 5000 repositories and the previous code queried all of them.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/lib/provider-review/bitbucket-read.ts 1014 Sorted 300-repository cap is applied on every load and silently drops all repositories beyond it

SUGGESTION

File Line Issue
apps/web/src/lib/provider-review/bitbucket-read.ts 1122 Pagination stops issuing a cursor when no read row has a parseable updated_on, truncating the inbox
apps/web/src/lib/provider-review/bitbucket-read.test.ts 1369 The omitted-next regression test also passes through the 404 path, so it no longer pins the hadNext check
Files Reviewed (3 files)
  • apps/web/src/lib/provider-review/bitbucket-read.ts - 2 issues
  • apps/web/src/lib/provider-review/bitbucket-read.test.ts - 1 issue
  • apps/web/src/routers/provider-review-router.test.ts - no issues

Fix these issues in Kilo Cloud

Previous review (commit 7ff7b43)

Status: No Issues Found | Recommendation: Merge

Executive Summary

The one-line hadNext fix correctly treats an omitted next link as the end of a repository's provider pages, and the added regression test fails on the previous code (page=3 Request on the exhausted repository answered 404) and passes now, serving all 151 tied rows exactly once.

Assumptions: [INFERENCE] Bitbucket omits next on the final page rather than returning null; every fixture in this suite writes next: null, and no live workspace credentials were available. Given that, Boolean(parsedPage.data.next) matches the truthiness test already used for repositoryPageFull (line 1045) and in pageFromPayload.

Files Reviewed (2 files)
  • apps/web/src/lib/provider-review/bitbucket-read.ts
  • apps/web/src/lib/provider-review/bitbucket-read.test.ts

Previous WARNING (dangling page after an omitted next) is resolved at 7ff7b435d3.

Previous review (commit 26679ed)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The page-local continuation fixes the tied-row loss, but an omitted final-page next is read as another page, so a repository can be positioned past its last page and fail the inbox with a 404.

Assumptions: [INFERENCE] Bitbucket omits next on the final page — this module's schema marks it optional and its other readers treat absence as end of pagination, but every fixture here writes next: null and no live workspace credentials were available to confirm. Stable provider tie ordering across pages is also assumed.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/lib/provider-review/bitbucket-read.ts 1052 hadNext compares next !== null, so an absent next (final page) counts as another page and the repository advances past the end.
Files Reviewed (2 files)
  • apps/web/src/lib/provider-review/bitbucket-read.ts - 1 issue
  • apps/web/src/lib/provider-review/bitbucket-read.test.ts - 0 issues

Previous review (commit f46f4de)

Status: 2 Issues Found | Recommendation: Address before merge

Executive Summary

The signed cursor is correctly bound, bounded and wire-sized, but a boundary timestamp that fills more than one provider page can still permanently drop rows once another repository advances the global tie cursor.

Assumptions: [INFERENCE] The tie-order analysis assumes the provider keeps a stable secondary order across pages of identical updated_on values; a provider that reorders ties could also cause drops. Cursor length is bounded under the pinned maximum-size test; no live Bitbucket credentials were available, so behavior against the real provider is unverified.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/lib/provider-review/bitbucket-read.ts 1052 The global lastRef tie cursor can move past a repository's unserved keys before that repository's next provider page is read, so tied rows are filtered out on the following load.

SUGGESTION

File Line Issue
apps/web/src/lib/provider-review/bitbucket-read.test.ts 1360 The TTL and workspace-scoping tests mint cursors in the obsolete servedRefs shape, so they are rejected by the payload schema before reaching the checks they name.
Files Reviewed (3 files)
  • apps/web/src/lib/provider-review/bitbucket-read.ts - 1 issue
  • apps/web/src/lib/provider-review/bitbucket-read.test.ts - 1 issue
  • apps/web/src/routers/provider-review-router.test.ts - 0 issues

Previous review (commit 5619475)

Status: No Issues Found | Recommendation: Merge

Executive Summary

The follow-up commit resolves the prior boundary-precision finding: the continuation cursor now carries the provider's own updated_on string, and inboxUpdatedMs restores sub-millisecond digits for ordering, with a new test exercising a microsecond boundary across repositories.

Files Reviewed (2 files)
  • apps/web/src/lib/provider-review/bitbucket-read.ts
  • apps/web/src/lib/provider-review/bitbucket-read.test.ts

Previous review (commit a8b1131)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The signed-cursor rewrite of the Bitbucket inbox is sound in its top-K fan-out and token handling, but reformatting the updated_on boundary through Date truncates Bitbucket's microsecond timestamps and can silently drop unserved PRs from later pages.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/lib/provider-review/bitbucket-read.ts 1054 inboxBoundaryValue normalizes updated_on to millisecond precision, so the continuation filter (updated_on <= <boundary>) excludes unserved rows in the truncated sub-millisecond interval; the new fixtures use whole-second timestamps and cannot catch it.
Files Reviewed (2 files)
  • apps/web/src/lib/provider-review/bitbucket-read.ts - 1 issue
  • apps/web/src/lib/provider-review/bitbucket-read.test.ts - 0 issues (test gap noted in the implementation finding)

Fix these issues in Kilo Cloud


Reviewed by deepseek-flash-latest · Input: 58.1K · Output: 9.9K · Cached: 576.6K

Review guidance: REVIEW.md from base branch main

The continuation cursor carries the provider's own `updated_on` string, so a
later inbox page asks for rows at or below that exact value. The merge orders
rows by the same sub-millisecond value, and the boundary comes from the last
row in the window that has a parseable timestamp.
@iscekic

iscekic commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Fixed in 5619475. The cursor now carries the provider's own updated_on string, so the continuation filter keeps full microsecond precision — e.g. state="OPEN" AND updated_on <= 2026-09-02T00:00:02.400902+00:00 — and the merge orders rows by the same sub-millisecond value. The boundary is taken from the last row in the window that has a parseable timestamp.

Added continues past a boundary that carries microseconds: a 50-row window ends on .400902 while an unserved row from another repository sits at .400500; the next page serves exactly that row.

@iscekic
iscekic enabled auto-merge October 20, 2026 03:11
@iscekic iscekic added the merge:human Substantive functional risk requires human review. label Oct 20, 2026
@iscekic
iscekic disabled auto-merge October 20, 2026 19:07
@iscekic iscekic added merge:blocked merge:blocked:insufficient-e2e Verification does not cover the change intent:ok Janitor change confirmed as a real bug fix, not intended behavior and removed merge:blocked merge:blocked:insufficient-e2e Verification does not cover the change labels Oct 20, 2026
@iscekic

iscekic commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator Author

Changes required before merge: the new signed inbox continuation has progress and wire-size gaps.

At apps/web/src/lib/provider-review/bitbucket-read.ts:997, every continuation reads provider page one. Rows already served at the boundary are removed locally at line 1075. If a tied timestamp fills that provider page, the next request sees only already-served rows, produces no boundary, and returns no continuation despite unserved provider pages. Carrying only the previous window of served refs can also repeat tied rows across later inbox pages.

The cursor also embeds a page of JSON-encoded reference keys at line 1110, while provider-review-router.ts:91 caps the received cursor at 2048 characters. A synthetic ordinary-reference payload already exceeds that bound before the signer adds its timestamp and nonce, so a server-issued cursor can fail the next tRPC request.

Use a stable tie-breaking continuation that advances across provider pages and remains within an explicit receiving wire limit. Add equal-timestamp cases spanning provider and inbox pages, plus a maximum-size cursor round trip through the real router.

@iscekic iscekic added the do-not-merge Do not merge this PR (yet) label Oct 24, 2026
…-inbox-paging

# Conflicts:
#	apps/web/src/lib/provider-review/bitbucket-read.test.ts
#	apps/web/src/lib/provider-review/bitbucket-read.ts
…pages

Every inbox load reads one provider page per integration repository. A
continuation now advances a repository's provider page when the page holds
only rows already served at the continuation boundary, so rows that share a
timestamp across provider pages stay reachable.

The merged order breaks timestamp ties by repository then pull-request id, so
the continuation carries only the last served ref at its boundary instead of
every ref at that boundary, and the per-repository page positions are packed
two bytes each. A three-hundred-repository continuation is checked against the
provider-review router's page-cursor limit.
@iscekic iscekic removed the merge:human Substantive functional risk requires human review. label Oct 24, 2026
@iscekic

iscekic commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Addressed in f46f4de838.

  • Ordering. The merged inbox order breaks equal provider timestamps by repository, then pull-request id, and a continuation carries the last ref it served at its boundary. A repository's provider page position advances past a page whose rows are all already served at that boundary, so tied rows that continue onto a later provider page are still delivered, and every load keeps issuing exactly one provider page request per integration repository.
  • Cursor size. The cursor carries the boundary, one ref, and two packed bytes of page position per repository. INBOX_CURSOR_MAX_CHARS (2048) names the provider-review router's receiving bound, and a test pins a 300-repository cursor with every repository positioned past its first page to it, including the round trip through the real router.
  • Tests. Equal-timestamp cases spanning provider pages (51 rows sharing a timestamp in one repository: 50 on the first page, the remainder on the second) and spanning inbox pages (three repositories × 40 tied rows: 120 rows served exactly once in repository-then-id order, every provider request on page 1), plus the maximum-size cursor round trip.

Evidence at head f46f4de838: 272 unit tests across the provider-review suites and the router suite, and the forge integration E2E (31/31 checks) with the inbox read served through the real tRPC procedure. Details are in the pull-request body.

Comment thread apps/web/src/lib/provider-review/bitbucket-read.ts Outdated
Comment thread apps/web/src/lib/provider-review/bitbucket-read.test.ts Outdated
The continuation records, per repository, the provider page it is on and how
many of that page's rows are served, instead of one cross-repository tie key.
A repository whose page rows are all served moves to the next provider page in
the same load, and a repository with nothing left at the boundary stops being
read until the boundary changes, so rows that share a timestamp across pages
stay reachable no matter which repository the merged order reaches last.

The cursor carries two packed bytes per repository, and a 300-repository
continuation stays within the provider-review router's page-cursor limit.
Comment thread apps/web/src/lib/provider-review/bitbucket-read.ts Outdated
Bitbucket omits the next link entirely on the last page, so a repository is
now paginated only while the link is present. Added a case where one
repository's single-row last page carries no next link while another
repository keeps paginating: the finished repository is not requested again,
and the provider's page-not-found answer never reaches the inbox.
@iscekic iscekic added merge:blocked:insufficient-e2e Verification does not cover the change merge:human Substantive functional risk requires human review. labels Oct 24, 2026
@iscekic iscekic removed the merge:human Substantive functional risk requires human review. label Oct 24, 2026
@iscekic

iscekic commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Addressed in 83b935baf2.

  • Per-repository position. The continuation carries the fixed provider ceiling (the newest updated_on the first load saw) and, per repository, the provider page plus how many leading rows of that page are served. Each load reads one page per repository that still has rows, skips the served leading rows, and moves to the next page once a page is fully served, so a continuation advances instead of restarting and a repository with nothing left drops out of later loads.
  • Every row once. Rows are emitted in the merged order, so a repository's served rows are exactly the leading rows of its current page; that invariant holds for distinct timestamps as well as ties.
  • Tests. 100 distinct timestamps in one repository spanning two provider pages plus a second repository whose single row is older than all of them: 101 rows served exactly once over three loads. Also a 60-row repository whose first page is fully served (the continuation asks for page=2 under the ceiling) and 51 rows sharing one millisecond (the ceiling keeps sub-millisecond precision).
  • E2E. Forge harness on this VM at the same head: 25/25 checks, providerReview.listInbox through the real tRPC router, two cached repositories, 61 rows served exactly once, and the stub observed one provider request per repository per load with page=2 reached only by the repository that needed it.

Unit: 7 suites / 279 tests. pnpm --filter web typecheck passed. Details are in the pull-request body.

…gone

A cached repository the provider no longer serves, and a provider page an
earlier load was promised for a collection that has since shrunk, now end
that repository's walk instead of the whole inbox load. The load still
serves every repository the provider answers for, and the repository stops
appearing in later continuations.
Comment thread apps/web/src/lib/provider-review/bitbucket-read.ts Outdated
Comment thread apps/web/src/lib/provider-review/bitbucket-read.ts Outdated
Comment thread apps/web/src/lib/provider-review/bitbucket-read.test.ts
@iscekic iscekic added merge:blocked:kilobot-not-ready Review bot has not recommended merge at this head merge:human Substantive functional risk requires human review. labels Oct 24, 2026
A workspace whose cached repository set is larger than one cursor carries is
now walked one window at a time, so every cached repository is served instead
of the walk stopping at the first window. A provider row without a comparable
timestamp is left out of the merged order instead of ending the walk before
the rows behind it, and the omitted-next regression test now records provider
requests so a page past a repository's last page fails it.
@iscekic iscekic removed merge:human Substantive functional risk requires human review. merge:blocked:kilobot-not-ready Review bot has not recommended merge at this head labels Oct 24, 2026
Comment thread apps/web/src/lib/provider-review/bitbucket-read.ts Outdated
@iscekic iscekic added merge:blocked:kilobot-not-ready Review bot has not recommended merge at this head merge:human Substantive functional risk requires human review. labels Oct 24, 2026
…tarts

A repository window's rows are read for the first time when the walk reaches
that window, so the previous window's newest timestamp cannot bound them: a
pull request newer than it stayed outside the collection for the whole walk.
Each window now starts without a ceiling, records the newest timestamp of its
own first load, and filters later loads of that window with it.
@iscekic iscekic removed merge:blocked:kilobot-not-ready Review bot has not recommended merge at this head merge:human Substantive functional risk requires human review. labels Oct 24, 2026
Comment thread apps/web/src/lib/provider-review/bitbucket-read.ts Outdated
@iscekic iscekic added merge:blocked:kilobot-not-ready Review bot has not recommended merge at this head merge:human Substantive functional risk requires human review. labels Oct 24, 2026
…d nothing

A window whose repositories the provider no longer serves, or whose pages are
empty, produces no timestamp and therefore no ceiling. The walk now moves to
the next repository window in that case, so the windows behind it are still
read instead of the inbox ending at an empty slice.
@iscekic iscekic removed merge:human Substantive functional risk requires human review. merge:blocked:kilobot-not-ready Review bot has not recommended merge at this head labels Oct 24, 2026
Comment thread apps/web/src/lib/provider-review/bitbucket-read.ts Outdated
@iscekic iscekic added merge:blocked:kilobot-not-ready Review bot has not recommended merge at this head merge:human Substantive functional risk requires human review. labels Oct 24, 2026
The continuation carries the window's provider timestamp bound whenever the
window has produced one. Where it has not, the next load reads the window
without that bound, so the walk reaches every remaining provider page and
every repository window behind it.
@iscekic iscekic added merge:blocked:kilobot-not-ready Review bot has not recommended merge at this head merge:human Substantive functional risk requires human review. and removed merge:blocked:kilobot-not-ready Review bot has not recommended merge at this head merge:human Substantive functional risk requires human review. labels Oct 24, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge Do not merge this PR (yet) drain Work item from the findings drain intent:ok Janitor change confirmed as a real bug fix, not intended behavior janitor:trust Janitor finding: trust boundary hardening merge:human Substantive functional risk requires human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants