Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
9.3 kB · 343 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'import type { SDKControlCancelRequest, SDKControlPermissionRequest, SDKControlRequest, SDKControlResponse,} from '../entrypoints/sdk/controlTypes.js'import { logForDebugging } from '../utils/debug.js'import { logError } from '../utils/log.js'import { type RemoteMessageContent, sendEventToRemoteSession,} from '../utils/teleport/api.js'import { SessionsWebSocket, type SessionsWebSocketCallbacks,} from './SessionsWebSocket.js'
/** * Type guard to check if a message is an SDKMessage (not a control message) */function isSDKMessage( message: | SDKMessage | SDKControlRequest | SDKControlResponse | SDKControlCancelRequest,): message is SDKMessage { return ( message.type !== 'control_request' && message.type !== 'control_response' && message.type !== 'control_cancel_request' )}
/** * Simple permission response for remote sessions. * This is a simplified version of PermissionResult for CCR communication. */export type RemotePermissionResponse = | { behavior: 'allow' updatedInput: Record<string, unknown> } | { behavior: 'deny' message: string }
export type RemoteSessionConfig = { sessionId: string getAccessToken: () => string orgUuid: string /** True if session was created with an initial prompt that's being processed */ hasInitialPrompt?: boolean /** * When true, this client is a pure viewer. Ctrl+C/Escape do NOT send * interrupt to the remote agent; 60s reconnect timeout is disabled; * session title is never updated. Used by `claude assistant`. */ viewerOnly?: boolean}
export type RemoteSessionCallbacks = { /** Called when an SDKMessage is received from the session */ onMessage: (message: SDKMessage) => void /** Called when a permission request is received from CCR */ onPermissionRequest: ( request: SDKControlPermissionRequest, requestId: string, ) => void /** Called when the server cancels a pending permission request */ onPermissionCancelled?: ( requestId: string, toolUseId: string | undefined, ) => void /** Called when connection is established */ onConnected?: () => void /** Called when connection is lost and cannot be restored */ onDisconnected?: () => void /** Called on transient WS drop while reconnect backoff is in progress */ onReconnecting?: () => void /** Called on error */ onError?: (error: Error) => void}
/** * Manages a remote CCR session. * * Coordinates: * - WebSocket subscription for receiving messages from CCR * - HTTP POST for sending user messages to CCR * - Permission request/response flow */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() }
/** * Handle messages from WebSocket */ 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) } }
/** * Handle control requests from CCR (e.g., permission requests) */ private handleControlRequest(request: SDKControlRequest): void { const { request_id, request: inner } = request
if (inner.subtype === 'can_use_tool') { logForDebugging( `[RemoteSessionManager] Permission request for tool: ${inner.tool_name}`, ) this.pendingPermissionRequests.set(request_id, inner) this.callbacks.onPermissionRequest(inner, request_id) } else { // Send an error response for unrecognized subtypes so the server // doesn't hang waiting for a reply that never comes. logForDebugging( `[RemoteSessionManager] Unsupported control request subtype: ${inner.subtype}`, ) const response: SDKControlResponse = { type: 'control_response', response: { subtype: 'error', request_id, error: `Unsupported control request subtype: ${inner.subtype}`, }, } this.websocket?.sendControlResponse(response) } }
/** * Send a user message to the remote session via HTTP POST */ async sendMessage( content: RemoteMessageContent, opts?: { uuid?: string }, ): Promise<boolean> { logForDebugging( `[RemoteSessionManager] Sending message to session ${this.config.sessionId}`, )
const success = await sendEventToRemoteSession( this.config.sessionId, content, opts, )
if (!success) { logError( new Error( `[RemoteSessionManager] Failed to send message to session ${this.config.sessionId}`, ), ) }
return success }
/** * Respond to a permission request from CCR */ 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) }
/** * Check if connected to the remote session */ isConnected(): boolean { return this.websocket?.isConnected() ?? false }
/** * Send an interrupt signal to cancel the current request on the remote session */ cancelSession(): void { logForDebugging('[RemoteSessionManager] Sending interrupt signal') this.websocket?.sendControlRequest({ subtype: 'interrupt' }) }
/** * Get the session ID */ getSessionId(): string { return this.config.sessionId }
/** * Disconnect from the remote session */ disconnect(): void { logForDebugging('[RemoteSessionManager] Disconnecting') this.websocket?.close() this.websocket = null this.pendingPermissionRequests.clear() }
/** * Force reconnect the WebSocket. * Useful when the subscription becomes stale after container shutdown. */ reconnect(): void { logForDebugging('[RemoteSessionManager] Reconnecting WebSocket') this.websocket?.reconnect() }}
/** * Create a remote session config from OAuth tokens */export function createRemoteSessionConfig( sessionId: string, getAccessToken: () => string, orgUuid: string, hasInitialPrompt = false, viewerOnly = false,): RemoteSessionConfig { return { sessionId, getAccessToken, orgUuid, hasInitialPrompt, viewerOnly, }}