Bugfix rollup (2026-06-10 session): cron, state DB, agent loop, MCP bridge, gateway, desktop, Windows - #44061
Bugfix rollup (2026-06-10 session): cron, state DB, agent loop, MCP bridge, gateway, desktop, Windows#44061AIalliAI wants to merge 476 commits into
Conversation
|
Synced with main (merge commit, no history rewrite). Two things worth flagging from the conflict resolution:
|
…atch tests Follow-up for salvaged PR NousResearch#44486: the adapter shipped remove_reaction but the tool only exposed 'react'. Generalize _handle_react(remove=) and add tool-level dispatch tests for react/unreact (missing from the original PR).
…fallback Widen the salvaged NousResearch#12851 fix to match the established classification pattern (WhatsApp/Slack/BlueBubbles/Mattermost): video/* -> VIDEO, and any remaining MIME type falls through to DOCUMENT instead of TEXT, so exotic types still trigger run.py's document-context injection.
Email cached document attachments and placed them in media_urls, but msg_type only flipped on image attachments — documents stayed TEXT and run.py's document-context injection (gated on MessageType.DOCUMENT) silently dropped them. Same bug class as Signal NousResearch#12845. DOCUMENT wins over PHOTO for mixed attachments since image handling keys off per-path mime types while document injection gates strictly on message_type.
SimpleX tagged unknown files application/octet-stream in media_types but classification only handled audio/image, leaving msg_type TEXT — run.py never injected the document context. Same bug class as NousResearch#12845.
Avoid recompiling the pattern on every _serialize_for_summary call; name it beside _PATH_MENTION_RE with the NousResearch#14665 rationale.
…ousResearch#44702) Modal prompt panels (dangerous-command approval, clarify questions) live in the prompt_toolkit layout and vanish on the next repaint, leaving no trace of the question or the decision in chat history. Emit a dim one-line summary after each prompt resolves: ⚠ Approval: <command> → allowed for session ? Clarify: <question> → <answer> Gated on display.persist_prompts (default true). Detail and outcome are whitespace-collapsed and capped at 120 chars.
Two latent failures exposed when maintainers approved workflow runs (every earlier run was action_required, so tests had never executed): - _broken_imports flagged packages whose modules tests inject into sys.modules as spec-less stubs (types.ModuleType) — find_spec reads module.__spec__ on already-loaded modules and raises. Already-loaded means importable; short-circuit before find_spec. Broke test_tts_mistral, test_transcription_tools, test_daytona_environment, test_managed_media_gateways, test_web_tools_config, test_fal_plugin in CI (packages really install there, then the smoke test ran against the test's stub). - TestTerminatePid taskkill fake_run lacked **kwargs and rejected the creationflags=windows_hide_flags() now passed in gateway/status.py. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks for approving the workflow runs — that was the first time CI actually executed on this branch, and it surfaced three things, all now resolved at
The earlier The new head needs another workflow approval when you get a chance. |
Conflict: agent/context_compressor.py — combined this branch's multimodal text extraction with upstream's MEDIA-directive stripping (286ecd2).
…onfig.yaml Per the AGENTS.md contribution rubric, behavioral settings belong in config.yaml, not new HERMES_* env vars. The user-facing knob is now network.tls_max_version (sibling of network.force_ipv4 — same "connectivity workarounds" section), bridged onto the internal HERMES_TLS_MAX_VERSION env var at process startup by hermes_constants.apply_tls_max_version(), following the established gateway.strict -> HERMES_MEDIA_DELIVERY_STRICT bridge pattern. The env var remains the mechanism (agent/process_bootstrap has no config access at client-build time, and spawned agent subprocesses must inherit the cap) and an explicitly exported value still wins over config.yaml for one-off shell overrides. Bridged in both entrypoints that already apply network.force_ipv4: the hermes_cli/main.py early raw-yaml block (covers CLI, desktop dashboard spawns, TUI gateway) and the gateway/run.py bootstrap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tform-gate tests Cherry-pick of upstream PR NousResearch#43298 (eazye19). The four TestUpdateCommandPlatformGate gate-pass tests spawn a REAL detached `setsid hermes update --gateway`; on CI's detached-HEAD checkout the fallback `git reset --hard origin/main` deletes branch-added test files minutes later, failing whichever shard is mid-collection (exit-4 "file or directory not found" flake). Carried here so the rollup CI stops tripping on it; drop when NousResearch#43298 merges upstream. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
CI update — runs now start without per-push approval, thanks for that. The run at
Fresh CI is running on |
check-attribution flagged support@captureclient.net, introduced by cherry-picking PR NousResearch#43298 onto this branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…usResearch#44776) * feat(billing): /usage → portal top-up browser handoff Add the terminal side of the billing slice (phase 2a): start a top-up by throwing the user to the portal billing page with the top-up modal open. The terminal does not confirm, poll, or track payment — checkout completes in the browser and the next /usage shows the new balance. - nous_account.py: parse organisation.slug/name from /api/oauth/account into NousPortalAccountInfo; add nous_portal_topup_url() building the org-pinned {base}/orgs/{slug}/billing?topup=open with a null-slug fallback to the legacy {base}/billing?topup=open (never /orgs/None/...). - portal_cli.py: 'hermes portal topup' — fresh account fetch, identity line (Topping up as <email> / org <name>), browser open with printed-URL fallback, no-wait closing copy. No polling/confirmation (deferred to 2b). - account_usage.py: the shared /usage credits block now links the org-pinned top-up URL (auto-opens the modal) + points to the command. Depends on NAS NousResearch#409 (organisation.slug/name + ?topup=open). Do not merge until that is live on the target env; until then /api/oauth/account returns organisation: { id } only and the URL falls back to legacy. * feat(billing): /credits command for balance + top-up handoff Replace the standalone `hermes portal topup` subcommand with an in-session /credits slash command — a focused money surface (balance in, top-up out) that works in the CLI, TUI, and every messaging platform from one registry entry. - commands.py: register /credits (Info category). Slack is at its 50-slash cap, so /credits is routed via /hermes credits on Slack only (new _SLACK_VIA_HERMES_ONLY set) to avoid clamping a canonical command off the native list and breaking Telegram parity; native everywhere else. - account_usage.py: build_credits_view() — one portal fetch → balance lines + identity line + org-pinned top-up URL + depleted flag, consumed by all surfaces. Reuses the same snapshot/URL builder as /usage so numbers match. - cli.py: _show_credits() — balance block + identity line + 3-button panel (Open top-up / Copy link / Cancel) via the existing prompt_toolkit modal. ASK, never auto-launch; headless falls back to printing the URL. - gateway/slash_commands.py: _handle_credits_command() — renders the block + tappable top-up URL + no-wait copy; works on button and plain-text platforms. - /usage credits line now points to /credits. - Retire `hermes portal topup` (portal_cli.py back to baseline); the engine (slug/name parse + nous_portal_topup_url) stays as the shared core. No polling, no payment confirmation (billing phase 2a). Depends on NAS NousResearch#409. * fix(credits): /credits works in the TUI slash-worker (non-interactive) In the TUI, /credits runs in the slash-worker subprocess where there is no live prompt_toolkit app and stdin is the JSON-RPC pipe. _show_credits called the 3-button modal unconditionally, which fell back to reading stdin → exception → slash.exec rejected → the command produced no output (only the pre-existing 'Credit access paused' banner showed). - _show_credits: when self._app is None (TUI worker / piped / non-interactive), render the text variant — balance block + tappable top-up URL + no-wait line, same affordance as the messaging surfaces — and skip the modal entirely. The 3-button panel still renders in the interactive CLI. - Depleted banner copy: 'run /usage for balance' → 'run /credits to top up' now that /credits is the dedicated money surface (+ tests). - Regression tests: _show_credits with self._app=None renders text and never invokes the modal; logged-out path. * feat(tui): credits.view RPC for the /credits tappable top-up button Add a credits.view JSON-RPC method returning the structured CreditsView (logged_in, balance_lines, identity_line, topup_url, depleted) so the TUI can render a clickable <Link> top-up button instead of plain text. Account- independent (portal fetch gated on a logged-in Nous account), fail-open to {logged_in: false} on any hiccup. Mirrors session.usage's credits-block pattern. Frontend (TUI-local /credits command + Ink component) lands separately. * feat(tui): /credits command with keyboard-driven top-up confirm TUI-local /credits: fetches the structured balance via the credits.view RPC, prints the balance + identity + top-up URL, then arms the EXISTING confirm overlay (Enter = open top-up in browser via openExternalUrl, Esc = cancel). Reuses ConfirmReq — no new overlay component/state/input handler. Headless (openExternalUrl returns false) falls back to printing the URL. - gatewayTypes.ts: CreditsViewResponse. - commands/credits.ts: the command (mirrors /status's rpc+guarded pattern). - registry.ts: register creditsCommands. - test: balance+overlay armed, headless fallback, no-url, logged-out (4 cases). Matches the CLI /credits 'Enter to open' affordance. Phase 2a: no polling.
…CUMENT (NousResearch#44778) The Teams adapter only handled image/* attachments — documents (the application/vnd.microsoft.teams.file.download.info consent-free download payload and any direct-URL non-image attachment) never reached media_urls at all, so run.py's document-context injection had nothing to surface. Completes the class-wide sweep from PR NousResearch#44695 (Signal/Email/SimpleX). - download.info attachments: fetch the pre-authed SharePoint downloadUrl (SSRF-guarded, same guard chain as base.py cache_*_from_url) and route through cache_media_bytes - direct-URL non-image attachments: same fetch + classify path - skip Teams' text/html message-body mirror and adaptive-card attachments - DOCUMENT > PHOTO > VIDEO > AUDIO precedence for mixed attachments, matching the Email precedence rationale from NousResearch#44695
* feat(yuanbao): support wechat forward msg * feat(yuanbao): support wechat forward msg --------- Co-authored-by: loongfay <izhaolongfei@gmail.com>
…drafts) Cherry-pick of upstream PR NousResearch#44780 (ITheEqualizer). Final replies send raw agent markdown via sendRichMessage; DM streaming previews use sendRichMessageDraft; MarkdownV2 stays as the fallback path. Opt out with platforms.telegram.extra.rich_messages: false. Resolved a trivial conflict with main's supports_code_blocks attribute (kept both). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… .env (NousResearch#44792) Two halves of the same community report (dashboard Profile Builder): 1. A fresh dashboard/CLI-created profile got no .env file unless cloned, so it silently inherited API keys and messaging tokens from the shell environment / root install. create_profile() now seeds a placeholder .env (0600) for non-clone profiles, matching the SOUL.md seeding. 2. The Channels endpoints (/api/messaging/platforms GET/PUT/test) were not profile-scoped: they read/wrote the dashboard process's own .env via load_env()/save_env_value() regardless of the global profile switcher. They now accept the standard optional profile param (body beats query on the PUT, matching other scoped writes) and run inside _profile_scope(). When scoped, the payload no longer falls back to os.environ or load_gateway_config()'s env-override layer — both carry the ROOT install's credentials and would misreport them as the profile's. /api/messaging/platforms added to PROFILE_SCOPED_PREFIXES so the sidebar switcher scopes the Channels page automatically.
…rch#46074) Gateway startup now queues real inbound messages until restart-interrupted auto-resume turns have completed, preventing duplicate agents for the same session after a restart.
Port 465 expects implicit TLS (SMTP_SSL) from the first byte. The email adapter always used SMTP() + starttls(), which is correct for port 587 but hangs/fails on port 465 providers (e.g., Swiss ISPs). Additionally, when the SMTP host has AAAA DNS records but IPv6 is unreachable, socket.create_connection() tries IPv6 first and hangs until timeout. Add an IPv4 fallback via AF_INET socket. Extract _connect_smtp() helper to consolidate the 4 duplicate SMTP connection sites into a single method with correct protocol selection and IPv6 fallback logic.
…tinue notes Three changes to prevent infinite re-execution loops when a user sends a new message while long-running tools are executing: 1. Filter interrupted tool results in _build_gateway_agent_history: skip tool messages whose content contains [Command interrupted] or exit_code 130 — they represent partial execution, not valid results. 2. Don't replay auto-continue notes as user messages: detect gateway-injected [System note: ...] / [IMPORTANT: ...] prefixes and skip them in _build_gateway_agent_history so the LLM doesn't see 4+ messages from 'the user' telling it to finish old work. 3. Fix the wording: the system note now instructs the model to address the user's NEW message FIRST, IGNORE pending results, and NOT re-execute old tool calls. Closes NousResearch#45230
# Conflicts: # apps/desktop/src/app/session/hooks/use-message-stream.ts
3cf88c2 to
dd111ee
Compare
… runs The genuine-rate-limit branch set retry_count = max_retries before continue, intending the top-of-loop Nous guard to handle fallback or bail cleanly. But the loop condition is retry_count < max_retries, so the guard never ran: no fallback activation, no clean rate-limit message — just the generic retry-exhaustion error. Set retry_count = max(0, max_retries - 1) so the loop body runs exactly once more and the guard sees the breaker state recorded moments earlier. Extracted from the NousResearch#44061 bugfix rollup by @AIalliAI.
… runs The genuine-rate-limit branch set retry_count = max_retries before continue, intending the top-of-loop Nous guard to handle fallback or bail cleanly. But the loop condition is retry_count < max_retries, so the guard never ran: no fallback activation, no clean rate-limit message — just the generic retry-exhaustion error. Set retry_count = max(0, max_retries - 1) so the loop body runs exactly once more and the guard sees the breaker state recorded moments earlier. Extracted from the NousResearch#44061 bugfix rollup by @AIalliAI.
… runs The genuine-rate-limit branch set retry_count = max_retries before continue, intending the top-of-loop Nous guard to handle fallback or bail cleanly. But the loop condition is retry_count < max_retries, so the guard never ran: no fallback activation, no clean rate-limit message — just the generic retry-exhaustion error. Set retry_count = max(0, max_retries - 1) so the loop body runs exactly once more and the guard sees the breaker state recorded moments earlier. Extracted from the NousResearch#44061 bugfix rollup by @AIalliAI.
… runs The genuine-rate-limit branch set retry_count = max_retries before continue, intending the top-of-loop Nous guard to handle fallback or bail cleanly. But the loop condition is retry_count < max_retries, so the guard never ran: no fallback activation, no clean rate-limit message — just the generic retry-exhaustion error. Set retry_count = max(0, max_retries - 1) so the loop body runs exactly once more and the guard sees the breaker state recorded moments earlier. Extracted from the NousResearch#44061 bugfix rollup by @AIalliAI.
… runs The genuine-rate-limit branch set retry_count = max_retries before continue, intending the top-of-loop Nous guard to handle fallback or bail cleanly. But the loop condition is retry_count < max_retries, so the guard never ran: no fallback activation, no clean rate-limit message — just the generic retry-exhaustion error. Set retry_count = max(0, max_retries - 1) so the loop body runs exactly once more and the guard sees the breaker state recorded moments earlier. Extracted from the NousResearch#44061 bugfix rollup by @AIalliAI.
… runs The genuine-rate-limit branch set retry_count = max_retries before continue, intending the top-of-loop Nous guard to handle fallback or bail cleanly. But the loop condition is retry_count < max_retries, so the guard never ran: no fallback activation, no clean rate-limit message — just the generic retry-exhaustion error. Set retry_count = max(0, max_retries - 1) so the loop body runs exactly once more and the guard sees the breaker state recorded moments earlier. Extracted from the NousResearch#44061 bugfix rollup by @AIalliAI.
… runs The genuine-rate-limit branch set retry_count = max_retries before continue, intending the top-of-loop Nous guard to handle fallback or bail cleanly. But the loop condition is retry_count < max_retries, so the guard never ran: no fallback activation, no clean rate-limit message — just the generic retry-exhaustion error. Set retry_count = max(0, max_retries - 1) so the loop body runs exactly once more and the guard sees the breaker state recorded moments earlier. Extracted from the NousResearch#44061 bugfix rollup by @AIalliAI.
… runs The genuine-rate-limit branch set retry_count = max_retries before continue, intending the top-of-loop Nous guard to handle fallback or bail cleanly. But the loop condition is retry_count < max_retries, so the guard never ran: no fallback activation, no clean rate-limit message — just the generic retry-exhaustion error. Set retry_count = max(0, max_retries - 1) so the loop body runs exactly once more and the guard sees the breaker state recorded moments earlier. Extracted from the NousResearch#44061 bugfix rollup by @AIalliAI.
What does this PR do?
Rollup of the bugfixes from my 2026-06-10/11 debugging session — ~35 fixes spanning the cron scheduler, state DB, agent loop, MCP bridge, runners, gateway, CLI, Windows support, and the Desktop app.
Most fixes are also submitted as focused standalone PRs (cross-referenced below) so they can be reviewed and landed independently. This branch exists for anyone who wants the whole batch at once; if the standalone PRs land first, the corresponding commits here become no-ops and I'll keep the branch rebased. Merged with current
main(post-#43956 per-job-profile revert) and conflict-free.Related Issue
Addresses #44030, #44035, #44100, #44116, #44117, #44119, #44135, #44150 (each carries a
Fixestag in its standalone PR).Type of Change
Changes Made
Cron / scheduling
_jobs_file_locknow coverscreate_job/update_job/remove_job(lost updates vs scheduler threads); malformednext_run_atno longer crashes every tick; repeat-limit auto-delete no longer leaks output dirs_EnvMutationGate: workdir jobs are excluded from overlapping parallel-pool jobs that read the same process-global state[SILENT]marker only suppresses delivery when leading/trailing, not mid-report; cron hint no longer mislabeled as user instruction in the skills pathHERMES_CRON_SESSIONis now a per-job contextvar instead of a sticky process-wide env var that flipped approval semantics for later interactive sessionsState DB / persistence
session_countincludes the_branched_fromclause; unlocked readers fixed; rewound rows no longer leak into search/anchored views; rewind works pastSQLITE_MAX_VARIABLE_NUMBER; rewind/restore are single indexed UPDATEsAgent loop / providers
prompt_cachingno longer deepcopies the entire history per API callMCP bridge / runners
mini_swe_runnerreasoning capture, max-iterations warning, default model id;batch_runnerexit codes;trajectory_compressorconnection-pool leakGateway / CLI / Desktop / Windows
hermes statusshows plugin-registered platforms (fix(status): show plugin-registered platforms in hermes status #44124); dashboard process detection with global flags (fix(cli): tokenized dashboard cmdline matcher — detect global flags before the subcommand #44165); files root not forced to/opt/dataon host installs (fix(dashboard): don't force /opt/data files root for remote requests on host installs #44139)CREATE_NO_WINDOWon gateway-runtime and terminal-tool subprocess spawns (fix(windows): hide console-window flash on gateway-runtime subprocess spawns #44105)How to Test
pytest tests/ -qpytest tests/cron tests/hermes_cli tests/gateway tests/tools -qChecklist
Code
pytest tests/ -q— 6078 passed; the 34 failures intests/tools/fail identically on currentmainon this machine (environment-specific: PulseAudio/Docker/file-tool platform assumptions)Documentation & Housekeeping