跳到主要内容

Loop 0.5.0 · 2026-10-31

文档对应最新 npm 发布版本,保留英文原文。

Streaming State

The frontend receives session updates through a same-origin EventSource. HTTP queries provide initial snapshots; SSE continues with activity, message, and tool events. There is no direct connection from the browser to the model service.

Loading and Subscribing

ChatWorkspace first requests /api/sessions/:id. After it succeeds, useSessionEvents connects to /api/sessions/:id/events?approvals=1 and passes frames to session-store. Components read the latest display snapshot instead of maintaining separate message histories. The query parameter explicitly enables the session's interactive approval adapter.

Frame type Display behavior
session.snapshot Initialize or completely replace current state
session.state Synchronize completion, model changes, or save state from a server snapshot
run.accepted Record the accepted requestId/runId and running state
loop.event Update drafts, completed messages, and tool cards by message position

loop.event preserves the native coding-agent event contract, including the model stream event inside message_update. The browser synchronously commits tool_execution_start updates to React so a burst of start/end events cannot skip the running icon's effect. Streaming message_update frames commit together on the next animation frame, or after 128 pending frames if paints are suspended. Every other frame drains earlier drafts first, so results, errors, approvals and run boundaries remain immediate. Disconnect drains pending frames before handing status to the sidebar; cleanup cancels scheduled work. Batch projection validates every cursor in order and rejects gaps atomically. This uses the existing brief success-icon hold rather than delaying SSE frames or tool execution. The projection replaces drafts with the cumulative message in the event. Do not append that cumulative text again or add the same final message to history twice. prompt_timing upserts state.promptTimings by userMessageIndex, so the duration row recovers the same timestamps on reconnect and stops before saving.

Reconnection and Deduplication

Drafts preserve the cumulative native assistant message at draftIndex, without a separate thinking/text/tool phase state. Ordinary phase updates, tool batch disclosures and final text render in history order. Native text deltas update cumulative text in place before session settlement; later calls do not relocate that text. Thinking remains in events and snapshots but is hidden from the conversation.

Each view records a streamId/seq cursor. Duplicate frames are ignored. A sequence gap or changed stream in incremental frames closes the connection and requests a full snapshot. Older snapshots behind the current cursor in the same stream are ignored.

After a network error, mark the view disconnected and reconnect with its cursor using exponential delays from 1.5 to 30 seconds. Messages and heartbeats reset the delay. Parse errors request a fresh snapshot. Existing content remains visible while disconnected, but normal sending and model switching are disabled. Reconnect guidance and its Retry button appear only after two seconds without a connection, with the grace period reset on session switches; browser offline guidance appears immediately. See the SSE service for replay limits.

Switching sessions closes only the previous view's EventSource. Backend sessions continue, and reopening synchronizes their latest results. If every interactive view disconnects, existing approvals remain pending until a decision, cancellation or session shutdown; reconnecting restores them. New disconnected requests fail closed. Session snapshots/state frames update the open conversation and its header title, including asynchronous title updates that preserve the current operation. See permissions and approvals.

Permission and approval submissions use the live SSE updates on success and request a fresh snapshot after a failed or uncertain HTTP result. The store then marks that session disconnected, clears its cursor and increments a local connection revision. The hook reconnects without replaying commands. This avoids replacing newer live state with an older HTTP response or interrupting a working connection after each decision.

Before disconnecting, useSessionEvents copies the last connected snapshot's generation status and title into the existing sidebar summary. It cancels older in-flight list requests before this handoff and invalidates the project list for a fresh server result. This preserves the spinner while switching sessions or reconnecting, without retaining a stale running snapshot after a background completion update. Callbacks from unmounted subscriptions are ignored.

List Synchronization

Session and list streams rebuild when the page returns from hidden to visible, a network-online event arrives, or a page is restored from the back/forward cache. Offline events close streams while preserving loaded content and drafts. A visible-page watchdog checks every 15 seconds and rebuilds connections after more than 45 seconds without a message or observable heartbeat. Late callbacks from closed connections are ignored. Session recovery reuses the cursor; unavailable replay falls back to a snapshot. Retry in the session status requests a fresh snapshot. These paths never resubmit commands. Browser connectivity signals cannot distinguish a sleeping computer from a disconnected tunnel; recovery waits until reachable.

AppShell mounts useListEvents once per page, connecting to /api/workspaces/events. This subscription stays open when switching sessions. The backend watches all loaded sessions, allowing background generation status and late titles to update every connected page without five-second polling. Project and archive changes use the same stream.

sessions.changed invalidates the affected project's session query and archived chats. projects.changed also invalidates the project list. lists.reset invalidates all three list caches. Active queries refetch; collapsed or unmounted lists remain stale until used. A 50ms window coalesces bursts of notifications; token deltas do not produce list events. This is change-driven batching, not a periodic refresh.

Before invalidating, the hook cancels older in-flight list requests using AbortSignal. Notifications arriving during a refresh are retained for another batch, preventing a stale response from swallowing a newer change. The shared recovery transport reconnects automatically, and every connection begins with lists.reset. Duplicate incremental frames are ignored; sequence gaps, changed stream identities or malformed JSON force a full list refresh. Unmount closes the connection and cancels pending batches.

The list stream does not change the selected conversation or discard unsent drafts on other pages. It covers this Web process's changes; external CLI or filesystem edits require a reload.

Boundaries

session-store is held only in memory; refreshing relies on the server for recovery. SSE reconnection does not resubmit prompts or guarantee exactly-once execution across server restarts. Chat input handles unconfirmed submissions separately.

Source and Tests