Commit graph

1101 commits

Author SHA1 Message Date
kshitijk4poor
39efad89a8 refactor: shared helpers for api_content sidecar pop/drop/extract
Deduplicates the sidecar handling across 9 sites:
- substitute_api_content(): 2 API-bound pop+substitute sites
  (chat_completion_helpers, transport — conversation_loop keeps its
  inline pop because the current-turn compose fallback needs the value)
- drop_stale_api_content(): 4 rewrite-drop sites
  (context_compressor x2, replay_cleanup, agent_runtime_helpers)
- extract_api_content_sidecar(): 3 gateway forwarding sites
  (gateway/session, gateway/slash_commands, cli_commands_mixin)

Also: restore the eager _pending_cli_user_message clear on the early
row-creation try (was lost when the crash-persist moved after prefetch —
a crash in compression between the two tries would leave a stale staged
input), and fix a comment indentation nit in run_agent.py.

Co-authored-by: Soju06 <qlskssk@gmail.com>
2026-07-19 08:25:35 +05:30
Soju06
7b3dcee928 feat(cache): persist the exact bytes sent to the API in an api_content sidecar
The first LLM call of every gateway turn gets ~0% provider prompt-cache
hit rate (in-turn calls: 97-99%) because the bytes sent for a turn's
user message are not the bytes replayed next turn: memory-prefetch and
pre_llm_call context are injected into the API copy only, the #48677
persist override writes cleaned content to the DB row, and
get_messages_as_conversation sanitize/strips user and assistant content
on load. Any of these diverges the request prefix at that message and
re-prefills everything after it — measured 27.9s for the first call vs
2.4-5.8s cached at a median ~156k-token context.

Persist what you send: a nullable messages.api_content column stores the
exact content string sent to the API when it differs from the clean
stored content, and replay substitutes it verbatim (no sanitize, no
strip). The injection composition lives in one helper
(turn_context.compose_user_api_content); the turn prologue stamps its
output onto the live user message, the api_messages build sends the
stamped bytes, and every outgoing copy pops the field so it never
reaches a provider. The crash-resilience user-turn persist moves after
prefetch/pre_llm_call so the user row is written once with its final
sidecar; _ensure_db_session stays before preflight compression (session
rotation needs the parent row under PRAGMA foreign_keys=ON). The
current-turn index trackers are re-anchored after compaction rebuilds
the message list, in-place preflight compaction backfills the stamp onto
the already-inserted row, and gateway replay forwards the sidecar only
when the replay pipeline did not rewrite the content. Rewrite paths that
would leave stale bytes (historical image strip, merge-summary-into-
tail, consecutive-user repair merge, stale-confirmation redaction) drop
the sidecar; the chat-completions transport and the max-iterations
summary path strip it defensively. codex_app_server and MoA turns are
excluded from stamping because their wire bytes differ from the
composition. A missing or dropped sidecar degrades to today's behavior
(one cache-boundary miss), never to wrong content.
2026-07-19 08:25:35 +05:30
Brooklyn Nicholson
42c240f580 fix(agent): request-local Anthropic clients so the stale/interrupt watchdog never corrupts SQLite (#67142)
Direct-Anthropic requests used a single shared _anthropic_client, and the
stale/interrupt watchdog closed + rebuilt it from the poll (stranger) thread
at four sites (non-streaming stale/interrupt, streaming stale/interrupt).
Closing a client whose TLS socket a worker thread was still reading released
the FD from a stranger thread; the kernel recycled it under a live SSL BIO,
which then wrote a 24-byte TLS record into an unrelated SQLite header
(cron/executions.db), bricking every cron on the profile. Same shape as the
OpenAI-only #29507 fix, but the Anthropic path never got the owner-thread
contract.

Extend the #29507 ownership contract to Anthropic: build a per-request client
(_create_request_anthropic_client), register it with the request-client
holder tagged by kind, and route _close_request_client_once by kind — a
stranger thread only shuts the request client's sockets down
(_abort_request_anthropic_client), while the owning worker performs the SDK
close (_close_request_anthropic_client). The shared _anthropic_client is now
never closed from inside a request (streaming or non-streaming), including the
worker retry-cleanup sites, since each attempt builds a fresh request client.
The #28161 no-hang guarantee is preserved: the poll-thread socket abort
unblocks the worker immediately.

Salvages the approach from #51688 (@raymondyan-zhijie), reimplemented onto
current main (non-streaming dispatch was refactored into
_dispatch_nonstreaming_api_request; streaming grew _cancel_current_stream_attempt
and worker retry-cleanup sites). Tests updated to the request-local mechanism
(incl. replacing a banned source-reading test with a behavior test) plus new
regression coverage proving the watchdog aborts the request client and never
touches the shared client.

Co-authored-by: raymondyan-zhijie <32435458+raymondyan-zhijie@users.noreply.github.com>
2026-07-18 21:20:48 -04:00
nanami7777777
aef0fe6f27 fix: handle multimodal content in interim assistant text and avoid retrying local processing errors (#66267) 2026-07-18 19:39:07 +05:30
Teknium
73057ed161 fix(auxiliary): scope runtime state to each turn 2026-07-17 09:08:30 -07:00
dfein38347g
fdc6c32d7d fix(auxiliary): isolate runtime cache by live context 2026-07-17 09:08:30 -07:00
HexLab98
0c9ac09313 fix(streaming): fence superseded streams out of the delta sink (single-writer)
When the stale-stream detector reconnects past a stream whose socket abort
raced (the close never actually stopped the old worker), the superseded stream
and the retry's stream both write deltas into the same turn. The persisted
transcript is then two coherent responses interleaved token-by-token —
de-interleaving the stored text by alternation yields two complete, independent
answers to the same prompt, which is a dual-writer race in the harness, not a
model/context failure (#65991).

The interrupt path already positively cancels before force-closing (#6600), but
the stale-kill path relies only on the socket abort, and nothing fenced late
chunks from a superseded stream out of the shared delta sink.

Enforce a single-writer invariant on the sink itself, guarded by attempt id
rather than only socket state: every streaming attempt (chat_completions,
anthropic_messages, and bedrock paths) claims a monotonic writer token before
it begins consuming its stream. A newer claim supersedes any older one, so the
consume loop bails the instant it is superseded and _fire_stream_delta /
_fire_reasoning_delta / _record_streamed_assistant_text drop chunks from a
stale writer. The token is stored per-thread, so a thread that never claimed
(a non-streaming delta caller) is never fenced — the guard can only ever drop a
superseded stream, never the single legitimate writer. Discards are counted and
logged sparsely so a real provider problem stays visible instead of being
silently swallowed.
2026-07-17 06:49:23 -07:00
Teknium
779019ef7d feat(agent): add display.show_commentary toggle for Codex commentary channel
Commentary delivery is on by default; users who find the extra mid-turn
narration noisy can set display.show_commentary: false to restore the
previous behavior (commentary routed to the reasoning channel, visible
only with show_reasoning).

- hermes_cli/config.py: display.show_commentary default true
- agent/agent_init.py: wire config -> agent.show_commentary
- run_agent.py: gate structured commentary extraction on the flag
- agent/codex_runtime.py: gate live-stream commentary callback (falls
  back to legacy reasoning-channel routing when off)
- docs + 2 tests (interim path off, live stream fallback)

Also adds AUTHOR_MAP entries for davidrobertson and 100yenadmin.
2026-07-16 23:27:12 -07:00
Eva
7041c56cdf feat(agent): stream Codex commentary separately 2026-07-16 23:27:12 -07:00
Eva
b008131b54 fix(agent): harden Codex commentary interim delivery 2026-07-16 23:27:12 -07:00
David Robertson
a15397d61a fix(agent): redact Codex interim commentary 2026-07-16 23:27:12 -07:00
David Robertson
136ade2ed5 fix(agent): surface Codex commentary items as interim messages 2026-07-16 23:27:12 -07:00
Al Sayed Hoota
633fc7ab88 fix: don't downgrade xhigh reasoning effort when provider supports it
The current code unconditionally downgrades 'xhigh' to 'high' whenever
'high' is in supported_efforts, even if 'xhigh' is also supported.
This prevents users from using extended thinking on providers like
Copilot that list 'xhigh' in their supported efforts.

Fix: only downgrade 'xhigh' to 'high' when 'xhigh' is NOT in the
provider's supported efforts list.
2026-07-16 08:47:10 -07:00
kshitijk4poor
5d9a72b7c2 fix(ollama-cloud): capability-gate reasoning_effort + correct disable semantics
Three follow-up fixes to the salvaged reasoning_effort support, all verified
live against ollama.com /v1/chat/completions + /api/show on deepseek-v4-pro,
gemma3, and qwen3-coder:

1. Capability-gate on /api/show 'thinking'. The original ignored the
   supports_reasoning flag and emitted reasoning_effort for every model. Now
   gated: only models whose native /api/show capabilities list contains
   'thinking' (deepseek-v4 yes; gemma3 / qwen3-coder no) get reasoning_effort.
   Mirrors the LM Studio pattern — capability resolved once per (model,
   base_url) in run_agent._supports_reasoning_extra_body via a cached probe
   (hermes_cli.models.ollama_model_supports_thinking), threaded into the
   profile hook as supports_reasoning. No live HTTP in the per-request path.

2. Disable actually disables. Ollama Cloud defaults to thinking ON and IGNORES
   the extra_body.thinking:{type:disabled} shape (verified: still returned
   reasoning). The only working off switch is top-level reasoning_effort:'none'.
   The salvaged code returned ({}, {}) for enabled:false / effort:none, leaving
   thinking ON. Now emits {'reasoning_effort': 'none'}.

3. Omit unrecognized effort. The original forwarded any unknown string verbatim
   including 'minimal' (a real Hermes effort level). Ollama Cloud rejects
   unrecognized values with a hard HTTP 400 (accepted set: low/medium/high/
   max/none), so forwarding 'minimal' would break the request. Now omitted.

Core touches (run_agent.py, hermes_cli/models.py) add the capability probe;
the plugin profile only consumes the resolved flag. 24/24 profile tests green;
194 provider/transport tests unaffected.
2026-07-16 07:58:04 -07:00
Regina
59787b9ada chore(agent): tripwire — warn when a turn starts before the previous turn's persist
Two turns interleaving on one session corrupt the durable transcript:
flushes race (user rows persist out of arrival order), the identity-marker
dedup over shared history dicts can swallow a row, and the second turn
runs on a history base that never saw the first turn's exchange. The
dispatch route that lets the second turn through the busy guard is not
yet identified.

Add note_turn_start (build_turn_context) / note_turn_persisted
(_persist_session funnel): one WARNING naming both turn_ids when a turn
starts before the previous turn's turn-end persist. Ownership transfer
keeps a crashed turn from warning more than once; the unconditional clear
makes the tripwire under-report rather than double-report under a real
overlap. Log-only, no behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 04:56:10 -07:00
sprmn24
9a21d0e3f2 fix(agent): canonicalise paths in parallel-batch planner to prevent same-file concurrent mutation
_extract_parallel_scope_path used Path.cwd() (process cwd) instead of the
tool's actual execution cwd, and os.path.abspath() instead of os.path.realpath(),
so symlink aliases and relative/absolute path pairs that resolve to the same
physical file were treated as distinct targets and placed in the same parallel
segment. On case-insensitive platforms (Windows) os.path.normcase() was also
absent, allowing Foo.txt and foo.txt to race.

Changes:
- agent/tool_dispatch_helpers.py: introduce _canonical_path(raw_path,
  execution_cwd) applying expanduser->abspath->realpath->normcase; thread
  execution_cwd through _extract_parallel_scope_path and
  _plan_tool_batch_segments
- agent/tool_executor.py: pass get_active_env(effective_task_id).cwd as
  execution_cwd to _plan_tool_batch_segments; add pathlib.Path import
- run_agent.py: pass active env cwd to _plan_tool_batch_segments at the
  second call site inside _execute_tool_calls
- tests/run_agent/test_tool_batch_segmentation.py: add 5 regression tests
  covering relative/absolute same target, symlink alias, execution_cwd vs
  process cwd, symlink parent + nonexistent write target, and Windows
  case-insensitive alias (skipped on non-Windows)

Fixes a file-corruption / lost-update race introduced by the mixed
tool-batch segmentation feature (perf commit #64460).
2026-07-16 04:26:32 -07:00
Teknium
eb6aa03609
feat(analytics): record auxiliary model usage per task in session accounting (#65537)
* feat(analytics): record auxiliary model usage per task in session accounting

Auxiliary LLM calls (vision, compression, title_generation, web_extract,
session_search, ...) discarded their token usage, leaving dashboard
analytics blind to aux model spend (issue #23270).

- hermes_state.py: session_model_usage gains a task PK dimension
  (''=main loop) via v22 table-rebuild migration (SQLite can't alter a
  PK); record_auxiliary_usage() writes per-(model,provider,task) deltas
  WITHOUT touching sessions counters (gateway overwrites those with
  absolute main-loop totals — folding aux in would double-count or be
  clobbered). Aux rows never inherit the session's main-loop route.
- agent/aux_accounting.py: ContextVar ambient accounting context
  (mirrors the portal_tags conversation context); record_aux_usage()
  normalizes usage via usage_pricing.normalize_usage, estimates cost,
  and is strictly best-effort. moa_reference/moa_aggregator excluded —
  conversation_loop already folds MoA usage+cost into the main delta.
- agent/auxiliary_client.py: _validate_llm_response is the recording
  chokepoint — every successful non-streaming aux response passes
  through it exactly once, sync and async, including fallback paths
  (model read from the response itself stays accurate across
  fallbacks).
- run_agent.py: run_conversation publishes/resets the accounting
  context; agent/title_generator.py republishes on its bare thread.
- hermes_cli/web_server.py: /api/analytics/usage folds aux rows into
  by_model (aux-only models finally appear) and adds a by_task
  summary; /api/analytics/models surfaces aux rows on the Models page.

Design per review of PR #62850 by @eeksock (thread-local + separate
auxiliary_usage table): rebuilt on ContextVar (async-safe — thread-local
cross-attributes concurrent coroutines on one event loop) and the
existing session_model_usage table instead of a parallel accounting
path, extended beyond vision to every aux task, and wired the analytics
endpoints so the dashboard actually shows it. Credit to @eeksock for
the approach and @tboatman for the detailed root-cause analysis.

* test(moa): match _validate_llm_response mock to new accounting-hint signature

* test(aux): accept accounting-hint kwargs in remaining _validate_llm_response mocks
2026-07-16 04:23:12 -07:00
Marcelo
1a323d608e feat: add LM Studio JIT load mode 2026-07-16 15:23:14 +05:30
Teknium
9ce0e67f27 feat(portal): ambient conversation context entangles aux/MoA/delegate calls
Extends the conversation=<id> Portal tag (salvaged from PR #65183 by
@J-SUPHA) from main-loop-only to every LLM call in a conversation:

- agent/portal_tags.py: ContextVar-based conversation context.
  nous_portal_tags() falls back to the ambient id when no explicit
  session_id is passed, so every aux tag site (auxiliary_client,
  chat_completion_helpers summary path, web_tools) inherits the tag
  with zero per-call-site plumbing. Ambient id wins over explicit
  per-segment ids since it carries the lineage root.
- hermes_state.py: SessionDB.get_conversation_root() — public wrapper
  over the lineage walk; returns the ROOT session id, so one
  user-facing conversation keeps a single conversation= value across
  context-compression rotation, and delegate subagent trees tag as
  their parent conversation.
- run_agent.py: run_conversation() publishes the root id for the turn
  and resets it in finally. _conversation_root_id() resolves via
  _parent_session_id for subagents.
- agent/moa_loop.py: MoA reference fan-out workers now run under
  propagate_context_to_thread so advisor slots attribute to the acting
  conversation (also fixes approval-callback propagation on that path).
- agent/title_generator.py: bare title thread republishes the context
  from its session id (spawned after turn reset).

Tests: ContextVar semantics, cross-context isolation, thread-hop
propagation, lineage-root resolution incl. cycle guard.
2026-07-16 01:13:43 -07:00
Burgunthy
e8b7ce8c19 fix(session): persist profile_name and route batch key by profile
Two follow-ups observed after deploying profile routing:

1. sessions.profile_name was NULL even when the agent ran inside the
   routed profile scope. _insert_session_row never wrote it,
   get_or_create_session / reset_session never passed it through, and
   the agent-side _ensure_db_session fallback had no way to read it.
   - Declare profile_name TEXT in SCHEMA_SQL so _reconcile_columns
     auto-adds it on existing DBs.
   - _insert_session_row takes profile_name and writes it.
   - SessionStore passes source.profile (or old_entry.origin.profile
     on reset) into db_create_kwargs.
   - _ensure_db_session reads the active profile via
     get_active_profile_name() inside _profile_runtime_scope.

2. DiscordAdapter._text_batch_key called build_session_key without
   profile=, so the batch key always landed in agent:main even when
   the routed profile differed — diverging from the agent session
   key namespace (agent:crypto-trader, agent:ai-expert, ...).
   Pass event.source.profile through so both namespaces agree.

Live verification (jth-server-2, 2026-06-28): a test message in a
routed #coin thread produced agent:crypto-trader:discord🧵...
in the batch log and profile_name=crypto-trader in the sessions
row. Default-routed chat still produced agent:main / NULL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-15 09:50:05 -07:00
Teknium
569b912d7d
feat(agent): explain long provider waits on the live status line (#64775)
Community reports of GPT 'infinitely thinking' are usually a slow or
overloaded provider plus silent retry machinery: the CLI/TUI/Desktop
spinner shows a generic 'cogitating...' verb for the whole wait and the
gateway heartbeat says only 'Working — N min'.

Add AIAgent._emit_wait_notice(): rewrites the live spinner/status line
(thinking_callback → CLI prompt_toolkit widget, thinking.delta → TUI +
Desktop) and updates the activity tracker (included in the gateway's
' Working — N min' heartbeat). Wired at the four wait points:

- non-streaming wait loop: after 30s with no response, the line becomes
  ' waiting on <model> — Ns with no response yet (provider may be slow
  or overloaded; auto-reconnect at Ns)'
- streaming wait loop: same explanation after 30s with no chunks,
  including the long-thinking case
- TTFB / stale-stream kills: '⚠ no response from provider in Ns —
  reconnecting...'
- Codex continuation retries: '↻ model returned reasoning with no final
  answer — asking it to continue (n/3)' instead of silence

Notices are fail-open (display errors never break the wait loop) and
gateway sessions without a display callback still get the improved
activity description.
2026-07-15 00:14:05 -07:00
Teknium
271a9d8ec6
perf(agent): segment mixed tool batches to recover lost concurrency (#64460)
A model response containing several parallel-safe reads plus one unsafe
tool used to lose ALL concurrency: _should_parallelize_tool_batch was
all-or-nothing, so a single barrier call (terminal, clarify, unknown
tool, malformed args) forced the entire batch onto the sequential path.

_plan_tool_batch_segments now splits the batch into ordered segments:
maximal contiguous runs of parallel-safe calls execute on the existing
concurrent path, barrier calls on the sequential path, strictly in the
model's emission order. Invariants preserved:

- one tool result per call, appended in emission order (segments are
  contiguous, so no result reordering across a barrier)
- side-effect boundaries: no call starts before an earlier barrier ends
- overlapping file targets split into separate ordered parallel runs
- turn-end budget enforcement + /steer injection run exactly once per
  batch (segment executors run with finalize=False; the segmented
  dispatcher owns the whole-turn finalize)
- interrupt during segment k drains segments k+1..n with cancelled
  results, keeping one result per tool_call_id

Homogeneous batches keep their original single-path dispatch (zero
behavior delta); _should_parallelize_tool_batch remains as a thin view
over the planner for existing callers and tests.
2026-07-14 11:53:05 -07:00
kshitij
0684506072
Merge pull request #64004 from kshitijk4poor/salvage/63274-cli-close-persist
fix(cli): persist close transcript without history alias
2026-07-14 16:45:35 +05:30
mdc2122
03fbf6edbb fix(kanban): nudge workers that exit without complete/block
Add a bounded turn-end stop guard for kanban workers. When a worker
tries to exit with finish_reason=stop without having called
kanban_complete or kanban_block, inject up to two synthetic nudges
so the conversation loop continues instead of exiting cleanly (which
the dispatcher records as protocol_violation).

Mirrors the existing verify-on-stop pattern: same ephemeral scaffolding
flag (_kanban_stop_synthetic), same role-alternation contract, same
_pending_verification_response fallback for budget exhaustion.

Disabled by default (gated on HERMES_KANBAN_TASK env var set by the
dispatcher); kill switch via HERMES_KANBAN_STOP_NUDGE=0.

Salvaged from #62262 by @mdc2122. The original branch was 272 commits
behind main with ~538 files of stale-base reversions; this salvage
applies only the 4 substantive files (agent/kanban_stop.py,
conversation_loop.py insertion, run_agent.py _EPHEMERAL_SCAFFOLDING_FLAGS,
tests/agent/test_kanban_stop.py).
2026-07-14 16:34:00 +05:30
kshitijk4poor
8341d775a9 fix(session): restore clean API-local turn content 2026-07-14 03:32:46 +05:30
kshitijk4poor
962189d9ea fix(cli): clear stale persistence override before staging 2026-07-14 03:32:46 +05:30
kshitijk4poor
50aebcbcff fix(session): preserve clean shortened close snapshots 2026-07-14 03:32:46 +05:30
kshitijk4poor
69fd846ef8 fix(session): serialize direct persistence flushes 2026-07-14 03:32:46 +05:30
kshitijk4poor
0b422559f3 fix(session): preserve clean multimodal persistence override 2026-07-14 03:32:45 +05:30
kshitijk4poor
475922f2ce fix(cli): serialize close persistence handoff
Preserve one durable staged input across terminal close and the worker's early turn flush, without duplicating resumed transcripts or creating a session with a null prompt. Fixes #63766.
2026-07-14 03:32:45 +05:30
kshitijk4poor
47c91e4c34 fix(cron): abort inline requests on timeout 2026-07-11 11:07:34 +05:30
Teknium
5ba2d167ba
Revert "fix(agent): release pool FDs on owning-thread client close (#61979)" (#62141)
This reverts commit cd7a8dfde0.
2026-07-10 19:12:32 -07:00
brooklyn!
291eae63b7
Merge pull request #62016 from NousResearch/bb/desktop-vibe-hearts
feat: vibe reactions — floating hearts on affection, across CLI/TUI/desktop
2026-07-10 16:14:27 -05:00
giggling-ginger
cd7a8dfde0 fix(agent): release pool FDs on owning-thread client close (#61979)
force_close_tcp_sockets stayed shutdown-only after #29507 to avoid
cross-thread FD recycle. That left CLOSED sockets unreclaimed when
httpx.close() skipped already-shutdown sockets under long-lived
gateways (~1 CLOSED fd / 6 min via proxy).

Add release_fds= for the owning-thread dispose path only; abort still
defaults to shutdown-only.
2026-07-10 07:28:39 -07:00
Brooklyn Nicholson
422d9da9bd feat(agent): core affection reaction detector + reaction_callback
Add a token-free, curated affection matcher (agent/reactions.py) — the single
source of truth for detecting user "vibes" (ily / <3 / good bot / heart emoji).
No model call, no tokens. Generalized to return a reaction *kind* so future
reactions can ride the same signal.

Wire an opt-in AIAgent.reaction_callback that fires from build_turn_context on
the incoming user message. It never touches the conversation (cache-safe) and
never fatal — a purely cosmetic side-beat each host can consume.
2026-07-10 05:41:59 -05:00
Umi4Life
3fe7f6d27a fix: preserve fallback switch notice on successful fallback
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 13:40:19 +05:30
Kshitij Kapoor
e0ed5dc9ed refactor: address Phase-2 review findings on /new boundary handoff
- Return the boundary snapshot from
  _launch_session_boundary_memory_flush as a local value instead of
  staging it on self._session_boundary_snapshot. The instance-attr
  handoff could leak (no memory manager configured) or mis-fire a
  stale snapshot on a later /new if an exception hit between staging
  and consumption. A local variable eliminates the class; the helper
  also returns None when no memory manager is configured so
  new_session takes the inline-switch path.
- Drop the now-dead session_id kwarg from commit_memory_session:
  after the redesign no production caller passes it (gateway, TUI,
  compression all use the default), and speculative params are
  rejected per AGENTS.md. The explicit-old-session need is served by
  cli.py's direct engine call + commit_session_boundary_async.
- Drop the dead providers snapshot in commit_session_boundary_async
  (only the emptiness check used it).
- Tests updated accordingly (dead-kwarg test removed, snapshot
  assertion now covered by return-value contract).

Phase-2 gates: 2a tests/cli 1048 passed + 6 memory files 137 passed;
2b programmatic live smoke 0.38ms non-blocking caller, end→switch→sync
ordering verified; 2c structured 4-angle review — no Criticals, these
warnings fixed.
2026-07-09 03:21:54 +05:30
Tosko4
2a293319b1 fix: run CLI new-session memory flush off-thread
(cherry picked from commit 3e82a861e6)
2026-07-09 03:21:54 +05:30
gauravsaxena1997
0569a637d0 fix(agent): guard response.text access in _summarize_api_error against httpx.ResponseNotRead
When an API error carries an httpx.Response whose body was consumed via
iter_bytes() during streaming error handling (e.g. GeminiAPIError from
agent/gemini_native_adapter.py), accessing .text raises
httpx.ResponseNotRead. The secondary exception replaced the real,
already-computed provider error (429 free-tier quota guidance) with the
generic 'Attempted to access streaming response content' message on
every turn.

Guard the .text access so it degrades to an empty snippet and falls
through to the str(error) fallback, which carries the full original
message. Mirrors the existing guards in
agent/error_classifier.py::_extract_error_body() and
agent/gemini_native_adapter.py::gemini_http_error().

Fixes #59769

Salvaged from PR #59868 (guard + regression test); the unrelated
desktop Ctrl-C fix bundled in that PR was intentionally dropped and is
triaged separately.
2026-07-09 02:01:56 +05:30
Teknium
b64b802131
feat(models): swap curated Tencent Hy3 Preview for GA tencent/hy3, drop owl-alpha (#60943)
- OPENROUTER_MODELS: remove openrouter/owl-alpha (free) and
  tencent/hy3-preview{,:free}; add tencent/hy3 and tencent/hy3:free
- _PROVIDER_MODELS[nous]: tencent/hy3-preview -> tencent/hy3
- run_agent.py reasoning-prefix list: tencent/hy3-preview -> tencent/hy3
  (prefix match still covers -preview if pinned)
- model_metadata: register hy3 context length (262144) alongside hy3-preview
- regenerate website/static/api/model-catalog.json
- update tokenhub curated-list tests to the new IDs

The tencent-tokenhub direct provider still serves hy3-preview and is
intentionally unchanged.
2026-07-08 07:09:08 -07:00
Teknium
ba31699091
chore(providers): remove dead cloudcode-pa quota-fallback branches (#51489)
The google-antigravity and google-gemini-cli OAuth providers were removed
in #50492. They were the only producers of a cloudcode-pa:// base_url, so
the account-level-quota early-returns in _pool_may_recover_from_rate_limit
and _credential_pool_may_recover_rate_limit are now unreachable.

- Drop the dead cloudcode-pa:// checks and the now-unused provider/base_url
  params on _pool_may_recover_from_rate_limit (only caller updated).
- Prune the obsolete CloudCode-specific regression tests; keep the live
  single/multi-entry pool-rotation invariants (#11314).
2026-07-05 13:44:33 -07:00
David Metcalfe
8324dd19ca fix(agent): replace custom socket_options transport with httpx pool-level keepalive expiry
The custom ``httpx.HTTPTransport(socket_options=[SO_KEEPALIVE, ...])``
in ``_build_keepalive_http_client()`` was introduced to fix CLOSE-WAIT
socket accumulation on long-lived connections (#10324).

That approach broke streaming for providers behind reverse proxies
(OpenResty, Cloudflare, etc.) because the custom socket options
conflict with the proxy's chunked-transfer handling (#54049, #12952).
It also stripped TCP_NODELAY, stalling TLS handshakes and SSE encoding.
Narrow per-provider bypasses were added for Copilot (#50298), Codex
(#36623, #12953), but the root cause remained.

The fix moves connection lifecycle management from the socket layer to
the HTTP pool layer:

- ``httpx.Limits(keepalive_expiry=20.0)`` tells httpx to close idle
  pooled connections at 20 s, before a reverse proxy's typical 30-60 s
  timeout drops them and causes CLOSE-WAIT accumulation.
- The default httpx transport preserves OS TCP defaults (including
  TCP_NODELAY), so TLS handshakes and SSE chunked encoding work
  correctly.
- ``trust_env=False`` prevents httpx from double-dipping on env vars
  (we handle proxy detection ourselves via ``_get_proxy_for_base_url``
  which respects NO_PROXY).
- The Copilot host bypass (line 3632) is no longer needed since all
  providers now use the same standard httpx.Client.

Closes #54049.  Supersedes #12010, #36623, #12953, #50298.
2026-07-05 03:14:55 -07:00
Teknium
6f052b7ff1
fix(copilot): set x-initiator per turn so user prompts bill as premium requests (salvage #4097) (#58544)
* fix(cli): set correct x-initiator header per Copilot turn

copilot_default_headers() always hardcoded x-initiator: agent, but
GitHub Copilot billing requires "user" for user-initiated prompts and
"agent" for tool/follow-up calls. This caused premium requests to never
be consumed correctly, risking billing issues or account bans.

Adds is_agent_turn param to copilot_default_headers() and injects
extra_headers={"x-initiator": "user"} on the first API call of each
user turn when targeting Copilot URLs. The flag flips to False after
injection so subsequent calls (tool use, streaming fallback) default
back to "agent".

Fixes #3040

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(release): add AUTHOR_MAP entry for @tjp2021 (PR #4097 salvage)

---------

Co-authored-by: Tim <tim@iteachyouai.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-05 00:44:15 -07:00
Eugeniusz Gilewski
e4dbb67bf5 fix(security): remove model-controlled delegate ACP transport
Source: https://github.com/NousResearch/hermes-agent/pull/52346
Related prior work: https://github.com/NousResearch/hermes-agent/pull/39462
Related prior work: https://github.com/NousResearch/hermes-agent/pull/27426
Maintainer direction: https://github.com/NousResearch/hermes-agent/pull/52346#issuecomment-4854881612

Remove acp_command and acp_args from the model-facing delegate_task schema and
dispatch paths. Child agents can still use ACP subprocess transport when it
comes from trusted delegation config or parent inheritance, but a model tool
call can no longer choose the command or arguments that reach child
construction.

This is salvageable because the risky boundary is model control over child ACP
transport, not ACP itself. The patch follows the maintainer direction from the
source discussion by preserving trusted ACP configuration and prior integration
work while removing the untrusted tool-call fields from both top-level and
per-task delegate inputs.

Reproduced on main by passing acp_command through delegate_task and observing it
reach _build_child_agent. Verified after the fix that model dispatch strips the
hidden top-level fields and per-task hidden fields are ignored before child
construction.

Co-authored-by: Carlosian <claudlos@agentmail.to>
Co-authored-by: ssiweifnag <120658181+ssiweifnag@users.noreply.github.com>
Co-authored-by: nikshepsvn <23241247+nikshepsvn@users.noreply.github.com>
2026-07-03 03:27:47 -07:00
Jaaneek
5ef0b8acb0 feat(auth): make xAI Grok OAuth device-code-only, drop loopback login
Replace the loopback/PKCE-callback server and manual-paste fallback with
the RFC 8628 device-code flow as the only xAI Grok OAuth login path. The
flow works in headless/SSH/container sessions with no 127.0.0.1 listener,
shrinking the local attack surface.

- Poll the token endpoint with server-provided interval, honoring
  slow_down and expires_in; store tokens with auth_mode
  oauth_device_code.
- Adaptive proactive refresh skew for short-lived device-code JWTs;
  rotated tokens sync back to auth.json, the global root store, and the
  credential pool (no refresh-token replay).
- Clear source suppression on successful re-login (CLI + dashboard) and
  drop the duplicate dashboard pool entry so exactly one seeded
  device_code entry exists.
- Use the shared device_code source name for consistency with the
  nous/codex device-code providers.
- Desktop: remove the loopback OAuth flow states and dead type variants;
  pkce providers' sign-in URL selection is unchanged.
- Docs (EN + zh-Hans) rewritten for device-code login; drop the deleted
  --manual-paste flag from documented commands.
2026-07-02 13:17:41 -07:00
Jneeee
b98baa3039 feat(config): extra HTTP headers for LLM API calls (#3526 salvage)
Named providers / custom_providers entries in config.yaml now accept an
extra_headers dict scoped to that endpoint — for reverse proxies, API
gateways, and custom auth schemes (e.g. Cloudflare Access service tokens).

- hermes_cli/config.py: normalize extra_headers on provider entries
  (_normalize_custom_provider_entry + providers-dict translation), add
  get_custom_provider_extra_headers /
  apply_custom_provider_extra_headers_to_client_kwargs helpers keyed on
  base_url (case/trailing-slash insensitive, no substring bypass —
  mirrors the TLS helpers)
- hermes_cli/runtime_provider.py: surface extra_headers in the resolved
  runtime for named custom providers (providers dict, legacy
  custom_providers list, and the credential-pool path)
- run_agent.py / agent/agent_init.py: merge per-provider extra_headers
  onto the OpenAI client default_headers at construction and on every
  _apply_client_headers_for_base_url re-application (credential swaps,
  rebuilds), most-specific level wins; OpenAI-wire only (native
  Anthropic/Bedrock scoped out)
- agent/auxiliary_client.py: accept model.extra_headers as an alias of
  model.default_headers for the global variant
- cli-config.yaml.example: documented commented example
- Header values are treated as secrets and never logged

Salvaged from PR #3526 by @jneeee, reimplemented against current main.

Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
2026-07-02 05:33:25 -07:00
kshitijk4poor
676236bb1d fix(agent): honor custom CA certs on aux client + harden TLS resolution
The salvaged fix wired per-provider ssl_ca_cert / ssl_verify (and
HERMES_CA_BUNDLE) into the MAIN OpenAI client. This follow-up:

- Auxiliary client parity: process_bootstrap.build_keepalive_http_client
  accepts and forwards verify; auxiliary_client._resolve_aux_verify mirrors
  the main-client TLS resolution (via load_config_readonly, the read-only
  fast path) so compression/vision/web_extract/title-gen/session_search
  honor the same per-provider CA. Without this, chat worked against a
  private-CA endpoint but every auxiliary call still failed APIConnectionError.
- switch_model now reads custom_providers from live config (load_config_readonly)
  instead of the init-time agent._custom_providers snapshot, so ssl_ca_cert /
  ssl_verify edits are honored on mid-session model switch — matching the
  context-length reload (#15779).
- Drop the dead client-level verify= where a custom httpx transport is used
  (httpx ignores it there); verify lives on the transport. Fix docstrings.
  Applies to both run_agent._build_keepalive_http_client and process_bootstrap.
- resolve_httpx_verify: add CURL_CA_BUNDLE to the env chain (consistency with
  agent/ssl_guard._CA_BUNDLE_ENV_VARS) and emit a loud logger.warning naming
  the endpoint whenever ssl_verify:false disables verification.
- get_custom_provider_tls_settings: case-insensitive base_url match (config
  dedup already lowercases; scheme/host are case-insensitive) so a mixed-case
  entry doesn't silently drop its CA. Exact match preserved — no prefix bypass.
- Demote best-effort except Exception: pass in agent_init/switch_model to
  logger.debug(exc_info=True).
- Tests for aux verify forwarding, _resolve_aux_verify, case-insensitive
  match, and prefix-bypass rejection.
2026-07-02 04:51:56 +05:30
HexLab98
3a2ba959ce fix(agent): honor custom CA certs for custom_providers HTTPS endpoints
Wire ssl_ca_cert and ssl_verify through custom_providers config and env
vars into the keepalive httpx client, fixing APIConnectionError against
mkcert/self-signed Ollama proxies behind HTTPS.
2026-07-02 04:51:56 +05:30
Teknium
ba0bc01d1f
feat(delegate): remove model-facing toolsets arg — subagents always inherit parent's (#56386)
The model could pass `toolsets` (top-level and per-task) to delegate_task,
letting it choose which toolsets a subagent got. Toolset selection is a
capability-scoping decision the model should not control; subagents inherit
the parent's enabled toolsets, period.

- Remove `toolsets` from the delegate_task() signature, the registry handler,
  the top-level + per-task JSON schema, and the live dispatch path
  (run_agent._dispatch_delegate_task — this forwarded it on every model call).
- Single-task and per-task child builds now pass toolsets=None so
  _build_child_agent resolves to pure parent inheritance.
- Drop the now-dead _SUBAGENT_TOOLSETS / _TOOLSET_LIST_STR schema-hint block.
- _build_child_agent keeps its internal toolsets param + intersection helpers
  (internal API; fed the inherited value only).
- Tests: schema assertions flipped to assertNotIn; added a regression test
  proving the dispatch path never forwards a smuggled model `toolsets`.
- Docs: update delegate_task signature refs in the autonomous-ai-agents skill.
2026-07-01 05:35:26 -07:00
Steve Lawton
c73e74386b feat(vertex): add Google Vertex AI provider for Gemini (OAuth2)
Adds Vertex AI as a first-class provider for Gemini models via Vertex's
OpenAI-compatible endpoint. Vertex authenticates with short-lived OAuth2
access tokens (service-account JSON or ADC), not a static API key — the
missing piece behind the recurring requests (#13484, #12639, #56259).

- agent/vertex_adapter.py: OAuth2 token minting + refresh-on-expiry
  (5-min margin), ADC->service-account fallback, global vs regional
  endpoint URLs. Config precedence: env var > config.yaml > default.
- plugins/model-providers/vertex/: provider profile (auth_type=vertex),
  reuses Gemini's extra_body.google.thinking_config translation.
- runtime_provider: vertex short-circuit BEFORE the credential pool so a
  credentials-file path is never mistaken for a static API key; mints a
  fresh token + computes base_url per resolve.
- run_agent + conversation_loop: _try_refresh_vertex_client_credentials()
  re-mints the token and rebuilds the client on a mid-session 401, so a
  long-lived gateway agent survives token expiry (~1h).
- auxiliary_client: vertex auth_type branch for side-LLM tasks.
- config.yaml: vertex.project_id / vertex.region (non-secret, bridged to
  env); credential path stays in .env (VERTEX_CREDENTIALS_PATH).
- setup wizard + model picker: dedicated _model_flow_vertex; curated
  google/gemini-* model list; --provider choices.
- pricing/metadata: Vertex prices off the gemini docs snapshot; endpoint
  host auto-maps to the vertex provider (no probe spam).
- lazy_deps + pyproject [vertex] extra: google-auth, opt-in only.
- docs: guides/google-vertex.md + providers page; tests for adapter +
  runtime resolution.

Salvages and modernizes #8427 by @slawt onto current main: rewired from
the legacy PROVIDER_REGISTRY path to the provider-profile architecture,
moved non-secret config out of .env into config.yaml, and added the
per-turn 401 token-refresh the original lacked.
2026-07-01 05:25:33 -07:00