| 260 | * All writes go through this.request(). |
| 261 | */ |
| 262 | export class CCRClient { |
| 263 | private workerEpoch = 0 |
| 264 | private readonly heartbeatIntervalMs: number |
| 265 | private readonly heartbeatJitterFraction: number |
| 266 | private heartbeatTimer: NodeJS.Timeout | null = null |
| 267 | private heartbeatInFlight = false |
| 268 | private closed = false |
| 269 | private consecutiveAuthFailures = 0 |
| 270 | private currentState: SessionState | null = null |
| 271 | private readonly sessionBaseUrl: string |
| 272 | private readonly sessionId: string |
| 273 | private readonly http = createAxiosInstance({ keepAlive: true }) |
| 274 | |
| 275 | // stream_event delay buffer — accumulates content deltas for up to |
| 276 | // STREAM_EVENT_FLUSH_INTERVAL_MS before enqueueing (reduces POST count |
| 277 | // and enables text_delta coalescing). Mirrors HybridTransport's pattern. |
| 278 | private streamEventBuffer: SDKPartialAssistantMessage[] = [] |
| 279 | private streamEventTimer: ReturnType<typeof setTimeout> | null = null |
| 280 | // Full-so-far text accumulator. Persists across flushes so each emitted |
| 281 | // text_delta event carries the complete text from the start of the block — |
| 282 | // mid-stream reconnects see a self-contained snapshot. Keyed by API message |
| 283 | // ID; cleared in writeEvent when the complete assistant message arrives. |
| 284 | private streamTextAccumulator = createStreamAccumulator() |
| 285 | |
| 286 | private readonly workerState: WorkerStateUploader |
| 287 | private readonly eventUploader: SerialBatchEventUploader<ClientEvent> |
| 288 | private readonly internalEventUploader: SerialBatchEventUploader<WorkerEvent> |
| 289 | private readonly deliveryUploader: SerialBatchEventUploader<{ |
| 290 | eventId: string |
| 291 | status: 'received' | 'processing' | 'processed' |
| 292 | }> |
| 293 | |
| 294 | /** |
| 295 | * Called when the server returns 409 (a newer worker epoch superseded ours). |
| 296 | * Default: process.exit(1) — correct for spawn-mode children where the |
| 297 | * parent bridge re-spawns. In-process callers (replBridge) MUST override |
| 298 | * this to close gracefully instead; exit would kill the user's REPL. |
| 299 | */ |
| 300 | private readonly onEpochMismatch: () => never |
| 301 | |
| 302 | /** |
| 303 | * Auth header source. Defaults to the process-wide session-ingress token |
| 304 | * (CLAUDE_CODE_SESSION_ACCESS_TOKEN env var). Callers managing multiple |
| 305 | * concurrent sessions with distinct JWTs MUST inject this — the env-var |
| 306 | * path is a process global and would stomp across sessions. |
| 307 | */ |
| 308 | private readonly getAuthHeaders: () => Record<string, string> |
| 309 | |
| 310 | constructor( |
| 311 | transport: SSETransport, |
| 312 | sessionUrl: URL, |
| 313 | opts?: { |
| 314 | onEpochMismatch?: () => never |
| 315 | heartbeatIntervalMs?: number |
| 316 | heartbeatJitterFraction?: number |
| 317 | /** |
| 318 | * Per-instance auth header source. Omit to read the process-wide |
| 319 | * CLAUDE_CODE_SESSION_ACCESS_TOKEN (single-session callers — REPL, |
nothing calls this directly
no test coverage detected