Skip to content

fix(delegate): tolerate non-string tool message content (#28639) - #1

Closed
Tranquil-Flow wants to merge 4098 commits into
mainfrom
fix/28639-delegate-list-content
Closed

fix(delegate): tolerate non-string tool message content (#28639)#1
Tranquil-Flow wants to merge 4098 commits into
mainfrom
fix/28639-delegate-list-content

Conversation

@Tranquil-Flow

Copy link
Copy Markdown
Owner

Summary

Fixes NousResearch#28639

When tool responses contain list or dict content (e.g. from browser or file tools), _looks_like_error_output in tools/delegate_tool.py crashed with AttributeError because it assumed all content is a string.

Changes

tools/delegate_tool.py (+6 -1):

  • Remove str type annotation from _looks_like_error_output parameter to accept any type
  • Add isinstance guard in _looks_like_error_output: coerce non-string content via str() before inspecting
  • Add isinstance guard in _run_with_thread_capture tool-trace assembly: coerce non-string tool message content before passing to _looks_like_error_output

tests/test_delegate_tool_trace.py (new, 132 lines):

Testing

  • All new tests pass
  • Fail-without-fix verified: reverting the production change while keeping the test file causes test failures on the regression tests
  • No regressions in existing test suite

Verification

$ python3 -m pytest tests/test_delegate_tool_trace.py -v -o "addopts="
# All 18 tests pass

hehehe0803 and others added 30 commits May 18, 2026 20:01
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).
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.
dependabot Bot and others added 22 commits May 19, 2026 03:43
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>
…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
@github-actions

Copy link
Copy Markdown

🚨 CRITICAL Supply Chain Risk Detected

This 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 modified

These files can execute code during package installation or interpreter startup.

Files:

hermes_cli/setup.py
setup.py
skills/productivity/google-workspace/scripts/setup.py

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.

@github-actions

Copy link
Copy Markdown

🔎 Lint report: fix/28639-delegate-list-content vs origin/main

ruff

Total: 3 on HEAD, 1650 on base (✅ -1647)

🆕 New issues (2):

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.

Tranquil-Flow added a commit that referenced this pull request May 25, 2026
…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
Tranquil-Flow pushed a commit that referenced this pull request May 25, 2026
…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.
Tranquil-Flow pushed a commit that referenced this pull request May 25, 2026
…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".
Tranquil-Flow pushed a commit that referenced this pull request Jul 20, 2026
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.
Tranquil-Flow pushed a commit that referenced this pull request Aug 11, 2026
…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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

delegate_task crashes when tool message content is a list