diff --git a/agent/account_usage_cache.py b/agent/account_usage_cache.py index 0358fd5c58..695361b8d8 100644 --- a/agent/account_usage_cache.py +++ b/agent/account_usage_cache.py @@ -32,7 +32,7 @@ SNAPSHOT_STALE_AFTER_S = 15 * 60.0 _lock = threading.Lock() # slot key (home, provider, identity_id, base_url) → snapshot -_snapshots: dict[tuple[str, str, str, str], "AccountUsageSnapshot"] = {} +_snapshots: dict[tuple[str, str, str, str], AccountUsageSnapshot] = {} # slot key → monotonic time the stored snapshot's fetch STARTED (late-fetch guard) _started: dict[tuple[str, str, str, str], float] = {} # slot key → monotonic time of the last refresh try (success or failure) @@ -87,7 +87,7 @@ def _identity_id_for(provider: str, entry: Any) -> str: def remember_account_usage( - provider: Optional[str], snapshot: Optional["AccountUsageSnapshot"], + provider: Optional[str], snapshot: Optional[AccountUsageSnapshot], *, identity_id: Optional[str] = None, base_url: Optional[str] = None, started_monotonic: Optional[float] = None, ) -> None: @@ -127,12 +127,12 @@ def remember_account_usage( def cached_account_usage( provider: str, *, identity_id: Optional[str] = None, base_url: Optional[str] = None, -) -> Optional["AccountUsageSnapshot"]: +) -> Optional[AccountUsageSnapshot]: with _lock: return _snapshots.get(_key(provider, identity_id, base_url)) -def snapshot_is_stale(snapshot: Optional["AccountUsageSnapshot"]) -> bool: +def snapshot_is_stale(snapshot: Optional[AccountUsageSnapshot]) -> bool: """True when the snapshot is too old to render as a live gauge (the picker shows state ``unknown`` instead of trusting its percentages).""" from agent.account_usage import AccountUsageSnapshot as _Snapshot diff --git a/agent/activity_tracking.py b/agent/activity_tracking.py index 55da3b5907..984b6d8fbe 100644 --- a/agent/activity_tracking.py +++ b/agent/activity_tracking.py @@ -16,7 +16,7 @@ from agent.session_activity import ActivityProvenance logger = logging.getLogger("run_agent") -def _activity_lock(obj) -> "threading.Lock": +def _activity_lock(obj) -> threading.Lock: """Lazy per-instance ``_turn_liveness_activity_lock`` (so ``__new__``/SimpleNamespace doubles work).""" _lock = getattr(obj, "_turn_liveness_activity_lock", None) if _lock is None: @@ -28,7 +28,7 @@ def _activity_lock(obj) -> "threading.Lock": class ActivityTrackingMixin: """Liveness timestamps/labels and rate-limited session activity persistence.""" - def _liveness_activity_lock(self) -> "threading.Lock": + def _liveness_activity_lock(self) -> threading.Lock: """Shared lock for the activity clock and its generation counter. ``_touch_activity`` stamps under it and the liveness watchdog samples/commits under it, so a stall diff --git a/agent/agent_init.py b/agent/agent_init.py index c51c857c0c..52eb03305c 100644 --- a/agent/agent_init.py +++ b/agent/agent_init.py @@ -2388,7 +2388,7 @@ def init_agent( gateway_session_key: str | None = None, skip_context_files: bool = False, load_soul_identity: bool = False, skip_memory: bool = False, skip_background_review: bool = False, session_db=None, parent_session_id: str | None = None, - iteration_budget: "IterationBudget" = None, run_budget_seconds: Optional[float] = None, + iteration_budget: IterationBudget = None, run_budget_seconds: Optional[float] = None, fallback_model: dict[str, Any] | None = None, credential_pool=None, checkpoints_enabled: bool = False, checkpoint_max_snapshots: int = 20, checkpoint_max_total_size_mb: int = 500, checkpoint_max_file_size_mb: int = 10, pass_session_id: bool = False, diff --git a/agent/anthropic_credentials.py b/agent/anthropic_credentials.py index 4f8d1477b4..e3ba2f9c09 100644 --- a/agent/anthropic_credentials.py +++ b/agent/anthropic_credentials.py @@ -132,7 +132,7 @@ _SPENT_ROTATION_LOCK = threading.Lock() # Fingerprints of Claude Code refresh tokens the endpoint rejected terminally: the WARNING fires once per token # per process and later attempts skip the POST (a re-login rotates the token, so a new one is tried normally). _DEAD_REFRESH_TOKEN_FINGERPRINTS: set = set() -_SPENT_ROTATION_FINGERPRINTS: "OrderedDict[str, None]" = OrderedDict() +_SPENT_ROTATION_FINGERPRINTS: OrderedDict[str, None] = OrderedDict() _SPENT_ROTATION_MAX_TRACKED = 64 _SPENT_ROTATION_SIDECAR_COMMENT = ( "Non-secret one-way fingerprints of Anthropic OAuth credentials whose rotation was " @@ -238,7 +238,7 @@ def _claude_oauth_record(data: Any, source: str) -> Optional[dict[str, Any]]: _KEYCHAIN_ATTR = r'(?:0x(?P[0-9A-Fa-f]+)\b.*|"(?P.*)")' -def _decode_keychain_attr(match: Optional["re.Match[str]"]) -> str: +def _decode_keychain_attr(match: Optional[re.Match[str]]) -> str: """``security`` prints an attribute as ``"text"`` when it is plain printable ASCII and as ``0x ""`` otherwise; the quoted form is NOT escaped (an embedded ``"`` appears raw), so the text group must run to the last quote on the line.""" diff --git a/agent/async_utils.py b/agent/async_utils.py index 2bc558fad2..def2bd23a8 100644 --- a/agent/async_utils.py +++ b/agent/async_utils.py @@ -40,7 +40,7 @@ def safe_schedule_threadsafe( return None -def consume_detached_task_result(task: "asyncio.Future[Any]") -> None: +def consume_detached_task_result(task: asyncio.Future[Any]) -> None: """``add_done_callback`` for cancelled-and-detached tasks: observe the exception so the loop does not log "exception was never retrieved"; cancellation and terminal errors are swallowed because the task's owner already gave up on it.""" diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 7c5471a3dd..10a5b1814e 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -655,7 +655,7 @@ def remember_temperature_rejection( def _fixed_temperature_for_model( model: Optional[str], base_url: Optional[str] = None, provider: Optional[str] = None, -) -> "Optional[float] | object": +) -> Optional[float] | object: """``OMIT_TEMPERATURE`` (drop the key; Kimi/Moonshot, OpenAI reasoning families, routes that already rejected it), a fixed ``float``, or ``None``.""" if _is_kimi_model(model): @@ -7016,7 +7016,7 @@ def _client_streams_internally(client: Any) -> bool: _MANAGED_LOCAL_STATE_TTL_S = 15.0 -_managed_local_cache: "tuple[float, str]" = (0.0, "") +_managed_local_cache: tuple[float, str] = (0.0, "") def _managed_local_netloc() -> str: @@ -7123,7 +7123,7 @@ def _create_with_progress( return _create_with_progress_once(client, retry_kwargs, task, force_stream=force_stream) -def _stream_request_plan(kwargs: dict[str, Any]) -> "tuple[dict[str, Any], str, float]": +def _stream_request_plan(kwargs: dict[str, Any]) -> tuple[dict[str, Any], str, float]: """(stream kwargs, model name, total ceiling) for a streamed re-aggregation.""" stream_kwargs = dict(kwargs) stream_kwargs["stream"] = True @@ -7190,7 +7190,7 @@ def _create_with_progress_once( def _aggregate_chat_stream( chunks: Any, *, model: str = "", total_ceiling: Optional[float] = None, - no_progress: "Optional[tuple[float, Optional[float]]]" = None, + no_progress: Optional[tuple[float, Optional[float]]] = None, ) -> Any: """Consume a chunk stream into a complete response; TimeoutError (phrased "timed out" so ``_is_timeout_error`` matches) past *total_ceiling* or the *no_progress* windows (#100501).""" @@ -7576,7 +7576,7 @@ _FALLBACK_REASONS: tuple[tuple[Callable[[Exception], bool], str], ...] = ( ) -def _rung(step: "_LadderStep", accept: Callable[[Exception], bool]): +def _rung(step: _LadderStep, accept: Callable[[Exception], bool]): """One ladder rung: perform ``step``; yields ``(response, None)`` on success, ``(None, exc)`` when ``accept(exc)`` lets the next rung handle it, else re-raises.""" try: diff --git a/agent/auxiliary_stream_watchdog.py b/agent/auxiliary_stream_watchdog.py index bc25a1e1e7..f3a6c5abfd 100644 --- a/agent/auxiliary_stream_watchdog.py +++ b/agent/auxiliary_stream_watchdog.py @@ -132,7 +132,7 @@ class ChatStreamWatchdog: f"({elapsed:.1f}s elapsed, timed out)") -def chat_stream_windows(client: Any, kwargs: dict, task: Optional[str]) -> "tuple[float, Optional[float]]": +def chat_stream_windows(client: Any, kwargs: dict, task: Optional[str]) -> tuple[float, Optional[float]]: """(inter-chunk window, first-token window) for a streamed chat-completions attempt. Between chunks: the Codex guard's window (``auxiliary..no_progress_timeout``, 60s default). Before the first token: the main loop's cloud stale patience for the routed provider (its explicit @@ -156,7 +156,7 @@ def chat_stream_windows(client: Any, kwargs: dict, task: Optional[str]) -> "tupl return window, first -def consume_chat_stream(chunks: Any, acc: Any, no_progress: "Optional[tuple[float, Optional[float]]]") -> None: +def consume_chat_stream(chunks: Any, acc: Any, no_progress: Optional[tuple[float, Optional[float]]]) -> None: """Feed *chunks* into the accumulator *acc*. Under a ``(window, first_token_window)`` watchdog a silent stream raises TimeoutError, except after the terminal chunk: with finish_reason (and any usage) in hand a stall is teardown, so the completed response and its billed usage are kept.""" diff --git a/agent/azure_identity_adapter.py b/agent/azure_identity_adapter.py index 443d1d0a64..a75b41e789 100644 --- a/agent/azure_identity_adapter.py +++ b/agent/azure_identity_adapter.py @@ -89,7 +89,7 @@ class EntraIdentityConfig: return {"scope": self.scope, "exclude_interactive_browser": self.exclude_interactive_browser} @classmethod - def from_dict(cls, data: Optional[dict[str, Any]], *, default_scope: Optional[str] = None) -> "EntraIdentityConfig": + def from_dict(cls, data: Optional[dict[str, Any]], *, default_scope: Optional[str] = None) -> EntraIdentityConfig: data = data or {} return cls( scope=str(data.get("scope") or "").strip() or default_scope or SCOPE_AI_AZURE_DEFAULT, @@ -318,7 +318,7 @@ def build_bearer_http_client(token_provider: Callable[[], str], **httpx_kwargs: raise ValueError("build_bearer_http_client requires a zero-arg callable token provider") import httpx - def _inject_bearer(request: "httpx.Request") -> None: + def _inject_bearer(request: httpx.Request) -> None: try: token = materialize_bearer_for_http(token_provider) except ValueError as exc: diff --git a/agent/backend_identity.py b/agent/backend_identity.py index 011a58f160..aba64f62c0 100644 --- a/agent/backend_identity.py +++ b/agent/backend_identity.py @@ -51,7 +51,7 @@ class BackendIdentity: def build( cls, provider: Optional[str] = None, model: Optional[str] = None, base_url: Optional[str] = None, - ) -> "BackendIdentity": + ) -> BackendIdentity: return cls( provider=_norm(provider), model=_norm(model), base_url=normalize_route_base_url(base_url), diff --git a/agent/chat_completion_helpers.py b/agent/chat_completion_helpers.py index ae234f95d6..2a31db654a 100644 --- a/agent/chat_completion_helpers.py +++ b/agent/chat_completion_helpers.py @@ -486,7 +486,7 @@ def _provider_preferences_for_agent(agent) -> dict[str, Any]: return {key: value for key, value in merged.items() if value} -def _prompt_cache_scope_for_agent(agent) -> "str | None": +def _prompt_cache_scope_for_agent(agent) -> str | None: """Rotation-stable logical cache scope for *agent*, or None (transports then fall back to the physical session_id, so a failure never blocks the build).""" try: @@ -618,7 +618,7 @@ def _check_stale_giveup(agent) -> None: ) -def _stream_env_stale_base() -> "tuple[float, bool]": +def _stream_env_stale_base() -> tuple[float, bool]: """(HERMES_STREAM_STALE_TIMEOUT or the implicit 180s, explicit) — like ``AIAgent._resolved_api_call_stale_timeout_base``; an explicit env value is the user's deadline, so it is never capped to the run budget.""" @@ -700,7 +700,7 @@ def _cloud_stale_timeout_for(agent, api_kwargs: dict) -> float: return timeout if explicit_env else cap_to_run_budget(agent, timeout) -def _bedrock_reasoning_stale_floor(model_id: object) -> "float | None": +def _bedrock_reasoning_stale_floor(model_id: object) -> float | None: """Map a Bedrock inference-profile id to its reasoning stale-timeout floor. ``us.anthropic.claude-opus-4-6-v1:0`` -> strip the region prefix, then try the @@ -831,7 +831,7 @@ def should_use_direct_api_call(agent) -> bool: _DIRECT_API_ACTIVITY_HEARTBEAT_SECONDS = 15.0 -def _managed_local_load_notice(agent, api_kwargs: dict) -> "Optional[str]": +def _managed_local_load_notice(agent, api_kwargs: dict) -> Optional[str]: """Live phase notice ("⏳ loading into memory — N%" / "⚙ processing prompt — P%") while the managed local server works before the first token; None when neither applies. Otherwise a cold load reads as a generic stall.""" @@ -1824,7 +1824,7 @@ _FALLBACK_REASON_LABELS = { } -def _fallback_reason_text(reason: "FailoverReason | None") -> str: +def _fallback_reason_text(reason: FailoverReason | None) -> str: """Return a concise operator-facing explanation for a fallback switch.""" label = _FALLBACK_REASON_LABELS.get(reason) return label or str(getattr(reason, "value", None) or reason or "provider failure").replace("_", " ") @@ -1930,7 +1930,7 @@ def _log_fallback_activated(agent, reason, old_model, old_provider, fb_model, fb ) -def _fallback_chain_exhausted(agent, reason: "FailoverReason | None") -> bool: +def _fallback_chain_exhausted(agent, reason: FailoverReason | None) -> bool: """Chain exhausted (always False). A non-empty chain walked on a non-rate-limit failure arms a short cooldown so next turn's restore_primary_runtime stays gated instead of replaying the whole context across every provider again.""" @@ -2069,7 +2069,7 @@ def _buffer_fallback_notice(agent, notice: str) -> None: agent._pending_fallback_notice = [str(pending), notice] if pending else [notice] -def try_activate_fallback(agent, reason: "FailoverReason | None" = None, reset_at=None) -> bool: +def try_activate_fallback(agent, reason: FailoverReason | None = None, reset_at=None) -> bool: """Switch to the next fallback model/provider in the chain; False when exhausted. Swaps client, model slug and provider in place so the retry loop continues on the new backend; client construction goes through resolve_provider_client (no duplicated provider→key mappings).""" diff --git a/agent/context_compressor.py b/agent/context_compressor.py index 191b4e9f35..dce3fc7788 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -1055,7 +1055,7 @@ Spend up to ~{_LEAN_SESSION_LOG_BUDGET_TOKENS} tokens here — this section is t # (SHAs, ids, error strings) cannot be paraphrased away; also a session_search map. _LEAN_ANCHOR_HEADING = "## Anchor Index (mechanically extracted, exact)" _LEAN_ANCHOR_BUDGET_CHARS = 7_000 -_ANCHOR_PATTERNS: "list[tuple[str, re.Pattern[str], int]]" = [ +_ANCHOR_PATTERNS: list[tuple[str, re.Pattern[str], int]] = [ ("PRs/issues", re.compile(r"#\d{3,6}\b"), 120), ("commits", re.compile(r"\b[0-9a-f]{9,40}\b"), 40), ("branches", re.compile(r"\b(?:fix|feat|docs|refactor|chore|salvage|ent)/[A-Za-z0-9._/-]{3,60}"), 40), @@ -1352,17 +1352,17 @@ def _estimate_msg_budget_tokens(msg: dict, charge_stale_thinking: bool = True) - return tokens -def _last_index_with_role(messages: "list[dict[str, Any]]", role: str) -> int: +def _last_index_with_role(messages: list[dict[str, Any]], role: str) -> int: """Index of the newest dict message with ``role``, or -1.""" return max((i for i, m in enumerate(messages) if isinstance(m, dict) and m.get("role") == role), default=-1) -def _last_assistant_index(messages: "list[dict[str, Any]]") -> int: +def _last_assistant_index(messages: list[dict[str, Any]]) -> int: """Newest assistant message index, or -1 (the one turn whose thinking may replay; see ``_NEWEST_TURN_ONLY_BUDGET_KEYS``).""" return _last_index_with_role(messages, "assistant") -def _pending_tool_round(messages: "list[dict[str, Any]]") -> range: +def _pending_tool_round(messages: list[dict[str, Any]]) -> range: """Indices of the tool results the transcript ends with — a round the model has not answered yet; empty when the transcript ends in any other row. /steer rows after the round do not answer it: a steer is delivered after the newest tool result before the next API call, and two can land in one iteration (one when the tool @@ -2011,7 +2011,7 @@ def _json_dict(text: Any) -> dict: return parsed if isinstance(parsed, dict) else {} -def _summarize_refused_tool_result(tool_name: str, args: dict, content: str) -> "str | None": +def _summarize_refused_tool_result(tool_name: str, args: dict, content: str) -> str | None: """Summary for a call an approval or write guard refused (``BLOCKED: ...`` / ``status`` ``blocked``/``pending_approval``), else None. The per-tool summarizers describe the call as done ("ran ...", "wrote to ..."), which would turn a user's denial into a record of the action and @@ -2050,7 +2050,7 @@ def _summarize_tool_result_unguarded(tool_name: str, tool_args: str, tool_conten return f"[{tool_name}]{first_arg} ({content_len:,} chars result)" + _result_failure_suffix(content) -def _model_threshold_key_rank(key: str, model: str, provider: str) -> "tuple[int, int] | None": +def _model_threshold_key_rank(key: str, model: str, provider: str) -> tuple[int, int] | None: """Match rank for one ``model_thresholds`` key, or None when it does not apply. ``":"`` keys apply only on that provider; bare keys apply on every route. The same slug means different windows on different routes (Codex caps Astra at 272K; OpenRouter @@ -2934,7 +2934,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi # Dedup key for the over-threshold "reclamation no-oped" warning # (#101889) so a tool loop riding above the threshold warns once per # distinct reason + rearm snapshot instead of every iteration. - self._last_reclaim_block_warn: "tuple[str, int] | None" = None + self._last_reclaim_block_warn: tuple[str, int] | None = None self.min_tail_user_messages = min_tail_user_messages self.summary_target_ratio = max(0.10, min(summary_target_ratio, 0.80)) self.quiet_mode = quiet_mode @@ -3090,7 +3090,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi """True when compression should run now (anti-thrash included; see :meth:`should_compress_info` for the reason).""" return self.should_compress_info(prompt_tokens)[0] - def should_compress_info(self, prompt_tokens: int | None = None) -> "tuple[bool, str | None]": + def should_compress_info(self, prompt_tokens: int | None = None) -> tuple[bool, str | None]: """Return ``(should_compress, reason)``. ``reason`` is None unless compression is needed but blocked: ``"cooldown:"`` or ``"ineffective"``. Callers should surface a warning when it is non-None.""" @@ -3101,7 +3101,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi return False, self._compression_block_reason() or "blocked" return True, None - def _compression_block_reason(self) -> "str | None": + def _compression_block_reason(self) -> str | None: """Block reason: ``"cooldown:"``, ``"structural_backoff:"``, ``"ineffective"``, or None.""" for label, until in ( ("cooldown", self._summary_failure_cooldown_until), ("structural_backoff", self._structural_no_op_backoff_until), @@ -3421,7 +3421,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi self._proactive_prune_rearm_tokens = 0 self._last_reclaim_block_warn = None - def _billed_basis_over_threshold(self, current_tokens: "int | None") -> bool: + def _billed_basis_over_threshold(self, current_tokens: int | None) -> bool: """Whether a provider-billed reading says the session is over threshold. ``current_tokens`` is the provider's ``prompt_tokens`` (or the @@ -3440,8 +3440,8 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi def _warn_reclamation_no_op( self, reason: str, - current_tokens: "int | None", - before: "int | None" = None, + current_tokens: int | None, + before: int | None = None, ) -> None: """Warn when an over-threshold session's reclamation path no-ops. @@ -5298,7 +5298,7 @@ Write only the summary body. Do not include any preamble or prefix.""" def _scan_window_handoffs( self, messages: list[dict[str, Any]], compress_start: int, compress_end: int, turns_to_summarize: list[dict[str, Any]], - ) -> "_HandoffScan": + ) -> _HandoffScan: """Rehydrate ``_previous_summary`` / user-turn provenance from in-transcript handoffs. Handoff rows are removed from the summarizer window (merged handoffs unwrap to their prior-tail content) and ``tail_start`` advances past a handoff beyond the window. The pre-scan state is @@ -5736,7 +5736,7 @@ Write only the summary body. Do not include any preamble or prefix.""" return self._finalize_compressed(compressed, canonical_messages, n_messages, spare_pending_images) def _assemble_compressed( - self, messages: list[dict[str, Any]], compress_start: int, compress_end: int, scan: "_HandoffScan", summary: str, + self, messages: list[dict[str, Any]], compress_start: int, compress_end: int, scan: _HandoffScan, summary: str, ) -> list[dict[str, Any]]: """Head + summary + tail from the pruned copy: its tool-result demotions are what let an oversized head/tail compress at all (#61932); tool-call arguments are never rewritten by pruning.""" diff --git a/agent/context_compressor_summary.py b/agent/context_compressor_summary.py index decf4aeab0..eb52028b93 100644 --- a/agent/context_compressor_summary.py +++ b/agent/context_compressor_summary.py @@ -58,7 +58,7 @@ class SummaryDispatchMixin: call_kwargs[key] = value def _summarize_window( - self, messages: list[dict[str, Any]], turns_to_summarize: list[dict[str, Any]], scan: "_HandoffScan", + self, messages: list[dict[str, Any]], turns_to_summarize: list[dict[str, Any]], scan: _HandoffScan, focus_topic: Optional[str], memory_context: str, bypass_cooldown: bool, ) -> Optional[str]: """Run the summary LLM; a cancellation rolls back the handoff scan's self-heal mutation first. diff --git a/agent/context_engine.py b/agent/context_engine.py index c9bc6d3300..25feb67ef3 100644 --- a/agent/context_engine.py +++ b/agent/context_engine.py @@ -85,7 +85,7 @@ class ContextEngine(ABC): def should_compress(self, prompt_tokens: int | None = None) -> bool: """Return True if compaction should fire this turn.""" - def should_compress_info(self, prompt_tokens: int | None = None) -> "tuple[bool, str | None]": + def should_compress_info(self, prompt_tokens: int | None = None) -> tuple[bool, str | None]: """Return ``(should_compress, reason)``. Engines with block reasons (summary-LLM cooldown, anti-thrashing guard) override @@ -223,7 +223,7 @@ class ContextEngine(ABC): "compression_count": self.compression_count, } - def clone_for_agent(self) -> "ContextEngine": + def clone_for_agent(self) -> ContextEngine: """Per-agent instance of a plugin-registered engine (the plugin system holds ONE shared instance; every AIAgent gets its own so a child's update_model() cannot mutate the parent's). Override when the engine holds uncopyable state (locks, DB connections): return a fresh diff --git a/agent/context_file_sources.py b/agent/context_file_sources.py index db0c77e5f5..0c77190364 100644 --- a/agent/context_file_sources.py +++ b/agent/context_file_sources.py @@ -59,7 +59,7 @@ def _loaded_status(content: str, rendered_len: int, max_chars: int, user_authore def list_context_file_sources( cwd: Optional[str] = None, context_length: Optional[int] = None, allow_install_tree_fallback: bool = False, - home_override: "Path | None" = None, skip_soul: bool = False, + home_override: Path | None = None, skip_soul: bool = False, ) -> list[dict[str, Any]]: """One dict per context file Hermes considered, in the builder's priority order. diff --git a/agent/context_references.py b/agent/context_references.py index 88cacf39f0..fa1c455800 100644 --- a/agent/context_references.py +++ b/agent/context_references.py @@ -27,7 +27,7 @@ from hermes_cli.sizefmt import format_bytes # provider API (Issue #26193) --------------------------------------------------------------------------- BUILTIN_PREFIXES = frozenset({"diff", "staged", "file", "folder", "git", "url"}) -_context_reference_providers: dict[str, "ContextReferenceProvider"] = {} +_context_reference_providers: dict[str, ContextReferenceProvider] = {} class ContextCompletionItem: diff --git a/agent/conversation_compression.py b/agent/conversation_compression.py index 092961c8f8..4d06784952 100644 --- a/agent/conversation_compression.py +++ b/agent/conversation_compression.py @@ -1787,7 +1787,7 @@ class _CompressionActivityHeartbeat: self._stop = threading.Event() self._thread = threading.Thread(target=self._run, name="compression-activity-heartbeat", daemon=True) - def start(self) -> "_CompressionActivityHeartbeat": + def start(self) -> _CompressionActivityHeartbeat: # A new compression episode always republishes agent.compression even # if a prior timeout/cooldown stamp is still on the agent. self._suppressed = False @@ -1905,7 +1905,7 @@ class _CompressionLockLeaseRefresher: self._stop = threading.Event() self._thread = threading.Thread(target=self._run, name="compression-lock-refresh", daemon=True) - def start(self) -> "_CompressionLockLeaseRefresher": + def start(self) -> _CompressionLockLeaseRefresher: self._thread.start() return self diff --git a/agent/credential_pool.py b/agent/credential_pool.py index e1e7d73434..9682aedcf6 100644 --- a/agent/credential_pool.py +++ b/agent/credential_pool.py @@ -251,7 +251,7 @@ class PooledCredential: raise AttributeError(f"'{type(self).__name__}' object has no attribute {name!r}") @classmethod - def from_dict(cls, provider: str, payload: dict[str, Any]) -> "PooledCredential": + def from_dict(cls, provider: str, payload: dict[str, Any]) -> PooledCredential: field_names = {f.name for f in fields(cls) if f.name != "provider"} data = {k: payload.get(k) for k in field_names if k in payload} # Rehydrated last_status_at may be an ISO string from to_dict() — normalize to float epoch @@ -425,7 +425,7 @@ def _parse_absolute_timestamp(value: Any) -> Optional[float]: return None -def _singleton_predates_entry(state: Any, entry: "PooledCredential") -> bool: +def _singleton_predates_entry(state: Any, entry: PooledCredential) -> bool: """True only when the auth.json singleton is PROVABLY older than *entry*. Both sides stamp ``last_refresh`` on every successful rotation. When @@ -819,7 +819,7 @@ def _write_through_provider_state_to_global_root( logger.debug("%s pool refresh: write-through to global root failed: %s", provider_id, exc) -def _singleton_target_for_entry(pool: "CredentialPool", entry: "PooledCredential") -> Optional[Path]: +def _singleton_target_for_entry(pool: CredentialPool, entry: PooledCredential) -> Optional[Path]: """Root ``.anthropic_oauth.json`` when *entry* is a borrowed hermes_pkce row, else None.""" if entry.source != "hermes_pkce" or entry.id not in getattr(pool, "_borrowed_root_ids", ()): return None @@ -999,7 +999,7 @@ _RESYNC_SOURCE = { class _RefreshDone(Exception): """Raised inside a provider refresher to short-circuit ``_refresh_entry_impl`` with ``result``.""" - def __init__(self, result: Optional["PooledCredential"]): + def __init__(self, result: Optional[PooledCredential]): super().__init__() self.result = result diff --git a/agent/credential_pool_model_cooldowns.py b/agent/credential_pool_model_cooldowns.py index 941799a810..4e267cf9ac 100644 --- a/agent/credential_pool_model_cooldowns.py +++ b/agent/credential_pool_model_cooldowns.py @@ -19,7 +19,7 @@ if TYPE_CHECKING: MODEL_ENTITLEMENT_BENCH_SECONDS = 365 * 24 * 60 * 60 -def model_cooldown_until(entry: "PooledCredential", model: Optional[str]) -> Optional[float]: +def model_cooldown_until(entry: PooledCredential, model: Optional[str]) -> Optional[float]: """Active cooldown blocking *entry* for *model*, or ``None``. Callers that do not know the model stay conservative: any active model @@ -75,7 +75,7 @@ class CredentialPoolModelCooldownMixin: ) def _cool_down_model( - self, entry: "PooledCredential", model: str, error_context: Optional[dict[str, Any]], + self, entry: PooledCredential, model: str, error_context: Optional[dict[str, Any]], failure_reason: Optional[str] = None, ) -> None: """Record a cooldown for *model* on *entry* and every sibling sharing its key. diff --git a/agent/credential_pool_plugin.py b/agent/credential_pool_plugin.py index e103f45af9..99dcb978a5 100644 --- a/agent/credential_pool_plugin.py +++ b/agent/credential_pool_plugin.py @@ -36,7 +36,7 @@ logger = logging.getLogger(__name__) _EXPIRY_SKEW_MS = 120_000 -def plugin_row_is_expiring(entry: "PooledCredential") -> bool: +def plugin_row_is_expiring(entry: PooledCredential) -> bool: """An expiry-stamped OAuth row (a plugin's, or Anthropic's) is due within the skew of ``expires_at_ms``. The token endpoint's ``expires_in`` is the only clock: many providers issue opaque bearers (Google's @@ -45,7 +45,7 @@ def plugin_row_is_expiring(entry: "PooledCredential") -> bool: return entry.expires_at_ms is not None and int(entry.expires_at_ms) <= int(time.time() * 1000) + _EXPIRY_SKEW_MS -def apply_plugin_refresh_result(entry: "PooledCredential", result: Any) -> "PooledCredential": +def apply_plugin_refresh_result(entry: PooledCredential, result: Any) -> PooledCredential: """Merge a ``refresh_credential`` return value into *entry*. Field names go through ``dataclasses.replace``; everything else is merged into ``extra`` @@ -78,8 +78,8 @@ def is_terminal_plugin_refresh_error(exc: BaseException) -> bool: def recover_failed_plugin_refresh( - pool: "CredentialPool", entry: "PooledCredential", exc: Exception, -) -> tuple[bool, Optional["PooledCredential"]]: + pool: CredentialPool, entry: PooledCredential, exc: Exception, +) -> tuple[bool, Optional[PooledCredential]]: """Recovery for a plugin hook that raised: adopt a peer's rotation, or quarantine a dead grant. Returns ``(handled, result)``; ``handled=False`` means the caller should bench the row as a diff --git a/agent/deadline.py b/agent/deadline.py index 34ead7cb14..82025d9819 100644 --- a/agent/deadline.py +++ b/agent/deadline.py @@ -195,7 +195,7 @@ def resolve_timeout(key: str, *, default: Optional[float], env_var: Optional[str # timer dumps all thread stacks when the loop provably failed to process the expiry — the one piece of # information loop-blocked hangs otherwise never surface. # --------------------------------------------------------------------------- -def _consume_abandoned(task: "asyncio.Future[Any]") -> None: +def _consume_abandoned(task: asyncio.Future[Any]) -> None: """Observe an abandoned task's outcome so it never logs 'never retrieved'.""" try: if not task.cancelled(): @@ -204,7 +204,7 @@ def _consume_abandoned(task: "asyncio.Future[Any]") -> None: pass -def _abandon(task: "asyncio.Future[Any]") -> None: +def _abandon(task: asyncio.Future[Any]) -> None: """Cancel ``task`` and never await it; its outcome is consumed so it stays unobserved-safe.""" task.cancel() task.add_done_callback(_consume_abandoned) @@ -253,7 +253,7 @@ async def run_bounded_async( task = asyncio.ensure_future(awaitable) loop = asyncio.get_running_loop() - deadline: "asyncio.Future[None]" = loop.create_future() + deadline: asyncio.Future[None] = loop.create_future() loop_processed_expiry = threading.Event() def _mark_expired() -> None: diff --git a/agent/delegation_context.py b/agent/delegation_context.py index 9528cbd2f3..ef2a465856 100644 --- a/agent/delegation_context.py +++ b/agent/delegation_context.py @@ -138,7 +138,7 @@ def scrub_kanban_env(env: Mapping[str, str] | MutableMapping[str, str]) -> dict[ return cleaned -def kanban_path_is_fenced(path: "os.PathLike[str] | str") -> bool: +def kanban_path_is_fenced(path: os.PathLike[str] | str) -> bool: """Whether Kanban mutations at *path* (a board DB or board-metadata root) are denied for this process: always for an in-process delegate child (the parent's own board); for a spawned descendant only when *path* is the dispatcher-pinned ``HERMES_KANBAN_DB`` or lies under the diff --git a/agent/error_classifier.py b/agent/error_classifier.py index 7c44299b6b..8b53887eb8 100644 --- a/agent/error_classifier.py +++ b/agent/error_classifier.py @@ -1301,7 +1301,7 @@ _CODEX_MASKED_REPLAY_MESSAGE = "request blocked." _CODEX_UNSUPPORTED_CONTENT_DETAIL = "unsupported content type" -def _is_codex_masked_replay_rejection(c: "_Ctx") -> bool: +def _is_codex_masked_replay_rejection(c: _Ctx) -> bool: """HTTP 400 / status-less ``{code: invalid_prompt, message: "Request blocked."}`` from ``openai-codex`` — as an SDK error body, a Responses ``error`` SSE frame, or the ``response.failed`` text ``"invalid_prompt: Request blocked."`` — or the bare diff --git a/agent/fallback_cooldown.py b/agent/fallback_cooldown.py index 769a5fbf5c..a42d7a2302 100644 --- a/agent/fallback_cooldown.py +++ b/agent/fallback_cooldown.py @@ -21,7 +21,7 @@ def _provider_reset_delay(reset_at) -> float | None: return None -def switch_deferred_by_reset(agent, reason: "FailoverReason | None", reset_at) -> bool: +def switch_deferred_by_reset(agent, reason: FailoverReason | None, reset_at) -> bool: """Opt-in ``fallback.min_switch_reset_seconds`` (default 0 = off, #117484): when the primary's rate limit reopens sooner than N seconds, switching model mid-task costs more than waiting, so the fallback walk is skipped and the retry loop's own backoff rides out the window. Only for @@ -43,7 +43,7 @@ def switch_deferred_by_reset(agent, reason: "FailoverReason | None", reset_at) - def _arm_rate_limit_cooldown( - agent, reason: "FailoverReason | None", reset_at=None, + agent, reason: FailoverReason | None, reset_at=None, ) -> int | None: """Arm the primary cooldown until the provider reset, or use exponential backoff. diff --git a/agent/gemini_native_adapter.py b/agent/gemini_native_adapter.py index bf4a568135..eeb22a45bd 100644 --- a/agent/gemini_native_adapter.py +++ b/agent/gemini_native_adapter.py @@ -850,7 +850,7 @@ class GeminiNativeClient: return {"Content-Type": "application/json", "Accept": "application/json", **self._auth_headers(), "User-Agent": f"{_API_CLIENT} (gemini-native)", "X-Goog-Api-Client": _API_CLIENT, **self._default_headers} - def _http_error(self, response: httpx.Response, body_text: Optional[str] = None) -> "GeminiAPIError": + def _http_error(self, response: httpx.Response, body_text: Optional[str] = None) -> GeminiAPIError: return gemini_http_error(response, body_text=body_text, api_key=self.api_key, base_url=self.base_url) @staticmethod diff --git a/agent/interrupt_scope.py b/agent/interrupt_scope.py index 26f664c81a..80d263f735 100644 --- a/agent/interrupt_scope.py +++ b/agent/interrupt_scope.py @@ -18,7 +18,7 @@ from typing import Any, Iterator, Optional from agent.interrupt_compat import request_hard_interrupt -_ACTIVE_SCOPE: ContextVar[Optional["InterruptScope"]] = ContextVar("hermes_interrupt_scope", default=None) +_ACTIVE_SCOPE: ContextVar[Optional[InterruptScope]] = ContextVar("hermes_interrupt_scope", default=None) _TOOL_REASON_HOST_CANCELLED = "host cancelled the command" diff --git a/agent/lsp/manager.py b/agent/lsp/manager.py index 7550aeef7f..75bf060483 100644 --- a/agent/lsp/manager.py +++ b/agent/lsp/manager.py @@ -189,7 +189,7 @@ class LSPService: self._loop.run(self._start_idle_reaper(), timeout=2.0) @classmethod - def create_from_config(cls) -> Optional["LSPService"]: + def create_from_config(cls) -> Optional[LSPService]: """Build a service from ``hermes_cli.config``; ``None`` if config can't load.""" try: from hermes_cli.config import load_config_readonly diff --git a/agent/message_sanitization.py b/agent/message_sanitization.py index 17c4969f60..310a59f839 100644 --- a/agent/message_sanitization.py +++ b/agent/message_sanitization.py @@ -665,7 +665,7 @@ def matches_reasoning_echo_family(family: str, provider: Any, model: Any, base_u ) -def reasoning_echo_family(provider: Any, model: Any, base_url: Any) -> "str | None": +def reasoning_echo_family(provider: Any, model: Any, base_url: Any) -> str | None: """``"kimi"`` / ``"deepseek"`` / ``"mimo"`` (first match in table order) when the endpoint enforces reasoning_content echo-back, else ``None`` (strip side).""" families = (rule[0] for rule in _REASONING_ECHO_RULES) diff --git a/agent/micro_compaction.py b/agent/micro_compaction.py index 1d729cd08f..d0401fd61d 100644 --- a/agent/micro_compaction.py +++ b/agent/micro_compaction.py @@ -371,7 +371,7 @@ class MicroCompactionMixin: except Exception as exc: logger.debug("failed to emit micro-compaction telemetry: %s", exc) - def _micro_start_watermark(self, held: list[dict[str, Any]]) -> "tuple[Optional[str], Optional[int]]": + def _micro_start_watermark(self, held: list[dict[str, Any]]) -> tuple[Optional[str], Optional[int]]: """``(skip_outcome, watermark)`` taken before a pass's slow step; *skip_outcome* is the telemetry label of a pass that must not run, or None. diff --git a/agent/monitoring/emitter.py b/agent/monitoring/emitter.py index 87cc10fa62..52e5b8c7e3 100644 --- a/agent/monitoring/emitter.py +++ b/agent/monitoring/emitter.py @@ -26,7 +26,7 @@ class MonitoringEmitter: def __init__(self, *, enabled: bool = True) -> None: self._enabled = enabled - self._q: "queue.Queue[dict[str, Any]]" = queue.Queue(maxsize=_MAX_QUEUE) + self._q: queue.Queue[dict[str, Any]] = queue.Queue(maxsize=_MAX_QUEUE) self._dropped = 0 self._dispatched = 0 self._stop = threading.Event() diff --git a/agent/outbound_webhooks.py b/agent/outbound_webhooks.py index f3b152ad5d..df6c07ea83 100644 --- a/agent/outbound_webhooks.py +++ b/agent/outbound_webhooks.py @@ -46,7 +46,7 @@ QUEUE_MAX_SIZE = 256 _registered: set[tuple[str, str, str]] = set() _registered_lock = threading.Lock() -_delivery_queue: "queue.Queue[Optional[dict[str, Any]]]" = queue.Queue(maxsize=QUEUE_MAX_SIZE) +_delivery_queue: queue.Queue[Optional[dict[str, Any]]] = queue.Queue(maxsize=QUEUE_MAX_SIZE) _worker_lock = threading.Lock() _worker: Optional[threading.Thread] = None diff --git a/agent/periodic_scheduler.py b/agent/periodic_scheduler.py index c3355876f8..421f93055f 100644 --- a/agent/periodic_scheduler.py +++ b/agent/periodic_scheduler.py @@ -40,7 +40,7 @@ class ScheduledHandle: __slots__ = ("_cancelled", "_context", "_fn", "_interval", "_runner", "_scheduler") - def __init__(self, scheduler: "PeriodicScheduler", fn: Callable[[], object], interval: float): + def __init__(self, scheduler: PeriodicScheduler, fn: Callable[[], object], interval: float): self._scheduler = scheduler self._fn = fn self._interval = interval diff --git a/agent/pet/constants.py b/agent/pet/constants.py index 1167966984..8eacf7cdb0 100644 --- a/agent/pet/constants.py +++ b/agent/pet/constants.py @@ -80,7 +80,7 @@ STATE_ALIASES: dict[str, tuple[str, ...]] = { } -def state_aliases_for(state: "PetState | str") -> tuple[str, ...]: +def state_aliases_for(state: PetState | str) -> tuple[str, ...]: """Return accepted row-name aliases for *state* (always non-empty).""" value = state.value if isinstance(state, PetState) else str(state) return STATE_ALIASES.get(value) or (value,) @@ -95,7 +95,7 @@ def state_rows_for_grid(row_count: int | None) -> list[str]: return CODEX_STATE_ROWS if rows >= len(CODEX_STATE_ROWS) else LEGACY_STATE_ROWS -def state_row_index(state: "PetState | str", row_count: int | None = None) -> int: +def state_row_index(state: PetState | str, row_count: int | None = None) -> int: """Return the spritesheet row index for *state* (clamped, never raises).""" rows = state_rows_for_grid(row_count) return next((rows.index(name) for name in state_aliases_for(state) if name in rows), 0) # 0 = idle row diff --git a/agent/pet/manifest.py b/agent/pet/manifest.py index e54372fa58..18f17cb1af 100644 --- a/agent/pet/manifest.py +++ b/agent/pet/manifest.py @@ -78,7 +78,7 @@ class ManifestEntry: zip_url: str @classmethod - def from_dict(cls, data: dict) -> "ManifestEntry": + def from_dict(cls, data: dict) -> ManifestEntry: return cls( slug=str(data.get("slug", "")).strip(), display_name=str(data.get("displayName", "") or data.get("slug", "")), diff --git a/agent/plugin_stream_hooks.py b/agent/plugin_stream_hooks.py index 87aec9d301..daa9281e09 100644 --- a/agent/plugin_stream_hooks.py +++ b/agent/plugin_stream_hooks.py @@ -27,7 +27,7 @@ _STOP = object() class _ConsumerDispatcher: hook_name: str callback: Callable[..., Any] - events: "queue.Queue[tuple[contextvars.Context, dict[str, Any]] | object]" + events: queue.Queue[tuple[contextvars.Context, dict[str, Any]] | object] thread: threading.Thread | None = None @@ -39,7 +39,7 @@ def _callback_name(callback: Callable[..., Any]) -> str: return getattr(callback, "__name__", repr(callback)) -def _put_drop_oldest(events: "queue.Queue[Any]", item: Any) -> bool: +def _put_drop_oldest(events: queue.Queue[Any], item: Any) -> bool: """put_nowait; on a full queue evict the oldest pending event and retry once.""" try: events.put_nowait(item) diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py index ac42f82409..293694213e 100644 --- a/agent/prompt_builder.py +++ b/agent/prompt_builder.py @@ -61,7 +61,7 @@ def _read_text_with_timeout( """ if timeout is None: timeout = _get_context_file_read_timeout() - result: "queue.Queue[tuple[bool, object]]" = queue.Queue(maxsize=1) + result: queue.Queue[tuple[bool, object]] = queue.Queue(maxsize=1) def _reader() -> None: try: @@ -620,8 +620,8 @@ STEER_CHANNEL_NOTE = ( ) -def hud_surface_note(valid_tool_names: "set[str] | None" = None, - deferred_tool_names: "frozenset[str] | set[str]" = frozenset()) -> str: +def hud_surface_note(valid_tool_names: set[str] | None = None, + deferred_tool_names: frozenset[str] | set[str] = frozenset()) -> str: """Per-turn note for a message typed into the desktop's floating HUD ("this"/"here" = the app behind it). A per-turn fact, not a platform (one session alternates between app window and HUD), so it rides the @@ -1062,7 +1062,7 @@ def _local_host_hints() -> list[str]: return ["\n".join(host_lines), _WINDOWS_BASH_SHELL_HINT] -def bot_screen_note(running: bool, display: "str | None", holder: str) -> str: +def bot_screen_note(running: bool, display: str | None, holder: str) -> str: """The one-line Bot Screen status the model sees — the prompt's ``_bot_screen_hint`` body, parameterised so the display watcher can stage the same sentence as a per-turn note when a screen starts or stops mid-session (#125830; the byte-stable prompt only converges at @@ -1184,7 +1184,7 @@ def _get_context_file_max_chars(context_length: Optional[int] = None) -> int: # Truncation warnings for run_agent to surface. A ContextVar so concurrent gateway prompt builds cannot # drain each other's. -_truncation_warnings: "contextvars.ContextVar[Optional[list]]" = contextvars.ContextVar( +_truncation_warnings: contextvars.ContextVar[Optional[list]] = contextvars.ContextVar( "context_file_truncation_warnings", default=None ) @@ -1294,8 +1294,8 @@ def _parse_skill_file(skill_file: Path) -> tuple[bool, dict, str]: def _skill_should_show( - conditions: dict, available_tools: "set[str] | None", available_toolsets: "set[str] | None", - session_platform: "str | None" = None, + conditions: dict, available_tools: set[str] | None, available_toolsets: set[str] | None, + session_platform: str | None = None, ) -> bool: """False if the skill's conditional activation rules exclude it.""" # Gateway-channel gate runs regardless of tool info; fails open when the platform is unknown. @@ -1315,16 +1315,16 @@ def _skill_should_show( def _plugin_skill_prompt_rows( - disabled: "set[str]", available_tools: "set[str] | None", available_toolsets: "set[str] | None", - session_platform: "str | None", -) -> "list[tuple[str, str]]": + disabled: set[str], available_tools: set[str] | None, available_toolsets: set[str] | None, + session_platform: str | None, +) -> list[tuple[str, str]]: """``(qualified_name, description)`` for every skill registered by an ENABLED plugin (``ctx.register_skill``), filtered through the same offer-time gates as on-disk skills. Plugin skills live in the plugin-manager registry — never under the profile skills tree — so the disk scans above cannot see them; this is their one path into ````. The qualified ``plugin:skill`` name is exactly what ``skill_view`` resolves, and disabling or unloading a plugin removes its registry entries, so enablement gating is inherent.""" - rows: "list[tuple[str, str]]" = [] + rows: list[tuple[str, str]] = [] try: from hermes_cli.plugins import discover_plugins, get_plugin_manager discover_plugins() # idempotent; joins an in-flight discovery (same call skills_list makes) @@ -1359,8 +1359,8 @@ def _current_session_platform_hint() -> str: def build_skills_system_prompt( - available_tools: "set[str] | None" = None, available_toolsets: "set[str] | None" = None, - compact_categories: "frozenset[str] | None" = None, skills_dir_override: "Path | None" = None, + available_tools: set[str] | None = None, available_toolsets: set[str] | None = None, + compact_categories: frozenset[str] | None = None, skills_dir_override: Path | None = None, ) -> str: """Compact skill index for the system prompt. @@ -1432,7 +1432,7 @@ def _label_visible_entries(visible_entries: list[dict], skills_by_category: dict def _render_skills_index( skills_by_category: dict[str, list[tuple[str, str]]], category_descriptions: dict[str, str], - compact_categories: "frozenset[str] | None", available_tools: "set[str] | None", unloadable: "list[str]" = (), + compact_categories: frozenset[str] | None, available_tools: set[str] | None, unloadable: list[str] = (), ) -> str: """Render the ## Skills block; "" when there is nothing to list. *unloadable* names (different skills sharing a name AND relative path within one tier — one root or several) get a rename note instead of a row skill_view would refuse.""" @@ -1501,8 +1501,8 @@ def _oneshot_prompt_variant() -> bool: def _build_skills_system_prompt_inner( - skills_dir: "Path", extra_roots: "list[tuple[int, Path]]", available_tools: "set[str] | None", - available_toolsets: "set[str] | None", compact_categories: "frozenset[str] | None", + skills_dir: Path, extra_roots: list[tuple[int, Path]], available_tools: set[str] | None, + available_toolsets: set[str] | None, compact_categories: frozenset[str] | None, ) -> str: # The resolved platform is part of the key: per-platform disabled-skill lists need distinct cache entries. _platform_hint = _current_session_platform_hint() @@ -1644,7 +1644,7 @@ def _omitted_headings(content: str, start: int, end: int, limit: int = 15) -> li return headings[:limit] + (["..."] if len(headings) > limit else []) -def load_soul_md(context_length: Optional[int] = None, home_override: "Path | None" = None) -> Optional[str]: +def load_soul_md(context_length: Optional[int] = None, home_override: Path | None = None) -> Optional[str]: """SOUL.md from HERMES_HOME (identity slot #1), or None. Callers must pass ``skip_soul=True`` to ``build_context_files_prompt`` so it isn't injected twice. @@ -1850,7 +1850,7 @@ def _load_cursorrules(cwd_path: Path, context_length: Optional[int] = None) -> s def build_context_files_prompt( cwd: Optional[str] = None, skip_soul: bool = False, context_length: Optional[int] = None, - allow_install_tree_fallback: bool = False, home_override: "Path | None" = None, + allow_install_tree_fallback: bool = False, home_override: Path | None = None, ) -> str: """Discover and load context files for the system prompt (each capped, see ``_get_context_file_max_chars``). diff --git a/agent/prompt_cache_boundary.py b/agent/prompt_cache_boundary.py index 6d0fa76f54..54b1be0dbd 100644 --- a/agent/prompt_cache_boundary.py +++ b/agent/prompt_cache_boundary.py @@ -20,7 +20,7 @@ _MAX_ENTRIES = 32 _MAX_CHARS = 4 * 1024 * 1024 _lock = threading.Lock() -_prefixes: "OrderedDict[str, None]" = OrderedDict() +_prefixes: OrderedDict[str, None] = OrderedDict() def register_stable_prefix(prefix: str) -> None: diff --git a/agent/proxy_sources/iron_proxy.py b/agent/proxy_sources/iron_proxy.py index b39704d853..aa45bc90e9 100644 --- a/agent/proxy_sources/iron_proxy.py +++ b/agent/proxy_sources/iron_proxy.py @@ -215,7 +215,7 @@ def allowlisted_env() -> dict[str, str]: return {n: os.environ[n] for n in _PROXY_SUBPROCESS_ENV_ALLOWLIST if n in os.environ} -def _run(argv: list[str], *, timeout: int, text: bool = False, **kwargs) -> "subprocess.CompletedProcess": +def _run(argv: list[str], *, timeout: int, text: bool = False, **kwargs) -> subprocess.CompletedProcess: if text: kwargs.update(text=True, encoding="utf-8", errors="replace") return subprocess.run(argv, capture_output=True, timeout=timeout, stdin=subprocess.DEVNULL, **kwargs) @@ -719,7 +719,7 @@ def _interrupt_guard(handler): signal.signal(sig, old) -def _spawn_daemon(bin_path: Path, cfg: Path, env: dict[str, str], log_path: Path) -> "subprocess.Popen": +def _spawn_daemon(bin_path: Path, cfg: Path, env: dict[str, str], log_path: Path) -> subprocess.Popen: """Popen with stdout/stderr appended to ``log_path`` (0o600 from the first byte, O_NOFOLLOW so a planted symlink e.g. to authorized_keys can't receive output, owner-checked). Our log fd closes after Popen — the child has its dup.""" try: @@ -743,7 +743,7 @@ def _spawn_daemon(bin_path: Path, cfg: Path, env: dict[str, str], log_path: Path os.close(log_fd) -def _await_listening(proc: "subprocess.Popen", host: str, port: int, *, on_exit) -> bool: +def _await_listening(proc: subprocess.Popen, host: str, port: int, *, on_exit) -> bool: """Poll until ``host:port`` accepts or the grace window lapses (do-while: >=1 check even at 0s); raises ``on_exit()`` if the child dies.""" deadline = time.time() + _STARTUP_GRACE_SECONDS while True: @@ -784,7 +784,7 @@ def _write_pidfile_safely(pidfile: Path, pid: int) -> None: _write_private_file(pidfile.with_suffix(".nonce"), _proxy_nonce.encode("utf-8")) -def _kill_and_wait(proc: "subprocess.Popen", *, grace_seconds: int = 2) -> None: +def _kill_and_wait(proc: subprocess.Popen, *, grace_seconds: int = 2) -> None: try: proc.terminate() except OSError: diff --git a/agent/reasoning_params.py b/agent/reasoning_params.py index 3749faa67e..8baa685a32 100644 --- a/agent/reasoning_params.py +++ b/agent/reasoning_params.py @@ -210,7 +210,7 @@ class ReasoningParamsMixin: _reapply_reasoning_echo_for_provider = _forward("agent.agent_runtime_helpers", "reapply_reasoning_echo_for_provider") @staticmethod - def _sanitize_tool_calls_for_strict_api(api_msg: dict, model: "str | None" = None) -> dict: + def _sanitize_tool_calls_for_strict_api(api_msg: dict, model: str | None = None) -> dict: """Strip Codex Responses fields from tool_calls for strict Chat Completions APIs (Mistral, Fireworks 400/422 on unknown fields). ``extra_content`` (Gemini thought_signature) is kept only for Gemini-family models. Builds new dicts so the internal history keeps the Codex fields for a later fallback.""" diff --git a/agent/redact.py b/agent/redact.py index ab30c88733..e76499be9d 100644 --- a/agent/redact.py +++ b/agent/redact.py @@ -363,7 +363,7 @@ def _is_word_end(s: str, j: int, *, allow_plural: bool = True) -> bool: return allow_plural and cur in "sS" and _is_word_end(s, j + 1, allow_plural=False) -def _has_word_bounded_keyword(key: str, keyword_re: "re.Pattern[str]") -> bool: +def _has_word_bounded_keyword(key: str, keyword_re: re.Pattern[str]) -> bool: """True if ``keyword_re`` matches ``key`` at a word boundary (see _KEY_KEYWORD_RE).""" return any(_is_word_start(key, m.start()) and _is_word_end(key, m.end()) for m in keyword_re.finditer(key)) @@ -580,7 +580,7 @@ _CONTROL_CHARS_RE = re.compile(r"[\x00-\x1f\x7f\u200b-\u200f\u2028-\u202f\u2060\ _TOKEN_BODY_CHARS = frozenset("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.") -def _compile_prefix_matcher(patterns: list) -> "re.Pattern[str]": +def _compile_prefix_matcher(patterns: list) -> re.Pattern[str]: return re.compile(r"(? "_ManagedAttempt | None": + ) -> _ManagedAttempt | None: """Return the managed attempt for ``session_id`` (None: the inherited turn's), or None to run unmanaged.""" if session_id is None: session_id = _current_session_id() @@ -429,7 +429,7 @@ class ManagedLlmStream(Iterator[Any]): self._release_runtime_lease() raise - def __iter__(self) -> "ManagedLlmStream": + def __iter__(self) -> ManagedLlmStream: return self def _prime_completed_response(self) -> None: diff --git a/agent/relay_runtime.py b/agent/relay_runtime.py index 5b3befef95..b0831f2a4d 100644 --- a/agent/relay_runtime.py +++ b/agent/relay_runtime.py @@ -270,7 +270,7 @@ _reset_segments_config_for_tests = _SEGMENTS_CONFIG.reset class RelayOperationLease: """Keep process-wide Relay plugins alive across a deferred operation.""" - def __init__(self, runtime: "RelayRuntime") -> None: + def __init__(self, runtime: RelayRuntime) -> None: self._lock, self._runtime = threading.Lock(), runtime def run_in_session(self, session: RelaySession, callback: Callable[..., Any], *args: Any, **kwargs: Any) -> Any: diff --git a/agent/retry_utils.py b/agent/retry_utils.py index 1d35ccdb9e..fc1d702f26 100644 --- a/agent/retry_utils.py +++ b/agent/retry_utils.py @@ -79,12 +79,12 @@ _RETRY_AFTER_SECONDS_RE = re.compile(r"retry\s+(?:after\s+)?(\d+(?:\.\d+)?)\s*(? _RESETS_IN_SECONDS_FIELD_RE = re.compile(r"resets_in_seconds\W{1,4}(\d+(?:\.\d+)?)", re.IGNORECASE) -def _quota_reset_seconds(m: "re.Match[str]") -> float: +def _quota_reset_seconds(m: re.Match[str]) -> float: value = float(m.group(1)) return value / 1000.0 if m.group(2).lower() == "ms" else value -def _resets_in_seconds(m: "re.Match[str]") -> Optional[float]: +def _resets_in_seconds(m: re.Match[str]) -> Optional[float]: if not any(m.groups()): # "resets in" with no unit-bearing number: not this grammar return None return float(m.group(1) or 0) * 3600 + float(m.group(2) or 0) * 60 + float(m.group(3) or 0) diff --git a/agent/secret_scope.py b/agent/secret_scope.py index 6175563e08..dfe185a288 100644 --- a/agent/secret_scope.py +++ b/agent/secret_scope.py @@ -307,7 +307,7 @@ def _parse_env_value(raw_value: str) -> str: # symlink repointed mid-read can't file one file's contents under another's identity. # ``invalidate_env_file_cache()`` is the explicit knob; ``hermes_cli.config.invalidate_env_cache()`` # calls it for Hermes's own .env writers. -_ENV_FILE_CACHE: "OrderedDict[str, tuple[tuple, dict[str, str]]]" = OrderedDict() +_ENV_FILE_CACHE: OrderedDict[str, tuple[tuple, dict[str, str]]] = OrderedDict() _ENV_FILE_CACHE_LOCK = threading.Lock() _ENV_FILE_CACHE_MAX = 64 # one entry per profile home in practice diff --git a/agent/secret_sources/base.py b/agent/secret_sources/base.py index c95e6aae64..eba98b6eeb 100644 --- a/agent/secret_sources/base.py +++ b/agent/secret_sources/base.py @@ -118,7 +118,7 @@ class FetchResult: def ok(self) -> bool: return self.error is None - def fail(self, error: str, kind: ErrorKind) -> "FetchResult": + def fail(self, error: str, kind: ErrorKind) -> FetchResult: self.error, self.error_kind = error, kind return self @@ -191,7 +191,7 @@ class SecretSource(ABC): """Informational ``{key: {"description": str, "default": Any}}`` for setup UIs.""" return {} - def remediation(self, kind: Optional["ErrorKind"], cfg: dict) -> str: + def remediation(self, kind: Optional[ErrorKind], cfg: dict) -> str: """One-line actionable next step for a failed fetch (pure); "" suppresses the hint.""" if kind is None: return "" diff --git a/agent/subagent_lifecycle.py b/agent/subagent_lifecycle.py index 82d6fa0dbb..3aca72ee77 100644 --- a/agent/subagent_lifecycle.py +++ b/agent/subagent_lifecycle.py @@ -77,7 +77,7 @@ class SubagentHandle: return dataclasses.asdict(self) @classmethod - def from_dict(cls, value: Mapping[str, Any]) -> "SubagentHandle": + def from_dict(cls, value: Mapping[str, Any]) -> SubagentHandle: try: return cls(**dict(value)) except (TypeError, ValueError) as exc: diff --git a/agent/thread_scoped_output.py b/agent/thread_scoped_output.py index 02b7e273d0..2363c73cab 100644 --- a/agent/thread_scoped_output.py +++ b/agent/thread_scoped_output.py @@ -19,11 +19,11 @@ __all__ = ["thread_scoped_silence"] _install_lock = threading.Lock() # Proxy installed per attribute ("stdout"/"stderr"): never double-wrap. -_installed: dict[str, "_ThreadRoutingStream"] = {} +_installed: dict[str, _ThreadRoutingStream] = {} # One process-lifetime sink per stream: global redirects that displace and # restore a proxy must not leak a new /dev/null descriptor each time. _sinks: dict[str, TextIO] = {} -_routing_states: dict[str, "_RoutingState"] = {} +_routing_states: dict[str, _RoutingState] = {} class _RoutingState: @@ -87,7 +87,7 @@ class _ThreadRoutingStream: return getattr(self._target(), name) -def _ensure_installed(attr: str, passthrough: TextIO) -> "_ThreadRoutingStream": +def _ensure_installed(attr: str, passthrough: TextIO) -> _ThreadRoutingStream: """Install (idempotently) a routing proxy as ``sys.`` and return it.""" with _install_lock: proxy = _installed.get(attr) diff --git a/agent/title_generator.py b/agent/title_generator.py index 558e06bdf1..fa14b71d40 100644 --- a/agent/title_generator.py +++ b/agent/title_generator.py @@ -25,7 +25,7 @@ logger = logging.getLogger(__name__) # In-flight stage-2 upgrade threads. They bill their aux usage to the session from a daemon thread, # so a process that reads the ledger right before exit (``-z --usage-file``) must be able to join # them (bounded) instead of racing the write (#112848). -_UPGRADE_THREADS: "weakref.WeakSet[threading.Thread]" = weakref.WeakSet() +_UPGRADE_THREADS: weakref.WeakSet[threading.Thread] = weakref.WeakSet() def wait_for_title_upgrades(timeout: float = 10.0) -> None: diff --git a/agent/tool_guardrails.py b/agent/tool_guardrails.py index 50601f87dd..258ca3ab92 100644 --- a/agent/tool_guardrails.py +++ b/agent/tool_guardrails.py @@ -106,7 +106,7 @@ class LoopCapConfig: max_subagents: int = _DEFAULT_MAX_SUBAGENTS_PER_TURN @classmethod - def from_mapping(cls, data: Mapping[str, Any] | None) -> "LoopCapConfig": + def from_mapping(cls, data: Mapping[str, Any] | None) -> LoopCapConfig: """Build config from the ``tool_loop_guardrails.loop_caps`` section.""" if not isinstance(data, Mapping): return cls() @@ -134,7 +134,7 @@ class ToolCallGuardrailConfig: @classmethod def from_mapping( cls, data: Mapping[str, Any] | None, *, platform: str | None = None, - ) -> "ToolCallGuardrailConfig": + ) -> ToolCallGuardrailConfig: """Build config from `tool_loop_guardrails`; nested ``warn_after`` / ``hard_stop_after`` win over flat legacy keys.""" if not isinstance(data, Mapping): data = {} @@ -170,7 +170,7 @@ class ToolCallSignature: args_hash: str @classmethod - def from_call(cls, tool_name: str, args: Mapping[str, Any] | None) -> "ToolCallSignature": + def from_call(cls, tool_name: str, args: Mapping[str, Any] | None) -> ToolCallSignature: return cls(tool_name=tool_name, args_hash=_sha256(canonical_tool_args(args or {}))) def to_metadata(self) -> dict[str, str]: diff --git a/agent/transcription_provider.py b/agent/transcription_provider.py index 48428d1d93..2935b19fc0 100644 --- a/agent/transcription_provider.py +++ b/agent/transcription_provider.py @@ -43,7 +43,7 @@ class TranscriptionProvider(CatalogProviderBase): def open_stream_session( self, *, language: Optional[str] = None, prompt: Optional[str] = None, - ) -> "TranscriptionStreamSession": + ) -> TranscriptionStreamSession: """A single-use live session for one utterance (streaming-capable providers only).""" raise NotImplementedError(f"{self.name} does not support live streaming transcription") diff --git a/agent/transports/codex_app_server.py b/agent/transports/codex_app_server.py index da3b777441..a64cec40d2 100644 --- a/agent/transports/codex_app_server.py +++ b/agent/transports/codex_app_server.py @@ -219,7 +219,7 @@ class CodexAppServerClient: for _rid, pending in pending_items: pending.put_nowait(synthetic) - def __enter__(self) -> "CodexAppServerClient": + def __enter__(self) -> CodexAppServerClient: return self def __exit__(self, *exc: object) -> None: diff --git a/agent/usage_pricing.py b/agent/usage_pricing.py index 154df0d815..91fdfa908d 100644 --- a/agent/usage_pricing.py +++ b/agent/usage_pricing.py @@ -80,7 +80,7 @@ class CanonicalUsage: def total_tokens(self) -> int: return self.prompt_tokens + self.output_tokens - def __add__(self, other: "CanonicalUsage") -> "CanonicalUsage": + def __add__(self, other: CanonicalUsage) -> CanonicalUsage: """Sum two usage buckets. ``raw_usage`` (single-response detail) is dropped; ``request_count`` adds so callers see how many API calls a combined figure covers.""" diff --git a/agent/vault_backends/base.py b/agent/vault_backends/base.py index 636f8f481d..6795a2ba75 100644 --- a/agent/vault_backends/base.py +++ b/agent/vault_backends/base.py @@ -20,7 +20,7 @@ from agent.vault_store import VaultItemMeta class UnlockRequired(Exception): """The backend is locked for this session; the surface must prompt for the master password.""" - def __init__(self, backend: "LoginBackend"): + def __init__(self, backend: LoginBackend): super().__init__(f"{backend.display_name} is locked") self.backend = backend diff --git a/agent/vault_login_classifier.py b/agent/vault_login_classifier.py index 8fe521a615..470ba80af6 100644 --- a/agent/vault_login_classifier.py +++ b/agent/vault_login_classifier.py @@ -76,7 +76,7 @@ class LoginControl: max_length: Optional[int] = None @classmethod - def from_dict(cls, raw: dict[str, Any]) -> "LoginControl": + def from_dict(cls, raw: dict[str, Any]) -> LoginControl: form_index = raw.get("formIndex", raw.get("form_index")) max_length = raw.get("maxLength", raw.get("max_length")) return cls( diff --git a/agent/verify/recipes.py b/agent/verify/recipes.py index 8ec5a1c41b..02dc573fd2 100644 --- a/agent/verify/recipes.py +++ b/agent/verify/recipes.py @@ -47,7 +47,7 @@ class Recipe: } @classmethod - def from_dict(cls, raw: Any) -> "Recipe | None": + def from_dict(cls, raw: Any) -> Recipe | None: """Tolerant loader mirroring grok's ``normalizeVerifyRecipe``; accepts both this module's field names and grok's camelCase aliases.""" if not isinstance(raw, dict): diff --git a/cli.py b/cli.py index 78ff9357c5..3c72066573 100644 --- a/cli.py +++ b/cli.py @@ -896,7 +896,7 @@ class HermesCLI(CLIInitMixin, CLITuiRuntimeMixin, CLIProcessNotificationsMixin, # Seeded -q first message (see _should_seed_interactive); run() re-creates # _pending_input, so it is enqueued only after the fresh queue exists. - _seeded_first_message: Optional["_SeededQueryMessage"] = None + _seeded_first_message: Optional[_SeededQueryMessage] = None # Inspection surfaces (banner, /tools, status line) read this on partially built instances too. disabled_toolsets: Optional[list[str]] = None diff --git a/cron/jobs.py b/cron/jobs.py index 7d16596518..4a6264ad61 100644 --- a/cron/jobs.py +++ b/cron/jobs.py @@ -121,7 +121,7 @@ class _CronStorePaths: output_dir: Path @classmethod - def for_dir(cls, cron_dir: Path) -> "_CronStorePaths": + def for_dir(cls, cron_dir: Path) -> _CronStorePaths: return cls(cron_dir, cron_dir / "jobs.json", cron_dir / "output") diff --git a/cron/scheduler.py b/cron/scheduler.py index bd715ff308..eb2abf1cd0 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -659,7 +659,7 @@ class _CombinedCancelEvent: ``lost_ownership`` + per-transport events). Workers only call is_set()/set(), so no pump thread. """ - def __init__(self, *events: Optional["_CancelEventLike"]) -> None: + def __init__(self, *events: Optional[_CancelEventLike]) -> None: self._events = [event for event in events if event is not None] def is_set(self) -> bool: @@ -670,7 +670,7 @@ class _CombinedCancelEvent: event.set() -def get_running_job_ids() -> "frozenset[str]": +def get_running_job_ids() -> frozenset[str]: """Thread-safe snapshot of executing job IDs (dispatch until ``_process_job`` returns). Read by the gateway shutdown drain, otherwise blind to cron work (runs outside ``_running_agents``). @@ -697,7 +697,7 @@ def get_running_job_details() -> list[dict]: ] -def get_wedged_job_ids() -> "frozenset[str]": +def get_wedged_job_ids() -> frozenset[str]: """In-flight job IDs older than their stale-inflight allowance (``max(2 * interval, cron.inflight_max_minutes)``) — the scheduler's own definition of a claim that can no longer be making progress. ``sweep_stale_inflight`` cannot release these while the worker thread is still @@ -707,7 +707,7 @@ def get_wedged_job_ids() -> "frozenset[str]": return frozenset(key[1] for key in _wedged_inflight_keys()) -def _wedged_inflight_keys() -> "frozenset[tuple]": +def _wedged_inflight_keys() -> frozenset[tuple]: """In-flight keys behind :func:`get_wedged_job_ids`, before the projection to bare job IDs.""" now = time.time() with _running_lock: @@ -3243,7 +3243,7 @@ def _deliver_crash_failure( -def _install_fire_secret_scope() -> "tuple[contextvars.Token, Optional[contextvars.Token]]": +def _install_fire_secret_scope() -> tuple[contextvars.Token, Optional[contextvars.Token]]: """Install the firing profile's secret scope for the span ``_run_one_job_body`` runs, delivery included, and return the tokens ``_reset_fire_secret_scope`` needs. @@ -3270,7 +3270,7 @@ def _install_fire_secret_scope() -> "tuple[contextvars.Token, Optional[contextva return scope_token, context_token -def _reset_fire_secret_scope(tokens: "tuple[contextvars.Token, Optional[contextvars.Token]]") -> None: +def _reset_fire_secret_scope(tokens: tuple[contextvars.Token, Optional[contextvars.Token]]) -> None: """Undo ``_install_fire_secret_scope`` — the context first, so multiplex semantics never outlive the scope they depend on.""" from agent.secret_scope import reset_multiplex_context, reset_secret_scope @@ -3291,7 +3291,7 @@ def _fire_secret_scope(): _reset_fire_secret_scope(tokens) -def _start_owned_run(job: dict, execution_id: str) -> "Optional[contextvars.Token]": +def _start_owned_run(job: dict, execution_id: str) -> Optional[contextvars.Token]: """Win the run's ``claimed`` → ``running`` CAS and bind its cron identity; ``None`` when the run lost ownership first. A restart-safe worker already won it by adopting the row.""" if os.environ.get("_HERMES_CRON_EXTERNAL_WORKER") == execution_id: diff --git a/cron/scheduler_detached_worker.py b/cron/scheduler_detached_worker.py index 66cbbccf47..a51a308cd2 100644 --- a/cron/scheduler_detached_worker.py +++ b/cron/scheduler_detached_worker.py @@ -15,7 +15,7 @@ import threading from typing import Optional -def _close_late_session_db_result(future: "concurrent.futures.Future") -> None: +def _close_late_session_db_result(future: concurrent.futures.Future) -> None: """Done-callback: close a SessionDB whose constructor finished after run_job's init timeout (worker abandoned via ``shutdown(wait=False)``), else its .db/WAL/SHM handles leak to EMFILE. diff --git a/cron/scheduler_provider.py b/cron/scheduler_provider.py index 8cc7811602..f753b0a7fa 100644 --- a/cron/scheduler_provider.py +++ b/cron/scheduler_provider.py @@ -291,7 +291,7 @@ def _misfire_grace_minutes() -> float: def fire_overdue_jobs( - provider: "CronScheduler", *, adapters: Any = None, loop: Any = None, now: Any = None, + provider: CronScheduler, *, adapters: Any = None, loop: Any = None, now: Any = None, ) -> int: """Misfire backstop (gateway housekeeping loop): fire jobs whose external HTTP fire never arrived, else ``next_run_at`` stays parked in the past forever. No-op for the built-in (its tick @@ -384,7 +384,7 @@ def fire_overdue_jobs( return fired -def resolve_cron_scheduler() -> "CronScheduler": +def resolve_cron_scheduler() -> CronScheduler: """Resolve ``cron.provider``; missing/failing/unavailable providers fall back to the built-in with a warning — cron must never be left without a trigger.""" name = "" @@ -414,8 +414,8 @@ def resolve_cron_scheduler() -> "CronScheduler": def scheduler_for_profile_mode( - provider: "CronScheduler", *, multiplex_profiles: bool -) -> "CronScheduler": + provider: CronScheduler, *, multiplex_profiles: bool +) -> CronScheduler: """External providers own one unscoped remote registry and cannot reconcile several profile stores: fail closed to the built-in multiplex ticker until the API carries profile identity.""" if not multiplex_profiles or isinstance(provider, InProcessCronScheduler): diff --git a/evals/postmortem/forensics/common.py b/evals/postmortem/forensics/common.py index a8f7a6efbc..76fe697877 100644 --- a/evals/postmortem/forensics/common.py +++ b/evals/postmortem/forensics/common.py @@ -67,12 +67,12 @@ class Run: return ap @classmethod - def from_args(cls, argv: Optional[list[str]] = None, description: str = "") -> "Run": + def from_args(cls, argv: Optional[list[str]] = None, description: str = "") -> Run: a = cls.parser(description).parse_args(argv) return cls.open(a.db, root=a.root, out=a.out) @classmethod - def open(cls, db: str, *, root: Optional[str] = None, out: str = "postmortem_out") -> "Run": + def open(cls, db: str, *, root: Optional[str] = None, out: str = "postmortem_out") -> Run: conn = sqlite3.connect(f"file:{db}?mode=ro", uri=True) conn.row_factory = sqlite3.Row sessions = {r["id"]: dict(r) for r in conn.execute("SELECT * FROM sessions")} diff --git a/gateway/authz_mixin.py b/gateway/authz_mixin.py index ad407168ea..97fc1c37a2 100644 --- a/gateway/authz_mixin.py +++ b/gateway/authz_mixin.py @@ -448,7 +448,7 @@ class GatewayAuthorizationMixin: return bool(sender_allow.strip()) return isinstance(sender_allow, (list, tuple, set)) and any(str(item).strip() for item in sender_allow) - def _pairing_store_for(self, source: "SessionSource"): + def _pairing_store_for(self, source: SessionSource): """Per-profile PairingStore for a source, else the global ``self.pairing_store``.""" per_profile = getattr(self, "pairing_stores", None) or {} profile = getattr(source, "profile", None) diff --git a/gateway/browser_control_broker.py b/gateway/browser_control_broker.py index 5286437d60..6767351723 100644 --- a/gateway/browser_control_broker.py +++ b/gateway/browser_control_broker.py @@ -201,7 +201,7 @@ class BrowserControlBroker: else: self._artifact_stores[profile_id] = store - def _artifact_store_for_scope(self, scope: "ControllerScope") -> Any: + def _artifact_store_for_scope(self, scope: ControllerScope) -> Any: store = self._artifact_stores.get(getattr(scope, "profile_id", None) or None) return store if store is not None else self._artifact_stores.get(None) diff --git a/gateway/config.py b/gateway/config.py index 79654878e3..8564642c81 100644 --- a/gateway/config.py +++ b/gateway/config.py @@ -43,7 +43,7 @@ def _coerce_bool(value: Any, default: bool = True) -> bool: return is_truthy_value(value, default=default) -def _env_multiplex_profiles_override() -> "bool | None": +def _env_multiplex_profiles_override() -> bool | None: """GATEWAY_MULTIPLEX_PROFILES operator override: True/False for a recognized token. ``None`` when unset, blank, or unrecognized so the caller keeps the config.yaml @@ -68,7 +68,7 @@ def _env_multiplex_profiles_override() -> "bool | None": ON_ALL_ADAPTERS_DOWN_POLICIES = ("exit", "stay_alive") -def _env_on_all_adapters_down_override() -> "str | None": +def _env_on_all_adapters_down_override() -> str | None: """GATEWAY_ON_ALL_ADAPTERS_DOWN operator override: 'exit'/'stay_alive' for a recognized token. ``None`` when unset, blank, or unrecognized so the caller keeps the config.yaml value @@ -267,7 +267,7 @@ class Platform(Enum): return cls._add_pseudo_member(value) if registered else None @classmethod - def _add_pseudo_member(cls, value: str) -> "Platform": + def _add_pseudo_member(cls, value: str) -> Platform: pseudo = object.__new__(cls) pseudo._value_ = value pseudo._name_ = value.upper().replace("-", "_").replace(" ", "_") @@ -276,7 +276,7 @@ class Platform(Enum): return pseudo @classmethod - def _scan_bundled_plugin_platforms(cls) -> "tuple[set, dict]": + def _scan_bundled_plugin_platforms(cls) -> tuple[set, dict]: """Directory names of bundled platform plugins under ``plugins/platforms/``, plus a map of manifest ``name:`` keys that differ from their directory (alias -> directory name). Aliases never shadow a directory name, so the directory stays the canonical platform value.""" @@ -359,7 +359,7 @@ class HomeChannel: return {"platform": self.platform.value, "chat_id": self.chat_id, "name": self.name, **optional} @classmethod - def from_dict(cls, data: dict[str, Any]) -> "HomeChannel": + def from_dict(cls, data: dict[str, Any]) -> HomeChannel: optional = {k: str(data[k]) if data.get(k) else None for k in ("thread_id", "user_id", "scope_id")} return cls(platform=Platform(data["platform"]), chat_id=str(data["chat_id"]), name=data.get("name", "Home"), **optional) @@ -386,7 +386,7 @@ class ChannelOverride: return {k: v for k, v in asdict(self).items() if v is not None} @classmethod - def from_dict(cls, data: dict[str, Any]) -> "ChannelOverride": + def from_dict(cls, data: dict[str, Any]) -> ChannelOverride: return cls(**{f.name: data.get(f.name) for f in fields(cls)}) if data else cls() @@ -395,7 +395,7 @@ class ChannelOverride: # authenticate another way and must never be skipped for a missing token. # Platforms absent from this map authenticate some other way (session files, port-bound webhooks, # api_key-only) and must never be skipped for a missing token. See #64674. -PLATFORM_TOKEN_ENV_NAMES: dict["Platform", str] = { +PLATFORM_TOKEN_ENV_NAMES: dict[Platform, str] = { Platform.TELEGRAM: "TELEGRAM_BOT_TOKEN", Platform.DISCORD: "DISCORD_BOT_TOKEN", Platform.SLACK: "SLACK_BOT_TOKEN", @@ -442,7 +442,7 @@ class PlatformConfig: }) @classmethod - def from_dict(cls, data: dict[str, Any]) -> "PlatformConfig": + def from_dict(cls, data: dict[str, Any]) -> PlatformConfig: data = _coerce_dict(data) home = data.get("home_channel") # Adapters read their settings from ``extra`` (``config.extra.get("port")``), but users @@ -518,7 +518,7 @@ class StreamingConfig: return asdict(self) @classmethod - def from_dict(cls, data: dict[str, Any]) -> "StreamingConfig": + def from_dict(cls, data: dict[str, Any]) -> StreamingConfig: if not isinstance(data, dict) or not data: return cls() @@ -721,7 +721,7 @@ class GatewayConfig: } @classmethod - def from_dict(cls, data: dict[str, Any]) -> "GatewayConfig": + def from_dict(cls, data: dict[str, Any]) -> GatewayConfig: data = _coerce_dict(data) nested_gateway = _coerce_dict(data.get("gateway")) @@ -865,7 +865,7 @@ def load_gateway_config() -> GatewayConfig: return config -def _validate_gateway_config(config: "GatewayConfig") -> None: +def _validate_gateway_config(config: GatewayConfig) -> None: """Validate and sanitize a loaded GatewayConfig in place (after all sources are merged).""" try: # Reject known-weak placeholder tokens. Ported from openclaw/openclaw#64586: users who copy diff --git a/gateway/delivery.py b/gateway/delivery.py index b38dfb1433..edcae16f5b 100644 --- a/gateway/delivery.py +++ b/gateway/delivery.py @@ -121,7 +121,7 @@ class DeliveryTarget: unknown_platform: Optional[str] = None @classmethod - def parse(cls, target: str, origin: Optional[SessionSource] = None) -> "DeliveryTarget": + def parse(cls, target: str, origin: Optional[SessionSource] = None) -> DeliveryTarget: """Parse "origin" | "local" | "" | ":[:]".""" target = target.strip() if target.lower() == "origin": diff --git a/gateway/host_rendezvous.py b/gateway/host_rendezvous.py index cf2db2bf28..ee1f8c2dd2 100644 --- a/gateway/host_rendezvous.py +++ b/gateway/host_rendezvous.py @@ -120,7 +120,7 @@ class HostRecord: } @classmethod - def from_json(cls, payload: Any) -> Optional["HostRecord"]: + def from_json(cls, payload: Any) -> Optional[HostRecord]: if not isinstance(payload, dict): return None pid = payload.get("pid") diff --git a/gateway/hosted_room_execution_policy.py b/gateway/hosted_room_execution_policy.py index 07e012eef4..f75a82a0c6 100644 --- a/gateway/hosted_room_execution_policy.py +++ b/gateway/hosted_room_execution_policy.py @@ -38,7 +38,7 @@ class RoomExecutionPolicy: policy_digest: str @classmethod - def from_mapping(cls, value: Mapping[str, Any]) -> "RoomExecutionPolicy": + def from_mapping(cls, value: Mapping[str, Any]) -> RoomExecutionPolicy: if not isinstance(value, Mapping) or set(value) != _POLICY_FIELDS: raise RoomExecutionPolicyError("execution policy fields are invalid") if value["version"] != POLICY_VERSION: diff --git a/gateway/hosted_room_links.py b/gateway/hosted_room_links.py index 000986b20c..9d55894824 100644 --- a/gateway/hosted_room_links.py +++ b/gateway/hosted_room_links.py @@ -50,7 +50,7 @@ class StoredRoomLink: updated_at: float @classmethod - def from_mapping(cls, value: Mapping[str, Any]) -> "StoredRoomLink": + def from_mapping(cls, value: Mapping[str, Any]) -> StoredRoomLink: _link_fields(value) room_id = _short_string(value["room_id"], "room_id") member_id = _short_string(value["member_id"], "member_id") @@ -76,7 +76,7 @@ class StoredRoomLink: status=status, updated_at=updated_at) @classmethod - def from_record(cls, value: Mapping[str, Any]) -> "StoredRoomLink": + def from_record(cls, value: Mapping[str, Any]) -> StoredRoomLink: try: catalog = json.loads(str(value["catalog_json"])) except Exception as exc: diff --git a/gateway/hosted_room_peer.py b/gateway/hosted_room_peer.py index eb1493d53e..e39f4d679d 100644 --- a/gateway/hosted_room_peer.py +++ b/gateway/hosted_room_peer.py @@ -206,7 +206,7 @@ class GatewayRoomCatalog: transport_security: TransportSecurity | None = None @classmethod - def from_mapping(cls, value: Mapping[str, Any]) -> "GatewayRoomCatalog": + def from_mapping(cls, value: Mapping[str, Any]) -> GatewayRoomCatalog: _exact_fields(value, required=_CATALOG_FIELDS, optional={"endpoint"}, label="capability catalog") installation_id = _identifier(value["installation_id"], field="installation_id") versions = tuple(_protocol_versions(_non_empty_list(value["protocol_versions"], field="protocol_versions"))) @@ -379,7 +379,7 @@ class HostedMemberDispatch: return asdict(self) @classmethod - def from_mapping(cls, value: Mapping[str, Any]) -> "HostedMemberDispatch": + def from_mapping(cls, value: Mapping[str, Any]) -> HostedMemberDispatch: _exact_fields(value, required=set(_DISPATCH_FIELDS) | {"prompt", "prompt_digest"}, label="dispatch") if not isinstance(prompt := value["prompt"], str) or not prompt.strip(): raise HostedRoomPeerError("prompt must be a non-empty string") diff --git a/gateway/kanban_watchers_common.py b/gateway/kanban_watchers_common.py index eb0bca88a4..4be46ea4c4 100644 --- a/gateway/kanban_watchers_common.py +++ b/gateway/kanban_watchers_common.py @@ -63,7 +63,7 @@ def _positive_int_setting(kanban_cfg: dict, key: str) -> Optional[int]: return value -def _resolve_auto_decompose_settings(load_config: Callable[[], Any]) -> "tuple[bool, int]": +def _resolve_auto_decompose_settings(load_config: Callable[[], Any]) -> tuple[bool, int]: """Live (enabled, per_tick) auto-decompose settings, re-read every dispatcher tick. Fails safe: a config read error returns ``(False, 3)`` rather than @@ -110,7 +110,7 @@ def _kanban_dispatch_allowed() -> bool: return not check_paused("kanban", logger) -def _acquire_singleton_lock(lock_path) -> "tuple[Optional[object], str]": +def _acquire_singleton_lock(lock_path) -> tuple[Optional[object], str]: """Take the exclusive, non-blocking advisory lock for the sole dispatcher. Only one gateway machine-wide may run the embedded dispatcher: concurrent diff --git a/gateway/kanban_watchers_notifier.py b/gateway/kanban_watchers_notifier.py index b19ebb0120..d94be2b853 100644 --- a/gateway/kanban_watchers_notifier.py +++ b/gateway/kanban_watchers_notifier.py @@ -470,7 +470,7 @@ def _fmt_timed_out(ev, n) -> tuple: # archived / unblocked are claimed (so the cursor advances past them) but # intentionally silent (no formatter), and excluded from _WAKE_KINDS so they # never wake the creator. -_EVENT_FORMATTERS: dict[str, Callable[[Any, "_KanbanNotification"], tuple]] = { +_EVENT_FORMATTERS: dict[str, Callable[[Any, _KanbanNotification], tuple]] = { "completed": _fmt_completed, "blocked": lambda ev, n: ( t("gateway.kanban.ping.blocked", head=n.head, reason=_clip(ev, "reason", "gateway.kanban.ping.reason_suffix", 160)), diff --git a/gateway/platforms/_http_client_limits.py b/gateway/platforms/_http_client_limits.py index d24f532129..1e4d3d90cf 100644 --- a/gateway/platforms/_http_client_limits.py +++ b/gateway/platforms/_http_client_limits.py @@ -34,7 +34,7 @@ def _positive_env(name: str, default, cast): return val if val > 0 else default -def platform_httpx_limits() -> "httpx.Limits | None": +def platform_httpx_limits() -> httpx.Limits | None: """``httpx.Limits`` tuned for persistent platform-adapter clients; ``None`` without httpx.""" if httpx is None: return None diff --git a/gateway/platforms/api_server.py b/gateway/platforms/api_server.py index 7db0bdd156..f28b6bf4f0 100644 --- a/gateway/platforms/api_server.py +++ b/gateway/platforms/api_server.py @@ -558,7 +558,7 @@ def _content_has_visible_payload(content: Any) -> bool: return False -def _multimodal_validation_error(exc: ValueError, *, param: str) -> "web.Response": +def _multimodal_validation_error(exc: ValueError, *, param: str) -> web.Response: """Translate a ``_normalize_multimodal_content`` ValueError into a 400 response.""" raw = str(exc) code, _, message = raw.partition(":") @@ -633,7 +633,7 @@ def _clear_turn_process_ownership(agent: Any) -> None: agent._gateway_turn_process_epoch = None -def _session_chat_user_message(body: dict[str, Any], *, param: str = "message") -> tuple[Any, Optional["web.Response"]]: +def _session_chat_user_message(body: dict[str, Any], *, param: str = "message") -> tuple[Any, Optional[web.Response]]: """Parse and normalize session chat ``message`` / ``input`` like chat completions.""" user_message = body.get("message") or body.get("input") if not _content_has_visible_payload(user_message): @@ -884,7 +884,7 @@ def _resolve_media_to_data_urls(text: str) -> str: return None return f"![image](data:{_MEDIA_MIME[suffix]};base64,{b64})" - def _repl(m: "re.Match[str]") -> str: + def _repl(m: re.Match[str]) -> str: return _to_data_url(m.group("path")) or m.group(0) try: # A leaked terminal <|eos|> glued to the last tag is not a path terminator (#111046): @@ -912,12 +912,12 @@ def _openai_error(message: str, err_type: str = "invalid_request_error", param: def _error_response( message: str, status: int, *, err_type: str = "invalid_request_error", param: str | None = None, code: str | None = None, headers: Optional[dict[str, str]] = None, -) -> "web.Response": +) -> web.Response: """``web.json_response(_openai_error(...), status=...)`` in one call.""" return web.json_response(_openai_error(message, err_type, param, code), status=status, headers=headers) -def _invalid_request(message: str) -> "web.Response": +def _invalid_request(message: str) -> web.Response: """400 with the bare ``{message, type}`` envelope the OpenAI-compatible validators use.""" return web.json_response({"error": {"message": message, "type": "invalid_request_error"}}, status=400) @@ -988,7 +988,7 @@ class _IdempotencyCache: def __init__(self, max_items: int = 1000, ttl_seconds: int = 300): from collections import OrderedDict self._store = OrderedDict() - self._inflight: dict[tuple[str, str], "asyncio.Task[Any]"] = {} + self._inflight: dict[tuple[str, str], asyncio.Task[Any]] = {} self._ttl = ttl_seconds self._max = max_items @@ -1016,7 +1016,7 @@ class _IdempotencyCache: task = asyncio.create_task(_compute_and_store()) self._inflight[inflight_key] = task - def _clear_inflight(done_task: "asyncio.Task[Any]") -> None: + def _clear_inflight(done_task: asyncio.Task[Any]) -> None: if self._inflight.get(inflight_key) is done_task: self._inflight.pop(inflight_key, None) task.add_done_callback(_clear_inflight) @@ -1124,7 +1124,7 @@ class _SessionEventQueue: def __init__(self, session_id: str, run_id: str): self.loop = asyncio.get_running_loop() - self.queue: "asyncio.Queue[Optional[tuple[str, dict[str, Any]]]]" = asyncio.Queue() + self.queue: asyncio.Queue[Optional[tuple[str, dict[str, Any]]]] = asyncio.Queue() self.session_id = session_id self.run_id = run_id self.seq = 0 @@ -1153,7 +1153,7 @@ class _SessionEventQueue: def _room_grant_delegate(name: str): """Adapter method forwarding to ``api_server_room_grants.`` (looked up at call time so test patches on that module take effect) with this module's error/profile bindings.""" - async def _handler(self, request: "web.Request") -> "web.Response": + async def _handler(self, request: web.Request) -> web.Response: return await getattr(_room_grants, name)( self, request, _openai_error=_openai_error, _api_request_profile=_api_request_profile) _handler.__name__ = name @@ -1163,7 +1163,7 @@ def _room_grant_delegate(name: str): def _run_route_delegate(name: str): """Adapter method forwarding to ``api_server_runs.`` (call-time lookup) with this module's namespace as ``_api_server``.""" - async def _handler(self, request: "web.Request") -> "web.StreamResponse": + async def _handler(self, request: web.Request) -> web.StreamResponse: return await getattr(_api_runs, name)(self, request, _api_server=sys.modules[__name__]) _handler.__name__ = name return _handler @@ -1214,9 +1214,9 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): # clients that choke on named events (#12020). Default on. self._tool_progress_events: bool = _coerce_request_bool( extra.get("tool_progress_events"), default=True) - self._app: Optional["web.Application"] = None - self._runner: Optional["web.AppRunner"] = None - self._site: Optional["web.TCPSite"] = None + self._app: Optional[web.Application] = None + self._runner: Optional[web.AppRunner] = None + self._site: Optional[web.TCPSite] = None from hermes_constants import get_hermes_home self._response_store = ResponseStore() # this home's; a /p// route gets its own self._response_store_home = str(get_hermes_home()) @@ -1310,7 +1310,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): except Exception: return False - def _draining_response(self) -> Optional["web.Response"]: + def _draining_response(self) -> Optional[web.Response]: """Return a retryable response while the gateway drains existing work.""" if not self._gateway_is_draining(): return None @@ -1478,7 +1478,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return {**_CORS_HEADERS, "Access-Control-Allow-Origin": origin, "Vary": "Origin", "Access-Control-Max-Age": "600"} - def _sse_headers(self, request: "web.Request", extra: Optional[dict[str, str]] = None) -> dict[str, str]: + def _sse_headers(self, request: web.Request, extra: Optional[dict[str, str]] = None) -> dict[str, str]: """Headers for an SSE StreamResponse prepared inside a handler: the CORS middleware only touches the response after the handler returns, by which point ``prepare()`` has already flushed the head, so CORS must be resolved up front (#72892, #6358).""" @@ -1500,7 +1500,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): text = str(value).replace("\r", " ").replace("\n", " ").strip() return text[:max_len] - def _request_audit_context(self, request: "web.Request") -> dict[str, str]: + def _request_audit_context(self, request: web.Request) -> dict[str, str]: """Return non-secret source metadata for security/audit warnings.""" peer_ip = "" with suppress(Exception): @@ -1516,12 +1516,12 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): "path": self._clean_log_value(request.path_qs, max_len=500), "user_agent": self._clean_log_value(request.headers.get("User-Agent", ""), max_len=300)} - def _request_audit_log_suffix(self, request: "web.Request") -> str: + def _request_audit_log_suffix(self, request: web.Request) -> str: ctx = self._request_audit_context(request) fields = [f"{key}={value!r}" for key, value in ctx.items() if value] return " ".join(fields) if fields else "source='unknown'" - def _cron_origin_from_request(self, request: "web.Request") -> dict[str, str]: + def _cron_origin_from_request(self, request: web.Request) -> dict[str, str]: """Persist safe API source metadata on cron jobs created over HTTP.""" ctx = self._request_audit_context(request) origin = {"platform": "api_server", "chat_id": "api"} @@ -1550,13 +1550,13 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return "" @staticmethod - def _auth_failed_response() -> "web.Response": + def _auth_failed_response() -> web.Response: return web.json_response( {"error": {"message": "Invalid gateway API key (API_SERVER_KEY)", "type": "gateway_auth_error", "code": "gateway_auth_failed"}}, status=401) - def _check_auth(self, request: "web.Request") -> Optional["web.Response"]: + def _check_auth(self, request: web.Request) -> Optional[web.Response]: """Validate the Bearer token; None when OK, else a 401. The no-key branch (connect() refuses to start without API_SERVER_KEY) exists for tests/manual wiring on the default listener only; named profiles fail closed rather than inherit the owner's key.""" @@ -1586,7 +1586,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return normalized if re.fullmatch(r"[a-z0-9_]+", normalized) else "" def _get_platform_callback_adapter( - self, request: "web.Request", platform_name: str) -> Optional[Any]: + self, request: web.Request, platform_name: str) -> Optional[Any]: injected = request.app.get("platform_event_adapters") adapter = injected.get(platform_name) if isinstance(injected, dict) else None if adapter is None: @@ -1605,7 +1605,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return None return runner._authorization_adapter(platform, _api_request_profile.get()) - async def _handle_platform_event_callback(self, request: "web.Request") -> "web.Response": + async def _handle_platform_event_callback(self, request: web.Request) -> web.Response: platform_name = self._normalize_callback_platform(request.match_info.get("platform", "")) if not platform_name: return _error_response("Invalid platform name", 400, code="invalid_platform") @@ -1644,7 +1644,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): # -- Multi-profile multiplexing (/p//...) -------------------------------- - def _resolve_request_profile(self, request: "web.Request"): + def _resolve_request_profile(self, request: web.Request): """Resolve + validate the /p// prefix: ``None`` (no prefix, or multiplexing off and the prefix names this gateway's own profile), the served profile name, or ``_PROFILE_REJECTED`` (-> 404). Fail closed: a foreign prefix must never be ignored.""" @@ -1682,7 +1682,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): from hermes_cli.profiles import get_profile_dir return _profile_runtime_scope(get_profile_dir(profile)) - async def _handle_profile_ingress(self, request: "web.Request") -> "web.StreamResponse": + async def _handle_profile_ingress(self, request: web.Request) -> web.StreamResponse: """``/p//`` → the served profile's shared-listener adapter (already scoped by the prefix middleware); a profile with no adapter for the path is a 404, never the default's.""" from gateway.platforms.shared_ingress import dispatch_profile_ingress @@ -1694,7 +1694,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): """Reject unknown /p// prefixes and scope the request home.""" @web.middleware - async def profile_prefix_middleware(request: "web.Request", handler): + async def profile_prefix_middleware(request: web.Request, handler): profile = self._resolve_request_profile(request) if profile is _PROFILE_REJECTED: return web.json_response({"error": "Unknown or unconfigured profile"}, status=404) @@ -1823,7 +1823,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): "[%s] declared-conversation bind failed for %s", self.name, sid, exc_info=True) def _parse_session_key_header( - self, request: "web.Request") -> tuple[Optional[str], Optional["web.Response"]]: + self, request: web.Request) -> tuple[Optional[str], Optional[web.Response]]: """Validate ``X-Hermes-Session-Key`` (per-channel memory scope) -> ``(key_or_None, None)`` or ``(None, error)``. Requires API-key auth so a client can't guess another scope.""" raw = request.headers.get("X-Hermes-Session-Key", "").strip() @@ -1845,7 +1845,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): # -- Responses state ---------------------------------------------------------------- - def _current_response_store(self) -> "ResponseStore": + def _current_response_store(self) -> ResponseStore: """Responses state of the routed profile's home. Conversation names are client-chosen, so one shared store let any profile's key read, chain onto and overwrite another's (#84253).""" from hermes_constants import get_hermes_home @@ -2049,7 +2049,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return (cls._clean_runtime_id(requested.get("model")), cls._clean_runtime_id(requested.get("provider"), max_len=80)) - def _runtime_lock_error(self, runtime_request: dict[str, Any]) -> Optional["web.Response"]: + def _runtime_lock_error(self, runtime_request: dict[str, Any]) -> Optional[web.Response]: if not runtime_request.get("require_model_lock"): return None model, provider = self._requested_ids(runtime_request.get("requested")) @@ -2422,12 +2422,12 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): # -- HTTP handlers ---------------------------------------------------------------- - async def _handle_health(self, request: "web.Request") -> "web.Response": + async def _handle_health(self, request: web.Request) -> web.Response: """GET /health — simple health check.""" return web.json_response({"status": "ok", "platform": "hermes-agent", "version": _hermes_version()}) @_require_auth - async def _handle_health_detailed(self, request: "web.Request") -> "web.Response": + async def _handle_health_detailed(self, request: web.Request) -> web.Response: """GET /health/detailed — gateway state, platforms, PID for dashboard probing (Bearer auth).""" from gateway.status import ( derive_gateway_busy, derive_gateway_drainable, normalize_updated_at, parse_active_agents, @@ -2472,7 +2472,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): "updated_at": normalize_updated_at(runtime.get("updated_at")), "pid": os.getpid()}) @_require_auth - async def _handle_models(self, request: "web.Request") -> "web.Response": + async def _handle_models(self, request: web.Request) -> web.Response: """GET /v1/models — hermes-agent plus configured model_routes aliases (alias + resolved model only, never credentials). Under /p// the primary id follows that profile.""" now = int(time.time()) @@ -2489,7 +2489,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return web.json_response({"object": "list", "data": models}) @_require_auth - async def _handle_model_options(self, request: "web.Request") -> "web.Response": + async def _handle_model_options(self, request: web.Request) -> web.Response: """GET /api/model/options — the dashboard/TUI model-picker inventory, so external clients can sync to the configured provider catalog instead of scraping /v1/models.""" refresh = _coerce_request_bool(request.query.get("refresh"), default=False) @@ -2509,7 +2509,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return _error_response("Failed to list model options.", 500, code="model_options_failed") @_require_auth - async def _handle_capabilities(self, request: "web.Request") -> "web.Response": + async def _handle_capabilities(self, request: web.Request) -> web.Response: """GET /v1/capabilities — the stable, machine-readable API surface for external UIs.""" return web.json_response({ "object": "hermes.api_server.capabilities", "platform": "hermes-agent", @@ -2551,7 +2551,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): # -- Browser-extension control (authenticated local/VPS API) ---------------------- - async def _handle_browser_control_register(self, request: "web.Request") -> "web.Response": + async def _handle_browser_control_register(self, request: web.Request) -> web.Response: """POST /v1/browser-control/register — mint a short-lived single-use controller ticket. Identity is NOT taken from the body: the principal is a server-derived digest of the @@ -2630,7 +2630,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): "capabilities": sorted(scope.capabilities)}}, status=201) - async def _handle_browser_control_ws(self, request: "web.Request") -> "web.WebSocketResponse": + async def _handle_browser_control_ws(self, request: web.Request) -> web.WebSocketResponse: """GET /v1/browser-control/ws — controller WebSocket (one-shot ticket). The ticket rides in ``Sec-WebSocket-Protocol`` (never the query string: targets land in @@ -2686,7 +2686,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return ws def _handle_browser_control_frame( - self, scope: "ControllerScope", frame: dict, *, owner: Any = None) -> Optional[dict]: + self, scope: ControllerScope, frame: dict, *, owner: Any = None) -> Optional[dict]: """Apply one controller->broker frame with exact-scope checks.""" method = frame.get("method") params = frame.get("params") @@ -2729,7 +2729,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): digest = hashlib.sha256(f"{profile}\x00{key}".encode()).hexdigest() return f"principal:{profile}:{digest[:32]}" - def _browser_control_transport_family(self, request: "web.Request") -> str: + def _browser_control_transport_family(self, request: web.Request) -> str: """``local-api`` for a loopback peer, else ``remote-api``; the broker treats the family as part of exact identity, so a remote controller never satisfies a local-only dispatch.""" host = None @@ -2830,7 +2830,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): if limiter is not None: self._browser_control_artifact_limiter = limiter - def _artifact_route_prelude(self, request: "web.Request", action: str, *, check_enabled: bool = True) -> tuple: + def _artifact_route_prelude(self, request: web.Request, action: str, *, check_enabled: bool = True) -> tuple: """Shared upload/download gate (feature flag -> API key -> Bearer -> per-principal rate limit) -> ``((profile, principal), None)`` or ``(None, error_response)``.""" if check_enabled and not self._browser_control_enabled(): @@ -2851,7 +2851,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): code="rate_limit_exceeded", headers={"Retry-After": "1"}) return (profile, principal), None - async def _handle_artifact_upload(self, request: "web.Request") -> "web.Response": + async def _handle_artifact_upload(self, request: web.Request) -> web.Response: """POST /v1/artifacts/upload — one-shot bounded raw upload; ``Content-Type`` must be an allowed MIME type, ``X-Artifact-Filename`` is display-only. Returns a provenance receipt (never a filesystem path). Ladder: 404 disabled, 403 no API key, 401 bad Bearer, 429 @@ -2895,7 +2895,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return web.json_response( receipt.to_dict(download_path=f"/v1/artifacts/download/{receipt.artifact_id}"), status=201) - async def _handle_artifact_download(self, request: "web.Request") -> "web.Response": + async def _handle_artifact_download(self, request: web.Request) -> web.Response: """GET /v1/artifacts/download/{artifact_id} — one-shot download (a second one 404s) with ``X-Artifact-Sha256``. Ladder: 404 disabled/unknown, 403 no API key, 401 bad Bearer, 429 rate limited, 410 expired, 400 invalid id/scope mismatch, 200.""" @@ -2924,7 +2924,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): "Content-Disposition": f'attachment; filename="{receipt.filename}"'}) @_require_auth - async def _handle_skills(self, request: "web.Request") -> "web.Response": + async def _handle_skills(self, request: web.Request) -> web.Response: """GET /v1/skills — deterministic JSON listing of installed skills (name, description, category), the same set ``/skills list`` shows.""" try: @@ -2940,7 +2940,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return web.json_response({"object": "list", "data": skills}) @_require_auth - async def _handle_toolsets(self, request: "web.Request") -> "web.Response": + async def _handle_toolsets(self, request: web.Request) -> web.Response: """GET /v1/toolsets — each toolset the api_server agent exposes: enabled/configured state plus the concrete tool names it expands to.""" try: @@ -2979,7 +2979,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return default if parsed < 0 else min(parsed, maximum) @staticmethod - def _session_db_unavailable() -> "web.Response": + def _session_db_unavailable() -> web.Response: return _error_response("Session database unavailable", 503, code="session_db_unavailable") @staticmethod @@ -3026,7 +3026,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): "display_kind") return {key: message.get(key) for key in safe_keys if key in message} - async def _read_json_body(self, request: "web.Request") -> tuple[dict[str, Any], Optional["web.Response"]]: + async def _read_json_body(self, request: web.Request) -> tuple[dict[str, Any], Optional[web.Response]]: try: body = await request.json() except Exception: @@ -3035,7 +3035,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return {}, _error_response("Request body must be a JSON object", 400) return body, None - async def _get_existing_session_or_404(self, session_id: str) -> tuple[Optional[dict[str, Any]], Optional["web.Response"]]: + async def _get_existing_session_or_404(self, session_id: str) -> tuple[Optional[dict[str, Any]], Optional[web.Response]]: db = await self._ensure_session_db_async() if db is None: return None, self._session_db_unavailable() @@ -3063,7 +3063,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): notification_category=notification_category, _api_server=sys.modules[__name__]) @_require_auth - async def _handle_list_sessions(self, request: "web.Request") -> "web.Response": + async def _handle_list_sessions(self, request: web.Request) -> web.Response: """GET /api/sessions — list persisted Hermes sessions.""" db = await self._ensure_session_db_async() if db is None: @@ -3118,7 +3118,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): "limit": limit, "offset": offset, "has_more": windowed >= limit}) @_require_auth - async def _handle_create_session(self, request: "web.Request") -> "web.Response": + async def _handle_create_session(self, request: web.Request) -> web.Response: """POST /api/sessions -- create an empty Hermes session row. Existence check, insert and title handling run as ONE off-loop write so concurrent same-id creates can't both 201.""" body, err = await self._read_json_body(request) @@ -3186,7 +3186,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return web.json_response({"object": "hermes.session", "session": self._session_response(session)}, status=201) @_require_auth - async def _handle_get_session(self, request: "web.Request") -> "web.Response": + async def _handle_get_session(self, request: web.Request) -> web.Response: """GET /api/sessions/{session_id}.""" session, err = await self._get_existing_session_or_404(request.match_info["session_id"]) if err: @@ -3194,7 +3194,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return web.json_response({"object": "hermes.session", "session": self._session_response(session)}) @_require_auth - async def _handle_patch_session(self, request: "web.Request") -> "web.Response": + async def _handle_patch_session(self, request: web.Request) -> web.Response: """PATCH /api/sessions/{session_id} — update client-safe session metadata.""" session_id = request.match_info["session_id"] session, err = await self._get_existing_session_or_404(session_id) @@ -3234,7 +3234,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return web.json_response({"object": "hermes.session", "session": self._session_response(session)}) @_require_auth - async def _handle_delete_session(self, request: "web.Request") -> "web.Response": + async def _handle_delete_session(self, request: web.Request) -> web.Response: """DELETE /api/sessions/{session_id}.""" session_id = request.match_info["session_id"] _session, err = await self._get_existing_session_or_404(session_id) @@ -3267,7 +3267,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return web.json_response({"object": "hermes.session.deleted", "id": session_id, "deleted": bool(deleted)}) @_require_auth - async def _handle_session_messages(self, request: "web.Request") -> "web.Response": + async def _handle_session_messages(self, request: web.Request) -> web.Response: """GET /api/sessions/{session_id}/messages.""" session_id = request.match_info["session_id"] _, err = await self._get_existing_session_or_404(session_id) @@ -3303,7 +3303,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): "returned": len(messages)}}) @_require_auth - async def _handle_fork_session(self, request: "web.Request") -> "web.Response": + async def _handle_fork_session(self, request: web.Request) -> web.Response: """POST /api/sessions/{session_id}/fork — branch via current SessionDB primitives.""" source_id = request.match_info["session_id"] source, err = await self._get_existing_session_or_404(source_id) @@ -3344,7 +3344,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): fork = await asyncio.to_thread(db.get_session, fork_id) or {"id": fork_id, "parent_session_id": source_id} return web.json_response({"object": "hermes.session", "session": self._session_response(fork)}, status=201) - async def _prepare_session_chat(self, request: "web.Request") -> tuple: + async def _prepare_session_chat(self, request: web.Request) -> tuple: """Shared prelude for /api/sessions/{id}/chat[/stream]: header/body validation, then runtime selection — a Browser model lock (body ``require_model_lock`` or a confirmed persisted lock) wins; else the session-persisted model routes via model_routes when an @@ -3481,7 +3481,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): record = await asyncio.to_thread(_admit) return None if record is None else (home, record) - async def _answer_through_live_bot_chat(self, ctx: dict[str, Any]) -> Optional["web.Response"]: + async def _answer_through_live_bot_chat(self, ctx: dict[str, Any]) -> Optional[web.Response]: """Hand a turn aimed at a canonical Bot Chat that a Desktop holds live to that owner. This is the ``hermes peer dm`` transport. Running the turn here would make this process a @@ -3528,7 +3528,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): await keepalive() return record - async def _stream_through_live_bot_chat(self, request: "web.Request", ctx: dict[str, Any]) -> Optional["web.StreamResponse"]: + async def _stream_through_live_bot_chat(self, request: web.Request, ctx: dict[str, Any]) -> Optional[web.StreamResponse]: """``_answer_through_live_bot_chat`` for the SSE sibling route: the owner's settled receipt is the run's single ``assistant.completed`` event; a receipt still open at the budget is a ``run.queued`` event (the 202 shape), a failed one an ``error`` event carrying the reason.""" @@ -3567,7 +3567,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return response @_admit_api_agent_request - async def _handle_session_chat(self, request: "web.Request") -> "web.Response": + async def _handle_session_chat(self, request: web.Request) -> web.Response: """POST /api/sessions/{session_id}/chat — one synchronous agent turn (plus the delivery lanes' one bounded re-run of a transient failure; ``hermes peer dm`` is the client).""" from tools.bot_failure_reasons import RETRY_NONE, result_retry_action @@ -3612,7 +3612,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): headers=headers) @_admit_api_agent_request - async def _handle_session_chat_stream(self, request: "web.Request") -> "web.StreamResponse": + async def _handle_session_chat_stream(self, request: web.Request) -> web.StreamResponse: """POST /api/sessions/{session_id}/chat/stream — SSE wrapper over _run_agent.""" limited = self._concurrency_limited_response() if limited is not None: @@ -3740,7 +3740,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): logger.debug("[api_server] session SSE stream error: %s", exc) return response - def _register_session_stream_approval(self, run_id: str, events: "_SessionEventQueue", message_id: str): + def _register_session_stream_approval(self, run_id: str, events: _SessionEventQueue, message_id: str): """Route a session-stream turn's dangerous-command approvals to its SSE queue and to ``POST /v1/runs/{run_id}/approval`` (#58856). Keyed by the run id (never the shared session key) so concurrent turns on one session can't cross-resolve.""" @@ -3753,7 +3753,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return _approval_notify async def _drain_session_stream_task_on_disconnect( - self, run_id: str, task: "asyncio.Task", *, interrupt_message: str, shield_wait: bool + self, run_id: str, task: asyncio.Task, *, interrupt_message: str, shield_wait: bool ) -> None: """Preserve live run control refs until the executor-backed turn actually exits.""" agent = self._active_run_agents.get(run_id) @@ -3770,7 +3770,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): await (asyncio.shield(task) if shield_wait else task) @_require_auth - async def _handle_session_model_lock(self, request: "web.Request") -> "web.Response": + async def _handle_session_model_lock(self, request: web.Request) -> web.Response: """POST /api/sessions/{session_id}/model — backend-ack a Browser model lock.""" session_id = request.match_info["session_id"] _, err = await self._get_existing_session_or_404(session_id) @@ -3809,7 +3809,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): _MAX_PROMPT_LENGTH = 5000 def _cron_request_guard( - self, request: "web.Request", *, need_job_id: bool = False, check_draining: bool = False, + self, request: web.Request, *, need_job_id: bool = False, check_draining: bool = False, ) -> tuple: """Shared /api/jobs prelude: auth → (drain) → cron available → (job_id). Returns (job_id, err).""" auth_err = self._check_auth(request) @@ -3831,10 +3831,10 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return job_id, None @staticmethod - def _cron_error_response(exc: BaseException) -> "web.Response": + def _cron_error_response(exc: BaseException) -> web.Response: return web.json_response({"error": _redact_api_error_text(exc)}, status=500) - def _validate_cron_prompt(self, prompt: str) -> Optional["web.Response"]: + def _validate_cron_prompt(self, prompt: str) -> Optional[web.Response]: """Length cap + injection scan shared by create/update/run.""" if len(prompt) > self._MAX_PROMPT_LENGTH: return web.json_response({"error": f"Prompt must be ≤ {self._MAX_PROMPT_LENGTH} characters"}, status=400) @@ -3844,7 +3844,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return web.json_response({"error": scan_error}, status=400) return None - def _job_response(self, fn, job_id: str, *, notify: bool) -> "web.Response": + def _job_response(self, fn, job_id: str, *, notify: bool) -> web.Response: """Run ``fn(job_id)``: 404 when falsy, else ``{"job": ...}``; exceptions -> 500.""" try: job = fn(job_id) @@ -3856,11 +3856,11 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): except Exception as e: return self._cron_error_response(e) - async def _job_lookup_or_mutate(self, request: "web.Request", fn, *, notify: bool) -> "web.Response": + async def _job_lookup_or_mutate(self, request: web.Request, fn, *, notify: bool) -> web.Response: job_id, err = self._cron_request_guard(request, need_job_id=True) return err if err else self._job_response(fn, job_id, notify=notify) - async def _handle_list_jobs(self, request: "web.Request") -> "web.Response": + async def _handle_list_jobs(self, request: web.Request) -> web.Response: """GET /api/jobs — list all cron jobs.""" _, err = self._cron_request_guard(request) if err: @@ -3871,7 +3871,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): except Exception as e: return self._cron_error_response(e) - async def _handle_create_job(self, request: "web.Request") -> "web.Response": + async def _handle_create_job(self, request: web.Request) -> web.Response: """POST /api/jobs — create a new cron job.""" _, err = self._cron_request_guard(request) if err: @@ -3913,11 +3913,11 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): except Exception as e: return self._cron_error_response(e) - async def _handle_get_job(self, request: "web.Request") -> "web.Response": + async def _handle_get_job(self, request: web.Request) -> web.Response: """GET /api/jobs/{job_id} — get a single cron job.""" return await self._job_lookup_or_mutate(request, _cron_get, notify=False) - async def _handle_update_job(self, request: "web.Request") -> "web.Response": + async def _handle_update_job(self, request: web.Request) -> web.Response: """PATCH /api/jobs/{job_id} — update a cron job.""" job_id, err = self._cron_request_guard(request, need_job_id=True) if err: @@ -3938,7 +3938,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return self._cron_error_response(e) return self._job_response(lambda jid: _cron_update(jid, sanitized), job_id, notify=True) - async def _handle_delete_job(self, request: "web.Request") -> "web.Response": + async def _handle_delete_job(self, request: web.Request) -> web.Response: """DELETE /api/jobs/{job_id} — delete a cron job.""" job_id, err = self._cron_request_guard(request, need_job_id=True) if err: @@ -3951,15 +3951,15 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): except Exception as e: return self._cron_error_response(e) - async def _handle_pause_job(self, request: "web.Request") -> "web.Response": + async def _handle_pause_job(self, request: web.Request) -> web.Response: """POST /api/jobs/{job_id}/pause — pause a cron job.""" return await self._job_lookup_or_mutate(request, _cron_pause, notify=True) - async def _handle_resume_job(self, request: "web.Request") -> "web.Response": + async def _handle_resume_job(self, request: web.Request) -> web.Response: """POST /api/jobs/{job_id}/resume — resume a paused cron job.""" return await self._job_lookup_or_mutate(request, _cron_resume, notify=True) - async def _handle_run_job(self, request: "web.Request") -> "web.Response": + async def _handle_run_job(self, request: web.Request) -> web.Response: """POST /api/jobs/{job_id}/run — trigger immediate execution.""" job_id, err = self._cron_request_guard(request, need_job_id=True, check_draining=True) if err: @@ -3980,7 +3980,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): return self._job_response( lambda jid: _cron_trigger(jid, extra_prompt=extra_prompt), job_id, notify=False) - async def _handle_cron_fire(self, request: "web.Request") -> "web.Response": + async def _handle_cron_fire(self, request: web.Request) -> web.Response: """POST /api/cron/fire — Chronos fire webhook (NAS -> agent), authenticated by a NAS-minted JWT via the pluggable verifier, NOT API_SERVER_KEY. 202 + background run so a long turn never trips NAS's timeout; the store CAS claim guards double-fire on retry.""" @@ -4038,7 +4038,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): if refusal is not None: return refusal - def _detach_fire(fire_fn, *fire_args) -> "web.Response": + def _detach_fire(fire_fn, *fire_args) -> web.Response: # The done callback owns the reservation once the task is detached. task = asyncio.create_task(asyncio.to_thread(fire_fn, *fire_args, adapters=adapters, loop=loop)) reservation["detached"] = True @@ -4077,7 +4077,7 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): if hasattr(task, "add_done_callback"): task.add_done_callback(self._background_tasks.discard) - def _concurrency_limited_response(self) -> Optional["web.Response"]: + def _concurrency_limited_response(self) -> Optional[web.Response]: """429 when the concurrent-run cap is reached (0 disables), else None. Uses the same adapter-owned work count as shutdown draining (admitted requests included).""" limit = self._max_concurrent_runs @@ -4361,29 +4361,29 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): def _set_run_status(self, run_id: str, status: str, **fields: Any) -> dict[str, Any]: return _api_runs._set_run_status(self, run_id, status, **fields) - def _make_run_event_callback(self, run_id: str, loop: "asyncio.AbstractEventLoop"): + def _make_run_event_callback(self, run_id: str, loop: asyncio.AbstractEventLoop): return _api_runs._make_run_event_callback(self, run_id, loop, _api_server=sys.modules[__name__]) - def _run_idempotency_scope(self, request: "web.Request") -> str: + def _run_idempotency_scope(self, request: web.Request) -> str: return _api_runs._run_idempotency_scope(self, request, _api_server=sys.modules[__name__]) @staticmethod - def _room_grant_token(request: "web.Request") -> str: + def _room_grant_token(request: web.Request) -> str: return _room_grants._room_grant_token(request) def _room_grant_secret(self) -> bytes: return _room_grants._room_grant_secret(self) - def _room_grant_claims(self, request: "web.Request", *, permission: str) -> dict[str, Any]: + def _room_grant_claims(self, request: web.Request, *, permission: str) -> dict[str, Any]: return _room_grants._room_grant_claims(self, request, permission=permission) - def _check_run_auth(self, request: "web.Request", *, permission: str) -> "web.Response | None": + def _check_run_auth(self, request: web.Request, *, permission: str) -> web.Response | None: return _api_runs._check_run_auth(self, request, permission=permission, _api_server=sys.modules[__name__]) async def _ensure_hosted_member_session(self, dispatch: Any) -> str: return await _room_dispatch._ensure_hosted_member_session(self, dispatch) - async def _normalize_room_dispatch(self, request: "web.Request", body: Any) -> tuple[Any, "web.Response | None"]: + async def _normalize_room_dispatch(self, request: web.Request, body: Any) -> tuple[Any, web.Response | None]: return await _room_dispatch._normalize_room_dispatch(self, request, body, _api_server=sys.modules[__name__]) _handle_room_member_invitation = _room_grant_delegate("_handle_room_member_invitation") @@ -4391,14 +4391,14 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): _handle_room_member_grant_refresh = _room_grant_delegate("_handle_room_member_grant_refresh") _handle_room_member_grant_revoke = _room_grant_delegate("_handle_room_member_grant_revoke") - def _durable_run_status(self, request: "web.Request", run_id: str) -> dict[str, Any] | None: + def _durable_run_status(self, request: web.Request, run_id: str) -> dict[str, Any] | None: return _api_runs._durable_run_status(self, request, run_id) @_admit_api_agent_request - async def _handle_runs(self, request: "web.Request") -> "web.Response": + async def _handle_runs(self, request: web.Request) -> web.Response: return await _api_runs._handle_runs(self, request, _api_server=sys.modules[__name__]) - def _request_owns_run(self, request: "web.Request", run_id: str) -> bool: + def _request_owns_run(self, request: web.Request, run_id: str) -> bool: return _api_runs._request_owns_run(self, request, run_id) def _release_run_owner_if_forgotten(self, run_id: str) -> None: diff --git a/gateway/platforms/api_server_fire_startup.py b/gateway/platforms/api_server_fire_startup.py index 50d5693e44..84f84f6048 100644 --- a/gateway/platforms/api_server_fire_startup.py +++ b/gateway/platforms/api_server_fire_startup.py @@ -27,7 +27,7 @@ def runner_is_draining(runner: Any) -> bool: return bool(getattr(runner, "_draining", False) or getattr(runner, "_external_drain_active", False)) -async def refuse_until_started(runner: Any, job_id: str, *, received_at: float) -> Optional["web.Response"]: +async def refuse_until_started(runner: Any, job_id: str, *, received_at: float) -> Optional[web.Response]: """None once ``runner`` has finished starting (``_running``: every platform connect attempted and its adapters published), or when there is no runner to ask (a self-hosted api_server, a test double). Otherwise the retryable 503, worded like the dashboard's own "gateway unreachable" so NAS classifies @@ -53,8 +53,8 @@ async def refuse_until_started(runner: Any, job_id: str, *, received_at: float) async def live_adapters_once_started( - api_adapter: Any, request: "web.Request", job_id: str, *, received_at: float, -) -> tuple[Optional["web.Response"], Any]: + api_adapter: Any, request: web.Request, job_id: str, *, received_at: float, +) -> tuple[Optional[web.Response], Any]: """``(refusal, adapters)`` for a fire: the startup gate's retryable 503, else the gateway's LIVE adapters (parity with the built-in ticker: E2EE / relay-fronted platforms have no native credential, so without them delivery fails). ``None`` adapters when there is no runner (a self-hosted api_server).""" diff --git a/gateway/platforms/api_server_memory_sessions.py b/gateway/platforms/api_server_memory_sessions.py index 0d3b8c469c..7dcf75e659 100644 --- a/gateway/platforms/api_server_memory_sessions.py +++ b/gateway/platforms/api_server_memory_sessions.py @@ -32,7 +32,7 @@ class ApiServerMemorySessions: """Session-keyed ``MemoryManager`` registry with exclusive check-out/check-in.""" def __init__(self, *, max_size: Optional[int] = None, idle_ttl_secs: Optional[float] = None) -> None: - self._entries: "OrderedDict[tuple[str, str], tuple[Any, Optional[Path], float]]" = OrderedDict() + self._entries: OrderedDict[tuple[str, str], tuple[Any, Optional[Path], float]] = OrderedDict() self._lock = threading.Lock() self._max_size = max_size self._idle_ttl_secs = idle_ttl_secs diff --git a/gateway/platforms/api_server_openai_routes.py b/gateway/platforms/api_server_openai_routes.py index 6d8b43adf4..0c3a134381 100644 --- a/gateway/platforms/api_server_openai_routes.py +++ b/gateway/platforms/api_server_openai_routes.py @@ -633,7 +633,7 @@ class OpenAICompatRoutesMixin: agent_task.add_done_callback(_done) return agent_task, agent_ref - async def _handle_chat_completions(self, request: "web.Request") -> "web.Response": + async def _handle_chat_completions(self, request: web.Request) -> web.Response: """POST /v1/chat/completions — OpenAI Chat Completions format.""" from gateway.platforms.api_server import ( ThreadSafeAsyncQueue, _api_request_profile, _chat_usage_payload, _coerce_request_bool, @@ -841,7 +841,7 @@ class OpenAICompatRoutesMixin: return web.json_response(response_data, headers=response_headers) async def _run_idempotent( - self, request: "web.Request", body: dict[str, Any], compute, *, + self, request: web.Request, body: dict[str, Any], compute, *, log_label: str, fingerprint_keys: list[str], route: str) -> tuple: """Run ``compute()`` once per (principal scope, logical route, Idempotency-Key) + body fingerprint -> ``((result, usage), None)`` or ``(None, 500 response)``. @@ -870,8 +870,8 @@ class OpenAICompatRoutesMixin: return None, _error_response(message, 500, err_type="server_error") async def _prepare_sse_response( - self, request: "web.Request", session_id: Optional[str], gateway_session_key: Optional[str], - ) -> "web.StreamResponse": + self, request: web.Request, session_id: Optional[str], gateway_session_key: Optional[str], + ) -> web.StreamResponse: """Open a prepared SSE StreamResponse with CORS + session headers (the CORS middleware can't inject headers after ``prepare()`` flushes them, so they are resolved here).""" sse_headers = self._sse_headers(request) @@ -884,9 +884,9 @@ class OpenAICompatRoutesMixin: return response async def _write_sse_chat_completion( - self, request: "web.Request", completion_id: str, model: str, + self, request: web.Request, completion_id: str, model: str, created: int, stream_q, agent_task, agent_ref=None, session_id: str | None = None, - gateway_session_key: str | None = None) -> "web.StreamResponse": + gateway_session_key: str | None = None) -> web.StreamResponse: """Stream ``chat.completion.chunk`` frames from the agent's delta queue. On client disconnect the agent is interrupted (stops LLM calls), then its task wrapper cancelled.""" from gateway.platforms.api_server import ( @@ -975,10 +975,10 @@ class OpenAICompatRoutesMixin: return response async def _write_sse_responses( - self, request: "web.Request", response_id: str, model: str, created_at: int, stream_q, + self, request: web.Request, response_id: str, model: str, created_at: int, stream_q, agent_task, agent_ref, conversation_history: list[dict[str, str]], user_message: str, instructions: Optional[str], conversation: Optional[str], store: bool, session_id: str, - gateway_session_key: Optional[str] = None) -> "web.StreamResponse": + gateway_session_key: Optional[str] = None) -> web.StreamResponse: """Write the SSE stream for POST /v1/responses. Events: ``response.created`` -> ``output_text.delta/done`` + ``output_item.added/done`` @@ -1031,7 +1031,7 @@ class OpenAICompatRoutesMixin: logger.error("Agent crashed mid-stream for %s: %s", response_id, str(st.agent_error)[:300]) return response - async def _handle_responses(self, request: "web.Request") -> "web.Response": + async def _handle_responses(self, request: web.Request) -> web.Response: """POST /v1/responses — OpenAI Responses API format.""" from gateway.platforms.api_server import ( ThreadSafeAsyncQueue, _auto_truncate_response_history, _coerce_request_bool, @@ -1213,7 +1213,7 @@ class OpenAICompatRoutesMixin: response_headers["X-Hermes-Session-Key"] = gateway_session_key return web.json_response(response_data, headers=response_headers) - async def _handle_get_response(self, request: "web.Request") -> "web.Response": + async def _handle_get_response(self, request: web.Request) -> web.Response: """GET /v1/responses/{response_id} — retrieve a stored response.""" from gateway.platforms.api_server import _error_response auth_err = self._check_auth(request) @@ -1225,7 +1225,7 @@ class OpenAICompatRoutesMixin: return _error_response(f"Response not found: {response_id}", 404) return web.json_response(stored["response"]) - async def _handle_delete_response(self, request: "web.Request") -> "web.Response": + async def _handle_delete_response(self, request: web.Request) -> web.Response: """DELETE /v1/responses/{response_id} — delete a stored response.""" from gateway.platforms.api_server import _error_response auth_err = self._check_auth(request) diff --git a/gateway/platforms/api_server_room_dispatch.py b/gateway/platforms/api_server_room_dispatch.py index 44f6ed940c..dc4061f5f6 100644 --- a/gateway/platforms/api_server_room_dispatch.py +++ b/gateway/platforms/api_server_room_dispatch.py @@ -48,7 +48,7 @@ async def _ensure_hosted_member_session(self, dispatch: Any) -> str: return await asyncio.to_thread(db._execute_write, atomic) -def _room_dispatch_error(exc: Exception, *, _openai_error) -> "web.Response": +def _room_dispatch_error(exc: Exception, *, _openai_error) -> web.Response: message, code = str(exc), "invalid_room_dispatch" lowered = message.lower() if "execution policy" in lowered or "remote room execution requires" in lowered: @@ -61,7 +61,7 @@ def _room_dispatch_error(exc: Exception, *, _openai_error) -> "web.Response": async def _normalize_room_dispatch( - self, request: "web.Request", body: Any, *, _api_server) -> tuple[Any, "web.Response | None"]: + self, request: web.Request, body: Any, *, _api_server) -> tuple[Any, web.Response | None]: """Validate and normalize a scoped RoomLink dispatch request.""" _openai_error, room_token = _api_server._openai_error, self._room_grant_token(request) if not room_token: diff --git a/gateway/platforms/api_server_room_grants.py b/gateway/platforms/api_server_room_grants.py index 1c2eab3e34..a32500ba6f 100644 --- a/gateway/platforms/api_server_room_grants.py +++ b/gateway/platforms/api_server_room_grants.py @@ -14,7 +14,7 @@ class RoomGrantReauthorizationRequired(ValueError): """A validly signed room grant was revoked or superseded.""" -def _json_error(_openai_error, message: str, *, status: int, **error_kwargs) -> "web.Response": +def _json_error(_openai_error, message: str, *, status: int, **error_kwargs) -> web.Response: """JSON error response built with the injected ``_openai_error`` envelope builder.""" return web.json_response(_openai_error(message, **error_kwargs), status=status) @@ -25,7 +25,7 @@ def _require_unchanged_execution_policy(claims: dict[str, Any], execution_policy raise RoomGrantReauthorizationRequired("room execution policy changed") -def _room_grant_error_response(exc: Optional[Exception] = None, *, _openai_error) -> "web.Response": +def _room_grant_error_response(exc: Optional[Exception] = None, *, _openai_error) -> web.Response: """401 invalid grant, or 403 reauthorization-required for a revoked/superseded grant.""" if isinstance(exc, RoomGrantReauthorizationRequired): message, code, status = "Room authorization needs to be renewed.", "room_reauthorization_required", 403 @@ -78,7 +78,7 @@ def _http_routes(self) -> list[tuple[str, str, Any]]: ("POST", "/v1/room-members/grants/revoke", self._handle_room_member_grant_revoke)] -def _room_grant_token(request: "web.Request") -> str: +def _room_grant_token(request: web.Request) -> str: scheme, separator, token = str(request.headers.get("Authorization") or "").partition(" ") return token.strip() if separator and scheme.lower() == "hermesroom" else "" @@ -88,7 +88,7 @@ def _room_grant_secret(self) -> bytes: return gateway_room_grant_secret() -def _decode_request_grant(self, request: "web.Request", *, permission: str) -> dict[str, Any]: +def _decode_request_grant(self, request: web.Request, *, permission: str) -> dict[str, Any]: """Signature/scope/horizon check only (no revocation lookup).""" from gateway.hosted_room_peer import decode_room_grant token = self._room_grant_token(request) @@ -97,7 +97,7 @@ def _decode_request_grant(self, request: "web.Request", *, permission: str) -> d return decode_room_grant(self._room_grant_secret(), token, permission=permission) -def _room_grant_claims(self, request: "web.Request", *, permission: str) -> dict[str, Any]: +def _room_grant_claims(self, request: web.Request, *, permission: str) -> dict[str, Any]: claims = _decode_request_grant(self, request, permission=permission) from gateway import hosted_rooms db_path = hosted_rooms.default_db_path() @@ -109,7 +109,7 @@ def _room_grant_claims(self, request: "web.Request", *, permission: str) -> dict async def _handle_room_member_invitation( - self, request: "web.Request", *, _openai_error, _api_request_profile) -> "web.Response": + self, request: web.Request, *, _openai_error, _api_request_profile) -> web.Response: """Mint a short-lived room/profile grant for a trusted home gateway.""" auth_err = self._check_auth(request) if auth_err: @@ -153,7 +153,7 @@ async def _handle_room_member_invitation( async def _handle_room_member_capabilities( - self, request: "web.Request", *, _openai_error, _api_request_profile) -> "web.Response": + self, request: web.Request, *, _openai_error, _api_request_profile) -> web.Response: """Verify a scoped grant and return this target's live room catalog.""" try: claims = self._room_grant_claims(request, permission="status") @@ -167,7 +167,7 @@ async def _handle_room_member_capabilities( async def _handle_room_member_grant_refresh( - self, request: "web.Request", *, _openai_error, _api_request_profile) -> "web.Response": + self, request: web.Request, *, _openai_error, _api_request_profile) -> web.Response: """Refresh dispatch access without a Desktop or broad gateway key.""" body, error = await self._read_json_body(request) if error: @@ -206,7 +206,7 @@ async def _handle_room_member_grant_refresh( async def _handle_room_member_grant_revoke( - self, request: "web.Request", *, _openai_error, _api_request_profile) -> "web.Response": + self, request: web.Request, *, _openai_error, _api_request_profile) -> web.Response: """Revoke exactly the scoped grant authenticating this request.""" body, error = await self._read_json_body(request) if error: diff --git a/gateway/platforms/api_server_runs.py b/gateway/platforms/api_server_runs.py index a349835527..bfb5b4dbee 100644 --- a/gateway/platforms/api_server_runs.py +++ b/gateway/platforms/api_server_runs.py @@ -157,7 +157,7 @@ class _RunStream: self.subscribers.discard(queue) -def _remember_room_retention(request: "web.Request", claims: dict[str, Any]) -> None: +def _remember_room_retention(request: web.Request, claims: dict[str, Any]) -> None: value = float(claims.get("status_expires_at") or claims.get("expires_at") or 0) try: request[_ROOM_RETENTION_REQUEST_KEY] = value @@ -165,7 +165,7 @@ def _remember_room_retention(request: "web.Request", claims: dict[str, Any]) -> request._hermes_room_run_retention_until = value -def _room_retention_until(request: "web.Request") -> float: +def _room_retention_until(request: web.Request) -> float: try: value = request.get(_ROOM_RETENTION_REQUEST_KEY, 0) except AttributeError: @@ -200,11 +200,11 @@ def terminal_run_status(result: dict[str, Any]) -> tuple[str, dict[str, Any]]: return status, fields -def _run_not_found(_openai_error, run_id: str) -> "web.Response": +def _run_not_found(_openai_error, run_id: str) -> web.Response: return _json_error(_openai_error, f"Run not found: {run_id}", code="run_not_found", status=404) -def _uses_room_run_auth(self, request: "web.Request") -> bool: +def _uses_room_run_auth(self, request: web.Request) -> bool: return request.path.endswith("/v1/runs") and bool(self._room_grant_token(request)) @@ -312,7 +312,7 @@ def _mark_shutdown_requested(self) -> int: return marked -def _make_run_event_callback(self, run_id: str, loop: "asyncio.AbstractEventLoop", *, _api_server): +def _make_run_event_callback(self, run_id: str, loop: asyncio.AbstractEventLoop, *, _api_server): """Return a callback that pushes structured events to the run SSE queue.""" redact_sensitive_text = _api_server.redact_sensitive_text @@ -349,7 +349,7 @@ def _make_run_event_callback(self, run_id: str, loop: "asyncio.AbstractEventLoop return _callback -def _room_permission_for(request: "web.Request") -> str: +def _room_permission_for(request: web.Request) -> str: if request.path.endswith("/stop"): return "stop" if request.path.endswith("/approval"): @@ -357,7 +357,7 @@ def _room_permission_for(request: "web.Request") -> str: return "status" if request.method == "GET" else "dispatch" -def _run_idempotency_scope(self, request: "web.Request", *, _api_server) -> str: +def _run_idempotency_scope(self, request: web.Request, *, _api_server) -> str: """Opaque auth/profile namespace; never persist bearer credentials.""" if self._room_grant_token(request): claims = self._room_grant_claims(request, permission=_room_permission_for(request)) @@ -371,7 +371,7 @@ def _run_idempotency_scope(self, request: "web.Request", *, _api_server) -> str: return hashlib.sha256("\0".join(map(str, parts)).encode()).hexdigest() -def _check_run_auth(self, request: "web.Request", *, permission: str, _api_server) -> "web.Response | None": +def _check_run_auth(self, request: web.Request, *, permission: str, _api_server) -> web.Response | None: if not self._room_grant_token(request): return self._check_auth(request) try: @@ -392,7 +392,7 @@ def _owner_alive(owner_pid: int, owner_started: int) -> bool: return False -def _durable_run_status(self, request: "web.Request", run_id: str) -> dict[str, Any] | None: +def _durable_run_status(self, request: web.Request, run_id: str) -> dict[str, Any] | None: """Hydrate a scoped run status and fail stale owners closed.""" status = self._run_statuses.get(run_id) if status is not None: @@ -420,7 +420,7 @@ def _durable_run_status(self, request: "web.Request", run_id: str) -> dict[str, def _resolve_conversation_history( self, body: dict, raw_input: Any, *, _openai_error -) -> "tuple[list[dict[str, str]], Any, Any, web.Response | None]": +) -> tuple[list[dict[str, str]], Any, Any, web.Response | None]: """Return ``(history, instructions, stored_session_id, error)``; precedence: ``conversation_history`` > ``previous_response_id`` chain > all-but-last ``input`` messages.""" instructions = body.get("instructions") @@ -458,7 +458,7 @@ def _resolve_conversation_history( return conversation_history, instructions, stored_session_id, None -def _accepted_response(run_id: str, status: str, gateway_session_key, *, replayed: bool) -> "web.Response": +def _accepted_response(run_id: str, status: str, gateway_session_key, *, replayed: bool) -> web.Response: """202 admission response; replays are flagged via ``Idempotency-Replayed``.""" headers = {"Idempotency-Replayed": "true"} if replayed else {} if gateway_session_key: @@ -467,7 +467,7 @@ def _accepted_response(run_id: str, status: str, gateway_session_key, *, replaye {"run_id": run_id, "status": status, "replayed": replayed}, status=202, headers=headers) -def _replay_or_conflict(self, request, outcome, record, gateway_session_key, _openai_error) -> "web.Response": +def _replay_or_conflict(self, request, outcome, record, gateway_session_key, _openai_error) -> web.Response: """409 for a fingerprint conflict, else a 202 replay of the already-admitted run.""" if outcome == "conflict": return _json_error( @@ -617,7 +617,7 @@ async def run_internal_session_turn(self, *, session_id: str, text: str, profile _api_server._api_request_profile.reset(token) -async def _handle_runs(self, request: "web.Request", *, _api_server) -> "web.Response": +async def _handle_runs(self, request: web.Request, *, _api_server) -> web.Response: """POST /v1/runs — start an agent run, return run_id immediately.""" _openai_error = _api_server._openai_error # Long-term memory scope header (see chat_completions for details). @@ -1020,7 +1020,7 @@ def _release_run_owner_if_forgotten(self, run_id: str) -> None: self._run_owners.pop(run_id, None) -def _request_owns_run(self, request: "web.Request", run_id: str) -> bool: +def _request_owns_run(self, request: web.Request, run_id: str) -> bool: scope = self._run_idempotency_scope(request) owner = self._run_owners.get(run_id) if owner is not None: @@ -1054,14 +1054,14 @@ def _load_owned_run(self, request, *, _api_server, permission: Optional[str], ac return run_id, status, agent, task, None -async def _handle_get_run(self, request: "web.Request", *, _api_server) -> "web.Response": +async def _handle_get_run(self, request: web.Request, *, _api_server) -> web.Response: """GET /v1/runs/{run_id} — return pollable run status for external UIs.""" _, status, _, _, err = _load_owned_run( self, request, _api_server=_api_server, permission="status", active_fallback=True) return err or web.json_response(status) -async def _handle_run_events(self, request: "web.Request", *, _api_server) -> "web.StreamResponse": +async def _handle_run_events(self, request: web.Request, *, _api_server) -> web.StreamResponse: """GET /v1/runs/{run_id}/events — stream structured agent lifecycle events.""" auth_err = self._check_auth(request) if auth_err: @@ -1165,7 +1165,7 @@ def _mark_run_event(self, run_id: str, name: str, **fields: Any) -> None: _APPROVAL_CHOICE_ALIASES = {"approve": "once", "approved": "once", "allow": "once"} -async def _handle_run_approval(self, request: "web.Request", *, _api_server) -> "web.Response": +async def _handle_run_approval(self, request: web.Request, *, _api_server) -> web.Response: """POST /v1/runs/{run_id}/approval — resolve a pending run approval.""" _openai_error = _api_server._openai_error run_id, _, _, _, err = _load_owned_run( @@ -1216,7 +1216,7 @@ async def _handle_run_approval(self, request: "web.Request", *, _api_server) -> "resolved": resolved}) -async def _handle_steer_run(self, request: "web.Request", *, _api_server) -> "web.Response": +async def _handle_steer_run(self, request: web.Request, *, _api_server) -> web.Response: """POST /v1/runs/{run_id}/steer — inject guidance into a running agent.""" _openai_error = _api_server._openai_error run_id, status, agent, _, err = _load_owned_run( @@ -1250,7 +1250,7 @@ async def _handle_steer_run(self, request: "web.Request", *, _api_server) -> "we return web.json_response({"object": "hermes.run.steer", "run_id": run_id, "accepted": True}) -async def _handle_stop_run(self, request: "web.Request", *, _api_server) -> "web.Response": +async def _handle_stop_run(self, request: web.Request, *, _api_server) -> web.Response: """POST /v1/runs/{run_id}/stop — interrupt a running agent.""" _openai_error = _api_server._openai_error run_id, status, agent, task, err = _load_owned_run( diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index 770f96267e..0257a9b5d4 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -28,7 +28,7 @@ from agent.proxy_bypass import first_proxy_env_value, should_bypass_proxy as _sh logger = logging.getLogger(__name__) -def _consume_detached_handler_exception(task: "asyncio.Task") -> None: +def _consume_detached_handler_exception(task: asyncio.Task) -> None: """Done-callback for a detached fatal-error handler task (carrier cancelled in ``_notify_fatal_error``): retrieve its exception so asyncio never logs "never retrieved".""" if task.cancelled(): @@ -53,7 +53,7 @@ _TELEGRAM_VOICE_EXTS = frozenset({'.ogg', '.opus'}) def transcode_to_ogg_opus(path: str, *, bitrate: str = "32k", timeout: int = 60, - output_path: "str | None" = None) -> "str | None": + output_path: str | None = None) -> str | None: """Best-effort ffmpeg transcode to Ogg/Opus (voip-tuned) for native voice bubbles: the written ``.ogg`` path (a NEW temp file unless ``output_path`` is given; caller cleans up), or None when ffmpeg is missing/fails. ``output_path`` may equal ``path`` (in-place container repair) — the @@ -182,7 +182,7 @@ def _reply_anchor_for_event(event) -> str | None: _MEDIA_KIND_KEYS = frozenset({"audio", "video", "file", "image"}) -def _media_failure_text(kind: str, file_name: "str | None" = None) -> str: +def _media_failure_text(kind: str, file_name: str | None = None) -> str: """User-facing "couldn't deliver" notice; ``file_name`` is the only name ever shown.""" kind_label = t(f"gateway.notify.media_kind.{kind}") if kind in _MEDIA_KIND_KEYS else kind if file_name: @@ -273,7 +273,7 @@ def is_network_accessible(host: str) -> bool: # goes out on a stale answer fails and is retried, which is the same outcome as any transient proxy error. # No lock: a race costs one extra fork and both answers are equally current. _MACOS_PROXY_TTL_SECONDS = 60.0 -_macos_proxy_cache: "tuple[float, str | None] | None" = None +_macos_proxy_cache: tuple[float, str | None] | None = None def _detect_macos_system_proxy() -> str | None: @@ -1362,7 +1362,7 @@ MEDIA_EXTENSIONLESS_TAG_RE = re.compile( re.IGNORECASE) -def _match_extensionless_path(scan_text: str, match: "re.Match") -> Optional[tuple[str, int]]: +def _match_extensionless_path(scan_text: str, match: re.Match) -> Optional[tuple[str, int]]: """Extensionless MEDIA tag match -> validated on-disk ``(safe_path, end_offset)`` or None: the captured path first, then extended across single spaces (max 8 tokens, never past a newline or the next ``MEDIA:``). @@ -1634,7 +1634,7 @@ class TextDebounceState: first_ts: float last_ts: float - def cancel_timer(self, *, unless: "asyncio.Task | None" = None) -> None: + def cancel_timer(self, *, unless: asyncio.Task | None = None) -> None: """Cancel the pending flush timer (if live and not ``unless``).""" if self.task is not None and self.task is not unless and not self.task.done(): self.task.cancel() @@ -1662,7 +1662,7 @@ _PLAINTEXT_GATEWAY_RESTART_PATTERNS: tuple[re.Pattern[str], ...] = ( re.compile(r"^(?:please\s+)?restart\s+hermes[.!?\s]*$", re.IGNORECASE)) -def coerce_plaintext_gateway_command(event: "MessageEvent") -> None: +def coerce_plaintext_gateway_command(event: MessageEvent) -> None: """Rewrite a tiny set of DM plaintext admin phrases (exact matches only) into slash commands so ``restart gateway`` never reaches the LLM/tool path (a self-restart from inside the running agent leaves the gateway stuck in ``draining`` waiting on that agent).""" @@ -1976,7 +1976,7 @@ class BasePlatformAdapter(ABC): self._running, self._fatal_error_retryable = False, True self._fatal_error_code: Optional[str] = None self._fatal_error_message: Optional[str] = None - self._fatal_error_handler: Optional[Callable[["BasePlatformAdapter"], Awaitable[None] | None]] = None + self._fatal_error_handler: Optional[Callable[[BasePlatformAdapter], Awaitable[None] | None]] = None # Strong refs to shielded fatal-error handler tasks that outlive their carrier task # (asyncio keeps only weak refs); without them the loop can GC the detached handler # mid-flight — the "handler killed mid-flight" class (#81335). @@ -2135,7 +2135,7 @@ class BasePlatformAdapter(ABC): return t("gateway.progress.tool_preview", emoji=emoji, tool=tool, preview=self.format_tool_preview(prepared)) - def format_tool_preview(self, preview: "ToolPreview") -> str: + def format_tool_preview(self, preview: ToolPreview) -> str: """Platform-native formatting of a compact tool preview; rich-text adapters may use the preview's metadata (e.g. a URL shortened for display).""" return preview.text @@ -2155,7 +2155,7 @@ class BasePlatformAdapter(ABC): return chat_id in self._auto_tts_enabled_chats or ( chat_id not in self._auto_tts_disabled_chats and bool(self._auto_tts_default)) - def set_fatal_error_handler(self, handler: Callable[["BasePlatformAdapter"], Awaitable[None] | None]) -> None: + def set_fatal_error_handler(self, handler: Callable[[BasePlatformAdapter], Awaitable[None] | None]) -> None: self._fatal_error_handler = handler #: Published when an adapter is installed and running but its receive @@ -2451,7 +2451,7 @@ class BasePlatformAdapter(ABC): owner = getattr(self, "_owner_profile", None) return owner if isinstance(owner, str) and owner.strip() else None - def _canonicalize(self, source: Optional["SessionSource"]): + def _canonicalize(self, source: Optional[SessionSource]): """Pin the source's :class:`RoutingIdentity` before anything derives a key from it. Every ingress path (fresh event, batch merge, busy path, control command, callback) calls this FIRST. Returns the identity, or ``None`` when it cannot be resolved (a rejected route under @@ -2475,7 +2475,7 @@ class BasePlatformAdapter(ABC): logger.debug("[%s] identity resolution failed; using legacy key readers", self.name, exc_info=True) return None - def _drop_unresolved(self, event: "MessageEvent") -> bool: + def _drop_unresolved(self, event: MessageEvent) -> bool: """True when *event* must be dropped: its identity could not be resolved because the route targets an unserved profile. Same disposition as the runner's ingress gate — one WARNING, never a fall-through to ``agent:main``.""" @@ -2554,11 +2554,11 @@ class BasePlatformAdapter(ABC): "text_batch_split_delay_seconds", self._TEXT_BATCH_DEFAULT_SPLIT_DELAY_S, min_value=self._text_batch_delay_seconds, max_value=self._TEXT_BATCH_MAX_SPLIT_DELAY_S) - def _event_session_key(self, event: "MessageEvent") -> str: + def _event_session_key(self, event: MessageEvent) -> str: """Adapter-level session key for ``event``, profile-namespaced like the agent run.""" return self._source_session_key(event.source) - def _source_session_key(self, source: "SessionSource") -> str: + def _source_session_key(self, source: SessionSource) -> str: self._canonicalize(source) # identity FIRST; no key derivation before it extra = self.config.extra return build_session_key( @@ -2566,11 +2566,11 @@ class BasePlatformAdapter(ABC): thread_sessions_per_user=extra.get("thread_sessions_per_user", False), profile=self._session_key_profile(source)) - def _text_batch_key(self, event: "MessageEvent") -> str: + def _text_batch_key(self, event: MessageEvent) -> str: """Session-scoped key for text batching (subclasses may override).""" return self._event_session_key(event) - def _enqueue_text_event(self, event: "MessageEvent") -> None: + def _enqueue_text_event(self, event: MessageEvent) -> None: """Buffer a text event (merging into a pending one) and restart the flush timer.""" if self._drop_unresolved(event): return @@ -2591,16 +2591,16 @@ class BasePlatformAdapter(ABC): prior_task.cancel() self._pending_text_batch_tasks[key] = asyncio.create_task(self._flush_text_batch(key)) - def _text_batch_delay_for(self, pending: Optional["MessageEvent"]) -> float: + def _text_batch_delay_for(self, pending: Optional[MessageEvent]) -> float: """Quiet period before ``pending`` is dispatched; near-split chunks wait longer.""" last_len = getattr(pending, "_last_chunk_len", 0) if pending is not None else 0 return self._text_batch_split_delay_seconds if last_len >= self._SPLIT_THRESHOLD else self._text_batch_delay_seconds - def _pop_text_batch(self, key: str) -> Optional["MessageEvent"]: + def _pop_text_batch(self, key: str) -> Optional[MessageEvent]: """Remove and return the pending batch for ``key`` (adapters with side tables override).""" return self._pending_text_batches.pop(key, None) - async def _dispatch_text_batch(self, event: "MessageEvent") -> None: + async def _dispatch_text_batch(self, event: MessageEvent) -> None: """Hand a flushed batch to the pipeline (adapters with per-chat guards override).""" await self.handle_message(event) @@ -2909,12 +2909,12 @@ class BasePlatformAdapter(ABC): allow_permanent=allow_permanent, allow_session=allow_session, smart_denied=smart_denied)) return await self._send_exec_approval_prompt(prompt) - async def _send_exec_approval_prompt(self, prompt: "ExecApprovalPrompt") -> SendResult: + async def _send_exec_approval_prompt(self, prompt: ExecApprovalPrompt) -> SendResult: """Render ``prompt`` with the platform's native buttons; the default has none.""" return SendResult(success=False, error="Not supported") @staticmethod - def _format_choice_page(options: list, page: int, per_page: int) -> "tuple[list, dict[str, Any]]": + def _format_choice_page(options: list, page: int, per_page: int) -> tuple[list, dict[str, Any]]: """Shared picker pagination: clamp ``page``, slice ``options`` -> ``(page_options, meta)`` with ``page``/``total_pages``/``start``/``end``/``total``/``page_info`` (`` (N–M of T)``, empty for one page).""" @@ -3628,7 +3628,7 @@ class BasePlatformAdapter(ABC): logger.debug("[%s] Failed to resolve media delivery scope", self.name, exc_info=True) return contextlib.nullcontext() - def _final_delivery_adapter(self, source: Optional[SessionSource]) -> "BasePlatformAdapter": + def _final_delivery_adapter(self, source: Optional[SessionSource]) -> BasePlatformAdapter: """The runner's CURRENT adapter for a new final-response send: a reconnect can swap the registry adapter mid-task; an unsent final response belongs on the replacement transport, while message IDs, edits and deletes stay owned by the old one (nothing is migrated).""" @@ -3647,13 +3647,13 @@ class BasePlatformAdapter(ABC): @records_delivery async def _send_with_retry( self, chat_id: str, content: str, reply_to: Optional[str] = None, metadata: Any = None, - max_retries: int = 2, base_delay: float = 2.0) -> "SendResult": + max_retries: int = 2, base_delay: float = 2.0) -> SendResult: """Send with exponential-backoff retry on transient network errors; permanent failures fall back to a plain-text send, exhausted retries notify the user.""" - async def _send(text: str) -> "SendResult": + async def _send(text: str) -> SendResult: return await self.send(chat_id=chat_id, content=text, reply_to=reply_to, metadata=metadata) - async def _send_again(previous: "SendResult") -> "Optional[SendResult]": + async def _send_again(previous: SendResult) -> Optional[SendResult]: """Retry: the whole payload normally; only the undelivered remainder after a partial split delivery (``raw_response["partial_overflow"]``). ``None`` when the adapter cannot resume — the caller then keeps the partial failure rather than re-sending the already-visible head.""" @@ -3767,13 +3767,13 @@ class BasePlatformAdapter(ABC): logger.error("[%s] Fallback send also failed: %s", self.name, fallback_result.error) return fallback_result - def _send_retry_is_final(self, result: "SendResult") -> bool: + def _send_retry_is_final(self, result: SendResult) -> bool: """True when a failed send must be returned as-is: neither a retry nor the plain-text fallback can fix it (a structured auth/target refusal). Default: never.""" return False @staticmethod - def _is_partial_delivery(result: "SendResult") -> bool: + def _is_partial_delivery(result: SendResult) -> bool: """True when a split payload was PARTLY delivered (``raw_response["partial_overflow"]``, the contract Telegram's send/edit-overflow paths set and the stream consumer reads): the visible head must never be sent again.""" @@ -3781,14 +3781,14 @@ class BasePlatformAdapter(ABC): return isinstance(raw, dict) and bool(raw.get("partial_overflow")) async def _resume_partial_send( - self, chat_id: str, result: "SendResult", *, reply_to: Optional[str], metadata: Any) -> "Optional[SendResult]": + self, chat_id: str, result: SendResult, *, reply_to: Optional[str], metadata: Any) -> Optional[SendResult]: """Deliver only the remainder of a partially delivered split payload. ``None`` (the default) means this adapter cannot resume; ``_send_with_retry`` then returns the partial failure instead of re-sending the whole payload. Override only where non-delivery of the remainder is CERTAIN.""" return None async def _send_plain_fallback( - self, chat_id: str, content: str, *, reply_to: Optional[str], metadata: Any) -> "SendResult": + self, chat_id: str, content: str, *, reply_to: Optional[str], metadata: Any) -> SendResult: """Last-resort send after a non-transient failure; platforms whose markup is not the likely culprit override it (Photon drops rich links instead of adding the banner).""" return await self.send( @@ -4244,7 +4244,7 @@ class BasePlatformAdapter(ABC): async def _record_delivery_obligation( self, event: MessageEvent, session_key: str, text_content: str, - delivery_adapter: "BasePlatformAdapter", is_ephemeral_response: bool) -> Optional[str]: + delivery_adapter: BasePlatformAdapter, is_ephemeral_response: bool) -> Optional[str]: """Ledger the final response BEFORE the send so a crash before platform ACK redelivers on next boot; best-effort, skips slash-command and ephemeral replies. Returns the obligation id or None.""" @@ -4278,7 +4278,7 @@ class BasePlatformAdapter(ABC): async def _finalize_delivery_obligation( self, obligation_id: str, result: Any, event: MessageEvent, - delivery_adapter: "BasePlatformAdapter") -> None: + delivery_adapter: BasePlatformAdapter) -> None: """Mark the ledger row delivered/failed (best-effort). On ``send_path_degraded`` with a replacement adapter live, trigger another redelivery sweep (the watcher's may have run before this failure landed; atomic claiming keeps it idempotent). On any other rejection arm @@ -4378,7 +4378,7 @@ class BasePlatformAdapter(ABC): async def send_final_ledgered( self, event: MessageEvent, session_key: str, text_content: str, metadata: dict[str, Any], *, reply_to: Optional[str], is_ephemeral_response: bool = False, - ) -> "tuple[SendResult, BasePlatformAdapter]": + ) -> tuple[SendResult, BasePlatformAdapter]: """The delivery-ledger bracket every final text goes through, on the CURRENT transport (a reconnect may have replaced this adapter): record the obligation before the send, send with retry, finalize from the result — so a refused final (flood control, a dead @@ -4440,7 +4440,7 @@ class BasePlatformAdapter(ABC): "[%s] Failed to send error notification to user: %s", self.name, notify_err, exc_info=True) return _thread_metadata - async def _deliver_attachments(self, event: MessageEvent, extracted: "_ExtractedResponse", + async def _deliver_attachments(self, event: MessageEvent, extracted: _ExtractedResponse, metadata: dict[str, Any], *, anything_sent: bool, record_delivery: Callable) -> None: """Send extracted image URLs, MEDIA files and bare local files (human-paced), @@ -4474,7 +4474,7 @@ class BasePlatformAdapter(ABC): return asyncio.create_task(self._keep_typing(event.source.chat_id, **kwargs)) async def _extract_response_content(self, response: str, event: MessageEvent, session_key: str, - *, is_ephemeral_response: bool) -> "_ExtractedResponse": + *, is_ephemeral_response: bool) -> _ExtractedResponse: """Split a handler response into deliverable text + attachments. Order matters: MEDIA tags → image URLs → residual directives → bare local paths (skipped for ephemeral notices so config paths stay text; unknown-extension MEDIA tags survive for the bare-path detector). History @@ -4859,7 +4859,7 @@ class BasePlatformAdapter(ABC): """Get information about a chat/channel; dict with at least ``name`` and ``type`` ("dm", "group", "channel").""" - def toolsets_for_source(self, source: "SessionSource") -> Optional[list[str]]: + def toolsets_for_source(self, source: SessionSource) -> Optional[list[str]]: """Per-source toolset override REPLACING ``platform_toolsets.``, or None (default); validated via ``_get_platform_tools`` (webhook adapter pins per-route).""" return None @@ -4871,7 +4871,7 @@ class BasePlatformAdapter(ABC): @staticmethod def truncate_message(content: str, max_length: int = 4096, - len_fn: Optional["Callable[[str], int]"] = None) -> list[str]: + len_fn: Optional[Callable[[str], int]] = None) -> list[str]: """Split a long message into chunks preserving code blocks: a split inside a fence closes it at the chunk end and reopens it (same language tag) in the next; multi-chunk output gets ``(1/3)`` indicators. ``len_fn`` overrides ``len`` (``utf16_len`` for Telegram).""" diff --git a/gateway/platforms/event.py b/gateway/platforms/event.py index 54ecff5519..2ddd39b1bd 100644 --- a/gateway/platforms/event.py +++ b/gateway/platforms/event.py @@ -107,7 +107,7 @@ class MessageEvent: # Run-owned final presentation snapshot; never deserialized from ingress metadata. _notification_reply_muted: Optional[bool] = field(default=None, init=False, repr=False, compare=False) - def absorb_reply_expected(self, other: "MessageEvent") -> None: + def absorb_reply_expected(self, other: MessageEvent) -> None: """One turn now answers *other* too: an addressed message wins, then an unknown one.""" if self.reply_expected is not True and other.reply_expected is not False: self.reply_expected = other.reply_expected diff --git a/gateway/platforms/helpers.py b/gateway/platforms/helpers.py index 8a2a824518..1e27210786 100644 --- a/gateway/platforms/helpers.py +++ b/gateway/platforms/helpers.py @@ -60,7 +60,7 @@ class MessageDeduplicator: def clear(self): self._seen.clear() - def absorb(self, other: "MessageDeduplicator") -> None: + def absorb(self, other: MessageDeduplicator) -> None: """Adopt *other*'s still-live IDs (at their original seen times) into this cache.""" cutoff = time.time() - self._ttl self._seen.update({k: v for k, v in other._seen.items() if v > cutoff and k not in self._seen}) @@ -135,7 +135,7 @@ _HTTP_TARGET_RE = re.compile(r"https?://", re.IGNORECASE) _NEWLINE_SQUEEZE_RE = re.compile(r"\n{3,}") -def _keep_link_target(match: "re.Match[str]") -> str: +def _keep_link_target(match: re.Match[str]) -> str: r"""``[label](https://url)`` -> ``label\nurl``. The bare URL is the only thing a platform with its own data detection @@ -345,8 +345,8 @@ def convert_table_to_bullets(text: str) -> str: def compile_mention_patterns(raw, *, log_prefix: str, platform_label: str | None = None, - display_label: str | None = None, defaults: 'list[str] | None' = None, - logger_: 'logging.Logger | None' = None) -> 'list[re.Pattern]': + display_label: str | None = None, defaults: list[str] | None = None, + logger_: logging.Logger | None = None) -> list[re.Pattern]: """Compile regex wake-word/mention patterns from config or env values. * **Config-style** (dingtalk, telegram): pass ``platform_label``. ``raw`` must be a @@ -459,11 +459,11 @@ def split_at_paragraph_boundary(text, max_chars, len_fn=None): return text[:cut], text[cut:] -def split_markdown_atoms(text: str) -> "list[str]": +def split_markdown_atoms(text: str) -> list[str]: """Split markdown into indivisible atoms: fenced code blocks, tables (consecutive ``|...|`` lines) and paragraphs. Blank lines belong to no atom.""" - atoms: "list[str]" = [] - current_lines: "list[str]" = [] + atoms: list[str] = [] + current_lines: list[str] = [] in_fence = False def _flush_current() -> None: @@ -505,10 +505,10 @@ def infer_block_separator(prev_chunk: str, next_chunk: str) -> str: return '\n\n' -def merge_streaming_fences(chunks: "list[str]") -> "list[str]": +def merge_streaming_fences(chunks: list[str]) -> list[str]: """Rejoin chunks truncated mid-fence: while chunk *i* has an unclosed fence and a successor exists, merge the successor in via :func:`infer_block_separator`.""" - result: "list[str]" = [] + result: list[str] = [] i = 0 while i < len(chunks): current = chunks[i] @@ -520,12 +520,12 @@ def merge_streaming_fences(chunks: "list[str]") -> "list[str]": return result -def balance_fences_across_chunks(chunks: "list[str]") -> "list[str]": +def balance_fences_across_chunks(chunks: list[str]) -> list[str]: """Close orphaned ``` fences at each chunk boundary and reopen (with the original language tag) on the next, so every chunk is fence-balanced alone.""" if len(chunks) <= 1: return chunks - out: "list[str]" = [] + out: list[str] = [] carry_lang = None for chunk in chunks: body = f"```{carry_lang}\n{chunk}" if carry_lang is not None else chunk @@ -535,7 +535,7 @@ def balance_fences_across_chunks(chunks: "list[str]") -> "list[str]": return out -def fence_state_after(text: str, in_code: bool = False, lang: str = "") -> "tuple[bool, str]": +def fence_state_after(text: str, in_code: bool = False, lang: str = "") -> tuple[bool, str]: """Walk ``text`` line by line toggling on ``` lines; return the final (in_code, lang).""" for line in text.split("\n"): stripped = line.strip() @@ -549,7 +549,7 @@ def greedy_pack_blocks(blocks, max_length, len_fn=None, sep="\n\n", overflow=Non """Greedily pack *blocks* (joined with *sep*) into chunks of at most *max_length*; an oversized block goes through *overflow(block)* (-> list of chunks) if given, else as-is.""" _len = len_fn or len - packed: "list[str]" = [] + packed: list[str] = [] current = "" for block in blocks: candidate = block if not current else f"{current}{sep}{block}" @@ -596,9 +596,9 @@ def _chunk_markdown_paragraphs(text, max_chars, len_fn=None): if _len(text) <= max_chars: return [text] # Phase 2: greedy merge; oversized fence/table atoms stay indivisible. - chunks: "list[str]" = [] - indivisible_set: "set[int]" = set() - current_parts: "list[str]" = [] + chunks: list[str] = [] + indivisible_set: set[int] = set() + current_parts: list[str] = [] current_len = 0 for atom in split_markdown_atoms(text): atom_len = _len(atom) @@ -616,7 +616,7 @@ def _chunk_markdown_paragraphs(text, max_chars, len_fn=None): if current_parts: chunks.append('\n\n'.join(current_parts)) # Phase 3: split still-oversized divisible chunks at paragraph boundaries. - result: "list[str]" = [] + result: list[str] = [] for idx, chunk in enumerate(chunks): if _len(chunk) <= max_chars or idx in indivisible_set or text_has_unclosed_fence(chunk): result.append(chunk) @@ -631,7 +631,7 @@ def _chunk_markdown_paragraphs(text, max_chars, len_fn=None): if remaining: result.append(remaining) # Phase 4: merge small chunks with neighbours. - merged: "list[str]" = result[:1] + merged: list[str] = result[:1] for chunk in result[1:]: combined = merged[-1] + '\n\n' + chunk if _len(combined) <= max_chars: @@ -647,7 +647,7 @@ def _chunk_newline_preferred(text, limit, len_fn): return [text] # Reserve headroom for fence markers a balancing pass may add. split_limit = max(limit - 16, limit // 2, 1) if "```" in text else limit - chunks: "list[str]" = [] + chunks: list[str] = [] remaining = text while len_fn(remaining) > split_limit: budget = _cp_budget(remaining, split_limit, len_fn) diff --git a/gateway/platforms/msgraph_webhook.py b/gateway/platforms/msgraph_webhook.py index a10d32139d..540a8a26fe 100644 --- a/gateway/platforms/msgraph_webhook.py +++ b/gateway/platforms/msgraph_webhook.py @@ -167,14 +167,14 @@ class MSGraphWebhookAdapter(BasePlatformAdapter): async def get_chat_info(self, chat_id: str) -> dict[str, Any]: return {"name": chat_id, "type": "webhook"} - async def _handle_health(self, request: "web.Request") -> "web.Response": + async def _handle_health(self, request: web.Request) -> web.Response: if not self._source_ip_allowed(request): return web.Response(status=403) return web.json_response({ "status": "ok", "platform": self.platform.value, "webhook_path": self._webhook_path, "accepted": self._accepted_count, "duplicates": self._duplicate_count}) - async def _handle_validation(self, request: "web.Request") -> "web.Response": + async def _handle_validation(self, request: web.Request) -> web.Response: """Graph subscription validation handshake: echo ``validationToken`` verbatim as text/plain. Bare GETs are rejected so the endpoint can't be enumerated.""" if not self._source_ip_allowed(request): @@ -203,7 +203,7 @@ class MSGraphWebhookAdapter(BasePlatformAdapter): self._schedule_notification(notification, self._build_message_event(notification, receipt_key)) return "accepted" - async def _handle_notification(self, request: "web.Request") -> "web.Response": + async def _handle_notification(self, request: web.Request) -> web.Response: if not self._source_ip_allowed(request): return web.Response(status=403) # Graph never sends validationToken on POST, but tolerate clients replaying it in-band. @@ -224,7 +224,7 @@ class MSGraphWebhookAdapter(BasePlatformAdapter): return web.Response(status=403) return web.Response(status=400) - async def _read_notifications(self, request: "web.Request") -> tuple[int, list]: + async def _read_notifications(self, request: web.Request) -> tuple[int, list]: """Read and validate the POST body; returns (error_status, []) or (0, notifications).""" try: content_length = request.content_length @@ -245,7 +245,7 @@ class MSGraphWebhookAdapter(BasePlatformAdapter): notifications = body.get("value") if isinstance(body, dict) else None return (0, notifications) if isinstance(notifications, list) else (400, []) - def _source_ip_allowed(self, request: "web.Request") -> bool: + def _source_ip_allowed(self, request: web.Request) -> bool: """Loopback-only binds may omit ``allowed_source_cidrs`` (local proxies, dev tunnels); network-accessible binds fail closed without one.""" if self._source_allowlist_required_but_missing(): diff --git a/gateway/platforms/qqbot/adapter.py b/gateway/platforms/qqbot/adapter.py index 0c5b74542c..ab4093d93a 100644 --- a/gateway/platforms/qqbot/adapter.py +++ b/gateway/platforms/qqbot/adapter.py @@ -1285,7 +1285,7 @@ class QQAdapter(OwnAccessPolicyMixin, BasePlatformAdapter): # ── Outbound messaging — REST API ── - def _require_http_client(self) -> "httpx.AsyncClient": + def _require_http_client(self) -> httpx.AsyncClient: if not self._http_client: raise RuntimeError("HTTP client not initialized — not connected?") return self._http_client diff --git a/gateway/platforms/shared_ingress.py b/gateway/platforms/shared_ingress.py index 8018ce1290..5c883734d0 100644 --- a/gateway/platforms/shared_ingress.py +++ b/gateway/platforms/shared_ingress.py @@ -55,9 +55,9 @@ def shared_listener_base(runner: Any) -> Optional[str]: async def bind_listener( - adapter: Any, app: "web.Application", host: Optional[str], port: int, ingress_path: str, *, + adapter: Any, app: web.Application, host: Optional[str], port: int, ingress_path: str, *, reuse_address: Optional[bool] = None, access_log: Any = ..., -) -> Optional["web.AppRunner"]: +) -> Optional[web.AppRunner]: """Start *app* on ``host:port`` and return its ``AppRunner`` — or, in shared-listener mode, publish *app* for ``/p//`` forwarding and return None (nothing bound). ``ingress_path`` is the adapter's primary callback path, used for the log line and runtime status.""" @@ -77,7 +77,7 @@ async def bind_listener( return runner -def publish_shared_ingress(adapter: Any, app: "web.Application", ingress_path: str) -> None: +def publish_shared_ingress(adapter: Any, app: web.Application, ingress_path: str) -> None: """Freeze *app* and expose it to the default listener; records the ``/p//`` URL.""" profile = shared_ingress_profile(adapter) app.freeze() @@ -104,7 +104,7 @@ def publish_shared_ingress(adapter: Any, app: "web.Application", ingress_path: s write("shared_ingress", ingress_url=adapter._shared_ingress_url) -def shared_ingress_apps(runner: Any, profile: Optional[str]) -> list[tuple[Any, "web.Application"]]: +def shared_ingress_apps(runner: Any, profile: Optional[str]) -> list[tuple[Any, web.Application]]: """``(adapter, app)`` for every shared-listener adapter of a NAMED served profile. ``default`` and unknown profiles yield nothing: the default's port-binders own their own ports, and a profile without a live adapter must never fall back to another profile's.""" @@ -118,8 +118,8 @@ def shared_ingress_apps(runner: Any, profile: Optional[str]) -> list[tuple[Any, async def dispatch_profile_ingress( - runner: Any, profile: Optional[str], tail: str, request: "web.Request", *, scoped: bool = False, -) -> "web.StreamResponse": + runner: Any, profile: Optional[str], tail: str, request: web.Request, *, scoped: bool = False, +) -> web.StreamResponse: """Forward ``/p//`` to the served profile's adapter app that routes ``/``, under that profile's runtime scope (``scoped=True`` when the caller already entered it). 404 when no adapter of *profile* serves the path.""" diff --git a/gateway/platforms/signal.py b/gateway/platforms/signal.py index c1d7f0f671..2430ae2814 100644 --- a/gateway/platforms/signal.py +++ b/gateway/platforms/signal.py @@ -209,12 +209,12 @@ class SignalAdapter(BasePlatformAdapter): self._account_normalized = self.account.strip() # Recently sent timestamps filter echo-backs (Note to Self / linked-device sync-sents); LRU + TTL # so a pending echo in a chatty group isn't evicted by many outbounds. - self._recent_sent_timestamps: "OrderedDict[int, float]" = OrderedDict() + self._recent_sent_timestamps: OrderedDict[int, float] = OrderedDict() self._max_recent_timestamps = 512 self._recent_sent_ttl_seconds = 300.0 # Separate FIFO of outbound timestamps: Signal quote.id is the quoted message's timestamp, so # replies to this bot are recognised after the echo was consumed. - self._sent_message_timestamps: "OrderedDict[str, None]" = OrderedDict() + self._sent_message_timestamps: OrderedDict[str, None] = OrderedDict() self._max_sent_message_timestamps = 500 # Best-effort number↔ACI/PNI UUID mapping so sends can upgrade a number to the UUID signal-cli prefers. self._recipient_uuid_by_number: dict[str, str] = {} @@ -950,7 +950,7 @@ class SignalAdapter(BasePlatformAdapter): ok = isinstance(raw, dict) and raw.get("sender") and raw.get("timestamp_ms") return (raw["sender"], raw["timestamp_ms"]) if ok else None - def _reactions_enabled(self, event: "MessageEvent" = None) -> bool: + def _reactions_enabled(self, event: MessageEvent = None) -> bool: """SIGNAL_REACTIONS env gate, then the DM allowlist: reactions fire before run.py's auth gate, so an unauthorized contact's 👀 would otherwise reveal a listening bot.""" if str(_sig_secret("SIGNAL_REACTIONS", "true")).lower() in {"false", "0", "no"}: @@ -963,7 +963,7 @@ class SignalAdapter(BasePlatformAdapter): if self._reactions_enabled(event) and (target := self._extract_reaction_target(event)): await self.send_reaction(event.source.chat_id, "👀", *target) - async def on_processing_complete(self, event: MessageEvent, outcome: "ProcessingOutcome") -> None: + async def on_processing_complete(self, event: MessageEvent, outcome: ProcessingOutcome) -> None: """Swap 👀 for ✅/❌; on CANCELLED the 👀 stays to keep reflecting "in progress" (matches Telegram).""" if outcome == ProcessingOutcome.CANCELLED or not self._reactions_enabled(event): return diff --git a/gateway/platforms/webhook.py b/gateway/platforms/webhook.py index 934bb2f0db..786aa49975 100644 --- a/gateway/platforms/webhook.py +++ b/gateway/platforms/webhook.py @@ -80,7 +80,7 @@ class _WebhookDeliveryIdentity: delivery_id: str @classmethod - def from_parts(cls, profile: Optional[str], route: str, delivery_id: str) -> "_WebhookDeliveryIdentity": + def from_parts(cls, profile: Optional[str], route: str, delivery_id: str) -> _WebhookDeliveryIdentity: return cls(profile=profile or "default", route=route, delivery_id=delivery_id) @property @@ -142,7 +142,7 @@ def _is_known_platform(name: str) -> bool: return False -def _json_error(message: str, status: int) -> "web.Response": +def _json_error(message: str, status: int) -> web.Response: return web.json_response({"error": message}, status=status) @@ -385,7 +385,7 @@ class WebhookAdapter(BasePlatformAdapter): # --- HTTP handlers --- - async def _handle_health(self, request: "web.Request") -> "web.Response": + async def _handle_health(self, request: web.Request) -> web.Response: """GET /health — simple health check.""" return web.json_response({"status": "ok", "platform": "webhook"}) @@ -454,7 +454,7 @@ class WebhookAdapter(BasePlatformAdapter): except Exception as e: logger.error("[webhook] Failed to reload dynamic routes: %s", e) - async def _handle_profile_ingress(self, request: "web.Request") -> "web.StreamResponse": + async def _handle_profile_ingress(self, request: web.Request) -> web.StreamResponse: profile = self._resolve_request_profile(request) if profile is _PROFILE_REJECTED or profile is None: return _json_error("Unknown or unconfigured profile", 404) @@ -462,7 +462,7 @@ class WebhookAdapter(BasePlatformAdapter): return await dispatch_profile_ingress( self.gateway_runner, profile, request.match_info.get("tail", ""), request) - def _resolve_request_profile(self, request: "web.Request"): + def _resolve_request_profile(self, request: web.Request): """Resolve + validate the /p// URL prefix: None (no prefix, or multiplexing off and the prefix names this gateway's own profile), the profile name (served under multiplexing), or ``_PROFILE_REJECTED`` (unknown / not served → 404).""" @@ -502,8 +502,8 @@ class WebhookAdapter(BasePlatformAdapter): from hermes_cli.profiles import get_profile_dir return _profile_runtime_scope(get_profile_dir(profile)) - async def _read_authenticated_body(self, request: "web.Request", route_name: str, - route_config: dict) -> "tuple[Optional[bytes], Optional[web.Response]]": + async def _read_authenticated_body(self, request: web.Request, route_name: str, + route_config: dict) -> tuple[Optional[bytes], Optional[web.Response]]: """Auth-before-body: size-cap, read, then HMAC-validate. Returns ``(body, None)`` or ``(None, response)``.""" if (request.content_length or 0) > self._max_body_bytes: return None, _json_error("Payload too large", 413) @@ -541,7 +541,7 @@ class WebhookAdapter(BasePlatformAdapter): return _UNPARSEABLE async def _handle_deliver_only(self, prompt: str, payload: Any, route_config: dict, route_name: str, - event_type: str, delivery_id: str, profile: Optional[str] = None) -> "web.Response": + event_type: str, delivery_id: str, profile: Optional[str] = None) -> web.Response: """deliver_only: the rendered prompt IS the message — skip the agent, reuse the same auth/rate-limit/idempotency/template pipeline.""" delivery = {"deliver": route_config.get("deliver", "log"), "payload": payload, "profile": profile, @@ -565,7 +565,7 @@ class WebhookAdapter(BasePlatformAdapter): return web.json_response(failed, status=502) def _handle_cron_trigger(self, prompt: str, route_config: dict, route_name: str, event_type: str, - delivery_id: str, profile: Optional[str] = None) -> "web.Response": + delivery_id: str, profile: Optional[str] = None) -> web.Response: """cron_job: fire an EXISTING cron job on this event instead of starting a webhook agent session. The rendered prompt is transient per-run context (same rail as ``cronjob(action='run', prompt=...)``); the job's own prompt, skills, model and delivery apply. Same auth/rate-limit/filter/script/idempotency @@ -596,7 +596,7 @@ class WebhookAdapter(BasePlatformAdapter): return web.json_response({"status": "accepted", "route": route_name, "cron_job": job_ref, "event": event_type, "delivery_id": delivery_id}, status=202) - def _resolve_route(self, request: "web.Request") -> "tuple[str, Optional[dict], Any, Optional[web.Response]]": + def _resolve_route(self, request: web.Request) -> tuple[str, Optional[dict], Any, Optional[web.Response]]: """Route + profile lookup for a POST; ``(route_name, route_config, profile, error_response)``.""" self._reload_dynamic_routes() # hot-reload dynamic subscriptions (stat-gated, lock-free) route_name = request.match_info.get("route_name", "") @@ -635,7 +635,7 @@ class WebhookAdapter(BasePlatformAdapter): logger.warning("[webhook] Skill loading failed: %s", e) return prompt - async def _handle_webhook(self, request: "web.Request") -> "web.Response": + async def _handle_webhook(self, request: web.Request) -> web.Response: """POST /webhooks/{route_name} — receive and process a webhook event.""" route_name, route_config, profile, error_response = self._resolve_route(request) if error_response is None: @@ -699,7 +699,7 @@ class WebhookAdapter(BasePlatformAdapter): delivery_id, now) def _dispatch_agent_run(self, request, route_config: dict, route_name: str, profile, payload: Any, prompt: str, - event_type: str, delivery_id: str, now: float) -> "web.Response": + event_type: str, delivery_id: str, now: float) -> web.Response: """Spawn the agent run for one POST and return 202 immediately.""" logger.info("[webhook] %s event=%s route=%s prompt_len=%d delivery=%s", request.method, event_type, route_name, len(prompt), delivery_id) @@ -709,7 +709,7 @@ class WebhookAdapter(BasePlatformAdapter): "delivery_id": delivery_id}, status=202) def _spawn_agent_run(self, payload: Any, prompt: str, delivery_id: str, now: float, *, route_config: dict, - route_name: str, profile, event_type: str) -> "asyncio.Task": + route_name: str, profile, event_type: str) -> asyncio.Task: """Record delivery info and fire the agent run (shared by the immediate and coalesced paths).""" identity = _WebhookDeliveryIdentity.from_parts(profile, route_name, delivery_id) session_chat_id = identity.session_chat_id @@ -736,13 +736,13 @@ class WebhookAdapter(BasePlatformAdapter): task.add_done_callback(self._background_tasks.discard) return task - async def on_processing_complete(self, event: "MessageEvent", outcome: Any) -> None: + async def on_processing_complete(self, event: MessageEvent, outcome: Any) -> None: """Close the one-shot per-delivery session: ``prune_sessions`` only reaps rows with ``ended_at`` set, so unclosed webhook sessions leak unbounded. Fires at the true end of the run; ``end_session()`` is first-reason-wins.""" await self._end_webhook_session(event, event.source.chat_id) - async def _end_webhook_session(self, event: "MessageEvent", session_chat_id: str) -> None: + async def _end_webhook_session(self, event: MessageEvent, session_chat_id: str) -> None: """Mark the per-delivery session ended via ``SessionDB.end_session`` (never a hand-written UPDATE), resolving session_id from the SAME source the run was keyed on.""" runner = self.gateway_runner @@ -766,7 +766,7 @@ class WebhookAdapter(BasePlatformAdapter): # --- Signature validation --- - def _validate_signature(self, request: "web.Request", body: bytes, secret: str) -> bool: + def _validate_signature(self, request: web.Request, body: bytes, secret: str) -> bool: """Validate webhook signature (GitHub, GitLab, Svix, Standard Webhooks, Linear, generic HMAC-SHA256).""" headers = request.headers diff --git a/gateway/platforms/weixin.py b/gateway/platforms/weixin.py index 6a89f40dae..59710ed248 100644 --- a/gateway/platforms/weixin.py +++ b/gateway/platforms/weixin.py @@ -66,7 +66,7 @@ _TABLE_RULE_RE = re.compile(r"^\s*\|?(?:\s*:?-{3,}:?\s*\|)+\s*:?-{3,}:?\s*\|?\s* _FENCE_RE = re.compile(r"^```([^\n`]*)\s*$") -def _is_stale_session_ret(ret: "Optional[int]", errcode: "Optional[int]", errmsg: "Optional[str]") -> bool: +def _is_stale_session_ret(ret: Optional[int], errcode: Optional[int], errmsg: Optional[str]) -> bool: """Recognize stale-session variants of iLink's ``-2`` response, not real rate limits.""" return (ret == RATE_LIMIT_ERRCODE or errcode == RATE_LIMIT_ERRCODE) and (errmsg or "").lower() in { "unknown error", @@ -88,7 +88,7 @@ def _session_not_ready_error(ret: Any, errcode: Any, errmsg: Any) -> RuntimeErro " — the user must send the bot a message first (or re-pair)") -def _make_ssl_connector() -> Optional["aiohttp.TCPConnector"]: +def _make_ssl_connector() -> Optional[aiohttp.TCPConnector]: """TCPConnector with certifi's CA bundle (``ilinkai.weixin.qq.com`` fails some system stores, e.g. Homebrew OpenSSL); None without certifi so aiohttp's default (honors ``SSL_CERT_FILE`` under trust_env) applies. ``keepalive_timeout=2`` + ``enable_cleanup_closed`` drain idle CLOSE_WAIT sockets behind proxies like Warp. @@ -108,7 +108,7 @@ def _make_ssl_connector() -> Optional["aiohttp.TCPConnector"]: return aiohttp.TCPConnector(ssl=ssl.create_default_context(cafile=certifi.where()), keepalive_timeout=2, enable_cleanup_closed=True) -def _new_session(**kwargs: Any) -> "aiohttp.ClientSession": +def _new_session(**kwargs: Any) -> aiohttp.ClientSession: return aiohttp.ClientSession(trust_env=gateway_trust_env(), connector=_make_ssl_connector(), **kwargs) @@ -264,7 +264,7 @@ def _guess_chat_type(message: dict[str, Any], account_id: str) -> tuple[str, str # HTTP helpers enforce timeouts via asyncio.wait_for(), not aiohttp ClientTimeout, which raises # "Timeout context manager should be used inside a task" under run_coroutine_threadsafe() from cron. async def _api_request( - session: "aiohttp.ClientSession", method: str, *, base_url: str, endpoint: str, headers: dict[str, str], timeout_ms: int, body: Optional[str] = None, + session: aiohttp.ClientSession, method: str, *, base_url: str, endpoint: str, headers: dict[str, str], timeout_ms: int, body: Optional[str] = None, ) -> dict[str, Any]: async def _do() -> dict[str, Any]: kwargs = {"data": body} if body is not None else {} @@ -277,18 +277,18 @@ async def _api_request( async def _api_post( - session: "aiohttp.ClientSession", *, base_url: str, endpoint: str, payload: dict[str, Any], token: Optional[str], timeout_ms: int, + session: aiohttp.ClientSession, *, base_url: str, endpoint: str, payload: dict[str, Any], token: Optional[str], timeout_ms: int, ) -> dict[str, Any]: body = json.dumps({**payload, "base_info": {"channel_version": CHANNEL_VERSION}}, ensure_ascii=False, separators=(",", ":")) return await _api_request(session, "POST", base_url=base_url, endpoint=endpoint, headers=_headers(token, body), timeout_ms=timeout_ms, body=body) -async def _api_get(session: "aiohttp.ClientSession", *, base_url: str, endpoint: str, timeout_ms: int) -> dict[str, Any]: +async def _api_get(session: aiohttp.ClientSession, *, base_url: str, endpoint: str, timeout_ms: int) -> dict[str, Any]: headers = {"iLink-App-Id": ILINK_APP_ID, "iLink-App-ClientVersion": str(ILINK_APP_CLIENT_VERSION)} return await _api_request(session, "GET", base_url=base_url, endpoint=endpoint, headers=headers, timeout_ms=timeout_ms) -async def _get_updates(session: "aiohttp.ClientSession", *, base_url: str, token: str, sync_buf: str, timeout_ms: int) -> dict[str, Any]: +async def _get_updates(session: aiohttp.ClientSession, *, base_url: str, token: str, sync_buf: str, timeout_ms: int) -> dict[str, Any]: try: return await _api_post(session, base_url=base_url, endpoint=EP_GET_UPDATES, payload={"get_updates_buf": sync_buf}, token=token, timeout_ms=timeout_ms) except TimeoutError: @@ -296,7 +296,7 @@ async def _get_updates(session: "aiohttp.ClientSession", *, base_url: str, token async def _send_items( - session: "aiohttp.ClientSession", *, base_url: str, token: str, to: str, item_list: list[dict[str, Any]], context_token: Optional[str], client_id: str, + session: aiohttp.ClientSession, *, base_url: str, token: str, to: str, item_list: list[dict[str, Any]], context_token: Optional[str], client_id: str, ) -> dict[str, Any]: message: dict[str, Any] = { "from_user_id": "", "to_user_id": to, "client_id": client_id, "message_type": MSG_TYPE_BOT, "message_state": MSG_STATE_FINISH, @@ -307,7 +307,7 @@ async def _send_items( async def _send_message( - session: "aiohttp.ClientSession", *, base_url: str, token: str, to: str, text: str, context_token: Optional[str], client_id: str, + session: aiohttp.ClientSession, *, base_url: str, token: str, to: str, text: str, context_token: Optional[str], client_id: str, ) -> dict[str, Any]: if not text or not text.strip(): raise ValueError("_send_message: text must not be empty") @@ -315,7 +315,7 @@ async def _send_message( return await _send_items(session, base_url=base_url, token=token, to=to, item_list=item_list, context_token=context_token, client_id=client_id) -async def _get_config(session: "aiohttp.ClientSession", *, base_url: str, token: str, user_id: str, context_token: Optional[str]) -> dict[str, Any]: +async def _get_config(session: aiohttp.ClientSession, *, base_url: str, token: str, user_id: str, context_token: Optional[str]) -> dict[str, Any]: payload: dict[str, Any] = {"ilink_user_id": user_id} if context_token: payload["context_token"] = context_token @@ -323,7 +323,7 @@ async def _get_config(session: "aiohttp.ClientSession", *, base_url: str, token: async def _get_upload_url( - session: "aiohttp.ClientSession", *, base_url: str, token: str, to_user_id: str, media_type: int, filekey: str, rawsize: int, + session: aiohttp.ClientSession, *, base_url: str, token: str, to_user_id: str, media_type: int, filekey: str, rawsize: int, rawfilemd5: str, filesize: int, aeskey_hex: str, ) -> dict[str, Any]: payload = { @@ -332,7 +332,7 @@ async def _get_upload_url( return await _api_post(session, base_url=base_url, endpoint=EP_GET_UPLOAD_URL, payload=payload, token=token, timeout_ms=API_TIMEOUT_MS) -async def _upload_ciphertext(session: "aiohttp.ClientSession", *, ciphertext: bytes, upload_url: str) -> str: +async def _upload_ciphertext(session: aiohttp.ClientSession, *, ciphertext: bytes, upload_url: str) -> str: async def _do() -> str: async with session.post(upload_url, data=ciphertext, headers={"Content-Type": "application/octet-stream"}) as response: encrypted_param = response.headers.get("x-encrypted-param") if response.status == 200 else None @@ -344,7 +344,7 @@ async def _upload_ciphertext(session: "aiohttp.ClientSession", *, ciphertext: by return await asyncio.wait_for(_do(), timeout=120) -async def _download_bytes(session: "aiohttp.ClientSession", *, url: str, timeout_seconds: float = 60.0) -> bytes: +async def _download_bytes(session: aiohttp.ClientSession, *, url: str, timeout_seconds: float = 60.0) -> bytes: async def _do() -> bytes: async with session.get(url) as response: response.raise_for_status() @@ -369,7 +369,7 @@ def _assert_weixin_cdn_url(url: str) -> None: async def _download_and_decrypt_media( - session: "aiohttp.ClientSession", *, cdn_base_url: str, encrypted_query_param: Optional[str], aes_key_b64: Optional[str], + session: aiohttp.ClientSession, *, cdn_base_url: str, encrypted_query_param: Optional[str], aes_key_b64: Optional[str], full_url: Optional[str], timeout_seconds: float, ) -> bytes: if encrypted_query_param: @@ -571,7 +571,7 @@ def _save_sync_buf(hermes_home: str, account_id: str, sync_buf: str) -> None: atomic_json_write(_account_dir(hermes_home) / f"{account_id}.sync.json", {"get_updates_buf": sync_buf}) -async def _fetch_qr(session: "aiohttp.ClientSession", bot_type: str) -> tuple[str, str]: +async def _fetch_qr(session: aiohttp.ClientSession, bot_type: str) -> tuple[str, str]: qr_resp = await _api_get(session, base_url=ILINK_BASE_URL, endpoint=f"{EP_GET_BOT_QR}?bot_type={bot_type}", timeout_ms=QR_TIMEOUT_MS) return str(qr_resp.get("qrcode") or ""), str(qr_resp.get("qrcode_img_content") or "") diff --git a/gateway/platforms/whatsapp_cloud.py b/gateway/platforms/whatsapp_cloud.py index 6c3eeafc14..030349dc4d 100644 --- a/gateway/platforms/whatsapp_cloud.py +++ b/gateway/platforms/whatsapp_cloud.py @@ -205,18 +205,18 @@ class WhatsAppCloudAdapter(WhatsAppBehaviorMixin, BasePlatformAdapter): self._group_allow_from: set[str] = self._normalize_allow_ids(self._coerce_allow_list(raw_groups)) self._mention_patterns = self._compile_mention_patterns() # Webhook dedup state (in-memory, FIFO-evicted) and counters. - self._seen_wamids: "OrderedDict[str, bool]" = OrderedDict() + self._seen_wamids: OrderedDict[str, bool] = OrderedDict() self._duplicate_count = self._accepted_count = self._rejected_signature_count = 0 self._warned_no_ffmpeg: bool = False # Latest inbound wamid per chat: Meta's typing/read-receipt API needs a # message_id to attach to, and the base send_typing contract has none. - self._last_inbound_wamid_by_chat: "OrderedDict[str, str]" = OrderedDict() + self._last_inbound_wamid_by_chat: OrderedDict[str, str] = OrderedDict() # Interactive-button state: short id (in the button payload) → session_key for # the gateway resolver. Popped on tap; FIFO-capped via bounded_put so ignored # prompts don't accumulate (an evicted tap degrades to text fallback). - self._clarify_state: "OrderedDict[str, str]" = OrderedDict() - self._exec_approval_state: "OrderedDict[str, str]" = OrderedDict() - self._slash_confirm_state: "OrderedDict[str, str]" = OrderedDict() + self._clarify_state: OrderedDict[str, str] = OrderedDict() + self._exec_approval_state: OrderedDict[str, str] = OrderedDict() + self._slash_confirm_state: OrderedDict[str, str] = OrderedDict() self._runner = self._http_client = None # ------------------------------------------------------------------ helpers @@ -417,7 +417,7 @@ class WhatsAppCloudAdapter(WhatsAppBehaviorMixin, BasePlatformAdapter): # ------------------------------------------------------------------ interactive messages async def _send_interactive( self, chat_id: str, interactive: dict[str, Any], metadata: Optional[dict[str, Any]], - state: "OrderedDict[str, str]", state_id: str, session_key: str, + state: OrderedDict[str, str], state_id: str, session_key: str, ) -> SendResult: """POST an ``interactive`` message (caller supplies ``type``/``body``/``action``) and, on success, remember ``state_id → session_key`` for the tap. Free-form interactives need no @@ -692,7 +692,7 @@ class WhatsAppCloudAdapter(WhatsAppBehaviorMixin, BasePlatformAdapter): return str(out_path), mime or None # ------------------------------------------------------------------ inbound - async def _handle_health(self, request: "web.Request") -> "web.Response": + async def _handle_health(self, request: web.Request) -> web.Response: return web.json_response({ "status": "ok", "platform": self.platform.value, "phone_number_id": self._phone_number_id, "webhook_path": self._webhook_path, "verify_token_configured": bool(self._verify_token), @@ -701,7 +701,7 @@ class WhatsAppCloudAdapter(WhatsAppBehaviorMixin, BasePlatformAdapter): "rejected_signature": self._rejected_signature_count, }) - async def _handle_verify(self, request: "web.Request") -> "web.Response": + async def _handle_verify(self, request: web.Request) -> web.Response: """Meta subscription handshake: echo ``hub.challenge`` iff mode is ``subscribe`` and ``hub.verify_token`` matches (constant-time).""" if not self._verify_token: @@ -717,7 +717,7 @@ class WhatsAppCloudAdapter(WhatsAppBehaviorMixin, BasePlatformAdapter): return web.Response(status=400, text="missing challenge") return web.Response(text=q["hub.challenge"], content_type="text/plain") - async def _handle_webhook(self, request: "web.Request") -> "web.Response": + async def _handle_webhook(self, request: web.Request) -> web.Response: """Inbound webhook POST: raw bytes → HMAC verify → JSON → dispatch. Signature is over the raw body, so JSON parsing must come after verification. Always 200 once a valid request is ack'd — Meta retries non-200 for up to 7 days and would multiply agent work.""" @@ -855,7 +855,7 @@ class WhatsAppCloudAdapter(WhatsAppBehaviorMixin, BasePlatformAdapter): @staticmethod def _pop_tap_state( - state: "OrderedDict[str, str]", key: str, stale_log: str, choice: str = "", valid: tuple = (), + state: OrderedDict[str, str], key: str, stale_log: str, choice: str = "", valid: tuple = (), ) -> Optional[str]: """Pop the session_key for a tapped prompt. None (info-logged) when nothing is live — likely a stale tap; an unrecognised ``choice`` keeps the prompt live and also yields None.""" diff --git a/gateway/platforms/yuanbao.py b/gateway/platforms/yuanbao.py index c7559d2a6d..fa0008a318 100644 --- a/gateway/platforms/yuanbao.py +++ b/gateway/platforms/yuanbao.py @@ -337,24 +337,24 @@ class InboundPipeline: return name_or_mw.name, name_or_mw return name_or_mw, handler - def use(self, name_or_mw, handler=None, when=None) -> "InboundPipeline": + def use(self, name_or_mw, handler=None, when=None) -> InboundPipeline: """Append ``pipeline.use(SomeMiddleware())`` or ``pipeline.use("name", fn)``.""" return self._insert_relative(None, 0, name_or_mw, handler, when) - def _insert_relative(self, target: Optional[str], offset: int, name_or_mw, handler, when) -> "InboundPipeline": + def _insert_relative(self, target: Optional[str], offset: int, name_or_mw, handler, when) -> InboundPipeline: """Insert at index(target)+offset; appends when *target* is None or not registered.""" name, h = self._normalize(name_or_mw, handler) idx = next((i for i, (n, _, _) in enumerate(self._middlewares) if n == target), None) self._middlewares.insert(len(self._middlewares) if idx is None else idx + offset, (name, h, when)) return self - def use_before(self, target: str, name_or_mw, handler=None, when=None) -> "InboundPipeline": + def use_before(self, target: str, name_or_mw, handler=None, when=None) -> InboundPipeline: return self._insert_relative(target, 0, name_or_mw, handler, when) - def use_after(self, target: str, name_or_mw, handler=None, when=None) -> "InboundPipeline": + def use_after(self, target: str, name_or_mw, handler=None, when=None) -> InboundPipeline: return self._insert_relative(target, 1, name_or_mw, handler, when) - def remove(self, name: str) -> "InboundPipeline": + def remove(self, name: str) -> InboundPipeline: self._middlewares = [(n, h, w) for n, h, w in self._middlewares if n != name] return self @@ -1721,7 +1721,7 @@ class DispatchMiddleware(InboundMiddleware): task.add_done_callback(adapter._inbound_tasks.discard) @staticmethod - async def _consume_group_queue(adapter: "YuanbaoAdapter", session_key: str) -> None: + async def _consume_group_queue(adapter: YuanbaoAdapter, session_key: str) -> None: """Drain the group queue one dispatch at a time, waiting for each to finish; exits after 2s idle.""" queue = adapter._group_queues.get(session_key) if not queue: @@ -1766,7 +1766,7 @@ class ConnectionManager: _DEBOUNCE_WINDOW: float = 1.5 # seconds to wait for companion frames of a multi-part message _LOOPS = (("_heartbeat_task", "_heartbeat_loop", "heartbeat"), ("_recv_task", "_receive_loop", "recv")) - def __init__(self, adapter: "YuanbaoAdapter") -> None: + def __init__(self, adapter: YuanbaoAdapter) -> None: self._adapter = adapter self._ws = None # websockets connection self._connect_id: Optional[str] = None @@ -2173,15 +2173,15 @@ class MediaSendHandler(ABC): return True @abstractmethod - async def acquire_file(self, adapter: "YuanbaoAdapter", **kwargs: Any) -> tuple[bytes, str, str]: + async def acquire_file(self, adapter: YuanbaoAdapter, **kwargs: Any) -> tuple[bytes, str, str]: """(file_bytes, filename, content_type); raise ValueError when unobtainable.""" @abstractmethod def build_msg_body(self, upload_result: dict, **kwargs: Any) -> list: """Platform-specific MsgBody list from the COS upload result.""" - async def handle(self, adapter: "YuanbaoAdapter", chat_id: str, reply_to: Optional[str] = None, - caption: Optional[str] = None, **kwargs: Any) -> "SendResult": + async def handle(self, adapter: YuanbaoAdapter, chat_id: str, reply_to: Optional[str] = None, + caption: Optional[str] = None, **kwargs: Any) -> SendResult: if adapter._connection.ws is None: return SendResult(success=False, error="Not connected", retryable=True) adapter._outbound.slow_notifier.cancel(chat_id) @@ -2277,7 +2277,7 @@ class StickerHandler(MediaSendHandler): class HeartbeatManager: """Reply heartbeat lifecycle: RUNNING every 2s, auto-FINISH after 30s idle, explicit stop.""" - def __init__(self, adapter: "YuanbaoAdapter") -> None: + def __init__(self, adapter: YuanbaoAdapter) -> None: self._adapter = adapter self._reply_heartbeat_tasks: dict[str, asyncio.Task] = {} self._reply_hb_last_active: dict[str, float] = {} @@ -2347,7 +2347,7 @@ class HeartbeatManager: class SlowResponseNotifier: """Per-chat timer that sends a courtesy 'please wait' after SLOW_RESPONSE_TIMEOUT_S without a reply.""" - def __init__(self, adapter: "YuanbaoAdapter", sender: "MessageSender") -> None: + def __init__(self, adapter: YuanbaoAdapter, sender: MessageSender) -> None: self._adapter = adapter self._sender = sender self._tasks: dict[str, asyncio.Task] = {} @@ -2383,7 +2383,7 @@ class MessageSender: # @nickname bounded by whitespace / line edges _AT_USER_RE = re.compile(r'(?:(?<=\s)|(?<=^))@(\S+?)(?=\s|$)', re.MULTILINE) - def __init__(self, adapter: "YuanbaoAdapter") -> None: + def __init__(self, adapter: YuanbaoAdapter) -> None: self._adapter = adapter self._chat_locks: collections.OrderedDict[str, asyncio.Lock] = collections.OrderedDict() self._media_handlers: dict[str, MediaSendHandler] = { @@ -2400,7 +2400,7 @@ class MessageSender: self._chat_locks[chat_id] = asyncio.Lock() return self._chat_locks[chat_id] - async def send_text(self, chat_id: str, content: str, reply_to: Optional[str] = None, group_code: str = "") -> "SendResult": + async def send_text(self, chat_id: str, content: str, reply_to: Optional[str] = None, group_code: str = "") -> SendResult: """Send text with auto-chunking and per-chat-id ordering guarantee.""" adapter = self._adapter if adapter._connection.ws is None: @@ -2420,7 +2420,7 @@ class MessageSender: return SendResult(success=True) async def send_media(self, chat_id: str, handler_name: str, reply_to: Optional[str] = None, - caption: Optional[str] = None, **kwargs: Any) -> "SendResult": + caption: Optional[str] = None, **kwargs: Any) -> SendResult: handler = self._media_handlers.get(handler_name) if handler is None: return SendResult(success=False, error=f"Unknown media handler: {handler_name!r}") @@ -2429,7 +2429,7 @@ class MessageSender: async def send_direct(self, chat_id: str, message: str, media_files: Optional[list[tuple[str, bool]]] = None) -> dict[str, Any]: """send_message tool entry: text first, then each media file by extension, on the running adapter.""" adapter = self._adapter - last_result: Optional["SendResult"] = None + last_result: Optional[SendResult] = None if message.strip(): last_result = await adapter.send(chat_id, message) if not last_result.success: @@ -2443,19 +2443,19 @@ class MessageSender: return {"error": "No deliverable text or media remained after processing"} return {"success": True, "platform": "yuanbao", "chat_id": chat_id, "message_id": last_result.message_id} - async def dispatch_msg_body(self, chat_id: str, msg_body: list, reply_to: Optional[str] = None, group_code: str = "") -> "SendResult": + async def dispatch_msg_body(self, chat_id: str, msg_body: list, reply_to: Optional[str] = None, group_code: str = "") -> SendResult: """Lock + dispatch an arbitrary MsgBody to C2C or group.""" async with self.get_chat_lock(chat_id): result = await self._send_msg_body(chat_id, msg_body, reply_to, group_code) return self._to_send_result(result) @staticmethod - def _to_send_result(raw: dict) -> "SendResult": + def _to_send_result(raw: dict) -> SendResult: if raw.get("success"): return SendResult(success=True, message_id=raw.get("msg_key")) return SendResult(success=False, error=raw.get("error", "Unknown error")) - async def send_text_chunk(self, chat_id: str, text: str, reply_to: Optional[str] = None, retry: int = 3, group_code: str = "") -> "SendResult": + async def send_text_chunk(self, chat_id: str, text: str, reply_to: Optional[str] = None, retry: int = 3, group_code: str = "") -> SendResult: """Send a single text chunk with retry (exponential backoff: 1s, 2s, 4s).""" adapter = self._adapter last_error: str = "Unknown error" @@ -2532,7 +2532,7 @@ class MessageSender: ), req_id) @staticmethod - async def _dispatch_encoded(adapter: "YuanbaoAdapter", encoded: bytes, req_id: str) -> dict: + async def _dispatch_encoded(adapter: YuanbaoAdapter, encoded: bytes, req_id: str) -> dict: """Send pre-encoded bytes via WS → ``{"success", "msg_key" | "error"}``.""" try: response = await adapter._connection.send_biz_request(encoded, req_id=req_id) @@ -2579,7 +2579,7 @@ class MessageSender: class OutboundManager: """Composes MessageSender, HeartbeatManager and SlowResponseNotifier (sender cancels the notifier before a send and emits the FINISH heartbeat after).""" - def __init__(self, adapter: "YuanbaoAdapter") -> None: + def __init__(self, adapter: YuanbaoAdapter) -> None: self._adapter = adapter self.sender: MessageSender = MessageSender(adapter) self.heartbeat: HeartbeatManager = HeartbeatManager(adapter) @@ -2598,16 +2598,16 @@ class YuanbaoAdapter(BasePlatformAdapter): splits_long_messages = True # send() auto-chunks via truncate_message(MAX_TEXT_CHUNK) MEDIA_MAX_SIZE_MB: int = 50 DM_MAX_CHARS = 10000 - _active_instance: ClassVar[Optional["YuanbaoAdapter"]] = None + _active_instance: ClassVar[Optional[YuanbaoAdapter]] = None # Per Hermes home: a multiplexed gateway runs one Yuanbao adapter per profile, and the tools / # send_message read "the" adapter from inside a profile-scoped turn, so last-wins would route # profile B's sends through profile A's bot. Registration and lookup both key on the ambient # override (connect/reconnect tasks inherit the profile's Context); the slot above serves the # unscoped path. - _active_instances: ClassVar[dict[str, "YuanbaoAdapter"]] = {} + _active_instances: ClassVar[dict[str, YuanbaoAdapter]] = {} @classmethod - def get_active(cls) -> Optional["YuanbaoAdapter"]: + def get_active(cls) -> Optional[YuanbaoAdapter]: from hermes_constants import get_hermes_home_override, hermes_home_key if get_hermes_home_override() is None: @@ -2615,7 +2615,7 @@ class YuanbaoAdapter(BasePlatformAdapter): return cls._active_instances.get(hermes_home_key()) @classmethod - def set_active(cls, adapter: Optional["YuanbaoAdapter"]) -> None: + def set_active(cls, adapter: Optional[YuanbaoAdapter]) -> None: from hermes_constants import get_hermes_home_override, hermes_home_key if get_hermes_home_override() is None: diff --git a/gateway/relay/adapter.py b/gateway/relay/adapter.py index 3424756c30..7219932d93 100644 --- a/gateway/relay/adapter.py +++ b/gateway/relay/adapter.py @@ -179,7 +179,7 @@ class RelayAdapter(BasePlatformAdapter): self._revocation_monitor: Optional[asyncio.Task[None]] = None # Lazily built client for the connector's /relay/media routes; None when # dial URL or creds are absent (media lanes degrade to text fallbacks). - self._media_client: Optional["RelayMediaClient"] = None + self._media_client: Optional[RelayMediaClient] = None # prompt_id -> pending-prompt state for the interactive `prompt` op; the # user's pick comes back as a prompt_response naming this id and resolves the # waiting primitive like native button callbacks. Expire lazily (_pop_prompt). @@ -193,7 +193,7 @@ class RelayAdapter(BasePlatformAdapter): self._prompt_owner_nonce: str = secrets.token_hex(3) # Prompt ids this process already resolved, newest last (repeat answers are # consumed silently instead of treated as stale). - self._resolved_prompts: "OrderedDict[str, float]" = OrderedDict() + self._resolved_prompts: OrderedDict[str, float] = OrderedDict() # ── capability surface (from descriptor) ───────────────────────────── @property diff --git a/gateway/relay/descriptor.py b/gateway/relay/descriptor.py index 5385576df3..eb29e7b61c 100644 --- a/gateway/relay/descriptor.py +++ b/gateway/relay/descriptor.py @@ -67,7 +67,7 @@ class CapabilityDescriptor: return json.dumps(asdict(self), sort_keys=True, ensure_ascii=False) @classmethod - def from_json(cls, data: str) -> "CapabilityDescriptor": + def from_json(cls, data: str) -> CapabilityDescriptor: """Deserialize a handshake JSON string; unknown keys ignored, missing keys default. Trust-boundary normalization (malformed input never breaks the handshake): diff --git a/gateway/relay/transport.py b/gateway/relay/transport.py index 45d320ea94..7c93179bbe 100644 --- a/gateway/relay/transport.py +++ b/gateway/relay/transport.py @@ -42,7 +42,7 @@ class RelayTransport(Protocol): def set_inbound_handler(self, handler: InboundHandler) -> None: ... - def set_passthrough_handler(self, handler: "PassthroughHandler") -> None: + def set_passthrough_handler(self, handler: PassthroughHandler) -> None: """Register the callback for each forwarded passthrough request (§5.1). The connector answers the provider's edge ACK itself, then forwards the real diff --git a/gateway/restart.py b/gateway/restart.py index 3d19d3d74c..64bd1fc398 100644 --- a/gateway/restart.py +++ b/gateway/restart.py @@ -125,7 +125,7 @@ def read_launchd_exit_timeout_s( *, environ: Mapping[str, str] | None = None, uid: int | None = None, - run: Callable[..., "subprocess.CompletedProcess[str]"] = subprocess.run, + run: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run, platform: str = sys.platform, ) -> float | None: """Live ``ExitTimeOut`` (seconds) launchd enforces for this gateway's job. diff --git a/gateway/run.py b/gateway/run.py index 02d9326066..f75b987a58 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -542,7 +542,7 @@ def _is_transient_network_error(exc: BaseException) -> bool: def _gateway_loop_exception_handler( - loop: "asyncio.AbstractEventLoop", context: dict[str, Any]) -> None: + loop: asyncio.AbstractEventLoop, context: dict[str, Any]) -> None: """Loop-level safety net for transient network errors (installed once by ``start_gateway``). Logs WARNING with traceback; non-transient errors go to the default handler so real bugs surface. @@ -574,7 +574,7 @@ def _redact_gateway_user_facing_secrets(text: str) -> str: return redact_for_egress(text) -def _redact_approval_command(cmd: "str | None") -> str: +def _redact_approval_command(cmd: str | None) -> str: """Redact credentials from a command before it goes into an approval prompt. The gateway approval prompt is built from the raw command string, so a credential-shaped value would @@ -1587,7 +1587,7 @@ def _reload_runtime_env_preserving_config_authority() -> None: _bridge_max_turns_from_config(_hermes_home) -def _bridge_max_turns_from_config(home: "Path") -> None: +def _bridge_max_turns_from_config(home: Path) -> None: """Re-bridge agent.max_turns (+ sessions.*) per turn; managed overlay applies or it reverts. Skipped inside a served secondary's scope: the env slots are the launch profile's and hermes_state reads the routed profile's ``sessions.*`` from its own config under scope.""" @@ -1638,13 +1638,13 @@ class HygieneTurnHoldExceeded(Exception): must NOT take the idle-timeout path (AGENT_COMPRESSION_TIMEOUT, "no output", failure cooldown).""" -def _multiplex_profile_homes(config: object) -> list[tuple[str, "Path"]]: +def _multiplex_profile_homes(config: object) -> list[tuple[str, Path]]: """Return the authoritative profile set for one multiplex gateway config.""" from hermes_cli.profiles import profiles_to_serve return list(profiles_to_serve(multiplex=True)) -def _cron_tick_profile_homes(config: object) -> list[tuple[str, "Path"]]: +def _cron_tick_profile_homes(config: object) -> list[tuple[str, Path]]: """Profile homes the in-process ticker visits: the served set PLUS the process-active profile: ``profiles_to_serve`` lists default + every live named profile, but a ``--profile `` gateway's own profile may sit outside ``profiles/`` (custom HERMES_HOME). One host @@ -1662,7 +1662,7 @@ def _cron_tick_profile_homes(config: object) -> list[tuple[str, "Path"]]: return homes -def _cron_profile_gate(name: str, home: "Path") -> bool: +def _cron_profile_gate(name: str, home: Path) -> bool: """Tick ``home`` this cycle unless ANOTHER gateway process owns it. Same stand-down the serve/Desktop ticker applies (``hermes_cli/web_server.py``): a host that @@ -1769,7 +1769,7 @@ def _terminal_scope_cwd(default: str = "") -> str: return _ts_env("TERMINAL_CWD", default) -def _load_profile_secret_scope(profile_home: "Path") -> dict: +def _load_profile_secret_scope(profile_home: Path) -> dict: """Hydrate and load one profile's secrets under its home override.""" from hermes_constants import set_hermes_home_override, reset_hermes_home_override # Caller already hydrated external sources off-loop (#99519). @@ -1786,7 +1786,7 @@ def _load_profile_secret_scope(profile_home: "Path") -> dict: @_contextmanager def _profile_runtime_scope( - profile_home: "Path", prepared_secret_scope: Optional[dict] = None, *, + profile_home: Path, prepared_secret_scope: Optional[dict] = None, *, hydrate_secrets: bool = True): """Scope config/skills/memory AND credentials to a profile for one turn (multiplexed path only). ``set_hermes_home_override`` is a contextvar (reaches the agent worker via ``copy_context()``); @@ -1821,14 +1821,14 @@ def _profile_runtime_scope( @_asynccontextmanager -async def _async_profile_runtime_scope(profile_home: "Path"): +async def _async_profile_runtime_scope(profile_home: Path): """Enter a profile scope without loading secret files on the event loop.""" secrets = await asyncio.to_thread(_load_profile_secret_scope, Path(profile_home)) with _profile_runtime_scope(Path(profile_home), secrets): yield -def load_gateway_config_for_runner() -> "GatewayConfig": +def load_gateway_config_for_runner() -> GatewayConfig: """Load gateway config for the process-level GatewayRunner. An UNSET ``multiplex_profiles`` is settled first by ``resolve_multiplex_mode`` (the default is on; the boot guard keeps a fleet that still runs per-profile gateways standalone). Multiplexed: set multiplex-active, then reload @@ -1898,7 +1898,7 @@ async def _discover_gateway_mcp_tools(config: object) -> None: logger.warning("MCP tool discovery failed for profile '%s'", profile_name, exc_info=True) -def _platform_has_bot_credential(platform: "Platform", platform_config: "PlatformConfig") -> bool: +def _platform_has_bot_credential(platform: Platform, platform_config: PlatformConfig) -> bool: """Return True when a token-authenticated platform has a usable bot credential; platforms not using ``PlatformConfig.token`` (Signal session paths, port-binding HTTP adapters) always return True.""" from gateway.config import PLATFORM_TOKEN_ENV_NAMES, Platform @@ -2846,7 +2846,7 @@ def _check_unavailable_skill(command_name: str) -> str | None: return None -def _platform_config_key(platform: "Platform") -> str: +def _platform_config_key(platform: Platform) -> str: """Map a Platform enum to its config.yaml key (LOCAL→"cli", rest→enum value).""" return "cli" if platform == Platform.LOCAL else platform.value @@ -2863,7 +2863,7 @@ def _gateway_config_home() -> Path: return Path(override) if override else _hermes_home -def _load_gateway_config(config_path: "Path | None" = None) -> dict: +def _load_gateway_config(config_path: Path | None = None) -> dict: """The effective user config.yaml (managed overlay, ``${VAR}`` expansion, model-key canon; no DEFAULT_CONFIG merge) — ``{}`` on any error (fail-open). Defaults to the active gateway home (``_hermes_home`` monkeypatches apply); multiplexers pass a path. @@ -2953,7 +2953,7 @@ def _resolve_hermes_bin() -> Optional[list[str]]: _PROFILE_ID_KEY_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$") -def _parse_session_key(session_key: str) -> "dict | None": +def _parse_session_key(session_key: str) -> dict | None: """Parse a session key (``agent:{ns}:{platform}:{chat_type}:{chat_id}[:{extra}...]``). ``{ns}`` is ``main`` for the default profile, ``main~`` for a profile literally named ``main`` @@ -3020,7 +3020,7 @@ def _format_concise_process_notification( return text -def _format_gateway_process_notification(evt: dict) -> "str | None": +def _format_gateway_process_notification(evt: dict) -> str | None: """Format a watch pattern event from completion_queue into a [IMPORTANT:] message.""" evt_type = evt.get("type", "completion") _sid = evt.get("session_id", "unknown") @@ -3050,7 +3050,7 @@ def _format_gateway_process_notification(evt: dict) -> "str | None": return None -def _drain_gateway_watch_events(completion_queue) -> "list[dict]": +def _drain_gateway_watch_events(completion_queue) -> list[dict]: """Drain gateway-owned watch events without spinning on requeued events. Foreign events requeued inside ``while not queue.empty()`` never terminate: detach, then requeue.""" watch_events: list[dict] = [] @@ -3205,7 +3205,7 @@ def _preserve_queued_followup_history_offset( return {**followup_result, "history_offset": current_offset} -async def _dispose_unused_adapter(adapter: "BasePlatformAdapter | None") -> None: +async def _dispose_unused_adapter(adapter: BasePlatformAdapter | None) -> None: """Best-effort dispose for an adapter that never made it onto ``self.adapters`` (may be ``None``). Nothing else calls ``disconnect()`` on it, so ``__init__`` resources (e.g. SQLite fds) would leak until GC (not prompt for asyncio-bound objects) and exhaust the fd ulimit over a long retry loop. @@ -3403,7 +3403,7 @@ class GatewayRunner( _pending_approvals = legacy_dict_property("_pending_approvals") _update_prompt_pending = legacy_dict_property("_update_prompt_pending") - def _sessions_map(self) -> dict[str, "SessionState"]: + def _sessions_map(self) -> dict[str, SessionState]: """Per-session state map; lazily created so bare ``object.__new__`` test runners work.""" sessions = self.__dict__.get("_sessions") if sessions is None: @@ -3411,7 +3411,7 @@ class GatewayRunner( self.__dict__["_sessions"] = sessions return sessions - def _session_state(self, session_key: str) -> "SessionState": + def _session_state(self, session_key: str) -> SessionState: """Get-or-create the :class:`SessionState` for ``session_key``.""" sessions = self._sessions_map() state = sessions.get(session_key) @@ -3420,7 +3420,7 @@ class GatewayRunner( sessions[session_key] = state return state - def _peek_session_state(self, session_key: str) -> Optional["SessionState"]: + def _peek_session_state(self, session_key: str) -> Optional[SessionState]: """Return the SessionState for ``session_key`` without creating one.""" sessions = self.__dict__.get("_sessions") return sessions.get(session_key) if sessions else None @@ -3437,13 +3437,13 @@ class GatewayRunner( # Loop-liveness / watchdog handles; class-level defaults so partially constructed test runners work. # Class-level defaults so partial construction in tests doesn't blow up on access; the real values are # set in __init__ / start() / stop(). See #66892, #69089. - _loop_heartbeat_task: Optional["asyncio.Task"] = None + _loop_heartbeat_task: Optional[asyncio.Task] = None _loop_floor_timer_handle: Optional[Any] = None _loop_liveness_watchdog: Optional[Any] = None _gateway_started_at: float = 0.0 - _shutdown_watchdog_done: Optional["threading.Event"] = None + _shutdown_watchdog_done: Optional[threading.Event] = None _platform_lock_takeover_on_start: bool = False - _reconnect_watcher_task: Optional["asyncio.Task"] = None + _reconnect_watcher_task: Optional[asyncio.Task] = None def __init__(self, config: Optional[GatewayConfig] = None): global _gateway_runner_ref @@ -3591,13 +3591,13 @@ class GatewayRunner( self._platform_lock_takeover_on_start = False # Capped LRU of live SessionSources for fallback routing (shutdown notices, synthetic events) when # the persisted origin is missing and _parse_session_key can't recover thread_id. - self._session_sources: "OrderedDict[str, SessionSource]" = OrderedDict() + self._session_sources: OrderedDict[str, SessionSource] = OrderedDict() self._session_sources_max = 512 # Lifecycle-scoped completion dedup: closes queue/watcher races inside one gateway without claiming # exactly-once across a crash; durable replay state stays owned by tools.async_delegation. self._completion_delivery_lock = threading.Lock() self._completion_deliveries_inflight: set[tuple[str, str, object]] = set() - self._completion_deliveries_delivered: "OrderedDict[tuple[str, str, object], None]" = OrderedDict() + self._completion_deliveries_delivered: OrderedDict[tuple[str, str, object], None] = OrderedDict() self._completion_delivery_retention = 2048 # Agent-triggered terminal completions from one conversation often land in the same scheduler # tick; hold them briefly so the agent gets one synthetic turn instead of one per process. @@ -3612,7 +3612,7 @@ class GatewayRunner( """Agent cache, profile identity, Teams runtime, failed-platform tracking, slash-confirm counter.""" # AIAgent per session preserves prompt caching (fresh agent per message ~10x cost on Anthropic). # Value: (AIAgent, config_signature); LRU cap in _enforce_agent_cache_cap, TTL in expiry watcher. - self._agent_cache: "OrderedDict[str, tuple]" = OrderedDict() + self._agent_cache: OrderedDict[str, tuple] = OrderedDict() self._agent_cache_lock = threading.Lock() # Launch-time identity of the profile that owns ``self.adapters``; ``_authorization_adapter`` # compares against this rather than the per-turn ``_active_profile_name()``. A multiplex @@ -3685,7 +3685,7 @@ class GatewayRunner( from gateway.pairing import PairingStore from gateway.hooks import ProfileHookRegistries self.pairing_store = PairingStore() - self.pairing_stores: dict[str, "PairingStore"] = {} + self.pairing_stores: dict[str, PairingStore] = {} # One HookRegistry per served profile home, resolved from the active scope at emit time. self.hooks = ProfileHookRegistries() # Per-chat voice reply mode: "off" | "voice_only" | "all" @@ -4432,7 +4432,7 @@ class GatewayRunner( getattr(source, "thread_id", None), getattr(source, "parent_chat_id", None)) return None - def _resolve_profile_home_for_source(self, source: SessionSource) -> "Path": + def _resolve_profile_home_for_source(self, source: SessionSource) -> Path: """Resolve which profile's HERMES_HOME serves this source: the pinned identity's runtime home, else ``source.profile``, then ``_profile_name_for_source`` (sources bypassing ``build_source``), then the active profile.""" diff --git a/gateway/run_adapters.py b/gateway/run_adapters.py index 2afc4b47f7..4e19825717 100644 --- a/gateway/run_adapters.py +++ b/gateway/run_adapters.py @@ -71,7 +71,7 @@ class GatewayAdapterLifecycleMixin: """Adapter lifecycle: connect/teardown, fatal recovery, reconnect watcher, multiplex profiles.""" @staticmethod - async def _wait_or_detach(task: "asyncio.Future", timeout: float) -> bool: + async def _wait_or_detach(task: asyncio.Future, timeout: float) -> bool: """Wait up to ``timeout`` for ``task``; on deadline (or our own cancellation) detach it. Not ``asyncio.wait_for``: that WAITS for the cancelled child, so a connect()/close() swallowing ``CancelledError`` blocks recovery forever. True if it finished in time.""" @@ -427,7 +427,7 @@ class GatewayAdapterLifecycleMixin: "retry in background.", len(self._failed_platforms), ) - def _retain_background_task(self, task: "asyncio.Task") -> "asyncio.Task": + def _retain_background_task(self, task: asyncio.Task) -> asyncio.Task: """Register ``task`` in ``_background_tasks`` (created lazily for bare test runners).""" tasks = getattr(self, "_background_tasks", None) if not isinstance(tasks, set): @@ -437,7 +437,7 @@ class GatewayAdapterLifecycleMixin: return task @staticmethod - def _track_task_in(tasks: set, task: "asyncio.Task") -> "asyncio.Task": + def _track_task_in(tasks: set, task: asyncio.Task) -> asyncio.Task: """Register ``task`` in an arbitrary lifecycle set with self-removal on completion.""" tasks.add(task) task.add_done_callback(tasks.discard) @@ -541,7 +541,7 @@ class GatewayAdapterLifecycleMixin: except Exception: _process_takes_profile = False # In-flight dispatches by session id: a handoff is a FULL agent turn, so never process inline. - inflight: dict[str, "asyncio.Task"] = {} + inflight: dict[str, asyncio.Task] = {} async def _dispatch(row, session_id, session_db, profile_name) -> None: """Run one claimed handoff to a terminal state, off the poll path.""" @@ -1061,7 +1061,7 @@ class GatewayAdapterLifecycleMixin: from hermes_constants import get_hermes_home publish_record(ROLE_GATEWAY, profiles=tuple(served), home=str(get_hermes_home())) - async def _load_secondary_profile_config(self, profile_name: str, profile_home: "Path"): + async def _load_secondary_profile_config(self, profile_name: str, profile_home: Path): """Hydrate + enter ``profile_home``'s scope once; return its gateway config. Raises ``MultiplexConfigError`` (open dm/group policy). Port-binding platforms are NOT refused: the default profile owns the single shared listener and a secondary's port-binders are built in @@ -1247,7 +1247,7 @@ class GatewayAdapterLifecycleMixin: return lines async def _start_one_profile_adapters( - self, profile_name: str, profile_home: "Path", claimed: dict[tuple, str] + self, profile_name: str, profile_home: Path, claimed: dict[tuple, str] ) -> int: """Create+connect one profile's adapters under its runtime scope.""" from gateway.run import _platform_has_bot_credential, _profile_runtime_scope diff --git a/gateway/run_busy.py b/gateway/run_busy.py index 4dc6b09992..e4e1b5d2f7 100644 --- a/gateway/run_busy.py +++ b/gateway/run_busy.py @@ -102,7 +102,7 @@ class GatewayBusySessionMixin: state = self._peek_session_state(session_key) return state.conversation.queued_events if state else None - def _enqueue_fifo(self, session_key: str, queued_event: "MessageEvent", adapter: Any) -> None: + def _enqueue_fifo(self, session_key: str, queued_event: MessageEvent, adapter: Any) -> None: """Append a /queue event to the FIFO chain for a session.""" pending_slot = getattr(adapter, "_pending_messages", None) if adapter is not None else None if pending_slot is None: @@ -114,8 +114,8 @@ class GatewayBusySessionMixin: queued_event._gateway_accepted = True def _promote_queued_event( - self, session_key: str, adapter: Any, pending_event: Optional["MessageEvent"] - ) -> Optional["MessageEvent"]: + self, session_key: str, adapter: Any, pending_event: Optional[MessageEvent] + ) -> Optional[MessageEvent]: """Promote the next overflow item after the slot drained. ``pending_event`` None → the overflow head becomes the pending event; otherwise the head is @@ -138,7 +138,7 @@ class GatewayBusySessionMixin: depth += 1 return depth - def _rescue_orphaned_overflow(self, session_key: str, adapter: Any) -> Optional["MessageEvent"]: + def _rescue_orphaned_overflow(self, session_key: str, adapter: Any) -> Optional[MessageEvent]: """Pop the oldest orphaned FIFO overflow event for an idle session (None if nothing to rescue). ``queued_events`` drains only at the post-turn promotion site; a busy window ending without @@ -582,7 +582,7 @@ class GatewayBusySessionMixin: async def _resolve_busy_steer_or_redirect( self, event: MessageEvent, session_key: str, effective_mode: str, running_agent: Any - ) -> "GatewayRunner._BusySteerOutcome": + ) -> GatewayRunner._BusySteerOutcome: """Apply interrupt->queue demotions, then attempt steer (steer mode) or redirect (interrupt mode).""" from gateway.run import _AGENT_PENDING_SENTINEL # Steer injects mid-run via running_agent.steer(), falling back to queue (nothing lost) when @@ -1287,7 +1287,7 @@ class GatewayBusySessionMixin: async def _maybe_confirm_destructive_slash( self, *, event: MessageEvent, command: str, title: str, detail: str, execute - ) -> str | "EphemeralReply" | None: + ) -> str | EphemeralReply | None: """Gate a destructive session slash command (/new, /reset, /undo). ``execute()`` (async → str | EphemeralReply) runs immediately when diff --git a/gateway/run_goals.py b/gateway/run_goals.py index 64519f2c4b..7d262716a0 100644 --- a/gateway/run_goals.py +++ b/gateway/run_goals.py @@ -56,7 +56,7 @@ class GatewayGoalsMixin: except Exception as exc: logger.warning("%s: session DB warm-up failed: %s", label, exc) - async def _session_entry_for_manager(self, event: "MessageEvent", label: str): + async def _session_entry_for_manager(self, event: MessageEvent, label: str): """Session entry for a /goal or /heartbeat manager, or None when lookup fails. Warms the SessionDB cache first (a cold cache drops the first write while the reply claims it was set). Internal events never touch activity (idle/daily reset clock).""" @@ -70,7 +70,7 @@ class GatewayGoalsMixin: return None return session_entry if getattr(session_entry, "session_id", None) else None - async def _manager_for_event(self, event: "MessageEvent", kind: str, load): + async def _manager_for_event(self, event: MessageEvent, kind: str, load): """``(manager, session_entry)`` for *kind* ("goal"/"heartbeat"), or ``(None, None)``. ``load()`` imports the manager class and returns a ``session_id -> manager`` factory.""" try: @@ -83,7 +83,7 @@ class GatewayGoalsMixin: return None, None return factory(session_entry.session_id), session_entry - async def _get_goal_manager_for_event(self, event: "MessageEvent"): + async def _get_goal_manager_for_event(self, event: MessageEvent): """Return ``(GoalManager, session_entry)`` for this event, or ``(None, None)``.""" def _load(): from hermes_cli.goals import GoalManager @@ -91,7 +91,7 @@ class GatewayGoalsMixin: return lambda sid: GoalManager(session_id=sid, default_max_turns=max_turns) return await self._manager_for_event(event, "goal", _load) - async def _get_heartbeat_manager_for_event(self, event: "MessageEvent"): + async def _get_heartbeat_manager_for_event(self, event: MessageEvent): """Return ``(HeartbeatManager, session_entry)`` for this event, or ``(None, None)``.""" def _load(): from hermes_cli.heartbeat import HeartbeatManager diff --git a/gateway/run_inbound.py b/gateway/run_inbound.py index 65ba2b9b2d..888d40e2bb 100644 --- a/gateway/run_inbound.py +++ b/gateway/run_inbound.py @@ -71,8 +71,8 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): """Inbound message pipeline (_handle_message, text/media preparation, durable-turn markers, plugin injection) for GatewayRunner.""" async def _hm_pre_gateway_dispatch_hook( - self, event: "MessageEvent", source: SessionSource - ) -> Optional["MessageEvent"]: + self, event: MessageEvent, source: SessionSource + ) -> Optional[MessageEvent]: """Run the ``pre_gateway_dispatch`` plugin hook; None = drop, else the (maybe rewritten) event. Results: ``{"action": "skip"}`` → drop; ``{"action": "rewrite", "text"}`` → replace ``event.text``; ``allow``/None → normal dispatch. Runs BEFORE auth so plugins can handle unauthorized senders.""" @@ -165,8 +165,8 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): await notifier.notify(self, source, hint) async def _hm_admit_event( - self, event: "MessageEvent" - ) -> Optional[tuple["MessageEvent", SessionSource, bool]]: + self, event: MessageEvent + ) -> Optional[tuple[MessageEvent, SessionSource, bool]]: """Ingress gates for ``_handle_message``; None when dropped, else ``(event, source, is_internal)`` (the ``pre_gateway_dispatch`` hook may have rewritten ``event``).""" from gateway.run import _is_slack_ignored_channel @@ -260,7 +260,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): return None return event, source, False - def _hm_estop_turn_allowed(self, event: "MessageEvent", source: SessionSource) -> bool: + def _hm_estop_turn_allowed(self, event: MessageEvent, source: SessionSource) -> bool: """Whether a turn may bypass the global emergency stop: pause blocks NEW agent turns, never running work or control traffic — recognized slash commands (incl. /pause off, the in-band resume) and replies owned by in-flight work (pending update prompt, running session, @@ -288,7 +288,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): return False def _hm_estop_gate( - self, event: "MessageEvent", source: SessionSource, is_internal: bool + self, event: MessageEvent, source: SessionSource, is_internal: bool ) -> Optional[str]: """Global emergency-stop (`hermes pause`) notice when this turn must be blocked, else None. Placed after auth so unauthorized senders can't probe pause state. A synthetic heartbeat that @@ -327,7 +327,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): return str(e) return None - def _hm_update_prompt_reply(self, event: "MessageEvent", _quick_key: str) -> Optional[str]: + def _hm_update_prompt_reply(self, event: MessageEvent, _quick_key: str) -> Optional[str]: """Consume a reply to a pending ``/update`` prompt (routed to the detached update process via ``.update_response``); None when nothing was consumed. Recognized slash commands must bypass this or /new, /help etc. get silently consumed as update answers.""" @@ -373,7 +373,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): return None async def _hm_clarify_reply( - self, event: "MessageEvent", source: SessionSource, _quick_key: str + self, event: MessageEvent, source: SessionSource, _quick_key: str ) -> Optional[str]: """Intercept a reply to a pending clarify prompt; None when the message falls through. Free text answers open-ended/"Other" prompts; "2" answers a multi-choice one. Resolved/retained @@ -468,7 +468,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): choices.setdefault(word, choice) return choices - async def _hm_slash_confirm_reply(self, event: "MessageEvent", _quick_key: str) -> Optional[str]: + async def _hm_slash_confirm_reply(self, event: MessageEvent, _quick_key: str) -> Optional[str]: """Resolve a reply (/approve, /always, /cancel + aliases) to a pending slash-confirm prompt; None when it falls through — a stale pending confirm does NOT block other commands. A pending dangerous-command approval takes precedence: /approve there unblocks the waiting tool thread.""" @@ -581,7 +581,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): self._drop_turn_slot(_quick_key, run_generation=_generation_at_interrupt) def _hm_merge_pending_for_source( - self, source: SessionSource, _quick_key: str, event: "MessageEvent", *, merge_text: bool = False + self, source: SessionSource, _quick_key: str, event: MessageEvent, *, merge_text: bool = False ) -> None: """Merge *event* into the source adapter's pending slot (no-op without an adapter).""" from gateway.platforms.base import merge_pending_message_event @@ -590,7 +590,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): merge_pending_message_event(adapter._pending_messages, _quick_key, event, merge_text=merge_text) async def _hm_busy_slash_or_photo( - self, event: "MessageEvent", source: SessionSource, _quick_key: str + self, event: MessageEvent, source: SessionSource, _quick_key: str ) -> tuple[bool, Optional[str]]: """Slash-command / photo-burst handling on the busy fast-path → ``(handled, result)``. Each command's mid-run behavior is declared on its CommandDef (busy_policy / busy_handler).""" @@ -622,7 +622,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): return False, None def _hm_busy_telegram_grace_queue( - self, event: "MessageEvent", source: SessionSource, _quick_key: str, effective_busy_input_mode: str + self, event: MessageEvent, source: SessionSource, _quick_key: str, effective_busy_input_mode: str ) -> bool: """Queue a Telegram text follow-up that lands within the post-start grace window.""" _grace = float(os.getenv("HERMES_TELEGRAM_FOLLOWUP_GRACE_SECONDS", "3.0")) @@ -646,10 +646,10 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): return True @staticmethod - def _hm_text_only(event: "MessageEvent") -> bool: + def _hm_text_only(event: MessageEvent) -> bool: return event.message_type == MessageType.TEXT and not event.media_urls and not event.media_types - def _hm_busy_steer(self, event: "MessageEvent", running_agent: Any, _quick_key: str) -> None: + def _hm_busy_steer(self, event: MessageEvent, running_agent: Any, _quick_key: str) -> None: """Steer mode: inject text mid-run via ``agent.steer()``, else fall back to queue semantics.""" steer_text = (event.text or "").strip() steered = False @@ -666,7 +666,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): self._queue_or_replace_pending_event(_quick_key, event) async def _hm_busy_interrupt( - self, event: "MessageEvent", source: SessionSource, running_agent: Any, _quick_key: str + self, event: MessageEvent, source: SessionSource, running_agent: Any, _quick_key: str ) -> None: """Interrupt path: redirect text-only corrections when supported, else ``agent.interrupt()``.""" from gateway.run import _build_media_placeholder @@ -691,7 +691,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): running_agent.interrupt(_interrupt_text) async def _hm_handle_running_session_message( - self, event: "MessageEvent", source: SessionSource, _quick_key: str + self, event: MessageEvent, source: SessionSource, _quick_key: str ) -> Optional[str]: """Fast-path while this session's agent is running: interrupt by default (minimal latency); busy_input_mode queue/steer, subagent and compression protection demote to queue.""" @@ -751,7 +751,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): return qc if isinstance(qc, dict) else {} @staticmethod - def _hm_expand_alias_quick_command(event: "MessageEvent", qcmd: dict) -> Optional[str]: + def _hm_expand_alias_quick_command(event: MessageEvent, qcmd: dict) -> Optional[str]: """Rewrite ``event.text`` to an alias quick command's target; returns the new command name.""" target = (qcmd.get("target") or "").strip() if not target: @@ -762,7 +762,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): return target_command.split()[0] if target_command else target_command async def _hm_command_hooks( - self, event: "MessageEvent", source: SessionSource, _quick_key: str, command: str, canonical: str + self, event: MessageEvent, source: SessionSource, _quick_key: str, command: str, canonical: str ) -> tuple[bool, Optional[str], Optional[str]]: """Fire ``pre_command`` (observer) and ``command:`` (interceptor) hooks → ``(handled, result, new_command)`` (``new_command`` set when a handler rewrote the command). @@ -809,7 +809,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): return False, None, None async def _hm_resolve_command( - self, event: "MessageEvent", source: SessionSource, _quick_key: str + self, event: MessageEvent, source: SessionSource, _quick_key: str ) -> tuple[bool, Optional[str], Optional[str], Optional[str]]: """Resolve the slash command (aliases, access gate, hooks) → ``(handled, result, command, canonical)``; when ``handled`` the caller returns ``result`` as-is (may be None).""" @@ -1008,7 +1008,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): }) async def _hm_dispatch_canonical_command( - self, event: "MessageEvent", source: SessionSource, _quick_key: str, + self, event: MessageEvent, source: SessionSource, _quick_key: str, canonical: Optional[str], ) -> tuple[bool, Optional[str]]: """Dispatch built-in idle-path commands → ``(handled, result)``; prompt-rewriting commands @@ -1045,7 +1045,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): return t("gateway.quick_command.error", error=e) async def _hm_dispatch_quick_and_plugin_commands( - self, event: "MessageEvent", source: SessionSource, command: Optional[str] + self, event: MessageEvent, source: SessionSource, command: Optional[str] ) -> tuple[bool, Optional[str], Optional[str]]: """Drain gate, user-defined quick commands (exec/alias) and plugin slash commands → ``(handled, result, command)``; an alias quick command rewrites ``command``.""" @@ -1105,7 +1105,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): return False, None, command def _hm_bundle_slash_rewrite( - self, event: "MessageEvent", source: SessionSource, _quick_key: str, command: str + self, event: MessageEvent, source: SessionSource, _quick_key: str, command: str ) -> bool: """Rewrite ``/`` to the bundle invocation message; True when handled. Skill bundles take precedence over individual skill commands (mirrors CLI dispatch).""" @@ -1156,7 +1156,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): return t("gateway.unknown_command", command=command) def _hm_skill_slash_rewrite( - self, event: "MessageEvent", source: SessionSource, _quick_key: str, command: Optional[str] + self, event: MessageEvent, source: SessionSource, _quick_key: str, command: Optional[str] ) -> Optional[str]: """Rewrite ``/`` / ``/`` invocations into the skill prompt on ``event.text``; returns a reply string when the command is disabled/unknown/failed, else None. @@ -1217,7 +1217,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): return None async def _hm_pending_reply_intercepts( - self, event: "MessageEvent", source: SessionSource, _quick_key: str + self, event: MessageEvent, source: SessionSource, _quick_key: str ) -> Optional[str]: """Replies owned by in-flight work: pending /update prompt, clarify, slash-confirm. Only events that may control the gateway (``allow_gateway_control``) can answer them.""" @@ -1231,7 +1231,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): return _reply async def _hm_dispatch_idle_commands( - self, event: "MessageEvent", source: SessionSource, _quick_key: str + self, event: MessageEvent, source: SessionSource, _quick_key: str ) -> tuple[bool, Optional[str]]: """Idle path: resolve + dispatch slash commands; rewriting commands fall through to the agent.""" _handled, _result, command, canonical = await self._hm_resolve_command(event, source, _quick_key) @@ -1250,8 +1250,8 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): return _handled, _result def _hm_rescue_orphaned_fifo( - self, event: "MessageEvent", source: SessionSource, is_internal: bool, _quick_key: str - ) -> tuple["MessageEvent", SessionSource, bool]: + self, event: MessageEvent, source: SessionSource, is_internal: bool, _quick_key: str + ) -> tuple[MessageEvent, SessionSource, bool]: """FIFO orphan rescue: a session that went idle with a populated overflow (post-turn drain never promoted, e.g. a compression-demoted follow-up) silently orphaned those events. The oldest orphan runs as THIS turn and the incoming event is parked behind the chain. Skipped @@ -1765,7 +1765,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): state.persistent.native_image_paths = [] return paths - async def _mark_durable_active_turn(self, event: "MessageEvent", session_key: str) -> bool: + async def _mark_durable_active_turn(self, event: MessageEvent, session_key: str) -> bool: """Persist the exact resolved routing key for this running turn.""" try: token = await self.async_session_store.mark_turn_active(session_key) @@ -1780,7 +1780,7 @@ class GatewayInboundMixin(GatewayPluginInjectionMixin): event._gateway_active_turn_token = token return True - async def _clear_durable_active_turn(self, event: "MessageEvent") -> bool: + async def _clear_durable_active_turn(self, event: MessageEvent) -> bool: """Best-effort CAS clear of the marker owned by *event* (3 attempts; never blocks agent/lease release — a stale marker is bounded by the agent timeout and clean-start discard).""" session_key = getattr(event, "_gateway_active_turn_session_key", None) diff --git a/gateway/run_notifications.py b/gateway/run_notifications.py index 177d000049..bf0a9b949a 100644 --- a/gateway/run_notifications.py +++ b/gateway/run_notifications.py @@ -519,7 +519,7 @@ class GatewayNotificationsMixin: logger.debug("Skipping update notification watcher: no running event loop") @classmethod - def _update_paths(cls) -> "GatewayNotificationsMixin._UpdatePaths": + def _update_paths(cls) -> GatewayNotificationsMixin._UpdatePaths: from gateway.run import _hermes_home return cls._UpdatePaths( pending=_hermes_home / ".update_pending.json", @@ -560,7 +560,7 @@ class GatewayNotificationsMixin: now = datetime.now(stamped.tzinfo) if stamped.tzinfo else datetime.now() return (now - stamped).total_seconds() - def _resolve_update_target(self, paths: "_UpdatePaths") -> Optional["_UpdateTarget"]: + def _resolve_update_target(self, paths: _UpdatePaths) -> Optional[_UpdateTarget]: """Resolve adapter/chat/session for update watcher messages from the pending marker.""" for path in (paths.claimed, paths.pending): if not path.exists(): @@ -592,7 +592,7 @@ class GatewayNotificationsMixin: reply_to_message_id=data.get("message_id"), adapter=adapter, ) - async def _watch_update_completion_only(self, paths: "_UpdatePaths", deadline: float, poll_interval: float) -> None: + async def _watch_update_completion_only(self, paths: _UpdatePaths, deadline: float, poll_interval: float) -> None: """Fallback when no adapter/chat can be resolved: wait for the exit code, then notify.""" logger.warning("Update watcher: cannot resolve adapter/chat_id, falling back to completion-only") # Poll until _send_update_notification delivers (it returns False while the platform reconnects). @@ -606,7 +606,7 @@ class GatewayNotificationsMixin: await self._send_update_notification() @staticmethod - def _update_exit_code(paths: "_UpdatePaths") -> int: + def _update_exit_code(paths: _UpdatePaths) -> int: return int(paths.exit_code.read_text(encoding="utf-8-sig").strip() or "1") @staticmethod @@ -620,7 +620,7 @@ class GatewayNotificationsMixin: return "", len(data) return data[offset:].decode("utf-8", errors="replace"), len(data) - async def _send_update_output(self, target: "_UpdateTarget", text: str) -> None: + async def _send_update_output(self, target: _UpdateTarget, text: str) -> None: """Send buffered update output as fenced chunks that fit message limits (Telegram: 4096).""" from tools.ansi_strip import strip_ansi clean = strip_ansi(text).strip() @@ -631,7 +631,7 @@ class GatewayNotificationsMixin: with _log_suppressed(logging.DEBUG, "Update stream send failed: %s"): await target.send(f"```\n{clean[i:i + max_chunk]}\n```") - async def _forward_update_prompt(self, target: "_UpdateTarget", prompt_text: str, default: str) -> None: + async def _forward_update_prompt(self, target: _UpdateTarget, prompt_text: str, default: str) -> None: """Forward an update prompt: platform-native buttons first (Discord, Telegram), else text.""" sent_buttons = False adapter = target.adapter @@ -650,7 +650,7 @@ class GatewayNotificationsMixin: self._session_state(target.session_key).persistent.update_prompt_pending = True logger.info("Forwarded update prompt to %s: %s", target.session_key, prompt_text[:80]) - def _clear_update_markers(self, paths: "_UpdatePaths", session_key: Optional[str]) -> None: + def _clear_update_markers(self, paths: _UpdatePaths, session_key: Optional[str]) -> None: paths.unlink_all() state = self._peek_session_state(session_key) if state is not None: @@ -1480,7 +1480,7 @@ class GatewayNotificationsMixin: return False return True - async def _preflight_completion_delivery(self, evt: dict) -> "_CompletionClaim": + async def _preflight_completion_delivery(self, evt: dict) -> _CompletionClaim: """Claim the durable row (async delegations) and verify the target before adapter acceptance. Adapter acceptance is not proof of delivery: the inner resolver can still fail closed inside diff --git a/gateway/run_profile_reconcile.py b/gateway/run_profile_reconcile.py index 66fa0df87a..c22e839725 100644 --- a/gateway/run_profile_reconcile.py +++ b/gateway/run_profile_reconcile.py @@ -34,7 +34,7 @@ _PROFILE_SIGNATURE_FILES = ("config.yaml", ".env") _OWN_GATEWAY_PROBE_TIMEOUT_SECS = 5.0 -def profile_serve_signature(home: "Path") -> tuple: +def profile_serve_signature(home: Path) -> tuple: """Cheap change detector for a served profile's credentials/config: file signature per file.""" sig = [] for name in _PROFILE_SIGNATURE_FILES: @@ -49,7 +49,7 @@ def profile_serve_signature(home: "Path") -> tuple: class GatewayProfileReconcileMixin: """Runtime reconciliation of the multiplexed served-profile set (hot add / unroute / credential-add).""" - _served_profile_homes: Optional[dict[str, "Path"]] = None + _served_profile_homes: Optional[dict[str, Path]] = None _served_profile_signatures: Optional[dict[str, tuple]] = None _profile_reconcile_lock: Optional[asyncio.Lock] = None _profile_own_gateway_warned: Optional[set[str]] = None @@ -239,7 +239,7 @@ class GatewayProfileReconcileMixin: except Exception: logger.warning("MCP tool discovery failed for profile '%s'", profile_name, exc_info=True) - async def _unserve_profile(self, name: str, home: "Path") -> None: + async def _unserve_profile(self, name: str, home: Path) -> None: """Stop and unroute one profile: cancel its reconnects, tear down its adapters, drop its bookkeeping and release this process's handles into its home so the deleter's rmtree succeeds. The releasing process is not always the deleter (#130244): under multiplexing THIS gateway diff --git a/gateway/run_shutdown.py b/gateway/run_shutdown.py index e04dfc23ab..7c04fc4dd1 100644 --- a/gateway/run_shutdown.py +++ b/gateway/run_shutdown.py @@ -1837,7 +1837,7 @@ class GatewayShutdownMixin(GatewaySessionEndMixin): GatewayShutdownMixin._stop_kill_tool_subprocesses, phase ) - async def _stop_begin_teardown(self, ctx: "GatewayShutdownMixin._StopContext") -> None: + async def _stop_begin_teardown(self, ctx: GatewayShutdownMixin._StopContext) -> None: """Flag teardown, stop room worker/watchdog, notify sessions.""" logger.info("Stopping gateway%s...", " for restart" if self._restart_requested else "") ctx.started_at = time.monotonic() @@ -1866,7 +1866,7 @@ class GatewayShutdownMixin(GatewaySessionEndMixin): await self._notify_active_sessions_of_shutdown() logger.info("Shutdown phase: notify_active_sessions done at +%.2fs", ctx.elapsed()) - async def _stop_drain_active_work(self, timeout: float, ctx: "GatewayShutdownMixin._StopContext") -> None: + async def _stop_drain_active_work(self, timeout: float, ctx: GatewayShutdownMixin._StopContext) -> None: """Pre-mark resume_pending, drain agents/cron/API work into ``ctx``.""" from gateway.run import GatewayRunner # Pre-mark resume_pending BEFORE the drain so a mid-drain SIGKILL still leaves a durable marker. @@ -1915,7 +1915,7 @@ class GatewayShutdownMixin(GatewaySessionEndMixin): except Exception as _e: logger.debug("clear_resume_pending after drain failed for %s: %s", _sk, _e) - async def _stop_interrupt_remaining_work(self, ctx: "GatewayShutdownMixin._StopContext") -> None: + async def _stop_interrupt_remaining_work(self, ctx: GatewayShutdownMixin._StopContext) -> None: """Drain timed out: mark resume_pending, interrupt, settle, kill tool subprocesses, notify cron.""" from gateway.run import GatewayRunner logger.warning( @@ -1957,7 +1957,7 @@ class GatewayShutdownMixin(GatewaySessionEndMixin): await self._notify_interrupted_cron_jobs(_interrupted_cron_jobs) logger.info("Shutdown phase: cron interrupt notices done at +%.2fs", ctx.elapsed()) - async def _stop_finalize_agents_and_adapters(self, ctx: "GatewayShutdownMixin._StopContext") -> None: + async def _stop_finalize_agents_and_adapters(self, ctx: GatewayShutdownMixin._StopContext) -> None: """Detached restart launch, agent finalization, idle-cache cleanup, adapter teardown.""" if self._restart_requested and self._restart_detached: with _log_suppressed(logging.ERROR, "Failed to launch detached gateway restart: %s"): @@ -1991,7 +1991,7 @@ class GatewayShutdownMixin(GatewaySessionEndMixin): _profile_adapters.clear() logger.info("Shutdown phase: all adapters disconnected at +%.2fs", ctx.elapsed()) - async def _stop_release_runtime_state(self, ctx: "GatewayShutdownMixin._StopContext") -> None: + async def _stop_release_runtime_state(self, ctx: GatewayShutdownMixin._StopContext) -> None: """Cancel background tasks, flush pending messages, clear per-session state, final tool kill.""" from gateway.run import GatewayRunner for _task in list(self._background_tasks): @@ -2043,7 +2043,7 @@ class GatewayShutdownMixin(GatewaySessionEndMixin): GatewayShutdownMixin._quiet_step("shutdown_cached_clients error", _reap_aux_clients) - def _stop_quiesce_and_close_session_dbs(self, timeout: float, ctx: "GatewayShutdownMixin._StopContext") -> None: + def _stop_quiesce_and_close_session_dbs(self, timeout: float, ctx: GatewayShutdownMixin._StopContext) -> None: """Quiesce the executor, then close SessionDB handles only if no worker is still live.""" from gateway.run import GatewayRunner, _EXECUTOR_QUIESCE_TIMEOUT # Quiesce the thread pool BEFORE closing session DBs: a late executor write after @@ -2122,7 +2122,7 @@ class GatewayShutdownMixin(GatewaySessionEndMixin): _step("Shared SessionDB close error", _close_shared) logger.info("Shutdown phase: SessionDB close done at +%.2fs", ctx.elapsed()) - async def _stop_persist_exit_state(self, ctx: "GatewayShutdownMixin._StopContext") -> None: + async def _stop_persist_exit_state(self, ctx: GatewayShutdownMixin._StopContext) -> None: """PID/lock release, clean-shutdown marker, restart markers, terminal runtime status.""" from gateway.run import _hermes_home, _planned_restart_notification_path, _shutdown_gateway_health_export from utils import atomic_json_write @@ -2185,7 +2185,7 @@ class GatewayShutdownMixin(GatewaySessionEndMixin): _shutdown_gateway_health_export(self) logger.info("Gateway stopped (total teardown %.2fs)", ctx.elapsed()) - def _shutdown_watchdog_snapshot(self, ctx: "GatewayShutdownMixin._StopContext") -> dict: + def _shutdown_watchdog_snapshot(self, ctx: GatewayShutdownMixin._StopContext) -> dict: """State dumped by the thread-based shutdown watchdog when teardown hangs.""" return { "restart_requested": bool(self._restart_requested), diff --git a/gateway/run_startup.py b/gateway/run_startup.py index 622a22f179..071330e0ab 100644 --- a/gateway/run_startup.py +++ b/gateway/run_startup.py @@ -273,7 +273,7 @@ class GatewayStartupMixin: """Done-callback for boot-path tasks that outlive the startup-restore gate: surface a late failure otherwise swallowed once the task leaves ``_background_tasks``. Cancellation is expected (shutdown), not an error.""" - def _report(task: "asyncio.Task") -> None: + def _report(task: asyncio.Task) -> None: if task.cancelled(): return exc = task.exception() @@ -1715,7 +1715,7 @@ class GatewayStartupMixin: async def _handoff_resolve_destination( self, row: dict[str, Any], profile_name: Optional[str] - ) -> "GatewayStartupMixin._HandoffDestination": + ) -> GatewayStartupMixin._HandoffDestination: """Resolve platform, transport, home channel, thread and destination source for a row.""" from gateway.delivery import resolve_delivery_transport cli_session_id = row["id"] diff --git a/gateway/run_turn.py b/gateway/run_turn.py index 593078cafe..bcfd8e18fc 100644 --- a/gateway/run_turn.py +++ b/gateway/run_turn.py @@ -2374,7 +2374,7 @@ class GatewayTurnMixin: return "\n".join(lines) async def _run_background_task( - self, prompt: str, source: "SessionSource", task_id: str, + self, prompt: str, source: SessionSource, task_id: str, event_message_id: Optional[str] = None, media_urls: Optional[list[str]] = None, media_types: Optional[list[str]] = None, ) -> None: @@ -2385,7 +2385,7 @@ class GatewayTurnMixin: ) def _resolve_enabled_toolsets_for_source( - self, user_config: dict, source: "SessionSource", platform_key: str, + self, user_config: dict, source: SessionSource, platform_key: str, ) -> list: """Enabled toolsets for an agent run, honoring an adapter ``toolsets_for_source()`` override validated through the SAME ``_get_platform_tools`` path (unknown / platform-restricted @@ -2402,7 +2402,7 @@ class GatewayTurnMixin: user_config = {**user_config, "platform_toolsets": pts} return sorted(_get_platform_tools(user_config, platform_key)) - def _resolve_turn_toolsets(self, user_config: dict, source: "SessionSource", platform_key: str): + def _resolve_turn_toolsets(self, user_config: dict, source: SessionSource, platform_key: str): """``(enabled_toolsets, disabled_toolsets)`` for an agent run on ``source``.""" from agent.skill_utils import parse_config_string_list enabled = self._resolve_enabled_toolsets_for_source(user_config, source, platform_key) @@ -2410,7 +2410,7 @@ class GatewayTurnMixin: return enabled, disabled async def _run_background_task_inner( - self, prompt: str, source: "SessionSource", task_id: str, + self, prompt: str, source: SessionSource, task_id: str, event_message_id: Optional[str] = None, media_urls: Optional[list[str]] = None, media_types: Optional[list[str]] = None, ) -> None: @@ -2668,8 +2668,8 @@ class GatewayTurnMixin: return url.rstrip("/") if url else None def _build_stream_consumer_config( - self, source: "SessionSource", scfg: Any, adapter: Any, *, on_missing_cursor: str, - ) -> "tuple[Any, Optional[Callable[[], None]]]": + self, source: SessionSource, scfg: Any, adapter: Any, *, on_missing_cursor: str, + ) -> tuple[Any, Optional[Callable[[], None]]]: """Build the shared ``StreamConsumerConfig`` and optional Telegram pause-typing closure. For non-editing adapters ``on_missing_cursor="fallback"`` streams with an empty cursor; ``"raise"`` raises ``RuntimeError`` so the caller skips streaming entirely.""" @@ -2716,7 +2716,7 @@ class GatewayTurnMixin: def _proxy_error_result(text: str) -> dict[str, Any]: return {"final_response": text, "messages": [], "api_calls": 0, "tools": []} - def _proxy_stream_consumer(self, source: "SessionSource", event_message_id, _thread_metadata, _run_still_current): + def _proxy_stream_consumer(self, source: SessionSource, event_message_id, _thread_metadata, _run_still_current): """Platform stream consumer for the proxy path when streaming is enabled, else ``None``.""" from gateway.run import _load_gateway_config, _platform_config_key _scfg = getattr(getattr(self, "config", None), "streaming", None) @@ -2752,7 +2752,7 @@ class GatewayTurnMixin: async def _run_agent_via_proxy( self, message: str, context_prompt: str, history: list[dict[str, Any]], - source: "SessionSource", session_id: str, session_key: str | None = None, + source: SessionSource, session_id: str, session_key: str | None = None, run_generation: Optional[int] = None, event_message_id: Optional[str] = None, scheduled_heartbeat: bool = False, ) -> dict[str, Any]: @@ -2934,7 +2934,7 @@ class GatewayTurnMixin: with self._profile_scope_for_source(source): return await self._run_agent_inner(message, context_prompt, history, source, session_id, **turn_kwargs) - def _run_agent_display_settings(self, source: SessionSource) -> "GatewayRunner._RunAgentDisplay": + def _run_agent_display_settings(self, source: SessionSource) -> GatewayRunner._RunAgentDisplay: """Resolve per-platform display, progress, status and streaming-surface settings for a turn.""" from gateway.run import ( _gateway_platform_value, _has_platform_display_override, _load_gateway_config, @@ -3059,7 +3059,7 @@ class GatewayTurnMixin: ) def _run_agent_build_turn_context( - self, disp: "GatewayRunner._RunAgentDisplay", AIAgent: Any, *, message: str, source: SessionSource, + self, disp: GatewayRunner._RunAgentDisplay, AIAgent: Any, *, message: str, source: SessionSource, session_key: Optional[str], run_generation: Optional[int], **turn_params, ) -> tuple[TurnContext, TurnRunner, Any]: """Build the ``TurnContext`` and its ``TurnRunner``; ``turn_params`` (history, context_prompt, @@ -3296,7 +3296,7 @@ class GatewayTurnMixin: async def _run_agent_fire_pending_interrupt( self, adapter: Any, agent: Any, source: SessionSource, session_key: str, - _interrupt_detected: "asyncio.Event", streaming_tts_consumer_holder: list, *, + _interrupt_detected: asyncio.Event, streaming_tts_consumer_holder: list, *, log_context: str, log: Callable[[], None], ) -> None: """Peek the adapter's pending event, transcribe voice, then signal the agent + abort streaming TTS. @@ -3332,7 +3332,7 @@ class GatewayTurnMixin: if _stts is not None: _stts.abort("barge-in") - async def _run_agent_monitor_for_interrupt(self, turn_ctx: TurnContext, _interrupt_detected: "asyncio.Event") -> None: + async def _run_agent_monitor_for_interrupt(self, turn_ctx: TurnContext, _interrupt_detected: asyncio.Event) -> None: """Poll the adapter for interrupts (new messages) every 200ms and signal the agent. Level 1 (base.py) catches regular text before _handle_message(); the inactivity poll loop @@ -3363,7 +3363,7 @@ class GatewayTurnMixin: logger.debug("monitor_for_interrupt error (will retry): %s", _mon_err) async def _run_agent_backup_interrupt_check( - self, turn_ctx: TurnContext, _interrupt_detected: "asyncio.Event", interrupt_monitor: "asyncio.Task", + self, turn_ctx: TurnContext, _interrupt_detected: asyncio.Event, interrupt_monitor: asyncio.Task, ) -> None: """Backup interrupt check: if the monitor task died or missed the interrupt, catch it here.""" source, session_key = turn_ctx.source, turn_ctx.session_key @@ -3413,7 +3413,7 @@ class GatewayTurnMixin: return False return False - def _run_agent_start_turn_worker(self, turn_ctx: TurnContext, run_sync: Callable[[], Any]) -> "GatewayRunner._RunAgentWorker": + def _run_agent_start_turn_worker(self, turn_ctx: TurnContext, run_sync: Callable[[], Any]) -> GatewayRunner._RunAgentWorker: """Schedule ``run_sync`` on the executor plus the inactivity watchdog thread. *Inactivity* timeout (agent.gateway_timeout / HERMES_AGENT_TIMEOUT, env wins; 0 = unlimited), @@ -3481,7 +3481,7 @@ class GatewayTurnMixin: return worker @staticmethod - def _reaper_kwargs(worker: "GatewayRunner._RunAgentWorker") -> dict: + def _reaper_kwargs(worker: GatewayRunner._RunAgentWorker) -> dict: """Shared kwargs of the watchdog + timeout-reaper threads.""" return { **{k: getattr(worker, k) for k in ("task_id", "process_baseline", "worker_done", "timeout_fired", "cleanup_lock")}, @@ -3556,8 +3556,8 @@ class GatewayTurnMixin: } async def _run_agent_await_turn_worker( - self, worker: "GatewayRunner._RunAgentWorker", turn_ctx: TurnContext, - _interrupt_detected: "asyncio.Event", interrupt_monitor: "asyncio.Task", + self, worker: GatewayRunner._RunAgentWorker, turn_ctx: TurnContext, + _interrupt_detected: asyncio.Event, interrupt_monitor: asyncio.Task, ) -> Any: """Poll the executor future (inactivity timeout + backup interrupt checks); return its result, or a synthetic failed run dict on inactivity timeout. Polls even with an unlimited timeout @@ -3977,8 +3977,8 @@ class GatewayTurnMixin: return merged async def _run_agent_cleanup_turn_tasks( - self, turn_ctx: TurnContext, *, progress_task: Any, log_task: Any, interrupt_monitor: "asyncio.Task", - _notify_task: "asyncio.Task", tracking_task: "asyncio.Task", stream_task: Any, + self, turn_ctx: TurnContext, *, progress_task: Any, log_task: Any, interrupt_monitor: asyncio.Task, + _notify_task: asyncio.Task, tracking_task: asyncio.Task, stream_task: Any, ) -> None: """``finally`` half of a turn: cancel background tasks, flush stream, release the session slot.""" stream_consumer_holder, session_key = turn_ctx.stream_consumer_holder, turn_ctx.session_key @@ -4188,7 +4188,7 @@ class GatewayTurnMixin: return _status_thread_metadata async def _run_agent_notify_long_running( - self, disp: "GatewayRunner._RunAgentDisplay", turn_ctx: TurnContext, _executor_task_holder: list, + self, disp: GatewayRunner._RunAgentDisplay, turn_ctx: TurnContext, _executor_task_holder: list, ) -> None: """Periodic "still working" heartbeat, edited in place where supported. Stops once this run no longer owns the session slot or the executor finished. ``_executor_task_holder[0]`` is diff --git a/gateway/run_turn_runner.py b/gateway/run_turn_runner.py index c822f98845..7e29a4cdf8 100644 --- a/gateway/run_turn_runner.py +++ b/gateway/run_turn_runner.py @@ -78,7 +78,7 @@ class _ExecApprovalDeclined(RuntimeError): class TurnRunner: """Per-turn collaborator carrying ``GatewayRunner._run_agent_inner``'s tool-progress callbacks.""" - def __init__(self, runner: "GatewayRunner", ctx: TurnContext) -> None: + def __init__(self, runner: GatewayRunner, ctx: TurnContext) -> None: self._runner = runner self._ctx = ctx @@ -546,7 +546,7 @@ class TurnRunner: _PROGRESS_TEXT_LIMIT: int _edit_accepts_metadata: bool - def _progress_edit_state(self, adapter) -> "TurnRunner._ProgressEditState": + def _progress_edit_state(self, adapter) -> TurnRunner._ProgressEditState: ctx = self._ctx len_fn = adapter.message_len_fn if isinstance(adapter, BasePlatformAdapter) else len try: diff --git a/gateway/session.py b/gateway/session.py index 3c016b5327..eb77ed128f 100644 --- a/gateway/session.py +++ b/gateway/session.py @@ -147,7 +147,7 @@ class SessionSource: return d @classmethod - def from_dict(cls, data: dict[str, Any]) -> "SessionSource": + def from_dict(cls, data: dict[str, Any]) -> SessionSource: plain = { name: data.get(name) for name in cls._ALWAYS_FIELDS[1:] + cls._OPTIONAL_PRE_SCOPE + cls._OPTIONAL_POST_SCOPE + cls._OPTIONAL_TAIL @@ -580,7 +580,7 @@ class SessionEntry: return result @classmethod - def from_dict(cls, data: dict[str, Any]) -> "SessionEntry": + def from_dict(cls, data: dict[str, Any]) -> SessionEntry: origin = data.get("origin") origin = SessionSource.from_dict(origin) if isinstance(origin, dict) else None platform = None @@ -621,7 +621,7 @@ class SessionEntry: ) -def build_channel_continuity_note(entry: "SessionEntry", source: SessionSource) -> Optional[str]: +def build_channel_continuity_note(entry: SessionEntry, source: SessionSource) -> Optional[str]: """One-line continuity hint for long-lived Slack/Discord channels/threads. After an auto-reset the agent could bind a new request to an unrelated recent session; this @@ -729,7 +729,7 @@ def build_session_key( class _SessionFlight: def __init__(self) -> None: self.event = threading.Event() - self.result: Optional["SessionEntry"] = None + self.result: Optional[SessionEntry] = None self.error: Optional[BaseException] = None @@ -745,7 +745,7 @@ class _RouteChecks: @dataclass class _RouteDecision: """What the locked apply-phase decided for one routing transition.""" - entry: Optional["SessionEntry"] = None + entry: Optional[SessionEntry] = None needs_save: bool = False # Healthy-path saves take the single-row UPSERT fast path; structural # transitions (recover/create) keep the full rewrite. @@ -757,7 +757,7 @@ class _RouteDecision: reset_had_activity: bool = False prev_session_id: Optional[str] = None - def schedule_reset(self, reason: str, ended: "SessionEntry", had_activity: bool) -> None: + def schedule_reset(self, reason: str, ended: SessionEntry, had_activity: bool) -> None: """Record that *ended* is auto-reset for *reason* (ends its row, seeds the successor).""" self.reset_reason = reason self.reset_had_activity = had_activity @@ -767,7 +767,7 @@ class _RouteDecision: class AsyncSessionStore: """Async boundary for the synchronous, thread-safe SessionStore.""" - def __init__(self, store: "SessionStore") -> None: + def __init__(self, store: SessionStore) -> None: self._store = store def __getattr__(self, name: str): diff --git a/gateway/session_identity.py b/gateway/session_identity.py index aba8789b50..7285aa2aa6 100644 --- a/gateway/session_identity.py +++ b/gateway/session_identity.py @@ -111,7 +111,7 @@ def transport_profile_of(source: Any) -> Optional[str]: return identity.transport_profile if identity is not None and identity.multiplexed else None -def replace_source(source: "SessionSource", **changes: Any) -> "SessionSource": +def replace_source(source: SessionSource, **changes: Any) -> SessionSource: """:func:`dataclasses.replace` that keeps the wire-invisible provenance (transport ref, authorization home, identity). A plain ``replace`` silently produces a source the runner can only route through heuristics.""" @@ -129,7 +129,7 @@ def _name(value: Any) -> Optional[str]: def canonical_identity( - source: "SessionSource", *, runner: Any, adapter: Any = None, + source: SessionSource, *, runner: Any, adapter: Any = None, transport_profile: Optional[str] = None, primary_home: Optional[Path] = None, ) -> Optional[RoutingIdentity]: """The identity already pinned on *source*, else :func:`resolve_identity` — the one call every @@ -149,7 +149,7 @@ def canonical_identity( def restore_identity( - source: "SessionSource", *, runner: Any, transport_profile: Optional[str], + source: SessionSource, *, runner: Any, transport_profile: Optional[str], ) -> Optional[RoutingIdentity]: """Pin the identity of a source rebuilt from durable state (``SessionEntry.origin``, a ``sessions`` row, a cached copy) — no live adapter, so ``transport=None``: the restored row of the @@ -189,7 +189,7 @@ def restore_identity( def resolve_identity( - source: "SessionSource", *, runner: Any, adapter: Any = None, + source: SessionSource, *, runner: Any, adapter: Any = None, transport_profile: Optional[str] = None, primary_home: Optional[Path] = None, ) -> RoutingIdentity: """Resolve and pin the :class:`RoutingIdentity` of an inbound *source*. diff --git a/gateway/session_transcript.py b/gateway/session_transcript.py index 52199d02d2..efdf599d8f 100644 --- a/gateway/session_transcript.py +++ b/gateway/session_transcript.py @@ -120,7 +120,7 @@ class SessionTranscriptMixin: return session_id def _heal_compression_tip_locked( - self, entry: "SessionEntry", original_session_id: Optional[str], + self, entry: SessionEntry, original_session_id: Optional[str], canonical_session_id: Optional[str]) -> bool: """Rewrite *entry* to the compression continuation if stale. Lock held.""" if not original_session_id or not canonical_session_id: diff --git a/gateway/stream_consumer.py b/gateway/stream_consumer.py index 061f3f3ba7..67c451b186 100644 --- a/gateway/stream_consumer.py +++ b/gateway/stream_consumer.py @@ -150,7 +150,7 @@ class GatewayStreamConsumer(StreamTransportMixin, StreamFallbackMixin, StreamThi # Wall-clock timestamp (time.monotonic) when ``_message_id`` was first assigned from a successful # first-send. Used by the fresh-final logic to detect long-lived previews whose edit timestamps # would be stale by completion time. Ported from openclaw/openclaw#72038. - self._preview_message_ids: "set[str]" = set() + self._preview_message_ids: set[str] = set() self._already_sent = False self._edit_supported = True # False once progressive edits stop working self._last_edit_time = 0.0 @@ -200,7 +200,7 @@ class GatewayStreamConsumer(StreamTransportMixin, StreamFallbackMixin, StreamThi self._fallback_prefix = "" # Fallback sends only the missing tail after a partial overflow delivery. self._fallback_preserve_partial_messages = False - self._segment_preview_message_ids: "set[str]" = set() + self._segment_preview_message_ids: set[str] = set() # Tool-progress overlay (native only): shown in the bubble until text arrives. self._tool_progress_lines: list[str] = [] self._tool_progress_active: bool = False @@ -618,7 +618,7 @@ class GatewayStreamConsumer(StreamTransportMixin, StreamFallbackMixin, StreamThi # ── run() collaborators ───────────────────────────────────────────── - def _resolve_length_budget(self) -> "tuple[Callable[[str], int], int]": + def _resolve_length_budget(self) -> tuple[Callable[[str], int], int]: """Per-chat length function (relay adapters differ per chat, e.g. utf16) + budget. isinstance gate: MagicMock auto-attributes aren't callables; test doubles use len.""" # Shares the guarded ladder with the fallback path: a git pull while the gateway @@ -650,7 +650,7 @@ class GatewayStreamConsumer(StreamTransportMixin, StreamFallbackMixin, StreamThi logger.debug("Stream consumer using native-draft transport (chat=%s draft_id=%s)", self.chat_id, self._draft_id) - def _drain_queue(self) -> "_Tick": + def _drain_queue(self) -> _Tick: """Drain everything queued so far into one tick. Control sentinels stop the drain (they take effect this tick); _FINAL_TEXT / _TOOL_PROGRESS / text deltas fold into state so simultaneous items batch.""" @@ -726,7 +726,7 @@ class GatewayStreamConsumer(StreamTransportMixin, StreamFallbackMixin, StreamThi # Degrade to a single buffered send(), like the approval path. self._degrade_native_to_buffered_send() - def _should_edit(self, tick: "_Tick") -> bool: + def _should_edit(self, tick: _Tick) -> bool: """Decide whether this tick flushes an edit/frame.""" if not tick.is_interim: return True @@ -749,7 +749,7 @@ class GatewayStreamConsumer(StreamTransportMixin, StreamFallbackMixin, StreamThi return should_edit and not _is_partial_silence_marker( self._clean_for_display(self._accumulated)) - async def _split_first_send(self, tick: "_Tick") -> bool: + async def _split_first_send(self, tick: _Tick) -> bool: """No message to edit yet and the buffer overflows: seal only the head chunks; the tail stays in _accumulated as the active preview later deltas edit in place. True when the turn finished here (the run loop returns).""" @@ -821,7 +821,7 @@ class GatewayStreamConsumer(StreamTransportMixin, StreamFallbackMixin, StreamThi self._last_sent_text = "" self._turn_split_delivery = True - async def _push_update(self, tick: "_Tick") -> None: + async def _push_update(self, tick: _Tick) -> None: """Send/edit this tick's visible text (cursor-suffixed unless finalizing).""" display_text = self._accumulated if tick.is_interim: @@ -846,7 +846,7 @@ class GatewayStreamConsumer(StreamTransportMixin, StreamFallbackMixin, StreamThi # Lines stay in _tool_progress_lines for the next compose. self._tool_progress_active = False - async def _finalize_turn(self, tick: "_Tick") -> None: + async def _finalize_turn(self, tick: _Tick) -> None: """got_done: final edit without cursor, or one continuation send if edits failed.""" if self._accumulated or self._message_id is not None or self._already_sent: await self._notify_before_finalize() @@ -872,7 +872,7 @@ class GatewayStreamConsumer(StreamTransportMixin, StreamFallbackMixin, StreamThi elif self._accumulated: await self._finalize_edit_path(tick) - async def _finalize_edit_path(self, tick: "_Tick") -> None: + async def _finalize_edit_path(self, tick: _Tick) -> None: """Edit-transport finalize (the non-native got_done branches, in priority order).""" if self._fallback_final_send: await self._send_fallback_final(self._accumulated) @@ -919,7 +919,7 @@ class GatewayStreamConsumer(StreamTransportMixin, StreamFallbackMixin, StreamThi if not cumulative: self._reset_segment_state() - async def _end_segment(self, tick: "_Tick") -> None: + async def _end_segment(self, tick: _Tick) -> None: """Tool boundary: edit-based transports reset so the next chunk is a fresh message. Cumulative transports must NOT reset — clearing _accumulated makes the next frame a non-prefix snapshot and the connector re-appends the whole answer. preserve_no_edit: diff --git a/gateway/stream_consumer_fallback.py b/gateway/stream_consumer_fallback.py index 135915d857..f6c571747c 100644 --- a/gateway/stream_consumer_fallback.py +++ b/gateway/stream_consumer_fallback.py @@ -71,14 +71,14 @@ class StreamFallbackMixin: return final_text @staticmethod - def _split_text_chunks(text: str, limit: int, len_fn: "Callable[[str], int]" = len, + def _split_text_chunks(text: str, limit: int, len_fn: Callable[[str], int] = len, ) -> list[str]: """Split text for fallback sends: newline-preferred, fence-balanced across chunks.""" from gateway.platforms.helpers import split_text_fence_aware return split_text_fence_aware(text, limit, len_fn, prefer_paragraphs=False, balance_fences=True) - def _truncate_for_stream(self, text: str, limit: int, len_fn: "Callable[[str], int]", + def _truncate_for_stream(self, text: str, limit: int, len_fn: Callable[[str], int], ) -> list[str]: """Split via the adapter's canonical truncate_message (platform-specific rules); non-base test doubles / legacy adapters keep the two-argument call shape.""" @@ -208,10 +208,10 @@ class StreamFallbackMixin: self._mark_final_delivered(record=final_text) return None - def _fallback_len_budget(self) -> "tuple[Callable[[str], int], int]": + def _fallback_len_budget(self) -> tuple[Callable[[str], int], int]: """(len_fn, raw_limit) for fallback chunking — per-chat cap/unit on base adapters.""" raw_limit = getattr(self.adapter, "MAX_MESSAGE_LENGTH", 4096) - _len_fn: "Callable[[str], int]" = len + _len_fn: Callable[[str], int] = len if isinstance(self.adapter, _BasePlatformAdapter): _len_fn = self.adapter.message_len_fn try: # per-chat cap/unit (relay adapter fronting N platforms) diff --git a/gateway/stream_consumer_think.py b/gateway/stream_consumer_think.py index e458312a45..61db132309 100644 --- a/gateway/stream_consumer_think.py +++ b/gateway/stream_consumer_think.py @@ -35,7 +35,7 @@ class StreamThinkFilterMixin: return acc_boundary and preceding.strip() == "" return preceding[last_nl + 1:].strip() == "" - def _earliest_open_tag(self, buf: str, lower_buf: str) -> "tuple[int, int]": + def _earliest_open_tag(self, buf: str, lower_buf: str) -> tuple[int, int]: """(index, length) of the earliest block-boundary opening tag, or (-1, 0).""" best_idx, best_len = -1, 0 for tag in self._OPEN_THINK_TAGS: diff --git a/gateway/streaming_tts_consumer.py b/gateway/streaming_tts_consumer.py index 418e38b09d..d6f7a357f5 100644 --- a/gateway/streaming_tts_consumer.py +++ b/gateway/streaming_tts_consumer.py @@ -43,7 +43,7 @@ class StreamingTTSConsumer: # since an OpenAI-compatible endpoint reports its real rate only in the response (#76466). self._audio_format = audio_format or AudioFormat() if self._streamer is None else self._streamer_format() # Thread-safe queue of completed clauses plus the _DONE/_ABORT sentinels. - self._queue: "queue.Queue[Any]" = queue.Queue(maxsize=256) + self._queue: queue.Queue[Any] = queue.Queue(maxsize=256) self._handle: Optional[StreamingTTSHandle] = None self._task: Optional[asyncio.Task] = None # drain task, created once by start() self._completed = self._partial = self._aborted = False diff --git a/gateway/turn_lease.py b/gateway/turn_lease.py index 2046f1b198..812b08cdaf 100644 --- a/gateway/turn_lease.py +++ b/gateway/turn_lease.py @@ -25,7 +25,7 @@ DEFAULT_MAX_LEASES = 512 DEFAULT_LEASE_WAIT = 5.0 -def _holder_desc(holder: Optional["TurnLeaseToken"]) -> tuple: +def _holder_desc(holder: Optional[TurnLeaseToken]) -> tuple: return (holder.owner_key, holder.generation) if holder else ("?", "?") @@ -45,7 +45,7 @@ class TurnLeaseToken: __slots__ = ("generation", "lease", "owner_key", "released", "session_id") - def __init__(self, session_id: str, owner_key: str, generation: int, lease: "_SessionLease") -> None: + def __init__(self, session_id: str, owner_key: str, generation: int, lease: _SessionLease) -> None: self.session_id, self.owner_key, self.generation = session_id, owner_key, generation self.released = False # The concrete lease, so release resolves by identity even after a rotation re-aliases diff --git a/hermes_cli/_subprocess_compat.py b/hermes_cli/_subprocess_compat.py index 4d0a9a08ff..53455472e5 100644 --- a/hermes_cli/_subprocess_compat.py +++ b/hermes_cli/_subprocess_compat.py @@ -303,7 +303,7 @@ _GIT_CONFIG_OVERRIDES = { } -def _safe_directory_cache_key(env: "Mapping[str, str]") -> tuple: +def _safe_directory_cache_key(env: Mapping[str, str]) -> tuple: """Everything that decides which files ``git config --system/--global`` reads, plus the global candidates' mtimes so an edit to ``~/.gitconfig`` is picked up without a restart.""" home = env.get("HOME", "") @@ -328,7 +328,7 @@ def _safe_directory_cache_key(env: "Mapping[str, str]") -> tuple: _safe_directory_cache: dict[tuple, list[str]] = {} -def _user_safe_directories(base_env: "Mapping[str, str]") -> list[str]: +def _user_safe_directories(base_env: Mapping[str, str]) -> list[str]: """The user's configured ``safe.directory`` values, in git's own effective order. Read with ``git config -z --get-all`` under *base_env* (the caller's untouched environment) so @@ -426,7 +426,7 @@ def expose_pm_git(project_root: Path) -> None: os.environ["PATH"] = path -def noninteractive_git_env(base: "Mapping[str, str] | None" = None) -> dict[str, str]: +def noninteractive_git_env(base: Mapping[str, str] | None = None) -> dict[str, str]: """Environment for *internal* git invocations that must never prompt. Copy of ``base`` (default ``os.environ``) with ``GIT_TERMINAL_PROMPT=0`` (fail instead of @@ -510,9 +510,9 @@ FILTER_DISCOVERY_FAILED = "git filter discovery failed" def noninteractive_repo_git_env( - cwd: "str | os.PathLike[str]", - base: "Mapping[str, str] | None" = None, -) -> "dict[str, str] | None": + cwd: str | os.PathLike[str], + base: Mapping[str, str] | None = None, +) -> dict[str, str] | None: """Harden internal git for one repository, including named clean/smudge/process filters. The static environment can pin fixed config keys such as core.fsmonitor and @@ -536,7 +536,7 @@ def noninteractive_repo_git_env( return None names: list[str] = [] targets: set[Path] = set() - toplevel: "Path | None" = None + toplevel: Path | None = None fields = proc.stdout.split("\0") for origin, entry in zip(fields[0::2], fields[1::2]): key, _, value = entry.partition("\n") @@ -734,7 +734,7 @@ def pid_is_hermes(pid: int, *, expected_start_time: int | None = None) -> bool: return False -def kill_process_tree(proc: "subprocess.Popen") -> None: +def kill_process_tree(proc: subprocess.Popen) -> None: """Best-effort terminate *proc* and its descendants on both platforms; never raises. ``proc.kill()`` alone only terminates the direct child. This is cleanup on an already-failing @@ -766,7 +766,7 @@ def kill_process_tree(proc: "subprocess.Popen") -> None: pass -def _legacy_kill_process_tree(proc: "subprocess.Popen") -> None: +def _legacy_kill_process_tree(proc: subprocess.Popen) -> None: """Local tree-kill fallback when agent.deadline is unavailable (partial install, cycle).""" if not IS_WINDOWS: # Verify the child leads its own process group before signalling, never a shared group. @@ -796,9 +796,9 @@ def _legacy_kill_process_tree(proc: "subprocess.Popen") -> None: def bounded_probe_run( argv: Sequence[str], *, timeout: float, errors: str = "replace", - env: "Mapping[str, str] | None" = None, cwd: "str | os.PathLike[str] | None" = None, - raise_on_spawn_failure: bool = False, input: "str | None" = None, -) -> "subprocess.CompletedProcess[str] | None": + env: Mapping[str, str] | None = None, cwd: str | os.PathLike[str] | None = None, + raise_on_spawn_failure: bool = False, input: str | None = None, +) -> subprocess.CompletedProcess[str] | None: """Deadlock-safe ``subprocess.run(argv, capture_output=True, timeout=…)`` for fail-open probes. ``input`` is written to the child's stdin (closed afterwards); without it stdin is ``DEVNULL``. @@ -846,7 +846,7 @@ def bounded_probe_run( return subprocess.CompletedProcess(list(argv), proc.returncode, stdout, stderr) -def kill_and_drain(proc: "subprocess.Popen", seconds: float) -> "tuple | None": +def kill_and_drain(proc: subprocess.Popen, seconds: float) -> tuple | None: """Tree-kill *proc* (:func:`kill_process_tree`), then read what its pipes still hold for at most *seconds*: ``(stdout, stderr)``, or ``None`` when a descendant the kill missed still holds them. Those pipes are left to ``communicate()``'s reader threads, never closed @@ -868,7 +868,7 @@ def _close_job(job) -> None: pass -def bounded_git_probe(argv: Sequence[str], *, timeout: float, env: "Mapping[str, str] | None" = None) -> str: +def bounded_git_probe(argv: Sequence[str], *, timeout: float, env: Mapping[str, str] | None = None) -> str: """Run a short ``git`` probe and return stripped stdout, or ``""`` on ANY failure. On Windows ``run()``'s post-timeout cleanup calls an unbounded ``communicate()``; a suspended diff --git a/hermes_cli/active_sessions.py b/hermes_cli/active_sessions.py index bda16edff1..7a279f9737 100644 --- a/hermes_cli/active_sessions.py +++ b/hermes_cli/active_sessions.py @@ -122,7 +122,7 @@ class ActiveSessionRefusal(str): reason: str - def __new__(cls, message: str, reason: str) -> "ActiveSessionRefusal": + def __new__(cls, message: str, reason: str) -> ActiveSessionRefusal: obj = super().__new__(cls, message) obj.reason = reason return obj @@ -181,7 +181,7 @@ def _lock_path(registry_home: str | Path | None = None) -> Path: def _lease_paths( - lease: Optional["ActiveSessionLease"] = None, registry_home: str | Path | None = None + lease: Optional[ActiveSessionLease] = None, registry_home: str | Path | None = None ) -> tuple[Path, Path]: if lease is not None and lease.state_path is not None and lease.lock_path is not None: return lease.state_path, lease.lock_path diff --git a/hermes_cli/approval_transport.py b/hermes_cli/approval_transport.py index bf141d0b9f..3d6b875736 100644 --- a/hermes_cli/approval_transport.py +++ b/hermes_cli/approval_transport.py @@ -57,7 +57,7 @@ class ApprovalRequest: cls, *, command: str, description: str, pattern_key: str, pattern_keys: tuple[str, ...], session_key: str, surface: str, allow_session: bool, allow_permanent: bool, timeout_seconds: float = 300, - ) -> "ApprovalRequest": + ) -> ApprovalRequest: choices: list[ApprovalChoice] = ["once"] if allow_session: choices.append("session") diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index d1dec24769..e296bbb623 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -1753,7 +1753,7 @@ def _tls_state_from_verify(verify: Any) -> dict[str, Any]: def _last_auth_error_marker( - provider: str, error: "AuthError", *, reason: str, default_code: Optional[str] = None, + provider: str, error: AuthError, *, reason: str, default_code: Optional[str] = None, ) -> dict[str, Any]: """The ``last_auth_error`` record persisted when dead OAuth material is quarantined.""" return { @@ -1765,7 +1765,7 @@ def _last_auth_error_marker( _FLAT_OAUTH_TOKEN_KEYS = ("access_token", "refresh_token", "expires_at", "expires_in", "obtained_at") -def _quarantine_flat_oauth_state(state: dict[str, Any], provider: str, exc: "AuthError") -> None: +def _quarantine_flat_oauth_state(state: dict[str, Any], provider: str, exc: AuthError) -> None: """Strip dead tokens from a flat OAuth state after a terminal runtime refresh failure so subsequent calls fail fast without a network retry (mirrors the Nous / xAI / Codex pattern).""" for _k in _FLAT_OAUTH_TOKEN_KEYS: @@ -1798,7 +1798,7 @@ _NOUS_PORTAL_ALLOWED_HOSTS: frozenset[str] = frozenset({ # per-turn HERMES_HOME override a multiplex gateway sets), so a single slot would hand profile A's # Portal bearer to profile B for up to the TTL. _RESOLVE_TOKEN_CACHE_LOCK = threading.Lock() -_RESOLVE_TOKEN_CACHE: "dict[str, tuple[float, str]]" = {} +_RESOLVE_TOKEN_CACHE: dict[str, tuple[float, str]] = {} _RESOLVE_TOKEN_CACHE_TTL_S = 5.0 diff --git a/hermes_cli/auth_codex.py b/hermes_cli/auth_codex.py index 4b9425d4b2..257b43145c 100644 --- a/hermes_cli/auth_codex.py +++ b/hermes_cli/auth_codex.py @@ -257,7 +257,7 @@ def _recover_codex_tokens_from_cli( def _refresh_payload_access_token( - response: "httpx.Response", *, provider: str, invalid_json: tuple[str, str], + response: httpx.Response, *, provider: str, invalid_json: tuple[str, str], invalid_response: Optional[tuple[str, str]], missing_access: tuple[str, str], relogin_required: bool = True, invalid_json_relogin: Optional[bool] = None, strict_str: bool = True) -> tuple[dict[str, Any], str]: @@ -335,7 +335,7 @@ def _is_transient_transport_error(exc: BaseException) -> bool: return False -def _codex_login_post(url: str, *, failure: tuple[str, str], **kwargs: Any) -> "httpx.Response": +def _codex_login_post(url: str, *, failure: tuple[str, str], **kwargs: Any) -> httpx.Response: """One 15s POST for the device-login flow; transport errors become ``_codex_err(*failure)``. A transient transport blip (a dropped connection mid-flow) is retried twice with a small @@ -376,7 +376,7 @@ def _capped_byte_stream_class() -> type: stream subclass rather than a bare generator. """ - def __init__(self, response: "httpx.Response") -> None: + def __init__(self, response: httpx.Response) -> None: self._response, self._raw = response, response.stream def __iter__(self) -> Iterator[bytes]: @@ -398,7 +398,7 @@ def _capped_byte_stream_class() -> type: return _CappedByteStream -def _cap_codex_response_body(response: "httpx.Response") -> None: +def _cap_codex_response_body(response: httpx.Response) -> None: """httpx response hook: refuse to buffer an auth body above ``_CODEX_AUTH_BODY_MAX_BYTES``. Runs before ``client.post()`` reads the body, so a hostile or broken endpoint/proxy answering @@ -408,7 +408,7 @@ def _cap_codex_response_body(response: "httpx.Response") -> None: response.stream = _capped_byte_stream_class()(response) -def _codex_http_client(**kwargs: Any) -> "httpx.Client": +def _codex_http_client(**kwargs: Any) -> httpx.Client: """Build an ``httpx.Client`` for Codex OAuth/probe endpoints with Happy-Eyeballs racing and a 1 MiB response-body cap (``_cap_codex_response_body``). @@ -440,7 +440,7 @@ def _codex_quota_exhausted_error(retry_after: Optional[int]) -> AuthError: return _codex_err(message, CODEX_RATE_LIMITED_CODE, relogin=False) -def _codex_refresh_failure_error(response: "httpx.Response") -> AuthError: +def _codex_refresh_failure_error(response: httpx.Response) -> AuthError: """Decode a non-200 Codex token-refresh response into a shaped AuthError.""" from hermes_cli.auth import _nonempty_str code = "codex_refresh_failed" @@ -981,7 +981,7 @@ def _login_openai_codex(args, pconfig: ProviderConfig, *, force_new_login: bool _print_login_success("openai-codex", config_path, show_auth_state=True) -def _codex_login_rate_limited_error(response: "httpx.Response", *, during: str = "") -> AuthError: +def _codex_login_rate_limited_error(response: httpx.Response, *, during: str = "") -> AuthError: """AuthError for a 429 from OpenAI's device-auth endpoints (throttle, not credential fault).""" # Upstream rate-limit / usage-quota exhaustion on the token endpoint. The stored refresh token is still # valid here — re-authenticating cannot lift a quota cap. Classify distinctly from auth failures so diff --git a/hermes_cli/auth_device_flow.py b/hermes_cli/auth_device_flow.py index f2ee8d7f91..b8ca803f41 100644 --- a/hermes_cli/auth_device_flow.py +++ b/hermes_cli/auth_device_flow.py @@ -314,10 +314,10 @@ def _print_device_code_instructions( def _poll_device_token_generic( - post: Callable[[], "httpx.Response"], *, expires_in: int, poll_interval: int, + post: Callable[[], httpx.Response], *, expires_in: int, poll_interval: int, validate_success: Callable[[dict[str, Any]], None], - on_non_json_error: Callable[["httpx.Response"], Exception], - on_error: Callable[["httpx.Response", dict[str, Any]], Exception], + on_non_json_error: Callable[[httpx.Response], Exception], + on_error: Callable[[httpx.Response, dict[str, Any]], Exception], on_timeout: Callable[[], Exception]) -> dict[str, Any]: """RFC 8628 device-code polling loop shared by the Nous and xAI flows. diff --git a/hermes_cli/banner.py b/hermes_cli/banner.py index 790807125e..84ac31df03 100644 --- a/hermes_cli/banner.py +++ b/hermes_cli/banner.py @@ -685,7 +685,7 @@ def _banner_skill_lines(skills_by_category: dict[str, list[str]], skills_enabled def build_welcome_banner( - console: "Console", model: str, cwd: str, tools: list[dict] | None = None, enabled_toolsets: list[str] | None = None, + console: Console, model: str, cwd: str, tools: list[dict] | None = None, enabled_toolsets: list[str] | None = None, session_id: str | None = None, get_toolset_for_tool=None, context_length: int | None = None, provider: str | None = None, availability: dict[str, Any] | None = None, skills_by_category: dict[str, list[str]] | None = None, context_pinned: bool = False, diff --git a/hermes_cli/cli_single_query.py b/hermes_cli/cli_single_query.py index 647e0a29d7..ddb719d544 100644 --- a/hermes_cli/cli_single_query.py +++ b/hermes_cli/cli_single_query.py @@ -50,7 +50,7 @@ def _interrupt_agent_for_signal(agent, signum) -> None: pass # never block signal handling -def _run_kanban_goal_loop_q(cli: "HermesCLI", first_response: str, run_turn=None, log=None) -> None: +def _run_kanban_goal_loop_q(cli: HermesCLI, first_response: str, run_turn=None, log=None) -> None: """Drive a kanban goal_mode worker through ``goals.run_kanban_goal_loop`` after its first turn. ``run_turn`` defaults to the bare ``-Q`` turn (final answer only). The ``-q`` worker path @@ -94,7 +94,7 @@ def _run_kanban_goal_loop_q(cli: "HermesCLI", first_response: str, run_turn=None "failure_reason": result.get("failure_reason") if isinstance(result, dict) else None, } - def _task_status() -> "str | None": + def _task_status() -> str | None: with _kbc.connect_closing() as c: return _kb.goal_run_status(c, task_id, worker_run_id) @@ -110,7 +110,7 @@ def _run_kanban_goal_loop_q(cli: "HermesCLI", first_response: str, run_turn=None ) -def _run_kanban_goal_loop_chat(cli: "HermesCLI", first_response: str) -> None: +def _run_kanban_goal_loop_chat(cli: HermesCLI, first_response: str) -> None: """``-q`` worker variant: follow-up turns go through ``cli.chat`` (tool feed stays on stdout, which is the Kanban worker log) and judge verdicts are printed there too, so a goal_mode card's log reads like any other worker's instead of staying blank until the final answer.""" diff --git a/hermes_cli/cli_status_bar_mixin.py b/hermes_cli/cli_status_bar_mixin.py index 959d60f18f..67aac0e126 100644 --- a/hermes_cli/cli_status_bar_mixin.py +++ b/hermes_cli/cli_status_bar_mixin.py @@ -52,7 +52,7 @@ class CLIStatusBarMixin: return "class:status-bar-bad" return _threshold_style(percent_used, ((50, "warn"),), "good") - def _cache_hit_rate(self, snapshot: dict, precision: int = 1) -> "tuple[float, str] | None": + def _cache_hit_rate(self, snapshot: dict, precision: int = 1) -> tuple[float, str] | None: """Return (cache_pct, label) or None without cache data. Prefers the baseline-delta pct from ``_get_status_bar_snapshot`` (resets on model switch / compression, so it reflects the *current* cache regime); falls back to the session-lifetime ratio.""" @@ -455,7 +455,7 @@ class CLIStatusBarMixin: return "".join(out).rstrip() + "..." @classmethod - def _status_title_badge(cls, title: str, width: int) -> "tuple[str, int] | None": + def _status_title_badge(cls, title: str, width: int) -> tuple[str, int] | None: """(badge, left_width) for the far-right session-title badge, or None when it doesn't fit (no title / bar narrower than 24 cells).""" title = str(title or "").strip() @@ -829,7 +829,7 @@ class CLIStatusBarMixin: except (OSError, ValueError): pass - def _pet_view(self) -> "tuple[str, bool] | None": + def _pet_view(self) -> tuple[str, bool] | None: """(state, is_kitty) for the current frame, or None when no pet shows.""" with self._pet_lock: if not self._pet_enabled or self._pet_renderer is None: diff --git a/hermes_cli/cli_terminal_input.py b/hermes_cli/cli_terminal_input.py index 76f7527ee3..604d848a5d 100644 --- a/hermes_cli/cli_terminal_input.py +++ b/hermes_cli/cli_terminal_input.py @@ -142,7 +142,7 @@ def _file_drop_result(path: Path, remainder: str) -> dict: return {"path": path, "is_image": path.suffix.lower() in _IMAGE_EXTENSIONS, "remainder": remainder} -def _detect_file_drop(user_input: str) -> "dict | None": +def _detect_file_drop(user_input: str) -> dict | None: """Detect a dragged/pasted file path at the start of *user_input* -> ``{path, is_image, remainder}`` or None.""" from cli import _file_drop_result, _resolve_attachment_path, _split_path_input if not isinstance(user_input, str): diff --git a/hermes_cli/commands_platforms.py b/hermes_cli/commands_platforms.py index e001bdad65..bf7a8c8bc5 100644 --- a/hermes_cli/commands_platforms.py +++ b/hermes_cli/commands_platforms.py @@ -232,7 +232,7 @@ def _iter_gateway_skills(platform: str): def _collect_gateway_skill_entries( platform: str, max_slots: int | None, reserved_names: set[str], desc_limit: int = 100, - sanitize_name: "Callable[[str], str] | None" = None, + sanitize_name: Callable[[str], str] | None = None, ) -> tuple[list[tuple[str, str, str, str]], int]: """Collect plugin + skill entries for a gateway platform. diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 576562b471..54b863b13c 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -987,12 +987,12 @@ class ConfigIssue: hint: str -def _issue(issues: list["ConfigIssue"], severity: str, message: str, hint: str) -> None: +def _issue(issues: list[ConfigIssue], severity: str, message: str, hint: str) -> None: issues.append(ConfigIssue(severity, message, hint)) def _require_fields( - issues: list["ConfigIssue"], entry: dict[str, Any], label: str, + issues: list[ConfigIssue], entry: dict[str, Any], label: str, fields: tuple[tuple[str, str], ...], suffix: str = "") -> None: """Append a warning for every falsy ``field`` of *entry* (message: ``