Summary
Show Agent Sessions currently merges Hermes Agent / CLI / Gateway rows from state.db into the WebUI sidebar as flat session entries.
That works for simple sessions, but Hermes Agent session rows can have different relationship semantics:
- empty/unimportable rows that have a
sessions record but no messages
- compression continuation chains, where one long user conversation is split into multiple session segments linked by
parent_session_id
- subagent delegation children, where a parent task intentionally spawns child sessions that should remain visible as a tree
Today the WebUI bridge does not distinguish these cases. It treats every non-webui row as a standalone conversation, which can make the sidebar confusing or broken when Show Agent Sessions is enabled.
Observed example
With Show Agent Sessions enabled, a Weixin conversation produced many sidebar entries like:
Magazine Style PPT Skill #91
Magazine Style PPT Skill #88
Magazine Style PPT Skill #76
Inspecting the local Hermes Agent state.db showed this was not simply duplicate data. These rows form a continuation chain through parent_session_id:
Magazine Style PPT Skill
-> Magazine Style PPT Skill #2
-> ...
-> Magazine Style PPT Skill #92 / #94
Many intermediate rows have end_reason = 'compression'. From the user's point of view this is one ongoing messaging conversation, but the WebUI sidebar exposes the backend compression segments as separate sessions.
There were also some empty rows with no message records. Those appear in the sidebar but cannot be imported, causing Session not found in CLI store when clicked.
Why this is not Weixin-specific
This surfaced with Weixin because that is where I could reproduce it locally, but the underlying model is shared by Hermes Agent:
hermes_state.py stores parent_session_id and explicitly supports compression-triggered session splitting.
gateway.session.build_session_key() is the shared gateway session key mechanism used by platforms such as Telegram, Weixin, Discord, Matrix, etc.
- Hermes Agent's own
SessionDB.list_sessions_rich() defaults to include_children=False and project_compression_tips=True, which treats compression continuations as one logical conversation projected to the latest tip.
The WebUI bridge currently bypasses that logical projection and directly queries raw sessions rows.
Current WebUI paths affected
Two WebUI paths need to stay in sync:
-
/api/sessions via api.models.get_cli_sessions()
- currently reads raw non-
webui rows from state.db
- applies
LIMIT 200 before any logical conversation projection
-
Gateway SSE watcher via api.gateway_watcher._get_agent_sessions_from_db()
- also reads raw non-
webui rows
- can reintroduce the same flattened list during live refresh even if
/api/sessions is fixed separately
Related issues / PRs
Proposed implementation plan
I think this should be split into two PRs for this issue.
PR 1: Hide empty/unimportable agent session rows
Scope:
- Filter out agent sessions that have no readable message rows.
- Prevent broken sidebar entries that fail during
/api/session/import_cli.
- Keep this narrow: do not collapse
parent_session_id chains yet.
Expected behavior:
- A
sessions row with source != 'webui' but zero messages rows should not appear in the sidebar.
- Existing valid agent sessions should continue to appear.
Suggested tests:
- Add a regression test where a non-
webui session row exists with no messages and verify /api/sessions does not return it.
This PR should reference this issue but should not close it.
PR 2: Collapse compression continuation chains into logical conversations
Scope:
- Use
parent_session_id and end_reason = 'compression' to identify continuation chains.
- Display one sidebar item per logical compression chain, projected to the latest non-empty tip.
- Apply the same projection in both:
api.models.get_cli_sessions()
api.gateway_watcher._get_agent_sessions_from_db()
- Apply the final list limit after filtering/collapsing, not before.
- Keep import/open behavior pointed at the latest real session id; do not create a fake merged WebUI session.
Expected behavior:
- A long Weixin/Telegram/Cron/CLI conversation split by context compression should show as one logical sidebar entry.
- Clicking the entry imports/opens the latest real continuation segment.
- Empty intermediate continuation rows are skipped.
- Independent sessions remain independent.
Suggested tests:
- A compression chain with multiple non-empty children returns only the latest non-empty tip.
- A chain with empty intermediate nodes still resolves to the latest non-empty usable node.
- Unrelated sessions are not collapsed together.
- The gateway watcher emits the same logical session shape as
/api/sessions.
This PR can close this issue if the issue scope remains limited to empty rows + compression continuation projection.
Explicit non-goals for this issue
Product principle
Show Agent Sessions should expose useful agent conversations, not raw backend storage rows.
Different parent_session_id relationships need different UI treatment:
Suggested triage metadata
suggested metadata:
- Labels:
bug, cli-parity, gateway, ux
- Milestone:
Milestone 1 — Core quality & parity
Summary
Show Agent Sessionscurrently merges Hermes Agent / CLI / Gateway rows fromstate.dbinto the WebUI sidebar as flat session entries.That works for simple sessions, but Hermes Agent session rows can have different relationship semantics:
sessionsrecord but nomessagesparent_session_idToday the WebUI bridge does not distinguish these cases. It treats every non-
webuirow as a standalone conversation, which can make the sidebar confusing or broken whenShow Agent Sessionsis enabled.Observed example
With
Show Agent Sessionsenabled, a Weixin conversation produced many sidebar entries like:Magazine Style PPT Skill #91Magazine Style PPT Skill #88Magazine Style PPT Skill #76Inspecting the local Hermes Agent
state.dbshowed this was not simply duplicate data. These rows form a continuation chain throughparent_session_id:Many intermediate rows have
end_reason = 'compression'. From the user's point of view this is one ongoing messaging conversation, but the WebUI sidebar exposes the backend compression segments as separate sessions.There were also some empty rows with no message records. Those appear in the sidebar but cannot be imported, causing
Session not found in CLI storewhen clicked.Why this is not Weixin-specific
This surfaced with Weixin because that is where I could reproduce it locally, but the underlying model is shared by Hermes Agent:
hermes_state.pystoresparent_session_idand explicitly supports compression-triggered session splitting.gateway.session.build_session_key()is the shared gateway session key mechanism used by platforms such as Telegram, Weixin, Discord, Matrix, etc.SessionDB.list_sessions_rich()defaults toinclude_children=Falseandproject_compression_tips=True, which treats compression continuations as one logical conversation projected to the latest tip.The WebUI bridge currently bypasses that logical projection and directly queries raw
sessionsrows.Current WebUI paths affected
Two WebUI paths need to stay in sync:
/api/sessionsviaapi.models.get_cli_sessions()webuirows fromstate.dbLIMIT 200before any logical conversation projectionGateway SSE watcher via
api.gateway_watcher._get_agent_sessions_from_db()webuirows/api/sessionsis fixed separatelyRelated issues / PRs
Show Agent Sessions; this issue is about the relationship semantics of those displayed sessions, not bidirectional gateway sync.Proposed implementation plan
I think this should be split into two PRs for this issue.
PR 1: Hide empty/unimportable agent session rows
Scope:
/api/session/import_cli.parent_session_idchains yet.Expected behavior:
sessionsrow withsource != 'webui'but zeromessagesrows should not appear in the sidebar.Suggested tests:
webuisession row exists with no messages and verify/api/sessionsdoes not return it.This PR should reference this issue but should not close it.
PR 2: Collapse compression continuation chains into logical conversations
Scope:
parent_session_idandend_reason = 'compression'to identify continuation chains.api.models.get_cli_sessions()api.gateway_watcher._get_agent_sessions_from_db()Expected behavior:
Suggested tests:
/api/sessions.This PR can close this issue if the issue scope remains limited to empty rows + compression continuation projection.
Explicit non-goals for this issue
Product principle
Show Agent Sessionsshould expose useful agent conversations, not raw backend storage rows.Different
parent_session_idrelationships need different UI treatment:Suggested triage metadata
suggested metadata:
bug,cli-parity,gateway,uxMilestone 1 — Core quality & parity