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).
ruff check --fix --select F541 . on current main. Pure prefix removals;
adjacent-string concatenations keep the f only on interpolating fragments.
No string content or live placeholder altered.
The two TestSourceGuardrail tests asserted the presence of literal
strings ("#58753", "_user_survives") in context_compressor.py. Those
are change-detector tests that break on any refactor without catching a
real regression. The four behavioral tests in
TestCompressAlwaysKeepsAUserTurn already exercise the real compress()
path and fully cover the invariant (user turn survives, summary pinned
to user, no consecutive user roles, surviving tail user untouched).
Regression coverage for the kanban-worker crash where compression left a
transcript with no user-role messages, triggering a non-retryable
`400 No user query found in messages` from vLLM/Qwen.
Exercises the real `compress()` path with the reporter's shape (no system
prompt in the list, a re-compaction with the only user turn in the
compressed middle) and asserts the output always keeps >=1 user turn,
never introduces consecutive user roles, and leaves a surviving tail user
message untouched. A source guardrail pins the guard so a future refactor
cannot silently drop it.
Inspired by Claude Code v2.1.199 (July 2, 2026): stacked slash-skill
invocations load all leading skills (up to 5), not just the first.
- agent/skill_commands.py: split_stacked_skill_commands() consumes leading
/skill tokens (stops at the first non-skill token so slash-path arguments
are never swallowed); build_stacked_skill_invocation_message() composes
the multi-skill turn reusing the existing bundle scaffolding markers so
extract_user_instruction_from_skill_message() keeps memory providers
storing the user's instruction, not N skill bodies.
- cli.py + gateway/run.py: dispatch the stacked path on both surfaces.
- 11 new tests + docs section in skills.md.
Inspired by Claude Code v2.1.199 (July 2, 2026): SSL certificate errors
(TLS-inspecting proxies, missing CA bundles, expired certs) no longer
burn retries before showing actionable guidance — they fail immediately
with the fix hint.
- agent/error_classifier.py: new FailoverReason.ssl_cert_verification +
_SSL_CERT_VERIFY_PATTERNS, checked BEFORE the transient-SSL patterns
(cert-verify messages also contain '[SSL:' and previously retried
forever as timeout). Non-retryable, no compression, no fallback churn.
- agent/conversation_loop.py: dedicated status line + per-cause fix
hints (corporate proxy CA bundle, certifi refresh, self-signed local
endpoints) on the non-retryable abort path.
- 7 new tests incl. regression guards (transient alerts still retry,
large-session cert failure doesn't trigger compression).
_try_anthropic() hard-failed (return None, None) when the anthropic
credential pool was present but had no selectable entry — e.g. the pooled
OAuth token expired and its refresh_token had gone stale, so
_select_pool_entry("anthropic") returned (True, None). This wedged every
auxiliary task routed to Anthropic (goal judge surfaced "no auxiliary
client configured") even when a perfectly valid ANTHROPIC_TOKEN /
credentials-file token was available. The main session stayed healthy
because it resolves the env token directly.
The openrouter path (_try_openrouter) and codex path already fall through
to their standalone credential on (True, None); anthropic was the only
provider that hard-failed. Make _try_anthropic fall through to
resolve_anthropic_token() on that branch so the three paths are symmetric:
a temporarily dead pool entry must not block auxiliary tasks when a valid
standalone credential exists.
Adds a regression test covering: (1) pool present + no entry + valid env
token -> client built from the env token, (2) pool present + no entry + no
resolvable token -> clean (None, None), (3) base_url defaults correctly
when falling through with pool_present=True.
'KEY=os.getenv(...)' / 'os.environ[...]' / 'process.env.X' values are
variable-name references in code snippets, not leaked secrets. Masking
them corrupted pasted code in prose/log contexts (issue #2852):
ha_token=os.getenv('HOMEASSISTANT_TOKEN') -> ha_token=os.get...EN').
Skip these values inside _redact_env, which covers all three passes that
share the closure (_ENV_ASSIGN_RE, _CFG_DOTTED_RE, _CFG_ANCHORED_RE).
Real secret values are still masked.
Salvage of PR #2852-fix #2854 — the PR's own placement (an unconditional
pass before the code_file gate) would have reintroduced the code-file
false-positive class; the skip is applied inside the existing gated pass
instead. Tests adapted from the PR.
Co-authored-by: crazywriter1 <sampiyonyus@gmail.com>
22c5048d9 restored Anthropic-style cache_control for two of MoA's three
call paths: the acting aggregator (MoAChatCompletions.create, the
persistent `provider: moa` model) and the advisor fan-out (_run_reference).
aggregate_moa_context() -- the /moa <prompt> one-shot command's synthesis
call -- is the third, independent call path and was never covered: its
call_llm(task="moa_aggregator", ...) sent a single undecorated user message
containing the full joined reference output, re-billing the entire input on
every invocation even when the resolved aggregator slot is a cache-honoring
route (Claude on OpenRouter/native Anthropic, MiniMax, Qwen/DashScope).
- Generalize _maybe_apply_advisor_cache_control to
_maybe_apply_moa_cache_control (it never had advisor-specific logic --
same policy function, same breakpoint layout as the main loop, judged
purely on the passed-in runtime) and reuse it in aggregate_moa_context
the same way _run_reference already does.
- Compute _slot_runtime(aggregator) once and reuse it for both the
decoration call and the call_llm kwargs, instead of calling it twice.
Mutation-verified: reverting the moa_loop.py change makes the new
regression test fail by asserting a plain string aggregator-message
content where the cache-honoring case expects native cache_control
content blocks.
Two review findings on the #57922 salvage:
1. Stale inline comment at the login-exchange site still claimed the token
endpoint uses the claude-code/ UA prefix and 404s claude-cli/ — now
contradicts the axios/ fix. Repointed it at _OAUTH_TOKEN_USER_AGENT.
2. The inherited Path.home test isolation on the three TestRefreshOauthToken
tests only stubbed the ~/.claude *file* source, not the macOS Keychain.
_refresh_oauth_token re-reads read_claude_code_credentials() (keychain
first) in its adopt-already-refreshed branch, so on any macOS dev/CI runner
with real Claude Code creds the branch short-circuits and the 3 tests fail.
Stub read_claude_code_credentials -> None so the tests are hermetic.
(The remaining TestResolveAnthropicToken/TestResolveWithRefresh/TestRunOauthSetupToken
failures on macOS are the same pre-existing keychain-leak class on origin/main,
unrelated to this OAuth-UA fix, and pass in CI — left out of scope.)
hermes auth add anthropic fails 100% at token exchange with HTTP 429 while
Claude Code /login succeeds through the same client_id/redirect/scope. The
discriminator is the User-Agent on the /v1/oauth/token request.
Verified live against platform.claude.com (throwaway code, nothing burned):
claude-code/2.1.200 (external, cli) -> 429 rate_limit (Hermes, blocked)
Mozilla/5.0 -> 429 rate_limit
axios/1.7.9 -> 400 invalid_grant (reached validation)
node / empty / SDK-style UAs -> 400 invalid_grant
Anthropic now rate-limits token-endpoint requests whose UA starts with
claude-code/ (the anti-abuse net for Max-sub-as-API-key). This is the same
prefix-block shape that #48534 first hit on claude-cli/, then #56263 dodged by
switching to claude-code/ — which held ~2 weeks and is now blocked too. Bumping
_CLAUDE_CODE_VERSION_FALLBACK cannot help; the gate is prefix-based.
Fix: shared _OAUTH_TOKEN_USER_AGENT (axios/) on the token endpoint only — the
two refresh POSTs (refresh_anthropic_oauth_pure) and the login exchange POST
(run_hermes_oauth_login_pure). The real Claude Code CLI exchanges the auth code
with a bare axios client, NOT its claude-code/ inference UA.
The INFERENCE client (build_anthropic_kwargs, /v1/messages) is deliberately left
on claude-code/ + x-app: cli — that fingerprint is required there and is NOT
throttled on the messages API. Two endpoints, opposite UA requirements.
Also isolate two _refresh_oauth_token tests from live ~/.claude creds and update
the UA regression tests to assert the split (token endpoint uses a
non-claude-code UA while inference keeps claude-code/).
Verified E2E: Hermes' own login path now returns 400 (past the 429 wall)
instead of 429, using the real _OAUTH_TOKEN_USER_AGENT constant against the live
platform.claude.com token endpoint.
Salvaged from #57922 (authorize-host + scope changes dropped as non-load-bearing;
they only add a redirect hop back to claude.ai and the UA fix alone clears 429).
Follow-up to the salvaged #57845 fix. _can_carry_marker used
any(isinstance(part, dict)) but _apply_cache_marker only marks the LAST
content part, so a list whose last element is a non-dict passed the carrier
gate yet received no marker — wasting one of the four breakpoints. Tighten
the predicate to require content[-1] to be a dict (mirroring the apply
logic) and add a regression test. Flagged by a 3-agent review.
A custom:<name> main provider resolves at runtime to the bare provider id
"custom". In the vision auto-detect chain, the main-provider branch called
resolve_provider_client("custom", ...) WITHOUT explicit_base_url/api_key,
so it returned (None, None) ("no endpoint credentials found") and the whole
chain fell through to OpenRouter/Nous. A user on a custom endpoint with no
aggregator configured then got "No LLM provider configured for task=vision
provider=auto" on every image, even though their main model fully supports
vision.
Recover the live endpoint that set_runtime_main() records each turn
(_RUNTIME_MAIN_BASE_URL/_API_KEY/_API_MODE) and forward it to Step 1, with
a fallback to _resolve_custom_runtime() for non-gateway callers. Mirrors the
existing explicit-base_url branch directly above.
Adds TestResolveVisionCustomProvider covering custom, custom:<name>, and the
no-runtime fallback path.
When model.provider is set to custom:<name>, _supports_vision_override()
previously tried only the runtime provider key ('custom') and the raw
config value ('custom:my-proxy'). It did not try the stripped name
('my-proxy'), which is the actual key under providers: in config.yaml.
This caused native image routing to fall back to text mode even when the
user explicitly declared supports_vision: true on the named provider's
model entry.
Fixes#39963
Follow-up to the per-site strips from the review gate. The two copy-site
strips are correct but positional — a copy site added after the assembly
loops would re-leak _db_persisted into the child-session flush. Add a single
terminal sweep (_strip_persistence_markers) run once on the fully-assembled
compressed list so the invariant 'no compacted message leaves compress()
carrying a persistence marker' is structural, not dependent on copy-site order.
- agent/context_compressor.py: _strip_persistence_markers() called before
compress() returns; helper docstring notes the sweep is the authoritative guard
- tests/agent/test_context_compressor.py: structural regression — neuter the
per-site helper to a leaking copy, assert the terminal sweep still strips
- tests/run_agent/test_compression_persistence.py: pin the fixture assumption
behind the exact-equality row-count assertion
Shallow messages[i].copy() during context compression propagated the
_db_persisted marker from cached gateway incremental flushes into the
post-rotation compressed list. _flush_messages_to_session_db then skipped
every row when writing to the new child session, so gateway restarts
lost the compacted transcript (severe amnesia).
Strip the marker in _fresh_compaction_message_copy() and add regression
tests for rotation flush + compressor assembly.
Fixes#57491
Self-review (ruff+ty lint diff = 0 net-new; 2-agent deep review) surfaced one
Warning + comment-accuracy nits; no Critical:
- W1: the local-probe TTL cache memoized None (probe failure) for 30s, so a
probe that failed during a startup race would suppress a legit retry once
the server came up. Cache only positive results — still fully bounds the
hot-path probe rate (reachable servers cache their value) while an
unreachable one re-probes on the next call. Add a regression test asserting
a None result is NOT cached (retry re-probes); mutation-verified.
- Tighten the platform-guard comment: gateway/TUI/cron already construct with
quiet_mode=True (gated by `not agent.quiet_mode`), so the guard's active job
is CLI dedup vs show_banner, not "filling the gateway/TUI gap" as originally
worded.
Verified not-issues (per review): positive-value 30s cache does not break the
reconcile-after-restart freshness contract (restart = fresh process, empty
cache); cache key is collision-safe; platform guard is correct in both
directions (no runtime path leaves platform None on a non-CLI surface).
Tests: 149 passed. ruff clean; ty 0 net-new vs base.
Salvage review of #56431 surfaced one Critical + two Warning issues; fix
them on top of the contributor's cherry-picked commits:
1. Critical — duplicate non-agentic warning on the interactive CLI. The new
agent_init warning fires on every platform, but cli.py show_banner()
already warns on CLI (richer output + /model hint), so a CLI user saw the
warning twice per startup. Guard the agent_init emit to skip platform=="cli"
— it now fills exactly the gateway/TUI gap the PR intended, no duplication.
2. Warning — vLLM error-parse regex under-matched. The patterns required a
literal space before the number, so "max_model_len: 32768", "=32768",
"(32768)", and "... is 32768" all returned None. Broaden both patterns to
accept :/=/(/ 'is' delimiters. Add a parametrized test over all delimiter
variants.
3. Warning — per-call live probe latency on local endpoints. The new
reconcile-on-hit + pre-defaults step-7 probe made every local resolution
fire a synchronous network probe (banner + /model switch + compressor
update_model each within one startup). Add a 30s in-process TTL cache
keyed by (model, base_url) around _query_local_context_length so back-to-
back resolutions reuse one round-trip; not persisted to disk, so the
reconcile freshness contract (re-probe after restart) is preserved. Add an
autouse fixture clearing the cache between tests + TTL coverage.
Tests: 148 passed (was 138). ruff clean.
Add regression tests for vLLM max_model_len error parsing, stale local
cache reconciliation, live probes over llama defaults, and the 64K minimum
guard on persistent cache writes.
(cherry picked from commit 1cb47ef437)
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.
Two related hardening fixes for auxiliary calls (which include MoA reference
advisors — a pinned-model path where provider fallback is not a meaningful
recovery):
1. Transient-transport retries: the same-provider retry on a connection reset /
timeout / 5xx / 408 was a single attempt, then fallback. For a pinned aux
call a second blip silently loses the call (root of the run2 double-advisor
'Connection error' collapse — a genuine upstream blip). Now retries N times
with exponential backoff, N = auxiliary.transient_retries (default 2 -> 3
total attempts, clamped [0,6]). Compression-on-timeout fast-fail carve-out
preserved.
2. Per-model client-cache isolation: _client_cache_key excluded the model, so
two concurrent auxiliary calls to the same provider/base_url/key but
different models (e.g. an opus + gpt-5.5 MoA fan-out) shared one cache entry
and could race each other's client lifecycle. Model now participates in the
key -> distinct clients, no cross-call races. Same-model reuse unchanged.
- agent/auxiliary_client.py: _transient_retry_count() + backoff loop; model in
_client_cache_key and both call sites.
- hermes_cli/config.py: auxiliary.transient_retries default (2).
- tests: new retry/isolation tests; updated 2 stale-expectation tests to the
corrected behavior (per-model resolve; N-retry escalation).
Backoff base is overridable (_TRANSIENT_RETRY_BACKOFF_BASE) so tests don't sleep.
Follow-up to the salvaged fix: the regression test asserted a frozen
max_tokens == 128_000 literal, coupling it to the Opus-4-8 model table.
Assert against _get_anthropic_max_output("claude-opus-4-8") plus > 2000
instead, so the test survives model-table churn while still catching a
regression to the old `or 2000` fallback.
agent/vertex_adapter.py resolved VERTEX_CREDENTIALS_PATH,
GOOGLE_APPLICATION_CREDENTIALS, VERTEX_PROJECT_ID, and VERTEX_REGION via raw
os.environ.get() instead of the profile-scoped get_secret() every other
credential lookup in hermes_cli/runtime_provider.py uses. In a multiplex
gateway serving several profiles from one process, os.environ still holds
whichever profile's .env python-dotenv loaded at boot — so a raw read here
let one profile's turn silently mint a Vertex OAuth2 token from, and get
billed against, a different profile's GCP service account. No error, no
fail-closed guard: the multiplex UnscopedSecretError protection was bypassed
entirely because these reads never went through get_secret().
- _resolve_credentials_path/_resolve_project_override/_resolve_region now
call agent.secret_scope.get_secret(), matching the _getenv() pattern
already used for every other provider's credentials.
- get_vertex_credentials()'s ADC fallback (google.auth.default()) reads
GOOGLE_APPLICATION_CREDENTIALS from os.environ internally, bypassing
get_secret() entirely — closed with a narrow guard: when multiplexing is
active and this profile's scope has no Vertex credentials of its own, but
os.environ still carries a value (left by a different profile's boot-time
dotenv load), refuse ADC rather than silently authenticate as a stranger.
- Zero behavior change for single-profile installs: get_secret() falls
through to os.environ transparently whenever multiplexing is off.
Same bug class as the already-fixed _HERMES_OAUTH_FILE/_AUTH_JSON_PATH/
HOOKS_DIR cross-profile leaks, now closed for Vertex's OAuth2 credential
path.
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.
parse_frontmatter's malformed-YAML fallback stores every value as a string,
so a skill's `metadata` can be a str. `_category`/`_related` chained
`.get("metadata", {}).get("hermes", {})` and blew up with `'str' object has
no attribute 'get'`, taking down `build_learning_graph()` (and thus /journey
and `hermes journey`) whenever any installed skill had bad frontmatter.
Extract a `_hermes_meta()` helper that returns the nested dict only when it
really is one. Fixes the whole class, not just the two call sites.
Two independent MoA auxiliary-call fixes:
#53866 — auxiliary.moa_reference.timeout and auxiliary.moa_aggregator.timeout
were 600s while moa_agent was 120s. Raise both to 900s so a genuinely long
reference/aggregator turn (mixed providers, deep reasoning, long tool chains)
has headroom instead of being cut mid-generation.
#53735 — _CodexCompletionsAdapter (the Codex/Responses auxiliary path used by
the MoA acting-aggregator, compression, web_extract, session_search, etc.)
never set prompt_cache_key, so it stayed cache-cold while the MAIN Responses
transport (agent/transports/codex.py) was warm. Derive the same
content-addressed key via the shared _content_cache_key(instructions, tools)
helper and set it on the aux Responses request, with the same host guards the
main transport uses (xAI carries the key in extra_body; GitHub/Copilot opts out
of cache-key routing).
Tests: 5 new prompt_cache_key cases (set+prefixed, stable across identical
prefix, differs on different instructions, skipped for xai/github hosts).
tests/agent/test_auxiliary_client.py 279 pass; tests/hermes_cli/test_config.py
130 pass.
On the MoA path agent.model/provider are the virtual preset name (e.g.
"closed") and "moa", which have no pricing entry. estimate_usage_cost()
returned None for the aggregator turn, so the `if amount_usd is not None`
guard skipped it and the session's estimated_cost_usd reflected only the
advisor fan-out — a ~50% undercount when the aggregator does the full acting
loop (verified: $0.91 advisor-only vs $1.96 true, aggregator = 54%).
MoAChatCompletions.create() now stashes the resolved aggregator slot as
last_aggregator_slot (exposed via MoAClient); conversation_loop reads it to
price the aggregator turn at its real model/provider. cost_source flips from
'none' to 'provider_models_api'.
Follow-up to the END-MARKER reorder: moving the summary prefix after the
[PRIOR CONTEXT] wrapper meant _is_context_summary_content (prefix-at-start)
no longer recognized a merged-tail summary. That silently broke three
consumers — the last-real-user anchor (would pick the merged summary as a
real user turn, causing active-task loss), the carry-forward summary find,
and the auto-focus skip. _strip_summary_prefix would also carry the wrapper
+ stale tail content forward as the next summary body.
Extract the two delimiter strings into _MERGED_PRIOR_CONTEXT_HEADER /
_MERGED_SUMMARY_DELIMITER constants (writer + detector stay in sync), teach
_is_context_summary_content and _strip_summary_prefix to look past the
delimiter, and add a regression test. Standalone summaries unchanged.
When the compression summary is merged into the first tail message
(the alternation corner case where a standalone summary role would
collide with both head and tail), the old format was
SUMMARY + END_MARKER + OLD_TAIL_CONTENT — so the preserved tail content
appeared AFTER the end marker and the model could read it as a fresh
message to respond to.
Reorder so the END MARKER is always last: old tail content is wrapped in
[PRIOR CONTEXT ...][END OF PRIOR CONTEXT — COMPACTION SUMMARY BELOW]
delimiters, then the summary, then the END MARKER. _append_text_to_content
handles both string and multimodal-list content.
Salvaged from #56372 by @Gromykoss. Only the END-MARKER reorder half is
carried over. The PR's second change (a post-compaction pass that strips
user-role messages before the first summary marker on compression_count>=2)
was dropped: on 2nd+ compactions the protected head decays to system-only
(_effective_protect_first_n -> 0, #11996) so the targeted 'ghost head user'
does not occur, and where the strip does fire it deletes legitimate recent
tail user turns (data loss) and can leave consecutive assistant messages
(role-alternation violation).
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.
`pathlib.Path('~user').expanduser()` raises RuntimeError when the
tilde-expansion can't resolve the user (e.g. `~500-700` where the LLM
meant "approximately 500-700" rather than a path). The hint walker's
existing `except (OSError, ValueError):` clauses do not catch
RuntimeError, so it escapes through the tool dispatcher and surfaces
in the conversation loop as a misleading
Error during OpenAI-compatible API call #N:
Could not determine home directory.
Reproduced across three unrelated models (openai/gpt-5-mini,
openai/gpt-5.1-codex, deepseek/deepseek-v4-flash) on terminal-tool
commands containing literal tildes in non-path contexts — common in
LLM output ("~500 agencies", "~45,000 CVEs", "~80/hr blended rate").
Reproduction (one-liner):
>>> from pathlib import Path
>>> Path("~500-700").expanduser()
RuntimeError: Could not determine home directory.
Fix: extend the three `except` clauses in
agent/subdirectory_hints.py to also catch RuntimeError:
line 138 (_add_path_candidate's outer catch around the Path().expanduser() call)
lines 198+202 (_load_hints_for_directory's nested catches around hint_path.relative_to(Path.home()))
Tests: tests/agent/test_subdirectory_hints_tilde.py adds three cases
covering: tilde-as-approximately in heredoc commands, ~unknown_user paths,
and a regression guard that legitimate ~/path expansion still works.
Follow-up correcting the salvaged fix's persistence approach to avoid a
duplicate user-message write (verified via E2E — the #860/#42039 bug class
the original diff aimed to avoid).
Root cause: in gateway mode the AIAgent is built WITH a session_db, so the
inbound user turn is already flushed at turn start (turn_context.
_persist_session). The original fix returned agent_persisted=False, making the
gateway re-write the whole new-message slice via append_to_transcript ->
append_message (a raw INSERT with no dedup), duplicating the already-flushed
user turn.
Corrected approach (single writer): run_codex_app_server_turn now flushes its
OWN projected assistant/tool messages via _flush_messages_to_session_db (which
dedups the already-persisted user turn through _DB_PERSISTED_MARKER) and
returns agent_persisted=True so the gateway skips its write. Net result:
session_search/distill see the full codex conversation, each message persisted
exactly once.
Adds regression coverage asserting exactly-once persistence on a real
SessionDB, agent_persisted=True, FTS visibility, and standard-runtime skip-db
behaviour preserved.
Co-authored-by: Lubos Buracinsky <lubos@komfi.health>
Phase 2c review flagged that only 2 of the 4 structurally-identical
resolve_provider_client routing dead-ends were demoted. Complete the bug-class:
also demote+dedup the external-process ('not directly supported') and OAuth
('not directly supported, try auto') fall-throughs, keyed by provider name, so
none of the four dead-ends spam WARNING on a retry loop.
Add direct tests for the unhandled-auth_type and OAuth dedup paths via a
monkeypatched PROVIDER_REGISTRY (the review noted these were unverified).
Mutation-checked: reverting either sibling demotion fails its test.
The two fall-through branches in resolve_provider_client (unknown provider,
unhandled auth_type) logged at WARNING on every retry of a misconfigured
provider, spamming logs during retry loops. Demote both to logger.debug with
per-process dedup: the first occurrence still surfaces (a provider-name typo or
PROVIDER_REGISTRY/auth_type-drift bug is worth seeing once), while identical
repeats are suppressed for the process lifetime.
Salvaged from #56283 (extracting only the stated auxiliary_client fix; the
original PR also bundled ~2800 lines of unrelated changes across 10 other
files, which are dropped).
Think-enabled models (MiniMax M2.7, DeepSeek, etc.) emit inline
<think>...</think> reasoning even for simple prompts like title
generation, and the raw XML was leaking into session titles. Route the
title-model response through the canonical strip_think_blocks scrubber
before cleanup so every tag variant — closed pairs, unterminated blocks,
orphan closes, mixed case — is handled, not just a single literal
<think> pair.
- 2 regression tests: closed <think> pair stripped, unterminated block
at start yields no title.
Salvaged from PR #44126 by @shawchanshek.
MoA full-turn traces (moa.save_traces) recorded the aggregator's acting
output only on the non-streaming path, where it's captured inline at
call time. On the streaming path — which every hermes chat --query run
and every live gateway/CLI turn takes — the aggregator's raw token
stream is handed to the live consumer, so the trace left output=null and
only pointed at the session-db assistant row. An offline audit of a
benchmark run (HermesBench drives --query) then couldn't see what the
aggregator produced without hand-joining to state.db.
Capture the resolved streamed acting text at trace-flush time (the agent
already holds it in _current_streamed_assistant_text) and fold it into
the trace, so the record is self-contained in both modes. New
output_location value inline_from_stream marks a streamed turn whose text
was captured this way; a genuinely empty acting turn (pure tool call)
still points at the session db, matching state.db exactly.
Touches only the trace side-channel — no change to the acting path,
message history, role alternation, or prompt cache.
- agent/moa_loop.py: consume_and_save_trace(..., aggregator_output_fallback)
on both the facade and the MoAClient wrapper; prefer inline capture,
fall back to the resolved streamed text.
- agent/moa_trace.py: embed the fallback; add inline_from_stream location.
- agent/conversation_loop.py: pass _current_streamed_assistant_text at flush.
- tests: 5 cases across streaming / non-streaming / empty-fallback / no-double-write.
Review nit (helix4u): the fix covers 500/502/503/529 but the positive tests
only asserted 500 and 503. Parametrize over all four so 502/529 are covered
too; keep the plain-5xx negatives.