Commit Graph

380 Commits

Author SHA1 Message Date
Teknium 55f9e472a0 perf(cli): sub-400ms warm startup — probe-mode check_fns, lazy MCP SDK, banner snapshot, parallel worktree add
Cold CLI time-to-banner was ~1.8s (hermes) / ~2.8s (hermes -w). The banner
path was paying for work the session doesn't need before first input:

- aux availability probes built REAL OpenAI/httpx clients (openai import
  ~0.3s + SSL context) just to answer check_fns. New aux_probe_mode()
  returns a cache-excluded stub; resolution policy unchanged.
- tools/mcp_tool imported the mcp SDK (~260ms, mcp.types pydantic model
  construction) at module import even with zero MCP servers configured.
  SDK import is now lazy behind _ensure_mcp_sdk(); _MCP_AVAILABLE is a
  find_spec probe so every existing gate/test keeps its semantics.
- banner blocked 500ms on the update-check prefetch; now waits 50ms and
  defers the warning line to a daemon thread (prints above the prompt).
- banner recomputed get_tool_definitions + skills scan + git state every
  launch; now snapshotted to ~/.hermes/cache/banner_snapshot.json keyed on
  (config.yaml, .env, checkout rev, toolsets) and replayed on warm launches
  with a background refresh. Agent tool list is still computed fresh.
- _resolve_active_context_length probed the Nous portal /models (~200ms
  network) per launch; the tool-search gate now prefers the on-disk
  context cache when present.
- schema reconciliation re-executed SCHEMA_SQL in a scratch SQLite DB
  (~85ms) per SessionDB(); the reference parse is now disk-memoized by
  DDL hash (live-DB diffing still runs every startup).
- bundled-skills sync (~120-170ms rglob/hash) moved off the startup path
  to a daemon thread; plugin discovery starts in the background and every
  synchronous consumer joins via discover_plugins().
- hermes_cli.auth imported httpx eagerly (~30ms); now a lazy proxy that
  test monkeypatching still reaches (setattr forwards to the real module).
- fast chat launch: unambiguous 'hermes'/'hermes chat' invocations skip
  building all ~40 subcommand parsers (bails to full dispatch on anything
  else, incl. container mode).
- -w path: git worktree add runs with checkout.workers=8 (0.6s→0.2s) and
  overlaps HermesCLI construction; --skills preload runs in the background
  and is folded in at agent init (finalize_preloaded_skills, same
  fail-loud contract for fully-unknown skill lists); stale-worktree prune
  moved off the banner path.

Warm results (PTY time-to-banner, 5-run): hermes 1.80s → 0.38-0.40s;
hermes -w -s hermes-agent-dev --yolo 2.82s → 0.57-0.69s.
2026-08-10 10:40:19 -07:00
Brooklyn Nicholson 53a4003208 fix(agent): stop titling a session after our own scaffolding, or a TTS model
Two lookalike gaps found auditing the titler.

_MACHINE_PREFIXES missed the compressor's legacy summary opener and the
"[System note:" injections, so a compacted or resumed session could be
named after the note that carried it. Take the summary prefix from the
compressor that emits it rather than keeping a fourth local copy.

The fast-model exclude list covered embedders but not the other non-chat
siblings a provider names after its chat model — "gpt-4o-mini-tts"
satisfies the "-mini" rung and cannot answer a prompt.
2026-08-09 04:33:58 -05:00
Brooklyn Nicholson 071eab821b fix(models): let the titler actually see a provider's model catalog
The fast-model picker reads /v1/models to find the small model a provider
currently serves, and it asked anonymously. Most of those endpoints need a
key, so the fetch 401'd and the empty result read as "this provider has no
small model" — the picker fell back to its curated list and never noticed.

Worse, a failed fetch cached its empty result forever, so one bad moment
during startup disabled live model discovery for the life of the process,
and the processes that read this run for weeks. Give the failure an expiry
and pass the provider's credentials.

The bare family rungs (-mini, -flash, haiku) also picked whichever id
sorted first, which is the oldest generation a provider still serves:
gpt-3.5-mini over gpt-5.4-mini, claude-3-haiku over claude-haiku-4.5.
Compare the digit runs as numbers so the rung meant to keep us current
does.
2026-08-09 04:33:58 -05:00
Brooklyn Nicholson e358eaf44a perf(sessions): resolve the titling model from the provider's live catalog
Titling ran on the user's main chat model, so a five-word title was billed
to a frontier reasoning model and inherited its latency. Pinning a cheap
model id instead just moves the problem: the hardcoded default was already
dead upstream and every call paid a 404 before the retry net caught it.

Match model FAMILIES against the provider's live /v1/models catalog,
preferring rolling '-latest' aliases where a provider publishes them, and
order the families by measured latency. Nothing to bump when a provider
ships a new mini/flash/haiku. Opt-in per task, so compression, vision, and
search keep 'auto means my chat model'.
2026-08-08 17:07:21 -05:00
Nolan 2fda6a384c fix(auth): cover remaining auth.json readers across modules
Follow-up to the auth.json UTF-8 read fix in this PR. A repo-wide scan for
the same bug class found three more callers that read ~/.hermes/auth.json
via Path.read_text() with no encoding — same Windows cp1252 hazard:

- agent/auxiliary_client.py _read_nous_auth: a non-ASCII byte raised
  UnicodeDecodeError, the broad except swallowed it, and Nous silently
  stopped being available as the auxiliary (vision/summarization) provider.
- tools/xai_http.py has_xai_credentials: same failure mode — xAI OAuth
  silently looked absent on Windows.
- hermes_cli/main.py is_setup_complete: same; has a config.yaml fallback so
  the impact is milder, but the read is still wrong.

All three now use read_text(encoding="utf-8-sig"), matching _save_auth_store's
write encoding. A repo-wide grep confirms there are no remaining
json.loads(...read_text()) reads of auth.json without an explicit encoding.

Tests: rewrote the Windows-encoding regression tests to actually exercise the
bug on POSIX too — a new windows_default_encoding fixture forces a no-encoding
read_text() to decode as cp1252 (the Windows default), and _write_utf8 now
emits real non-ASCII UTF-8 bytes (ensure_ascii=False) so the bytes actually
trip cp1252. Verified each test fails when its fix is reverted (including
the two new sibling-reader tests).
2026-08-08 12:32:23 -07:00
kshitij c95a1b7171 fix(auxiliary): widen effective provider to relay, logging, and endpoint detection
Thread request_provider through the sibling callsites that the
original PR left on resolved_provider: _set_relay_auxiliary_route
(observability metadata), the 'using X' log line, _is_anthropic_compat_endpoint
(Anthropic image conversion), _provider_requires_stream (streaming
detection), and the initial _relay_sync/async_completion calls.

These are NOT regressions (they passed 'auto' before), but widening
them ensures auto-routed calls to MiniMax get correct image conversion,
streaming-only providers are detected, and observability metadata
records the concrete backend instead of 'auto'.
2026-08-07 16:56:37 +05:30
Gille 293e67328c fix(agent): preserve auto-routed provider identity 2026-08-07 16:56:37 +05:30
Justin Bennington a9acb400ba feat(providers): add Actual Computer inference provider 2026-08-05 14:08:32 -07:00
joaomarcos 34c3f06f91 fix(cache): scope prompt_cache_key by session to stop cross-session bucket sharing
Cherry-picked from PR #78959 by @JoaoMarcos44 with authorship preserved.
Follow-up: hoist _cache_scope_from_session_id(session_id) to a local in
build_kwargs so it's computed once instead of 4 times per call.

Closes #78941. Closes #79012. Closes #79013. Closes #79014. Closes #79015.

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
2026-08-05 12:42:46 +05:30
Jeffrey Quesnelle 5943bab1ec
Merge branch 'main' into feat/hermes-relay-model-metrics 2026-08-04 12:07:51 -04:00
kshitijk4poor ddae511ab1 fix: thread extra_headers through the call_llm split
The PR's concurrency wrapper splits call_llm into a semaphore-guarded
entry + _call_llm_impl; main added extra_headers to call_llm's
signature after the PR's base, so the split has to forward it too
(dropped silently otherwise — Azure Foundry and custom-endpoint
callers set it).
2026-08-03 23:01:03 +05:30
konsisumer 23f8ae32c0 fix(agent): cap auxiliary LLM concurrency per task 2026-08-03 23:01:03 +05:30
Alex Fournier a97abcd55a Merge upstream main into model metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>

# Conflicts:
#	tests/agent/test_auxiliary_relay.py
2026-08-02 20:10:47 -07:00
Teknium ca5ce1110b fix(secrets): route auxiliary-client provider key reads through the profile secret scope
OPENROUTER_API_KEY (client construction, unavailability description,
max-tokens param heuristic), OPENAI_API_KEY (custom runtime + explicit
custom endpoint), named-custom-provider key_env, and auxiliary task-config
key_env now resolve via a shared _scoped_key_env helper (get_secret with
UnscopedSecretError → os.environ fallback for unscoped CLI/startup probes).
2026-08-02 10:02:33 -07:00
Shaun Prince d15b638a88 fix(compression): let explicit interrupts cancel safely
Makes interrupt-protected context compression cancellable by an explicit
user or lifecycle stop, without weakening protection against ordinary
incoming messages, voice interjections, or active-turn redirects.

Separates explicit hard cancellation from ordinary interrupt/redirect
state with a dedicated threading.Event; introduces
AuxiliaryExplicitCancellation as an attempt-local frozen-cause signal;
isolates the synchronous provider callback in a bounded daemon worker
during protected compression; atomically linearizes Codex timeout
cleanup against explicit cancellation; propagates hard cancellation
through child agents and explicit stop surfaces; serializes hard-cancel
admission against compression commit admission with
CompressionCommitFence; aborts before session rotation or late DB commit,
restores in-place transcript mutations and compressor state, and releases
the heartbeat and compression lease.

Based on #74449 by @suparious. Resolved merge conflicts in
agent/context_compressor.py (feasibility check + try/except) and
tui_gateway/methods_session.py.
2026-08-02 22:15:20 +05:30
kshitij 077317a0f6 simplify: collapse aux config reads, trim comments, use load_config_readonly
- Merge _aux_free_only() + _aux_openrouter_model() into single
  _aux_openrouter_settings() that reads config once via
  load_config_readonly (avoids double deepcopy).
- Remove 15-line block comment and 5-line inline comment that
  restated what the code already says.
- Trim module docstring from 10 lines to 3.
- Update test patches to target load_config_readonly.
2026-08-02 12:19:04 +05:30
webtecnica c19c63d9e6 fix(agent): make auxiliary auto-chain fallback configurable and free-only (#75803) 2026-08-02 12:19:04 +05:30
kshitij 536754919d fix(auxiliary): replan cache sections on the async fallback path too
_call_fallback_candidate_sync replans messages/tools for each resolved
destination, but its async mirror still shipped the caller's decorated
sections verbatim — the primary destination's markers (including a
direct-native tool marker) leaked to fallback candidates with different
cache contracts, and the relay saw the display label instead of the
resolved provider/api_mode. Mirror the sync path: resolve the
destination, replan both sections, thread provider/api_mode through
_relay_async_completion, and replan again for the auth-refresh retry
client. Mutation-checked: the new parity test fails on the verbatim
pass-through shape.

Follow-up to #76032 (#20880).
2026-08-01 15:39:58 +05:30
kshitij af06308425 refactor(prompt-caching): collapse triplicated destination-plan and label parsing
Three copies of the same logic landed with #76032:
- MoA's _call_prepared_aggregator and auxiliary_client's
  _replan_synchronous_cache_sections both implemented stub → policy →
  strip → plan for a resolved destination. Extract
  plan_cache_sections_for_destination() into agent_runtime_helpers (which
  already owns the policy functions) and route both through it. Also
  removes a redundant full-transcript deepcopy+strip per request (the
  caller pre-stripped what build_prompt_cache_plan strips again).
- The fallback_chain[N] label regex + chain-entry lookup lived in
  _fallback_entry_timeout AND _fallback_destination. Extract
  _fallback_chain_entry() and reuse.

MoA's cache-plan failure log is promoted debug → warning: the call-block
site skips MoA, so this block is the aggregator's only decoration path —
a silent failure ships an undecorated request (the 0%-cache MoA bug class).

Behavior-preserving; 195 targeted tests green.

Follow-up to #76032 (#20880).
2026-08-01 15:39:58 +05:30
Rod Boev 9fc12bf7a4 perf(prompt-caching): preserve tool-loop cache boundaries (#20880) 2026-08-01 14:27:12 +05:30
joaomarcos d52a1c25e0 fix(auth): resolve fallback api keys through secret_scope, not raw env
resolve_entry_api_key() and the duplicated _fallback_entry_api_key()
read key_env via a raw os.getenv(), bypassing per-profile secret
scoping in the multiplexed gateway. Under multiplexing this can hand
a fallback request another profile's credential. Both now resolve
through agent.secret_scope.get_secret(), which reads the active
profile scope when multiplexing is on and falls back to os.environ
unchanged when it's off, so single-profile behavior is preserved.

Closes #74311
2026-07-31 22:35:26 -07:00
Kyle 48ded07155
fix(codex): handle completed auxiliary responses 2026-07-31 21:52:36 -07:00
knocksen 716d274f58
test(moa): cover direct provider streaming for Codex-shim aggregators
Narrow the moa_aggregator Relay bypass to CodexAuxiliaryClient and add
coverage that call_llm(stream=True) returns the provider's direct
create() result for Responses-shim clients.

Salvaged from PR #74903 by @liusencomic-cyber.
2026-07-31 21:52:35 -07:00
knocksen 8e191af0fb
fix(moa): stream completed aggregator responses safely
Convert a completed MoA aggregator response into one valid Chat
Completions delta chunk at the MoA facade boundary, normalize completed
message.tool_calls into indexed stream deltas, and classify these local
MoA adapter-shape errors as non-fallback format errors so a local
compatibility bug cannot silently drift the user's MoA route to a
single model (#55933 follow-up).

Salvaged from PR #74903 by @liusencomic-cyber.
2026-07-31 21:52:35 -07:00
AKAZIK-py 654915f187
fix(moa): unwrap completed responses in the auxiliary streaming path 2026-07-31 21:52:35 -07:00
Alex Fournier 0de9b65c23 Merge upstream main into model route metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-31 07:53:12 -07:00
rob-maron 126ff7071b
Portal free user vision fix + flux3 polling improvements (#75448)
* flux3 polling improvments

* poll gap to 4s

* back to 5s

* vision model fix

* minor fix
2026-07-31 10:17:55 -04:00
Alex Fournier 43d29a37c8 fix(observability): include auxiliary model routes
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-31 06:58:41 -07:00
Teknium ad12df6ba4 Revert "remove Vercel AI Gateway and Vercel Sandbox (#33067)"
This reverts commit febc4cfec0.
2026-07-29 19:48:37 -07:00
Gabriel Stoltemberg 59ee85ed50 perf: use load_config_readonly() at read-only call sites in agent/
Salvaged from #56085 (@Stoltemberg), rebased onto current main: sites
main had already converted (credential_pool, auxiliary_client MoA
paths, model_metadata, moa_loop, agent_runtime_helpers) resolve to
main's versions; the remaining ~29 read-only sites across 16 agent/
files swap to the no-deepcopy readonly loader (~135us saved per call).

Full per-site mutation audit performed (every enclosing function read,
escapes traced): 23 SAFE, 5 ESCAPES with read-only consumers, 1 UNSAFE
path (init_agent -> get_compatible_custom_providers -> normalizer
in-place alias writes) fixed by the preceding no-mutate commits, which
make the normalizer copy-safe for ALL callers.
2026-07-29 15:28:15 -07:00
Teknium 63fc810b95 fix(gemini): sweep hardcoded Gemini default models to gemini-3.6-flash (#32360)
gemini-2.5-flash shuts down Oct 16 2026 (Google deprecation schedule)
and gemini-3-flash-preview is superseded. Update every hardcoded
default to the current GA flash model:

- gemini_native_adapter: probe_gemini_tier + _create_chat_completion
  default params; free-tier guidance de-pinned from a specific model's
  RPD number so it doesn't stale again
- auxiliary_client: gemini/kilocode fallback aux models,
  _OPENROUTER_MODEL, _NOUS_MODEL -> google/gemini-3.6-flash
  (verified live on both OpenRouter and Nous portal /models)
- provider plugins: kilocode + vertex default_aux_model
- hindsight memory plugin: gemini provider default
- setup wizard gemini list: 3-flash-preview -> 3.6-flash (matches the
  curated picker catalog)
- tests: aux-client assertions that pinned the old default literal now
  reference the _NOUS_MODEL constant, so the next default bump can't
  break them (change-detector cleanup)

Fixes #32360.
2026-07-28 18:17:56 -07:00
Alex Fournier 14bed44c8c Reapply "feat(observability): integrate NeMo Relay runtime and shared metrics"
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-27 21:10:51 -07:00
Jeffrey Quesnelle 841a5a744a
Revert "feat(observability): integrate NeMo Relay runtime and shared metrics" 2026-07-27 22:28:08 -04:00
Alex Fournier 147e451cc8 Merge upstream main into feat/hermes-relay-shared-metrics 2026-07-27 17:54:42 -07:00
Jaaneek 5cffc53194 fix(xai): send Hermes-Agent User-Agent on chat/completions
Direct tool HTTP calls already identified as Hermes-Agent, but the main
OpenAI-SDK chat path still sent OpenAI/Python. Set Hermes-Agent/<ver> for
api.x.ai clients (xai + xai-oauth) so normal text traffic is attributed correctly.
2026-07-27 17:47:28 -07:00
Alex Fournier 8ac686fa27 Merge remote-tracking branch 'origin/main' into fix/hermes-relay-review-round3
Signed-off-by: Alex Fournier <afournier@nvidia.com>

# Conflicts:
#	agent/chat_completion_helpers.py
2026-07-27 10:11:53 -07:00
rob-maron 02d5e23085 nous portal anthropic wire 2026-07-27 11:53:48 -04:00
Alex Fournier 4fe4b0dca7 Merge origin/main into feat/hermes-relay-shared-metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-27 08:12:37 -07:00
teknium1 39b5965569 refactor(fallback): single owner for backend identity and failure-scoped skips
Every fallback/dedup/skip decision asks one question — 'is this candidate
the same backend as the one that failed, along the axis that failure
invalidated?' — but it was re-implemented inline at six sites across four
subsystems, each comparing whatever string was locally convenient. Each
incident fixed one site while the others kept the bug: #22548, #70893,
#59561, #72468, #62984/#54250/#57584.

agent/backend_identity.py now owns the concept: BackendIdentity (provider /
model / base_url axes), FailureScope (MODEL / CREDENTIAL / ENDPOINT — each
failure class invalidates a different axis), and should_skip_candidate().
Unknown axes never manufacture a skip (over-skipping strands failover; a
wrong try costs one RTT).

Migrated sites:
- chat_completion_helpers.try_activate_fallback: replaces the provider+model
  early-exit (the #62984 bug: ignored base_url, stranding multi-endpoint
  pools) AND _fallback_entry_is_same_backend_by_base_url (deleted)
- auxiliary_client._try_configured_fallback_chain +
  _try_main_agent_model_fallback: replace label/model comparisons; auth and
  payment map to CREDENTIAL scope, keeping the #59561 carve-out
- hermes_cli/fallback_cmd add: primary-match + duplicate checks now identity-
  aware (#54250/#57584): same provider+model on a different explicit
  base_url is a pool entry, not a duplicate

_mark_provider_unhealthy stays label-keyed deliberately: its only triggers
are confirmed 402s, which ARE credential-scoped.

Owner-level tests pin each incident's semantics by number; sabotage-verified
(removing the base_url axis fails the #62984 test).
2026-07-26 23:41:54 -07:00
teknium1 0a2c245cd6 fix(auxiliary): reach the main agent model when a sibling aux model fails on the same provider
Widen okalentiev's failed_model narrowing (#59561) to
_try_main_agent_model_fallback. The safety-net layer still skipped on a
provider-label match alone, so single-provider users whose aux compression
model and main model share one custom endpoint had ZERO fallbacks: the aux
model timing out exhausted the chain in one hop and compression aborted.

Real incident (0.19.0 debug dump): aux zai-org/glm-5.2 hung 324s and timed
out while main mindai/macaron-v1-venti on the SAME endpoint was serving
448K-token turns — the label-only skip discarded the one viable summarizer,
the session wedged over threshold, and the anti-thrash breaker tripped.

Same convention as the chain fix: model-specific failures (timeout,
connection, rate limit) pass failed_model so only the exact failed model is
skipped; provider-wide failures (auth 401 / payment 402) pass None and keep
the whole-provider skip. Both sync and async call_llm sites pass it.

Sabotage-verified: the new regression test fails on the provider-only skip.
2026-07-26 22:33:06 -07:00
Oleksii Kalentiev 83f20e07e0 fix(auxiliary): keep provider-wide skip for auth/payment failures in fallback chain
Follow-up to the same-provider fallback fix: narrowing the configured-chain
skip to the exact failed model is only correct for model-specific failures.
Auth (401) and payment (402) errors are provider-wide — every model on the
provider shares the same broken credentials/account — so trying a sibling
model can't recover and merely burns another doomed request before the
aux task fails. Worse, returning that sibling client bypasses the
main-agent-model safety net that a provider-wide skip would have reached.

Only forward failed_model to _try_configured_fallback_chain for
model-specific failures (timeout, connection, rate limit, model-incompatible,
invalid response). Auth/payment keep failed_model=None (whole-provider skip),
preserving the pre-existing safety-net behaviour for credential/billing
failures.

Adds an integration test that a timeout forwards the failed model, and
updates the payment-error test to assert failed_model=None.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 22:33:06 -07:00
Oleksii Kalentiev e8f8b34b0c fix(auxiliary): don't skip sibling models when a configured fallback_chain reuses the same provider
_try_configured_fallback_chain skipped every fallback_chain entry whose
provider matched the one that just failed. A chain that intentionally lists
several models under the same provider (e.g. two more NVIDIA NIM models
after the primary NIM model times out) was therefore skipped wholesale,
falling straight through to the main-agent-model safety net instead of
trying the other configured models on that provider.

Add failed_model so the skip narrows to the exact (provider, model) pair
that failed. Callers that only know the provider (client-build failures,
where the whole provider is unreachable regardless of model) keep the old
provider-wide skip; the two runtime request-error call sites (call_llm,
async_call_llm) now pass the model that just failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-26 22:33:06 -07:00
Alex Fournier 45580cc93a Merge origin/main into feat/hermes-relay-shared-metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-26 09:21:19 -07:00
kudi88 5121a2a20e feat(aux): force streaming for providers that reject non-stream requests
Some OpenAI-compatible endpoints — notably Tencent Copilot
(copilot.tencent.com) — only accept streaming chat requests; any
non-streaming call returns HTTP 400 (code 11101, 'Non-stream chat
request is currently not supported'). The main conversation loop already
streams, so interactive chat works, but every auxiliary task (title
generation, compression, web extraction) used the non-streaming path and
failed on each call.

_provider_requires_stream() detects stream-only endpoints
(copilot.tencent.com built in, plus user-configurable
auxiliary.stream_only_base_urls substring markers in config.yaml).
Matching sync auxiliary calls route through _create_with_progress
(force_stream=True) and async calls through the new
_acreate_with_stream, aggregating the chunk stream — including tool-call
deltas and reasoning deltas — into a complete response via the shared
_ChatStreamAccumulator.

Salvaged from PR #60686 by @kudi88 onto the progress-aware streaming
machinery from #71508, addressing both sweeper-review gaps: the async
path now consumes the stream with 'async for' (awaiting create() and
iterating synchronously raised on AsyncOpenAI streams), and tool-call
deltas are reassembled instead of dropped (MCP passes tools= through
call_llm). Under force_stream there is no silent non-streaming retry —
a stream-only provider rejects those by definition, so the original
error surfaces to the normal recovery chains.
2026-07-25 14:58:04 -07:00
Teknium 32fd9d65cf feat(compression): progress-aware timeouts — stop punishing slow summary models
The gateway's pre-agent session-hygiene compression killed the summary
call at a fixed 30s wall-clock deadline (compression.hygiene_timeout_seconds),
regardless of whether the summary model was hung or merely slow. A reasoning
model happily streaming a large summary was cut off mid-generation, the user
got '⚠️ Context compression timed out after 30.0s', and a 300s failure
cooldown left the session oversized — a doom loop for slow-but-healthy
auxiliary models.

Timeouts are now liveness-based instead of wall-clock-based:

- agent/auxiliary_client.py: new thread-local aux_progress_hook. When
  installed (only by context compression today), the primary call_llm
  attempt streams (stream=True) and aggregates chunks back into a complete
  response, ticking the hook per chunk. The configured timeout then acts
  per stream read (idle) instead of as a total budget. Providers that
  reject streaming fall back to the plain non-streaming call; auth/payment/
  rate-limit/transport errors propagate unchanged into the existing
  recovery chains. Codex Responses (per SSE event) and Anthropic Messages
  (per stream event, via the new create_anthropic_message on_stream_event
  callback) tick the same hook from inside their wire adapters.

- agent/conversation_compression.py: CompressionCommitFence gains
  touch_progress()/seconds_since_progress(); compress_context() installs
  fence.touch_progress as the progress hook around the compress call.

- gateway/run.py: the hygiene wait loop treats hygiene_timeout_seconds as
  an INACTIVITY budget — while the fence reports fresh progress the wait
  extends, bounded by the new compression.hygiene_total_ceiling_seconds
  (default 600s, clamped >= the idle budget) so a degenerate trickle
  stream still dies. The timeout warning now says the summary model
  produced no output, which is the only case that still triggers it.

- config/docs: hygiene_total_ceiling_seconds added to DEFAULT_CONFIG and
  configuration.md; hygiene_timeout_seconds documented as inactivity-based.

Tests: tests/agent/test_aux_progress_streaming.py (hook plumbing, stream
aggregation incl. tool-call deltas and reasoning deltas, rejection
fallback, ceiling kill, fence progress surface); two new gateway tests
prove a slow-but-streaming worker survives past the fixed timeout
(sabotage-verified: fails with the old fixed deadline) and a
forever-trickling worker is still cut off at the ceiling.
2026-07-25 12:26:28 -07:00
AlexFucuson9 cf35fd6de5 fix(core,cli,gateway,plugins): add encoding='utf-8' to read_text() calls
Path.read_text() without an explicit encoding uses the platform's
default encoding. On Windows this is typically cp1252 or mbcs, which
causes UnicodeDecodeError or silent data corruption when reading
UTF-8 content (JSON files, user text, config with non-ASCII chars).

This is the read-side companion to the write_text() encoding fix.
Fixed the most critical locations that read JSON data, user content,
and config files across 14 files with 31 call sites.

Pattern: .read_text() → .read_text(encoding='utf-8')
         json.loads(path.read_text()) → json.loads(path.read_text(encoding='utf-8'))
2026-07-24 17:10:39 -07:00
Israel Lot 6bc8d68ad7 fix(codex): scope 24h retention to Bedrock Mantle 2026-07-24 15:54:08 -07:00
Israel Lot f54fa1bcb7 fix(codex): exclude models.github.ai from auxiliary cache retention 2026-07-24 15:54:08 -07:00
Israel Lot 48049a1d31 fix(codex): skip auxiliary cache retention on Codex backend 2026-07-24 15:54:08 -07:00
Israel Lot 339be21542 fix(codex): send prompt_cache_retention 24h for the GPT-5.5 family
OpenAI documents GPT-5.5 / GPT-5.5 Pro as extended-cache-only: in-memory
prompt cache retention is not available for them, and only
prompt_cache_retention: "24h" is supported. Responses requests that omit
the field see near-zero cached_tokens even with a stable prompt_cache_key
and identical prefixes (observed on an OpenAI-compatible Responses relay:
0 cached across repeated identical calls before; 97% cache reads after).

Send the field for the gpt-5.5 model family (bare and namespaced ids like
openai.gpt-5.5) on OpenAI-compatible Responses routes, mirrored in the
auxiliary Codex adapter, and pass it through preflight normalization.
Skipped for xAI, GitHub/Copilot, and the chatgpt.com Codex backend, which
reject or ignore body-level cache fields.
2026-07-24 15:54:08 -07:00