fix(oauth,gateway): monotonic deadlines for polling/timeout loops

Widen PR #20314's fix to the other timeout-polling sites in the codebase
that share the same wall-clock-jump bug class. All of these measure elapsed
timeout duration, not civil time, so they belong on time.monotonic().

- hermes_cli/auth.py: auth-store file-lock timeout, Spotify OAuth callback
  wait, Nous portal device-auth token poll.
- hermes_cli/copilot_auth.py: Copilot OAuth device-flow token poll.
- hermes_cli/gateway.py: gateway systemd restart wait.
- hermes_cli/web_server.py: dashboard Codex device-auth user_code wait,
  dashboard Nous device-auth token poll. (sess["expires_at"] stays on
  time.time() — it's a persisted absolute timestamp, not a local
  deadline-polling variable.)
- agent/copilot_acp_client.py: Copilot ACP JSON-RPC request timeout.
This commit is contained in:
teknium1 2026-05-07 05:05:24 -07:00 committed by Teknium
parent 6e8f1e09a9
commit 2e00bcaaab
5 changed files with 16 additions and 16 deletions

View file

@ -894,7 +894,7 @@ def _file_lock(
lock_path.write_text(" ", encoding="utf-8")
with lock_path.open("r+" if msvcrt else "a+") as lock_file:
deadline = time.time() + max(1.0, timeout_seconds)
deadline = time.monotonic() + max(1.0, timeout_seconds)
while True:
try:
if fcntl:
@ -904,7 +904,7 @@ def _file_lock(
msvcrt.locking(lock_file.fileno(), msvcrt.LK_NBLCK, 1)
break
except (BlockingIOError, OSError, PermissionError):
if time.time() >= deadline:
if time.monotonic() >= deadline:
raise TimeoutError(timeout_message)
time.sleep(0.05)
@ -1974,9 +1974,9 @@ def _spotify_wait_for_callback(
thread = threading.Thread(target=server.serve_forever, kwargs={"poll_interval": 0.1}, daemon=True)
thread.start()
deadline = time.time() + max(5.0, timeout_seconds)
deadline = time.monotonic() + max(5.0, timeout_seconds)
try:
while time.time() < deadline:
while time.monotonic() < deadline:
if result["code"] or result["error"]:
return result
time.sleep(0.1)
@ -2739,10 +2739,10 @@ def _poll_for_token(
poll_interval: int,
) -> Dict[str, Any]:
"""Poll the token endpoint until the user approves or the code expires."""
deadline = time.time() + max(1, expires_in)
deadline = time.monotonic() + max(1, expires_in)
current_interval = max(1, min(poll_interval, DEVICE_AUTH_POLL_INTERVAL_CAP_SECONDS))
while time.time() < deadline:
while time.monotonic() < deadline:
response = client.post(
f"{portal_base_url}/api/oauth/token",
data={

View file

@ -212,9 +212,9 @@ def copilot_device_code_login(
print(" Waiting for authorization...", end="", flush=True)
# Step 3: Poll for completion
deadline = time.time() + timeout_seconds
deadline = time.monotonic() + timeout_seconds
while time.time() < deadline:
while time.monotonic() < deadline:
time.sleep(interval + _DEVICE_CODE_POLL_SAFETY_MARGIN)
poll_data = urllib.parse.urlencode({

View file

@ -585,10 +585,10 @@ def _wait_for_systemd_service_restart(
svc = get_service_name()
scope_label = _service_scope_label(system).capitalize()
deadline = time.time() + timeout
deadline = time.monotonic() + timeout
printed_runtime_wait = False
while time.time() < deadline:
while time.monotonic() < deadline:
props = _read_systemd_unit_properties(system=system)
active_state = props.get("ActiveState", "")
sub_state = props.get("SubState", "")

View file

@ -1877,8 +1877,8 @@ async def _start_device_code_flow(provider_id: str) -> Dict[str, Any]:
name=f"oauth-codex-{sid[:6]}",
).start()
# Block briefly until the worker has populated the user_code, OR error.
deadline = time.time() + 10
while time.time() < deadline:
deadline = time.monotonic() + 10
while time.monotonic() < deadline:
with _oauth_sessions_lock:
s = _oauth_sessions.get(sid)
if s and (s.get("user_code") or s["status"] != "pending"):
@ -2012,10 +2012,10 @@ def _codex_full_login_worker(session_id: str) -> None:
sess["expires_at"] = time.time() + sess["expires_in"]
# Step 2: poll until authorized
deadline = time.time() + sess["expires_in"]
deadline = time.monotonic() + sess["expires_in"]
code_resp = None
with httpx.Client(timeout=httpx.Timeout(15.0)) as client:
while time.time() < deadline:
while time.monotonic() < deadline:
time.sleep(poll_interval)
poll = client.post(
f"{issuer}/api/accounts/deviceauth/token",