Skip to content

Let an agent poll an endpoint with a cursor - #47

Merged
botre merged 1 commit into
masterfrom
feat/agent-cursor-api
Aug 9, 2026
Merged

Let an agent poll an endpoint with a cursor#47
botre merged 1 commit into
masterfrom
feat/agent-cursor-api

Conversation

@botre

@botre botre commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Implements PRD-agent-polling.md and its companion technical spec.

The JSON API already returned everything an agent needs, but nothing pointed at it, every call returned the newest 128 captures, and the safe polling rate was unstated. Watching a webhook from a coding agent meant hand-writing the integration.

Cursor on the listing

GET /api/endpoints/:endpoint/requests takes an optional since and answers with cursor and hasMore. Echoing the cursor back is a promise that no capture is handed over twice and none is skipped. Omitting it changes nothing, so public/endpoint.js is untouched.

A malformed since is a 400 rather than a silently ignored parameter. Falling back to the full window would make a poller reprocess its whole history while looking like a successful call.

The ordering flips with the cursor, and that is a correctness rule rather than a preference. Under DESC with a full page, a burst larger than the limit returns its newest captures, the caller's cursor advances past the rest, and the backlog is never served. ASC hands out the oldest unseen page and the next call resumes where it ended.

Timestamps are now stored in UTC

Not in the spec's file list, added after measuring. CreatedAt was stamped with time.Now() in the process's local zone and glebarez stores it as TEXT carrying that offset. SQLite has no date type, so this column is compared and ordered as text, and a +02:00 row is ranked against a UTC cursor by its digits rather than its instant:

same instant, local zone  -> 0 rows, want 0  ok
same instant, UTC         -> 1 row,  want 0  WRONG
1 hour later, UTC         -> 1 row,  want 0  WRONG

Containers run UTC and so does CI, so the spec's since = parsed.UTC() would have been green in production and in CI while replaying up to two hours of captures on any developer machine that is not. CreateRequest now normalises on the way in and both read paths normalise on the way out. No migration: captures are ephemeral by design. It also closes a latent misordering across a DST fall-back.

Prompt, limit, and discovery

The endpoint page carries a collapsed panel with a prompt teaching the loop, shown before it is copied because it is about to be pasted into something with tool access. It is built from the request, so a self-hosted instance points at itself, and it quotes the rate limit and retention window from the constants that enforce them rather than restating them.

productionRequestsPerMinute goes to 150, so the recommended 2 second poll (30/min) leaves four fifths of the budget for the traffic under test. PRODUCT.md named the old figure and is updated.

Homepage use-cases grid becomes 2x2 with the fourth card from PRD §5.4. docs/api.md is PRD §9's cuttable addition; say the word and I will drop it.

Verification

  • go vet, go test, npm run lint, npx tsc --noEmit, prettier, and the npm run css staleness check all pass. 64/64 Playwright tests pass, including 5 new ones for the panel.
  • The burst-drain test is the load-bearing one. Mutation-checked: flipping ASC back to DESC makes it fail with "the drain is not converging".
  • Drove the real loop against a running server under TZ=Europe/Brussels, the configuration CI cannot cover: 300 captures across 3 pages (128/128/44), hasMore true then false, every capture exactly once in order, nothing replayed on a final poll.
  • Clipboard asserted directly in e2e rather than just the label flip, since the <pre> whitespace is the risk. Confirmed in the browser that the rendered prompt starts at Y with no leading newline.

Note on copy

The PRD names agents in the panel heading and the card, which departs from the house rule of labelling by format or action rather than by consumer. Followed as written since it is an explicit product decision, but flagging it so it is a deliberate call rather than a slip.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XkbFE6pgcxRfsMAvnwyuqS

The JSON API already returned everything an agent needs, but nothing
pointed at it, every call returned the newest 128 captures, and the safe
polling rate was unstated. Watching a webhook from a coding agent meant
hand-writing the integration.

The listing now takes an optional `since` and answers with a `cursor` and
`hasMore`. Echoing the cursor back is a promise that no capture is handed
over twice and none is skipped. Omitting it changes nothing, so the page
is untouched.

The ordering flips with the cursor, which is a correctness rule rather
than a preference. Under DESC with a full page, a burst larger than the
limit returns its newest captures, the caller's cursor advances past the
rest, and the backlog is never served. ASC hands out the oldest unseen
page and the next call resumes where it ended.

CreatedAt is now stamped in UTC. SQLite has no date type and this column
is compared and ordered as text, so a row carrying a +02:00 offset was
ranked against a UTC cursor by its digits rather than its instant, off by
the whole offset. Containers run UTC, so this was invisible in production
and in CI while being wrong on any developer machine that is not. It also
closes a latent misordering across a DST fall-back.

The endpoint page carries a prompt teaching the loop, built from the
request so a self-hosted instance points at itself, and quoting the rate
limit and retention window from the constants that enforce them rather
than restating them. The rate limit goes to 150 so a 2 second poll leaves
four fifths of the budget for everything else.

Claude-Session: https://claude.ai/code/session_01XkbFE6pgcxRfsMAvnwyuqS
@botre
botre merged commit 67a1af0 into master Aug 9, 2026
4 checks passed
@botre
botre deleted the feat/agent-cursor-api branch August 9, 2026 08:07
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.

1 participant