Commit graph

1449 commits

Author SHA1 Message Date
seamusmore
d0c6353999 fix(feishu): voice-note duration on upload, turn-scoped TTS dedup, audio path dedup
Trimmed cherry-pick of PR #40592 (duration + dedup hunks only; the
voice-classification hunk duplicates #29235 and the send_voice Opus
rewrite is out of scope for this inbound-focused PR):

- adapter.py: ffprobe duration (off-loop) attached to Feishu voice
  uploads via _build_file_upload_body(duration=...) and the audio
  message payload (#16524, #8300)
- gateway/run.py: TTS dedup narrowed to the current turn;
  _enrich_message_with_transcription dedups repeated audio paths

Refs #40592 #16524 #8300
2026-07-28 14:06:56 -07:00
Teknium
76b0ea5118 fix(state): rebuild legacy gateway_routing PK; guard session_store in dispatch hook
Two log-spam bugs found in live gateway logs:

1. gateway_routing UNIQUE-constraint spam (261 warnings in one errors.log):
   early builds of the #59203 routing-index migration created
   gateway_routing with 'session_key TEXT PRIMARY KEY' and no scope
   column. _reconcile_columns() ADDs the missing scope column but SQLite
   cannot ALTER a primary key, so the shipped composite
   PRIMARY KEY (scope, session_key) never lands on those databases. Both
   write paths then fail on every save:
   - save_gateway_routing_entry: 'ON CONFLICT clause does not match any
     PRIMARY KEY or UNIQUE constraint'
   - replace_gateway_routing_entries: 'UNIQUE constraint failed:
     gateway_routing.session_key' whenever the same session_key exists
     under another scope (e.g. test-suite scopes leaked into a live DB).
   New _heal_gateway_routing_pk() rebuilds the table once with the
   composite key, preserving rows (newest wins on collisions, NULL scope
   coalesced to ''). Same one-time-heal pattern as the #51646 active-
   column repair. Verified E2E against a copy of a real affected state.db.

2. pre_gateway_dispatch warned ''GatewayRunner' object has no attribute
   'session_store'' and silently dropped the hook for every message on
   partially-initialized runners (bare object.__new__ runners in tests,
   and any future init-order change). Pass
   getattr(self, 'session_store', None) so the hook always fires
   (pitfall #17 pattern).

Both regression tests fail without their fixes (sabotage-verified).
2026-07-28 11:58:54 -07:00
Teknium
ea80b557ae fix: route busy-steer voice through the shared out-of-band STT choke point
Follow-up for salvaged #65023/#53020: _prepare_busy_steer_text now calls
_transcribe_and_echo_pending_voice (the same helper the interrupt monitor
and pending-drain paths use) instead of a private transcription+echo copy,
so out-of-band voice pays one STT call per platform message and the echo
respects the count-based ledger from #67281. can_steer now accepts events
whose attachments are all STT-eligible voice media, completing the steer
half of #58780. Adds extract_media gating tests for #44826 and the
contributor mapping for chefboyrdave21.
2026-07-28 11:57:11 -07:00
Frowtek
753d0d77e3 fix(gateway): keep the STT echo ledger across pending-media merges
_invalidate_pending_stt_cache() clears the gateway-side transcription
cache when merge_pending_message_event() folds a follow-up message into a
still-pending event, so the next transcription picks up the merged text
and attachments.  It also cleared _gateway_pending_stt_echo_sent, but that
flag is not derived state — it records that the transcript was already
delivered to the user.

Dropping it makes the re-run transcription echo the earlier notes a second
time.  Both merge branches are affected, including the text-only follow-up
case where no new audio arrived at all: there the cache is invalidated,
the same voice note is transcribed again (a second paid STT call) and the
same line is echoed again.

Sequence:

  1. voice note arrives, interrupt monitor transcribes it and echoes
     '🎙️ "hello"'
  2. user sends a follow-up while the turn is still pending, so it merges
  3. drain path re-transcribes and echoes '🎙️ "hello"' a second time

Keep the ledger out of the invalidation set and track it as a count of
already-echoed transcripts instead of a single boolean.  A count is what
the merge case actually needs: re-running transcription over the extended
media list returns the earlier transcripts as a prefix of the new one, so
echoing only the unsent tail suppresses the repeat while still surfacing a
newly merged voice note.  A count rather than a set of seen values, so two
separate notes that transcribe identically stay two distinct deliveries —
covered by test_pending_stt_merge_echoes_two_identical_transcripts.

The guard stays within the 12-line window that
test_all_gateway_transcript_echo_sends_are_gated enforces over run.py.
2026-07-28 11:57:11 -07:00
canorionen
0fd0161dfd fix(gateway): steer busy voice follow-ups after STT 2026-07-28 11:57:11 -07:00
izumi0uu
aa40f16d3e fix(gateway): transcribe clarify voice replies 2026-07-28 11:57:11 -07:00
Teknium
f76b2b47aa fix(gateway): pass channel_prompt into voice-channel STT events; guard empty transcripts
Two hand-written fixes in the voice input path:

- _handle_voice_channel_input now resolves the bound text channel's
  channel_prompt via the adapter's _resolve_channel_prompt so voice input
  gets the same per-channel context as typed messages (fixes #50149).
- _enrich_message_with_transcription now guards success=True results whose
  transcript is empty/whitespace-only (silence, cut-off, inaudible audio):
  instead of emitting empty quotes the agent gets a clear sentinel note.
  Reimplemented against the current plain-quoted note wording; original
  concept and tests by @deacon-botdoctor in PR #41603 (fixes #41603).
2026-07-28 11:56:37 -07:00
isheng-eqi
31301c1af7 fix(discord): wire voice input callback at adapter connect time
- Wire adapter._voice_input_callback at connect and reconnect so voice
  transcription is forwarded without requiring /voice join (#60623).
- Add optional text_channel_id and source params to DiscordAdapter
  .join_voice_channel() so automatic/programmatic voice joins can
  establish the text-channel binding needed by _handle_voice_channel_input.
- Add TestVoiceInputCallbackWiring: asserts callback wiring on startup
  and reconnect for Discord adapters with voice attributes.
2026-07-28 11:56:37 -07:00
isheng
1b0836c7c7 fix(discord): wire voice input callback at adapter connect time
_voice_input_callback was only set in _handle_voice_channel_join, not at adapter connect or reconnect. Voice transcription was logged but never forwarded as an inbound message without explicit /voice join.

Wire the callback at both connect and reconnect paths.

Fixes #60623
2026-07-28 11:56:37 -07:00
Gilad Bauman
1753369f7c fix(gateway): platform-aware auto-TTS output path for native voice bubbles
Salvaged from PR #62040 (@giladbau), simplified per post-#73072 main: the
central _repair_ogg_container transcode makes an explicit .ogg output path
sufficient — no target_platform plumbing through the TTS tool needed.

Root cause (class-level): both gateway auto-TTS delivery call sites relied
on the TTS tool reading HERMES_SESSION_PLATFORM to pick Ogg/Opus vs MP3,
but that contextvar is cleared by _clear_session_env before the base
adapter's post-handler auto-TTS block runs, so want_opus was always False
on that path → MP3 → Telegram sent an audio attachment instead of a native
voice bubble (#57049, #36685). The runner's _send_voice_reply had the
sibling bug: it hardcoded .ogg for Telegram only, leaving Matrix and
Feishu runner voice replies as MP3 (#14841, #45557).

Fix: new build_auto_tts_output_path(platform) in gateway/platforms/base.py
hands an explicit .ogg temp path when the platform is in the TTS tool's
OPUS_VOICE_PLATFORMS set (single source of truth — telegram/matrix/feishu/
whatsapp/signal), .mp3 otherwise. Used by BOTH delivery call sites:
- BasePlatformAdapter auto-TTS block (also honors the tool's success flag
  and cleans up requested + returned paths)
- GatewayRunner._send_voice_reply (replaces the telegram-only ternary)

Fixes #57049
Fixes #36685
Refs #14841 #45557
2026-07-28 11:55:48 -07:00
55nx954gn6-debug
28adb86891 fix(gateway): honor global voice.auto_tts in runner voice-reply gate
Salvaged from PR #51196 (@55nx954gn6-debug). _should_send_voice_reply only
consulted the runner's _voice_mode dict (/voice on|voice_only|all), so the
global voice.auto_tts config default — which is synced into each adapter's
_auto_tts_default on gateway connect — was invisible to the runner path.
Net effect: with streaming enabled and only global auto-TTS configured
(no per-chat /voice opt-in), the streamed reply consumed the text, the
base adapter's auto-TTS got text_content=None, and no voice reply was
ever sent (#51867/#23983 remainder).

The runner now also asks the adapter's _should_auto_tts_for_chat(chat_id)
(which encodes per-chat /voice on|off overrides over the global default);
an explicit /voice off chat mode remains a hard override.

Refs #51867 #23983 #51282 #13126
2026-07-28 11:55:48 -07:00
Richard Jang
3290c18247 fix: handle missing transcription module gracefully 2026-07-28 11:53:36 -07:00
Brooklyn Nicholson
78aeeab8d1 feat(sessions): say which surfaces hold the active-session slots
The cap is shared across CLI, desktop/TUI and the messaging gateway, so the
surface that gets rejected is rarely the one holding the slots. The rejection
read "Hermes is at the active session limit (5/5). Try again when another
session finishes." while every slot was an idle desktop tab, which took
filesystem access to work out.

Name the holders in the message, and show slot usage plus each holder in
`hermes status`. Both are inert when max_concurrent_sessions is unset, which
is the default. The gateway's duplicate copy of the message now reuses the
shared helper.
2026-07-28 12:11:52 -05:00
Alex Fournier
b1a5d67e71 Merge upstream main into fix/hermes-relay-anthropic-context
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-28 08:10:58 -07:00
dsad
f6abc6a046 fix(gateway): write hygiene compressed transcript before rebinding session
Manual /compress already persists the rotated child transcript first and
only then repoints the live session_entry; a False rewrite_transcript
return keeps the entry on the original session_id so the conversation
stays reachable. Session hygiene auto-compress did the opposite: it
rebound session_id (and lease/topic) first, then called rewrite_transcript
without checking the return value. On a failed write the live entry
already pointed at an empty child SID and the turn continued — permanent
silent conversation loss. Persist first; rebind only after success.
2026-07-28 19:38:11 +05:30
JonthanaHanh
58f6678e6d fix(gateway): flush pending messages to disk before shutdown clear (#72680)
When FTS5 index corruption prevents INSERT INTO messages, the gateway
accumulates messages in _pending_messages (memory-only). On shutdown,
.clear() discards the only surviving copy — permanent user data loss.

Changes:
- Add gateway/shutdown_flush.py with flush_pending_to_file() and
  recover_pending_to_db() for two-phase data preservation.
- Patch gateway/run.py: flush runner._pending_messages before clear()
  in _stop_impl_body, and recover on startup after runner.start().
- Patch gateway/platforms/base.py: flush adapter._pending_messages
  before clear() in the adapter shutdown path.

Recovery behavior:
- Reads pending JSON files from ~/.hermes/pending_messages/
- Inserts messages into state.db directly
- Per-session isolation: one corrupt session doesn't block others
- Successful recovery deletes the flush file
- Failed recovery re-saves for next startup retry
2026-07-28 18:08:24 +05:30
Gille
ff12b62e82 fix(gateway): await hygiene prompt restore 2026-07-28 17:43:28 +05:30
Gille
76a17046e2 fix(gateway): preserve memory prompt during hygiene compression 2026-07-28 17:43:28 +05:30
Alex Fournier
14bed44c8c Reapply "feat(observability): integrate NeMo Relay runtime and shared metrics"
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-27 21:10:51 -07:00
Jeffrey Quesnelle
841a5a744a
Revert "feat(observability): integrate NeMo Relay runtime and shared metrics" 2026-07-27 22:28:08 -04:00
Alex Fournier
4fe4b0dca7 Merge origin/main into feat/hermes-relay-shared-metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-27 08:12:37 -07:00
Ben Barclay
96996a55bf
fix(relay): per-platform capability descriptors for multi-platform gateways (#70717)
One relay adapter fronts N platforms on one WS, but the capability surface
(MAX_MESSAGE_LENGTH / message_len_fn) was a scalar from whichever descriptor
resolved the handshake — and the transport's read loop OVERWROTE it on every
descriptor frame (last-writer-wins). A Discord chat on a gateway whose
applied descriptor was Telegram's inherited the 4,096-char cap and over-sent
into Discord's 2,000-char API 400 (observed live: 2,543/2,641-char replies
silently lost while inbound kept working).

- ws_transport: accumulate one descriptor per platform in
  _descriptors_by_platform (exposed via descriptor_for_platform); the FIRST
  descriptor of a connection generation stays the session default instead of
  last-writer-wins; the map resets on re-dial.
- BasePlatformAdapter: new max_message_length_for_chat /
  message_len_fn_for_chat hooks defaulting to the scalar surface (native
  single-platform adapters unchanged).
- RelayAdapter: overrides resolve the chat's platform from _platform_by_chat
  (the same map per-frame egress uses) and look up that platform's negotiated
  descriptor; falls back to the scalar for unknown chats/transports.
- stream_consumer (streaming budget, _raw_message_limit, fallback-continuation
  chunking) + run.py tool-progress limit now resolve per-chat.

Tests: tests/gateway/relay/test_relay_per_platform_caps.py (7) — verified
fail-without/pass-with (all 7 fail with the fix stashed). Relay + stream
consumer suites green (213 + 223).
2026-07-27 14:24:14 +10:00
Teknium
f9cd577915 feat(approvals): add cross-surface mode command
Co-authored-by: luxiaolu4827 <227715866+luxiaolu4827@users.noreply.github.com>
2026-07-26 21:13:03 -07:00
Jeffrey Quesnelle
9216198601
Merge branch 'main' into feat/hermes-relay-shared-metrics 2026-07-26 23:14:26 -04:00
teknium1
bd7938fa0c fix(gateway): keep _reconnect_watcher_task tracking the live task after a supervised respawn
Follow-up to the salvaged #71867 supervision fix. _spawn_supervised's own
backoff respawn created a new task without updating the external handle
self._reconnect_watcher_task, so after the reconnect watcher crashed and
self-restarted, _ensure_reconnect_watcher_running() saw the stale handle as
done() and spawned a SECOND concurrent watcher (double reconnect attempts).

Add an optional on_spawn callback to _spawn_supervised, fired with the live
task on every spawn INCLUDING internal respawns, and pass it at both reconnect-
watcher spawn sites so the tracked handle always advances. The two supervision
mechanisms (supervisor auto-restart + ensure-respawn) now compose instead of
racing. Regression test sabotage-verified.
2026-07-26 19:43:17 -07:00
ygd58
4b039e9543 fix(gateway): spawn platform reconnect watcher with task-level supervision
Fixes #71758.

A platform adapter that dies on a transient upstream failure (marked
retryable=True, e.g. photon's sidecar exiting when its upstream
gRPC/CDN returns errors) is correctly queued into _failed_platforms
for background reconnection. But the reconnect watcher task itself
was spawned via a bare asyncio.create_task -- if an exception ever
escaped its OUTER while-loop (not just the per-platform inner
try/except), the watcher died silently: no log, no restart.

_ensure_reconnect_watcher_running() already existed to respawn a dead
watcher, but it's only called from _handle_adapter_fatal_error_impl()
when a NEW platform's fatal error arrives. If the watcher dies while a
platform is already sitting in the queue and no OTHER platform ever
fails afterward, nothing ever notices the watcher is dead -- exactly
matching the reported symptom: photon queued for reconnect, the
gateway itself healthy (other platforms kept working, so nothing
re-triggered the ensure-alive check), and the platform stayed dead for
17.5h until a manual restart, well after the transient upstream outage
had recovered.

Fix: spawn the reconnect watcher via the existing _spawn_supervised()
task-level supervisor (already used for kanban_dispatcher_watcher,
handoff_watcher, etc.) instead of a bare asyncio.create_task, at both
the initial startup spawn and the manual-respawn path in
_ensure_reconnect_watcher_running(). _spawn_supervised already
provides exactly what's missing here: catches and logs any exception
escaping the task, and auto-restarts with capped exponential backoff
(healthy-run counter resets so a daemon that crashes occasionally over
days is never permanently abandoned) -- self-healing independent of
any new fatal-error event.

Also hardened a related race: the watcher's per-platform loop looked
up self._failed_platforms[platform] via direct indexing after
snapshotting the keys with list(...). A platform removed concurrently
between the snapshot and the lookup (e.g. a manual /platform resume,
or a reconnect that succeeded via a different path) would raise an
uncaught KeyError -- exactly the class of bug this fix's supervision
now catches, but avoiding the crash-and-restart cycle entirely is
better than relying on it. Changed to .get() with a skip-if-missing
guard.

6 new tests pass (initial spawn uses _spawn_supervised, manual respawn
uses _spawn_supervised, the core regression -- watcher self-heals
after an uncaught exception with no new fatal-error event -- and the
race-guard scenario); 52/52 in the full
tests/gateway/test_platform_reconnect.py file (including the 4
pre-existing _ensure_reconnect_watcher_running tests, confirming no
regression to that respawn-when-dead-or-missing behavior).
2026-07-26 19:43:17 -07:00
TheEpTic
c2ee5039ee fix(gateway): preserve media dedup after streamed replies 2026-07-26 19:31:08 -07:00
Frowtek
a228b81501 fix(sessions): preserve recently active sessions during pruning 2026-07-26 19:30:21 -07:00
Kyzcreig
769dba1758 fix(gateway): bound the startup-restore inbound gate on a slow boot-resume turn 2026-07-26 19:30:14 -07:00
teknium1
0fa5e41c86 feat(diff): cross-surface /diff with staged/all/session modes
Widen the cherry-picked /diff base (#4839 by @SHL0MS) into one
cross-surface implementation, folding in the review feedback and the
best ideas from the two sibling PRs (#22703, #53527):

- tools/working_diff.py: shared git collection layer — unstaged
  (default), staged, and all (vs HEAD) modes; untracked files folded in
  via `git diff --no-index` so new files appear as additions (Codex
  /diff parity); shlex-split arguments preserve quoted paths.
- CLI: handler moved to hermes_cli/cli_commands_mixin.py per the
  current god-file decomposition (dispatch stays in cli.py), renders
  through the rich console with a 400-line terminal-flood guard.
- Gateway: _handle_diff_command in gateway/slash_commands.py + dispatch
  in gateway/run.py; fenced ```diff output truncated to 60 lines /
  3000 chars before the platform senders apply their own per-platform
  message clamps (tool-progress-style layered truncation). Localized
  strings in all 17 locale catalogs.
- /diff session (from #53527): cumulative checkpoint-baseline diff of
  everything Hermes changed, via new CheckpointManager.session_diff();
  docstring records the retained-baseline approximation caveat from
  review. Works on both surfaces; degrades with an actionable message
  when checkpoints are off.
- Slack: /diff routed via /hermes diff (50-slash cap; keeps
  telegram-parity test green and /version native).
- Registry: cross-surface CommandDef with staged|all|session
  subcommands; docs: slash-commands reference (CLI + gateway tables +
  both-surfaces list) and hermes-agent skill reference.
- Tests: tests/tools/test_working_diff.py (real git repos),
  tests/hermes_cli/test_diff_command.py (real git + stubbed checkpoint
  manager), tests/gateway/test_diff_command.py (end-to-end handler,
  real checkpoint store), TestSessionDiff in
  tests/tools/test_checkpoint_manager.py.

Salvaged from the /diff PR cluster #4839 + #22703 + #53527.

Co-authored-by: Ninso112 <ninso112@proton.me>
Co-authored-by: Harshkamdar67 <harshkamdar67@gmail.com>
2026-07-26 18:28:20 -07:00
CharlesMcquade
4fe2fecf54 feat(gateway): add /context command for a detailed context-window view
A dedicated /context (alias /ctx) gateway slash command that gives a full
context-window view with:

- Usage gauge: visual bar + fraction + percentage + headroom
- Auto-compression threshold and how far away it is
- Compression count and how much the last one freed
- Cumulative session throughput (explicitly labelled as throughput,
  NOT context size — each call re-sends the window)
- Cascading fallback: running agent → cached agent → SessionStore metadata
  → rough transcript estimate

Not included (per current-main design):
- Cache reporting removed: commit 446b8e239 intentionally removed cache
  reporting from user-facing surfaces because providers that omit cached-token
  details produce misleading values
- Sync DB calls replaced with async_session_store (current main requires
  AsyncSessionStore with await)

Also rewords the /status tokens line from 'Cumulative API tokens (re-sent
each call)' to 'Lifetime tokens billed: ... (not your current context size;
use /context)' to reduce the recurring confusion that the cumulative figure
is the current context window.

Fixes salvation of PR #52184 (salvage commit replaces a 12K-commit-behind
fork branch with a fresh implementation against current main, incorporating
reviewer feedback from @whoislikemiha and the hermes-sweeper).
2026-07-26 18:06:21 -07:00
teknium1
10b7ab5cb6 feat(clarify): extend multi-select to gateway text fallback and TUI bridge
Cross-surface coverage #23768 missed (per review feedback):

- tools/clarify_gateway.py: _ClarifyEntry carries a multi_select flag
  (register() accepts it; signature() exposes it to adapters).
  _coerce_text_response now parses multi-select replies — comma- or
  space-separated numbers ('1,3' / '1 3'), exact labels, dedup — into a
  JSON array string that _parse_multi_select_response decodes into a
  list. Out-of-range/unknown tokens reject the reply (native button UI)
  or fall back to custom text (awaiting_text/'Other' mode).
- gateway/run.py: _clarify_callback_sync accepts multi_select and
  registers it on the pending entry.
- gateway/platforms/base.py: default numbered-list text fallback tells
  the user multiple selections are allowed and how to reply.
- tui_gateway/server.py: clarify_callback passes multi_select through
  the clarify.request payload as a hint; renderers without checkbox
  support ignore the field and remain single-select-compatible.
- tests: 13 new gateway tests (flag storage, comma/space/single-number
  parsing, label matching, out-of-range rejection, dedup, end-to-end
  resolve, single-select regressions).
2026-07-26 17:46:55 -07:00
teknium1
95b7ea5e5d feat(cli): /init — generate or update AGENTS.md from a project scan
Cross-surface slash command (CLI, gateway, TUI) following the /learn prompt-injection pattern. Port of Codex /init.
2026-07-26 17:46:29 -07:00
hereicq
3ec5fb076e fix(gateway): survive faulthandler.enable() when sys.stderr is None
faulthandler.enable() writes to sys.stderr by default, and raises
RuntimeError('sys.stderr is None') when the gateway is launched
without an attached console — e.g. via the Windows Startup VBS shim,
pythonw.exe, a detached service, or any parent that redirects stderr
to DEVNULL. Because this happens on the very first line of
GatewayRunner.start(), the whole gateway used to die at startup and
every configured platform adapter (Discord bot, Telegram, Slack, …)
would silently show offline until the user manually re-ran
'hermes gateway run --replace' from a real terminal.

Wrap the call and fall back to a log-file file descriptor
(logs/gateway_faulthandler.log) when stderr is unavailable, so
fatal-error stack dumps still land somewhere useful. If even the
fallback fails we log-and-continue rather than kill the gateway.

Repro traceback (from a real user's gateway-exit-diag.log, launched
via the Startup VBS with stdin_is_tty=false):

    File "gateway/run.py", line 7821, in start
        faulthandler.enable()
    RuntimeError: sys.stderr is None
2026-07-26 17:15:38 -07:00
teknium1
26670bce95 fix(gateway): dedupe chat_type wiring after composing #58615 with #60769
Both the wake chat-scope salvage (#72191, merged) and the DM-topic
metadata salvage added HERMES_SESSION_CHAT_TYPE plumbing; the rebase
auto-merge kept both copies. Dedupe the ContextVar declaration, _VAR_MAP
entry, set_session_vars parameter/token, and the run.py call-site kwarg,
and prefer the persisted chat_type column with delivery_metadata as the
legacy fallback in the notifier wake path.
2026-07-26 16:01:32 -07:00
embwl0x
f174a08c93 fix(gateway): let internal events bypass topic lobby 2026-07-26 16:01:32 -07:00
embwl0x
1bdec6f065 fix(kanban): preserve telegram dm topic metadata 2026-07-26 16:01:32 -07:00
lijun
a417c6e08d fix(gateway): preserve kanban notifier chat type 2026-07-26 13:42:33 -07:00
dhruvkej9
83bba799e2 fix(gateway): deduplicate repeated media tags against prior assistant/tool output
The model sometimes echoes a previous MEDIA:path tag or bare file path in a later response. Previously both streaming delivery and non-streaming delivery would re-send those files, causing duplicate documents/images on later turns.

Changes:

- _collect_history_media_paths now also scans assistant messages for MEDIA: tags, not just tool results.

- _deliver_media_from_response accepts pre-computed history_media_paths and filters extracted media/local files.

- BasePlatformAdapter gains _history_media_paths_for_session for non-streaming dedup.
2026-07-26 13:41:53 -07:00
teknium1
1b081e4891 feat: raise default tool-calling iteration limit from 90 to 500
The default max_iterations/agent.max_turns budget was set when long
agentic runs were rare; complex tasks now routinely exceed 90 tool
calls. Raise the default to 500 across every surface that hardcodes
the fallback: AIAgent constructor, DEFAULT_CONFIG, CLI resolution
chain, gateway env bridge, cron scheduler, and TUI gateway. Explicit
user config values are unaffected (deep-merge preserves them; no
_config_version bump needed).

Docs (en + zh-Hans), CLI help text, tips, and pinned tests updated
to match.
2026-07-26 12:54:45 -07:00
Alex Fournier
45580cc93a Merge origin/main into feat/hermes-relay-shared-metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-26 09:21:19 -07:00
Teknium
32fd9d65cf feat(compression): progress-aware timeouts — stop punishing slow summary models
The gateway's pre-agent session-hygiene compression killed the summary
call at a fixed 30s wall-clock deadline (compression.hygiene_timeout_seconds),
regardless of whether the summary model was hung or merely slow. A reasoning
model happily streaming a large summary was cut off mid-generation, the user
got '⚠️ Context compression timed out after 30.0s', and a 300s failure
cooldown left the session oversized — a doom loop for slow-but-healthy
auxiliary models.

Timeouts are now liveness-based instead of wall-clock-based:

- agent/auxiliary_client.py: new thread-local aux_progress_hook. When
  installed (only by context compression today), the primary call_llm
  attempt streams (stream=True) and aggregates chunks back into a complete
  response, ticking the hook per chunk. The configured timeout then acts
  per stream read (idle) instead of as a total budget. Providers that
  reject streaming fall back to the plain non-streaming call; auth/payment/
  rate-limit/transport errors propagate unchanged into the existing
  recovery chains. Codex Responses (per SSE event) and Anthropic Messages
  (per stream event, via the new create_anthropic_message on_stream_event
  callback) tick the same hook from inside their wire adapters.

- agent/conversation_compression.py: CompressionCommitFence gains
  touch_progress()/seconds_since_progress(); compress_context() installs
  fence.touch_progress as the progress hook around the compress call.

- gateway/run.py: the hygiene wait loop treats hygiene_timeout_seconds as
  an INACTIVITY budget — while the fence reports fresh progress the wait
  extends, bounded by the new compression.hygiene_total_ceiling_seconds
  (default 600s, clamped >= the idle budget) so a degenerate trickle
  stream still dies. The timeout warning now says the summary model
  produced no output, which is the only case that still triggers it.

- config/docs: hygiene_total_ceiling_seconds added to DEFAULT_CONFIG and
  configuration.md; hygiene_timeout_seconds documented as inactivity-based.

Tests: tests/agent/test_aux_progress_streaming.py (hook plumbing, stream
aggregation incl. tool-call deltas and reasoning deltas, rejection
fallback, ceiling kill, fence progress surface); two new gateway tests
prove a slow-but-streaming worker survives past the fixed timeout
(sabotage-verified: fails with the old fixed deadline) and a
forever-trickling worker is still cut off at the ceiling.
2026-07-25 12:26:28 -07:00
Ben Barclay
ebab890ae5
feat(relay): Phase 1 parity — supported_ops discovery, wire identity fields, /handoff aliasing, provision displayName (#71300)
- CapabilityDescriptor.supported_ops + supports_op() with legacy-op-set
  fallback (additive, contract doc §2 updated)
- get_chat_info gated on op discovery (skip round trip on legacy connectors)
- _event_from_wire consumes user_display_name/user_handle (§3 fields
  previously dropped); native display-name parity, session-key stable
- /handoff <fronted-platform> works on relay-fronted gateways: CLI pre-check
  reads the GATEWAY_RELAY_PLATFORMS fronted set; watcher resolves through
  resolve_delivery_transport and replies via send_for_platform
- self-provision forwards displayName (env > skin branding, stock brand
  suppressed) — the primary name source for gg#171 attribution
2026-07-25 20:36:16 +10:00
mannnrachman
9f8810a693 fix(gateway): allow Telegram readiness budget
Give Telegram a 180s default outer connect budget so cold polling can prove getUpdates readiness. Preserve the 30s default for other platforms and all explicit config/env overrides.\n\nRefs #67498
2026-07-24 19:11:04 -07:00
teknium1
86084d03ba fix: getattr-guard _stop_loop_liveness_guards in GatewayRunner.stop
Teardown-path tests build bare runners via object.__new__ without
the liveness-guard machinery; the unguarded call raised
AttributeError in 8 tests. Same guard pattern as the start path.
2026-07-24 16:03:42 -07:00
Hermes Agent
9cd7296849 refactor(gateway): gate loop-liveness watchdog via config.yaml, drop HERMES_* env knobs
Follow-up to the salvaged #69164 commits: policy forbids introducing new
HERMES_* environment variables, so the four watchdog env knobs
(HERMES_GATEWAY_LOOP_WATCHDOG / _INTERVAL / _TIMEOUT / _STRIKES) are
replaced with a single config.yaml boolean:

  gateway:
    loop_watchdog: true   # default; false disables both guards

- gateway/config.py: new GatewayConfig.loop_watchdog field (default True),
  parsed from top-level or nested gateway: form, round-trips via
  to_dict/from_dict.
- gateway/run.py: _start_loop_liveness_guards() checks config.loop_watchdog
  before arming the floor timer + watchdog (getattr-guarded for bare
  object.__new__ runners).
- gateway/shutdown_watchdog.py: start_loop_liveness_watchdog() no longer
  reads the environment; probe interval/timeout/strikes are module
  constants (30s/10s/3 — ~90s to restart, matching the systemd watchdog
  layer's posture).
- hermes_cli/config.py: documented gateway.loop_watchdog default so
  'hermes config set gateway.loop_watchdog false' validates.
- tests: env-knob tests replaced with config-gate + round-trip tests;
  the final-strike boundary test injects its probe via max_strikes
  directly instead of patching the removed env helper.
2026-07-24 16:03:42 -07:00
Josh Tsai
7af3a6fc7c fix(gateway): detect and escape silent event-loop freezes
- A self-rescheduling 5s call_later floor timer, armed before any
  adapter connects, guarantees the selector always has a finite
  timeout, so the existing async defenses (polling heartbeat, timeout
  guards) regain a chance to run after a zero-pending-timer stall.
- A resident daemon-thread liveness watchdog probes the loop via
  call_soon_threadsafe every 30s; after 3 consecutive 10s-timeout
  misses (~120s of total unresponsiveness) it dumps all thread
  tracebacks and exits with the established
  GATEWAY_SERVICE_RESTART_EXIT_CODE (75) so a supervisor restarts the
  gateway - async-level recovery cannot run on a frozen loop.
- stop() disarms both guards before any teardown await so a busy
  shutdown is never misjudged as a freeze.
  HERMES_GATEWAY_LOOP_WATCHDOG=0 disables; _INTERVAL/_TIMEOUT/_STRIKES
  tune the thresholds.

Fixes #69089

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 16:03:42 -07:00
teknium1
dee0b5bf69 fix: gate SIGUSR2 faulthandler registration behind POSIX check
signal.SIGUSR2 and faulthandler.register() don't exist on Windows;
the bare reference raised AttributeError at import time per the
windows-footgun checker. faulthandler.enable() still covers
fatal-error dumps on all platforms.
2026-07-24 16:03:10 -07:00
Shannon Sands
eb2f5b5723 fix(gateway): stay alive on mixed retryable + non-retryable startup failures
When connected_count == 0 and at least one platform failed with a
non-retryable error, the runner exited with GATEWAY_FATAL_CONFIG_EXIT_CODE
(78) even if OTHER platforms failed for merely transient reasons.

Real-world shape (NS-609, hosted instance): WhatsApp enabled but never
paired (non-retryable whatsapp_not_paired) + Telegram TimedOut during
polling startup (retryable) => exit 78 => the gateway either goes
permanently down (supervisors honoring the exit-78 contract via
RestartPreventExitStatus / the s6 finish->125 translation from #51228) or
crash-loops (anything else). Either way Telegram never gets its retry and
the dashboard drops with every exit, so a single unpaired platform plus
one network blip disconnected every channel on the instance.

Now exit 78 is reserved for the case where ALL startup failures are
non-retryable (true config error, nothing to wait for). With mixed
failures the gateway stays alive in degraded state: the reconnect watcher
recovers the retryable platforms and the misconfigured ones stay
fatal-parked and visible in runtime status.
2026-07-24 16:03:10 -07:00
kshitijk4poor
7a62f62197 fix: explicit encoding for faulthandler file open (ruff PLW1514) 2026-07-24 16:03:10 -07:00