Sync upstream hermes-agent @ 5ff11a689 (CONFLICTS)#256
Draft
hermes-upstream-sync[bot] wants to merge 802 commits into
Draft
Sync upstream hermes-agent @ 5ff11a689 (CONFLICTS)#256hermes-upstream-sync[bot] wants to merge 802 commits into
hermes-upstream-sync[bot] wants to merge 802 commits into
Conversation
…ion_search Follow-up to the soft-archive durability fix. Reusing the rewind/undo active=0 flag for compaction-archived turns inherited the wrong search semantics: undo rows are intentionally HIDDEN from session_search (the user took them back), but compaction-archived turns must stay DISCOVERABLE — that is the whole point of Teknium's "searchable / recoverable" requirement. As built, search_messages defaulted to WHERE active=1, so after in-place compaction the pre-compaction turns were in the FTS index but filtered out of the default search. (The earlier "searchable" claim only held for a raw FTS query / include_inactive=True, not the actual session_search tool.) Empirically confirmed the gap: search 'HMAC' returned 2 hits before compaction, 1 after (only the summary's mention) — the originals were hidden. Fix — a `compacted` flag distinct from `active`, giving a 3-way state: - active=1, compacted=0 → live context (normal) - active=0, compacted=1 → compaction-archived: OUT of live context, IN search - active=0, compacted=0 → rewind/undo: OUT of live context, OUT of search Changes: - messages.compacted INTEGER NOT NULL DEFAULT 0 added to SCHEMA_SQL. Declarative _reconcile_columns adds it on existing DBs — no version bump (plain column add). - archive_and_compact: UPDATE … SET active=0, compacted=1 (was active=0 only). - search_messages: default WHERE active=1 → (active=1 OR compacted=1), on BOTH the main FTS5 path and the trigram CJK path. include_inactive=True still returns everything. The short-CJK LIKE fallback already returns all rows (no active filter) — unchanged. - Docstrings on archive_and_compact + search_messages document the 3-way state. Verified: after compaction, session_search default finds the archived originals (ids 1 & 4); rewind/undo rows stay hidden by default (recoverable via include_inactive); live context still excludes both. 322 in-place + hermes_state tests and 46 session_search tests green; ruff clean. Mutation check: reverting the search WHERE to active-only fails the new searchable test. (Surfaced by the question "is search semantic or only FTS?" — answer: session search is FTS5 keyword/BM25 only, no embeddings over the transcript; semantic retrieval lives in the optional memory-provider layer. Tracing that confirmed the active-only filter gap above.)
Review nit (yoniebans): the config.py comment still said compaction is 'lossy: the pre-compaction transcript is discarded, matching Claude Code / Codex' — leftover from the original destructive design. The shipped behavior is soft-archive: lossy for the LIVE context (what the model reloads), but the pre-compaction turns are kept on disk (active=0, compacted=1), searchable via session_search and recoverable. Comment now says so. Comment-only; no behavior change.
…ion event (#49738) Async-delegation completions (delegate_task(background=true)) and background-process completions (terminal notify_on_complete) re-enter the originating session as internal MessageEvents. When the session was busy, _handle_active_session_busy_message treated them like a user TEXT message and the default busy_input_mode='interrupt' aborted the active turn (and sent a 'Interrupting current task' ack) — the opposite of the design invariant that a completion surfaces as a new turn only when idle. Short-circuit internal events to return False so the base adapter queues them silently (it already excludes internal events from debounce), cascading them as the next turn after the current one finishes.
…idated return (#49734)
* feat(delegation): single-task delegate_task always runs in the background
The model no longer decides whether a subagent runs in the background — a
single-task delegate_task from the top-level agent is now always dispatched
async, so the parent turn returns immediately and the subagent's result
re-enters the conversation when it finishes.
- run_agent._dispatch_delegate_task (the live model path) forces
background=True for top-level single-task calls; the schema-level
`background` param is ignored.
- A batch (tasks with >1 item) stays synchronous (fan-out can't go async).
- A delegation from an orchestrator subagent (depth > 0) stays synchronous —
it needs its workers' results within its own turn.
- The function-level default is unchanged, so direct Python callers/tests keep
the historical synchronous behavior.
- On async-pool capacity rejection, single-task now falls through to a
synchronous run instead of erroring (the child stays attached for interrupt
propagation; detach happens only on a successful dispatch).
- Schema `background` param marked deprecated/ignored; tool description
updated to state the always-background single-task rule.
* feat(delegation): all delegate_task fan-out runs in the background
Extend the always-background behavior to the full fan-out. A batch is now
dispatched as N independent async subagents (one handle each), instead of
running synchronously. Single task and batch both return immediately; each
subagent's result re-enters the conversation as its own message when it
finishes.
- delegate_task: when background is set, loop over ALL built children and
dispatch each via dispatch_async_delegation; return a combined handle block
(count + per-task delegation_ids). Children the async pool rejects (at
capacity) run synchronously inline and are reported alongside the dispatched
handles, so nothing is silently dropped.
- run_agent._dispatch_delegate_task + registry handler: force background for
any top-level model delegation (single OR batch); orchestrator subagents
(depth > 0) still run synchronously since they need workers' results within
their own turn.
- Removed the v1 'batch async not supported' rejection.
- Tool description updated: BOTH MODES RUN IN THE BACKGROUND.
- Tests updated to assert batch fan-out dispatches each task async (verified
E2E: 3-task batch -> 3 independent completion-queue events).
* fix(delegation): background fan-out joins and returns one consolidated block
Correct the fan-out semantics: a backgrounded batch is dispatched as ONE
async unit (one handle, one async-pool slot), not N independent dispatches.
The unit runs all children in parallel, waits on every one, and emits a
SINGLE completion event carrying the consolidated per-task results. The chat
is never blocked; when all subagents finish, their full summaries re-enter
the conversation together as one message.
- async_delegation.dispatch_async_delegation_batch + _finalize_batch: a batch
occupies one slot; its runner returns the combined {results:[...]} dict and
one event with the full results list is pushed to the completion queue.
- delegate_tool: extract the sync execution+aggregation into
_execute_and_aggregate(); background dispatches it via the batch unit and
returns one handle; on pool-capacity rejection it runs the batch inline.
- process_registry._format_async_delegation: render a consolidated multi-task
block (TASK i/N + per-task summary) when the event carries is_batch/results.
- Tests updated; E2E verified: 3-task batch -> immediate return -> one combined
completion block with all three summaries.
… (401/403)
When the active provider returns a 401/403 that survives its per-provider
credential-refresh attempt (revoked OAuth, blocked/expired key, or an
account pinned to a dead/staging inference endpoint), the conversation
loop now escalates to the configured fallback chain instead of dead-ending.
Before: the generic failover dispatch fired only for {rate_limit, billing};
auth/auth_permanent fell through to 'switch providers manually' advice and
never called _try_activate_fallback(). A user whose primary credential was
broken kept thrashing on the same dead credential every turn — the main
agent appeared 'stuck in fallback mode' while never actually failing over.
This also affected auxiliary tasks (compression, vision, title-gen), since
auto-resolved aux follows the main provider.
After: a persistent auth failure with a configured fallback chain switches
to the next provider (mirroring the rate-limit/billing failover path),
guarded one-shot per attempt by TurnRetryState.auth_failover_attempted.
When no fallback is configured the behavior is unchanged — it falls through
to the existing terminal handling and provider-specific troubleshooting
guidance.
Tests: test_auth_provider_failover.py — 401/403 classify as auth, the
gating condition fires only with a chain present + guard unset, the guard
blocks repeats, and non-auth (500) errors do not trigger auth failover.
…graded session When the auxiliary summary call fails with an authentication/permission error (HTTP 401/403), context compression now ABORTS and preserves the session unchanged instead of rotating into a child session with a placeholder summary. Before: a 401 (invalid/blocked key, or a token pointed at the wrong inference host) fell through every transient-error check to 'return None', and because compression.abort_on_summary_failure defaults False, compress() took the static-fallback path and rotated the session anyway (messages N->N). The user landed on a fresh-but-broken session that kept failing the same way — paying for a full-context API call each turn with no useful compression. After: _generate_summary classifies 401/403 as a non-recoverable auth failure (_last_summary_auth_failure) and compress() aborts on it regardless of abort_on_summary_failure. A distinct auxiliary summary_model that 401s still retries once on the main model first (its dedicated creds may be the only broken thing); the abort only sticks when the main model itself auth-fails or the fallback also auth-fails. The existing _last_compress_aborted handling in conversation_compression.py already skips rotation and emits a warning, so no session rotation occurs. Tests: TestAuthFailureAborts — 401/403 flagging, compress() aborts despite flag=False, non-auth failures keep the historical fallback path, and aux-model auth failure recovers on main without aborting.
Adds a ConvertTo-LongPath helper to install.ps1 that expands a Windows 8.3 short path (e.g. C:\Users\FIRST~1.LAS) back to its long form via Scripting.FileSystemObject. Paths without a "~<digit>" component are returned unchanged (no COM round-trip), and any COM failure falls back to the input. Adds an AST-loaded unit test that exercises the helper without executing the installer body (pass-through, null/empty, and graceful fallback).
… don't abort On a Windows profile whose folder name contains a space (e.g. "First Last"), Windows can expose %TEMP%/%TMP% as an 8.3 short path (C:\Users\FIRST~1.LAS\AppData\Local\Temp). PowerShell's FileSystem provider mishandles the "~1.ext" component when the path reaches a provider cmdlet such as `Tee-Object -FilePath`, throwing: An object at the specified path C:\Users\FIRST~1.LAS does not exist. Every Node/Electron install+build stage streams its log to %TEMP% via Tee-Object, so they all abort with that error (browser-tools npm, Playwright, TUI npm, and the hard-failing desktop build), while the Python/uv stages -- which never write a side log to %TEMP% through a provider cmdlet -- succeed. Normalize %TEMP%/%TMP% to their long form once, up front, so every downstream cmdlet and child process sees a path the provider can resolve. Fixes #39308
…o strict providers Per-message timestamp metadata injected by _apply_persist_user_message_override leaks into the Chat Completions payload sent to the provider. Strict OpenAI-compatible providers (e.g. Fireworks-backed endpoints like OpenCode Go 'glm-5.2', Mistral, Kimi) reject this schema-foreign field with HTTP 400: Extra inputs are not permitted, field: 'messages[0].timestamp' The ChatCompletionsTransport.convert_messages already strips known internal-only fields (tool_name, _-prefixed scaffolding keys, codex_reasoning_items, etc.) — add timestamp to that list. Closes #47868
Add a regression test for #47868 asserting convert_messages strips the internal per-message timestamp field, plus the identity-return path for timestamp-free message lists. Map x7peeps for the release attribution gate.
In Docker the install tree (/opt/hermes) is read-only, so npm install for the WhatsApp bridge fails with EACCES. Add resolve_whatsapp_bridge_dir() in whatsapp_common.py: when the install dir is read-only, mirror the bridge source into a writable HERMES_HOME location and use that. Both the adapter and the 'hermes whatsapp' CLI resolve through the shared helper so the install and runtime paths agree. Fixes #49561
Follow-up for salvaged #49654: unit tests for resolve_whatsapp_bridge_dir() (writable passthrough, read-only mirror, existing-mirror reuse) and the AUTHOR_MAP entry for the contributor.
…aram (#49854) The kanban-worker skill taught kanban_complete with three full examples but never mentioned the artifacts=[...] parameter added in #27813 — so a worker reading the skill had no way to learn it can ship a chart/PDF/image as a native upload to the subscriber's chat. Adds a 'Shipping deliverables' section covering absolute-path rules, the inline-vs-file extension behavior, and the trap that the notifier reads the top-level artifacts list (NOT metadata.*).
The dispatcher treated workspace_kind=worktree as metadata only and never ran 'git worktree add', so every worktree task ran in the main repo checkout instead of an isolated worktree — concurrent tasks silently shared one tree and contaminated each other. This materializes a real linked worktree at <repo>/.worktrees/<task_id> on branch wt/<task_id> when resolve_workspace() handles a worktree task, treats a repo-root workspace_path as shorthand for that location, persists the derived workspace/branch back onto the task row, and — on rerun/redispatch — detects an already-materialized linked worktree (via git-common-dir) and reuses it instead of nesting a second .worktrees/<id> inside it.
Follow-up to the salvaged worktree-materialization fix. When a worktree task has no explicit workspace_path, resolve the anchor from the board's default_workdir (a git repo) and materialize <repo>/.worktrees/<id> per task, instead of silently rooting under the dispatcher's CWD (whatever directory launched the gateway, e.g. the Hermes checkout). If no default_workdir is configured, raise with a clear message rather than guessing from CWD. Adds AUTHOR_MAP entry for the salvaged commit.
On Windows, hermes writes writer.bat (@echo off / hermes -p writer %*) with CRLF endings instead of the POSIX writer shell script. The test hardcoded the POSIX path and exact bytes, so it failed on Windows hosts. Assert on stripped non-empty lines per platform, making it line-ending- and OS-independent.
…(#49890) doctor's npm audit hardcoded PROJECT_ROOT/scripts/whatsapp-bridge. In read-only Docker installs the bridge deps live in the writable HERMES_HOME mirror (#49561), so node_modules was never found there and the bridge audit silently skipped. Resolve the dir through the shared resolve_whatsapp_bridge_dir() helper so doctor audits where deps actually install. Falls back to the install-tree path if the helper is unavailable.
…ng them When the agent is busy and the user sends multiple text follow-ups, the interrupt-mode and steer-fallback path stored them via merge_pending_message_event(merge_text=True), which newline-joins consecutive TEXT messages into a SINGLE pending turn — collapsing two separate user messages into one mashed-together turn and destroying the message boundaries the user sees (#43066 sub-bug 2). Route that storage through _queue_or_replace_pending_event (the same FIFO infrastructure used by busy queue-mode and /queue) so each follow-up gets its own next-turn slot in arrival order, while still preserving photo-burst / album merge semantics for media. Pure queue-mode already used FIFO; this brings the interrupt/steer-fallback path in line. The sibling defect in #43066 (assistant messages lost after compaction) was already fixed on main by the identity-tracking flush rewrite (#46053) plus the pre-rotation flush (#47202), so this only addresses the remaining busy-message-merge half. Co-authored-by: KiruyaMomochi <65301509+KiruyaMomochi@users.noreply.github.com>
… call The subagent-demotion busy-handler test asserted the internal merge_pending_message_event call, which the FIFO refactor replaced with _queue_or_replace_pending_event. Assert the behavioral outcome (the follow-up lands in the pending slot for the next turn) instead — same fix already applied to the two steer-fallback tests.
…s rotation Three state-loss bugs at the compression rotation boundary, fixed together because they all live in the same ~80-line rotation block: - #33618: a persistent /goal did not follow the rotation. load_goal does a flat per-session lookup with no lineage walk, so a goal silently died when compression minted a fresh child id. Added migrate_goal_to_session() and call it after the child session is created (move-not-copy: the parent row is archived as cleared so exactly one active goal row exists). - #33906/#33907: if the child create_session raised (FK constraint, contended write), the outer handler only warned and let the agent continue on the NEW id — which has no row in state.db — producing an orphan session. Now the rotation rolls agent.session_id back to the still-indexed parent (reopening it) instead of stranding the conversation on a phantom id. - #27633: the compaction-boundary on_session_start notification omitted the platform kwarg, so context-engine plugins saw source=unknown for every message after the boundary. Forward platform (matching the initial session-start call in agent_init.py). Co-authored-by: denisqq <21260182+denisqq@users.noreply.github.com> Co-authored-by: zccyman <16263913+zccyman@users.noreply.github.com> Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
…ssistant troubleshooting
Closes #48835 The bundled himalaya skill and its website docs documented command syntax that does not match Himalaya CLI v1.2.0. Verified against pimalaya/himalaya v1.2.0 source: - message move: MessageMoveCommand declares target_folder BEFORE envelopes (src/email/message/command/move.rs) -> usage is '<TARGET> <ID>...', so 'move 42 "Archive"' is wrong; correct is 'move "Archive" 42'. - message copy: same ordering in copy.rs. - attachment download: AttachmentDownloadCommand exposes the flag as '-d, --downloads-dir <PATH>' (src/email/message/attachment/command/ download.rs), not '--dir'. Fixed in all three surfaces that carried the wrong examples: - skills/email/himalaya/SKILL.md - website/docs/.../email-himalaya.md - website/i18n/zh-Hans/.../email-himalaya.md
…tive PowerShell The Chinese README still told Windows users to install WSL2 and run the Linux installer. Hermes now ships a native PowerShell install script, so replace the outdated WSL2-only note with the direct PowerShell one-liner. Fixes: documentation accuracy / Windows onboarding
CONTRIBUTING.md had no pre-work search step; the only duplicate-check is a PR-template checkbox that fires at review time, after the work is already done. Add a "Before You Start: Search First" section near the top so contributors search open and merged PRs and issues (and the source, since the tracker can lag the code) before building. References #38284 (the agent-side analog). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When model.context_length is set in config.yaml, it blocks auto-detection from the server's /v1/models endpoint. The skill incorrectly implied a hard fallback to 131072. Add the resolution chain and the fix command (hermes config set model.context_length "") to both the config table and a new troubleshooting section.
…ct provider (#50480)
* fix(agent): strip stale reasoning_content when falling back to a strict provider
A reasoning primary (DeepSeek/Kimi/MiMo thinking mode) pins reasoning_content
on every assistant tool-call turn (a single space " " pad). api_messages is
built once under the primary; on a mid-session fallback to a strict
OpenAI-compatible provider (Mistral, Cerebras, Groq, SambaNova), those stale
pads were replayed verbatim and rejected with HTTP 400/422:
body.messages.2.assistant.reasoning_content: Extra inputs are not
permitted (input: ' ')
reapply_reasoning_echo_for_provider() only ever ADDED pads, so it never
reconciled history built under a reasoning primary against a strict fallback.
copy_reasoning_content_for_api() also leaked empty-string and 'reasoning'-only
shapes to non-pad providers.
Fix both sites: when the active provider does not enforce echo-back, strip
reasoning_content (empty, space-pad, or non-empty) entirely. Re-padding when
switching TO a reasoning provider is preserved. Covers the Cerebras 400 from
#45655 and the DeepSeek->Mistral 422 fallback report.
Refs #45655.
* test: update reasoning-replay tests for strict-provider stripping
test_explicit_reasoning_content_beats_normalized_reasoning_on_replay was
implicitly running on the OpenRouter fixture (non-pad); pin it to a reasoning
provider so the precedence it checks is observable. Add a positive
strict-provider test asserting reasoning_content is stripped on replay.
logging.basicConfig() in TrajectoryCompressor.__init__ overrides the root logger configuration every time the class is instantiated. Library code should use logging.getLogger(__name__) and let the application entry point configure the root logger. Fixes inconsistent log formatting when the compressor is used alongside other logging configuration in the gateway.
… main Same library-code anti-pattern as the compressor fix: MiniSWERunner.__init__ called logging.basicConfig(), overriding the application's root logger config every time a runner was instantiated. Moved the call into main() (the CLI entry point) where it belongs; __init__ now only does getLogger(__name__). Standalone verbose logging is preserved.
Remove the dashboard --insecure auth-bypass, add an MCP persistence guard + IOC blocklist, and raise the API-server key entropy floor. Driven by the June 2026 hermes-0day campaign (r/hermesagent, live 854.media instance): scanners find exposed Hermes dashboards/API servers, drive the root agent to plant a 'command: bash' MCP entry that appends an attacker SSH key to authorized_keys, which cron + startup then re-execute every tick. - dashboard: --insecure no longer disables the auth gate. should_require_auth returns True for every non-loopback bind; a public bind ALWAYS requires an auth provider (bundled password provider or OAuth). --insecure kept as a warned no-op for backward compat. Fail-closed error now points at the password provider, not at --insecure. - mcp_security: validate_mcp_server_entry now also rejects shell payloads that write to OS persistence surfaces (authorized_keys/.ssh/pam.d/sudoers/cron/ rc files) and hard-rejects a hermes-0day IOC blocklist (attacker SSH key + source IPs) anywhere in command/args/env. Runs at save AND spawn time. - api_server: raise network-bind API_SERVER_KEY entropy floor 8->16 chars; warn when a network-accessible API server runs an unsandboxed local backend.
The s6 dashboard entrypoint and docker integration tests relied on HERMES_DASHBOARD_INSECURE=1 to bring up a 0.0.0.0 dashboard with no auth provider. With --insecure now a no-op (auth gate mandatory on non-loopback binds), that path fails closed. - s6 dashboard/run: drop --insecure derivation; warn that the env is a no-op and point operators at HERMES_DASHBOARD_BASIC_AUTH_* / OAuth. - docker tests: supervision tests now register the bundled basic password provider (HERMES_DASHBOARD_BASIC_AUTH_USERNAME/_PASSWORD) so the gate has a provider and the dashboard binds. Rewrote the insecure-opt-out test to assert fail-closed (dashboard does NOT serve) instead of gate-bypass. - docs (en + zh-Hans): HERMES_DASHBOARD_INSECURE documented as deprecated no-op; basic-auth is the zero-infra way to authenticate a containerized public dashboard.
Surface dangerous host/deployment posture at gateway startup so operators get the 'you're exposed' signal the June 2026 MCP-config persistence campaign victims never had. Warn-only — never blocks startup, never raises. Checks (each independently fail-safe): - Running as root (POSIX uid 0) - SSH daemon with PasswordAuthentication enabled (incl. the 'yes' default) - Running in a container with no persistent volume mount over HERMES_HOME - Network-accessible API server with no API_SERVER_KEY New module hermes_cli/security_audit_startup.py; invoked once per process from start_gateway() right after setup_logging(). Cross-platform (root/SSH checks no-op on Windows). Idea: @cthulhu.
…r grace A daemon that ignores or stalls in its SIGTERM handler currently survives the process-registry reap and leaks until reboot (observed as agent-browser daemons accumulating to EMFILE on long-running gateways). _terminate_host_pid now snapshots the tree, SIGTERMs it, waits a bounded grace window (terminal.daemon_term_grace_seconds, default 2.0s, 0 disables), then SIGKILLs any survivor. The recycled-PID identity guard still gates the whole path, so escalation never reaches a stranger; Windows is unchanged (taskkill /F is already a hard kill). Config lives in config.yaml (terminal.daemon_term_grace_seconds), NOT an env var, per the .env-secrets-only policy. Implements the SIGKILL-escalation idea from @tkwong's #15008, reworked onto the current _terminate_host_pid tree-kill path (the original predated it) and config-gated instead of env-var-gated. Co-authored-by: Benjamin Wong <tkwong@inspiresynergy.com>
…cs survivors Live testing against a real SIGTERM-ignoring process TREE (parent + children, the agent-browser daemon + renderer shape) revealed psutil.wait_procs's gone/alive partition mis-handles a parent/child tree: it reaps via Process.wait() and could mark targets gone/alive inconsistently across the tree, leaving survivors un-killed (flaky — sometimes the parent lived, sometimes a child). Replace it with: sleep out the grace window, then directly re-probe every captured target (_proc_alive, treating zombies as dead) and SIGKILL any that's still running. Add a multi-child-tree regression test. 6/6 escalation tests green across repeated runs; the real-tree E2E now kills the full tree 6/6 runs.
…#50497) The welcome banner's 'Available Tools' merged in every toolset from the global check_tool_availability() registry walk, regardless of whether it was enabled for the current platform. On a Blank Slate CLI (file + terminal only) that surfaced discord / feishu / kanban tools the agent was never actually given — they are not in the agent's tool schema, but the banner displayed them, making it look like they were exposed. - Filter the unavailable-toolset merge to toolsets actually in enabled_toolsets (a toolset that's enabled but has unmet deps still legitimately shows as disabled/lazy). - Gate the 'Available Skills' section on the skills toolset being enabled — when it's off, the agent can't load any skill, so show 'Skills toolset disabled' instead of the on-disk catalog. When enabled_toolsets is empty (older callers), behavior is unchanged. Validation: blank-slate banner now shows only file + terminal and 'Skills toolset disabled'; a skills-enabled banner still lists the catalog. Added regression tests; full banner suite green (15/15).
…providers (#50492) * feat(providers): remove google-gemini-cli + google-antigravity OAuth providers Google now actively bans accounts for third-party tools that piggyback on Gemini CLI / Antigravity / Code Assist OAuth, and because abuse prevention sits at a backend layer the ban can extend to the entire Google account (Gmail/Drive), with a second violation being permanent. Ref: google-gemini/gemini-cli#20632 Removes both OAuth inference providers entirely (modules, provider profiles, auth/runtime/config/models wiring, the /gquota Code Assist quota command, the antigravity-cli optional skill, desktop + docs surface in en + zh-Hans). The API-key 'gemini' provider (GOOGLE_API_KEY/GEMINI_API_KEY against generativelanguage.googleapis.com) is unaffected and stays fully supported. * fix(skills): keep the antigravity-cli skill — only the OAuth provider is removed The antigravity-cli optional skill orchestrates the external `agy` binary as a coding-agent tool via the terminal tool — it does NOT wrap Hermes inference through the banned google-antigravity OAuth provider, so it carries none of the account-ban risk that motivated removing that provider. Restore the skill, its docs page, the sidebar entry, and the optional-skills catalog row. The google-antigravity / google-gemini-cli inference providers stay fully removed.
gateway/platforms/telegram.py no longer exists (adapters moved to plugins/platforms/<name>/adapter.py) and telegram no longer uses the scoped-lock pattern. Point the token-lock canonical-pattern reference to plugins/platforms/irc/adapter.py, which acquires the lock in connect() and releases it in disconnect() — and is already cited as a canonical example in ADDING_A_PLATFORM.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ns/platforms Sibling-site follow-up to the AGENTS.md token-lock fix (#50481). Platform adapters migrated from gateway/platforms/<name>.py to plugins/platforms/<name>/adapter.py; a handful (signal, weixin, bluebubbles, qqbot, yuanbao, msgraph_webhook, webhook, api_server) still live in gateway/platforms/. - adding-platform-adapters.md: new-adapter creation path + reference-impl table - gateway-internals.md: rewrite the adapter tree to reflect the actual split - zh-Hans mirrors of both kept in parity - scripts/release.py: add TutkuEroglu to AUTHOR_MAP (CI gate)
…0534) Plugins shelling out to bare `hermes` via the terminal tool hit `command not found` (exit 127) when the gateway was launched without the hermes install dir on PATH (systemd, service managers, cron, desktop launchers) — even though `hermes` works in the user's own interactive terminal, which sources the shell rc that exports that dir. The terminal tool's subshell PATH was the agent process PATH plus a static set of system dirs (_SANE_PATH); it never included wherever the hermes console-script actually lives (~/.local/bin, the venv bin/Scripts, pipx, nix). Resolve that dir once (which/argv0/sys.executable) and prepend-if-missing it so bare `hermes` resolves regardless of launch method.
…p (#50499) * feat(cli): /reasoning full to show complete thinking, not 10-line clamp The post-response Reasoning recap box hard-clamped long thinking to the first 10 lines, so there was no way to see the full reasoning trace after a turn (live streaming already shows it in full). Add display.reasoning_full (default off) plus /reasoning full|clamp to toggle it at runtime; the clamp truncation note now points at the command. Addresses repeated user requests to show all thinking tokens. * test(gateway): de-snapshot /reasoning help assertion The test froze the exact args-hint literal '/reasoning [level|show|hide]', which the new full/clamp args change to '[level|show|hide|full|clamp]'. Convert to an invariant: assert /reasoning is in help and carries its core args, not the exact hint string. * feat(tui): /reasoning full|clamp parity in tui_gateway The classic-CLI reasoning_full toggle had no TUI equivalent — typing /reasoning full in the TUI fell through to parse_reasoning_effort and errored. The TUI renders thinking as an expand/collapse section (no fixed 10-line recap), so map full -> sections.thinking=expanded (raw, uncapped via thinkingPreview mode='full') and clamp -> collapsed, persisting display.reasoning_full for cross-surface config consistency.
* feat(cli): /prompt — compose your next prompt in $EDITOR Adds /prompt (alias /compose): opens $VISUAL/$EDITOR on a temp markdown file so you can hand-edit a multi-line prompt, then sends the saved buffer as the next agent turn. Text after the command pre-seeds the buffer; an empty save cancels. Reuses the one-shot _pending_agent_seed the interactive loop already consumes (same mechanism as /blueprint), so no changes to the input event loop or message pipeline. CLI-only. * feat(tui): /prompt slash command opens $EDITOR (parity with CLI) The TUI already opens $EDITOR via Ctrl+G (openEditor), but had no /prompt slash command like the classic CLI. Wire openEditor into the slash handler context and register /prompt (alias /compose) to call it; inline text after the command is dropped into the composer first so it carries into the editor, matching the CLI's /prompt <text>.
…ind (#50551)
When `hermes dashboard --host 0.0.0.0` is run interactively with the auth
gate engaged but no DashboardAuthProvider configured, prompt to set up the
bundled username/password provider on the spot (or point at `hermes dashboard
register` for OAuth) instead of only emitting the fail-closed error.
- main.py: `_maybe_setup_dashboard_auth_interactively()` runs before
start_server. No-ops on loopback binds, when a provider is already
registered, or when stdin/stdout isn't a TTY (Docker/s6, CI, piped runs) so
the fail-closed SystemExit stays the backstop for unattended deploys. On the
password path it writes dashboard.basic_auth.{username,password_hash,secret}
to config.yaml (scrypt hash, never plaintext), then force-rediscovers
plugins so the basic provider registers before the gate check.
- web_server.py: fix the fail-closed hint — it told operators to set
`dashboard_auth.basic.username` but the provider reads `dashboard.basic_auth`.
- docs: note the interactive setup under Fail-closed semantics.
No new env vars; reuses the existing dashboard.basic_auth config surface.
…50600)
* fix(gateway): walk /proc/*/cmdline to find main-wrapper.sh under s6-overlay v3 (#49196)
(cherry picked from commit 3a108c2df0edce4ce0e6f9f3a8eb8db3839a4630)
* fix(container): peel s6-v3 rc.init prefix so dashboard role is detected
kyssta-exe's preceding commit (#49238) fixed _read_container_argv() to
locate the rc.init-launched main-wrapper.sh process under s6-overlay v3,
but the skip still never fired: _strip_container_argv_prefix() only peeled
a prefix when args[0] was init/main-wrapper.sh/hermes. Under s6 v3 the
matched argv is
/bin/sh -e /run/s6/basedir/scripts/rc.init top
/opt/hermes/docker/main-wrapper.sh dashboard ...
so args[0] stayed /bin/sh, _is_dashboard_container() returned False, and
the dashboard container reconciled + started its own gateway-default —
the exact dual Telegram getUpdates 409 in issue #49196.
Fix: strip everything up to and including the main-wrapper.sh token (the
stable boundary the image owns), covering both the v2 (/init ...) and v3
(/bin/sh ... rc.init top ...) shapes with one rule, instead of matching
launcher tokens positionally. This also repairs _is_legacy_gateway_run_request()
under v3, which shares the same strip helper (the issue called this out).
Tests: extend the dashboard true/false parametrize sets with the s6-v3
argv shape, and add test_main_skips_reconcile_in_dashboard_container_s6v3
exercising main() end-to-end with the v3 argv. Verified via mutation that
both new v3 assertions fail under the old positional strip and pass with
the fix.
---------
Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
… in CLI + gateway (#50631) terminal.docker_extra_args passes flags verbatim to `docker run` (e.g. --gpus=all, --shm-size=16g). It was wired into DEFAULT_CONFIG, TERMINAL_CONFIG_ENV_MAP (so `hermes config set` bridged it), terminal_tool._get_env_config (reads TERMINAL_DOCKER_EXTRA_ARGS), and DockerEnvironment (applies extra_args) -- but it was MISSING from cli.py's env_mappings and gateway/run.py's _terminal_env_map. Consequence: a user who hand-edits config.yaml (rather than running `hermes config set`) has docker_extra_args silently dropped on the CLI and gateway/desktop startup paths, while docker_image / docker_volumes (which ARE in those maps) bridge correctly -- producing the reported 'Hermes partially reads the Docker config' symptom where --gpus=all and --shm-size=16g never reach docker run. This is the same bridge-coverage bug class that shipped before for docker_run_as_host_user (cli + gateway) and docker_mount_cwd_to_workspace (gateway). Fix by adding the key to both maps, plus a dedicated regression pin in test_terminal_config_env_sync.py mirroring the existing test_docker_*_is_bridged_everywhere guards.
…orms Authorization to message the agent is the gate, not the file extension. Previously the inbound-attachment allowlist (SUPPORTED_DOCUMENT_TYPES) was opt-OUT on Discord (allow_any_attachment defaulted false) and had no bypass at all on Telegram/Slack — so an .html (or any non-allowlisted type) was dropped or hard-rejected before the agent saw it. Now every authorized upload is cached and surfaced to the agent regardless of type: - base.cache_media_bytes(): unknown types cache as octet-stream (or the caller-supplied MIME) instead of returning None — fixes the chokepoint that Teams/Telegram-media route through. - discord/telegram/slack adapters: removed the allowlist reject/skip; any non-media attachment is typed DOCUMENT and cached. Known types keep their precise MIME. - Text inlining now gates on a shared _TEXT_INJECT_EXTENSIONS set (text + code + config + markup) instead of a blind UTF-8 decode, so binary formats (PDF/zip/docx) with ASCII headers are never inlined. - gateway/run.py emits the path-pointing context note for every DOCUMENT, including non text/application MIME types. - discord.allow_any_attachment is now a documented no-op kept for config back-compat. Validation: 357 gateway tests pass; E2E confirms .html/.bin/custom types cache, known types stay precise, PDFs are not inlined.
Follow-up to the accept-any-file-type change. The observe-unmentioned and replied-media paths relied on cache_media_bytes() returning None for unsupported document types to emit an 'unsupported, not cached' note. Now that any file type is always cached, those docs are cached and surfaced with a path-pointing note — consistent with the main document path. The remaining cached-is-None branch is image-validation-failure only; its note is reworded accordingly. Updates the group-gating test to the new contract.
Ctrl+G already opened $EDITOR with the current draft, but used open_in_editor(validate_and_handle=False), which only loaded the saved text back into the input area — the user still had to press Enter. The TUI's Ctrl+G (openEditor) submits the draft on a clean exit. Since CLI submission is driven by the custom Enter keybinding (not the buffer accept_handler), validate_and_handle can't route through it; instead chain a done-callback on the editor Task that calls the new _submit_editor_buffer(), which mirrors the Enter handler's idle/queue/slash branches and drops an empty save.
display.timestamps already drove the [HH:MM] suffix on live submitted and streamed message labels, but there was no runtime command to toggle it and /history ignored the setting entirely. Add /timestamps [on|off|status] (alias /ts) and render [HH:MM] in /history for turns that carry a stored unix timestamp (resumed sessions). Live unsaved turns without a stored time are never given a fabricated one. Uses the existing sanctioned non-wire 'timestamp' message key (stripped before the API call in chat_completions), so message-alternation and prompt-cache invariants are untouched.
Contributor
🔎 Lint report:
|
| Rule | Count |
|---|---|
unknown |
47 |
First entries
tools/skill_manager_tool.py:744: [unknown] Expected `,`, found `>>`
plugins/platforms/slack/adapter.py:5077: [unknown] unindent does not match any outer indentation level
gateway/config.py:1083: [unknown] Expected `except` or `finally` after `try` block
tools/skill_manager_tool.py:741: [unknown] Expected `:`, found `,`
tests/gateway/test_display_config.py:504: [unknown] Expected `,`, found string
tools/skill_manager_tool.py:931: [unknown] Expected a statement
tests/tools/test_skill_manager_tool.py:1326: [unknown] Expected a statement
tools/skill_manager_tool.py:212: [unknown] Simple statements must be separated by newlines or semicolons
tools/skill_manager_tool.py:742: [unknown] Expected `,`, found `==`
tools/skill_manager_tool.py:745: [unknown] Expected `:`, found `}`
plugins/platforms/slack/adapter.py:5406: [unknown] Invalid annotated assignment target
tools/skill_manager_tool.py:742: [unknown] Expected `,`, found `=`
tools/skill_manager_tool.py:741: [unknown] Expected `,`, found `:`
tools/skill_manager_tool.py:744: [unknown] Expected `,`, found `>`
gateway/config.py:1193: [unknown] Expected a statement
agent/prompt_builder.py:1725: [unknown] Expected a statement
plugins/platforms/slack/adapter.py:5407: [unknown] Expected dedent, found end of file
tools/send_message_tool.py:1500: [unknown] Expected a statement
tests/gateway/test_display_config.py:566: [unknown] Expected a statement
tools/skill_manager_tool.py:193: [unknown] Compound statements are not allowed on the same line as simple statements
plugins/platforms/slack/adapter.py:5095: [unknown] Unexpected indentation
tools/send_message_tool.py:950: [unknown] Expected an indented block after `if` statement
tools/skill_manager_tool.py:251: [unknown] Expected an expression
tests/gateway/test_display_config.py:505: [unknown] Expected `,`, found `:`
tests/tools/test_send_message_target_parse.py:95: [unknown] Expected a statement
... and 22 more
✅ Fixed issues: none
Unchanged: 0 pre-existing issues carried over.
ty (type checker)
Total: 12067 on HEAD, 10971 on base (🆕 +1096)
🆕 New issues (764):
| Rule | Count |
|---|---|
unresolved-import |
196 |
unresolved-attribute |
174 |
invalid-argument-type |
81 |
unresolved-reference |
75 |
invalid-syntax |
53 |
invalid-type-form |
50 |
not-subscriptable |
35 |
invalid-assignment |
35 |
unsupported-operator |
28 |
invalid-method-override |
18 |
invalid-return-type |
7 |
unused-type-ignore-comment |
3 |
call-non-callable |
3 |
no-matching-overload |
2 |
invalid-raise |
1 |
| +3 more rules |
First entries
tests/tools/test_clarify_tool.py:213: [invalid-argument-type] invalid-argument-type: Argument to function `clarify_tool` is incorrect: Expected `list[str] | None`, found `list[str | dict[str, str]]`
tests/tools/test_browser_hardening.py:125: [not-subscriptable] not-subscriptable: Cannot subscript object of type `int` with no `__getitem__` method
plugins/platforms/feishu/feishu_comment.py:41: [unresolved-import] unresolved-import: Cannot resolve imported module `lark_oapi.core.model.base_request`
plugins/platforms/telegram/adapter.py:134: [unresolved-import] unresolved-import: Module `telegram` has no member `Bot`
tools/delegate_tool.py:2554: [invalid-argument-type] invalid-argument-type: Argument to function `dispatch_async_delegation_batch` is incorrect: Expected `list[str]`, found `list[Any | str | None | list[str]]`
tests/hermes_cli/test_managed_scope_loaders.py:93: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `None` in union `dict[Unknown, Unknown] | None`
plugins/platforms/wecom/callback_adapter.py:33: [unresolved-import] unresolved-import: Cannot resolve imported module `aiohttp`
tests/agent/test_compression_rotation_state.py:56: [unresolved-attribute] unresolved-attribute: Unresolved attribute `context_compressor` on type `AIAgent`
tools/send_message_tool.py:991: [invalid-syntax] invalid-syntax: Expected an expression
tests/run_agent/test_run_agent.py:5873: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_disable_streaming` on type `AIAgent`
plugins/platforms/wecom/adapter.py:1572: [unresolved-import] unresolved-import: Cannot resolve imported module `qrcode`
tests/gateway/relay/test_relay_registration.py:12: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/gateway/test_pre_gateway_dispatch.py:44: [invalid-type-form] invalid-type-form: Variable of type `Never` is not allowed in a parameter annotation
tests/gateway/test_display_config.py:535: [invalid-syntax] invalid-syntax: Invalid annotated assignment target
tests/gateway/relay/test_relay_roundtrip.py:15: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
gateway/run.py:16006: [invalid-argument-type] invalid-argument-type: Argument to bound method `AIAgent.run_conversation` is incorrect: Expected `str`, found `Any | str | int | float | list[dict[str, Any]]`
plugins/platforms/matrix/adapter.py:1858: [invalid-method-override] invalid-method-override: Invalid override of method `send_document`: Definition is incompatible with `BasePlatformAdapter.send_document`
tests/test_tui_gateway_server.py:3312: [unresolved-attribute] unresolved-attribute: Attribute `append` is not defined on `int` in union `int | list[Unknown]`
tests/agent/test_secret_scope.py:2: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/tools/test_browser_hardening.py:125: [not-subscriptable] not-subscriptable: Cannot subscript object of type `None` with no `__getitem__` method
tests/hermes_cli/test_default_interface_resolution.py:191: [not-subscriptable] not-subscriptable: Cannot subscript object of type `float` with no `__getitem__` method
plugins/platforms/dingtalk/adapter.py:137: [unresolved-import] unresolved-import: Cannot resolve imported module `dingtalk_stream.frames`
plugins/platforms/slack/adapter.py:536: [invalid-type-form] invalid-type-form: Variable of type `Never` is not allowed in a parameter annotation
gateway/relay/ws_transport.py:181: [unresolved-attribute] unresolved-attribute: Attribute `connect` is not defined on `None` in union `Unknown | None`
tests/agent/test_turn_finalizer_cleanup_guard.py:11: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
... and 739 more
✅ Fixed issues (348):
| Rule | Count |
|---|---|
unresolved-attribute |
123 |
unresolved-import |
73 |
invalid-argument-type |
59 |
unsupported-operator |
27 |
invalid-assignment |
25 |
invalid-method-override |
20 |
invalid-return-type |
5 |
no-matching-overload |
4 |
call-non-callable |
3 |
unknown-argument |
2 |
unresolved-reference |
2 |
unresolved-global |
1 |
not-subscriptable |
1 |
unused-type-ignore-comment |
1 |
invalid-parameter-default |
1 |
| +1 more rules |
First entries
gateway/platforms/whatsapp.py:156: [unresolved-import] unresolved-import: Cannot resolve imported module `psutil`
gateway/platforms/matrix.py:1784: [unresolved-import] unresolved-import: Cannot resolve imported module `httpx`
gateway/platforms/telegram.py:4857: [unresolved-attribute] unresolved-attribute: Attribute `SUPERGROUP` is not defined on `None` in union `Unknown | None`
tests/agent/test_gemini_cloudcode.py:246: [unresolved-attribute] unresolved-attribute: Attribute `project_id` is not defined on `None` in union `GoogleCredentials | None`
gateway/run.py:11869: [invalid-argument-type] invalid-argument-type: Argument to bound method `set.add` is incorrect: Expected `tuple[str, str, str | None]`, found `tuple[Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown], str, str | None]`
gateway/platforms/matrix.py:1246: [unresolved-import] unresolved-import: Cannot resolve imported module `mautrix.crypto`
gateway/platforms/wecom.py:372: [unresolved-attribute] unresolved-attribute: Attribute `WSMsgType` is not defined on `None` in union `Unknown | None`
gateway/platforms/feishu.py:3537: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_last_chunk_len` on type `MessageEvent`
gateway/platforms/dingtalk.py:1067: [unresolved-attribute] unresolved-attribute: Attribute `CreateCardHeaders` is not defined on `None` in union `Unknown | None`
gateway/platforms/feishu.py:3281: [unresolved-import] unresolved-import: Cannot resolve imported module `httpx`
gateway/platforms/feishu.py:1369: [unresolved-import] unresolved-import: Cannot resolve imported module `lark_oapi.core.const`
agent/gemini_cloudcode_adapter.py:523: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `None` in union `Any | None | dict[str, Any]`
agent/display.py:303: [invalid-argument-type] invalid-argument-type: Argument to function `_resolve_skill_dir` is incorrect: Expected `str`, found `Unknown | None`
gateway/platforms/telegram.py:138: [unresolved-import] unresolved-import: Cannot resolve imported module `telegram.ext`
gateway/platforms/whatsapp.py:1019: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_last_chunk_len` on type `MessageEvent`
gateway/platforms/matrix.py:1141: [unresolved-import] unresolved-import: Cannot resolve imported module `mautrix.client.state_store`
tests/skills/test_google_oauth_setup.py:109: [unresolved-attribute] unresolved-attribute: Unresolved attribute `flow` on type `ModuleType`
gateway/platforms/dingtalk.py:1051: [unresolved-attribute] unresolved-attribute: Attribute `CreateCardRequestCardData` is not defined on `None` in union `Unknown | None`
gateway/platforms/wecom_callback.py:271: [unresolved-attribute] unresolved-attribute: Attribute `Request` is not defined on `None` in union `Unknown | None`
agent/auxiliary_client.py:3124: [unknown-argument] unknown-argument: Argument `api_key` does not match any known parameter of function `resolve_provider_client`
tests/gateway/test_busy_session_auth_bypass.py:44: [invalid-argument-type] invalid-argument-type: Argument is incorrect: Expected `Platform`, found `MagicMock`
hermes_cli/dump.py:236: [invalid-assignment] invalid-assignment: Object of type `Literal["(unknown)"]` is not assignable to `Literal["0.16.0"]`
gateway/platforms/slack.py:4560: [invalid-return-type] invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `str`
gateway/platforms/dingtalk.py:921: [unresolved-attribute] unresolved-attribute: Attribute `TimeoutException` is not defined on `None` in union `Unknown | None`
tests/cron/test_suggestions.py:197: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["monitor"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 33 union elements`
... and 323 more
Unchanged: 5389 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
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.
Conflicts detected. Resolve manually before marking ready.
Files with conflict markers:
Brings in upstream commits up to
5ff11a689(upstream/main).Generated by
ops/hermes-upstream-sync. Branch protection onmainenforces that the agent cannot self-merge — review and merge manually.