Skip to content

fix(timeline): persist typed display events - #69771

Merged
ethernet8023 merged 7 commits into
mainfrom
ethie/fix-verifiction-unhide
Jul 23, 2026
Merged

fix(timeline): persist typed display events#69771
ethernet8023 merged 7 commits into
mainfrom
ethie/fix-verifiction-unhide

Conversation

@ethernet8023

@ethernet8023 ethernet8023 commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Persists durable display_kind and JSON display_metadata separately from model-facing message roles/content, so synthetic transcript entries (model switches, async delegation completions, compaction handoffs) render as timeline events instead of opaque user messages across all three chat surfaces: CLI, TUI, and desktop.

The core problem: model switch markers, async delegation completions, and compaction handoffs were all persisted as role: user messages with opaque content. On resume, every surface displayed them as if the user had typed that text. This PR introduces a display_kind column on the messages table and a display_metadata JSON sidecar, so the model still receives the original role/content unchanged while each surface renders the row as a typed timeline event (or hides it entirely for compaction handoffs).

Review fixes (commit d58369f0c)

Three findings from top-to-bottom code review, all fixed with regression tests:

  1. Provider payload leakdisplay_kind/display_metadata were forwarded to the provider API as unknown message fields. Strict OpenAI-compatible backends could reject the next request after a model switch or resumed typed event. Fixed by stripping both from the per-request api_msg copy in conversation_loop.py alongside the existing api_content pop.

  2. Rewrite/import data loss_insert_message_rows preserved display_kind but silently dropped display_metadata. After replace_messages, archive_and_compact, or session import, async-delegation events lost their task counts. Fixed by adding display_metadata to the INSERT columns and bind tuple.

  3. CLI /resume stale recap — Startup --resume A set _resume_display_history from A's lineage. A subsequent in-session /resume B loaded B only into conversation_history, leaving the stale A display projection. Fixed by switching /resume to get_resume_conversations and updating _resume_display_history alongside conversation_history.

TUI event rendering (commit a567ae795)

The TUI was not handling display_kind at all — model switch markers and async delegation completions rendered as opaque user messages, and hidden compaction handoffs were visible. Wired display_kind through the full TUI chain: _history_to_messages forwards the fields, toTranscriptMessages maps them to a new 'event' message kind, and messageLine.tsx renders events as dim markers.

Related Issue

Fixes #69629 (timeline events leak as user messages on resume)

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • hermes_state.py — Add display_kind and display_metadata columns to messages table. append_message() accepts and persists both. set_latest_matching_message_display_kind() stamps presentation metadata. _insert_message_rows() preserves display_metadata through rewrites. get_resume_conversations() returns (model_history, display_history) from one lineage SELECT.
  • tui_gateway/server.py — Model switch marker and async delegation completion stamp display_kind/display_metadata. _history_to_messages() forwards display fields to the TUI transcript payload.
  • agent/conversation_loop.py — Strip display_kind/display_metadata from the per-request api_msg copy before provider dispatch.
  • run_agent.py — Stamps display_kind="hidden" on compaction handoff rows.
  • hermes_cli/cli_agent_setup_mixin.py + cli_commands_mixin.py — CLI resume and /resume use get_resume_conversations(), render timeline events as markers, skip hidden rows.
  • ui-tui/src/GatewayTranscriptMessage + Msg + TranscriptRow gain display fields. toTranscriptMessages() maps display_kind to 'event' kind. messageLine.tsx renders events as dim markers. blockLayout.ts adds 'event' group.
  • apps/desktop/src/lib/chat-messages.ts + types/hermes.tstoChatMessages() projects display_kind into system-role timeline bubbles.
  • apps/desktop/e2e/RealSessionBuilder creates real persisted sessions. hidden-history-messages.spec.ts E2E.
  • nix/devShell.nix — Export VIRTUAL_ENV for uv.
  • Tests — Python (911), TUI (9 new), desktop (35).

How to Test

  1. nix develop -c bash -lc 'export HERMES_PYTHON="$(command -v python)"; scripts/run_tests.sh tests/test_hermes_state.py tests/test_tui_gateway_server.py tests/tui_gateway/test_model_switch_marker_role.py tests/cli/test_resume_display.py tests/cli/test_cli_resume_command.py tests/run_agent/test_session_meta_filtering.py' — 911 passed
  2. cd ui-tui && npm run typecheck && npm run lint && npx vitest run src/__tests__/messages.test.ts — 9 passed (1 pre-existing failure on main)
  3. cd apps/desktop && npm run typecheck && npm run lint && npm test -- --run src/lib/chat-messages.test.ts — 35 passed
  4. nix build .# then resume a seeded session in CLI, TUI, and desktop under WLR_BACKENDS=headless cage

Checklist

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature
  • I've run tests and all pass
  • I've added tests for my changes
  • I've tested on my platform: NixOS (Linux 7.1.0)

Screenshots / Logs

CLI resume — event markers, hidden rows suppressed

cli-timeline-screenshot

TUI resume — dim markers in the Ink TUI

tui-timeline-screenshot

Desktop — timeline events rendered via Playwright E2E

desktop-timeline-events

Captured with the desktop E2E Playwright fixtures (launchDesktop + page.screenshot) under WLR_BACKENDS=headless cage, using a real persisted session seeded via RealSessionBuilder. toChatMessages() projects display_kind into system-role timeline bubbles.

Verification output

Python: 911 tests passed, 0 failed (6 files)
TUI typecheck: clean, lint: 0 errors, 9 tests passed
Desktop typecheck: clean, lint: 0 errors, 35 tests passed

Timeline display evidence

All captures use the same seeded display event history: a model-switch marker,
a two-task delegation completion, and a hidden compaction row. The hidden row
does not render.

CLI TUI Desktop
cli-timeline.png tui-timeline.png desktop-timeline.png

CLI and TUI were captured in Ghostty under headless Cage. Desktop was captured
by the real Electron Playwright fixture under headless Cage.

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on a567ae7

all good!

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/desktop Electron desktop app (apps/desktop/*) sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 23, 2026
@ethernet8023 ethernet8023 changed the title fix(desktop): hide agent-only history scaffolding fix(timeline): persist typed display events Jul 23, 2026
Filter verification-stop nudges and context-compaction handoffs at the
stored-history mapper boundary. Preserve a real reply when a compaction
handoff shares its stored message.
Drive tui_gateway.entry over its stdio JSON-RPC transport against the mock
provider, wait for real completion events, and persist normal session history
through AIAgent and SessionDB. Migrate resume and hidden-history coverage,
including real compression and live verify-on-stop scaffolding, then remove
the unused direct SessionDB import scripts.
Run the stdio gateway through uv's synced project environment outside the
Nix dev shell, while retaining the fully provisioned Nix Python when the
shell advertises HERMES_PYTHON_SRC_ROOT.
Mark the Nix-built Python environment active in the dev shell so the shared
E2E session builder can always run through `uv run --active --no-sync`.
@ethernet8023
ethernet8023 force-pushed the ethie/fix-verifiction-unhide branch from e66907c to 19f3b3d Compare July 23, 2026 04:41
ethernet8023 added a commit that referenced this pull request Jul 23, 2026
…erve through rewrites, fix /resume display history

Three review findings from PR #69771:

1. Provider payload leak: display_kind and display_metadata were forwarded
   to the provider API as unknown message fields. Strict OpenAI-compatible
   backends can reject the next request after a model switch or resumed
   typed event. Strip both from the per-request api_msg copy in
   conversation_loop alongside the existing api_content pop.

2. Rewrite/import data loss: _insert_message_rows preserved display_kind
   but silently dropped display_metadata. After replace_messages,
   archive_and_compact, or session import, async-delegation completion
   events lost their task counts and fell back to generic display text.
   Add display_metadata to the INSERT columns and bind tuple.

3. CLI /resume stale recap: startup --resume A set _resume_display_history
   from A's lineage. A subsequent in-session /resume B loaded B only into
   conversation_history via get_messages_as_conversation, leaving the stale
   A display projection. _display_resumed_history preferentially read the
   stale attribute, showing A's recap for B. Switch /resume to
   get_resume_conversations and update _resume_display_history alongside
   conversation_history.

Tests: 890 Python (5 files), 35 desktop TS — all green.
…erve through rewrites, fix /resume display history

Three review findings from PR #69771:

1. Provider payload leak: display_kind and display_metadata were forwarded
   to the provider API as unknown message fields. Strict OpenAI-compatible
   backends can reject the next request after a model switch or resumed
   typed event. Strip both from the per-request api_msg copy in
   conversation_loop alongside the existing api_content pop.

2. Rewrite/import data loss: _insert_message_rows preserved display_kind
   but silently dropped display_metadata. After replace_messages,
   archive_and_compact, or session import, async-delegation completion
   events lost their task counts and fell back to generic display text.
   Add display_metadata to the INSERT columns and bind tuple.

3. CLI /resume stale recap: startup --resume A set _resume_display_history
   from A's lineage. A subsequent in-session /resume B loaded B only into
   conversation_history via get_messages_as_conversation, leaving the stale
   A display projection. _display_resumed_history preferentially read the
   stale attribute, showing A's recap for B. Switch /resume to
   get_resume_conversations and update _resume_display_history alongside
   conversation_history.

Tests: 890 Python (5 files), 35 desktop TS — all green.
@ethernet8023
ethernet8023 force-pushed the ethie/fix-verifiction-unhide branch from d58369f to 4d5180a Compare July 23, 2026 06:21
The TUI was not handling display_kind at all — model switch markers and
async delegation completions rendered as opaque user messages with the
full [System: ...] text, and hidden compaction handoffs were visible.

Wire display_kind through the full TUI chain:

- _history_to_messages (tui_gateway/server.py) forwards display_kind
  and display_metadata to the gateway transcript payload.
- GatewayTranscriptMessage (gatewayTypes.ts) gains both fields.
- Msg.kind (types.ts) gains 'event' value.
- toTranscriptMessages (domain/messages.ts) maps:
  - hidden → skip entirely
  - model_switch → event "model changed"
  - async_delegation_complete → event "N background agents finished"
    (or "background agent work finished" without metadata)
- messageGroup (blockLayout.ts) routes event to its own group, with
  SELF_SPACED + PAINTS_TRAILING_GAP so it owns its margins.
- messageLine.tsx renders event-kind as a dim ◈ marker with no gutter,
  matching the CLI's ◈ event rendering.
- 4 new TUI tests for hidden/model_switch/async_delegation mapping.

TUI typecheck: clean. TUI lint: 0 errors (2 pre-existing warnings).
TUI tests: 9 passed (1 pre-existing failure on main, unrelated).
@ethernet8023
ethernet8023 merged commit a4bc1ca into main Jul 23, 2026
44 checks passed
@ethernet8023
ethernet8023 deleted the ethie/fix-verifiction-unhide branch July 23, 2026 18:46
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
* fix(desktop): hide persisted agent-only history scaffolding

Filter verification-stop nudges and context-compaction handoffs at the
stored-history mapper boundary. Preserve a real reply when a compaction
handoff shares its stored message.

* test(desktop): build persisted E2E sessions through the real agent

Drive tui_gateway.entry over its stdio JSON-RPC transport against the mock
provider, wait for real completion events, and persist normal session history
through AIAgent and SessionDB. Migrate resume and hidden-history coverage,
including real compression and live verify-on-stop scaffolding, then remove
the unused direct SessionDB import scripts.

* fix(desktop): use the provisioned Python for real-session E2Es

Run the stdio gateway through uv's synced project environment outside the
Nix dev shell, while retaining the fully provisioned Nix Python when the
shell advertises HERMES_PYTHON_SRC_ROOT.

* fix(nix): expose the provisioned Python environment to uv

Mark the Nix-built Python environment active in the dev shell so the shared
E2E session builder can always run through `uv run --active --no-sync`.

* fix(timeline): persist typed display events

* fix(timeline): strip display-only fields from provider payloads, preserve through rewrites, fix /resume display history

Three review findings from PR NousResearch#69771:

1. Provider payload leak: display_kind and display_metadata were forwarded
   to the provider API as unknown message fields. Strict OpenAI-compatible
   backends can reject the next request after a model switch or resumed
   typed event. Strip both from the per-request api_msg copy in
   conversation_loop alongside the existing api_content pop.

2. Rewrite/import data loss: _insert_message_rows preserved display_kind
   but silently dropped display_metadata. After replace_messages,
   archive_and_compact, or session import, async-delegation completion
   events lost their task counts and fell back to generic display text.
   Add display_metadata to the INSERT columns and bind tuple.

3. CLI /resume stale recap: startup --resume A set _resume_display_history
   from A's lineage. A subsequent in-session /resume B loaded B only into
   conversation_history via get_messages_as_conversation, leaving the stale
   A display projection. _display_resumed_history preferentially read the
   stale attribute, showing A's recap for B. Switch /resume to
   get_resume_conversations and update _resume_display_history alongside
   conversation_history.

Tests: 890 Python (5 files), 35 desktop TS — all green.

* feat(tui): render typed display events as ◈ markers in the Ink TUI

The TUI was not handling display_kind at all — model switch markers and
async delegation completions rendered as opaque user messages with the
full [System: ...] text, and hidden compaction handoffs were visible.

Wire display_kind through the full TUI chain:

- _history_to_messages (tui_gateway/server.py) forwards display_kind
  and display_metadata to the gateway transcript payload.
- GatewayTranscriptMessage (gatewayTypes.ts) gains both fields.
- Msg.kind (types.ts) gains 'event' value.
- toTranscriptMessages (domain/messages.ts) maps:
  - hidden → skip entirely
  - model_switch → event "model changed"
  - async_delegation_complete → event "N background agents finished"
    (or "background agent work finished" without metadata)
- messageGroup (blockLayout.ts) routes event to its own group, with
  SELF_SPACED + PAINTS_TRAILING_GAP so it owns its margins.
- messageLine.tsx renders event-kind as a dim ◈ marker with no gutter,
  matching the CLI's ◈ event rendering.
- 4 new TUI tests for hidden/model_switch/async_delegation mapping.

TUI typecheck: clean. TUI lint: 0 errors (2 pre-existing warnings).
TUI tests: 9 passed (1 pre-existing failure on main, unrelated).
prmartinow pushed a commit to prmartinow/hermes-agent that referenced this pull request Aug 26, 2026
* fix(desktop): hide persisted agent-only history scaffolding

Filter verification-stop nudges and context-compaction handoffs at the
stored-history mapper boundary. Preserve a real reply when a compaction
handoff shares its stored message.

* test(desktop): build persisted E2E sessions through the real agent

Drive tui_gateway.entry over its stdio JSON-RPC transport against the mock
provider, wait for real completion events, and persist normal session history
through AIAgent and SessionDB. Migrate resume and hidden-history coverage,
including real compression and live verify-on-stop scaffolding, then remove
the unused direct SessionDB import scripts.

* fix(desktop): use the provisioned Python for real-session E2Es

Run the stdio gateway through uv's synced project environment outside the
Nix dev shell, while retaining the fully provisioned Nix Python when the
shell advertises HERMES_PYTHON_SRC_ROOT.

* fix(nix): expose the provisioned Python environment to uv

Mark the Nix-built Python environment active in the dev shell so the shared
E2E session builder can always run through `uv run --active --no-sync`.

* fix(timeline): persist typed display events

* fix(timeline): strip display-only fields from provider payloads, preserve through rewrites, fix /resume display history

Three review findings from PR NousResearch#69771:

1. Provider payload leak: display_kind and display_metadata were forwarded
   to the provider API as unknown message fields. Strict OpenAI-compatible
   backends can reject the next request after a model switch or resumed
   typed event. Strip both from the per-request api_msg copy in
   conversation_loop alongside the existing api_content pop.

2. Rewrite/import data loss: _insert_message_rows preserved display_kind
   but silently dropped display_metadata. After replace_messages,
   archive_and_compact, or session import, async-delegation completion
   events lost their task counts and fell back to generic display text.
   Add display_metadata to the INSERT columns and bind tuple.

3. CLI /resume stale recap: startup --resume A set _resume_display_history
   from A's lineage. A subsequent in-session /resume B loaded B only into
   conversation_history via get_messages_as_conversation, leaving the stale
   A display projection. _display_resumed_history preferentially read the
   stale attribute, showing A's recap for B. Switch /resume to
   get_resume_conversations and update _resume_display_history alongside
   conversation_history.

Tests: 890 Python (5 files), 35 desktop TS — all green.

* feat(tui): render typed display events as ◈ markers in the Ink TUI

The TUI was not handling display_kind at all — model switch markers and
async delegation completions rendered as opaque user messages with the
full [System: ...] text, and hidden compaction handoffs were visible.

Wire display_kind through the full TUI chain:

- _history_to_messages (tui_gateway/server.py) forwards display_kind
  and display_metadata to the gateway transcript payload.
- GatewayTranscriptMessage (gatewayTypes.ts) gains both fields.
- Msg.kind (types.ts) gains 'event' value.
- toTranscriptMessages (domain/messages.ts) maps:
  - hidden → skip entirely
  - model_switch → event "model changed"
  - async_delegation_complete → event "N background agents finished"
    (or "background agent work finished" without metadata)
- messageGroup (blockLayout.ts) routes event to its own group, with
  SELF_SPACED + PAINTS_TRAILING_GAP so it owns its margins.
- messageLine.tsx renders event-kind as a dim ◈ marker with no gutter,
  matching the CLI's ◈ event rendering.
- 4 new TUI tests for hidden/model_switch/async_delegation mapping.

TUI typecheck: clean. TUI lint: 0 errors (2 pre-existing warnings).
TUI tests: 9 passed (1 pre-existing failure on main, unrelated).
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
* fix(desktop): hide persisted agent-only history scaffolding

Filter verification-stop nudges and context-compaction handoffs at the
stored-history mapper boundary. Preserve a real reply when a compaction
handoff shares its stored message.

* test(desktop): build persisted E2E sessions through the real agent

Drive tui_gateway.entry over its stdio JSON-RPC transport against the mock
provider, wait for real completion events, and persist normal session history
through AIAgent and SessionDB. Migrate resume and hidden-history coverage,
including real compression and live verify-on-stop scaffolding, then remove
the unused direct SessionDB import scripts.

* fix(desktop): use the provisioned Python for real-session E2Es

Run the stdio gateway through uv's synced project environment outside the
Nix dev shell, while retaining the fully provisioned Nix Python when the
shell advertises HERMES_PYTHON_SRC_ROOT.

* fix(nix): expose the provisioned Python environment to uv

Mark the Nix-built Python environment active in the dev shell so the shared
E2E session builder can always run through `uv run --active --no-sync`.

* fix(timeline): persist typed display events

* fix(timeline): strip display-only fields from provider payloads, preserve through rewrites, fix /resume display history

Three review findings from PR NousResearch#69771:

1. Provider payload leak: display_kind and display_metadata were forwarded
   to the provider API as unknown message fields. Strict OpenAI-compatible
   backends can reject the next request after a model switch or resumed
   typed event. Strip both from the per-request api_msg copy in
   conversation_loop alongside the existing api_content pop.

2. Rewrite/import data loss: _insert_message_rows preserved display_kind
   but silently dropped display_metadata. After replace_messages,
   archive_and_compact, or session import, async-delegation completion
   events lost their task counts and fell back to generic display text.
   Add display_metadata to the INSERT columns and bind tuple.

3. CLI /resume stale recap: startup --resume A set _resume_display_history
   from A's lineage. A subsequent in-session /resume B loaded B only into
   conversation_history via get_messages_as_conversation, leaving the stale
   A display projection. _display_resumed_history preferentially read the
   stale attribute, showing A's recap for B. Switch /resume to
   get_resume_conversations and update _resume_display_history alongside
   conversation_history.

Tests: 890 Python (5 files), 35 desktop TS — all green.

* feat(tui): render typed display events as ◈ markers in the Ink TUI

The TUI was not handling display_kind at all — model switch markers and
async delegation completions rendered as opaque user messages with the
full [System: ...] text, and hidden compaction handoffs were visible.

Wire display_kind through the full TUI chain:

- _history_to_messages (tui_gateway/server.py) forwards display_kind
  and display_metadata to the gateway transcript payload.
- GatewayTranscriptMessage (gatewayTypes.ts) gains both fields.
- Msg.kind (types.ts) gains 'event' value.
- toTranscriptMessages (domain/messages.ts) maps:
  - hidden → skip entirely
  - model_switch → event "model changed"
  - async_delegation_complete → event "N background agents finished"
    (or "background agent work finished" without metadata)
- messageGroup (blockLayout.ts) routes event to its own group, with
  SELF_SPACED + PAINTS_TRAILING_GAP so it owns its margins.
- messageLine.tsx renders event-kind as a dim ◈ marker with no gutter,
  matching the CLI's ◈ event rendering.
- 4 new TUI tests for hidden/model_switch/async_delegation mapping.

TUI typecheck: clean. TUI lint: 0 errors (2 pre-existing warnings).
TUI tests: 9 passed (1 pre-existing failure on main, unrelated).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants