Studio Notebook

Claude Code Atlas

Remote Session Manager And CCR Lifecycle

Follow the CCR session lifecycle from WebSocket connect through permission responses.

Why this matters

Remote session management is the layer that keeps CCR readable over the network. It separates the ordinary SDK stream from permission requests, the cancel path that clears a pending prompt, and the acknowledgments that close the loop so the local side can stay simple.

The lifecycle, from connect to the response roundtrip

RemoteSessionManager opens the websocket first. After that, handleMessage() sorts each inbound message into one of four buckets: SDK messages are the ordinary session stream. Control requests carry permission prompts from CCR. Control cancel requests tell the client to clear a pending prompt. Control responses are acknowledgments that close the loop. When CCR asks for permission, respondToPermissionRequest() turns the local decision into a response and sends it back.

See the appendix for the full field-by-field reference on RemoteSessionConfig and RemoteSessionCallbacks.

The permission response shape

RemotePermissionResponse is the tiny allow-or-deny reply that goes back to CCR.

export type RemotePermissionResponse =
  | {
      behavior: 'allow'
      updatedInput: Record<string, unknown>
    }
  | {
      behavior: 'deny'
      message: string
    }

The session settings

RemoteSessionConfig tells the manager which session to join, which org owns it, and how to fetch a token. The shape starts with export type RemoteSessionConfig = {, and the appendix has the full field-by-field reference.

The callback hooks

RemoteSessionCallbacks tells the manager where to hand remote events back once they arrive. The shape starts with export type RemoteSessionCallbacks = {, and the appendix keeps the full callback-by-callback reference.

The manager itself

RemoteSessionManager is the class that opens the socket, classifies inbound traffic, and sends permission answers back out.

export class RemoteSessionManager {
  private websocket: SessionsWebSocket | null = null
  private pendingPermissionRequests: Map<string, SDKControlPermissionRequest> =
    new Map()

  constructor(
    private readonly config: RemoteSessionConfig,
    private readonly callbacks: RemoteSessionCallbacks,
  ) {}

  /**
   * Connect to the remote session via WebSocket
   */
  connect(): void {
    logForDebugging(
      `[RemoteSessionManager] Connecting to session ${this.config.sessionId}`,
    )

    const wsCallbacks: SessionsWebSocketCallbacks = {
      onMessage: message => this.handleMessage(message),
      onConnected: () => {
        logForDebugging('[RemoteSessionManager] Connected')
        this.callbacks.onConnected?.()
      },
      onClose: () => {
        logForDebugging('[RemoteSessionManager] Disconnected')
        this.callbacks.onDisconnected?.()
      },
      onReconnecting: () => {
        logForDebugging('[RemoteSessionManager] Reconnecting')
        this.callbacks.onReconnecting?.()
      },
      onError: error => {
        logError(error)
        this.callbacks.onError?.(error)
      },
    }

    this.websocket = new SessionsWebSocket(
      this.config.sessionId,
      this.config.orgUuid,
      this.config.getAccessToken,
      wsCallbacks,
    )

    void this.websocket.connect()
  }

connect() just wires the websocket and the lifecycle callbacks. The real routing happens in handleMessage(), where the manager keeps SDK messages, permission prompts, cancellations, and acknowledgments separate.

  private handleMessage(
    message:
      | SDKMessage
      | SDKControlRequest
      | SDKControlResponse
      | SDKControlCancelRequest,
  ): void {
    // Handle control requests (permission prompts from CCR)
    if (message.type === 'control_request') {
      this.handleControlRequest(message)
      return
    }

    // Handle control cancel requests (server cancelling a pending permission prompt)
    if (message.type === 'control_cancel_request') {
      const { request_id } = message
      const pendingRequest = this.pendingPermissionRequests.get(request_id)
      logForDebugging(
        `[RemoteSessionManager] Permission request cancelled: ${request_id}`,
      )
      this.pendingPermissionRequests.delete(request_id)
      this.callbacks.onPermissionCancelled?.(
        request_id,
        pendingRequest?.tool_use_id,
      )
      return
    }

    // Handle control responses (acknowledgments)
    if (message.type === 'control_response') {
      logForDebugging('[RemoteSessionManager] Received control response')
      return
    }

    // Forward SDK messages to callback (type guard ensures proper narrowing)
    if (isSDKMessage(message)) {
      this.callbacks.onMessage(message)
    }
  }

When CCR asks for permission, respondToPermissionRequest() turns the local allow-or-deny decision into a control_response and sends it back over the socket.

  respondToPermissionRequest(
    requestId: string,
    result: RemotePermissionResponse,
  ): void {
    const pendingRequest = this.pendingPermissionRequests.get(requestId)
    if (!pendingRequest) {
      logError(
        new Error(
          `[RemoteSessionManager] No pending permission request with ID: ${requestId}`,
        ),
      )
      return
    }

    this.pendingPermissionRequests.delete(requestId)

    const response: SDKControlResponse = {
      type: 'control_response',
      response: {
        subtype: 'success',
        request_id: requestId,
        response: {
          behavior: result.behavior,
          ...(result.behavior === 'allow'
            ? { updatedInput: result.updatedInput }
            : { message: result.message }),
        },
      },
    }

    logForDebugging(
      `[RemoteSessionManager] Sending permission response: ${result.behavior}`,
    )

    this.websocket?.sendControlResponse(response)
  }