@agentuity/core 3.0.0-alpha.5 → 3.0.0-alpha.7

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 (110) hide show
  1. package/AGENTS.md +0 -1
  2. package/dist/index.d.ts +0 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +0 -3
  5. package/dist/index.js.map +1 -1
  6. package/dist/services/coder/api-reference.d.ts.map +1 -1
  7. package/dist/services/coder/api-reference.js +30 -1
  8. package/dist/services/coder/api-reference.js.map +1 -1
  9. package/dist/services/coder/client.d.ts +5 -1
  10. package/dist/services/coder/client.d.ts.map +1 -1
  11. package/dist/services/coder/client.js +8 -1
  12. package/dist/services/coder/client.js.map +1 -1
  13. package/dist/services/coder/index.d.ts +2 -2
  14. package/dist/services/coder/index.d.ts.map +1 -1
  15. package/dist/services/coder/index.js +1 -1
  16. package/dist/services/coder/index.js.map +1 -1
  17. package/dist/services/coder/protocol.d.ts +65 -0
  18. package/dist/services/coder/protocol.d.ts.map +1 -1
  19. package/dist/services/coder/protocol.js +8 -0
  20. package/dist/services/coder/protocol.js.map +1 -1
  21. package/dist/services/coder/sessions.d.ts +22 -0
  22. package/dist/services/coder/sessions.d.ts.map +1 -1
  23. package/dist/services/coder/sessions.js +10 -1
  24. package/dist/services/coder/sessions.js.map +1 -1
  25. package/dist/services/coder/sse.d.ts +2 -2
  26. package/dist/services/coder/sse.d.ts.map +1 -1
  27. package/dist/services/coder/sse.js +290 -178
  28. package/dist/services/coder/sse.js.map +1 -1
  29. package/dist/services/coder/types.d.ts +554 -0
  30. package/dist/services/coder/types.d.ts.map +1 -1
  31. package/dist/services/coder/types.js +138 -0
  32. package/dist/services/coder/types.js.map +1 -1
  33. package/dist/services/index.d.ts +0 -2
  34. package/dist/services/index.d.ts.map +1 -1
  35. package/dist/services/index.js +0 -2
  36. package/dist/services/index.js.map +1 -1
  37. package/dist/services/sandbox/run.d.ts.map +1 -1
  38. package/dist/services/sandbox/run.js +15 -2
  39. package/dist/services/sandbox/run.js.map +1 -1
  40. package/package.json +2 -16
  41. package/src/index.ts +0 -15
  42. package/src/services/coder/api-reference.ts +31 -0
  43. package/src/services/coder/client.ts +12 -0
  44. package/src/services/coder/index.ts +3 -0
  45. package/src/services/coder/protocol.ts +12 -0
  46. package/src/services/coder/sessions.ts +26 -0
  47. package/src/services/coder/sse.ts +343 -184
  48. package/src/services/coder/types.ts +179 -0
  49. package/src/services/index.ts +0 -2
  50. package/src/services/sandbox/run.ts +13 -2
  51. package/dist/services/auth/index.d.ts +0 -7
  52. package/dist/services/auth/index.d.ts.map +0 -1
  53. package/dist/services/auth/index.js +0 -7
  54. package/dist/services/auth/index.js.map +0 -1
  55. package/dist/services/auth/types.d.ts +0 -192
  56. package/dist/services/auth/types.d.ts.map +0 -1
  57. package/dist/services/auth/types.js +0 -11
  58. package/dist/services/auth/types.js.map +0 -1
  59. package/dist/services/eval/api-reference.d.ts +0 -4
  60. package/dist/services/eval/api-reference.d.ts.map +0 -1
  61. package/dist/services/eval/api-reference.js +0 -121
  62. package/dist/services/eval/api-reference.js.map +0 -1
  63. package/dist/services/eval/events.d.ts +0 -93
  64. package/dist/services/eval/events.d.ts.map +0 -1
  65. package/dist/services/eval/events.js +0 -24
  66. package/dist/services/eval/events.js.map +0 -1
  67. package/dist/services/eval/get.d.ts +0 -36
  68. package/dist/services/eval/get.d.ts.map +0 -1
  69. package/dist/services/eval/get.js +0 -23
  70. package/dist/services/eval/get.js.map +0 -1
  71. package/dist/services/eval/index.d.ts +0 -6
  72. package/dist/services/eval/index.d.ts.map +0 -1
  73. package/dist/services/eval/index.js +0 -6
  74. package/dist/services/eval/index.js.map +0 -1
  75. package/dist/services/eval/list.d.ts +0 -50
  76. package/dist/services/eval/list.d.ts.map +0 -1
  77. package/dist/services/eval/list.js +0 -32
  78. package/dist/services/eval/list.js.map +0 -1
  79. package/dist/services/eval/run-get.d.ts +0 -48
  80. package/dist/services/eval/run-get.d.ts.map +0 -1
  81. package/dist/services/eval/run-get.js +0 -29
  82. package/dist/services/eval/run-get.js.map +0 -1
  83. package/dist/services/eval/run-list.d.ts +0 -70
  84. package/dist/services/eval/run-list.d.ts.map +0 -1
  85. package/dist/services/eval/run-list.js +0 -42
  86. package/dist/services/eval/run-list.js.map +0 -1
  87. package/dist/webrtc.d.ts +0 -243
  88. package/dist/webrtc.d.ts.map +0 -1
  89. package/dist/webrtc.js +0 -5
  90. package/dist/webrtc.js.map +0 -1
  91. package/dist/workbench-config.d.ts +0 -62
  92. package/dist/workbench-config.d.ts.map +0 -1
  93. package/dist/workbench-config.js +0 -58
  94. package/dist/workbench-config.js.map +0 -1
  95. package/dist/workbench.d.ts +0 -2
  96. package/dist/workbench.d.ts.map +0 -1
  97. package/dist/workbench.js +0 -2
  98. package/dist/workbench.js.map +0 -1
  99. package/src/services/auth/index.ts +0 -19
  100. package/src/services/auth/types.ts +0 -223
  101. package/src/services/eval/api-reference.ts +0 -124
  102. package/src/services/eval/events.ts +0 -92
  103. package/src/services/eval/get.ts +0 -33
  104. package/src/services/eval/index.ts +0 -29
  105. package/src/services/eval/list.ts +0 -49
  106. package/src/services/eval/run-get.ts +0 -39
  107. package/src/services/eval/run-list.ts +0 -59
  108. package/src/webrtc.ts +0 -259
  109. package/src/workbench-config.ts +0 -79
  110. package/src/workbench.ts +0 -1
@@ -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
  * }
@@ -189,12 +189,124 @@ async function buildSSEUrl(sessionId, options) {
189
189
  const queryString = params.toString();
190
190
  return queryString ? `${baseUrl}${path}?${queryString}` : `${baseUrl}${path}`;
191
191
  }
192
- function getSSEData(event) {
193
- const msgEvent = event;
194
- if (typeof msgEvent.data === 'string') {
195
- return msgEvent.data;
192
+ const TYPED_TRANSPORT_EVENTS = new Set(['snapshot', 'hydration', 'presence', 'broadcast']);
193
+ function isAbortError(err) {
194
+ return err instanceof Error && err.name === 'AbortError';
195
+ }
196
+ function parseSSEFrame(block) {
197
+ let event = 'message';
198
+ const dataLines = [];
199
+ for (const line of block.split('\n')) {
200
+ if (!line || line.startsWith(':'))
201
+ continue;
202
+ const separatorIndex = line.indexOf(':');
203
+ const field = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
204
+ let value = separatorIndex === -1 ? '' : line.slice(separatorIndex + 1);
205
+ if (value.startsWith(' ')) {
206
+ value = value.slice(1);
207
+ }
208
+ if (field === 'event') {
209
+ event = value || 'message';
210
+ }
211
+ else if (field === 'data') {
212
+ dataLines.push(value);
213
+ }
214
+ }
215
+ if (dataLines.length === 0) {
216
+ return null;
217
+ }
218
+ return {
219
+ event,
220
+ data: dataLines.join('\n'),
221
+ };
222
+ }
223
+ function consumeSSEBuffer(rawBuffer, onFrame) {
224
+ const normalized = rawBuffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
225
+ let cursor = 0;
226
+ while (true) {
227
+ const boundary = normalized.indexOf('\n\n', cursor);
228
+ if (boundary === -1)
229
+ break;
230
+ const block = normalized.slice(cursor, boundary);
231
+ cursor = boundary + 2;
232
+ if (!block.trim())
233
+ continue;
234
+ const frame = parseSSEFrame(block);
235
+ if (frame) {
236
+ onFrame(frame);
237
+ }
238
+ }
239
+ return normalized.slice(cursor);
240
+ }
241
+ function decodeCoderSSEEvent(frame, sessionId) {
242
+ let parsed;
243
+ try {
244
+ parsed = JSON.parse(frame.data);
245
+ }
246
+ catch (err) {
247
+ throw new CoderSSEError({
248
+ message: `Failed to parse SSE ${frame.event} event: ${err instanceof Error ? err.message : String(err)}`,
249
+ code: 'parse_error',
250
+ sessionId,
251
+ });
252
+ }
253
+ const payload = TYPED_TRANSPORT_EVENTS.has(frame.event) && parsed && typeof parsed === 'object'
254
+ ? { type: frame.event, ...parsed }
255
+ : parsed;
256
+ const result = ObserverSseMessageSchema.safeParse(payload);
257
+ if (!result.success) {
258
+ throw new CoderSSEError({
259
+ message: `Invalid SSE ${frame.event} event format`,
260
+ code: 'parse_error',
261
+ sessionId,
262
+ });
263
+ }
264
+ const event = frame.event === 'message' ? result.data.type : frame.event;
265
+ return { event, data: result.data };
266
+ }
267
+ async function readSSEStream(response, signal, onEvent, sessionId) {
268
+ if (!response.body) {
269
+ throw new CoderSSEError({
270
+ message: 'SSE response did not include a readable body',
271
+ code: 'connection_failed',
272
+ sessionId,
273
+ });
274
+ }
275
+ const reader = response.body.getReader();
276
+ const decoder = new TextDecoder();
277
+ let buffer = '';
278
+ try {
279
+ while (!signal.aborted) {
280
+ const { done, value } = await reader.read();
281
+ if (done)
282
+ break;
283
+ buffer += decoder.decode(value, { stream: true });
284
+ buffer = consumeSSEBuffer(buffer, (frame) => {
285
+ onEvent(decodeCoderSSEEvent(frame, sessionId));
286
+ });
287
+ }
288
+ }
289
+ finally {
290
+ try {
291
+ reader.releaseLock();
292
+ }
293
+ catch {
294
+ // Some runtimes throw if the reader is already released after abort.
295
+ }
296
+ }
297
+ }
298
+ function buildConnectionError(response, sessionId) {
299
+ return new CoderSSEError({
300
+ message: `SSE connection failed: ${response.status} ${response.statusText || 'HTTP error'}`,
301
+ code: response.status === 401 || response.status === 403 ? 'auth_failed' : 'connection_failed',
302
+ sessionId,
303
+ });
304
+ }
305
+ function isRetryableStreamError(error) {
306
+ if (error instanceof CoderSSEError) {
307
+ return error.code === 'connection_failed';
196
308
  }
197
- return null;
309
+ return true;
198
310
  }
199
311
  /**
200
312
  * Class-based SSE client for observing Coder Hub sessions.
@@ -225,7 +337,7 @@ function getSSEData(event) {
225
337
  export class CoderSSEClient {
226
338
  #options;
227
339
  #state = 'closed';
228
- #eventSource = null;
340
+ #abortController = null;
229
341
  #reconnectAttempts = 0;
230
342
  #reconnectTimer = null;
231
343
  #intentionallyClosed = false;
@@ -267,7 +379,7 @@ export class CoderSSEClient {
267
379
  * Whether the client is currently connected and receiving events.
268
380
  */
269
381
  get isConnected() {
270
- return this.#state === 'connected' && this.#eventSource?.readyState === 1;
382
+ return this.#state === 'connected' && this.#abortController !== null;
271
383
  }
272
384
  /**
273
385
  * Establish the SSE connection and start receiving events.
@@ -298,9 +410,9 @@ export class CoderSSEClient {
298
410
  clearTimeout(this.#reconnectTimer);
299
411
  this.#reconnectTimer = null;
300
412
  }
301
- if (this.#eventSource) {
302
- this.#eventSource.close();
303
- this.#eventSource = null;
413
+ if (this.#abortController) {
414
+ this.#abortController.abort();
415
+ this.#abortController = null;
304
416
  }
305
417
  this.#state = 'closed';
306
418
  this.#options.onClose?.();
@@ -308,50 +420,20 @@ export class CoderSSEClient {
308
420
  #setState(state) {
309
421
  this.#state = state;
310
422
  }
311
- #handleEvent(eventName, typeOverride) {
312
- this.#eventSource.addEventListener(eventName, (event) => {
313
- const data = getSSEData(event);
314
- if (!data)
315
- return;
316
- try {
317
- const parsed = JSON.parse(data);
318
- const payload = typeOverride ? { type: typeOverride, ...parsed } : parsed;
319
- const result = ObserverSseMessageSchema.safeParse(payload);
320
- if (result.success) {
321
- const semanticEvent = typeOverride || result.data.type;
322
- const sseEvent = { event: semanticEvent, data: result.data };
323
- this.#options.onEvent?.(sseEvent);
324
- if (result.data.type === 'snapshot') {
325
- this.#options.onSnapshot?.(result.data);
326
- }
327
- else if (result.data.type === 'hydration') {
328
- this.#options.onHydration?.(result.data);
329
- }
330
- else if (result.data.type === 'presence') {
331
- this.#options.onPresence?.(result.data);
332
- }
333
- else if (result.data.type === 'broadcast') {
334
- this.#options.onBroadcast?.(result.data);
335
- }
336
- }
337
- else {
338
- const parseError = new CoderSSEError({
339
- message: `Invalid SSE ${eventName} event format`,
340
- code: 'parse_error',
341
- sessionId: this.#options.sessionId,
342
- });
343
- this.#options.onError?.(parseError);
344
- }
345
- }
346
- catch (err) {
347
- const parseError = new CoderSSEError({
348
- message: `Failed to parse SSE ${eventName} event: ${err instanceof Error ? err.message : String(err)}`,
349
- code: 'parse_error',
350
- sessionId: this.#options.sessionId,
351
- });
352
- this.#options.onError?.(parseError);
353
- }
354
- });
423
+ #dispatchEvent(event) {
424
+ this.#options.onEvent?.(event);
425
+ if (event.data.type === 'snapshot') {
426
+ this.#options.onSnapshot?.(event.data);
427
+ }
428
+ else if (event.data.type === 'hydration') {
429
+ this.#options.onHydration?.(event.data);
430
+ }
431
+ else if (event.data.type === 'presence') {
432
+ this.#options.onPresence?.(event.data);
433
+ }
434
+ else if (event.data.type === 'broadcast') {
435
+ this.#options.onBroadcast?.(event.data);
436
+ }
355
437
  }
356
438
  async #connectInternal() {
357
439
  if (this.#intentionallyClosed) {
@@ -370,46 +452,60 @@ export class CoderSSEClient {
370
452
  if (this.#intentionallyClosed || this.#state === 'closed') {
371
453
  return;
372
454
  }
373
- // Workaround for bun-types EventSource constructor typing issue.
374
- // The type definitions don't match the runtime signature, so we use
375
- // a double type assertion to construct EventSource with a URL parameter.
455
+ const controller = new AbortController();
456
+ this.#abortController = controller;
457
+ let response;
376
458
  try {
377
- const EventSourceCtor = EventSource;
378
- this.#eventSource = new EventSourceCtor(url);
459
+ response = await fetch(url, {
460
+ headers: {
461
+ accept: 'text/event-stream',
462
+ },
463
+ signal: controller.signal,
464
+ });
379
465
  }
380
466
  catch (err) {
467
+ this.#abortController = null;
468
+ if (this.#intentionallyClosed || isAbortError(err)) {
469
+ this.#setState('closed');
470
+ return;
471
+ }
381
472
  this.#setState('closed');
382
473
  this.#options.onError?.(new CoderSSEError({
383
- message: `Failed to create EventSource: ${err instanceof Error ? err.message : String(err)}`,
474
+ message: `Failed to connect SSE stream: ${err instanceof Error ? err.message : String(err)}`,
384
475
  code: 'connection_failed',
385
476
  sessionId: this.#options.sessionId,
386
477
  }));
387
478
  this.#scheduleReconnect();
388
479
  return;
389
480
  }
390
- this.#eventSource.onerror = () => {
391
- // Notify caller of transient error before reconnecting
392
- this.#options.onError?.(new Error('EventSource transient error'));
393
- if (this.#eventSource) {
394
- this.#eventSource.close();
395
- this.#eventSource = null;
396
- }
397
- if (this.#intentionallyClosed) {
398
- return;
399
- }
481
+ if (!response.ok) {
482
+ this.#abortController = null;
483
+ this.#setState('closed');
484
+ this.#options.onError?.(buildConnectionError(response, this.#options.sessionId));
400
485
  this.#scheduleReconnect();
401
- };
402
- this.#eventSource.onopen = () => {
486
+ return;
487
+ }
488
+ try {
403
489
  this.#reconnectAttempts = 0;
404
490
  this.#setState('connected');
405
491
  this.#options.logger.debug('SSE connection established for session %s', this.#options.sessionId);
406
492
  this.#options.onOpen?.();
407
- };
408
- this.#handleEvent('snapshot', 'snapshot');
409
- this.#handleEvent('hydration', 'hydration');
410
- this.#handleEvent('presence', 'presence');
411
- this.#handleEvent('broadcast', 'broadcast');
412
- this.#handleEvent('message');
493
+ await readSSEStream(response, controller.signal, (event) => this.#dispatchEvent(event), this.#options.sessionId);
494
+ }
495
+ catch (err) {
496
+ if (this.#intentionallyClosed || isAbortError(err)) {
497
+ return;
498
+ }
499
+ this.#options.onError?.(err instanceof Error ? err : new Error(String(err)));
500
+ }
501
+ finally {
502
+ if (this.#abortController === controller) {
503
+ this.#abortController = null;
504
+ }
505
+ }
506
+ if (!this.#intentionallyClosed) {
507
+ this.#scheduleReconnect();
508
+ }
413
509
  }
414
510
  #scheduleReconnect() {
415
511
  if (this.#intentionallyClosed || !this.#options.reconnect) {
@@ -498,13 +594,14 @@ export async function* streamCoderSessionSSE(options) {
498
594
  if (signal?.aborted) {
499
595
  return;
500
596
  }
501
- let eventSource = null;
597
+ let activeController = null;
502
598
  let reconnectAttempts = 0;
503
599
  const buffer = [];
504
600
  const MAX_BUFFER = 1000;
505
601
  let resolve = null;
506
602
  let done = false;
507
603
  let terminalError = null;
604
+ let reconnectTimer = null;
508
605
  const wake = () => {
509
606
  if (resolve) {
510
607
  resolve();
@@ -512,51 +609,55 @@ export async function* streamCoderSessionSSE(options) {
512
609
  }
513
610
  };
514
611
  const cleanup = () => {
515
- if (eventSource) {
516
- eventSource.close();
517
- eventSource = null;
612
+ if (reconnectTimer !== null) {
613
+ clearTimeout(reconnectTimer);
614
+ reconnectTimer = null;
615
+ }
616
+ if (activeController) {
617
+ activeController.abort();
618
+ activeController = null;
518
619
  }
519
620
  };
520
- const handleSSEEvent = (eventName, typeOverride) => {
521
- eventSource.addEventListener(eventName, (event) => {
522
- const data = getSSEData(event);
523
- if (!data)
524
- return;
525
- try {
526
- const parsed = JSON.parse(data);
527
- const payload = typeOverride ? { type: typeOverride, ...parsed } : parsed;
528
- const result = ObserverSseMessageSchema.safeParse(payload);
529
- if (result.success) {
530
- if (buffer.length >= MAX_BUFFER) {
531
- buffer.shift();
532
- logger.debug('SSE buffer full, dropped oldest event');
533
- }
534
- const semanticEvent = typeOverride || result.data.type;
535
- buffer.push({ event: semanticEvent, data: result.data });
536
- wake();
537
- }
538
- else {
539
- terminalError = new CoderSSEError({
540
- message: `Invalid SSE ${eventName} event format`,
541
- code: 'parse_error',
542
- sessionId: options.sessionId,
543
- });
544
- done = true;
545
- wake();
546
- return;
547
- }
548
- }
549
- catch (err) {
550
- terminalError = new CoderSSEError({
551
- message: `Failed to parse SSE ${eventName} event: ${err instanceof Error ? err.message : String(err)}`,
552
- code: 'parse_error',
553
- sessionId: options.sessionId,
554
- });
555
- done = true;
556
- wake();
557
- return;
558
- }
559
- });
621
+ const finish = (error) => {
622
+ if (error) {
623
+ terminalError = error;
624
+ }
625
+ done = true;
626
+ wake();
627
+ };
628
+ const scheduleReconnect = (error) => {
629
+ if (done) {
630
+ return;
631
+ }
632
+ if (signal?.aborted || (error && isAbortError(error))) {
633
+ finish();
634
+ return;
635
+ }
636
+ if (error && !isRetryableStreamError(error)) {
637
+ finish(error);
638
+ return;
639
+ }
640
+ if (!reconnect) {
641
+ finish(error);
642
+ return;
643
+ }
644
+ if (reconnectAttempts >= maxReconnectAttempts) {
645
+ finish(new CoderSSEError({
646
+ message: `Exceeded maximum reconnection attempts (${maxReconnectAttempts})`,
647
+ code: 'max_reconnects_exceeded',
648
+ sessionId: options.sessionId,
649
+ }));
650
+ return;
651
+ }
652
+ const baseDelay = reconnectDelayMs * 2 ** reconnectAttempts;
653
+ const jitter = 0.5 + Math.random() * 0.5;
654
+ const delay = Math.min(Math.floor(baseDelay * jitter), maxReconnectDelayMs);
655
+ reconnectAttempts++;
656
+ logger.debug('SSE connection lost, reconnecting in %dms (attempt %d)', delay, reconnectAttempts);
657
+ reconnectTimer = setTimeout(() => {
658
+ reconnectTimer = null;
659
+ void connect();
660
+ }, delay);
560
661
  };
561
662
  const connect = async () => {
562
663
  if (done || signal?.aborted) {
@@ -570,82 +671,92 @@ export async function* streamCoderSessionSSE(options) {
570
671
  });
571
672
  }
572
673
  catch (err) {
573
- terminalError = err;
574
- done = true;
575
- wake();
674
+ finish(err instanceof Error ? err : new Error(String(err)));
576
675
  return;
577
676
  }
578
677
  if (signal?.aborted) {
579
- done = true;
580
- wake();
678
+ finish();
581
679
  return;
582
680
  }
583
- // Workaround for bun-types EventSource constructor typing issue (see above).
681
+ const controller = new AbortController();
682
+ activeController = controller;
683
+ const abortFromCaller = () => controller.abort();
684
+ signal?.addEventListener('abort', abortFromCaller, { once: true });
685
+ let response;
584
686
  try {
585
- const EventSourceCtor = EventSource;
586
- eventSource = new EventSourceCtor(url);
687
+ response = await fetch(url, {
688
+ headers: {
689
+ accept: 'text/event-stream',
690
+ },
691
+ signal: controller.signal,
692
+ });
587
693
  }
588
694
  catch (err) {
589
- terminalError = new CoderSSEError({
590
- message: `Failed to create EventSource: ${err instanceof Error ? err.message : String(err)}`,
591
- code: 'connection_failed',
592
- sessionId: options.sessionId,
593
- });
594
- done = true;
595
- wake();
695
+ signal?.removeEventListener('abort', abortFromCaller);
696
+ if (activeController === controller) {
697
+ activeController = null;
698
+ }
699
+ if (signal?.aborted || isAbortError(err)) {
700
+ finish();
701
+ return;
702
+ }
703
+ const error = err instanceof CoderSSEError
704
+ ? err
705
+ : new CoderSSEError({
706
+ message: `Failed to connect SSE stream: ${err instanceof Error ? err.message : String(err)}`,
707
+ code: 'connection_failed',
708
+ sessionId: options.sessionId,
709
+ });
710
+ scheduleReconnect(error);
596
711
  return;
597
712
  }
598
713
  if (signal?.aborted) {
714
+ signal?.removeEventListener('abort', abortFromCaller);
599
715
  cleanup();
600
- done = true;
601
- wake();
716
+ finish();
602
717
  return;
603
718
  }
604
- eventSource.onerror = () => {
605
- cleanup();
606
- if (signal?.aborted) {
607
- done = true;
608
- wake();
609
- return;
719
+ if (!response.ok) {
720
+ signal?.removeEventListener('abort', abortFromCaller);
721
+ if (activeController === controller) {
722
+ activeController = null;
610
723
  }
611
- if (reconnect && reconnectAttempts < maxReconnectAttempts) {
612
- const baseDelay = reconnectDelayMs * 2 ** reconnectAttempts;
613
- const jitter = 0.5 + Math.random() * 0.5;
614
- const delay = Math.min(Math.floor(baseDelay * jitter), maxReconnectDelayMs);
615
- reconnectAttempts++;
616
- logger.debug('SSE connection lost, reconnecting in %dms (attempt %d)', delay, reconnectAttempts);
617
- setTimeout(() => {
618
- connect();
619
- }, delay);
724
+ scheduleReconnect(buildConnectionError(response, options.sessionId));
725
+ return;
726
+ }
727
+ reconnectAttempts = 0;
728
+ logger.debug('SSE connection established for session %s', options.sessionId);
729
+ let readFailed = false;
730
+ void readSSEStream(response, controller.signal, (event) => {
731
+ if (buffer.length >= MAX_BUFFER) {
732
+ buffer.shift();
733
+ logger.debug('SSE buffer full, dropped oldest event');
620
734
  }
621
- else if (reconnect) {
622
- terminalError = new CoderSSEError({
623
- message: `Exceeded maximum reconnection attempts (${maxReconnectAttempts})`,
624
- code: 'max_reconnects_exceeded',
625
- sessionId: options.sessionId,
626
- });
627
- done = true;
628
- wake();
735
+ buffer.push(event);
736
+ wake();
737
+ }, options.sessionId)
738
+ .catch((err) => {
739
+ readFailed = true;
740
+ scheduleReconnect(err instanceof Error ? err : new Error(String(err)));
741
+ })
742
+ .finally(() => {
743
+ signal?.removeEventListener('abort', abortFromCaller);
744
+ if (activeController === controller) {
745
+ activeController = null;
629
746
  }
630
- else {
631
- done = true;
632
- wake();
747
+ if (done || signal?.aborted) {
748
+ finish();
749
+ return;
633
750
  }
634
- };
635
- eventSource.onopen = () => {
636
- reconnectAttempts = 0;
637
- logger.debug('SSE connection established for session %s', options.sessionId);
638
- };
639
- handleSSEEvent('snapshot', 'snapshot');
640
- handleSSEEvent('hydration', 'hydration');
641
- handleSSEEvent('presence', 'presence');
642
- handleSSEEvent('broadcast', 'broadcast');
643
- handleSSEEvent('message');
751
+ if (readFailed) {
752
+ return;
753
+ }
754
+ scheduleReconnect();
755
+ });
644
756
  };
645
757
  const onAbort = () => {
646
- done = true;
647
758
  cleanup();
648
- wake();
759
+ finish();
649
760
  };
650
761
  signal?.addEventListener('abort', onAbort, { once: true });
651
762
  await connect();
@@ -670,6 +781,7 @@ export async function* streamCoderSessionSSE(options) {
670
781
  }
671
782
  finally {
672
783
  signal?.removeEventListener('abort', onAbort);
784
+ done = true;
673
785
  cleanup();
674
786
  }
675
787
  }