mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-09 13:21:42 +00:00
fix: prevent /stop signal loss and empty provider credential corruption
Two deep bugs found through systematic analysis of the streaming API call and fallback credential subsystems: 1. Interrupt signal loss (chat_completion_helpers.py): When the worker thread exits before the main thread's poll loop checks the interrupt flag (e.g. _call_anthropic() detects the flag and returns None), the while loop exits normally and the InterruptedError is never raised. /stop is silently swallowed. Fix: re-check _interrupt_requested after the while loop exits. 2. Empty provider bypasses credential guard (agent_runtime_helpers.py): recover_with_credential_pool() guards against cross-provider pool swaps with 'if current_provider and pool_provider and current != pool_provider'. When agent.provider is '' (valid unset state from agent_init.py:326), current_provider is falsy, the guard is skipped, and the pool swaps credentials onto an agent with empty provider. This is the root cause of the 'provider= model=' empty-string error. Fix: only skip the guard when pool_provider is empty (unscoped pool), not when agent provider is empty.
This commit is contained in:
parent
179ca25a38
commit
2e30a5e628
2 changed files with 15 additions and 1 deletions
|
|
@ -729,7 +729,14 @@ def recover_with_credential_pool(
|
|||
# that seeded the pool.
|
||||
current_provider = (getattr(agent, "provider", "") or "").strip().lower()
|
||||
pool_provider = (getattr(pool, "provider", "") or "").strip().lower()
|
||||
if current_provider and pool_provider and current_provider != pool_provider:
|
||||
# Guard: skip credential pool recovery when the pool is scoped to a
|
||||
# different provider than the agent. Only guard when the pool has a
|
||||
# known provider — an empty pool provider means "unscoped" (applies to
|
||||
# any provider). An empty agent provider is treated as a mismatch
|
||||
# because swapping the pool's credentials would set base_url/api_key
|
||||
# without fixing the empty provider field, leaving the agent in a
|
||||
# corrupted state (provider="" model="").
|
||||
if pool_provider and current_provider != pool_provider:
|
||||
# Custom endpoints use two naming conventions for the SAME provider:
|
||||
# the agent carries the generic ``custom`` label while the pool is
|
||||
# keyed ``custom:<name>`` (see CUSTOM_POOL_PREFIX). A literal string
|
||||
|
|
|
|||
|
|
@ -2902,6 +2902,13 @@ def interruptible_streaming_api_call(agent, api_kwargs: dict, *, on_first_delta=
|
|||
except Exception:
|
||||
pass
|
||||
raise InterruptedError("Agent interrupted during streaming API call")
|
||||
# Worker thread exited before the main thread's poll loop could check
|
||||
# the interrupt flag. If the worker returned early due to an interrupt
|
||||
# (e.g. _call_anthropic() detected _interrupt_requested and returned
|
||||
# None), the InterruptedError above was never raised. Re-check the
|
||||
# flag here so /stop is not silently swallowed. (#59999 area)
|
||||
if agent._interrupt_requested:
|
||||
raise InterruptedError("Agent interrupted during streaming API call (post-worker)")
|
||||
if result["error"] is not None:
|
||||
if deltas_were_sent["yes"]:
|
||||
# Streaming failed AFTER some tokens were already delivered to
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue