diff --git a/codex_retry_gateway_tui.py b/codex_retry_gateway_tui.py index 21f8834..2c848b6 100644 --- a/codex_retry_gateway_tui.py +++ b/codex_retry_gateway_tui.py @@ -23,7 +23,7 @@ from typing import Any APP_NAME = "codex-retry-gateway-tui" -FALLBACK_VERSION = "0.1.7" +FALLBACK_VERSION = "0.1.8" DEFAULT_GATEWAY_ADMIN_PATH = "/__codex_retry_gateway" DEFAULT_GATEWAY_URL = "http://127.0.0.1:4610/__codex_retry_gateway" DEFAULT_API_URL = DEFAULT_GATEWAY_URL @@ -870,6 +870,7 @@ def normalize_request_retry_firsts(value: Any) -> list[dict[str, Any]]: { "round": parse_int_value(item.get("round")), "slot": parse_int_value(item.get("slot")), + "first_response_at": item.get("first_response_at"), "first_delay_ms": item.get("first_response_delay_ms", item.get("first_delay_ms", item.get("first_ms"))), "outcome": str(item.get("outcome") or "").strip(), "status_code": item.get("status_code", item.get("upstream_status_code", item.get("status"))), @@ -1339,13 +1340,38 @@ def request_match_score(row: dict[str, Any], needle: str) -> bool: return needle in haystack +def request_retry_first_received_at(row: dict[str, Any]) -> Any: + latest_value = None + latest_at = None + for first in row.get("reasoning_retry_current_firsts") or []: + if not isinstance(first, dict): + continue + value = first.get("first_response_at") + parsed = parse_datetime(value) + if parsed and (latest_at is None or parsed > latest_at): + latest_value = value + latest_at = parsed + return latest_value + + +def request_has_received_first(row: dict[str, Any]) -> bool: + if parse_datetime(row.get("first_response_at")): + return True + for first in row.get("reasoning_retry_current_firsts") or []: + if not isinstance(first, dict): + continue + if parse_datetime(first.get("first_response_at")) or parse_int_value(first.get("first_delay_ms")) is not None: + return True + return False + + def request_lifecycle_label(row: dict[str, Any]) -> str: state = str(row.get("lifecycle_state") or "").strip().lower() if request_is_discarded(row): return "discarded" if state in {"finish", "finished", "complete", "completed"} or parse_datetime(row.get("finished_at")): return "finished" - if state in {"receive_first", "streaming"} or parse_datetime(row.get("first_response_at")): + if state in {"receive_first", "streaming"} or request_has_received_first(row): return "streaming" return "waiting" @@ -1368,11 +1394,12 @@ def request_is_discarded(row: dict[str, Any]) -> bool: def request_activity_at(row: dict[str, Any]) -> Any: - for key in ("last_activity_at", "first_response_at", "started_at"): - value = row.get(key) - if parse_datetime(value): - return value - return None + candidates = [row.get("last_activity_at"), row.get("first_response_at"), request_retry_first_received_at(row), row.get("started_at")] + parsed_candidates = [(parse_datetime(value), value) for value in candidates] + valid_candidates = [(parsed, value) for parsed, value in parsed_candidates if parsed] + if not valid_candidates: + return None + return max(valid_candidates, key=lambda item: item[0])[1] def request_activity_age_seconds(row: dict[str, Any], now: dt.datetime | None = None) -> float | None: diff --git a/pyproject.toml b/pyproject.toml index 7df42dc..7adae8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "codex-retry-gateway-tui" -version = "0.1.7" +version = "0.1.8" description = "Terminal UI for codex-retry-gateway monitoring and control" readme = "README.md" requires-python = ">=3.11" diff --git a/tests/test_payload.py b/tests/test_payload.py index 46a368b..7fd5a3c 100644 --- a/tests/test_payload.py +++ b/tests/test_payload.py @@ -143,6 +143,18 @@ class CodexRetryGatewayTUITests(unittest.TestCase): "first_response_at": "2026-07-10T11:59:55Z", "last_activity_at": "2026-07-10T11:59:55Z", } + retry_streaming = { + "lifecycle_state": "sent", + "started_at": "2026-07-10T11:59:00Z", + "reasoning_retry_current_firsts": [ + { + "round": 1, + "slot": 1, + "first_response_at": "2026-07-10T11:59:58Z", + "first_delay_ms": 2200, + } + ], + } complete = {"lifecycle_state": "finish", "status_code": 200} discarded = { "lifecycle_state": "finish", @@ -161,6 +173,8 @@ class CodexRetryGatewayTUITests(unittest.TestCase): self.assertEqual(mod.request_status_label(waiting, now), "waiting") self.assertEqual(mod.request_status_label(streaming, now), "streaming") + self.assertEqual(mod.request_status_label(retry_streaming, now), "streaming") + self.assertEqual(mod.request_activity_at(retry_streaming), "2026-07-10T11:59:58Z") self.assertEqual(mod.request_status_label(complete, now), "HTTP 200") self.assertEqual(mod.request_status_label(discarded, now), "discarded") self.assertEqual(mod.request_status_label(slow_waiting, now), "waiting 30.0s") diff --git a/uv.lock b/uv.lock index df2e362..739c1c0 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.11" [[package]] name = "codex-retry-gateway-tui" -version = "0.1.7" +version = "0.1.8" source = { editable = "." } dependencies = [ { name = "textual" },