Routa Session Lifecycle and Reconnection Flow: Managing ACP Sessions Across Network Interruptions

Routa implements a seven-phase session lifecycle that persists Agent Communication Protocol (ACP) state from in-memory execution through permanent storage, while Server-Sent Events automatically reconnect and preserve message continuity without losing user input.

The phodal/routa repository treats the session as the fundamental unit of work for AI interactions, maintaining state both in-memory via the ACP manager and in persistent storage. Understanding the complete session lifecycle and reconnection flow helps developers build resilient applications that survive network interruptions while maintaining consistent transcript history and tool-call boundaries.

Session Lifecycle Phases

Routa manages sessions through a structured lifecycle that spans creation, execution, persistence, and eventual deletion. Each phase transitions through specific REST endpoints while the SSE stream provides real-time updates.

Create

A new session begins when the provider-specific run API instantiates an ACP session through the manager. The system generates a UUID for the session ID and inserts a row into acp_session_store. On the frontend, the UI issues a POST request via desktopAwareFetch to an endpoint such as /api/acp, receives a {sessionId} payload, and stores it in the client state.

Run and Update

While the session executes, the ACP manager streams notification events containing user messages, assistant messages, and tool calls. The manager simultaneously records a trace file under the workspace for audit purposes. The client opens an SSE channel using new EventSource(resolveApiPath(\/api/acp?sessionId=${id}`)), where each messageevent contains a JSON record merged into the local transcript throughhistory_to_transcript_messagesandtraces_to_transcript_messages. The backend serves history via GET /api/sessions/{sessionId}/history`, checking in-memory state first before falling back to the database.

Persist

When the session stops or the user disconnects, the manager flushes in-memory history to the database. Calling POST /api/sessions/{sessionId}/disconnect triggers the backend to first execute get_session_history, write it using acp_session_store.save_history, and then terminate the process via acp_manager.kill_session. This ensures no conversation state is lost when the connection closes.

Fork and Clone

Running or completed sessions can spawn child sessions through the fork operation. The POST /api/sessions/{sessionId}/fork endpoint creates a fresh session that inherits the provider, workspace, and current working directory from its parent. The UI posts the request, receives a new sessionId, and navigates to the new session page while maintaining the hierarchical relationship.

Rename

Users can update session metadata without interrupting execution. The PATCH /api/sessions/{sessionId} endpoint updates the name in both locations: the manager updates the in-memory map through acp_manager.rename_session, while the database layer executes acp_session_store.rename to persist the change.

Delete

Complete removal requires terminating the in-memory entry and database record. The DELETE /api/sessions/{sessionId} endpoint invokes acp_manager.delete_session for the running state and acp_session_store.delete for the persistent row, freeing all associated resources.

Retrieve

Clients reconstruct the session view through several read endpoints. GET /api/sessions/{sessionId} returns metadata, /api/sessions/{sessionId}/transcript provides the canonical transcript combining history and trace data, and /api/sessions/{sessionId}/context exposes the hierarchical structure including parents, children, and siblings. The UI fetches these using desktopAwareFetch and renders them through components like session-panel and session-context-panel.

Reconnection Flow and SSE Resilience

Routa's frontend handles network instability through a combination of native EventSource capabilities and explicit cache invalidation logic implemented in hooks like useChatMessages and useKanbanEvents.

Opening the Stream

The client initializes the live connection by creating an EventSource pointing to resolveApiPath('/api/acp?sessionId=${id}'). This establishes a persistent HTTP connection that the server uses to push notification events as the ACP manager generates them.

Automatic Retry Mechanism

The native EventSource implementation automatically attempts to reconnect following any network interruption. This behavior requires no custom retry logic, as the browser manages the exponential backoff and connection attempts according to the SSE specification.

Client-Side Cache Invalidation

Upon the onopen event firing after a reconnection, the hook invalidates the React Query cache via queryClient.invalidateQueries(['session', sessionId]). This pattern is verified in src/client/hooks/__tests__/use-kanban-events.test.tsx through the test case "invalidates when the SSE connection reconnects after the first connect", ensuring the UI fetches fresh state rather than displaying stale data cached before the interruption.

Message Continuity Across Reconnects

The server maintains session state in the ACP manager during brief disconnections. Pending prompts that were not yet transmitted are re-queued upon reconnection, as indicated by the implementation comment // Re-store pending text so the pending-prompt effect can re-send after reconnect. This guarantees no user-typed input is lost. Additionally, the stream logic ensures tool-call chunks are never merged across a reconnect boundary, preventing stale chunks from attaching to new tool invocations—a behavior validated in use-chat-messages.test.tsx.

Implementation Details and Code Examples

The following patterns demonstrate how to interact with the session lifecycle and SSE reconnection flow:

// Start a new session (client side)
const { data } = await desktopAwareFetch(
  resolveApiPath('/api/acp'),
  { method: 'POST', body: JSON.stringify(payload) }
);
const sessionId = data.sessionId;

// Subscribe to live updates with reconnection handling
const source = new EventSource(
  resolveApiPath(`/api/acp?sessionId=${sessionId}`)
);
source.onmessage = ev => {
  const notif = JSON.parse(ev.data);
  // Merge into transcript via history_to_transcript_messages()
};
source.onopen = () => queryClient.invalidateQueries(['session', sessionId]);

// Disconnect and persist history
await desktopAwareFetch(
  resolveApiPath(`/api/sessions/${sessionId}/disconnect`),
  { method: 'POST' }
);

// Fork an existing session
const forkRes = await desktopAwareFetch(
  resolveApiPath(`/api/sessions/${sessionId}/fork`),
  { method: 'POST' }
);
const newSessionId = forkRes.sessionId;

Key implementation files include:

Summary

  • Routa sessions exist both in the ACP manager's memory and in persistent storage, with UUID-based identification.
  • The seven-phase lifecycle covers creation, execution, persistence, forking, renaming, deletion, and retrieval through specific REST endpoints.
  • SSE streams provide real-time updates via EventSource, with automatic browser-managed reconnection.
  • Cache invalidation on reconnect ensures the UI displays current server state rather than stale data.
  • Message continuity is preserved through re-queuing pending prompts and protecting tool-call boundaries across reconnections.

Frequently Asked Questions

What happens to unsent user messages during a network reconnection?

The Routa frontend maintains pending text in a re-queue buffer. When the SSE connection reopens after a network interruption, the pending-prompt effect automatically re-sends any unsent input, ensuring no user messages are lost during transient disconnections.

How does Routa persist session history when a user clicks disconnect?

The disconnect flow in crates/routa-server/src/application/sessions.rs first retrieves the complete in-memory history via get_session_history, persists it to the database using acp_session_store.save_history, and then terminates the process with acp_manager.kill_session, guaranteeing all conversation data is written before resources are released.

What is the difference between forking and renaming a session?

Forking creates a new child session via POST /api/sessions/{sessionId}/fork that inherits the parent's configuration but receives a new UUID and starts fresh execution. Renaming uses PATCH /api/sessions/{sessionId} to update only the display name through acp_manager.rename_session without affecting execution state or creating a new session entity.

How does Routa prevent corrupted tool calls across SSE reconnections?

The stream processing logic in hooks like useChatMessages ensures that partial tool-call chunks are never merged across reconnection boundaries. This prevents a stale chunk from the previous connection from being incorrectly attached to a new tool invocation after the stream reconnects, maintaining the integrity of the assistant's tool-use boundaries.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →