Sync upstream Hermes v2026.6.28 (~1185 commits)#13
Open
tomi204 wants to merge 1186 commits into
Open
Conversation
Follow-up on the gateway-picker salvage: the cherry-picked change added a second copy of the MoA virtual-provider row in model_switch.py, duplicating inventory._moa_provider_row (same slug/name/preset-models, identical extra fields). Make _moa_provider_row take a bare current_provider string and reuse it from the gateway picker path so the row shape lives in one place and the two surfaces can't drift.
…ding it (#53564) The 'whatsapp' and 'signal' PLATFORM_HINTS told the agent 'Please do not use markdown as it does not render' — factually wrong. Both adapters actively convert markdown to native formatting: - whatsapp_common.format_message(): **bold**, ~~strike~~, # headers, links, code blocks -> WhatsApp native syntax - signal_format.markdown_to_signal(): same conversions via bodyRanges, plus '- item' / '* item' bullets -> '• ' Unicode bullets The wrong hint made the agent strip bullets and bold the adapter would have rendered (#12224). Rewrote both hints to mirror whatsapp_cloud: markdown is auto-converted, bullet lists work, tables are not supported. Added a contract test asserting markdown-converting platforms never forbid markdown in their hint.
The dashboard form is built from CONFIG_SCHEMA, which doesn't enumerate every root-level key the YAML supports. Most visibly, `custom_providers` is in `_KNOWN_ROOT_KEYS` but is absent from the schema — so the frontend never sends it in the PUT body. The previous full-replace save() then silently wiped the key from disk every time the user clicked anything that triggered a save. Other casualties (less visible because defaults re-mask them on load) include `agent.personalities`, `agent.reasoning_effort`, `terminal.lifetime_seconds`, etc. Fix: read the raw on-disk config and deep-merge the incoming PUT body on top of it before saving. The frontend can only overwrite what it explicitly sends; everything else is preserved verbatim. Reuses the existing `_deep_merge` helper from `hermes_cli.config`. Tests: - `test_round_trip_preserves_custom_providers` exercises the exact bug: seed config with custom_providers, GET → drop the key → PUT, assert it's still on disk. - `test_round_trip_preserves_schema_invisible_nested_keys` covers the shallow-vs-deep-merge case for nested dicts under `agent` etc. Both fail on current main; both pass with this patch.
…s, tighten (#53566) Refresh the hermes-agent skill against the last 5 major releases and the current codebase, and cut verbose prose. Coverage added (v0.13.0–v0.17.0): - New gateway platforms: iMessage (Photon), Teams, LINE, SimpleX, ntfy, Google Chat, Raft, official WhatsApp Business Cloud API (now 20+). - New surfaces section: desktop app, web dashboard admin panel, hermes proxy (OpenAI-compatible OAuth proxy), Automation Blueprints. - delegate_task(background=true) async subagents; memory-tool atomic batch operations; session_search three-mode shape; x_search/video_analyze toolsets; image_gen image-to-image; xAI Grok via SuperGrok OAuth. - display.interface (cli/tui), curator.consolidate opt-in, PyPI install. Accuracy fixes: - Adding-a-Tool is two files (auto-discovery), not three. - Testing uses scripts/run_tests.sh (canonical runner), not bare pytest. - Dropped change-detector test count and a dangling references/ pointer. - Refreshed overview (Windows-native, 20+ providers, many surfaces). Conciseness: trimmed over-explained Windows keybinding/sandbox/test prose and deep prompt-builder internals to pointers.
Lock the contract that a clean stream-queue termination followed by an agent failure never reports finish_reason: "stop". Covers the raised- exception case (#12422 repro), the flagged failed-result case, truncation (length), and the success happy path. Follow-up to the salvaged #12504 fix from @flobo3.
Salvage of #14261 by @ms-alan — rebased onto current main, scoped to the overloaded-classification fix, with a regression test that fails without it.
The curator_env fixture left async review threads (synchronous=False spawns a daemon 'curator-review' thread that calls save_state() on completion) running past test teardown. save_state() resolves the state path from HERMES_HOME at write time, so a straggler could write into the next test's tmp home, corrupting test_state_file_survives_corrupt_read (and others) under CI load. Join the thread on teardown while HERMES_HOME is still pinned to this test's home.
…707) (#53570) A profile's cron jobs now provably live in AND execute under that profile's HERMES_HOME. A job authored under profile `coder` is stored at `~/.hermes/profiles/coder/cron/jobs.json` and runs with coder's .env, config.yaml, scripts and skills — never the default root's. This was the de-facto behavior on main but only by accident: PR #50112 had re-anchored cron storage at the shared default root, and a later stale-branch squash merge (#52147) silently reverted it back to the profile home. Neither direction was guarded by a test, so it could flip again on the next stale merge. Changes: - cron/jobs.py: document the per-profile storage anchor (get_hermes_home, NOT get_default_hermes_root) and why anchoring at the root leaks config/credentials/skills across profiles — the #4707 security boundary. - cron/scheduler.py, cron/suggestions.py: same intent documented at the dynamic resolution helper and the suggestions store. - tests/cron/test_cron_profile_isolation.py: pin storage, lock-path, and execution-home resolution to the active profile so a re-anchor can't regress. Verified E2E: jobs created under two profiles land in separate per-profile stores with zero cross-profile leakage and no shared-root store; scheduler execution-home follows the active profile. Full cron suite: 576/576.
…age (#53569) The error raised when a model's context window is below the 64K minimum advertised "or set model.context_length in config.yaml to override" — but the guard intentionally has no sub-64K escape hatch. Sub-64K models are rejected by design (tool schemas + system prompt need the headroom). The misleading clause invited a cluster of dup PRs (#11097, #11110, #8962, #9142, #37548) all trying to wire an override that we don't want. Reword to state the real options: pick a >=64K model, or — if your local server under-reports its true window — declare the real value (which must itself be >=64K). Guard behavior is unchanged.
Regression tests for the self-author guard added in the salvaged fix: - bot-authored DM-topic watcher echo is dropped (the exact #11905 symptom) - bot self-messages dropped in groups/supergroups too - other bots in the same chat are still processed (self-id, not is_bot) - observe-unmentioned sibling path also rejects self-messages - missing from_user does not crash Test scaffolding ported from @cola-runner's PR #12817 and adapted to the current plugins/platforms/telegram/adapter.py and _is_own_message().
…zombie on /restart When request_restart() adds _run_restart to _background_tasks, _stop_impl later cancels all entries in that set. Since _run_restart is awaiting _stop_task at that point, the CancelledError propagates into _stop_impl, interrupting cleanup before _shutdown_event.set() and _exit_code = 75 execute. This leaves the gateway as a zombie (alive but disconnected) or exiting with code 0 instead of 75, preventing systemd Restart=on-failure from restarting the service. Fix: don't add _run_restart to _background_tasks — it self-terminates in ~50ms and needs no lifecycle management. Fixes #12875
…op skip Follow-up on the cherry-picked #13173 fix. Holds the _run_restart task in self._restart_task (a bare asyncio.create_task keeps only a weak reference, so a still-pending task can be GC'd mid-flight) and explicitly skips it in the _stop_impl cancel loop alongside _stop_task. Adds AUTHOR_MAP entry for the contributor and a regression test that fails when the task is cancellable. Refs #12875
TELEGRAM_HOME_CHANNEL set to an @username (not a numeric chat ID) crashed all webhook/cron->Telegram home-channel delivery with 'ValueError: invalid literal for int()'. The Telegram Bot API accepts both a numeric chat_id and an @username string; Hermes was force-coercing every chat_id with int(). Add normalize_telegram_chat_id() (returns int for numeric values, passes @username strings through) and apply it at the Bot API send/edit sites in the Telegram adapter and the send_message tool. Username targets are now recognized as explicit targets in _parse_target_ref. Reapplies the approach from #13274 (season179), whose branch predated the gateway/platforms/telegram.py -> plugins/platforms/telegram/adapter.py relocation. Dupes: #13535 (Tranquil-Flow), #37572 (chewkaah). Co-authored-by: season179 <season.saw@gmail.com>
The _is_ollama_glm_backend() function was too broad: any local endpoint running a GLM model was treated as Ollama, triggering the stop->length misreport heuristic introduced in 8011aa3. This caused false truncation detection on sglang, vLLM, LM Studio, and other non-Ollama servers that correctly report finish_reason. When a GLM model on sglang/vLLM returned finish_reason='stop', the agent mistakenly reclassified it as 'length' if the response didn't end with a whitelisted punctuation character (ASCII or CJK). This particularly affected Chinese-language responses and Markdown-formatted text. Root cause: the is_local_endpoint() fallback assumed any local GLM endpoint = Ollama. But many non-Ollama servers also run on localhost. Fix: remove the is_local_endpoint() catch-all. Only detect Ollama via its distinctive signatures (port 11434, 'ollama' in URL). All other local servers are assumed to report finish_reason correctly. This is the correct tradeoff because: - False negatives (Ollama at custom port, heuristic not triggered) only mean the user sees a truncated response — same as having no heuristic - False positives (non-Ollama server, heuristic wrongly triggered) inject spurious continuation messages into the conversation — strictly worse Adds two tests: - sglang GLM response is NOT reclassified as truncated - Ollama GLM on port 11434 still triggers the heuristic as before Co-authored-by: Hermes Agent <hermes@nousresearch.com>
Remove commentary about the previous is_local_endpoint() approach from _is_ollama_glm_backend() — git history suffices.
Follow-up to the #13971 fix: a genuine native Ollama provider reached through a reverse proxy carries no ollama/:11434 URL signature, so the restricted detection would miss it. Add provider=="ollama" as an explicit True case (idea from #14789, @Tranquil-Flow) and cover both it and the #13971 LiteLLM-proxy-to-zai false-positive with E2E tests.
Feishu SDK calls ran on asyncio's shared default executor, so a torn-down default executor wedged every send with 'Executor shutdown has been called' and left the gateway a zombie (#10849). The adapter now owns a ThreadPoolExecutor recreated on demand if shut down, mirroring the gateway-owned executor change. Routes all 17 self._client SDK calls through _run_blocking; shuts the pool down on disconnect.
Add an explicit _closing guard to both owned executors so the recreate-on-shutdown path only recovers from an *external* teardown of the loop default — never resurrects a pool the gateway/adapter itself stopped. _shutdown_*executor() sets the flag; _get_*executor() raises if closing; feishu connect() re-arms on reconnect. Updates the gateway recreate test to assert the refusal contract and adds feishu coverage.
…583) The dashboard Config tab's Model field is a flat string with no provider info. _denormalize_config_from_web only updated model.default and kept the stale provider, so picking an OpenRouter model while the default provider was ollama-local left provider=ollama-local and every call 404'd. When the model string actually changes, infer the serving provider — curated catalog first, then a vendor/model-slug heuristic for non-aggregator providers — and route the switch through the existing _normalize_main_model_assignment / _apply_main_model_assignment chokepoints so stale base_url/api_mode/api_key are cleared on a provider change and preserved on a same-provider re-pick. Saving an unchanged model never re-detects, so unrelated config saves keep an explicit provider. Closes #14058
…leak (#13870) prompt_toolkit's renderer sends ESC[6n cursor-position queries before painting in non-fullscreen mode; the terminal replies ESC[<row>;<col>R. Over SSH/cloudflared tunnels and slow PTYs these replies race past the input parser and land in the display as raw '20;1R21;1R' text, and the pending-CPR future can stall the renderer so the prompt freezes after the agent's final answer. Build the prompt_toolkit output with enable_cpr=False so CPR is marked NOT_SUPPORTED up front and ESC[6n is never sent. This is the root-cause counterpart to the existing input-side _strip_leaked_terminal_responses scrubbing. Vt100_Output.from_pty() does not expose enable_cpr in prompt_toolkit 3.x, so _build_cpr_disabled_output() reproduces its get_size setup and calls the constructor directly; it returns None on any failure so startup falls back to the default output. Verified in a real PTY: baseline emits 1 ESC[6n query, the fix emits 0, banner/UI render identically. Layout is unaffected — with CPR off the renderer sizes the prompt to its preferred height (the same fallback prompt_toolkit uses on any terminal that doesn't answer CPR). Co-authored-by: Hermes Agent <noreply@nousresearch.com>
_normalize_custom_provider_entry() ran urlparse() on base_url and dropped
any entry whose value was an un-expanded placeholder, so a caller reaching
the normalizer with raw config (e.g. the Dockerized gateway path) silently
skipped the provider with a 'not a valid URL' warning. Skip URL validation
when the candidate contains a placeholder token — both ${ENV_VAR} env-refs
and bare {region}-style templates — since those are expanded at runtime.
Closes #14457
…limit Z.AI / Zhipu reuse HTTP 429 for server-wide overload. The 429 status path classified these unconditionally as rate_limit with should_rotate_credential=True, so an overloaded provider exhausted the credential pool after two errors — fatal for a single-key user, who has nothing to rotate to. The credential is valid; the server is just busy. Disambiguate the 429 body against a shared _OVERLOADED_PATTERNS list and route overload language to FailoverReason.overloaded (retryable, no rotation), matching the existing 503/529 path and the message-only path (#52890). Genuine rate limits (no overload language) still rotate. Extracted the inline overloaded tuple #52890 added into the shared _OVERLOADED_PATTERNS constant so the status-code and message paths use one list. Closes #14038.
…t (#53588) The adapter-level intake gate (_is_dm_allowed / _is_group_allowed, reached via _should_process_message) did a raw set-membership check against the configured allowlist. WhatsApp now delivers inbound DM senders in LID form (<id>@lid) while operators configure allowlists with phone numbers, so the check never matched and every DM from an allowed contact was silently dropped before the gateway authz layer ran. Route both gates through the existing gateway.whatsapp_identity. expand_whatsapp_aliases helper (already used by gateway authz and session keys), which walks the bridge's lid-mapping-*.json session files. Phone and LID forms now resolve to each other in both directions; exact JID matches, wildcard, disabled/open policies, and empty-allowlist fail-closed behavior are all preserved. Fixes #14486
The Telegram/Discord /model command's actual switch calls switch_model() directly on the asyncio event loop. switch_model() can fall through to a synchronous models.dev HTTP fetch (requests.get, 15s timeout) on a cold or expired cache, freezing the gateway for up to 15s and dropping the Telegram connection while a user switches models. The picker provider-list and fallback text-list sites were already offloaded (#41289), but the two _switch_model() calls — the picker callback and the direct /model <name> path — were not. Wrap both in asyncio.to_thread. Closes #20525.
The zombie status probe referenced psutil.Process/NoSuchProcess/Error unconditionally, which raised AttributeError when psutil is a partial stub that only defines pid_exists (as in test_windows_native_support's fallback tests). Guard the probe so any failure to read status degrades to the authoritative pid_exists() instead of raising.
…#35519) When security.redact_secrets is on (default), read_file/search_files/cat applied redact_sensitive_text(code_file=True) to file content, which still ran prefix masking. An API key in config.yaml (ghp_..., sk-..., xai-..., etc.) came back as a head/tail mask like `ghp_S1...Pn2T` — a plausible-looking truncated key. When an agent read that and wrote it back to config, the masked value replaced the real credential, silently breaking auth (401). Production evidence: a config.yaml found containing the exact 13-char masked GitHub PAT. The two community PRs (#35529, #35534) fixed the corruption by NOT redacting prefixes for config reads — but that exposes the user's real keys to the agent context, model, and logs (a security regression). This takes the safer route: keep redacting, but for file content emit a NON-REUSABLE sentinel. - New `_mask_token_nonreusable`: prefix secrets -> `«redacted:ghp_…»` (vendor label preserved for debuggability; zero secret bytes; angle-bracket/ellipsis wrapper is syntactically invalid as a token so it can't be mistaken for or written back as a usable key). - New `redact_sensitive_text(file_read=True)` routes prefix matches through it (implies code_file=True). Default/log/display mode is UNCHANGED — `_mask_token` still keeps head/tail (fine for logs, never written back). - Wired the 3 file_tools.py call sites (read_file / search_files / cat) to file_read=True. Fixes both the corruption AND avoids the secret-exposure of the un-redact approach. 6 new tests (sentinel shape, no-leak, not-a-plausible-key, default mode unchanged, file_read implies code_file, sk- prefix); 88 redact tests pass; mutation-verified (reverting to the old mask fails the sentinel/leak tests). Co-authored-by: liuhao1024 <sunsky.lau@gmail.com> Co-authored-by: adammatski1972 <289282750+adammatski1972@users.noreply.github.com> Closes #35519. Supersedes #35529, #35534.
…llapse The salvaged #35519 regression guard asserted that default (non-file_read) mode keeps a head/tail `ghp_S1...Pn2T` mask for a `token: <key>` line. On current main the YAML config pass (`_YAML_ASSIGN_RE`, key `token`) re-masks the already-prefix-masked value to `***`, so the assertion was stale. Switch to a bare-token context so the guard isolates what it claims (prefix-mask head/tail shape in default mode) without depending on the YAML collapse.
The desktop app and dashboard chat reach the agent through the /api/ws JSON-RPC sidecar (tui_gateway.ws.handle_ws), NOT through tui_gateway.entry.main() — the stdio-TUI path that spawns the background MCP discovery thread. In the WS process discovery was therefore never started: _make_agent only *waits* (wait_for_mcp_discovery), which no-ops when the thread was never created, so the agent snapshotted an MCP-less tool list. The only discovery trigger reachable was a manual /reload-mcp, which is why tools appeared after a reload but vanished on restart. Start the shared, idempotent, config-gated background discovery in handle_ws right after accept() and before gateway.ready, so the first agent build picks up already-spawning servers (and the existing late-binding refresh handles slow ones). Fixes #38945.
The README line enumerated 11 providers inline, which dilutes the point and goes stale as providers come and go. Replace with Nous Portal, OpenRouter, OpenAI, your own endpoint, and a 'many others' link to the canonical AI Providers docs page that already lists them all.
…e (#54203)
test_spinner_elapsed_format_is_fixed_width_to_reduce_wrap_jitter derived
_tool_start_time from the live time.monotonic() clock (now - 65.2 / now - 9.2).
monotonic()'s epoch is arbitrary — on a host where monotonic() < 65.2 (fresh
subprocess on a freshly-booted CI runner) the start time went negative, the
(t0 > 0) guard in _render_spinner_text() dropped the '(elapsed)' suffix, and
short.split('(',1)[1] raised IndexError: list index out of range. Deterministic
given a small clock, so it would keep flaking, not clear on rerun.
Pin time.monotonic to a fixed 1000.0 and offset _tool_start_time from it so both
the <60s and >=60s paths always render the elapsed suffix regardless of the
runner's monotonic epoch.
Pre-existing main flake (surfaced in CI test slice 1/8).
…strap launchd restart can leave the gateway job stopped but still registered after update-time drain logic, so a direct bootstrap hits exit 5 and falls back to a detached process. Booting the stale registration out before bootstrap keeps the launchd-managed restart path intact and locks it with a regression test. Constraint: Keep upstream-facing conventional commit style while preserving local decision context Rejected: Treat bootstrap exit 5 as expected | Leaves macOS launchd restart outside launchd supervision after update Confidence: high Scope-risk: narrow Directive: Keep launchd start/restart recovery flows aligned when changing launchctl handling Tested: pytest -q tests/hermes_cli/test_gateway_service.py -k "launchd_restart_boots_out_stale_registration_before_bootstrap or launchd_restart_falls_back_to_detached_on_error_5 or launchd_restart_drains_running_gateway_before_kickstart or launchd_restart_self_requests_graceful_restart_without_kickstart" Tested: pytest -q tests/hermes_cli/test_gateway_service.py -k launchd Not-tested: Manual macOS launchctl restart after hermes update
On macOS app.quit() closes windows but window-all-closed deliberately keeps the process alive (Dock convention). Every detached hand-off (update swap, relaunch, Windows bootstrap recovery, uninstall cleanup) waits for the desktop PID to exit before replacing/removing the bundle — so the process never dying means the script spins its full PID-wait and the user sees a blank app, or an uninstall that appears to do nothing. Add a module-level isQuittingForHandoff flag, set before every hand-off app.quit(); window-all-closed then quits on all platforms when it's set. Covers all five hand-off sites including the Linux relaunch path.
…strict providers (#48338)
_append_model_switch_marker() appended the post-/model-switch context marker
to session history as {"role": "system"}. The cached system prompt is
prepended to the API message list (conversation_loop.py), so this marker
became a SECOND system message mid-array after prior user/assistant turns.
Strict OpenAI-compatible providers (vLLM, Qwen) reject any system message
that is not at the beginning of the array, returning HTTP 400 and killing
the conversation on the next turn.
Flip the marker to role="user" (history entry + both session-DB persist
sites), matching the existing personality-overlay marker which already uses
role="user". repair_message_sequence() then coalesces it with adjacent user
turns as needed.
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: Lucas Nicolas <lucas.nicolas@proton.me>
…anently fatal When a stale lock file survives a gateway crash, `acquire_scoped_lock()` may return `(False, existing_dict)` even after detecting and deleting the stale lock (e.g. if unlink fails or a race condition occurs). Previously, `_acquire_platform_lock()` called `_set_fatal_error(..., retryable=False)`, which permanently killed the platform — the reconnect watcher never retries a non-retryable fatal error. Change to `retryable=True` so the platform enters the "retrying" state and the reconnect watcher can attempt acquisition again after the standard backoff delay. Fixes #54167
OAuth-protected MCP servers (e.g. Hospitable) return 200 text/html on an unauthenticated HEAD probe — a login/landing page the server cannot substitute for a real MCP response without a Bearer token. The preflight cannot distinguish this from a misconfigured URL, so it raises NonMcpEndpointError before the OAuth browser flow has a chance to run. Add `and self._auth_type != "oauth"` to the preflight condition in MCPServerTask.run(). The probe is inapplicable to OAuth servers: their URL legitimacy is established by .well-known/oauth-protected-resource during the OAuth handshake, not by a GET content-type check. Concrete repro: Hospitable (https://mcp.hospitable.com/mcp) returns `200 text/html` to an unauthenticated httpx HEAD. Without the guard: ✗ NonMcpEndpointError at `hermes mcp test` With the guard: ✓ Connected (1487ms) — 63 tools discovered Relation to open PRs: - #37598 adds a POST probe fallback for POST-only non-OAuth servers (e.g. DocuSeal), but only passes when POST returns 2xx + MCP content-type. Hospitable returns 401 on the POST probe (Bearer challenge), so #37598 does not cover this case. - #49463 extends the POST probe to also pass on non-2xx auth challenges (making it OAuth-aware), but is labeled duplicate of #37598 and may not land independently. This fix is complementary: it handles OAuth servers with zero extra round-trips rather than adding a POST probe step. Tests: - test_oauth_server_html_response_raises_without_skip: documents that _preflight_content_type raises NonMcpEndpointError for 200 text/html (the underlying issue), with an OAuth-server docstring. - test_run_skips_preflight_for_oauth: verifies that run() does NOT invoke _preflight_content_type when auth_type=="oauth", using class-level monkeypatching so the gate is exercised without a live MCP transport. 23 passed tests/tools/test_mcp_preflight_content_type.py
…ubprocess spawns The Windows desktop GUI runs its backend headless via pythonw.exe. Several auxiliary subprocess sites that run inside that windowless backend spawned console-subsystem children (git, gh, wmic, powershell, bash, rg, taskkill) WITHOUT CREATE_NO_WINDOW, so Windows allocated a fresh conhost per call and flashed a black window on screen — sometimes continuously (the dashboard Projects-tree git probe alone fired ~118 spawns in 60s on startup). The terminal tool, cron, browser, code_execution, and gateway-spawn paths already carry windows_hide_flags(); these auxiliary probe/scan/launcher legs were missed. Wire the existing helper into them: - tui_gateway/git_probe.py: run_git (+ encoding=utf-8/errors=replace, fixes the cp950 UnicodeDecodeError on CJK paths from the same site) - agent/coding_context.py: _git (per-turn git status/log/diff) - agent/context_references.py: _run_git + _rg_files (@file/@ref resolution) - hermes_cli/copilot_auth.py: gh auth token probe (auxiliary provider:auto) - hermes_cli/gateway.py: wmic + PowerShell Get-CimInstance PID scan - hermes_cli/main.py: wmic stale-dashboard PID scan - gateway/status.py: taskkill /T /F force-kill windows_hide_flags() returns 0 on POSIX, so every changed call is a no-op on Linux/macOS (verified: real git/rg probes still work; Windows-simulated calls all pass creationflags=CREATE_NO_WINDOW). Scoped to the windowless-backend paths that cause the reported flashing. The Electron updater-handoff leg (main.cjs windowsHide:false) and the interactive-CLI banner probes (cli.py) are intentionally NOT touched here — the former needs a Windows-tested change of its own, the latter runs in a visible console anyway. Tracking: #54220 Refs: #53178 #53631 #53781 #53957 #49602 #52982 #53424 #53053 #53016
main (cb982ad) wired windows_hide_flags() into the auxiliary git/gh/wmic/ bash/powershell/taskkill legs but left two it didn't reach, plus the Electron backend-launch leg it explicitly deferred. Cover them the same way: - apps/desktop/electron/main.cjs: getNoConsoleVenvPython resolves the BASE pythonw.exe instead of the venv Scripts\pythonw.exe shim, which re-execs a console python.exe and flashes a conhost the desktop backend can't suppress. Both backend creators put the venv site-packages on PYTHONPATH so imports still resolve under the base interpreter. (main's commit said this Electron leg "needs a Windows-tested change of its own".) - tools/tts_tool.py, tools/transcription_tools.py, plugins/platforms/discord: ffmpeg conversions (voice notes / TTS / STT) via windows_hide_flags(). - plugins/platforms/whatsapp: netstat + taskkill bridge-port cleanup via windows_hide_flags(). All no-ops on POSIX. Tests assert the base-pythonw preference and the ffmpeg legs pass CREATE_NO_WINDOW.
_get_platform_tools() applies agent.disabled_toolsets as a final override AFTER reading platform_toolsets.<platform>, so a toolset listed there stays permanently OFF no matter what the toggle write path saves. Blank Slate installs pre-populate this list with ~27 toolsets, making most of the desktop Toolsets UI un-enableable (issue #49995). Fix: _save_platform_tools() now removes any toolset the user just explicitly enabled FOR THIS PLATFORM from agent.disabled_toolsets. Toolsets the user did not touch, or that remain disabled on other platforms, are left alone -- disabled_toolsets keeps working as a cross-platform suppression list for anything not actively re-enabled. Disabling a toolset (unchecking it) does not touch disabled_toolsets at all -- only enables reconcile it. Verified end-to-end with the exact repro from the issue: Blank Slate config (disabled_toolsets=['todo','memory','browser'], cli=['file', 'terminal']) -> enable 'todo' via the toggle -> _get_platform_tools() now resolves 'todo' as enabled while 'memory'/'browser' (untouched) remain disabled. Added 4 regression tests. Full tools_config suite: 101 passed (97 existing + 4 new), no regressions. Fixes #49995
fix(desktop): retry empty resumed transcripts
Local browser_navigate cold-starts the agent-browser daemon and Chromium; 60s was too short on slow Linux hosts and timeouts discarded stderr, leaving users with a generic failure. Use a 120s floor on first open, inject --no-sandbox in Docker, include captured daemon output plus install hints when commands time out, and show "Failed to open" in the desktop tool chip when navigation returns success=false.
Add regression tests for open-command timeout floors, sandbox bypass, stderr capture formatting, first-navigation timeout wiring, and desktop failed-navigate labeling.
When a local browser_navigate (or any browser command) fails fast because Chromium isn't on disk, attempt a one-shot binary download via `agent-browser install` and retry instead of only printing a hint. Scope is narrow on purpose: - binary only, never `--with-deps` (that shells apt/needs root, so missing system libraries stay a user action) - gated by `security.allow_lazy_installs` (same opt-out as every lazy install) - skipped in Docker (Chromium ships in the image) - attempted once per process Follow-up to #54353, which made the cold-start failure legible; this closes the "doesn't actually install the missing browser" gap for the common case.
…eout fix(browser): extend first-open timeout & surface daemon errors on Linux (salvage #52575)
…nstall feat(browser): auto-install Chromium binary on local cold-start failure
Syncs ~1185 upstream Hermes commits up to b699d27. Conflicts resolved (kept both ClawPump overlay + upstream features): - cli.py: keep UsePod auto-apply hook AND upstream delegate_task background notice; inline-diff guard moved after both. - hermes_cli/banner.py: keep skin org/credit branding AND upstream MoA banner. - hermes_cli/model_setup_flows.py: usepod base-url handling alongside upstream zai endpoint picker + clear_model_endpoint_credentials. - web ChatSidebar/ChatPage: keep dashboard rail refactor (picker in navbar, showTools/variant) alongside upstream onSessionTitleChange. - scripts/release.py: union of AUTHOR_MAP entries. - package-lock.json: regenerated to upstream (root package.json unchanged). web typecheck passes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Manual upstream sync (the daily
upstream-syncAction has been failing on a GitHub Actions billing error, so this was done by hand perdocs/UPSTREAM_SYNC.md).Merges
upstream/mainup tob699d27a4— ~1185 commits — as a real merge commit to preserve the shared merge-base.Conflicts resolved (kept ClawPump overlay and upstream features)
_on_tool_completeAND upstream'sdelegate_taskbackground notice; inline-diff guard moved after both.org/creditbranding AND upstream's MoA banner branch.usepodbase-url handling alongside upstream'szaiendpoint picker +clear_model_endpoint_credentials.showTools/variantprops) alongside upstream'sonSessionTitleChange.AUTHOR_MAPentries.package.jsonis unchanged vs upstream).Validation
cli.py/banner.py/model_setup_flows.py/release.pyparse.webnpm run typecheckpasses.UPSTREAM_SYNC_PATis set). Re-run CI manually.contributor-checkmay flag new upstream commit-author emails missing fromAUTHOR_MAP(scripts/release.py); add the lines it prints.🤖 Generated with Claude Code