@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
@@ -49,12 +49,11 @@ var WebSocketConnection = class extends EventEmitter {
49
49
  this.parseFrames();
50
50
  }
51
51
  });
52
- socket.on("close", () => {
53
- if (!this.closed) {
54
- this.closed = true;
55
- this.release(/* @__PURE__ */ new Error("WebSocket closed"));
56
- this.emit("close", 1006, "Connection lost");
57
- }
52
+ socket.on("close", () => this.lost());
53
+ socket.on("end", () => {
54
+ if (this.closed) return;
55
+ this.socket.destroy();
56
+ this.lost();
58
57
  });
59
58
  socket.on("error", (err) => {
60
59
  this.emit("error", err);
@@ -102,6 +101,13 @@ var WebSocketConnection = class extends EventEmitter {
102
101
  }
103
102
  });
104
103
  }
104
+ /** Mark an unannounced connection loss and emit close once. */
105
+ lost() {
106
+ if (this.closed) return;
107
+ this.closed = true;
108
+ this.release(/* @__PURE__ */ new Error("WebSocket closed"));
109
+ this.emit("close", 1006, "Connection lost");
110
+ }
105
111
  release(error) {
106
112
  this.buffer = Buffer.alloc(0);
107
113
  this.fragments = [];
@@ -1 +1 @@
1
- {"version":3,"file":"ws-framing.js","names":[],"sources":["../src/ws-framing.ts"],"sourcesContent":["/**\n * Minimal RFC 6455 WebSocket server implementation.\n *\n * Zero dependencies — uses only Node.js builtins (node:crypto, node:events).\n * Supports text frames, ping/pong, close handshake, and client frame unmasking.\n * Designed for a mock server — no extensions, no binary frames, no compression.\n */\n\nimport { createHash, randomBytes } from \"node:crypto\";\nimport { EventEmitter } from \"node:events\";\nimport type * as net from \"node:net\";\nimport type * as http from \"node:http\";\n\nconst WS_GUID = \"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\";\n\n// Opcodes\nconst OP_CONTINUATION = 0x0;\nconst OP_TEXT = 0x1;\nconst OP_CLOSE = 0x8;\nconst OP_PING = 0x9;\nconst OP_PONG = 0xa;\n\nexport interface WebSocketLimits {\n maxMessageBytes?: number;\n maxBufferedBytes?: number;\n maxWriteBytes?: number;\n /** Client role: mask outgoing frames and require unmasked incoming frames. */\n maskOutgoing?: boolean;\n}\n\nexport class WebSocketConnection extends EventEmitter {\n private socket: net.Socket;\n private buffer: Buffer = Buffer.alloc(0);\n private closed = false;\n\n // For fragmented messages (continuation frames)\n private fragments: Buffer[] = [];\n\n private fragmentBytes = 0;\n private fragmented = false;\n private pendingWriteBytes = 0;\n private pendingWrites = new Set<(error: Error) => void>();\n private limits: WebSocketLimits;\n\n constructor(socket: net.Socket, limits: WebSocketLimits = {}) {\n super();\n for (const value of [limits.maxMessageBytes, limits.maxBufferedBytes, limits.maxWriteBytes]) {\n if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0)) {\n throw new RangeError(\"WebSocket limits must be positive safe integers\");\n }\n }\n this.limits = { ...limits };\n this.socket = socket;\n\n socket.on(\"data\", (data: Buffer) => {\n // Byte-idle consumers must see partial frames and control traffic too.\n // Emit no payload, and notify before parsing can dispatch a message.\n if (data.length > 0 && !this.closed) this.emit(\"activity\");\n let offset = 0;\n while (offset < data.length && !this.closed) {\n const room =\n (this.limits.maxBufferedBytes ?? Infinity) - this.fragmentBytes - this.buffer.length;\n if (room <= 0) {\n this.close(1009, \"WebSocket buffer limit exceeded\");\n return;\n }\n const end = offset + Math.min(room, data.length - offset);\n this.buffer = Buffer.concat([this.buffer, data.subarray(offset, end)]);\n offset = end;\n this.parseFrames();\n }\n });\n\n socket.on(\"close\", () => {\n if (!this.closed) {\n this.closed = true;\n this.release(new Error(\"WebSocket closed\"));\n this.emit(\"close\", 1006, \"Connection lost\");\n }\n });\n\n socket.on(\"error\", (err: Error) => {\n this.emit(\"error\", err);\n });\n }\n\n send(data: string): void {\n if (this.closed) return;\n void this.sendAsync(data).catch(() => {\n /* Legacy fire-and-forget API. */\n });\n }\n\n sendAsync(data: string, signal?: AbortSignal): Promise<void> {\n if (this.closed || this.socket.destroyed) return Promise.reject(new Error(\"WebSocket closed\"));\n if (signal?.aborted) return Promise.reject(new Error(\"WebSocket send aborted\"));\n const length = Buffer.byteLength(data);\n const frameBytes =\n length + (length < 126 ? 2 : length < 65536 ? 4 : 10) + (this.limits.maskOutgoing ? 4 : 0);\n if (\n frameBytes >\n (this.limits.maxWriteBytes ?? Infinity) -\n Math.max(this.pendingWriteBytes, this.socket.writableLength)\n ) {\n this.close(1009, \"WebSocket write limit exceeded\");\n return Promise.reject(new Error(\"WebSocket write limit exceeded\"));\n }\n this.pendingWriteBytes += frameBytes;\n return new Promise<void>((resolve, reject) => {\n let settled = false;\n const finish = (error?: Error) => {\n if (settled) return;\n settled = true;\n this.pendingWriteBytes -= frameBytes;\n this.pendingWrites.delete(fail);\n signal?.removeEventListener(\"abort\", abort);\n if (error) reject(error);\n else resolve();\n };\n const fail = (error: Error) => finish(error);\n const abort = () => {\n finish(new Error(\"WebSocket send aborted\"));\n this.destroy();\n };\n this.pendingWrites.add(fail);\n signal?.addEventListener(\"abort\", abort, { once: true });\n try {\n this.socket.write(this.encodeFrame(OP_TEXT, Buffer.from(data)), (error?: Error | null) => {\n if (error) finish(new Error(\"WebSocket write failed\"));\n else finish();\n });\n } catch {\n finish(new Error(\"WebSocket write failed\"));\n }\n });\n }\n\n private release(error: Error): void {\n this.buffer = Buffer.alloc(0);\n this.fragments = [];\n this.fragmentBytes = 0;\n for (const fail of this.pendingWrites) fail(error);\n }\n\n close(code = 1000, reason = \"\"): void {\n if (this.closed) return;\n this.closed = true;\n this.release(new Error(\"WebSocket closed\"));\n\n const encodedReason = Buffer.from(reason, \"utf-8\");\n let reasonEnd = Math.min(encodedReason.length, 123);\n // A continuation byte at the cut means its code point straddles the limit.\n while (reasonEnd < encodedReason.length && (encodedReason[reasonEnd] & 0xc0) === 0x80) {\n reasonEnd--;\n }\n const reasonBuf = encodedReason.subarray(0, reasonEnd);\n const payload = Buffer.alloc(2 + reasonBuf.length);\n payload.writeUInt16BE(code, 0);\n reasonBuf.copy(payload, 2);\n this.writeFrame(OP_CLOSE, payload);\n\n // Give the client a moment to receive the close frame before destroying.\n // If writeFrame failed (socket already destroyed), this is a no-op.\n setTimeout(() => {\n if (!this.socket.destroyed) {\n this.socket.destroy();\n }\n // Emit close event for server-initiated closes so listeners\n // (e.g. activeConnections.delete) always fire.\n this.emit(\"close\", code, reason);\n }, 100);\n }\n\n destroy(): void {\n if (this.closed) return;\n this.closed = true;\n this.release(new Error(\"WebSocket destroyed\"));\n if (!this.socket.destroyed) {\n this.socket.destroy();\n }\n this.emit(\"close\", 1006, \"Connection destroyed\");\n }\n\n get isClosed(): boolean {\n return this.closed;\n }\n\n private encodeFrame(opcode: number, payload: Buffer): Buffer {\n const length = payload.length;\n const headerSize = length < 126 ? 2 : length < 65536 ? 4 : 10;\n const maskSize = this.limits.maskOutgoing ? 4 : 0;\n const frame = Buffer.allocUnsafe(headerSize + maskSize + length);\n frame[0] = 0x80 | opcode;\n frame[1] = (maskSize ? 0x80 : 0) | (length < 126 ? length : length < 65536 ? 126 : 127);\n if (headerSize === 4) frame.writeUInt16BE(length, 2);\n if (headerSize === 10) frame.writeBigUInt64BE(BigInt(length), 2);\n if (maskSize) {\n const mask = randomBytes(4);\n mask.copy(frame, headerSize);\n for (let i = 0; i < length; i++) frame[headerSize + 4 + i] = payload[i] ^ mask[i % 4];\n } else payload.copy(frame, headerSize);\n return frame;\n }\n\n private writeFrame(opcode: number, payload: Buffer): void {\n if (this.socket.destroyed) return;\n const available =\n (this.limits.maxWriteBytes ?? Infinity) -\n Math.max(this.pendingWriteBytes, this.socket.writableLength);\n const headerBytes = 2 + (this.limits.maskOutgoing ? 4 : 0);\n if (opcode === OP_CLOSE && payload.length + headerBytes > available) {\n // Keep even the final control frame inside the configured socket budget.\n if (available < headerBytes + 2) {\n this.socket.destroy();\n return;\n }\n payload = payload.subarray(0, 2);\n } else if (payload.length + headerBytes > available) {\n this.close(1009, \"WebSocket write limit exceeded\");\n return;\n }\n const frame = this.encodeFrame(opcode, payload);\n this.socket.write(frame);\n }\n\n private parseFrames(): void {\n while (this.buffer.length >= 2 && !this.closed) {\n const byte0 = this.buffer[0];\n const byte1 = this.buffer[1];\n\n const fin = (byte0 & 0x80) !== 0;\n const opcode = byte0 & 0x0f;\n const masked = (byte1 & 0x80) !== 0;\n if (\n byte0 & 0x70 ||\n masked === Boolean(this.limits.maskOutgoing) ||\n ![OP_CONTINUATION, OP_TEXT, 2, OP_CLOSE, OP_PING, OP_PONG].includes(opcode) ||\n (opcode >= 8 && (!fin || (byte1 & 0x7f) > 125)) ||\n (opcode === OP_CONTINUATION && !this.fragmented) ||\n (opcode === OP_TEXT && this.fragmented)\n ) {\n this.close(1002, \"Invalid WebSocket frame\");\n return;\n }\n let payloadLength = byte1 & 0x7f;\n let offset = 2;\n\n if (payloadLength === 126) {\n if (this.buffer.length < 4) return; // need more data\n payloadLength = this.buffer.readUInt16BE(2);\n offset = 4;\n } else if (payloadLength === 127) {\n if (this.buffer.length < 10) return;\n const advertised = this.buffer.readBigUInt64BE(2);\n if (advertised & (1n << 63n)) {\n this.close(1002, \"Invalid WebSocket length\");\n return;\n }\n if (advertised > BigInt(Number.MAX_SAFE_INTEGER)) {\n this.close(1009, \"WebSocket message limit exceeded\");\n return;\n }\n payloadLength = Number(advertised);\n offset = 10;\n }\n\n if (\n ((byte1 & 0x7f) === 126 && payloadLength < 126) ||\n ((byte1 & 0x7f) === 127 && payloadLength < 65536)\n ) {\n this.close(1002, \"Invalid WebSocket length\");\n return;\n }\n if (\n opcode < 8 &&\n payloadLength > (this.limits.maxMessageBytes ?? Infinity) - this.fragmentBytes\n ) {\n this.close(1009, \"WebSocket message limit exceeded\");\n return;\n }\n if (\n payloadLength + offset + (masked ? 4 : 0) >\n (this.limits.maxBufferedBytes ?? Infinity) - this.fragmentBytes\n ) {\n this.close(1009, \"WebSocket buffer limit exceeded\");\n return;\n }\n const maskSize = masked ? 4 : 0;\n const totalFrameSize = offset + maskSize + payloadLength;\n\n if (this.buffer.length < totalFrameSize) return; // need more data\n\n let maskKey: Buffer | null = null;\n if (masked) {\n maskKey = this.buffer.subarray(offset, offset + 4);\n offset += 4;\n }\n\n let payload = this.buffer.subarray(offset, offset + payloadLength);\n\n // Unmask client payload\n if (maskKey) {\n payload = Buffer.from(payload); // copy before mutating\n for (let i = 0; i < payload.length; i++) {\n payload[i] ^= maskKey[i % 4];\n }\n }\n\n // Consume the frame from the buffer\n this.buffer = this.buffer.subarray(totalFrameSize);\n\n this.handleFrame(fin, opcode, payload);\n }\n }\n\n private handleFrame(fin: boolean, opcode: number, payload: Buffer): void {\n // Control frames (opcode >= 0x8) must not be fragmented\n if (opcode === OP_PING) {\n this.writeFrame(OP_PONG, payload);\n return;\n }\n\n if (opcode === OP_PONG) {\n // Ignore unsolicited pongs\n return;\n }\n\n if (opcode === OP_CLOSE) {\n if (payload.length === 1) {\n this.close(1002, \"Invalid WebSocket close\");\n return;\n }\n const code = payload.length >= 2 ? payload.readUInt16BE(0) : 1005;\n const reason = payload.length > 2 ? payload.subarray(2).toString(\"utf-8\") : \"\";\n\n if (!this.closed) {\n this.closed = true;\n this.release(new Error(\"WebSocket closed\"));\n // Echo close frame back\n this.writeFrame(OP_CLOSE, payload);\n this.socket.end();\n this.emit(\"close\", code, reason);\n }\n // If already closed (server-initiated or duplicate), ignore — the\n // close event was already emitted by close() or the first OP_CLOSE.\n return;\n }\n\n // Text or continuation frames\n if (opcode === OP_TEXT || opcode === OP_CONTINUATION) {\n this.fragmented = !fin;\n this.fragmentBytes += payload.length;\n if (payload.length) this.fragments.push(payload);\n\n if (fin) {\n const message = Buffer.concat(this.fragments).toString(\"utf-8\");\n this.fragments = [];\n this.fragmentBytes = 0;\n this.emit(\"message\", message);\n }\n // If !fin, wait for more continuation frames\n return;\n }\n\n // Binary or unknown — just ignore for a mock server\n }\n}\n\nexport function computeAcceptKey(wsKey: string): string {\n return createHash(\"sha1\")\n .update(wsKey + WS_GUID)\n .digest(\"base64\");\n}\n\nexport function upgradeToWebSocket(\n req: http.IncomingMessage,\n socket: net.Socket,\n limits?: WebSocketLimits,\n): WebSocketConnection {\n const key = req.headers[\"sec-websocket-key\"];\n if (!key) {\n socket.write(\"HTTP/1.1 400 Bad Request\\r\\n\\r\\n\");\n socket.destroy();\n throw new Error(\"Missing Sec-WebSocket-Key header\");\n }\n\n const acceptKey = computeAcceptKey(key);\n\n let responseHeaders =\n \"HTTP/1.1 101 Switching Protocols\\r\\n\" +\n \"Upgrade: websocket\\r\\n\" +\n \"Connection: Upgrade\\r\\n\" +\n `Sec-WebSocket-Accept: ${acceptKey}\\r\\n`;\n\n // Echo back requested subprotocol if present\n const protocol = req.headers[\"sec-websocket-protocol\"];\n if (protocol) {\n // Take the first offered protocol\n const first = protocol.split(\",\")[0].trim();\n responseHeaders += `Sec-WebSocket-Protocol: ${first}\\r\\n`;\n }\n\n responseHeaders += \"\\r\\n\";\n\n socket.write(responseHeaders);\n\n return new WebSocketConnection(socket, limits);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ws-framing.js","names":[],"sources":["../src/ws-framing.ts"],"sourcesContent":["/**\n * Minimal RFC 6455 WebSocket server implementation.\n *\n * Zero dependencies — uses only Node.js builtins (node:crypto, node:events).\n * Supports text frames, ping/pong, close handshake, and client frame unmasking.\n * Designed for a mock server — no extensions, no binary frames, no compression.\n */\n\nimport { createHash, randomBytes } from \"node:crypto\";\nimport { EventEmitter } from \"node:events\";\nimport type * as net from \"node:net\";\nimport type * as http from \"node:http\";\n\nconst WS_GUID = \"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\";\n\n// Opcodes\nconst OP_CONTINUATION = 0x0;\nconst OP_TEXT = 0x1;\nconst OP_CLOSE = 0x8;\nconst OP_PING = 0x9;\nconst OP_PONG = 0xa;\n\nexport interface WebSocketLimits {\n maxMessageBytes?: number;\n maxBufferedBytes?: number;\n maxWriteBytes?: number;\n /** Client role: mask outgoing frames and require unmasked incoming frames. */\n maskOutgoing?: boolean;\n}\n\nexport class WebSocketConnection extends EventEmitter {\n private socket: net.Socket;\n private buffer: Buffer = Buffer.alloc(0);\n private closed = false;\n\n // For fragmented messages (continuation frames)\n private fragments: Buffer[] = [];\n\n private fragmentBytes = 0;\n private fragmented = false;\n private pendingWriteBytes = 0;\n private pendingWrites = new Set<(error: Error) => void>();\n private limits: WebSocketLimits;\n\n constructor(socket: net.Socket, limits: WebSocketLimits = {}) {\n super();\n for (const value of [limits.maxMessageBytes, limits.maxBufferedBytes, limits.maxWriteBytes]) {\n if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0)) {\n throw new RangeError(\"WebSocket limits must be positive safe integers\");\n }\n }\n this.limits = { ...limits };\n this.socket = socket;\n\n socket.on(\"data\", (data: Buffer) => {\n // Byte-idle consumers must see partial frames and control traffic too.\n // Emit no payload, and notify before parsing can dispatch a message.\n if (data.length > 0 && !this.closed) this.emit(\"activity\");\n let offset = 0;\n while (offset < data.length && !this.closed) {\n const room =\n (this.limits.maxBufferedBytes ?? Infinity) - this.fragmentBytes - this.buffer.length;\n if (room <= 0) {\n this.close(1009, \"WebSocket buffer limit exceeded\");\n return;\n }\n const end = offset + Math.min(room, data.length - offset);\n this.buffer = Buffer.concat([this.buffer, data.subarray(offset, end)]);\n offset = end;\n this.parseFrames();\n }\n });\n\n socket.on(\"close\", () => this.lost());\n\n // A peer FIN without a close frame is a disconnect. HTTP upgrade sockets\n // allow half-open, so without this the loss surfaces only on a later\n // failed write. Data events precede \"end\", so frames that arrive just\n // before the FIN still parse, but any turn they start is cancelled at once\n // because the connection is then lost.\n socket.on(\"end\", () => {\n if (this.closed) return;\n this.socket.destroy();\n this.lost();\n });\n\n socket.on(\"error\", (err: Error) => {\n this.emit(\"error\", err);\n });\n }\n\n send(data: string): void {\n if (this.closed) return;\n void this.sendAsync(data).catch(() => {\n /* Legacy fire-and-forget API. */\n });\n }\n\n sendAsync(data: string, signal?: AbortSignal): Promise<void> {\n if (this.closed || this.socket.destroyed) return Promise.reject(new Error(\"WebSocket closed\"));\n if (signal?.aborted) return Promise.reject(new Error(\"WebSocket send aborted\"));\n const length = Buffer.byteLength(data);\n const frameBytes =\n length + (length < 126 ? 2 : length < 65536 ? 4 : 10) + (this.limits.maskOutgoing ? 4 : 0);\n if (\n frameBytes >\n (this.limits.maxWriteBytes ?? Infinity) -\n Math.max(this.pendingWriteBytes, this.socket.writableLength)\n ) {\n this.close(1009, \"WebSocket write limit exceeded\");\n return Promise.reject(new Error(\"WebSocket write limit exceeded\"));\n }\n this.pendingWriteBytes += frameBytes;\n return new Promise<void>((resolve, reject) => {\n let settled = false;\n const finish = (error?: Error) => {\n if (settled) return;\n settled = true;\n this.pendingWriteBytes -= frameBytes;\n this.pendingWrites.delete(fail);\n signal?.removeEventListener(\"abort\", abort);\n if (error) reject(error);\n else resolve();\n };\n const fail = (error: Error) => finish(error);\n const abort = () => {\n finish(new Error(\"WebSocket send aborted\"));\n this.destroy();\n };\n this.pendingWrites.add(fail);\n signal?.addEventListener(\"abort\", abort, { once: true });\n try {\n this.socket.write(this.encodeFrame(OP_TEXT, Buffer.from(data)), (error?: Error | null) => {\n if (error) finish(new Error(\"WebSocket write failed\"));\n else finish();\n });\n } catch {\n finish(new Error(\"WebSocket write failed\"));\n }\n });\n }\n\n /** Mark an unannounced connection loss and emit close once. */\n private lost(): void {\n if (this.closed) return;\n this.closed = true;\n this.release(new Error(\"WebSocket closed\"));\n this.emit(\"close\", 1006, \"Connection lost\");\n }\n\n private release(error: Error): void {\n this.buffer = Buffer.alloc(0);\n this.fragments = [];\n this.fragmentBytes = 0;\n for (const fail of this.pendingWrites) fail(error);\n }\n\n close(code = 1000, reason = \"\"): void {\n if (this.closed) return;\n this.closed = true;\n this.release(new Error(\"WebSocket closed\"));\n\n const encodedReason = Buffer.from(reason, \"utf-8\");\n let reasonEnd = Math.min(encodedReason.length, 123);\n // A continuation byte at the cut means its code point straddles the limit.\n while (reasonEnd < encodedReason.length && (encodedReason[reasonEnd] & 0xc0) === 0x80) {\n reasonEnd--;\n }\n const reasonBuf = encodedReason.subarray(0, reasonEnd);\n const payload = Buffer.alloc(2 + reasonBuf.length);\n payload.writeUInt16BE(code, 0);\n reasonBuf.copy(payload, 2);\n this.writeFrame(OP_CLOSE, payload);\n\n // Give the client a moment to receive the close frame before destroying.\n // If writeFrame failed (socket already destroyed), this is a no-op.\n setTimeout(() => {\n if (!this.socket.destroyed) {\n this.socket.destroy();\n }\n // Emit close event for server-initiated closes so listeners\n // (e.g. activeConnections.delete) always fire.\n this.emit(\"close\", code, reason);\n }, 100);\n }\n\n destroy(): void {\n if (this.closed) return;\n this.closed = true;\n this.release(new Error(\"WebSocket destroyed\"));\n if (!this.socket.destroyed) {\n this.socket.destroy();\n }\n this.emit(\"close\", 1006, \"Connection destroyed\");\n }\n\n get isClosed(): boolean {\n return this.closed;\n }\n\n private encodeFrame(opcode: number, payload: Buffer): Buffer {\n const length = payload.length;\n const headerSize = length < 126 ? 2 : length < 65536 ? 4 : 10;\n const maskSize = this.limits.maskOutgoing ? 4 : 0;\n const frame = Buffer.allocUnsafe(headerSize + maskSize + length);\n frame[0] = 0x80 | opcode;\n frame[1] = (maskSize ? 0x80 : 0) | (length < 126 ? length : length < 65536 ? 126 : 127);\n if (headerSize === 4) frame.writeUInt16BE(length, 2);\n if (headerSize === 10) frame.writeBigUInt64BE(BigInt(length), 2);\n if (maskSize) {\n const mask = randomBytes(4);\n mask.copy(frame, headerSize);\n for (let i = 0; i < length; i++) frame[headerSize + 4 + i] = payload[i] ^ mask[i % 4];\n } else payload.copy(frame, headerSize);\n return frame;\n }\n\n private writeFrame(opcode: number, payload: Buffer): void {\n if (this.socket.destroyed) return;\n const available =\n (this.limits.maxWriteBytes ?? Infinity) -\n Math.max(this.pendingWriteBytes, this.socket.writableLength);\n const headerBytes = 2 + (this.limits.maskOutgoing ? 4 : 0);\n if (opcode === OP_CLOSE && payload.length + headerBytes > available) {\n // Keep even the final control frame inside the configured socket budget.\n if (available < headerBytes + 2) {\n this.socket.destroy();\n return;\n }\n payload = payload.subarray(0, 2);\n } else if (payload.length + headerBytes > available) {\n this.close(1009, \"WebSocket write limit exceeded\");\n return;\n }\n const frame = this.encodeFrame(opcode, payload);\n this.socket.write(frame);\n }\n\n private parseFrames(): void {\n while (this.buffer.length >= 2 && !this.closed) {\n const byte0 = this.buffer[0];\n const byte1 = this.buffer[1];\n\n const fin = (byte0 & 0x80) !== 0;\n const opcode = byte0 & 0x0f;\n const masked = (byte1 & 0x80) !== 0;\n if (\n byte0 & 0x70 ||\n masked === Boolean(this.limits.maskOutgoing) ||\n ![OP_CONTINUATION, OP_TEXT, 2, OP_CLOSE, OP_PING, OP_PONG].includes(opcode) ||\n (opcode >= 8 && (!fin || (byte1 & 0x7f) > 125)) ||\n (opcode === OP_CONTINUATION && !this.fragmented) ||\n (opcode === OP_TEXT && this.fragmented)\n ) {\n this.close(1002, \"Invalid WebSocket frame\");\n return;\n }\n let payloadLength = byte1 & 0x7f;\n let offset = 2;\n\n if (payloadLength === 126) {\n if (this.buffer.length < 4) return; // need more data\n payloadLength = this.buffer.readUInt16BE(2);\n offset = 4;\n } else if (payloadLength === 127) {\n if (this.buffer.length < 10) return;\n const advertised = this.buffer.readBigUInt64BE(2);\n if (advertised & (1n << 63n)) {\n this.close(1002, \"Invalid WebSocket length\");\n return;\n }\n if (advertised > BigInt(Number.MAX_SAFE_INTEGER)) {\n this.close(1009, \"WebSocket message limit exceeded\");\n return;\n }\n payloadLength = Number(advertised);\n offset = 10;\n }\n\n if (\n ((byte1 & 0x7f) === 126 && payloadLength < 126) ||\n ((byte1 & 0x7f) === 127 && payloadLength < 65536)\n ) {\n this.close(1002, \"Invalid WebSocket length\");\n return;\n }\n if (\n opcode < 8 &&\n payloadLength > (this.limits.maxMessageBytes ?? Infinity) - this.fragmentBytes\n ) {\n this.close(1009, \"WebSocket message limit exceeded\");\n return;\n }\n if (\n payloadLength + offset + (masked ? 4 : 0) >\n (this.limits.maxBufferedBytes ?? Infinity) - this.fragmentBytes\n ) {\n this.close(1009, \"WebSocket buffer limit exceeded\");\n return;\n }\n const maskSize = masked ? 4 : 0;\n const totalFrameSize = offset + maskSize + payloadLength;\n\n if (this.buffer.length < totalFrameSize) return; // need more data\n\n let maskKey: Buffer | null = null;\n if (masked) {\n maskKey = this.buffer.subarray(offset, offset + 4);\n offset += 4;\n }\n\n let payload = this.buffer.subarray(offset, offset + payloadLength);\n\n // Unmask client payload\n if (maskKey) {\n payload = Buffer.from(payload); // copy before mutating\n for (let i = 0; i < payload.length; i++) {\n payload[i] ^= maskKey[i % 4];\n }\n }\n\n // Consume the frame from the buffer\n this.buffer = this.buffer.subarray(totalFrameSize);\n\n this.handleFrame(fin, opcode, payload);\n }\n }\n\n private handleFrame(fin: boolean, opcode: number, payload: Buffer): void {\n // Control frames (opcode >= 0x8) must not be fragmented\n if (opcode === OP_PING) {\n this.writeFrame(OP_PONG, payload);\n return;\n }\n\n if (opcode === OP_PONG) {\n // Ignore unsolicited pongs\n return;\n }\n\n if (opcode === OP_CLOSE) {\n if (payload.length === 1) {\n this.close(1002, \"Invalid WebSocket close\");\n return;\n }\n const code = payload.length >= 2 ? payload.readUInt16BE(0) : 1005;\n const reason = payload.length > 2 ? payload.subarray(2).toString(\"utf-8\") : \"\";\n\n if (!this.closed) {\n this.closed = true;\n this.release(new Error(\"WebSocket closed\"));\n // Echo close frame back\n this.writeFrame(OP_CLOSE, payload);\n this.socket.end();\n this.emit(\"close\", code, reason);\n }\n // If already closed (server-initiated or duplicate), ignore — the\n // close event was already emitted by close() or the first OP_CLOSE.\n return;\n }\n\n // Text or continuation frames\n if (opcode === OP_TEXT || opcode === OP_CONTINUATION) {\n this.fragmented = !fin;\n this.fragmentBytes += payload.length;\n if (payload.length) this.fragments.push(payload);\n\n if (fin) {\n const message = Buffer.concat(this.fragments).toString(\"utf-8\");\n this.fragments = [];\n this.fragmentBytes = 0;\n this.emit(\"message\", message);\n }\n // If !fin, wait for more continuation frames\n return;\n }\n\n // Binary or unknown — just ignore for a mock server\n }\n}\n\nexport function computeAcceptKey(wsKey: string): string {\n return createHash(\"sha1\")\n .update(wsKey + WS_GUID)\n .digest(\"base64\");\n}\n\nexport function upgradeToWebSocket(\n req: http.IncomingMessage,\n socket: net.Socket,\n limits?: WebSocketLimits,\n): WebSocketConnection {\n const key = req.headers[\"sec-websocket-key\"];\n if (!key) {\n socket.write(\"HTTP/1.1 400 Bad Request\\r\\n\\r\\n\");\n socket.destroy();\n throw new Error(\"Missing Sec-WebSocket-Key header\");\n }\n\n const acceptKey = computeAcceptKey(key);\n\n let responseHeaders =\n \"HTTP/1.1 101 Switching Protocols\\r\\n\" +\n \"Upgrade: websocket\\r\\n\" +\n \"Connection: Upgrade\\r\\n\" +\n `Sec-WebSocket-Accept: ${acceptKey}\\r\\n`;\n\n // Echo back requested subprotocol if present\n const protocol = req.headers[\"sec-websocket-protocol\"];\n if (protocol) {\n // Take the first offered protocol\n const first = protocol.split(\",\")[0].trim();\n responseHeaders += `Sec-WebSocket-Protocol: ${first}\\r\\n`;\n }\n\n responseHeaders += \"\\r\\n\";\n\n socket.write(responseHeaders);\n\n return new WebSocketConnection(socket, limits);\n}\n"],"mappings":"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"}
@@ -5,7 +5,7 @@ const require_sse_writer = require('./sse-writer.cjs');
5
5
  const require_misbehavior = require('./misbehavior.cjs');
6
6
  const require_router = require('./router.cjs');
7
7
  require('./journal.cjs');
8
- const require_interruption = require('./interruption.cjs');
8
+ const require_lifecycle = require('./response/lifecycle.cjs');
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
+ require_lifecycle.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 = require_interruption.createInterruptionSignal(fixture);
401
- const markInterrupted = () => {
402
- journalEntry.response.interrupted = true;
403
- journalEntry.response.interruptReason = interruption?.reason();
401
+ const lifecycle = require_lifecycle.wsLifecycle(ws, fixture);
402
+ const interruptTurn = () => {
403
+ require_lifecycle.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 = require_sse_writer.calculateDelay(0, void 0, latency, fixture.recordedTimings, fixture.replaySpeed ?? defaults.replaySpeed);
409
- if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, interruption?.signal);
410
- if (interruption?.signal.aborted) {
411
- markInterrupted();
412
+ if (chunkDelay > 0) await require_sse_writer.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 ?? require_helpers.generateToolCallId()
469
475
  }));
476
+ const stoppedByClose = () => {
477
+ if (!ws.isClosed) return false;
478
+ require_lifecycle.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 = require_interruption.createInterruptionSignal(fixture);
520
+ const lifecycle = require_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 = require_sse_writer.calculateDelay(chunkIndex, void 0, latency, recordedTimings, replaySpeed);
517
- if (tcDelay > 0) await require_sse_writer.delay(tcDelay, interruption?.signal);
518
- if (interruption?.signal.aborted) {
530
+ if (tcDelay > 0) await require_sse_writer.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 = require_sse_writer.calculateDelay(chunkIndex, void 0, latency, recordedTimings, replaySpeed);
556
- if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, interruption?.signal);
557
- if (interruption?.signal.aborted) {
569
+ if (chunkDelay > 0) await require_sse_writer.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
+ require_lifecycle.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
+ require_lifecycle.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 = require_interruption.createInterruptionSignal(fixture);
612
+ const lifecycle = require_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 = require_sse_writer.calculateDelay(i, void 0, latency, recordedTimings, replaySpeed);
605
- if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, interruption?.signal);
606
- if (interruption?.signal.aborted) {
621
+ if (chunkDelay > 0) await require_sse_writer.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
+ require_lifecycle.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 = require_sse_writer.calculateDelay(chunkList.length, void 0, latency, recordedTimings, replaySpeed);
636
- if (tcDelay > 0) await require_sse_writer.delay(tcDelay, interruption?.signal);
637
- if (interruption?.signal.aborted) {
651
+ if (tcDelay > 0) await require_sse_writer.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
+ require_lifecycle.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
+ require_lifecycle.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
+ require_lifecycle.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
+ require_lifecycle.markWsTurnStopped(journalEntry, ws);
701
+ return;
702
+ }
683
703
  ws.send(JSON.stringify({ serverContent: { modelTurn: { parts: [{ text: "" }] } } }));
704
+ if (ws.isClosed) {
705
+ require_lifecycle.markWsTurnStopped(journalEntry, ws);
706
+ return;
707
+ }
684
708
  ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
709
+ if (ws.isClosed) require_lifecycle.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 = require_interruption.createInterruptionSignal(fixture);
714
+ const lifecycle = require_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 = require_sse_writer.calculateDelay(i, void 0, latency, recordedTimings, replaySpeed);
696
- if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, interruption?.signal);
697
- if (interruption?.signal.aborted) {
721
+ if (chunkDelay > 0) await require_sse_writer.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
+ require_lifecycle.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
+ require_lifecycle.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 (require_helpers.isToolCallResponse(response)) {
730
758
  const journalEntry = addResponseEntry(200);
731
759
  const functionToolCalls = preflightToolArguments(response, journalEntry);
732
- const interruption = require_interruption.createInterruptionSignal(fixture);
760
+ const lifecycle = require_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
+ require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
765
+ lifecycle.dispose();
737
766
  return;
738
767
  }
739
768
  const tcDelay = require_sse_writer.calculateDelay(0, void 0, latency, recordedTimings, replaySpeed);
740
- if (tcDelay > 0) await require_sse_writer.delay(tcDelay, interruption?.signal);
741
- if (interruption?.signal.aborted) {
769
+ if (tcDelay > 0) await require_sse_writer.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
+ require_lifecycle.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
+ require_lifecycle.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
+ require_lifecycle.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,