Files
Teknium 43cbb0e479 fix(kanban): managed workers book credential start failures as 75/78, not 1
The one-shot worker's credential mapping (main 89096ba56c / 5fd24f47fd:
_single_query_exit_code(None, credentials_rate_limited=..., credentials_terminal=...))
moved to hermes_cli/turn_exit.turn_exit_code, but the managed path never
passed the flags: a provider quota wall or revoked login during agent
construction crashed the worker with no worker_result, so the dispatcher's
submitter exited 1 and the card spent its failure budget (or retried into
a dead credential) instead of EX_TEMPFAIL requeue / EX_CONFIG park.

- turn_exit.credential_failure_flags(exc): the CLI's predicates
  (is_rate_limited_auth_error / AuthError.relogin_required), walking the
  cause chain because agent construction wraps the resolver error.
- session_kanban.record_start_failure + managed_worker._construct_agent:
  book the mapped exit before the crash; run_worker_turns maps exceptions
  the same way. Unknown failures keep exit 1.

Also: web_routers/display.py annotated _open_rfb with Path without a
module-level import (only a function-local one); import it at module scope.

(cherry picked from commit 7363a92d60f312d8670cbd44115b910262d13a68)
2026-10-06 21:58:27 -07:00

60 lines
2.7 KiB
Python

"""Turn-result exit policy shared by CLI views and authority-owned workers.
Worker identity is supplied by the caller, not inferred from a daemon's environment.
"""
# Provider walls do not spend a Kanban task's retry budget.
TRANSIENT_PROVIDER_REASONS = frozenset({
"rate_limit", "upstream_rate_limit", "billing", "overloaded", "server_error", "timeout",
})
# These failures need an operator change; the dispatcher parks the task immediately.
TERMINAL_PROVIDER_REASONS = frozenset({
"auth", "auth_permanent", "model_not_found", "ssl_cert_verification", "upstream_blocked",
})
def turn_exit_code(
result, *, kanban_worker: bool, credentials_rate_limited: bool = False,
credentials_terminal: bool = False,
transient_reasons=TRANSIENT_PROVIDER_REASONS, terminal_reasons=TERMINAL_PROVIDER_REASONS,
) -> int:
"""Preserve completion, interruption and retryable/terminal provider outcomes."""
if not isinstance(result, dict):
if credentials_rate_limited and kanban_worker:
from hermes_cli.kanban_db import KANBAN_RATE_LIMIT_EXIT_CODE
return KANBAN_RATE_LIMIT_EXIT_CODE
if credentials_terminal and kanban_worker:
from hermes_cli.kanban_db import KANBAN_TERMINAL_PROVIDER_EXIT_CODE
return KANBAN_TERMINAL_PROVIDER_EXIT_CODE
return 1
if result.get("interrupted"):
return 130
if not (result.get("failed") or result.get("partial") or result.get("completed") is False):
return 0
if kanban_worker:
reason = result.get("failure_reason")
if reason in transient_reasons:
from hermes_cli.kanban_db import KANBAN_RATE_LIMIT_EXIT_CODE
return KANBAN_RATE_LIMIT_EXIT_CODE
if reason in terminal_reasons:
from hermes_cli.kanban_db import KANBAN_TERMINAL_PROVIDER_EXIT_CODE
return KANBAN_TERMINAL_PROVIDER_EXIT_CODE
return 1
def credential_failure_flags(exc) -> dict:
"""``turn_exit_code`` credential kwargs for an exception raised before any turn result exists.
Same predicates as the one-shot CLI's ``_ensure_runtime_credentials``: a quota/rate-limit
``AuthError`` is transient, only an explicit re-authentication requirement is terminal. The
cause chain is walked because agent construction wraps the resolver's error."""
from hermes_cli.auth import AuthError, is_rate_limited_auth_error
seen = set()
while exc is not None and id(exc) not in seen:
seen.add(id(exc))
if isinstance(exc, AuthError):
return {"credentials_rate_limited": is_rate_limited_auth_error(exc),
"credentials_terminal": bool(exc.relogin_required)}
exc = exc.__cause__ or exc.__context__
return {}