Custom providers that have a curated models: list in config.yaml
(e.g. ZenMux gateways) should show ONLY those configured models in
the picker dropdown, not the full /v1/models catalog.
Before this fix, _named_custom_groups unconditionally called
_read_custom_endpoint_models() which would pull hundreds of models
from aggregator gateways and overwrite the user's curated list.
Now the build checks if the custom_provider entry has a non-empty
models dict/list in config.yaml — if so, it skips the live fetch
and uses only the configured models (same behavior as hermes-agent
model_switch.py Section 4 patch).
Closes: configure-model-list-should-be-authoritative
The _normalized_message_timestamp_for_key helper was preserving
microsecond precision (%.6f). When the same message is persisted by
both the WebUI sidecar JSON writer and the Hermes agent state.db
writer, their timestamps can differ by a few microseconds, causing
_session_message_merge_key to produce different keys for the same
logical message and letting both copies survive the dedup pass in
merge_session_messages_append_only.
Truncating to second-level granularity collapses sub-second drift to
the same key, so the duplicate is suppressed correctly.
Fixes#2616
Drop the redundant 'if gw_data else []' guard — gw_data is already
guaranteed to be a dict by the 'or {}' fallback above.
Add a one-line comment explaining the peek-without-pop race window:
a concurrent resolver may pop a different gateway entry, but
approve_session is idempotent over the session key set so the
outcome is the same regardless.
During active streaming, dangerous-command approvals go through the
gateway path and are stored in _gateway_queues as _ApprovalEntry
objects, not in _pending. The _resolve_approval_legacy helper only
looked at _pending, so 'Allow for this session' never called
approve_session() — the user clicked Allow, the card vanished, but
the next dangerous command asked again.
Now when _pending has no matching entry, the helper peeks into
_gateway_queues to extract pattern_keys, calls approve_session(),
and marks found_target=True so resolve_gateway_approval also fires.
This commit is re-scoped to peek-only (no agent_session_key round-trip,
no state_db metadata changes).
Includes:
- Import + fallback for _gateway_queues
- Null-safe key filtering in all_keys
- Source-contract test (static) + functional test with
@requires_agent_modules skip marker for CI
- All comments and docstrings in English
Per reviewer note: because the zip streams straight into handler.wfile
(no io.BytesIO buffering), peak memory is bounded by zipfile's per-file
read buffer, not the HERMES_WEBUI_FOLDER_ZIP_MAX_MB cap. Adds a comment
so the next reader doesn't have to trace it to learn the cap's actual
shape.