- load_gateway_config (536 LOC) is now a thin orchestrator: legacy gateway.json
-> config_loader.load_yaml_layer -> GatewayConfig.from_dict -> env overrides
-> validation. The yaml phase lives in gateway/config_loader.py as small
functions driven by tables: _TOPLEVEL_BRIDGE (23 top-level/nested
gateway.<key> bridges with 5 fallback modes), _SHARED_KEYS (28 per-platform
keys copied into extra, with per-platform restrictions and transforms) and
_PORT_BRIDGE_KEYS. Logger name kept as "gateway.config".
- GatewayConfig.from_dict: shared pick()/key_label() helpers replace the
repeated "top-level key present else nested gateway.<key>" blocks; warning
order preserved.
- _normalize_unauthorized_dm_behavior / _normalize_notice_delivery unified into
_normalize_choice; _ensure_platform_extra_dict -> _dict_slot (also used by
persist_home_channel); _getenv_int removed (dead since the env pass moved to
config_env, which has its own _int_or).
- Platform._missing_: one _add_pseudo_member helper for both branches.
- Single warning sites in _coerce_optional_positive_int and
coerce_systemd_watchdog_seconds; _validate_gateway_config placeholder pass
flattened; small to_dict/getter collapses. Ruff F401/SIM102 clean.
- tests/hermes_cli/test_config_read_guard.py: allowlist gateway/config_loader.py
(same owner as gateway/config.py — the extracted load_gateway_config phase).
gateway/config.py 2684 -> 1319 LOC (-50.9%); largest function now
GatewayConfig.from_dict at 119 LOC. Resolved-config parity: 160 cells
(16 yaml fixtures x 10 env sets) byte-identical to the integration base,
including captured log records and stderr.
Byte-identical bodies moved out of hermes_cli/kanban_db.py into four sibling
modules, re-exported from the origin so kanban_db.<name> keeps resolving and
stays the single monkeypatch target; origin-resident helpers are reached via a
late-bound _kb namespace. AST-identity verified for all 329 moved symbols; SQL
statement multiset parity vs base. Three source-inspection tests repointed at
kanban_db_dispatch; the _add_column_if_missing alias test now imports the real
owner (hermes_cli.sqlite_util).
cli_model_switch_mixin: the identical snapshot->stage->agent.switch_model->rollback
block in _apply_model_switch_result and _confirm_and_apply_cli_model_switch (2x47
lines) -> _stage_and_swap_model(result, old_model) -> bool. cli_loops_mixin: the
identical reversed-history assistant-text extraction in the loop-tick and goal
post-turn hooks -> _last_assistant_response_text(). Four _StubCLI test doubles bind
the new helper to the real implementation.
Conflict in hermes_cli/model_setup_flows.py: took simp/cli-models's
simplified version and re-applied simp/cli-main's 2-line change (import
_current_reasoning_effort/_set_reasoning_effort from hermes_cli.setup
instead of hermes_cli.main, since cli-main removed the main.py copies).
simp/cli-models had dropped those two helpers from hermes_cli.setup.py as
dead (their only base caller was main.py's own copy); restored them there
so the cli-main import path resolves.
hermes_cli/plugins.py (7193 -> 4961):
- _register_scoped_provider: one body for the 8 scope-keyed register_*_provider methods;
_track_callback/_track_mapping_entry unify manager-mapping lease tracking (4 sites);
_track_scoped_registration for the ownership ledger. Public method names, signatures,
return values and warning strings unchanged.
- Manifest v2 type checks table-driven (_manifest_field_of_type); _unload_scoped split into
_unload_target_keys + _reset_after_unload_all; _gate_manifest/_record_placeholder out of
_discover_and_load_inner; _track_tool_override_policy/_attribute_registrations out of
_load_plugin_scoped; bounded hook worker lifted into _run_hook_callback_bounded;
prompt-section rendering extracted; resolve_pre_tool_block delegates to
_dispatch_pre_tool_call_hooks; _evict_modules (3 sites); _remove_name_if_unowned and
_nowait_plugin_set unify unowned-name cleanup and *_nowait probes.
- Dead (zero references outside their own test): unload_plugins, has_portable_mcp_servers,
_classify_entrypoint_kind, _reset_event_bus, get_plugin_subscriptions,
get_telegram_handler_factories, pass-through _restore_* helpers; _env_enabled is now an alias
of utils.env_var_enabled (plugins/memory still imports it).
- Docstrings/comments compacted; contract sentences and rationale kept (multi-profile ledger
keying, persistent auth-provider registration, capability declaration is not a grant).
- Drop dead code: _telegram_effective_priority, _prioritize_telegram_menu_commands,
discord_skill_commands, _TG_NAME_LIMIT/_clamp_telegram_names compat aliases,
the empty _SLACK_PRIORITY_ALIASES pinning pass (and tests that only pinned them).
- Unify the Telegram and Discord skill collectors behind _iter_gateway_skills
(one eligibility/root-matching implementation) and _truncate_desc.
- Table-drive Telegram menu priority modes (_TELEGRAM_PRIORITY_TIERS) and the
completer's per-command dynamic completions (_DYNAMIC_COMPLETIONS).
- Unify path and @file:/@folder: directory-listing completions (_dir_completions),
command-completion construction (_short_desc), /tools candidate rows, the
Slack canonical/alias passes and the derived COMMANDS/COMMANDS_BY_CATEGORY loops.
- Compact docstrings/comments, keeping every rule, invariant and rationale.
Behavior-neutral: registry-derived outputs, menus, manifests and completions
byte-identical against origin/main on a fixture sweep.
moa, fallback, worktree, browser, secrets, egress, migrate, whatsapp-cloud,
checkpoints, bundles, curator, pets, journey, computer-use, sessions and
completion each become a build_<group>_parser() builder. Closure handlers
that only closed over their own parser moved verbatim; sessions/completion
take the handler by injection. --help/usage/defaults byte-identical for all
399 parsers in the tree (in-process dump before/after).
model_setup_flows.py (3313 -> 2848):
- _load_config_model_section, _begin/_commit_model_config, _ensure_flow_api_key,
_pick_model_or_prompt, _run_login, _models_dev_merged, _copilot_model_list,
_show_curated replace ~15 copies of config-save / api-key / picker boilerplate.
- _gemini_tier_ok and _api_key_provider_model_list lift the two inline blocks
out of _model_flow_api_key_provider; five-way provider branch -> early returns.
- Comments compacted, keeping every rationale (Bedrock geo routing, key_env
hygiene, discover_models semantics, Nous free/paid partition, etc.).
Also drops two tests that only asserted the existence of setup.py helpers
removed in the next commit.
Follow-up to the off-loop move: once the credential-pool handlers run on
worker threads, the dashboard's periodic /api/credentials/pool polls can
overlap, and during a DNS outage each poll would have started its own
exchange and abandoned its own hung resolver thread.
- Per-fingerprint threading.Lock around the exchange: concurrent callers
wait on the one in-flight attempt, then hit the positive or negative
cache (bounded worker count, no duplicate network calls).
- _urlopen_bounded: when the hard cap fires and the abandoned worker later
succeeds, close the HTTPResponse instead of leaking the socket.
- Tests (none shipped with the original PR): hard cap + late-close,
single-flight success and failure paths, and the pool endpoint running
off-loop / keeping the loop responsive under a 200 ms blocking read.