Dosu LogoDosu Logo
Ask
Join our Discord
agentaPublic
Agenta
Documentsagenta
Agent Session Continuity
Agent Session Continuity
Type
Topic
Status
Published
Created
Aug 4, 2026
Updated
Aug 4, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

Agent Session Continuity#

Agent Session Continuity covers the client-side problem of keeping a browser view synchronized with an agent session it may not be driving. This is distinct from Session Pool Management (runner keep-alive pool, history fingerprinting, server-side eviction) and Sandbox Infrastructure (sandbox lifecycle states).

The central challenge: there is no push channel to browsers today. The runner publishes every event to a Redis stream, but the only consumer is the ingest worker that writes to Postgres — entries are deleted on ingest, so a second browser cannot subscribe to a run it is not driving. The only live stream is the driving browser's own /run response.

Four concrete failure modes were identified and partially addressed as of mid-2026:

  1. Second browser permanently stuck on a stale partial transcript — #5530 , fixed in PR #5589
  2. Refresh while paused on an approval gate loses the gate — #5542 , open
  3. One browser tab kills another's live stream — #5530, fixed in PR #5589
  4. Second open tab misses short runs entirely — #5624 , stopgap in PR #5629

Failure Modes#

Stale Partial Transcript After Refresh (Multi-Browser)#

The revalidate-on-open path in AgentChatSlice refetched durable records on mount but only adopted them if the server had more messages than the local copy. transcriptToMessages folds a paused turn and its resume into a single assistant message — only a done record closes a message. A turn that grows in place (tool results landing, an approval round-trip completing) therefore has the same message count from start to finish. The adoption guard concluded the server was not ahead and kept the stale partial copy — permanently, on every reload.

One Tab Killing Another's Live Stream#

Session history, open-tab list, active tab, and cached transcripts were all atomWithStorage atoms subscribed to the window storage event. Every write in one tab replaced those records live in every other tab of the same origin. The open-tab list drives the Ant Design Tabs items, so an incoming replacement could remove a tab and unmount its useChat stream mid-turn — the in-flight turn was lost. Switching browser tabs made it worse because both session queries revalidate on focus, and the reconcile that follows writes to all three keys.

Refresh While Paused on an Approval Gate#

Triggering a Terminal/Write approval gate and reloading the page caused the approval card to disappear entirely. The turn rendered as "No response — the agent ended its turn without answering", and there was no way to re-answer the gate. Only sending a brand-new message unblocked the session, which triggered a new tool call rather than resuming the parked one.

Second Tab Misses Short Runs Entirely#

For an already-mounted session, only two paths ever refetched the transcript: (1) mount (once per session tab), and (2) the remote-run poll, only while runningElsewhere is true. The poll had two sharp edges: the first fetch was scheduled with a setTimeout using REMOTE_RUN_POLL_MS (15 s), and the effect cleanup cancelled the pending timer the moment runningElsewhere flipped false. A run that ended before the next tick discarded the pending fetch with no final catch-up. For short turns (the common case), a second open tab converged never rather than late.

Fixes (PR #5589 and PR #5629)#

Record-Count Watermark Adoption Guard#

PR #5589 replaced message-count comparison with a record-count watermark. The durable record log is append-only and ordered, so "the server has more records than my transcript was built from" is an exact test.

loadSessionMessages now returns the record count it built from, and that watermark is persisted next to the cached messages. The adoption function shouldAdoptServerTranscript compares server record count against the stored watermark; both the cache-miss hydration path and the revalidate-on-open path now share the same rule. A message-count check is kept only as a floor (ingest lag can serve a snapshot shorter than what the browser renders, and that must never be traded down).

The watermark is cleared when a turn goes live locally — since the runner's log for that turn is unknown at that point, the next open re-syncs from the log. Messages and watermarks can only move together: one writer sets both, and a single dropSessionMessages helper is the only deletion path.

Per-Tab Storage#

The four session stores were switched from cross-tab synchronized atomWithStorage (with subscribe removed) to per-tab localStorage. Cross-tab awareness is correctly handled by the server reconcile on focus and renames pushed to the durable stream header — the storage-event push was a redundant second channel that replaced whole records instead of merging them.

Running-Elsewhere Signal and Polling#

liveness.ts maintains a project-scoped is_alive signal (Redis TTL, polled every 15 s, refetched on window focus). When runningElsewhere is true, useSessionHydration.ts polls the record log using chained timeouts (not setInterval — the log is large and backend-slow), with backoff from 15 s to 60 s while the log is quiet and a reset on real growth. A visual strip is shown above the composer.

PR #5629 added two stopgaps to useSessionHydration.ts:

  1. Immediate first tick — void poll() instead of setTimeout(poll, delay), so short runs that complete inside 15 s are not missed.
  2. Falling-edge fetch — a useEffect on the runningElsewhere → false transition fires one final guarded adoption to capture records written between the last tick and the run's end. A prevRemoteRunRef tracks both sessionId and running to avoid spurious fetches on session switches.

Remaining Gaps and Planned Work#

The stopgaps in PR #5629 close the short-run reproduction but do not eliminate the full-log poll cost. Three longer-term options were documented in issue #5624:

OptionCostWhat it solves
A. Lightweight count endpoint (GET /sessions/{id}/records/count)~1 dayTabs poll a single indexed COUNT instead of the full record log. Dissolves the request-budget objection and replaces the stopgap polling. Scheduled as next ticket.
B. Per-session pub/sub + SSE~1 weekPUBLISH each record batch to a per-session Redis channel alongside the existing XADD; expose GET /sessions/{id}/events as SSE. Enables near-real-time following from a second browser. Requires careful subscribe-before-backfill ordering to avoid the ~250 ms ingest-lag gap. Reverses an explicit budget decision.
C. BroadcastChannel (frontend only)HoursTab 1 posts "session X grew" to sibling tabs, which run guarded adoption. Same-browser only; safe because it triggers guarded adoption, not raw state replacement.

The recommended sequence is A now, B as a deliberate architecture decision, C optional.

The approval-gate-lost-on-refresh issue (#5542) is separately tracked and requires correct reconstruction of pause sentinels from interaction_request records on reload.

Key Files and References#

FilePurpose
web/oss/src/components/AgentChatSlice/hooks/useSessionHydration.tsTranscript hydration, adoption guard, remote-run poll, falling-edge fetch
web/oss/src/components/AgentChatSlice/state/liveness.tsProject-scoped is_alive signal; per-session running / alive derivation
web/oss/src/components/AgentChatSlice/assets/loadSession.tsReturns record count (watermark) alongside the loaded messages
web/oss/src/components/AgentChatSlice/assets/transcriptToMessages.tsFolds paused turn + resume into single assistant message — explains why message-count was wrong

The adoption rule (shouldAdoptServerTranscript) lives in @agenta/entities so it runs in CI unit tests; transcriptToMessages.test.ts pins the property that made message counts wrong.

Related issues and PRs:

  • PR #5589 — Record-count watermark, per-tab storage, running-elsewhere poll
  • PR #5629 — Immediate first poll tick, falling-edge fetch stopgap
  • Issue #5530 — Second browser stuck on stale transcript
  • Issue #5542 — Refresh while paused on approval gate
  • Issue #5624 — Second tab misses short runs
Documents
Agent and App Rename System
Agent Durable Storage
Agent Model Configuration and Validation
Agent Playground Streaming
Agent Runner Timeout Enforcement
Agent Session Continuity
App Management Interface
Authentication Access Controls
Chat Message Components
Client-Tool Registry and Render Hints
Codex Harness Integration
Credential and Secrets Management
Drive File Browsing
Evaluator Management UI
File Attachment Handling
Human-in-the-Loop Tool Approval
Jotai State Management
MCP Tool Execution
Model Provider Integration
Mount Archive Export
Object Store File Iteration
Organization Navigation and Switching
Passwordless Authentication
Playground Output Components
Playground Session Management
Playground Streaming and Content Block Support
Playground Tool Configuration
Playground Variant Architecture
Project Data Model
Redis Permission Caching
Runner Service Infrastructure
Sandbox Infrastructure
Sandbox Provider Configuration
Schema-Driven Elicitation
Self-Hosting Configuration
Session Pool Management
Skill Management
Tool Permission System
Tool Result Handling and Resume Flow
Workflow Reference Model
Workflow Run Context and Commit State
Workflow Slug System