Commit Graph

2885 Commits

Author SHA1 Message Date
kshitij
5118692c25 fix: replace double-lambda with functools.partial, close from_env config_path gap
- _submit_background and _prefetch_provider: replace unreadable
  (lambda inner: (lambda: ctx.run(inner)))(fn) with functools.partial(ctx.run, fn)
- from_env(): set config_path=resolve_config_path() so bound_config_path()
  doesn't re-resolve from ContextVar on daemon threads (the exact bug
  the PR fixes for from_global_config)

Review follow-ups for salvaged PR #83525.
2026-08-13 23:43:15 +05:30
Erosika
a5dde2d176 fix(memory): propagate contextvars through MemoryManager background lanes
MemoryManager dispatches provider sync_turn/queue_prefetch work on a
single-worker executor and hot prefetch on a plain thread. Neither
carried the caller's contextvars, so in multi-profile processes the
provider work ran outside the profile's ContextVar-scoped HERMES_HOME
override — any ambient resolution inside a provider landed on the
default profile.

Wrap the submitted callable and the prefetch thread target with
contextvars.copy_context().run, mirroring the gateway's
_run_in_executor_with_context pattern. Provider-agnostic: benefits
every external memory provider, not just Honcho.
2026-08-13 23:43:15 +05:30
luoxiao6645
b09e1daa84 fix(agent): reject stale 32k metadata for MiniMax 2026-08-13 11:12:05 -07:00
sasquatch9818
6def7ce1df fix(models): write context-length cache atomically
save_context_length() and _invalidate_cached_context_length() did an
unguarded read-modify-write into $HERMES_HOME/context_length_cache.yaml.
The plain `open(path, "w")` truncates the file before the dump runs. If
the process is killed mid-dump, the file is left empty or partial. The
next _load_context_cache() swallows the YAML error and returns {} —
silently wiping every persisted context length. A concurrent process
reading between truncate and dump-complete also sees a torn file.

After the cache is lost, every model re-probes the network, and when a
probe fails it falls back to the generic 256K default — so a user on a
1M-window model ends up with a wrong, short context window.

Hermes routinely runs several processes against one shared $HERMES_HOME
(a cron agent plus an interactive session, multiple gateway sessions),
so this is hit in normal use.

Switch both writers to the existing utils.atomic_yaml_write helper
(temp file + fsync + os.replace, symlink- and mode-preserving). The real
file is only ever swapped from a fully written temp file, so an
interrupted write leaves the previous cache intact and readers never see
a partial file. Matches the atomic-write pattern already used for
auth.json, config.yaml, and other persisted state.

Makes the persistent model context-length cache write crash-safe. The
old non-atomic write could truncate or wipe the entire cache on an
interrupted or concurrent write, which then forces models onto the wrong
fallback context window. The fix routes both cache writers through the
repo's atomic temp-file + os.replace helper.

N/A

- [x] 🐛 Bug fix (non-breaking change that fixes an issue)
- [ ] ✨ New feature (non-breaking change that adds functionality)
- [ ] 🔒 Security fix
- [ ] 📝 Documentation update
- [ ] ✅ Tests (adding or improving test coverage)
- [ ] ♻️ Refactor (no behavior change)
- [ ] 🎯 New skill (bundled or hub)

- `agent/model_metadata.py`: `save_context_length()` and
  `_invalidate_cached_context_length()` now write via
  `utils.atomic_yaml_write` instead of a truncating `open(path, "w")`.
  Added the `atomic_yaml_write` import.
- `tests/agent/test_model_metadata.py`: added
  `test_write_failure_leaves_existing_cache_intact` — simulates a crash
  during the atomic swap and asserts the existing cache survives
  byte-for-byte with no stray temp file.

1. `pytest tests/agent/test_model_metadata.py -q` — 98 pass, including
   the new crash-safety test.
2. The new test seeds a valid cache, forces the swap step to raise, and
   confirms the file is not truncated and no `.cache_*.tmp` is left.
3. `ruff check agent/model_metadata.py` passes.

- [x] I've read the Contributing Guide
- [x] My commit messages follow Conventional Commits (`fix(scope):`, etc.)
- [x] I searched for existing PRs to make sure this isn't a duplicate
- [x] My PR contains **only** changes related to this fix
- [x] I've run the affected tests (`pytest tests/agent/test_model_metadata.py -q`) and they pass
- [x] I've added tests for my changes
- [x] I've tested on my platform: macOS 15 (Darwin 25.5)

- [x] I've updated relevant documentation (README, `docs/`, docstrings) — or N/A
- [x] I've updated `cli-config.yaml.example` if I added/changed config keys — or N/A
- [x] I've updated `CONTRIBUTING.md` or `AGENTS.md` if I changed architecture or workflows — or N/A
- [x] I've considered cross-platform impact (Windows, macOS) — the helper uses os.replace, which is atomic on both
- [x] I've updated tool descriptions/schemas if I changed tool behavior — or N/A
2026-08-13 11:08:26 -07:00
fangliquan
db5e2402c2 fix(xai): preserve Grok 4.6 wire capabilities 2026-08-13 11:07:37 -07:00
Omar Baradei
2fb28ad3d4 Refresh context-length zero-guard on current upstream/main
Reapply the non-positive context-length guards onto the post-history-replacement
mainline without carrying any stale branch history. save_context_length() now
refuses to persist length <= 0 (keeping upstream's normalized _context_cache_key),
and get_model_context_length() drops non-positive cache hits at the head of the
invalidation chain (Codex/Kimi/MiniMax/Grok branches become elif) so a poisoned
entry re-resolves instead of short-circuiting to 0.

Refresh of PR #25812; original head d62ed5eb92f057d8c707ba937b44f168f2df0677.
2026-08-13 11:05:49 -07:00
whirmill
4a6d3640b9 fix(agent): default context lookup for empty model IDs
An empty/blank model id reaching get_model_context_length() can't be
meaningfully resolved — and it's worse than a miss: the endpoint
metadata fuzzy matcher ('model in key or key in model') is vacuously
true for "", so it matches an ARBITRARY catalog entry from the live
/v1/models response and returns whatever context length that entry
happens to have, persisting it under a junk '@<base_url>' cache key.

This started failing CI on main when the Nous portal catalog changed:
tests/run_agent/test_primary_runtime_restore.py constructs agents with
model='' against the live portal URL, the arbitrary match now lands on
a 32K entry, and init_agent raises the 64K-floor ValueError
(test_allowed_for_nous_anthropic_messages, red on every PR's slice).

Guard early: a blank model id falls back to DEFAULT_FALLBACK_CONTEXT
immediately, before any cache write or network probe.

Salvaged from #65515 by @whirmill (rebased onto current main; the
guard now sits after the malformed-base_url normalization added since,
and carries an explanatory comment for the fuzzy-match footgun).

Fixes the red slice on #85444, #85452 and every other open PR.

Co-authored-by: whirmill <5079591+whirmill@users.noreply.github.com>
2026-08-13 23:19:07 +05:30
kshitij
8b243dff62 fix: security + efficiency review fixes for salvaged PR #74379
1. Use open_credentialed_url() instead of bare urlopen() in
   templates.py apply_template() and probe_existing_customization().
   Both send Authorization: Bearer headers; bare urlopen forwards
   credentials on cross-origin redirects. The codebase has
   open_credentialed_url() in hermes_cli/urllib_security.py that
   strips credentials on cross-origin redirects — used by 4 other
   modules.

2. Guard unavailable_reason() with the dedup set check before
   calling it. The gateway builds a fresh AIAgent per message, so
   without this guard unavailable_reason() (which calls _load_config()
   → stat + file read + JSON parse, and _check_local_runtime() →
   importlib probes) runs on every gateway turn for an unavailable
   provider, even though the warning is deduped after the first.

3. Move INDICATOR_GLYPH from Hindsight's eye emoji to a generic
   brain (🧠) in core (agent/memory_provider.py). Hindsight overrides
   with its own _HINDSIGHT_GLYPH (👁️) in recall_status() and
   _emit_saving_indicator(). Other memory providers no longer inherit
   Hindsight's brand mark as the default glyph.
2026-08-13 23:15:25 +05:30
Ben
34c727c5c2 feat(hindsight): memory provider improvements — recall_sync, retain_source, setup templates, memory indicators, error hints
Bundles previously-separate Hindsight/memory PRs into a single review surface:
- opt-in synchronous recall (recall_sync) — recall the injected memory in-turn instead of next-turn prefetch (#5820)
- actionable error when local_embedded runtime is missing — tells the user which package to install (#7718)
- default retain_source to 'hermes' so every stored memory self-identifies its provenance
- offer a starter memory template during hermes memory setup, plus warn before overwriting an already-configured bank
- warn when a configured memory provider reports unavailable (#2765)
- deterministic 'recalled N memories' recall indicator — Hermes itself emits a status line when auto-recall injects memory
- 'saving to memory' retain indicator — emitted the moment a turn is dispatched to the writer

Authored by @benfrank241 (ben.bartholomew@vectorize.io).
Salvaged from PR #74379.
2026-08-13 23:15:25 +05:30
Teknium
85e08110fb fix(relay): defer rotating-compaction session close while a turn is live
notify_session_compacted closed the old session scope immediately on a
legacy rotating compaction. A compaction can complete while a turn is
still live on the old session; closing then pops the session scope under
the live turn scope, violating the stack's LIFO order — the exact
invariant the rest of the segmentation feature protects.

Now: when the old session has an active turn, set close_pending instead;
that turn's end_turn consumes the flag after its own turn scope pops and
it unregisters from the active-turn table. Sabotage-verified: the new
test fails without the fix.
2026-08-13 10:45:15 -07:00
Victor Kyriazakos
11c74beffa feat(relay): session-span segmentation for continuous sessions
Continuous gateway sessions keep the Relay session scope open for days;
close-driven export means the session root span and out-of-turn marks
never export until /new or idle-end, and a crash loses the open segment
entirely.

Opt-in segmentation (both defaults OFF => scope lifecycle byte-identical
to today):

  gateway.telemetry.session_segments.on_compaction: false
  gateway.telemetry.session_segments.max_turns: 0

Rotation closes the current session scope and pushes the next segment
(same session_id attribute, plus hermes.session.segment=N and
segment_reason=compaction|max_turns) ONLY at a turn boundary in
begin_turn — never mid-turn (scope stack is LIFO). Compaction completion
just flags rotate_pending (observer semantics, nothing on the compaction
critical path); legacy rotating compaction closes the orphaned old
session scope so its segment exports. Both native calls ride the
existing bounded scope-op executor: a wedged rotation costs one segment
span, never the agent. Segment bookkeeping advances even on native
failure so a degraded rotation cannot retry every turn.
2026-08-13 10:45:15 -07:00
kshitij
1df33dccfe fix: restore stale-base revert hunks in conversation_loop.py and moa_loop.py
The diff-apply salvage introduced stale-base revert hunks — the PR was 1246
commits behind main, and its diff for conversation_loop.py and moa_loop.py
silently dropped symbols added after the PR's base (e.g.
_CODEX_ACK_CONTINUATION_NUDGE, _INTERRUPT_SCAFFOLD_MARKER, cache_ttl plumbing,
finalize_turn import, _restore_user_after_reference_handoff).

Restored both files to origin/main and re-applied only the PR's additive
changes: _moa_reference_metrics_for_hook, _system_prompt_for_hooks, the
system_prompt= and moa_references= hook kwargs, _last_reference_metrics
attribute and accessors, and the slot_metrics population in the fan-out path.

Fixes CI ImportError: cannot import name '_CODEX_ACK_CONTINUATION_NUDGE' from
'agent.conversation_loop'.
2026-08-13 23:10:16 +05:30
kshitij
ace830134e fix: reuse redact_sensitive_text, fix leaky abstraction, fix test data
Follow-up fixes from /hermes-pr-review + /simplify-code on PR #83437:

1. Replace _redact_secrets with agent.redact.redact_sensitive_text(force=True)
   — the plugin's 11-pattern list was a strict subset of the 50+ patterns in
   agent/redact.py. Secrets like Stripe keys, Google API keys, GitLab tokens,
   HuggingFace tokens, DB connection strings, and Telegram bot tokens would
   all leak through the plugin's list but are caught by the existing redactor.
   Added pk-lf- (Langfuse public key) to _PREFIX_PATTERNS in agent/redact.py.

2. Remove dead 'not isinstance(client, object)' check in on_session_finalize —
   always False for any Python value.

3. Fix MoAClient.last_reference_metrics() to call the public
   self.chat.completions.last_reference_metrics() instead of reaching into
   the private _last_reference_metrics attribute via getattr.

4. Deduplicate _coerce_request_messages call in on_pre_llm_request — pass
   pre_coerced=input_messages to _messages_for_langfuse_input to avoid
   double-coercion + double _capture_content serialization per API request.

5. Add HERMES_LANGFUSE_CAPTURE to OPTIONAL_ENV_VARS in hermes_cli/config.py
   for consistency with the other HERMES_LANGFUSE_* env vars.

6. Fix test_sanitized_mode_redacts_secrets test data — the old samples
   ('sk-abc...1234', 'sk-ant...1234', 'Authorization: Bearer ***') were too
   short to match the regex thresholds and never actually tested redaction.
   Updated to realistic-length secrets and changed assertions to check that
   the output differs from input (redact_sensitive_text masks rather than
   inserting the literal string 'REDACTED').
2026-08-13 23:10:16 +05:30
kshitij
e665300d6b feat(langfuse): widen tracing to errors, sessions, subagents, and MoA fan-out
Salvaged from PR #83437 by @erosika, with adopted fixes from @bgodlin (#81054),
@aldoeliacim (#82332), @nftpoetrist (#42326), @rodboev (#39653), @FnExpress
(#64292, supersedes #32175 by @db-aeon), @Per0-1 (#61166), @NaMinhyeok (#64797),
and @liuhao1024 (#43130).

Widens the bundled Langfuse plugin from 6 to 11 hooks and fixes two
attribution bugs. Also adopts shutdown/atexit lifecycle fixes and composes
8 prior community PRs with interaction-fix follow-ups.

Model attribution: on_pre_llm_request and on_post_llm_call now prefer the
wire value (request body model, response model) over the agent attribute,
which goes stale after /model switch or provider fallback.

Cost total: both cost paths now send a summed total alongside the per-type
breakdown, since Langfuse does not derive calculatedTotalCost from
cost_details keys. Subscription-included routes send no cost keys at all.

New coverage: api_request_error closes failed generations with ERROR level;
on_session_finalize/on_session_end close dangling traces for tool-only and
interrupted turns; subagent_start/subagent_stop trace delegated children as
spans; MoA advisor fan-out emits one generation per advisor priced at the
advisor's own model.

Capture modes: HERMES_LANGFUSE_CAPTURE=metadata|sanitized|full (default
sanitized). Sanitized mode redacts secret patterns before truncation.

Adopted lifecycle fixes: shutdown client at session finalize when
reason=shutdown (not on session rotation); atexit finalizer ends open root
spans for short-lived processes; root context manager exited to prevent
interpreter-teardown TypeError; TOCTOU on _get_langfuse() fixed with lock;
reasoning_content surfaced in traces; system prompt included in generation
input for Anthropic/Codex/Bedrock; SDK v3 update_trace replaces set_trace_io.

Closes #29482, #43129, #72661.
Supersedes #81054, #82332, #42326, #39653, #64292, #32175, #61166, #64797, #43130.
Partially addresses #67544 (capture modes + secret redaction; user_id remains open).
2026-08-13 23:10:16 +05:30
Nikita Barkov
3cf8293e44 fix(auxiliary): keep /anthropic base_url for anthropic_messages custom endpoints
The custom + explicit_base_url branch of resolve_provider_client()
unconditionally rewrote a trailing /anthropic to /v1 via
_to_openai_base_url(), even when api_mode was anthropic_messages. The
Anthropic wrapper then never saw the real /anthropic path, so auxiliary
tasks (title generation, compression, vision, web_extract,
session_search) hit .../v1/chat/completions on a Messages-only endpoint
and failed.

Guard the wrap base on api_mode: for anthropic_messages, pass the raw
/anthropic base to _wrap_if_needed (which builds the Anthropic wrapper),
while the plain OpenAI client keeps the /v1-rewritten base so the
OpenAI-wire fallback (used when the anthropic SDK is unavailable) never
lands on /anthropic/chat/completions.

Refs #16254
2026-08-13 10:32:51 -07:00
Teknium
91e550b0cf fix(model_metadata): generalize pre-catalog stale context-cache guard
Replaces the per-model _model_name_suggests_grok_4_3/_grok_4_6/
_minimax_m3 stale-cache predicates with one generic
_stale_pre_catalog_cache_entry() guard driven by
_PRE_CATALOG_STALE_KEYS. A cached context length is dropped when the
model resolves (longest-key-first, same as step 8) to a listed catalog
key and the cached value is at or below what the old resolution path
could have produced (largest shorter matching catch-all, or the 256K
fallback).

Also covers qwen3.6-plus, grok-4-fast, and grok-4.20 (the models
PR #37684 requested guards for), absorbing that PR.

_model_name_suggests_minimax_m3 is kept for its two non-cache callers
(models.dev underreport guard, cache-control gating in
agent_runtime_helpers).
2026-08-13 10:21:50 -07:00
Julientalbot
53ad7794e5 fix(xai): drop stale 256K grok-4.6 context cache
docs.x.ai (2026-08-12): grok-4.6 is the flagship, 500K context.
Live GET /v1/models lists grok-4.6 at context_length 500000
(no grok-4.6-latest alias).

#84661 landed the catalog. Main already lists native grok-4.6
on the xAI picker. This is only the leftover cache guard
(same pattern as grok-4.3): pre-catalog builds persisted the
grok-4 catch-all (256K).
2026-08-13 10:21:50 -07:00
Teknium
1a796a1247 fix(model_metadata): never fuzzy-match an empty model name against endpoint catalogs
'' is a substring of every catalog key, so _resolve_endpoint_context_length
with an empty model name "matched" whatever the endpoint listed first —
on the Nous portal that is currently a 32K embedding model, which poisoned
the resolved context length and made AIAgent init fail the 64K minimum.
This is what turned tests/run_agent/test_primary_runtime_restore.py::
TestTryRecoverPrimaryTransport::test_allowed_for_nous_anthropic_messages
red on every PR (CI slice 7/12) after the portal catalog reordered.

Single-model endpoints still resolve with an empty name (unambiguous);
non-empty names keep the substring fuzzy match.
2026-08-13 10:15:12 -07:00
webdevtodayjason
c7c687aa4b feat(plugins): rename hook to transform_api_error_classification per #64231 verdict
Applies the batch-disposition SALVAGE conditions from #64231: the hook id
moves to the taxonomy transform-family name, and run-all-then-pick-first
dispatch now logs a runtime warning when a valid-but-losing classification
is skipped (the #64714 skipped-transform rule). Chaining semantics are
stated explicitly at the VALID_HOOKS entry, the dispatch helper docstring,
and the hooks.md catalog row and detail section.
2026-08-13 09:36:02 -07:00
webdevtodayjason
0180907fe8 fix(plugins): synthetic hook fixture, shell-hook exclusion, docs per review
Rebased onto current main, where the OpenRouter tool-use 404 is now
handled natively (the bundled demo's exact reason to exist), so the demo
plugin is removed per the standalone-repo policy and every test now uses
a synthetic unclaimed error (fake provider, neutral message, no status
code) that no present or future built-in rule can claim.

classify_api_error is now explicitly Python-plugin-only: VALID_HOOKS
doubles as the shell-hook allow-list, but the shell response parser has
no channel for the classification directive, so shell registrations are
refused at config parse with a warning instead of being silently
ignored (new SHELL_UNSUPPORTED_HOOKS set + regression test).

The hook is documented in the hooks reference as the third
behavior-changing hook, with the full kwargs contract, return shape,
and the Python-only note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnMCvi2vXqfs996AjVeF2F
2026-08-13 09:36:02 -07:00
webdevtodayjason
1d93b549ca feat(plugins): add classify_api_error hook so provider plugins can own error quirks
Adds a plugin seam at the top of agent/error_classifier.classify_api_error()
(step 0, before the built-in pipeline) so model-provider plugins can classify
their provider's error quirks without patching core:

- New "classify_api_error" entry in VALID_HOOKS. Callbacks receive the parsed
  error context (provider, model, status_code, error_type, error_code,
  error_message, error_body, error, approx_tokens, context_length,
  num_messages), self-scope on `provider`, and return None to pass or a dict
  {"reason": "<FailoverReason name>", ...optional recovery-hint overrides}.
- get_plugin_error_classification() helper mirrors
  get_pre_tool_call_block_message(): first valid result wins, invalid dicts
  and unknown reasons are skipped, callback exceptions are isolated — a
  broken plugin can never break classification. Zero behavior change when no
  plugin claims the error (all 179 existing classifier tests pass untouched).
- Bundled reference plugin `openrouter-tool-use-404` (opt-in, like all
  bundled standalone plugins) re-implements PR #58451: OpenRouter's
  "No endpoints found that support tool use" 404 carries no
  _MODEL_NOT_FOUND_PATTERNS signal, so it classifies as unknown/retryable
  and the retry loop burns 3-5 attempts on a deterministic rejection.
  The plugin classifies it as model_not_found (retryable=False,
  should_fallback=True) so the fast-fallback path fires immediately —
  demonstrating a waiting core PR converted to a publishable plugin.

Motivation: ~10 open PRs are single-provider error-classification patches
(#58451, #58355, #58502, #58474, #58366, ...). This hook turns that whole
class of contribution into plugin territory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWMcB7RPSYUpsXDfBgwjzM
2026-08-13 09:36:02 -07:00
Teknium
2a26693e22 feat(delegation): live orchestration of running subagents via delegate_task action param
delegate_task gains a control plane: action='list' / 'steer' / 'stop'
let the parent agent see, redirect, and early-stop its own running
subagents mid-flight — the model-facing counterpart of the TUI's
delegation.pause / subagent.interrupt / subagent.steer RPCs.

- action='list': live children of this conversation's spawn tree
  (ids, goal, status, running_seconds, accepting_steer, live
  transcript path). Ownership is enforced via a _delegate_parent_ref
  weakref chain stamped at child build time, so a conversation can
  only control its own descendants, never a sibling tree.
- action='steer': queues text into a running child via the existing
  steer_subagent() registry path (delivered at the child's next tool
  boundary; missed steers surface as missed_steer in the completion).
- action='stop': interrupt_subagent() — child stops at its next
  iteration boundary, partial result still re-enters as a completion.
- Spawn dispatch response now includes subagent_ids + control hint.
- Control actions run synchronously (never backgrounded) and bypass
  the spawn pause gate and depth limit; they also never consume the
  per-turn subagent spawn cap, and remain usable once the cap is hit
  (that is when stop matters most).
- Small-model robustness (found live with gpt-5.4-mini on Nous
  Portal): tasks=[] alongside goal no longer trips the "Batch mode
  requires at least 2 tasks" gate — treated as single-goal.
- CLI display: control calls render as "steer sa-…" / "list" instead
  of an empty goal.

Live-tested E2E on Nous Portal (fable-5 + gpt-5.4-mini): full
spawn→list→steer→stop cycle, plus a steer-efficacy run where the
child acked the steer mid-essay and switched topics before finishing.
2026-08-13 09:34:36 -07:00
Drexuxux
6c2d4efd02 fix(agent): keep native compaction checkpoints out of the thinking-only drop
A type="compaction" item is the server-side stand-in for history that has
already been pruned, and it rides the same codex_reasoning_items sidecar as
per-turn reasoning. e00965a7e taught compaction pruning to filter that
sidecar instead of popping it so checkpoints survive on every retained
message.

The thinking-only sanitizer reaches the same sidecar from the other
direction and asks a coarser question: does any item have type ==
"reasoning"? A commentary turn carrying both a reasoning item and a
checkpoint answers yes, so the whole message is dropped from the wire copy
and the only copy of the checkpoint goes with it. The request then carries
neither the compacted history nor the checkpoint standing in for it.

Extract has_compaction_checkpoint() into agent/native_compaction.py — the
module that owns the concept, and where merge_interim_reasoning_items()
already spelled the same predicate inline — and consult it before the
thinking-only verdict. A reasoning-only carrier is still dropped.
2026-08-13 03:04:45 -07:00
Teknium
e029e300ca fix(compression): harden native compaction rejection matcher + config coercion (#82777)
Two reliability gaps from #82777:

1. Rejection matcher required only a field-name mention, so a transient
   5xx/timeout whose body echoed the request (which contains
   context_management) permanently downgraded native compaction for the
   session. Now requires rejection language (unknown/unsupported/invalid/...)
   alongside the field name, and when a parsed HTTP status is available,
   400 specifically — non-400 statuses never match. Message-only transports
   (no status attribute) keep working unchanged.

2. compression.codex_responses_native was coerced with bool(), so the
   strings "false"/"off" enabled the feature. Now uses the shared
   utils.is_truthy_value helper.

Conversation-loop call site passes api_error.status_code through.
Sabotage-verified: reverting the matcher to field-name-only fails the new
echo and non-400 tests.
2026-08-13 03:04:31 -07:00
kshitij
a723351a92 refactor: hoist preflight clear into restart handler, single-source qwen predicate
Simplify-pass follow-ups on the salvage stack (all guard tests re-run,
mutation-checked):

1. conversation_loop.py: moved `_preflight_compression_blocked = False`
   from 9 per-site copies into the restart_with_rebuilt_messages handler
   (its single consumer). Besides removing the 9 duplicated blocks, this
   fixes a 10th pre-existing retry-loop site (content-filter stall
   failover, #32421) that set the flag and broke WITHOUT clearing the
   preflight block — a content-filter failover previously restarted with
   preflight compression still blocked against the fallback's smaller
   window, the same #84733 bug class. The outer-loop empty-response site
   keeps its own clear (it never passes through the handler). New AST
   guard test_restart_handler_clears_preflight_block pins the hoisted
   clear (mutation-checked).

2. agent_runtime_helpers.py: extracted _raw_cache_ttl_from_config() —
   prompt_caching_disabled_from_config and configured_cache_ttl were
   verbatim copies of the same config read. Added VALID_CACHE_TTLS.

3. prompt_caching.py: added is_qwen_model() next to
   ALIBABA_FAMILY_PROVIDERS; effective_cache_ttl and
   anthropic_prompt_cache_policy now share both the family set and the
   qwen predicate — neither can desync.

4. Guard-test hardening: assert every _try_activate_fallback reference
   is a direct `if agent._try_activate_fallback(...):` site, so a future
   `activated = ...` form can't silently escape the restart-discipline
   guard.
2026-08-13 15:24:47 +05:30
kshitij
d7517d6e73 fix: restore empty-response fallback retry, dedupe alibaba set, thread TTL into aux replan
Follow-ups on the salvaged #84782 (webtecnica):

1. conversation_loop.py: the empty-response fallback site sits directly
   in the OUTER iteration loop, not the retry loop. The salvaged commit's
   `break` there exited the conversation loop and ended the turn without
   ever calling the just-activated fallback (caught by CI:
   test_empty_response_triggers_fallback_provider). Restored `continue`
   (which already re-runs the pre-API preflight at the top of the next
   outer iteration) while keeping the `_preflight_compression_blocked`
   reset. The other 9 sites are inside the retry loop, where `break` to
   the restart_with_rebuilt_messages handler is correct.

2. test_prompt_cache_ttl_propagation.py: made the AST guard loop-aware —
   retry-loop sites must break, outer-loop sites must continue (the old
   assertion pinned the bug in (1)). Mutation-checked both directions.

3. test_failover_identity.py: added `model` to the SimpleNamespace agent
   fixture — _redecorate_prompt_cache_for_provider now reads agent.model
   for the per-destination TTL clamp (2 CI failures).

4. prompt_caching.py / agent_runtime_helpers.py: single source of truth
   for the alibaba-family provider set — ALIBABA_FAMILY_PROVIDERS lives
   in prompt_caching and anthropic_prompt_cache_policy imports it, so the
   cache-policy opt-in and the TTL clamp can never desync.

5. auxiliary_client.py: threaded the configured tier into
   _replan_synchronous_cache_sections via new configured_cache_ttl()
   (no live agent on that path) — the aux half of #84733's report also
   stopped regressing 1h to 5m. Guarded by
   TestAuxFallbackReplanThreadsTtl (mutation-checked).

6. Dropped the redundant `or "5m"` at the two threaded call sites —
   effective_cache_ttl already resolves None to "5m", and the `or`
   masked the cache-disabled (None) semantics.
2026-08-13 15:24:47 +05:30
webtecnica
9a5cf83541 fix(agent): propagate prompt-cache TTL to MoA/aux, clamp Qwen 1h, re-preflight on failover (#84733) 2026-08-13 15:24:47 +05:30
Adolanium
7fa084f58e fix: send Hermes Agent attribution headers to OpenCode Zen and Go
OpenCode identifies clients by request headers, the same way OpenRouter
does. Our opencode-zen and opencode-go profiles never set any, so every
request went out with the OpenAI SDK default "OpenAI/Python x.y.z"
User-Agent and OpenCode had no way to tell the traffic was Hermes Agent.

Two changes:

- Add HTTP-Referer, X-Title, and a HermesAgent User-Agent to both
  OpenCode profiles through profile.default_headers, the same path
  Fireworks uses. This covers chat_completions, codex_responses,
  auxiliary clients, model switches, and the models catalog fetch.
- Merge the same headers in build_anthropic_client for opencode.ai
  base URLs. The Anthropic Messages route (Claude on Zen, MiniMax and
  Qwen on Go) builds its client there and never sees profile headers.

Verified against the live Go relay with a real key. Both wire formats
return HTTP 200 and the requests now carry X-Title "Hermes Agent",
HTTP-Referer, and User-Agent HermesAgent/0.20.0.
2026-08-13 02:03:40 -07:00
Teknium
e4b3b91b62 fix(compression): prune pre-checkpoint history on native compaction replay
Live verification (gpt-5.6 @ api.openai.com) proved the Responses server
renders NOTHING placed before a replayed compaction checkpoint: a fact
stated in a pre-checkpoint input item is invisible to the model, while the
same item after the checkpoint recalls perfectly. Hermes was replaying the
full pre-checkpoint transcript anyway — dead upload weight, and worse, every
plaintext user ask from before the boundary silently vanished from the
model's view, surviving only inside the opaque server summary. That is the
goal-drift failure mode reported against native compaction sessions.

Codex CLI never hits this because it rebuilds history client-side after
compaction, retaining user messages verbatim under a token budget. This
change is the wire-level equivalent: when a replayed checkpoint is present,
_chat_messages_to_responses_input restructures the input as

  [newest checkpoint run] + [retained pre-checkpoint user messages,
  newest-first within a 64K-token budget] + [post-checkpoint tail]

Histories without a checkpoint are returned unchanged, so non-native
sessions see a byte-identical wire.
2026-08-13 01:51:24 -07:00
Brooklyn Nicholson
adbc77eb50 feat(desktop): setup_mcp tool — inline MCP consent card over the clarify-style blocking bridge
New desktop_ui tool: the agent proposes an MCP server (install/enable/
authorize + a one-line reason) and blocks on mcp.setup.request until the
renderer's consent card answers mcp.setup.respond with the outcome
(installed/enabled/authorized/declined/unanswered/error). Same lifecycle
as clarify: 10-min timeout, allow_expired late answers, tool lifecycle
events forced on so the card mounts even with tool progress off. Desktop
prompt hint steers the model to the tool instead of hand-editing config;
every other surface keeps the schema out and is pointed at hermes mcp
install.
2026-08-13 01:06:51 -05:00
Teknium
4ea2a0e546 Revert "Inspired by Perplexity Computer: Model Council mode for Mixture of Agents"
This reverts commit 8d9e18d40b.
2026-08-12 21:50:35 -07:00
Hermes Agent
8d9e18d40b Inspired by Perplexity Computer: Model Council mode for Mixture of Agents
Adds a 'council' synthesis style to MoA (per preset via synthesis_style,
one-shot via the new /council command on CLI + gateway). Reference models
answer independently; the aggregator chairs the deliberation and produces
a user-facing report of consensus, per-model disagreements (with the
differing assumptions behind them), unique contributions, and a
recommendation with an explicit confidence level.

Inspired by Perplexity's Model Council rollout to Perplexity Computer
(changelog 08/04/26): pick a board of 2-8 models, run them independently,
synthesize where they agree/disagree and what each uniquely surfaces.
2026-08-12 19:44:09 -07:00
Teknium
3b9d1b3cde fix(hooks): kill the whole process tree when a shell hook times out
Port from openai/codex#37527: Terminate timed-out hook process trees.

A shell hook that forked helpers (scanners, watchers, "cmd &") and then hit
its timeout left those descendants running forever — subprocess.run() only
kills the direct child. Worse, descendants holding the inherited pipe write
ends could stall run()'s post-kill communicate() drain.

- agent/shell_hooks.py _spawn(): spawn hooks in their own process group on
  POSIX (process_group=0, Python >=3.11); on timeout/error, reap the whole
  tree via the shared kill_process_tree() helper, then drain bounded (1s).
  Hooks that complete in time keep their descendants, so intentionally
  detached helpers survive successful runs (mirrors codex semantics).
- hermes_cli/_subprocess_compat.py: rename _kill_git_process_tree ->
  kill_process_tree (it was never git-specific; taskkill /T /F on Windows,
  ownership-gated os.killpg on POSIX). Backward-compat alias retained.
- tests/agent/test_shell_hooks_tree_kill.py: real-subprocess regression
  tests (descendant killed on timeout, preserved on success, own-group
  spawn, fast-path contract, fail-open). Sabotage-verified: reverting the
  process_group spawn fails exactly the two new behavior tests.

Gap proven live on main first: a forking hook timed out at 2s and its
descendant survived; same probe against this branch shows it reaped.
2026-08-12 19:44:04 -07:00
Bartok9
8be9c76f8c fix(plugins): hook delivery parity + symmetric force-reload (#64178)
Salvaged from PR #64188 (@Bartok9), re-reviewed against the #64229
ownership ledger (landed in #84923).

Delivery parity (survived):
- Module-level invoke_hook/invoke_middleware/has_hook/has_middleware
  lazily run plugin discovery via _delivery_manager(), so surfaces that
  never import model_tools (dashboards, TUI slash workers, query mode,
  cron, gateway platform events) deliver plugin callbacks instead of
  silently dropping them (#50776, #67597, #67890, #50937).
- _delivery_manager() joins any in-flight background discovery first and
  tolerates test doubles that monkeypatch get_plugin_manager().

Symmetric force-reload (survived):
- agent/shell_hooks.py gains re_register_config_hooks(); the force branch
  of discover_and_load() calls it after a successful sweep, restoring
  config.yaml shell hooks that the ledger-driven unload wiped but cannot
  restore (they are config-owned, not plugin-owned) (#60036).
- unload(plugin=None) now sweeps pre-ledger _plugin_tool_names entries
  out of the process-global tools.registry, mirroring the platform-name
  sweep that already existed, so zombie tools cannot survive a force
  reload in long-lived pre-ledger processes (#60050).

Superseded by the ownership ledger (dropped from #64188):
- _unload_global_plugin_registrations() bulk tool/platform teardown —
  the ledger's reverse-order handle disposal with previous-entry
  restoration covers it more precisely.
- tools.registry/platform_registry displaced-entry LIFO restore stacks —
  the ledger's restore_registration() identity-checked previous-entry
  restoration made them redundant.
- Discovery serialization lock + double-checked singleton — main already
  has _discovery_lock on every discover/unload path and a keyed,
  lock-guarded per-home manager cache (#24714 concern is covered).

Fixes tracked under #64178 (#50776, #60036, #60050, #24714, #67798,
#50937, #67597, #67890, #31480 — #31480 already handled on main by
_parse_hooks_block warn+suggest).
2026-08-12 19:40:59 -07:00
Victor Kyriazakos
24be384bb8 fix(relay): bound the interpreter-shutdown fallback lane; unwedge test fakes at teardown
CI caught the file hanging AFTER '6 passed in 4.32s' until the runner's
300s SIGKILL. Two defects, same class the PR fixes:

1. The executor-refused (interpreter shutdown) fallback ran the native
   call UNBOUNDED on the calling thread — a wedged pipeline would block
   process exit forever. Now runs on a bounded daemon exit-thread with
   the same timeout/abandon semantics as the executor lane.
2. The wedge tests left daemon workers parked on Event.wait() and live
   sessions registered on the atexit shutdown hook; exit re-ran the
   wedged pops (bounded, 10s each) and the per-file runner timed out.
   Autouse teardown now releases every wedge and drains each runtime.

Canonical runner: 4.4s (was 300s file-timeout kill). Bare pytest was a
false green for this class — it exits before atexit replay cost shows.
2026-08-12 19:19:54 -07:00
Victor Kyriazakos
d607f0cafb fix(relay): bound native scope lifecycle operations so a wedged pipeline cannot block the agent
The NeMo Relay native binding's scope.pop/push are synchronous and
unbounded ('returns after the scope is closed successfully'). When the
native pipeline cannot make progress, the session coordinator's turn and
session finalization block forever inside run_conversation: delegated
children finish their turns but never return, and delegation batches die
on the stall watchdog. Proven live 2026-08-10 on the staging fleet — a
falsification probe (plugin disabled, identical config) completed the
same delegation batch that wedged with the plugin active.

Bound every scope lifecycle operation that gates turn/session completion
(session push, turn push, turn pop, logical-LLM pops, session pop,
subscriber flush) by running the native call on a shared
DaemonThreadPoolExecutor and honoring a 10s result timeout. On breach a
TimeoutError propagates into each call site's existing exception
handling — warn, retain the unclosed-prefix diagnostics, continue — so
the worst case is one lost span, never a blocked agent. timeout=None
preserves byte-identical synchronous behavior for all other callers, and
interpreter-shutdown paths fall back to the synchronous call so the
atexit flush still exports.

Observability must never block the product.
2026-08-12 19:19:54 -07:00
Teknium
b9542f8e1f fix(plugins): preserve force-path platform sweep + scoped plugin-source listing after rebase
Rebase over the capability-model merge dropped two behaviors the tests
pin: (1) unload_all must still unregister every _plugin_platform_names
entry from the global platform registry (pre-ledger state has no
handles); (2) list_plugin_sources() must see profile-scoped
registrations — scoped entries are plugin-registered by definition.
2026-08-12 19:13:32 -07:00
doncazper
85020f2238 fix(plugins): isolate ownership by profile 2026-08-12 19:13:32 -07:00
doncazper
22af80bcfd feat(plugins): add ownership ledger unload lifecycle 2026-08-12 19:13:32 -07:00
Hans
52eb8eb533 feat(plugins): add pre_transcription hook and STT prompt threading
Adds a pre_transcription transform hook (prompt/language/model mutable,
file_path read-only, last-writer-wins per the transform_* convention)
fired before any STT backend, threads prompt to faster-whisper
(initial_prompt) and OpenAI/Groq/Mistral/DeepInfra (prompt), adds an
optional stt.prompt config key on the same plumbing, and keeps the
no-hook dispatch path byte-identical. Fixes #64168.

Documents the new surface for users: a "Transcription prompt
(vocabulary hints)" subsection in the configuration guide (composition
order, per-provider support matrix, length contract, privacy warning),
a pre_transcription entry in the hooks reference, and the mirrored row
in the plugins hook table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AG6LyYMvHC2o6HbVUozmVR
2026-08-12 19:01:30 -07:00
webdevtodayjason
22002b1d3e feat(plugins): per-source pattern attribution + explicit pre-screen rebuild proof
Two review items raised on #65449 (thanks @hansai-art):

1. Explicit test that post-module-load registration REBUILDS the
   _PREFIX_SUBSTRINGS pre-screen tuple — plugin patterns flow through
   the same fast path as built-ins, never around it. This was covered
   implicitly by the masking tests; now it is asserted directly.

2. Plugin patterns are now stored keyed by registration source, giving
   the #64229 lifecycle/ownership-ledger work a clean seam to drop one
   plugin's patterns on unload. No public removal API is added —
   additive-only stands; unload remains a host-owned lifecycle concern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnMCvi2vXqfs996AjVeF2F
2026-08-12 18:55:14 -07:00
webdevtodayjason
50f12e6ad8 feat(plugins): reject ReDoS-shaped patterns at redaction registration
Nested unbounded quantifiers ((a+)+, (?:x*)*, (a{2,})+) backtrack
catastrophically, and registered patterns run against every log line
and tool output, so a pathological pattern from a buggy plugin would
stall the host process. Registration now rejects the structural
nesting shape with a logged warning, same fail-soft contract as the
other validators.

Detection is a hand-rolled scanner matching the top-level-alternation
check's idiom: escapes and character classes skipped, group stack
tracks whether each group body contains an unbounded repeat, reject
when such a group closes into an unbounded quantifier. Overlapping
alternation ambiguity ((a|aa)+) is documented as out of scope.

Also refreshes the test module docstring left stale by the demo-plugin
unbundling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnMCvi2vXqfs996AjVeF2F
2026-08-12 18:55:14 -07:00
webdevtodayjason
cfeae1497b fix(plugins): reject top-level alternation in redaction patterns, unbundle demo plugin
'ab|.*' compiled and carried the accepted 'ab' literal prefix while its
'.*' branch stayed unprefixed, escaping the no-redact-everything
guarantee (_extract_literal_prefix stops at '|'). Registration now
rejects top-level alternation with a regression test for exactly that
shape; grouped alternation after the prefix, escaped pipes, and
character-class pipes remain accepted.

The bundled nvapi-redaction reference plugin is removed per repo policy
(vendor integrations ship as standalone plugin repos); the end-to-end
register() coverage now uses a synthetic plugin written at test time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnMCvi2vXqfs996AjVeF2F
2026-08-12 18:55:14 -07:00
webdevtodayjason
fdd45323bf feat(plugins): redaction pattern registry — vendor token formats as plugins
Every new vendor token format has required a core PR appending to
_PREFIX_PATTERNS in agent/redact.py (fw_, retaindb_, hsk-, mem0_, brv_
all landed that way; #58466/#58501 are the latest of the class). This
adds an additive-only registry so provider plugins own their format:

- agent/redact.py: register_redaction_patterns(patterns, source) —
  validates each pattern (must compile; must start with >=2 literal
  characters so the pre-screen substring gate keeps working and
  redact-everything patterns like `.*` are structurally impossible),
  dedupes against built-ins and prior registrations, then atomically
  rebuilds _PREFIX_RE and _PREFIX_SUBSTRINGS. Registered patterns get
  identical treatment to built-ins everywhere: same head/tail masking,
  same non-reusable «redacted:label…» sentinel on file_read, same
  security.redact_secrets operator opt-out. Additive-only by design —
  a plugin can extend masking, never weaken it. Includes a
  test/teardown reset helper.
- hermes_cli/plugins.py: PluginContext.register_redaction_patterns()
  delegating with per-plugin attribution; warns and returns 0 on any
  failure so a broken plugin can never break startup.
- Bundled reference plugin `nvapi-redaction` (opt-in): masks NVIDIA
  API keys (nvapi-, used by NIM / build.nvidia.com) — a real format
  missing from core, shipped as the one-liner plugin that previously
  would have been a one-line core PR.

13 new tests: baseline gap, masking + built-ins unaffected, invalid
regex / no-literal-prefix / dedupe / non-string rejection, file_read
sentinel labeling, reset semantics, PluginContext wiring incl.
exception isolation, and a no-mocks end-to-end through the demo
plugin. Existing redaction suites (tests/agent/test_redact.py,
tests/tools/test_kanban_redaction.py) pass untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWMcB7RPSYUpsXDfBgwjzM
2026-08-12 18:55:14 -07:00
zccyman
b85e5bb4ba feat(plugins): allow plugins to register custom @-prefix context references
Closes #26193

Adds ContextReferenceProvider ABC so plugins can register custom
@-prefixes (e.g. @issue:ENG-123) with autocomplete and expansion.
Plugin output flows through existing token-limit guards. Zero
breaking changes.
2026-08-12 18:41:59 -07:00
Dineth Hettiarachchi
00f4da01ec feat(plugins): add streaming output observer hooks
Salvage of PR #64317 (@deaneeth) onto current main, implementing #64161:
observer-only on_stream_start / on_stream_delta / on_stream_end /
on_interim_message plugin hooks dispatched through a host-owned bounded
queue (one worker per callback) so plugin callbacks never run inline on
the token path. Reasoning deltas are opt-in via
plugins.stream_reasoning_deltas.
2026-08-12 18:38:25 -07:00
SeoYeonKim
9acf0db889 feat(plugins): add cache-safe system prompt sections
Salvage the plugin-owned static prompt idea from PR #51589 into the constrained #64167 contract: stable IDs, deterministic placement, bounded fail-open rendering, and full-prompt resume recovery without new session columns.

Co-authored-by: Topher Ross <biz@topherross.com>
2026-08-12 16:34:58 -07:00
Magnus Hedemark
79c11aea7a fix(plugins): complete task-routed LLM integration 2026-08-12 16:25:20 -07:00
hans
1176222b7c feat(plugins): route ctx.llm.complete(task=) through registered aux slots
Wire an optional `task=<key>` kwarg through the PluginLlm facade so a
plugin can route an LLM call through an auxiliary model slot it
registered via `ctx.register_auxiliary_task`. Registration already
existed; this adds the missing consumption half. Closes #44673.
Sub-issue 08/14 of the plugin-interface expansion tracking issue #64182.

- New optional `task:` kwarg on complete/acomplete/complete_structured/
  acomplete_structured. Unset or "auto" keeps today's main-model path
  byte-for-byte (task=None reaches call_llm exactly as before), so no
  prompt-cache or default-behavior change.
- A set task resolves provider/model through `auxiliary.<task>` via the
  existing auxiliary_client path, identical to built-in aux tasks.
- Trust gate (per the round-2 design correction): a plugin may only pass
  a key it registered itself; a built-in key additionally requires
  `plugins.entries.<id>.llm.allow_task_override: true`. A foreign or
  unknown key is rejected with a PluginLlmTrustError and a logged warning
  naming the offending plugin and key -- fail loud, NOT a silent fallback
  to auto (which would mask misconfiguration and could route to the main
  model the user steered elsewhere).
- The plugin_llm audit dict and audit-log line gain a `task` field.
- register_auxiliary_task now stores the plugin's canonical id
  (`key or name`, the same id ctx.llm is bound to) as the slot owner, so
  the trust gate matches ownership even when a manifest sets a distinct
  key. For the common no-key case this equals the name (unchanged).

Tests (tests/agent/test_plugin_llm_task_routing.py, 24): _check_task
resolution incl. own/foreign/unknown/built-in-gated keys and loud
rejection; end-to-end routing sync+async+structured; production-path
forwarding into call_llm/async_call_llm (covers the task=None->task line);
and ownership resolution against the real plugin registry incl. the
name/key reconciliation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b1XyXitAxV7phGmKWigJX
2026-08-12 16:25:20 -07:00
Teknium
e0bb71cb73 fix: track secret source registration origin 2026-08-12 16:25:10 -07:00