Let an agent poll an endpoint with a cursor - #47
Merged
Conversation
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
This was referenced Aug 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements
PRD-agent-polling.mdand 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/requeststakes an optionalsinceand answers withcursorandhasMore. Echoing the cursor back is a promise that no capture is handed over twice and none is skipped. Omitting it changes nothing, sopublic/endpoint.jsis untouched.A malformed
sinceis a400rather 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
DESCwith 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.ASChands 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.
CreatedAtwas stamped withtime.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:00row is ranked against a UTC cursor by its digits rather than its instant: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.CreateRequestnow 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.
productionRequestsPerMinutegoes to 150, so the recommended 2 second poll (30/min) leaves four fifths of the budget for the traffic under test.PRODUCT.mdnamed the old figure and is updated.Homepage use-cases grid becomes 2x2 with the fourth card from PRD §5.4.
docs/api.mdis 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 thenpm run cssstaleness check all pass. 64/64 Playwright tests pass, including 5 new ones for the panel.ASCback toDESCmakes it fail with "the drain is not converging".TZ=Europe/Brussels, the configuration CI cannot cover: 300 captures across 3 pages (128/128/44),hasMoretrue then false, every capture exactly once in order, nothing replayed on a final poll.<pre>whitespace is the risk. Confirmed in the browser that the rendered prompt starts atYwith 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