Skip to content

feat(native-chat): serve the chat queue as its own bounded read - #26707

Draft
brennanb2025 wants to merge 3 commits into
brennanb2025/chat-queue-own-readfrom
brennanb2025/chat-queue-pages
Draft

brennanb2025 wants to merge 3 commits into
brennanb2025/chat-queue-own-readfrom
brennanb2025/chat-queue-pages

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 14 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1093 $\color{#cf222e}{\Huge{\mathbf{−}}}$​60 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1033
Prod 34 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1232 $\color{#cf222e}{\Huge{\mathbf{−}}}$​116 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1116

ELI5

When an agent is busy, messages sent to its chat wait in a queue. Today every time a phone or a paired computer opens or reconnects to that chat, the host sends it the full text of every waiting message, attached to the chat history. A long queue makes that reply huge. To keep the reply under the network size limit, the host refuses new messages once the waiting text adds up to 1 MiB ("Too much text is waiting").

This PR teaches the host a second way to hand out the queue: a small summary (how many cards, from whom, which one blocks) rides the chat instead of the full list, and a device that wants the cards asks for them a page at a time, with each card's full text fetched only when it is opened. Nothing uses the new way yet; the next PRs switch desktop and phone to it and then remove the 1 MiB refusal.

What Changed

Problem. The host treats its queue as one value: history replies and live chat frames carry queuedMessages, every waiting and returned card with its full body. So the size of the whole backlog rides every chat open and reconnect, and the host has to reject otherwise valid messages (the 1 MiB body-sum bound) to keep those frames under the 4 MiB remote limit.

User-facing change. None in this PR. Every existing desktop, phone and paired computer gets exactly the same frames as before (a test compares the serialized frames). The host now advertises a new capability, agent-session.queue-pages.v1, that no client uses yet.

Mechanism (host and shared contract only; no UI).

  • Capability. agent-session.queue-pages.v1 in protocol-version.ts. Its comment says that clients must check for it before sending the new request field, because older hosts reject unknown params. Person-typed queueing (agent-session.queued-messages.v1) stays off.
  • Opt-in summary. agentSession.history and agentSession.subscribe accept queueView: 'paged-v1'. Wherever a reply or frame would have attached queuedMessages, an opted-in reader gets queueSummary instead: generation, revision, total, counts by source (person / agent / unknown), the newest person card id, the returned card that blocks the queue, and whether Resume is available. The summary is at most 8 KiB and carries no bodies or id lists. queuePause and nextQueuedMessageId still ride as before.
  • agentSession.queuedMessagesPage. A keyset page of compact previews, optionally filtered by source, anchored by a cursor or by a message id ("around this card"). Each preview has at most 512 bytes of text, up to three sender labels, the hold, the returned reason and failure kind, and bodyBytes. Each page is built until the next row would push the complete RPC reply envelope over 512 KiB. If a cursor is from an old generation, or its card has moved or is gone, the reply is stale, never a misleadingly empty queue.
  • agentSession.queuedMessageRead. The exact stored body of one card. When the body is larger than the reply budget, it comes in ordered parts of the original JSON. Cards that have settled come back readable: gone, handed-off or state-changed, not an error.
  • Counting without loading bodies. Source counts and the newest person card come from SQLite expression indexes over the existing table. The indexes classify each card from the provenance it already stores (body.from), and unknown or malformed provenance never counts as a person. No column is added and there is no schema-version bump, so older builds still open the database. All queue reads share D1's single "readable row" condition.
  • Live updates. Opted-in subscribers get a new summary after each queue commit. While a reader's output is blocked, later changes collapse into one pending re-read of the latest state. Nothing is stored durably, and the pending re-read ends when the reader detaches.
  • Plumbing. The new methods sit behind the same structured-chat gates as history. They are on the mobile allowlist, and they reach paired and SSH-managed hosts through the existing generic routing, so the execution host always answers. Local IPC uses the same budgeted builders.

Why

The root cause of the queue limits is that the queue's whole contents travel with every chat read. The cure is to serve the queue as its own read, separate from the chat, and refresh it when it changes. This PR adds that read, plus a fixed byte budget for each reply, which a phone link needs. Desktop and phone switch to it in the next PR. After that, the last PR in this sequence removes the size refusal and stops publishing the full list.

Alternatives considered:

  • Shortening the cards inside the existing full list. The reply would still grow with the number of cards.
  • Raising the 1 MiB limit. That doesn't guarantee frame size.
  • Giving old clients an adapter that serves a partial list. That needs new "update this device" UI. Instead, the final PR treats un-updated paired devices like other devices without a needed chat capability: they don't show the host's native chats until they update.

Differences from the common pattern (the common pattern serves queued messages as a separate read, invalidated on change, not attached to every history frame):

  • INTENDED: pages and full-text reads have byte budgets and keyset cursors. The common pattern returns the whole queue in that separate read. Orca's remote links (phone end-to-end encrypted relay, paired computers) have fixed frame limits, so an unbounded separate read would move the same failure rather than remove it.
  • INTENDED: full text comes in lossless parts of the stored JSON rather than a truncated preview, so Edit can never lose text.
  • TEMPORARY: the full queuedMessages list is still published to readers that don't opt in, and the 1 MiB refusal still applies. Both are removed in the last PR of this sequence, after desktop and phone use the new read.

Linked Issue

N/A (maintainer). Stacked on #26651 (reads the queue by row headers).

Visual Proof

N/A: host and wire contract only. No client uses the new capability yet, and existing frames are unchanged (pinned by serialized-shape tests).

Testing

  • Targeted vitest (76 files, 825 tests) covers the new page, body and summary builders; full reply envelopes stay within budget, including emoji, escaping, long ids and huge metadata; walking cursors both ways reaches every row; reassembled bodies equal the stored body; stale cursors; "around" anchors; and a 10,000-card summary that query-plan checks and JSON.parse spies show loads no bodies. They also cover subscriber coalescing and idle release; byte-identical legacy frames; RPC gates, strict params and the mobile allowlist; paired/SSH routing; and the D1 and existing queue suites.

  • orca-ci-checks (lint, code quality, localization, typecheck): 23/23.

  • Cross-version wire test: CI runs it. Locally, extracting the release checkout timed out twice before any assertion ran, so it is not counted as passing here.

  • I manually tested these changes locally (no UI or client change in this PR)

  • Automated tests added/updated, or explained why not below

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Security: the new methods sit behind the same structured-chat and mobile gates as history, and their params are strict.
  • Cross-platform: SQLite and TypeScript only.
  • Remote SSH: the execution host answers through the existing routing. Losing contact with the host is never treated as a verdict.
  • Mobile: methods are allowlisted, and frames are unchanged until the client opts in.
  • Backwards compatibility: the request field is optional, and old clients get the same frames. Indexes are additive and created with IF NOT EXISTS, and their builds are best-effort.
  • Performance: summaries and counts are indexed and never load bodies. Each page reads only the bounded rows it returns.

Author: @BrennanKB5

Hosts advertise agent-session.queue-pages.v1 and serve the queue as its own
read: a summary on history/subscribe frames for readers that opt in with
queueView 'paged-v1', plus byte-bounded queuedMessagesPage and
queuedMessageRead methods. Shipped readers keep the full v1 list.
…t-queue-pages

# Conflicts:
#	src/main/native-chat/agent-session-journal/queued-message-schema.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
#	src/shared/protocol-version.ts

This branch has not been deployed

No deployments
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