Skip to content

fix(kanban-dashboard): add human_review column to BOARD_COLUMNS#3

Closed
sahilm-ti wants to merge 224 commits into
mainfrom
fix/dashboard-human-review-column
Closed

fix(kanban-dashboard): add human_review column to BOARD_COLUMNS#3
sahilm-ti wants to merge 224 commits into
mainfrom
fix/dashboard-human-review-column

Conversation

@sahilm-ti

Copy link
Copy Markdown
Owner

Add the new human_review status to the dashboard so cards in that state are visible on the board.

Backend

plugins/kanban/dashboard/plugin_api.py: append human_review between review and done in BOARD_COLUMNS. The endpoint already buckets tasks by status into BOARD_COLUMNS, so this is the only Python change required.

Frontend (dist/index.js)

There is no src/ tree — the bundled JS is the canonical UI.

  • COLUMN_ORDER: extended to the full backend status list. scheduled and review were also missing from the JS fallback; aligning the array with the backend keeps the frontend's hard-coded order honest.
  • FALLBACK_COLUMN_LABEL / FALLBACK_COLUMN_HELP: added entries for scheduled, review, and human_review so the dashboard renders sane text when the i18n catalog has no key.
  • COLUMN_DOT: added classes for the new columns.

Style (dist/style.css)

Added .hermes-kanban-dot-scheduled, .hermes-kanban-dot-review, .hermes-kanban-dot-human-review with distinct colors so the new columns are visually distinguishable.

Context

Safe to merge independently — until the human_review status itself lands, this is a no-op (no tasks will be bucketed into the new column). Once it lands, those cards now have a home between review and done.

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 297 files, which is 147 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 21945a84-241c-4a0d-b1bf-fd680912b0cb

📥 Commits

Reviewing files that changed from the base of the PR and between 6016199 and 363ed44.

⛔ Files ignored due to path filters (3)
  • infographic/kanban-db-corruption-defense/infographic.png is excluded by !**/*.png
  • plugins/kanban/dashboard/dist/index.js is excluded by !**/dist/**
  • plugins/kanban/dashboard/dist/style.css is excluded by !**/dist/**
📒 Files selected for processing (297)
  • .github/actions/hermes-smoke-test/action.yml
  • .github/workflows/docker-lint.yml
  • .github/workflows/docker-publish.yml
  • .github/workflows/supply-chain-audit.yml
  • .hadolint.yaml
  • Dockerfile
  • README.md
  • README.zh-CN.md
  • acp_adapter/server.py
  • agent/agent_init.py
  • agent/agent_runtime_helpers.py
  • agent/anthropic_adapter.py
  • agent/auxiliary_client.py
  • agent/background_review.py
  • agent/chat_completion_helpers.py
  • agent/codex_responses_adapter.py
  • agent/context_compressor.py
  • agent/context_engine.py
  • agent/conversation_compression.py
  • agent/conversation_loop.py
  • agent/credential_persistence.py
  • agent/credential_pool.py
  • agent/credential_sources.py
  • agent/display.py
  • agent/error_classifier.py
  • agent/file_safety.py
  • agent/image_gen_provider.py
  • agent/model_metadata.py
  • agent/redact.py
  • agent/secret_sources/bitwarden.py
  • agent/system_prompt.py
  • agent/tool_executor.py
  • agent/transcription_provider.py
  • agent/transcription_registry.py
  • agent/transports/anthropic.py
  • agent/transports/chat_completions.py
  • agent/transports/codex.py
  • agent/transports/codex_app_server_session.py
  • agent/tts_provider.py
  • agent/tts_registry.py
  • cli-config.yaml.example
  • cli.py
  • cron/jobs.py
  • cron/scheduler.py
  • docker-compose.yml
  • docker/cont-init.d/015-supervise-perms
  • docker/cont-init.d/02-reconcile-profiles
  • docker/entrypoint.sh
  • docker/main-wrapper.sh
  • docker/s6-rc.d/dashboard/dependencies.d/base
  • docker/s6-rc.d/dashboard/finish
  • docker/s6-rc.d/dashboard/run
  • docker/s6-rc.d/dashboard/type
  • docker/s6-rc.d/main-hermes/dependencies.d/base
  • docker/s6-rc.d/main-hermes/run
  • docker/s6-rc.d/main-hermes/type
  • docker/s6-rc.d/user/contents.d/dashboard
  • docker/s6-rc.d/user/contents.d/main-hermes
  • docker/stage2-hook.sh
  • docs/plans/2026-05-07-s6-overlay-dynamic-subagent-gateways.md
  • gateway/config.py
  • gateway/platforms/api_server.py
  • gateway/platforms/base.py
  • gateway/platforms/bluebubbles.py
  • gateway/platforms/dingtalk.py
  • gateway/platforms/feishu.py
  • gateway/platforms/matrix.py
  • gateway/platforms/msgraph_webhook.py
  • gateway/platforms/qqbot/adapter.py
  • gateway/platforms/telegram.py
  • gateway/platforms/webhook.py
  • gateway/platforms/wecom.py
  • gateway/platforms/wecom_callback.py
  • gateway/run.py
  • gateway/session_context.py
  • gateway/stream_consumer.py
  • hermes_cli/_parser.py
  • hermes_cli/auth.py
  • hermes_cli/auth_commands.py
  • hermes_cli/callbacks.py
  • hermes_cli/cli_output.py
  • hermes_cli/commands.py
  • hermes_cli/config.py
  • hermes_cli/container_boot.py
  • hermes_cli/debug.py
  • hermes_cli/doctor.py
  • hermes_cli/env_loader.py
  • hermes_cli/fallback_cmd.py
  • hermes_cli/fallback_config.py
  • hermes_cli/gateway.py
  • hermes_cli/kanban.py
  • hermes_cli/kanban_db.py
  • hermes_cli/main.py
  • hermes_cli/memory_setup.py
  • hermes_cli/models.py
  • hermes_cli/oneshot.py
  • hermes_cli/plugins.py
  • hermes_cli/plugins_cmd.py
  • hermes_cli/profiles.py
  • hermes_cli/providers.py
  • hermes_cli/secret_prompt.py
  • hermes_cli/secrets_cli.py
  • hermes_cli/security_audit.py
  • hermes_cli/service_manager.py
  • hermes_cli/setup.py
  • hermes_cli/skills_hub.py
  • hermes_cli/tools_config.py
  • hermes_cli/web_server.py
  • hermes_cli/webhook.py
  • locales/af.yaml
  • locales/de.yaml
  • locales/en.yaml
  • locales/es.yaml
  • locales/fr.yaml
  • locales/ga.yaml
  • locales/hu.yaml
  • locales/it.yaml
  • locales/ja.yaml
  • locales/ko.yaml
  • locales/pt.yaml
  • locales/ru.yaml
  • locales/tr.yaml
  • locales/uk.yaml
  • locales/zh-hant.yaml
  • locales/zh.yaml
  • optional-skills/research/darwinian-evolver/scripts/show_snapshot.py
  • plugins/image_gen/openai/__init__.py
  • plugins/image_gen/xai/__init__.py
  • plugins/kanban/dashboard/plugin_api.py
  • plugins/memory/hindsight/__init__.py
  • plugins/memory/honcho/cli.py
  • plugins/platforms/google_chat/oauth.py
  • plugins/platforms/line/adapter.py
  • plugins/platforms/mattermost/__init__.py
  • plugins/platforms/mattermost/adapter.py
  • plugins/platforms/mattermost/plugin.yaml
  • plugins/platforms/ntfy/__init__.py
  • plugins/platforms/ntfy/adapter.py
  • plugins/platforms/ntfy/plugin.yaml
  • plugins/platforms/simplex/adapter.py
  • plugins/video_gen/xai/__init__.py
  • run_agent.py
  • scripts/release.py
  • scripts/run_tests_parallel.py
  • skills/software-development/hermes-s6-container-supervision/SKILL.md
  • tests/acp/test_server.py
  • tests/agent/test_anthropic_adapter.py
  • tests/agent/test_anthropic_mcp_prefix_strip.py
  • tests/agent/test_auxiliary_config_bridge.py
  • tests/agent/test_credential_pool.py
  • tests/agent/test_display_todo_progress.py
  • tests/agent/test_display_tool_failure.py
  • tests/agent/test_error_classifier.py
  • tests/agent/test_file_safety_credentials.py
  • tests/agent/test_file_safety_cross_profile.py
  • tests/agent/test_last_total_tokens.py
  • tests/agent/test_non_stream_stale_timeout.py
  • tests/agent/test_redact.py
  • tests/agent/test_save_url_image.py
  • tests/agent/test_transcription_registry.py
  • tests/agent/test_tts_registry.py
  • tests/agent/test_vision_routing_31179.py
  • tests/agent/transports/test_chat_completions.py
  • tests/agent/transports/test_codex_app_server_session.py
  • tests/agent/transports/test_codex_transport.py
  • tests/cli/test_branch_command.py
  • tests/cli/test_cli_context_warning.py
  • tests/cli/test_cli_init.py
  • tests/cli/test_cli_new_session.py
  • tests/cli/test_cli_provider_resolution.py
  • tests/cli/test_cli_resume_command.py
  • tests/cli/test_cli_secret_capture.py
  • tests/cli/test_destructive_slash_confirm.py
  • tests/cli/test_destructive_slash_inline_skip_e2e.py
  • tests/cli/test_exit_summary_resume_hint.py
  • tests/cli/test_resume_display.py
  • tests/cli/test_resume_quiet_stderr.py
  • tests/cli/test_tool_progress_scrollback.py
  • tests/cron/test_cron_context_from.py
  • tests/cron/test_jobs.py
  • tests/cron/test_scheduler.py
  • tests/docker/__init__.py
  • tests/docker/conftest.py
  • tests/docker/test_container_restart.py
  • tests/docker/test_dashboard.py
  • tests/docker/test_main_invocation.py
  • tests/docker/test_profile_gateway.py
  • tests/docker/test_s6_profile_gateway_integration.py
  • tests/docker/test_tui_passthrough.py
  • tests/docker/test_zombie_reaping.py
  • tests/gateway/test_active_session_text_merge.py
  • tests/gateway/test_api_server.py
  • tests/gateway/test_api_server_jobs.py
  • tests/gateway/test_auth_fallback.py
  • tests/gateway/test_base_topic_sessions.py
  • tests/gateway/test_bluebubbles.py
  • tests/gateway/test_busy_session_ack.py
  • tests/gateway/test_command_bypass_active_session.py
  • tests/gateway/test_compression_session_id_persistence.py
  • tests/gateway/test_config_env_bridge_authority.py
  • tests/gateway/test_dingtalk.py
  • tests/gateway/test_discord_bot_auth_bypass.py
  • tests/gateway/test_feishu.py
  • tests/gateway/test_feishu_approval_buttons.py
  • tests/gateway/test_google_chat.py
  • tests/gateway/test_interrupt_key_match.py
  • tests/gateway/test_loop_exception_handler.py
  • tests/gateway/test_matrix.py
  • tests/gateway/test_mattermost.py
  • tests/gateway/test_media_download_retry.py
  • tests/gateway/test_msgraph_webhook.py
  • tests/gateway/test_ntfy_plugin.py
  • tests/gateway/test_platform_connected_checkers.py
  • tests/gateway/test_platform_registry.py
  • tests/gateway/test_qqbot.py
  • tests/gateway/test_restart_drain.py
  • tests/gateway/test_resume_command.py
  • tests/gateway/test_run_progress_topics.py
  • tests/gateway/test_send_multiple_images.py
  • tests/gateway/test_session_split_brain_11016.py
  • tests/gateway/test_stream_consumer.py
  • tests/gateway/test_telegram_group_gating.py
  • tests/gateway/test_telegram_send_path_health.py
  • tests/gateway/test_telegram_thread_fallback.py
  • tests/gateway/test_telegram_topic_mode.py
  • tests/gateway/test_webhook_adapter.py
  • tests/gateway/test_wecom.py
  • tests/gateway/test_wecom_callback.py
  • tests/gateway/test_ws_auth_retry.py
  • tests/hermes_cli/test_anthropic_model_flow_stale_oauth.py
  • tests/hermes_cli/test_anthropic_oauth_flow.py
  • tests/hermes_cli/test_argparse_flag_propagation.py
  • tests/hermes_cli/test_auth_commands.py
  • tests/hermes_cli/test_auth_qwen_provider.py
  • tests/hermes_cli/test_auth_usable_secret.py
  • tests/hermes_cli/test_cli_output.py
  • tests/hermes_cli/test_config.py
  • tests/hermes_cli/test_container_boot.py
  • tests/hermes_cli/test_debug.py
  • tests/hermes_cli/test_env_loader.py
  • tests/hermes_cli/test_fallback_cmd.py
  • tests/hermes_cli/test_gateway_s6_dispatch.py
  • tests/hermes_cli/test_kanban_db.py
  • tests/hermes_cli/test_kanban_promote.py
  • tests/hermes_cli/test_plugin_auxiliary_tasks.py
  • tests/hermes_cli/test_plugins_cmd.py
  • tests/hermes_cli/test_plugins_transcription_registration.py
  • tests/hermes_cli/test_plugins_tts_registration.py
  • tests/hermes_cli/test_profiles_s6_hooks.py
  • tests/hermes_cli/test_prompt_api_key.py
  • tests/hermes_cli/test_secret_prompt.py
  • tests/hermes_cli/test_security_audit.py
  • tests/hermes_cli/test_service_manager.py
  • tests/hermes_cli/test_setup_prompt_menus.py
  • tests/hermes_cli/test_skills_hub.py
  • tests/hermes_cli/test_timeouts.py
  • tests/hermes_cli/test_tts_picker.py
  • tests/hermes_cli/test_update_concurrent_quarantine.py
  • tests/hermes_cli/test_web_server.py
  • tests/hermes_cli/test_web_server_cron_profiles.py
  • tests/hermes_cli/test_web_server_host_header.py
  • tests/hermes_cli/test_web_server_oauth_write.py
  • tests/hermes_cli/test_webhook_cli.py
  • tests/hermes_cli/test_xai_provider_labels.py
  • tests/plugins/image_gen/test_openai_provider.py
  • tests/plugins/image_gen/test_xai_provider.py
  • tests/plugins/transcription/__init__.py
  • tests/plugins/transcription/check_parity_vs_main.py
  • tests/plugins/tts/__init__.py
  • tests/plugins/tts/check_parity_vs_main.py
  • tests/run_agent/test_31273_402_not_retried.py
  • tests/run_agent/test_413_compression.py
  • tests/run_agent/test_codex_xai_oauth_recovery.py
  • tests/run_agent/test_openai_client_lifecycle.py
  • tests/run_agent/test_partial_stream_finish_reason.py
  • tests/run_agent/test_plugin_context_engine_init.py
  • tests/run_agent/test_run_agent.py
  • tests/run_agent/test_run_agent_codex_responses.py
  • tests/run_agent/test_tool_call_guardrail_runtime.py
  • tests/test_bitwarden_secrets.py
  • tests/test_tui_gateway_server.py
  • tests/tools/test_approval.py
  • tests/tools/test_browser_orphan_reaper.py
  • tests/tools/test_cross_profile_guard.py
  • tests/tools/test_docker_environment.py
  • tests/tools/test_dockerfile_pid1_reaping.py
  • tests/tools/test_file_operations.py
  • tests/tools/test_file_tools.py
  • tests/tools/test_local_interrupt_cleanup.py
  • tests/tools/test_mcp_tool.py
  • tests/tools/test_memory_tool.py
  • tests/tools/test_notify_on_complete.py
  • tests/tools/test_process_registry.py
  • tests/tools/test_send_message_missing_platforms.py
  • tests/tools/test_skills_ast_audit.py
  • tests/tools/test_skills_guard.py
  • tests/tools/test_transcription_command_providers.py

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dashboard-human-review-column

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented May 25, 2026

Copy link
Copy Markdown

🔎 Lint report: fix/dashboard-human-review-column vs origin/main

ruff

Total: 1382 on HEAD, 0 on base (🆕 +1382)

🆕 New issues (10):

Rule Count
invalid-syntax 10
First entries
scripts/release.py:47: [invalid-syntax] Starred expression cannot be used here
scripts/release.py:410: [invalid-syntax] Expected `:`, found name
scripts/release.py:47: [invalid-syntax] Expected an expression
scripts/release.py:527: [invalid-syntax] Expected a statement
scripts/release.py:413: [invalid-syntax] Unexpected indentation
scripts/release.py:410: [invalid-syntax] Expected `]`, found name
scripts/release.py:412: [invalid-syntax] Expected `,`, found name
scripts/release.py:526: [invalid-syntax] Invalid annotated assignment target
scripts/release.py:408: [invalid-syntax] missing closing quote in string literal
scripts/release.py:410: [invalid-syntax] Simple statements must be separated by newlines or semicolons

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 11105 on HEAD, 9028 on base (🆕 +2077)

🆕 New issues (461):

Rule Count
unresolved-reference 320
unresolved-import 37
invalid-argument-type 33
unresolved-attribute 21
unsupported-operator 16
invalid-syntax 10
invalid-syntax-in-forward-annotation 7
invalid-assignment 6
invalid-method-override 4
not-subscriptable 4
call-non-callable 1
not-iterable 1
invalid-type-form 1
First entries
scripts/release.py:386: [unresolved-reference] unresolved-reference: Name `alexg0bot` used when not defined
tests/hermes_cli/test_kanban_promote.py:143: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["promote only applies"]` and `str | None`
scripts/release.py:400: [unresolved-reference] unresolved-reference: Name `nazirulhafiy` used when not defined
tests/tools/test_transcription_command_providers.py:115: [invalid-argument-type] invalid-argument-type: Argument to function `_resolve_command_stt_provider_config` is incorrect: Expected `str`, found `None`
scripts/release.py:375: [unresolved-reference] unresolved-reference: Name `devorun` used when not defined
scripts/release.py:191: [unresolved-reference] unresolved-reference: Name `rylena` used when not defined
scripts/release.py:449: [unresolved-reference] unresolved-reference: Name `AhmetArif0` used when not defined
scripts/release.py:413: [invalid-syntax] invalid-syntax: Unexpected indentation
scripts/release.py:152: [unresolved-reference] unresolved-reference: Name `NivOO5` used when not defined
scripts/release.py:274: [unresolved-reference] unresolved-reference: Name `TheEpTic` used when not defined
scripts/release.py:457: [unresolved-reference] unresolved-reference: Name `wesleysimplicio` used when not defined
scripts/release.py:281: [unresolved-reference] unresolved-reference: Name `qxxaa` used when not defined
tests/tools/test_memory_tool.py:117: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["deception_hide"]` and `str | None`
scripts/release.py:410: [unresolved-reference] unresolved-reference: Name `total` used when not defined
scripts/release.py:268: [unresolved-reference] unresolved-reference: Name `JamesX88` used when not defined
scripts/release.py:127: [invalid-syntax-in-forward-annotation] invalid-syntax-in-forward-annotation: Syntax error in forward annotation: Unexpected token at the end of an expression: Did you mean `typing.Literal["1000Delta"]`?
scripts/release.py:100: [unresolved-reference] unresolved-reference: Name `mrshu` used when not defined
scripts/release.py:410: [unresolved-reference] unresolved-reference: Name `omitted` used when not defined
gateway/run.py:12759: [unresolved-attribute] unresolved-attribute: Attribute `list_sessions_rich` is not defined on `None` in union `None | SessionDB`
scripts/release.py:163: [unresolved-reference] unresolved-reference: Name `ReqX` used when not defined
plugins/platforms/mattermost/adapter.py:392: [invalid-method-override] invalid-method-override: Invalid override of method `send_video`: Definition is incompatible with `BasePlatformAdapter.send_video`
scripts/release.py:527: [invalid-syntax] invalid-syntax: Expected a statement
scripts/release.py:349: [unresolved-reference] unresolved-reference: Name `heyitsaamir` used when not defined
plugins/platforms/ntfy/adapter.py:57: [unresolved-import] unresolved-import: Cannot resolve imported module `httpx`
scripts/release.py:108: [unresolved-reference] unresolved-reference: Name `thelumiereguy` used when not defined
... and 436 more

✅ Fixed issues (17):

Rule Count
invalid-argument-type 8
invalid-method-override 4
unresolved-import 2
unresolved-attribute 2
invalid-assignment 1
First entries
gateway/run.py:16399: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `dict[str, Any]`, found `list[Unknown] | dict[Unknown, Unknown] | None`
gateway/platforms/matrix.py:243: [unresolved-import] unresolved-import: Cannot resolve imported module `mautrix`
gateway/platforms/mattermost.py:365: [invalid-method-override] invalid-method-override: Invalid override of method `send_document`: Definition is incompatible with `BasePlatformAdapter.send_document`
tests/run_agent/test_plugin_context_engine_init.py:86: [unresolved-attribute] unresolved-attribute: Object of type `bound method _StubEngine.update_model(model: str, context_length: int, base_url: str = "", api_key: str = "", provider: str = "") -> None` has no attribute `call_args`
gateway/platforms/mattermost.py:392: [invalid-method-override] invalid-method-override: Invalid override of method `send_video`: Definition is incompatible with `BasePlatformAdapter.send_video`
gateway/platforms/api_server.py:2492: [invalid-argument-type] invalid-argument-type: Argument to function `create_job` is incorrect: Expected `bool`, found `Unknown | LiteralString`
gateway/platforms/mattermost.py:809: [unresolved-import] unresolved-import: Cannot resolve imported module `aiohttp`
gateway/platforms/mattermost.py:352: [invalid-method-override] invalid-method-override: Invalid override of method `send_image_file`: Definition is incompatible with `BasePlatformAdapter.send_image_file`
gateway/platforms/api_server.py:2492: [invalid-argument-type] invalid-argument-type: Argument to function `create_job` is incorrect: Expected `dict[str, Any] | None`, found `Unknown | LiteralString`
gateway/platforms/api_server.py:2492: [invalid-argument-type] invalid-argument-type: Argument to function `create_job` is incorrect: Expected `int | None`, found `Unknown | LiteralString`
hermes_cli/oneshot.py:324: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `dict[str, Any]`, found `(Any & ~AlwaysFalsy & ~Top[dict[Unknown, Unknown]]) | (list[Unknown] & ~AlwaysFalsy) | (list[Any & Top[dict[Unknown, Unknown]]] & ~AlwaysFalsy) | None`
gateway/platforms/api_server.py:908: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `dict[str, Any]`, found `list[Unknown] | dict[Unknown, Unknown] | None`
gateway/platforms/mattermost.py:867: [invalid-argument-type] invalid-argument-type: Argument is incorrect: Expected `list[str]`, found `(list[str] & ~AlwaysFalsy) | None`
gateway/platforms/api_server.py:2492: [invalid-argument-type] invalid-argument-type: Argument to function `create_job` is incorrect: Expected `list[str] | None`, found `Unknown | LiteralString`
gateway/platforms/mattermost.py:379: [invalid-method-override] invalid-method-override: Invalid override of method `send_voice`: Definition is incompatible with `BasePlatformAdapter.send_voice`
tests/run_agent/test_plugin_context_engine_init.py:85: [unresolved-attribute] unresolved-attribute: Object of type `bound method _StubEngine.update_model(model: str, context_length: int, base_url: str = "", api_key: str = "", provider: str = "") -> None` has no attribute `assert_called_once`
tests/run_agent/test_plugin_context_engine_init.py:62: [invalid-assignment] invalid-assignment: Object of type `MagicMock` is not assignable to attribute `update_model` of type `def update_model(self, model: str, context_length: int, base_url: str = "", api_key: str = "", provider: str = "") -> None`

Unchanged: 4788 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

teknium1 and others added 28 commits May 25, 2026 15:50
… page describes (NousResearch#30874)

Follow-up to NousResearch#30869. Adds Portal mentions on user-facing pages that
naturally call for an LLM + tool credentials but didn't previously
acknowledge Portal as a one-stop option.

- getting-started/installation.md: tip after the 'after install' block
  pointing at 'hermes setup --portal' for users who want everything wired
  at once instead of piecewise via 'hermes model' + 'hermes tools'.
- user-guide/configuring-models.md: small tip near the top — the page is
  literally about provider/model choice and previously had zero Portal
  mention.
- user-guide/features/voice-mode.md: Prerequisites need both an LLM and
  TTS — a Portal subscription is the single setup that covers both.
- user-guide/features/batch-processing.md: highlights Portal as a
  predictable-cost option for parallel agent runs that hit many APIs.
- user-guide/features/api-server.md: backend needs models + tools; one
  Portal sub gives a fully-equipped OpenAI-compatible endpoint.
- user-guide/windows-native.md: early-beta users on Windows benefit most
  from skipping per-tool Windows-key-juggling.
- integrations/providers.md: updates the existing Tool Gateway tip and
  the Nous Portal section to mention the new commands.
- user-guide/features/fallback-providers.md: Nous row in the provider
  table now lists 'hermes setup --portal' as the fresh-install path.

Tone discipline: one Portal mention per page, concrete CLI commands
(no marketing copy), always solving a problem the page itself sets up.
…tlement classifier (NousResearch#29344)

``_is_entitlement_failure`` over-matched on xAI 403s.  xAI returns the
same permission-denied ``code`` text for two distinct conditions:

  1. Unsubscribed account ("active Grok subscription. Manage at
     https://grok.com" in the ``error`` field).
  2. Stale OAuth access token ("OAuth2 access token could not be
     validated. [WKE=unauthenticated:bad-credentials]" in the ``error``
     field).

The classifier's "does not have permission + grok" substring heuristic
treated both identically, so the credential-pool refresh path was
short-circuited for case (2) — long-running TUI sessions stuck on a
stale OAuth token surfaced a non-retryable client error and the user
had to exit + reopen the TUI to recover (the startup-resolve path
bypasses the classifier entirely, which is why bridge adapters with
proactive refresh cadences didn't see this in practice).

This patch adopts the reporter's recommended fix (option 1, tightest):
honor xAI's explicit ``[WKE=unauthenticated:...]`` suffix and the
``OAuth2 access token could not be validated`` phrasing as
authoritative "this is auth, not entitlement" signals.  When either
appears anywhere in the body's text fields, the classifier returns
False eagerly — *before* the entitlement keyword checks run — so the
refresh-on-401 path takes over and the existing loop-protection still
guards against runaway refresh storms if the refresh itself fails.

Two small adjustments fall out of this:

* The haystack now also covers ``code`` and ``error`` keys directly,
  not just the ``message``/``reason`` shape ``_extract_api_error_context``
  produces.  Real runtime paths use the normalised shape, but the test
  suite and any future call sites that pass raw bodies get the same
  treatment.  Backwards compatible: missing keys default to empty
  strings, the haystack still skips when everything is blank.

* Both disambiguator checks fire BEFORE the entitlement keyword
  checks.  If a future xAI body somehow lands with both an entitlement
  message AND the WKE suffix, the WKE suffix wins (correct — auth is
  recoverable; entitlement is not, and a refreshed token will surface
  the entitlement message on the next request anyway).

Existing tests (``test_is_entitlement_failure_matches_real_xai_bodies``,
``test_is_entitlement_failure_false_for_unrelated_auth_errors``,
``test_recover_with_credential_pool_skips_refresh_on_entitlement_403``,
``test_recover_with_credential_pool_still_refreshes_genuine_auth_failure``)
continue to pass unchanged — the unsubscribed-account path, the
generic auth-error path, and the refresh-on-401 path are all left
intact.
…uator (NousResearch#29344)

Eleven new tests pinning the NousResearch#29344 fix.  Layout mirrors the existing
"Fix D" entitlement section so the bad-credentials disambiguator
sits alongside the entitlement-block tests it complements.

Classifier-level coverage:

* ``test_is_entitlement_failure_false_for_bad_credentials_wke_suffix``
  — verbatim shape from the reporter's wire capture
  (``{code: 'caller does not have permission', error: 'OAuth2 access
  token could not be validated. [WKE=unauthenticated:bad-credentials]'}``)
  ↦ classifier must return False so the refresh path runs.
* ``test_is_entitlement_failure_false_for_wke_suffix_in_normalized_shape``
  — same body after ``_extract_api_error_context`` has rewritten it
  to ``{reason, message}``.  The disambiguator must fire in BOTH
  shapes; without this guard the production call site at
  ``_recover_with_credential_pool`` (which goes through the
  normalised extractor) would still misclassify.
* ``test_is_entitlement_failure_false_for_any_wke_unauthenticated_variant``
  — parametrised forward-compat: ``bad-credentials``,
  ``expired-token``, ``revoked``, ``some-future-reason``.  xAI
  documents the prefix as stable, the suffix after the colon as a
  reason code that can grow; every variant under
  ``unauthenticated:`` must route to refresh.
* ``test_is_entitlement_failure_false_via_oauth2_validation_phrase_alone``
  — belt-and-braces guard: if a future API revision drops the WKE
  suffix but keeps "OAuth2 access token could not be validated", we
  still classify correctly.
* ``test_is_entitlement_failure_wke_signal_overrides_entitlement_keywords``
  — defensive: if a body ever carries BOTH the WKE suffix and
  entitlement language, the WKE signal wins.  Auth is recoverable;
  entitlement isn't, and a refreshed token will resurface the
  entitlement message on the next request.
* ``test_is_entitlement_failure_case_insensitive_wke_match`` —
  pins that the classifier lowercases the haystack so a future xAI
  build that uppercases the prefix doesn't reintroduce the bug.

Recovery-path coverage (end-to-end through
``_recover_with_credential_pool``):

* ``test_recover_with_credential_pool_refreshes_on_xai_bad_credentials_403``
  — the headline test the reporter requested: a bad-credentials 403
  with the exact wire body must call ``try_refresh_current()``
  exactly once and ``_swap_credential`` once.  Pre-fix this returned
  ``(False, _)`` because the entitlement classifier over-matched and
  short-circuited the refresh path.
* ``test_recover_with_credential_pool_still_blocks_real_entitlement``
  — companion regression guard for NousResearch#26847: a pure unsubscribed-
  account body (no WKE suffix, no OAuth2-validation phrase) must
  still surface as entitlement and skip refresh.  The new
  disambiguator must not weaken the original loop-protection it
  was added to preserve.

The scaffolding reuses ``_make_codex_agent``, ``_FakePool``, and the
existing ``MagicMock`` patterns from the surrounding tests so the
new section reads as a natural extension of "Fix D" rather than a
separate test file.
…ousResearch#29344)

_recover_with_credential_pool had a second classification site that blanket-
treated any 403 against xai-oauth as entitlement (defense-in-depth for
NousResearch#26847).  That override defeated the new _is_entitlement_failure
disambiguator from the parent commit — bad-credentials 403s still
short-circuited the refresh path.

Apply the same WKE-unauthenticated / OAuth2-validation-phrase guard at
the override site so xAI's authoritative 'this is auth, not entitlement'
signal wins there too.  The NousResearch#26847 catch-all still triggers for genuine
entitlement bodies that don't carry the disambiguator.

Closes the end-to-end gap exposed by
test_recover_with_credential_pool_refreshes_on_xai_bad_credentials_403.
…esearch#26045) (NousResearch#30877)

Reproduction (production, 2026-05-14): two concurrent sessions on the
same agent. Session A patches MEMORY.md directly via the patch tool,
appending ~8KB of structured content (Vendor Master, Standing Orders,
Pin Board) — none of it through the memory tool, so no § delimiters.
Session B starts later with stale in-memory state (1 entry, ~331
chars). Session B calls memory(action=replace) on its one known
entry. The tool's _read_file parses A's content as a single 8KB
'entry' (no § splits), then replace truncates that entry to B's new
333-byte content. ~8KB of structured content silently destroyed.

The atomic-rename write path is fine in isolation. The bug is the
implicit contract: the tool assumes MEMORY.md is exclusively a
§-delimited list of small entries it wrote, but the v0.13 install
runbook itself uses 'cat >> MEMORY.md' for onboarding, the patch tool
edits the file directly, and operators do too.

Fix: a drift guard in MemoryStore._detect_external_drift that fires
on either signal:

  1. Re-parse + re-serialize doesn't produce identical bytes
     (catches oddly-encoded delimiters / partial writes).
  2. Any single parsed entry exceeds the store's whole-file char
     limit. The tool budgets the ENTIRE store against that limit
     (2200 chars for memory, 1375 for user), so no tool-written
     entry can legitimately be larger. An entry bigger than the
     store limit means an external writer dropped free-form content
     into what the tool will treat as one entry.

When drift fires, _reload_target writes a .bak.<ts> snapshot of the
on-disk file, then add/replace/remove refuse to flush. The original
file stays untouched. The error dict surfaces the .bak path AND a
remediation string ('integrate missing entries via memory(add=...)
one at a time, then rewrite the file clean') so the model can act on
it without escalating to the operator.

Tests:
  - test_replace_refuses_on_drift, test_add_refuses_on_drift,
    test_remove_refuses_on_drift — all three mutators refuse
  - test_clean_file_does_not_trigger_drift — false-positive check
  - test_error_message_points_at_remediation — error string shape
  - test_drift_guard_also_protects_user_target — USER.md too
  - test_drift_backup_filename_is_unique_per_invocation — bak.<ts>
    naming pin

144 memory tests passing (was 137; +7).

Fixes NousResearch#26045
…ousResearch#24912) (NousResearch#30879)

User incident (Slack, 2026-05-13): user walked away mid-conversation,
agent requested approval to run `rm -rf .git`, the prompt timed out
after the gateway_timeout (default 300s), and the agent removed the
.git folder on its own. Corroborated by an independent report from a
Telegram user.

The underlying code path was correct — `check_all_command_guards`
returns `approved=False` with a BLOCKED message on both timeout and
explicit deny, and `terminal_tool` surfaces that as `status=blocked`
to the agent. The bug is at the model-interface layer: the message
"BLOCKED: Command timed out. Do NOT retry this command." reads to
some models as "try a different command achieving the same outcome."

This commit changes only the model-facing message + the structured
return shape:

  - Timeout message now explicitly names the three evasion paths the
    agent must avoid: retry, rephrase, AND achieve the same outcome
    via a different command. Ends with "Silence is not consent."
  - Explicit deny gets the same shape minus the silence-is-not-consent
    line (it WAS an explicit deny, not silence).
  - New structured fields on the return dict: `outcome` ("timeout"
    or "denied") and `user_consent` (always False on this branch)
    so plugins, hooks, and audit pipelines don't have to string-parse
    the message to distinguish the two cases.

The mechanism that should already have prevented the original incident
— timeout treated as deny, BLOCKED result, post hook fires with
`choice="timeout"` — is unchanged. This commit hardens only the
agent's reading of the result.

Tests:
  - test_timeout_returns_approved_false_with_no_consent — pins the
    return shape on the Slack-shaped notify_cb-registered path
  - test_timeout_message_is_emphatic_against_retry_and_rephrase —
    pins the exact phrases the message must contain
  - test_explicit_deny_carries_same_no_consent_shape — same contract
    on explicit /deny
  - test_timeout_emits_post_hook_with_timeout_outcome — pins the
    post_approval_response hook payload so audit plugins can act

329 approval tests passing (4 new + 325 existing).

Fixes NousResearch#24912
…/Messaging reference (NousResearch#30941)

A small, self-contained section under 'Skip the API-key collection —
Nous Portal' explaining what Portal gives you (300+ models + Tool
Gateway), the one-shot install command, and how to inspect routing.
No buzzwords, no comparison tables, no overselling.

Positioned right after 'Getting Started' so it lands where someone
scanning the README has just seen the install steps and is deciding
their next move. Skippable by anyone who already knows their provider.

The line 'You can still bring your own keys per-tool whenever you
want' is the deliberate honesty rail — Portal is an option, not a
funnel. Existing per-provider language elsewhere in the README is
unchanged.

Mirrored to README.zh-CN.md to keep the two READMEs in sync.
…indings

Path.resolve() before any I/O and confine backup writes to the resolved
parent directory. Adds explicit parent-equality assertions so static
analyzers see the containment guarantee, and walks WAL/SHM sidecars
through the same resolved-parent path so accidental .. segments are
collapsed before shutil.copy2.

Functionally equivalent to the original PR; preserves the corrupt bytes
to <db>.corrupt.<ts>.bak in the same directory, still raises
KanbanDbCorruptError from connect(). E2E with Stefan's exact hex header
+ malformed pages still passes. 163/163 kanban tests still pass.
…etion (NousResearch#30949)

First scratch workspace creation on an install now emits a one-shot
warning log + a 'tip_scratch_workspace' event on the task. Sentinel
file at ~/.hermes/kanban/.scratch_tip_shown silences subsequent
creations across the whole install.

Behavior unchanged — scratch is still ephemeral by design. This just
makes the design visible to new users (reported in user community:
'progress files vanished, no warning anywhere').

Docs (en + ko) updated to spell out 'Deleted when the task completes'
on the scratch bullet and 'Preserved on completion' on worktree/dir.
* fix(tui): commit composer input bursts immediately

Salvage the WSL/terminal multi-character input burst fix with focused regression coverage so delayed pseudo-paste buffers cannot reorder later edits.

* fix(tui): keep newline input bursts on paste path

Preserve paste handling for multi-character chunks with newlines while keeping repeated printable key bursts on the immediate composer path.

* refactor(tui): share composer frame batch interval

Use one frame-sized batching constant for parent updates, local renders, and input burst flushes.
* fix(tui): log parent gateway lifecycle exits

Add parent-side breadcrumbs for TUI gateway shutdown and transport exits so future backend EOF/SIGTERM reports identify the parent action that caused them.

* chore(tui): retrigger lifecycle logging checks

Retry transient GitHub checkout failures on the lifecycle logging PR.
…31055)

* fix(tui): ignore late thinking deltas after completion

Prevent stale reasoning events from repainting the TUI status after a turn has already completed and the UI is idle.

* test(tui): restore timers after thinking delta assertion

Keep fake timer cleanup in a finally block so assertion failures cannot leak timer mode into later tests.
ntfy now ships as a self-contained plugin under plugins/platforms/ntfy/
instead of editing 8 core files (gateway/config.py Platform enum,
gateway/run.py factory + auth maps, cron/scheduler.py, toolsets.py,
hermes_cli/status.py, agent/prompt_builder.py, gateway/channel_directory.py,
tools/send_message_tool.py).

All routing goes through gateway/platform_registry via register_platform():
- adapter_factory, check_fn, validate_config, is_connected
- env_enablement_fn seeds PlatformConfig.extra from NTFY_* env vars so
  gateway status reflects env-only setups without instantiating httpx
- standalone_sender_fn handles deliver=ntfy cron jobs when cron runs
  out-of-process from the gateway
- allowed_users_env / allow_all_env hook into _is_user_authorized
- cron_deliver_env_var=NTFY_HOME_CHANNEL for cron home routing
- platform_hint surfaces in the system prompt
- pii_safe=True (topic names are the only identifier; no PII to redact)

Tests moved to tests/gateway/test_ntfy_plugin.py using _plugin_adapter_loader
so the module lives under plugin_adapter_ntfy in sys.modules and cannot
collide with sibling plugin-adapter tests on the same xdist worker. The
core-file grep tests (Platform.NTFY in source, hermes-ntfy in toolsets,
etc.) are replaced with plugin-shape tests covering register() metadata,
env_enablement_fn output, and standalone_sender_fn behavior.

68 tests pass under scripts/run_tests.sh.
Robustness:
- Surface 401/404 stream failures via _set_fatal_error() so the gateway's
  runtime status reflects 'fatal: ntfy_unauthorized' / 'ntfy_topic_not_found'
  instead of staying 'connected' when the reconnect loop halts. Matches
  the pattern in whatsapp / telegram / sms adapters.
- Strip whitespace from auth tokens so pasted tokens with trailing
  newlines don't produce malformed Authorization headers.

Simplicity:
- Extract _build_auth_header() and _truncate_body() to module-level
  helpers, used by both NtfyAdapter and _standalone_send. Removes the
  duplicated auth/truncation logic between the two paths.

Docs:
- website/docs/user-guide/messaging/ntfy.md — full setup guide,
  identity-model warning, self-hosting, cron usage, troubleshooting.
- website/docs/reference/environment-variables.md — all 9 NTFY_* vars.
- website/docs/user-guide/messaging/index.md — platform comparison row.
- website/sidebars.ts — sidebar entry between simplex and open-webui.

Tests: 78/78 (+ 10 new robustness tests covering token hygiene, fatal
error propagation for 401/404, and the _truncate_body helper).
Move shutil.rmtree into a finally block so the temp directory is always
cleaned up, even when an exception occurs during download, extraction,
or file copying.
…atus bar

Bug 1: /voice off in TUI mode did not clear HERMES_VOICE_TTS,
leaving TTS stuck ON with no way to disable it (the voice.toggle
tts handler requires voice mode to be ON).

Bug 2: TUI status bar only showed 'voice on/off' without any
indication of whether TTS speech output is active, because the
frontend never tracked voiceTts state.

- tui_gateway/server.py: clear HERMES_VOICE_TTS when voice is turned off
- ui-tui/src/app/useMainApp.ts: add voiceTts state, thread setVoiceTts
  through voice contexts, display [tts] in status bar
- ui-tui/src/app/slash/commands/session.ts: sync tts from voice.toggle response
- ui-tui/src/app/interfaces.ts: add setVoiceTts to all voice context interfaces
…) (NousResearch#26975)

* docs(simplex): remove broken Docker install command (NousResearch#26974)

The "Or Docker" snippet pointed at `simplexchat/simplex-chat`, which is
not a published Docker Hub image. Users following the docs hit:

  docker: Error response from daemon: pull access denied for
  simplexchat/simplex-chat, repository does not exist or may require
  'docker login'.

The SimpleX Chat project only publishes Docker images for its server
components (smp-server, xftp-server) — the chat CLI is distributed as a
binary release. Drop the broken `docker run` line and keep the verified
binary-download path, with a note pointing users to the upstream
Dockerfile if they want to build a container themselves.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* docs(simplex): drop misleading "Dockerfile" link text

Copilot review flagged that the link text claimed "Dockerfile in the
upstream repo" but the URL pointed at the repository root, not a
specific Dockerfile path. Reword to "build from source from the
simplex-chat repository" so the link text and target match.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

---------

Co-authored-by: briandevans <[email protected]>
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
    Null bytes in API key values (introduced by copy-paste) crash
    os.environ[k] = v with ValueError: embedded null byte, preventing
    hermes from starting at all.
- Add api_mode to 4 update_model() call sites:
  - conversation_loop.py: long_context failover and probe stepping
  - agent_runtime_helpers.py: rollback restore (also saves compressor_api_mode)
  - chat_completion_helpers.py: fallback activation
- Fix 31 root-logger calls across 5 files (logging.warning/error/info
  -> logger.warning/error/info) to respect module-level log filtering
Notify scroll subscribers when ScrollBox viewport bounds change and key virtual-history updates on viewport height so resize/keyboard changes remount the tail rows instead of leaving stale spacers visible.
teknium1 and others added 16 commits May 25, 2026 15:50
Mirror of the TTS command-provider registry (PR NousResearch#17843) for STT. Lets any
shell-driven ASR engine — Doubao ASR, NVIDIA Parakeet, whisper.cpp builds,
SenseVoice, curl pipelines — become an STT backend with zero Python.
Complements the legacy HERMES_LOCAL_STT_COMMAND escape hatch (preserved
untouched via the built-in local_command path) and the
register_transcription_provider() Python plugin hook also shipped in this
PR.

Resolution order (mirrors TTS exactly):

  1. Built-in (local, local_command, groq, openai, mistral, xai)
     → native handler. Always wins.
  2. stt.providers.<name>: type: command  → command-provider runner.
  3. Plugin-registered TranscriptionProvider → plugin dispatch.
  4. No match → 'No STT provider available'.

Files
-----
- tools/transcription_tools.py: BUILTIN_STT_PROVIDERS frozenset retained;
  added _resolve_command_stt_provider_config, _transcribe_command_stt,
  and local helpers for template rendering, shell-quote context, and
  process-tree termination. Helpers are documented as mirrors of their
  tts_tool.py counterparts (kept local to avoid cross-tool private
  import). Wire-in is one insertion point in transcribe_audio() after
  the xai elif and before the plugin dispatcher. Plugin dispatcher
  additionally defensively short-circuits when a same-name command
  config exists (command-wins-over-plugin invariant).

- tests/tools/test_transcription_command_providers.py: 50 new tests
  covering resolution (builtin precedence, type/command gating,
  case-insensitive lookup, legacy stt.<name> back-compat), helpers
  (timeout fallback, format validation, iter, has-any), template
  rendering (shell-quote contexts, doubled-brace preservation),
  end-to-end via _transcribe_command_stt (output_path read, stdout
  fallback, timeout, nonzero exit envelope, model override,
  language precedence), and dispatcher integration via the real
  transcribe_audio() including command-wins-over-plugin and
  builtin-shadow-rejection.

- tests/plugins/transcription/check_parity_vs_main.py: extended from
  10 to 13 scenarios. New cases: command-provider-installed,
  command-vs-plugin-same-name (verifies command wins precedence),
  explicit-openai-with-command-shadow (verifies built-in wins).
  Adds command_provider dispatch_kind detection via transcript prefix
  (CMD: vs PLUGIN:) so command-provider scenarios can be distinguished
  from plugin scenarios even when sharing a provider name.

- website/docs/user-guide/features/tts.md: new 'STT custom command
  providers' section symmetric to the TTS section — example config,
  placeholder grammar table (input_path / output_path / output_dir /
  format / language / model), transcript-read-back semantics (file
  first, then stdout fallback), optional keys table, behavior notes,
  security note. Updated 'Python plugin providers (STT)' to include
  the new 'When to pick which (STT)' decision table and updated
  resolution-order section (now 4 layers instead of 3).

Verification
------------
189/189 STT targeted tests + 50/50 new command-provider tests pass.
Combined sweep: tests/tools/ 5576/5576, tests/agent/ + tests/hermes_cli/
8623/8623 — zero regressions across 14,199 tests.

Parity harness: 13 scenarios, 9 OK + 4 expected diffs
(no_provider_error → plugin, plugin_unavailable, command_provider × 2).

E2E live-verified in an isolated HERMES_HOME with a real .wav file:

  command:                    → dispatched to stt.providers.my-fake-cli
  plugin:                     → dispatched to registered TranscriptionProvider
  command-wins-over-plugin:   → command provider beats same-name plugin
  builtin-wins-over-command:  → built-in OpenAI handler fires;
                                stt.providers.openai: type: command
                                does NOT hijack it.
Session IDs are profile-constrained, so the resume hint needs to
include the active profile for multi-profile users. Without this,
copying the hint from a non-default profile fails to resume the
correct session.

Before:  hermes --resume 20260414_063228_c1240e
After:   hermes --resume 20260414_063228_c1240e -p dev

Also includes -p on the resume-by-title hint. Skipped for
'default' and 'custom' profiles (no -p needed).

Surgical reapply of PR NousResearch#9652. Original branch was stale against
current main (~6 months); reapplied onto current cli.py by hand
with original authorship preserved.
5 tests: default/custom profiles emit no -p; named profile emits
-p <name> on both --resume and -c hints; lookup failure falls back
gracefully.
…rch#29335)

Three compounding root causes:

A) run_conversation() result dict missing session_id — gateway's
   dead-code guard at gateway/run.py:8700 never triggers
B) preflight compression bypasses should_compress() anti-thrashing —
   re-triggers every turn when tool schemas dominate token budget
C) gateway updates session_entry.session_id in memory but doesn't
   persist via session_store._save()

Fixes: NousResearch#29335
… persistence

Follow-up to @someaka's fix.

Polish:
- Drop the redundant `_preflight_tokens >= threshold_tokens` clause.
  `should_compress(tokens)` already short-circuits when tokens < threshold,
  so the explicit comparison was dead code on the True branch.

Tests:
- Preflight: pin that should_compress() is called (anti-thrash has a vote).
  Mocks should_compress to return False even with tokens past the raw
  threshold and asserts no compression runs — exact bug shape from NousResearch#29335.
- Gateway: AST scan of gateway/run.py asserts every
  `session_entry.session_id = ...` assignment is followed by a
  `session_store._save()` call within the same block. Three sites mutate
  the session_id after compression; all three must persist or the next
  turn loads the pre-compression transcript and re-loops. Empirically
  verified the test catches the bug (drops the new _save() line → red).

AUTHOR_MAP:
- Map [email protected] -> someaka so the salvaged commit resolves to
  @someaka in release notes.
…esearch#11793)

When 'hermes chat --quiet --resume <id> -q "..."' is used, three status
messages were written to stdout via ChatConsole / _cprint:

  - '↻ Resumed session <id> (N user messages, M total messages)'
  - 'Session <id> found but has no messages. Starting fresh.'
  - 'Session not found: <id>' / usage hint

This polluted the machine-readable stdout that automation wrappers capture
with $(...), making it impossible to cleanly separate the agent's answer
from the resume banner.

Fix: detect quiet mode via tool_progress_mode == 'off' and route the three
resume status messages to stderr (as plain text, matching the existing
stderr convention for session_id). Interactive mode is unchanged — it
still uses the Rich-rendered path through ChatConsole.

Surgical reapply of PR NousResearch#11868. Original branch was stale against current
main; reapplied onto current cli.py by hand with original authorship
preserved via --author.
4 tests: session-not-found in quiet mode -> stderr; in full mode -> stdout
(unchanged); resumed banner in quiet mode -> stderr; has-no-messages in
quiet mode -> stderr.
…sResearch#31942)

Translates the full English docs corpus (335 files) into Simplified
Chinese under website/i18n/zh-Hans/. Combined with PR NousResearch#31895 (cross-
locale link fix), the 简体中文 locale toggle now serves a complete
Chinese site with working cross-page navigation.

Pipeline:
- Claude Sonnet 4.6 via OpenRouter, 8-way concurrent
- Preserves frontmatter keys, code blocks, MDX/JSX, link URLs, brand
  names, and technical jargon (prompt/token/hook/MCP/ACP/etc.)
- Translates only frontmatter title/description and prose
- Two largest files (configuration.md 93KB, research-paper-writing.md
  107KB) retried with 64K max_tokens after initial fence-drift
- 3 manual post-fixes for MDX edge cases the model didn't escape:
  &lt; in optional-skills-catalog table, double-quotes in an alt= tag,
  and a bare URL adjacent to a full-width period

Cost: ~$30 total (Sonnet 4.6 input $3/M + output $15/M).

Verified `npm run build` succeeds for both en and zh-Hans locales,
no double-prefixed /docs/zh-Hans/docs/ URLs in rendered output,
all in-page navigation resolves correctly.

Translations are machine-generated and may need human review on
specific pages — but they're an enormous improvement over the
previous state (3 zh-Hans pages out of 335).
…ower stale defaults

Codex / Responses-API requests had three latent timeout bugs that combined
into the long silent hangs reported on NousResearch#21444:

1. The non-stream stale-call detector estimated context tokens from
   ``api_kwargs["messages"]`` only. Codex / Responses-API payloads carry
   their conversational load in ``input`` (with ``instructions`` and
   ``tools``), so every Codex turn logged ``context=~0 tokens`` and the
   detector never applied its >50k / >100k tier bumps.

2. ``providers.<id>.request_timeout_seconds`` was silently dropped on the
   main Codex path. The chat_completions path and the auxiliary Codex
   adapter both forwarded it; the main path skipped it through three
   places (``build_api_kwargs``, ``ResponsesApiTransport.build_kwargs``,
   ``_preflight_codex_api_kwargs``).

3. The streaming stale detector had the same payload-shape bug for
   ``codex_responses`` requests, which route through the non-streaming
   detector (it's the path that emits the user-facing
   "No response from provider for 300s (non-streaming, ...)" warning that
   reporters keep pasting).

This commit:

- Adds ``estimate_request_context_tokens`` in ``chat_completion_helpers``,
  used by both the non-stream and stream detectors. Handles ``messages``
  (Chat Completions), ``input + instructions + tools`` (Responses API),
  bare lists, and an unknown-dict fallback.
- Forwards ``timeout`` through ``ResponsesApiTransport.build_kwargs``
  and ``_preflight_codex_api_kwargs`` (with guards against
  zero/negative/inf/bool values), and wires
  ``_resolved_api_call_timeout()`` into the Codex branch of
  ``build_api_kwargs``.
- Lowers the implicit non-stream stale defaults so fallback providers
  kick in faster when upstream stalls:
    * base   300s -> 90s
    * >50k   450s -> 150s
    * >100k  600s -> 240s
  These only apply when the user has *not* set
  ``providers.<id>.stale_timeout_seconds`` or
  ``HERMES_API_CALL_STALE_TIMEOUT``. Explicit config still wins.
- Adds regression tests for the estimator shapes, the new defaults, the
  context-tier scaling, transport timeout pass-through, and preflight
  timeout pass-through / rejection of invalid values.

Closes NousResearch#21444
Supersedes NousResearch#21652 NousResearch#24126 NousResearch#31855

Co-authored-by: Hoang V. Pham <[email protected]>
…h explicit flag (NousResearch#29276)

show_snapshot.py unpickled a user-supplied path unconditionally. pickle.loads
is equivalent to arbitrary code execution, so a snapshot from an untrusted
source = RCE. Require an explicit --i-trust-this-file acknowledgement before
calling pickle.loads, and emit a stderr warning when proceeding.

Co-authored-by: Jiahui-Gu <[email protected]>
… skills guard (NousResearch#9151)

Expand _MEMORY_THREAT_PATTERNS from 13 to 24 regex patterns and align
_INVISIBLE_CHARS with skills_guard.py (10 → 17 characters).

Key changes:
- Add multi-word bypass prevention (?:\w+\s+)* to injection patterns
- Add missing injection patterns: role_pretend, leak_system_prompt,
  remove_filters, fake_update, translate_execute, html_comment_injection,
  hidden_div
- Add exfiltration patterns: send_to_url, context_exfil
- Add persistence patterns: agent_config_mod, hermes_config_mod
  (both require modification-verb prefix to avoid false positives on
  mere mentions of config filenames)
- Add hardcoded secret detection pattern
- Add role_hijack precision fix: require article after "now" to avoid
  blocking "you are now ready/connected/set up" etc.
- Expand invisible unicode set with directional isolates (U+2066-2069)
  and invisible math operators (U+2062-2064)

Test coverage expanded from ~8 to ~30 scan tests including dedicated
false-positive regression tests for all precision-sensitive patterns.

Known limitations (deferred to follow-up PRs):
- prompt_builder.py and cronjob_tools.py still use older pattern sets
- No semantic/LLM-based scanning (regex-only approach)
- No cross-entry or cross-store analysis
V4A patch '*** Update File:', '*** Add File:', '*** Delete File:' headers
come from patch CONTENT, not the explicit `path=` argument. That makes
them attacker-influenceable through skill content, web extract output,
prompt injection, and other surfaces the agent processes. Headers like
'*** Update File: ../../../etc/shadow' would resolve relative to the
agent's cwd; in deployment configurations where that cwd is deep enough
to land outside Hermes' protected paths, the write could land somewhere
the agent operator did not intend.

Reject any V4A header containing a '..' path component before applying
the patch. The explicit `path=` argument on patch_tool is UNCHANGED —
the agent legitimately uses '..' there (e.g. `patch path='../other_module/x.py'`
from a worktree dir is normal cross-module editing).

Regression tests: V4A Update header with traversal rejected, V4A Add
header with traversal rejected, patch_v4a never invoked when rejection
fires.

Salvaged from PR NousResearch#29395 by @waefrebeorn. The original PR added
has_traversal_component as a blanket reject on read_file_tool,
write_file_tool, patch_tool's explicit path, and search_tool — that
would break legitimate agent operation where '..' is normal. Also
dropped the over-eager skills_guard pattern additions
(pickle.loads/marshal.loads/ctypes.CDLL/importlib at high/critical
severity would false-positive on legit data-science and FFI skills).

Co-authored-by: teknium1 <[email protected]>
Add the new 'human_review' status (introduced alongside the
kanban_review/approve/reject machinery) to the dashboard's column
list so cards in that state are actually visible on the board.

Backend: append "human_review" between "review" and "done" in
plugins/kanban/dashboard/plugin_api.py::BOARD_COLUMNS. The endpoint
already buckets tasks by status into BOARD_COLUMNS, so this is the
only Python change required.

Frontend (dist/index.js — there is no src/ tree, the bundled JS is
the canonical UI):
- COLUMN_ORDER: extended to the full status list (scheduled and
  review were also missing from the JS fallback). The render loop
  iterates board.columns from the API, so missing keys here would
  just degrade labels; aligning it with the backend keeps the
  frontend's hard-coded order honest.
- FALLBACK_COLUMN_LABEL / FALLBACK_COLUMN_HELP: added entries for
  scheduled, review, and human_review so the dashboard renders sane
  text when the i18n catalog has no key.
- COLUMN_DOT: added scheduled/review/human_review classes.

Style (dist/style.css): added .hermes-kanban-dot-scheduled,
.hermes-kanban-dot-review, .hermes-kanban-dot-human-review with
distinct colors so the new columns are visually distinguishable.

The Python human_review status itself ships on the
feat/kanban-human-review branch and PR NousResearch#30967; this change is a
no-op until that lands but is safe to merge independently — any
tasks ever set to status='human_review' will now have a home.
sahilm-ti pushed a commit that referenced this pull request May 25, 2026
…y prefixed

Companion to the NousResearchGH-25255 incoming-strip fix from @hayka-pacha. Without
this, build_anthropic_kwargs unconditionally added 'mcp_' to every tool
name in step 3, so a native MCP server tool registered as
'mcp_composio_X' was sent as 'mcp_mcp_composio_X' on the wire. The
incoming strip only removes ONE prefix, which still worked on first
call, but on subsequent calls the model pattern-matched the
single-prefixed form from message history and produced names that
stripped to 'composio_X' — registry miss, dispatch fail.

The history-rewrite block (#4) already has this guard. Apply the same
guard to the schema-rewrite block (#3) so round-trip is symmetric.

Added 4 outgoing-side tests. Existing 7 incoming-side tests still pass.

Author map: hayka-pacha added for PR NousResearch#25270 salvage attribution.

Refs NousResearchGH-25255.
@sahilm-ti
sahilm-ti force-pushed the fix/dashboard-human-review-column branch from 9c7df7d to 363ed44 Compare May 25, 2026 10:20
@sahilm-ti

Copy link
Copy Markdown
Owner Author

Closing: branch was based on a foreign trunk and pulled 224 unrelated commits into the diff. Reopening a clean branch with just the dashboard column change.

@sahilm-ti sahilm-ti closed this May 25, 2026
sahilm-ti pushed a commit that referenced this pull request May 29, 2026
… OAuth gates

Two parallel public-path allowlists drifted: _PUBLIC_API_PATHS in
hermes_cli/web_server.py (legacy _SESSION_TOKEN middleware) and
_GATE_PUBLIC_PREFIXES in hermes_cli/dashboard_auth/middleware.py
(OAuth gate). The legacy list included /api/status (documented as a
non-sensitive read-only liveness target); the OAuth gate's list did not.

Effect: every wildcard-subdomain agent surfaced as STARTING/down to the
portal even though the dashboard was serving correctly. Nous account
service (src/server/agents/fly-provider.ts
getInstanceRuntimeStatus) fetches ``/api/status`` without a cookie
as its sole liveness probe; the OAuth gate's 401 looked identical to
'agent dead' on the portal side.

Fix: lift the allowlist into hermes_cli/dashboard_auth/public_paths.py
and have both middlewares import it. _path_is_public now consults
the shared frozenset first, then falls back to the gate's
auth-bootstrap/static prefix list. Future additions to the public list
hit both gates automatically.

Endpoint inventory (verified safe to remain public):

* /api/status            — version, gateway state, active session count,
                           auth-gate shape. Portal liveness probe target.
* /api/config/defaults   — config-defaults feed for the SPA's Config page
* /api/config/schema     — config schema for the SPA's Config page
* /api/model/info        — model catalogue metadata (context windows)
* /api/dashboard/themes  — theme manifests for the skin engine
* /api/dashboard/plugins — plugin manifests for the dashboard

No user data, no session content, no secrets. Same shape an external
monitoring agent would hit on /healthz.

Tests:

* New: test_gated_status_is_public (regression guard with the NAS
  fly-provider.ts liveness-probe rationale spelled out in the docstring)
* New: test_other_public_api_paths_are_public_under_gate (parametrised
  over the rest of PUBLIC_API_PATHS — proves 401 / 302-to-login is
  never the response)
* New: docker integration check #3 in
  test_dashboard_oauth_gate_engaged_by_default — /api/status
  remains 200 under the gate AND reports auth_required=True so the
  portal can distinguish modes
* Updated: test_full_login_round_trip_unlocks_gated_api now probes
  /api/sessions instead of /api/status (status is public, so it
  can no longer distinguish 'logged in' from 'gate accidentally
  disabled')
* Updated: TestApi401Envelope (the no-cookie / invalid-cookie /
  dead-cookie tests) probes /api/sessions for the same reason
* Updated: docker integration check #2 in
  test_dashboard_oauth_gate_engaged_by_default probes
  /api/sessions to prove the gate is intercepting
* Removed: dead _login() helper in
  test_dashboard_auth_status_endpoint.py (no longer needed since
  /api/status is reachable cold)

Companion to docs/handover/hermes-agent-dashboard-s6-insecure-fix.md
(the --insecure flag fix that shipped earlier).
sahilm-ti pushed a commit that referenced this pull request May 29, 2026
…hain probe (NousResearch#34340)

* fix(codex): surface error code in Responses 'failed' status errors

When a Codex Responses turn ends with status=failed, the response carries
the failure details under `response.error` as
`{code, message, param, ...}`. The previous extractor pulled only
`message`, so users seeing a rate-limit failure got a bare "Slow down"
string indistinguishable from a generic stream truncation; an
internal_error with empty message degraded to a dict dump
("{'code': 'internal_error', 'message': ''}").

Extract a `_format_responses_error()` helper that:
- prefixes `code` when both code and message are present
  (e.g. 'rate_limit_exceeded: Slow down')
- falls back to the bare `code` when message is empty
- accepts both dict and attribute-style payloads (SDK and JSON-RPC paths)
- preserves the prior status-only fallback when no error payload exists

Apply the same helper at the sibling site in
`codex_app_server_session.run_turn()` so codex-CLI subprocess turn
failures get the same treatment.

Tests:
- 8 new unit tests for `_format_responses_error` covering both shapes,
  empty/missing fields, non-string fields, and the status-only fallback.
- 2 regression tests on `_normalize_codex_response` for failed status
  with and without a code, asserting the exact RuntimeError message.
- All 3603 tests in tests/agent/ pass.

Adapted from anomalyco/opencode#28757.

* feat(prompt): universal task-completion guidance + local Python toolchain probe

Two cross-model failure modes get a single-line answer in the cached
system prompt. Both gated by config (default on), both add zero overhead
when not needed, both verified via real AIAgent prompt builds.

## What changed

`TASK_COMPLETION_GUIDANCE` — short prompt block applied to ALL models.
Targets two failure modes observed on a real Sarasota real-estate build
task: (1) Opus stopped after writing an 85-byte stub and gave a prose
response with finish_reason=stop on call #3 of 90; (2) DeepSeek pushed
through a PEP-668 wall, then returned fabricated listings instead of
admitting the blocker. Both behaviors are model-family-agnostic, so the
guidance lives outside the existing tool_use_enforcement gate (~192
tokens, paid once per session via prefix cache).

`tools/env_probe.py` — local Python toolchain probe. Detects
python3/pip/uv/PEP-668 state and emits ONE short line in the system
prompt when something is non-default. Emits NOTHING when the env is
clean (zero token cost for normal users). Skipped entirely for remote
terminal backends (docker/modal/ssh) — they have their own probe.

Example output on a broken environment (the actual case):

    Python toolchain: python3=3.11.15 (no pip module),
    python=missing (use python3), pip→python3.12 (mismatch),
    PEP 668=yes (use venv or uv).

## Config

Both flags live under `agent.` in config.yaml, default True:

    agent:
      task_completion_guidance: true   # universal "finish the job" block
      environment_probe: true          # local Python toolchain hints

Neither addition required a `_config_version` bump — deep-merge fills
defaults in for existing user configs.

## Validation

| Test surface | Result |
|---|---|
| tests/tools/test_env_probe.py | 10/10 pass (probe unit) |
| tests/run_agent/test_run_agent.py — new classes | 8/8 pass (integration) |
| TestToolUseEnforcementConfig | 17/17 pass (no regression) |
| TestBuildSystemPrompt | 9/9 pass (no regression) |
| TestInvalidateSystemPrompt | 2/2 pass (no regression) |
| tests/agent/test_prompt_builder.py | 124/124 pass (no regression) |
| tests/hermes_cli/ | 5662/5662 pass (config defaults) |
| E2E AIAgent build (broken env) | Both blocks present, 2,178 chars |
| E2E AIAgent build (clean env) | 771-char net overhead, env probe silent |
sahilm-ti pushed a commit that referenced this pull request Jun 1, 2026
A bare `/resume` printed the recent-sessions list but armed no selection
state, so typing just `3` on the next line was sent to the agent as chat
instead of resuming session #3. `/resume 3` worked, but the natural
list-then-pick flow did not.

Arm a one-shot pending-resume prompt when bare `/resume` shows the list,
and consume the next bare numeric input as the selection (out-of-range is
reported, non-numeric/other commands disarm it). Resolves against the same
_list_recent_sessions(limit=10) list used everywhere else.

Closes NousResearch#34584.
sahilm-ti pushed a commit that referenced this pull request Jun 1, 2026
…NousResearch#34192) (NousResearch#34382)

NousResearch#34192 reports Hostinger's 'Hermes WebUI' catalog crashes on startup
with:

  /usr/bin/tini: No such file or directory

The image moved from tini to s6-overlay as PID 1 (/init) earlier in
2026. Orchestration templates that still pin /usr/bin/tini as the
entrypoint \u2014 like the Hostinger Hermes WebUI catalog \u2014 have no
binary to exec and the container crashes immediately.

Hermes has no control over the Hostinger catalog template, but we can
make the image backward-compatible by symlinking /usr/bin/tini -> /init
during the s6-overlay install step. External wrappers that exec
/usr/bin/tini will land on the same s6-overlay reaper they would have
landed on if they'd used the canonical /init entrypoint.

The image's own ENTRYPOINT continues to be /init verbatim \u2014 the shim
is purely for legacy external wrappers, not for the image's own
runtime path. Once affected catalogs are updated, the symlink can be
removed.

Other issues NousResearch#34192 raises that are NOT addressed by this PR:

  * Problem #2 (UID 1024 vs 10000 mismatch): already fixed by NousResearch#33148
    (S6_KEEP_ENV=1) and NousResearch#32412 (with-contenv shebangs). The Hostinger
    template likely needs to update its env-var propagation.

  * Problem #3 (incompatible session formats): RFC for pluggable
    SessionDB is tracked in NousResearch#23717.

  * Problem #4 (Telegram polling conflict): an operations problem on
    Hostinger's side, not in this codebase.

This PR is scoped to the one issue that can be fixed inside
Dockerfile: the missing /usr/bin/tini binary.

Tests (3 in test_dockerfile_tini_compat_shim.py):

  - test_tini_compat_symlink_present
    Guard: the symlink line must exist in Dockerfile.
  - test_tini_compat_comment_explains_why
    The NousResearch#34192 anchor comment must be present so future readers know
    why the shim is there (avoid accidental removal).
  - test_entrypoint_still_init_not_tini
    Sanity check: ENTRYPOINT remains /init (s6-overlay). The shim is
    only for external wrappers.

Refs: NousResearch#34192
Partial fix: addresses the immediate tini-binary crash. Catalog-side
fixes still needed by Hostinger for the UID and session-format
problems documented in the issue.

Co-authored-by: Cursor <[email protected]>
sahilm-ti pushed a commit that referenced this pull request Jun 9, 2026
…bes + test-leak fix (NousResearch#40909)

* fix(gateway,windows): reliability — supervisor task, JOB breakaway, status --deep

Three coordinated fixes for the Windows gateway reliability story:

1. CREATE_BREAKAWAY_FROM_JOB on every detached spawn

   The 'hermes update' triggered from the Electron Desktop GUI ran inside
   Electron's job object. Without breakaway, the post-update gateway
   watcher spawned by update — already DETACHED_PROCESS — was still
   reaped when Electron's job tore down, so the gateway never came back
   after a GUI-initiated update. Adds CREATE_BREAKAWAY_FROM_JOB (0x01000000)
   to:
     - hermes_cli/_subprocess_compat.py::windows_detach_flags() — used by
       every helper that calls windows_detach_popen_kwargs(), including
       launch_detached_profile_gateway_restart()
     - The watcher subprocess's own respawn snippet in
       hermes_cli/gateway.py (inlined flags so the watcher's child
       respawn also breaks away)

   _spawn_detached() in gateway_windows.py already had the flag; this
   change brings the rest of the codebase to parity.

2. Per-minute supervisor Scheduled Task — Windows equivalent of
   systemd Restart=always

   Introduces hermes_cli/gateway_supervisor.py and registers it as a
   second Scheduled Task ('Hermes_Gateway_Supervisor', SC MINUTE /MO 1,
   LIMITED rights) alongside the existing ONLOGON task. Every minute,
   the supervisor uses the same gateway.status.get_running_pid() probe
   as 'hermes gateway status' and, if no gateway is alive, calls
   gateway_windows._spawn_detached() (which now includes BREAKAWAY) to
   bring one back.

   Covers every crash mode, not just 'machine rebooted': taskkill,
   OOM, GUI update SIGTERM, parent job teardown. Cheap — one pythonw
   startup per minute when down, one PID-existence check per minute
   when up.

   Wired into both the schtasks-success and Startup-folder-fallback
   install paths via _install_supervisor_best_effort(), and removed in
   uninstall(). Best-effort: a failing supervisor install logs a
   warning but doesn't roll back the primary install.

3. 'hermes gateway status --deep' shows per-probe PASS/FAIL

   Replaces the existing terse '--deep' output (which only printed
   paths) with an actual diagnostic table:
     [1] PID file present
     [2] Lock file held by a live process
     [3] get_running_pid() result
     [4] _pid_exists(pid) — OS-level liveness
     [5] gateway_state.json (state + age)
     [6] Last lifecycle event from gateway-exit-diag.log

   When the high-level summary disagrees with reality, the user can
   see exactly which signal is lying.

Test-leak fix
-------------

tests/hermes_cli/test_gateway_wsl.py::TestGatewayCommandWSLMessages
monkey-patched is_linux/is_wsl/supports_systemd_services to simulate
WSL but did NOT stub is_windows(). On a Windows host, the dispatcher
in _gateway_command_inner takes the is_windows() branch BEFORE the
WSL guidance branch, so the test invoked gateway_windows.install()
for real. install() writes to %APPDATA%\...\Startup\Hermes_Gateway.cmd
— the REAL user Startup folder, never sandboxed by tmp_path — pointing
at the test's pytest-of-<user>/pytest-<N>/.../gateway-service/ wrapper.
When pytest tore down the tmp_path, every subsequent Windows login
flashed a cmd.exe window that failed to find the missing target.

Stubs is_windows=False on all four affected tests:
  test_install_wsl_no_systemd
  test_start_wsl_no_systemd
  test_status_wsl_running_manual
  test_status_wsl_not_running

Defense-in-depth: _build_startup_launcher() now prefixes the launcher
with 'if not exist <target> exit /b 0', so any future stale Startup
entry silently no-ops instead of flashing a console window.

Status enhancements
-------------------

- status() now reports supervisor task presence alongside the existing
  schtasks/Startup info, and nudges the user to reinstall if the
  supervisor isn't registered.
- Deep mode dumps both the supervisor task name + script path.

* fix(gateway,windows): drop the per-minute supervisor task — keep breakaway + deep probes

Earlier in this branch we added a per-minute schtasks-based supervisor to
respawn the gateway after crashes / GUI-update SIGTERMs. The implementation
flashed a brief console window on every firing, which stole window focus.
We tried several variants:

  - cmd.exe wrapper invoking pythonw  -> flashes (cmd.exe is console-subsystem)
  - schtasks /TR pointing at pythonw  -> flashes (uv venv launcher pythonw is
    actually subsystem=Console, not GUI; it respawns the real pythonw)
  - schtasks /TR pointing at base uv  -> still flashes (Task Scheduler-side
    conhost preallocation; documented Windows quirk)
  - XML registration with <Hidden>true>  -> still flashes (<Hidden> only hides
    the task in the Task Scheduler UI, not the spawned window)

Researched what leading projects do:

  - Ollama: GUI-subsystem tray exe + Startup-folder shortcut. No supervisor.
  - Tailscale: real Windows Service via SCM. Session 0, no console possible.
  - Syncthing: --no-console flag inside the binary + Startup folder.
  - openclaw: VBS Run(..., 0, False) wrapper. Suppresses the *window* but
    Super User Q971162 confirms focus-steal still occurs in some cases.

None of these use a per-minute polling scheduled task. The 'auto-restart on
crash' responsibility belongs INSIDE the daemon (Tailscale's in-process
recovery / Ollama's monitor+worker pair) OR is delegated to the Windows
Service Control Manager — not Task Scheduler.

So this commit drops the supervisor entirely. The CREATE_BREAKAWAY_FROM_JOB
fix in _subprocess_compat.py (from commit c1e5fa4) survives — that is the
*real* fix for problem #2 (GUI-update kills gateway): the post-update
watcher in launch_detached_profile_gateway_restart() now breaks out of
Electron's job object, so the gateway respawn watcher survives the GUI
quit and successfully respawns the gateway.

Surviving from c1e5fa4:
  * CREATE_BREAKAWAY_FROM_JOB in hermes_cli/_subprocess_compat.py (fixes #2)
  * Inlined breakaway flag in the watcher respawn snippet in gateway.py
  * hermes gateway status --deep PASS/FAIL probes (fixes #1 — visibility)
  * 'if not exist <target> exit /b 0' guard in _build_startup_launcher
    (fixes #3 — silent no-op for stale Startup entries)
  * tests/hermes_cli/test_gateway_wsl.py is_windows=False stubs (root cause
    of #3 — pytest WSL tests no longer leak Startup entries on Win hosts)

Removed in this commit:
  * hermes_cli/gateway_supervisor.py (entire file)
  * Supervisor section in hermes_cli/gateway_windows.py (~180 lines):
      get_supervisor_task_name, get_supervisor_script_path,
      _build_supervisor_cmd_script, _write_supervisor_script,
      _install_supervisor_task, is_supervisor_task_registered,
      _install_supervisor_best_effort
  * _install_supervisor_best_effort() calls in install() (3 spots)
  * supervisor cleanup block in uninstall()
  * supervisor display lines in status() / status(deep=True)

Future direction (out of scope for this PR): the right place for Windows
'Restart=always' semantics is a real Windows Service installed via
pywin32's win32serviceutil.ServiceFramework — session-0 isolation, SCM
auto-restart, no console window possible. That's a meaningful next-PR
project, not a band-aid.

Tests: 51 pass / 2 pre-existing failures in
tests/hermes_cli/test_gateway_{windows,wsl}.py (the 2 failures are
TestSupportsSystemdServicesWSL cases that fail on origin/main too —
unrelated to this PR).
sahilm-ti pushed a commit that referenced this pull request Jun 22, 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.
sahilm-ti pushed a commit that referenced this pull request Jul 21, 2026
Blocking #1 — gateway-connecting-overlay.tsx reduced-motion regression:
the top `if (reduce) setPhase('gone')` fired unconditionally on mount
whenever reduce-motion was on, so every OS reduced-motion user lost the
CONNECTING overlay during cold boot entirely (jumped to 'gone' before the
gateway was even open). The intent was to skip the exit *choreography*,
not to skip showing the overlay. Removed the unconditional top block and
the redundant nested preview block; kept only the third branch
(`gatewayState === 'open' && shownRef.current` → `reduce ? 'gone' :
'text-out'`) which correctly gates the short-circuit on connect. Also
fixed `if(reduce)` missing-space, 6-space misindent, and the same 3-line
comment pasted three times.

Nit #1 — tsconfig excludes e2e, so specs were never typechecked in CI.
Added tsconfig.e2e.json (extends base, includes e2e/ + playwright.config.ts,
adds @playwright/test types) and wired it into the typecheck script. This
surfaced three latent type errors that are fixed in the same commit:
  - fix-electron-tracing.ts: `app._context` and `electron._playwright` are
    private APIs — added `as any` on the access before the existing cast.
  - playwright.config.ts: `reducedMotion: 'reduce'` directly under `use:`
    is not a valid UseOptions property in playwright 1.58; it's a
    BrowserContextOption accessed via `contextOptions: { reducedMotion:
    'reduce' }`. The old form was silently ignored at runtime, so
    reduced-motion emulation wasn't actually active — screenshots could
    catch overlays mid-fade (exactly what the comment warned about).

Nit #2 — fix-electron-tracing.ts reaches into Playwright internals
(_playwright, _allContexts, _context) with no public contract. Added a
header comment calling out the `@playwright/test` exact pin (=1.58.2) so a
future bump knows to re-verify the private symbols still exist.

Nit #3 — main.ts TEST_WORKER_INDEX block had stray 6-space indentation.

Verified: tsc -p . && tsconfig.electron && tsconfig.e2e → 0 errors;
vitest boot-failure-overlay (3/3) + boot-failure-reauth (21/21) pass;
npm run build clean; playwright e2e/boot-failure.spec.ts 2/2 pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.