mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 23:32:12 +03:00
fix(cli): log Codex Responses WebSocket history/usage per logical turn, not per connection (#7588)
ResponsesWsSession.persistHistory() guarded on a single historyLogged boolean set once for the lifetime of the WebSocket connection. When a Codex client reuses one connection for multiple sequential response.create turns, only the first terminal event was persisted to call_logs — every subsequent turn's usage/history was silently dropped. firstResponseBody had the same per-connection freeze (||=), so even a hypothetical second log entry would still carry turn 1's request body. Replace the boolean with a Set keyed by the terminal event's response.id (falling back to a session-scoped sentinel for session-ending failure paths that don't carry a response id: prepare failure, upstream error/close, connect failure), and track each turn's own request body via currentRequestBody instead of freezing on firstResponseBody. This logs exactly once per logical turn while keeping session-ending failures logged exactly once, and each logged call now carries its own terminal response id and request payload. Regression test: tests/unit/responses-ws-proxy-multi-turn-history.test.ts opens one WS connection, sends two response.create turns, and asserts two distinct call-log entries land at the internal bridge, each with its own response id and request body. Closes #7388
This commit is contained in:
committed by
GitHub
parent
52b26c88c9
commit
de9cfcd940
@@ -31,6 +31,9 @@ const WS_QUERY_TOKEN_KEYS = ["api_key", "token", "access_token"];
|
||||
const textDecoder = new TextDecoder();
|
||||
const DEFAULT_MAX_WS_BUFFER_BYTES = 16 * 1024 * 1024;
|
||||
const DEFAULT_MAX_WS_MESSAGE_BYTES = 16 * 1024 * 1024;
|
||||
// #7388: sentinel turn key for session-ending terminal events that don't carry
|
||||
// a `response.id` (prepare failure, upstream error/close, connect failure).
|
||||
const SESSION_TERMINAL_TURN_KEY = "__session_terminal__";
|
||||
|
||||
class WebSocketInputTooLargeError extends Error {
|
||||
constructor(message, reason = "message_too_large") {
|
||||
@@ -414,8 +417,16 @@ class ResponsesWsSession {
|
||||
this.upstream = null;
|
||||
this.upstreamReady = null;
|
||||
this.firstResponseBody = null;
|
||||
this.currentRequestBody = null;
|
||||
this.preparedContext = null;
|
||||
this.historyLogged = false;
|
||||
// #7388: logging must be scoped per logical turn (one `response.create`
|
||||
// through its terminal event), not once for the lifetime of the WS
|
||||
// connection — a single boolean here silently dropped every turn after
|
||||
// the first on a reused connection. Terminal events carry a
|
||||
// `response.id` we can key on; session-ending failure paths (prepare
|
||||
// failure, upstream error/close, connect failure) don't, so they fall
|
||||
// back to a session-scoped sentinel key that still logs exactly once.
|
||||
this.loggedTurnIds = new Set();
|
||||
this.lastSeenAt = Date.now();
|
||||
|
||||
this.pingTimer = setInterval(() => {
|
||||
@@ -577,6 +588,7 @@ class ResponsesWsSession {
|
||||
throw new Error("First Responses WebSocket message must be response.create");
|
||||
}
|
||||
this.firstResponseBody ||= responseBody;
|
||||
this.currentRequestBody = responseBody;
|
||||
|
||||
const prepared = await callInternal(
|
||||
this.fetchImpl,
|
||||
@@ -681,6 +693,12 @@ class ResponsesWsSession {
|
||||
upstream.send(jsonStringifySafe(firstMessage));
|
||||
return;
|
||||
}
|
||||
// #7388: a reused WS connection forwards subsequent response.create
|
||||
// turns straight through (ensureUpstream() only runs once); track each
|
||||
// turn's own request body so persistHistory() attaches the right
|
||||
// clientRequest instead of always the first turn's.
|
||||
const nextTurnBody = getResponseCreatePayload(message);
|
||||
if (nextTurnBody !== null) this.currentRequestBody = nextTurnBody;
|
||||
this.upstream.send(jsonStringifySafe(message));
|
||||
} catch (error) {
|
||||
const code = error?.code || "upstream_websocket_connect_failed";
|
||||
@@ -705,8 +723,17 @@ class ResponsesWsSession {
|
||||
terminalMessage = null,
|
||||
responseBody = null,
|
||||
} = {}) {
|
||||
if (this.historyLogged || !this.firstResponseBody) return;
|
||||
this.historyLogged = true;
|
||||
if (!this.firstResponseBody) return;
|
||||
// #7388: key the "already logged" guard per logical turn instead of once
|
||||
// per WS connection. Terminal events from a real response carry
|
||||
// `response.id` — use it so each turn on a reused connection logs
|
||||
// independently, while the same id firing twice (retries) still logs
|
||||
// exactly once. Session-ending failure paths (prepare failure, upstream
|
||||
// error/close, connect failure) don't carry a response id — they end the
|
||||
// session, so they share one sentinel key and still log exactly once.
|
||||
const turnId = toStringOrNull(terminalMessage?.response?.id) || SESSION_TERMINAL_TURN_KEY;
|
||||
if (this.loggedTurnIds.has(turnId)) return;
|
||||
this.loggedTurnIds.add(turnId);
|
||||
|
||||
const finishedAt = Date.now();
|
||||
try {
|
||||
@@ -723,7 +750,7 @@ class ResponsesWsSession {
|
||||
success,
|
||||
errorCode,
|
||||
errorMessage,
|
||||
clientRequest: this.firstResponseBody,
|
||||
clientRequest: this.currentRequestBody || this.firstResponseBody,
|
||||
terminalMessage,
|
||||
responseBody,
|
||||
sourceFormat: "openai-responses",
|
||||
|
||||
Reference in New Issue
Block a user