Files
PaperClipAI/doc/live-transcript-delivery.md
T
DottaandPaperclip 6cc765bd81 feat(live): deliver recoverable transcripts to interested views (#17456)
## Thinking Path

> - Paperclip should show live agent output without repeating
presentation work for every view.
> - Execution, acknowledgements and durable checkpoints must remain
independent of browser interest.
> - The server already owns authorization and redaction at delivery.
> - A shared browser connection can declare which visible runs need full
transcripts.
> - Catch-up must handle subscription races, disconnects, terminal
output and slow consumers.
> - This PR delivers the backend and UI contract together without a
database migration.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Visible transcript consumers receive recoverable output through one
shared browser connection.

**Current behavior**

Independent views can create duplicate connections, presentation work
and recurring history reads. A subscription race or filtered native
history can lose the saved tail.

**Proposed behavior**

Negotiate run interests, subscribe before catch-up and retain
independent canonical recovery cursors. Keep lightweight lifecycle and
attention events available without full transcript delivery.

PR 4 of #17097 → #17439 → #17455 → #17456. The runtime feature is
independent of hot-upgrade recovery; the stack order is for review
convenience.

## What Changed

- Add additive WebSocket capability negotiation, replacement interests,
revision acknowledgement and resynchronization notices.
- Reference-count visible consumer interests on one company-scoped
browser connection. Preserve older-client/server behavior.
- Reuse canonical structural/current-user/registered-secret redaction
for plugin run notifications through the same bounded delivery queue.
Keep producer and uninterested streams free of recipient lookups.
- Refresh application queries after the matching recovery
acknowledgement, including one follow-up after an older in-flight read.
Ordinary interest acknowledgements add no refresh.
- Keep authorization and redaction at delivery. Browser interest cannot
change harness execution, PRP ACKs, checkpoints, accounting or canonical
logs.
- Deduplicate by sequence and log identity. Catch up terminal output and
saved tails without recurring reads during healthy negotiated streaming.
- Bound delivery to 1,000 items and 1 MiB. Overflow requires
resynchronization; persistent congestion closes the connection.
- Preserve compact-history expansion from current master across
paginated catch-up. Snapshot the original read window before React can
defer its update.

## Verification

On current source `f6710f0b3c`, Linux [job
122604880503](https://github.com/paperclipai/paperclip/actions/runs/40624101891/job/122604880503)
executes all five run-read permission cases successfully (2.574
seconds), including grant and revocation on the same connection. Its
shard passes all 2,430 cases.

Current [server-shard job
122604880416](https://github.com/paperclipai/paperclip/actions/runs/40624101891/job/122604880416)
has 1,513 passes, one failure and three skips: an unchanged nested-clone
expiration/restore test exceeds its 15-second deadline. The preceding
`5efc6c48bf` source has identical non-document bytes; [its corresponding
job
122601359272](https://github.com/paperclipai/paperclip/actions/runs/40623017917/job/122601359272)
passes all 21 nested-clone cases, including that case in 9.505 seconds.
The exact timing cause is unproven. The single diagnostic [retry job
122609332597](https://github.com/paperclipai/paperclip/actions/runs/40624101891/job/122609332597)
passes all 21 nested-clone cases (61.316 seconds), including the
formerly timed-out case in 7.003 seconds, and all 1,514 shard cases
(three skips). The failure remains recorded; no timeout or assertion was
changed.

After the server squash merge and qualification retarget, all 50 focused
server cases and all 91 focused UI cases pass again, with both
typechecks and token gates. Runner vendor preparation rebuilds current
master inputs before those tests. This source needs fresh CI and
reviews; prior CI and browser receipts do not qualify it by inference.

Current candidate `f6710f0b3c` now targets master `6987c2f20b`, after
merged #17097, #17439 and #17455. Its diff contains only transcript
delivery and its documentation. It adds no database migration.
Retargeting changes only the evidence document; all production and test
code is byte-identical to `5efc6c48bf`.

On the eighth integration, all 50 focused server delivery cases and all
91 focused UI cases pass, with both typechecks and token gates. The
following 35-case receipt belongs to the earlier delivery correction.
Four low-queue authorization/decoration stall variants and the
late-rejection case fail before correction. They now verify the
five-second read deadline, old and negotiated clients, revisions that
cannot reset the deadline, unaffected sibling connections, rejected late
results, and timer cleanup. The first combined toast run has four
loopback EPERM failures and 31 passes; its permitted repeat passes all
35. Earlier 28-case evidence excluded the toast file.

The permission correction requires the current HTTP company-scope
transcript permission, plus task access, for both native events and
legacy logs. Prior Linux source `49a8783e9b`, job 122549913643, executes
all five run-read cases, including grant/revocation on the same
connection, successfully. That source placed this file in server shard
11; later shard assignments can differ and must be checked against their
actual inventories. The older `405bedd189` receipt retains a separate
scheduled-retry teardown race and inherited fixture collision; neither
is waived. The new head must pass again in Linux CI.

The seven extracted stream-recovery tests preserve their assertions. All
39 related legacy/native cases, UI typecheck and token gates pass after
restoring the initially missed fixture helper. Earlier full workspace
build/typecheck and Chrome journeys at `445a76563c` cover two views,
company switches, reconnect, terminal catch-up, reload and zero
recurring healthy transcript reads. Those browser receipts are
historical source evidence and do not qualify this new head by
inference. The [qualification
record](https://github.com/paperclipai/paperclip/blob/f6710f0b3c84c6fdb2021df67206261b446dfc97/doc/plans/2026-10-22-hot-upgrade-qualification.md)
retains exact earlier UI/history, source-boundary and cleanup failures
with their fixes and limits.

The workload-cost test preserves bounds and authorization callback
counts for zero interests, one viewer, multiple browsers and a slow
viewer. Its socket congestion is simulated. It measures neither SQL nor
network costs. Current permission checks and the final hosted
active-agent/viewer matrix remain unmeasured; no production savings are
claimed.

Fresh exact-head CI, legacy status, completed Greptile 5/5, resolved
feedback and ordinary merge eligibility remain required.

The earlier recipient corrections pass all 47
registry/plugin/delivery/WebSocket/toast cases and all 174 UI
provider/recovery/interest cases, both typechecks and token gates on
source `b202e59a25`. A real bus/host-service test registers a synthetic
run secret through the production registry and reproduces its raw
notification before the fix; native/legacy/final notifications now
redact it, disposal prevents late worker notification. A later P1 review
reproduces lost final notifications on settings-read and
secret-resolution failure. Both cases fail before correction. Any
delivery failure or subscription timeout now sends one content-free
stream error and removes the plugin subscription. It never changes the
agent execution or run status. Later events start no new redaction
reads. The final integrated six-file server lane passes all 50 cases,
with server typecheck and token gates. Existing browser delivery
behavior is unchanged. Storage and secret-provider seams are scripted.
Two real React Query/provider regressions first miss a run during
resync; both now refresh after the valid replacement ACK, including
pending-read and stale-scope cases. The prerequisite qualification batch
passes all 118 cases and E2E typecheck after master integration. Fresh
CI, security scan and reviews remain pending on this head.

The contribution-rule review correction removes 104 explanatory comments
introduced by this slice and keeps the ordering, deadline and
plugin-delivery explanations in the delivery document. Existing comments
and functional test/lint directives remain. All edited source files have
identical parsed non-comment tokens before and after the change. All 50
server delivery cases and all 91 cases in six affected UI suites pass,
along with token gates. The new head requires fresh CI, security and
review.

## Risks

A slow or disconnected viewer must resynchronize from authorized
canonical history. It may see a display gap until catch-up completes.
Company changes and revoked access must remove delivery interests. This
slice adds no database migration and cannot change execution or recovery
authority. Full transcript delivery evaluates current company-scope
permission per delivery. Active plugin recipients also perform canonical
redaction reads; viewer-load database costs on this final source remain
unmeasured.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, repository tools and code
execution. The runtime does not expose a more specific model ID or
context-window size.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-27 22:23:30 -05:00

181 lines
11 KiB
Markdown

# Interested transcript delivery
The company WebSocket is a presentation channel. Its subscriptions cannot change
runner execution, PRP acknowledgements, recovery checkpoints, accounting, or
canonical run logs. Authorization and redaction still run at delivery, including
after access is revoked. A subscription is never an authorization grant. Full event
rows and log chunks require the current `company_scope:read` permission used by
HTTP transcript reads, as well as access to the run's task. A restricted agent
can still receive authorized lifecycle and lightweight activity notifications.
The check runs for each full delivery on both negotiated and older connections;
changing a trust policy takes effect without reconnecting.
## Additive negotiation
Older clients receive the existing full event stream. New clients first send
`live.hello` with `version: 1` and `capabilities: ["run-interests"]`. A compatible
server replies with `live.capabilities` carrying the same version and capability.
An older server can ignore the hello; the client retains HTTP polling.
The browser then sends `live.interests`, a monotonically increasing `revision`,
and a replacement `runIds` array. The server applies that complete set before
replying with `live.subscribed` and the applied revision. The browser ignores
stale acknowledgements. Every connection starts a new revision sequence.
An unchanged run stays healthy while unrelated interests change. New interests
wait for an acknowledgement that includes them; only connection changes or an
explicit resynchronization advance the shared recovery generation. This avoids
aborting existing readers when another view starts or stops watching a run.
Views exceeding the 1,000-interest protocol limit retain HTTP fallback for
interests that cannot be advertised.
All mounted consumers share the existing company connection. Interests are
reference counted: removing one of two views of the same run does not unsubscribe
the other. Disabled views and hidden tabs withdraw their interests. Company and
account changes dispose the old connection. Progress, lifecycle, attention, and
activity notifications continue independently of transcript interests.
For an uninterested run, full `heartbeat.run.event` and `heartbeat.run.log`
payloads become an authorized `heartbeat.run.activity` notification containing
only run ID, agent ID, and last-output time. These notifications are coalesced to
at most one per run per second, driven by output rather than an idle timer.
## Catch-up and ordering
Consumers install their event listener before advertising interest. After the
latest subscription is acknowledged they read canonical history, merging it with
events that arrive during the read. Native events deduplicate by run sequence;
legacy logs also track log identity and persisted byte offsets. Streamed rows
never advance the persisted-history cursor. A second sequence gap observed during
an in-flight read remains pending until history covers it.
Compact conversation history intentionally excludes tool and reasoning rows.
After a complete filtered scan, its coverage includes the committed sequence known
before that read began, even when no matching conversation row has that sequence.
This settles excluded activity without recurring reads or downloading the full
event stream. A later notification cannot be covered by an older in-flight scan;
its catch-up must still recover any intervening conversation rows.
Native publication signals carry a committed sequence without a PRP body. They
trigger a canonical read through that sequence, including consecutive signals,
and never enter the transcript merge. A delayed signal cannot replace complete
text. Signals that arrive during an older read remain pending until history
covers them. This adds event-driven reads for payload-free notifications, not a
recurring poll or a recovery-checkpoint write.
Live catch-up refreshes only the affected run. Each run has at most one history
read in flight and one retry timer. Terminal notifications during a read coalesce
into one subsequent read. A busy run cannot cancel a sibling's paginated history.
This applies to both native commit signals and truncated legacy log chunks.
If subscription acknowledgement aborts Agent Detail's first history request,
negotiated recovery still drains the saved live tail without waiting for another
source event. Its first page initializes the manual history cursor; subsequent
catch-up pages use the independent recovery cursor. A completed run that was
never live retains manual pagination.
Agent Detail also refreshes its event history and shell log independently. A
signal for one cannot abort the other's page or advance its persisted cursor;
terminal notifications queue a follow-up for each active reader.
Catch-up runs on subscription, reconnect, show, explicit resynchronization, gaps,
and terminal status. Task transcript catch-up drains pending pages. Failed reads retry even
when the socket remains connected. After successful negotiated catch-up there
are no recurring transcript reads. Historical agent-detail logs retain their
existing explicit pagination. Agent Detail event history also reads at most one
1,000-row page automatically; a full page exposes **Load more events** and pauses
automatic history reads until the user requests the next page. Full live events
continue to arrive. Live retention keeps the opening 200 events (including the
invocation card) plus the latest 2,000, including through terminal transition.
Completed runs retain explicitly requested history. Initial and paginated read
failures remain visible, preserve loaded rows, and offer **Retry events**. Older clients and servers
retain fallback polling when no explicit event page is waiting.
## Backpressure
Each connection retains at most 1,000 queued items and 1 MiB across queued payloads
and socket buffering. Authorization that is still in flight counts toward the
queue. Interests are checked again after asynchronous authorization. Incoming
interest messages have a 64 KiB transport limit and at most 1,000 run IDs.
Overflow discards pending delivery work, invalidates in-flight results, and sends
`live.resync` with the affected revision. The browser replaces its interests and
catches up after the new acknowledgement. It also refreshes active application
queries because overflow can drop lifecycle messages. Persistent congestion
closes the socket with code 1013 after five seconds; this timer exists only while
congestion is pending. An older client is closed on overflow so its normal
reconnect path can catch up.
A new interest acknowledgement does not cancel the congestion deadline while an
old authorization or decoration read remains in flight. If that read settles,
the connection may recover; if it remains stalled, the original deadline closes
the socket even without another source event.
The server does not persist a browser-specific event backlog. Canonical history
is the recovery source. Execution continues whether there are zero viewers or
many viewers.
## Verification
The delivery tests cover legacy compatibility, authorization/redaction, revoked
access, interest changes during authorization, item and byte bounds, and the
congestion deadline. Browser-hook tests cover reference counting, acknowledgement
races, history/live ordering, failed reads, hide/show, company changes, terminal
catch-up, and zero recurring reads after negotiation.
Run the native browser journey with
`pnpm exec playwright test --config tests/e2e/playwright-transcript-delivery.config.ts`.
It uses the production runner and the existing deterministic Codex app-server
fixture. It creates a task through the current composer, opens two views, verifies
zero recurring transcript reads during settled streaming, changes companies,
disconnects the presentation socket, and checks terminal output after reload.
The fixture pins provider selection through the existing acceptance-only loader;
it does not contact a real model or add a production admission option.
The legacy journey is
`pnpm exec playwright test --config tests/e2e/playwright.config.ts agent-chat-sessions.spec.ts --grep 'chat first open'`.
Both use a disposable server and can use an explicitly supplied disposable
`DATABASE_URL` when local embedded Postgres is unavailable. Browser fixtures
establish delivery behavior, not native-provider or hosted-upgrade qualification.
`server/src/realtime/live-event-delivery.cost.test.ts` compares identical output
with zero interests, one viewer, three browser connections, and simulated socket
congestion. Set `PAPERCLIP_LIVE_DELIVERY_COST_REPORT` to retain its JSON report.
This measures the in-process presentation boundary, not production network or
database cost. CPU, heap deltas, and latency are diagnostic measurements, not
performance assertions. Current acceptance evidence lives in
`doc/plans/2026-10-22-hot-upgrade-qualification.md`.
Agent Detail's event log also treats notifications without a message or payload as
commit signals. It retains saved event details when a delayed signal arrives and
requests canonical history for a signal not yet covered by its persisted cursor,
including consecutive sequences. Complete live rows still merge directly without
recurring history reads. Both development and production page variants use this
behavior; their regressions fail on the preceding merge and pass after the fix.
## Recovery reads and plugin delivery
Resynchronization can also discard lifecycle events used by application lists.
Those views refresh after the replacement interest acknowledgement, when the
server has resumed delivery. An older in-flight query cannot satisfy that
boundary. One follow-up waits for it without cancelling the useful older read.
The follow-up checks the current company, connection, visibility and query cache
identity. Ordinary interest acknowledgements add no application refresh.
The five-second delivery deadline also covers an authorization, redaction or
decoration read that stalls before a queue reaches its bounds. Ping traffic and
interest revisions cannot reset that deadline. Closing disposes queued work and
rejects late results; the one already-started dependency read may still finish.
A legacy browser retains its existing skip-and-poll handling of a rejected read.
Transcript and activity events skip toast decoration and its business lookup.
Plugin run notifications share the bounded queue and canonical structural,
current-user and registered-secret redaction after their existing scope checks.
This work belongs to the active recipient, never the producer. Plugins do not
poll canonical history. Any failed delivery, backlog closure or subscription
timeout sends one content-free stream error and removes the subscription.
Disposal suppresses late notifications and new lookups. The error ends plugin
delivery without changing the underlying execution or recorded run status.
Explanatory code comments introduced by this slice are kept in this document,
as required by the contribution guide. Vitest environment and scoped lint
directives remain because they configure test execution and analysis.