Commit graph

14502 commits

Author SHA1 Message Date
annguyenNous
538173f679 fix(codex): route commentary-phase preamble text to reasoning channel (fixes #41293)
GPT-5.x models on the Codex Responses API emit short pre-tool-call
"preamble" text as message items with phase="commentary". Previously,
_normalize_codex_response() added ALL message items to content_parts
regardless of phase, causing commentary text to leak as visible
assistant content on chat gateways.

Fix: when normalized_phase is "commentary" or "analysis", route the
message text to reasoning_parts instead of content_parts. This keeps
preamble/internal planning in the reasoning channel where it belongs.

Fixes NousResearch/hermes-agent#41293
2026-07-05 06:29:45 -07:00
devatnull
ea125dd62e fix: keep Codex commentary phase out of user-visible text 2026-07-05 06:29:45 -07:00
teknium1
372c0b5f45 chore: add devatnull to AUTHOR_MAP for PR #58700 salvage 2026-07-05 06:29:26 -07:00
devatnull
14c91ade32 fix: normalize display boolean strings 2026-07-05 06:29:26 -07:00
devatnull
b9de7044aa fix: preserve log tool-progress mode with status phrases 2026-07-05 06:29:26 -07:00
devatnull
d111faa3a7 fix: preserve busy steer env override 2026-07-05 06:29:26 -07:00
devatnull
12f03b11ff feat: make busy steer ack configurable 2026-07-05 06:29:26 -07:00
devatnull
46fbd73f66 fix: strip tool progress display modes 2026-07-05 06:29:26 -07:00
devatnull
fddc95f4c2 chore: limit generic status phrases to long-running notifications 2026-07-05 06:29:26 -07:00
devatnull
4bf5b563bd feat: add generic gateway status phrases 2026-07-05 06:29:26 -07:00
teknium1
b0f2bdbe8b fix(whatsapp): gate poll-vote events to Hermes-created polls + salvage follow-ups
- bridge: only enqueue poll_update events for polls Hermes itself created
  (tracked via recentlySentIds when /send-poll returns) so arbitrary human
  polls in group chats don't inject agent-visible messages on every vote
- update test_already_whatsapp_italic for the new markdown-italic mapping
- AUTHOR_MAP entry for @devatnull (PR #58704 salvage)
2026-07-05 06:27:20 -07:00
devatnull
11627fdcb9 feat(whatsapp): native Baileys polls, clarify-as-poll, locations, and rich inbound metadata
Salvaged from PR #58704 by @devatnull, scoped to the WhatsApp surface:
- bridge_helpers.js: pure, tested extraction of inbound Baileys message
  parsing (quoted text, MIME/filename, PTT vs audio, stickers, contacts,
  reactions, polls, locations, GIF playback metadata)
- native poll primitive: /send-poll endpoint, poll messageSecret caching,
  encrypted vote decryption + aggregation via Baileys
- send_clarify() renders multi-choice clarify prompts as native polls;
  votes flow back through the existing clarify text-intercept
- send_location() + /send-location for native WhatsApp location pins
- structured quoted-reply context (fixes duplicated '[Replying to: ...]'
  rendered both by the adapter and gateway/run.py)
- outbound formatting: markdown *italic* -> WhatsApp _italic_, invisible
  unicode sanitization; execSync -> execFileSync hardening; GIF -> mp4
  gifPlayback conversion with truthful image/gif fallback

Out of scope (deliberately not salvaged from #58704): cross-platform
ordered-delivery machinery in gateway/platforms/base.py, LOCATION: and
hermes:poll response-text directives (no prompt wiring exists yet), and
the unconditional WhatsApp reply-anchor suppression.
2026-07-05 06:27:20 -07:00
teknium1
0ca2a927cf chore: add devatnull to AUTHOR_MAP for PR #58697 salvage 2026-07-05 06:12:49 -07:00
Kong
558001307a feat(desktop,docs): surface stt.echo_transcripts in desktop settings and docs
Adapted from PR #53038 (stt.echo) to the stt.echo_transcripts key:
- desktop Voice settings section gains the Echo Transcripts toggle with
  label + description copy
- configuration.md documents stt.enabled / stt.echo_transcripts
2026-07-05 06:12:49 -07:00
devatnull
4be749d151 fix: honor top-level STT transcript echo config 2026-07-05 06:12:49 -07:00
devatnull
406eb719c3 fix: gate interrupt STT transcript echoes 2026-07-05 06:12:49 -07:00
devatnull
bfc5262725 feat: add STT transcript echo toggle 2026-07-05 06:12:49 -07:00
teknium1
95fc3c6b45 chore: add alastraz to AUTHOR_MAP for PR #41383 salvage
Some checks are pending
CI / Detect affected areas (push) Waiting to run
CI / Python tests (push) Blocked by required conditions
CI / Python lints (push) Blocked by required conditions
CI / TypeScript (push) Blocked by required conditions
CI / Docs Site (push) Blocked by required conditions
CI / Deny unrelated histories (push) Blocked by required conditions
CI / Check contributors (push) Blocked by required conditions
CI / Check uv.lock (push) Blocked by required conditions
CI / Lint Docker scripts (push) Blocked by required conditions
CI / Build&Test Docker image (push) Blocked by required conditions
CI / Supply-chain scan (push) Blocked by required conditions
CI / OSV scan (push) Waiting to run
CI / All required checks pass (push) Blocked by required conditions
CI / CI timing report (push) Blocked by required conditions
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
2026-07-05 05:36:31 -07:00
Adrian Lastra
519ec7b3b3 fix(computer_use): parse (label) and = "value" AX element label forms
The SOM/AX element list dropped labels for two extremely common cua-driver
render forms, leaving the model unable to target elements by name:
  - [79] AXButton (Dark)              -> parenthesised label
  - [4]  AXStaticText = "Wi-Fi"       -> = "value" form
  - [92] AXPopUpButton = "Automatic"  -> = "value" form
The old regex only matched quoted "label" and id=Label, so System Settings
buttons/text/popups all surfaced with empty labels. That's why selecting the
macOS Appearance 'Dark' button by element index required guessing — the
labels weren't available to aim with.

Fix: extend _ELEMENT_LINE_RE to capture all four label forms (= "value",
"quoted", (parenthesised), id=Label), skipping a pure-digit (N) order number
in favour of the id= label. Verified live against System Settings: the
Appearance buttons now surface as Auto/Light/Dark.

Adds a regression test covering all label forms. Full suite: 84 passed.
2026-07-05 05:36:31 -07:00
Adrian Lastra
13b75e73ff fix(computer_use): re-fetch via CLI when MCP returns silent-empty captures
The first fix handled the EAGAIN McpError path. But the persistent MCP
session (long-running gateway/desktop worker) has a second failure mode:
list_windows or get_window_state 'succeed' over MCP yet return a
degenerate/empty payload (no windows, or no screenshot + blank tree)
WITHOUT raising — typically when the bridge reconnected mid-call and
dropped the heavy response. That surfaced to the model as a silent 0x0
capture with no error and no fallback firing (0.00s empty return).

Fix: detect empty results in capture() and re-fetch over the CLI
transport before giving up:
  - empty list_windows -> CLI re-fetch the window list
  - empty get_window_state (som/ax) -> CLI re-fetch the AX tree + screenshot
  - empty screenshot (vision) -> CLI re-fetch get_window_state for the PNG

Adds 2 regression tests. Full suite: 83 passed.
2026-07-05 05:36:31 -07:00
Adrian Lastra
7af9abd174 fix(computer_use): fall back to CLI transport when cua-driver MCP bridge hits EAGAIN
The cua-driver MCP stdio bridge intermittently (and on some machines
persistently) fails to forward heavier calls like get_window_state to
the daemon with POSIX EAGAIN — 'daemon transport error forwarding
get_window_state: Resource temporarily unavailable (os error 35)'.
The wrapper surfaced this as an empty 0x0 capture, so computer_use
returned blank screenshots even though the display, permissions, and
the daemon were all healthy (the direct 'cua-driver call' CLI path
worked fine throughout).

Fix: when the MCP path raises the transient/transport error, fall back
to the 'cua-driver call' subprocess transport, which talks to the
daemon over a different socket. The CLI fallback routes get_window_state
screenshots to a temp file via screenshot_out_file (tiny JSON response
instead of a multi-MB base64 blob that congests the socket), reads the
PNG back, retries with backoff, and remaps the JSON into the same
{data, images, structuredContent, isError} shape the MCP path produces
so capture()/_action() are transport-agnostic.

Adds _is_transient_daemon_error() classifier and 3 regression tests.
Verified live: captures that returned 0x0 now return full
1567x905 screenshots with the AX element tree.
2026-07-05 05:36:31 -07:00
Teknium
de4310c8f6
fix(computer-use): report the wedged startup phase in the session ready-timeout error (#58801)
The 'never reached ready' error (issue #57025) was undiagnosable — doctor
and MCP test pass while the wrapper times out, with no hint where startup
stalled. Track a phase marker through _lifecycle_coro (binary-check →
manifest-discovery → mcp-initialize → capability-discovery → ready) and
include it in the timeout RuntimeError plus a pointer to doctor and the
agent.log phase timings.

Complements the 15s→30s bump + success-path phase timing log from #58760.
2026-07-05 05:36:09 -07:00
Teknium
24a7546918
fix(cli): drop shell=True from cua-driver installer — download to mkstemp, exec as argv (#58796)
Replaces the POSIX `/bin/bash -c "$(curl …)"` invocation with a
download-then-exec flow: curl the upstream install.sh into a mkstemp
temp file (unpredictable name, 0600) and run it as a plain argv list.
No shell=True, no command substitution. The temp script is removed in
a finally block; download failures return cleanly without exec.

Salvages the intent of #34974 by @ErnestHysa. His original patch
targeted a fixed /tmp/cua-driver-install.sh path (symlink/TOCTOU-prone
on multi-user hosts) and predates Windows/Linux installer support;
this version uses mkstemp and keeps the powershell path untouched.

Co-authored-by: ErnestHysa <takis312@hotmail.com>
2026-07-05 05:35:44 -07:00
Teknium
2c0820c9ff
feat(cli): autocomplete + ghost text for stacked slash-skill invocations (#58763)
Follow-up to #57987: after /skill-a the completer previously went silent
for a second /skill token. Now, while the leading tokens form an unbroken
skill chain (each token a distinct installed skill, under the 5-cap) and
the word under the cursor starts with '/', the completer keeps offering
the remaining skill commands, and SlashCommandAutoSuggest ghost-suggests
the rest of the next skill name. Instruction text, path-like tokens, and
broken chains get no suggestions. The TUI's complete.slash RPC reuses
SlashCommandCompleter, so it inherits the behavior with no changes.
2026-07-05 04:34:14 -07:00
Teknium
1c156736dc
docs: warn that mid-session model switches break prompt caching (#58747)
/model switches, primary-model fallback, and credential-pool key
rotation all change the prompt-cache key (model and/or account), so
the next turn re-reads the entire conversation at full input price.
Add cost warnings everywhere docs recommend or describe these paths:

- reference/slash-commands.md: cost note on both /model rows
- user-guide/features/fallback-providers.md: warning admonition
- user-guide/features/credential-pools.md: warning admonition
- user-guide/configuring-models.md: mid-session switch warning
- guides/tips.md: expand cache tip + /model tip
- reference/faq.md: warning on the switch-back-and-forth example
- user-guide/desktop.md: composer picker bullet
- developer-guide/context-compression-and-caching.md: new
  cache-aware design pattern (model identity is part of the key)
2026-07-05 04:34:05 -07:00
Teknium
7fde19afcc
fix(cli): unwedge cua-driver installer timeouts — group-kill, stale-lock pre-clear, 660s ceiling (#58767)
* fix(cli): unwedge cua-driver installer timeouts — group-kill on timeout, stale-lock pre-clear, 660s ceiling

The cua-driver refresh in hermes update could wedge permanently:
subprocess timeout (300s) killed only the outer shell, orphaning the
curl|bash grandchildren and the upstream installer's concurrent-install
lock (~/.cua-driver/packages/.install.lock.d). The installer only
reclaims a stale lock after 600s of waiting — longer than our old
ceiling — so every subsequent run was killed before recovery could
fire: 'always times out'.

- Run the installer in its own process group (start_new_session) and
  SIGKILL the whole group on timeout, so no lock-holding orphans survive.
- Pre-clear a provably-stale lock (dead holder pid, or pid-less and
  older than the upstream 600s window) before invoking the installer.
- Raise the ceiling to 660s (> upstream LOCK_STALE_AFTER_SECONDS=600).
- Timeout message now names the lock path and the manual re-run command.

Fixes #58762

* chore: suppress windows-footgun lint on platform-gated kill calls

Both sites are POSIX-only: _clear_stale_cua_install_lock early-returns
on win32, and os.killpg sits in the 'not is_windows' branch.
2026-07-05 03:16:06 -07:00
liuhao1024
d537d29a6f fix(computer-use): increase cua-driver session startup timeout from 15s to 30s
On Windows, the cua-driver MCP session initialization can exceed the 15s
timeout due to manifest subprocess discovery + MCP transport setup.
This makes the computer_use tool permanently unavailable even though
hermes computer-use doctor and hermes mcp test both pass.

- Increase _ready_event.wait timeout from 15s to 30s
- Add startup timing instrumentation (manifest + mcp_init durations)
- Log timing at INFO level for diagnosability

Fixes #57025
2026-07-05 03:15:49 -07:00
Sami Rusani
d8b51269ca fix(update): skip cua-driver refresh when Applications is unwritable 2026-07-05 03:15:06 -07:00
dsad
c13281ab57 Guard native image routing with file safety 2026-07-05 03:15:03 -07:00
teknium1
51c1ba6976 fix(agent): apply pool-level keepalive to the process_bootstrap sibling builder
The salvaged #54550 converted AIAgent._build_keepalive_http_client but the
near-identical build_keepalive_http_client in agent/process_bootstrap.py
(used by auxiliary clients: compression, vision, web_extract, titles) kept
the socket_options transport and the api.githubcopilot.com bypass. Same
conversion: httpx.Limits(keepalive_expiry=20) + pool timeouts, verify
forwarded on client and no-proxy mounts, copilot hardcode removed.
2026-07-05 03:14:55 -07:00
David Metcalfe
8324dd19ca fix(agent): replace custom socket_options transport with httpx pool-level keepalive expiry
The custom ``httpx.HTTPTransport(socket_options=[SO_KEEPALIVE, ...])``
in ``_build_keepalive_http_client()`` was introduced to fix CLOSE-WAIT
socket accumulation on long-lived connections (#10324).

That approach broke streaming for providers behind reverse proxies
(OpenResty, Cloudflare, etc.) because the custom socket options
conflict with the proxy's chunked-transfer handling (#54049, #12952).
It also stripped TCP_NODELAY, stalling TLS handshakes and SSE encoding.
Narrow per-provider bypasses were added for Copilot (#50298), Codex
(#36623, #12953), but the root cause remained.

The fix moves connection lifecycle management from the socket layer to
the HTTP pool layer:

- ``httpx.Limits(keepalive_expiry=20.0)`` tells httpx to close idle
  pooled connections at 20 s, before a reverse proxy's typical 30-60 s
  timeout drops them and causes CLOSE-WAIT accumulation.
- The default httpx transport preserves OS TCP defaults (including
  TCP_NODELAY), so TLS handshakes and SSE chunked encoding work
  correctly.
- ``trust_env=False`` prevents httpx from double-dipping on env vars
  (we handle proxy detection ourselves via ``_get_proxy_for_base_url``
  which respects NO_PROXY).
- The Copilot host bypass (line 3632) is no longer needed since all
  providers now use the same standard httpx.Client.

Closes #54049.  Supersedes #12010, #36623, #12953, #50298.
2026-07-05 03:14:55 -07:00
dsad
e02cef0d0d fix(memory): guard local uploads against credential reads 2026-07-05 03:14:49 -07:00
MorAlekss
d577408f3f fix(webhook): reject generic V2 signature missing timestamp instead of falling back to V1 2026-07-05 02:25:38 -07:00
Teknium
ebfc49c4d9
fix(approval): require exact ./.. segments in the root-collapse hardline token (#56179)
Follow-up to #56236: the broadened root token /[/.]*\** treats any run of
dots after the root slash as a collapse spelling, so a literal root-level
directory named '...' (rm -rf /...) was unconditionally hardline-blocked
with no approval path. Tighten the token to /(?:(?:\.\.?)?/)*(?:\.\.?)?\**
so each inter-slash segment must be exactly '.' or '..' — all real collapse
spellings (//, /., /./, /.., //*, ///, /../..) stay on the hardline floor
while literal dot-run dirs fall through to the softer DANGEROUS_PATTERNS
rules like every other real path.
2026-07-05 02:24:00 -07:00
Teknium
cb6c47af08
feat(approvals): /deny <reason> relays denial reason to the agent (port nanoclaw#2832) (#54518)
* feat(approvals): /deny <reason> relays denial reason to the agent

Port from qwibitai/nanoclaw#2832 (reject with reason).

Gateway /deny now accepts an optional trailing reason (/deny <reason>
or /deny all <reason>). The reason rides on the per-session approval
entry through resolve_gateway_approval -> _await_gateway_decision and is
appended to the BLOCKED tool result the agent receives, so a declined
agent can adapt instead of only hearing 'denied'.

Adapted to hermes-agent's synchronous single-command /deny model: no DB
state, no second-message capture step, no migration. Reason is capped at
280 chars and threaded through both the terminal-command guard and the
execute_code guard. Plain /deny and the approve paths are unchanged.

- tools/approval.py: _ApprovalEntry.reason; resolve_gateway_approval gains
  optional reason; _await_gateway_decision returns it; both gateway BLOCKED
  messages include it
- gateway/slash_commands.py: parse leading 'all' + trailing reason
- locales/en.yaml: deny.denied_reason_{singular,plural}
- hermes_cli/commands.py: /deny args_hint '[all] [reason]'
- tests: 3 new (with-reason, all+reason, plain-deny regression)

* fix(ci): localize deny-reason keys across all locales + update interrupt-path assertions

CI surfaced two enforced invariants broken by the deny-with-reason change:
- test_i18n catalog-parity requires every locale to carry the same keys as
  en.yaml with matching placeholders. Added deny.denied_reason_singular/plural
  (with {count}/{reason}) to all 15 non-English locales.
- test_approval_interrupt asserts the exact dict from _await_gateway_decision,
  which now carries a 'reason' key (None on the interrupt/timeout paths).
2026-07-05 02:22:08 -07:00
Teknium
9767e19b60
feat(skills): stacked slash-skill invocations — /skill-a /skill-b do XYZ (#57987)
Inspired by Claude Code v2.1.199 (July 2, 2026): stacked slash-skill
invocations load all leading skills (up to 5), not just the first.

- agent/skill_commands.py: split_stacked_skill_commands() consumes leading
  /skill tokens (stops at the first non-skill token so slash-path arguments
  are never swallowed); build_stacked_skill_invocation_message() composes
  the multi-skill turn reusing the existing bundle scaffolding markers so
  extract_user_instruction_from_skill_message() keeps memory providers
  storing the user's instruction, not N skill bodies.
- cli.py + gateway/run.py: dispatch the stacked path on both surfaces.
- 11 new tests + docs section in skills.md.
2026-07-05 02:20:01 -07:00
luyifan
30479961b8 fix(gateway): tolerate punctuation on silence markers 2026-07-05 02:12:26 -07:00
Teknium
edf8e0ba94
feat(mcp): surface MCP server log notifications in agent.log (#57416)
Port from anomalyco/opencode#34529: MCP servers can emit
notifications/message logging notifications (RFC 5424 levels), but the
MCP SDK's default logging_callback silently discards them — server-side
warnings/errors during tool calls were invisible.

- tools/mcp_tool.py: pass a logging_callback to every ClientSession
  (stdio, SSE, streamable HTTP old+new API paths via the shared
  sampling_kwargs sites), mapping the 8 MCP log levels onto Python
  logging levels and tagging entries with [server/logger] origin.
- JSON-serialize non-string payloads, cap at 2000 chars so a chatty
  server can't flood agent.log, never raise from the handler.
- Gated on SDK support (_check_logging_callback_support) mirroring the
  existing message_handler gate for old SDK versions.
- tests/tools/test_mcp_server_log_notifications.py: 10 tests covering
  level mapping, origin tagging, JSON payloads, truncation, and the
  never-raise contract.
2026-07-05 02:06:39 -07:00
Teknium
4751af0a0b
feat(errors): fail fast on TLS certificate verification failures with fix hints (#57992)
Inspired by Claude Code v2.1.199 (July 2, 2026): SSL certificate errors
(TLS-inspecting proxies, missing CA bundles, expired certs) no longer
burn retries before showing actionable guidance — they fail immediately
with the fix hint.

- agent/error_classifier.py: new FailoverReason.ssl_cert_verification +
  _SSL_CERT_VERIFY_PATTERNS, checked BEFORE the transient-SSL patterns
  (cert-verify messages also contain '[SSL:' and previously retried
  forever as timeout). Non-retryable, no compression, no fallback churn.
- agent/conversation_loop.py: dedicated status line + per-cause fix
  hints (corporate proxy CA bundle, certifi refresh, self-signed local
  endpoints) on the non-retryable abort path.
- 7 new tests incl. regression guards (transient alerts still retry,
  large-session cert failure doesn't trigger compression).
2026-07-05 02:05:51 -07:00
teknium1
619db0175d fix(gateway): move PATH bootstrap below imports, gate to POSIX
Follow-up to #3850's cherry-pick: keeps the fix but avoids the mid-import
E402 wart and skips the POSIX system dirs on Windows.
2026-07-05 02:05:23 -07:00
Kevin Rajaram
1b7853d7bc fix(gateway): add system dirs to PATH for UV Python compatibility
UV's bundled Python ships a minimal PATH that excludes /bin and /usr/bin,
causing launchctl/systemctl subprocess calls to fail with FileNotFoundError.

Fixes #3849
2026-07-05 02:05:23 -07:00
teknium1
708b57e009 fix(webhook): rate-limit V1 deprecation warning + document V2 signature
- warn once per route instead of on every request (busy senders would
  spam the log)
- document X-Webhook-Signature-V2 / X-Webhook-Timestamp in the webhooks
  user guide

Follow-ups for salvaged #58461.
2026-07-05 01:36:53 -07:00
MorAlekss
70449a4939 fix(security): add timestamp-bound V2 signature for generic webhook replay protection 2026-07-05 01:36:53 -07:00
teknium1
dec4485d2f chore(release): AUTHOR_MAP entries for salvaged PR authors 2026-07-05 00:59:35 -07:00
teknium1
7e037e1a30 fix: cover remaining GNU-only %-d strftime site in learning graph render
format_date() at line 76 still used %-d, which raises ValueError on
Windows strftime. Same class as the axis-label site fixed by #56640;
use dt.day directly. Credit also to @x7peeps (#58480) who flagged both
sites.
2026-07-05 00:59:35 -07:00
wyuebei-cloud
ce82b0c3cf fix: hermes journey crashes on Windows due to %-d strftime directive
`_period_label()` in `learning_graph_render.py` used `%-d %b`
strftime, which is a Linux-only format — the `%-` prefix for
zero-padding suppression doesn't exist on Windows `strftime`, causing
`ValueError: Invalid format string` on `hermes journey`.

Fix by using `dt.day` directly (an integer, no zero-padding by
default) combined with `strftime('%b')` for the month. This is
cross-platform and produces identical output.

Reproduced and tested on Windows 10 with Hermes v0.18.0.
2026-07-05 00:59:35 -07:00
teknium1
e4da3a7a52 chore(release): AUTHOR_MAP entry for salvaged PR author 2026-07-05 00:55:51 -07:00
yoma
791583704b fix(auth): prune stale custom model credentials 2026-07-05 00:55:51 -07:00
Lord_dubious
fc18d15f40 fix: preserve static custom provider models 2026-07-05 00:55:51 -07:00
teknium1
020a71678c chore(release): AUTHOR_MAP entries for salvaged PR authors 2026-07-05 00:48:50 -07:00