/ src / bridge / types.ts
types.ts
  1  /** Default per-session timeout (24 hours). */
  2  export const DEFAULT_SESSION_TIMEOUT_MS = 24 * 60 * 60 * 1000
  3  
  4  /** Reusable login guidance appended to bridge auth errors. */
  5  export const BRIDGE_LOGIN_INSTRUCTION =
  6    'Remote Control is only available with claude.ai subscriptions. Please use `/login` to sign in with your claude.ai account.'
  7  
  8  /** Full error printed when `claude remote-control` is run without auth. */
  9  export const BRIDGE_LOGIN_ERROR =
 10    'Error: You must be logged in to use Remote Control.\n\n' +
 11    BRIDGE_LOGIN_INSTRUCTION
 12  
 13  /** Shown when the user disconnects Remote Control (via /remote-control or ultraplan launch). */
 14  export const REMOTE_CONTROL_DISCONNECTED_MSG = 'Remote Control disconnected.'
 15  
 16  // --- Protocol types for the environments API ---
 17  
 18  export type WorkData = {
 19    type: 'session' | 'healthcheck'
 20    id: string
 21  }
 22  
 23  export type WorkResponse = {
 24    id: string
 25    type: 'work'
 26    environment_id: string
 27    state: string
 28    data: WorkData
 29    secret: string // base64url-encoded JSON
 30    created_at: string
 31  }
 32  
 33  export type WorkSecret = {
 34    version: number
 35    session_ingress_token: string
 36    api_base_url: string
 37    sources: Array<{
 38      type: string
 39      git_info?: { type: string; repo: string; ref?: string; token?: string }
 40    }>
 41    auth: Array<{ type: string; token: string }>
 42    claude_code_args?: Record<string, string> | null
 43    mcp_config?: unknown | null
 44    environment_variables?: Record<string, string> | null
 45    /**
 46     * Server-driven CCR v2 selector. Set by prepare_work_secret() when the
 47     * session was created via the v2 compat layer (ccr_v2_compat_enabled).
 48     * Same field the BYOC runner reads at environment-runner/sessionExecutor.ts.
 49     */
 50    use_code_sessions?: boolean
 51  }
 52  
 53  export type SessionDoneStatus = 'completed' | 'failed' | 'interrupted'
 54  
 55  export type SessionActivityType = 'tool_start' | 'text' | 'result' | 'error'
 56  
 57  export type SessionActivity = {
 58    type: SessionActivityType
 59    summary: string // e.g. "Editing src/foo.ts", "Reading package.json"
 60    timestamp: number
 61  }
 62  
 63  /**
 64   * How `claude remote-control` chooses session working directories.
 65   * - `single-session`: one session in cwd, bridge tears down when it ends
 66   * - `worktree`: persistent server, every session gets an isolated git worktree
 67   * - `same-dir`: persistent server, every session shares cwd (can stomp each other)
 68   */
 69  export type SpawnMode = 'single-session' | 'worktree' | 'same-dir'
 70  
 71  /**
 72   * Well-known worker_type values THIS codebase produces. Sent as
 73   * `metadata.worker_type` at environment registration so claude.ai can filter
 74   * the session picker by origin (e.g. assistant tab only shows assistant
 75   * workers). The backend treats this as an opaque string — desktop cowork
 76   * sends `"cowork"`, which isn't in this union. REPL code uses this narrow
 77   * type for its own exhaustiveness; wire-level fields accept any string.
 78   */
 79  export type BridgeWorkerType = 'claude_code' | 'claude_code_assistant'
 80  
 81  export type BridgeConfig = {
 82    dir: string
 83    machineName: string
 84    branch: string
 85    gitRepoUrl: string | null
 86    maxSessions: number
 87    spawnMode: SpawnMode
 88    verbose: boolean
 89    sandbox: boolean
 90    /** Client-generated UUID identifying this bridge instance. */
 91    bridgeId: string
 92    /**
 93     * Sent as metadata.worker_type so web clients can filter by origin.
 94     * Backend treats this as opaque — any string, not just BridgeWorkerType.
 95     */
 96    workerType: string
 97    /** Client-generated UUID for idempotent environment registration. */
 98    environmentId: string
 99    /**
100     * Backend-issued environment_id to reuse on re-register. When set, the
101     * backend treats registration as a reconnect to the existing environment
102     * instead of creating a new one. Used by `claude remote-control
103     * --session-id` resume. Must be a backend-format ID — client UUIDs are
104     * rejected with 400.
105     */
106    reuseEnvironmentId?: string
107    /** API base URL the bridge is connected to (used for polling). */
108    apiBaseUrl: string
109    /** Session ingress base URL for WebSocket connections (may differ from apiBaseUrl locally). */
110    sessionIngressUrl: string
111    /** Debug file path passed via --debug-file. */
112    debugFile?: string
113    /** Per-session timeout in milliseconds. Sessions exceeding this are killed. */
114    sessionTimeoutMs?: number
115  }
116  
117  // --- Dependency interfaces (for testability) ---
118  
119  /**
120   * A control_response event sent back to a session (e.g. a permission decision).
121   * The `subtype` is `'success'` per the SDK protocol; the inner `response`
122   * carries the permission decision payload (e.g. `{ behavior: 'allow' }`).
123   */
124  export type PermissionResponseEvent = {
125    type: 'control_response'
126    response: {
127      subtype: 'success'
128      request_id: string
129      response: Record<string, unknown>
130    }
131  }
132  
133  export type BridgeApiClient = {
134    registerBridgeEnvironment(config: BridgeConfig): Promise<{
135      environment_id: string
136      environment_secret: string
137    }>
138    pollForWork(
139      environmentId: string,
140      environmentSecret: string,
141      signal?: AbortSignal,
142      reclaimOlderThanMs?: number,
143    ): Promise<WorkResponse | null>
144    acknowledgeWork(
145      environmentId: string,
146      workId: string,
147      sessionToken: string,
148    ): Promise<void>
149    /** Stop a work item via the environments API. */
150    stopWork(environmentId: string, workId: string, force: boolean): Promise<void>
151    /** Deregister/delete the bridge environment on graceful shutdown. */
152    deregisterEnvironment(environmentId: string): Promise<void>
153    /** Send a permission response (control_response) to a session via the session events API. */
154    sendPermissionResponseEvent(
155      sessionId: string,
156      event: PermissionResponseEvent,
157      sessionToken: string,
158    ): Promise<void>
159    /** Archive a session so it no longer appears as active on the server. */
160    archiveSession(sessionId: string): Promise<void>
161    /**
162     * Force-stop stale worker instances and re-queue a session on an environment.
163     * Used by `--session-id` to resume a session after the original bridge died.
164     */
165    reconnectSession(environmentId: string, sessionId: string): Promise<void>
166    /**
167     * Send a lightweight heartbeat for an active work item, extending its lease.
168     * Uses SessionIngressAuth (JWT, no DB hit) instead of EnvironmentSecretAuth.
169     * Returns the server's response with lease status.
170     */
171    heartbeatWork(
172      environmentId: string,
173      workId: string,
174      sessionToken: string,
175    ): Promise<{ lease_extended: boolean; state: string }>
176  }
177  
178  export type SessionHandle = {
179    sessionId: string
180    done: Promise<SessionDoneStatus>
181    kill(): void
182    forceKill(): void
183    activities: SessionActivity[] // ring buffer of recent activities (last ~10)
184    currentActivity: SessionActivity | null // most recent
185    accessToken: string // session_ingress_token for API calls
186    lastStderr: string[] // ring buffer of last stderr lines
187    writeStdin(data: string): void // write directly to child stdin
188    /** Update the access token for a running session (e.g. after token refresh). */
189    updateAccessToken(token: string): void
190  }
191  
192  export type SessionSpawnOpts = {
193    sessionId: string
194    sdkUrl: string
195    accessToken: string
196    /** When true, spawn the child with CCR v2 env vars (SSE transport + CCRClient). */
197    useCcrV2?: boolean
198    /** Required when useCcrV2 is true. Obtained from POST /worker/register. */
199    workerEpoch?: number
200    /**
201     * Fires once with the text of the first real user message seen on the
202     * child's stdout (via --replay-user-messages). Lets the caller derive a
203     * session title when none exists yet. Tool-result and synthetic user
204     * messages are skipped.
205     */
206    onFirstUserMessage?: (text: string) => void
207  }
208  
209  export type SessionSpawner = {
210    spawn(opts: SessionSpawnOpts, dir: string): SessionHandle
211  }
212  
213  export type BridgeLogger = {
214    printBanner(config: BridgeConfig, environmentId: string): void
215    logSessionStart(sessionId: string, prompt: string): void
216    logSessionComplete(sessionId: string, durationMs: number): void
217    logSessionFailed(sessionId: string, error: string): void
218    logStatus(message: string): void
219    logVerbose(message: string): void
220    logError(message: string): void
221    /** Log a reconnection success event after recovering from connection errors. */
222    logReconnected(disconnectedMs: number): void
223    /** Show idle status with repo/branch info and shimmer animation. */
224    updateIdleStatus(): void
225    /** Show reconnecting status in the live display. */
226    updateReconnectingStatus(delayStr: string, elapsedStr: string): void
227    updateSessionStatus(
228      sessionId: string,
229      elapsed: string,
230      activity: SessionActivity,
231      trail: string[],
232    ): void
233    clearStatus(): void
234    /** Set repository info for status line display. */
235    setRepoInfo(repoName: string, branch: string): void
236    /** Set debug log glob shown above the status line (ant users). */
237    setDebugLogPath(path: string): void
238    /** Transition to "Attached" state when a session starts. */
239    setAttached(sessionId: string): void
240    /** Show failed status in the live display. */
241    updateFailedStatus(error: string): void
242    /** Toggle QR code visibility. */
243    toggleQr(): void
244    /** Update the "<n> of <m> sessions" indicator and spawn mode hint. */
245    updateSessionCount(active: number, max: number, mode: SpawnMode): void
246    /** Update the spawn mode shown in the session-count line. Pass null to hide (single-session or toggle unavailable). */
247    setSpawnModeDisplay(mode: 'same-dir' | 'worktree' | null): void
248    /** Register a new session for multi-session display (called after spawn succeeds). */
249    addSession(sessionId: string, url: string): void
250    /** Update the per-session activity summary (tool being run) in the multi-session list. */
251    updateSessionActivity(sessionId: string, activity: SessionActivity): void
252    /**
253     * Set a session's display title. In multi-session mode, updates the bullet list
254     * entry. In single-session mode, also shows the title in the main status line.
255     * Triggers a render (guarded against reconnecting/failed states).
256     */
257    setSessionTitle(sessionId: string, title: string): void
258    /** Remove a session from the multi-session display when it ends. */
259    removeSession(sessionId: string): void
260    /** Force a re-render of the status display (for multi-session activity refresh). */
261    refreshDisplay(): void
262  }