Skip to content

feat(middleware): rework agent console buffer, previews, and cursors - #432

Merged
V3RON merged 1 commit into
mainfrom
feat/console-agent-buffer-and-anchors
Aug 20, 2026
Merged

feat(middleware): rework agent console buffer, previews, and cursors#432
V3RON merged 1 commit into
mainfrom
feat/console-agent-buffer-and-anchors

Conversation

@V3RON

@V3RON V3RON commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Description

Reworks the agent console domain so it holds up over a long session and renders the logs that actually matter.

Storage and reads. The per-device buffer was an array that splice(0, n)-ed on overflow, so every log past the 5000th shifted 5000 elements. It is now a real ring buffer, and an append costs the same whether it is empty or full. Reads used to filter, sort, copy, and reverse the entire buffer on every call — five passes over 5000 entries to return 50. A read now seeks straight to its start position and stops once the page is full, materialising only the rows it returns. The ring buffer is extracted as CircularBuffer<T>, a general collection with nothing about logs in it.

Rendering. Hermes sends a CDP object preview with every console.log, and the code was ignoring it — so console.log({ userId: 42 }) was stored as the single word Object. It now renders {userId: 42}, and arrays render their elements with an explicit when the runtime truncated the property list. Every rendered value is length-capped: Hermes applies no limit of its own, and a single logged string arrives whole (confirmed at 1MB over the wire). Each entry is also stored once instead of twice — text was the join of argsPreview and both were retained.

Cursors. getMessages items now carry a cursor, and the tool accepts before and after bounds. An agent can find an error under levels: ["error"] and then read the entries surrounding it in one follow-up call. A cursor is now an opaque position and nothing more.

Related Issue

Context

On dropping the cursor context. Cursors previously carried a filters hash and a sort order, and rejected any mismatch. That guard is what made "find the error, then read around it" impossible: a cursor from a filtered query could not be replayed against an unfiltered one. The guard also was not buying much — it protects offset pagination, where position 50 is meaningless if the filter changes underneath you, but these positions are absolute indices over an append-only log, so reusing one under a different query is well defined rather than corrupting. Removing it took a cursor from 143 characters to 6 (~96%), which matters now that every row carries one.

The last thing removed was the tool/device binding. A cursor from one device replayed against another now resolves in the target device's own position space and returns a well-formed but likely unintended slice, silently, rather than erroring. That is a real regression in diagnosability. It takes a deliberate mistake to hit and the damage is bounded, and 137 bytes on every row was a certain cost against an unlikely one — but it is a trade, not a free win. There is a test pinning the actual behaviour.

On cursor vs before/after. In the matching direction these are now the same code path and return identical bytes. They differ in that before/after select a side while order selects a direction, so {before, order: "asc"} reads the older side oldest-first, which cursor cannot express; and only the bounds can describe a two-sided range or survive paging inside one.

Rendering decisions, from captured device frames. An Error's preview repeats the entire stack in its stack property, so errors deliberately use description and skip the preview — reading it would store the stack twice. Hermes reports Map and Set as className: "Object" with no properties and no entries, indistinguishable from {}, so an empty preview falls back to the description rather than claiming the value is empty. NaN/Infinity/-0/BigInt arrive as unserializableValue but always with a description, so they already rendered correctly and are unchanged.

Breaking. argsPreview is removed from getMessages — it only ever repeated text and was never in the default projection. Cursors issued by an older session are not accepted by this version. Both are noted in the changeset.

hashFilters moved to pagination/filters-hash.ts; the React domain keeps its own cursor implementation and only borrowed that helper, and its semantics are untouched.

Testing

Automated:

  • pnpm typecheck:all — 63/63 packages
  • pnpm lint:all — 62/62 packages
  • pnpm format:all — clean for every file touched here (three pre-existing failures elsewhere in the repo were left alone)
  • pnpm --filter @rozenite/middleware test — 134 passed
  • pnpm --filter rozenite test — 113 passed
  • pnpm --filter @rozenite/agent-sdk test — 42 passed
  • pnpm --filter @rozenite/agent-shared test — 16 passed

New coverage: circular-buffer.test.ts (wraparound, capacity of one, evicted-index lookup, both walk directions, clear-then-refill), console-extract.test.ts (fixtures taken verbatim from real Runtime.consoleAPICalled frames off a Hermes device — object and array previews, overflow, errors, Map/Set, unserializable numbers, and each truncation cap), and eight cases in console-log-store.test.ts covering the anchor workflow, two-sided ranges, paging inside a bounded range, and cursor interchangeability.

Not verified on a device: the rendering changes are covered by captured frames rather than a live session, so a manual pass over the panel output is worth doing before release.

Make the agent `console` domain hold up on long sessions and render the
logs that actually matter.

Storage and reads:
- Replace the array-with-splice buffer with a real ring buffer, so an
  append costs the same whether it is empty or full instead of shifting
  5000 elements once the buffer fills.
- Rewrite the read path to seek to its start position and stop once the
  page is full, replacing a filter + sort + copy + reverse of the whole
  buffer on every call.
- Extract `CircularBuffer<T>` as a general, log-agnostic collection.

Rendering:
- Read the CDP object preview Hermes already sends, so `{ userId: 42 }`
  renders as `{userId: 42}` rather than `Object`, and arrays show their
  elements with an explicit overflow marker.
- Cap every rendered value; Hermes applies no length limit, so a single
  logged string can arrive at megabyte scale.
- Prefer an Error's description over its preview, which repeats the whole
  stack, and store each entry once instead of twice.

Cursors:
- Give every item a `cursor` and accept `before`/`after` bounds, so an
  agent can find an error under a filter and then read around it.
- Reduce a cursor to an opaque position, no longer bound to the filters
  or order of the request that produced it.

BREAKING CHANGE: `argsPreview` is removed from `getMessages` and cursors
from an older session are no longer accepted.

Claude-Session: https://claude.ai/code/session_01UQ7D5i9m3b8aMXhjLLkiSQ
@V3RON
V3RON merged commit 907ba2f into main Aug 20, 2026
4 checks passed
@V3RON
V3RON deleted the feat/console-agent-buffer-and-anchors branch August 20, 2026 09:36
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