Iteration-limit summaries retry once when the first attempt comes back empty.
Both attempts share one builder closure, so assert the retry body is as
tool-free as the first: forces an empty summary, then checks every captured
codex request for tools / tool_choice / parallel_tool_calls.
Addresses the retry-coverage request on #32777.
Application errors (isError payloads) keep counting as breaker strikes:
that is #10447's point (a server answering errors made the model hammer
it 8x in 10s) and #109180 just reasserted it. What #11113 actually hit is
the open-breaker MESSAGE: after three rejected fetches the model was told
the server was "unreachable" and went to the user instead of fixing its
URL. Track whether the streak was all application errors and word the
pause accordingly; one transport strike restores the unreachable text.
- PUT /api/messaging/platforms on a pooled `hermes --profile X serve` arrives without
?profile= (Desktop local topology, #109088): resolve the hot-serve target from the
process's own profile so the multiplexer is pinged and the UI skips the restart banner.
- A profile deleted while the reconcile lock was held by its own adapter connect was
recorded back into served_profiles; re-check the live set before recording.
- Drop a deleted profile's `<name>:<platform>` runtime-status entries instead of leaving
them as `stopped`.
A `gateway.multiplex_profiles` gateway enumerated `profiles/` once at boot, so a profile
created afterwards (CLI, dashboard, Desktop, TUI) was never served until `hermes gateway
restart`; Desktop and the dashboard gave no reminder, so a new profile's bot simply never
connected.
The served set is now reconciled at runtime (`gateway/run_profile_reconcile.py`):
- `hermes_cli/profiles.py` create/delete ping the multiplexer over its control socket
(new `rescan-profiles` verb); a supervised watcher rescans every 30s as the safety net.
- A new profile gets its adapters under its own runtime scope from its config/.env
(`_start_one_profile_adapters`, same duplicate-credential guard as boot, now seeded
with the LIVE secondaries' claims), `served_profiles` in gateway_state.json is
updated, MCP discovery + log routing run for it. Other profiles' adapters are never
touched.
- A served profile whose config.yaml/.env changed is re-scanned so a token added after
create builds the adapter; already-live/queued platforms are skipped (no second poller).
- A deleted profile (tombstone) has its reconnects cancelled, adapters torn down,
pairing/busy bookkeeping and cached agents dropped, and this process's SQLite /
memory-store handles released so the deleter's rmtree succeeds.
- The in-process cron ticker takes a live enumerator so new profiles' jobs fire.
- PUT /api/messaging/platforms/<id>?profile=X returns `hot_served` when a live
multiplexer rebuilt X's adapters; Desktop/dashboard skip the restart banner then.
- `hermes profile create` confirms hot-serve; the restart reminder stays for a gateway
that did not pick the profile up (older build / signal failed).
The profiles guide promised "fresh sessions and memory" while
_CLONE_SUBDIR_FILES deliberately copies memories/MEMORY.md and USER.md
(curated identity, same tier as SOUL.md). State the real behaviour and how
to get a blank memory, so #10376's first half stops surprising people.
Refs #10376
Drop the categorical 'missing any step always means 200340' and
'200672/673 necessarily is an adapter bug' claims; the Interactive Card
toggle's role is unverified. Bring the Chinese guide in line (it still
pointed at the Event tab). Review follow-up.
Six issues (#10251, #10073, #13924, #38305, #25886, #8246) and four PRs
proposed json.dumps()-ing CallBackCard.data. The lark_oapi SDK types
`data` as Any and marshals the whole response with JSON.marshal(), so a
dict already reaches Feishu as the nested object the card-callback doc
shows in its response example; a string would nest a JSON *string* there
instead. Feishu documents 200340 as "the application has not configured
the card callback address or the configured request address is invalid"
— the click is rejected before delivery (nothing reaches Hermes), which
matches the reports (no gateway log, all four buttons identical).
The docs told users to subscribe to `card.action.trigger` under Event
Subscriptions; it lives under the separate Callback Configuration tab and
needs its own long-connection/URL mode and a published app version.
Spell out the four steps and map the sibling codes (200342/200343 =
unreachable URL; 200672/200673 = our payload).
Refs #10251
The check called the registry check_fn inside the updater's own process,
whose import caches predate the install just performed (and which may be
the outer Python entirely), so a healthy freshly installed SDK produced a
false "will fail to load" warning. Run the same registry check in the
target interpreter via the existing _venv_probe path used by the core
dependency verifier.
Found by independent review before merge.
When `.[all]` fails and the per-extra fallback also fails for e.g.
`feishu`, the update printed only "Skipped optional extras that still
failed" and finished green. The running gateway kept its already-imported
modules, so the loss surfaced hours later as "No adapter available for
feishu" on the next restart (#10651).
After the fallback, check every enabled+configured platform through its
registry `check_fn` (and MCP when `mcp_servers` is set) and print which
configured feature will fail to load, with its install hint. Unconfigured
extras stay a quiet skipped line.
Reworks PR #10733 (LeonSGP43) against the registry instead of a
hand-written platform->module table so plugin platforms are covered.
Fixes#10651
Co-authored-by: LeonSGP43 <cine.dreamer.one@gmail.com>
Two regressions in the mirror support: (1) the canonical
https://openrouter.ai/api/v1 that `hermes setup` persists under
provider: openrouter was treated as a custom endpoint, dropping the
auth.json credential pool and returning an empty API key; (2) an
unrelated CUSTOM_BASE_URL (which outranks the config mirror) still
received OPENROUTER_API_KEY because key selection tested mirror
eligibility, not the endpoint actually selected. A config URL is a
mirror only when its host is not openrouter.ai, and the mirror key
branch fires only when base_url is the config URL.
Found by independent review before merge.
When config.yaml sets `model.provider: openrouter` together with a
`model.base_url` mirror/proxy, an explicit `--provider openrouter`
request ignored the mirror and sent traffic to the public OpenRouter
endpoint: the config base_url was only trusted for auto/custom, the
credential pool was still consulted (so a pooled key won over the
mirror), and even when the mirror URL was used its host failed the
openrouter.ai match so OPENROUTER_API_KEY was not selected for it.
Trust the config base_url for the explicit openrouter case, treat that
mirror as an OpenRouter context for key selection, and bypass the pool
like the other custom-endpoint cases already do.
Fixes#10622
send_document() accepted reply_to but never passed it down, so attachments
always threaded from the cached per-address context (or not at all) even
when the caller named the message to reply to. The plain-text path
(_send_email) already honored it; the attachment path now does too.
The metadata half of #10131 (send_image rejecting metadata=) was already
fixed on main by the adapter parity pass. Diagnosis from #10131 and the
explicit-reply_to-wins shape from PR #10321 (which targeted the
pre-plugin path).
Fixes#10131
Co-authored-by: LeonSGP43 <cine.dreamer.one@gmail.com>
Every start (and every `ensure_hermes_home` from a sibling CLI invocation)
chmod'd the data directory to 0700, wiping group/other bits and the ACL
mask on a bind mount shared with other containers (hermes-webui, Nix
desktop + dashboard). _secure_file already skipped containers for this
reason; _secure_dir did not.
In a container the directory mode is now left to the operator unless
HERMES_HOME_MODE is set explicitly, which is still applied. cron/jobs.py
had its own 0700/0600 copies that bypassed the managed/container rules;
they now delegate to the shared helpers so cron/output stops re-locking
the mount as well.
Fixes#10757
A second Ctrl+C while the atexit hook joins the browser janitor thread
surfaced as "Exception ignored in atexit callback: _stop_browser_cleanup_thread"
with a KeyboardInterrupt traceback. The janitor is a daemon thread and the
interpreter is already exiting, so nothing is lost by swallowing the
interrupt — the terminal tool's sibling _stop_cleanup_thread already does.
Salvaged from #10765 (function has since moved to browser_tool_lifecycle.py).
Fixes#10764
Co-authored-by: LehaoLin <lehaolin98@outlook.com>
A tool batch that is cancelled or errors before any tool.started event
left the tool name in _tool_gen_announced, muting the next invocation's
'preparing <tool>…' line. Reset it in _reset_stream_state alongside the
other per-invocation state. Found by independent review before merge.
The tool-gen callback fires once per tool CALL, so a model issuing three
parallel terminal calls printed three identical status lines before the
spinner took over (#10478). Coalesce repeats of the same tool name until a
tool actually starts (tool.started), which marks the next generation batch.
The line itself stays: it is the only feedback while a large argument
payload (e.g. a 45 KB write_file) streams, before the spinner exists.
PR #10598 removed it entirely; this keeps the first line and drops only
the duplicates.
Live A/B (in-process HTTP mock streaming 3 parallel terminal calls into a
real HermesCLI with display.streaming on): before 3 lines, after 1.
Fixes#10478
A root-level `webhook:` block (the pre-`platforms:` spelling, still
supported by platform_section) is never copied into platforms_data, so
the removed _PORT_BRIDGE_KEYS table was its only route to `extra` and
the previous commit regressed it (port fell back to 8644). Bridge every
non-typed key of a root block in _bridged_keys with the same typed-key
exclusion and explicit-extra precedence as PlatformConfig.from_dict.
Found by independent review before merge.
`platforms.webhook.port: 9100` (and `routes`, `secret`, api_server `key`/
`cors_origins`, any adapter setting) was silently dropped unless nested
under `extra:` — PlatformConfig.from_dict only read a fixed set of typed
fields. Two partial bridges (a per-platform port/host/secret table in the
loader and an api_server-only block) covered a few keys and had to be
extended for every new one.
from_dict now promotes every non-typed top-level key into `extra`, with an
explicit `extra:` value winning on a clash and typed fields never leaking
into `extra` on a to_dict/from_dict roundtrip. Both hand-written bridges
are removed.
Same direction as PRs #10208/#10211/#10453 (rainow's #10206 diagnosis) and
#20506; those targeted the pre-loader layout.
Fixes#10206
The package-manager uninstall patterns were bare \b-anchored, so quoted
prose (`git commit -m "document npm uninstall usage"`) prompted while the
real `npm --prefix DIR uninstall x` slipped past because the option group
did not allow an operand. Use the file's _CMDPOS anchor like every other
command-name rule and let each global option take one operand.
Found by independent review before merge.
`npm uninstall -g`, pnpm/yarn remove, `pip uninstall` and `brew uninstall`
remove software outside the project yet matched no dangerous-command
pattern, so the agent ran them without asking (#10199). Add one
"package manager uninstall" rule per manager; installs and updates stay
unprompted.
Hand-ported from PR #64175 (the patterns moved from tools/approval.py to
tools/approval_detection.py after it was opened).
Previously, setting HERMES_MANAGED=false (or 0, no, off) would be
interpreted as a literal managed-system name, causing is_managed()
to incorrectly return True and block update/config commands.
- Add _MANAGED_FALSE_VALUES tuple for canonical false strings
- Check false values before true values in get_managed_system()
- Add parametrized regression tests for all false variants
Fixes#12864
Review finding: the salvaged rule assumed a lowercase-hex grammar that
AgentMail's docs do not establish (only the `am_` / `am_org_` prefix is
documented), so a non-hex key would have gone unmasked. Discriminate on
what actually separates keys from identifiers: an alphanumeric body with
no `_`/`-` and a 20-char floor. `am_example_identifier_123` still passes.
The pattern am_[A-Za-z0-9_-]{10,} was too broad and matched ordinary
identifiers like 'am_example_identifier' that happen to start with 'am_'.
Changed to am_[a-f0-9]{32,} which:
- Requires lowercase hex characters only (real AgentMail keys are hex)
- Requires 32+ characters after the prefix (reducing false positives)
FixesNousResearch/hermes-agent#10983
Review finding: the guard ran before `edit_message` was awaited; a restart
notice sent during that await followed by a failed edit produced a fresh
"Working" fallback bubble after the notice. Recheck before the fallback
send; the notifier ends instead.
The gateway's long-running notification task was sending "Still working...
messages even after a restart was requested, causing confusing UX where
users received a restart warning followed by normal heartbeat messages.
Added a check in _notify_long_running() to skip notifications when
gateway is draining or restart has been requested.
FixesNousResearch/hermes-agent#10990
Review finding: with multiline shortcuts on, a `\` + Enter arriving inside
a paste kept the literal backslash while a typed one removed it. The
continuation branch inserts a newline anyway, so it runs first and the
outcome no longer depends on timing.
Without bracketed paste (tmux strips it; IME/voice dictation emits keys
one at a time) each pasted newline reaches _tui_handle_enter as its own
Enter event, so a multi-line paste while the agent ran became N separate
steering messages. An Enter within 50 ms of the last buffer change is
text still arriving; insert the newline and let the buffer accumulate.
Humans type >50 ms apart, so a real submit is unaffected.
Same mechanism as #10997 (@iRonin) and #19498 (@Ralojeil1), which
targeted the removed classic-CLI handle_enter.
Refs #10994
Upstream sets process.title="openclaw-gateway" in the gateway run loop, so
the real daemon has comm "openclaw-gatewa" (15-char truncation) and no
`node … openclaw` argv for the script probe to match. Add the exact comm
probe; substring matching stays out.
The runtime already registers nothing for an explicit empty include list
(fb1ec36a4b), but every CLI reader still coerced `[]` to "no filter":
`hermes mcp list` printed "all", `hermes mcp configure` and `hermes tools`
pre-checked every tool (so confirming the picker silently re-enabled all of
them), and a catalog reinstall pre-checked the manifest defaults over the
user's zero-tool choice.
`_tool_filters` now returns the list whenever the key holds a list; only an
absent/non-list key is None. The pickers and list output branch on `is not
None`, matching `tools/mcp_tool_registration.py`.
Fixes#12865. Builds on #13096 (@dingn42) and #52874 (@Bartok9).
`isToolEnabled` used `include?.length`, so `tools.include: []` (the block-all
whitelist the runtime honours) rendered every tool as enabled, and toggling the
last include entry off deleted the key, silently flipping the server back to
"all tools". Keep the empty list.
Salvaged from #52874 (desktop portion) by @Bartok9. Part of #12865.
Review finding: `echo cd backend` injected backend/AGENTS.md, and the
rstrip(";") applied after shlex had removed quoting turned `cd 'backend;'`
into `backend`. Tokenize with punctuation_chars so operators are their own
tokens: a `cd` counts only at a segment start, `backend;ls` splits at the
operator, and a quoted `'backend;'` stays the literal name.
SubdirectoryHintTracker's generic token filter in
_extract_paths_from_command drops any token that does not contain /
or ., so relative directory names in commands like 'cd backend && ls'
were silently ignored. As a result, Hermes missed AGENTS.md /
CLAUDE.md / .cursorrules in the entered subdirectory whenever users
navigated with plain relative names -- the common case.
Add _extract_nav_command_targets, which scans the token stream for
'cd' / 'pushd' and treats the next non-flag token as a path candidate
resolved against working_dir. The generic token pass still runs so
all other shapes (absolute paths, files with extensions, etc.) keep
working. 'cd -' and bare 'cd' are intentionally skipped -- neither
points at a project subdirectory.
Three new tests cover 'cd backend && ls', 'pushd backend', and
'cd backend' appearing after an earlier chained sub-command.
Known limitation called out in review: multi-step chains like
'cd backend && cd src' still resolve each hop against working_dir
rather than simulating the shell's evolving cwd. That's a bigger
change (shell state tracking) and is out of scope for this fix;
the common single-hop case reported in the issue is now covered.
Fixes#11032
Review finding: with the child now created before the parent is ended,
child.started_at < parent.ended_at, so _BRANCH_CHILD_SQL's timestamp
fallback no longer classifies the fork as a branch child and the default
GET /api/sessions dropped it. Persist the explicit marker the CLI /branch
path already writes; test covers listing + the failed-fork parent survival.
Both paths ended the source session as "branched" before create_session
ran, so a failed create left the user on a session already marked ended
with no branch behind it. Create the child first; the parent is ended
only once the branch is real.
Salvage of #11048 (targeted the pre-split cli.py handler; ported to
hermes_cli/cli_commands_mixin.py and the api_server fork sibling);
authored by @vominh1919.
Refs #11030
Review finding: with the install's own venv activated (a re-run), `uv python
find <range>` returned venv/bin/python3, which setup_venv then deleted before
handing the dead path to `uv venv`; the stage still exited 0 and printed
"Virtual environment ready". Probe with --system so only base interpreters
qualify, and fail the stage when the venv was not created.
check_python only asked uv for 3.11 and, missing that, downloaded it —
a hard failure on hosts that cannot reach GitHub releases even when a
3.12/3.13 inside requires-python is already installed. Ask uv for the
supported range second and pin the venv onto that interpreter.
Same direction as #10824 (@gnanirahulnutakki), reduced to the shell
path; the PowerShell installer already has this fallback.
Refs #10778
The cap only fires on add/replace, so an externally written over-budget
file rode silently in the system prompt while every later add was refused
with no visible cause. Warn at load; entries stay loaded (never truncate
a user's memories).
Salvage of #10886 (original hunk targeted memory_tool.py before the
store split); authored by @easyvibecoding.
Refs #10877
test_loop_tick_witness_arms_over_tcp_on_windows swapped the module's `os`
binding for a proxy reporting name="nt" so a Linux interpreter would take
the non-POSIX branch. That is the "Don't fake the host OS" case: the test
only passes if the interpreter believes it is on another OS. Mark it
windows_only so the tests-os lane runs it on windows-latest for real;
scripts/ci/list_os_marked_tests.py picks the file up by marker name and
it is skipped (not errored) on Linux/macOS. The create=True tripwire from
the earlier salvage commit is what makes it runnable there.
The salvaged paragraph described one mechanism that is actually two.
Per developer.chrome.com/blog/remote-debugging-port, Chrome 136+ simply
stops respecting --remote-debugging-port/--remote-debugging-pipe against
the default data dir — no dialog, nothing listens. The "Allow remote
debugging?" prompt the contributor saw is the separate Chrome 144+ opt-in
approval flow (chrome://inspect/#remote-debugging), which asks per
incoming connection, not per launch. Say so, cite both sources, and keep
the fix (non-default --user-data-dir, or use_real_profile) unchanged.
Same site class as the e2e fixtures: tests-js/scripts/mock-server.ts writes
the config for `npm run dev:mock`. Entry-level providers.<name>.context_length
is honoured since #98387, so a 4096 window now fails agent init below
MINIMUM_CONTEXT_LENGTH exactly as it did for the Playwright fixtures.