Skip to content

feat: durable native Kanban review handoff - #2

Merged
larry-hermes-bot merged 1678 commits into
mainfrom
wt/native-kanban-autoreview
Jul 18, 2026
Merged

feat: durable native Kanban review handoff#2
larry-hermes-bot merged 1678 commits into
mainfrom
wt/native-kanban-autoreview

Conversation

@larry-hermes-bot

Copy link
Copy Markdown
Collaborator

Summary

  • add durable writer-to-native-review transition with separate reviewer ownership
  • add reviewer revise-to-original-writer transition and native review regression coverage
  • add canonical sdlc-review skill and retire docs for review-required blocked convention

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.py
  • git diff --check

Scope 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.

teknium1 and others added 30 commits July 5, 2026 21:48
…_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>
teknium1 and others added 28 commits July 7, 2026 15:16
…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.
@larry-hermes-bot
larry-hermes-bot merged commit cb484b6 into main Jul 18, 2026
36 checks passed
@larry-hermes-bot
larry-hermes-bot deleted the wt/native-kanban-autoreview branch July 18, 2026 15:03
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.