Commit graph

15636 commits

Author SHA1 Message Date
Teknium
b6c11a35ac refactor(skills): adapt salvaged references to the MCP-tool surface, bump to 2.1.0
Follow-up on kshitijk4poor's cherry-picked references:
- pitfalls.md rewritten from the raw-socket blender_exec() frame to the
  MCP-tool frame (dropped 'MCP server is optional, talk to the socket
  directly' — now the anti-pattern; dropped TCP-helper internals items;
  kept all bpy/addon knowledge: empty code results in 5.x, temp-file
  readback, ops-vs-data context, engine names by version, GPU setup)
- recipes.md: blender_exec -> execute_blender_code in the agent-side
  verification snippet
- SKILL.md: reference-file table added to Quick Reference, version
  2.1.0, kshitijk4poor added to authors
- docs page regenerated (scoped to this skill)
2026-07-15 12:28:13 -07:00
kshitijk4poor
5601f24449 feat(skills): enhance blender-mcp with comprehensive references and recipes
- Rewrite SKILL.md with full setup guide, MCP config, typed commands, object types table
- Add references/bpy-api.md: scene, transforms, bmesh, materials, modifiers, camera, rendering, animation
- Add references/pitfalls.md: 19 real-session pitfalls (connection, Python exec, rendering, version quirks)
- Add references/recipes.md: 5 copy-paste recipes (landscape, glass sphere, donut, turntable, render)
- Preserve original blender-mcp name and credit to alireza78a
- Tested with Blender 5.1 on macOS
2026-07-15 12:28:13 -07:00
Teknium
bd7e480236
fix(compression): give fallback candidates their own timeout budget + escalate repeat-timeout cooldowns (#65143)
Fixes #62452. Two amplifiers turned one slow auxiliary route into a
per-turn multi-minute stall:

1. Fallback candidates inherited the exact effective_timeout the primary
   was called with. When the primary's deadline was short (tuned or
   already burned), an independently healthy fallback died on the same
   clock — the reporter's 163k-token compression needed ~90s on the
   fallback and got the primary's 30s, every turn. fallback_chain
   entries may now declare their own 'timeout' (seconds); both fallback
   candidate call sites (sync + async) resolve it via
   _fallback_entry_timeout, label-scoped so only configured-chain
   candidates are affected. No entry timeout → task-level timeout,
   preserving existing behavior.

2. A session whose transcript structurally cannot be summarized within
   the deadline re-attempted every 60s, re-burning the full timeout on
   every subsequent turn. Consecutive timeout-class failures now
   escalate the cooldown 60s → 300s → 900s (capped); any successful
   summary or session reset clears the streak. Timeout classification
   takes precedence over the streaming-closed 30s rung ('timed out'
   also matches _is_connection_error) and now recognizes the SDK's
   'Request timed out.' phrasing.

Fail-safe behavior is unchanged: all messages are preserved when every
candidate fails; the cooldown only spaces out retries.
2026-07-15 12:28:09 -07:00
brooklyn!
c81afd6e50
Merge pull request #65156 from NousResearch/bb/fix-desktop-reasoning-flash
fix(desktop): stop reasoning text flashing on every stream delta
2026-07-15 15:16:25 -04:00
Teknium
5f1991bf6b
fix(gateway): import PairingStore in _start_secondary_profile_adapters (#65118)
The served-profiles block in _start_secondary_profile_adapters references
PairingStore, but the class's only import in gateway/run.py is method-local
inside __init__ — so the reference raised NameError at runtime, silently
swallowed by the enclosing try/except ('could not record served_profiles').
Result: multiplexing gateways never created per-profile pairing stores, and
authz pairing checks for secondary profiles fell through to the global
whitelist. Also masked the served_profiles runtime-status write.

One-line fix (local import alongside write_runtime_status) + regression
tests that drive the real method and assert the stores materialize,
verified red without the import and green with it.

Surfaced during the profile-routing sweep by @CocaKova's PR #61689, which
included the same fix as part of a larger feature.
2026-07-15 12:11:16 -07:00
kshitij
2b6897f982
fix(computer-use): target Linux app windows reliably (#63725)
* fix(computer-use): target Linux app windows reliably

Resolve app filters through the canonical cua-driver MCP app metadata and join running app PIDs back to windows. Preserve an exact selected window across capture_after, support direct capture by pid/window_id, and send the active window ID for coordinate pointer actions on Linux.

Co-authored-by: annguyenNous <annguyenNous@users.noreply.github.com>
Co-authored-by: grimmjoww578 <willies578@gmail.com>
Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>

* fix(computer-use): address review on PR #63725

Address three review comments from @f-trycua:

1. type_text, press_key, and hotkey now carry _active_window_id and
   fail closed when it is missing. Previously they sent only the PID,
   so CUA Driver fell back to the first window for that PID — input
   could reach the wrong window in multi-window apps.

2. Coordinate scroll x/y are now capability-gated behind
   input.scroll.coordinates. CUA Driver 0.7.1 Linux schema rejects
   x/y on scroll; omitting them when the driver doesn't advertise
   support avoids the schema rejection while still routing via
   window_id.

3. Windows are sorted by z_index descending (higher = front, per CUA
   Driver semantics) instead of ascending. Null z_index (Wayland) is
   coerced to 0 in _ingest_windows so it doesn't crash the sort and
   sorts to the back instead of being selected as the capture target.

---------

Co-authored-by: LeonSGP43 <cine.dreamer.one@gmail.com>
Co-authored-by: annguyenNous <annguyenNous@users.noreply.github.com>
Co-authored-by: grimmjoww578 <willies578@gmail.com>
Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
2026-07-16 00:34:20 +05:30
kshitijk4poor
58033baba2 fix(plugins): classify stale-call circuit breaker as failover, not retry
The cross-turn stale-call circuit breaker (_check_stale_giveup in
agent/chat_completion_helpers.py) raises a RuntimeError when the
provider has been unresponsive for N consecutive stale attempts.
This error was classified as FailoverReason.unknown (retryable=True,
should_fallback=False), causing the retry loop to burn all max_retries
against the same dead provider — each retry hitting the circuit breaker
instantly with zero network overhead — before fallback was attempted.

Add a classification rule (section 7b in classify_api_error) that
recognizes the stale-breaker RuntimeError by its signature phrases and
classifies it as FailoverReason.timeout with retryable=False,
should_fallback=True. This makes the retry loop skip retries and go
straight to fallback provider activation on the first hit.

Test: test_stale_breaker_runtime_error_triggers_fallback_not_retry
2026-07-16 00:27:39 +05:30
Jason
c346f018d4 fix(gateway): evict stale-self-heal agent cache entries pointing at dead sessions
The #54878 self-heal (SessionStore.get_or_create_session) drops a routing
key pointing at a session already ended in state.db and recovers/recreates
a fresh session_id under the same session_key. The #54947 fix (agent-cache
cache-hit guard in gateway/run.py) treats a cached agent whose snapshot
session_id differs from the current session_id, under the same
session_key, as an intentional /resume-/branch-style switch between two
live sibling conversations, and reuses it unchanged to protect the prompt
cache.

These two fixes compose incorrectly: when the #54878 self-heal just fired,
the cached agent's session_id is not a live sibling — it's the dead session
just routed away from. #54947's "different session_id -> reuse freely" rule
reuses it anyway. The stale agent runs the turn, and the post-run "session
split" sync (agent.session_id != session_id) then writes the routing key
straight back onto the dead session_id, undoing the self-heal. This repeats
on every subsequent message until an interrupt (e.g. /stop) happens to race
in before that post-run sync, silently discarding conversation context.

Reproduced live on the engineering gateway (2026-07-12, routing key
agent:main:telegram:dm:170829464:544520): 5 consecutive self-heal log lines
over ~40 minutes, each followed by the dead session_id being reused and
re-synced back, until an interrupted /stop finally let a fresh session
stick — at which point all prior context was gone.

No open upstream issue tracks this specific interaction as of 2026-07-12
(checked #54878, #54947, #59580, #59597, #61220 — all cover adjacent but
distinct edges of the self-heal / agent-cache system).

Fix: before applying #54947's reuse-on-mismatch rule, check (outside the
cache lock, via SessionStore._is_session_ended_in_db) whether the cached
snapshot's session_id is itself ended in state.db. If so, treat it as a
stale self-heal artifact and evict/rebuild fresh -- same as a genuine
cross-process write -- instead of reusing it. Re-validates the peeked
verdict against the tuple actually held under the lock so a race can't
apply a stale verdict to a different (possibly live) cache entry.

Tests: tests/gateway/test_stale_self_heal_agent_cache_eviction.py (5 new
cases: dead-session eviction, live-sibling reuse preserved [#54947 intact],
cross-process invalidation preserved [#45966 intact], same-session_id dead
edge case, lock-race re-validation). Full tests/gateway/ suite: 14 failed,
9040 passed, 11 skipped -- all 14 failures verified pre-existing on
unpatched main (confirmed via git stash + re-run), unrelated to this
change.
2026-07-16 00:11:22 +05:30
Brooklyn Nicholson
a4c9b069dc fix(desktop): stop reasoning text flashing/re-typing on every delta
Reasoning ("Thinking") rendered through MarkdownTextContent with a smooth
typewriter reveal. TextMessagePartProvider mints a fresh part object on every
text change, and useSmooth resets its reveal to empty whenever the part
identity changes — so each reasoning delta restarted the animation from the
first character (h / hell / hello wo ...). Token-streaming reasoners
(R1/Qwen/GLM/Claude thinking) fire a delta per token and flash hard; GPT-5's
coarse reasoning summary updates too rarely to notice, which is why it looked
fine.

Drop smooth on the reasoning path so it plain-appends, exactly like the
assistant answer already does. Removes the now-dead smooth plumbing too.
2026-07-15 14:35:15 -04:00
Siddharth Balyan
e0e7cfa673
fix(dashboard): add HTTP MCP authentication (#65146) 2026-07-16 00:02:09 +05:30
Frowtek
170959d80a fix(gateway): sanitize sender-name prefix in shared multi-user sessions
GatewayRunner._prepare_inbound_message_text() interpolated
source.user_name — the platform-supplied, user-settable display name —
directly into the message text of every turn in a shared multi-user
session: f"[{source.user_name}] {message_text}". Shared sessions are the
default for any threaded conversation (thread_sessions_per_user defaults
to False) and apply to any group with group_sessions_per_user=False, so
no special configuration is needed to reach this path.

An unescaped display name containing embedded newlines could therefore
masquerade as a new markdown section (a fake "## Override" heading)
inside the live conversation the model reads on every turn — the same
indirect-prompt-injection vector gateway/session.py's
build_session_context_prompt() already guards against for the identical
user_name field via _format_untrusted_prompt_value(), which was never
applied to this sibling call site.

Add neutralize_untrusted_inline_text() alongside the existing helper in
gateway/session.py: it collapses embedded newlines/control characters to
a single inert line without JSON-quoting, so inline "[Name] message"
formatting is preserved byte-for-byte for the common case (unlike
reusing _format_untrusted_prompt_value directly, which would add visible
quote marks to every sender prefix). Wire it into the sender-prefix
construction in gateway/run.py.
2026-07-15 23:48:34 +05:30
brooklyn!
91f87137b6
Merge pull request #65142 from NousResearch/bb/fix-image-tool-overflow
fix(desktop): keep generated images out of tool overflow
2026-07-15 14:13:16 -04:00
Brooklyn Nicholson
7a5a6ef99b fix(desktop): keep generated images out of tool overflow
Generalize the clarify opt-out into an UNBOUNDABLE_TOOLS set so a run
containing image_generate also stays a plain, fully-visible stack instead
of collapsing into the bounded window, where the max-height + gradient mask
clipped the image the same way it clipped clarify forms.
2026-07-15 14:08:35 -04:00
brooklyn!
da4a28ec6d
Merge pull request #65109 from NousResearch/ethie/finished-indicator
feat(desktop): green unread dot for background-finished sessions
2026-07-15 13:55:41 -04:00
ethernet
587e76fbc2 feat(desktop): green unread dot for background-finished sessions
When an agent turn finishes while the user is viewing a different
session, the sidebar now shows a steady green dot on that session —
distinct from the blue pulsing dot of a running turn and the gray dot
of an idle one. Opening the session clears the indicator.

The unread state is ephemeral renderer-side state, matching the
existing $workingSessionIds and $attentionSessionIds pattern: no
persistence, no backend involvement, wiped on gateway-mode switch.

Co-authored-by: liuhao1024 <liuhao1024@users.noreply.github.com>
Co-authored-by: dschnurbusch <dschnurbusch@users.noreply.github.com>
Co-authored-by: Flow Digital Inc. <flow-digital-ny@users.noreply.github.com>
2026-07-15 13:51:41 -04:00
brooklyn!
1f89f3102f
Merge pull request #60638 from NousResearch/bb/contrib-areas
feat(desktop): contribution-driven shell on a layout-tree model
2026-07-15 13:28:38 -04:00
Brooklyn Nicholson
5c03e27ce2 Merge remote-tracking branch 'origin/main' into bb/contrib-areas
# Conflicts:
#	apps/desktop/src/app/session/hooks/use-prompt-actions/index.ts
2026-07-15 13:22:08 -04:00
Teknium
6020b9f4fe chore: AUTHOR_MAP entry for JiaDe-Wu (PR #34742 salvage) 2026-07-15 09:59:38 -07:00
Teknium
3b15afafa2 fix(bedrock): region-scoped model picker + geo-aware recommendations (#28156)
Bug 2 of #28156: the picker offered us./global. inference profiles to
EU-region endpoints (unroutable — AWS rejects them regardless of
credentials) and _RECOMMENDED hardcoded us.anthropic.* ids, so non-US
pickers pinned profiles their endpoint can't invoke.

- bedrock_model_routable_from_region(): geo-prefixed profiles are only
  offered in their own geography (full AWS prefix set incl. apac./jp./
  ca./sa./me./af.); bare ids and global.* pass everywhere; unknown
  region shapes hide nothing.
- Recommendations match geo-agnostically on the base model id, so an EU
  picker pins eu.anthropic.claude-sonnet-4-6; in-region geo profiles
  sort above global.* for the same model (addresses the global.*-first
  ordering complaint from the issue thread).
- Dedup generalized from (us., global.) to all profile prefixes.
2026-07-15 09:59:38 -07:00
Teknium
b6f749af94 test(bedrock): monkeypatch-based bearer routing tests + SigV4 regression
Rewrites the cherry-picked test to import os locally and monkeypatch the
resolver seams instead of patching bedrock_adapter internals; adds the
inverse assertion (no bearer -> AnthropicBedrock SDK path preserved).
2026-07-15 09:59:38 -07:00
JiaDe-Wu
5e6a0d9eea fix(bedrock): streaming fallback to Converse API + image base64 decode + bearer token routing
Three fixes for the Bedrock Claude path:

1. Streaming fallback: When AnthropicBedrock SDK raises 'Unexpected event
   order' (SDK misparses Bedrock error events as message_start), auto-switch
   to native Converse API for the rest of the session instead of failing
   after 3 retries.

2. Image base64 decode (#33317): data URL payloads were passed as base64
   strings to source.bytes, but boto3 re-encodes at the wire layer. Now
   decoded to raw bytes before passing to Converse API.

3. Bearer token routing (#28156): Users with AWS_BEARER_TOKEN_BEDROCK are
   now routed through Converse API regardless of model, since the
   AnthropicBedrock SDK only supports SigV4 signing.

3 new tests. 121 bedrock_adapter tests passing.
2026-07-15 09:59:38 -07:00
Teknium
2106f63723
fix(docs): disable fuzzy matching in docs search (exact word or prefix only) (#65103)
The docs search theme (@easyops-cn/docusaurus-search-local) defaults
fuzzyMatchingDistance to 1, so every term also matched words one edit
away. Two user-visible failures on the 14.4 MB production index:

- Wrong results: 'keet' returned 'Microsoft Teams Meetings',
  'google_meet', 'Keep the Model Loaded' etc. — 'meet' and 'keep' are
  both one edit from 'keet', and the stemmer indexes 'meetings' as
  'meet'.
- Search appearing to die: fuzzy matching multiplies the generated
  lunr queries (distance matrix x maybe-typing variants x
  leave-one-out terms — up to 210 queries per keystroke on multi-word
  input), and fuzzy REQUIRED terms are the expensive scan kind. A
  typo'd 3-word query stalled the single-threaded search Web Worker
  for 25+ seconds; every later keystroke's search queued behind it,
  so the bar stopped returning results.

Setting fuzzyMatchingDistance: 0 keeps exact-word-or-prefix semantics
(keet -> keet*), which is the behavior users asked for. Validated by
running the plugin's shipped smartQueries/tokenize code against the
downloaded production search-index.json: legitimate queries (cron,
telegram, prefix 'memor') return identical results; worst-case typo
queries drop from 210 queries / 365-532ms per keystroke to 50-105 /
53-188ms; false 'keet' matches gone.
2026-07-15 09:57:46 -07:00
Teknium
fcdc10a0f3 fix(moa): reject half-filled MoA saves at the API boundary and hold desktop autosave until slots are complete
Follow-up hardening on top of #64158 (@DavidMetcalfe):

Backend (the root-cause fix):
- hermes_cli/moa_config.py: add validate_moa_payload() — strict write-time
  counterpart to the deliberately tolerant normalize_moa_config(). Flags
  half-filled slots, empty reference lists, recursive moa slots, naming the
  exact preset/slot.
- hermes_cli/web_server.py: PUT /api/model/moa validates before normalizing
  and returns 422 with the specific problems instead of silently swapping the
  user's preset for hardcoded defaults (#64156). Also declares
  fanout / reference_max_tokens / reasoning_effort on the Pydantic payload so
  client round-trips no longer erase hand-set values.

Desktop:
- Replace sanitize-then-send with hold-while-incomplete: the debounced
  autosave is deferred (not repaired) while any slot is half-filled, and
  flushes once the model pick completes the edit. Mid-edit UI state is never
  repainted by a save response (generation guard covers held edits too).
- updateMoaSlot only clears the model when the provider actually changed.
- Explicit preset ops (set default / add / delete) cancel the pending
  autosave and invalidate in-flight responses so the two writers can't race.
- Stable row keys (preset+index) so mid-edit rows don't remount; cleared
  model shows the 'Model' placeholder instead of vanishing.

Both TS clients' MoaConfigResponse types now declare the round-tripped
fields (fanout, reference_max_tokens, reasoning_effort).

Tests: 12 new backend unit tests (validate_moa_payload contract incl.
validate/normalize agreement), 3 new web_server endpoint tests (422 on
half-filled ref/aggregator, fanout round-trip), 3 new desktop vitest cases
(autosave held while half-filled, flush on completion, same-provider
reselect no-op). E2E validated against a live TestClient with isolated
HERMES_HOME: bug sequence now 422s with config untouched.

Fixes #64156
2026-07-15 09:50:08 -07:00
David Metcalfe
a61a0bc019 fix(desktop): prevent MoA autosave defaults explosion from half-filled slots
Three fixes in model-settings.tsx:

1. Filter incomplete slots before autosave: sanitizeMoaRefsForSave() strips
   reference slots with empty model before any autosave (the 600ms debounce
   was sending half-filled provider-but-no-model slots to the backend, where
   _clean_slot rejects them and _normalize_preset falls back to hardcoded
   defaults). Presets with zero valid refs keep their empty reference_models
   array rather than being silently dropped. The aggregator slot is also
   sanitized when its model is empty.

2. Add withActive() to MoA provider dropdowns: the reference and aggregator
   provider Selects filtered to authenticated-only, so unauthenticated
   current values (e.g. openai-codex) rendered blank. Mirror the existing
   pattern from the model Select.

3. Add generation counter to scheduleMoaSave(): stale save responses could
   overwrite newer state. Bump a counter on each save and skip setMoa/setError
   if a newer save was scheduled in the meantime.
2026-07-15 09:50:08 -07:00
Teknium
f8630a1456 chore: add Burgunthy to AUTHOR_MAP (PR #20096 salvage) 2026-07-15 09:50:05 -07:00
Teknium
647520f83e fix(gateway): gate profile routing on multiplex_profiles + widen batch-key routing to all adapters
Follow-ups on the salvaged #20096 profile-routing feature:

- _profile_name_for_source now returns None unless gateway.multiplex_profiles
  is on. Routing stamps source.profile, which namespaces session/batch keys,
  but the profile-scoped agent run only activates under multiplexing — without
  the gate, configured routes with multiplexing off split batch/session keys
  into agent:<profile> while the agent still ran from agent:main.
- Widen the profile-aware _text_batch_key fix from Discord to every adapter
  that builds batch keys via build_session_key (telegram, whatsapp, matrix,
  feishu, wecom, weixin) — routing is platform-generic, so the batch-key
  namespace fix must be too.
- Downgrade the no-route-matched log from INFO to DEBUG (fired on every
  unrouted inbound message).
- GatewayConfig.to_dict(): serialize profile_routes as plain dicts
  (ProfileRoute dataclasses are not JSON-safe).
- Docs: correct the 'independent of multiplexing' claim in
  docs/profile-routing.md (routing requires multiplexing), fix the
  platform-only specificity row (0, not 1), and document profile_routes in
  website/docs/user-guide/multi-profile-gateways.md.
- Tests: pin the multiplex gate (routes ignored when off, active when on,
  build_source end-to-end stays in agent:main when off).
2026-07-15 09:50:05 -07:00
Burgunthy
c29ab7b9fe test(gateway): adapter→session-key integration for Discord + Telegram
Completes the review's ask for "adapter-to-session-key integration coverage
for Discord and a non-Discord platform" on #20096.

Drives a concrete adapter's real BasePlatformAdapter.build_source with an
injected gateway_runner, asserts the matched route's profile is stamped on
the source, and that build_session_key scopes the key under agent:<profile>:
(versus the shared agent:main: namespace). Covers Discord and Telegram — the
Telegram case is the bug-#2 path that previously fell through to default.
Adds a regression anchor: without gateway_runner, profile stays None and the
key lands in agent:main (the silent fallback the fix removes for non-Discord).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-15 09:50:05 -07:00
Burgunthy
f58e4622cf fix(gateway): profile routing — conjunctive matching + universal gateway_runner
Addresses hermes-sweeper review on #20096.

Problem 1 (profile_routing.py): route matching returned True on a chat_id
hit before the guild_id constraint was consulted, so a route declaring both
guild_id and chat_id matched on chat_id alone. Restored conjunctive (AND)
semantics — every declared discriminator must hold; hierarchical parent_chat_id
matching is preserved. Added a regression test for the guild+chat case.

Problem 2 (base.py / run.py): gateway_runner was injected only when an adapter
pre-declared the attribute, and only Discord did — so build_source never called
_profile_name_for_source for Telegram/Feishu/Slack/etc., despite the
platform-generic claim. Declared gateway_runner on BasePlatformAdapter and made
the plugin-registry injection unconditional, so profile routing now reaches
every platform. Added non-Discord (Telegram) resolution coverage and an
injection-inheritance test.

Also adds docs/profile-routing.md documenting gateway.profile_routes
(matching rules, specificity, profile isolation) — requested in review.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-15 09:50:05 -07:00
Burgunthy
e8b7ce8c19 fix(session): persist profile_name and route batch key by profile
Two follow-ups observed after deploying profile routing:

1. sessions.profile_name was NULL even when the agent ran inside the
   routed profile scope. _insert_session_row never wrote it,
   get_or_create_session / reset_session never passed it through, and
   the agent-side _ensure_db_session fallback had no way to read it.
   - Declare profile_name TEXT in SCHEMA_SQL so _reconcile_columns
     auto-adds it on existing DBs.
   - _insert_session_row takes profile_name and writes it.
   - SessionStore passes source.profile (or old_entry.origin.profile
     on reset) into db_create_kwargs.
   - _ensure_db_session reads the active profile via
     get_active_profile_name() inside _profile_runtime_scope.

2. DiscordAdapter._text_batch_key called build_session_key without
   profile=, so the batch key always landed in agent:main even when
   the routed profile differed — diverging from the agent session
   key namespace (agent:crypto-trader, agent:ai-expert, ...).
   Pass event.source.profile through so both namespaces agree.

Live verification (jth-server-2, 2026-06-28): a test message in a
routed #coin thread produced agent:crypto-trader:discord🧵...
in the batch log and profile_name=crypto-trader in the sessions
row. Default-routed chat still produced agent:main / NULL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-15 09:50:05 -07:00
Burgunthy
d7993ab178 fix(config): honor gateway.multiplex_profiles nested form
load_gateway_config only forwarded the top-level multiplex_profiles
key, ignoring the nested gateway.multiplex_profiles form. The latter
is what `hermes config set gateway.multiplex_profiles true` writes,
so users who ran that command got multiplex_profiles=False silently
— no warning, no fallback, profile_routes just stopped matching.

Loader now checks the top-level key first, falls back to the nested
gateway section, and only then defaults to False. Same precedence is
applied to other nested-form keys (profile_routes already did this).

Tests cover: top-level honored, nested honored (regression test for
the silent-fallback bug), default False, top-level overrides nested.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-15 09:50:05 -07:00
Burgunthy
a1d6654264 fix(gateway): read adapter token from config for fingerprint check
_adapter_credential_fingerprint only looked at adapter.token directly,
but Discord (and similar) adapters store the bot token on their config
sub-object, not on self. Every Discord adapter in a multiplexed
gateway therefore returned None, the same-token conflict check was
silently skipped, and N adapters all polled the same bot token —
producing a per-message race where whichever adapter won the GIL
answered the user.

Adds a config-token fallback (token, then bot_token) so the check
actually fires for config-backed adapters. Direct adapter.token
still takes precedence when both exist.

Tests cover: config-backed token produces a fingerprint, distinct
tokens produce distinct fingerprints, direct token wins over config,
config without token attributes returns None.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-15 09:50:05 -07:00
Burgunthy
a55523fd6d refactor(profiles): drop dead helpers from hermes_constants
STANDARD_PROFILES, normalize_profile, validate_profile_name, and
is_standard_profile in hermes_constants were superseded by
hermes_cli.profiles.{normalize_profile_name, validate_profile_name}
but never removed. profile_routing.py is updated to import from the
canonical location; the old helpers are deleted.

Lazy import inside parse_profile_routes avoids the circular dependency
at module load time (hermes_constants -> hermes_cli -> hermes_constants).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-15 09:50:05 -07:00
Burgunthy
9166d727b8 fix(profile-routing): remove dead forum cache, warn on missing profile
Three follow-ups to the initial routing PR after code review:

1. Remove dead forum-post hierarchy cache. The `_forum_post_cache`,
   `register_forum_post()`, and `resolve_forum_channel()` were never
   wired up — no caller in the codebase. Discord's adapter already
   sets `parent_chat_id` to the immediate parent (forum channel for a
   forum post), so the existing `self.chat_id == parent_chat_id`
   branch in `matches()` handles forum posts correctly without a
   cache. The hierarchical-resolution branch in `matches()` and the
   bounded-LRU infrastructure are removed.

2. Fix docstring specificity numbers (8 → 14, 4 → 6) and rewrite the
   "Hierarchical matching" section to describe the actual one-level
   parent_chat_id behavior. Removed unused `OrderedDict` and `Set`
   imports.

3. Warn loudly when a routed profile doesn't exist on disk. Previously,
   a typo in `profile_routes` (e.g. `crypto-tradr`) silently fell back
   to the global HERMES_HOME, causing the message to read the default
   profile's memory/credentials with no signal to the operator. Now
   emits a `logger.warning` with the profile name, source identifier,
   and the fallback reason. Bare-exception path also gets `exc_info`.

Tests:
- test_profile_routing.py: +2 tests verifying forum post matching via
  direct parent_chat_id (covers the case the removed cache was meant
  for). 31 total, all pass.
- test_profile_resolution.py: NEW, 12 tests covering resolution order
  (source.profile > routing > active > default), missing-profile
  warning, exception handling, and routing consultation. All pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-15 09:50:05 -07:00
Burgunthy
5e65f6d79f feat(gateway): add profile-based routing for inbound messages
Adds gateway.profile_routes config that routes specific Discord
guilds/channels/threads (and other platforms) to different profiles.
The routing engine uses hierarchical specificity matching
(thread > channel > guild) with bounded LRU caching for forum post
resolution.

Routing result is stamped on source.profile by BasePlatformAdapter
.build_source() at inbound time. When gateway.multiplex_profiles is on,
the existing _profile_runtime_scope machinery picks up source.profile
and runs the whole turn inside the profile's HERMES_HOME — so memory,
skills, config, and secrets all resolve to that profile automatically.
No new isolation code is added; this PR only adds the routing decision
layer on top of the existing multiplexing infrastructure.

Configuration:

    gateway:
      multiplex_profiles: true
      profile_routes:
        - name: server-default
          platform: discord
          guild_id: "GUILD_ID"
          profile: server-profile
        - name: special-channel
          platform: discord
          guild_id: "GUILD_ID"
          chat_id: "CHANNEL_ID"
          profile: channel-profile

When multiplex_profiles is off, profile_routes is ignored (no behavior
change for single-profile gateways).

Tests: 29 unit tests covering specificity scoring, hierarchical
matching, path-traversal validation, and config parsing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-15 09:50:05 -07:00
Teknium
6efec39ecf
refactor(skills): rework blender-mcp skill around the catalog MCP entry (#65066)
The optional blender-mcp skill predates the blender MCP catalog entry
(#64463) and taught the agent to hand-roll raw TCP JSON to the addon's
socket on port 9876 from execute_code — bypassing the catalog's version
pinning and install-time tool curation.

Reworked to v2.0.0 as the companion skill for the catalog entry:
- prerequisites now go through 'hermes mcp install blender'
- interaction surface is the four curated MCP tools, not a raw socket
- keeps the valuable content: addon setup, bpy recipes (materials,
  keyframes, render-to-file), pitfalls (timeouts, absolute paths,
  object mode), plus new pitfalls (xvfb headless, no-sandbox warning,
  remote-host path resolution)
- explicit anti-pattern note: do not hand-roll TCP to 9876
- description shortened to <=60 chars per skill authoring standards

alireza78a's original bpy patterns and pitfalls are preserved and
credited. Docs page regenerated via generate-skill-docs.py (scoped to
this skill only; unrelated generator drift left untouched).
2026-07-15 09:49:42 -07:00
liuhao1024
6a8e7069b4 fix(agent): dedup codex incomplete interims on visible content
Two consecutive incomplete assistant interims with identical visible
content (content + reasoning) are collapsed even when opaque provider
state (encrypted reasoning item ids, message item phases) drifts per
continuation — previously that drift defeated dedup and caused message
storms (#52711). The latest opaque payload is written onto the existing
message in place, so continuation replay still uses fresh provider
state.

Salvage note: the original PR also made the 3-retry continuation cap
cumulative per turn (no reset on progress). That half is intentionally
dropped — a legitimate long turn alternating incomplete/progress would
hard-fail at 3 cumulative, changing semantics beyond the reported bug.
2026-07-15 09:49:11 -07:00
Teknium
09b6d22dfe fix(gateway): harden hidden-incomplete detection against the sentinel final_response
Follow-up to the salvaged #51657: the conversation loop returns the
retry-exhaustion sentinel as BOTH final_response and error, so the
original detector (which required final_response to be falsy) never
fired on real exhaustion turns — the sentinel text was delivered
verbatim into the channel, exactly the #51628 poisoning vector. Detect
the sentinel echo, blank it before empty-response normalization, and
never suppress a turn whose final_response is genuine model text.
Also: dedupe-guard mock fix in the test fixture (has_platform_message_id
must return False, not a truthy MagicMock) and two guard tests
(real answer never suppressed; interrupted/failed never classified).
2026-07-15 09:48:24 -07:00
LeonSGP43
08015c3a8f fix(gateway): suppress hidden-only incomplete codex turns 2026-07-15 09:48:24 -07:00
Sk
fe1ab949fd fix(agent): treat Codex incomplete content filter as refusal
Map Codex Responses status=incomplete with incomplete_details.reason=content_filter to finish_reason=content_filter so the existing refusal/fallback path runs instead of burning incomplete continuation attempts.
2026-07-15 09:47:37 -07:00
Teknium
704bbcca80 feat(compressor): keep image URLs and unknown-part markers in summarizer serialization
Follow-up on @AlexFucuson9's cherry-picked multimodal flattening:

- http(s) image parts render as '[image: <url>]' so the summary keeps a
  referenceable handle after compaction (base64 data: URLs still
  collapse to '[image]' — no reusable reference, and leaking them is
  the bug being fixed)
- unknown part types (document, future shapes) render as '[<type>]'
  instead of being silently dropped
- test docstrings corrected: the original crash premise is stale
  (redact_sensitive_text grew str() coercion in #52147); the live bug
  is repr-noise + base64 leakage into the summarizer input
2026-07-15 09:47:04 -07:00
AlexFucuson9
868a2f7d17 fix: handle list content in _serialize_for_summary for multimodal messages
msg.get('content') can return a list of parts for multimodal messages
(containing text, images, etc.). The old code passed this list directly
to redact_sensitive_text(text: str), which raised AttributeError on
list.replace(), causing context compression to fail entirely for any
session with attached images.

Fix: detect list content and extract text parts before redacting.
Image parts are replaced with '[image]' placeholder.
2026-07-15 09:47:04 -07:00
ethernet
1600008ab0 fix(desktop): show +/- summary on collapsed review folders
ReviewDirRow never rendered a DiffCount, so collapsed folders gave no
indication of the additions/deletions inside them. The tree builder
already aggregates added/removed onto directory nodes (verified by
tree-data tests) — this just renders them, matching the file rows.
2026-07-15 11:57:33 -04:00
ethernet
1d48863b85 fix(desktop): inline git identity in worktree tracking test for CI
CI runners have no global git identity. The remote repo seed commit
needs inline -c user.email/user.name flags, matching the pattern
already used by ensureGitRepo.
2026-07-15 11:46:29 -04:00
ethernet
678b86df1c fix(desktop): worktree from origin/main should not set up upstream tracking
When "new worktree" branches off a remote-tracking ref like origin/main,
`git worktree add -b <branch> <dir> origin/main` auto-sets upstream
tracking (branch → origin/main), producing `branch:origin/main` in branch
listings. The user wants a standalone local branch — like `git checkout
origin/main && git checkout -b branch` — not one silently wired to the
remote. Add `--no-track` when the base is an `origin/` ref. Local branch
bases are unaffected (they never triggered tracking).
2026-07-15 11:46:29 -04:00
Teknium
07be37d996 fix(auxiliary_client): warn once + regression tests for bootstrap version skew
Follow-up on the salvaged fallback: silent degradation is how #64333 went
unnoticed (jobs dead on arrival, only errors.log knew). Warn once with a
resync hint, and cover both the skewed and healthy paths with tests.
2026-07-15 07:47:18 -07:00
liuhao1024
f3ec79964e fix(auxiliary_client): add backward compatibility for build_keepalive_http_client import (#64333) 2026-07-15 07:47:18 -07:00
HexLab98
2388e0687b test(input): preservation regressions and prompt.submit boundary (#62557)
Add cases for mid-string markers, trailing punctuation, and insufficient
tail repeats; verify prompt.submit passes sanitized text to run_conversation.
2026-07-15 07:39:42 -07:00
HexLab98
1011cd24e2 fix(input): strip bracketed-paste leaks before prompt persistence (#62557)
Extract shared hermes_cli/input_sanitize.py (bracketed-paste wrapper stripping
and terminal ~[[e artifact suffix collapse) and wire it into prompt.submit
and the Desktop composer so corrupted user text is cleaned before
messages.content is persisted.
2026-07-15 07:39:42 -07:00
Teknium
7c954969b7
fix(auxiliary): route direct-create aux callers through call_llm (#65029)
* fix(auxiliary): route direct-create aux callers through call_llm (#35566)

Five callers (kanban_decompose, kanban_specify, profile_describer, and
goals.py's judge + draft-contract) built raw clients via
get_text_auxiliary_client() and passed extra_body=get_auxiliary_extra_body()
— which only returns Nous portal tags and ignores
auxiliary.<task>.extra_body from config.yaml entirely. That was the
remaining half of #35566 after the call_llm path was fixed.

Routing them through call_llm(task=...) gives each caller the full
auxiliary contract for free: task extra_body, the reasoning_effort
shorthand, transient retries, provider-profile projection, and fallback
chains. goal_judge gains a DEFAULT_CONFIG block (it had none — its
provider/model overrides silently didn't exist as documented keys).

get_auxiliary_extra_body() now has zero non-test callers; kept for
plugin back-compat.

Fixes #35566.

* test: migrate kanban dashboard + CLI specify mocks to call_llm

Two more consumers of specify_task mocked the old
get_text_auxiliary_client symbol (missed in the first sibling sweep —
they live outside tests/hermes_cli's kanban files): the dashboard
plugin's /specify endpoint tests and the /kanban slash-command E2E.
Same migration as the rest: mock call_llm at the source, no-provider
now surfaces via the LLM-error branch.
2026-07-15 07:39:17 -07:00
Teknium
306e2d2318 chore: AUTHOR_MAP entry for Epoxidex (PR #29820 salvage)
Some checks are pending
CI / Detect affected areas (push) Waiting to run
CI / Python tests (push) Blocked by required conditions
CI / Python lints (push) Blocked by required conditions
CI / JS & TS checks (push) Blocked by required conditions
CI / Docs Site (push) Blocked by required conditions
CI / Deny unrelated histories (push) Blocked by required conditions
CI / Check contributors (push) Blocked by required conditions
CI / Check uv.lock (push) Blocked by required conditions
CI / Lint Docker scripts (push) Blocked by required conditions
CI / Build&Test Docker image (push) Blocked by required conditions
CI / Supply-chain scan (push) Blocked by required conditions
CI / OSV scan (push) Waiting to run
CI / All required checks pass (push) Blocked by required conditions
CI / CI timing report (push) Blocked by required conditions
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
2026-07-15 06:38:28 -07:00