Commit Graph

1082 Commits

Author SHA1 Message Date
Teknium 4ea2a0e546 Revert "Inspired by Perplexity Computer: Model Council mode for Mixture of Agents"
This reverts commit 8d9e18d40b.
2026-08-12 21:50:35 -07:00
Hermes Agent 8d9e18d40b Inspired by Perplexity Computer: Model Council mode for Mixture of Agents
Adds a 'council' synthesis style to MoA (per preset via synthesis_style,
one-shot via the new /council command on CLI + gateway). Reference models
answer independently; the aggregator chairs the deliberation and produces
a user-facing report of consensus, per-model disagreements (with the
differing assumptions behind them), unique contributions, and a
recommendation with an explicit confidence level.

Inspired by Perplexity's Model Council rollout to Perplexity Computer
(changelog 08/04/26): pick a board of 2-8 models, run them independently,
synthesize where they agree/disagree and what each uniquely surfaces.
2026-08-12 19:44:09 -07:00
Teknium 11310068c6 feat(plugins): pre_command observer hook + capability-gated ctx.call_mcp (#64204)
Part A — pre_command observer hook (observer-first per #64182 ground rule 3):
- New VALID_HOOKS event `pre_command`: fires when a recognized slash command
  is about to be dispatched, BEFORE the handler runs, on both surfaces:
  - CLI: cli.py process_command (right after alias resolution)
  - Gateway: gateway/run.py _handle_message cold-path canonical dispatch
- Payload: surface ('cli'|'gateway'), command (canonical), alias_used,
  args_raw, session_key, platform. Return values IGNORED in v1; a plugin
  returning a directive-shaped dict gets a debug log so future
  block/rewrite adopters are discoverable (#64231 taxonomy).
- Deliberately NOT fired on the gateway running-agent intercept path
  (/stop, /approve, busy_policy dispatch during an active run): those are
  control-plane escape hatches on an in-flight run and must stay outside
  plugin observation/veto reach.
- fire_pre_command_hook() helper never raises, so broken plugin infra can
  never break command dispatch.

Part B — ctx.call_mcp (capability-gated, default-off, ground rule 4):
- PluginContext.call_mcp(server, tool, arguments, timeout=30): synchronous,
  callable from plugin hooks/tools, routes through the EXISTING native MCP
  client machinery (tools.mcp_tool._make_tool_handler: background loop,
  trust-tier gates, circuit breaker, reconnect) — never a parallel client.
- Gate: plugins.entries.<id>.mcp_allowlist (list of server names).
  Absent key / unreadable config / non-list value => default-deny.
  Unlisted server raises PermissionError naming the exact config key.
  TODO seam left for the #64228 declared-capability model.
- Bounded: timeout clamped to 1-600s and forwarded to the MCP loop call;
  results capped at 64KB with truncation marker; stable
  {ok, result|error, structuredContent?, truncated?} envelope.

Tests (transport mocked, no live MCP servers):
- tests/hermes_cli/test_pre_command_hook.py: both surfaces fire, canonical
  alias reporting (/exit->quit, /q->queue), hook-before-handler ordering,
  control-plane exclusion, hook failure non-fatal, observer-only directive
  handling.
- tests/hermes_cli/test_plugin_call_mcp.py: default-deny (absent entry,
  unreadable config, non-list, '*'), allowlist enforced per-server,
  denied calls never touch transport, timeout forwarding/clamping,
  result truncation, error/structuredContent envelopes.

Docs: hooks.md shipped-catalog row for pre_command; plugins.md
"Calling MCP servers from plugins" section with the security note.

Closes #64204
2026-08-12 19:16:59 -07:00
Teknium baa6b2e34d feat(browser): auto-install the Browser Use CLI instead of silently downgrading
The Browser Use CLI became the default browser backend, but nothing
provisioned it: users without uv/uvx (field report from DongyangHe on
macOS) silently fell back to the built-in browser tools with no notice.

- install_cli() in tools/browser_use_cli.py: uv tool install browser-use
  via the managed uv (bootstrapped on demand), linked into
  $HERMES_HOME/bin (UV_TOOL_BIN_DIR)
- _find_cli() now also probes $HERMES_HOME/bin for browser-use/uvx —
  Hermes' managed uv is not on the user's PATH
- hermes tools post_setup actually installs (Camofox standard) instead
  of printing instructions
- install.sh / install.ps1 provision the CLI at install time
  (best-effort, non-fatal, honors --skip-browser)
- CLI startup shows a one-line notice (24h rate-limited) when the
  default backend downgraded to the built-in tools
2026-08-11 17:06:15 -05:00
Jakub Wolniewicz 0acf49b16f fix(kanban): isolate review handoff ownership 2026-08-10 12:43:46 -07:00
Jakub Wolniewicz ae23b1f676 fix: complete kanban review lifecycle
Close the autonomous implement-review-rework loop, preserve parent gating and implementer provenance, distinguish downstream review cards, and surface legacy review dependency deadlocks immediately.

Co-authored-by: kaishi00 <6590895+kaishi00@users.noreply.github.com>
2026-08-10 12:43:46 -07:00
Teknium 55f9e472a0 perf(cli): sub-400ms warm startup — probe-mode check_fns, lazy MCP SDK, banner snapshot, parallel worktree add
Cold CLI time-to-banner was ~1.8s (hermes) / ~2.8s (hermes -w). The banner
path was paying for work the session doesn't need before first input:

- aux availability probes built REAL OpenAI/httpx clients (openai import
  ~0.3s + SSL context) just to answer check_fns. New aux_probe_mode()
  returns a cache-excluded stub; resolution policy unchanged.
- tools/mcp_tool imported the mcp SDK (~260ms, mcp.types pydantic model
  construction) at module import even with zero MCP servers configured.
  SDK import is now lazy behind _ensure_mcp_sdk(); _MCP_AVAILABLE is a
  find_spec probe so every existing gate/test keeps its semantics.
- banner blocked 500ms on the update-check prefetch; now waits 50ms and
  defers the warning line to a daemon thread (prints above the prompt).
- banner recomputed get_tool_definitions + skills scan + git state every
  launch; now snapshotted to ~/.hermes/cache/banner_snapshot.json keyed on
  (config.yaml, .env, checkout rev, toolsets) and replayed on warm launches
  with a background refresh. Agent tool list is still computed fresh.
- _resolve_active_context_length probed the Nous portal /models (~200ms
  network) per launch; the tool-search gate now prefers the on-disk
  context cache when present.
- schema reconciliation re-executed SCHEMA_SQL in a scratch SQLite DB
  (~85ms) per SessionDB(); the reference parse is now disk-memoized by
  DDL hash (live-DB diffing still runs every startup).
- bundled-skills sync (~120-170ms rglob/hash) moved off the startup path
  to a daemon thread; plugin discovery starts in the background and every
  synchronous consumer joins via discover_plugins().
- hermes_cli.auth imported httpx eagerly (~30ms); now a lazy proxy that
  test monkeypatching still reaches (setattr forwards to the real module).
- fast chat launch: unambiguous 'hermes'/'hermes chat' invocations skip
  building all ~40 subcommand parsers (bails to full dispatch on anything
  else, incl. container mode).
- -w path: git worktree add runs with checkout.workers=8 (0.6s→0.2s) and
  overlaps HermesCLI construction; --skills preload runs in the background
  and is folded in at agent init (finalize_preloaded_skills, same
  fail-loud contract for fully-unknown skill lists); stale-worktree prune
  moved off the banner path.

Warm results (PTY time-to-banner, 5-run): hermes 1.80s → 0.38-0.40s;
hermes -w -s hermes-agent-dev --yolo 2.82s → 0.57-0.69s.
2026-08-10 10:40:19 -07:00
Teknium 244d296646 fix(personality): single-owner personality state + one-time reset migration
Personality persistence used to be split per surface: the TUI/desktop wrote
the NAME to display.personality while the CLI/gateway wrote rendered TEXT
into agent.system_prompt (and their /personality none only blanked the
text, leaving the name behind). When #81946 made display.personality
authoritative everywhere, stale names written long ago resurrected
personalities users had turned off - kawaii defaulting on after updating.

- hermes_cli/personality.py: new single owner of personality state.
  Built-in personality definitions, neutral-name normalization, rendering,
  availability (built-ins overlaid by agent.personalities), overlay
  resolution, and the ONLY sanctioned persistence path
  (persist_personality -> display.personality; never agent.system_prompt).
- v34 config migration: one-time reset of display.personality to none
  (announcing which personality was cleared and how to re-enable), plus a
  scrub of agent.system_prompt when it verbatim-equals a known personality
  render (machine-written by the old CLI/gateway). Hand-written manual
  prompts are never touched.
- All surfaces rewired through the module: CLI /personality (incl. active
  marker in the list), gateway /personality, TUI config.set + slash path
  (which previously applied without persisting), TUI config.get (reports
  the EFFECTIVE personality), completer, hermes config display, and the
  tui_gateway health probe.
- cli.py/config duplicates removed: built-ins now defined once; the
  desktop mirrors them from one lib module (src/lib/personalities.ts).
- Docs updated: selection lives in display.personality, built-ins always
  available, one-time reset note.
2026-08-09 10:33:58 -07:00
Teknium 851f23ebc6 fix(cli): fence OSC 11 background query with DA1 so late replies can't leak into the prompt
The classic CLI's light-mode detection sends an OSC 11 background-color
query and blind-waits 100ms. Terminal managers that swallow OSC 11
(herdr) made every startup pay the full 100ms for nothing, and any
in-order relay that answers slower than 100ms (SSH bridges, WSL,
loaded tmux servers) delivered the reply AFTER prompt_toolkit owned
the tty — the rgb:.../escape payload leaked into the input line as
gibberish characters.

Fix: send the OSC 11 query followed by a DA1 sentinel (ESC [ c) in one
write — the same fence pattern the Ink TUI's TerminalQuerier uses.
Terminals answer queries in order and effectively all of them answer
DA1, so the DA1 reply proves the terminal has already processed (or
ignored) our OSC 11. Fast terminals and herdr-style multiplexers now
resolve in ~1ms; slow relays get their reply consumed instead of
leaked; a hypothetical DA1-mute terminal falls back at a 1s safety
net, same clean timeout path as before.

Adds real-PTY regression tests covering the herdr-style (DA1-only),
slow-relay (+300ms reply), and fully mute emulator behaviors, each
asserting zero leftover bytes in the tty buffer. Sabotage-verified:
the slow-relay test fails against the old un-fenced code with the
exact leak payload in LEFTOVER.
2026-08-08 19:22:53 -07:00
liuhao1024 54641186ff fix(cli): drain late OSC 11 replies after TCSAFLUSH to prevent input leak
On slow terminals (VPS, containers under load), the OSC 11 background
color response can arrive after TCSAFLUSH completes — leaking into
prompt_toolkit's input buffer and silently consuming the first 1–3
characters of every response.

Add a 50ms post-flush drain window that reads and discards any late
bytes via select() + os.read() before prompt_toolkit grabs the tty.

Fixes #40250
2026-08-08 19:22:53 -07:00
Teknium 5a16635f40 feat(cli): show session titles in status bars 2026-08-08 17:02:55 -07:00
Brooklyn Nicholson f726090d48 feat(sessions): name a session the moment it starts
Titling fired on the first response, so a session sat unnamed for the whole
opening turn - p50 151s, p90 1212s across real sessions, because a turn is
tool calls, not one round-trip. A turn that failed or was interrupted never
got a title at all. Four surfaces each carried their own copy of the call.

Move it into the shared turn prologue and split it in two: a deterministic
title derived from the user's opening message, written inline before the
model runs, then one small-model call that upgrades it. The response is
constrained to a JSON object so there is no preamble to strip, and control
wrappers are stripped rather than refused, so a slash command titles as
what the user asked for instead of the command itself.
2026-08-08 17:07:21 -05:00
Brooklyn Nicholson a0d406dcd8 fix(personality): stop writing personality into agent.system_prompt
Persist display.personality only; apply rendered text as an in-session
overlay across CLI, TUI config.set, and gateway /personality.

Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
Co-authored-by: EMT5320 <1908937833@qq.com>
2026-08-08 14:01:56 -05:00
kshitij 73997c41bb fix(tts): split long speech by provider and platform limits
Salvage of PR #17973 by @TKCen (Sebastian Hänisch), re-implemented on
current main to preserve speed/instructions/provider params,
prepare_spoken_text normalization, OPUS_VOICE_PLATFORMS, is_write_denied
path security, microsecond timestamps, and the streaming-TTS gate.

- Split long TTS text into provider-safe chunks instead of truncating
- Pack generated audio against platform upload limits (Discord 10MB,
  Telegram 50MB, configurable via tts.delivery_profiles)
- Combine chunks with ffmpeg (OGG/Opus re-encoded, MP3 stream-copied)
- Multi-file delivery when combination fails or would exceed limits
- Remove hard [:4000] truncation from all callers (cli.py, voice.py,
  gateway/run.py, gateway/platforms/base.py)
- Gemini TTS raises ValueError instead of silently truncating when
  composed prompt exceeds the provider limit

Simplify-code fixes: removed dead all_touched_paths set, added
try/finally for scratch file cleanup on exception, clean error response
on chunk failure instead of leaking stale file_path.
2026-08-08 22:54:20 +05:30
Ken Weiner b68a532295 fix: handle replacement transforms after CLI streaming 2026-08-08 14:06:30 +05:30
Ken Weiner 367dda813c fix: print transform_llm_output appended content after CLI streaming
When the CLI streams a response token-by-token, it marks the response as
already displayed and skips re-printing after the tool loop. This means any
content appended by a transform_llm_output plugin fires after streaming — the
appended text is in the final response and stored in history, but never shown
to the user.

Fix by tracking the pre-transform response in finalize_turn() and including it
in the result dict as pre_transform_response. The CLI then checks whether the
response was transformed and, if so, prints only the appended suffix.

Previously the already_streamed branch was a no-op pass. Now it detects
post-stream plugin additions and outputs them without re-printing the streamed
body.
2026-08-08 14:06:30 +05:30
Teknium 5c29566e8d feat(terminal): graceful degradation for remote backend connection failures
Connection-class infrastructure failures on remote terminal backends (SSH
host unreachable/timed out, Docker daemon down or missing, remote file
sync failing on a dead link) previously surfaced to the model as raised
RuntimeError tracebacks. The model got a stack blob with no guidance and
the failure was indistinguishable from a tool bug.

Now:

- New EnvironmentConnectionError(RuntimeError) in tools/environments/base.py
  carrying a reason + retry_hint. Subclassing RuntimeError keeps every
  existing catcher working.
- ssh.py classifies connect-refused, connect-timeout, scp, remote mkdir,
  bulk upload/download, and remote rm failures as connection errors.
- docker.py classifies all four _ensure_docker_available() failure paths
  (missing exe, non-executable exe, daemon timeout, `docker version`
  failure).
- terminal_tool catches EnvironmentConnectionError and returns a
  structured tool result the model can act on:
    {"status": "degraded", "reason": ..., "retry_hint": ..., "exit_code": -1}
  The failed backend is evicted from the environment cache so a later
  call retries from scratch — recovery is automatic once the backend is
  reachable again.
- Config gate terminal.degraded_mode: warn|fail (default warn) in
  config.yaml, bridged as TERMINAL_DEGRADED_MODE across all four bridge
  sites (cli.py env_mappings, gateway/run.py _terminal_env_map,
  TERMINAL_CONFIG_ENV_MAP, DEFAULT_CONFIG). "fail" preserves the
  historical error+traceback tool result.
- Command failures (nonzero exit, command-not-found) are NOT touched —
  only infrastructure failures classify as degraded.

Tests: tests/tools/test_terminal_degraded_mode.py (15 tests) covering
exception classification for ssh+docker, structured degraded results,
no-caching of degraded envs, recovery after the backend returns,
nonzero-exit results unaffected, fail-mode preservation, invalid-mode
fallback to warn, and the four-site config bridge invariant.

Inspired by: Claude Cowork degraded-backend behavior (idea-level,
docs-only evidence).
2026-08-07 09:07:55 -07:00
kshitij 4eabb595f0 fix(agent): finish the #80622 bug class — sibling predicates, refund ordering, prompt carve-out, honest skip response
Follow-ups on top of the salvaged #80696 fix (review findings):

- Sibling sites: rollback.restore, gateway /retry, CLI /retry and /undo N,
  and both CLI resume turn counters now use is_user_originated_turn so
  legacy-persisted standalone handoffs (durable role=user, no display_kind)
  can never be truncation targets or counted as user turns (#80622
  suggested regression 4, dispatcher-wide).
- Site-1 guard: hoist the api_call_count decrement + iteration-budget
  refund above the break so a skipped turn no longer leaks a budget unit
  and finalize_turn logs the true call count (matches the ollama early-exit
  and the site-2 sibling).
- Site-2 guard: run the handoff guard BEFORE reanchoring so a restored
  user ask is what the anchor lands on, not a stale pre-restore index.
- SUMMARY_PREFIX: add the mid-tool-loop carve-out the code-side guard
  already implements, so a literal-minded model doesn't halt an in-flight
  exchange after in-place compaction.
- Skip path returns a short compaction status instead of replaying the
  previous turn's answer (finalize_turn would append it as a fresh
  assistant row — duplicate prose in transcript and delivery).
2026-08-07 19:44:35 +05:30
kshitij 20e01f935b fix(voice): early-exit sliding window on match, clear barge phase in finally, fix test helper
- is_tts_echo sliding window: return True immediately when ratio >= threshold
  instead of scanning all remaining windows. Common echo case drops from
  ~4s to <1ms for long spoken text (found by /simplify-code efficiency review).
- Clear _voice_barge_phase in _voice_submit_barge_utterance finally block
  alongside _voice_barge_capture, preventing stale phase from a previous
  trip affecting a future call.
- Add _voice_last_tts_text and _voice_barge_phase to _make_voice_cli test
  helper so it matches __init__ state.
2026-08-07 14:38:02 +05:30
chelsealong d4a753ea42 fix(voice): drop playback-phase barge transcripts that echo Hermes' own TTS
The full-duplex barge-in listener added in 5081551f0 stays active during
TTS playback with no acoustic echo cancellation. On some speaker/mic
combinations, TTS bleed alone crosses the barge threshold, gets
transcribed, and is queued as the next user turn -- whose reply is then
spoken, captured, and queued again, producing an unbounded TTS -> STT ->
TTS feedback loop (#75780).

Add a fail-closed transcript-level guard: when a barge trip happens during
the playback phase, compare the captured transcript against the TTS text
Hermes just spoke (tools/voice_mode.is_tts_echo, a language-agnostic
character-level similarity ratio). A close match is dropped instead of
queued, and the mic is handed back to the normal continuous-listening
loop. Generation-phase trips (no TTS playing, so no bleed is possible)
are unaffected.
2026-08-07 14:38:02 +05:30
Teknium 8f2712725a feat: /refine — run the memory/skill self-improvement review on demand
/refine [focus] fires the existing background review fork
(AIAgent._spawn_background_review) immediately instead of waiting for
the automatic 10-turn memory / 10-iteration skill nudge counters.
Optional focus instructions are appended to the review prompt so the
fork prioritizes what the user asked for (e.g. '/refine save the
deploy workflow as a skill').

- New optional focus parameter threaded through
  _spawn_background_review -> spawn_background_review_thread.
  Automatic post-turn reviews pass None and their prompts are
  byte-identical to before.
- CLI handler snapshots conversation_history; gateway handler pulls
  the idle session's cached AIAgent from _agent_cache (rejected while
  the agent is running).
- Review runs in a daemon thread against the snapshot — live
  conversation, message alternation, and prompt cache untouched.
- Slack stays under the 50-slash cap via /hermes refine.

Adapted from the /refine concept in Prime Intellect's Prime-Agent
(Continual Harness); Hermes' equivalent durable state is the
memory + skill stores, so the review fork is the natural target.
2026-08-05 22:40:51 -07:00
Teknium 6518aa184e feat: /heartbeat — recurring session re-entry prompt fired when idle
/heartbeat every <interval> <prompt> gives the current session one
recurring instruction. When the session is idle and the interval has
elapsed, the prompt is injected as a plain user turn — same
conversation, same context, prompt cache and role alternation
untouched.

- CLI: idle-poll watchdog thread (wake-word watchdog pattern) feeding
  _pending_input; gateway: single gateway-wide async poller injecting
  through the adapter FIFO. Busy sessions coalesce their tick to the
  next idle poll.
- Missed ticks coalesce (anchor resets on fire) — a busy hour yields
  ONE heartbeat turn, never a backlog. Real user messages always win.
- 60s interval floor; injected prompt carries a don't-invent-work
  guard so idle heartbeats don't generate busywork.
- State persists in SessionDB.state_meta (heartbeat:<session_id>),
  survives /resume, migrates across compression session rotations
  alongside /goal state.
- Session-scoped and in-process by design — durable cross-process
  schedules remain the cron subsystem's job (docs draw the boundary).
- Slack stays under the 50-slash cap via /hermes heartbeat; ghost-text
  suggester now prefers the shortest prefix match so /he still
  suggests /help.

Adapted from the session-heartbeat concept in Prime Intellect's
Prime-Agent (/heartbeat).
2026-08-05 22:32:55 -07:00
Brooklyn Nicholson bde8c4e108 feat(cli): /export and /import slash commands for profile sharing
/export [profile] [-o output.tar.gz] bundles a profile into the shareable
archive; /import <archive> [--name <name>] adopts one as a new profile
(wrapper alias created when safe). Registry-driven, cli_only, so the CLI
and TUI both pick them up in autocomplete and help.
2026-08-04 12:07:29 -06:00
Teknium ef9f6effaf fix(cli): persist YOLO mode across --resume
A session's YOLO bypass lived only in the in-memory
tools.approval._session_yolo set (or the process-frozen --yolo env
var), so resuming a session in a fresh process silently reverted the
user's /yolo ON — dangerous commands started prompting again.

Persist a yolo_mode flag in the session row's model_config JSON and
restore it on every CLI resume path:

- SessionDB.set_session_yolo() merges the flag into model_config
  (same lineage-preserving merge as update_session_runtime_lock);
  SessionDB.session_yolo_enabled() reads it back, false on any parse
  failure.
- /yolo toggle persists ON and OFF through the new helper; the
  compression/branch session-id rotation carries the flag onto the
  continuation row.
- --yolo launches record the flag at session creation (agent_init),
  and a /yolo toggled before the lazily-created row exists is carried
  into the creation-time model_config (_ensure_db_session).
- HermesCLI._restore_session_yolo() re-enables the bypass on startup
  --resume/-c, the deferred init path, and mid-chat /resume, with a
  visible ' YOLO mode restored from session' notice. No-op under a
  frozen process-wide --yolo and never enables on absent/garbage flags.
2026-08-02 20:30:06 -07:00
Teknium aac74be2f1 fix(approval): classify CLI/TUI approval timeouts separately from explicit denials
When an approval prompt expired without a response, every CLI-side path
collapsed the timeout into the same 'deny' choice as an explicit user
refusal, so the agent was told the user denied the action when the user
simply never answered. The gateway wait already distinguished the two
('timed out without user response... Silence is not consent.'); this
brings the CLI/TUI/ACP surfaces to parity.

- prompt_dangerous_approval(): input()-path expiry now returns a distinct
  'timeout' choice (still fail-closed).
- cli.py _approval_callback + hermes_cli/callbacks.py approval_callback:
  deadline expiry returns 'timeout' instead of 'deny'.
- check_all_command_guards / _run_approval_gate CLI tails: 'timeout' maps
  to outcome='timeout' with a 'timed out without user response... Silence
  is not consent.' BLOCKED message (matching the gateway wording);
  explicit deny keeps outcome='denied' and gains user_consent=False for
  shape parity.
- computer_use: 'timeout' verdict threads through the CLI adapter and
  yields a 'prompt timed out — the user did not respond' error instead of
  'denied by user'.
- ACP permissions bridge: FutureTimeout returns 'timeout' (other failures
  still 'deny'); elicitation maps 'timeout' to 'cancel' like the gateway's
  unresolved outcome; codex wire mapping documents deny/timeout→decline.
- write_approval already treats unknown choices as 'stage, not drop', so
  a timeout now stages the memory write instead of silently refusing it.

Every timeout path remains fail-closed — the action never runs; only the
classification reported to the agent changes.
2026-08-02 20:21:59 -07:00
Teknium 3829e34e23 feat(hooks): outbound webhooks — push signed lifecycle events to external HTTP endpoints
The inverse of the inbound webhook platform: hooks.outbound in
config.yaml lists HTTP targets + the plugin-hook events they subscribe
to (on_session_end, subagent_stop, post_tool_call, ...). Each firing
POSTs a JSON payload (same top-level shape as shell hooks' stdin wire)
signed GitHub-style with HMAC-SHA256 (X-Hermes-Signature-256).

Rides the existing hook bus — notify-only callbacks registered on the
plugin manager at the same CLI/gateway/main entry points as shell
hooks. Delivery is fire-and-forget via a bounded queue + single daemon
worker thread, so a dead endpoint can never stall a tool call. Bounded
retries (5xx/conn errors once; 4xx never). secret_env preferred over
inline secret. HERMES_SAFE_MODE skips registration. hermes hooks list
shows outbound targets with signed/UNSIGNED status.

Zero new model tools, zero new subsystems.
2026-08-02 15:01:11 -07:00
Shaun Prince d15b638a88 fix(compression): let explicit interrupts cancel safely
Makes interrupt-protected context compression cancellable by an explicit
user or lifecycle stop, without weakening protection against ordinary
incoming messages, voice interjections, or active-turn redirects.

Separates explicit hard cancellation from ordinary interrupt/redirect
state with a dedicated threading.Event; introduces
AuxiliaryExplicitCancellation as an attempt-local frozen-cause signal;
isolates the synchronous provider callback in a bounded daemon worker
during protected compression; atomically linearizes Codex timeout
cleanup against explicit cancellation; propagates hard cancellation
through child agents and explicit stop surfaces; serializes hard-cancel
admission against compression commit admission with
CompressionCommitFence; aborts before session rotation or late DB commit,
restores in-place transcript mutations and compressor state, and releases
the heartbeat and compression lease.

Based on #74449 by @suparious. Resolved merge conflicts in
agent/context_compressor.py (feasibility check + try/except) and
tui_gateway/methods_session.py.
2026-08-02 22:15:20 +05:30
Hermes agent 4e0a775580 fix(state): make VACUUM interval configurable 2026-08-02 21:32:13 +05:30
Teknium d7522118ef fix(cli): route keyless first run into provider onboarding instead of a broken chat
A completely unconfigured install previously booted into a working-looking
chat (banner showed model 'unknown'), accepted a message, spun ~30s, then
failed with 'Set OPENROUTER_API_KEY' — a provider the user never chose —
and never offered setup.

- HermesCLI.run() now probes provider readiness at startup (TTY only) and
  offers the shared provider picker (hermes model flow, which fronts Quick
  Setup / Nous Portal OAuth) when nothing is configured. Decline is
  respected; picker state re-syncs into the live CLI so the next turn works
  without a restart.
- New silent probe _runtime_credentials_ready(): no printing, no state
  mutation; handles keyless local endpoints and callable bearer providers.
- The empty-api-key error is provider-aware: names the actual resolved
  provider and points at 'hermes model' / 'hermes setup' instead of
  hardcoding OPENROUTER_API_KEY.
- Banner: unconfigured installs render 'no model configured — run /model'
  in red instead of the silent 'unknown' model slug.

Consumer-onboarding audit finding #2 (sev 5), Aug 2026.
2026-08-01 15:34:16 -07:00
Brooklyn Nicholson 0b69a6ac02 feat(kanban): let a task pin its own thinking depth
A task could already pin a model and provider, but not how hard the worker
thinks: reasoning effort came from the assigned profile's config and nothing
per-task could reach it. Pairing a small model with high effort, or a big one
with thinking off, meant editing the worker profile itself.

Adds a tasks.reasoning_effort column (migrated, NULL = inherit the profile)
with set_reasoning_effort(), a create_task kwarg, and a --reasoning spawn flag.
Kept deliberately independent of model_override: a task may run the profile's
own model at a different depth, and clearing a model override no longer resets
the depth the operator chose. "none" is a value (thinking off), not a clear.

--reasoning is new on the CLI too — the level was only reachable through the
/reasoning slash command, so the dispatcher had no flag to pass. It overrides
agent.reasoning_effort for one run and is never persisted.
2026-08-01 16:09:56 -05:00
kshitij 3572d4bca1 fix(mcp): ensure MCP discovery completes before agent build in non-interactive sessions
Non-interactive sessions (hermes chat -q, hermes -z) snapshot the tool
registry at AIAgent construction time. If background MCP discovery hasn't
finished, MCP tools are invisible for the entire session — and unlike
interactive mode, there is no between-turns late-binding refresh to recover.

Root cause: wait_for_mcp_discovery() only joins an already-created discovery
thread, so it no-ops if a direct/single-query path reaches agent construction
before MCP startup created that thread. Oneshot._run_agent() didn't call it
at all.

Fix:
- Add ensure_mcp_discovery_before_agent_build() helper to mcp_startup.py:
  idempotently starts discovery if needed + bounded wait. Fail-open on errors.
- Add single_query parameter to _resolve_discovery_timeout/wait_for_mcp_discovery:
  uses mcp_single_query_discovery_timeout (default 15s) instead of the
  interactive mcp_discovery_timeout (1.5s) because one-shot sessions have no
  second turn to recover.
- Wire into CLI _init_agent (single_query from _single_query_mode flag set
  in cli.py's single-query path) and oneshot._run_agent (single_query=True).
- Interactive sessions unchanged: keep 1.5s bound (between-turns refresh covers).

Closes #38448, #51316, #37013, #68137
Composite salvage of #60017 (chrishart0), #51322 (Bartok9), #38620 (buptwz),
#43544 (halonke), #36882 (vanhoof).
2026-08-01 12:27:31 +05:30
MaxFreedomPollard 3dee0634c1 fix(cli): dispatch /background inline instead of queuing it behind the turn
/background (/bg, /btw) exists to start independent work while the current
turn keeps running. Typed while the agent was busy it went into
_pending_input like ordinary input, and process_loop is blocked inside
self.chat() for the whole run, so the background task only started once the
foreground turn had finished. That is the one moment it was not needed.

/steer had the identical problem and was fixed the same way, by dispatching
inline on the UI thread. The command's own CommandDef already declares
busy_policy="dispatch"; the gateway honours that, the classic CLI never
consulted it.

The foreground turn is untouched: no interrupt, no steer, and ordinary
non-slash input keeps following the configured busy-input behaviour.
2026-07-31 22:35:12 -07:00
dongjiang cc0146f2de fix(cli): dispatch /indicator to set the busy-indicator style (#50618)
The /indicator command was registered in COMMAND_REGISTRY, listed in
/help, offered by tab-completion, recommended by the tips system, and
even documented in config.py — but it had no actual handler. Running
/indicator in the CLI produced "Unknown command: indicator".

Add _handle_indicator_command to CLICommandsMixin that:
- Shows the current indicator style when called with no args or "status"
- Validates the requested style against the shared INDICATOR_STYLES
  allowlist (ascii | emoji | kaomoji | unicode)
- Persists the choice to display.tui_status_indicator in config.yaml
  via the existing save_config_value helper
- Falls back to session-only when config save fails

The indicator-style allowlist is defined once in hermes_constants as
INDICATOR_STYLES + DEFAULT_INDICATOR_STYLE and imported by all three
consumers (CLI handler, command registry, TUI gateway config handler),
preventing drift between the TUI and CLI validation.

Also adds tests/cli/test_indicator_command.py covering dispatch,
validation, persistence, and registry integration.

Signed-off-by: dongjiang <dongjiang1989@126.com>
2026-07-31 22:33:16 -07:00
teknium1 dc87d15586 feat(terminal): raise Docker sandbox /dev/shm to 1g by default (configurable)
Port from nanocoai/nanoclaw#2748: Docker's built-in 64 MB /dev/shm silently
breaks shared-memory-hungry workloads inside the sandbox — Chromium/Playwright
renderers crash tabs, and PyTorch DataLoader workers die with 'bus error' /
'insufficient shared memory'. tmpfs is lazily allocated, so the higher ceiling
costs nothing until actually used, and usage still counts against the
container's --memory cgroup limit.

- tools/environments/docker.py: --shm-size 1g in resource args (not
  cgroup-gated; tmpfs mount option). Skipped when docker_extra_args already
  sets --shm-size, or when configured empty/'0' (Docker default).
- terminal.docker_shm_size config key (DEFAULT_CONFIG + all three
  config->TERMINAL_DOCKER_SHM_SIZE env bridges: CLI, gateway, config.py map)
- tests: default emit, custom value, opt-out, extra_args precedence,
  helper edge cases (sabotage-verified: default/custom tests fail without
  the emit)
2026-07-31 21:31:51 -07:00
teknium1 9704ed86c1 feat(cli): ! shell mode — run a command without spending a model turn 2026-07-31 21:21:20 -07:00
teknium1 a55a52c72f feat(cli): complete the Ctrl+S prompt stash — keybinding, state machine, tests
Finishes the input stash started in the preceding commit from PR #4771.
That PR shipped only the panel renderer: its `@kb.add('c-s')` handler and
stash-state initialization were lost in a rebase, so the panel predicate
read undefined `_stash_panel_open` / `_stash_list` and the feature was
unreachable. This adds the missing half and the tests the PR never had.

Resolves the review feedback on #4771:

- Rebuilt the stash on current main's keybinding setup. The `c-s` key was
  unbound repo-wide, so there is no conflict.
- Extracted the state machine into `hermes_cli/prompt_stash.py` as pure
  functions (no prompt_toolkit import) so it is directly unit testable —
  the PR was cli.py-only with zero tests.
- Dropped the PR's unrelated changes: delegation `supervisor_model` /
  `execution_model` config aliases, and stale reverts of the banner
  builder, worktree pruning, logging setup, and MCP toolset validation
  that its 14k-commit-old base dragged along.
- Fixed the 📌 double-width measurement for real. Three commits in the PR
  ("subtract 1 from len()", "use bare len()", "subtract 1 again") were
  chasing this by tweaking `len()`; all horizontal math now goes through
  `_status_bar_display_width` (prompt_toolkit `get_cwidth`), which also
  keeps CJK previews inside the border. Narrow terminals fall back to
  compact header/footer labels instead of overflowing — caught by a
  parametrized width test, not by eyeballing.

Gesture (the contributor's design, kept):

- Composer has content → push onto the stash, clear the input.
- Composer empty, one stashed → pop it straight back.
- Composer empty, 2+ stashed → open the browse panel (↑↓ / Enter / D / Esc).
- Panel open → Ctrl+S closes it.

Pushing onto a stack rather than a single slot is what makes repeated
Ctrl+S safe: a second stash never silently overwrites the first, and with
2+ parked the panel asks rather than guessing which to restore. A `📌 N`
status-bar badge and a composer placeholder advertise the parked draft so
it cannot be silently forgotten.

Deliberate departures from the PR:

- No auto-restore after the agent responds, and no `display.stash_auto_restore`
  config key. The PR itself had already defaulted this to false as
  "avoids surprising the user"; a keystroke the user pressed should not
  cause text to reappear on its own, so the dead default is dropped
  rather than carried as config surface.
- Nothing is persisted to disk. Drafts routinely contain pasted
  credentials and NDA material, so the stash is session-scoped and
  in-memory only. Any future persistence must route through
  `get_hermes_home()`.
- Suppressed while a modal prompt owns the composer (sudo / secret /
  approval / clarify / slash-confirm / model picker) so Ctrl+S can never
  stash a password.
- Restoring images extends `_attached_images` instead of replacing it, so
  an attachment added since the stash was taken is not silently dropped.
- `buf.reset()` on stash (not `text = ""`) clears completion state,
  selection, and undo stack with the text.

Tests: 95 new tests across two files — 66 on the state machine (empty
buffer is a no-op, exact round-trips including newlines/tabs/CJK/fences,
no-clobber ordering, cap eviction, indicator states, panel cursor
clamping and deletion, the full resolve_ctrl_s decision table) and 29 on
the cli.py wiring (per-instance stash, keybinding registration guard,
layout slot, panel bounded at 8 widths, status-bar indicator lifecycle).
The keybinding-registration test asserts the `c-s` handler exists in
source specifically so the rebase loss that broke #4771 cannot recur.

Verified: 153 passed, 0 failed across the two new files plus
tests/cli/test_cli_init.py and tests/cli/test_cli_extension_hooks.py.
ruff check clean; check-windows-footguns clean.

Docs: Ctrl+S added to the CLI keybindings table.

Co-authored-by: CK iRonin.IT <cyprian@ironin.pl>
2026-07-31 21:21:16 -07:00
CK iRonin.IT cfc5dd6aaa feat(stash): multi-item stash with browsable panel
Ctrl+S pushes/pops/browses a stash stack instead of a single slot:
- Buffer has content: push to stash
- Buffer empty + 1 item: pop immediately
- Buffer empty + 2+ items: open panel browser

Panel: ↑↓ navigate, Enter restore, D delete, Esc/Ctrl+S close.
Status bar shows 📌 N count, 📌 N ▲ when panel open.
2026-07-31 21:21:16 -07:00
Brooklyn Nicholson e41d2029b7 fix(sessions): keep kanban worker runs out of the session lists
Exclude the source from the desktop sidebar and project tree, the TUI
resume picker, session_search, and the CLI session listings.
2026-07-31 13:53:04 -05:00
Yi Lok Enoch Lam 5835201de1
fix: keep queued paste payloads atomic (#74797)
Co-authored-by: eloklam <22125285+eloklam@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-31 12:04:59 -04:00
CK iRonin.IT c1ec394160 feat(cli): double ESC discards the draft, even mid-stream
Ctrl+C interrupts a running turn and only clears the composer when idle,
so there was no way to discard a half-typed prompt while the agent was
streaming. Claude Code and Gemini CLI both bind that to double-Esc.

Appends the draft to history before clearing, so Up recalls it — the same
undo affordance Claude Code gives, which is what makes this safe on a key
people hit by reflex. Excluded when a modal prompt is up, since those bind
ESC eagerly and cancel should still win.

Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
2026-07-30 04:20:41 -05:00
Brooklyn Nicholson 8d112c05f7 fix(cli): route Cmd+Backspace and Cmd+ForwardDelete to the kill bindings
Terminals that rewrite Cmd+Backspace to Ctrl+U already reach
unix-line-discard. Kitty keyboard protocol and xterm modifyOtherKeys
terminals instead report Cmd as the super modifier bit, producing CSI
sequences prompt_toolkit has no entry for — the raw bytes fall through
the VT100 parser and land in the buffer as literal text.

Alias those to the readline kill bindings prompt_toolkit already ships.
Backspace is a CSI-u codepoint (127); ForwardDelete is a CSI tilde key,
so its modifier rides in the CSI 3 ; mod ~ form rather than CSI-u.
Ctrl+ForwardDelete keeps its own binding — that is delete-word on
Linux/Windows, not kill-line.
2026-07-30 03:39:26 -05:00
Jeff Watts 53f7d137ed fix(windows): native Windows correctness for CLI, gateway status, banner, and WSL browser paths
Salvaged from #57016 by @lEWFkRAD:
- cli.py: handle file:///C:/... drive-letter URIs on nt (strip the
  leading slash urlparse leaves); join Termux example paths with literal
  forward slashes so hints stay POSIX on Windows.
- gateway/status.py + hermes_cli/gateway.py: normalize backslashes to
  forward slashes before the HERMES_HOME substring match so separator
  style cannot defeat profile ownership detection.
- hermes_cli/banner.py: cprint degrades to plain print when
  prompt_toolkit has no console (NoConsoleScreenBufferError on
  redirected/absent Windows stdout).
- hermes_cli/browser_connect.py: posixpath.join for WSL /mnt/c/... bases
  (os.path.join would emit backslashes on nt).
- Test hardening: symlink skip-guards, USERPROFILE alongside HOME for
  ntpath.expanduser, SIGKILL absence skipif fixed via monkeypatch,
  drive-letter URI / separator-normalization / banner-fallback coverage.

Dropped from the original PR: tests/cli/conftest.py fixture and the
AppSession _output monkeypatch — main's merged tests/cli/conftest.py
already handles that prompt_toolkit pollution.
2026-07-29 23:16:18 -07:00
Teknium ad12df6ba4 Revert "remove Vercel AI Gateway and Vercel Sandbox (#33067)"
This reverts commit febc4cfec0.
2026-07-29 19:48:37 -07:00
teknium1 4b33e5663b refactor: config auto-migration support floor at v12 + deprecated shim retirement 2026-07-29 16:44:31 -07:00
Teknium 1cf5d3841b perf(cli): stop hermes -w stalling 30-60s on a flaky fetch in _resolve_worktree_base
The #71637 prune fix cut one stage of -w startup, but the base-ref
resolution right after it still ran an uncapped-in-practice
'git fetch origin main' (timeout=30) on every launch — and on a flaky
smart-HTTP connection that fetch intermittently stalled to the full 30s,
then cascaded into step 2's SECOND 30s fetch. Measured: back-to-back
fetches of 0.9s, 1.0s, 63.5s on the same box with healthy TLS (~185ms).

_resolve_worktree_base now:
- skips the fetch entirely when FETCH_HEAD is < 5 min old and the
  tracking ref exists (repeat launches pay zero network cost)
- caps the fetch at 5s and falls back to the locally-known tracking
  ref (labelled 'cached') on timeout/failure instead of cascading into
  a second fetch — genuine staleness stays backstopped by the pre-push
  stale-base gate
- caps 'git remote show origin' the same way

Worst case drops ~60s -> ~5s; warm path is ~0.02s (was up to 30.8s).
sync_base=False and the offline HEAD fallback are unchanged.
2026-07-29 15:34:53 -07:00
Ben Barclay f5b68ad58b Merge origin/main into feat/hsp-sync-client
Resolves the PR's conflict with main (2252 commits). Two conflicts, both
"each side added an independent block in the same place" — kept both:

- gateway/run.py — the housekeeping loop. This branch adds the Skill Sync
  pulls inside the CURATOR_EVERY branch (12-space indent); main adds a
  stale-session auto-archive as a sibling `if` at loop level (8-space).
  Different scopes, so the naive union would have mis-nested the archive
  block into the curator branch; kept each at its own indent level.
- tools/skill_manager_tool.py — the _edit_skill result dict. This branch
  appends the org auto-propose note; main appends
  _add_description_prompt_preview(). Independent, order-insensitive.

No behaviour dropped from either side.

Verified: 3552 passed / 0 failed across 63 suites (scope regenerated to
include main's new maybe_auto_archive / _add_description_prompt_preview
consumers) via scripts/run_tests.sh. `hermes sync` and `hermes sync status`
still work against a live token, resolving the production plane default.

The Pyright Optional-parameter warnings in skill_manager_tool.py are
pre-existing on main (`content: str = None` etc.), not introduced here.
2026-07-29 13:01:36 -07:00
teknium1 bcb352eeab refactor: registry-owned execute() on CommandDef — informational commands unified (thin slice) 2026-07-29 12:11:24 -07:00
teknium1 ba7da1332c refactor: single-owner model switch parsing + effective-model resolution (kills the api_server/run.py divergence class) 2026-07-29 11:54:09 -07:00
teknium1 5b751dc0ad chore: remove unused imports and dead locals (ruff F401/F841 sweep)
Cleans F401 unused imports and F841 dead local assignments across
root *.py, agent/, hermes_cli/, tools/, gateway/, cron/, tui_gateway/
(tests/, plugins/, skills/ excluded).

Intentionally KEPT (false positives / test-patch surfaces):
- agent/transports/__init__.py package re-exports
- cli.py browser_connect re-exports (DEFAULT_BROWSER_CDP_URL area,
  used by tests/cli/test_cli_browser_connect.py)
- hermes_cli/main.py _prompt_auth_credentials_choice /
  _model_flow_bedrock_api_key (accessed via main_mod attr in tests)
- gateway/run.py aliased replay_cleanup + whatsapp_identity re-exports
  and _PORT_BINDING_PLATFORM_VALUES (test-referenced)
- hermes_cli/web_server.py get_running_pid (tests monkeypatch it) and
  _OAUTH_TOKEN_URL availability probe
- hermes_cli/config.py get_process_hermes_home re-export (noqa'd F811
  chain) and yaml availability-probe import
- hermes_cli/nous_subscription.py managed_nous_tools_enabled
  (tests patch hermes_cli.nous_subscription.managed_nous_tools_enabled)
- try/except ImportError availability probes (env_loader, tts_tool,
  mcp_tool, web_server anthropic OAuth block)
- tools/web_tools.py noqa F401 re-exports
- hermes_cli/setup_whatsapp_cloud.py:263 'proceed' skipped: possible
  missing-guard bug, flagged for separate review
- unused function parameters (signature changes out of scope)

Side-effect RHS calls preserved where only the binding was dead
(e.g. web_server proc = _spawn_hermes_action -> bare call).
2026-07-29 11:53:39 -07:00
teknium1 3d48f893da refactor: single build_subprocess_env() factory for all child-process spawns (profile + secret-scrub single owner) 2026-07-29 10:14:11 -07:00