Commit Graph

28271 Commits

Author SHA1 Message Date
spfcraze 9cc428cff8 fix(desktop): keep responsePreviewed settle gated on the boundary flag
Sweeper review on #76583: dropping interimBoundaryPending from the
previewed settle path let a previewed final arriving after a
message.start reset OVERWRITE a distinct interim instead of appending
(interim('old') → message.start → complete({response_previewed: true,
text: 'new'}) destroyed 'old'). responsePreviewed may rewrite the final
with no prefix guarantee, so it must stay flag-gated; only
finalContinuesInterim (prefix-either-way continuity, which can only hold
for the same message) settles flag-free. New test: distinct previewed
final after a reset appends its own bubble. Production ordering cited in
the test: compaction-resume events exclude message.start
(gateway-event.ts), and the TUI gateway emits message.complete before
goal-followup starts (tui_gateway/server.py).

74/74 use-message-stream tests pass.
2026-08-14 20:23:18 -07:00
spfcraze 71a98f69ee fix(desktop): settle final reply onto interim even after message.start reset the boundary flag
completeAssistantMessage merged a turn's final text onto its sealed interim
bubble only when the session's volatile interimBoundaryPending flag was still
true. A subsequent message.start (chained turn, follow-up, or mid-turn
compaction) resets that flag to false; when it landed between the same turn's
message.interim and message.complete, the flag-based gate fell through and the
UI appended a duplicate bubble.

Key the merge on the message's OWN durable interim state instead of the
session flag. finalContinuesInterim already requires existing.interim plus
prefix continuity, so distinct replies (which don't continue the interim) are
still appended as their own bubble.

Adds a regression test: interim + message.start + completing final that
continues the interim must yield one bubble, not two.
2026-08-14 20:23:18 -07:00
李灵航 cf63b79467 fix(desktop): drop pending stream rows whose reply the transcript already carries
A still-pending assistant stream row (id `assistant-stream-*`) whose reply
the authoritative transcript already committed used to fall through to
`preserved.push` when ordinal pairing missed it — the commit shifted the
row's ordinal under compaction/history rewrites, so `nextByRoleOrdinal`
returned nothing and the local copy was appended to the tail, rendering the
same answer twice (reported as A B C D E C D tail duplication).

The #70209 guard only covers SETTLED local rows (`pending !== true`);
pending rows were unprotected. Match pending rows against SETTLED
authoritative rows before appending:

- identical answer text            -> authoritative already carries it
- authoritative extends local text -> authoritative is the settled final
  version of the still-streaming local copy
- local extends authoritative text -> replace the committed row with the
  richer local body instead of appending

Live projection shells (still-pending candidates) never match, so the
traces-only local row keeps replacing the empty shell.
2026-08-14 20:23:18 -07:00
Teknium 56121e528d chore: map contributor email (audit_pr_attribution) 2026-08-14 20:23:07 -07:00
Aleks Clark 62eefff697 perf(desktop): bound long-running app resource use
Persist backend ownership for reliable cleanup, park inactive panes, and
evict unreferenced transcripts so Desktop stays responsive over long sessions.

💘 Generated with Crush

Assisted-by: Crush:gpt-5.6
2026-08-14 20:23:07 -07:00
AlexDev_ 4712721033 perf(desktop): pause decorative animations when unfocused 2026-08-14 20:23:07 -07:00
LemonSchneid d7e95315f7 fix(desktop): expand HUD transcript when resized
Use the available HUD window height for non-empty transcript scrollback instead of a fixed glance-band cap. This makes the corner resize affordance reveal additional conversation content while preserving the compact empty HUD state.
2026-08-14 20:22:56 -07:00
Jakub Wolniewicz b55677077f fix(desktop): keep completion selection visible 2026-08-14 20:22:56 -07:00
nanami7777777 957a7c20cd fix(desktop): keep text navigation keys in composer 2026-08-14 20:22:56 -07:00
LemonSchneid 0611f9f856 fix(desktop): retain HUD composer focus
Keep the native HUD window mouse-solid while focus is inside the rich composer. On Windows this prevents click-through from reactivating the app beneath the HUD, stealing the caret, and collapsing the transcript.
2026-08-14 20:22:56 -07:00
Jakub Wolniewicz 23954e3e31 fix(desktop): prevent navigation from stealing focus 2026-08-14 20:22:56 -07:00
kshitij cb47f59ffa fix: standardize media-group CancelledError to raise
Pre-existing inconsistency: _flush_media_group_event used return
in CancelledError while _flush_text_batch and _flush_photo_batch
used raise. Changed to raise for consistency and to properly
propagate task cancellation. Made more visible by the hold-queue
changes in #83878.
2026-08-15 07:09:52 +05:30
Daniel V. Baecker 42dc17ec97 fix(telegram): close hold-lifecycle gaps on permanent fatal and connected drain
Address review on #83878:

- Permanent fatal fences all hold producers and discards pending maps on
  teardown instead of re-populating a queue that can never drain.
- Any hold created while connected schedules a tracked redispatch (cancel-
  after-pop no longer orphans until a future reconnect).
- Redispatch failures re-hold current + remainder without tight-looping.

Regression coverage for the three residual paths, plus the interaction
with OOF-156's connect-failure classification: the retryable network
path (telegram_connect_error) must NOT clear the hold queue — reconnect
is precisely what drains it; only non-retryable fatals discard.
2026-08-15 07:09:52 +05:30
Daniel V. Baecker b4da6b15e1 fix(telegram): hold inbound messages across disconnect instead of destroying them
The disconnect drop-guard (#55971) correctly prevents dispatch into a
torn-down session. Destroying the event was wrong: by enqueue/flush time
python-telegram-bot has already acked the update and advanced the polling
offset, so Telegram never redelivers. Result: silent permanent loss, no
log, no error.

Hold inbound events (text/photo/media-group) when the drop-guard fires,
salvage pending batch maps on teardown, cancel+await the redispatch task
in the delivery cancel map (lifecycle-tracked), and redispatch from
_mark_connected after reconnect. Cap the hold queue (default 64), dedupe
by object identity, discard on non-retryable fatal. Cancel-after-pop in
flush paths also holds.

Distinct from #72037 (cancel-after-pop during follow-up supersession) and
#81528 (boundary discard). Tests use delay=0 and entered/release Events —
no wall-clock races; includes production terminal-step coverage.
2026-08-15 07:09:52 +05:30
Adolanium ed4f50de51 fix(send_message): hand unresolved cron and react targets to the adapter again
Restores pass-through behavior for cron delivery and react/unreact that
was lost when d409f6748 routed them through resolve_send_target. Stored
cron job targets the channel directory doesn't recognize (e.g.
telegram:ops-room on a fresh install, photon group GUIDs) used to go to
the adapter verbatim; after d409f6748 they were silently dropped. Same
for react on platform-native ids.

Adds an opt-in pass_unresolved_references flag to resolve_send_target,
passed only by cron and react. Model-facing send tool stays strict.
Plugin platforms with a parser stay strict for all callers. The optional
validator still has the final say over passed-through ids.

Follow-up fixes on salvage:
- Update test_cron_relay_delivery_guards.py mock lambdas to accept **kw
  (file added to main after PR branch point; lambdas didn't accept the
  new keyword argument)
- Consolidate duplicated pass-through blocks into _pass_through_unresolved
  local helper

Fixes #85128

Co-authored-by: Adolanium <Adolanium@users.noreply.github.com>
2026-08-15 07:07:40 +05:30
hermes-seaeye[bot] b9fa46b5a5 fmt(js): npm run fix on merge (#86559)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 00:39:34 +00:00
Teknium b6726d57e4 feat(desktop): per-profile scope selector in the Capabilities view (#86548)
* feat(desktop): per-profile scope selector in the Capabilities view

Adds a 'Configuring:' profile selector above the Tools and MCP tabs in the
Capabilities (Skills) view, so a user can configure ANY profile's toolsets
and MCP servers without switching the whole app into that profile.

- hermes.ts: every capability fetcher (getToolsets, setToolsetEnabled,
  getToolsetConfig/Models, selectToolsetModel/Provider, runToolsetPostSetup,
  getMcpCatalog, installMcpCatalogEntry, testMcpServer, saveMcpServers,
  auth/oauth flow, setEnvVar/deleteEnvVar/revealEnvVar, startOAuthLogin/
  pollOAuthSession, getActionStatus, getHermesConfigRecord) takes an optional
  trailing profile? that forwards to profileScoped(profile). Omitting it
  preserves exact app-wide behavior (profileScoped(undefined) → _apiProfile).
- use-config-record.ts: hermesConfigKey(profile)/useHermesConfigRecord(profile)/
  hermesConfigCacheWriter(profile) — per-profile RQ keys (scope-in-key).
- toolset-config-panel.tsx + mcp-tab.tsx: thread profile through every fetch
  and their nested children (EnvVarField, PostSetupRunner, ModelCatalogPicker),
  keyed/remounted per selected profile so switching never shows stale state.
- skills/index.tsx: the selector (seeded from profiles.list, default→'Hermes',
  shown only with >1 profile), defaulting to ; toolsets
  query + toggles keyed and scoped to the selection; McpTab/ToolsetDetail
  remounted per scope.
- i18n: skills.configuringProfile (en + zh; others fall back).

When the selected profile equals the active one (the default), behavior is
identical to before — the selector is a pure override layered on top.

Tests: index.test.tsx — new case asserts picking a non-active profile in the
selector refetches toolsets scoped to that profile; existing single-profile
cases still pass (selector hidden with one profile). 5/5. Full-project tsc clean.

* Fix CI: command-palette getHermesConfigRecord call, panel test mock, lint

- command-palette/index.tsx: getHermesConfigRecord now takes an optional
  profile; passing the bare fn as queryFn fed it react-query's context object
  (TS2769 + mcp_servers on {}). Wrap in an arrow.
- toolset-config-panel.test.tsx: use-config-record now imports normalizeProfileKey
  from @/store/profile, which calls setApiRequestProfile at module-init; the
  full-replacement @/hermes mock must provide it (+ getApiRequestProfile).
- index.test.tsx selector test: stub Element.prototype.scrollIntoView (Radix
  Select calls it on open; jsdom lacks it).
- toolset-config-panel.tsx: PostSetupRunner useCallback missing 'profile' dep
  (stale-closure correctness); jsx-prop sort order.

Verified in a full-dep checkout: tsc 0 errors, eslint clean, panel 28/28 + skills 5/5.

---------

Co-authored-by: Teknium <teknium1@users.noreply.github.com>
2026-08-14 17:33:49 -07:00
hermes-seaeye[bot] bab9a85b67 fmt(js): npm run fix on merge (#86533)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 00:07:02 +00:00
Brooklyn Nicholson c83061ba3d test(desktop): cover the context gauge fetching before a turn runs 2026-08-14 19:01:15 -05:00
Brooklyn Nicholson 25d1c8d740 fix(desktop): context gauge reads the session it is on, not the last turn
The statusbar gauge painted only what the backend reported as measured
occupancy, which a session has none of until a turn runs in this process.
Turning the gauge on mid-conversation, or resuming a chat, therefore showed
nothing until the next message.

Fetch session.context_breakdown as soon as the gauge is on screen instead of
when its popover opens. It is the same read-only estimate the popover already
used (chars/4 over the live prompt, tools and transcript — no provider call),
and it reports the measured figure once the backend has one. The popover
becomes presentational and reads the gauge s merged usage, so the bar and the
panel cannot disagree.
2026-08-14 19:01:15 -05:00
Teknium 50d98fc1f3 feat(delegation): raise subagent iteration cap default 50 -> 250 (+migration) (#86506)
delegation.max_iterations is the per-subagent tool-call budget. The old
default of 50 truncated substantial delegated work: leaf agents spend
~15-20 turns on reconnaissance before producing output, then ran out of
budget mid-task and returned 'completed but unfinished' summaries. 250
gives real delegated work room to finish.

Changes:
- config_defaults.py: delegation.max_iterations 50 -> 250; _config_version 35 -> 36
- tools/delegate_tool.py: DEFAULT_MAX_ITERATIONS fallback 50 -> 250 (kept in
  sync with the shipped default to prevent drift)
- config_migrations.py: _migrate_to_36 lifts configs still pinned at exactly
  the OLD default 50 -> 250 on update, so existing installs inherit the new
  headroom. Any other explicit value (deliberate override) is preserved;
  unset inherits 250 at read time.
- cli-config.yaml.example: doc the new default

The cap is per-child and children run concurrently (max_concurrent_children
default 3), so this raises worst-case fan-out cost; delegation.child_timeout_seconds
(default 0 = off) remains available as a wall-clock guardrail, and users can
still pin a lower max_iterations explicitly.

Verified: migration lifts 50->250, preserves a deliberate 120, leaves unset
untouched (3/3); DEFAULT_CONFIG reads version=36, max_iterations=250, fallback=250.
2026-08-14 16:50:30 -07:00
Justin Bennington 832eb56b77 chore: map contributor email 2026-08-14 16:43:37 -07:00
Justin Bennington 167dd48b40 fix(models): trust certifi for credentialed catalogs (E-1002) 2026-08-14 16:43:37 -07:00
pierrenode 90253cc0a0 fix(providers): route Actual's fetch_models through the credential-redirect guard
ActualProfile.fetch_models() overrides ProviderProfile's default
implementation with its own Actual-specific base_url resolution
(ACTUAL_BASE_URL env var, hosted-vs-local normalization), but called raw
urllib.request.urlopen(req, timeout=timeout) directly instead of the base
class's open_credentialed_url(). Every other provider either uses the
base class default or forwards to it via super() and gets
SafeCredentialRedirectHandler for free — Actual is the only provider that
attaches a Bearer token to its own Request object and opens it with the
stdlib's default redirect handling, which forwards every header,
including Authorization, across a cross-origin redirect.

Actual's own feature surface makes the trigger realistic: ACTUAL_BASE_URL
is a first-class, documented way to point this provider at a self-hosted
or local-offline endpoint (see the local-loopback no-auth path already
handled elsewhere in this provider), so a misconfigured or compromised
endpoint 302-ing to another host leaks ACTUAL_API_KEY to it.

Fix: import and call the same open_credentialed_url() the base class
uses, keeping Actual's own URL-resolution logic unchanged.

Adds an end-to-end regression test using two real local HTTP servers (no
mocking of the security module itself) — one redirects, the other
records the Authorization header it receives — mirroring
test_urllib_security.py's own redirect tests. Also repoints the existing
fetch_models test's mock from urllib.request.urlopen to
hermes_cli.urllib_security.open_credentialed_url, since fetch_models no
longer calls the former. Mutation-verified: the new redirect test fails
on pre-fix code with the Authorization header observed at the redirect
target.
2026-08-14 16:43:37 -07:00
Teknium 2eda1292f6 chore: map contributor email for #56522 salvage 2026-08-14 16:40:02 -07:00
beiyesi b55dd04711 fix(streaming): adapt provider errors to relay 2026-08-14 16:40:02 -07:00
beiyesi 04bc5321c9 fix: preserve non-JSON provider stream errors 2026-08-14 16:40:02 -07:00
beiyesi 5cbc645d09 fix: require terminal signal for streamed errors 2026-08-14 16:40:02 -07:00
beiyesi 0fcebb29f4 fix streaming bare data error payloads 2026-08-14 16:40:02 -07:00
beiyesi 661a4c4f88 fix streaming provider error events 2026-08-14 16:40:02 -07:00
SHL0MS 44d580617f test(providers): tolerate stream kwarg in metadata probe mock
Upstream added stream=True to the /models metadata probe; widen the
fake_get signature so the captured verify assertion still runs.
2026-08-14 16:37:36 -07:00
SHL0MS b21e0bd8c9 fix(providers): honor per-provider TLS on custom /models and pricing probes
Per-provider ssl_ca_cert / ssl_verify reached the httpx chat client and the
auxiliary clients (#56681), but the endpoint discovery and pricing probes did
not. Both probe families resolved TLS from process-wide env vars only:

- the requests-based metadata/pricing probe
  (agent/model_metadata.py::_resolve_requests_verify)
- the urllib-based /models catalog probe
  (hermes_cli/models.py::probe_api_models)

A custom endpoint whose chain verifies against the provider's configured
bundle, but not the process SSL_CERT_FILE, then logged a spurious
CERTIFICATE_VERIFY_FAILED on every probe even though the chat path worked.
Pointing a global CA env var at the bundle fixes it but changes verification
for every provider, defeating the point of a per-provider setting.

This threads the selected provider's TLS settings into both probe paths,
reusing get_custom_provider_tls_settings so there is no second precedence
chain:

- _resolve_requests_verify(base_url) looks up the provider's ssl_verify /
  ssl_ca_cert before falling back to the env vars. Callers with no base_url
  keep the exact env-only behavior.
- probe_api_models builds an ssl.SSLContext from the provider settings and
  passes it through open_credentialed_url, which gains an ssl_context seam on
  the cloned secure opener. Unmatched or public endpoints pass None and keep
  urllib's default policy.

Tests: tests/agent/test_custom_provider_ca_probes.py covers both probe
families (provider CA, ssl_verify:false, unmatched, missing file, config
lookup failure) plus end-to-end assertions that the resolved verify value and
SSLContext actually reach the request seam. Verified against the neighboring
metadata, pricing, TLS, and urllib-security suites (266 tests) with no
regressions.
2026-08-14 16:37:36 -07:00
SHL0MS cbfe186da8 fix(config): canonicalize legacy api_mode spellings instead of silently discarding them
Earlier releases accepted api_mode: openai on custom provider entries.
The canonical transport set is now {chat_completions, codex_responses,
anthropic_messages, bedrock_converse, codex_app_server}, and an
unrecognized value was silently ignored at both consumption sites
(_normalize_custom_provider_entry passes the raw string through and
agent_init's accepted-set check drops it; _parse_api_mode returns None),
falling through to hostname-based detection.

For hosts with a detection rule the provider silently switches
transports after an update. Observed live: a custom entry for
api.actual.inc with api_mode: openai (valid when written) flipped to
codex_responses via the hostname rule, and every reasoning-bearing
request to the relay's /v1/responses failed with a wrapped non-JSON
error while /v1/chat/completions worked throughout.

Fix: one shared alias map (_canonical_api_mode) consulted by both
sites. openai/openai_chat -> chat_completions, responses ->
codex_responses, anthropic/messages -> anthropic_messages, bedrock ->
bedrock_converse. Canonical names and unknown values pass through
unchanged, so invalid-config behavior is untouched.

Tests: alias map contract (every alias lands in _VALID_API_MODES),
normalizer canonicalization incl. the transport: key alias, and the
runtime gate accepting legacy spellings while still rejecting unknowns.
2026-08-14 16:36:39 -07:00
Teknium f92accd05d fix(tui): lead slash_worker PATH with Hermes' managed bin dir
Follow-up on the #83854 salvage: prepend $HERMES_HOME/bin ahead of the
venv and user-local bin dirs, matching the managed-first Browser Use
CLI resolution policy — the worker resolves the same canonical binary
the agent process does.
2026-08-14 16:22:47 -07:00
webtecnica 73b4e41ab2 fix(tui): prepend Hermes venv and user-local bin to slash_worker PATH (#83845) 2026-08-14 16:22:47 -07:00
Benjamin 47fd2eb7c8 fix(browser): strip PYTHONPATH/PYTHONHOME from browser-use CLI subprocess env
The browser-use CLI runs under its own Python (uv tool / uvx), which
can differ from Hermes's venv interpreter. PYTHONPATH/PYTHONHOME
inherited from the agent process point at Hermes's venv
site-packages, and a child interpreter honors them ahead of its own —
so the CLI imported compiled C-extensions (pydantic_core) built for
the wrong interpreter and crashed with ABI mismatch /
ModuleNotFoundError (issues 83427, 84841, 86006, 86104; hits the
desktop backend on py3.14 and any shell exporting PYTHONPATH).

Strip both vars in _base_subprocess_env() — the CLI manages its own
environment and never needs Hermes's import path.

Salvaged from PR 83471 by Benjamin (@n1majne3), the earliest of two
independent fixes (also PR 84022 by @jklance16, PYTHONPATH-only);
regression test covers both vars and preserves unrelated env.
2026-08-14 16:22:38 -07:00
Teknium d275b96bfd feat(gateway): per-profile MCP server lifecycle RPCs (mcp.servers.*) (#86473)
Adds the full MCP setup surface as profile-scoped gateway RPCs so a
desktop client (Bot Mode's bot editor, the core Capabilities tab) can
add/configure/test/authenticate/remove MCP servers for ANY profile, not
just the launch profile:

- mcp.servers.list (profile) -> configured servers (transport, auth,
  oauth_tokens_present, enabled, tool names; no secret values)
- mcp.servers.add (profile, name, config|preset, bearer_token?) -> reuses
  mcp_config._apply_mcp_preset / _save_mcp_server / _save_bearer_auth_token
- mcp.servers.set_api_key (profile, name, value, env_var?) -> http auth
  header template or stdio env ref, via save_env_value
- mcp.servers.test (profile, name) -> _probe_single_server + oauth state
- mcp.servers.remove (profile, name)
- mcp.servers.oauth.start/poll (profile, name[, session_id]) -> mirrors the
  PROVIDER oauth session/poll model (not the FastAPI dashboard flow): a
  background worker drives the same interactive machinery 'hermes mcp login'
  uses, capturing the browser redirect on a local loopback listener. Client
  opens auth_url via openExternal and polls until status=='approved'.

All handlers are profile-scoped via set_hermes_home_override in try/finally
(mirrors skills.manage). Shared helpers live in tui_gateway/mcp_rpc_helpers.py
and are aliased onto server.py's namespace so the rebound handler bodies
(HandlerRegistry.install) can resolve them — a plain def in methods_tools is
unreachable post-rebind. Reuses hermes_cli/mcp_config.py throughout; no config
logic duplicated; no raw yaml near config.yaml (config-read-guard safe).

Tests: tests/tui_gateway/test_mcp_profile_rpcs.py, 8 E2E against real temp
HERMES_HOME profiles asserting add/list/set_api_key/remove land in the RIGHT
profile's config.yaml and not the launch profile's. 8/8. Registration +
live mcp.servers.list verified in an imported gateway.

Co-authored-by: Teknium <teknium1@users.noreply.github.com>
2026-08-14 16:17:22 -07:00
Teknium 452465bf78 fix(tests): reap leaked TUI notification pollers between tests
test_run_prompt_submit_requeues_all_unstarted_notifications_with_real_threading
failed twice in one hour on CI slices for two UNRELATED PRs (86371,
86374) with `assert set() == {proc_batch_2, proc_batch_3}`. Root cause:
session.init/create tests earlier in the file start real per-session
notification poller daemon threads and never stop them. Those pollers
outlive their test and keep polling the PROCESS-GLOBAL
process_registry.completion_queue, stealing-and-requeuing the target
test's events mid-assertion so its bounded drain loop can starve.

Reproduced: with 30 leaked foreign-session pollers injected via a
sabotage conftest, the target test fails standalone ~1 in 3 runs with
the exact CI assertion; with the reap fixture active it passed 8/8
under the same sabotage.

Fix:
- tui_gateway/server.py: _start_notification_poller registers
  (stop_event, thread) in module-level _notification_pollers (pruned of
  dead threads on each spawn; threads get a stable
  tui-notif-poller-<sid> name for debugging).
- tests/test_tui_gateway_server.py: autouse fixture sets every
  registered live poller's stop event after each test and joins them
  under ONE shared 3s budget (the poller loop wakes at least every
  0.5s), so no poller survives into the next test. No per-thread
  timeout, no session-dict mutation — a first draft that mutated
  session state and joined per-thread hung the file; full-file runtime
  with this version is 15.2s vs 13.2s baseline.
2026-08-14 16:04:10 -07:00
hermes-seaeye[bot] 8e509c729e fmt(js): npm run fix on merge (#86430)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-14 22:48:47 +00:00
Teknium 7d96537bc8 fix(desktop): the main agent's model pick persists as the profile default (#86414)
* fix(desktop): the main agent's model pick persists as the profile default

Reported: the default bot switches to the OpenAI API account instead of
the user's subscription, and doesn't retain the previous selection.

Root cause: the composer model picker always sent the switch as
--session scope, even for the PRIMARY profile's main agent. So the pick
never wrote config.yaml model.provider — and with model.provider unset,
resolve_provider('auto') falls through to a leftover OPENAI_API_KEY env
var and picks OpenAI/OpenRouter. The subscription the user selected was
only ever a per-session override that evaporated on the next session.

Fix: when the pick targets the primary profile's main agent
(touchesPrimary), send --global so it persists to config.yaml
(model.default + model.provider) via the existing model-switch persist
path. A SET model.provider already outranks the OPENAI_API_KEY env var
in resolve_provider (tier 2 vs tier 3), so the main agent now keeps the
chosen provider across restarts. Secondary chat tiles stay --session so
picking a model in one chat never rewrites the profile default (the
cross-session-contamination guard the old comment protected).

No change to resolve_provider's priority chain, so #29285 (an explicit
env key beating a STALE oauth login) is untouched — we simply make the
user's explicit main-agent selection the config default it always
should have been.

* MoA presets stay session-scoped; update tests for primary-persist intent

Fix CI (ui shard 3of3): the primary main-agent pick now persists via
--global, but MoA (mixture-of-agents) presets must NOT — a transient
orchestration choice can't become the global gateway default. Exclude
provider==='moa' from the persist path (stays --session). Update the
primary-picker test to assert --global (the new intent) and keep the
MoA + secondary-tile tests asserting --session (the guards that prove
the narrowing). 19/19 green locally.
2026-08-14 15:40:53 -07:00
hermes-seaeye[bot] 31e571acf6 fmt(js): npm run fix on merge (#86407)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-14 22:16:25 +00:00
Teknium eb20188d2e style: import order + padding lines in terminal.test.ts 2026-08-14 15:10:42 -07:00
Teknium 3f7140fbed chore: contributor email mapping for salvaged PR #66538 2026-08-14 15:10:42 -07:00
Teknium 5d8569d043 fix(tui): gate BSU/ESU on capability for ALL write paths, not just alt-screen
Follow-up to the salvaged #66538 commit: the ZELLIJ env gate fixed
detection, but writeDiffToTerminal still wrapped main-screen frames in
BSU/ESU unconditionally (skipSyncMarkers was only set for alt-screen).
Under Zellij the multiplexer re-chunks the stream, so the markers buy no
atomicity and stale frames leak into main-screen scrollback as the
repeated chrome reported in #66490. The renderer now passes
!SYNC_OUTPUT_SUPPORTED for every write path; supported terminals keep
today's behavior on both screens. Adds emitted-frame regression tests
for both marker modes.
2026-08-14 15:10:42 -07:00
Brian Sweatt d509e6df2f fix(tui): don't trust DEC 2026 synchronized output under Zellij
isSynchronizedOutputSupported() only excluded tmux, so running inside
Zellij under an outer terminal that advertises DEC 2026 (e.g. WezTerm
via TERM_PROGRAM) returned true. Zellij, like tmux, sits between us and
the outer terminal and chunks the stream, breaking BSU/ESU atomicity and
pushing old TUI frames into scrollback as repeated output.

Guard on the ZELLIJ env var (set to the session index, e.g. "0") the
same way we already guard on TMUX. Also thread an optional env argument
through the function so the behavior is unit-testable, mirroring
needsAltScreenResizeScrollbackClear() in the same module.

Closes #66490
2026-08-14 15:10:42 -07:00
Max Hsu 802a60a150 test(gateway): pin restoration of pre-existing HERMES_HOME override
Per hermes-sweeper review suggestion on #50242: the existing tests only
covered the unset case (override is None after the call). Add a test
where a caller already holds an override — _persist_live_session_system_prompt
must build the prompt under the session's profile and then restore the
caller's override via the reset token, not clear it to None.
2026-08-14 15:08:41 -07:00
Max Hsu 308bb56349 fix(gateway): bind profile override in _persist_live_session_system_prompt
Fixes #50233

_persist_live_session_system_prompt rebuilds the system prompt after a
live model switch (/model), but _start_agent_build's finally block has
already reset set_hermes_home_override by then. load_soul_md() and
build_skills_system_prompt() call get_hermes_home() which falls back to
the root ~/.hermes, loading the wrong SOUL.md identity and skills for
the session's profile.

Fix: set_hermes_home_override(session["profile_home"]) before calling
agent._build_system_prompt() in _persist_live_session_system_prompt,
and reset it in a finally block. Also upgrade the failure log from
DEBUG to WARNING so silent profile-wrong-prompt issues are visible.

The first-prompt lazy-build path is unaffected — _run_prompt_submit
already re-sets the override before run_conversation. The /model slash
worker path does not, which is the gap this fixes.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-14 15:08:41 -07:00
Teknium 4d1ef7fbd2 style: satisfy curly + padding-line eslint rules in usage comparator 2026-08-14 15:07:24 -07:00
Teknium 513f683aa7 fix(tui): widen usage comparator to key union + regression tests
Follow-up to the salvaged #41484 commit:
- usageChanged() iterates the union of Usage keys generically instead of a
  hardcoded field list — the original PR's list omitted active_subagents
  (consumed by the status rule's subagent segment and resume hint), which
  would have suppressed legitimate updates
- The memo(StatusRule) half of the original PR is intentionally dropped:
  main's StatusRule gained battery/subagent/resume-hint segments since,
  and the wrapper broke the direct-call test seam. The load-bearing fix is
  the stable usage reference: unchanged deltas no longer mint fresh objects,
  so $uiState subscribers stop re-rendering per streaming event
- Regression tests: unchanged-reference retention, active_subagents-only
  update, key-union asymmetry
2026-08-14 15:07:24 -07:00
liuhao1024 8606867ebc fix(tui): reduce status bar flicker during streaming
The status bar flickers visibly during streaming because every state
patch (thinking.delta, reasoning.delta, tool.*, usage notifications)
creates a new $uiState object, forcing StatusRulePane and StatusRule
to re-render and redo expensive layout calculations on every event.

Three fixes applied:

1. Stabilize usage object references in createGatewayEventHandler.ts
   - Add mergeUsageStable() that shallow-compares Usage fields before
     creating a new object. When values haven't changed, returns the
     existing reference, preventing unnecessary StatusRule re-renders.

2. Memoize expensive computations inside StatusRule (appChrome.tsx)
   - statusBarSegments(cols) → useMemo([cols])
   - modelLabel() → useMemo([model, effort, fast])
   - ctxLabel, bar → useMemo([usage fields, segs])
   - Tail segment budget + fits() calculations → single useMemo block
     covering all progressive-disclosure logic

3. Wrap StatusRule in React.memo (appChrome.tsx)
   - Combined with stable usage references, allows React to skip
     re-renders when props haven't actually changed.

Fixes #41480
2026-08-14 15:07:24 -07:00