fix(skills): use _github_get retry in _get_repo_tree to prevent hangs on network timeout - #2
Open
luxles wants to merge 785 commits into
Open
fix(skills): use _github_get retry in _get_repo_tree to prevent hangs on network timeout#2luxles wants to merge 785 commits into
luxles wants to merge 785 commits into
Conversation
Mirror built-in memory writes to external providers only after the native memory tool succeeds and is not staged for approval. Keep OpenViking's built-in memory mirroring add-only, since Hermes native memory entries do not yet have stable OpenViking file URIs for replace/remove. Add a narrow viking_forget tool for exact user memory file deletion and document the current OpenViking write/delete behavior.
…face The success/staged gating and op-expansion for mirroring built-in memory writes to external providers lived in a standalone agent/memory_write_bridge.py helper called inline from two core call sites (tool_executor.py, agent_runtime_helpers.py). That left the mirror decision-making in the agent loop, outside the memory-provider interface. Fold it into a new MemoryManager.notify_memory_tool_write() entry point: the loop now hands over the raw tool result + args and a metadata callback, and the manager decides whether/what to mirror. Both core call sites collapse to a single call; the orphan module is removed. No MemoryProvider ABC change. Tests rewritten as behavior tests against the manager method.
…ousResearch#50848) * chore: re-trigger CI (workflows did not dispatch on prior head) * fix(delegation): emit high-concurrency cost warning once per process _get_max_concurrent_children() runs on every get_definitions() schema rebuild (via _build_top_level_description / _build_tasks_param_description), not just on actual delegate_task calls. With max_concurrent_children>10 the cost advisory fired on every turn / agent spawn across every session, spamming the log even when delegate_task was never used. Gate it behind a module-level _HIGH_CONCURRENCY_WARNED flag so it warns at most once per process.
The Slack docs document `slack.mention_patterns` as custom wake words that trigger the bot alongside `@mention`, and the config layer bridges the key into the Slack adapter's `config.extra` — but the adapter never read it. With `require_mention` on, a channel message containing a configured wake word (and no literal `<@BOTUID>`) was silently ignored. Every other adapter that documents `mention_patterns` (Telegram, DingTalk, Mattermost, WhatsApp, BlueBubbles, Photon) implements it; Slack was the odd one out. Add `_slack_mention_patterns()` (compiled, cached; reads `slack.mention_patterns` as a list/string or `SLACK_MENTION_PATTERNS` as a JSON/CSV/newline list, invalid regexes warned and skipped) and `_slack_message_matches_mention_patterns()`, mirroring the existing adapters. Channel mention detection now also triggers on a wake-word match, so the documented field works as described. Adds tests for pattern compilation (list/string/env/invalid-regex) and for the channel-trigger gating with a wake word under require_mention.
…in (NousResearch#50842) * feat(computer_use): disable cua-driver telemetry by default, add opt-in cua-driver ships anonymous PostHog usage telemetry ENABLED by default upstream (fires cua_driver_install / cua_driver_doctor events to eu.i.posthog.com). Hermes now disables it for our users unless they explicitly opt in. - New config key `computer_use.cua_telemetry` (default false) in DEFAULT_CONFIG. - `cua_backend.cua_driver_child_env()` injects `CUA_DRIVER_RS_TELEMETRY_ENABLED=0` into the child env when telemetry is disabled (the default); leaves the var untouched on opt-in so the driver uses its own default. Reads config fail-safe — any error defaults to telemetry off. - Routed every cua-driver spawn site through the policy: MCP backend (StdioServerParameters env), `cua_driver_update_check`, doctor's health_report Popen, the install.sh/install.ps1 runner, and the `--version` / status probes. - Docs: new Telemetry subsection in computer-use.md (EN). - Tests: tests/computer_use/test_cua_telemetry.py — default disables, explicit-false disables, opt-in leaves var untouched, config-failure fails safe, inherited-enabled is overridden off. Verified live on Linux against the real cua-driver-rs 0.6.0 binary: with the var=0 the driver reports "telemetry: disabled via CUA_DRIVER_RS_TELEMETRY_ENABLED" and sends no event; with it unset it logs "sending event: cua_driver_doctor". 213 computer_use + install tests green. * fix(dashboard): fold computer_use config category into agent tab The new computer_use.cua_telemetry key created a single-field dashboard config category, tripping test_no_single_field_categories (web_server's invariant that categories with <2 fields must be merged to avoid tab sprawl). Add computer_use -> agent to _CATEGORY_MERGE, matching the existing onboarding/telegram single-field folds.
…tant /simplify-code (LOW, flagged by two reviewers): the source tags 'user' / 'project' / 'bundled' were bare string literals scattered across the discovery scrub and the two mount-time refuse guards. A typo in any one site (e.g. 'users') would SILENTLY disable a security gate with no error — the exact failure mode this RCE boundary must not have. Introduce a shared module-level _NON_BUNDLED_PLUGIN_SOURCES frozenset referenced by both the discovery scrub and the (now single) mount guard, so the auto-import policy lives in one place. The two mount guards collapse into one gate that still emits the distinct per-source operator message via a map (no loss of guidance). Behavior unchanged: 39 RCE-bypass tests pass, and the constant is mutation-checked (typo'ing it fails the bypass tests). Defence-in-depth (discovery scrub + mount refuse) is retained intentionally.
…-dashboard-plugin-rce fix(security): restrict dashboard plugin backend auto-import to bundled plugins — defense-in-depth (NousResearch#43719)
… off-screen The popped-out composer is position:fixed, but the chat content wrapper sets `contain: layout paint`, which makes it a containing block for — and clips — fixed descendants. Inline, the floating composer was positioned/clipped relative to the chat column (which shifts with the sidebars), not the viewport, so the viewport-based bounds clamp from NousResearch#50466 couldn't keep it reachable: users still lost it off-screen. Portal it to <body> when popped out so fixed positioning and the clamp finally share the viewport as their reference. Docked stays inline (it's absolute within the chat column by design).
Replaces the body-portal approach: render ChatBar as a sibling of the contain:[layout paint] chat wrapper (inside the same runtime boundary) rather than portaling the floating instance to <body>. The wrapper is a containing block for — and clips — position:fixed descendants, which is what stranded the popped-out composer off-screen. As a sibling it anchors to the outer relative container: docked stays absolute (identical placement), floating resolves against the viewport. Both states stay mounted, so dock<->float no longer remounts the editor (the portal toggle did).
…le window Now that the popped-out composer is fixed to the viewport, clamping against the window let it slide under a pinned sidebar. Confine it to the thread region (data-slot="composer-bounds") instead — its rect already excludes a pinned sidebar and the header — falling back to the full window before it's measured. This subsumes the old titlebar top-margin (the thread rect starts below the header).
Re-clamp once more on the next frame after pop-out so layout (sidebar widths, fonts) has settled, and treat a degenerate pre-layout bounds rect as "unknown" (fall back to the window) so we never clamp the box into a collapsed area. Net: anyone who loads in with a stranded position is pulled back on-screen and the fix is persisted, even if the first measure was premature.
…ixed-portal fix(desktop): keep floating composer on-screen, scoped to the thread area
…earch#50849) * chore: re-trigger CI (workflows did not dispatch on prior head) * feat(skills): add cloudflare-temporary-deploy optional skill Optional web-development skill teaching the agent to deploy a Worker to a live workers.dev URL with no Cloudflare account via 'wrangler deploy --temporary' (Wrangler 4.102.0+). Cloudflare provisions a throwaway, claimable account valid for 60 minutes — ideal for an autonomous write->deploy->verify loop with no OAuth/signup hard stop. - SKILL.md: when/when-not, prereqs (unauth requirement, version floor), step-by-step deploy + verify flow, product limits table, pitfalls (hidden flag, stale global wrangler, auth-present error, rate limits, workers.dev edge cache), verification. - scripts/parse_deploy_output.py: stdlib-only parser extracting live URL, claim URL, account name/state, expiry, deploy status from wrangler output. - tests/skills/test_cloudflare_temporary_deploy_skill.py: 16 tests incl. a real-output regression case. Verified live end-to-end: temporary account created with no creds, deployed to a live URL, curl confirmed body, redeploy reused the account.
…ousResearch#50501) Adds an optional structured completion contract to the standing-goal loop, adapted from OpenAI Codex's /goal guidance (a durable objective works best when it names what done means, how to prove it, what not to break, what's in scope, and when to stop). A contract has five optional fields — outcome, verification, constraints, boundaries, stop_when. When set, the continuation prompt tells the agent to target the verification surface and respect constraints, and the judge marks the goal done only when the verification criterion is met with concrete evidence (command result, file excerpt, test output) instead of a loose "looks done" claim. This tightens the most common /goal failure mode: premature completion / endless over-continuation on an underspecified goal. Two ways to set a contract, both backward compatible (bare /goal <text> behaves exactly as before): - /goal draft <objective> — expands plain text into a full contract via the goal_judge aux model (cache-safe side call), falls back to a free-form goal if the model is unavailable. - /goal <text> with inline 'field: value' lines (verify:, constraints:, boundaries:, stop when:, ...). Plain goals with an incidental colon are not mangled — only known field prefixes are pulled out. - /goal show prints the active contract. Contracts persist in SessionDB.state_meta alongside the goal (survive /resume), compose with /subgoal criteria, and old goal rows load unchanged. CLI + every gateway platform via the shared GoalManager engine; zero new model tools. Tests: +18 in tests/hermes_cli/test_goals.py (parse/serialize/judge-prompt/ draft/fallback), 73/73 green; 42/42 across the broader goal test surface; live E2E roundtrip (set -> persist -> reload -> contract-aware prompts) green.
cua-driver 0.6.x removed the standalone screenshot MCP tool, so capture(mode='vision') hit 'Unknown tool: screenshot' and returned a 0x0 image with no PNG while som/ax (which use get_window_state) still worked. Route vision through get_window_state(capture_mode='vision'). Salvaged from PR NousResearch#50771; same fix submitted earlier as NousResearch#39262 by @Tranquil-Flow.
capture(app='screen'|'desktop') now resolves to the OS shell/desktop window (Windows Progman/WorkerW desktop or Shell_TrayWnd taskbar, macOS Finder/Dock) so 'show me my screen' and 'click the taskbar' work. Previously capture() only matched application windows, and the schema advertised 'or the whole screen' without any code path delivering it. cua-driver is window-oriented (no virtual-desktop or per-monitor MCP tool), so a single image still cannot span multiple monitors — the schema now states this and the no-desktop-window path returns a clear message instead of silently grabbing the frontmost app.
…ch#31501) Targeted ``(chat_id, thread_id)`` prune for the ``telegram_dm_topic_bindings`` table — the missing piece for NousResearch#31501, where the Telegram adapter detects a topic the user deleted out-of-band but the binding row keeps living in state.db. The recovery logic in ``gateway.run._recover_telegram_topic_thread_id`` then steers every future inbound message back to the dead topic, dropping tool progress, approvals and replies into the wrong place. Returns the number of rows deleted; silently no-ops when the topic-mode tables haven't been migrated yet (read-only / pristine profile) so the helper is safe to call from a send-fallback hot path before the schema has run.
…ousResearch#31501) Both fallback sites that currently log "Thread X not found, retrying without message_thread_id" now also drop the ``telegram_dm_topic_bindings`` row keyed on ``(chat_id, thread_id)``: * The streaming send loop (``send`` body) — fires on the second failure, after the same-thread one-shot retry confirms the thread really is gone (the first attempt is left alone because Bot API has been observed to return a transient "Thread not found" that recovers on immediate retry). * The control-message helper ``_send_message_with_thread_fallback`` (approval prompts, model picker, update prompts) — single-shot retry, prune unconditionally on the BadRequest match. Without this prune, a user who deletes a Telegram DM topic in the client keeps getting their next inbound message recovered back to the dead thread by ``_recover_telegram_topic_thread_id`` in ``gateway/run.py``, which walks the per-user binding list newest-first and treats the deleted thread as authoritative. The reproduction in the bug report is exactly this: tool progress, approvals, activity messages and replies all land in the wrong place until the user manually runs DELETE on state.db. Cleanup is best-effort — we log at INFO when it succeeds, swallow any exception from the SessionDB call, and the user-facing send proceeds either way. Refs NousResearch#31501
…h#31501) Thirteen tests across four layers: * ``SessionDB.delete_telegram_topic_binding`` — pin the new helper's contract: removes only the (chat_id, thread_id) row it was asked about, leaves siblings alone, returns 0 silently when the row never existed, and is a no-op on a pristine database whose topic-mode tables haven't been migrated yet. * ``TelegramAdapter._prune_stale_dm_topic_binding`` — the glue must drop the binding when ``self._session_store._db`` exposes the helper, swallow exceptions so a failed cleanup never breaks the user-facing send, and refuse to issue a DELETE for ``chat_id=None`` / ``thread_id=None`` so a bookkeeping miss can't accidentally null-match every row. * Source-level guards on ``TelegramAdapter.send`` and ``_send_message_with_thread_fallback`` — the prune call must sit beside the two existing "Thread X not found, retrying without message_thread_id" warnings, before the retry runs, so a future refactor can't silently drop the cleanup wire. * End-to-end semantic — once a topic is pruned, the ``GatewayRunner._recover_telegram_topic_thread_id`` walk steers future inbound messages to the surviving binding instead of the dead one. This is the exact behaviour change the bug report's reproduction asks for: no more landings in the wrong topic until the operator hand-edits ``state.db``. Refs NousResearch#31501
Follow-up to NousResearch#31501. When the send-fallback prune removes a chat's final telegram_dm_topic_bindings row, also flip telegram_dm_topic_mode.enabled to 0 in the same transaction. Without this, a user who turns topics off in the Telegram client (rather than via /topic off) leaves enabled=1 with zero lanes: _recover_telegram_topic_thread_id keeps treating the chat as topic-enabled and lobby messages keep hunting for bindings that no longer exist. Clearing the flag makes recovery fully stand down once the dead topics are gone. Adds 3 regression tests covering the last-binding clear, the multi-binding no-op, and the unmatched-prune no-op.
…usResearch#51005) Salvages NousResearch#50469 by @libre-7. _dashboard_local_update_managed_externally() previously blocked every containerized dashboard from the local update API, even when the running install was a bind-mounted git checkout that can be updated with hermes update. Allow the dashboard updater only for git installs inside containers, while keeping hosted /opt/data, docker, and pip installs managed externally. Pip remains blocked because its apply path mutates the running container filesystem and is not the self-managed checkout case. Adds regression coverage for docker, git, and pip install-method handling inside containers, and maps the contributor email for release attribution. Co-authored-by: libre-7 <libre-7@users.noreply.github.com>
…usResearch#45006) A typed `/model <name>` where `<name>` is declared under `providers.<slug>` or `custom_providers` — but typed while the current provider is a soft-accepting one (e.g. `openai-codex`) — stayed on the current provider and was swallowed as an unknown hidden Codex model, instead of routing to the provider that actually declares it. Add configured-provider exact-match detection (`_configured_provider_matches`) and a new Step d.5 in `switch_model`: if the typed model is declared in user/custom provider config, route to that provider BEFORE `detect_provider_for_model()` guesses from static catalogs and BEFORE the common-path validation lets a soft-accepting current provider swallow the name. - Matching is exact (case-insensitive) against explicitly-declared model collections only (`models`, `model`, `default_model`) — never fuzzy/family. - Same-provider declarer → keep current provider (canonicalize the id). - Multiple declarers → fail clearly and ask for `--provider <slug>`. - Single declarer → route there; for `providers.<slug>` user providers, set `explicit_provider` so the credential block resolves base_url/key from config. - Step e (`detect_provider_for_model`) is gated off when `config_routed`. The deliberately-supported openai-codex / xai-oauth hidden-model soft-accept (NousResearch#16172 / NousResearch#19729) is left untouched: when nothing in config matches, detection is a no-op. Salvaged from NousResearch#45442 by harjothkhara (authorship preserved). Tests: tests/hermes_cli/test_model_switch_configured_provider_routing.py (7 tests). Full model_switch suite: 214 passed. Fixes NousResearch#45006
…al (NousResearch#51031) * chore: re-trigger CI (workflows did not dispatch on prior head) * fix(image/video gen): make schema delivery instruction platform-neutral The image_generate and video_generate tool schema descriptions hardcoded a gateway-only delivery instruction ('display it with markdown  and the gateway will deliver it'). That schema is sent on every platform, so on CLI it directly contradicted the CLI platform hint ('Do NOT emit MEDIA:/path tags ... state its absolute path in plain text'), and on messaging platforms it was also wrong about the mechanism (local file paths are delivered via MEDIA: tags, not markdown image syntax — markdown ![]() only works for URLs). The per-platform file-delivery convention is already owned correctly by the platform hints in prompt_builder.py. The tool schema now just describes the result shape (URL or absolute path in the image/video field) and defers 'how to deliver' to the active platform's guidance. Provider/model injection already works via _build_dynamic_image_schema() (the 'Active backend: <provider> · model: <model>' line); no change there.
…pstream installer `hermes computer-use install` refused to install on Linux, Windows, and macOS x86_64 because the pre-install asset probe was hitting the wrong GitHub endpoint AND duplicating tag-resolution logic the upstream installer already does correctly. `_check_cua_driver_asset_for_arch()` queried `https://api.github.com/repos/trycua/cua/releases/latest`. On trycua/cua: - cua-driver-rs releases (the binary the installer fetches) are marked **prerelease** on every cut. GitHub's `/releases/latest` explicitly skips prereleases. - The Python package releases (`cua-agent`, `cua-computer`, `cua-train`) are non-prerelease and end up as the "latest" instead. Live API check today: $ curl -sf https://api.github.com/repos/trycua/cua/releases/latest \ | jq '{tag:.tag_name, asset_count: (.assets|length)}' { "tag": "agent-v0.8.3", "asset_count": 0 } The probe sees zero assets, prints "Latest CUA release has no Linux x86_64 asset", and skips install on every Linux / Windows / macOS-x86_64 host — even though the cua-driver-rs-v0.6.0 release ships 19 binary assets covering all those platforms. Filtering `/releases?per_page=N` for the `cua-driver-rs-v*` prefix fixes the bug, but it duplicates tag-resolution logic the upstream `_install-rust.sh` already does correctly via `CUA_DRIVER_RS_BAKED_VERSION` (auto-baked by CD on every release, with a `/releases?per_page=N` API fallback for dev checkouts). The right answer is to trust that contract instead of mirroring it in Python where it can drift. Two paths get the same outcome without the probe: 1. **Fresh install**: run `install.sh` directly. It has the baked release tag, fetches the right asset, and errors with a clear message on missing-arch downloads. No preflight needed. 2. **Upgrade path**: `cua_driver_update_check()` (separately added) shells `cua-driver check-update --json` against the installed binary, which returns the canonical update answer from the same source the installer uses. - `hermes_cli/tools_config.py`: delete `_check_cua_driver_asset_for_arch` and its two call sites in `install_cua_driver`. Replace with an inline comment near the top of the module explaining the rationale. - `tests/hermes_cli/test_install_cua_driver.py`: drop the `TestCheckCuaDriverAssetForArch` block. Add `TestArchProbeRemoval` with three regressions: - `test_probe_function_is_gone` — asserts the deleted helpers stay deleted. - `test_fresh_install_does_not_call_github_api` — asserts the install path doesn't hit GitHub directly from Python anymore. - `test_upgrade_with_binary_does_not_call_github_api_directly` — same for the upgrade path. All 9 `test_install_cua_driver` tests pass. Reported by @teknium1 while testing on a headed Ubuntu host.
f-trycua's NousResearch#50855 test file predated the cross-platform PR (NousResearch#50552) and reintroduced two stale tests asserting Linux is unsupported (test_*_non_macos_*, patching platform.system="Linux" and expecting a no-op/warn). Linux + Windows are supported now, so install proceeds on those platforms. Restore main's cross-platform-correct versions: test_*_on_unsupported_platform_* using FreeBSD as the genuinely unsupported case.
The 8-minute stream-silence watchdog only removed a stuck session from $workingSessionIds (the sidebar dot). The composer's busy state lives in the session-state cache and was never cleared, so a hung or looping turn that never delivered its terminal event — including an old session re-opened while the backend still reports it "running" — stayed wedged on "Thinking" / Stop indefinitely. Have the watchdog notify subscribers when it force-clears a session, and subscribe from the session-state cache to also drop that session's busy/awaiting/needsInput flags. updateSessionState re-syncs $busy when the healed session is the one on screen, so the composer recovers instead of spinning forever. Frontend-only safety net; doesn't touch the turn lifecycle. The backend root (a stale in-memory session["running"] surviving a dead turn thread and re-arming busy on every resume) is a separate follow-up.
…ents-status fix(desktop): make Agents indicator match the Spawn-tree panel
fix(desktop): wire Ctrl+B voice, declutter voice settings, stop endless TTS hang
…fline fix(desktop): give the gateway reconnect loop an escape hatch
…op-guard fix(desktop): let the session watchdog heal a stuck "looping" turn
…uthz
A hosted instance fronted by the Team Gateway connector dropped EVERY relay
message as "Unauthorized user" and the agent never replied — despite the
message routing correctly through the connector to the instance.
Root cause: gateway authorization (_is_user_authorized) had no notion of
upstream-enforced authz. Platform.RELAY matches no {PLATFORM}_ALLOWED_USERS
allowlist and isn't in the HA/WEBHOOK always-authorized set, so a relay user
with no env allowlist configured hit the default-deny ("No user allowlists
configured. All unauthorized users will be denied."). The message was received,
then silently denied before reaching the agent.
This is incorrect for relay: the connector authenticates the gateway's WS with
a per-instance secret and performs owner-only author-binding resolution BEFORE
delivering. A message only reaches this gateway because the connector resolved
it to THIS instance's bound user (user_instance_binding), keyed on the author id
the connector OBSERVED off the event — never a gateway claim. The authorization
decision is already made by a trusted, authenticated upstream; there is no local
RELAY_ALLOWED_USERS allowlist to consult, and default-denying for its absence is
the bug.
Fix: add a generic BasePlatformAdapter.authorization_is_upstream capability
(default False) that the relay adapter overrides to True, plus a dedicated
trusted branch in _is_user_authorized that honors it. This is delegation to a
trusted upstream, NOT a fail-open: it fires only for an adapter that explicitly
declares the flag; every direct network-exposed adapter leaves it False and the
env-allowlist default-deny (SECURITY.md §2.6) is unchanged. Distinct from
enforces_own_access_policy, which mirrors a LOCAL config-driven allowlist —
this delegates to an authenticated upstream's decision.
Tests: behavior contract that the base defaults False, the relay adapter
declares True, a relay user (group + DM) is authorized with no env allowlist,
and crucially a non-upstream adapter with no allowlist still default-denies
(guards against the fix becoming a blanket fail-open). 6 new tests; relay +
authz + config-policy suites green (134 + 90).
Found via live staging debug of the Discord self-serve onboarding flow.
Ship the final pet-generation UX polish (provider picker behavior, step-2 cancel flow, banner integration, and visual consistency) and make saturated-chroma background removal C-op driven so hatch processing no longer hammers the machine during long runs.
The desktop installer clones with `--depth 1`, so a public install's local history often shares no merge-base with the freshly fetched origin tip. In that state `git rev-list HEAD..origin/<branch> --count` enumerates the entire remote ancestry and returns a meaningless huge number, surfacing as e.g. "v0.17.0 (+12104)" in the update indicator (NousResearch#51922). The official-SSH branch of checkUpdates() already sidesteps this by reporting a binary up-to-date check (`behind: currentSha === targetSha ? 0 : 1`), and hermes_cli/banner.py guards the identical class for the CLI banner. The passive desktop count path was the one place the shallow guard was missing. Detect shallow + no-merge-base up front and fall back to the same SHA-based binary check; full clones (developers / Docker dev images) keep the exact count path unchanged. The resolution logic lives in a pure update-count.cjs helper so it is unit-testable without booting Electron.
checkUpdates() ran `git rev-list HEAD..origin/<branch> --count` unconditionally in the parallel probe batch, even on the shallow + no-merge-base path where resolveBehindCount() ignores the result and falls back to a SHA compare. In the NousResearch#51922 failure mode that count walks the entire remote ancestry (thousands of commits), so the work was pure latency on every update check for the exact case the fix targets. Split the probes into two phases: resolve --is-shallow-repository and merge-base first, then run rev-list --count only when shouldCountCommits says the number is meaningful (full clone, or shallow-with-merge-base). The shallow/no-merge-base SHA fallback is preserved unchanged.
…ok hung On macOS, the desktop updater's stage 1 (hermes update --gateway) ends by restarting running gateways. launchd_restart() SIGTERMs the gateway and silently waits up to agent.restart_drain_timeout (default 180s) for the drain; the manual profile-gateway loop waits its drain budget per gateway the same way. Neither path prints anything before the wait, so the desktop updater's live output goes dead for minutes right after '✓ Update complete!' — users read it as a hung update and force-kill their gateway processes to make it move (NousResearch#44515). The systemd branch already announces its drain ('draining (up to Ns)...'); launchd and the manual loop did not. Print the stop/drain (with PID and budget) before the wait in both paths, mirroring the systemd branch, and assert the message in the existing launchd drain test. Fixes NousResearch#44515
…rofile
restartGateway, getActionStatus, getStatus, updateHermes and
checkHermesUpdate all hit window.hermesDesktop.api WITHOUT spreading
profileScoped() — unlike their siblings (getModelInfo, setModelAssignment,
grantComputerUsePermissions). _apiProfile tracks the active gateway
profile, and the Electron proxy uses request.profile to pick which pooled
/ remote backend serves the call.
So for a multi-profile or global-remote user, the System-panel "Restart
gateway" (and its status poll, plus Update / status reads) targeted the
primary/default backend instead of the one they're on: the restart hit
the wrong gateway and the poll never saw the action → it looked like
restart silently failed. Single-profile users are unaffected
(profileScoped() returns {} when no profile is active).
Add ...profileScoped() to the five backend-action helpers so they follow
the active profile like the rest of the API surface.
Two ways the update overlay read as stuck even though the update was streaming progress underneath: - In-app (macOS/Linux) UpdatesOverlay: runStreamedUpdate forwards every stdout line as a progress event with percent: null, and ingestProgress wrote that straight through — clobbering the milestone percents (10/60) so the bar fell back to indeterminate on every log line. Keep the last percent when a line carries null. - Staged install/update overlay: the bar is completedCount / totalCount, which counts only *finished* stages, so a long first stage pinned it at "0 of 2" / 0% until the stage ended. Count the running stage as half a unit so the bar advances during the stage (the per-stage spinner already shows which step is live). Both are display-only; no stage/event semantics change. (The Windows hermes-setup Tauri progress UI in apps/bootstrap-installer has the same counter-only-on-completion logic — parity follow-up.)
Remove cute/chibi-biased wording from base draft variations and explicitly preserve the requested mood across base and row prompts so scary, eerie, or other non-cute concepts are honored while keeping sprite constraints.
PR NousResearch#52151 hardened the runtime-status liveness check to trust a readable live process command line over stale gateway_state.json argv, so a recycled PID now owned by an s6 supervisor no longer counts as a running gateway. That fix is correct but incomplete for the reported symptom: the web dashboard showed a named profile's gateway green while `hermes -p <name> gateway status` showed it stopped. Two further issues: 1. Cross-profile PID reuse. In per-profile Docker supervision, one profile's stale `gateway_state.json` can record a PID the OS later recycled onto a DIFFERENT profile's live gateway. That PID's command line still `looks_like_gateway`, so the dead profile was reported running. The recorded argv has its `-p <name>` selector stripped in-process by `_apply_profile_override`, so it cannot disambiguate; the live `/proc` cmdline still carries it. `get_runtime_status_running_pid` now accepts an `expected_home` and validates the live command line belongs to THAT profile (mirroring `hermes_cli.gateway._matches_current_profile`, the logic the CLI scan path already uses — which is why the CLI was correct). `_check_gateway_running` passes the enumerated profile dir. 2. The existing regression test `test_gateway_running_check_falls_back_to_ runtime_state` used the live pytest PID with a gateway-shaped record; once the live cmdline became authoritative it no longer looked like a gateway. Updated to mock the live cmdline to the real separate-process scenario it describes. The active-profile path (`get_running_pid`) is intentionally left unscoped: it is lock-verified and any live gateway cmdline is acceptable there. Multiplex mode is unaffected — `running` state is only ever written to a gateway's own home, never a secondary served profile's. Adds coverage for: cross-profile PID reuse (named + default), matching profile cmdline (`-p`, `--profile`, explicit HERMES_HOME=), the bare default gateway, and the unreadable-cmdline cross-platform fallback. Each new cross-profile assertion fails without the profile scope and passes with it. Co-authored-by: helix4u <4317663+helix4u@users.noreply.github.com>
…in-announce fix(update): announce gateway drain waits so desktop updates don't look hung
…start-profile fix(desktop): route gateway restart / status / update to the active profile
…date-steps fix(desktop): stop the update overlay looking frozen while it works
Wire the sparkle generate button's cancel action to the same discard/reset path as step-2 cancel so abort semantics are consistent and always return to step 1 while retaining the prompt input.
Stabilize the long-running-tool heartbeat test by patching stale thresholds inside the test and asserting the heartbeat exceeds the idle ceiling, which preserves intent while removing scheduler-sensitive assumptions that flake in CI.
…allow-update-count fix(desktop): don't report a bogus update count for a shallow checkout
Pet generation: frame-perfect hatch flow, backend picker, CPU-safe chroma, and CI-hardening
…-progress-visibility fix(desktop): surface update progress lines
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-import |
214 |
unresolved-attribute |
195 |
invalid-argument-type |
73 |
invalid-assignment |
33 |
not-subscriptable |
33 |
unsupported-operator |
27 |
invalid-method-override |
20 |
unresolved-reference |
6 |
call-non-callable |
5 |
invalid-return-type |
5 |
not-iterable |
3 |
no-matching-overload |
3 |
unused-type-ignore-comment |
3 |
invalid-raise |
1 |
invalid-parameter-default |
1 |
First entries
plugins/platforms/feishu/adapter.py:4603: [unresolved-attribute] unresolved-attribute: Attribute `Application` is not defined on `None` in union `Unknown | None`
tests/cli/test_cli_pet_pane.py:23: [unresolved-import] unresolved-import: Cannot resolve imported module `PIL`
tests/gateway/relay/test_relay_going_idle.py:15: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
plugins/platforms/email/adapter.py:839: [invalid-method-override] invalid-method-override: Invalid override of method `send_document`: Definition is incompatible with `BasePlatformAdapter.send_document`
tests/tools/test_signal_media.py:186: [invalid-assignment] invalid-assignment: Object of type `((...) -> Awaitable[dict[Unknown, Unknown]]) | None` is not assignable to attribute `standalone_sender_fn` on type `PlatformEntry | None`
tests/tools/test_async_delegation.py:421: [invalid-argument-type] invalid-argument-type: Argument to function `AIAgent._dispatch_delegate_task` is incorrect: Expected `AIAgent`, found `_FakeAgent`
tests/run_agent/test_session_source.py:1: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
gateway/restart.py:16: [invalid-argument-type] invalid-argument-type: Argument to constructor `float.__new__` is incorrect: Expected `str | Buffer | SupportsFloat | SupportsIndex`, found `Unknown | int | str | ... omitted 16 union elements`
tests/agent/test_pet_generate.py:258: [unresolved-attribute] unresolved-attribute: Attribute `generated` is not defined on `None` in union `InstalledPet | None`
tests/hermes_cli/test_aux_config.py:54: [unresolved-attribute] unresolved-attribute: Attribute `keys` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int`, `float` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 34 union elements`
plugins/platforms/dingtalk/adapter.py:1546: [unresolved-import] unresolved-import: Cannot resolve imported module `httpx`
tests/plugins/model_providers/test_ollama_cloud_profile.py:15: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/agent/test_openrouter_response_cache.py:232: [not-subscriptable] not-subscriptable: Cannot subscript object of type `float` with no `__getitem__` method
tests/tools/test_mcp_elicitation.py:14: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_setup_noninteractive.py:122: [not-subscriptable] not-subscriptable: Cannot subscript object of type `float` with no `__getitem__` method
plugins/memory/mem0/_setup.py:550: [unresolved-import] unresolved-import: Cannot resolve imported module `psycopg2`
plugins/platforms/dingtalk/adapter.py:84: [unresolved-import] unresolved-import: Cannot resolve imported module `alibabacloud_tea_openapi`
plugins/platforms/email/adapter.py:295: [invalid-argument-type] invalid-argument-type: Argument to function `cache_document_from_bytes` is incorrect: Expected `bytes`, found `(Message[str, str] & ~AlwaysFalsy) | (bytes & ~AlwaysFalsy) | (Any & ~AlwaysFalsy)`
plugins/platforms/slack/adapter.py:94: [unresolved-import] unresolved-import: Cannot resolve imported module `slack_sdk.web.async_client`
plugins/platforms/raft/adapter.py:476: [unresolved-attribute] unresolved-attribute: Attribute `Application` is not defined on `None` in union `Unknown | None`
tests/agent/test_compression_rotation_state.py:56: [unresolved-attribute] unresolved-attribute: Unresolved attribute `context_compressor` on type `AIAgent`
tests/plugins/memory/test_mem0_setup.py:31: [unresolved-attribute] unresolved-attribute: Unresolved attribute `config` on type `ModuleType`
tests/hermes_cli/test_provider_parity.py:14: [unresolved-import] unresolved-import: Cannot resolve imported module `fastapi.testclient`
plugins/platforms/dingtalk/adapter.py:1448: [unresolved-attribute] unresolved-attribute: Attribute `data` is not defined on `None` in union `Unknown | None`
tests/agent/test_curator.py:1105: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["curator"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 34 union elements`
... and 597 more
✅ Fixed issues (270):
| Rule | Count |
|---|---|
unresolved-attribute |
108 |
unresolved-import |
69 |
invalid-argument-type |
32 |
invalid-method-override |
18 |
invalid-assignment |
15 |
unsupported-operator |
14 |
invalid-return-type |
4 |
call-non-callable |
3 |
unresolved-reference |
2 |
invalid-raise |
1 |
no-matching-overload |
1 |
unresolved-global |
1 |
unused-type-ignore-comment |
1 |
not-subscriptable |
1 |
First entries
tests/run_agent/test_credits_notices_toggle.py:76: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to attribute `_credits_session_start_micros` of type `int`
gateway/platforms/dingtalk.py:73: [unresolved-import] unresolved-import: Cannot resolve imported module `alibabacloud_dingtalk.robot_1_0`
gateway/platforms/feishu.py:5073: [unresolved-attribute] unresolved-attribute: Attribute `Client` is not defined on `None` in union `Unknown | None`
tests/tools/test_web_providers.py:218: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["search_backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 33 union elements`
gateway/platforms/wecom_callback.py:139: [unresolved-attribute] unresolved-attribute: Attribute `AppRunner` is not defined on `None` in union `Unknown | None`
gateway/platforms/telegram.py:6014: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_last_chunk_len` on type `MessageEvent`
tools/delegate_tool.py:2308: [invalid-argument-type] invalid-argument-type: Argument to function `dispatch_async_delegation` is incorrect: Expected `list[str] | None`, found `(Any & ~AlwaysFalsy) | (str & ~AlwaysFalsy) | list[str] | None`
agent/background_review.py:527: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `str`, found `(Any & ~AlwaysFalsy & ~Literal["codex_app_server"]) | None | Literal["codex_responses"]`
gateway/platforms/telegram.py:4168: [unresolved-attribute] unresolved-attribute: Attribute `HTML` is not defined on `None` in union `Unknown | None`
gateway/platforms/whatsapp.py:347: [invalid-argument-type] invalid-argument-type: Argument to constructor `Path.__new__` is incorrect: Expected `str | PathLike[str]`, found `str | None`
gateway/platforms/feishu.py:4606: [unresolved-attribute] unresolved-attribute: Attribute `TCPSite` is not defined on `None` in union `Unknown | None`
gateway/platforms/telegram.py:2096: [unresolved-attribute] unresolved-attribute: Attribute `PHOTO` is not defined on `None` in union `Unknown | None`
tools/delegate_tool.py:2470: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> dict[str, str | None | list[str]], (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[dict[str, str | None | list[str]]]]` cannot be called with key of type `str` on object of type `list[dict[str, str | None | list[str]]]`
gateway/platforms/matrix.py:4077: [unresolved-attribute] unresolved-attribute: Attribute `group` is not defined on `None` in union `Match[str] | None`
agent/google_oauth.py:235: [unresolved-attribute] unresolved-attribute: Module `msvcrt` has no member `locking`
gateway/platforms/slack.py:1964: [invalid-method-override] invalid-method-override: Invalid override of method `send_document`: Definition is incompatible with `BasePlatformAdapter.send_document`
tools/delegate_tool.py:2470: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> dict[str, str | None | list[str]], (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[dict[str, str | None | list[str]]]]` cannot be called with key of type `None` on object of type `list[dict[str, str | None | list[str]]]`
gateway/platforms/matrix.py:1891: [invalid-method-override] invalid-method-override: Invalid override of method `send_video`: Definition is incompatible with `BasePlatformAdapter.send_video`
gateway/platforms/telegram.py:4988: [unresolved-attribute] unresolved-attribute: Attribute `CHANNEL` is not defined on `None` in union `Unknown | None`
gateway/platforms/telegram.py:2096: [unresolved-attribute] unresolved-attribute: Attribute `VOICE` is not defined on `None` in union `Unknown | None`
gateway/platforms/dingtalk.py:1192: [unresolved-attribute] unresolved-attribute: Attribute `StreamingUpdateRequest` is not defined on `None` in union `Unknown | None`
gateway/platforms/telegram.py:2092: [unresolved-attribute] unresolved-attribute: Attribute `LOCATION` is not defined on `None` in union `Unknown | None`
hermes_cli/gateway.py:5158: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to `def qr_scan_for_bot_info(*, timeout_seconds: int = 300) -> dict[str, str] | None`
gateway/platforms/telegram.py:2387: [unresolved-import] unresolved-import: Cannot resolve imported module `telegram.error`
plugins/memory/mem0/__init__.py:175: [unresolved-import] unresolved-import: Cannot resolve imported module `mem0`
... and 245 more
Unchanged: 5316 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
luxles
force-pushed
the
fix/get-repo-tree-retry
branch
2 times, most recently
from
June 25, 2026 05:57
90fc038 to
f260431
Compare
… on network timeout _get_repo_tree was the only GitHub API caller in GitHubSource still using bare httpx.get() calls without retry. When the GitHub API is unreachable through a proxy (intermittent SSL/TLS failures, VPS packet loss, or corporate firewall), the request hung on ssl.recv() until Ctrl+C. Changes: 1. _github_get: support max_retries=0 (infinite retry) with while True loop 2. _get_repo_tree: switch both HTTP calls to self._github_get(max_retries=0) 3. do_inspect (CLI layer): background heartbeat with increasing intervals (0s -> 5s -> 15s -> 30s -> 60s -> every 30s), Ctrl+C to cancel 4. Tests: transport failure, non-200 status, happy path Unlike PR NousResearch#41195 (parallel_search_sources ThreadPoolExecutor hang for browse/search), this addresses the transport-level _get_repo_tree hang that affects inspect/fetch/install. Closes NousResearch#41053
luxles
force-pushed
the
fix/get-repo-tree-retry
branch
from
June 25, 2026 07:00
f260431 to
590bd7c
Compare
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.
Bug Description
When the GitHub API is unreachable through a proxy (intermittent SSL/TLS failures, VPS packet loss, or corporate firewall),
hermes skills inspect/install/fetchhang indefinitely onssl.recv()until Ctrl+C.Reported in NousResearch#41053 (user in a network-restricted environment).
Existing Fixes
PR NousResearch#41195 (open) fixes the
parallel_search_sourcesThreadPoolExecutor hang inbrowse/searchby changingshutdown(wait=True)towait=False, cancel_futures=True. That fix is complementary to this one.PR NousResearch#41811 was closed as duplicate of NousResearch#41195.
This Fix
_get_repo_tree()was the only GitHub API caller inGitHubSourcestill using barehttpx.get()calls without retry. This PR switches both HTTP calls (repo info + git tree) to the existing_github_get()wrapper.Key design decisions:
max_retries=0(infinite retry): Retries forever until the request succeeds or the user presses Ctrl+C. The CLI never hangs silently.Fetching owner/repo tree... attempt 3, 4s timeout (Ctrl+C to cancel)) so the user sees activity and knows how to cancel.Changes
tools/skills_hub.py:_github_getgainsmax_retries=0(infinite retry) andretry_labelfor stderr progress;_get_repo_treeuses both.tests/tools/test_skills_hub.py: Three new tests (transport failure, non-200 status, happy path).Closes NousResearch#41053