@copilotkit/aimock 1.46.0 → 1.47.0

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 (183) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +12 -1
  4. package/README.md +4 -1
  5. package/dist/aws-event-stream.cjs +20 -23
  6. package/dist/aws-event-stream.cjs.map +1 -1
  7. package/dist/aws-event-stream.d.cts +0 -1
  8. package/dist/aws-event-stream.d.ts +0 -1
  9. package/dist/aws-event-stream.js +20 -24
  10. package/dist/aws-event-stream.js.map +1 -1
  11. package/dist/bedrock-converse.cjs +79 -82
  12. package/dist/bedrock-converse.cjs.map +1 -1
  13. package/dist/bedrock-converse.d.cts.map +1 -1
  14. package/dist/bedrock-converse.d.ts.map +1 -1
  15. package/dist/bedrock-converse.js +80 -83
  16. package/dist/bedrock-converse.js.map +1 -1
  17. package/dist/bedrock.cjs +112 -121
  18. package/dist/bedrock.cjs.map +1 -1
  19. package/dist/bedrock.d.cts.map +1 -1
  20. package/dist/bedrock.d.ts.map +1 -1
  21. package/dist/bedrock.js +113 -122
  22. package/dist/bedrock.js.map +1 -1
  23. package/dist/cohere.cjs +58 -106
  24. package/dist/cohere.cjs.map +1 -1
  25. package/dist/cohere.d.cts.map +1 -1
  26. package/dist/cohere.d.ts.map +1 -1
  27. package/dist/cohere.js +60 -108
  28. package/dist/cohere.js.map +1 -1
  29. package/dist/config-loader.d.cts.map +1 -1
  30. package/dist/config-loader.d.ts.map +1 -1
  31. package/dist/fixture-loader.cjs +103 -2
  32. package/dist/fixture-loader.cjs.map +1 -1
  33. package/dist/fixture-loader.d.cts.map +1 -1
  34. package/dist/fixture-loader.d.ts.map +1 -1
  35. package/dist/fixture-loader.js +104 -4
  36. package/dist/fixture-loader.js.map +1 -1
  37. package/dist/gemini-interactions.cjs +63 -121
  38. package/dist/gemini-interactions.cjs.map +1 -1
  39. package/dist/gemini-interactions.d.cts.map +1 -1
  40. package/dist/gemini-interactions.d.ts.map +1 -1
  41. package/dist/gemini-interactions.js +65 -123
  42. package/dist/gemini-interactions.js.map +1 -1
  43. package/dist/gemini.cjs +242 -155
  44. package/dist/gemini.cjs.map +1 -1
  45. package/dist/gemini.d.cts.map +1 -1
  46. package/dist/gemini.d.ts.map +1 -1
  47. package/dist/gemini.js +244 -157
  48. package/dist/gemini.js.map +1 -1
  49. package/dist/helpers.cjs +400 -7
  50. package/dist/helpers.cjs.map +1 -1
  51. package/dist/helpers.d.cts +51 -1
  52. package/dist/helpers.d.cts.map +1 -1
  53. package/dist/helpers.d.ts +51 -1
  54. package/dist/helpers.d.ts.map +1 -1
  55. package/dist/helpers.js +391 -8
  56. package/dist/helpers.js.map +1 -1
  57. package/dist/index.cjs +4 -2
  58. package/dist/index.d.cts +4 -4
  59. package/dist/index.d.ts +4 -4
  60. package/dist/index.js +4 -4
  61. package/dist/mcp-fakes.d.cts.map +1 -1
  62. package/dist/messages.cjs +159 -141
  63. package/dist/messages.cjs.map +1 -1
  64. package/dist/messages.d.cts.map +1 -1
  65. package/dist/messages.d.ts.map +1 -1
  66. package/dist/messages.js +161 -143
  67. package/dist/messages.js.map +1 -1
  68. package/dist/misbehavior.cjs +62 -3
  69. package/dist/misbehavior.cjs.map +1 -1
  70. package/dist/misbehavior.d.cts.map +1 -1
  71. package/dist/misbehavior.d.ts.map +1 -1
  72. package/dist/misbehavior.js +63 -4
  73. package/dist/misbehavior.js.map +1 -1
  74. package/dist/ndjson-writer.cjs +21 -23
  75. package/dist/ndjson-writer.cjs.map +1 -1
  76. package/dist/ndjson-writer.d.cts +0 -1
  77. package/dist/ndjson-writer.d.cts.map +1 -1
  78. package/dist/ndjson-writer.d.ts +0 -1
  79. package/dist/ndjson-writer.d.ts.map +1 -1
  80. package/dist/ndjson-writer.js +21 -24
  81. package/dist/ndjson-writer.js.map +1 -1
  82. package/dist/ollama.cjs +153 -147
  83. package/dist/ollama.cjs.map +1 -1
  84. package/dist/ollama.d.cts.map +1 -1
  85. package/dist/ollama.d.ts.map +1 -1
  86. package/dist/ollama.js +155 -149
  87. package/dist/ollama.js.map +1 -1
  88. package/dist/openrouter-chat.cjs +1 -1
  89. package/dist/openrouter-chat.js +1 -1
  90. package/dist/raw-validation.cjs +177 -0
  91. package/dist/raw-validation.cjs.map +1 -0
  92. package/dist/raw-validation.js +176 -0
  93. package/dist/raw-validation.js.map +1 -0
  94. package/dist/response/deliver.cjs +144 -0
  95. package/dist/response/deliver.cjs.map +1 -0
  96. package/dist/response/deliver.js +143 -0
  97. package/dist/response/deliver.js.map +1 -0
  98. package/dist/response/frame-loop.cjs +102 -0
  99. package/dist/response/frame-loop.cjs.map +1 -0
  100. package/dist/response/frame-loop.js +100 -0
  101. package/dist/response/frame-loop.js.map +1 -0
  102. package/dist/response/lifecycle.cjs +223 -0
  103. package/dist/response/lifecycle.cjs.map +1 -0
  104. package/dist/response/lifecycle.js +211 -0
  105. package/dist/response/lifecycle.js.map +1 -0
  106. package/dist/response/proxy.cjs +71 -0
  107. package/dist/response/proxy.cjs.map +1 -0
  108. package/dist/response/proxy.js +70 -0
  109. package/dist/response/proxy.js.map +1 -0
  110. package/dist/response/raw.cjs +228 -0
  111. package/dist/response/raw.cjs.map +1 -0
  112. package/dist/response/raw.js +227 -0
  113. package/dist/response/raw.js.map +1 -0
  114. package/dist/response/timing.cjs +157 -0
  115. package/dist/response/timing.cjs.map +1 -0
  116. package/dist/response/timing.js +150 -0
  117. package/dist/response/timing.js.map +1 -0
  118. package/dist/responses.cjs +269 -148
  119. package/dist/responses.cjs.map +1 -1
  120. package/dist/responses.d.cts.map +1 -1
  121. package/dist/responses.d.ts.map +1 -1
  122. package/dist/responses.js +270 -149
  123. package/dist/responses.js.map +1 -1
  124. package/dist/router.cjs +20 -4
  125. package/dist/router.cjs.map +1 -1
  126. package/dist/router.d.cts +7 -1
  127. package/dist/router.d.cts.map +1 -1
  128. package/dist/router.d.ts +7 -1
  129. package/dist/router.d.ts.map +1 -1
  130. package/dist/router.js +21 -5
  131. package/dist/router.js.map +1 -1
  132. package/dist/server.cjs +89 -120
  133. package/dist/server.cjs.map +1 -1
  134. package/dist/server.d.cts.map +1 -1
  135. package/dist/server.d.ts.map +1 -1
  136. package/dist/server.js +91 -122
  137. package/dist/server.js.map +1 -1
  138. package/dist/sse-writer.cjs +49 -30
  139. package/dist/sse-writer.cjs.map +1 -1
  140. package/dist/sse-writer.d.cts +0 -1
  141. package/dist/sse-writer.d.cts.map +1 -1
  142. package/dist/sse-writer.d.ts +0 -1
  143. package/dist/sse-writer.d.ts.map +1 -1
  144. package/dist/sse-writer.js +49 -31
  145. package/dist/sse-writer.js.map +1 -1
  146. package/dist/transcription.cjs +19 -33
  147. package/dist/transcription.cjs.map +1 -1
  148. package/dist/transcription.d.cts.map +1 -1
  149. package/dist/transcription.d.ts.map +1 -1
  150. package/dist/transcription.js +20 -34
  151. package/dist/transcription.js.map +1 -1
  152. package/dist/types.cjs.map +1 -1
  153. package/dist/types.d.cts +173 -10
  154. package/dist/types.d.cts.map +1 -1
  155. package/dist/types.d.ts +173 -10
  156. package/dist/types.d.ts.map +1 -1
  157. package/dist/types.js.map +1 -1
  158. package/dist/ws-framing.cjs +12 -6
  159. package/dist/ws-framing.cjs.map +1 -1
  160. package/dist/ws-framing.d.cts +2 -0
  161. package/dist/ws-framing.d.cts.map +1 -1
  162. package/dist/ws-framing.d.ts +2 -0
  163. package/dist/ws-framing.d.ts.map +1 -1
  164. package/dist/ws-framing.js +12 -6
  165. package/dist/ws-framing.js.map +1 -1
  166. package/dist/ws-gemini-live.cjs +120 -93
  167. package/dist/ws-gemini-live.cjs.map +1 -1
  168. package/dist/ws-gemini-live.js +120 -93
  169. package/dist/ws-gemini-live.js.map +1 -1
  170. package/dist/ws-realtime.cjs +80 -70
  171. package/dist/ws-realtime.cjs.map +1 -1
  172. package/dist/ws-realtime.d.cts.map +1 -1
  173. package/dist/ws-realtime.d.ts.map +1 -1
  174. package/dist/ws-realtime.js +80 -70
  175. package/dist/ws-realtime.js.map +1 -1
  176. package/dist/ws-responses.cjs +72 -53
  177. package/dist/ws-responses.cjs.map +1 -1
  178. package/dist/ws-responses.d.cts.map +1 -1
  179. package/dist/ws-responses.d.ts.map +1 -1
  180. package/dist/ws-responses.js +73 -54
  181. package/dist/ws-responses.js.map +1 -1
  182. package/package.json +1 -1
  183. package/skills/write-fixtures/SKILL.md +34 -2
@@ -5,7 +5,7 @@ import { calculateDelay, delay } from "./sse-writer.js";
5
5
  import { planMisbehavior, recordMisbehaviorOutcome } from "./misbehavior.js";
6
6
  import { matchFixtureDiagnostic } from "./router.js";
7
7
  import "./journal.js";
8
- import { createInterruptionSignal } from "./interruption.js";
8
+ import { markWsTurnStopped, watchWsClose, wsLifecycle } from "./response/lifecycle.js";
9
9
 
10
10
  //#region src/ws-gemini-live.ts
11
11
  /**
@@ -201,6 +201,7 @@ function preflightBlocks(response, journalEntry) {
201
201
  }
202
202
  function handleWebSocketGeminiLive(ws, fixtures, journal, defaults) {
203
203
  const { logger } = defaults;
204
+ watchWsClose(ws);
204
205
  const session = {
205
206
  setupDone: false,
206
207
  model: defaults.model,
@@ -397,40 +398,45 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
397
398
  const latency = fixture.latency ?? defaults.latency;
398
399
  if (misbehavior.kind === "applied" && (misbehavior.summary.fault === "tool-args-invalid-json" || misbehavior.summary.fault === "empty-response")) {
399
400
  const journalEntry = addResponseEntry(200);
400
- const interruption = createInterruptionSignal(fixture);
401
- const markInterrupted = () => {
402
- journalEntry.response.interrupted = true;
403
- journalEntry.response.interruptReason = interruption?.reason();
401
+ const lifecycle = wsLifecycle(ws, fixture);
402
+ const interruptTurn = () => {
403
+ markWsTurnStopped(journalEntry, ws, lifecycle);
404
404
  ws.destroy();
405
405
  };
406
406
  try {
407
- if (ws.isClosed) return;
407
+ if (ws.isClosed) {
408
+ interruptTurn();
409
+ return;
410
+ }
408
411
  const chunkDelay = calculateDelay(0, void 0, latency, fixture.recordedTimings, fixture.replaySpeed ?? defaults.replaySpeed);
409
- if (chunkDelay > 0) await delay(chunkDelay, interruption?.signal);
410
- if (interruption?.signal.aborted) {
411
- markInterrupted();
412
+ if (chunkDelay > 0) await delay(chunkDelay, lifecycle.signal);
413
+ if (lifecycle.signal.aborted || ws.isClosed) {
414
+ interruptTurn();
412
415
  return;
413
416
  }
414
- if (ws.isClosed) return;
415
417
  const malformed = misbehavior.summary.fault === "tool-args-invalid-json";
416
418
  ws.send(JSON.stringify({ serverContent: malformed ? {
417
419
  turnComplete: true,
418
420
  turnCompleteReason: "MALFORMED_FUNCTION_CALL"
419
421
  } : { modelTurn: { parts: [{ text: "" }] } } }));
420
- interruption?.tick();
421
- if (interruption?.signal.aborted) {
422
- markInterrupted();
422
+ lifecycle.tick();
423
+ if (lifecycle.signal.aborted || ws.isClosed) {
424
+ interruptTurn();
423
425
  return;
424
426
  }
425
- if (!malformed && !ws.isClosed) {
427
+ if (!malformed) {
426
428
  ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
429
+ if (ws.isClosed) {
430
+ interruptTurn();
431
+ return;
432
+ }
427
433
  session.conversationHistory.push({
428
434
  role: "assistant",
429
435
  content: ""
430
436
  });
431
437
  }
432
438
  } finally {
433
- interruption?.cleanup();
439
+ lifecycle.dispose();
434
440
  }
435
441
  return;
436
442
  }
@@ -467,25 +473,32 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
467
473
  ...tc,
468
474
  resolvedId: tc.id ?? generateToolCallId()
469
475
  }));
476
+ const stoppedByClose = () => {
477
+ if (!ws.isClosed) return false;
478
+ markWsTurnStopped(journalEntry, ws);
479
+ return true;
480
+ };
481
+ if (stoppedByClose()) return;
470
482
  if (resolvedToolCalls.length === 0) ws.send(JSON.stringify({ serverContent: {
471
483
  modelTurn: { parts },
472
484
  turnComplete: true
473
485
  } }));
474
486
  else {
475
487
  ws.send(JSON.stringify({ serverContent: { modelTurn: { parts } } }));
476
- if (!ws.isClosed) {
477
- const functionCalls = resolvedToolCalls.map((tc) => {
478
- const argsObj = liveToolArguments(tc);
479
- return {
480
- name: tc.name,
481
- args: argsObj,
482
- id: tc.resolvedId
483
- };
484
- });
485
- ws.send(JSON.stringify({ toolCall: { functionCalls } }));
486
- }
487
- if (!ws.isClosed) ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
488
+ if (stoppedByClose()) return;
489
+ const functionCalls = resolvedToolCalls.map((tc) => {
490
+ const argsObj = liveToolArguments(tc);
491
+ return {
492
+ name: tc.name,
493
+ args: argsObj,
494
+ id: tc.resolvedId
495
+ };
496
+ });
497
+ ws.send(JSON.stringify({ toolCall: { functionCalls } }));
498
+ if (stoppedByClose()) return;
499
+ ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
488
500
  }
501
+ if (stoppedByClose()) return;
489
502
  session.conversationHistory.push({
490
503
  role: "assistant",
491
504
  content: audioResp.content ?? "[audio]",
@@ -504,7 +517,7 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
504
517
  const journalEntry = addResponseEntry(200);
505
518
  if (response.blocks && response.blocks.length > 0) {
506
519
  const resolvedBlocks = preflightBlocks(response, journalEntry);
507
- const interruption = createInterruptionSignal(fixture);
520
+ const lifecycle = wsLifecycle(ws, fixture);
508
521
  const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
509
522
  const { recordedTimings } = fixture;
510
523
  let chunkIndex = 0;
@@ -514,8 +527,8 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
514
527
  outer: for (const block of resolvedBlocks) if (block.type === "toolCall") {
515
528
  if (ws.isClosed) break;
516
529
  const tcDelay = calculateDelay(chunkIndex, void 0, latency, recordedTimings, replaySpeed);
517
- if (tcDelay > 0) await delay(tcDelay, interruption?.signal);
518
- if (interruption?.signal.aborted) {
530
+ if (tcDelay > 0) await delay(tcDelay, lifecycle.signal);
531
+ if (lifecycle.signal.aborted) {
519
532
  interrupted = true;
520
533
  break;
521
534
  }
@@ -541,8 +554,8 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
541
554
  arguments: block.arguments
542
555
  }
543
556
  });
544
- interruption?.tick();
545
- if (interruption?.signal.aborted) {
557
+ lifecycle.tick();
558
+ if (lifecycle.signal.aborted) {
546
559
  interrupted = true;
547
560
  break;
548
561
  }
@@ -553,8 +566,8 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
553
566
  for (let i = 0; i < text.length; i += chunkSize) {
554
567
  if (ws.isClosed) break outer;
555
568
  const chunkDelay = calculateDelay(chunkIndex, void 0, latency, recordedTimings, replaySpeed);
556
- if (chunkDelay > 0) await delay(chunkDelay, interruption?.signal);
557
- if (interruption?.signal.aborted) {
569
+ if (chunkDelay > 0) await delay(chunkDelay, lifecycle.signal);
570
+ if (lifecycle.signal.aborted) {
558
571
  interrupted = true;
559
572
  break outer;
560
573
  }
@@ -566,22 +579,25 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
566
579
  break outer;
567
580
  }
568
581
  chunkIndex++;
569
- interruption?.tick();
570
- if (interruption?.signal.aborted) {
582
+ lifecycle.tick();
583
+ if (lifecycle.signal.aborted) {
571
584
  interrupted = true;
572
585
  break outer;
573
586
  }
574
587
  }
575
588
  }
576
- if (interrupted) {
589
+ if (interrupted || ws.isClosed) {
577
590
  ws.destroy();
578
- journalEntry.response.interrupted = true;
579
- journalEntry.response.interruptReason = interruption?.reason();
580
- interruption?.cleanup();
591
+ markWsTurnStopped(journalEntry, ws, lifecycle);
592
+ lifecycle.dispose();
593
+ return;
594
+ }
595
+ lifecycle.dispose();
596
+ ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
597
+ if (ws.isClosed) {
598
+ markWsTurnStopped(journalEntry, ws, lifecycle);
581
599
  return;
582
600
  }
583
- interruption?.cleanup();
584
- if (!ws.isClosed) ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
585
601
  session.conversationHistory.push({
586
602
  role: "assistant",
587
603
  content: historyContent || null,
@@ -593,7 +609,7 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
593
609
  const content = response.content ?? "";
594
610
  const chunkList = [];
595
611
  for (let i = 0; i < content.length; i += chunkSize) chunkList.push(content.slice(i, i + chunkSize));
596
- const interruption = createInterruptionSignal(fixture);
612
+ const lifecycle = wsLifecycle(ws, fixture);
597
613
  const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
598
614
  const { recordedTimings } = fixture;
599
615
  let interrupted = false;
@@ -602,8 +618,8 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
602
618
  } else for (let i = 0; i < chunkList.length; i++) {
603
619
  if (ws.isClosed) break;
604
620
  const chunkDelay = calculateDelay(i, void 0, latency, recordedTimings, replaySpeed);
605
- if (chunkDelay > 0) await delay(chunkDelay, interruption?.signal);
606
- if (interruption?.signal.aborted) {
621
+ if (chunkDelay > 0) await delay(chunkDelay, lifecycle.signal);
622
+ if (lifecycle.signal.aborted) {
607
623
  interrupted = true;
608
624
  break;
609
625
  }
@@ -614,17 +630,16 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
614
630
  defaults.logger.debug("[gemini-live] send failed during text streaming, closing", err);
615
631
  break;
616
632
  }
617
- interruption?.tick();
618
- if (interruption?.signal.aborted) {
633
+ lifecycle.tick();
634
+ if (lifecycle.signal.aborted) {
619
635
  interrupted = true;
620
636
  break;
621
637
  }
622
638
  }
623
- if (interrupted) {
639
+ if (interrupted || ws.isClosed) {
624
640
  ws.destroy();
625
- journalEntry.response.interrupted = true;
626
- journalEntry.response.interruptReason = interruption?.reason();
627
- interruption?.cleanup();
641
+ markWsTurnStopped(journalEntry, ws, lifecycle);
642
+ lifecycle.dispose();
628
643
  return;
629
644
  }
630
645
  const resolvedToolCalls = functionToolCalls.map((tc) => ({
@@ -633,12 +648,11 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
633
648
  }));
634
649
  if (!ws.isClosed) {
635
650
  const tcDelay = calculateDelay(chunkList.length, void 0, latency, recordedTimings, replaySpeed);
636
- if (tcDelay > 0) await delay(tcDelay, interruption?.signal);
637
- if (interruption?.signal.aborted) {
651
+ if (tcDelay > 0) await delay(tcDelay, lifecycle.signal);
652
+ if (lifecycle.signal.aborted || ws.isClosed) {
638
653
  ws.destroy();
639
- journalEntry.response.interrupted = true;
640
- journalEntry.response.interruptReason = interruption?.reason();
641
- interruption?.cleanup();
654
+ markWsTurnStopped(journalEntry, ws, lifecycle);
655
+ lifecycle.dispose();
642
656
  return;
643
657
  }
644
658
  const functionCalls = resolvedToolCalls.map((tc) => {
@@ -650,17 +664,20 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
650
664
  };
651
665
  });
652
666
  ws.send(JSON.stringify({ toolCall: { functionCalls } }));
653
- interruption?.tick();
667
+ lifecycle.tick();
654
668
  }
655
- if (interruption?.signal.aborted) {
669
+ if (lifecycle.signal.aborted || ws.isClosed) {
656
670
  ws.destroy();
657
- journalEntry.response.interrupted = true;
658
- journalEntry.response.interruptReason = interruption?.reason();
659
- interruption?.cleanup();
671
+ markWsTurnStopped(journalEntry, ws, lifecycle);
672
+ lifecycle.dispose();
673
+ return;
674
+ }
675
+ lifecycle.dispose();
676
+ ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
677
+ if (ws.isClosed) {
678
+ markWsTurnStopped(journalEntry, ws, lifecycle);
660
679
  return;
661
680
  }
662
- interruption?.cleanup();
663
- if (!ws.isClosed) ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
664
681
  session.conversationHistory.push({
665
682
  role: "assistant",
666
683
  content: content || null,
@@ -679,22 +696,30 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
679
696
  const journalEntry = addResponseEntry(200);
680
697
  const content = response.content;
681
698
  if (content.length === 0) {
682
- if (ws.isClosed) return;
699
+ if (ws.isClosed) {
700
+ markWsTurnStopped(journalEntry, ws);
701
+ return;
702
+ }
683
703
  ws.send(JSON.stringify({ serverContent: { modelTurn: { parts: [{ text: "" }] } } }));
704
+ if (ws.isClosed) {
705
+ markWsTurnStopped(journalEntry, ws);
706
+ return;
707
+ }
684
708
  ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
709
+ if (ws.isClosed) markWsTurnStopped(journalEntry, ws);
685
710
  return;
686
711
  }
687
712
  const chunks = [];
688
713
  for (let i = 0; i < content.length; i += chunkSize) chunks.push(content.slice(i, i + chunkSize));
689
- const interruption = createInterruptionSignal(fixture);
714
+ const lifecycle = wsLifecycle(ws, fixture);
690
715
  const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
691
716
  const { recordedTimings } = fixture;
692
717
  let interrupted = false;
693
718
  for (let i = 0; i < chunks.length; i++) {
694
719
  if (ws.isClosed) break;
695
720
  const chunkDelay = calculateDelay(i, void 0, latency, recordedTimings, replaySpeed);
696
- if (chunkDelay > 0) await delay(chunkDelay, interruption?.signal);
697
- if (interruption?.signal.aborted) {
721
+ if (chunkDelay > 0) await delay(chunkDelay, lifecycle.signal);
722
+ if (lifecycle.signal.aborted) {
698
723
  interrupted = true;
699
724
  break;
700
725
  }
@@ -705,21 +730,24 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
705
730
  defaults.logger.debug("[gemini-live] send failed during text streaming, closing", err);
706
731
  break;
707
732
  }
708
- interruption?.tick();
709
- if (interruption?.signal.aborted) {
733
+ lifecycle.tick();
734
+ if (lifecycle.signal.aborted) {
710
735
  interrupted = true;
711
736
  break;
712
737
  }
713
738
  }
714
- if (interrupted) {
739
+ if (interrupted || ws.isClosed) {
715
740
  ws.destroy();
716
- journalEntry.response.interrupted = true;
717
- journalEntry.response.interruptReason = interruption?.reason();
718
- interruption?.cleanup();
741
+ markWsTurnStopped(journalEntry, ws, lifecycle);
742
+ lifecycle.dispose();
743
+ return;
744
+ }
745
+ lifecycle.dispose();
746
+ ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
747
+ if (ws.isClosed) {
748
+ markWsTurnStopped(journalEntry, ws, lifecycle);
719
749
  return;
720
750
  }
721
- interruption?.cleanup();
722
- if (!ws.isClosed) ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
723
751
  session.conversationHistory.push({
724
752
  role: "assistant",
725
753
  content
@@ -729,24 +757,20 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
729
757
  if (isToolCallResponse(response)) {
730
758
  const journalEntry = addResponseEntry(200);
731
759
  const functionToolCalls = preflightToolArguments(response, journalEntry);
732
- const interruption = createInterruptionSignal(fixture);
760
+ const lifecycle = wsLifecycle(ws, fixture);
733
761
  const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
734
762
  const { recordedTimings } = fixture;
735
763
  if (ws.isClosed) {
736
- interruption?.cleanup();
764
+ markWsTurnStopped(journalEntry, ws, lifecycle);
765
+ lifecycle.dispose();
737
766
  return;
738
767
  }
739
768
  const tcDelay = calculateDelay(0, void 0, latency, recordedTimings, replaySpeed);
740
- if (tcDelay > 0) await delay(tcDelay, interruption?.signal);
741
- if (interruption?.signal.aborted) {
769
+ if (tcDelay > 0) await delay(tcDelay, lifecycle.signal);
770
+ if (lifecycle.signal.aborted || ws.isClosed) {
742
771
  ws.destroy();
743
- journalEntry.response.interrupted = true;
744
- journalEntry.response.interruptReason = interruption?.reason();
745
- interruption?.cleanup();
746
- return;
747
- }
748
- if (ws.isClosed) {
749
- interruption?.cleanup();
772
+ markWsTurnStopped(journalEntry, ws, lifecycle);
773
+ lifecycle.dispose();
750
774
  return;
751
775
  }
752
776
  const resolvedToolCalls = functionToolCalls.map((tc) => ({
@@ -762,16 +786,19 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
762
786
  };
763
787
  });
764
788
  ws.send(JSON.stringify({ toolCall: { functionCalls } }));
765
- interruption?.tick();
766
- if (interruption?.signal.aborted) {
789
+ lifecycle.tick();
790
+ if (lifecycle.signal.aborted || ws.isClosed) {
767
791
  ws.destroy();
768
- journalEntry.response.interrupted = true;
769
- journalEntry.response.interruptReason = interruption?.reason();
770
- interruption?.cleanup();
792
+ markWsTurnStopped(journalEntry, ws, lifecycle);
793
+ lifecycle.dispose();
794
+ return;
795
+ }
796
+ lifecycle.dispose();
797
+ ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
798
+ if (ws.isClosed) {
799
+ markWsTurnStopped(journalEntry, ws, lifecycle);
771
800
  return;
772
801
  }
773
- interruption?.cleanup();
774
- if (!ws.isClosed) ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
775
802
  session.conversationHistory.push({
776
803
  role: "assistant",
777
804
  content: null,