@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +12 -1
- package/README.md +4 -1
- package/dist/aws-event-stream.cjs +20 -23
- package/dist/aws-event-stream.cjs.map +1 -1
- package/dist/aws-event-stream.d.cts +0 -1
- package/dist/aws-event-stream.d.ts +0 -1
- package/dist/aws-event-stream.js +20 -24
- package/dist/aws-event-stream.js.map +1 -1
- package/dist/bedrock-converse.cjs +79 -82
- package/dist/bedrock-converse.cjs.map +1 -1
- package/dist/bedrock-converse.d.cts.map +1 -1
- package/dist/bedrock-converse.d.ts.map +1 -1
- package/dist/bedrock-converse.js +80 -83
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +112 -121
- package/dist/bedrock.cjs.map +1 -1
- package/dist/bedrock.d.cts.map +1 -1
- package/dist/bedrock.d.ts.map +1 -1
- package/dist/bedrock.js +113 -122
- package/dist/bedrock.js.map +1 -1
- package/dist/cohere.cjs +58 -106
- package/dist/cohere.cjs.map +1 -1
- package/dist/cohere.d.cts.map +1 -1
- package/dist/cohere.d.ts.map +1 -1
- package/dist/cohere.js +60 -108
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/fixture-loader.cjs +103 -2
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +104 -4
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-interactions.cjs +63 -121
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +65 -123
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +242 -155
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.d.cts.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +244 -157
- package/dist/gemini.js.map +1 -1
- package/dist/helpers.cjs +400 -7
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +51 -1
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +51 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +391 -8
- package/dist/helpers.js.map +1 -1
- package/dist/index.cjs +4 -2
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/mcp-fakes.d.cts.map +1 -1
- package/dist/messages.cjs +159 -141
- package/dist/messages.cjs.map +1 -1
- package/dist/messages.d.cts.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +161 -143
- package/dist/messages.js.map +1 -1
- package/dist/misbehavior.cjs +62 -3
- package/dist/misbehavior.cjs.map +1 -1
- package/dist/misbehavior.d.cts.map +1 -1
- package/dist/misbehavior.d.ts.map +1 -1
- package/dist/misbehavior.js +63 -4
- package/dist/misbehavior.js.map +1 -1
- package/dist/ndjson-writer.cjs +21 -23
- package/dist/ndjson-writer.cjs.map +1 -1
- package/dist/ndjson-writer.d.cts +0 -1
- package/dist/ndjson-writer.d.cts.map +1 -1
- package/dist/ndjson-writer.d.ts +0 -1
- package/dist/ndjson-writer.d.ts.map +1 -1
- package/dist/ndjson-writer.js +21 -24
- package/dist/ndjson-writer.js.map +1 -1
- package/dist/ollama.cjs +153 -147
- package/dist/ollama.cjs.map +1 -1
- package/dist/ollama.d.cts.map +1 -1
- package/dist/ollama.d.ts.map +1 -1
- package/dist/ollama.js +155 -149
- package/dist/ollama.js.map +1 -1
- package/dist/openrouter-chat.cjs +1 -1
- package/dist/openrouter-chat.js +1 -1
- package/dist/raw-validation.cjs +177 -0
- package/dist/raw-validation.cjs.map +1 -0
- package/dist/raw-validation.js +176 -0
- package/dist/raw-validation.js.map +1 -0
- package/dist/response/deliver.cjs +144 -0
- package/dist/response/deliver.cjs.map +1 -0
- package/dist/response/deliver.js +143 -0
- package/dist/response/deliver.js.map +1 -0
- package/dist/response/frame-loop.cjs +102 -0
- package/dist/response/frame-loop.cjs.map +1 -0
- package/dist/response/frame-loop.js +100 -0
- package/dist/response/frame-loop.js.map +1 -0
- package/dist/response/lifecycle.cjs +223 -0
- package/dist/response/lifecycle.cjs.map +1 -0
- package/dist/response/lifecycle.js +211 -0
- package/dist/response/lifecycle.js.map +1 -0
- package/dist/response/proxy.cjs +71 -0
- package/dist/response/proxy.cjs.map +1 -0
- package/dist/response/proxy.js +70 -0
- package/dist/response/proxy.js.map +1 -0
- package/dist/response/raw.cjs +228 -0
- package/dist/response/raw.cjs.map +1 -0
- package/dist/response/raw.js +227 -0
- package/dist/response/raw.js.map +1 -0
- package/dist/response/timing.cjs +157 -0
- package/dist/response/timing.cjs.map +1 -0
- package/dist/response/timing.js +150 -0
- package/dist/response/timing.js.map +1 -0
- package/dist/responses.cjs +269 -148
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +270 -149
- package/dist/responses.js.map +1 -1
- package/dist/router.cjs +20 -4
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts +7 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts +7 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +21 -5
- package/dist/router.js.map +1 -1
- package/dist/server.cjs +89 -120
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +91 -122
- package/dist/server.js.map +1 -1
- package/dist/sse-writer.cjs +49 -30
- package/dist/sse-writer.cjs.map +1 -1
- package/dist/sse-writer.d.cts +0 -1
- package/dist/sse-writer.d.cts.map +1 -1
- package/dist/sse-writer.d.ts +0 -1
- package/dist/sse-writer.d.ts.map +1 -1
- package/dist/sse-writer.js +49 -31
- package/dist/sse-writer.js.map +1 -1
- package/dist/transcription.cjs +19 -33
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.d.cts.map +1 -1
- package/dist/transcription.d.ts.map +1 -1
- package/dist/transcription.js +20 -34
- package/dist/transcription.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +173 -10
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +173 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/ws-framing.cjs +12 -6
- package/dist/ws-framing.cjs.map +1 -1
- package/dist/ws-framing.d.cts +2 -0
- package/dist/ws-framing.d.cts.map +1 -1
- package/dist/ws-framing.d.ts +2 -0
- package/dist/ws-framing.d.ts.map +1 -1
- package/dist/ws-framing.js +12 -6
- package/dist/ws-framing.js.map +1 -1
- package/dist/ws-gemini-live.cjs +120 -93
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.js +120 -93
- package/dist/ws-gemini-live.js.map +1 -1
- package/dist/ws-realtime.cjs +80 -70
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +80 -70
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +72 -53
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.d.cts.map +1 -1
- package/dist/ws-responses.d.ts.map +1 -1
- package/dist/ws-responses.js +73 -54
- package/dist/ws-responses.js.map +1 -1
- package/package.json +1 -1
- package/skills/write-fixtures/SKILL.md +34 -2
package/dist/ws-framing.js
CHANGED
|
@@ -49,12 +49,11 @@ var WebSocketConnection = class extends EventEmitter {
|
|
|
49
49
|
this.parseFrames();
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
|
-
socket.on("close", () =>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 = [];
|
package/dist/ws-framing.js.map
CHANGED
|
@@ -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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"}
|
package/dist/ws-gemini-live.cjs
CHANGED
|
@@ -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
|
|
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
|
|
401
|
-
const
|
|
402
|
-
journalEntry
|
|
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)
|
|
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,
|
|
410
|
-
if (
|
|
411
|
-
|
|
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
|
-
|
|
421
|
-
if (
|
|
422
|
-
|
|
422
|
+
lifecycle.tick();
|
|
423
|
+
if (lifecycle.signal.aborted || ws.isClosed) {
|
|
424
|
+
interruptTurn();
|
|
423
425
|
return;
|
|
424
426
|
}
|
|
425
|
-
if (!malformed
|
|
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
|
-
|
|
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 (
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
|
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,
|
|
518
|
-
if (
|
|
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
|
-
|
|
545
|
-
if (
|
|
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,
|
|
557
|
-
if (
|
|
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
|
-
|
|
570
|
-
if (
|
|
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
|
|
579
|
-
|
|
580
|
-
|
|
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
|
|
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,
|
|
606
|
-
if (
|
|
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
|
-
|
|
618
|
-
if (
|
|
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
|
|
626
|
-
|
|
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,
|
|
637
|
-
if (
|
|
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
|
|
640
|
-
|
|
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
|
-
|
|
667
|
+
lifecycle.tick();
|
|
654
668
|
}
|
|
655
|
-
if (
|
|
669
|
+
if (lifecycle.signal.aborted || ws.isClosed) {
|
|
656
670
|
ws.destroy();
|
|
657
|
-
journalEntry
|
|
658
|
-
|
|
659
|
-
|
|
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)
|
|
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
|
|
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,
|
|
697
|
-
if (
|
|
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
|
-
|
|
709
|
-
if (
|
|
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
|
|
717
|
-
|
|
718
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
741
|
-
if (
|
|
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
|
|
744
|
-
|
|
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
|
-
|
|
766
|
-
if (
|
|
789
|
+
lifecycle.tick();
|
|
790
|
+
if (lifecycle.signal.aborted || ws.isClosed) {
|
|
767
791
|
ws.destroy();
|
|
768
|
-
journalEntry
|
|
769
|
-
|
|
770
|
-
|
|
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,
|