feat: durable native Kanban review handoff - #2
Merged
Conversation
…_adapter_for_source The routing sweep sends these paths through _adapter_for_source, which reads source.profile. A bare MagicMock auto-attribute is truthy, so the fixtures looked like stamped secondary profiles and hit the new fail-closed branch. Real SessionSource.profile is None or str (AGENTS.md pitfall NousResearch#17).
…0 flake (NousResearch#59380) test_group_new_keeps_existing_reset_semantics_when_dm_topic_mode_enabled asserts 'parallel work' not in the /new reply — but /new appends a random tip from hermes_cli.tips (380 entries), and one tip's text contains exactly that phrase (the delegate_task concurrency tip). CI failed on PR NousResearch#59331 slice 2 when the dice landed on it. Pin get_random_tip in the test.
Fixes NousResearch#50051 by preserving nested gateway.multiplex_profiles and routing gateway config env reads through the active profile secret scope when present. This keeps secondary profile adapter startup from inheriting default-profile platform tokens or port-binding enables while preserving legacy single-profile behavior outside a scope. Constraint: latest upstream main f57ff7a still reproduced both nested-config loss and cross-profile env leakage Rejected: special-casing API_SERVER_* only | left other profile-scoped tokens vulnerable to the same leak Confidence: high Scope-risk: moderate Directive: keep future gateway/config env reads on the scoped helper path unless a variable is explicitly process-global Tested: pytest -q tests/gateway/test_multiplex_phase0.py tests/gateway/test_multiplex_credential_isolation.py tests/gateway/test_config.py -k 'multiplex or scope or getenv or api_server or relay' Not-tested: full gateway startup across live platform adapters
…nd (NousResearch#59327) * feat(sessions): full filter surface for prune + new bulk archive subcommand hermes sessions prune previously only supported --older-than N (integer days) and --source — no way to target a window like 'the last 5 hours' (e.g. a batch of CI smoke-test sessions), and no non-destructive option. - SessionDB.prune_sessions gains keyword filters that AND together: started_before/started_after epoch bounds, title_like, end_reason, cwd_prefix, min/max_messages, archived tri-state. Default call is byte-for-byte compatible (90-day cutoff, ended-only, source). - New SessionDB.list_prune_candidates (backs --dry-run + confirmation previews) and SessionDB.archive_sessions (bulk soft-hide via the existing set_session_archived lineage-aware path; nothing deleted). - CLI: prune gains --newer-than/--before/--after (durations like 5h/2d/1w, bare days, or ISO timestamps), --title, --end-reason, --cwd, --min/--max-messages, --include-archived, --dry-run. New 'hermes sessions archive' takes the same filters, requires at least one, and is idempotent. Both show a preview before confirming. - Dashboard /api/sessions/prune accepts the same filters + dry_run. - Docs: sessions.md + cli-commands.md updated. Filter parsing lives in hermes_cli/session_filters.py with unit tests; DB filters covered in tests/test_hermes_state.py. * feat(sessions): prune/archive filters for model, provider, user, chat, branch, tokens, cost, tool calls Extends the prune/archive filter surface to everything identifiable in the sessions table: - --model (substring on model slug), --provider (exact on billing_provider, case-insensitive), --user, --chat-id, --chat-type (exact), --branch (substring on git_branch), --min/--max-tokens (input+output), --min/--max-cost (USD, actual_cost_usd falling back to estimated_cost_usd), --min/--max-tool-calls. - SessionDB prune/archive/list_prune_candidates now share the filter kwargs via **filters into _prune_filter_where (unknown names raise TypeError); candidates listing + CLI preview now include the model. - Any attribute filter (except legacy --source) suppresses the implicit 90-day default so 'prune --model X' matches all ages. - Dashboard /api/sessions/prune passes the new fields through. - Docs + tests updated (7 new DB tests, 3 new parser tests).
load_gateway_config() only surfaced the top-level `multiplex_profiles` key into gw_data before calling GatewayConfig.from_dict(). A config.yaml that pinned the flag under the nested `gateway:` section -- the form written by `hermes config set gateway.multiplex_profiles true` -- was silently ignored, so the gateway loaded with multiplex_profiles=False. from_dict() already honors the nested fallback, but load_gateway_config() builds gw_data from top-level keys first, so the nested value never reached it. Read gateway.multiplex_profiles into gw_data when the top-level key is absent, mirroring the existing nested fallback for max_concurrent_sessions. Adds a load_gateway_config() regression test that writes a config.yaml with `gateway.multiplex_profiles: true` and asserts the loaded config has multiplex_profiles=True (fails without the fix). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Skills Hub 'Browse Hub' landing page and index-backed search render
empty on fresh deployments (e.g. Fly.io VPS agents) with no stale cache.
Root cause: the centralized index at /docs/api/skills-index.json is a
large body (~34MB, tens of MB compressed) served with Content-Encoding:
br. httpx's streaming Brotli decoder — backed by brotlicffi 1.2.0.1,
which is pinned so aiohttp can decode Discord attachments — trips over
its own output_buffer_limit on a payload this size and raises:
DecodingError("brotli: decoder process called with data when
'can_accept_more_data()' is False")
_load_hermes_index() catches that (DecodingError is an httpx.HTTPError
subclass) and silently falls back to the on-disk cache. On a fresh box
that cache never existed, so HermesIndexSource.is_available is False,
the index contributes 0 skills, and the hub landing page — which is
built solely from an empty-query index search — is blank. Existing
installs only appear to work because they serve a (possibly weeks-)stale
cached index instead.
Fix: request 'gzip, deflate' on the index fetch so httpx never
negotiates the broken Brotli path, and retry once with 'identity' if a
DecodingError still occurs (defends against a proxy that ignores the
header). Falls through to the stale cache only when both attempts fail.
Verified on a live staging VPS agent: index_available flips False->True
and the featured landing list repopulates from 0 to 12.
Also un-freezes already-deployed images: skills added after an image was
built (e.g. the 'unbroker' optional skill) become reachable again via
the index, which is the whole point of the centralized catalog.
…span (NousResearch#59415) Bare 'hermes sessions prune' keeps the historical 90-day default, but any filter — now including --source — suppresses the implicit cutoff, so 'prune --source cron' targets ALL cron sessions instead of silently only those older than 90 days (the surprise a user hit live: 'No sessions match ... source cron' despite plenty of recent cron runs). - CLI preview + confirmation now show the match count plus the oldest and newest matching session start times before deleting. - Dashboard /api/sessions/prune mirrors the semantics: attribute filters without an explicit older_than_days match all ages (model_fields_set distinguishes an explicit 90 from the Pydantic default); dry_run responses gain oldest_started_at/newest_started_at. - Docs + argparse help updated; tests for both surfaces.
…ousResearch#59395) Widen the NousResearch#59395 fix to the sibling site: update_job's schedule-change path (cron/jobs.py) had the SAME unguarded compute_next_run -> next_run_at pattern, so updating a job's schedule to a one-shot >ONESHOT_GRACE_SECONDS in the past would re-create the ghost job (next_run_at=None, state='scheduled', never fires) that create_job now rejects. Apply the identical guard on update (raise before any disk write, so the original job is left intact), with regression tests for the reject + future-accept cases. Also surface ONESHOT_GRACE_SECONDS in the raised ValueError (not just the warning log) so a caller knows how far in the past is too far. Message from the competing PR NousResearch#59410 by @isheng-eqi. Co-authored-by: isheng-eqi <265044697+isheng-eqi@users.noreply.github.com>
…eaming, prompt-build cache, stale budget-warning docs (NousResearch#59389) Follow-up to NousResearch#59332 targeting the remaining PERCEIVED first-token latency (the wire streaming was already per-token; these fix what the user sees): 1. display.show_reasoning default ON. On thinking models the reasoning phase streams for tens of seconds; with the display off users stare at a spinner the whole time and read it as a stall. Flipped in DEFAULT_CONFIG, load_cli_config defaults, tui_gateway raw-YAML fallbacks, and the hermes setup status line (all four read sites kept in sync). Gateway per-platform defaults intentionally stay off — messaging chats shouldn't fill with thinking text. /reasoning hide still turns it off and persists. 2. Response box force-flushes long partial lines. _emit_stream_text only painted on newline, so a response opening with a long paragraph stayed invisible until the first \n — seconds of blank box. Now partial lines wrap at terminal width and paint as tokens arrive (mirrors the reasoning box's 80-char force-flush that existed since day one). Table blocks remain batch-aligned; no content loss at wrap boundaries (regression tests added). 3. hermes_time timezone resolution uses read_raw_config (mtime-cached + libyaml C loader) instead of a raw yaml.safe_load of config.yaml (~110-140ms measured) inside the FIRST system prompt build. First build drops 320ms -> ~155ms on a 200-skill install. 4. Stale docs: configuration.md (en+zh) still documented the 70%/90% [BUDGET WARNING] tool-result injections. Those were removed in April 2026 (c8aff74) precisely because they hurt task completion; current behavior is exhaustion-message + one grace call, no mid-loop injection, no cache impact. Docs now describe reality. Verified: token-count compression decisions already use API-reported last_prompt_tokens (rough estimators are preflight-only and cost ~1.7ms even on 1.7MB histories — not worth touching).
…esume_job (NousResearch#59395) Completes the NousResearch#59395 bug-class fix. create_job and update_job's schedule-change path already reject past one-shots (via NousResearch#59410/NousResearch#59438); this closes the two remaining doors that stored next_run_at=None for a 'once' schedule and re-created the silent ghost job: 1. update_job fallback-recompute (the safety-net that re-derives next_run_at when it's missing on an enabled, non-paused job) 2. resume_job (resuming a paused one-shot whose time has already passed — empirically confirmed to create a scheduled job that never fires) The redundant update_job schedule-change hunk from the original PR was dropped (already on main via NousResearch#59438). Adds resume-reject + update-reject/ accept regression tests. Salvaged from NousResearch#59428 by isheng-eqi.
check-attribution CI fails on unmapped bare (non-noreply) contributor emails. isheng-eqi's commit email (ishengeqi@163.com) has no + so it does not auto-resolve — add the explicit mapping.
…ill (Windows hard-kill) _sync_back_once defers a SIGINT that lands mid-sync, then re-delivers it once the sync completes so the user's Ctrl+C isn't lost. It did so with os.kill(os.getpid(), signal.SIGINT). That is not graceful on Windows: os.kill only treats CTRL_C_EVENT(0)/CTRL_BREAK_EVENT(1) as console events; any other value (SIGINT == 2) routes to TerminateProcess(sig), so a Ctrl+C during a remote-backend (ssh/daytona/modal) sync-back hard-kills the whole CLI session (exit code 2) on Windows instead of raising KeyboardInterrupt. Use signal.raise_signal(signal.SIGINT) (3.8+), which invokes the restored handler through C raise() on every platform. Verified on Windows: raise_signal runs the handler (graceful) while os.kill(getpid, SIGINT) TerminateProcess-es the process. Adds a cross-platform regression test that runs on Windows too (it stubs the locked sync body, so unlike test_file_sync_back.py it needs no fcntl).
…as format_error _classify_by_status() routes every other transient HTTP status to a retryable reason (500/502 -> server_error, 503/529 -> overloaded, 429 -> rate_limit, 413 -> payload_too_large), but 408 Request Timeout fell through to the generic `400 <= status < 500` branch and was classified as a non-retryable format_error -- the same bucket as a 400 Bad Request. A 408 is a transient timing failure the server itself flags as safe to retry (RFC 9110 15.5.9), not a malformed request, so the retry loop aborted the turn when a simple retry would recover. Common trigger: a reverse proxy in front of a self-hosted backend (llama.cpp / Ollama / vLLM) returns 408 when a long generation outruns the proxy's request-read window. Route 408 to the existing FailoverReason.timeout (rebuild client + retry). Add a regression test plus a boundary test asserting 400 stays non-retryable.
…timeout shape, never auto-compress, falsification guard Folded from PR NousResearch#56932 (@allenliang2022) — same fix as NousResearch#56909, submitted 45min later; the test coverage was the richer half.
Salvaged from NousResearch#40430; re-verified on main, tightened, tested. Co-authored-by: xuezhaolan <xuezhaolan@users.noreply.github.com>
The Firecrawl provider used os.getenv() to read FIRECRAWL_API_KEY and FIRECRAWL_API_URL, which only checks the process environment. When values are supplied through Hermes's ~/.hermes/.env config mechanism (via hermes_cli.config.get_env_value), they are not guaranteed to be present in os.environ for every gateway/tool execution path. Switch to get_env_value() which checks both os.environ and the .env file, matching the pattern used by other providers (nous_subscription, setup, discord adapter). Fixes NousResearch#40190
…ave-free providers Same bug class as NousResearch#40190: these providers read credentials via bare os.getenv(), so keys stored in ~/.hermes/.env (hermes config layer) were invisible in execution paths that never exported them into the process environment. Add get_provider_env() on the WebSearchProvider module as the shared config-aware lookup (get_env_value with os.getenv fallback) and route all credential reads through it. SearXNG already did this (NousResearch#34290); Firecrawl fixed in the preceding cherry-picked commit by @liuhao1024.
build_preloaded_skills_prompt() (hermes -s <skill>, and tui_gateway's HERMES_TUI_SKILLS deployment env var) loads skills via _load_skill_payload() with a raw identifier, bypassing get_skill_commands()' scan-time disabled filter entirely. Result: a skill an operator disabled via skills.disabled still gets force-loaded and injected into every session — including every session on a shared tui_gateway deployment where the operator set HERMES_TUI_SKILLS. The bundle-invocation path (NousResearch#59156) already re-checks get_disabled_skill_names() for exactly this reason; preloaded-skill loading was the other _load_skill_payload call site still missing it. Fix: check each resolved skill's name (and raw identifier) against get_disabled_skill_names() before injecting it. A disabled skill is now reported the same way an unknown one already is (skipped, listed in the returned missing_identifiers) — no return-shape or caller changes needed. No behavior change when no skill is disabled.
…usResearch#59314) The CLI model-switch display (both picker and direct-switch paths) omitted the custom_providers keyword when calling resolve_display_context_length(). The function already supports it (and the gateway correctly passes it), but the CLI call sites relied on the fallthrough to probe-down default (256K) even when a custom_providers entry specified a per-model context_length. Fix: pass agent._custom_providers at both resolve_display_context_length call sites in HermesCLI._apply_model_switch_result(), matching the pattern already used for config_context_length.
…NousResearch#59322) The CWE-22 traversal guard in SessionEntry.from_dict rejects any interior '/' in session_key, but session_key is a logical routing key (never used as a filesystem path) and Google Chat resource names legitimately contain '/' (spaces/<id>, spaces/<id>/threads/<id>). All Google Chat sessions were silently dropped on gateway start. Split the validation: session_id keeps the strict _is_path_unsafe guard (it's the value used as a filename); session_key now uses a relaxed _is_session_key_unsafe helper that only blocks genuine traversal vectors (parent-dir '..', leading '/', leading '\', leading Windows drive-letter prefix) and allows interior '/'.
Dashboard /chat for the default (launch) profile attaches to the dashboard process's in-memory TUI gateway. The Node PTY child receives a bridged TERMINAL_CWD env var, but the in-memory gateway process does not, so cwd resolution fell through to os.getcwd() (wherever `hermes dashboard` was launched) and ignored the configured terminal.cwd. Read the launch profile's config.yaml directly in the in-memory cwd resolution: a configured terminal.cwd now wins over a stale process env and the launch directory. Widened to the resume/fallback session-cwd sites (not just _completion_cwd) via a shared _default_session_cwd() helper so fresh AND resumed sessions honor the config. Co-authored-by: ygd58 <buraysandro9@gmail.com>
…t schedulers When two scheduler processes (gateway + desktop) run concurrently, both could pick up the same one-shot job from get_due_jobs() because its next_run_at was not advanced before execution started — only recurring jobs were advanced (L3446). This caused duplicate deliveries and wasted token spend (NousResearch#59229). Now _get_due_jobs_locked advances a one-shot's next_run_at by 60s before returning it as due, persisted immediately under the same file lock. mark_job_run re-anchors next_run_at on completion, so a tick death between advance and execution only delays the job by one tick window — it is never lost. Closes NousResearch#59229
…vance The +60s next_run_at advance only delayed a duplicate one-shot dispatch by one tick — a job that outlives the 60s tick interval (the reported 2.5-min research prompt) still re-fired on the next tick after the window expired, so the concurrent gateway+desktop double-delivery persisted. Replace it with a durable run_claim (at+by, mirroring fire_claim) stamped on the one-shot under the same jobs lock get_due_jobs holds, and checked at the top of the due-scan: a fresh claim held by an in-flight run makes every other scheduler process skip the job for its ENTIRE run, not one tick. mark_job_run() clears the claim on completion; a ONESHOT_RUN_CLAIM_TTL (30 min) safety valve re-dispatches a claim left by a tick that died mid-run so a one-shot is never wedged. E2E: long-running one-shot no longer double-fires at +28/+61/+120/+179s; completion clears the claim + disables the job; crash recovery re-arms past the TTL. +3 regression tests.
Allow mainstream reverse-proxy path mounts to keep their X-Forwarded-Prefix when Home Assistant Supervisor ingress already consumes nearly the old 64-character budget. Keep validation bounded and keep rejected non-empty prefixes diagnosable with a deduplicated warning. Constraint: HA Supervisor ingress prefixes are 63 chars before add-on subpaths, so the old 64-char cap dropped valid dashboard deployments. Rejected: remove the length cap entirely | a bounded header budget is still a conservative validation guard. Confidence: high Scope-risk: narrow Directive: Keep prefix validation centralized in hermes_cli.dashboard_auth.prefix so auth routes, cookies, and SPA asset rewriting agree. Tested: python probe for the 73-char HA ingress prefix; scripts/run_tests.sh tests/hermes_cli/test_dashboard_auth_prefix.py -q; .venv/bin/python -m pytest tests/hermes_cli/test_web_server.py -k 'spa_assets_are_read_as_utf8' -q; python -m ruff check hermes_cli/dashboard_auth/prefix.py tests/hermes_cli/test_dashboard_auth_prefix.py; git diff --check Not-tested: full test suite
Server names with non-env-safe characters (dots, slashes, spaces)
produced invalid env-var keys like MCP_MY.SERVER_API_KEY or
MCP_GITHUB/MCP_API_KEY, breaking .env writes and ${VAR} header
substitution. _env_key_for_server now replaces any character outside
[A-Za-z0-9_] with an underscore.
Co-authored-by: Hermes Agent <agent@nousresearch.com>
…Windows footgun linter signal.SIGKILL / os.killpg don't exist on Windows. The watchdog is only spawned on POSIX (wrap site gates on os.name), but guard via getattr with a plain terminate/kill fallback so an accidental Windows import can't AttributeError.
…usResearch#60537) /api/status (loopback/insecure binds only) now includes: - profiles: every profile on the host (default + named) - gateway_mode: none | single | multiple | multiplex - gateways: one entry per live gateway with the host ports its port-binding platforms listen on, plus served_profiles when the default gateway is multiplexing Ports resolve from each profile's config.yaml (top-level platforms: wins over gateway.platforms:, matching load_gateway_config precedence) with adapter defaults as fallback. Topology enumeration runs in an executor so the profile scan + process-table probes stay off the event loop, and the whole block is gated behind the same loopback-only split as hermes_home/gateway_pid so gated binds leak nothing new.
…xplicitly configured (NousResearch#35527) When a user explicitly configures a platform with its native composite (e.g. platform_toolsets.discord: [hermes-discord]), the discord and discord_admin toolsets were silently stripped by _DEFAULT_OFF_TOOLSETS even though the composite contains those tools. The strip could not tell an explicit composite opt-in apart from the unconfigured default. Track whether the platform was explicitly configured and, when it was, exempt toolsets that are both default-off and platform-restricted to the current platform from the strip. Only discord/discord_admin are affected (the sole entries in both _DEFAULT_OFF_TOOLSETS and _TOOLSET_PLATFORM_RESTRICTIONS). Unconfigured and empty-list platforms keep the security default-off behaviour.
…earch#60554) Restructures the five parallel export sections into a single 'Export Sessions' section: a format table (jsonl/md/qmd/html/trace + --only user-prompts), one shared-filters paragraph covering all formats, and per-format subsections nested beneath. EN + zh-Hans.
Log a one-shot structured warning when Discord denies traffic because no allowlist/policy is configured, and correct the setup wizard's inverted warning text. The fail-closed default itself is unchanged. Fixes NousResearch#58682.
Docs portion of PR NousResearch#57067: 'bot connects but never replies' section pointing at the gateway.log warning and the allowlist/policy knobs. Co-authored-by: ooovenenoso <120500656+ooovenenoso@users.noreply.github.com>
…tforms (NousResearch#60574) Session-based channel discovery resurrected historical origins for platforms with no connected adapter, exposing stale send_message targets that can no longer deliver. Gate both the enum loop and the plugin-registry loop on the live adapter set. Surgical reapply of the channel-directory portion of PR NousResearch#25959 (branch was 6.5k commits stale; the text-batching delay changes bundled there were dropped - separate concern, defaults have since been retuned on main). Co-authored-by: Marco-Olivier Lavoie <marcolivier@gmail.com>
…earch#60576) Headless/hosted deploys run the dashboard server without COLORTERM in the process environment, so chalk inside the PTY-spawned TUI child downgraded every skin hex color to the xterm 256 palette — the default skin's bronze banner border (#CD7F32) snapped to palette 173 (#D7875F, salmon red) and the gold caduceus rendered red/yellow on fresh cloud instances. Local launches never reproduced it because the operator's interactive terminal leaks COLORTERM=truecolor into the server env. xterm.js always renders 24-bit RGB, so the dashboard PTY child should always advertise truecolor: backfill COLORTERM=truecolor in _resolve_chat_argv via setdefault (an explicit operator value wins). Verified with a clean-env PTY probe of the real TUI binary: no COLORTERM -> 0 truecolor SGRs / 165 palette-256 (salmon 38;5;173); with the backfill -> 166 truecolor SGRs, exact bronze 38;2;205;127;50.
…atus (NousResearch#60585) The profile+gateway topology added in NousResearch#60537 sits entirely behind the loopback/--insecure auth gate. But a hosted agent (Hermes Cloud) binds non-loopback with OAuth, so should_require_auth is True, and NAS reads /api/status over the network (fly-provider.ts getInstanceRuntimeStatus) with no session token. On that gated path the whole topology block was omitted, so the Portal could never render the profile list. Split the topology readout by sensitivity: - profile NAMES (profiles) + gateway_mode are low-sensitivity product surface and now ride the always-public status body, surviving the auth gate so NAS/the Portal can enumerate profiles. - the per-gateway detail (gateways[], carrying host ports) is deployment recon and stays gated alongside hermes_home / config_path / env_path / gateway_pid / gateway_health_url. The collector now runs unconditionally (still in the executor, off the event loop). No new fields; only the gate placement changes.
…sResearch#60586) The multiplex machinery already routes an inbound message to a profile via SessionSource.profile (build_session_key namespacing + the per-turn config/credential scope in SessionStore._resolve_profile_for_key). But the relay path never populated it: _event_from_wire rebuilt the SessionSource field-by-field and dropped any 'profile' the connector sent, so a Team-Gateway (connector + relay) message could not be routed to a specific profile the way the /p/<profile>/ HTTP prefix and per-credential polling adapters already can. Stamp source.profile from the wire payload in _event_from_wire. This is the last missing link for NAS-driven per-profile routing over the relay in multiplex mode; the connector populating the field ships separately (gateway-gateway contract adds the optional wire field). Back-compat: absent 'profile' → None → legacy agent:main namespace, byte-identical to today for every single-profile gateway.
…p commands
Strict charset allowlist (alnum + - _, max 64) on the {name} path param of
the memory-provider config/setup endpoints. Prevents traversal-shaped names
from reaching find_provider_dir(), and setup now 404s when neither a
loadable provider nor a plugin manifest exists, so the command-running path
is only reachable for discoverable plugins. Adds regression tests.
…flag (NousResearch#60589) The connector now depends on the single multiplexed gateway for per-profile relay routing, so hosted deployments need to FORCE multiplexing on regardless of the image's config.yaml. gateway.multiplex_profiles was config.yaml-only, which a user could leave unset or flip off. Add GATEWAY_MULTIPLEX_PROFILES as a standard operator override on top of the existing config key — the same 'config.yaml is canonical, env is the operator override' pattern the Telegram/Signal require_mention bridges use: env (recognized token) > config.yaml (top-level or nested gateway.*) > False - gateway/config.py: _env_multiplex_profiles_override() resolves the env var tri-state — recognized truthy/falsy token → bool; unset/blank/unrecognized → None (fall through to config). Blank is deliberately None, not False, so a provisioned-but-unpopulated Fly secret ('') can't shadow a config.yaml opt-in (the empty-secret trap). Wired into GatewayConfig.from_dict so every consumer (run.py, session.py via self.config) sees the resolved value. - hermes_cli/gateway.py: the named-profile-start guard (_guard_named_profile_under_multiplexer) reads config.yaml directly, so it gets the SAME env precedence — otherwise env-forced multiplex would leave the guard blind and someone could start a conflicting per-profile gateway that double-binds a bot token. Env-forced-on trips the guard even with no config.yaml key; env-forced-off disables it over a config opt-in. Tests: full 3-tier precedence in test_config.py (incl. the discriminating env-overrides-config cases + the empty/whitespace/unrecognized fall-through trap + resolver tri-state), mutation-verified (flipping precedence fails exactly the two env-wins tests); guard env cases in test_multiplex_lifecycle.py. Force-on is safe on a single-profile instance: session keys stay byte-identical (agent:main) and the _run_agent wrapper installs the per-turn secret scope, so the fail-closed get_secret() path is satisfied.
…ashboard chat PTY resume The chat PTY launch path landed on main after PR NousResearch#50558 and still called _session_latest_descendant() with the old one-arg signature. Open the requested profile's state DB (matching the REST endpoint) so profile-scoped resume resolves descendants in the right database.
NousResearch#60643) The April 2026 pin to WhiskeySockets/Baileys#01047deb existed only to pick up the abprops bad-request fix (Baileys PR NousResearch#2473) before it was released. That fix shipped in v7.0.0-rc11 (May 2026); our pinned commit is now 48 commits behind rc13. The git pin forced npm to clone the repo and compile Baileys from TypeScript source on every fresh install (~3 min), which blew past the dashboard pairing flow's timeout. Registry install takes ~3s. Validation: all 9 bridge.js imports present in rc13, bridge.native.test.mjs passes (13/13), live bridge boot renders pairing QR against real WA servers.
Merge the scoped v2026.7.7.2 maintenance hotfix after local regression tests and full GitHub Python/lint/security CI passed. The remaining contributor-attribution check is administrative-only on this fork.
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
Verification
scripts/run_tests.sh tests/hermes_cli/test_kanban_native_review.py tests/hermes_cli/test_kanban_blocked_sticky.py(9 passed)python -m py_compile hermes_cli/kanban_db.pygit diff --checkScope note
This scoped implementation establishes the DB-native transition, writer/reviewer boundary, tests, skill, and docs. CLI/tool submission wiring and feature-flag/shadow controller remain follow-up work.