The Hermes Agent's state-module refactor (agent commit 53db597201, released
v2026.9.7) renamed SessionDB.set_auto_title_if_empty -> set_auto_title(
session_id, title, *, source) and (agent commit 14791b4d4e) dropped the
tools.approval._ApprovalEntry back-compat facade (class moved, unchanged, to
tools.approval_gateway_wait).
Production impact (real, silent): api.state_sync.sync_session_title called the
removed set_auto_title_if_empty, whose AttributeError was swallowed by the
surrounding `except Exception: logger.debug(...)`, so WebUI auto-generated
session titles stopped persisting to state.db on agent >= v2026.9.7 — hermes
sessions list went blank again (the exact #6892 regression). Now calls
set_auto_title with LLM provenance, which preserves the identical semantics
(only fills NULL/lower-authority title, returns False untouched on a manual
rename, ValueError -> get_next_title_in_lineage #N de-dup).
Test fixes (agent-coupled; skip on GitHub CI which installs no agent, so they
only surface on an agent-equipped box like our gate):
1. tools.approval._ApprovalEntry -> idempotent autouse conftest compat shim
backfilling the class from its post-split location only when the installed
agent lacks the dropped facade. No production code imports it.
2. test_goal_command_webui::test_profile_goal_falls_back_when_session_db_path_is_frozen
was missing the monkeypatch.syspath_prepend(...) that every sibling test in
the file has, so its bare `import hermes_state` failed under full-suite
ordering after a predecessor stripped the agent dir from sys.path. Added it.
Verified: authoritative 5-shard CI-equivalent split all green (0 failed);
each failure proven pre-existing on clean master (not this diff).
Address gate-review blockers from nesquena-hermes:
1. Wrong title-authority API: replace `db.set_session_title()` with
`db.set_auto_title_if_empty()` so auto-generated titles never
overwrite a manual rename (set_session_title writes with 'user'
authority; set_auto_title_if_empty only populates NULL titles).
2. Null profile fallback: pass `or 'default'` at both streaming.py
call sites so background title workers (which lack request TLS)
always resolve to the correct state.db instead of falling through
to the process-global active profile.
Background title generation (`_run_background_title_update` and
`_run_background_title_refresh`) writes the title to the WebUI sidecar
JSON but never syncs it back to hermes-agent's `state.db`. This causes
`hermes sessions list` to show blank titles for all WebUI-originated
sessions.
The existing `sync_session_usage` path is gated by the `sync_to_insights`
setting (default: off) and executes before the background title thread
completes, so it cannot cover this case.
Fix: add `sync_session_title()` to `state_sync.py` — an unconditional
(not gated by `sync_to_insights`) title-only sync — and call it from both
title generation paths immediately after persisting the title to the
sidecar.
Fixes type annotations where non-optional types (str, int) are used with
None defaults. All affected parameters now use Optional[T] = None.
Closes#3322
* fix: sync message_count to state.db for /insights (#163)
sync_session_usage() didn't write message_count to state.db, so
/insights showed 0 messages for all WebUI sessions even with
sync_to_insights enabled.
Added message_count parameter to sync_session_usage() and pass
len(s.messages) from both the streaming and non-streaming chat paths.
Fixes#163
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use callable pattern for _execute_write in sync_session_usage
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three bugs found during review:
1. Class is SessionDB not HermesState -- would silently no-op on every install
2. SessionDB.__init__ takes Path not str -- would crash with AttributeError
3. _execute_write() takes a callable not SQL+params -- wrong signature.
Replaced with public set_session_title() API.
4. Each call opened a persistent SQLite connection and never closed it.
Added try/finally db.close() to prevent WAL leak under sustained load.
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
WebUI sessions were invisible to 'hermes /insights' because the WebUI
bypasses the gateway and calls AIAgent.run_conversation() directly,
never writing to state.db.
New 'Sync usage to /insights' setting (default: off) that mirrors
WebUI session metadata (tokens, cost, model, title) into state.db
after each turn. Uses absolute token counts to avoid double-counting.
Components:
- api/state_sync.py: bridge module with sync_session_start() and
sync_session_usage(). Uses ensure_session() (idempotent) and
update_token_counts(absolute=True). All wrapped in try/except.
- api/config.py: new 'sync_to_insights' boolean setting
- api/streaming.py: calls sync_session_usage() after s.save()
- api/routes.py: same for the non-streaming chat path
- Settings UI: checkbox toggle with description
Default off because:
- Writing to state.db while CLI/gateway also writes could cause
WAL lock contention on busy systems
- Some users may not want WebUI sessions in /insights stats
Closes#92
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>