feat: add Devin ACP provider support - #2
Closed
nkowne63 wants to merge 728 commits into
Closed
Conversation
…4383) * fix(discord): recover from runtime gateway task exits Salvaged from NousResearch#39416 (AMEOBIUS) — cherry-picked only the task-exit recovery; the original PR was 1081 commits behind with 28 unrelated commits. A post-ready discord.py WebSocket crash left the gateway split-brained: producers stayed active while Discord stopped responding. After this fix the adapter calls _set_fatal_error(retryable=True) + _notify_fatal_error() so the existing GatewayRunner reconnect watcher replaces the dead adapter. Also adds _wait_for_ready_or_bot_exit() so startup failures (SOCKS/proxy errors, invalid tokens) surface fast instead of burning the full ready timeout. Because connect() no longer waits via asyncio.wait_for on that path, test_connect_releases_token_lock_on_timeout is updated to trigger the timeout through the new helper (same lock-release contract). 3 tests pass (2 new runtime-failure tests + the updated timeout test); test_discord_connect.py and test_discord_slash_commands.py green. Co-Authored-By: ameobius <ameobius@local.host> * fix(test): patch _wait_for_ready_or_bot_exit in timeout cancel test connect() no longer uses asyncio.wait_for for the ready handshake, so test_connect_timeout_cancels_bot_task was hanging for 30s in CI. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: ameobius <ameobius@local.host> Co-authored-by: Cursor <cursoragent@cursor.com>
…ry doesn't
web_search/web_extract are documented to work with zero setup via the bundled
keyless Parallel free-MCP backend, but that only holds when the bundled
plugins/web/* providers are registered. The dispatch relied entirely on the
general plugin sweep to do that; when the sweep finishes without registering
them (its exception swallowed as a warning, a packaged layout where it ran
before the bundled tree was importable, or a stale empty-discovery cache), the
registry is empty and BOTH tools dead-end on "No web {search,extract} provider
configured" — despite needing no setup at all.
_ensure_web_plugins_loaded now verifies the keyless default landed after the
sweep and, if not, registers the bundled web providers directly against the
registry. Idempotent, a no-op on the healthy path (one dict lookup), and honors
an explicit plugins.disabled entry.
Pins the invariant that _ensure_web_plugins_loaded registers the keyless Parallel default (and the wider bundled set) even when the general plugin discovery raises, that the direct-registration fallback honors plugins.disabled, and that it stays a no-op on the healthy path.
Root-cause hardening for the stranded-empty-registry failure behind 'No web search/extract provider configured': discover_and_load() set _discovered=True before scanning, so a sweep that raised partway was swallowed by callers as a warning and every later call early-returned against an empty registry for the process lifetime. The flag now acts only as a re-entrancy guard and is reset when the sweep raises, so the next call retries discovery.
…ousResearch#44453) A WSL2 user reported the top two left-sidebar items being unclickable while the rest of the UI works. That symptom shape matches an -webkit-app-region:drag hit-test band eating clicks, not GPU/compositing: the shell's titlebar drag strips (app-shell.tsx) span the top 34px and the nav group clears them by only 6px, and drag regions win hit-testing over DOM regardless of pointer-events. Linux WCO (Electron >=32) is the newest implementation and has known region quirks (electron#43030). Apply the same no-drag carve-out the codebase already uses for sticky user bubbles (USER_BUBBLE_BASE_CLASS in thread.tsx) to the sidebar nav buttons. Harmless on every platform: the rows were never meant to be draggable surface.
The prompt consolidation above retires the carveout-era prefix. Without a frozen copy in _HISTORICAL_SUMMARY_PREFIXES, summaries persisted by pre-upgrade builds would lose detection (_is_context_summary_content) and renormalization (_strip_summary_prefix) — the exact regression class the tuple exists to prevent. Adds contract tests covering every frozen prefix. Refs NousResearch#41607 NousResearch#38364 NousResearch#42812
…erences Legibility pass on the consolidated prefix: collapse the topic-overlap rule from three overlapping sentences into one WINS sentence + one discard/no-wrap-up sentence (same constraints, less dilution), fix the module docstring to describe the headings that actually shipped, and correct the NousResearch#10896 comment's heading name (Historical Pending User Asks).
…44470) * docs: finish Automation Blueprints terminology rebrand Replace leftover "Automation Templates" wording from the Cron Recipes rebrand, rename the copy-paste cookbook guide to Automation Recipes, and point the marketing gallery link at the blueprints catalog. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: use Automation Blueprints instead of Recipes in guide Rename the cookbook guide from automation-recipes to automation-blueprints so sidebar and copy match the product term. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: rename automation-blueprints-catalog to automation-blueprints Drop the -catalog suffix from the reference page slug and title, and move the copy-paste cookbook to automation-blueprint-examples so the main Automation Blueprints doc is unambiguous. Co-authored-by: Cursor <cursoragent@cursor.com> * Revert "docs: rename automation-blueprints-catalog to automation-blueprints" This reverts commit 605f1ee. --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…ousResearch#44500) The deploy-site skills index crawl was capped at ~3k ClawHub entries because CATALOG_WALK_BUDGET_SECONDS applied to max_items=0 walks too. Only enforce the wall-clock budget for bounded browse requests and pass limit=0 from build_skills_index so CI walks the full catalog. Co-authored-by: Cursor <cursoragent@cursor.com>
…Research#44512) The desktop chat surface talks to the dashboard's in-process /api/ws gateway, which builds agents through tui_gateway.server._make_agent. That path only snapshots the existing tool registry — MCP discovery is started by tui_gateway/entry.py (the stdio TUI), which the dashboard process never runs. So a profile's configured MCP servers never connect under the desktop app and sessions show no MCP tools. Start a shared background MCP discovery thread at dashboard startup (via hermes_cli.mcp_startup, bounded so a slow/dead server can't block boot), and have _make_agent briefly join that thread in addition to the existing entry-owned TUI thread before snapshotting tools. Carved out of NousResearch#44478. Co-authored-by: AJ <yspdev@gmail.com>
…search#44510) Desktop spawns its dashboard backend with `--profile <name>` and `HERMES_DESKTOP=1`. cmd_dashboard's unified-launch routing treats any named profile as a request for the shared machine dashboard: it re-execs as the default profile (dropping HERMES_HOME) or, when one is already listening, prints "Machine dashboard already running ... Managing profile '<name>'" and exits 0. Either way the desktop-spawned child exits before the app sees a ready backend, so Desktop retries forever — the Windows named-profile boot loop in the post-mortem. Skip the machine-dashboard reroute when HERMES_DESKTOP=1 so desktop pool backends stay per-profile (which is what the pool expects). Carved out of NousResearch#44478. Co-authored-by: AJ <yspdev@gmail.com>
…ousResearch#44493) `@assistant-ui/store`'s index-keyed child-scope lookup (`tapClientLookup`) throws — rather than returning undefined — when a subscriber reads an index the message/parts list no longer has. During high-frequency store replacement (switching sessions mid-stream, gateway reconnect replay) a subscriber from the previous, longer list is still in React's notification queue and reads one slot past the new, shorter array before it can unmount. The throw (`Index N out of bounds (length: N)`, the classic index === length off-by-one) unwinds all the way to the root error boundary and blanks the entire window, even though the store self-heals on the very next consistent snapshot. Wrap each virtualized message group in a tiny boundary that swallows ONLY this transient lookup race and auto-recovers when the message signature changes (the existing list-mutation key). Any other error re-throws to the root boundary, so genuine bugs still surface. Upstream-tracked and unresolved: assistant-ui/assistant-ui#4051, NousResearch#3652. Co-authored-by: mollusk <mollusk@users.noreply.github.com>
(cherry picked from commit 64aaf58)
The served-token fallback adopts whatever token the dashboard HTML injects. That is correct when our own child regenerated the token (env pin lost across a shell-wrapped spawn), but wrong when the readiness probe answered from a process we did not spawn: /api/status is public, so an orphaned dashboard squatting the port passes waitForHermes while our child dies on the bind conflict. Silently adopting that process's token would authenticate the renderer against a foreign backend, possibly on the wrong profile. Discriminate on child liveness: the desktop pins HERMES_DASHBOARD_SESSION_TOKEN on every spawn, so a live child always serves our token. Served-token mismatch + dead child = foreign backend; fail the boot loudly instead of connecting. Mismatch + live child keeps the adopt-served-token salvage from NousResearch#43720.
…collision
Two fixes to the Electron desktop launch path, with the port-reservation logic extracted into a unit-tested module:
1. hermes:bootstrap:reset ("Reload and retry") only cleared connectionPromise, leaving the live backend alive; the orphan kept binding PORT_FLOOR (9120) so the next startHermes() hit EADDRINUSE / "Object has been destroyed" and the window looped. Await teardownPrimaryBackendAndWait() so the reset stops the old backend before restarting.
2. pickPort() probes-then-closes a socket before the real bind happens in a separate Python child, so two concurrent spawns (primary + pool backend) could both be handed PORT_FLOOR and one died with EADDRINUSE. The reservation bookkeeping is extracted into electron/port-pool.cjs (PortPool): pickPort() reserves the chosen port until the child exits and releases it on every exit/error/throw-before-spawn path, closing the TOCTOU window.
PortPool is dependency-injected (probe passed in) and socket-free, unit-tested in electron/port-pool.test.cjs (8 cases) and wired into the test:desktop:platforms script.
(cherry picked from commit d413394)
When a tirith content-security warning is present the approval backend forces allow_permanent=False and silently downgrades an "always" choice to session scope (the persistence loop in check_all_command_guards only honors "always" → permanent when no tirith finding exists). But the gateway notify payload that drives the TUI and the Electron desktop app never carried that flag, so both surfaces always rendered "Always allow" — offering a permanent allow the backend would quietly refuse to persist. Plumb allow_permanent end-to-end: - tools/approval.py: include `allow_permanent: not has_tirith` in the gateway approval_data the notify callback emits as `approval.request`. - ui-tui: thread `allowPermanent` through the event handler, gateway types, and ApprovalReq; ApprovalPrompt drops the "always" option (and renumbers the quick-pick keys) when it's false. - apps/desktop: thread `allow_permanent` through the gateway payload type, the per-session approval store, and the inline ApprovalBar, which now hides the "Always allow…" dropdown item when permanent allow is disallowed — reusing the existing DropdownMenu / confirm-Dialog UI. The desktop/TUI render path for approvals already landed in NousResearch#38578 (the root cause of approvals not surfacing in the GUI); this completes the salvage of NousResearch#37856 by carrying allow_permanent across both surfaces. NousResearch#37856's original thread-local _block() approach is dropped: desktop/TUI approvals resolve via approval.respond → resolve_gateway_approval (the per-session queue), not the _block()/request_id correlation, so a worker-thread callback waiting on _block would never be released by the real UI. Tests: gateway notify payload carries allow_permanent (True without tirith, False with a tirith warning); ui-tui approvalAction reduced option set + event-handler allowPermanent propagation; desktop store round-trip + the ApprovalBar showing/hiding "Always allow". Supersedes NousResearch#37856 Closes NousResearch#37812 Co-authored-by: LeonSGP43 <cine.dreamer.one@gmail.com>
…al into one helper
Both spawn paths (startHermes, spawnPoolBackend) duplicated the same
resolve -> log-fallback -> foreign-check -> throw dance. Collapse it into
adoptServedDashboardToken(baseUrl, spawnToken, {childAlive, label}) in
dashboard-token.cjs; childAlive is a thunk so liveness is sampled after
the fetch. Drop the redundant backendPool.delete in the pool's throw
path (the child exit/error handlers already own pool eviction).
Validated end-to-end against a real web_server.py backend, not just
units: token-injection regex vs the actual served index.html, foreign
refusal (dead child + live squatter), benign drift adoption, and the
401-vs-200 token auth split on /api/sessions.
Node >=18 / Electron 40 ship fetch; the hand-rolled http/https.request plumbing buys nothing. AbortSignal.timeout replaces the socket timeout, protocol guard and >=400 rejection semantics preserved. 13/13 unit tests and the live web_server.py repro both green over the new transport.
… opt set Collapse the verbose multi-line rationale comments across the TUI/desktop/ backend approval surfaces into single-line "why" notes, and derive APPROVAL_OPTS_NO_ALWAYS from APPROVAL_OPTS instead of re-listing it. No behavior change.
…44528) Cherry-picked from NousResearch#39840 by @flyinhigh and rebased cleanly on main. - Defer config fetch in createGatewayEventHandler until gateway.ready to avoid render-phase RPC that can mutate transcript state and trigger React error 301 in embedded dashboard PTYs. - Use undici WebSocket fallback when globalThis.WebSocket is unavailable (Node attach mode and sidecar mirror sockets). - Add regression tests for both fixes. Co-authored-by: flyinhigh <flyinhigh@users.noreply.github.com>
…llow-permanent fix(approval): carry allow_permanent to TUI + desktop approval prompts
…ousResearch#43896) The desktop "Local / custom endpoint" onboarding never collected an API key and /api/model/set silently dropped one, so an auth-gated endpoint (e.g. a hosted vLLM behind a key) could never enumerate models — and Settings' "Set up custom endpoint" routed `custom` into a non-existent OAuth flow, booting the user back to the first screen (the reported loop). Backend (web_server.py): - /api/providers/validate accepts an optional api_key and sends it as a Bearer header when probing a custom endpoint's /v1/models. - /api/model/set accepts api_key, persists it to model.api_key (same switch/preserve lifecycle as base_url), and registers a named custom_providers entry via _save_custom_provider — matching the `hermes model` CLI flow so the endpoint shows up as a ready picker row. Desktop: - ApiKeyForm shows an optional API key field for the local/custom option; the key is threaded through saveOnboardingLocalEndpoint → validate + setModelAssignment. - New onboarding `localEndpoint` intent + startManualLocalEndpoint(); the Settings "Set up custom endpoint" button now opens the local-endpoint form (URL + key) instead of the OAuth dead-end. - Added localApiKeyPlaceholder i18n key (en + types + zh). Tests: api_key lifecycle on _apply_main_model_assignment, key persistence + custom_providers registration on /api/model/set, Bearer-header probe; onboarding store forwards + persists the key.
…ofile-fallout fix(desktop): close out the multi-profile desktop fallout — WS auth + cross-profile session reads
this makes the agent frequently edit files in the wrong worktree. what the agent doesn't know can't hurt it.
…onnect (NousResearch#44550) Port from anomalyco/opencode#31271: only call tools/list when the server advertises the 'tools' capability in InitializeResult.capabilities. Previously, _discover_tools() unconditionally called session.list_tools() right after initialize. Prompt-only / resource-only servers (which omit the tools capability per the MCP spec) raise McpError(-32601 Method not found), which aborted the connection — burning all 3 initial-connect retries and permanently failing the server even though its prompts and resources were perfectly usable. The 180s keepalive had the same problem: it probed with list_tools(), so even a successfully connected prompt-only server would be torn down on the first keepalive cycle. Changes: - MCPServerTask._advertises_tools(): capability check with a legacy fallback (no captured InitializeResult -> behave as before) - _discover_tools(): skip tools/list for non-tool servers - keepalive: use the universal ping request for non-tool servers - _refresh_tools(): guard against tools/list_changed from non-tool servers E2E verified with a real stdio prompt-only FastMCP-style server: on main it fails all 3 connection attempts with Method-not-found; with this fix it connects, lists prompts, answers ping keepalives, and shuts down cleanly.
…ee-code fix(coding): don't expose primary worktree path in coding context
…trol (NousResearch#45853) * fix(desktop): jump-to-approval pill for off-screen approvals A blocked approval's only response surface is the inline Run/Reject bar on the pending tool row. When that row is scrolled out of view the session looks stalled with no visible action. Surface a composer-anchored "Approval needed" pill only when an approval is pending AND its inline bar is scrolled away; clicking scrolls the bar back into view. Preserves the deliberate inline (not modal) approval design — the pill never duplicates the approve/reject controls. The inline bar mirrors its own viewport visibility via IntersectionObserver (tracks scroll/resize/layout) and registers a scroll-into-view handler the pill fires, mirroring the existing thread-scroll jump-button bridge. Supersedes NousResearch#45828. * fix(desktop): morph jump-to-bottom into approval prompt; drop scroll bridge Collapse the separate "jump to approval" pill into the existing scroll-to-bottom control: when scrolled away from the bottom while an approval is pending, it relabels to "Approval needed". A parked approval's inline Run/Reject bar is always the bottom-most content, so the existing scroll-to-bottom action lands the user right on it — one control, no collision. This also fixes the layout corruption from the first cut: the pill called native el.scrollIntoView(), which scrolls every scrollable ancestor including the overflow:hidden chat shell containers. Those have no scrollbar to scroll back and don't remount on session switch, so the composer stayed shoved and the breakage persisted across sessions. Reusing requestScrollToBottom() (the use-stick-to-bottom path) only touches the one designated scroll container. Removes the now-unused approval-scroll store + IntersectionObserver wiring.
A stale certifi CA bundle after a partial `hermes update` used to crash
the agent on the first outbound HTTPS call with a raw traceback and
trap the gateway in a retry loop.
This patch:
* Adds `agent/errors.py` with a typed `SSLConfigurationError`
* Adds `agent/ssl_guard.py` with a `verify_ca_bundle()` pre-flight
that asserts the bundle exists, is non-trivial in size, and can build
a working SSLContext. On macOS, it falls back to the system trust
store when the bundle is empty but the system store is healthy
(covers corporate proxies / MDM setups).
* Wires the guard into `run_agent.py` and `gateway/run.py` right
after the `hermes_bootstrap` import, inside a try/except so a bug
in the guard itself can never prevent startup.
* Adds a `SSL / CA Certificates` section to `hermes_cli doctor` so
users can detect the failure with one command.
* Adds unit tests covering the healthy, missing, empty, skip-env, and
macOS-fallback paths.
* Adds an RCA document describing the failure mode and the recovery
path (`pip install -e .`).
When the bundle is broken the user sees:
\u26a0\ufe0f SSL certificate bundle issue detected.
Run: pip install -e .
`HERMES_SKIP_SSL_GUARD=1` disables the check for sandboxed
environments that ship their own trust store.
…ason The previous test patched ssl.create_default_context globally with a bare SSLContext that has zero CA certs. Both verify_ca_bundle() and the macOS fallback got the same mocked context, so the test verified nothing useful: both paths produced empty get_ca_certs() and the assertion that no exception escaped was vacuously satisfied. Only mock the fallback call (no cafile) — let the certifi call hit the real SSL stack and fail with SSLError on the broken PEM. The mock fallback returns a context with load_default_certs() so the test now verifies the real scenario: broken certifi → SSLConfigurationError, macOS system trust store → success. Also pads the broken PEM past the 1 KB size guard so the size check doesn't short-circuit before ssl.create_default_context(cafile=...) runs. Reported by @liuhao1024 in PR review.
…h#28479) Both the regular and execute_code dispatch paths forward task_id into registry.dispatch via middleware _dispatch lambdas but silently dropped session_id. Dispatch-layer hooks (e.g. set_enforcement_fn) that correlate calls with the active session received "" for every invocation. Pass session_id=session_id at both _dispatch call sites inside handle_function_call, matching the existing task_id pattern. Hooks already received session_id; this closes the registry.dispatch gap. Rebased onto current main where dispatch is wrapped by run_tool_execution_middleware — the old direct-dispatch sites from NousResearch#28479 no longer exist. test(dispatch): add tests for session_id forwarding (NousResearch#28479) Covers standard and execute_code paths through the middleware wrapper. Verifies task_id forwarding is not broken by the change.
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-attribute |
351 |
unresolved-import |
105 |
invalid-argument-type |
88 |
invalid-assignment |
39 |
unsupported-operator |
36 |
not-subscriptable |
23 |
invalid-method-override |
10 |
invalid-parameter-default |
5 |
no-matching-overload |
4 |
invalid-return-type |
4 |
unresolved-reference |
4 |
not-iterable |
2 |
call-non-callable |
2 |
unused-type-ignore-comment |
1 |
unresolved-global |
1 |
| +1 more rules |
First entries
gateway/slash_commands.py:3479: [unresolved-attribute] unresolved-attribute: Object of type `Self@_handle_deny_command` has no attribute `adapters`
hermes_cli/cli_agent_setup_mixin.py:378: [unresolved-attribute] unresolved-attribute: Object of type `Self@_init_agent` has no attribute `_on_thinking`
optional-skills/security/godmode/scripts/auto_jailbreak.py:383: [invalid-parameter-default] invalid-parameter-default: Default value of type `None` is not assignable to annotated parameter type `str`
gateway/slash_commands.py:424: [unresolved-attribute] unresolved-attribute: Object of type `Self@_handle_status_command` has no attribute `_session_db`
hermes_cli/cli_agent_setup_mixin.py:368: [unresolved-attribute] unresolved-attribute: Object of type `Self@_init_agent` has no attribute `_provider_require_params`
gateway/slash_commands.py:376: [unresolved-attribute] unresolved-attribute: Object of type `Self@_handle_kanban_command` has no attribute `_active_profile_name`
gateway/slash_commands.py:91: [unresolved-attribute] unresolved-attribute: Object of type `Self@_handle_reset_command` has no attribute `_cleanup_agent_resources`
gateway/slash_commands.py:1172: [invalid-argument-type] invalid-argument-type: Argument to function `list_authenticated_providers` is incorrect: Expected `dict[Unknown, Unknown]`, found `None | Unknown`
tests/gateway/test_compression_concurrent_sessions.py:26: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/gateway/test_matrix_approval_reaction_fail_closed.py:62: [unresolved-attribute] unresolved-attribute: Unresolved attribute `TrustState` on type `ModuleType`
hermes_cli/cli_commands_mixin.py:1453: [unresolved-attribute] unresolved-attribute: Object of type `Self@_handle_background_command` has no attribute `_providers_only`
tests/tools/test_blueprints.py:12: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
hermes_cli/cli_commands_mixin.py:1457: [unresolved-attribute] unresolved-attribute: Object of type `Self@_handle_background_command` has no attribute `_provider_require_params`
tui_gateway/server.py:3409: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `list[str]`, found `Unknown | None`
gateway/slash_commands.py:2494: [unresolved-attribute] unresolved-attribute: Object of type `Self@_handle_compress_command` has no attribute `_sync_telegram_topic_binding`
hermes_cli/cli_commands_mixin.py:286: [unresolved-attribute] unresolved-attribute: Object of type `Self@_handle_paste_command` has no attribute `_attached_images`
tests/gateway/test_42039_duplicate_user_message.py:33: [unresolved-attribute] unresolved-attribute: Unresolved attribute `load_dotenv` on type `ModuleType`
tests/gateway/test_telegram_overflow_partial.py:137: [unresolved-attribute] unresolved-attribute: Attribute `kwargs` is not defined on `None` in union `_Call | None`
gateway/slash_commands.py:1601: [unresolved-attribute] unresolved-attribute: Object of type `Self@_handle_goal_command` has no attribute `_clear_goal_pending_continuations`
gateway/slash_commands.py:119: [unresolved-attribute] unresolved-attribute: Object of type `Self@_handle_reset_command` has no attribute `_set_session_reasoning_override`
gateway/platforms/whatsapp_cloud.py:1352: [unresolved-attribute] unresolved-attribute: Attribute `Request` is not defined on `None` in union `Unknown | None`
gateway/slash_commands.py:1538: [unresolved-attribute] unresolved-attribute: Object of type `Self@_handle_retry_command` has no attribute `session_store`
tests/hermes_cli/test_resolve_last_session.py:148: [unresolved-attribute] unresolved-attribute: Attribute `execute` is not defined on `None` in union `None | Connection`
tests/gateway/test_slack_plugin_action_handlers.py:136: [invalid-argument-type] invalid-argument-type: Argument to bound method `PluginContext.register_slack_action_handler` is incorrect: Expected `(...) -> Unknown`, found `Literal["not a function"]`
tests/tools/test_discord_tool.py:637: [unsupported-operator] unsupported-operator: Operator `not in` is not supported between objects of type `Literal["discord"]` and `str | list[str] | list[Unknown] | bool`
... and 651 more
✅ Fixed issues (130):
| Rule | Count |
|---|---|
invalid-argument-type |
56 |
unresolved-attribute |
41 |
unsupported-operator |
11 |
unresolved-import |
8 |
invalid-assignment |
5 |
invalid-return-type |
3 |
call-non-callable |
2 |
unresolved-reference |
2 |
invalid-parameter-default |
1 |
invalid-type-form |
1 |
First entries
gateway/run.py:6846: [invalid-argument-type] invalid-argument-type: Argument to bound method `PlatformRegistry.create_adapter` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
tests/gateway/test_whatsapp_group_gating.py:372: [invalid-argument-type] invalid-argument-type: Argument to function `WhatsAppAdapter._is_broadcast_chat` is incorrect: Expected `str`, found `None`
tests/hermes_cli/test_destructive_slash_confirm_gate.py:32: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 30 union elements`
tests/cli/test_cli_status_bar.py:163: [unresolved-import] unresolved-import: Cannot resolve imported module `prompt_toolkit.utils`
tools/process_registry.py:1098: [invalid-argument-type] invalid-argument-type: Argument to bound method `ProcessRegistry._reconcile_local_exit` is incorrect: Expected `ProcessSession`, found `ProcessSession | None`
tests/gateway/test_version_command.py:11: [invalid-argument-type] invalid-argument-type: Argument to function `GatewayRunner._handle_version_command` is incorrect: Expected `GatewayRunner`, found `None`
gateway/run.py:17504: [invalid-argument-type] invalid-argument-type: Argument to bound method `BasePlatformAdapter.edit_message` is incorrect: Expected `bool`, found `dict[str, Any] | None | str`
tests/hermes_cli/test_portal_cli.py:13: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/tools/test_web_providers.py:218: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["search_backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 30 union elements`
tests/hermes_cli/test_web_server.py:628: [unresolved-attribute] unresolved-attribute: Attribute `commit` is not defined on `None` in union `Connection | None`
gateway/run.py:11199: [invalid-argument-type] invalid-argument-type: Argument to function `list_picker_providers` is incorrect: Expected `dict[Unknown, Unknown]`, found `None | Unknown`
tests/tools/test_browser_lightpanda.py:242: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["engine"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 30 union elements`
tests/tools/test_browser_console.py:341: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["record_sessions"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 30 union elements`
gateway/config.py:544: [invalid-argument-type] invalid-argument-type: Argument to bound method `PlatformRegistry.get` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/run.py:15773: [invalid-argument-type] invalid-argument-type: Argument to function `set_session_vars` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
hermes_cli/config.py:4655: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 30 union elements`
tests/hermes_cli/test_aux_config.py:37: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["title_generation"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 30 union elements`
gateway/platforms/yuanbao.py:2695: [invalid-argument-type] invalid-argument-type: Argument is incorrect: Expected `MessageType`, found `Literal[MessageType.DOCUMENT] | Any | None`
tests/gateway/test_version_command.py:11: [invalid-argument-type] invalid-argument-type: Argument to function `GatewayRunner._handle_version_command` is incorrect: Expected `MessageEvent`, found `None`
tests/cli/test_reasoning_command.py:552: [invalid-argument-type] invalid-argument-type: Argument to bound method `TestCase.assertIn` is incorrect: Expected `Iterable[Any] | Container[Any]`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 30 union elements`
cli.py:12070: [invalid-argument-type] invalid-argument-type: Argument to function `_wrap_panel_text` is incorrect: Expected `str`, found `str | list[str] | int | Queue[Unknown] | Unknown`
hermes_cli/web_server.py:3130: [invalid-argument-type] invalid-argument-type: Argument to bound method `set.add` is incorrect: Expected `str`, found `Literal["telegram", "discord", "whatsapp", "slack", "signal", ... omitted 16 literals] | set[Unknown]`
cli.py:12047: [unsupported-operator] unsupported-operator: Operator `+` is not supported between objects of type `str | list[str] | Unknown` and `Literal["..."]`
gateway/run.py:18637: [invalid-assignment] invalid-assignment: Object of type `object` is not assignable to attribute `session_id` on type `SessionEntry & ~AlwaysFalsy`
hermes_cli/web_server.py:8177: [unresolved-attribute] unresolved-attribute: Attribute `execute` is not defined on `None` in union `Connection | None`
... and 105 more
Unchanged: 4787 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
nkowne63
pushed a commit
that referenced
this pull request
Jun 26, 2026
…bound/outbound round-trip (NousResearch#48828) * fix(relay): enable RELAY platform + normalize dial URL so hosted gateways actually connect Three bugs blocked a self-provisioned hosted gateway from ever establishing its inbound relay WS (found while standing up the live staging end-to-end). Each masked the next; all three are needed for inbound to work. 1. RELAY platform never enabled in config.platforms (gateway/config.py). register_relay_adapter() puts the adapter in the platform_registry, but start_gateway()'s connect loop iterates self.config.platforms — which never contained Platform.RELAY. So the adapter was "registered" but never connected (logs showed "relay adapter registered" then "No messaging platforms enabled"). Fix: _apply_env_overrides now enables Platform.RELAY (mirroring relay_url into extra for the connected-checker) when GATEWAY_RELAY_URL (env) or gateway.relay_url (yaml) is set. Absent -> no RELAY entry (direct/ single-tenant gateways unaffected). 2. URL scheme not converted for the WS dial (gateway/relay/ws_transport.py). The relay URL is configured once as the http(s):// base (used as-is for the provision POST), but websockets.connect rejects http(s):// with "scheme isn't ws or wss". Fix: _ws_dial_url converts https->wss / http->ws. 3. /relay path not appended (same helper). The connector mounts its WebSocketServer at path "/relay" and returns HTTP 400 on an upgrade to any other path. GATEWAY_RELAY_URL is the base (no /relay), so the dial hit "/" -> 400. Fix: _ws_dial_url ensures the path ends in /relay. Idempotent — a URL already carrying ws(s):// and/or /relay is unchanged, so provision's _provision_url (which derives /relay/provision from either form) still works. Why the cross-repo E2E missed #2/#3: the stub connector binds ws://host:port and its websockets.serve accepts ANY path, so neither the scheme nor the /relay path was exercised. Real connector needs both. Verified live on staging hermes-agent-stg-automated-perception-5054: after the fixes the gateway logs "Connecting to relay..." -> "✓ relay connected" -> "Gateway running with 1 platform(s)" against wss://gateway-gateway.staging-nousresearch.com/relay, stable. Tests: added _ws_dial_url scheme+path+idempotency cases (test_ws_transport.py) and RELAY-platform-enablement cases for env + yaml + absent (test_config.py). Full gateway/relay + config suites green (191 passed). Relay-adapter lane. EXPERIMENTAL. * fix(relay): re-attach guild_id to outbound so connector egress resolves the tenant The final bug in the hosted-relay round-trip. Inbound worked end to end (Discord -> connector -> bus -> agent WS -> agent runs -> reply), but the reply's egress was declined by the connector: "discord egress declined: target not routed to an onboarded tenant". Cause: the connector's routedEgressGuard resolves the owning tenant from the OUTBOUND action's metadata.guild_id (Discord's routing discriminator). The gateway's generic delivery path builds outbound metadata via run.py _thread_metadata_for_source, which only carries thread_id (and returns None entirely for a non-threaded message) — so guild_id never reached the connector, tenant resolution failed, and the shared bot refused to post. Fix (relay-adapter-local, no perturbation of the generic delivery path or other platforms): RelayAdapter learns chat_id -> guild_id from each inbound event (_capture_scope) and re-attaches it to the outbound action's metadata in send() (_with_scope) when not already present. No-op for chats we never saw inbound (e.g. DMs) and never overwrites an explicit guild_id. Verified live on staging hermes-agent-stg-automated-perception-5054: an @mention in #general now produces a visible bot reply — full multi-tenant relay round-trip (real Discord -> shared connector bot -> tenant routing -> agent WS -> reply egress -> Discord). Tests: _capture_scope/_with_scope reattach, no-scope no-op, explicit-guild_id preserved (test_relay_adapter.py). Full relay + config suites green (160 passed). Relay-adapter lane. EXPERIMENTAL.
nkowne63
pushed a commit
that referenced
this pull request
Jun 26, 2026
When context compression rotates a session, the original is ended and the continuation is auto-numbered (e.g. "name" -> "name #2"). The session list projects the ended root behind its live tip, so the user never sees the predecessor. But set_session_title's uniqueness check compared against ALL sessions, so renaming the visible tip back to "name" dead-ended with "Title 'name' is already in use by session <id the user can't find>". When the conflicting title is held by a compression ancestor of the session being renamed, transfer the title instead of raising: clear it from the ended predecessor and apply it to the continuation. Uniqueness is preserved (still exactly one session carries the title) and the parent-link lineage is untouched, so resume-by-title and tip projection keep working. Genuine conflicts with unrelated sessions, and with non-compression children (delegate/branch), still raise as before.
nkowne63
pushed a commit
that referenced
this pull request
Jun 26, 2026
…id (NousResearch#38763) Context compression today rewrites the message list AND rotates the session id — it ends the session, forks a parent_session_id child, and renumbers the title (name -> name #2). That moving identity key is the root cause of a whole bug cluster: /goal lost (NousResearch#33618), pending response lost at the split (NousResearch#14238), orphan sessions (NousResearch#33907), TUI sid desync (NousResearch#36777), FTS search gaps + duplicate sidebar entries (NousResearch#45117), null continuation cwd (NousResearch#42228), and title-rename dead-ends (NousResearch#48989). It also forced a large defensive apparatus (compression lock, contextvar/env/ logging triple-sync, orphan finalization, gateway SessionEntry re-propagation, tip projection) whose only job is surviving a mid-conversation id change. Add a compression.in_place config flag (default False during rollout). When True, compaction rewrites the transcript and rebuilds the system prompt but keeps the SAME session_id: no end_session, no child row, no title renumber, no contextvar/logging re-sync, no memory/context-engine session-switch. The conversation keeps one durable id for life, like Claude Code / Codex. Compaction is lossy by design — the pre-compaction transcript is summarized away, not archived. The rotation path is unchanged when the flag is off (moved verbatim into an else branch). Staged rollout: this PR ships the option behind a default-off flag for live validation; a follow-up flips the default and deletes the now-redundant rotation machinery, superseding the 14 open band-aid PRs in this area. - hermes_cli/config.py: add compression.in_place (default False), documented - agent/agent_init.py: resolve the flag -> agent.compression_in_place - agent/conversation_compression.py: branch compress_context() on the flag - tests/run_agent/test_in_place_compaction.py: in-place invariants + rotation regression guard + config default The pre-flush of current-turn messages (NousResearch#47202) runs in BOTH modes, so no boundary data loss. Prompt-cache invariant preserved: the system-prompt rebuild is the same single sanctioned invalidation that already happens during compaction — no NEW invalidation. Message alternation preserved.
Owner
Author
|
このPRは古い fork main / stacked branch をベースにしてしまい、意図しない大量差分を含んでいます。fork main を upstream に追従させたうえで、Devin ACP の変更だけを含む小さいPRとして作り直します。 |
nkowne63
pushed a commit
that referenced
this pull request
Jun 26, 2026
…eation snapshot (NousResearch#44585) An unpinned cron job follows the global default provider (config.yaml model.default + resolve_runtime_provider). If that global state is changed after the job is created — e.g. a temporary switch to a paid provider like nous/claude-fable-5 — the job silently inherits it on its next tick and spends real money. This is the reported $7.73 incident: a job created under a free/default provider later inherited a temporary paid switch. Fix (ask #1 only) preserves the legitimate "unpinned job should follow model.default" use case by detecting *drift* rather than freezing the model: - create_job (cron/jobs.py): for UNPINNED, agent-backed jobs (no explicit provider, not no_agent), snapshot the provider that resolution WOULD pick right now into a new optional `provider_snapshot` field, resolved via the same resolve_runtime_provider() path the ticker uses. Fail-open to None on any resolution error so job creation never breaks. - run_job (cron/scheduler.py): right after runtime resolution, if the job has a provider_snapshot AND is unpinned AND the currently-resolved provider DIFFERS from the snapshot, fail closed for that run — make no paid call and deliver a loud, actionable alert naming both providers and telling the user to pin explicitly (`cronjob action=update job_id=.. provider=..`). Back-compat: jobs with no snapshot (pre-existing jobs, no_agent jobs, or any job whose creation-time resolution failed) behave exactly as before — the guard only engages when a snapshot exists. Explicitly-pinned jobs (job.provider set) are unaffected since they don't drift with global state. Tests: tests/cron/test_cron_provider_pin.py covers snapshot-matches (runs), snapshot-differs (fail closed, no agent constructed), no-snapshot back-compat, None-snapshot back-compat, explicitly-pinned (runs regardless), plus create_job snapshot capture/skip/fail-open. The fail-closed case is load-bearing (fails without the guard). Issue NousResearch#44585 asks #2-4 (hard-stop a running job, gateway-stop containment, fail-closed on provider mutation) are out of scope for this change.
nkowne63
pushed a commit
that referenced
this pull request
Jul 2, 2026
…_id signature churn Two independent bugs evicted the cached gateway AIAgent on every turn, preventing the prompt cache from ever warming: 1. Model normalization mismatch: the post-run fallback-eviction check compared _agent.model (stripped in AIAgent.__init__) against the raw _resolve_gateway_model() config string. For vendor-prefixed config on native providers (e.g. 'deepseek/deepseek-v4-pro' vs 'deepseek-v4-pro') this was always unequal, so the agent was evicted after every successful run. Normalize _cfg_model the same way (skip aggregators). 2. Discord triggering message_id leaked into the cached system prompt via build_session_context_prompt()'s Discord IDs block. message_id changes every turn, so the agent-cache signature (computed from the ephemeral prompt) changed every Discord turn -> rebuild every message. The id is now injected per-turn into the user message (where per-turn content belongs and does not touch the cache signature); the cached IDs block carries a static pointer to it, preserving reply/react/pin via the discord tools. Adapted from NousResearch#28846. Bug #1 fix is the contributor's; bug #2 reworked to be non-destructive (keeps the triggering-id capability instead of deleting it). Redundant auto-reset eviction (already on main via NousResearch#9893/NousResearch#48031) and the wrong-premise reset_context_note plumbing from the original PR were dropped. Co-authored-by: Hermes Agent <hermes@nousresearch.com>
nkowne63
pushed a commit
that referenced
this pull request
Jul 2, 2026
… fail on '(empty)' sentinel Two related bugs caused subagent delegation to silently return empty summaries with 0 tokens when the user configured delegation.provider=bedrock alongside delegation.base_url=https://bedrock-runtime.<region>.amazonaws.com. Root cause #1 — misrouting in _resolve_delegation_credentials(): The configured_base_url branch unconditionally forced provider='custom' and api_mode='chat_completions', only specializing for chatgpt.com, anthropic, and kimi hosts. Bedrock (and other native-SDK providers) fell through as 'custom' + chat_completions, which then POSTed OpenAI-shaped JSON at Bedrock's native API. Bedrock rejected the payload and returned nothing, which looked like an empty LLM response to the child agent. Fix: when provider is one of {bedrock, vertex, google, google-genai}, skip the base_url short-circuit and fall through to resolve_runtime_provider(), which knows how to construct the proper SDK client. base_url can still be forwarded through that path for regional overrides. Root cause #2 — '(empty)' sentinel accepted as success: After N retries of empty LLM responses, run_agent.py emits the literal string '(empty)' as final_response. _run_single_child then hit `elif summary:` — '(empty)' is truthy, so status became 'completed' and the parent surfaced a blank result with no error. Users saw api_calls=4, tokens=0, duration~0.4s, status=completed. Fix: treat final_response.strip() == '(empty)' as a failure so the parent surfaces it instead of silently accepting zero-content 'success'. Both paths were reproduced in a live Hermes TUI session on us-west-2 Bedrock (provider=bedrock, model=us.anthropic.claude-sonnet-4-6) and are covered by new tests in tests/tools/test_delegate.py.
nkowne63
pushed a commit
that referenced
this pull request
Jul 17, 2026
Completes the review's ask for "adapter-to-session-key integration coverage for Discord and a non-Discord platform" on NousResearch#20096. Drives a concrete adapter's real BasePlatformAdapter.build_source with an injected gateway_runner, asserts the matched route's profile is stamped on the source, and that build_session_key scopes the key under agent:<profile>: (versus the shared agent:main: namespace). Covers Discord and Telegram — the Telegram case is the bug-#2 path that previously fell through to default. Adds a regression anchor: without gateway_runner, profile stays None and the key lands in agent:main (the silent fallback the fix removes for non-Discord). Co-Authored-By: Claude <noreply@anthropic.com>
nkowne63
pushed a commit
that referenced
this pull request
Jul 17, 2026
…st (NousResearch#65214) Moves the fireworks entry in CANONICAL_PROVIDERS from its old slot (after GMI Cloud) to directly below Nous Portal, ahead of OpenRouter. Order propagates automatically to hermes model, the setup wizard, Telegram /model, and the desktop provider catalog.
nkowne63
pushed a commit
that referenced
this pull request
Jul 17, 2026
…etry Combines the two salvaged fixes so they compose instead of conflict: _persist_session_title (NousResearch#50575) now writes through set_auto_title_if_empty (NousResearch#51483) when the store provides it — the collision-dedup retry and the manual-/title race protection apply together. Predicate failure (a manual title landed while generation was in flight) returns None: nothing written, no callback. Legacy stores without the atomic method keep the plain set_session_title path, including the vanished-session RuntimeError. Tests cover both store shapes plus the race-skip path; E2E verified against a real SQLite SessionDB (collision -> 'Weekly Report #2', manual title preserved, cron dedup, blank guard). AUTHOR_MAP entry for rasitakyol.
nkowne63
pushed a commit
that referenced
this pull request
Aug 5, 2026
… a broken chat A completely unconfigured install previously booted into a working-looking chat (banner showed model 'unknown'), accepted a message, spun ~30s, then failed with 'Set OPENROUTER_API_KEY' — a provider the user never chose — and never offered setup. - HermesCLI.run() now probes provider readiness at startup (TTY only) and offers the shared provider picker (hermes model flow, which fronts Quick Setup / Nous Portal OAuth) when nothing is configured. Decline is respected; picker state re-syncs into the live CLI so the next turn works without a restart. - New silent probe _runtime_credentials_ready(): no printing, no state mutation; handles keyless local endpoints and callable bearer providers. - The empty-api-key error is provider-aware: names the actual resolved provider and points at 'hermes model' / 'hermes setup' instead of hardcoding OPENROUTER_API_KEY. - Banner: unconfigured installs render 'no model configured — run /model' in red instead of the silent 'unknown' model slug. Consumer-onboarding audit finding #2 (sev 5), Aug 2026.
nkowne63
pushed a commit
that referenced
this pull request
Aug 5, 2026
A wedged adapter transport (network hang, dead websocket) previously blocked _check_session_stalls forever: sibling candidates in the same pass were never evaluated and the watcher stopped ticking. Wrap the send in asyncio.wait_for (15s); on timeout log a WARNING and do NOT latch, so the next tick retries. Regression uses a never-resolving fake adapter and proves the pass completes, a healthy sibling candidate is still notified in the same pass, and the watcher ticks again (sabotage-verified against the unbounded send).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds Devin ACP provider support and wires it through auth/runtime/streaming paths.
Verification:
The second check returned: OK