@agentuity/core 2.0.11 → 2.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/services/api.d.ts +1 -1
  2. package/dist/services/api.d.ts.map +1 -1
  3. package/dist/services/api.js +4 -6
  4. package/dist/services/api.js.map +1 -1
  5. package/dist/services/coder/agents.d.ts +6 -4
  6. package/dist/services/coder/agents.d.ts.map +1 -1
  7. package/dist/services/coder/api-reference.d.ts.map +1 -1
  8. package/dist/services/coder/api-reference.js +78 -10
  9. package/dist/services/coder/api-reference.js.map +1 -1
  10. package/dist/services/coder/client.d.ts +5 -1
  11. package/dist/services/coder/client.d.ts.map +1 -1
  12. package/dist/services/coder/client.js +8 -1
  13. package/dist/services/coder/client.js.map +1 -1
  14. package/dist/services/coder/index.d.ts +2 -2
  15. package/dist/services/coder/index.d.ts.map +1 -1
  16. package/dist/services/coder/index.js +1 -1
  17. package/dist/services/coder/index.js.map +1 -1
  18. package/dist/services/coder/protocol.d.ts +385 -15
  19. package/dist/services/coder/protocol.d.ts.map +1 -1
  20. package/dist/services/coder/protocol.js +148 -2
  21. package/dist/services/coder/protocol.js.map +1 -1
  22. package/dist/services/coder/sessions.d.ts +24 -2
  23. package/dist/services/coder/sessions.d.ts.map +1 -1
  24. package/dist/services/coder/sessions.js +10 -1
  25. package/dist/services/coder/sessions.js.map +1 -1
  26. package/dist/services/coder/sse.d.ts +2 -2
  27. package/dist/services/coder/sse.d.ts.map +1 -1
  28. package/dist/services/coder/sse.js +290 -178
  29. package/dist/services/coder/sse.js.map +1 -1
  30. package/dist/services/coder/types.d.ts +607 -42
  31. package/dist/services/coder/types.d.ts.map +1 -1
  32. package/dist/services/coder/types.js +202 -40
  33. package/dist/services/coder/types.js.map +1 -1
  34. package/dist/services/coder/websocket.d.ts +13 -1
  35. package/dist/services/coder/websocket.d.ts.map +1 -1
  36. package/dist/services/coder/websocket.js +91 -19
  37. package/dist/services/coder/websocket.js.map +1 -1
  38. package/dist/services/sandbox/api-reference.js +7 -7
  39. package/dist/services/sandbox/api-reference.js.map +1 -1
  40. package/dist/services/sandbox/client.d.ts +3 -2
  41. package/dist/services/sandbox/client.d.ts.map +1 -1
  42. package/dist/services/sandbox/client.js.map +1 -1
  43. package/dist/services/sandbox/create.d.ts +5 -0
  44. package/dist/services/sandbox/create.d.ts.map +1 -1
  45. package/dist/services/sandbox/create.js +8 -0
  46. package/dist/services/sandbox/create.js.map +1 -1
  47. package/dist/services/sandbox/get.d.ts +8 -4
  48. package/dist/services/sandbox/get.d.ts.map +1 -1
  49. package/dist/services/sandbox/get.js +28 -3
  50. package/dist/services/sandbox/get.js.map +1 -1
  51. package/dist/services/sandbox/getStatus.d.ts +2 -0
  52. package/dist/services/sandbox/getStatus.d.ts.map +1 -1
  53. package/dist/services/sandbox/getStatus.js +17 -1
  54. package/dist/services/sandbox/getStatus.js.map +1 -1
  55. package/dist/services/sandbox/index.d.ts +1 -1
  56. package/dist/services/sandbox/index.d.ts.map +1 -1
  57. package/dist/services/sandbox/list.d.ts +3 -0
  58. package/dist/services/sandbox/list.d.ts.map +1 -1
  59. package/dist/services/sandbox/list.js +5 -0
  60. package/dist/services/sandbox/list.js.map +1 -1
  61. package/dist/services/sandbox/pause.d.ts +17 -1
  62. package/dist/services/sandbox/pause.d.ts.map +1 -1
  63. package/dist/services/sandbox/pause.js +21 -3
  64. package/dist/services/sandbox/pause.js.map +1 -1
  65. package/dist/services/sandbox/run.d.ts +1 -0
  66. package/dist/services/sandbox/run.d.ts.map +1 -1
  67. package/dist/services/sandbox/run.js +145 -85
  68. package/dist/services/sandbox/run.js.map +1 -1
  69. package/dist/services/sandbox/types.d.ts +8 -2
  70. package/dist/services/sandbox/types.d.ts.map +1 -1
  71. package/dist/services/sandbox/types.js +10 -0
  72. package/dist/services/sandbox/types.js.map +1 -1
  73. package/dist/services/stream/namespaces.d.ts +2 -2
  74. package/dist/services/stream/namespaces.js +2 -2
  75. package/dist/services/stream/namespaces.js.map +1 -1
  76. package/package.json +2 -2
  77. package/src/services/api.ts +6 -7
  78. package/src/services/coder/api-reference.ts +79 -9
  79. package/src/services/coder/client.ts +12 -0
  80. package/src/services/coder/index.ts +3 -0
  81. package/src/services/coder/protocol.ts +166 -2
  82. package/src/services/coder/sessions.ts +26 -0
  83. package/src/services/coder/sse.ts +343 -184
  84. package/src/services/coder/types.ts +257 -44
  85. package/src/services/coder/websocket.ts +120 -21
  86. package/src/services/sandbox/api-reference.ts +7 -7
  87. package/src/services/sandbox/client.ts +4 -4
  88. package/src/services/sandbox/create.ts +10 -0
  89. package/src/services/sandbox/get.ts +32 -3
  90. package/src/services/sandbox/getStatus.ts +20 -1
  91. package/src/services/sandbox/index.ts +1 -1
  92. package/src/services/sandbox/list.ts +5 -0
  93. package/src/services/sandbox/pause.ts +38 -4
  94. package/src/services/sandbox/run.ts +202 -108
  95. package/src/services/sandbox/types.ts +15 -2
  96. package/src/services/stream/namespaces.ts +2 -2
@@ -45,7 +45,7 @@
45
45
  * })) {
46
46
  * console.log('Event:', event.event, event.data);
47
47
  *
48
- * if (event.event === 'broadcast' && event.data.event === 'session_complete') {
48
+ * if (event.data.type === 'broadcast' && event.data.event === 'session_complete') {
49
49
  * controller.abort(); // Stop the stream
50
50
  * }
51
51
  * }
@@ -166,7 +166,7 @@ export const CoderSSEError = StructuredError('CoderSSEError')<{
166
166
  * A single SSE event with its event name and parsed data.
167
167
  */
168
168
  export interface CoderSSEEvent {
169
- /** The SSE event name (e.g., 'snapshot', 'broadcast', 'presence') */
169
+ /** The SSE event name (e.g., 'snapshot', 'message_update', 'session_join') */
170
170
  event: string;
171
171
  /** The parsed event data */
172
172
  data: ObserverSseMessage;
@@ -234,12 +234,149 @@ async function buildSSEUrl(
234
234
  return queryString ? `${baseUrl}${path}?${queryString}` : `${baseUrl}${path}`;
235
235
  }
236
236
 
237
- function getSSEData(event: Event): string | null {
238
- const msgEvent = event as unknown as { data?: unknown };
239
- if (typeof msgEvent.data === 'string') {
240
- return msgEvent.data;
237
+ interface ParsedSSEFrame {
238
+ event: string;
239
+ data: string;
240
+ }
241
+
242
+ const TYPED_TRANSPORT_EVENTS = new Set(['snapshot', 'hydration', 'presence', 'broadcast']);
243
+
244
+ function isAbortError(err: unknown): boolean {
245
+ return err instanceof Error && err.name === 'AbortError';
246
+ }
247
+
248
+ function parseSSEFrame(block: string): ParsedSSEFrame | null {
249
+ let event = 'message';
250
+ const dataLines: string[] = [];
251
+
252
+ for (const line of block.split('\n')) {
253
+ if (!line || line.startsWith(':')) continue;
254
+
255
+ const separatorIndex = line.indexOf(':');
256
+ const field = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
257
+ let value = separatorIndex === -1 ? '' : line.slice(separatorIndex + 1);
258
+ if (value.startsWith(' ')) {
259
+ value = value.slice(1);
260
+ }
261
+
262
+ if (field === 'event') {
263
+ event = value || 'message';
264
+ } else if (field === 'data') {
265
+ dataLines.push(value);
266
+ }
267
+ }
268
+
269
+ if (dataLines.length === 0) {
270
+ return null;
271
+ }
272
+
273
+ return {
274
+ event,
275
+ data: dataLines.join('\n'),
276
+ };
277
+ }
278
+
279
+ function consumeSSEBuffer(rawBuffer: string, onFrame: (frame: ParsedSSEFrame) => void): string {
280
+ const normalized = rawBuffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
281
+ let cursor = 0;
282
+
283
+ while (true) {
284
+ const boundary = normalized.indexOf('\n\n', cursor);
285
+ if (boundary === -1) break;
286
+
287
+ const block = normalized.slice(cursor, boundary);
288
+ cursor = boundary + 2;
289
+ if (!block.trim()) continue;
290
+
291
+ const frame = parseSSEFrame(block);
292
+ if (frame) {
293
+ onFrame(frame);
294
+ }
295
+ }
296
+
297
+ return normalized.slice(cursor);
298
+ }
299
+
300
+ function decodeCoderSSEEvent(frame: ParsedSSEFrame, sessionId: string): CoderSSEEvent {
301
+ let parsed: unknown;
302
+ try {
303
+ parsed = JSON.parse(frame.data);
304
+ } catch (err) {
305
+ throw new CoderSSEError({
306
+ message: `Failed to parse SSE ${frame.event} event: ${err instanceof Error ? err.message : String(err)}`,
307
+ code: 'parse_error',
308
+ sessionId,
309
+ });
310
+ }
311
+
312
+ const payload =
313
+ TYPED_TRANSPORT_EVENTS.has(frame.event) && parsed && typeof parsed === 'object'
314
+ ? { type: frame.event, ...(parsed as Record<string, unknown>) }
315
+ : parsed;
316
+ const result = ObserverSseMessageSchema.safeParse(payload);
317
+
318
+ if (!result.success) {
319
+ throw new CoderSSEError({
320
+ message: `Invalid SSE ${frame.event} event format`,
321
+ code: 'parse_error',
322
+ sessionId,
323
+ });
324
+ }
325
+
326
+ const event = frame.event === 'message' ? result.data.type : frame.event;
327
+ return { event, data: result.data };
328
+ }
329
+
330
+ async function readSSEStream(
331
+ response: Response,
332
+ signal: AbortSignal,
333
+ onEvent: (event: CoderSSEEvent) => void,
334
+ sessionId: string
335
+ ): Promise<void> {
336
+ if (!response.body) {
337
+ throw new CoderSSEError({
338
+ message: 'SSE response did not include a readable body',
339
+ code: 'connection_failed',
340
+ sessionId,
341
+ });
342
+ }
343
+
344
+ const reader = response.body.getReader();
345
+ const decoder = new TextDecoder();
346
+ let buffer = '';
347
+
348
+ try {
349
+ while (!signal.aborted) {
350
+ const { done, value } = await reader.read();
351
+ if (done) break;
352
+ buffer += decoder.decode(value, { stream: true });
353
+ buffer = consumeSSEBuffer(buffer, (frame) => {
354
+ onEvent(decodeCoderSSEEvent(frame, sessionId));
355
+ });
356
+ }
357
+ } finally {
358
+ try {
359
+ reader.releaseLock();
360
+ } catch {
361
+ // Some runtimes throw if the reader is already released after abort.
362
+ }
363
+ }
364
+ }
365
+
366
+ function buildConnectionError(response: Response, sessionId: string): Error {
367
+ return new CoderSSEError({
368
+ message: `SSE connection failed: ${response.status} ${response.statusText || 'HTTP error'}`,
369
+ code:
370
+ response.status === 401 || response.status === 403 ? 'auth_failed' : 'connection_failed',
371
+ sessionId,
372
+ });
373
+ }
374
+
375
+ function isRetryableStreamError(error: Error): boolean {
376
+ if (error instanceof CoderSSEError) {
377
+ return error.code === 'connection_failed';
241
378
  }
242
- return null;
379
+ return true;
243
380
  }
244
381
 
245
382
  /**
@@ -291,7 +428,7 @@ export class CoderSSEClient {
291
428
  onError?: (error: Error) => void;
292
429
  };
293
430
  #state: CoderSSEState = 'closed';
294
- #eventSource: EventSource | null = null;
431
+ #abortController: AbortController | null = null;
295
432
  #reconnectAttempts = 0;
296
433
  #reconnectTimer: ReturnType<typeof setTimeout> | null = null;
297
434
  #intentionallyClosed = false;
@@ -336,7 +473,7 @@ export class CoderSSEClient {
336
473
  * Whether the client is currently connected and receiving events.
337
474
  */
338
475
  get isConnected(): boolean {
339
- return this.#state === 'connected' && this.#eventSource?.readyState === 1;
476
+ return this.#state === 'connected' && this.#abortController !== null;
340
477
  }
341
478
 
342
479
  /**
@@ -369,9 +506,9 @@ export class CoderSSEClient {
369
506
  clearTimeout(this.#reconnectTimer);
370
507
  this.#reconnectTimer = null;
371
508
  }
372
- if (this.#eventSource) {
373
- this.#eventSource.close();
374
- this.#eventSource = null;
509
+ if (this.#abortController) {
510
+ this.#abortController.abort();
511
+ this.#abortController = null;
375
512
  }
376
513
  this.#state = 'closed';
377
514
  this.#options.onClose?.();
@@ -381,47 +518,18 @@ export class CoderSSEClient {
381
518
  this.#state = state;
382
519
  }
383
520
 
384
- #handleEvent(eventName: string, typeOverride?: string): void {
385
- this.#eventSource!.addEventListener(eventName, (event: Event) => {
386
- const data = getSSEData(event);
387
- if (!data) return;
388
-
389
- try {
390
- const parsed = JSON.parse(data);
391
- const payload = typeOverride ? { type: typeOverride, ...parsed } : parsed;
392
- const result = ObserverSseMessageSchema.safeParse(payload);
393
-
394
- if (result.success) {
395
- const semanticEvent = typeOverride || result.data.type;
396
- const sseEvent: CoderSSEEvent = { event: semanticEvent, data: result.data };
397
- this.#options.onEvent?.(sseEvent);
398
-
399
- if (result.data.type === 'snapshot') {
400
- this.#options.onSnapshot?.(result.data);
401
- } else if (result.data.type === 'hydration') {
402
- this.#options.onHydration?.(result.data);
403
- } else if (result.data.type === 'presence') {
404
- this.#options.onPresence?.(result.data);
405
- } else if (result.data.type === 'broadcast') {
406
- this.#options.onBroadcast?.(result.data);
407
- }
408
- } else {
409
- const parseError = new CoderSSEError({
410
- message: `Invalid SSE ${eventName} event format`,
411
- code: 'parse_error',
412
- sessionId: this.#options.sessionId,
413
- });
414
- this.#options.onError?.(parseError);
415
- }
416
- } catch (err) {
417
- const parseError = new CoderSSEError({
418
- message: `Failed to parse SSE ${eventName} event: ${err instanceof Error ? err.message : String(err)}`,
419
- code: 'parse_error',
420
- sessionId: this.#options.sessionId,
421
- });
422
- this.#options.onError?.(parseError);
423
- }
424
- });
521
+ #dispatchEvent(event: CoderSSEEvent): void {
522
+ this.#options.onEvent?.(event);
523
+
524
+ if (event.data.type === 'snapshot') {
525
+ this.#options.onSnapshot?.(event.data);
526
+ } else if (event.data.type === 'hydration') {
527
+ this.#options.onHydration?.(event.data);
528
+ } else if (event.data.type === 'presence') {
529
+ this.#options.onPresence?.(event.data);
530
+ } else if (event.data.type === 'broadcast') {
531
+ this.#options.onBroadcast?.(event.data);
532
+ }
425
533
  }
426
534
 
427
535
  async #connectInternal(): Promise<void> {
@@ -444,19 +552,27 @@ export class CoderSSEClient {
444
552
  return;
445
553
  }
446
554
 
447
- // Workaround for bun-types EventSource constructor typing issue.
448
- // The type definitions don't match the runtime signature, so we use
449
- // a double type assertion to construct EventSource with a URL parameter.
555
+ const controller = new AbortController();
556
+ this.#abortController = controller;
557
+
558
+ let response: Response;
450
559
  try {
451
- const EventSourceCtor: typeof EventSource = EventSource;
452
- this.#eventSource = new (EventSourceCtor as unknown as new (url: string) => EventSource)(
453
- url
454
- );
560
+ response = await fetch(url, {
561
+ headers: {
562
+ accept: 'text/event-stream',
563
+ },
564
+ signal: controller.signal,
565
+ });
455
566
  } catch (err) {
567
+ this.#abortController = null;
568
+ if (this.#intentionallyClosed || isAbortError(err)) {
569
+ this.#setState('closed');
570
+ return;
571
+ }
456
572
  this.#setState('closed');
457
573
  this.#options.onError?.(
458
574
  new CoderSSEError({
459
- message: `Failed to create EventSource: ${err instanceof Error ? err.message : String(err)}`,
575
+ message: `Failed to connect SSE stream: ${err instanceof Error ? err.message : String(err)}`,
460
576
  code: 'connection_failed',
461
577
  sessionId: this.#options.sessionId,
462
578
  })
@@ -465,23 +581,15 @@ export class CoderSSEClient {
465
581
  return;
466
582
  }
467
583
 
468
- this.#eventSource.onerror = () => {
469
- // Notify caller of transient error before reconnecting
470
- this.#options.onError?.(new Error('EventSource transient error'));
471
-
472
- if (this.#eventSource) {
473
- this.#eventSource.close();
474
- this.#eventSource = null;
475
- }
476
-
477
- if (this.#intentionallyClosed) {
478
- return;
479
- }
480
-
584
+ if (!response.ok) {
585
+ this.#abortController = null;
586
+ this.#setState('closed');
587
+ this.#options.onError?.(buildConnectionError(response, this.#options.sessionId));
481
588
  this.#scheduleReconnect();
482
- };
589
+ return;
590
+ }
483
591
 
484
- this.#eventSource.onopen = () => {
592
+ try {
485
593
  this.#reconnectAttempts = 0;
486
594
  this.#setState('connected');
487
595
  this.#options.logger.debug(
@@ -489,13 +597,27 @@ export class CoderSSEClient {
489
597
  this.#options.sessionId
490
598
  );
491
599
  this.#options.onOpen?.();
492
- };
493
600
 
494
- this.#handleEvent('snapshot', 'snapshot');
495
- this.#handleEvent('hydration', 'hydration');
496
- this.#handleEvent('presence', 'presence');
497
- this.#handleEvent('broadcast', 'broadcast');
498
- this.#handleEvent('message');
601
+ await readSSEStream(
602
+ response,
603
+ controller.signal,
604
+ (event) => this.#dispatchEvent(event),
605
+ this.#options.sessionId
606
+ );
607
+ } catch (err) {
608
+ if (this.#intentionallyClosed || isAbortError(err)) {
609
+ return;
610
+ }
611
+ this.#options.onError?.(err instanceof Error ? err : new Error(String(err)));
612
+ } finally {
613
+ if (this.#abortController === controller) {
614
+ this.#abortController = null;
615
+ }
616
+ }
617
+
618
+ if (!this.#intentionallyClosed) {
619
+ this.#scheduleReconnect();
620
+ }
499
621
  }
500
622
 
501
623
  #scheduleReconnect(): void {
@@ -600,13 +722,14 @@ export async function* streamCoderSessionSSE(
600
722
  return;
601
723
  }
602
724
 
603
- let eventSource: EventSource | null = null;
725
+ let activeController: AbortController | null = null;
604
726
  let reconnectAttempts = 0;
605
727
  const buffer: CoderSSEEvent[] = [];
606
728
  const MAX_BUFFER = 1000;
607
729
  let resolve: (() => void) | null = null;
608
730
  let done = false;
609
731
  let terminalError: Error | null = null;
732
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
610
733
 
611
734
  const wake = () => {
612
735
  if (resolve) {
@@ -616,49 +739,70 @@ export async function* streamCoderSessionSSE(
616
739
  };
617
740
 
618
741
  const cleanup = () => {
619
- if (eventSource) {
620
- eventSource.close();
621
- eventSource = null;
742
+ if (reconnectTimer !== null) {
743
+ clearTimeout(reconnectTimer);
744
+ reconnectTimer = null;
745
+ }
746
+ if (activeController) {
747
+ activeController.abort();
748
+ activeController = null;
622
749
  }
623
750
  };
624
751
 
625
- const handleSSEEvent = (eventName: string, typeOverride?: string) => {
626
- eventSource!.addEventListener(eventName, (event: Event) => {
627
- const data = getSSEData(event);
628
- if (!data) return;
629
- try {
630
- const parsed = JSON.parse(data);
631
- const payload = typeOverride ? { type: typeOverride, ...parsed } : parsed;
632
- const result = ObserverSseMessageSchema.safeParse(payload);
633
- if (result.success) {
634
- if (buffer.length >= MAX_BUFFER) {
635
- buffer.shift();
636
- logger.debug('SSE buffer full, dropped oldest event');
637
- }
638
- const semanticEvent = typeOverride || result.data.type;
639
- buffer.push({ event: semanticEvent, data: result.data });
640
- wake();
641
- } else {
642
- terminalError = new CoderSSEError({
643
- message: `Invalid SSE ${eventName} event format`,
644
- code: 'parse_error',
645
- sessionId: options.sessionId,
646
- });
647
- done = true;
648
- wake();
649
- return;
650
- }
651
- } catch (err) {
652
- terminalError = new CoderSSEError({
653
- message: `Failed to parse SSE ${eventName} event: ${err instanceof Error ? err.message : String(err)}`,
654
- code: 'parse_error',
752
+ const finish = (error?: Error) => {
753
+ if (error) {
754
+ terminalError = error;
755
+ }
756
+ done = true;
757
+ wake();
758
+ };
759
+
760
+ const scheduleReconnect = (error?: Error) => {
761
+ if (done) {
762
+ return;
763
+ }
764
+
765
+ if (signal?.aborted || (error && isAbortError(error))) {
766
+ finish();
767
+ return;
768
+ }
769
+
770
+ if (error && !isRetryableStreamError(error)) {
771
+ finish(error);
772
+ return;
773
+ }
774
+
775
+ if (!reconnect) {
776
+ finish(error);
777
+ return;
778
+ }
779
+
780
+ if (reconnectAttempts >= maxReconnectAttempts) {
781
+ finish(
782
+ new CoderSSEError({
783
+ message: `Exceeded maximum reconnection attempts (${maxReconnectAttempts})`,
784
+ code: 'max_reconnects_exceeded',
655
785
  sessionId: options.sessionId,
656
- });
657
- done = true;
658
- wake();
659
- return;
660
- }
661
- });
786
+ })
787
+ );
788
+ return;
789
+ }
790
+
791
+ const baseDelay = reconnectDelayMs * 2 ** reconnectAttempts;
792
+ const jitter = 0.5 + Math.random() * 0.5;
793
+ const delay = Math.min(Math.floor(baseDelay * jitter), maxReconnectDelayMs);
794
+
795
+ reconnectAttempts++;
796
+ logger.debug(
797
+ 'SSE connection lost, reconnecting in %dms (attempt %d)',
798
+ delay,
799
+ reconnectAttempts
800
+ );
801
+
802
+ reconnectTimer = setTimeout(() => {
803
+ reconnectTimer = null;
804
+ void connect();
805
+ }, delay);
662
806
  };
663
807
 
664
808
  const connect = async (): Promise<void> => {
@@ -673,94 +817,108 @@ export async function* streamCoderSessionSSE(
673
817
  logger,
674
818
  });
675
819
  } catch (err) {
676
- terminalError = err as Error;
677
- done = true;
678
- wake();
820
+ finish(err instanceof Error ? err : new Error(String(err)));
679
821
  return;
680
822
  }
681
823
 
682
824
  if (signal?.aborted) {
683
- done = true;
684
- wake();
825
+ finish();
685
826
  return;
686
827
  }
687
828
 
688
- // Workaround for bun-types EventSource constructor typing issue (see above).
829
+ const controller = new AbortController();
830
+ activeController = controller;
831
+ const abortFromCaller = () => controller.abort();
832
+ signal?.addEventListener('abort', abortFromCaller, { once: true });
833
+
834
+ let response: Response;
689
835
  try {
690
- const EventSourceCtor: typeof EventSource = EventSource;
691
- eventSource = new (EventSourceCtor as unknown as new (url: string) => EventSource)(url);
692
- } catch (err) {
693
- terminalError = new CoderSSEError({
694
- message: `Failed to create EventSource: ${err instanceof Error ? err.message : String(err)}`,
695
- code: 'connection_failed',
696
- sessionId: options.sessionId,
836
+ response = await fetch(url, {
837
+ headers: {
838
+ accept: 'text/event-stream',
839
+ },
840
+ signal: controller.signal,
697
841
  });
698
- done = true;
699
- wake();
842
+ } catch (err) {
843
+ signal?.removeEventListener('abort', abortFromCaller);
844
+ if (activeController === controller) {
845
+ activeController = null;
846
+ }
847
+ if (signal?.aborted || isAbortError(err)) {
848
+ finish();
849
+ return;
850
+ }
851
+ const error =
852
+ err instanceof CoderSSEError
853
+ ? err
854
+ : new CoderSSEError({
855
+ message: `Failed to connect SSE stream: ${err instanceof Error ? err.message : String(err)}`,
856
+ code: 'connection_failed',
857
+ sessionId: options.sessionId,
858
+ });
859
+ scheduleReconnect(error);
700
860
  return;
701
861
  }
702
862
 
703
863
  if (signal?.aborted) {
864
+ signal?.removeEventListener('abort', abortFromCaller);
704
865
  cleanup();
705
- done = true;
706
- wake();
866
+ finish();
707
867
  return;
708
868
  }
709
869
 
710
- eventSource.onerror = () => {
711
- cleanup();
712
-
713
- if (signal?.aborted) {
714
- done = true;
715
- wake();
716
- return;
870
+ if (!response.ok) {
871
+ signal?.removeEventListener('abort', abortFromCaller);
872
+ if (activeController === controller) {
873
+ activeController = null;
717
874
  }
875
+ scheduleReconnect(buildConnectionError(response, options.sessionId));
876
+ return;
877
+ }
718
878
 
719
- if (reconnect && reconnectAttempts < maxReconnectAttempts) {
720
- const baseDelay = reconnectDelayMs * 2 ** reconnectAttempts;
721
- const jitter = 0.5 + Math.random() * 0.5;
722
- const delay = Math.min(Math.floor(baseDelay * jitter), maxReconnectDelayMs);
723
-
724
- reconnectAttempts++;
725
- logger.debug(
726
- 'SSE connection lost, reconnecting in %dms (attempt %d)',
727
- delay,
728
- reconnectAttempts
729
- );
730
-
731
- setTimeout(() => {
732
- connect();
733
- }, delay);
734
- } else if (reconnect) {
735
- terminalError = new CoderSSEError({
736
- message: `Exceeded maximum reconnection attempts (${maxReconnectAttempts})`,
737
- code: 'max_reconnects_exceeded',
738
- sessionId: options.sessionId,
739
- });
740
- done = true;
741
- wake();
742
- } else {
743
- done = true;
879
+ reconnectAttempts = 0;
880
+ logger.debug('SSE connection established for session %s', options.sessionId);
881
+ let readFailed = false;
882
+
883
+ void readSSEStream(
884
+ response,
885
+ controller.signal,
886
+ (event) => {
887
+ if (buffer.length >= MAX_BUFFER) {
888
+ buffer.shift();
889
+ logger.debug('SSE buffer full, dropped oldest event');
890
+ }
891
+ buffer.push(event);
744
892
  wake();
745
- }
746
- };
893
+ },
894
+ options.sessionId
895
+ )
896
+ .catch((err) => {
897
+ readFailed = true;
898
+ scheduleReconnect(err instanceof Error ? err : new Error(String(err)));
899
+ })
900
+ .finally(() => {
901
+ signal?.removeEventListener('abort', abortFromCaller);
902
+ if (activeController === controller) {
903
+ activeController = null;
904
+ }
747
905
 
748
- eventSource.onopen = () => {
749
- reconnectAttempts = 0;
750
- logger.debug('SSE connection established for session %s', options.sessionId);
751
- };
906
+ if (done || signal?.aborted) {
907
+ finish();
908
+ return;
909
+ }
910
+
911
+ if (readFailed) {
912
+ return;
913
+ }
752
914
 
753
- handleSSEEvent('snapshot', 'snapshot');
754
- handleSSEEvent('hydration', 'hydration');
755
- handleSSEEvent('presence', 'presence');
756
- handleSSEEvent('broadcast', 'broadcast');
757
- handleSSEEvent('message');
915
+ scheduleReconnect();
916
+ });
758
917
  };
759
918
 
760
919
  const onAbort = () => {
761
- done = true;
762
920
  cleanup();
763
- wake();
921
+ finish();
764
922
  };
765
923
 
766
924
  signal?.addEventListener('abort', onAbort, { once: true });
@@ -791,6 +949,7 @@ export async function* streamCoderSessionSSE(
791
949
  }
792
950
  } finally {
793
951
  signal?.removeEventListener('abort', onAbort);
952
+ done = true;
794
953
  cleanup();
795
954
  }
796
955
  }