[codex] Fix empty gateway session hiding messaging history - #2286
1 commit merged into
Conversation
|
@nesquena Ready for independent review when you have a moment. CI is green; staging this for the next batch release once approved. |
36c1c8f to
98f2814
Compare
nesquena
left a comment
There was a problem hiding this comment.
Review — end-to-end ✅ (approved after rebase)
First — thanks for the contribution, @junjunjunbong! This is a clean, narrow fix to a real sidebar-projection bug with the right test scope.
What this ships
When _keep_latest_messaging_session_per_source() computes "which messaging sources have an active Gateway session," it used to treat every entry in sessions/sessions.json as active — even Gateway sessions that weren't actually present in the input rows (e.g. because they were filtered upstream for having zero messages). The fix intersects gateway-metadata session IDs with the currently visible row IDs, so the "this source is active, hide older rows" logic only fires when the active session is something the sidebar can actually show.
Note: the branch was stale (rebased onto current master)
The PR was opened 2026-05-15 against master @ stage-353 (v0.51.60-era). Master is now at v0.51.89; the _keep_latest_messaging_session_per_source block had moved + neighbouring tests had grown. I rebased the branch and resolved one keep-both conflict in tests/test_gateway_sync.py (your new test sat in the same insertion gap as an email-source test added later in #2349/stage-360-ish). Now at 98f2814e. CI re-ran green on 3.11/3.12/3.13 after I approved the workflow.
Traced against upstream hermes-agent
Pulled a fresh nousresearch/hermes-agent tarball. Grep for _keep_latest_messaging_session_per_source and gateway_session_identity_map in hermes_cli/: no CLI counterpart. The gateway-sessions read pipeline is purely WebUI sidebar projection — Hermes Gateway writes sessions/sessions.json and WebUI reads it; the agent runtime itself doesn't touch this surface. No cross-tool contract to break. ✓
End-to-end trace
Setup (api/routes.py:2162-2176):
gateway_metadata = _load_gateway_session_identity_map() # all gateway-known sessions
active_gateway_session_ids = {str(sid) for sid in gateway_metadata.keys() if sid}
session_ids = { # NEW: input-row IDs
_safe_first(session.get("session_id"))
for session in sessions
if isinstance(session, dict)
}
visible_active_gateway_session_ids = active_gateway_session_ids & session_ids # NEW: intersection
active_gateway_sources = {
_normalize_messaging_source(_safe_first(meta.get("raw_source"), meta.get("platform")))
for sid, meta in gateway_metadata.items()
if sid in visible_active_gateway_session_ids and isinstance(meta, dict) # NEW: scoped to visible
}Hide check (api/routes.py:2186): _should_hide_stale_messaging_session(session, visible_active_gateway_session_ids, active_gateway_sources) — now receives only the gateway sessions that exist in the input rows, plus the sources that those visible sessions belong to.
Why this fixes the bug: _should_hide_stale_messaging_session at api/routes.py:1878-1916 bails out early (return False) unless raw_source in active_gateway_sources. If the only gateway-advertised Discord session is invisible (e.g. zero messages, filtered upstream), active_gateway_sources no longer contains "discord", so older Discord history rows are no longer flagged stale. The downstream staleness predicates (end_reason in _STALE_MESSAGING_END_REASONS, parent_session_id, message_count <= 0) only fire when there's a legitimate active Gateway row to defer to.
Behavioral harness (semantic correctness)
To prove the fix doesn't regress the legitimate "hide stale rows when an active Gateway session IS visible" case, I built a 6-scenario Python harness exercising the helper directly:
ok empty active gateway session — older Discord history preserved (PR's regression)
ok gateway active session visible — older stale Discord row hidden (original use case)
ok gateway has discord+slack, only slack visible — older Discord preserved
ok no gateway metadata — fallback keeps history
ok empty rows list — empty result
ok non-messaging sessions passthrough — webui session always kept
=== Overall: ALL PASS
Scenario 2 is the load-bearing one: when the active Gateway session IS in the rows, visible_active_gateway_session_ids correctly contains it, active_gateway_sources = {"discord"}, and older session_reset rows are hidden exactly as before. Behaviour is preserved.
Edge-case matrix
| Scenario | Expected | Actual |
|---|---|---|
Empty sessions list |
empty result | ✅ |
| Empty gateway metadata | all rows kept (fallback) | ✅ |
| Gateway active session visible + stale older row | active kept, stale hidden | ✅ (unchanged from before fix) |
| Gateway active session invisible + history row | history preserved | ✅ (the fix; was hidden before) |
| Gateway has discord+slack, only slack visible, older Discord row present | older Discord preserved (discord source no longer "active") | ✅ |
| Non-messaging session (webui) | always kept | ✅ |
Session with no session_id |
_safe_first(None) → None; harmless (not in active_gateway_session_ids either) |
✅ |
Cross-tool (CLI reads same sessions.json) |
no CLI consumer of this WebUI-only projection | ✅ |
Other audit — things that are correct already
- Security: zero new surface. The helper reads from
sessions/sessions.json(gateway-owned), filters input rows, and returns a projection. No user input controls anything; no auth, file IO, or HTTP changes. - Test scope is exactly right.
test_empty_active_gateway_session_does_not_hide_messaging_historyexercises the helper through its monkeypatched gateway-identity-map input — directly tests the regression without coupling to upstream filesystem state. Asserts the projectedsession_idlist, not just count. - Existing tests preserved:
test_messaging_projection_does_not_aggressively_hide_without_gateway_metadatastill passes — confirms no-gateway-metadata fallback unchanged.test_messaging_projection_keeps_distinct_active_gateway_conversationsstill passes — confirms multi-source visible-active scenarios unchanged.
Tests
tests/test_gateway_sync.py— 53 passed in 1.25s (the PR's new test included)- Full suite — 5828 passed, 63 skipped, 0 failed in 54.25s
- Behavioral harness — 6/6 scenarios pass (including the legitimate hide-stale case)
- CI after rebase: test (3.11/3.12/3.13) all green
Minor observations (non-blocking)
- Variable naming is consistent with the rest of the file:
_visible_active_*for the filtered set, original name retained for the unfiltered set. Reads clearly. - The fix is fully backwards-compatible for the no-gateway-metadata path: when
gateway_metadatais empty,active_gateway_session_ids = {}and sovisible_active_gateway_session_ids = {} & session_ids = {}— same as before for the downstream check (not active_gateway_session_idsshort-circuits at api/routes.py:1895). - No CHANGELOG entry was added with the PR. Not blocking — release agent stages typically write these on the merge sweep.
Recommendation
✅ Approved. Parked at approval — ready for the release agent's merge/tag pipeline.
Thanks again for narrowing this from "treat all gateway-metadata sources as active" to "only the sources whose advertised sessions are actually visible." That's the right design — the upstream filter (zero-message hiding, whatever it is) shouldn't silently cascade into "hide history under that source too."
4589dbe
… 0.51.90) (#556) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.89` → `0.51.90` | --- ### Release Notes <details> <summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary> ### [`v0.51.90`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05190--2026-05-18--Release-BN-stage-383--10-PR-full-sweep-batch--empty-gateway-messaging-history-fix--previous-messaging-sessions-setting--Kanban-board-switcher-layout--UIUX-demo-theme-controls--Slice-3c-queuegoal-RFC-gate--keyless-custom-endpoints--custom-provider-remote-model-catalog-parity--auto-compression-elapsed-timer--new-conversation-cold-start-guard--Kanban-drag-drop-detail-open-fix) [Compare Source](nesquena/hermes-webui@v0.51.89...v0.51.90) ##### Fixed - **PR [#​2286](nesquena/hermes-webui#2286 by [@​junjunjunbong](https://github.com/junjunjunbong) (refs [#​2275](nesquena/hermes-webui#2275)) — Narrow messaging stale-session filtering to active gateway sessions that are visible in the current sidebar candidate set. Older Discord/messaging history is now preserved when the gateway advertises a fresh zero-message session that hasn't yet entered the visible projection, instead of being hidden as stale. Adds a regression test for an empty active Discord gateway row preserving prior history. - **PR [#​2459](nesquena/hermes-webui#2459 by [@​franksong2702](https://github.com/franksong2702) (closes [#​2458](nesquena/hermes-webui#2458)) — Fix the Kanban board switcher menu when a board's icon slot carries a long text label (e.g. `layout-kanban`). The icon column changed from a fixed `18px` slot to a bounded flex cell with `min-width:18px;max-width:7.5rem`, with overflow ellipsis on the icon itself so long labels render fully when space allows and truncate cleanly when not. Title and count columns keep stable spacing. Adds before/after screenshots and a CSS contract regression in `tests/test_kanban_ui_static.py`. - **PR [#​2522](nesquena/hermes-webui#2522 by [@​Michaelyklam](https://github.com/Michaelyklam) (refs [#​2271](nesquena/hermes-webui#2271)) — Treat named custom OpenAI-compatible endpoints with a configured `base_url` as key-optional at WebUI agent startup. Local keyless servers (llama-server / vLLM-style LAN deployments) no longer fail early with a synthetic `CUSTOM:<slug>_API_KEY` env-var prompt before the request reaches the endpoint; instead the OpenAI-compatible client initialises with a harmless placeholder key and real configured keys are still preferred when present. Refactors the three near-identical custom-provider rebuild blocks (initial agent setup + two retry/healing paths) through the existing `resolve_custom_provider_connection` helper. - **PR [#​2515](nesquena/hermes-webui#2515 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​2513](nesquena/hermes-webui#2513)) — Keep named custom-provider model pickers populated from each configured endpoint's live `/models` catalog even when `custom_providers[].model` is present. The singular `model` field now acts as a sticky/fallback entry appended *after* the remote catalog rather than collapsing the picker to just the configured model and hiding sibling named custom providers. Extracts reusable OpenAI-compatible `/models` parsing/fetching helpers and threads them through both the active-base-url and per-named-provider paths. - **PR [#​2512](nesquena/hermes-webui#2512 by [@​dso2ng](https://github.com/dso2ng) (refs [#​2477](nesquena/hermes-webui#2477), Slice A) — Show an elapsed timer on the running automatic-compression card so long WebUI context-compression pauses no longer look frozen while the browser waits for the `compressed` event. Stamps `startedAt` on the `compressing` SSE event, ticks once per second, and switches to a `5+ min` cap label past the Slice A bound so the UI never frame-freezes at `05:00`. Browser-transient state only — no SSE contract change and no server-side resume reconstruction. - **PR [#​2528](nesquena/hermes-webui#2528 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​2518](nesquena/hermes-webui#2518)) — Guard New Conversation creation while a previous `/api/session/new` request is still in flight, so cold model/provider catalog resolution gives immediate pending feedback and rapid repeated clicks reuse the same create request instead of enqueueing duplicate blank sessions. Coalesces concurrent `newSession()` calls behind a single in-flight promise, disables the sidebar button with `aria-busy="true"`, and shows a localized `Creating new conversation…` composer status. - **PR [#​2530](nesquena/hermes-webui#2530 by [@​franksong2702](https://github.com/franksong2702) (refs [#​2529](nesquena/hermes-webui#2529)) — Keep Kanban drag/drop status updates from also opening the task detail pane. Two failure paths were both producing detail-pane opens after drag/drop: the browser's trailing synthetic click after `drop`, and the generic task-update helper opening detail on every PATCH. The fix adds a time-windowed `_kanbanSuppressCardClickUntil` set on `ondragstart`/`ondragend`/`ondrop` and routes drag/drop status changes through a board-only update path. Explicit card click and keyboard activation remain unchanged. ##### Added - **PR [#​2294](nesquena/hermes-webui#2294 by [@​junjunjunbong](https://github.com/junjunjunbong) — Add a `show_previous_messaging_sessions` setting so users can opt back into seeing previous messaging sessions that were replaced by `session_reset` or auto-compression. The preference is wired through boot, settings persistence, and the sidebar projection. Also adds a separate "Hide from list" action for imported messaging/CLI sessions that hides individual rows from the sidebar without deleting source history. ##### Documentation - **PR [#​2511](nesquena/hermes-webui#2511 by [@​franksong2702](https://github.com/franksong2702) (refs [#​2502](nesquena/hermes-webui#2502) / [#​2503](nesquena/hermes-webui#2503)) — Update the `docs/ui-ux/` demo appearance controls to initialize as `class="dark" data-skin="slate"` instead of the deprecated `data-theme`-only buttons and legacy theme names. Brings the demo pages in line with the live Theme + Skin contract referenced from the new `docs/CONTRACTS.md` so contributors following the contract-index path don't land on stale demos. - **PR [#​2509](nesquena/hermes-webui#2509 by [@​Michaelyklam](https://github.com/Michaelyklam) (refs [#​1925](nesquena/hermes-webui#1925)) — Advance the runtime-adapter RFC after the Slice 3b approval/clarify seam shipped in v0.51.89. The RFC now marks Slice 3b as shipped and defines the next Slice 3c queue/continue + goal control gate: route those controls through `RuntimeAdapter.queue_message(...)` / `update_goal(...)` only after pinning stable response contracts, bounded unavailable-control behavior, replayable lifecycle/status evidence, ordering/idempotency expectations, and explicit non-goals for runner/sidecar ownership or a WebUI-owned queue/goal scheduler. Docs + adapter-seam regression test only — no runtime/control routing changes in this PR. </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19--> Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/556
Summary
Root cause
When the gateway advertised a fresh Discord session in sessions.json, the sidebar treated that source as active and hid older Discord rows as stale. If the fresh session had zero messages, it was omitted from the visible session projection, leaving no Discord rows visible.
Validation