Commit Graph

22221 Commits

Author SHA1 Message Date
kshitij 8f52040dd0 test(cli): regression tests pinning auth-first ordering skips registry sweep
Teknium's review on #63457: existing tests pin the final boolean but not
that the slow PROVIDER_REGISTRY sweep is skipped. Add three tests that
booby-trap hermes_cli.auth.get_auth_status and verify
_has_any_provider_configured() short-circuits on:
- config.yaml model.provider
- config.yaml base_url/api_key (custom endpoint shape)
- auth.json active_provider (sweep-only call-pattern guard)

Mutation-checked: reverting the reorder makes all three fail.
2026-08-04 12:28:39 +05:30
frohsinnllc dbafb59227 perf(cli): check local auth.json/config before slow provider registry sweep
_has_any_provider_configured() probed every api_key provider (gh subprocess
for copilot alone takes 5s; full sweep ~18s) before consulting auth.json and
config.yaml, which are instant local reads. Desktop setup.status calls
blocked past the UI's timeout, causing the connect/disconnect boot loop.
Reorder so cheap local checks run first. Same semantics, ~35x faster here.
2026-08-04 12:28:39 +05:30
kshitij 5c078b987e test(tui): pin picker-cache prewarm wiring in entry.main()
teknium's review gap on #72021: the helper's worker/once-guard was
covered, but nothing asserted the stdio TUI entry point actually
invokes prewarm_picker_cache_async() — or that it does so in the right
place. Add a focused entrypoint test that runs the real entry.main()
with stubbed collaborators (same monkeypatch-module-attrs harness as
test_tui_entry_mcp_owner.py), spies on the helper in
hermes_cli.model_switch (the lazy-import source), and asserts:

- prewarm fires exactly once, strictly AFTER the gateway.ready write
- startup stays non-blocking: main() reaches the stdin loop and
  returns on EOF
- a prewarm failure is swallowed (fire-and-forget) without breaking
  startup

Mutation-checked: deleting the prewarm hunk from entry.py fails both
tests.
2026-08-04 12:28:05 +05:30
ZachariahChu bdc82e39e9 perf(gateway): prewarm /model picker cache on TUI startup
The classic CLI run() loop calls prewarm_picker_cache_async() during the
idle window after the banner is shown, so the first /model open hits a warm
provider-models disk cache and renders in ~100ms. The stdio TUI entry point
never did this, so the first /model open in a TUI session blocked on serial
/v1/models fetches for every authenticated provider.

Mirror the CLI behaviour: kick off the same off-thread prewarm right after
gateway.ready is emitted (banner shown, user about to type). Fire-and-forget,
guarded once-per-process, fully exception-isolated so a slow or offline
provider can never affect TUI startup.
2026-08-04 12:28:05 +05:30
Bryan Bednarski 9ce917f8a9
chore(ci): rerun checks
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-04 00:25:41 -06:00
kshitij 743dc94ab9 chore: AUTHOR_MAP — add BobClawblaw for PR #77870 salvage
Bare noreply email (no NNN+ prefix) needs explicit mapping.
2026-08-04 11:26:39 +05:30
kshitij 9fc8926975 perf: reuse request_input_estimate instead of recomputing estimate_request_tokens_rough
The output-cap error handler already computes request_input_estimate at
line 4722 via estimate_request_tokens_rough(api_messages, tools=...).
The new compression block ~50 lines below was calling the same function
with the same inputs again. Reuse the existing local.
2026-08-04 11:26:39 +05:30
kshitij 78c8bcd122 chore: drop CHANGELOG.md and docs/reports/ — not shipped with salvage PRs 2026-08-04 11:26:39 +05:30
BobClawblaw 04098e2b5f fix(conversation_loop): prune dead vision-strip fallback; harden output-cap retry tests 2026-08-04 11:26:39 +05:30
Hermes Agent 9938d20503 fix(conversation_loop): compress messages on output-cap retry path (#55546)
The output-cap retry loop reduced max_tokens by 64 tokens per attempt but
never called _compress_context(), so the compressor never fired. Input
growth (~65 tokens/attempt) canceled the savings, leaving the session
stuck at 200,001 tokens — 1 over the 200,000 ceiling.

The fix adds compression to the output-cap retry path. The compressor
drops the middle window, freeing ~50% of tokens. If compression makes
>=5% savings, the session continues; otherwise vision payloads are
stripped or the session ends with compression_exhausted=True.

Also adds CHANGELOG.md entry and bug fix report.
2026-08-04 11:26:39 +05:30
kshitij 15d51bb88a refactor: dedup stale-marker regex — use compiled _STALE_MARKER_RE in conversation_loop
The bracketed-marker regex was inlined in conversation_loop.py as
re.fullmatch(r"\[...", ...) while hermes_state.py defines the same
pattern as _STALE_TOOL_CALL_MARKER_RE. Both must agree on what counts
as a stale marker — a drift here means the runtime guard silently
disagrees with the load-on-read repair and CLI purge in hermes_state.

Consolidate onto a single compiled constant (_STALE_MARKER_RE) at
module level in conversation_loop.py, with a comment noting it must
mirror _STALE_TOOL_CALL_MARKER_RE in hermes_state.py. A direct import
from hermes_state was tried first but caused a regression: hermes_state
initializes DEFAULT_DB_PATH = get_hermes_home() / 'state.db' at module
import time, which breaks tests that monkeypatch get_hermes_home() to
return a str (test_slash_worker_accepts_profile_home).

Follow-up to PR #78175 (@JoaoMarcos44).
2026-08-04 11:26:15 +05:30
joaomarcos e18c040c3d fix(cli): back up state.db before clean-markers writes by default
purge_stale_tool_call_markers ran a permanent, irreversible UPDATE with
no backup — inconsistent with repair_state_db_schema's backup-by-default
convention for destructive state.db operations elsewhere in this file.

Take a full snapshot via VACUUM INTO (safe against a live connection,
unlike the raw-copy _backup_db_file used for malformed-schema repair)
before the write, timestamped beside state.db. Skipped when dry_run or
when there's nothing to change. Add --no-backup to `hermes sessions
clean-markers`, mirroring `sessions repair`.

Verified end-to-end: the CLI run against a real temp state.db produces
the backup file before printing the cleared-row count.
2026-08-04 11:26:15 +05:30
joaomarcos e1a2739692 feat(cli): add sessions clean-markers to permanently purge stale tool-call markers (#78148)
The load-on-read repair (_strip_stale_tool_call_markers) fixes affected
sessions in memory on every resume, but never touches the DB — long-lived
sessions re-scan and re-repair the same rows on every load, and the
contaminated bytes stay in state.db (and any backup/cache snapshot of it)
indefinitely.

Add SessionDB.purge_stale_tool_call_markers(dry_run=False): a one-time,
idempotent UPDATE that permanently blanks the content column on affected
rows. Only content is touched — tool_calls and every other column are
left untouched, so provider tool_call/tool_result pairing survives.
dry_run reads through the no-lock read path and never writes.

Wire it up as `hermes sessions clean-markers [--dry-run]`, mirroring the
existing optimize/repair subcommands. Verified end-to-end against a real
temp state.db: dry-run reports the row without writing, the real run
clears it and preserves tool_calls, and a second run is a no-op.
2026-08-04 11:26:15 +05:30
joaomarcos 70d7e4cbdf fix(agent): repair sessions already contaminated with stale tool-call markers (#78148)
The conversation_loop fix (previous commit) stops new "[memory]"-style
bare tool-call markers from being cached/persisted, but sessions written
before that fix can still carry rows where a bare marker was saved as
the assistant's "final response".

Add a load-on-read repair pass in hermes_state.py, mirroring the existing
_strip_background_review_harness defense-in-depth: on session restore,
any assistant row whose content is only a bracketed marker (e.g.
"[memory]", "[skill_manage]") AND that carries tool_calls has its content
blanked before the history re-enters the model's context. The tool call
and its result are left untouched so provider tool_call/tool_result
pairing stays intact. Sessions with no affected rows pass through the
normal path unchanged.
2026-08-04 11:26:15 +05:30
joaomarcos ba9068c8b6 fix(agent): discard bare tool-call marker before fallback/persistence (#78148)
Local tool-call templates can emit a bare bracketed token (e.g. "[memory]")
as assistant content alongside a function call. The loop treated that
protocol scaffolding as visible content: it got cached as the post-tool
fallback, and when the next turn came back empty, the marker was replayed
as the final response and written into the persisted transcript. Later
context compaction preserved that history, letting the model repeat the
marker in subsequent turns.

Detect content that is only a bracketed marker (`[name]`) when the
response also carries tool_calls, and drop it before it can be cached
or persisted. Scoped narrowly: only fires alongside tool_calls, so a
genuine final response of "[memory]" without a tool call is unaffected.
2026-08-04 11:26:15 +05:30
JR Razmus e623432b89 fix: close the Codex app-server session on agent teardown
Salvage of #65260's b7d7cfd0e (ported — the PR's close() predates ~4K
commits of teardown-step churn, so the hunk is re-anchored after step
6b rather than cherry-picked).

agent/codex_runtime.py already drops _codex_session on turn crash and
on retirement, but AIAgent.close() — the hard teardown for /new,
/reset, and session expiry — had no owner for it, so the app-server
child process survived until interpreter exit. Long-lived gateways
accumulate one leaked subprocess per ended Codex session.

The attribute is cleared BEFORE close() so a concurrent reader can't
observe a half-closed session and a raising close() can't strand a
stale reference (tested).

Tests extend the author's original lifecycle test with the
raising-close and no-codex-session cases.
2026-08-04 11:25:18 +05:30
kshitij 9c88625e25 fix(gateway): bound go_dormant ws.close with teardown timeout
Sibling site to the disconnect() fix: go_dormant() still did an
unbounded await self._ws.close(), the exact same pattern bounded in
disconnect(). go_dormant runs on the scale-to-zero suspend path (Fly
autostop), which also has timeout constraints. Apply the same 1s
wait_for treatment using _TEARDOWN_AWAIT_TIMEOUT_S.

Found during review of PR #78027.
2026-08-04 11:17:11 +05:30
Dannyzen 3b0bb3b8bb fix(gateway): keep event loop alive during /compress and Relay drain
Offload manual /compress temporary-agent cleanup through the existing
bounded off-loop helper so a slow agent.close() cannot freeze the
gateway event loop, heartbeat, or platform polling.

Guarantee Relay transport teardown even when the runner cancels
adapter.disconnect() during go_idle: shielded finally, 2s drain-path
idle ACK budget under the 5s outer disconnect budget, and bounded
supervisor/reader/ws.close awaits.

Original commits:
- fix(gateway): offload manual /compress cleanup from the event loop
- fix(gateway): tear down Relay transport even if go_idle is cancelled
- fix(gateway): keep Relay disconnect budgets inside the runner window

By @Dannyzen (PR #78027), salvaged onto current main.
2026-08-04 11:17:11 +05:30
kshitij e4888a9f21
Merge pull request #78226 from kshitijk4poor/chore-razmus-email
chore: add contributor email mapping for johnrazmus
2026-08-04 11:15:55 +05:30
kshitijk4poor f66d625825 chore: add contributor email mapping for johnrazmus 2026-08-04 11:15:36 +05:30
Jordan 60c721ada6 fix(model_metadata): read llama.cpp context from meta.n_ctx + accept sole model 2026-08-04 11:08:32 +05:30
xxxigm 942ff91f21 test(gateway): cover named-custom context pin on session-info banner 2026-08-04 11:00:04 +05:30
xxxigm 3a0a295109 fix(agent): keep context_length pin for named custom providers
Empty model.base_url plus a runtime custom-provider URL was treated as a
route mismatch, so gateway session-reset banners dropped model.context_length
and fell back to the Qwen family default (131K) while /status still showed
the configured 262K pin.
2026-08-04 11:00:04 +05:30
kshitij d218496113
Merge pull request #77924 from kshitijk4poor/chore/author-map-elsnacko
chore: add contributor email mapping for ElSnacko
2026-08-04 10:56:47 +05:30
Jeffrey Quesnelle 70db671fac
Merge pull request #76643 from NousResearch/fix/termux-nemo-relay
exempt android installs from nemo-relay
2026-08-04 00:06:56 -04:00
Teknium 91937a6dc3 test: swap context-switch-guard fixture off qwen3.8-max-preview
test_custom_provider_context_avoids_false_shrink_warning used
qwen3.8-max-preview as a slug that deliberately falls through to the
generic 'qwen' 131K catalog match. The new qwen3.8-max
DEFAULT_CONTEXT_LENGTHS entry (1M) now substring-matches the preview
slug too, so the no-custom-providers branch stopped warning. Swap the
fixture to qwen3.9-max-preview, which still hits the generic fallback
— the test's intent (custom_providers threading) is unchanged.
2026-08-03 17:19:49 -07:00
Teknium 3c3ae7428d feat(models): add qwen3.8-max to Nous portal + OpenRouter catalogs, replacing qwen3.7-max
Qwen3.8 Max is live on both OpenRouter and the Nous portal
(qwen/qwen3.8-max, 1M context, 131K max output). Per the
newest-max-replaces-last-max convention, it takes qwen3.7-max's slot
in both curated lists.

- hermes_cli/models.py: OPENROUTER_MODELS + _PROVIDER_MODELS[nous]
  swap qwen/qwen3.7-max -> qwen/qwen3.8-max
- agent/model_metadata.py: DEFAULT_CONTEXT_LENGTHS entry for
  qwen3.8-max at 1,000,000 (verified against OpenRouter live
  metadata and Nous /v1/models 2026-08-03)
- tests/test_empty_model_fallback.py: swap incidental catalog fixture
  to the surviving slug
- website/static/api/model-catalog.json: regenerated

Pricing snapshot skipped: both routes bill via official_models_api
(live pricing), verified with resolve_billing_route. Reasoning
timeout floor already covered by the qwen3 prefix (180s).
2026-08-03 17:19:49 -07:00
Jeffrey Quesnelle dac4bbea09
fix comment about relay workaround 2026-08-03 19:13:22 -04:00
Bryan Bednarski e1caa611bf
fix(relay): preserve skipped turn context
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-03 13:43:17 -06:00
Bryan Bednarski 2e65b0c604
test(relay): enforce LIFO in overlap regression
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-03 13:43:17 -06:00
Bryan Bednarski a2a08fe147
fix(relay): gate skipped turn metrics
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-03 13:43:17 -06:00
Bryan Bednarski 704baa5c33
fix(relay): preserve legacy turn shims
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-03 13:43:16 -06:00
Bryan Bednarski 9a9b670e29
fix(relay): avoid concurrent turn scope corruption
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-03 13:43:16 -06:00
Teknium a991dfc25d docs: document /personality none|default|neutral reset across personality docs
The reset keywords have existed in both CLI and gateway handlers since
June but were undocumented — users couldn't find how to cancel a
personality overlay. Adds a 'Resetting to the default' section to the
personality feature page and mentions the reset in the CLI guide,
slash-command reference (both tables), and messaging command table.
2026-08-03 12:29:49 -07:00
kshitij 0845232d76 fix: prefer explicit anthropic api key
Cherry-picked from PR #58560 by @itsflownium, adapted to current main
(_getenv instead of os.getenv). Moves ANTHROPIC_API_KEY check ahead of
Claude Code credential file and credential_pool auto-discovery so an
explicitly configured key is never shadowed by auto-discovered OAuth.

Fixes #58546
2026-08-04 00:12:45 +05:30
kshitij 827bb0dd16 chore: add contributor email mapping for ElSnacko 2026-08-04 00:10:03 +05:30
Hao Wang aad8f7412c fix(backup): serialize and atomically publish snapshots 2026-08-03 23:48:55 +05:30
kshitij 53cc628045
Merge pull request #77912 from kshitijk4poor/chore-haowang-email
chore: add contributor email mapping for HAOWANG116
2026-08-03 23:42:45 +05:30
kshitijk4poor f40d63d5db chore: add contributor email mapping for HAOWANG116 2026-08-03 23:42:33 +05:30
kshitij d92a28dd34
Merge pull request #77902 from kshitijk4poor/chore-zabih-email
chore: add contributor email mapping for zabih-sudo
2026-08-03 23:32:26 +05:30
kshitijk4poor 164c3d60b2 chore: add contributor email mapping for zabih-sudo 2026-08-03 23:32:12 +05:30
kshitij efbfe0842f fix(prompt_size): search volatile tier for skills block after the stable->volatile move
CI-caught: compute_prompt_breakdown still looked for <available_skills> in the stable tier, but #37117 moved it to volatile. Search volatile first, fall back to stable for older sessions.
2026-08-03 23:23:39 +05:30
kshitij 9555525a7d docs(system_prompt): fix stale reconstruct_static_prefix docstring example
Simplify-pass finding: the safety note still cited 'skills edited' as a stable-tier input whose change mismatches the rebuilt prefix — after this PR a skill edit changes only the volatile tail (that's the point). Swap the example for genuinely stable-tier inputs.
2026-08-03 23:23:39 +05:30
marzukia 9b9cbdd7eb fix(system_prompt): move skills index to the volatile band
The skills index is runtime-mutable: the agent adds and patches skills mid-session, so it is not byte-stable. Keeping it in the stable band breaks that band prefix-cache contract, because every skill edit changes the stable band and invalidates the entire cached prefix in front of it. Move it to the front of the volatile band so the stable scaffold (identity, tool guidance, model guidance) stays cacheable across skill edits.
2026-08-03 23:23:39 +05:30
ethernet 7eefb09314 fix(nix): tie devShell's HERMES_PYTHON to the venv actually on PATH
`nix/devShell.nix` collected `devShellHook` by scanning every package:

    nonNpmHooks = map (p: p.passthru.devShellHook or "") packages;

But `minimal` and `messaging` are `.override` variants of `default`, so
each carries its own `devShellHook` exporting its own HERMES_PYTHON. The
scan therefore concatenated three conflicting exports and forced Nix to
evaluate and realise three separate uv2nix editable venvs on every
`nix develop`.

`attrValues` is alphabetical, so the last hook won (`minimal`) while
`python`/VIRTUAL_ENV came from `default`'s devDeps:

    HERMES_PYTHON        = ...dimim2... (minimal — no optional deps)
    python / VIRTUAL_ENV = ...85r28... (full)

Inside the shell `$HERMES_PYTHON -c "import anthropic"` failed while
`python -c "import anthropic"` succeeded. Worse, `scripts/run_tests.sh`
prefers HERMES_PYTHON, so the suite ran against the minimal venv. Its
guard did not catch this: it only checks that HERMES_PYTHON has pytest,
and minimal's venv does (pytest is in the `dev` group), so the wrong
interpreter was silently accepted.

Tying the hook to `packages.default` — the same package whose `devDeps`
are installed — keeps HERMES_PYTHON, `python`, and VIRTUAL_ENV pointing
at one venv by construction.

    editable venvs referenced   3      -> 1
    their combined closure      421 MB -> 140 MB
    test failures               85     -> 32

The venv mismatch was masking 53 failures; e.g. test_web_tools_config.py
goes 2-failed -> 38-passed. Full suite is now 25369 passed / 32 failed,
and those 32 reproduce identically on a pristine HEAD worktree with no
nix/ changes under the same interpreter (mostly NixOS artifacts — tests
spawning bare `python3` in a scrubbed env exit 127).
2026-08-03 13:32:08 -04:00
kshitijk4poor ddae511ab1 fix: thread extra_headers through the call_llm split
The PR's concurrency wrapper splits call_llm into a semaphore-guarded
entry + _call_llm_impl; main added extra_headers to call_llm's
signature after the PR's base, so the split has to forward it too
(dropped silently otherwise — Azure Foundry and custom-endpoint
callers set it).
2026-08-03 23:01:03 +05:30
konsisumer 23f8ae32c0 fix(agent): cap auxiliary LLM concurrency per task 2026-08-03 23:01:03 +05:30
kshitij 00475e1b26 fix(catalog): validate http+api_key manifests declare the header's env key
Simplify-pass follow-up on the #70782 salvage: _bearer_auth_headers
hard-emits ${MCP_<NAME>_API_KEY} but install_entry only persists
auth.env-declared vars — a manifest naming its key differently (the
shipped n8n style) would install cleanly yet send a literal-placeholder
header at connect time (silent 401, the #37792 bug class). Enforce the
naming contract at parse time. Also pins the secret-stays-in-.env
property in the install test (raw config.yaml carries the template,
never the secret). Mutation-checked: validation disabled -> guard test
fails.
2026-08-03 22:55:36 +05:30
kshitij f8f475569f perf(compressor): release allocator pages after successful compaction
A successful compaction frees the largest allocation a long session ever
drops (the compressed-away message dicts), but Python's arena allocator
keeps those pages in the heap — RSS retains the pre-compaction
high-water mark until exit. #76905's trim_memory lifecycle covers the
gateway/TUI housekeeping loops but not the CLI compression path.

Call trim_memory(reason='post-compression') at the compression-success
point in ContextCompressor.compress(), following the house pattern
(lazy import in try, debug-level log on failure). The helper is
glibc-gated, config-gated and rate-limited, so it is a safe no-op on
other platforms and cannot fail compression.

Re-expresses the intent of #70782 (JonthanaHanh), which reached for a
bare gc.collect(); trim_memory is the house mechanism and already
wraps a collect.
2026-08-03 22:55:36 +05:30
JonthanaHanh 861ca18c67 fix(catalog): wire api_key auth headers for http MCP servers
When an optional-mcps manifest declares transport.type=http with
auth.type=api_key, install_entry() prompts for the key and saves it to
.env, but _build_server_config() only handled the oauth case — the
api_key case produced a bare url entry with no headers, so every
request to the server was unauthenticated (-> 401).

Reuse _bearer_auth_headers(entry.name) from mcp_config.py so the
catalog path emits the same 'Authorization: Bearer ${MCP_..._API_KEY}'
template as the manual 'hermes mcp add --url' path.

Salvaged from #70782 (production hunk applied clean; tests re-anchored
onto current main). Credit: JonthanaHanh.
2026-08-03 22:55:36 +05:30