Skip to content

fix: stop looping on routes whose pagination cursor never advances - #157

Open
cvkramer wants to merge 1 commit into
mainfrom
fix/broken-server-pagination
Open

cvkramer wants to merge 1 commit into
mainfrom
fix/broken-server-pagination

Conversation

@cvkramer

@cvkramer cvkramer commented Nov 4, 2026 •

Copy link
Copy Markdown

Problem

A confirmed set of public GET list routes never bind page_size/page_token
server-side (the API-gateway route is missing a query: block, so the
generated decoder never reads either param from the URL). The server always
answers page 1 and reseals a fresh-looking nextPageToken on every call, so
following the cursor returns page 1 forever instead of reaching the rest of a
large result set:

  • /api/v1/automations
  • /api/v1/automation_executions
  • /api/v1/functions
  • /api/v1/functions/{id}/commits
  • (confirmed affected but with no c1i command today: /api/v1/apps/{app_id}/entitlement_configuration_rules, /api/v1/apps/{app_id}/entitlements/{app_entitlement_id}/automation/exclusions, /api/v1/decoys, /api/v1/functions/{id}/invocations, /api/v1/iam/model_api_keys, /api/v1/iam/personal_clients, /api/v1/iam/personal_devices/{id}/clients, /api/v1/iam/tunnel/bridges, /api/v1/llm-gateway/keys, /api/v1/role-mining/{suggestions,runs,custom-analysis}, /api/v1/service_principals/{id}/trusts, /api/v1/settings/ai-governance/history, /api/v1/step-up/providers, /api/v1/workload_federation/providers)

Live on a test tenant: with 150 automations, GET /api/v1/automations returned
the same 54 rows on every call; GET /api/v1/automation_executions returned
the same 30 of 34. c1i automations list and c1i automations executions list
both looped forever on the shifting-but-non-advancing cursor. functions list
and functions commits were exposed to the same defect with no guard at all.

This is a server-side bug, being reported separately. There is no
client-side fix that produces a complete result for these routes, so this PR makes c1i detect the stuck
cursor and fail loudly instead of hanging or silently returning an incomplete
page at exit 0.

What changed

  • paginationCursorGuard (cmd/flags.go, from prior work on this branch) now
    backs automations list, automations executions list, functions list,
    and functions commits (and functions usage, which also reads
    /api/v1/automations). It flags a server that sends the same page_token
    twice, or a non-empty page whose every item id was already emitted under an
    earlier (possibly different-looking) token.
  • A tripped guard stops auto-pagination, keeps every row already printed (no
    duplicates, no re-fetching the stuck page), prints a stderr warning naming
    the route and saying results are INCOMPLETE, and now returns a new
    stuckPaginationCursorError so the process exits 6 ("C1 failed")
    instead of silently exiting 0.
  • c1i api --paginate (the generic escape hatch) gets the same
    duplicate-page detection, not just its previous same-token-twice check: it
    compares each page's items by "id" when present, falling back to the
    item's own canonical JSON when it has none, since api can target any
    endpoint and can't assume an id field exists.
  • cmd/errors.go, CLAUDE.md, .claude/commands/c1i.md, cmd/agents.md
    (the c1i docs agents source), and README.md document the new exit-6
    cause and the stuckPaginationCursorError type.

Per-command / per-route disposition

Route c1i command(s) Action
GET /api/v1/automations automations list, functions usage guarded + exit 6
GET /api/v1/automation_executions automations executions list guarded + exit 6
GET /api/v1/functions functions list guarded + exit 6
GET /api/v1/functions/{id}/commits functions commits guarded + exit 6
any route, via the raw escape hatch api --paginate guarded + exit 6 (generic, id-or-canonical-JSON dedup)
other 14 confirmed-affected routes none exist in c1i today untouched — no command to guard
/api/v1/service_principals (not confirmed broken) service_principals list untouched — correctly-bound route, out of scope

Tests

  • cmd/list_pagination_test.go: the shared listPaginationCase table gains a
    stuckCursorRoute field and TestListCommandsStuckCursorReturnsExitServer,
    covering automations executions list, functions list,
    functions commits, and functions usage against a server that reseals a
    new token over an unchanged page.
  • cmd/automations_list_pagination_test.go: the three existing stuck-cursor
    cases (same token, changing token, repeated empty page) are updated to
    assert exit 6 instead of silent exit 0.
  • cmd/api_test.go: three new cases for api --paginate — same token twice,
    changing token/same rows, and the no-"id" canonical-JSON fallback.
  • All new/updated assertions were confirmed to fail against the pre-fix
    code
    (an unguarded command spins past the test's request-count ceiling
    and the mock server exhausts its fixture; api --paginate returns the old
    generic exit 1) and pass after the fix.
  • go test -count=1 ./cmd/...: pass. go vet ./...: clean. gofmt -l .:
    clean. golangci-lint run --timeout=3m (v2.15.0, matching CI): 0 issues.
    go mod tidy: no diff. go build ./...: clean.
  • Full-repo go test -count=1 ./... has one unrelated pre-existing failure,
    TestStoreSweepLeavesCredentialsDir in internal/tokensource, confirmed to
    fail identically on a clean origin/main checkout on this machine (a
    macOS-specific tmp-dir/Application Support path assumption, unrelated to
    this change).

Live smoke (read-only against a test tenant, except the documented test data)

Created 100 disabled, draft "inert" automations (zz-pagetest 001..100) via
c1i api --path /api/v1/automations --body '{...isDraft/enabled:false...}'
to push the tenant past the ~54-row page-1 cap, per the reproduction recipe.

$ timeout 30 c1i automations list --url https://<tenant>.conductor.one
# 56 rows printed, no duplicate ids, then:
Warning: results from GET /api/v1/automations are INCOMPLETE: the server did
not advance the pagination cursor (repeated or non-advancing nextPageToken).
Rows already fetched are printed above with no duplicates; this is a known
C1 API server defect, not a c1i bug.
exit code: 6

$ timeout 30 c1i automations executions list --url https://<tenant>.conductor.one
# 30 rows printed, no duplicate ids, same warning shape, exit 6

$ timeout 20 c1i api --path /api/v1/automations --paginate --url https://<tenant>.conductor.one
# same behavior generically: 56 rows, warning, exit 6

$ timeout 20 c1i functions list --url https://<tenant>.conductor.one
# 1 row, exit 0 (tenant has too few functions to trigger the defect; no regression)

All 100 inert automations were deleted afterward
(DELETE /api/v1/automations/{id} per id); automations list now shows
exactly the tenant's 2 real automations ("Onboarding - birthright access &
welcome", "Mover + Leaver - department change & offboarding"), confirming
cleanup.

Uncertainties

  • The 14 other confirmed-affected routes have no corresponding c1i command
    today, so there's nothing in this repo to guard — flagged for awareness,
    not fixed here.
  • gosec/govulncheck weren't run locally (not installed in this
    environment); the changes here are pure JSON/string handling with no new
    file, network, or process operations, so no new findings are expected, but
    CI will run both.
  • The server-side bug itself is being reported to the API team separately;
    this PR intentionally contains no server-side change.

A confirmed set of public GET list routes never bind page_size/page_token
server-side (the API-gateway route lacks a query: block, so the generated
decoder reads neither param from the URL): /api/v1/automations,
/api/v1/automation_executions, /api/v1/functions, and
/api/v1/functions/{id}/commits among them. The server always answers page
1, resealing a fresh-looking nextPageToken on every call, so following the
cursor returns page 1 forever instead of reaching the rest of a large
result set. Live on a test tenant: with 150 automations, GET /api/v1/automations
returned the same 54 rows on every call; automation_executions returned
the same 30 of 34. c1i's automations list and automations executions list
both looped forever; functions list and functions commits were exposed to
the same defect without any guard at all.

This is a server defect, reported separately. There is no client-side
workaround that produces a complete result (no search/* equivalent is
substituted here), so the fix is to detect the stuck cursor and fail
loudly instead of hanging or silently returning an incomplete page at
exit 0:

- paginationCursorGuard (cmd/flags.go, from prior work) now backs
  automations list, automations executions list, functions list, and
  functions commits (and functions usage, which also reads
  /api/v1/automations). It flags a server that sends the same page_token
  twice, or a non-empty page whose every item id was already emitted
  under an earlier (possibly different-looking) token.
- A tripped guard stops auto-pagination, keeps every row already printed
  (no duplicates, no re-fetching), prints a stderr warning naming the
  route and saying results are INCOMPLETE, and now returns a new
  stuckPaginationCursorError so the process exits 6 ("C1 failed") instead
  of silently exiting 0.
- `c1i api --paginate` (the generic escape hatch, cmd/api.go) gets the
  same duplicate-page detection, not just its old same-token-twice check:
  it compares each page's items by "id" when present, falling back to an
  item's own canonical JSON when it has none, since `api` can target any
  endpoint and can't assume an id field exists.
- cmd/errors.go, CLAUDE.md, .claude/commands/c1i.md, cmd/agents.md (the
  `c1i docs agents` source), and README.md document the new exit-6 cause
  and the stuckPaginationCursorError type.

Tests: cmd/list_pagination_test.go's shared table gains a
stuckCursorRoute field and TestListCommandsStuckCursorReturnsExitServer,
covering automations executions list, functions list, functions commits,
and functions usage against a server that reseals a new token over an
unchanged page; cmd/automations_list_pagination_test.go's three existing
stuck-cursor cases (same token, changing token, repeated empty page) are
updated to assert exit 6 instead of silent exit 0;
cmd/api_test.go gains three cases for `api --paginate` (same token twice,
changing token same rows, and the no-"id" canonical-JSON fallback). All
were confirmed to fail against the pre-fix code (an unguarded command
spins past the test's request-count ceiling; `api --paginate` returns the
old generic exit 1) and pass after.

Verified live on a test tenant (read-only except ~100 disabled draft
automations created and deleted for the reproduction, per the test
instructions): with 102 automations, automations list stops at 56 rows,
warns, and exits 6; automations executions list stops at 30 of 34 (cf.
POST /api/v1/search/automation_executions, which returns all 34); `api
--paginate --path /api/v1/automations` shows the same behavior generically.
Only the tenant's 2 real automations remain afterward.
@cvkramer
cvkramer force-pushed the fix/broken-server-pagination branch from 028f94d to 1d823b1 Compare November 4, 2026 22:16
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.

2 participants