Skip to content

fix(agent-sdk): stop maxItems from dropping rows and reporting a stale cursor - #332

Open
V3RON wants to merge 1 commit into
mainfrom
fix/agent-sdk-max-items-row-loss
Open

fix(agent-sdk): stop maxItems from dropping rows and reporting a stale cursor#332
V3RON wants to merge 1 commit into
mainfrom
fix/agent-sdk-max-items-row-loss

Conversation

@V3RON

@V3RON V3RON commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Description

callToolWithOptionalPagination trimmed collected items down to maxItems but passed the producer's page envelope through unchanged, so the returned page.nextCursor pointed past the rows it had just discarded. Those rows became unreachable with no error and no gap marker.

Two independent loss paths, both fixed:

  • Initial page — with limit: 20, maxItems: 5, the tool returned 20 rows, the caller received 5, and the cursor still pointed past row 19. Rows 5–19 vanished, and the result looked complete.
  • Mid-loop — with limit: 5, maxItems: 7, rows 7–9 were fetched, discarded, and skipped by the propagated cursor.

The fix is to stop over-fetching: every request, including the first, now asks for min(callerLimit, maxItems - collectedSoFar), so a well-behaved producer's own cursor lands exactly on the truncation boundary. No cursors are synthesized.

For producers that clamp or ignore limit (console.getMessages documents "Default 50, max 200"), truncation can still happen. In that case the cursor is untrustworthy, so it is dropped and a new truncated: true field is set on PageEnvelope alongside hasMore: true, meaning "rows were dropped; resume position is unknown — restart or narrow the query."

Related Issue

Closes #329

Context

The SDK cannot fix this by computing a cursor. Cursor encoding is producer-owned and opaque — built-in domains use base64url of {v,scope,index}, third-party plugins may use anything — so there is no way to express "row 5 of this page". Hence the two-part approach: make truncation not happen in the common case, and signal it honestly when a producer forces it.

hasMore is deliberately true whenever truncated is set: we positively know unconsumed rows exist, since we just discarded them, even though the resume position is lost.

Reviewer notes:

  • One existing test (caps an oversized first page...) encoded the bug as expected behavior, asserting that a limit-ignoring producer's stale cursor was propagated past a discarded row. Its expectations were updated. Worth a look, since the bug had a test blessing it.
  • merged.page.limit still reports the producer's real page size, not the SDK's internally reduced request limit.
  • page.reset handling is untouched and its existing tests pass unmodified.
  • validateAutoPagination still requires pagesLimit alongside maxItems. That constraint is arguably now unnecessary, since maxItems alone is well-defined once requests are budgeted, but relaxing it is a public API change and is left out of a bugfix.
  • Deliberately excludes CLI output changes. feat(cli): emit reusable columnar JSON for paginated output #322 renders page.nextCursor into a runnable next command, which is what makes this bug user-visible; consuming the new truncated signal there is a follow-up on that branch, since the code it needs does not exist on main.

Testing

  • Added regression tests to packages/agent-sdk/src/__tests__/pagination.test.ts for both loss paths, using a fake producer that mirrors the real cursor encoding in local-domains.ts. Each asserts on concrete row identities: the union of rows received plus rows reachable via the returned cursor. Both were confirmed failing before the fix (expected [5,6,...,19] to deeply equal [] and expected [7,8,9] to deeply equal []).
  • npx turbo run typecheck lint test --force across the whole repo: 99 successful, 99 total.
  • Version plan added: .changeset/agent-sdk-max-items-row-loss.md (@rozenite/agent-sdk patch, @rozenite/agent-shared minor for the new PageEnvelope field).

…e cursor

autoPaginate.maxItems trimmed the merged items down to the requested
count but kept the underlying page's nextCursor, which pointed past
the rows that were just discarded - both on the initial page and
mid-loop. Callers following the reported cursor would silently skip
real rows.

Fix by never over-fetching: every call (including the first) now
requests min(callerLimit, remaining budget) so a well-behaved
producer's own cursor lands exactly where the caller stopped. For
producers that clamp/ignore limit and still over-return, the extra
rows are trimmed and nextCursor is dropped (it can't be trusted to
resume past a truncation), signalled via a new truncated: true field
on PageEnvelope (@rozenite/agent-shared) alongside hasMore: true.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

autoPaginate.maxItems silently drops rows and returns a cursor that skips them

1 participant