fix(aux-routing): _read_main_provider string-form inference + credential transient-failure hardening - #6
Merged
Conversation
…g-form model: Myah-marker patch. When config.yaml has model: as a bare string (e.g. 'anthropic/claude-haiku-4-5-20251001'), _read_main_provider previously returned '' because it only accepted dict-form config. This broke _resolve_auto Step 1 for every Myah frontend user, routing all aux calls to the OpenRouter hardcoded fallback instead of the user's picked provider. New behavior (string-form path, inside Myah marker block): 1. Fast path: if the model string is '<provider>/<rest>' and the prefix is a known provider key in _PROVIDER_MODELS, return that prefix directly (e.g. 'anthropic/claude-haiku...' -> 'anthropic'). 2. Fallback: delegate to detect_provider_for_model(model_str, current_provider='') — the same helper the interactive hermes model flow uses at hermes_cli/main.py:1997-2004 when writing dict-form config. Return the inferred provider id, or '' if no confident match. No mutation of config.yaml on disk. CLI users (who write dict-form via 'hermes model') are unaffected. No impact on prompt caching (aux-routing-only path, outside conversation context). Tests added: TestReadMainProvider (7 tests) covering string-form anthropic/openrouter inference, dict-form regression, auto pass-through, unknown/empty/missing key safety. Upstream PR to be filed (see platform PR description). Marker to be removed on next upstream merge that lands this fix. Refs T3-1001 Co-Authored-By: Claude <noreply@anthropic.com>
…m inference Covers: vendor-prefix ground truth (resilient to catalog churn), dict-with-no-provider-key scope limit, load_config exception safety, known-prefix fast path, unknown-prefix fallthrough. Refs T3-1001 Co-Authored-By: Claude <noreply@anthropic.com>
…ts in _validate_api_key Myah-marker block. Previously _validate_api_key returned False on any exception (DNS flake, timeout, 429, 5xx), treating transient infra issues as invalid keys. Evidence: 8 consecutive 400s for a valid OpenRouter key over 40s (backend log 16:48:14-16:48:54), then 200 for the same key 16 seconds later — same user, same key, same endpoint. New contract: only explicit HTTP 401/403 (provider auth denial) returns (False, reason). Timeouts, 429, 5xx, and network errors return (True, 'optimistic accept ...') with a warning log. This matches native Hermes CLI behavior (accepts keys optimistically; first real API call surfaces auth failures). Updated return type from bool to tuple[bool, str]; call site in handle_connect_credential updated accordingly. Existing test updated to use tuple mock return value. New tests: 8 _validate_api_key unit tests + 2 handler-level tests covering 401/403 rejection, 429/5xx/timeout/network optimistic accept, full handler 400 on auth denial, full handler 200 on optimistic accept. Refs T3-1001 Co-Authored-By: Claude <noreply@anthropic.com>
Regression fence: auxiliary_client.py and the Myah /myah/v1/aux endpoint must never reference Honcho, MemoryManager, or peer_card. Current code is clean (verified 2026-04-22). These tests fail immediately if a future refactor accidentally re-couples the aux call path to user memory. Refs T3-1001 Co-Authored-By: Claude <noreply@anthropic.com>
deestax
added a commit
that referenced
this pull request
May 7, 2026
…ner + Sentry/admin moves (#24) * chore(hermes): drop unused _get_adapter_for_platform() helper Verified zero callers via repo-wide grep. Function was a leftover from earlier dispatch refactoring. Drop candidate #2 from spec 2026-05-06 §2.3 (Tier 2A Task 2A.1.1). * chore(hermes): drop os.environ CHAT_ID writer (concurrency bug) Per-request mutation of process-wide environment, racing with concurrent requests. The TODO comment confirmed this was a known defect awaiting migration. set_session_vars() in session_context.py is the contextvar-safe replacement. Drop candidate #5 from spec 2026-05-06 §2.3 (Tier 2A Task 2A.1.2). * chore(hermes): catch up MAX_REQUEST_BYTES to upstream (10MB) Verified via git blame: the 1MB value at line 88 was authored by Teknium upstream at commit 80cc27e (2026-03-24) and later bumped to 10MB upstream. Fork has the older value due to drift, not a Myah security decision. Drop candidate #6 from spec 2026-05-06 §2.3 (Tier 2A Task 2A.1.3). * chore(hermes): drop /health Myah credential-validation extension The MYAH_ADAPTER_ENABLED-gated credential check was specific to the fork-bundled adapter and goes away with the plugin-based OSS architecture. /health/detailed (line 832 below) provides rich runtime status; 'hermes doctor' covers credential validation. Drop candidate #4 from spec 2026-05-06 §2.3 (Tier 2A Task 2A.1.4). * chore(hermes): drop cronjob_tools origin-unresolved WARNING Pure debug logging — the function returns None either way; the WARNING was added for diagnostic purposes during a 2025-era cron delivery bug. Function behavior unchanged. Drop candidate #7 from spec 2026-05-06 §2.3 (Tier 2A Task 2A.1.5). * feat(plugin): move myah_overrides.py from hermes_cli/ into plugin Provider catalog augmentation (230 LOC) was Myah-only data living in an upstream-counterpart directory. Moves it into the pip-installed plugin at myah_hermes_plugin/myah_admin/myah_overrides.py and updates the two known importers (plugins/myah-admin/dashboard/_providers.py and the plugin tests). After this commit, hermes_cli/ contains zero Myah-only files — one fewer cardinal-counterpart file to keep marker hygiene on. Implements Tier 2A Task 2A.6 from spec 2026-05-06 §3. * feat(plugin): move Sentry init from upstream into plugin register() (Task 2A.7 + drop #3) Previously gateway/platforms/api_server.py:76-82 had an inline 'try: from logging_setup import setup_sentry; setup_sentry(); except ImportError: pass' Myah marker block. logging_setup.py was Myah-specific (agent-container Sentry init + SentryHook adapter for agent.telemetry.TelemetryHook protocol) but lived in an upstream-counterpart top-level path. This commit: - Renames logging_setup.py -> myah_hermes_plugin/sentry_init.py. - Calls sentry_init.setup_sentry() from the plugin's register() so hosted-Myah agent containers still get Sentry on gateway boot. - Drops the inline init Myah-marker block from api_server.py. Idempotent: setup_sentry() returns silently when SENTRY_DSN_AGENT is unset (the OSS-user case). Closes drop candidate #3 from spec 2026-05-06 §2.3 (deferred from Tier 2A Task 2A.1 to 2A.7 because it depended on having a plugin-side sentry_init.py to call into). Implements Tier 2A Task 2A.7 from spec 2026-05-06 §3. * feat(plugin): add pre_gateway_dispatch hook (Tier 2A Task 2A.4) Replaces the skip_user_authorization semantics that PR #20 removed. Adds myah_hermes_plugin/myah_platform/pre_dispatch_hook.py with a single hook callback that returns {'action': 'allow'} for Myah-platform messages and None (passthrough) for everything else. Architectural note: returning 'allow' does NOT bypass the gateway-level user-allowlist check at gateway/run.py:3655. Auth bypass for Myah's single-tenant deployment is handled by allow_all_env=MYAH_ALLOW_ALL_USERS on the platform registration. The hook exists as a documented choke point for future Myah-specific routing logic (rate limiting, silent ingest, content rewrite) that must NOT live in upstream gateway/run.py. Wired via ctx.register_hook('pre_gateway_dispatch', ...) in the plugin's register() — guarded with hasattr(ctx, 'register_hook') so older PluginContext versions don't fail. Implements Tier 2A Task 2A.4 from spec 2026-05-06 §3. * feat(plugin): vendor _dispatch_approval_notify + register_gateway_notify Plugin owns its own copy of the notify-dispatch chain. Without it, the plugin's vendored request_action_confirmation has no transport to the platform adapter and the agent silently auto-approves. Variadic dispatch (1/2/3-arity callbacks) matches upstream's gateway/run.py:_dispatch_approval_notify behavior at lines 334-460. Uses threading.Lock for the registry to match upstream's thread-safe semantics in tools/approval.py. Implements docs/superpowers/plans/2026-05-06-myah-oss-completion-2a-plugin-stock-upstream.md Task 2A.2.0. * feat(plugin): vendor request_action_confirmation + _action_queues registry Plugin owns its own action confirmation primitives mirroring upstream's tools/approval.py:request_action_confirmation (sync, threading.Event-based). The sync contract is required because cronjob_tools._execute() runs in a tool-runner threadpool and blocks on event.wait(timeout) — an asyncio version would force rewriting the cron tool. Two of the six tests intentionally fail until Task 2A.2.2 lands the plugin's cron_tool.py shadow. Implements docs/superpowers/plans/2026-05-06-myah-oss-completion-2a-plugin-stock-upstream.md Task 2A.2.1. * feat(plugin): shadow upstream cronjob_tools.py with plugin-owned cron tool Verbatim copy of upstream's tools/cronjob_tools.py, with the single change of importing request_action_confirmation from the plugin-vendored myah_hermes_plugin.cron_approval. Tool registration uses the same 'cronjob' name as upstream so last-writer-wins on import order shadows upstream's handler with the plugin's. All 6 tests in test_cron_approval.py now pass (the two cron_tool sanity checks added in 2A.2.1 had been failing pending this commit). Implements docs/superpowers/plans/2026-05-06-myah-oss-completion-2a-plugin-stock-upstream.md Task 2A.2.2. * refactor(plugin): adapter.py uses plugin-vendored approval primitives Move 4 import sites in adapter.py from upstream's tools.approval to the plugin's vendored modules: - resolve_action_confirmation → myah_hermes_plugin.cron_approval - resolve_action_confirmation_by_session → myah_hermes_plugin.cron_approval - unregister_gateway_notify (×2) → myah_hermes_plugin.dispatcher Update mock targets in test_myah_confirm_dispatch.py to follow the new import paths. Comment block in send_action_confirmation updated to reference the plugin's cron_approval module. resolve_gateway_approval (legacy terminal-command approval) stays on tools.approval — it's outside Tier 2A's cron-approval scope. Implements docs/superpowers/plans/2026-05-06-myah-oss-completion-2a-plugin-stock-upstream.md Task 2A.2.3. * feat(plugin): adapter standalone aiohttp runner on MYAH_GATEWAY_PORT Tier 2A Task 2A.3 — collapse the previous hosted/standalone split. The plugin's MyahAdapter now ALWAYS owns its own aiohttp AppRunner + TCPSite via the new MyahStandaloneRunner helper, removing the dependency on upstream's gateway/platforms/api_server.py register_pre_setup_hook + get_shared_app. This is a one-way door for hosted Myah: once shipped, hosted Myah uses the standalone runner forever. See spec docs/superpowers/specs/2026-05-06-myah-oss-completion-design.md §3 Task 2A.3 for the rationale. Default port is now 8643 via MYAH_GATEWAY_PORT (was 8642 hard-coded). Port resolution still respects config.extra.port and MYAH_ADAPTER_PORT overrides for backward compat with existing yaml. Tests cover ephemeral binding (port=0 → OS-assigned), live HTTP route dispatch, and env-var resolution fallback. Zero new failures in the gateway/tools test directories vs baseline (55 pre-existing failures unchanged). * test: fix Tier 2A regressions surfaced by full CI suite The PR's local verification only ran tests/gateway/ + tests/tools/ — three tests in tests/cron/, tests/plugins/, and tests/tools/test_resolve_path.py broke under the full CI run. 1. tests/cron/test_origin_capture_log.py — DELETED. Task 2A.1.5 dropped the WARNING that this entire 'Bug E' file was designed to verify. The file's purpose is gone with the warning; keeping the happy-path companion test would just duplicate coverage that lives elsewhere. 2. tests/plugins/test_myah_admin_providers.py::test_list_providers_against_real_catalog — import switched to pytest.importorskip(). Task 2A.6 moved myah_overrides.py from hermes_cli/ into the plugin distribution, but the hermes Tests workflow only runs 'uv pip install -e ".[all,dev]"' against the core package — it never installs the plugin. Local dev runs (where the plugin is editable-installed) still exercise the real-catalog assertion; CI now skips it cleanly. The plugin's own pytest suite covers the catalog path independently. 3. tests/tools/test_resolve_path.py — added isolated_resolve_state fixture that clears tools.file_tools._file_ops_cache and tools.terminal_tool._active_environments before the two tests that rely on falling through to TERMINAL_CWD. Adding the new plugin modules to the suite shifted xdist worker assignment in a way that exposed pre-existing pollution from other tests leaving a 'default' task_id entry in those module-level dicts. The fixture is per-test (monkeypatch.setattr) so it doesn't disrupt other tests. * test: gentler test_resolve_path isolation (don't rebind module attrs) Previous fix used monkeypatch.setattr to swap _file_ops_cache and _active_environments with empty dicts. This unintentionally caused test_file_state_registry tests to fail on Linux CI under xdist — rebinding the module attribute discarded entries other tests had written under non-'default' keys, and the cross-worker timing exposed the leak. The replacement fixture mutates the existing dicts in place — only pops the 'default' key (which is what _resolve_path reads when no explicit task_id is passed) and restores the prior value after the test. No module-attribute rebinding, no impact on entries other tests created. * ci: trigger re-run to confirm test flakiness Recent runs showed 3 new tests intermittently failing on Linux CI that pass locally and pass in isolation: - test_concurrent_inserts_settle_at_cap (30s timeout) - test_modal_sandbox_fixes::TestToolResolution::test_terminal_tool_present - test_modal_sandbox_fixes::TestToolResolution::test_terminal_and_file_toolsets_resolve_all_tools This empty commit re-runs CI to confirm whether they're deterministic regressions or pre-existing test-ordering flakes exposed by the changed test count from this PR.
deestax
added a commit
that referenced
this pull request
May 11, 2026
Closes Issue #6 from the OSS follow-up spec without requiring an upstream PR or core modification. The watcher polls the stable ~/.hermes/cron/output/{job_id}/*.md convention every 2s, reads job metadata via vanilla cron.jobs.load_jobs(), and POSTs to the platform's existing /api/v1/processes/webhook/run-complete. Trade-off: tool_calls_log is None because the on-disk .md doesn't carry it. Cron AG-UI render_* artifacts deliver as plain Markdown (accepted degradation per spec). Started lazily from pre_gateway_dispatch hook to avoid Python 3.12+ asyncio.get_event_loop() deprecation. Silent no-op when MYAH_PLATFORM_BASE_URL is unset. Filtered to origin.platform == 'myah' so telegram/discord cron jobs are unaffected. Includes a CI guard that catches upstream API drift on cron.jobs.OUTPUT_DIR and load_jobs. Refs: docs/superpowers/specs/2026-05-10-myah-oss-followup-issues.md Phase E.
deestax
added a commit
that referenced
this pull request
May 11, 2026
…nect (#27) * feat(plugin): MYAH_USER_ID bootstrap via platform /whoami in OSS mode Hosted Myah injects MYAH_USER_ID per-container at spawn time. OSS self-hosted users have no spawner — without this bootstrap they would have to manually copy their user_id from the platform UI to ~/.hermes/.env or cron deliveries would fail with HTTP 400. The plugin's register(ctx) now calls _bootstrap_user_id() which: - Idempotent: no-op if MYAH_USER_ID already set (hosted case). - Skips silently if MYAH_PLATFORM_BASE_URL or bearer is missing. - Calls GET /api/v1/myah/whoami with the platform bearer token. - On success: populates MYAH_USER_ID in os.environ. - On any error: logs at WARNING level and continues (chat still works without cron-webhook delivery). Bearer alias fallback: MYAH_PLATFORM_BEARER, MYAH_AGENT_BEARER_TOKEN, or MYAH_AGENT_TOKEN — any one is acceptable, matching the env-var naming in agent/Dockerfile.stock and ~/.hermes/.env templates. Test: 9 tests in test_user_id_bootstrap.py cover idempotency, missing config, HTTP 4xx/network errors, empty response, alias env vars, trailing-slash URL handling. Refs: Phase 8.2 of docs/superpowers/plans/2026-05-09-myah-no-fork-vendoring.md * feat(plugin): F4 secret-capture global callback registration (Phase 5.1) Vanilla upstream's tools/skills_tool exposes set_secret_capture_callback(fn) — a single global slot for the secret-prompt callback, invoked when a tool needs the user to provide a secret value. The fork carries hosted-Myah-specific session-keyed wiring inside _run_agent's closure; on stock vanilla that wiring doesn't exist, so without this commit the plugin's _secret_capture_callback bound method never fires and secret prompts silently auto-skip. This commit registers a global wrapper at plugin register-time: 1. tools.skills_tool.set_secret_capture_callback(_global_secret_callback) takes the vanilla-shaped (name, prompt, metadata) call. 2. _global_secret_callback consults adapter._LATEST_ADAPTER (a late-bound module attr set by MyahAdapter.__init__) plus the session-key contextvar (from tools.approval) to resolve the right stream_id. 3. Delegates to adapter._secret_capture_callback(name, prompt, metadata, stream_id) — which already does the SSE event push and threading.Event blocking dance. No adapter-side changes needed. Single-adapter / single-process assumption (single-tenant OSS or per-user agent container in hosted Myah). Multi-tenant in-process would need a contextvar-keyed lookup; out of scope for v1. Failure modes (all silent + logged at WARNING): - No active adapter → auto-skip with success=true, skipped=true. - No active stream → adapter still called with empty stream_id; adapter handles via its existing 'No stream' branch. - tools.skills_tool unavailable → ImportError swallowed, no-op. Test: 6 tests in test_secret_capture_wiring.py cover all branches. All 310 plugin tests still pass. Refs: Phase 5.1 of docs/superpowers/plans/2026-05-09-myah-no-fork-vendoring.md * feat(plugin): F7 per-server MCP disconnect via direct private-attribute access Vanilla upstream's tools/mcp_tool exposes shutdown_mcp_servers() which tears down ALL configured MCP servers at once. The Myah admin UI needs per-server teardown so a user can remove one MCP connection (e.g. a misbehaving server) while leaving the others alive. Vanilla does not expose a public per-server disconnect API. The implementation accesses upstream's private state directly: - _servers: Dict[str, MCPServerTask] (upstream/main:tools/mcp_tool.py:1607) - _lock: threading.Lock (line 1969 — sync, NOT asyncio) - MCPServerTask.shutdown(self) (line 1568 — async coro) - _run_on_mcp_loop(coro, timeout=...) (line 2042 — cross-loop bridge) Per AGENTS.md interpretation (see runtime_extensions/__init__.py docstring): direct attribute access is normal Python, NOT modifying core files on disk. Same pattern adapter.py:get_session_override_direct already uses for runner._session_model_overrides. Behavior: - Acquire the threading.Lock (sync — never use asyncio.acquire). - Look up _servers[name]; return False if missing. - Call MCPServerTask.shutdown() coro through _run_on_mcp_loop bridge with caller-supplied timeout (default 15s). - Swallow shutdown exceptions — the user wants the server gone, don't block removal on a hung close. - Pop from _servers and return True. Robustness: every getattr falls back to None and degrades to a no-op-with-False return on upstream API drift. Two CI guard tests catch the rename at plugin-CI time: - test_upstream_state_present: assert _servers / _lock / _run_on_mcp_loop attributes still exist. - test_upstream_lock_is_threading_not_asyncio: assert lock works in a sync 'with' block (asyncio.Lock would error). Test: 7 tests in test_mcp_disconnect.py cover happy path, missing server, missing private state, exception during shutdown, custom timeout propagation, and the two CI guards. Refs: Phase 5.2 of docs/superpowers/plans/2026-05-09-myah-no-fork-vendoring.md * docs(plugin): release v1.1.0 with OSS bootstrap, F4 secret wiring, F7 MCP disconnect CHANGELOG.md gains: - New 'Known limitations' section at the top documenting F5 BOOT.md unavailability on stock vanilla upstream + plugin, and the single-tenant assumption of /whoami. - v1.1.0 entry documenting Phase 8.2 (user_id bootstrap), Phase 5.1 (secret-capture global callback), Phase 5.2 (MCP disconnect helper). - Test count update: 333 plugin tests (310 prior + 23 new). pyproject.toml version bumped 1.0.0 → 1.1.0 (additive features, no breaking changes per semver). Refs: Phase 6 of docs/superpowers/plans/2026-05-09-myah-no-fork-vendoring.md * docs(plugin): document F6 cron-on-vanilla limitation and resolution paths Vanilla cron/scheduler.py:_deliver_result does NOT call runtime_adapter.build_delivery_metadata(...) — that polymorphic hook is fork-only (Tier 2B Task 2B.4). Result on stock vanilla + plugin: cron jobs run and write to ~/.hermes/cron/output/, but do not appear in chat history. Two resolution paths documented (neither shipped in v1.1.0): 1. Upstream PR U-CRON adding the polymorphic call to vanilla. 2. Plugin-side cron output-dir watcher (~150 LOC, no upstream PR). The hosted Myah deployment is unaffected — it uses the fork build with Tier 2B's polymorphic hook, so cron deliveries reach chat normally. Refs: Phase 3 of docs/superpowers/plans/2026-05-09-myah-no-fork-vendoring.md (documented as known limitation rather than shipping the contentious runtime monkey-patch on cron.scheduler._deliver_result) * chore(plugin): INFO log model switch flow for OSS Issue #2 Adds three logger.info calls in _handle_message_endpoint's model override block: pre-switch (raw_input + current state), post-switch (success/new_model/target_provider/error_message), and post-write (confirmed override). Lets us reproduce why OSS model picks fail to take effect and triage the right fix. Refs: docs/superpowers/specs/2026-05-10-myah-oss-followup-issues.md Phase B.1. * fix(plugin): OSS graceful fallback when model name unresolvable When MYAH_DEPLOYMENT_MODE=oss and the platform-supplied model is not in any of the user's hermes credentials (e.g. user.default_model is openai/gpt-4o-mini but hermes pool is openrouter+opencode-go), fall through to the agent's existing default rather than 400-failing the entire chat turn. Hosted mode behaviour is unchanged (multi-tenant: a 400 surfaces an auth/config bug to the user). OSS is single-tenant: the user wants their message to go through with whatever model the agent has. The session override is cleared either way, so subsequent turns re-attempt resolution. Closes Issue #2 from the OSS follow-up spec. Refs: docs/superpowers/specs/2026-05-10-myah-oss-followup-issues.md Phase B.4. * feat(plugin): bootstrap user.default_model from hermes config Extends _bootstrap_user_id() to read default_model from /whoami's OSS response and POST it to the platform's /api/v1/users/user/default-model endpoint. Idempotent and best-effort: logs and continues on any failure, never blocking the bootstrap. Closes Issue #5 plugin side from the OSS follow-up spec. Refs: docs/superpowers/specs/2026-05-10-myah-oss-followup-issues.md Phase C.4. * feat(plugin): F6 cron->chat output watcher for stock vanilla Closes Issue #6 from the OSS follow-up spec without requiring an upstream PR or core modification. The watcher polls the stable ~/.hermes/cron/output/{job_id}/*.md convention every 2s, reads job metadata via vanilla cron.jobs.load_jobs(), and POSTs to the platform's existing /api/v1/processes/webhook/run-complete. Trade-off: tool_calls_log is None because the on-disk .md doesn't carry it. Cron AG-UI render_* artifacts deliver as plain Markdown (accepted degradation per spec). Started lazily from pre_gateway_dispatch hook to avoid Python 3.12+ asyncio.get_event_loop() deprecation. Silent no-op when MYAH_PLATFORM_BASE_URL is unset. Filtered to origin.platform == 'myah' so telegram/discord cron jobs are unaffected. Includes a CI guard that catches upstream API drift on cron.jobs.OUTPUT_DIR and load_jobs. Refs: docs/superpowers/specs/2026-05-10-myah-oss-followup-issues.md Phase E. * chore(plugin): add _chat_id_session_keys + _native_streaming_used state Prepares MyahAdapter for the Phase F streaming workaround. New fields are wired up but have no consumer yet — adding them in a separate commit lets the next commits be focused. Includes defensive cleanup in the hosted-mode failure path so the map doesn't leak entries when model-override fails. Refs: docs/superpowers/specs/2026-05-10-myah-oss-followup-issues.md Phase F.1. * feat(plugin): F-streaming pre_llm_call hook for stock vanilla Closes Issue #7 from the OSS follow-up spec. Plugin-side replacement for the fork's get_structured_callbacks polymorphic dispatch in gateway/run.py:_run_agent. Three coordinated pieces: 1. pre_llm_call hook (runtime_extensions/streaming_callbacks.py) that mutates AIAgent callback attributes just-in-time before each LLM call. Resolves session_key from chat_id via the adapter's _chat_id_session_keys map; finds the cached agent in runner._agent_cache (private-attribute pattern; same risk class as _session_model_overrides used elsewhere in the plugin). 2. Marks _native_streaming_used per session so send() knows. 3. Duplicate-send suppression in MyahAdapter.send() — drops the gateway's final adapter.send(chat_id, full_response) call after streaming completes, preventing the message from duplicating. CI guards verify pre_llm_call is in VALID_HOOKS, GatewayRunner has _agent_cache, and AIAgent has the four callback attributes. Removable when upstream U-CB PR lands. Until then, structured streaming on stock vanilla matches the fork's behaviour. Refs: docs/superpowers/specs/2026-05-10-myah-oss-followup-issues.md Phase F. * fix(plugin): lazy gateway_runner self-discovery via _gateway_runner_ref `GatewayRunner._create_adapter` sets `adapter.gateway_runner` only for built-in adapters (Discord, Webhook at gateway/run.py:4620/4730). For plugin-registered platforms it returns the adapter from `platform_registry.create_adapter` directly without setting the backref, so MyahAdapter.gateway_runner stays None forever. This silently disabled three flows: 1. Phase B model override block (_handle_message_endpoint) — every model pick from the platform was dropped, agent always used hermes default. 2. Per-message model attribution (_dispatch_message finally) — _attribution_model/_attribution_provider stayed empty. 3. Phase F pre_llm_call streaming hook — early-returned at runner-None. Diagnostic confirmed during E2E: [myah-modelswitch] DIAGNOSTIC entry _override_model='moonshotai/kimi-k2.6' runner_is_none=True gateway_runner_id=None Add `MyahAdapter._resolve_runner()` that reads the upstream-exposed module weakref `gateway.run._gateway_runner_ref` (populated by GatewayRunner.__init__ at run.py:1206) when the fast-path attribute is unset. Cache the resolved runner on `self.gateway_runner` for next call. Same direct-attribute access pattern Tier 2B established for `_session_model_overrides` and `_agent_cache` access. Replace three reader sites — Phase B override, per-message attribution, admin route registration — with `self._resolve_runner()`. Update the `pre_llm_call` hook in `streaming_callbacks.py` similarly; keep a defensive fallback to `getattr(adapter, 'gateway_runner', None)` for older plugin builds. Works on both this branch and stock vanilla pip-installed hermes — both expose the same `_gateway_runner_ref` symbol. Hosted Myah behavior is unchanged because the fast-path returns the externally-set attribute when present. Verified E2E by sending a curl POST to /myah/v1/message with model='moonshotai/kimi-k2.6'; observed all three modelswitch INFO logs fire, agent's auxiliary client used `provider=openrouter model=moonshotai/kimi-k2.6`. Refs: e2e-output/report.md ISSUE-001 * fix(plugin): add /myah/v1/admin/config endpoint for hermes config readback Phase C.5's `fetch_hermes_default_model` previously hit the hermes dashboard at :9119/api/config which requires `HERMES_WEB_SESSION_TOKEN` — a token the OSS `setup-myah-oss.sh` script doesn't generate. Result: the dashboard returned 401, the helper silently returned None, the platform's /whoami returned `default_model: null`, and no sync ever fired. User stuck with open-webui's gpt-4o-mini default. Hosted Myah dodged this because the per-user-container spawner injects both tokens. OSS users have to opt into dashboard auth manually, which is invisible / poorly documented. Replace the dashboard call with a new plugin-side endpoint `GET /myah/v1/admin/config` that: - Reads `get_hermes_home() / 'config.yaml'` in-process - Returns only the model block (provider/default/base_url/api_mode) — no API keys, no plugin settings - Auths via MYAH_ADAPTER_AUTH_KEY (same bearer as every other /myah/v1/admin/* endpoint) which OSS sets to empty (allow-all) and hosted spawner sets to the per-user token The platform's `fetch_hermes_default_model` rewires to call this new endpoint on the adapter port (8643 OSS, per-user-container port hosted) instead of the dashboard. Auth simplifies: same bearer everywhere, no separate session token needed. Drops the broken plugin-side POST to /api/v1/users/user/default-model (that endpoint requires JWT auth via get_verified_user — the plugin only has the agent bearer). The platform's /whoami now performs the sync itself directly via Users.update_user_by_id — single-tenant OSS makes this safe; multi-tenant hosted doesn't reach this path because of the deployment_mode gate. Tests: - 7 new tests in tests/test_runtime_admin_config.py covering happy path, missing file, malformed YAML, partial model block, auth on/off - 4 new tests in platform test_myah.py covering the sync behavior - E2E verified: /whoami returns hermes default; DB updates to match; deliberate user choices (anything not gpt-4o-mini) are preserved Refs: e2e-output/report.md ISSUE-004 * fix(plugin): add /myah/v1/admin/providers endpoint with credential status Phase C.2's catalog short-circuit and the planned auto-import path both need a way to enumerate hermes-configured providers from the platform. The existing dashboard endpoint at /api/plugins/myah-admin/providers returns the raw catalog without a 'has_credential' field — callers had no way to distinguish 'provider available in UI' from 'user actually configured this'. The dashboard also requires HERMES_WEB_SESSION_TOKEN which OSS users don't have. New plugin-side admin endpoint: GET /myah/v1/admin/providers -> {"providers": [{"id": ..., "has_credential": bool, ...}, ...]} Calls the dashboard's _build_catalog() in-process (same plugin pip package) and enriches each entry with has_credential computed from: 1. auth.json['credential_pool'] — the canonical hermes-configured provider set (covers both api_key + oauth via 'hermes auth') 2. auth.json['providers'] — legacy OAuth-token storage 3. os.environ[<env_var>] — api_key fallback for providers configured via .env (e.g. OPENROUTER_API_KEY) Auth uses the standard adapter auth_key (MYAH_AGENT_BEARER_TOKEN in both OSS and hosted) — no separate session token needed. E2E verified: against the user's hermes (6 configured providers: openrouter, opencode-go, zai, openai-codex, copilot, xiaomi), the endpoint returns 36 total providers with exactly those 6 flagged as has_credential=True. Tests added (7 in tests/test_runtime_admin_providers.py): api_key via env, oauth via auth.json.providers, credential_pool path, all-three empty, response shape, catalog failure, auth on/off. Refs: e2e-output/report.md ISSUE-003 * fix(plugin): surface inline error when gateway suppresses failed response When the agent's LLM call fails (e.g. provider 402 / fallback exhausted / network timeout), the agent's `run_conversation` returns: { final_response: 'API call failed after 3 retries: ...', failed: True } But the gateway's `_run_agent` at gateway/run.py:14701-14720 constructs a NEW response dict that DOES NOT preserve the `failed` field. The downstream suppression check at gateway/run.py:15326 then sees `failed=None` (falsy), treats the run as successful, and combined with `native_streamed=True` (set optimistically when our structured callbacks were wired) suppresses the call to `adapter.send()`. Net result: user sees 'Thinking...' forever — no token streamed, no error rendered, no indication anything happened. Backend log shows 'Suppressing normal final send for session ... (streamed=False previewed=False native_streamed=True)' with no follow-up Send. We can't fix the upstream bug (don't-touch-hermes-core constraint). Plugin-side workaround: track whether `stream_delta` actually fired for each session. In `_dispatch_message`'s finally block (right before emitting `run.completed`), if no stream_delta ever fired for this session, emit a generic 'LLM call did not produce a response' error as a synthetic `message.delta` event. The frontend renders this inline so the user sees SOMETHING instead of staring at an empty Thinking spinner. Three coordinated changes: 1. MyahAdapter._stream_delta_invoked: new per-session set tracking actual stream_delta callback invocations. 2. get_structured_callbacks: stream_delta wrapper marks the session on first call. 3. _dispatch_message finally: emit error message.delta when the set doesn't contain this session_key by the time the run ends. Also adds a `post_llm_call` hook in streaming_callbacks.py for cases where the agent's run_conversation completes normally and the post-call hook can surface the actual response text. The hook is defensive and complements the _dispatch_message catch-all. E2E verified: triggered openrouter 402 fallback via curl POST to /myah/v1/message with model=moonshotai/kimi-k2.6; SSE stream now contains a message.delta event with the user-visible error string followed by run.completed. Browser test confirmed the error renders inline in the chat instead of empty Thinking spinner. Tests added (5 in test_streaming_callbacks.py): post_llm_call success path, no-op when stream fired, ignores other platforms, ignores empty response, no-op when no active stream, post_llm_call in VALID_HOOKS CI guard. Refs: e2e-output/report.md (new finding — surfaced during user manual testing after the original 4 ISSUE fixes landed) * fix(plugin): slash commands no longer trigger false-positive warning The previous gateway-suppression-bug workaround checked `session_key in _stream_delta_invoked` to decide whether to emit the fallback warning. `_stream_delta_invoked` is marked inside the `stream_delta_callback` wrapper in `get_structured_callbacks` — which only fires for LLM streaming output. Slash commands like `/model` deliver their response a different way: hermes' gateway processes the command in-process and calls `adapter.send(chat_id, response_text)`. `send()` pushes a `message.delta` event via `_push_event_sync` directly — never touching the stream_delta callback. So `_stream_delta_invoked` stays empty for slash commands, and the finally block fired the warning even after a valid response had just been delivered. User-visible bug: chat showed the /model picker output with the warning text appended at the end: Current: gpt-5.4-mini on OpenAI Codex ... /model <name> --global — persist⚠️ The agent's LLM call did not produce a response. This usually means... Fix: add `_stream_had_content`, a per-stream_id set that's marked whenever ANY `message.delta` event passes through `_push_event_sync`. Unified tracking across: - stream_delta_callback wrapper (LLM streaming output) - send() live-chat path (slash commands, agent reply) - send() cron-delivery live-preview - post_llm_call hook synthetic emission The `_dispatch_message` finally block now checks `stream_id not in self._stream_had_content` instead of `session_key not in self._stream_delta_invoked`. False-positive eliminated; real failure case still triggers the warning. E2E verified: - /model command: SSE stream contains exactly one message.delta with the picker text, NO warning appended - Failure case (kimi via openrouter 402): warning still emitted as expected Tests added (4 in TestStreamHadContent): - message.delta push marks the set - non-delta events (reasoning/status/tool/run.*) don't mark - unknown stream_id is a graceful no-op - repeated pushes are idempotent (set semantics) Found via user manual testing — chat log analysis showed the warning appended to a successful /model response. Refs: e2e-output/report.md (follow-up to ISSUE-001..004 fixes)
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.
Summary
Four commits covering two root-cause fixes surfaced during Myah PR 1a E2E testing (T3-1001).
Changes
fix(auxiliary_client):
_read_main_providerinfers provider from string-formmodel:_read_main_provider()atauxiliary_client.py:900-916only handled dict-formmodel:. Whenconfig.yamlhasmodel: "<id>"as a bare string (written by Myah'sPATCH /api/v1/agent/configpath viaset_config_value), the function returned""._resolve_autoStep 1 at:1290-1294then fell through to the hardcoded OpenRouter fallback — so every aux call (title, follow-ups, compression, vision, etc.) routed to OpenRouter regardless of what provider the user configured.Fix: extend
_read_main_providerwith a string-form branch (Myah marker block):<provider>/<rest>and the prefix is a known key in_PROVIDER_MODELS, return that prefix directly (anthropic/claude-haiku-4-5-20251001→anthropic)detect_provider_for_model(model_str, current_provider=""), the same helper the interactivehermes modelflow already uses when writing dict-form configRead-side only. No
config.yamlmutation. No prompt-cache impact (_read_main_provideris aux-routing only, outside conversation context). Mirrors_read_main_model's existing string-form handling at:889-890.fix(myah_management):
_validate_api_keytransient-failure handling_validate_api_keyreturnedFalseon any exception (timeout, DNS flake, 429, 5xx), treating every network transient as an invalid key. Observed: 8 consecutive 400s for a valid OpenRouter key over 40s, then 200 for the same key 16s later.Fix: distinguish genuine auth failures from transients:
(False, "auth denied by provider (HTTP N)")— reject(True, "optimistic accept ...")with warning log — acceptReturn type changed from
booltotuple[bool, str]. Call site inhandle_connect_credentialupdated accordingly.Tests
TestReadMainProvider(7 tests) — string-form anthropic/openrouter inference, dict-form regression, empty/missing/exception safetyTestReadMainProviderEdgeCases(5 tests) — vendor-prefix ground truth, dict-no-provider scope limit, swallows exception, known prefix fast-path, unknown prefix fallthroughTestHonchoAuxIsolation(2 tests) — regression fence ensuring aux calls never reference Honcho or MemoryManager_validate_api_keyunit tests (8) + handler-level tests (2) intest_myah_providers_catalog.pyAll tests pass via
scripts/run_tests.sh. Pre-existing failures (74) are unrelated and reproduced on the base commit.Refs T3-1001