@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.
- package/AGENTS.md +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -3
- package/dist/index.js.map +1 -1
- package/dist/services/coder/api-reference.d.ts.map +1 -1
- package/dist/services/coder/api-reference.js +30 -1
- package/dist/services/coder/api-reference.js.map +1 -1
- package/dist/services/coder/client.d.ts +5 -1
- package/dist/services/coder/client.d.ts.map +1 -1
- package/dist/services/coder/client.js +8 -1
- package/dist/services/coder/client.js.map +1 -1
- package/dist/services/coder/index.d.ts +2 -2
- package/dist/services/coder/index.d.ts.map +1 -1
- package/dist/services/coder/index.js +1 -1
- package/dist/services/coder/index.js.map +1 -1
- package/dist/services/coder/protocol.d.ts +65 -0
- package/dist/services/coder/protocol.d.ts.map +1 -1
- package/dist/services/coder/protocol.js +8 -0
- package/dist/services/coder/protocol.js.map +1 -1
- package/dist/services/coder/sessions.d.ts +22 -0
- package/dist/services/coder/sessions.d.ts.map +1 -1
- package/dist/services/coder/sessions.js +10 -1
- package/dist/services/coder/sessions.js.map +1 -1
- package/dist/services/coder/sse.d.ts +2 -2
- package/dist/services/coder/sse.d.ts.map +1 -1
- package/dist/services/coder/sse.js +290 -178
- package/dist/services/coder/sse.js.map +1 -1
- package/dist/services/coder/types.d.ts +554 -0
- package/dist/services/coder/types.d.ts.map +1 -1
- package/dist/services/coder/types.js +138 -0
- package/dist/services/coder/types.js.map +1 -1
- package/dist/services/index.d.ts +0 -2
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +0 -2
- package/dist/services/index.js.map +1 -1
- package/dist/services/sandbox/run.d.ts.map +1 -1
- package/dist/services/sandbox/run.js +15 -2
- package/dist/services/sandbox/run.js.map +1 -1
- package/package.json +2 -16
- package/src/index.ts +0 -15
- package/src/services/coder/api-reference.ts +31 -0
- package/src/services/coder/client.ts +12 -0
- package/src/services/coder/index.ts +3 -0
- package/src/services/coder/protocol.ts +12 -0
- package/src/services/coder/sessions.ts +26 -0
- package/src/services/coder/sse.ts +343 -184
- package/src/services/coder/types.ts +179 -0
- package/src/services/index.ts +0 -2
- package/src/services/sandbox/run.ts +13 -2
- package/dist/services/auth/index.d.ts +0 -7
- package/dist/services/auth/index.d.ts.map +0 -1
- package/dist/services/auth/index.js +0 -7
- package/dist/services/auth/index.js.map +0 -1
- package/dist/services/auth/types.d.ts +0 -192
- package/dist/services/auth/types.d.ts.map +0 -1
- package/dist/services/auth/types.js +0 -11
- package/dist/services/auth/types.js.map +0 -1
- package/dist/services/eval/api-reference.d.ts +0 -4
- package/dist/services/eval/api-reference.d.ts.map +0 -1
- package/dist/services/eval/api-reference.js +0 -121
- package/dist/services/eval/api-reference.js.map +0 -1
- package/dist/services/eval/events.d.ts +0 -93
- package/dist/services/eval/events.d.ts.map +0 -1
- package/dist/services/eval/events.js +0 -24
- package/dist/services/eval/events.js.map +0 -1
- package/dist/services/eval/get.d.ts +0 -36
- package/dist/services/eval/get.d.ts.map +0 -1
- package/dist/services/eval/get.js +0 -23
- package/dist/services/eval/get.js.map +0 -1
- package/dist/services/eval/index.d.ts +0 -6
- package/dist/services/eval/index.d.ts.map +0 -1
- package/dist/services/eval/index.js +0 -6
- package/dist/services/eval/index.js.map +0 -1
- package/dist/services/eval/list.d.ts +0 -50
- package/dist/services/eval/list.d.ts.map +0 -1
- package/dist/services/eval/list.js +0 -32
- package/dist/services/eval/list.js.map +0 -1
- package/dist/services/eval/run-get.d.ts +0 -48
- package/dist/services/eval/run-get.d.ts.map +0 -1
- package/dist/services/eval/run-get.js +0 -29
- package/dist/services/eval/run-get.js.map +0 -1
- package/dist/services/eval/run-list.d.ts +0 -70
- package/dist/services/eval/run-list.d.ts.map +0 -1
- package/dist/services/eval/run-list.js +0 -42
- package/dist/services/eval/run-list.js.map +0 -1
- package/dist/webrtc.d.ts +0 -243
- package/dist/webrtc.d.ts.map +0 -1
- package/dist/webrtc.js +0 -5
- package/dist/webrtc.js.map +0 -1
- package/dist/workbench-config.d.ts +0 -62
- package/dist/workbench-config.d.ts.map +0 -1
- package/dist/workbench-config.js +0 -58
- package/dist/workbench-config.js.map +0 -1
- package/dist/workbench.d.ts +0 -2
- package/dist/workbench.d.ts.map +0 -1
- package/dist/workbench.js +0 -2
- package/dist/workbench.js.map +0 -1
- package/src/services/auth/index.ts +0 -19
- package/src/services/auth/types.ts +0 -223
- package/src/services/eval/api-reference.ts +0 -124
- package/src/services/eval/events.ts +0 -92
- package/src/services/eval/get.ts +0 -33
- package/src/services/eval/index.ts +0 -29
- package/src/services/eval/list.ts +0 -49
- package/src/services/eval/run-get.ts +0 -39
- package/src/services/eval/run-list.ts +0 -59
- package/src/webrtc.ts +0 -259
- package/src/workbench-config.ts +0 -79
- 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.
|
|
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', '
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
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
|
-
#
|
|
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.#
|
|
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.#
|
|
373
|
-
this.#
|
|
374
|
-
this.#
|
|
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
|
-
#
|
|
385
|
-
this.#
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
555
|
+
const controller = new AbortController();
|
|
556
|
+
this.#abortController = controller;
|
|
557
|
+
|
|
558
|
+
let response: Response;
|
|
450
559
|
try {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
this.#
|
|
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
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
|
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 (
|
|
620
|
-
|
|
621
|
-
|
|
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
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
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
|
-
|
|
684
|
-
wake();
|
|
825
|
+
finish();
|
|
685
826
|
return;
|
|
686
827
|
}
|
|
687
828
|
|
|
688
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
-
|
|
699
|
-
|
|
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
|
-
|
|
706
|
-
wake();
|
|
866
|
+
finish();
|
|
707
867
|
return;
|
|
708
868
|
}
|
|
709
869
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
906
|
+
if (done || signal?.aborted) {
|
|
907
|
+
finish();
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (readFailed) {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
752
914
|
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
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
|
}
|