fix(delegate): tolerate non-string tool message content (#28639) - #1
fix(delegate): tolerate non-string tool message content (#28639)#1Tranquil-Flow wants to merge 4098 commits into
Conversation
resolve_xai_oauth_runtime_credentials() called _refresh_xai_oauth_tokens() with no try/except. A terminal refresh failure (HTTP 400/401/403 — invalid_grant, token revoked) propagated without clearing the dead access_token / refresh_token from auth.json, causing every subsequent session to retry the same doomed network request. Add a try/except around the refresh call that mirrors the existing credential_pool.py quarantine: when _is_terminal_xai_oauth_refresh_error identifies a non-retryable failure, clear the dead token fields from auth.json and write a last_auth_error diagnostic marker so future calls fail fast with a clear relogin_required error instead of hitting the network. active_provider is preserved (set_active=False) so multi-provider users whose chosen provider is not xai-oauth are unaffected. Tests: two new cases in test_auth_xai_oauth_provider.py cover terminal quarantine and transient pass-through.
…prompts (NousResearch#27644) The background review prompts (_SKILL_REVIEW_PROMPT and _COMBINED_REVIEW_PROMPT) now include explicit protection rules for bundled, hub-installed, and pinned skills — aligning with the curator's existing policy at curator.py L345/350. Before this change, bg-review could freely rewrite bundled skills like 'hermes-agent' or pinned skills, while the 7-day curator explicitly skips them. The review agent now sees: • Bundled skills (shipped with Hermes) • Hub-installed skills (installed via hermes skills install) • Pinned skills (marked via hermes curator pin) If only protected skills need updating, the review says 'Nothing to save.' and stops. Fixes NousResearch#27644
The dashboard's main column is `relative z-2` (App.tsx), which creates a stacking context that traps fixed descendants below the app sidebar (`z-50`). `ModelPickerDialog` renders `fixed inset-0 z-[100]` inline, so its z-100 is scoped to z-2 and the sidebar covers its left edge. The bug is visible across all themes but only obvious in the Large theme variants (Hermes Teal (Large), etc.) where the larger root font widens the dialog into the sidebar's column. Toast.tsx already documents the same trap and uses the same `createPortal(..., document.body)` escape. This commit ports the picker; the same pattern affects other inline z-[100] modals in the dashboard (OAuthLoginModal, Cron / Models / Profiles page modals) and is left for a follow-up — keeping this PR scoped to the reporter's specific case. Fixes NousResearch#28103 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the gateway receives SIGUSR1 (graceful restart via launchd_restart), the SIGUSR1 handler calls request_restart(via_service=True) and the gateway shuts down cleanly with exit code 0. However, the generated launchd plist uses KeepAlive → SuccessfulExit → false, meaning launchd only relaunches on *non-zero* exit codes. A clean exit(0) is treated as "successful, don't restart", so the gateway stays down after /restart, /update, or SIGUSR1. The systemd unit template already uses RestartForceExitStatus=75 for the same scenario. Mirror that convention: when _restart_via_service is True, raise SystemExit(75) so launchd's SuccessfulExit=false policy triggers a relaunch. Closes NousResearch#28135
Two code paths call json.loads() on output from external tools without catching JSONDecodeError. If the tool returns a non-JSON string (error message, empty string, or None), the entire call path crashes. 1. gateway/run.py — text_to_speech_tool() result in voice reply path. A TTS failure that returns an error string instead of JSON crashes the voice reply handler, killing the message response entirely. 2. cron/scheduler.py — skill_view() result when loading skills for cron jobs. A corrupted or missing skill file that returns an error string instead of JSON crashes the cron tick, preventing all jobs from executing that cycle. Both fixes catch (json.JSONDecodeError, TypeError), log a warning, and gracefully skip the failed operation instead of crashing.
…sections Two related bugs in gateway/config.py prevented per-platform gateway_restart_notification from working through config.yaml: 1. The shared-key bridging loop (load_gateway_config) omitted 'gateway_restart_notification', so the key never landed in platform_data['extra'] even when set under e.g. 'discord:' or 'mattermost:' sections. 2. PlatformConfig.from_dict() only read gateway_restart_notification from the top-level data dict, ignoring the 'extra' sub-dict where bridged keys are stored. Fix: add the key to the bridging loop, and add an 'extra' fallback in from_dict() so that round-tripped values (YAML → bridged → extra → from_dict) resolve correctly. Impact: users can now set gateway_restart_notification: false per platform in config.yaml instead of relying on env vars or the global platforms: block.
When the kanban auto-decomposer fans a triage task into child tasks, recompute_ready() immediately promotes parent-free children to 'ready' so the dispatcher picks them up. Some users want a manual workflow where children stay in 'todo' for review before dispatch. Add 'kanban.auto_promote_children' config key (default: true): - false: children stay in 'todo' after decomposition - true: existing behavior (auto-promote to 'ready') Changes: - kanban_db.py: decompose_triage_task() gains auto_promote param - kanban_decompose.py: reads auto_promote_children from config - kanban dashboard API: exposes the new setting in GET/PUT /orchestration Closes NousResearch#28016
…mespace The conversation_loop.py references _pool_may_recover_from_rate_limit which was defined in run_agent.py. After the conversation-loop extraction refactor, the helper was no longer in the same module scope. Wrap the call as _ra()._pool_may_recover_from_rate_limit() to route through the run_agent monkeypatch namespace where the helper is available. Adds regression test in test_gemini_fast_fallback.py. Fixes: MAILROOM Email Triage NameError, OPS Execution Monitor NameError.
Qwen3.x and DeepSeek-V3.x default to chatty/hallucinatory tool use without enforcement steering — agents narrate "calling tool X" without actually emitting a tool call, or run partial loops. Both model families fit the same failure pattern TOOL_USE_ENFORCEMENT_GUIDANCE was already injected for (gpt, codex, gemini, gemma, grok, glm). Co-authored-by: briandevans <252620095+briandevans@users.noreply.github.com> Squashed salvage of: - 403e567 fix(agent): add qwen and deepseek to TOOL_USE_ENFORCEMENT_MODELS - 9433eab test(agent): use realistic qwen-plus identifier in enforcement test Fixes NousResearch#28079.
The _SLACK_TARGET_RE regex only matched IDs starting with C (channel), G (group), or D (direct message). Slack user IDs start with U, causing 'Could not resolve' errors when trying to send DMs to specific users. Changes: - Expand _SLACK_TARGET_RE to accept U-prefixed IDs (user IDs) - Add conversations.open fallback to resolve user IDs to DM channel IDs before sending, since chat.postMessage requires a conversation ID Fixes #ISSUE_NUMBER
…t-found Three related fixes for the MEDIA:<path> extraction pipeline that caused 'file not found' noise in platform channels: 1. run.py — tighten tool-result MEDIA regex from \S+ (any non- whitespace) to require a path pattern with known extensions. Prevents LLM-generated placeholder paths like 'MEDIA:/path/to/example.mp4' from being captured as real media. 2. base.py — remove the |\S+ fallback in extract_media() that catches anything non-whitespace as a potential MEDIA path. This was the primary cause of false positives — strings like '' in tool output were captured as MEDIA: paths. 3. mattermost.py — replace the file-not-found error message sent to the channel with a silent logger.warning() skip. When a path extracted by MEDIA doesn't exist on disk, the channel no longer gets a noisy '(file not found: ...)' message. Impact: eliminates the persistent 'file not found' spam in Mattermost channels caused by over-broad MEDIA regex patterns matching non-path text in tool output.
…dpoint xAI's token endpoint returns HTTP 403 to the OAuth grant when the account isn't on the allowlist for API access (e.g. standard SuperGrok subscribers — see NousResearch#26847). Treating it like a stale-token 400/401 made ``format_auth_error`` append "Run ``hermes model`` to re-authenticate", which is misleading because re-login can't change xAI's tier decision. Split 403 off in both ``refresh_xai_oauth_pure`` and the loopback login token exchange: * New error code ``xai_oauth_tier_denied`` with ``relogin_required=False`` * Message explains the entitlement gate and points at the ``XAI_API_KEY`` + ``provider: xai`` fallback * 400/401 still set ``relogin_required=True`` as before * 5xx still set ``relogin_required=False`` as before
…resh-loop
The existing ``_is_entitlement_failure`` heuristic only fires when
the response body contains specific substrings ("do not have an
active Grok subscription", etc.). xAI has been seen to 403 standard
SuperGrok subscribers with a terser body that doesn't match those
keywords (NousResearch#26847), and the recovery path would then mint a fresh
token, get a fresh 403, and loop until Ctrl+C.
Add a defense-in-depth check at the recovery call site: any 403 on
``provider == "xai-oauth"`` short-circuits ``try_refresh_current``
so the error surfaces immediately with the friendly hint from
``_summarize_api_error``. Keeps the existing keyword path for all
other providers untouched.
…Research#26847 Tests: * ``test_refresh_xai_oauth_pure_403_marked_tier_denied_not_relogin`` — refresh-403 raises ``xai_oauth_tier_denied`` with ``relogin_required=False`` and the API-key fallback hint in body. * ``test_format_auth_error_tier_denied_does_not_suggest_relogin`` — the renderer does not append "Run ``hermes model``" for the new code. * ``test_recover_with_credential_pool_skips_refresh_on_bare_403_for_xai_oauth`` — bare ``{"reason":"forbidden","message":"Forbidden"}`` body (which does not match the existing keyword heuristic) still short-circuits ``try_refresh_current`` on xai-oauth. Docs: * Drop the "(any active tier)" claim from the xai-grok-oauth guide, add a top-of-page warning callout, and a Troubleshooting section for the 403-after-login case pointing at ``XAI_API_KEY`` + ``provider: xai`` as the documented fallback.
Two Mattermost thread-related bugs: 1. _resolve_root_id() — Mattermost CRT requires root_id to be the thread root post. Using any reply's own ID as root_id causes '400 Invalid RootId'. Add _resolve_root_id() that walks up the post chain via API to find the actual root, and apply it in send(), _send_url_as_file(), and _send_local_file(). 2. _progress_reply_to — The condition in run.py only checked Platform.FEISHU, missing Mattermost entirely. This caused tool progress messages to always land in the main channel instead of the thread. Add Platform.MATTERMOST to the condition so progress messages are routed to threads when reply_mode=thread. Impact: Tool progress messages now appear in Mattermost threads instead of flooding the main channel; thread replies no longer fail with Invalid RootId when the reply target is itself a reply.
Salvages NousResearch#19964 by @Beandon13. Adds `hermes kanban archive --rm` to permanently remove already-archived tasks with cascading cleanup of links, comments, events, runs, and notify-subs. Safety guard: only archived tasks can be deleted; active/blocked/done must be archived first. Cherry-picked from NousResearch#19964 onto current main (severe stale base, applied manually to preserve substance only).
…pter salvage
xAI Grok OAuth (and Spotify) use a loopback redirect to ``http://127.0.0.1:<port>/callback`` to capture the authorization code. That works when the browser and Hermes run on the same machine, and the SSH tunnel recipe handles the regular remote case. It breaks completely on **browser-only remote consoles** (GCP Cloud Shell, GitHub Codespaces, AWS EC2 Instance Connect, Gitpod, Replit, …) where the user has a browser but no real SSH client to forward a port — the redirect to 127.0.0.1 on the remote VM simply isn't reachable from the laptop, and there's nothing the existing flow can do about it (NousResearch#26923). This commit adds the foundation for a manual-paste fallback: * ``_is_remote_session`` now also recognises Cloud Shell, Codespaces, Gitpod, Replit, StackBlitz (in addition to SSH), so the existing tunnel hint at least fires in those environments. * ``_parse_pasted_callback`` accepts any of: a full ``http(s)://...?code=...&state=...`` URL, a bare ``?code=...`` query string, a bare ``code=...&state=...`` fragment, or a bare opaque code value. Returns the same dict shape the HTTP callback handler produces, so the caller's state / error validation works unchanged (no CSRF bypass). * ``_prompt_manual_callback_paste`` reads stdin with a clear multi-line explanation of what's happening and what to paste. * ``_xai_oauth_loopback_login`` gains a ``manual_paste`` kwarg that skips the HTTP listener entirely. The redirect_uri, PKCE verifier, state, and nonce are byte-identical to the loopback path so xAI's token endpoint can't tell the difference at the protocol level. * ``_print_loopback_ssh_hint`` now also mentions ``--manual-paste`` so users without a real SSH client see a path forward instead of a dead-end tunnel recipe. * ``_login_xai_oauth`` threads ``args.manual_paste`` into the loopback helper.
…model`` Register the new ``--manual-paste`` flag on both entry points and thread it through to the xAI loopback login: * ``hermes auth add xai-oauth --manual-paste`` — pool-add path, forwarded inside ``auth_commands.handle_auth_add``. * ``hermes model --manual-paste`` — model-picker path, forwarded by ``_model_flow_xai_oauth`` into the synthetic ``argparse.Namespace`` it passes to ``_login_xai_oauth``. The picker also now forwards ``--no-browser`` and ``--timeout`` for consistency (previously hardcoded to defaults regardless of CLI flags). Help text on both flags points at NousResearch#26923 and names the browser-only remote consoles (Cloud Shell, Codespaces, EC2 Instance Connect) so users searching ``hermes --help`` can find the workaround.
…y path (NousResearch#26923) Tests (``tests/hermes_cli/test_auth_manual_paste.py``): * 9 parametrised + scalar cases for ``_is_remote_session`` covering the new Cloud Shell / Codespaces / Gitpod / Replit / StackBlitz env vars (plus the existing SSH ones). * 9 cases for ``_parse_pasted_callback`` covering every paste form (full URL, https URL with extra params, bare ``?code=...``, bare ``code=...`` fragment, bare opaque value, error+description, empty, whitespace-only, malformed URL). * 3 cases for ``_prompt_manual_callback_paste`` (happy path, EOF, Ctrl-C). * 3 end-to-end ``_xai_oauth_loopback_login(manual_paste=True)`` cases: the HTTP server MUST NOT be started (asserted via a callable that raises if invoked), wrong state still rejected with ``xai_state_mismatch`` (no CSRF bypass), and empty paste surfaces ``xai_code_missing``. * SSH-hint mention test ensures the ``--manual-paste`` instruction is printed in the remote-session hint. Docs: * ``oauth-over-ssh.md`` — new "Browser-only remote (Cloud Shell / Codespaces / EC2 Instance Connect)" section with the ``--manual-paste`` recipe, plus a TL;DR note for the new flag. * ``xai-grok-oauth.md`` — short subsection pointing at the same recipe and the OAuth-over-SSH guide anchor.
Bumps [python-multipart](https://github.com/Kludex/python-multipart) from 0.0.22 to 0.0.27. - [Release notes](https://github.com/Kludex/python-multipart/releases) - [Changelog](https://github.com/Kludex/python-multipart/blob/main/CHANGELOG.md) - [Commits](Kludex/python-multipart@0.0.22...0.0.27) --- updated-dependencies: - dependency-name: python-multipart dependency-version: 0.0.27 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [python-dotenv](https://github.com/theskumar/python-dotenv) from 1.2.1 to 1.2.2. - [Release notes](https://github.com/theskumar/python-dotenv/releases) - [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md) - [Commits](theskumar/python-dotenv@v1.2.1...v1.2.2) --- updated-dependencies: - dependency-name: python-dotenv dependency-version: 1.2.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.0 to 3.1.2. - [Release notes](https://github.com/fastify/fast-uri/releases) - [Commits](fastify/fast-uri@v3.1.0...v3.1.2) --- updated-dependencies: - dependency-name: fast-uri dependency-version: 3.1.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [@babel/plugin-transform-modules-systemjs](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-modules-systemjs) from 7.29.0 to 7.29.4. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.29.4/packages/babel-plugin-transform-modules-systemjs) --- updated-dependencies: - dependency-name: "@babel/plugin-transform-modules-systemjs" dependency-version: 7.29.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com>
* fix: update design system package, replace bg image, remove sync assets * fix(web): update bundled asset metadata * fix(web): normalize npm lockfile metadata * fix(nix): refresh npm lockfile hashes * chore(ci): trigger PR checks * fix(web): declare motion peer dependency * fix(nix): refresh npm lockfile hashes * chore(ci): trigger PR checks after dependency update * fix(web): restore cross-platform lockfile entries * fix(nix): refresh npm lockfile hashes * chore(ci): trigger PR checks after lockfile restore --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…ousResearch#28104) Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 5.2.3 to 5.2.4. - [Release notes](https://github.com/webpack/webpack-dev-server/releases) - [Changelog](https://github.com/webpack/webpack-dev-server/blob/main/CHANGELOG.md) - [Commits](webpack/webpack-dev-server@v5.2.3...v5.2.4) --- updated-dependencies: - dependency-name: webpack-dev-server dependency-version: 5.2.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [ws](https://github.com/websockets/ws) from 8.20.0 to 8.20.1. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](websockets/ws@8.20.0...8.20.1) --- updated-dependencies: - dependency-name: ws dependency-version: 8.20.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…search#24011) Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 11.13.0 to 11.15.0. - [Release notes](https://github.com/mermaid-js/mermaid/releases) - [Commits](https://github.com/mermaid-js/mermaid/compare/mermaid@11.13.0...mermaid@11.15.0) --- updated-dependencies: - dependency-name: mermaid dependency-version: 11.15.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Extends the previous commit to cover the remaining additive-column index that sits on the same migration trap: - ``task_events.run_id`` -> ``idx_events_run`` was still in SCHEMA_SQL. A legacy ``task_events`` table predating NousResearch#17805 (no ``run_id``) would still abort ``executescript`` before ``_migrate_add_optional_columns`` could add the column. Hoisted out of SCHEMA_SQL and made unconditional in the migration alongside the other three indexes. - Removed the now-redundant ``CREATE INDEX idx_tasks_idempotency`` that was nested inside the ``if "idempotency_key" not in cols`` branch. The unconditional create lower in the function makes it idempotent on both fresh and legacy DBs. - Strengthened the regression test to cover all four indexes (``idx_tasks_session_id``, ``idx_tasks_tenant``, ``idx_tasks_idempotency``, ``idx_events_run``) and to seed a pre-NousResearch#17805 ``task_events`` shape that exercises the ``run_id`` migration path. The result: every ``CREATE INDEX`` that depends on an additive column now runs after the migration ensures the column exists. Verified against a realistic pre-NousResearch#16081 board fixture (tasks + task_events both legacy shape) — origin/main reproduces ``no such column: session_id``; this branch migrates cleanly and creates all four indexes.
When discord.py is not installed at import time, DISCORD_AVAILABLE=False and the view class definitions at module bottom are skipped. check_discord_requirements() performs a lazy install and sets DISCORD_AVAILABLE=True but never re-ran the class definitions, causing NameError on the first button interaction (exec approval, slash confirm, etc.). Extract the five ui.View subclasses into _define_discord_view_classes() and call it both at module load (when discord.py is pre-installed) and inside check_discord_requirements() after a successful lazy install.
…tory-truncation fix(tui): render full assistant text in scrollback (no history truncation)
For PR NousResearch#28774 (firecrawl integration tag). Co-authored-by: alt-glitch <balyan.sid@gmail.com>
…ser and web providers" (NousResearch#28862) This reverts commit 273ff5c.
…usResearch#26670) (NousResearch#26677) * fix(update): detect concurrent hermes.exe on Windows; retry + restart-defer quarantine Closes NousResearch#26670. When 'hermes update' runs on Windows with another hermes.exe alive (most commonly the Hermes Desktop Electron app's spawned backend) _quarantine_running_hermes_exe() fails to rename the venv shim with [WinError 32]. uv pip install -e . then exits 2, the git-pull fast path is silently abandoned, and the ZIP fallback runs (and fails the same way) before eventually succeeding. This change implements three of the five proposed fixes from the issue: 1. Concurrent-instance detection (preferred fix). _detect_concurrent_hermes_instances() uses psutil to enumerate processes whose .exe is one of our venv shims (hermes.exe / hermes-gateway.exe), excluding the caller's PID. When any match exists, cmd_update prints an actionable message naming the blocking PIDs and exits 2 BEFORE any destructive work. New --force flag bypasses the gate. 2. Retry + restart-deferred fallback. _quarantine_running_hermes_exe() now retries the rename up to 4 times with 100/250/500/1000 ms backoff (covers the transient AV-scanner-handle case). If all retries fail, it schedules the replacement via MoveFileExW with the OS deferred-rename flag so the new shim can land at the original path and the update completes; the old image is fully unloaded after the user's next system restart. 3. Actionable warning text. The old 'Could not quarantine: [WinError 32]' warning is replaced with one that names the likely culprits (Hermes Desktop, REPLs, gateway, AV) and points to the new --force flag. Tests: - 13 new tests in tests/hermes_cli/test_update_concurrent_quarantine.py covering: psutil-based enumeration, self-pid exclusion, case-insensitive matching of .EXE, no-psutil graceful degradation, off-Windows no-op, helpful warning formatting, retry-then-succeed, restart-deferred fallback, cmd_update abort + exit code 2, and --force bypass. - New autouse fixture in tests/hermes_cli/conftest.py defaults _detect_concurrent_hermes_instances to [] so the rest of the suite isn't tripped by the developer's own running hermes.exe. Opt-out marker 'real_concurrent_gate' registered in pyproject.toml. - Updating docs page (website/docs/getting-started/updating.md) gains a short section explaining the new Windows error and remediation. * chore: refresh uv.lock to match pyproject.toml exact pins aiohttp 3.13.4 -> 3.13.3 (matches pyproject pin: aiohttp==3.13.3) anthropic 0.87.0 -> 0.86.0 (matches pyproject pin: anthropic==0.86.0) hermes-agent 0.13.0 -> 0.14.0 (matches pyproject version) CI's uv lock --check was failing on the merged state because main drifted: pyproject.toml uses exact == pins for those two deps and the hermes-agent version was bumped to 0.14.0 but the lockfile still had 0.13.0.
Apply CREATE_NO_WINDOW flags when the cron scheduler launches job scripts on Windows so gateway-managed no-agent cron jobs do not flash cmd or python console windows every tick.
Apply Windows CREATE_NO_WINDOW flags to foreground local terminal subprocesses and tracked background processes so Hermes operations do not flash or steal focus with extra console windows.
Preserve Windows profile install decisions across UAC handoff, avoid visible console windows by launching via pythonw, make repeated install/start idempotent, recreate stale Scheduled Tasks, and separate start-now from login auto-start behavior. Add Windows gateway regression coverage and systemd setup tests for the shared install flow.
…-Windows Linux/macOS CI runners don't have ctypes.windll, so the elevated-gateway test fails at module load. Adding raising=False lets monkeypatch install the mock attribute without first requiring it to exist.
…e_error_output (NousResearch#28639) When tool responses contain list or dict content (e.g. from browser or file tools), _looks_like_error_output crashed with AttributeError because it assumed all content is a string. Add isinstance guards to handle list and dict content gracefully. Fixes NousResearch#28639
🚨 CRITICAL Supply Chain Risk DetectedThis PR contains a pattern that has been used in real supply chain attacks. A maintainer must review the flagged code carefully before merging. 🚨 CRITICAL: Install-hook file added or modifiedThese files can execute code during package installation or interpreter startup. Files: Scanner only fires on high-signal indicators: .pth files, base64+exec/eval combos, subprocess with encoded commands, or install-hook files. Low-signal warnings were removed intentionally — if you're seeing this comment, the finding is worth inspecting. |
🔎 Lint report:
|
| Rule | Count |
|---|---|
PLW1514 |
2 |
First entries
scripts/auto_issue_fix_flow.py:115: [PLW1514] `pathlib.Path(...).read_text` without explicit `encoding` argument
scripts/auto_issue_fix_flow.py:1788: [PLW1514] `pathlib.Path(...).write_text` without explicit `encoding` argument
✅ Fixed issues (1149):
| Rule | Count |
|---|---|
F401 |
801 |
F841 |
124 |
E402 |
101 |
F541 |
37 |
E401 |
19 |
E741 |
16 |
F821 |
15 |
F811 |
13 |
E731 |
9 |
E701 |
7 |
E702 |
5 |
F601 |
2 |
First entries
../../../../../tmp/lint-base/tests/hermes_cli/test_logs.py:5: [F401] `pathlib.Path` imported but unused
../../../../../tmp/lint-base/tests/hermes_cli/test_webhook_cli.py:7: [F401] `pathlib.Path` imported but unused
../../../../../tmp/lint-base/tests/run_agent/test_413_compression.py:14: [F401] `uuid` imported but unused
../../../../../tmp/lint-base/tests/gateway/test_internal_event_bypass_pairing.py:12: [F401] `unittest.mock.patch` imported but unused
../../../../../tmp/lint-base/tests/hermes_cli/test_ollama_cloud_provider.py:3: [F401] `os` imported but unused
../../../../../tmp/lint-base/tests/acp/test_mcp_e2e.py:11: [F401] `collections.deque` imported but unused
../../../../../tmp/lint-base/tests/hermes_cli/test_profiles.py:8: [F401] `json` imported but unused
../../../../../tmp/lint-base/tests/gateway/test_sse_agent_cancel.py:10: [F401] `json` imported but unused
../../../../../tmp/lint-base/tests/run_agent/test_860_dedup.py:12: [F401] `sqlite3` imported but unused
../../../../../tmp/lint-base/tests/acp/test_server.py:24: [F401] `acp.schema.SetSessionConfigOptionResponse` imported but unused
../../../../../tmp/lint-base/tests/cron/test_cron_script.py:12: [F401] `stat` imported but unused
../../../../../tmp/lint-base/tests/hermes_cli/test_xiaomi_provider.py:12: [F401] `hermes_cli.auth.AuthError` imported but unused
../../../../../tmp/lint-base/tests/run_agent/test_run_agent.py:4021: [E731] Do not assign a `lambda` expression, use a `def`
../../../../../tmp/lint-base/tests/tools/test_file_read_guards.py:21: [F401] `tools.file_tools._get_max_read_chars` imported but unused
../../../../../tmp/lint-base/tests/hermes_cli/test_profile_export_credentials.py:9: [F401] `pathlib.Path` imported but unused
../../../../../tmp/lint-base/tests/tools/test_voice_mode.py:772: [F401] `tools.voice_mode.SAMPLE_RATE` imported but unused
../../../../../tmp/lint-base/tests/tools/test_skill_improvements.py:6: [F401] `unittest.mock.patch` imported but unused
../../../../../tmp/lint-base/tests/tools/test_delegate_toolset_scope.py:9: [F401] `unittest.mock.patch` imported but unused
../../../../../tmp/lint-base/tests/tools/test_daytona_environment.py:355: [F841] Local variable `env` is assigned to but never used
../../../../../tmp/lint-base/tests/tools/test_skill_size_limits.py:11: [F401] `unittest.mock.patch` imported but unused
../../../../../tmp/lint-base/tests/run_agent/test_agent_loop.py:25: [F401] `environments.agent_loop.ToolError` imported but unused; consider using `importlib.util.find_spec` to test for availability
../../../../../tmp/lint-base/tests/tools/test_voice_cli_integration.py:1209: [E401] Multiple imports on one line
../../../../../tmp/lint-base/tests/agent/test_rate_limit_tracker.py:193: [F401] `os` imported but unused
../../../../../tmp/lint-base/tests/test_mcp_serve.py:18: [F401] `unittest.mock.patch` imported but unused
../../../../../tmp/lint-base/environments/tool_call_parsers/deepseek_v3_1_parser.py:14: [F401] `typing.Optional` imported but unused
... and 1124 more
Unchanged: 0 pre-existing issues carried over.
ty (type checker)
Total: 8972 on HEAD, 5300 on base (🆕 +3672)
🆕 New issues (2175):
| Rule | Count |
|---|---|
unresolved-attribute |
651 |
unresolved-import |
608 |
invalid-argument-type |
464 |
invalid-assignment |
183 |
unsupported-operator |
58 |
invalid-method-override |
51 |
not-subscriptable |
37 |
invalid-parameter-default |
23 |
unused-type-ignore-comment |
19 |
invalid-return-type |
18 |
no-matching-overload |
13 |
unresolved-reference |
12 |
invalid-type-form |
10 |
call-non-callable |
10 |
unresolved-global |
5 |
| +6 more rules |
First entries
tests/agent/test_auxiliary_client.py:2771: [invalid-argument-type] invalid-argument-type: Argument to function `resolve_provider_client` is incorrect: Expected `str`, found `None`
run_agent.py:1954: [unresolved-attribute] unresolved-attribute: Object of type `~AlwaysFalsy` has no attribute `on_session_end`
agent/conversation_loop.py:3077: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `None` in union `dict[str, Any] | Unknown | None`
plugins/platforms/teams/adapter.py:56: [unresolved-import] unresolved-import: Cannot resolve imported module `microsoft_teams.api.models.adaptive_card`
tools/environments/base.py:127: [unresolved-attribute] unresolved-attribute: Attribute `write` is not defined on `None` in union `Any | IO[Unknown] | None`
tests/tools/test_browser_lightpanda.py:243: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> Unknown, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[Unknown]]` cannot be called with key of type `Literal["engine"]` on object of type `list[Unknown]`
tests/hermes_cli/test_detect_api_mode_for_url.py:76: [invalid-argument-type] invalid-argument-type: Argument to function `_detect_api_mode_for_url` is incorrect: Expected `str`, found `None`
tests/tools/test_delegate.py:2173: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> str, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[str]]` cannot be called with key of type `Literal["role"]` on object of type `list[str]`
tests/gateway/test_teams.py:12: [unresolved-import] unresolved-import: Cannot resolve imported module `httpx`
tests/stress/test_concurrency_mixed.py:132: [unresolved-attribute] unresolved-attribute: Attribute `id` is not defined on `None` in union `Run | None`
hermes_cli/kanban_db.py:4415: [invalid-parameter-default] invalid-parameter-default: Default value of type `None` is not assignable to annotated parameter type `int`
gateway/platforms/base.py:716: [invalid-return-type] invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `str`
tests/run_agent/test_run_agent_codex_responses.py:285: [unresolved-attribute] unresolved-attribute: Object of type `AIAgent` has no attribute `api_mode`
optional-skills/research/darwinian-evolver/templates/custom_problem_template.py:33: [unresolved-import] unresolved-import: Cannot resolve imported module `darwinian_evolver.learning_log`
tests/cron/test_cron_prompt_injection_skill.py:18: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
run_agent.py:2142: [unresolved-attribute] unresolved-attribute: Object of type `Self@_hydrate_todo_store` has no attribute `log_prefix`
tests/tools/test_file_tools.py:380: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> str, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[str]]` cannot be called with key of type `Literal["patch"]` on object of type `list[str]`
tests/gateway/test_matrix_exec_approval.py:30: [unresolved-attribute] unresolved-attribute: Object of type `bound method MatrixAdapter._send_reaction(room_id: str, event_id: str, emoji: str) -> CoroutineType[Any, Any, str | None]` has no attribute `await_count`
tests/test_yuanbao_pipeline.py:23: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
scripts/keystroke_diagnostic.py:40: [unresolved-import] unresolved-import: Cannot resolve imported module `prompt_toolkit.layout.controls`
run_agent.py:1885: [unresolved-attribute] unresolved-attribute: Object of type `Self@_check_openrouter_cache_status` has no attribute `_or_cache_hits`
tests/hermes_cli/test_kanban_decompose_db.py:67: [unresolved-attribute] unresolved-attribute: Attribute `status` is not defined on `None` in union `Task | None`
tests/tools/test_delegate.py:2144: [invalid-argument-type] invalid-argument-type: Argument to function `delegate_task` is incorrect: Expected `int | None`, found `str | Unknown`
cli.py:9797: [unresolved-attribute] unresolved-attribute: Object of type `AIAgent` has no attribute `tools`
tests/tools/test_web_tools_config.py:457: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> LiteralString, (key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str]` cannot be called with key of type `Literal["properties"]` on object of type `str`
... and 2150 more
✅ Fixed issues (360):
| Rule | Count |
|---|---|
invalid-argument-type |
120 |
unresolved-import |
87 |
invalid-assignment |
51 |
unresolved-attribute |
42 |
unsupported-operator |
13 |
not-iterable |
7 |
invalid-method-override |
7 |
unresolved-reference |
6 |
too-many-positional-arguments |
5 |
not-subscriptable |
5 |
invalid-parameter-default |
4 |
no-matching-overload |
4 |
invalid-return-type |
4 |
invalid-raise |
3 |
call-non-callable |
1 |
| +1 more rules |
First entries
gateway/session.py:1025: [invalid-argument-type] invalid-argument-type: Argument to bound method `SessionDB.create_session` is incorrect: Expected `dict[str, Any]`, found `str | None`
tools/rl_training_tool.py:778: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["wandb_name"]` and value of type `Any & ~AlwaysFalsy` on object of type `list[dict[str, str | int | float]]`
tests/run_agent/test_agent_loop_tool_calling.py:63: [invalid-parameter-default] invalid-parameter-default: Default value of type `None` is not assignable to annotated parameter type `str`
tests/environments/benchmarks/test_terminalbench2_env_security.py:10: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
run_agent.py:1145: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["default_headers"]` and value of type `(str & ~AlwaysFalsy) | (Any & ~AlwaysFalsy) | dict[Unknown, Unknown]` on object of type `dict[str, str]`
run_agent.py:11425: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to `list[dict[str, Any]]`
run_agent.py:10303: [invalid-argument-type] invalid-argument-type: Argument to function `_is_oauth_token` is incorrect: Expected `str`, found `str | dict[Unknown | str, Unknown | str] | Any | dict[Unknown, Unknown] | int`
environments/benchmarks/terminalbench_2/terminalbench2_env.py:58: [unresolved-import] unresolved-import: Cannot resolve imported module `atroposlib.envs.server_handling.server_manager`
environments/benchmarks/terminalbench_2/terminalbench2_env.py:317: [unresolved-import] unresolved-import: Cannot resolve imported module `datasets`
environments/agent_loop.py:143: [unresolved-reference] unresolved-reference: Name `BudgetConfig` used when not defined
agent/copilot_acp_client.py:389: [not-iterable] not-iterable: Object of type `IO[str] | None` may not be iterable
gateway/platforms/api_server.py:1921: [too-many-positional-arguments] too-many-positional-arguments: Too many positional arguments to bound method `trigger_job`: expected 1, got 2
gateway/platforms/api_server.py:1709: [invalid-assignment] invalid-assignment: Object of type `staticmethod[(job_id: str), dict[str, Any] | None]` is not assignable to `def trigger_job(job_id: str) -> dict[str, Any] | None`
environments/web_research_env.py:67: [unresolved-import] unresolved-import: Cannot resolve imported module `atroposlib.envs.base`
run_agent.py:10979: [invalid-argument-type] invalid-argument-type: Argument to function `normalize_anthropic_response` is incorrect: Expected `bool`, found `int | str | Any | dict[Unknown | str, Unknown | str] | dict[Unknown, Unknown]`
environments/benchmarks/tblite/tblite_env.py:35: [unresolved-import] unresolved-import: Cannot resolve imported module `atroposlib.envs.base`
environments/benchmarks/yc_bench/yc_bench_env.py:62: [unresolved-import] unresolved-import: Cannot resolve imported module `atroposlib.envs.base`
rl_cli.py:27: [unresolved-import] unresolved-import: Cannot resolve imported module `fire`
tests/run_agent/test_agent_loop_tool_calling.py:66: [unresolved-import] unresolved-import: Cannot resolve imported module `atroposlib.envs.server_handling.server_manager`
tests/tools/test_rl_training_tool.py:49: [unresolved-attribute] unresolved-attribute: Object of type `RunState` has no attribute `api_log_file`
tests/run_agent/test_agent_loop.py:380: [unresolved-attribute] unresolved-attribute: Unresolved attribute `get_state` on type `MockServer`
tests/run_agent/test_agent_loop_tool_calling.py:65: [unresolved-import] unresolved-import: Cannot resolve imported module `atroposlib.envs.server_handling.openai_server`
cli.py:5580: [invalid-argument-type] invalid-argument-type: Argument to function `build_welcome_banner` is incorrect: Expected `int`, found `None | Unknown | int`
hermes_cli/main.py:7960: [unresolved-reference] unresolved-reference: Name `sys` used when not defined
cli.py:6906: [invalid-argument-type] invalid-argument-type: Argument to function `get_tool_definitions` is incorrect: Expected `list[str]`, found `list[str] | None`
... and 335 more
Unchanged: 2552 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
…easibility check (NousResearch#12977) Two-part fix in agent/conversation_compression.check_compression_model_feasibility (after the main-init refactor moved the function out of run_agent): 1. When the aux compression model matches the main model (same name + base_url, which happens when no separate compression model is configured), fall back to self._config_context_length for the aux model's get_model_context_length call. Without this fallback the aux probe re-detects context for the same endpoint and ignores custom_providers.models.context_length. 2. Re-derive the main model's compression threshold from get_model_context_length instead of reading the potentially-stale threshold_tokens from the compressor — important when custom_providers context_length is loaded after the compressor was originally constructed. (Fix #1 from the original PR — propagating _config_context_length after custom_providers resolution in __init__ — is already integrated in agent/agent_init.py line 1227, after the custom_providers branch.) Fixes NousResearch#12977
…NousResearch#31416) PR NousResearch#31416 (avoid persisting borrowed credential secrets) added sanitize_borrowed_credential_payload, which strips access_token from any auth.json pool entry whose (provider, source) isn't in the _PERSISTABLE_PROVIDER_SOURCES allowlist. (copilot, gh_cli) is borrowed (not in the allowlist), so the test fixture's pre-seeded access_token now gets stripped at load_pool() time, leaving the pool empty. resolve_target('1') then fails with 'No credential #1. Provider: copilot.' Fix: align the test with the new contract. At runtime, copilot tokens are hydrated by resolve_copilot_token() — mock that path so the pool gets an entry the test can remove. The behavior under test (suppression of gh_cli + env variants on remove) is unchanged. CI repro on origin/main HEAD; reproduced locally with stock checkout.
…s reached After key #1 is marked exhausted the retry still called the API with key #1 due to env-var bias in _get_cached_client / resolve_api_key_provider_credentials. Fix: peek the pool and pass the active entry's key as explicit_api_key. Secondary: api_key_hint in mark_exhausted_and_rotate pins the correct entry under concurrent CLI+gateway calls; _is_payment_error matches GoUsageLimitError; extract_api_error_context parses "Resets in Xhr Ymin".
Blocking #1 — gateway-connecting-overlay.tsx reduced-motion regression: the top `if (reduce) setPhase('gone')` fired unconditionally on mount whenever reduce-motion was on, so every OS reduced-motion user lost the CONNECTING overlay during cold boot entirely (jumped to 'gone' before the gateway was even open). The intent was to skip the exit *choreography*, not to skip showing the overlay. Removed the unconditional top block and the redundant nested preview block; kept only the third branch (`gatewayState === 'open' && shownRef.current` → `reduce ? 'gone' : 'text-out'`) which correctly gates the short-circuit on connect. Also fixed `if(reduce)` missing-space, 6-space misindent, and the same 3-line comment pasted three times. Nit #1 — tsconfig excludes e2e, so specs were never typechecked in CI. Added tsconfig.e2e.json (extends base, includes e2e/ + playwright.config.ts, adds @playwright/test types) and wired it into the typecheck script. This surfaced three latent type errors that are fixed in the same commit: - fix-electron-tracing.ts: `app._context` and `electron._playwright` are private APIs — added `as any` on the access before the existing cast. - playwright.config.ts: `reducedMotion: 'reduce'` directly under `use:` is not a valid UseOptions property in playwright 1.58; it's a BrowserContextOption accessed via `contextOptions: { reducedMotion: 'reduce' }`. The old form was silently ignored at runtime, so reduced-motion emulation wasn't actually active — screenshots could catch overlays mid-fade (exactly what the comment warned about). Nit #2 — fix-electron-tracing.ts reaches into Playwright internals (_playwright, _allContexts, _context) with no public contract. Added a header comment calling out the `@playwright/test` exact pin (=1.58.2) so a future bump knows to re-verify the private symbols still exist. Nit NousResearch#3 — main.ts TEST_WORKER_INDEX block had stray 6-space indentation. Verified: tsc -p . && tsconfig.electron && tsconfig.e2e → 0 errors; vitest boot-failure-overlay (3/3) + boot-failure-reauth (21/21) pass; npm run build clean; playwright e2e/boot-failure.spec.ts 2/2 pass.
…rst run The first-run provider picker showed Fireworks AI alongside Nous Portal before the user opened the 'Other providers' disclosure. Only Nous Portal should be visible up front; Fireworks now lives inside the expanded list but keeps its #1 position there (Nous -> Fireworks ordering preserved).
Summary
Fixes NousResearch#28639
When tool responses contain list or dict content (e.g. from browser or file tools),
_looks_like_error_outputintools/delegate_tool.pycrashed withAttributeErrorbecause it assumed all content is a string.Changes
tools/delegate_tool.py(+6 -1):strtype annotation from_looks_like_error_outputparameter to accept any typeisinstanceguard in_looks_like_error_output: coerce non-string content viastr()before inspectingisinstanceguard in_run_with_thread_capturetool-trace assembly: coerce non-string tool message content before passing to_looks_like_error_outputtests/test_delegate_tool_trace.py(new, 132 lines):_looks_like_error_outputwith string, list, dict, number, and nested contentTesting
Verification