Skip to content

Add opt-in message pagination to remote chat HTTP API#498

Merged
ericdallo merged 3 commits into
editor-code-assistant:masterfrom
rschmukler:rs/http-endpoint-optimization
Jun 13, 2026
Merged

Add opt-in message pagination to remote chat HTTP API#498
ericdallo merged 3 commits into
editor-code-assistant:masterfrom
rschmukler:rs/http-endpoint-optimization

Conversation

@rschmukler

@rschmukler rschmukler commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

GET /api/v1/chats/:id accepts limit/before/after query params with opaque cursors and an after=lastCompaction sentinel, returning a messagesMeta object with before/after/compaction cursors.

Add opt-in pagination via limit/before/after query params. When none are
present the response is unchanged, so existing clients keep working; the
new behavior is purely additive (a sibling messagesMeta object alongside
the existing messages array), which the object-shaped response already
accommodates without a breaking change.

Cursors are opaque base64url tokens encoding <index>.<checksum>, where
the checksum fingerprints the message. Resolution trusts the encoded
index when its checksum still matches, otherwise rescans by checksum, so
cursors survive mid-history shifts (e.g. flag insert/remove) and only a
genuinely removed message (rollback/clear) yields 409 cursor_expired,
prompting the client to refetch. Token pruning rewrites content in place
and never shifts positions, so the active window stays stable.

Pages are newest-anchored so opening a chat shows the latest context,
with before/after cursors computed against full history so paging
transparently crosses the compaction boundary. The after=lastCompaction
sentinel returns the active context since the last compaction in a single
call, fulfilling the original "messages since last compaction" goal
without a dedicated flag; before=lastCompaction returns the summarized
history. Cursors are nil at the ends (nil-punning) instead of separate
hasMore/hasBefore fields.

Request examples

# Newest 50 messages (with pagination metadata)
GET /api/v1/chats/abc123?limit=50

# Active context since the last compaction
GET /api/v1/chats/abc123?after=lastCompaction&limit=50

# Older page (scroll up) — use beforeCursor from a previous response
GET /api/v1/chats/abc123?before=eyJpZHgiOjEyfQ&limit=50

# Newer page (scroll down) — use afterCursor from a previous response
GET /api/v1/chats/abc123?after=eyJpZHgiOjYyfQ&limit=50

# The summarized-away history (before the last compaction)
GET /api/v1/chats/abc123?before=lastCompaction

Response shape

{
  "id": "abc123",
  "title": "My chat",
  "status": "idle",
  "messages": [ /* ...the page... */ ],
  "messagesMeta": {
    "total": 412,                  // total messages in full history
    "returned": 50,                // messages in this page
    "beforeCursor": "eyJpZHgiOjEyfQ", // pass as `before` to load older (null at start)
    "afterCursor": null,           // pass as `after` to load newer (null at tail)
    "compactionCursor": "eyJpZHgiOjUwfQ" // boundary of last compaction (null if never compacted)
  }
  // ...model, agent, variant, pendingToolCalls, etc.
}

With no limit/before/after params, the response omits messagesMeta and returns the full messages array (unchanged).

A stale cursor returns:

// 409 Conflict
{ "error": { "code": "cursor_expired", "message": "..." } }
  • I added a entry in changelog under unreleased section.
  • This is not an AI slop.

Comment thread docs/config/remote.md Outdated

**Multi-session** — You can connect to multiple ECA instances simultaneously. Each connection appears as a tab in the top bar.

## HTTP API: message pagination

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to add any of this, this doc is for ECA users, not a explanation how ECA clients remote connection work, more to teach how users can use ECA remote feature.

GET /api/v1/chats/:id accepts limit/before/after query params with
opaque cursors and an after=lastCompaction sentinel, returning a
messagesMeta object with before/after/compaction cursors.
Fetching a chat's history previously had no pull API on the editor
(JSON-RPC) side: chat/open always replayed the entire history as a
stream, and the only windowed access lived on the remote HTTP endpoint.
For long chats that means streaming tens of thousands of messages on
every open, and the optimization was reachable only by the web client.

Unify both transports around one transport-agnostic windowing core
(eca.features.chat.history) that operates on raw persisted messages:
opaque base64url "index.checksum" cursors that resolve by index when the
checksum still matches and rescan by checksum otherwise (tolerating
flag-insert/remove shifts), a lastCompaction sentinel, and a
compaction-cursor. Pages are newest-anchored so opening a chat shows the
latest context; an opaque after cursor pages forward instead. before/
after cursors are computed against full history so paging transparently
crosses the compaction boundary, and are nil at the ends.

On top of the core:

- New JSON-RPC chat/history request returns a window inline as
  {contents, meta} where contents are the same ChatContent items
  chat/contentReceived streams, so editors reuse their renderer. Inline
  (not streamed) so the client can place an older page deterministically
  while live updates keep arriving on the stream.
- chat/open gains optional limit/before/after to replay only a window and
  return meta; no params replays the full history as before.
- Remote HTTP GET /api/v1/chats/:id keeps its opt-in query-param
  pagination, now backed by the shared core.

To feed both the stream and the inline response from one place, the
message->wire-content transform (including subagent expansion) is lifted
out of send-chat-contents! into a pure messages->contents collector, so
replay behavior is unchanged.
@rschmukler rschmukler force-pushed the rs/http-endpoint-optimization branch from b1570cc to 0689f7f Compare June 12, 2026 22:18

@ericdallo ericdallo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good, also I checked with eca-emacs if it will fit good and looks good too.
I'll make the changes in editor clients starting with emacs

@ericdallo ericdallo merged commit b986a52 into editor-code-assistant:master Jun 13, 2026
7 checks passed
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