Commit Graph

11347 Commits

Author SHA1 Message Date
Erosika
756aa54b67 fix(honcho): honor writeFrequency in sync_turn by routing through manager.save()
sync_turn called manager._flush_session() directly, which flushes
synchronously every turn no matter what writeFrequency says — the
"async", "session", and every-N-turns modes were dead configuration
on the main turn path. Route through save(), the dispatcher that
actually implements those modes.

Same bug class reported in #19650 (starship-s) and #72708 (Diaspar4u);
this takes the minimal one-line routing fix without their broader
lifecycle refactors.

Co-authored-by: starship-s <45587122+starship-s@users.noreply.github.com>
2026-08-13 23:43:15 +05:30
Erosika
9cfff1546d fix(honcho): join the session manager's async-writer thread on provider shutdown
Provider shutdown() only called manager.flush_all(), which drains the
queue but never joins the async-writer thread — manager.shutdown()
exists and nothing called it. The writer thread could still be blocked
in httpx I/O at interpreter exit (the #37632 crash class). Now
shutdown() calls manager.shutdown() (flush + join) when persistence is
enabled, and a new manager.stop_async_writer() (join only, no flush)
when saveMessages is false, so containment and clean teardown compose.
2026-08-13 23:43:15 +05:30
赵桂雄
d610b238c6 fix(honcho): extend saveMessages=false guard to shutdown() flush
Salvages #67559 — original gated sync_turn/on_memory_write/on_session_end but missed shutdown(), whose flush_all() still persisted on exit. hermes-sweeper review (salvageability=high) flagged this as the one gap.

Guard sits after the worker-thread joins, not at the top: cleanup is independent of persistence, and a top-of-method return would leak _prefetch_thread/_sync_thread. Adds TestShutdown and clarifies the saveMessages=false README row.

Credit @Matroskin86 (original PR author).
2026-08-13 23:43:15 +05:30
eapwrk
2042b3122b honcho: honor saveMessages=false across all automatic write paths
The saveMessages knob has been parsed by HonchoClientConfig since its
introduction but was never consumed: sync_turn, on_memory_write and
on_session_end persisted to Honcho regardless. With saveMessages=false the
provider now never writes automatically (raw turns, memory-write conclusion
mirroring, session-end flush) while read/tools paths stay fully functional.
Guard uses getattr with a True default so legacy/injected configs keep the
old behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018giroL5zeMPnPxERxAxXHY
2026-08-13 23:43:15 +05:30
Dillon Townsel
ffdc0b0be6 fix(honcho): enforce saveMessages write containment + reject gateway-internal turns 2026-08-13 23:43:15 +05:30
Erosika
606481586a fix(honcho): honor explicit top-level apiKey on local base_urls; warn on keyless profile host blocks
Two silent-auth-failure paths from #36098 (also #66125):

- the local-URL guard only escaped the 'local' placeholder when the
  HOST BLOCK had apiKey. A top-level apiKey in honcho.json — explicit
  user intent, and what 'hermes honcho setup' writes for single-host
  configs — was dropped on the floor, so AUTH_USE_AUTH self-hosts
  401'd on every request. Now any explicit key in honcho.json (host
  block or top level) is honored; only env-sourced keys are still
  treated as likely-cloud and skipped for local URLs.

- named-profile host blocks do not inherit the default host's apiKey
  (credential isolation is by design), but the failure was silent:
  the profile ran unauthenticated and every tool said 'no context'.
  Affirm isolation and warn loudly at config-resolution time instead,
  the outcome #66125 proposed if inheritance is rejected.
2026-08-13 23:43:15 +05:30
spfcraze
32238f9942 fix(honcho): resolve peers host keys via profile_host_key (underscore form) (#76414)
_all_profile_host_configs() built per-profile host keys inline as
f"{HOST}.{profile}" ("hermes.work") while profile_host_key() — used by
honcho status/enable/sync and the runtime memory plugin — produces the
underscore form ("hermes_work"). The lookup always missed, so
'hermes honcho peers' showed "(not set)" / leaked the raw malformed key
into the AI-peer column for every non-default profile. Profile names
needing sanitization (dots/spaces) were doubly broken.

Verified live: with hosts["hermes_work"] populated, cmd_peers showed
'work ... hermes.work' before the fix and 'work ... hermes' after.

Tests: host keys match the writer form, sanitized profile names resolve,
peers output shows populated identities with no key leak, and clean
fallback for profiles without a block.
2026-08-13 23:43:15 +05:30
Bartok9
41d77caf11 fix(honcho): drop non-printable base_url values before client init
Salvage of #2757 by @teyrebaz33 — rebased onto current Honcho plugin layout.

Stray control characters (e.g. terminal escapes pasted into HONCHO_BASE_URL
or config baseUrl) are dropped with a warning so SDK construction cannot
crash startup on Invalid non-printable ASCII character errors.
2026-08-13 23:43:15 +05:30
Erosika
968f5dbe00 test(honcho): pin the composed baseUrl precedence chain and the dot-form 401 regression
Adds the regression test #37671 shipped without (dot-form legacy host
block must keep its explicit apiKey on local base_urls instead of
silently degrading to the 'local' placeholder and 401ing every write),
its inverse (no host key -> placeholder), and an invariant test pinning
the full resolution order the three adopted fixes compose into:
host block > endpoint.baseUrl > flat root > HONCHO_BASE_URL > HONCHO_URL.
2026-08-13 23:43:15 +05:30
LeonSGP43
a97d6747f3 fix(honcho): honor host-specific baseUrl 2026-08-13 23:43:15 +05:30
Rob Sherman
ad588542ea fix(memory): read endpoint.baseUrl from Honcho config; accept HONCHO_URL
HonchoClientConfig.from_global_config() only consulted top-level
baseUrl / base_url / HONCHO_BASE_URL in ~/.honcho/config.json. The
Honcho SDK's native config format — and what Claude Desktop writes —
nests the URL at endpoint.baseUrl. Users with that config format had
their self-hosted Honcho container silently ignored: every honcho_*
call routed to https://api.honcho.dev with a workspace_id that does not
exist there, so tools returned empty data with no error anywhere.

Resolution order in from_global_config(), highest first:
  1. endpoint.baseUrl    (SDK-native, what Claude Desktop writes)
  2. baseUrl / base_url  (root-level, existing behavior)
  3. HONCHO_BASE_URL     (existing env var)
  4. HONCHO_URL          (the SDK's own env var, honcho/client.py:234)

HONCHO_URL is also read in from_env(). from_global_config() delegates to
from_env() whenever the config file is missing or unreadable, so an env
fallback wired into only one of the two would silently do nothing for
users with no config file.

A non-dict endpoint value falls through cleanly rather than raising.
Existing users are unaffected — the new sources are consulted only when
the existing ones resolve to None.

The INFO log for the base_url-unset case now says so explicitly instead
of printing only the host. The SDK resolves that case from its own
ENVIRONMENTS map (honcho/client.py:36-39), which for environment=
production means the public cloud; a self-hosted user whose config was
not picked up otherwise sees a healthy-looking startup line.

Closes #43800.
2026-08-13 23:43:15 +05:30
Erosika
1248e4e7bc test(honcho): pin multi-profile client isolation end to end
Drives the real resolution chain against real honcho.json files under
temp HERMES_HOMEs with the same ContextVar override the multiplexer and
dashboard use. Pins:

- #69123's minimal repro: two profile scopes get distinct clients with
  their own workspaces and bearers
- the daemon-thread case: a bound config acquires its profile's client
  from a thread that cannot see the ContextVar, and
  spawn_context_thread carries the override where a plain Thread
  (control test) does not
- credential identity: account swap on the same path/host creates a new
  client and EVICTS the old slot; the OAuth fingerprint survives
  access-token rotation but changes on re-auth; timeout changes rebuild
  via the key
- provenance capture and its stability outside the profile scope

Two-profile repro shape from #69142 (NaMinhyeok); scenario set extends
the multiplex isolation tests from #81401 (angel12).

Co-authored-by: NaMinhyeok <NaMinhyeok@users.noreply.github.com>
Co-authored-by: angel12 <angel12@users.noreply.github.com>
2026-08-13 23:43:15 +05:30
Simon van Laak
21245fefac test(slack): cover native task-card progress — ID correlation, workspace scoping, fallback
Ports the test coverage from PR #29496 onto the salvaged implementation:
adapter-level serialization/workspace isolation/disconnect sealing, and
gateway-level ID-correlated concurrent duplicate tools plus the editable
text fallback when the native stream fails.
2026-08-13 11:12:19 -07:00
Arman
47ed5e4964 feat(slack): native streaming via chat.startStream/appendStream/stopStream
Slack's Agents & AI Apps feature ships a native streaming surface that
renders a live-typing message instead of the edit-based progressive
updates the adapter used until now.

The adapter now implements the existing draft-streaming interface:

- supports_draft_streaming() opts in whenever the app is connected and
  native streaming hasn't been detected as unavailable.
- send_draft() starts a stream on the first frame (chat.startStream,
  anchored to the resolved thread_ts, with recipient_team_id/user_id
  for channel streams) and appends only the delta on subsequent frames
  (chat.appendStream is append-only). The consumer's trailing cursor
  glyph is stripped before delta computation.
- Unlike Telegram drafts (ephemeral, replaced by a real sendMessage),
  a Slack stream IS the final message. send() therefore intercepts the
  turn-final delivery for a chat with an active stream whose streamed
  text is a prefix of the final content, and seals it via
  chat.stopStream with the remaining delta instead of posting a
  duplicate. Rich Block Kit (when enabled) is applied to the sealed
  message via chat_update, mirroring the finalize path in edit_message.
- Feature-gate errors from chat.startStream (not_allowed,
  missing_scope, unknown_method, ...) are cached on the adapter so
  subsequent runs skip straight to edit-based streaming with a single
  warning naming the fix (enable Agents & AI Apps for the app);
  transient errors only disable drafts for the current run via the
  consumer's existing send_draft failure handling.
- Segment breaks (new draft_id) and disconnect() seal any open stream
  so chats are never left with a dangling live-typing indicator.

No consumer or config changes: streaming.transport auto/draft now
lights up native streaming on Slack through the same interface
Telegram drafts use, and the edit-based path remains the fallback.
2026-08-13 11:12:19 -07:00
luoxiao6645
b09e1daa84 fix(agent): reject stale 32k metadata for MiniMax 2026-08-13 11:12:05 -07:00
Teknium
69b27e3c74 fix: gate entry-point provider scan on plugins.enabled and skip register(ctx) targets
Follow-ups on salvaged #81419:
- Honor the plugins.enabled allow-list / plugins.disabled deny-list (same
  opt-in contract as the general PluginManager) — installed != loaded.
- Skip callables that require arguments: general plugins share the
  hermes_agent.plugins group with register(ctx) targets; invoking them
  zero-arg would TypeError-spam every startup.
- Fix test docstring (entry points are discovered FIRST, lowest precedence)
  and docs mechanism wording; document the config gate.
- New tests: opt-in gate, deny-list, register(ctx) never invoked.
E2E-verified with a real pip-built package against a temp HERMES_HOME.
2026-08-13 11:11:53 -07:00
Beto de Paola
dbbd8935e9 feat(providers): discover pip-installed model providers via entry points
Model-provider discovery was filesystem-only (bundled dir, $HERMES_HOME,
legacy providers/*.py). The general PluginManager scans the
hermes_agent.plugins entry-point group but deliberately does NOT import
kind=model-provider manifests (providers/ owns their lifecycle), so a
pip-installed provider was recorded yet never called register_provider() —
it never appeared in the picker, contradicting the 'Distribute via pip' docs.

Add a _discover_entry_point_providers() step that scans the
hermes_agent.plugins group and imports each entry, supporting both a
module:func callable target and a bare self-registering module target.

- Runs BEFORE filesystem plugins (lowest precedence): last-writer-wins means
  bundled/$HERMES_HOME profiles always override a pip provider of the same
  name, so a third-party package cannot hijack a first-party provider id.
- Per-entry failures are isolated (logged + skipped), so one broken package
  can't break discovery.
- Docs updated to describe the real mechanism; tests cover callable + module
  targets, failure isolation, and first-party precedence.
2026-08-13 11:11:53 -07:00
Teknium
005dfcbfcc fix(tools): symlink-safe exclusive creation for all spill/cache writers
Spill files (terminal overflow, hook context, web_extract full text,
subagent summaries) were written with plain open()/write_text into
predictable directories. A pre-planted symlink at any of those paths
redirected the write onto an arbitrary user-owned file, and raw
pre-redaction terminal/hook spills landed world-readable under the
default umask.

New tools/spill_safety.py helpers create files with
O_CREAT|O_EXCL|O_NOFOLLOW (a link-shaped path fails the write instead of
following it) and overwrite via lstat-checked unlink + exclusive
re-create, so even the redaction rewrite cannot be diverted. Private
tier (0o700 dir / 0o600 file) covers raw terminal and hook spills;
cache/web and cache/delegation keep umask perms because those dirs are
bind-mounted into remote backends that must read them.

Pattern borrowed from DeepSeek Harness dsh-spill-local (MIT):
private root + exclusive owner-only opens for spill artifacts.
2026-08-13 11:09:51 -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
Teknium
d3a8be4630 test: regression coverage for the non-positive context-cache guard
Follow-up to the salvaged #25812 — the original PR shipped without tests.
2026-08-13 11:05:49 -07:00
andyst-dev
e49a7fe568 fix(google-chat): post cron deliveries as new top-level threads, not replies
_resolve_thread_id() falls back to _last_inbound_thread[chat_id] when no
explicit thread is present. That fallback exists for interactive DMs, where
Google Chat spawns a fresh thread per top-level user message and the adapter
drops thread_id to keep the session key stable. It also fired for cron
deliveries, which carry job_id in their metadata but no thread: the output
landed as a reply inside the last inbound thread instead of starting a new
top-level message.

Bypass the _last_inbound_thread fallback when metadata has a job_id (i.e. the
message is an automated cron delivery), so cron output posts at top level
unless an explicit thread is requested.
2026-08-13 11:05:40 -07:00
Teknium
c495be19aa fix(kanban): inherit ALL routing columns in notify-sub inheritance
_inherit_notify_subs (link_tasks / triage-decompose / create-parents path)
copied only platform/chat/thread/user/profile, dropping chat_type,
user_id_alt, delivery_mode, and delivery_metadata. A DM-originated child
completion then fell back to chat_type='group' and woke a fresh
group-scoped session instead of the originating DM; Telegram DM-topic subs
lost their persisted reply-fallback metadata (issue #73030).

Consolidates the duplicated inline inheritance block in create_task onto
the single-owner helper — one inheritance path, every column, ONE owner.
Sabotage-verified regression tests for both the link_tasks and
create-with-parents paths.
2026-08-13 10:58:30 -07:00
kshitij
8855766716 fix(gateway): expire orphaned drain markers past a max-age so a leaked marker can't wedge the gateway (#85433)
The NS-570 epoch stamp clears a drain marker that survives a machine
restart — but it assumes every drain-gated action ends in a restart. When
a maintenance action completes WITHOUT recreating the container and the
writer never cancels the drain, the orphaned marker still carries the
current epoch, so the 1s drain watcher honours it forever and the gateway
bounces every inbound message with the 'draining for a maintenance
action' text (observed in the field: a Hermes Cloud instance refused all
Telegram turns for ~3 days).

The marker already records requested_at; now the readers check it. A
marker older than DRAIN_REQUEST_MAX_AGE_SECONDS (1h) reads as stale in
drain_requested() and drain_notification_suppressed(), with a loud
warning log. Leniency mirrors the epoch check: a missing or unparseable
timestamp still reads as drain-active (fail-safe toward quiescing), and
a legitimately long drain keeps a sanctioned keep-alive — re-calling
write_drain_request() refreshes requested_at.

Fixes #85433
2026-08-13 23:23:48 +05:30
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
Teknium
0818086c50 fix(kanban): backfill legacy gateway notify subs to notify+wake on first migration
Before delivery_mode existed the notifier woke unconditionally when the task
carried a session_id — pre-existing gateway subscriptions had de facto active
wake. The column's 'notify' default alone would silently disable that on
upgrade. Backfill gateway rows to notify+wake on first-add only (tui stays
notify); explicit user downgrades are never overwritten by re-migration.
Sabotage-verified regression tests included.
2026-08-13 10:47:40 -07:00
verybigdog
6e81ce273c feat(kanban): explicit notify/wake delivery modes with faithful wake session routing
Salvage of #37865 by @verybigdog. Adds delivery_mode (notify / notify+wake / wake)
on kanban notify subscriptions, persists chat_type + user_id_alt so a woken turn
reconstructs the creator's real session key, inherits the return path to child
tasks, and keeps wake out of the model-exposed send_message schema.

Original commits were authored under a local placeholder identity
(hermes-agent@users.noreply.local); re-attributed to the contributor's
public email.
2026-08-13 10:47:40 -07:00
Victor Kyriazakos
9c5d08c0d5 fix(gateway): /sethome must not persist Slack's synthetic per-message session thread as the home target
Third lane of the same contract (found in live staging validation):
/sethome run as a top-level relay-fronted Slack DM message captured the
adapter's session-keying thread stamp (the /sethome message's own id)
into the persisted HomeChannel.thread_id and its legacy env mirror.
Every bare-platform delivery (deliver="slack") then resolved home chat +
home thread and landed inside the ephemeral thread around the old
/sethome message. Extracted _home_thread_from_source with the same
synthetic-stamp recognition as cron origin capture; a /sethome run
inside a genuine thread keeps that thread as the home target. Users
repair an already-poisoned home target by rerunning /sethome.
2026-08-13 10:46:13 -07:00
Victor Kyriazakos
58ff0fd302 fix(cron): relay-fronted Slack delivery — synthetic creation-thread capture + preflight fronted-platform blindness
Bug 1: relay-fronted Slack in thread-per-message mode stamps each top-level
message's own id as source.thread_id (session KEYING, native thread_ts
parity). Cron origin capture persisted that stamp as durable routing, so
every delivery landed inside the ephemeral thread spawned around the
creation message instead of the top-level conversation. Fix at the source:
_origin_from_env drops a Slack thread id equal to the creation message's
own id (genuine in-thread creations keep theirs). Fire-time repair for
already-persisted jobs: deliver=origin and the explicit-target Slack
re-attach treat an origin thread as stale when the origin chat is the
configured Slack home chat — top-level (or the home target's configured
thread) wins; non-home working threads are preserved.

Bug 2: _preflight_check_delivery and cron_delivery_targets validated
deliver prefixes against get_connected_platforms(), which only sees
natively configured platforms — a relay-only deployment ({relay}) rejected
'slack:CHAT' with 'no gateway credentials configured' although fire-time
routing (resolve_delivery_transport + RelayAdapter.fronts_platform)
delivers it. New gateway.relay.relay_fronted_platforms() (env-derived from
GATEWAY_RELAY_PLATFORMS — the same source that seeds the live adapter's
identity set, so validation and routing cannot disagree) is unioned into
the connected set when the relay is connected. Native topologies keep the
strict credential check unchanged.
2026-08-13 10:46:13 -07:00
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
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
Nikita Barkov
24ba866275 test(slack): cover handoff-thread ts and standalone media send response reads
Review on #74658 flagged that the response-shape suite exercised identity,
ephemeral and upload paths but left two changed call sites untested:

- create_handoff_thread's seed-message ts (adapter.py:2262), which anchors
  every subsequent handoff send onto the thread;
- the standalone media branch's chat_postMessage reads (adapter.py:8721 text
  post, :8749 caption fallback), where an SDK-shaped reply used to drop the
  ts and report a caption-only delivery as 'nothing deliverable'.

Both new cases run against the hand-rolled stand-in and the real
AsyncSlackResponse. Verified they fail against the pre-fix adapter.

Co-authored-by: Junie <junie@jetbrains.com>
2026-08-13 10:32:07 -07:00
Nikita Barkov
9cf2cbd382 fix(slack): read real SDK responses instead of gating on isinstance dict
Slack Web API calls return `SlackResponse`/`AsyncSlackResponse`, which are
mapping-like but not `dict` subclasses, so every `isinstance(resp, dict)`
gate took its "unexpected shape" branch at runtime: user and channel names
collapsed to raw IDs, every user resolved as a non-bot (defeating the
allow_bots loop guard), ephemeral replies were reported as failures, and
uploads/caption fallbacks lost their message_id.

Normalize responses through a single `_slack_response_payload()` helper
(dict passes through, SDK response yields `.data`, anything else yields
`{}` so callers keep their fallbacks) and use it at every call site.

Existing Slack tests injected plain dicts, which is why the defect was
invisible; the new tests run each behavioral case against a real
`AsyncSlackResponse` as well.
2026-08-13 10:32:07 -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
kshitij
996ae10ebd fix: use handle_request for voice.toggle in audio guard test
voice.toggle is now pool-routed (returns None from dispatch), so the
audio playback guard test must call handle_request directly to get
the response dict.
2026-08-13 22:44:23 +05:30
kshitij
3b3bda7b00 fix(gateway): pool-route wake.start/wake.status — same STT lazy-install chain
wake.start calls check_wake_word_requirements() → _stt_ready() →
_get_provider() → _try_lazy_install_stt() → ensure("stt.faster_whisper")
(same synchronous subprocess install chain as voice.toggle), and
start_listening() → _build_engine() whose constructors call
lazy_deps.ensure("wake.openwakeword" / "wake.sherpa" / …).
wake.status calls check_wake_word_requirements() too and is polled
by the desktop on every gateway-ready. Same bug class as #21123 /
#50005 — sibling to the voice RPC fix in the prior commit.

Update existing wake.start test call sites from server.dispatch() to
_dispatch_sync() since dispatch() now returns None for pool-routed
methods. Extend the pool-routing regression test to cover wake RPCs.
2026-08-13 22:44:23 +05:30
hustwkr
6a9d2dc2f3 fix(gateway): pool-route voice RPCs so STT lazy install can't block WS sends
voice.toggle (status) triggers check_voice_requirements() -> STT provider
auto-detect -> a synchronous faster-whisper lazy install (uv/pip subprocess
with a 300s timeout). Inline on the WS reader thread it stalls handle_ws
before it reads the next frame, so prompt.submit / session.list queued
behind a voice.toggle sit unread and the desktop 'send message' appears
dead for minutes while the install churns (reproduced: voice.toggle ->
session.list 40s+ timeout).

Route voice.toggle/voice.record/voice.tts to the RPC pool (same bug class
as #21123 / #50005) so a slow lazy install can't block message handling.
Adapt the voice handler tests to drive the handler inline via a small
_dispatch_sync helper (preserving transport-binding semantics) since
dispatch() now returns None for pool-routed methods, and add a regression
test asserting the voice RPCs stay pool-routed.
2026-08-13 22:44:23 +05:30
Teknium
2ffed55c32 feat: server-side ui_meta on profiles.list/configure (#85440)
* feat: server-side ui_meta on profiles.list/configure

Roster UIs built on profiles.* have per-profile presentation state
(avatar, accent color, display title, pet) with nowhere server-side to
live — client plugin storage paints a different roster on every
machine. profiles.configure now accepts ui_meta (merged key-wise into
profile.yaml's ui_meta block via the existing atomic_yaml_write path,
null deletes a key, 64KB cap since it rides every roster paint) and
profiles.list returns the block per row. Consumers namespace under
their own key. No new files or config; profiles without the block are
unchanged.

* test: stop primary-runtime-restore tests probing live endpoints

_make_agent left the compressor's lazy context-length resolution
unmocked; for reachable base_urls (the nous portal test) the endpoint's
32K answer for the empty test model trips agent_init's 64K floor and
fails the suite on network behavior. Pin get_model_context_length in
the fixture.
2026-08-13 10:07:39 -07:00
Victor Kyriazakos
8d4b1e4b0e fix(cron): apply create-time origin resolution to the update path too
Review caught a real gap: action='update' also accepts deliver, and the
tool description explicitly steers agents toward update-over-create — so
a cron-context agent updating a job to deliver='origin' would recreate
exactly the dangling literal-origin shape the create-path resolution
prevents (stored 'origin' on an origin-less job → fire-time home-channel
guessing or silent drop).

Wrap the update site in the same resolver. Semantics follow the create
precedent: in cron context, 'origin' means 'my run's target', resolved
concretely at mutation time; outside cron context updates are
byte-identical to before.
2026-08-13 09:42:39 -07:00
Victor Kyriazakos
a297edf3ce feat(cron): resolve origin delivery at create time for cron-context job creation
A job created from within a cron run must never store the literal
'origin' delivery target: the creating session is ephemeral, so by fire
time there is no origin to resolve and the scheduler falls back to
guessing a home channel. With agent scheduling enabled
(cron.allow_agent_scheduling), a scheduled agent creating follow-up jobs
would silently produce exactly that dangling shape.

Resolve at create time instead, in cron context only: 'origin' elements
(and an omitted deliver) are replaced with the creating run's concrete
target from the per-run HERMES_CRON_AUTO_DELIVER_* contextvars —
platform:chat_id[:thread_id], or 'local' when the creating run has no
concrete target. Explicit values ('local', 'all', platform:chat_id
targets) pass through verbatim, including inside comma lists. Chat and
CLI creates are byte-identical to before: the resolver is a no-op
outside cron-context sessions (HERMES_CRON_SESSION unset).
2026-08-13 09:42:39 -07:00
Victor Kyriazakos
6e76c2698c feat(cron): config-gated agent scheduling in cron context
Cron-spawned agents have the cronjob toolset unconditionally denied, so
scheduled agents cannot create, tune, or remove jobs even when an
operator wants exactly that (reconciler-style jobs that manage a team's
cron table, follow-up one-shots scheduled from within scheduled work).
The denial is loop-prevention policy, not a security boundary: an agent
with the terminal toolset can already shell out to the CLI, so the
workaround exists but skips every limit and accounting layer.

Add cron.allow_agent_scheduling (config.yaml, default false — byte-exact
current behavior). When enabled, only 'cronjob' leaves the cron-context
denylist; 'messaging' and 'clarify' remain denied as interactivity
constraints, and the user-level agent.disabled_toolsets layering is
unchanged, so a user denylist entry still beats the gate. The cronjob
tool description now states the real policy and the quota bounds instead
of a blanket prohibition.
2026-08-13 09:42:39 -07:00
uperLu
02df90fc0c fix(gateway): propagate compression exhaustion result 2026-08-13 22:07:32 +05:30
Teknium
09993ea41a chore: trim hook test suite to core coverage and condense hooks docs
Per review: keep fall-through, claim, first-valid-wins, skipped-result
warning, malformed-result isolation, sanitization, and the end-to-end
synthetic-plugin test; drop the auxiliary variants. Compress the hooks.md
section to prose with a minimal return example.
2026-08-13 09:36:02 -07:00