Commit Graph

28271 Commits

Author SHA1 Message Date
kshitij 4ce0d64be4 test(caching): pin signal precedence and the openrouter-host opt-out
Review follow-up. Three coverage gaps in the LiteLLM matrix:

- The operator opt-out on a litellm-named provider pointed at an OpenRouter
  host. That route previously took the OpenRouter branch and ignored an
  explicit per-model `prompt_caching: false`; it is the only cell in the
  differential matrix where the salvage REMOVES caching, so pin it as
  intended rather than leaving it to be read as a regression.
- Signal precedence: an explicitly litellm-named provider grants even on a
  lookalike host, because the provider id is an independent signal and only
  the host-derived signal is token-gated. Intentional, now documented.
- A hyphen-delimited host label (`my-litellm-gw.internal.example.com`),
  which the token matcher handles but nothing exercised.

Traded the redundant `claude-3-7-sonnet` parametrize cell for the new host
case, so the matrix covers more shapes with the same cell count.

Tests: 83 passed. All three production fixes re-mutation-checked against
the final stack.
2026-08-16 14:34:30 +05:30
kshitij 0038d96b78 perf(caching): narrow the widened capability lookup to the LiteLLM grant
Self-review follow-up, caught by benchmarking the previous commit.

Widening the custom-provider capability-lookup gate to `is_anthropic_wire or
_is_litellm_route(...)` made EVERY chat_completions route with a litellm-ish
provider/host enter the lookup, including non-Claude models that the grant
branch below can never match. Measured on a route with no config.yaml
(the uncached worst case) that was ~7.5us -> ~1528us per evaluation.

Narrowed the gate to the exact condition the LiteLLM branch grants on
(chat_completions + Claude + litellm route), computed once into a local and
reused by the branch itself so the predicate no longer runs twice.

Measured with a realistic config.yaml present (mtime cache warm), vs
origin/main:
  live-agent policy      20.6us -> 61.7us
  destination planning  219.3us -> 347.7us

Sub-millisecond and scoped to the routes that actually opted in. The
earlier 1.5ms figures were a tempdir artifact: load_config_readonly's
mtime cache cannot engage when no config.yaml exists, which is never true
of a real install. Non-LiteLLM and non-Claude routes are unaffected
(openrouter Claude measured flat at ~7.9us).

Tests: 82 passed across the policy and TTL-propagation modules.
2026-08-16 14:34:30 +05:30
kshitij 435d6f30b5 fix(caching): match the litellm provider id token-wise too
Self-review follow-up. The previous commit fixed substring matching on the
HOST but left the provider-id side as a bare substring, so a user-named
provider like `custom:notlitellm` or `mylitellmthing` still matched and was
handed Anthropic markers — the same bug class, half-fixed.

Both signals now match `litellm` as a whole delimited token via a shared
helper. Real spellings (`litellm`, `custom:litellm`, `litellm-router`, and
the already-lowercased `LiteLLM`) still match; lookalikes no longer do.

Tests: 71 passed. Adds lookalike-provider and real-spelling guards; both
new guards mutation-checked. Differential matrix over 2688 configs vs
origin/main: 60 changes, every one a Claude model on a genuine LiteLLM
route getting the envelope layout, zero pre-existing routes altered.
2026-08-16 14:34:30 +05:30
kshitij 1b0e953b47 fix(caching): use the envelope layout for LiteLLM Claude on the OpenAI wire
Follow-up to the salvaged LiteLLM cache grant. The grant itself is right;
four things about how it was scoped were not.

1. Layout. The branch returned the native inner-block layout
   (use_native_layout=True) on api_mode == "chat_completions". That layout
   writes a TOP-LEVEL msg["cache_control"] on role:tool and empty-content
   messages and depends on the Anthropic adapter to relocate it into the
   block — but that adapter only runs for api_mode == "anthropic_messages"
   (agent/transports/anthropic.py registers there), and the
   chat_completions transport does no relocation. Measured on a 3-tool-turn
   transcript: 2 of the 4 available breakpoints landed on markers the
   provider never sees. Worse, when LiteLLM itself relocates a top-level
   marker for an OpenRouter-backed Claude route
   (OpenrouterConfig._move_cache_control_to_content), the marker lands on
   an empty assistant turn and produces a cache_control-marked empty text
   block — the HTTP 400 "text content blocks must contain" shape already
   guarded in agent/anthropic_adapter.py (#69512). Switched to the envelope
   layout, matching every other OpenAI-wire grant in this function:
   4 of 4 breakpoints honored, zero empty blocks.

2. Host matching. `"litellm" in base_url_hostname(...)` is the substring
   false-positive class base_url_hostname's own docstring warns against; it
   granted Anthropic markers to notlitellm.example.com,
   foolitellmbar.example and friends. Replaced with a label-token match in
   a named helper, so "litellm" must be a whole dot- or hyphen-delimited
   token. All three of the original test hosts still match; a "litellm"
   path segment on an unrelated host still does not.

3. Transport gate. `not is_anthropic_wire` also swept in codex_responses,
   bedrock_converse and codex_app_server. Gated on
   api_mode == "chat_completions" explicitly.

4. Operator override. The grant is inferred from a provider/host name, but
   the custom-provider capability lookup was gated on is_anthropic_wire, so
   an explicit `prompt_caching: false` for the route+model was honored on
   /v1/messages and silently ignored on /v1/chat/completions. The lookup
   now also runs for a LiteLLM route, and its layout follows the transport
   rather than the declaration (an explicit `true` must not promote a
   chat_completions request to the native layout).

Tests: 64 passed. Adds the wire-shape contract the original matrix was
missing (asserts no breakpoint sits on the message envelope, rather than
only checking the returned tuple), plus lookalike-host, other-transport,
and both operator-override directions. All five guards mutation-checked —
reverting each fix turns the corresponding test red.
2026-08-16 14:34:30 +05:30
Justin Bowes ff4df5e54d fix(caching): engage prompt caching for LiteLLM Claude on the OpenAI wire
anthropic_prompt_cache_policy() only granted Anthropic cache_control
markers to LiteLLM over the native Anthropic wire
(api_mode == "anthropic_messages"). A LiteLLM deployment exposing the
OpenAI-compatible surface instead (/v1/chat/completions, /v1/messages
-> 404) matched no grant branch and fell through to (False, False): no
cache_control injected, the system prompt sent as a plain string, and
the provider serving zero cache hits -- the entire prompt re-billed at
full price on every turn. Silent: no error, no warning, usage simply
shows 100% uncached input forever.

Add one branch after the is_anthropic_wire/is_claude case that grants
caching to Claude-family models on a LiteLLM endpoint regardless of
wire, with the native inner-block layout. Same failure class already
documented in-function for Qwen/DashScope.

Design:
- Gated on the Claude family only (is_claude); a Gemini/GPT/Qwen route
  through the same proxy must not receive markers (they may reject the
  cache_control block format -- cf. the DeepSeek/OpenCode exclusion).
- Matches on provider string OR base_url host, since provider naming
  varies per install (litellm, custom:litellm, or a bare custom alias
  pointed at a LiteLLM host).
- prompt_caching.cache_ttl: false still wins (the _cache_disabled early
  return is untouched).
- Generic strict OpenAI-wire custom providers (e.g. Fireworks) remain
  excluded -- verified by the existing over-reach regression test.

Tests: adds TestLiteLLMOpenAIWire covering the grant (several model
spellings x provider/host signals), no-over-reach (non-Claude on the
same proxy get nothing; operator disable wins), and adjacent behavior
(LiteLLM in Anthropic proxy mode still native layout). Full module:
43 passed.

Closes #84506. Original diagnosis, patch design, and measurements by
@ottosulin.
2026-08-16 14:34:30 +05:30
Junhao Wang 53daff0644 fix(desktop): don't cancel the running turn on Esc while an overlay is open
The composer's global Esc-to-cancel listener (useComposerEscCancel) fires
whenever the turn is busy and the active composer matches — but overlays
(Settings, Command Center, agents, cron, …) cover the chat while the
composer stays mounted and 'active' beneath them, so pressing Esc on any
of those pages interrupted the session the user wasn't even looking at.
OverlayView's own escape-layer Esc-to-close fired too, but the stream was
already dead.

Stand Esc down with composerFocusBlockedBySurface() — the same signal the
type-to-focus path uses (BLOCKING_OVERLAY includes OverlayView's
[data-overlay-surface] marker). Esc on an overlay now closes the overlay
via its escape layer instead of canceling the stream beneath it.

Fixes #82618
2026-08-16 02:03:49 -07:00
fangliquanflq 095d25c612 fix(gateway): keep persisted model routes consistent 2026-08-16 02:03:31 -07:00
Ryan Hart c32dd7580a fix(desktop): identify unsafe update blockers 2026-08-16 02:03:22 -07:00
Ryan Hart 7af27c65ab fix(desktop): close safe preview blockers before update 2026-08-16 02:03:22 -07:00
PRATHAMESH75 9ca11399c0 fix(telegram): honor group_allowed_chats in early auth under multiplex profiles (#87132)
With gateway.multiplex_profiles enabled, the primary Telegram message
handler is the closure returned by _make_default_profile_message_handler(),
so its __self__ is absent. The early intake filter
(_is_user_authorized_from_message) recovered the GatewayRunner via
self._message_handler.__self__ and, finding none, fell back to env-only
authorization — never evaluating the configured chat allowlist through
GatewayRunner._is_user_authorized(). Every non-global sender was then
default-denied in an explicitly allowlisted group.

Prefer the platform-bound authorization callback registered via
set_authorization_check(): it routes through the runner's full auth chain
(platform + group allowlists, pairing store, allow-all) and survives the
closure wrapping, whereas the bound-handler lookup does not. The bound
handler remains the fallback for setups without a registered callback, and
the pairing-passthrough guard for unknown DMs is preserved.

Fixes #87132
2026-08-16 02:03:01 -07:00
Chen Jin ed0a8a480e fix(skills): resolve skills dir before relative_to so junction installs work (#86971) 2026-08-16 02:02:34 -07:00
fangliquanflq b3df990832 fix(gateway): offload hygiene streak persistence 2026-08-16 02:02:06 -07:00
fangliquanflq 39d2d858fd fix(gateway): persist hygiene failure cooldown rung 2026-08-16 02:02:06 -07:00
Christopher 5602f04a6e fix(agent): clear in-memory cooldown when hygiene overwrites the shared row
A later hygiene idle-timeout write can replace an aux-model cooldown on
the shared column. Drop the in-memory timer on that refresh so the
in-agent compressor is not still blocked after the DB row is hygiene.
2026-08-16 02:01:41 -07:00
Christopher 2cb8381963 fix(agent): do not let hygiene idle timeouts block in-agent compression
Session hygiene persists compression_failure_cooldown_until after a
30s no-progress watchdog so the pre-agent pass can skip. The
in-conversation compressor read the same column and then refused to
run even though its own budget is sufficient.

Ignore hygiene idle-timeout errors on the in-agent path. Real
aux-model faults such as rate limits still block.

Fixes #86972
2026-08-16 02:01:41 -07:00
PRATHAMESH75 2d7c9ef6b9 fix(gateway): don't crash on a foreign XDG_RUNTIME_DIR in user-systemd preflight (#86558)
runuser/su/sudo -u from a root shell leaks XDG_RUNTIME_DIR=/run/user/0 into
the child. _user_systemd_socket_ready() stat-ed sockets under it with a bare
Path.exists(), which only suppresses ENOENT/ENOTDIR/EBADF/ELOOP — EACCES on
the 0700 root-owned dir escaped as a raw PermissionError traceback instead of
the documented UserSystemdUnavailableError remediation path.

- _path_exists_safe(): Path.exists() that treats EACCES as absent; used at
  both the readiness and DBUS-detection call sites.
- _ensure_user_systemd_env(): drop an XDG_RUNTIME_DIR that is unset or owned
  by another user in favour of our own /run/user/{uid}, so the restart
  actually succeeds after su/sudo -u instead of only failing cleanly.

Regression tests cover the EACCES readiness probe, foreign-dir replacement,
and that preflight raises UserSystemdUnavailableError (not PermissionError).
2026-08-16 02:01:15 -07:00
lextiz 95190e4544 fix(gemini): raise maxOutputTokens when thinking is enabled
Gemini bills thought tokens against maxOutputTokens/max_tokens, so a
global 4096 cap can be fully consumed by thinking on the first
request, leaving zero content tokens and aborting after 4
continuations. When thinking is enabled, raise the effective output
cap to the 65,535 ceiling on both the native and chat-completions
paths.

Refs #83915
2026-08-16 02:00:54 -07:00
fangliquanflq 076b8a5aa8 fix(backup): bound locked database snapshot waits 2026-08-16 02:00:50 -07:00
liuhao1024 8fe8633254 fix(codex): clamp xAI max/ultra aliases to the model's reasoning ceiling (#87279) 2026-08-16 02:00:24 -07:00
fangliquanflq 7f1c49cdaa fix(ui): scrub esbuild override during builds 2026-08-16 01:59:58 -07:00
fangliquanflq f33ec68478 fix(ui): ignore inherited esbuild binary overrides 2026-08-16 01:59:58 -07:00
Ayush Nangia be8b58dcbc fix(gateway): let an explicit workspace move win for a running session
session.workspace.move refused a running live session with 4009
(session busy), but the desktop's Move-to-project flow calls exactly
this RPC — so the UI updated its local grouping while state.db kept the
old cwd and the agent's tools kept running in the old workspace. Two
sources of truth disagreed (#86626).

An explicit move now wins: the stored row and the live session re-anchor
together. In-flight tool calls keep the cwd they were launched with; the
next tool call uses the new workspace.
2026-08-16 01:59:34 -07:00
fangliquanflq 8fc4189edd fix(agent): harden canonical tool call deduplication 2026-08-16 01:59:08 -07:00
fangliquanflq a55d29d9fb fix(agent): canonicalize duplicate tool call arguments 2026-08-16 01:59:08 -07:00
Shawn Wang f4f08f9dae fix(gateway): make managed Node suppress PATH fallback 2026-08-16 01:58:25 -07:00
Martin Koistinen da392043a9 fix(agent): include timezone and UTC offset in system prompt timestamp
The "Conversation started:" line carried a bare date (%A, %B %d, %Y). Tools
that accept instants -- nutrition, calendar and similar MCP servers -- reject
naive datetimes and require an explicit UTC offset, so the model had to infer
EST vs EDT from the date alone. Near a DST boundary that is a coin flip, and a
wrong guess does not error: it silently writes the record onto the wrong day.

Append the IANA zone (when configured), the zone abbreviation and the UTC
offset, e.g.:

  Conversation started: Saturday, August 15, 2026 (America/New_York, EDT, UTC-04:00)

get_timezone() returns None when no timezone is configured; in that case the
line falls back to the abbreviation and offset of the server-local (still
tz-aware) time, so behaviour is unchanged for users who never set one:

  Conversation started: Saturday, August 15, 2026 (EDT, UTC-04:00)

Daily byte-stability is preserved -- the property the date-only format exists
to protect (PR #20451). Zone name, abbreviation and offset are all constant for
the whole day; they shift only at a DST transition, where a change is correct.
The static-prefix reconstruction guard in _restore_plugin_sections matches on
"\n\nConversation started:" and is unaffected by a suffix after the date.

test_datetime_is_date_only_not_minute_precision used `re.search(r":\d{2}")`
over the whole line as a proxy for "no time-of-day". A UTC offset also matches
that pattern, so the check now applies to the date portion (everything before
the zone parenthetical) and the invariant is tightened rather than relaxed:

- test_datetime_includes_utc_offset asserts the offset is present
- test_datetime_line_is_stable_across_rebuilds asserts two rebuilds in the
  same day produce a byte-identical line

Fixes #87403

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 01:57:59 -07:00
YappLeCunt a7b25b3c00 fix(desktop): never reap a backend whose parent Electron is alive
reapOrphans() treats any recorded backend with a matching process identity
as orphan-reapable — including a backend owned by another live instance. The
ownership file is shared across instances, so a second launch that reaches
reap (even without the lock) SIGTERMs the running instance's backend.

Claims now record the spawning Electron (parentPid + parentStartMarker, the
same values already passed to the backend as HERMES_PARENT_PID /
HERMES_PARENT_START_MARKER), and reapOrphans() skips any entry whose parent
is still running. Even a second instance that wins a stale lock can never
kill a live instance's backend. Legacy entries without parent data keep the
old behaviour.

Known tradeoff: a parent-liveness probe failure preserves the record, so a
genuinely orphaned backend under a still-running parent is leaked until it
dies naturally. That is preferable to killing a live instance's backend.

Tests: parent-aware reap in backend-ownership.test.ts (live parent
preserved, dead parent still reaped, probe failure preserved, parent
identity round-trips through claim/parse).
2026-08-16 01:57:34 -07:00
YappLeCunt df89995674 fix(desktop): keep startHermes inert without the single-instance lock
startHermes() is the only entry point that can reap, spawn, claim, and
therefore destroy a backend. Belt-and-suspenders on the exact killing line:
even if some future path reaches it in a lock-losing process (a refactor, a
dev harness, a race), the instance stays inert — no reap, no spawn, no
claim — instead of SIGTERMing the running instance's backend (#87295).
2026-08-16 01:57:34 -07:00
YappLeCunt 7cc006a2ef fix(desktop): hard-exit a lock-losing second instance before ready
app.quit() does not stop a lock-losing instance from reaching whenReady:
the before-quit teardown coordinator defers the quit (event.preventDefault
+ async backend shutdown), and ready fires in that window. The losing
instance then runs the full startup whose reapOrphans() SIGTERMs the
running instance's live backend (#87295).

The lock-loser holds no state and no backend — requestSingleInstanceLock()
has already delivered the argv to the primary by the time it returns false —
so there is nothing to clean up. app.exit(0) terminates immediately, before
ready, so a second launch routes into the running window and never touches
backend machinery.
2026-08-16 01:57:34 -07:00
zhao 0a8092ac32 fix: guard exit watchdog against mid-cleanup overlap 2026-08-16 01:56:43 -07:00
webtecnica f1e3e0a4d9 fix(agent): bound worker finalization when iteration budget exhausted (#87096)
Adds a bounded fallback path in turn_finalizer.py that always records
a terminal timed_out outcome via _record_task_failure (CAS receipt path)
when the iteration budget is exhausted, regardless of whether the normal
fallback paths (interrupted/failed/anomalous exit_reason) were eligible.

Previously, a kanban worker whose budget was exhausted but whose turn was
interrupted, failed, or exited with an anomalous reason would silently
leave its task in an ambiguous lifecycle state — the dispatcher would
eventually detect it as a crashed or protocol-violation worker, but the
failure was not bounded and could take a full tick cycle to reconcile.

The CAS invariant in _end_run (WHERE ended_at IS NULL) guarantees
idempotence: if another path already closed the run, the call is a no-op.

Extracted the inline kanban-budget-exhausted recording into a shared
helper function (_record_kanban_budget_exhausted) used by both the
existing iteration_limit_fallback path and the new bounded fallback path.

Closes #87096
2026-08-16 01:56:18 -07:00
webtecnica b48ab1b4ad fix(agent+discord): guard truncated-response continuation loops and cap Discord split delivery (#86581) 2026-08-16 01:55:51 -07:00
fangliquanflq 0a42bc7113 fix(sessions): align prune filter derivation 2026-08-16 01:55:25 -07:00
fangliquanflq 0b8a09759c fix(sessions): address prune skip review notes 2026-08-16 01:55:25 -07:00
fangliquanflq 29dfbf2d6a fix(sessions): surface open sessions skipped by prune 2026-08-16 01:55:25 -07:00
fangliquanflq a6f405314f fix(agent): cover provider wait teardown paths 2026-08-16 01:55:00 -07:00
fangliquanflq 0fd059e745 fix(agent): preserve stalled-provider escalation 2026-08-16 01:55:00 -07:00
Christopher 7008fb81b3 fix(gateway): put --external-supervisor on launchd gateway argv
hermes update decides restart ownership from the live grandchild argv,
not from an env marker. Newly generated plists now include the flag.
The stderr_timestamp wrapper upgrades only historical Hermes gateway
run shapes for stale plists and leaves arbitrary launchd children unmarked.
2026-08-16 01:54:35 -07:00
Christopher c69a0872ea fix(gateway): preserve launchd supervisor marker across stderr_timestamp wrapper
launchd only stamps XPC_SERVICE_NAME on its direct child. The timestamp
wrapper is that child, so the grandchild gateway sees XPC_SERVICE_NAME=0
and the supervised-conflict guard refuses the service's own spawn.

Forward HERMES_GATEWAY_EXTERNAL_SUPERVISOR=1 when the wrapper itself is
launchd-supervised. Interactive XPC_SERVICE_NAME=0 starts stay unmarked.

Fixes #86893
2026-08-16 01:54:35 -07:00
monerostar 730c5fc5d0 fix(gateway): treat Ready scheduled tasks as Windows supervisors
After the VBS/cmd launcher exits, Task Scheduler marks
Hermes_Gateway_* Ready while the detached gateway keeps running.
The orphan reaper only bailed on Running, then fail-opened the
parent-chain check and killed the live bot on desktop serve start.

Fixes #87001
2026-08-16 01:54:10 -07:00
Vivaan Dhawan 1596148ff2 fix(approval): deterministic approvals.single_query_mode for -q sessions
hermes chat -q sets HERMES_INTERACTIVE=1 (for interactive sudo prompts) but
runs one turn with no user waiting to answer approval prompts. Previously a
dangerous command triggered the interactive gate, waited the full 300s
timeout, then failed closed — and the agent was effectively forced to work
around the block, often silently auto-approving via execute_code (which
auto-approves in non-gateway mode).

Add approvals.single_query_mode (default deny, mirror of cron_mode):
  deny    — block dangerous commands and execute_code deterministically with
            a clear 'no user present' message (no 300s wait)
  approve — auto-approve dangerous commands/execute_code in -q mode

cli.py marks the session with HERMES_SINGLE_QUERY_SESSION; the shared gate
(_run_approval_gate, check_all_command_guards, check_execute_code_guard)
treats -q as a deterministic non-interactive context when that marker is set.
execute_code, the -q escape hatch, now honors single_query_mode instead of
auto-approving headlessly. Includes tirith parity in the combined guard and
docs. Fixes #86878.
2026-08-16 01:53:44 -07:00
fangliquanflq 26b2b47593 fix(agent): preserve local reasoning timeout opt-out 2026-08-16 01:53:19 -07:00
David Metcalfe ca50ff332f fix(desktop): reject bare modifier combos in the input gate
Review NIT: a malformed stored binding of just 'mod'/'ctrl' (never
produced by comboFromEvent) could pass the shape-only mod/ctrl check.
Reject bare-modifier bases in actionAllowedInInput and pin it in the
suite.
2026-08-16 01:52:51 -07:00
David Metcalfe a11fb1cd89 fix(desktop): restore mod-chord keybinds while typing in inputs
#86586 replaced the combo-based input gate (any Cmd/Ctrl chord fires
while typing) with an action allowlist that dropped session.new and
every other mod-chord not explicitly listed. ⌘N/⌘T/⌘⇧N and friends
became dead keys whenever focus was in the composer.

Restore the pre-regression rule: primary-modifier chords stay global
even in text fields; the allowlist now gates only bare/Shift/Alt combos,
so rebound letter keys can never hijack typing. Text-navigation chords
(Ctrl+Arrow/PgUp/PgDn) still stay with the input.
2026-08-16 01:52:51 -07:00
PRATHAMESH75 d6f18cd7db fix(mcp): prefer server-native tool over generated utility on name collision (#87112)
An MCP server exposing a native tool named read_resource (or
list_resources/list_prompts/get_prompt) collided with the auto-generated
resource/prompt utility of the same name. The registration collision
handler flagged the pair as ambiguous and skipped BOTH entries, so the
server's own tool became silently unavailable on every gateway boot.

Resolve this specific native-vs-utility collision in favour of the native
tool: keep it and drop the shadowed utility, which is only convenience
sugar for servers that expose no such tool of their own. The conservative
skip-everything path still applies to genuinely ambiguous collisions (two
or more native tools normalizing to one name), which we cannot
disambiguate. Add a regression test covering the native-tool-wins path.

Fixes #87112
2026-08-16 01:52:26 -07:00
fangliquanflq 25672c0e7f fix(cli): allow persisted contributor tier consent 2026-08-16 01:52:00 -07:00
Christopher 3cbd86aac8 fix(cli): warn when --provider default_model cannot be resolved
A named --provider without -m used to swallow lookup failures and
silently keep the global model.default. Log the resolution error so
the fallback is visible.
2026-08-16 01:51:35 -07:00
Christopher d6688adcc2 fix(cli): use custom provider default_model when --provider is set
hermes chat --provider <name> without -m sent the global model.default
to the custom endpoint. Named custom entries already expose
default_model via _get_named_custom_provider(); honor that when the
user selected the provider and did not pass an explicit model.

Fixes #86978
2026-08-16 01:51:35 -07:00
webtecnica b7936c892d fix(gateway): catch BaseException in _process_message_background to notify on SystemExit
The fire-and-forget handler only caught asyncio.CancelledError and
Exception, so a SystemExit/KeyboardInterrupt escaping a turn (e.g. a
plugin calling sys.exit() in a tool call or summary-LLM path) skipped
the user-facing failure notification and surfaced only as 'Task
exception was never retrieved' — radio silence for the user.

Catch BaseException instead; send the failure notification first, then
re-raise SystemExit/KeyboardInterrupt to preserve shutdown semantics
for the loop's own signal handling. Other BaseExceptions stay contained.

Closes #86651
2026-08-16 01:51:10 -07:00
Christopher 3a61fa86fa fix(gateway): keep broad connection phrases out of the provider-error gate
Fixes #86570
2026-08-16 01:50:44 -07:00