31 Commits

Author SHA1 Message Date
kshitijk4poor
e326520d50 refactor(codex): one credential/route authority for aux + image paths
Gate round-1 follow-ups on the #121486 fix:
- auxiliary_client: inline the pool route lookup (no dead try/except or
  fallbacks; HERMES_CODEX_BASE_URL short-circuits once) and read auth.json
  directly when the pool yields no token (no second uncached pool load,
  no re-select race pairing a new pool key with chatgpt.com).
- image plugin: _read_codex_credential() is the single source for both
  is_available() and generate(); _post_image_request requires base_url.
- auth_codex: drop the unused _pool_codex_access_token wrapper; the route
  helper's error fallback reads the profile-scoped override, not the raw
  process env.
- model setup flow: the confirm guards get the resolved Codex base, not
  the chatgpt.com constant.
- cli_model_switch_mixin: self.base_url is always set.
2026-09-25 21:27:06 +05:30
kshitijk4poor
e87f673faa fix(codex): send catalog/image credentials only to their own route
Follow-up to the two contributor commits for #121486. The picker, the
image plugin and the auxiliary Codex client still composed a pooled
gateway key with a base re-read from ambient state (HERMES_CODEX_BASE_URL
or the chatgpt.com default), so a model.base_url-only gateway (env unset)
still sent its key to chatgpt.com.

- auth_codex: resolve_codex_runtime_credentials reports the host a pooled
  credential actually routes to (runtime_provider._pool_entry_mode_and_url:
  env > model.base_url while the row is canonical > row URL) instead of the
  ambient default; get_codex_auth_status carries the same bound base_url.
- picker: get_codex_model_ids(access_token, base_url=) now receives the base
  resolved with the token from hermes_cli/models.py, the CLI default-model
  swap (self.base_url) and the `hermes model` Codex flow.
- aux/image: _resolve_codex_credential_and_base() returns (token, base) from
  one pool selection; the image plugin, _build_codex_client and the raw
  Codex client use it (profile-scoped override from #121497 still wins).
- model_metadata: the non-JWT refusal now applies only when the target is
  chatgpt.com; a gateway key may probe its own gateway's /models.

Adversarial regressions: model.base_url with env unset, env/route mismatch,
opaque + JWT gateway keys, pool-selected credential, pool row with its own
gateway URL, direct-ChatGPT positive control.

Addresses @andrexibiza's review on #121508.
2026-09-25 21:27:06 +05:30
brooklyn!
5bc1761cf1 fix(models): allow spaces in self-hosted and user-configured model ids
The whitespace reject ran for every provider, so a self-hosted catalog or a
user-configured base_url could not select an id that legitimately contains
spaces. Cloud providers still reject. Picker payloads drop an id that
reject will still refuse.

Fixes #43140
2026-09-24 06:24:56 -05:00
teknium1
07646a7f72 fix: a chat switched to Codex no longer sends its model to the previous provider's endpoint
The messaging gateway's /model wrote only model + provider to the session row. A row the
Desktop/TUI had persisted with the Nous Portal route kept that base_url and api_mode, so resuming
a chat switched to openai-codex built provider=openai-codex on the Portal URL and posted
gpt-6-luna-900k to inference-api.nousresearch.com/v1/chat/completions: "Model 'gpt-6-luna-900k'
isn't available on ChatGPT or Codex Subscription".

- SessionDB.update_session_model writes the whole route (provider, base_url, api_mode) in both
  shapes resume reads (top-level for TUI/Desktop, gateway_runtime for the CLI) whenever a
  provider is given; the gateway /model passes the switch result's endpoint.
- Resume readers (TUI/Desktop, CLI, gateway rehydrate) drop a persisted base_url that is another
  built-in provider's canonical endpoint, so rows already written by older builds heal.
- A gateway session override whose credentials failed to re-resolve is resolved for its own
  provider on the turn instead of being layered over the default provider's runtime (which
  produced openai-codex + Nous key + Nous URL). If it still cannot resolve, that turn runs on the
  whole default route with the existing one-shot "Provider fallback" notice; the override is kept
  and retried next turn.
2026-09-23 04:44:31 -07:00
teknium1
e03af55edd chore: merge origin/main (resolve website/docs/integrations/providers.md) 2026-09-19 10:47:48 -07:00
teknium1
8e180c69f6 feat: label a model.context_length pin and warn once when it disagrees with the provider
model.context_length short-circuits get_model_context_length ahead of every
provider source, but nothing told the user the number they saw was their own
pin rather than provider metadata (#66168). Keep the pin semantics (custom
endpoints depend on it) and make it visible instead:

- agent/context_pin.py: is_context_pinned / context_pin_suffix for renderers,
  and warn_once_on_pin_disagreement, called from _resolve_context_length at
  agent init. The advertised value comes from LOCAL sources only (endpoint
  cache, models.dev disk cache, hardcoded catalog) so the check never adds a
  network probe or latency to startup; one warning per (model, pin) per process.
- "(pinned)" label on every CLI surface that renders the window: welcome
  banner, /model switch summary, /usage current-context line, wide status bar.
- No new config key or env var; the numeric value used is unchanged.

Fixes #66168
2026-09-19 10:28:34 -07:00
teknium1
3934fe551c fix(codex): sweep the retired gpt-5.3-codex slug off every Codex-OAuth surface
What: the contributor's pick removes the slug from DEFAULT_CODEX_MODELS and the
forward-compat templates. This follow-up finishes the class on the sibling
surfaces that still taught users the retired slug:

- hermes_cli/cli_model_switch_mixin.py: `/model` -> openai-codex replaced an
  untouched default with the literal "gpt-5.3-codex" when live discovery failed;
  it now uses DEFAULT_CODEX_MODELS[0] so the fallback can never drift from the
  curated list again.
- run_agent.py: the Codex silent-hang hint no longer recommends the retired slug.
- website/docs (+ zh-Hans): the Codex OAuth vision/fallback examples used the
  retired slug and claimed a default that no longer exists (openai-codex has no
  implicit auxiliary model); examples now use gpt-5.4 and say to set `model`.
- tests: one invariant test pins the slug out of the curated list and every
  template tuple (#52492); hint/watchdog tests assert the retired slug is absent;
  catalog fixtures switched to live slugs.

Why: the ChatGPT Codex backend returns HTTP 400 "not supported when using Codex
with a ChatGPT account" for gpt-5.3-codex (#52492; second field report incl. the
official CLI on the #81558 thread, 2026-08-10). Same shape as e8955f222c, which
dropped gpt-5.2-codex / gpt-5.1-codex-max / gpt-5.1-codex-mini. Live discovery
still surfaces the slug if the backend re-enables it.
2026-09-19 10:22:33 -07:00
teknium1
c30b750ad2 fix(cli): release the old agent's clients before dropping it for a rebuild
Interactive CLI sites that set self.agent = None (/personality, /reasoning, /fast,
model/route/credential change, first-run setup, MoA one-shot) now go through
_retire_agent(), which calls agent.release_clients() first: on the codex_app_server
route the app-server child belongs to the instance and was orphaned for the CLI
process lifetime (#72548).
2026-09-19 09:43:20 -07:00
kshitij
8c6e97ee7a fix(cli): the /model picker reads the disk-cached live catalog like the gateway pickers
The CLI provider->model step called `provider_model_ids` directly when a row had
no curated models. For per-resource providers (azure-foundry) that is now a live
probe that can walk several api-version fallbacks with a 6 s timeout each, which
must not block the REPL on every select; `cached_provider_model_ids` is what the
gateway/Desktop pickers already read.
2026-09-19 09:42:49 -07:00
teknium1
ba9d68a838 fix(cli): a resumed opencode-go session re-derives its wire format from the stored model
The classic CLI (`hermes --resume`, mid-chat `/resume`, oneshot resume) read the
session row's persisted api_mode/base_url verbatim in stored_session_route(), so
a row written while the session ran an anthropic_messages model on opencode-go
(MiniMax) pinned that wire onto a chat_completions model such as
deepseek-v4-flash-vision-exp after the model column moved. This is the CLI twin
of the tui_gateway _rederive_per_model_route() fix already on this branch: for
providers that pick the wire per model (model_derived_api_mode() is not None)
the route follows the stored model and the relay URL is healed; fixed-wire
providers keep honoring their row.

Probe (direct call of _restore_session_model on the PR head, no network): a row
with provider opencode-go, base_url '' and api_mode anthropic_messages for
deepseek-v4-flash-vision-exp resumed on the Go relay (never api.anthropic.com —
the empty base_url is re-resolved to https://opencode.ai/zen/go/v1 on both the
same-provider and provider-changed paths) but with api_mode anthropic_messages;
after: chat_completions on the same relay.

Docs: the OpenCode paragraph in providers.md now states both behaviours users
can observe from #96066 — per-model routing survives resume, and `*-vision*`
OpenCode ids attach images natively without a supports_vision override.

Part of #96066
2026-09-19 00:45:43 -07:00
Austin Pickett
1792e8bf5f fix(local-runtime): resumed llamacpp sessions follow the live managed port on every surface (#114336)
* test(local-runtime): pin resume to the live managed llama.cpp port

A session that stored last boot's loopback URL must not keep the client
on a dead ephemeral port after the supervisor moves.

* fix(local-runtime): follow the live managed llama.cpp port on resume

Sessions persist last boot's loopback URL, so a supervisor port change
left the client on a dead endpoint. Drop that snapshot for llamacpp
and keep the live supervisor URL.

* fix(tui_gateway): drop the llamacpp snapshot URL at one seam

_resolve_agent_model_runtime already discards a persisted base_url when the
resolution came from the local runtime; the second blank in
_stored_session_runtime_overrides (wrapped in a try/except around an import
and a string compare) duplicated it.

* fix(cli): keep a launch-time --base-url on a same-provider llamacpp resume

Re-resolving the managed endpoint is for the snapshot URL a session persisted;
an explicit --base-url for the provider the session already ran on is user
intent and stays in charge. Also hand target_model to the resolver like the
provider-changed branch does.

* fix(gateway): rehydrated llamacpp overrides follow the live managed port

Same bug class as the CLI and TUI resume paths: after a gateway restart the
persisted /model override kept last boot's loopback URL over the freshly
resolved managed endpoint, so a supervisor that came back on an ephemeral port
(18434 busy) left the session on connection errors.

* docs(local-runtime): port-fallback warning no longer asks for a model re-pick

---------

Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
2026-09-17 14:24:53 -04:00
teknium1
931b5ff9e7 fix(opencode): every credential-resolution surface keys off the model it will send; family heal only for built-in providers
Closes the remaining atoms of #112600.

A) The CLI startup path already passes `-m` as target_model (c358a6fba0), but
   its siblings still resolved credentials against config's `default`: the CLI
   auth-fallback rung, `--resume` credential re-resolution, the gateway
   provider-override helper (channel overrides, persisted /model switches,
   API-server provider refresh), the gateway fallback chain, the TUI /model
   switch-from runtime and ACP agent construction. With a `*-free` default the
   OpenCode free-tier rung fired first and a Go-only model was built against
   the keyless Zen relay ("Model mimo-v2.5 is not supported"). Each now passes
   the effective model; `_resolve_runtime_agent_kwargs_for_provider` grows an
   optional `target_model` and the two test stubs of it accept the kwarg.

B) normalize_opencode_base_url rewrote the /zen vs /zen/go segment for ANY
   provider matched by opencode_provider_family, including custom providers
   merely named after a family (`opencode-go-bridge`, #85589) whose relay the
   user declared explicitly in `providers:`. The family heal now applies to the
   built-in canonical providers only; custom prefix-named providers keep their
   per-model api_mode routing and /v1 handling. Documented in the providers
   guide.

C) Same function: the official-host check uses parsed.hostname (a port no
   longer defeats the heal) and only the path is edited, so query/fragment
   round-trip instead of being dropped.

Fixes #112600
2026-09-17 08:59:22 -07:00
teknium1
587c7d9631 fix(moa): show who pays on every MoA surface and trim the salvage to two invariants
Follow-up to the cherry-picked #112368 so the whole class lands in one PR:

- `hermes model` inventory row (also the gateway picker): the virtual `moa`
  row's warning now says the aggregator is the acting model billed for the
  run and references only advise once per user turn (ported from #112362).
- In-session `/model` switch onto a MoA preset prints
  `Acting model (billed for the run): <provider>:<model>` under the Provider
  line — the preset name alone hides who pays (issue suggestion 3; reuses the
  same aggregator lookup the picker rows already do).
- Desktop: the MoA section blurb moves behind a `moaDescription` i18n key
  (en/zh/zh-hant; the other locales inherit English via `defineLocale`) instead
  of hardcoded component text, following #112362's shape.
- Tests trimmed to two CLI invariants (notice fires on provider mismatch /
  stays silent when providers match or the main provider is unknown) and one
  Desktop assertion on the aggregator row pill.
- Docs: `website/docs/user-guide/features/mixture-of-agents.md` gains a
  "Who pays for a MoA run" callout and documents the mismatch notice.

WHY: a Codex-subscription user put the aggregator on another provider and
had the entire tool loop billed there (#112359). The aggregator is the acting
model on every step, so each surface that shows a preset must say so.

Co-authored-by: KoNit-K <124019182+KoNit-K@users.noreply.github.com>
2026-09-16 17:11:01 -07:00
teknium1
9429f106f7 docs(cli): say why the runtime snapshot deep-copies reasoning_config explicitly
Review flagged the key as emitted twice (by reference via _runtime_fields, then
the explicit deepcopy). The override is intentional: _runtime_fields copies by
reference and reasoning_config is a mutable dict, so dropping the explicit line
would alias the snapshot to the live config. Comment only; no behaviour change.
2026-09-16 17:02:56 -07:00
teknium1
77f5de23dd fix(cli): /model switch, session restore and /new re-resolve reasoning effort for the new model
The classic CLI resolves `reasoning_config` once at startup for the launch model and
passes that field into every lazily built agent. `_stage_and_swap_model` (typed /model
and picker), `_restore_session_model` (--resume, /resume) and `new_session` (/new)
moved `self.model` without re-running the chokepoint, so the first turn after a switch
issued before the first message went out with the OLD model's effort — always-thinking
models that accept only their own level set (GLM/ARK: low/high/max) reject that with a
non-retryable HTTP 400. `agent.switch_model` already re-resolves its own copy, so only
the CLI-level field was stale.

- `_resolve_cli_reasoning(cli)` runs `resolve_reasoning_config(CLI_CONFIG, cli.model)`
  and is called BEFORE the agent branch on all three paths, covering the lazy-build case.
- `reasoning_config` joins `_RUNTIME_FIELDS`, so a failed in-place swap and the
  `/model X --once` restore roll it back with the rest of the route (drops the explicit
  one-turn restore line the field loop now covers).
- `/new` resolves after the config-default model reset, so the default model's
  per-model override is kept while a `/reasoning` session override is still dropped.
- Docs: the override list names the switch-before-first-message, resume and /new paths.

Slimmer redo of #96023 (@liuhao1024): same core hunk, without the `--reasoning`-pins-
for-the-whole-run flag (the live-agent path already re-resolves on switch, so the CLI
field follows the same rule) and with the two entry points #96023 missed
(`_restore_session_model`, `_RUNTIME_FIELDS`), which @catecholamin identified on
#112921. #112924 (@li-lizhe) covers the same `_stage_and_swap_model` hunk.

Fixes #112921
Fixes #96012

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: li-lizhe <li-lizhe@noreply.github.com>
2026-09-16 17:02:56 -07:00
Teknium
8b6931393e Port from langchain-ai/deepagents#5829: confirm mid-session model switches that abandon a large cached context
Providers key prompt caches per model, so a mid-session /model switch makes
the next reply re-read the entire conversation at full input price. deepagents
gates user-initiated switches behind a confirmation once the active thread
exceeds a configurable token threshold; this ports the same protection into
Hermes' unified selection-guard registry so it renders on every surface at
once (CLI/TUI picker, gateway /model, Telegram/Discord pickers, dashboard).

- hermes_cli/model_selection_guards.py: new context_cache guard +
  SelectionContext carrier + selection_context_for_agent() helper;
  registry threads live-session facts to guards (6-arg signature with a
  TypeError fallback for externally patched 5-arg guards).
- config: model.switch_context_confirm_tokens (default 100000, 0 disables).
- cli.py / gateway/slash_commands.py / tui_gateway/server.py: thread the
  live agent's measured context into the guard call.
- docs: configuring-models.md mid-session switch section.
- tests: tests/hermes_cli/test_context_cache_switch_guard.py (13 cases).
2026-09-13 20:54:50 -07:00
teknium1
2c0bec33f9 feat(model-pickers): reasoning effort selection on every model picker
The Desktop composer got a reasoning-effort pill this morning; every other place a
model is picked still left the effort to a separate command (`/reasoning`) or a
hand edit of config.yaml. `hermes model` had one effort step for Copilot only, and
its auxiliary-model menu had none at all even though every aux block already reads
`auxiliary.<task>.reasoning_effort`.

One request now carries a model pick AND its effort on every surface:

- `hermes_cli/model_switch.py`: the single `/model` parser accepts `--reasoning
  <level>` (validated against `parse_reasoning_effort`; unknown level ->
  `MODEL_SWITCH_ERR_BAD_REASONING`; Unicode-dash normalized like the other flags).
  `ModelSwitchRequest.reasoning_effort` rides with the pick.
- Classic CLI (`cli_model_switch_mixin`, `cli_tui_mixin`): `/model X --reasoning
  high` applies the effort AFTER the agent swap (`switch_model` re-resolves
  `reasoning_config` from config.yaml, so an earlier write is clobbered) with the
  pick's scope (session; config on `--global`; `--once` snapshots and restores it).
  The `/model` picker gains a third stage, "Reasoning effort for <model>", built
  from `VALID_REASONING_EFFORTS` + none + "Keep current effort"; hidden when the
  inventory capability map says the route has no reasoning control.
- TUI gateway (`tui_gateway/model_switch.py`, serves Ink TUI + Desktop):
  `config.set model "X --reasoning high"` applies after the swap; session pin
  (`create_reasoning_override`) by default, `agent.reasoning_effort` on --global,
  one-turn restore carries `reasoning_config`; re-emits `session_info` so the
  status bar shows the new effort.
- Ink TUI `ModelPicker`: step 3/3 (same rows, same capability gate) emitting
  `<model> --provider <slug> --reasoning <level> <scope>`; the new-session draft
  label strips the flag like `--provider`.
- Messaging gateway `/model`: `--reasoning` goes through the existing
  `_apply_reasoning_selection` (the `/reasoning` applier) with the pick's scope.
- `hermes model`: one shared post-pick effort step for the MAIN model (replaces
  the Copilot-only inline prompt; Copilot keeps its per-model level set via
  `github_model_reasoning_efforts`, other routes get the ladder, catalog
  `supports_reasoning=False` skips it) plus a "Reasoning effort for the current
  model..." row. The auxiliary menu's provider->model and custom-endpoint flows end
  with the same step (+ "Provider default"), stored as
  `auxiliary.<task>.reasoning_effort` / `delegation.reasoning_effort`, shown in
  the task list ("openrouter · model · high"), cleared by "Reset all to auto";
  tasks whose block omits the key by design (MoA slots, memory_query_rewrite) skip
  it.

Live (temp HERMES_HOME, stub key, no model call):
- `hermes model` -> aux -> Vision -> OpenRouter -> model: before ends at
  "Vision: openrouter · <m>", no key written; after adds "Select reasoning effort"
  and saves `reasoning_effort: high`.
- `hermes model` -> DeepSeek -> model: before no effort step; after the step
  writes `agent.reasoning_effort: xhigh`.
- tui_gateway stdio: `config.set model "... --reasoning high --session"` before
  errors "Model names cannot contain spaces"; after switches and `config.get
  reasoning` returns high; bad level -> the canonical error text.
- classic CLI `process_command`: before the same spaces error; after "Reasoning
  effort: high" under the switch summary, `--global` writes config.
- `hermes --tui` PTY: /model -> step 1/3 -> 2/3 -> 3/3 -> high; transcript
  "reasoning: high", status bar "fable 5.1 high".
2026-09-13 16:43:50 -07:00
teknium1
11576390fe refactor(model): one persist writer for /model across CLI, gateway, TUI, dashboard; ACP + dashboard validate through switch_model
One `/model --global` produced four config.yaml shapes. CLI wrote
default/provider/base_url/api_mode and cleared the context pin on a route
change; the gateway rewrote the whole `model:` block (whole-file save_config)
and only set api_mode for `custom`; the TUI wrote three keys and never
touched api_mode, so a switch off an Anthropic-wire endpoint left a stale
`api_mode: anthropic_messages` in config; the dashboard main slot had its own
switched-provider logic, wrote `base_url: ""` and always dropped
context_length. ACP `session/set_model` and `POST /api/model/set` accepted
any model string (parse_model_input + detect_provider_for_model) so a model
no catalog knows, or a provider with no credentials, was handed to the
session / persisted and only failed at inference time.

Canonical: `hermes_cli.model_switch.model_selection_config_updates` (the
shape) + `persist_model_selection(result, config_path=None)` (targeted
per-key `atomic_roundtrip_yaml_update` writes, so sibling
`model_slots`/`model_fallback` keys survive; explicit path for the
multiplexed gateway's profile config) + `apply_model_selection` (same shape
applied to an in-memory `model:` dict for callers that save a whole
document). `atomic_roundtrip_yaml_update(value=None)` now REMOVES the key
instead of writing `key: null`, so per-key and whole-document writers land
the same file. Shape = CLI/gateway semantics: default, provider, base_url
(cleared when the target has none), api_mode (cleared when unresolved),
context_length cleared only when `should_clear_context_pin` says the route
identity changed, inline api_key/api cleared for non-custom targets.

Sites -> canonical:
  hermes_cli/cli_model_switch_mixin.py::_persist_global_switch          -> deleted; _commit_model_switch calls persist_model_selection
  hermes_cli/cli_model_switch_mixin.py::_clear_persisted_context_for_model_switch -> deleted (folded into the shape)
  gateway/slash_commands_model.py::_persist_model_switch_to_config       -> to_thread forwarder: persist_model_selection(result, ctx.config_path)
  tui_gateway/model_switch.py::_persist_model_switch                     -> deleted; _apply_model_switch calls persist_model_selection
  hermes_cli/web_server_config.py::_apply_main_model_assignment          -> apply_model_selection(result) (+ explicit custom api_key)
  hermes_cli/web_server_config.py::_validated_main_model_selection       -> NEW: switch_model(--provider) gate; rejection -> HTTP 400
  hermes_cli/web_routers/{models,profiles,config_env}.py main-slot paths -> through _validated_main_model_selection
  acp_adapter/server.py::_resolve_model_selection                        -> deleted; _switch_model calls switch_model (provider:model -> --provider), rejection -> ValueError

Behavior changes: TUI --global now writes/clears model.api_mode and clears a
route-changed context pin; gateway --global no longer rewrites the whole
model block (sibling keys survive) and clears api_mode for every target;
dashboard main slot / profile-create model / custom-endpoint activate now
reject unknown/uncredentialed/unlisted models (HTTP 400) and persist the
resolved base_url/api_mode instead of `base_url: ""`; ACP rejects the same
(ValueError surfaced by the command/protocol handler). Gateway persist runs
on a worker thread against the routed profile's config_path (multiplex-safe).
Cleared keys are removed from config.yaml rather than left as `null`. ACP
still never persists.

Kept `_normalize_main_model_assignment`: switch_model rejects a vendor name
posing as a provider (`moonshotai` -> "Unknown provider"), so the
vendor->aggregator repair is not a duplicate; E2E verified both branches.
No config migration: readers already coalesce `base_url: ""` to absent
(`_config_base_url_for_provider`) and gate api_mode on provider match
(`_provider_supports_explicit_api_mode`), so no stale-shape reader bug.

Tests: tests/hermes_cli/test_model_persist_one_shape.py (four surfaces land
one block; same-route re-pick keeps the pin), tests/acp_adapter/
test_acp_dashboard_model_switch_validation.py (rejection + explicit
provider prefix). Replaces test_acp_set_model_explicit_provider.py and the
two TUI-only persist tests; tests that intercepted the old per-surface seams
(`cli.save_config_value`, `load_config_readonly`, `tui_gateway.server.
_persist_model_switch`) now intercept the canonical seam. Each fix
sabotage-verified red.
2026-09-13 05:21:02 -07:00
kshitijk4poor
8077206073 fix(cli): keep the no-stored-model early return ahead of the route read
CI: tests/cli/test_cli_resume_command.py builds bare HermesCLI objects without .model; the
refactor read self.model before the stored-model check the contributor's code made first.
2026-09-09 12:41:07 +05:30
kshitijk4poor
32273b8118 refactor(cli): one stored_session_route for interactive and one-shot resume
_apply_stored_session_runtime was a line-for-line copy of the first half of
_restore_session_model (stored-model guard, session_gateway_runtime, bare-custom heal,
model/provider-changed check). Extract that pure decision into
cli_model_switch_mixin.stored_session_route and have both resume paths call it; the
one-shot keeps only the _ModelChoice mapping and the drop-ambient-key rule.

main.py stops re-normalising `resume` — _resolve_chat_session_args already did.
Tests trimmed from 20 to 13: near-duplicate unit tests of the private helpers go, the
end-to-end _run_agent contracts (stored runtime + reopen; explicit --model wins) and the
empty-session-keeps-id case stay.
2026-09-09 12:41:07 +05:30
Teknium
7a33369e81 simplify(compat): interrupt — drop _ThreadAwareEventProxy/_interrupt_event legacy alias, repoint 2 test files
No runtime consumer read the proxy (terminal_tool/environments call is_interrupted()/set_interrupt()
directly); its only users were tests patching tools.interrupt._interrupt_event, which had no effect on
the code under test. tools/terminal_tool.py's own re-export of the name is owned by another worker.
2026-09-03 14:00:59 -07:00
Teknium
c93ace77c2 simplify(compat): config/runtime_provider/plugins/commands/secrets_cli/kanban — drop 96 re-exports (incl. PEP 562 facades) + 3 aliases (get_pre_tool_call_directive/_block_message, get_telegram_handler_factories), repoint 56 callers + 50 test files 2026-09-03 14:00:17 -07:00
Teknium
e83816a4d1 review-fix(comments): restore lost #NNNN rationale comments across non-test source (mechanical sweep, condensed, code unchanged)
For each issue anchor present in BASE 63279301bc non-test .py and absent on HEAD, the BASE comment/docstring block was re-attached at the HEAD location of the code it explained (matched by the distinctive code line / enclosing def). Sentences already covered by an existing HEAD comment were deduped; the issue number always survives. Insert-only: no code lines changed.
2026-09-03 09:44:26 -07:00
Teknium
3494f7cf23 refactor(hermes_cli): AST-neutral closer hugging across r3-17 slice files 2026-09-02 23:58:31 -07:00
Teknium
670b974bed refactor(hermes_cli): compact mixin docstrings/comments by hand (keep every WHY), drop intra-body blank padding 2026-09-02 21:30:29 -07:00
Teknium
165bcb705d refactor(hermes_cli): collapse announce-gated wake prints, provider-family normalize helper, stream boundary predicate 2026-09-02 21:13:10 -07:00
Teknium
c95a56be06 refactor(hermes_cli): fold picker/typed /model commit into _commit_model_switch + shared switch/dispatch helpers 2026-09-02 21:06:11 -07:00
Teknium
5ddca91a31 refactor(hermes_cli): AST-neutral bracket hug/pack layout pass on g5-model files 2026-09-02 20:56:03 -07:00
Teknium
b39f0337f1 refactor(hermes_cli): unify /model switch commit path (summary, global persist, preflight, runtime-field snapshot) 2026-09-02 20:26:45 -07:00
Teknium
79aeb22886 refactor(cli): unify duplicated model-switch staging and last-assistant-text blocks
cli_model_switch_mixin: the identical snapshot->stage->agent.switch_model->rollback
block in _apply_model_switch_result and _confirm_and_apply_cli_model_switch (2x47
lines) -> _stage_and_swap_model(result, old_model) -> bool. cli_loops_mixin: the
identical reversed-history assistant-text extraction in the loop-tick and goal
post-turn hooks -> _last_assistant_response_text(). Four _StubCLI test doubles bind
the new helper to the real implementation.
2026-09-02 16:17:31 -07:00
Teknium
eb74a00c71 refactor(cli): split HermesCLI into 10 cohesive mixins (cli.py 22,284 -> 9,150)
326 methods lifted by AST (bodies identical; ast.dump-verified) into
hermes_cli/cli_{tui,status_bar,voice,model_switch,session,stream,modal,
terminal,info,loops}_mixin.py. cli.py-internal symbols resolve via lazy
'from cli import ...' inside each method (no import cycle; patch('cli.X')
keeps working). The three 'global' writers (_skill_commands, _cli_wake_owner)
now write the cli module attribute explicitly so the origin's readers still
see them. Dropped imports left unused in cli.py; kept display_hermes_home /
build_welcome_banner as re-exports (mixins + tests resolve them via cli).
Repointed two AST change-detector tests to cli_tui_mixin.py; one test
fixture now keeps 'cli' in sys.modules across its patch.dict scope.
2026-09-02 15:42:24 -07:00