From e7a7e077cbfb4b6f63ce32a06d5ac43b0e643f4e Mon Sep 17 00:00:00 2001 From: Darafei Praliaskouski Date: Tue, 4 Aug 2026 00:44:12 +0400 Subject: [PATCH 1/4] fix(http-bridge): keep missing-created watchdog armed after prelude --- .../proxy/_service/http_bridge/helpers.py | 1 - tests/unit/test_proxy_http_bridge.py | 24 +++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/app/modules/proxy/_service/http_bridge/helpers.py b/app/modules/proxy/_service/http_bridge/helpers.py index 146c10fa42..93e6e035d1 100644 --- a/app/modules/proxy/_service/http_bridge/helpers.py +++ b/app/modules/proxy/_service/http_bridge/helpers.py @@ -731,7 +731,6 @@ def _http_bridge_eventless_precreated_deadline( or sent_at is None or request_state.response_id is not None or request_state.latency_response_created_ms is not None - or request_state.response_event_count != 0 or request_state.downstream_visible or request_state.last_downstream_sequence_number is not None ): diff --git a/tests/unit/test_proxy_http_bridge.py b/tests/unit/test_proxy_http_bridge.py index dce89bd5a1..b6a80eb53e 100644 --- a/tests/unit/test_proxy_http_bridge.py +++ b/tests/unit/test_proxy_http_bridge.py @@ -222,7 +222,6 @@ def test_http_bridge_eventless_precreated_deadline_uses_current_send_and_client_ [ ("response_id", "resp-created"), ("latency_response_created_ms", 12), - ("response_event_count", 1), ("downstream_visible", True), ("last_downstream_sequence_number", 0), ("awaiting_response_created", False), @@ -247,6 +246,24 @@ def test_http_bridge_eventless_precreated_deadline_requires_narrow_owner_evidenc ) +def test_http_bridge_eventless_precreated_deadline_survives_reasoning_prelude_without_created() -> None: + request_state = _make_eventless_http_bridge_owner() + request_state.response_event_count = 3 + request_state.upstream_model_output_seen = True + request_state.deferred_reasoning_downstream_texts.append( + 'data: {"type":"response.output_item.added","item":{"type":"reasoning"}}\n\n' + ) + client_safe_cap_seconds = http_bridge_helpers_module._HTTP_BRIDGE_EVENTLESS_RESPONSE_CREATED_MAX_SECONDS + + assert ( + http_bridge_helpers_module._http_bridge_eventless_precreated_deadline( + request_state, + stuck_gate_retire_after_seconds=300.0, + ) + == 100.0 + min(300.0, client_safe_cap_seconds) + ) + + @pytest.mark.asyncio async def test_http_bridge_send_replaces_timestamp_and_wakes_existing_reader( monkeypatch: pytest.MonkeyPatch, @@ -19519,6 +19536,9 @@ async def close(self) -> None: owner.request_text = '{"type":"response.create","model":"gpt-5.6-sol","input":"hello"}' owner.preferred_account_id = "acc-bridge" owner.excluded_account_ids.add("acc-excluded") + if leading_telemetry: + owner.response_event_count = 1 + owner.upstream_model_output_seen = True sibling_queue: asyncio.Queue[str | None] = asyncio.Queue() sibling = proxy_service._WebSocketRequestState( request_id="req-created-sibling", @@ -19570,7 +19590,7 @@ async def close(self) -> None: assert owner.preferred_account_id == "acc-bridge" assert owner.excluded_account_ids == {"acc-excluded"} assert owner.replay_count == 0 - assert owner.response_event_count == 0 + assert owner.response_event_count == (1 if leading_telemetry else 0) if leading_telemetry: assert owner.latency_first_upstream_event_ms is not None retry_precreated.assert_awaited_once_with(session) From a9250327966c13acefcdaca9f94470100f879a5c Mon Sep 17 00:00:00 2001 From: Darafei Praliaskouski Date: Tue, 4 Aug 2026 01:25:24 +0400 Subject: [PATCH 2/4] test(http-bridge): format precreated deadline assertion --- tests/unit/test_proxy_http_bridge.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_proxy_http_bridge.py b/tests/unit/test_proxy_http_bridge.py index b6a80eb53e..579a061757 100644 --- a/tests/unit/test_proxy_http_bridge.py +++ b/tests/unit/test_proxy_http_bridge.py @@ -255,13 +255,10 @@ def test_http_bridge_eventless_precreated_deadline_survives_reasoning_prelude_wi ) client_safe_cap_seconds = http_bridge_helpers_module._HTTP_BRIDGE_EVENTLESS_RESPONSE_CREATED_MAX_SECONDS - assert ( - http_bridge_helpers_module._http_bridge_eventless_precreated_deadline( - request_state, - stuck_gate_retire_after_seconds=300.0, - ) - == 100.0 + min(300.0, client_safe_cap_seconds) - ) + assert http_bridge_helpers_module._http_bridge_eventless_precreated_deadline( + request_state, + stuck_gate_retire_after_seconds=300.0, + ) == 100.0 + min(300.0, client_safe_cap_seconds) @pytest.mark.asyncio From 7ea33f4a3aaf601c174a72cf423de5de52171322 Mon Sep 17 00:00:00 2001 From: Darafei Praliaskouski Date: Thu, 6 Aug 2026 08:48:24 +0400 Subject: [PATCH 3/4] fix(http-bridge): re-anchor missing-created watchdog after prelude --- app/modules/proxy/_service/http_bridge/helpers.py | 10 +++++++--- .../specs/proxy-admission-control/spec.md | 13 ++++++++++--- tests/unit/test_proxy_http_bridge.py | 4 ++-- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/modules/proxy/_service/http_bridge/helpers.py b/app/modules/proxy/_service/http_bridge/helpers.py index 93e6e035d1..0524e53f39 100644 --- a/app/modules/proxy/_service/http_bridge/helpers.py +++ b/app/modules/proxy/_service/http_bridge/helpers.py @@ -737,9 +737,13 @@ def _http_bridge_eventless_precreated_deadline( return None # Non-response telemetry (for example ``codex.rate_limits``) may update # the generic activity marker, but it must not extend the response.create - # acknowledgement deadline. The eventless watchdog is intentionally - # anchored to the send time until a response-lifecycle event is observed. - return sent_at + min( + # acknowledgement deadline. Keep the send-time anchor until a matched + # response-lifecycle event exists; then use the latest lifecycle activity + # so deferred reasoning is not retired from the original send time. + deadline_anchor = sent_at + if request_state.response_event_count > 0 and request_state.last_upstream_activity_at is not None: + deadline_anchor = request_state.last_upstream_activity_at + return deadline_anchor + min( float(stuck_gate_retire_after_seconds), _HTTP_BRIDGE_EVENTLESS_RESPONSE_CREATED_MAX_SECONDS, ) diff --git a/openspec/changes/recover-codex-desktop-idle-bridge/specs/proxy-admission-control/spec.md b/openspec/changes/recover-codex-desktop-idle-bridge/specs/proxy-admission-control/spec.md index 293763926d..87e239f368 100644 --- a/openspec/changes/recover-codex-desktop-idle-bridge/specs/proxy-admission-control/spec.md +++ b/openspec/changes/recover-codex-desktop-idle-bridge/specs/proxy-admission-control/spec.md @@ -4,7 +4,7 @@ The proxy MUST retain the existing waiter-triggered retirement behavior for stale HTTP bridge response-create gate owners and MUST additionally enforce an owner-side deadline for a visible HTTP request whose current upstream `response.create` send remains completely eventless before `response.created`. The owner-side deadline MUST be measured from a monotonic timestamp recorded immediately before the current upstream send, MUST use the smaller of the configured stuck-gate retirement threshold and 60 seconds, MUST run without a second gate waiter, and MUST remain active when periodic SSE keepalives are disabled. -The owner-side watchdog MUST apply only while the request owns the response-create gate, awaits `response.created`, has neither a response id nor recorded `response.created` latency, has received no matched `response.*` lifecycle event, and has produced no downstream-visible output or sequence evidence. Non-response telemetry such as `codex.rate_limits` MUST NOT suppress this watchdog. Any matched `response.*` lifecycle event, response-created milestone, or downstream-visible evidence MUST suppress the owner-side watchdog and leave existing timeout behavior unchanged. +The owner-side watchdog MUST apply only while the request owns the response-create gate, awaits `response.created`, has neither a response id nor recorded `response.created` latency, and has produced no downstream-visible output or sequence evidence. Before any matched `response.*` lifecycle event, the deadline MUST remain anchored to the current upstream send; non-response telemetry such as `codex.rate_limits` MUST NOT suppress or extend it. If matched `response.*` lifecycle events arrive without `response.created`, the watchdog MUST remain armed and re-anchor from the most recent upstream response-lifecycle activity instead of the original send. A response-created milestone or downstream-visible evidence MUST suppress this narrow watchdog and leave existing timeout behavior unchanged. When the owner-side deadline expires, the proxy MUST recheck eligibility and emit a structured low-cardinality log and the existing stuck-retirement Prometheus counter. For requests that are not eligible for the bounded fresh-hard recovery defined by `recover-fresh-hard-bridge-timeouts`, it MUST terminally fail and settle every pending request exactly once, retire the whole bridge session, and MUST NOT transparently replay the timed-out request or move it to another account. An eligible fresh hard request MAY take that single bounded recovery path; if recovery is unavailable or fails, it MUST fall back to the same terminal fail-closed retirement. Neither path may write an account-health failure solely because `response.created` was missing. @@ -32,9 +32,16 @@ When the owner-side deadline expires, the proxy MUST recheck eligibility and emi - **THEN** the telemetry does not refresh or suppress the deadline - **AND** the proxy fails and retires the session -#### Scenario: Response lifecycle evidence suppresses the narrow watchdog +#### Scenario: Response lifecycle evidence re-anchors the missing-created watchdog -- **GIVEN** a pre-created request receives any matched `response.*` lifecycle event, a response id, recorded `response.created` latency, or downstream-visible output +- **GIVEN** a pre-created request receives matched `response.*` lifecycle events but no response id, recorded `response.created` latency, or downstream-visible output +- **WHEN** a new response-lifecycle event arrives +- **THEN** the watchdog deadline is re-anchored from the most recent upstream response-lifecycle activity +- **AND** the watchdog remains armed until response-created or downstream-visible evidence appears + +#### Scenario: Response-created or visible evidence suppresses the narrow watchdog + +- **GIVEN** a pre-created request receives a response id, recorded `response.created` latency, or downstream-visible output - **WHEN** the eventless owner-side deadline would otherwise elapse - **THEN** this watchdog does not retire the session - **AND** existing stream, request-budget, and waiter-triggered timeout behavior remains authoritative diff --git a/tests/unit/test_proxy_http_bridge.py b/tests/unit/test_proxy_http_bridge.py index 579a061757..2daf24f5af 100644 --- a/tests/unit/test_proxy_http_bridge.py +++ b/tests/unit/test_proxy_http_bridge.py @@ -249,16 +249,16 @@ def test_http_bridge_eventless_precreated_deadline_requires_narrow_owner_evidenc def test_http_bridge_eventless_precreated_deadline_survives_reasoning_prelude_without_created() -> None: request_state = _make_eventless_http_bridge_owner() request_state.response_event_count = 3 + request_state.last_upstream_activity_at = 150.0 request_state.upstream_model_output_seen = True request_state.deferred_reasoning_downstream_texts.append( 'data: {"type":"response.output_item.added","item":{"type":"reasoning"}}\n\n' ) - client_safe_cap_seconds = http_bridge_helpers_module._HTTP_BRIDGE_EVENTLESS_RESPONSE_CREATED_MAX_SECONDS assert http_bridge_helpers_module._http_bridge_eventless_precreated_deadline( request_state, stuck_gate_retire_after_seconds=300.0, - ) == 100.0 + min(300.0, client_safe_cap_seconds) + ) == 150.0 + http_bridge_helpers_module._HTTP_BRIDGE_EVENTLESS_RESPONSE_CREATED_MAX_SECONDS @pytest.mark.asyncio From 66030e1a46ba4067b7cbbcd88b219c79f5e3fe82 Mon Sep 17 00:00:00 2001 From: Darafei Praliaskouski Date: Thu, 6 Aug 2026 08:51:33 +0400 Subject: [PATCH 4/4] style(http-bridge): format deadline regression assertion --- tests/unit/test_proxy_http_bridge.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_proxy_http_bridge.py b/tests/unit/test_proxy_http_bridge.py index 2daf24f5af..ea88d53d01 100644 --- a/tests/unit/test_proxy_http_bridge.py +++ b/tests/unit/test_proxy_http_bridge.py @@ -255,10 +255,13 @@ def test_http_bridge_eventless_precreated_deadline_survives_reasoning_prelude_wi 'data: {"type":"response.output_item.added","item":{"type":"reasoning"}}\n\n' ) - assert http_bridge_helpers_module._http_bridge_eventless_precreated_deadline( - request_state, - stuck_gate_retire_after_seconds=300.0, - ) == 150.0 + http_bridge_helpers_module._HTTP_BRIDGE_EVENTLESS_RESPONSE_CREATED_MAX_SECONDS + assert ( + http_bridge_helpers_module._http_bridge_eventless_precreated_deadline( + request_state, + stuck_gate_retire_after_seconds=300.0, + ) + == 150.0 + http_bridge_helpers_module._HTTP_BRIDGE_EVENTLESS_RESPONSE_CREATED_MAX_SECONDS + ) @pytest.mark.asyncio