# WebSocket Transcript Protocol Reconnection and Sync in Kimi Code

> Discover how the Kimi Code WebSocket transcript protocol uses exponential backoff and cursor-based replay for reliable reconnection and sync, ensuring seamless real-time data streams.

- Repository: [Moonshot AI/kimi-code](https://github.com/MoonshotAI/kimi-code)
- Tags: internals
- Published: 2026-07-25

---

**The Kimi Code WebSocket transcript protocol implements an exponential backoff reconnection strategy with cursor-based replay, allowing clients to resume real-time operation streams from the last known batch watermark or trigger a full REST resync when the server-side journal expires.**

The MoonshotAI/kimi-code repository delivers real-time transcript streams over the `/api/v1/ws` WebSocket endpoint using a dedicated control channel that guarantees durability across network interruptions. This system separates volatile transcript operations from durable session events, ensuring clients can automatically recover from dropped connections without losing critical state. Understanding how this WebSocket transcript protocol handles reconnection and synchronization reveals a robust architecture designed for production-grade real-time collaboration.

## Client-Side Reconnection Flow with Exponential Backoff

The client implementation in [`apps/kimi-inspect/src/transcript/ws.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/apps/kimi-inspect/src/transcript/ws.ts) manages connection lifecycle through a stateful wrapper that detects failures and orchestrates recovery.

### Detecting Connection Drops and Scheduling Reconnect

When the underlying socket closes, the `TranscriptWs` class clears its internal `this.ws` reference and invokes `scheduleReconnect()` unless the closure was intentional. The reconnection strategy uses **exponential backoff** starting at `500 ms` and doubling with each attempt, capping at `10_000 ms` (10 seconds). This calculation appears in the source as:

```typescript
delay = Math.min(this.reconnectDelayMs * 2 ** (this.reconnectAttempt - 1), 10_000)

```

Once the delay elapses, the `connect()` method instantiates a fresh `WebSocket` using `new this.WsCtor(this.wsUrl, protocols)` and reattaches all event listeners, creating a completely new transport while preserving the client's session context.

### Handshake and Subscription Resumption

Upon establishing a new socket, the client immediately transmits a `client_hello` control frame containing the `client_id` and desired `subscriptions`. This is followed by a critical `subscribe_v2` frame that carries two key pieces of state:

- **Transcript grade map**: Specifies granularity levels (e.g., `{agentId: 'block'}` or `'delta'`) for each agent
- **Cursor watermark**: Optional `transcript_since` parameter reflecting the latest operation batch sequence known to the client

The client then awaits an `ack` control frame matching the `subscribe_v2` message ID. The source code checks this condition with `if (!this.subscribeV2Acked && frame.id === this.subscribeV2Id)`, firing `onReconnected()` only after server confirmation. This ensures the transcript stream is fully attached before the client processes new operations.

## Server-Side Replay and Resync Logic

The server implementation in [`packages/kap-server/src/transport/ws/v1/wsConnectionV1.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/packages/kap-server/src/transport/ws/v1/wsConnectionV1.ts) handles reconnection through serialized control processing and optional operation replay.

### Control Frame Serialization and Session Attachment

All incoming control frames enter a `controlQueue` to guarantee ordering during high-concurrency scenarios. When processing `client_hello`, the server validates tokens and records the handshake via `onClientHello()` before forwarding to `attachSession()`.

The `subscribe_v2` handler parses the payload using `transcriptSubscribeV2PayloadSchema`, extracting the `session_id`, transcript grades, and optional `transcript_since` cursor. The `attachSession()` method then registers the connection with the `SessionEventBroadcaster`, passing the cursor to the replay subsystem if present.

### Operation Replay and Fallback Mechanisms

The `replay()` method attempts to satisfy the reconnection request by calling `broadcaster.getBufferedSince()` to retrieve missed operations from the in-memory journal. Two outcomes are possible:

- **Successful replay**: If the requested cursor range exists in the journal, the server streams the buffered operations followed by a transcript seed (baseline) via `flushTranscriptSeed()`, then sends an `ack` with status `0`
- **Journal gap**: If the cursor predates the journal's retention window, the server emits a `resync_required` control frame containing the `session_id`, forcing the client to perform a full REST transcript fetch

This fallback mechanism ensures clients never receive stale or partial state when the ephemeral operation buffer cannot satisfy the request.

## Synchronization Guarantees and Durability

The protocol provides distinct durability semantics for different event types:

- **Durable ordering**: Non-volatile events (session events, agent events) travel through the standard `subscribe` path with guaranteed delivery of every envelope in order
- **Volatile transcript ops**: Ephemeral operations reside only in an in-memory journal with limited `TRANSCRIPT_OPS_JOURNAL_CAPACITY`. Reconnection replay succeeds only while the cursor remains within this window
- **Grade preservation**: The `subscribe_v2` channel exclusively carries transcript grade specifications, ensuring granularity preferences persist across re-attachments without accidental fallback to default settings
- **Idempotent unsubscription**: The `unsubscribe_v2` frame removes specific transcript streams without affecting legacy event subscriptions, allowing granular control over data flow

## Implementation Example

The following client-side instantiation demonstrates automatic reconnection with cursor tracking:

```typescript
import { TranscriptWs } from './transcript/ws';

const ws = new TranscriptWs({
  url: 'http://localhost:58627',
  token: 'YOUR_BEARER_TOKEN',
  sessionId: 's123',
  agentId: 'main',
  getSince: () => transcriptStore.latestSeq(),
  handlers: {
    onOps: (agent, ops, meta) => store.appendOps(agent, ops, meta),
    onResyncRequired: () => fetchFullTranscript(),
    onReconnected: () => console.log('Transcript stream re‑attached'),
  },
});

```

On the server, the subscription handling logic manages replay and acknowledgment:

```typescript
private async onSubscribeV2(frame: InboundFrame): Promise<void> {
  const parsed = transcriptSubscribeV2PayloadSchema.safeParse(frame.payload ?? {});
  if (!parsed.success) {
    this.sendFrame(buildAck(frame.id ?? '', 1, 'invalid subscribe_v2 payload', {}));
    return;
  }
  const sid = parsed.data.session_id;

  await this.attachSession(
    sid,
    undefined,
    this.subscriptions.get(sid)?.agentFilter,
    parsed.data.transcript,
    parsed.data.transcript_since,
    { accepted: [], resyncRequired: [], serverCursors: {} },
  );

  this.sendFrame(buildAck(frame.id ?? '', 0, 'success', { 
    accepted: [], 
    not_found: [], 
    resync_required: [], 
    cursors: {} 
  }));
}

```

## Summary

- **Exponential backoff reconnection**: Client-side logic implements a capped exponential backoff starting at 500ms to prevent thundering herd scenarios while minimizing downtime
- **Cursor-based resumption**: The `transcript_since` parameter allows clients to request replay of specific operation batches from the server's in-memory journal
- **Graceful degradation**: When the journal cannot satisfy a cursor request, the `resync_required` frame triggers a full REST fetch, ensuring consistency at the cost of bandwidth
- **Control channel isolation**: The `subscribe_v2` protocol separates transcript grades from durable event subscriptions, maintaining granularity preferences across reconnections
- **Serialized processing**: Server-side control frame queuing guarantees ordered handling of `client_hello` and `subscribe_v2` messages during concurrent reconnections

## Frequently Asked Questions

### What happens if the client's transcript_since cursor is too old?

If the requested cursor predates the server's `TRANSCRIPT_OPS_JOURNAL_CAPACITY` window, the `replay()` method in [`wsConnectionV1.ts`](https://github.com/MoonshotAI/kimi-code/blob/main/wsConnectionV1.ts) cannot retrieve the missing operations. The server emits a `resync_required` control frame, and the client's `onResyncRequired` handler must perform a full REST transcript fetch to obtain a complete baseline before continuing.

### How does the protocol prevent duplicate operations during reconnection?

The server streams buffered operations only once during the `attachSession()` replay phase, followed immediately by an `ack` frame. The client ignores any operations until this acknowledgment arrives, ensuring the replay window closes atomically before new real-time operations flow. The cursor-based sequencing ensures each operation batch carries a unique watermark for idempotent client-side processing.

### What is the difference between subscribe and subscribe_v2?

The legacy `subscribe` path handles durable session and agent events with guaranteed ordering, while `subscribe_v2` exclusively manages **transcript grade specifications** and volatile operation streams. The v2 channel supports granular control over operation granularity (block vs. delta levels) and carries the `transcript_since` cursor necessary for replay resumption.

### How does the client know when reconnection is complete?

The client tracks the `subscribeV2Id` sent in the `subscribe_v2` frame and awaits a matching `ack` response from the server. The condition `frame.id === this.subscribeV2Id` in the message handler triggers `onReconnected()`, signaling that the transcript stream is attached and the connection has fully recovered from the interruption.