How the Two-Phase Coordinated Update Prevents Session Disruption in Prime Agent

Prime Agent's two-phase coordinated update prevents session disruption by locking the session state, draining pending tool-result streams, applying atomic mutations, and releasing the lock only after the commit completes, ensuring users never see partial updates or lost messages.

In Prime Intellect's Prime Agent, the session state is shared between the daemon (which manages LLM inference) and the terminal UI front-end. When users trigger disruptive actions—such as switching conversation branches, editing prompts, or loading new tools—the system employs a two-phase coordinated update protocol to maintain session continuity without visible glitches or message loss.

The Challenge: Shared State Modifications

The Prime Agent architecture splits responsibilities between the back-end daemon (packages/coding-agent) and the front-end TUI (packages/tui). Since both components access the same session tree concurrently, abrupt state changes risk leaving the UI in an inconsistent state or losing in-flight tool results. The two-phase protocol solves this by treating session updates as atomic transactions.

Phase 1: Prepare (Lock-and-Drain)

The first phase establishes a safety perimeter around the session state, ensuring no new operations interfere while the update is being staged.

Acquiring the Update Lock

When the UI initiates a change, it sends a session_update_start message to the daemon. The daemon responds by setting the SessionManager.updateLock flag to true. According to the source code in [packages/coding-agent/src/core/session-manager.ts](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/coding-agent/src/core/session-manager.ts) (lines 48-55), this boolean flag gates all new input, preventing concurrent writers from corrupting the session log during the update.

// Simplified logic from SessionManager
private updateLock = false;

async handleMessage(msg: DaemonMessage) {
  if (msg.type === "session_update_start") {
    this.updateLock = true;
    await this.drainPendingStreams();
  }
}

Draining Pending Streams

With the lock held, the daemon stops accepting new user messages and begins draining any pending tool-result streams. Workers are notified via Worker.postMessage({ type: "prepareUpdate" }), allowing them to finish outstanding tool calls before acknowledging. As implemented in [packages/coding-agent/src/worker/worker.ts](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/coding-agent/src/worker/worker.ts) (lines 210-225), this ensures no partial responses remain hanging when the mutation occurs.

Phase 2: Apply (Commit-and-Resume)

Once the session is quiescent and all workers have acknowledged the preparation phase, the system applies the actual mutation atomically.

Atomic Session Mutation

The UI transmits the concrete payload—such as a new branch ID or edited prompt—via a session_update message. The daemon applies this change inside SessionManager.applyUpdate(), located at lines 102-118 of [packages/coding-agent/src/core/session-manager.ts](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/coding-agent/src/core/session-manager.ts). Because the update lock is still held during this operation, the session tree cannot be observed in an inconsistent intermediate state.

case "session_update":
  if (!this.updateLock) throw new Error("Lock not held");
  this.applyUpdate(msg.payload); // Atomic tree mutation
  break;

Resuming Normal Operations

After the mutation succeeds, the daemon releases the lock and emits session_update_done. This signal tells the UI that the session tree is in a consistent state and normal message flow can resume. The front-end only renders the updated session after receiving this confirmation, eliminating flicker or loss of messages.

Implementation: Coordinating UI and Daemon

The complete handshake spans both the front-end TUI and the back-end daemon. Here is how a branch switch is implemented in [packages/tui/src/tui.ts](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/tui/src/tui.ts):

async function switchBranch(newBranch: string) {
  // Phase 1 – tell daemon we are about to change state
  await daemon.send({ type: "session_update_start" });

  // Phase 2 – deliver the actual mutation
  await daemon.send({
    type: "session_update",
    payload: { branchId: newBranch },
  });

  // Phase 3 – release the lock so normal traffic resumes
  await daemon.send({ type: "session_update_done" });
}

On the daemon side, the SessionManager implements the corresponding handler logic:

class SessionManager {
  private updateLock = false;

  async handleMessage(msg: DaemonMessage) {
    switch (msg.type) {
      case "session_update_start":
        this.updateLock = true;
        await this.drainPendingStreams();
        break;
      case "session_update":
        if (!this.updateLock) throw new Error("Lock not held");
        this.applyUpdate(msg.payload);
        break;
      case "session_update_done":
        this.updateLock = false;
        this.resumeIncomingMessages();
        break;
    }
  }
}

Summary

  • Lock-and-Drain: The session_update_start message triggers SessionManager.updateLock, pausing new input and draining pending tool-result streams to prevent partial states.
  • Atomic Commit: SessionManager.applyUpdate() executes the mutation while the lock is held, ensuring the session tree transitions directly from one valid state to another.
  • Coordinated Resume: The session_update_done signal guarantees the UI only renders complete snapshots, eliminating flicker and message loss during branch switches or prompt edits.
  • Worker Coordination: Background workers in packages/coding-agent/src/worker/worker.ts finish in-flight operations before acknowledging the prepare phase, protecting in-progress tool calls.

Frequently Asked Questions

What happens if a tool call is still running when an update starts?

The daemon sends a "prepareUpdate" message to all workers via Worker.postMessage(). As implemented in [packages/coding-agent/src/worker/worker.ts](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/coding-agent/src/worker/worker.ts) (lines 210-225), workers complete their outstanding tool calls and stream the final results back before acknowledging. The update only proceeds after all pending streams are drained, ensuring no tool results are lost.

How does the UI know when it is safe to resume rendering?

The UI waits for the session_update_done message from the daemon. This signal confirms that SessionManager.applyUpdate() has completed and the updateLock has been released. Only then does the front-end resume normal message flow and render the updated session tree.

Can the session tree be observed in an inconsistent state during updates?

No. The updateLock boolean flag guarantees that SessionManager.applyUpdate() runs exclusively while the lock is held. Since new input is paused and workers have drained their streams, there are no concurrent readers or writers during the atomic mutation. The UI only accesses the session after receiving the completion signal.

Why is a two-phase protocol necessary instead of a single message?

A single message would risk applying changes while tool results are still streaming or while the UI is rendering, causing visible flicker or lost messages. The two-phase coordinated update separates the "prepare" (lock-and-drain) from the "commit" (apply-and-resume), ensuring the session remains valid and complete throughout the transition.

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 →