Pre-existing failure on master: the test assumed only the broken-proxy /v1/models call would route through fake_urlopen, but get_available_models() also probes the Copilot internal v2 token endpoint (10.0s timeout) and OpenRouter free-tier discovery (8.0s), which now pollute the recorded timeout list.
Fix narrows the recorder to the broken-proxy endpoint only. The contract being pinned (broken-proxy probe uses CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS, not the urllib default 10) is unchanged.
Caught by stage-batch33 sequential pytest gate.
PR #3023 only updated Session.load() and Session.load_metadata_only(), leaving
three sibling validators (Session-internal _repair_stale_pending and the
/api/session/worktree/remove + /api/session/delete route handlers) still
gated on the old lowercase-only character set. That would have shipped a
confusing UX where api-* and reachy-voice-* sessions could be loaded into
the sidebar but rejected with HTTP 400 on delete or worktree removal.
This commit factors the validation into a single is_safe_session_id helper
in api.models and updates all five call sites to use it. Adds regression
coverage in tests/test_issue3023_safe_session_id_validators.py for both
the helper itself and a repo-wide guarantee that no narrow lowercase-only
magic string survives.
Closes the follow-up flagged by the parallel reviewer agent on #3023.
The .usage.json file is owned by hermes-agent (tools/skill_usage.py).
This change removes the webui-side increment logic to avoid:
1. File ownership conflict - both writing to same file
2. Schema mismatch - agent uses ISO strings, webui used floats
3. Concurrency issues - agent uses fcntl locks, webui had no locking
4. Double-counting - agent already increments counters server-side
Changes:
- api/skill_usage.py: keep only read_skill_usage(), remove increment functions
- api/streaming.py: remove skill usage counter hook
- api/routes.py: adapt response to pass through agent's format as-is,
with defensive coercion for None values and metadata preservation
- tests/test_skill_usage.py: remove increment tests (17→7 cases)
Use _merged_session_messages_for_display for is_messaging_session even in the
metadata-only (messages=0) path. This ensures message_count and last_message_at
match the full load path for Telegram / external messaging sessions that have
stitched or duplicate rows in state.db + sidecar.
Prevents spurious refresh loops, scroll resets, and open panel closures when
resuming cross-surface sessions in the WebUI.
No impact on CLI, non-messaging, or full-message paths. All 580 session tests pass.
Fixes the root cause identified in the SessionDB / render interaction changes.