Commit Graph

33922 Commits

Author SHA1 Message Date
teknium1 477e8a631a chore: contributor mapping for bixycler 2026-09-12 08:25:54 -07:00
bixycler 70d0f556d7 fix(branding): use the Caduceus ☤ (U+2624), not the Rod of Asclepius ⚕ (U+2625)
Every inline glyph — CLI banner/status bar/response labels/goodbye, setup
and doctor boxes, gateway update prompts, WhatsApp reply prefix, TUI theme,
locale strings and the docs — used ⚕, the staff of Asclepius (medicine).
Hermes carries the Caduceus ☤. The ASCII-art logo was already correct.

Mechanical swap across 60 files (no logic change); both glyphs are
East-Asian-width Neutral so no layout shifts. Skins that set their own
`response_label` / `goodbye` are unaffected.

Direction from PR #7064 (@bixycler), the earliest of #7064 / #9611 / #15574,
redone against current main.

Fixes #9565
2026-09-12 08:25:54 -07:00
teknium1 535fd88c70 fix(backup): keep durable cache/ artifacts (images, citation ledger) in full backups
Excluding cache/ wholesale at profile roots dropped media the gateway
delivered to or received from the user (cache/images, audio, videos,
documents, screenshots) and the grounded-citations evidence ledger
(cache/citations/ledger.json) — none of which can be regenerated.
Prune only the regenerable cache/<x> subtrees; keep those six.
2026-09-12 08:25:49 -07:00
teknium1 b97ae5c9b8 fix(backup): make the socket test runner-safe and document the new exclusions
The picked test bound an AF_UNIX socket at pytest's tmp_path, which overflows
the ~108-byte sun_path limit under scripts/run_tests.sh's deep temp root
("AF_UNIX path too long"). Bind by a relative name from inside the temp
HERMES_HOME instead; the walker still sees the same absolute entry.

Also list cache/ + runtime roots and non-regular entries in the `hermes backup`
"What's excluded" docs so the user-visible behaviour change is documented.
2026-09-12 08:25:49 -07:00
teknium1 428cdbff52 chore: map contributor email mrwanstudio@gmail.com -> mrwanstudio 2026-09-12 08:25:49 -07:00
mrwanstudio 947e027f61 fix(backup): skip non-regular filesystem entries 2026-09-12 08:25:49 -07:00
Brad Estes 3b3f354933 fix(backup): exclude profile caches from full backups 2026-09-12 08:25:49 -07:00
teknium1 10864f394d fix(agent): stream_options compatibility retry does not consume the transient budget
Review finding on #109143: `_handle_stream_error` returned True for the
stream_options rejection without checking whether `_call` had another
iteration. With HERMES_STREAM_RETRIES=0, or after the transient budget was
spent, the loop ended with neither a response nor an error set and the call
returned None instead of raising.

The compatibility retry now extends the loop by one attempt exactly once
(`_compat_retries`); the transient budget is untouched. Test pinned with
HERMES_STREAM_RETRIES=0 (red on the previous head).
2026-09-12 08:25:40 -07:00
DavidMetcalfe 6527af2286 fix(agent): retry once without stream_options when an endpoint rejects it (HTTP 400/422)
Azure AI Foundry serverless (MaaS) endpoints validate the request body
strictly and reject `stream_options: {"include_usage": true}` with 422
`extra_forbidden`. Hermes sent the field on every streaming call, so the
agent was unusable against that endpoint family and the fallback chain
failed too.

When a 400/422 names `stream_options` as an extra/unsupported field and no
delta has been delivered yet, re-open the stream without the field and
remember the rejection on the agent (`_stream_options_unsupported`) so later
turns skip it up front. Streaming itself stays on — this is not the
"stream not supported" case. Usage accounting for such endpoints falls back
to the estimator, as it already does for native Gemini.

Salvage of PR #53271 by @DavidMetcalfe, reshaped onto the `_StreamingCall`
retry loop with per-agent state instead of a process-wide host set; one
end-to-end invariant test through `_interruptible_streaming_api_call`.

Fixes #9705
2026-09-12 08:25:40 -07:00
teknium1 7817af2a16 fix(claw): detect the gateway's process title openclaw-gateway
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.
2026-09-12 08:25:36 -07:00
teknium1 a340710389 chore(tests): explicit utf-8 encoding in test_skill_commands (windows-footgun ratchet) 2026-09-12 08:25:36 -07:00
teknium1 392c6221a3 chore(contributors): map zjc-enigma (salvage #12522) 2026-09-12 08:25:36 -07:00
enigma 036b3ff5ba fix(skills): allow non-ASCII characters in skill command names (#12351)
The _SKILL_INVALID_CHARS regex stripped all non-ASCII characters from
skill names, so skills with CJK, Cyrillic, or other Unicode names
(e.g. "小说拆条") produced an empty slug and were silently skipped.

Change the regex from [^a-z0-9-] to [\w-] so Unicode word characters
are preserved. Platform-specific sanitizers (Telegram, Discord) already
handle their own character restrictions downstream.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-12 08:25:36 -07:00
teknium1 215a820ace chore(tests): encoding="utf-8" on read_text/write_text in test_subdirectory_hints.py
Windows-footgun ratchet for the file touched by this fix (no behaviour change).
2026-09-12 08:25:25 -07:00
teknium1 5148b76677 fix(agent): skip_context_files also disables subdirectory hint injection
`skip_context_files=True` kept AGENTS.md/CLAUDE.md out of the system prompt,
but `SubdirectoryHintTracker` was always on, so the first tool call touching
a directory with such a file spliced its full text onto the tool result.
Cron jobs without a workdir (which set skip_context_files) that relay exact
stdout then delivered `[Subdirectory context discovered: ...]` plus the file
body to Telegram/Discord.

The tracker now takes `enabled=` and agent init wires it to
`not skip_context_files`: one flag, both injection paths. Interactive
sessions and cron jobs with a workdir are unchanged.

Direction proposed in PR #9434 (@zhitiao), which gated on platform == "cron";
gating on the existing skip flag covers the same case without a platform
special-case.

Fixes #9441
2026-09-12 08:25:25 -07:00
teknium1 330004e597 fix(agent): keep trajectory serialization inside the best-effort handler
A non-JSON value used to log 'Failed to save trajectory' and return; moving
json.dumps above the try made it raise to the caller.
2026-09-12 08:25:14 -07:00
teknium1 9c614484b3 chore(contributors): map shafdev (salvage #12685) 2026-09-12 08:25:14 -07:00
shafdev bd70d7387a fix(agent): lock save_trajectory() appends so concurrent processes cannot corrupt the JSONL
Gateway sessions and batch workers append to the same default
trajectory_samples.jsonl / failed_trajectories.jsonl with a plain open("a") +
write(); concurrent writers interleaved mid-object and the file stopped
parsing (#12684). The append now holds an exclusive lock for write+flush:
flock on POSIX, a 1-byte msvcrt.locking range on Windows.

Tests: a foreign process holding the lock must block the append (red on
base); six processes appending oversized entries all land parseable.

Fixes #12684. Salvaged from #12685 by @shafdev; Windows arm and test trim ours.
2026-09-12 08:25:14 -07:00
nightq d131391ff9 fix(feishu): stop markdown-escaping inbound post text
`_render_text_element` ran every inbound post text element through
`_escape_markdown_text`, so a user's `` `print('hi')` `` reached the model as
`` \`print\('hi'\)\` ``. The agent echoed those backslashes in its reply and
Feishu's `md` renderer then showed literal `\*\*bold\*\*` instead of bold.

Post elements carry raw text plus separate style flags; the style wrappers
already rebuild the markdown, so the text itself is passed through untouched.
The outbound path never escaped. Mentions/emoji labels keep their escaping.

Salvage of PR #9991 by @nightq onto `plugins/platforms/feishu/adapter.py`.

Fixes #9816
2026-09-12 08:25:09 -07:00
teknium1 5fd8f02e3c fix(wecom): URL-decode aeskey and store inbound images with an image MIME
Review findings on #109139 (the "aeskey half is already on main" claim was
wrong): `_decrypt_file_bytes` restored base64 padding but the key was never
percent-decoded, so `%2F`/`%3D` keys failed to decrypt and `_cache_media`
returned None. And `_store_media` received the CDN's `application/octet-stream`
as the image MIME, which the gateway image classifier rejects even with the
corrected `.png` name.

Decode the key once at the payload boundary; for images forward the response
MIME only when it is `image/*`, otherwise derive it from the resolved
extension. Covered by one end-to-end test through `_cache_media` (red on the
previous head).
2026-09-12 08:24:55 -07:00
nightq f39680de45 fix(wecom): inbound images cached with their real extension, not .bin
WeCom's CDN returns `content-type: application/octet-stream` for images.
`mimetypes.guess_extension` maps that to ".bin", a truthy value that
short-circuited `_guess_extension()` before the magic-byte detector ran, so
every inbound image was cached as `img_*.bin`.

Treat ".bin" as "unknown" so the URL suffix / magic-byte fallback decides.
The second half of the report (URL-encoded, unpadded `aeskey`) is already
handled on main by `_decrypt_file_bytes`.

Salvage of PR #10187 by @nightq onto `plugins/platforms/wecom/media.py`.

Fixes #10085
2026-09-12 08:24:55 -07:00
teknium1 2b685f08fa fix(claw): detect the gateway's process title openclaw-gateway
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.
2026-09-12 08:24:52 -07:00
teknium1 07ead2249b chore(tests): explicit utf-8 encoding in test_claw (windows-footgun ratchet) 2026-09-12 08:24:52 -07:00
Drexuxux 45dd97a6f7 fix(claw): cleanup no longer mistakes any "openclaw" in argv for a running daemon
`_detect_openclaw_processes()` ran `pgrep -f openclaw`, which matches every
process whose command line contains the word: an editor open on
~/.openclaw/config.json, `tail -f openclaw.log`, even the checking shell.
`hermes claw cleanup` then warned "OpenClaw is still running" and aborted on
idle hosts (#12648).

POSIX detection now mirrors the Windows branch: exact binary names
(`pgrep -x openclaw`, `pgrep -x clawd`) plus node interpreters whose script
argv names openclaw/clawd (anchored ERE), deduplicated into one report.

Fixes #12648. Exact-name approach from #24121 by @Drexuxux, re-applied onto
the current `_posix_probe` helper.

Co-authored-by: Drexuxux <Drexuxux@users.noreply.github.com>
2026-09-12 08:24:52 -07:00
teknium1 d267bc7f78 fix(model-switch): provider:model resolves like provider/model off aggregators too
Step c converted `vendor:model` to `vendor/model` only while the current
provider was an aggregator. On a direct provider (`alibaba`),
`/model Alibaba:qwen3.6-plus` skipped the conversion and went into the
catalog lookup as an unknown id, while `Alibaba/qwen3.6-plus` worked.

Convert on any provider when the left side names a provider Hermes knows
(built-in id/alias or a configured `providers:` entry). Ollama-style tags
(`qwen3.5:4b`) have no provider on the left and stay intact; aggregators
keep the unconditional conversion.

Fixes #9748
2026-09-12 08:24:42 -07:00
zhao c5cdd92254 fix(copilot): validate supported token prefixes 2026-09-12 08:24:29 -07:00
teknium1 fd15cd003e chore(tests): encoding="utf-8" on read_text/write_text in test_personality_single_owner.py
Windows-footgun ratchet for the file touched by this fix (no behaviour change).
2026-09-12 08:24:26 -07:00
teknium1 44ce128a27 fix(personality): honour the top-level personalities: config block on every surface
DEFAULT_CONFIG ships a root-level `personalities: {}` (from #643) and the schema
whitelists it, but the single personality resolver read only
`agent.personalities`. A user who followed the generated config saw
"No personalities configured" from /personality on CLI, gateway and TUI.

`available_personalities()` now merges root `personalities` then
`agent.personalities` (later wins), so all three consumers pick both up.
Earlier attempt: PR #9657 (@flobo3) patched the CLI loader only.

Fixes #9636
2026-09-12 08:24:26 -07:00
teknium1 5685b76fde fix(doctor): image_gen hint covers a selected provider with a missing key or SDK
Review finding on #109136: "no provider configured" was wrong when a provider IS
selected but its SDK/key is absent. Word it as unavailable + where to look.
2026-09-12 08:24:11 -07:00
teknium1 6ea3b00e7f chore(tests): encoding="utf-8" on read_text/write_text in test_doctor.py
Windows-footgun ratchet for the file touched by this fix (no behaviour change).
2026-09-12 08:24:11 -07:00
skyc1e 7d25ae58a7 fix(doctor): image_gen reports "no provider configured", not "system dependency not met"
image_gen has several setup paths (FAL_KEY, managed Nous image generation,
plugin providers) so it declares no single `requires_env`; doctor's generic
branch then labelled a missing credential a "system dependency not met" and
left it out of the "run hermes setup" summary.

A small per-toolset setup-hint table: image_gen gets an actionable line
pointing at `hermes tools`, counts toward the setup summary, and toolsets
with a genuine system dependency (homeassistant) keep the old wording.

Port of PR #9548 by @skyc1e onto `hermes_cli/doctor_tools.py`.

Fixes #9516
2026-09-12 08:24:11 -07:00
teknium1 1dce69be4e chore: contributor mapping for ggascoigne 2026-09-12 08:23:58 -07:00
teknium1 81cec06c77 test(cli): banner hero line is not center-padded
Invariant test for #9879 (red on origin/main: Rich inserted centering spaces
before the braille-padded hero). Adapted from PR #9880's test to the current
banner internals.
2026-09-12 08:23:58 -07:00
Guy Gascoigne-Piggford e38d63b7f0 fix(cli): left-align banner hero art 2026-09-12 08:23:58 -07:00
Season 6d20c321d6 fix(gateway): enable linger on systemd user-service repair path (#12863)
The repair branch in `systemd_install()` exits as soon as it rewrites an
outdated unit and re-runs `systemctl enable`, bypassing
`_ensure_linger_enabled()`. On headless Linux the command reports
success, but the repaired user service still stops at logout.

Call `_ensure_linger_enabled()` before the early return when the install
is user-scoped, mirroring what the fresh-install path already does.

Adds two regression tests in `tests/hermes_cli/test_gateway_linger.py`:
- repair path (user scope) calls the linger helper
- repair path (system scope) does not call it
2026-09-12 08:23:44 -07:00
ygd58 5be0c4921c fix(gateway): surface launchctl bootstrap failures in refresh_launchd_plist_if_needed
Ports #63762 forward onto current main per teknium1's review.

refresh_launchd_plist_if_needed() logged the retry failure but still
returned True and printed success. launchd_install() then
unconditionally printed '✓ Service definition updated' even when the
service was not registered with launchd (#12882).

1. refresh_launchd_plist_if_needed(): return False after retry
   exhaustion so callers can distinguish failure from success.
2. launchd_install(): check the bool; on False print a warning instead
   of the success message.

Per review: the warning now renders the reload-log location via
display_hermes_home() (the existing lazy-import convention used
elsewhere in this module for user-facing paths, e.g. the gateway.log
path prints a few lines away) instead of a hardcoded ~/.hermes path,
so named/custom Hermes home profiles show the correct location.

Existing _retry_launchctl_bootstrap_until_registered() retry/EIO/
timeout/verify logic unchanged.

5/5 tests pass (4 ported + 1 new for the display_hermes_home fix).
2026-09-12 08:23:44 -07:00
HeLLGURD 9d09598111 fix(display): tool previews never exceed a tiny max_len (1-3)
`build_tool_preview()`'s generic-key fallback and the cute-message helpers
still truncated with a bare `text[:max_len - 3] + "..."`; for max_len 1-3 the
slice goes negative and returns almost the whole string (27 chars for
max_len=1). `_truncate_preview` already had the guard, so the two code paths
disagreed.

One truncation helper (`_tail_trunc`) with the guard, used everywhere; the
head-truncating `_cute_path` gets the same clamp.

Salvage of PR #48483 by @HeLLGURD (current-code fix); the earliest reports and
patches were #9464 (@LarHope), #9477 (@kagura-agent) and #9497.

Co-authored-by: LarHope <12761142+LarHope@users.noreply.github.com>
Fixes #9439
2026-09-12 08:23:42 -07:00
teknium1 dfa84e4a84 chore: map contributor email d.hermsmeier@mittwald.de -> Hermsi1337 2026-09-12 08:23:28 -07:00
teknium1 02b398bda5 fix(mcp): recovered application errors keep the breaker strike
The pick returns the real result after a transport recovery instead of
dropping it, but it also skipped the breaker bookkeeping. Application
errors counting as strikes is the point of the breaker (3ff18ffe14,
#10447: a server answering errors made the model hammer it 8x in 10s).
Route the recovered result through _record_call_outcome so the caller
sees the tool's answer and the counter still moves the right way.
2026-09-12 08:23:28 -07:00
Dennis Hermsmeier 5dca47a651 fix(mcp): preserve application results after transport recovery 2026-09-12 08:23:28 -07:00
teknium1 b5b697e91d chore: contributor mapping for MonkeyLeeT 2026-09-12 08:23:13 -07:00
MonkeyLeeT 289e0d506f fix(insights): count tool usage per session before merging the two sources
`_get_tool_usage()` merged `tool_name` rows and assistant `tool_calls` JSON with
a GLOBAL per-tool max. That is right inside one session (both columns describe
the same call) but wrong across sessions: a gateway session recording
`tool_name` only plus a CLI session recording `tool_calls` only for the same
tool reported 1 use instead of 2.

Group both queries by (session_id, tool_name), reconcile with max per session,
then sum across sessions.

Port of PR #9896 by @MonkeyLeeT onto the `_scoped` query layout; one invariant
test covering disjoint sessions AND a paired session.

Fixes #9814
2026-09-12 08:23:13 -07:00
teknium1 befa5735e9 chore(tests): encoding="utf-8" on read_text/write_text in test_memory_provider.py
Windows-footgun ratchet for the file touched by this fix (no behaviour change).
2026-09-12 08:22:57 -07:00
teknium1 f8f16b5e7f chore: contributor mapping for zhouhe-xydt 2026-09-12 08:22:57 -07:00
zhouhe-xydt 5084237f8e fix(memory): load provider schemas before mutating MemoryManager state
`add_provider()` flipped `_has_external` and appended the provider before
calling `get_tool_schemas()`. When schema loading raised, the broken provider
stayed registered and the single-external slot was poisoned for the rest of
the process: every later provider was rejected as "already registered".

Materialize the schema list first; state changes only after it succeeds.
Exception propagation is unchanged.

Hand-port of PR #9997 by @zhouhe-xydt onto the current add_provider() (the
reserved-core-tool filter landed in between); one invariant test.

Fixes #9948
2026-09-12 08:22:57 -07:00
teknium1 eca2cea5c6 chore(tests): encoding="utf-8" on read_text/write_text in test_mirror.py
Windows-footgun ratchet for the file touched by this fix (no behaviour change).
2026-09-12 08:22:42 -07:00
teknium1 6716f22396 fix(gateway): mirror_to_session reports False when the transcript write fails
`_append_to_sqlite` caught and debug-logged its own exceptions, so the outer
handler in `mirror_to_session` never fired and every failed SQLite write was
reported as a successful mirror. Callers (cron in_channel seed, send_message)
had no way to know the transcript was never updated.

Let the write helper raise; the caller already warns and returns False.

Fixes #10130
2026-09-12 08:22:42 -07:00
teknium1 66884c4397 fix(cron): honour job workdir on the inline _build_job_prompt script path too
Same bug class as the wake-gate call fixed in the picked commit: when a caller
invokes _build_job_prompt without a cached prerun_script, the sibling ran the
job's script inline via _run_job_script(script_path) and dropped the job's
configured workdir, so the script ran from the scripts-dir parent. Route it
through _resolve_job_workdir like the no_agent and wake-gate sites already do.

Sweep of every _run_job_script / _run_job_script_with_claim_heartbeat caller:
_run_no_agent_job and cron/monitor.py already pass workdir; the wake-gate site
is fixed by the salvaged commit; this was the last one.
2026-09-12 08:21:08 -07:00
teknium1 64edb8a9ce chore: map contributor email ycy1990@gmail.com -> metricano 2026-09-12 08:21:08 -07:00
Yong Chang Yi 62b0235d45 fix(cron): pass workdir to agent pre-run scripts 2026-09-12 08:21:08 -07:00