Skip to content

fix(desktop): persist /title set before the first message instead of queuing - #47987

Merged
austinpickett merged 1 commit into
mainfrom
fix/desktop-title-before-first-message
Jun 17, 2026
Merged

fix(desktop): persist /title set before the first message instead of queuing#47987
austinpickett merged 1 commit into
mainfrom
fix/desktop-title-before-first-message

Conversation

@austinpickett

Copy link
Copy Markdown
Collaborator

Symptom

In a fresh desktop chat, typing /title my-custom-name as the first action, then sending a message (e.g. hello), leaves the session titled hello (the first-message preview) — the manual title is silently lost. The status line even confirms it queued: "Session title set: my-custom-name (queued while session initializes)".

Root cause

The desktop/TUI defers the session's SQLite row to the first prompt (so abandoned empty drafts don't litter the sidebar). When /title runs before any message, session.title (tui_gateway/server.py) finds no row, so set_session_title returns rowcount 0. The handler then only stashed pending_title and returned pending: true, betting that the post-turn apply block would write the title later.

That deferral is fragile: when the first turn doesn't land/persist under the same session_key (or the apply path doesn't fire), the queued title is dropped and the row's title stays NULL. The sidebar's sessionTitle() then falls back to title || preview || 'Untitled' → the message preview hello.

The messaging gateway's _handle_title_command never had this bug — it creates the row immediately and sets the title directly.

Fix

Make the desktop path match the messaging gateway: an explicit /title is clear user intent, not an abandoned draft, so persist the row up front (_ensure_session_db_row) and set the title immediately through the profile-aware _session_db handle, returning pending: false.

This also fixes the frontend symptom for free. The desktop /title handler does an optimistic setSessions(...) then an immediate refreshSessions(); while pending:true that pull hit the still-NULL row and clobbered the optimistic title back to the preview. With the row now persisted synchronously, that same refresh pulls the correct title.

If row creation can't take (DB unavailable / racing writer), it falls back to the existing pending_title queue, so the post-turn apply block remains a recovery path. The sidebar's min_messages >= 1 filter keeps a titled 0-message row hidden, so a /title'd-but-never-used draft still doesn't clutter the list.

Testing

  • Replaced test_session_title_queues_when_db_row_not_ready (asserted the buggy queue behavior) with test_session_title_creates_row_and_sets_immediately_when_not_ready, which asserts the row is created up front, the title is set, pending:false, and pending_title is cleared.
  • Added test_session_title_falls_back_to_queue_when_row_create_fails to lock in the recovery path.
  • Verified against the real handler with a temp HERMES_HOME + real SessionDB: /title before any row now creates the row and sets the title (pending:false).
  • pytest tests/test_tui_gateway_server.py -k "title or pending or auto_title or prompt_submit" → all pass.

…queuing

A /title typed before any message in a fresh desktop chat could be silently
lost: the session DB row is deferred to the first prompt, so session.title
found no row, only stashed pending_title, and returned pending:true. It then
relied on a post-turn apply block to write the title. When that turn never
landed under the same session_key (or the apply path didn't fire), the title
was dropped and the sidebar fell back to the first-message preview — e.g.
"/title my-custom-name" then "hello" left the session titled "hello".

Mirror the messaging gateway's _handle_title_command: an explicit /title is
clear user intent, not an abandoned draft, so create the row up front
(_ensure_session_db_row) and set the title immediately via the profile-aware
_session_db handle, returning pending:false. This also fixes the frontend
symptom for free — the desktop handler's immediate refreshSessions() now pulls
the correct persisted title instead of clobbering the optimistic value with a
still-NULL row.

If row creation can't take (DB unavailable / racing writer), fall back to the
existing pending_title queue so the post-turn apply block remains a recovery
path. The sidebar's min-messages filter keeps a titled 0-message row hidden, so
a /title'd-but-never-used draft still doesn't clutter the list.

Updates the test that asserted the old queue-on-missing-row behavior and adds a
fallback-to-queue regression test.
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Lint report: fix/desktop-title-before-first-message vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 10990 on HEAD, 10989 on base (🆕 +1)

🆕 New issues (2):

Rule Count
invalid-assignment 2
First entries
tests/test_tui_gateway_server.py:1797: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["row"]` and value of type `dict[str, str | None]` on object of type `dict[str, None | bool]`
tests/run_agent/test_credits_notices_toggle.py:76: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to attribute `_credits_session_start_micros` of type `int`

✅ Fixed issues (2):

Rule Count
unresolved-attribute 2
First entries
tests/run_agent/test_credits_notices_toggle.py:76: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_credits_session_start_micros` on type `AIAgent`
run_agent.py:2941: [unresolved-attribute] unresolved-attribute: Object of type `Self@get_credits_spent_micros` has no attribute `_credits_session_start_micros`

Unchanged: 5790 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/bug Something isn't working comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have labels Jun 17, 2026

@OutThisLife OutThisLife left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Verified against current main:

  • The desktop /title-before-first-message path previously only stashed pending_title and bet on the post-turn apply block — exactly the race the writeup describes (the handler returns pending:true on rowcount 0 when no row exists yet).
  • The fix correctly mirrors the messaging gateway's _handle_title_command: _ensure_session_db_row(session) (INSERT OR IGNORE) + setting the title through the profile-aware _session_db handle. Routing through _session_db instead of the bare _get_db() is also strictly more correct for remote/profile sessions, whose row lives in that profile's own state.db rather than the launch profile's.
  • The fallback to pending_title on a failed persist preserves the post-turn apply recovery path.
  • The min_messages >= 1 sidebar filter keeps a titled 0-message draft hidden, so this doesn't reintroduce empty-draft clutter.
  • Tests swap the buggy queue assertion for behavior assertions (row created, title set, pending:false, pending_title cleared) and add the fallback-to-queue case. CI green.

LGTM.

@austinpickett
austinpickett merged commit 5a00bd1 into main Jun 17, 2026
35 checks passed
@austinpickett
austinpickett deleted the fix/desktop-title-before-first-message branch June 17, 2026 20:46
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…queuing (NousResearch#47987)

A /title typed before any message in a fresh desktop chat could be silently
lost: the session DB row is deferred to the first prompt, so session.title
found no row, only stashed pending_title, and returned pending:true. It then
relied on a post-turn apply block to write the title. When that turn never
landed under the same session_key (or the apply path didn't fire), the title
was dropped and the sidebar fell back to the first-message preview — e.g.
"/title my-custom-name" then "hello" left the session titled "hello".

Mirror the messaging gateway's _handle_title_command: an explicit /title is
clear user intent, not an abandoned draft, so create the row up front
(_ensure_session_db_row) and set the title immediately via the profile-aware
_session_db handle, returning pending:false. This also fixes the frontend
symptom for free — the desktop handler's immediate refreshSessions() now pulls
the correct persisted title instead of clobbering the optimistic value with a
still-NULL row.

If row creation can't take (DB unavailable / racing writer), fall back to the
existing pending_title queue so the post-turn apply block remains a recovery
path. The sidebar's min-messages filter keeps a titled 0-message row hidden, so
a /title'd-but-never-used draft still doesn't clutter the list.

Updates the test that asserted the old queue-on-missing-row behavior and adds a
fallback-to-queue regression test.

Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
…queuing (NousResearch#47987)

A /title typed before any message in a fresh desktop chat could be silently
lost: the session DB row is deferred to the first prompt, so session.title
found no row, only stashed pending_title, and returned pending:true. It then
relied on a post-turn apply block to write the title. When that turn never
landed under the same session_key (or the apply path didn't fire), the title
was dropped and the sidebar fell back to the first-message preview — e.g.
"/title my-custom-name" then "hello" left the session titled "hello".

Mirror the messaging gateway's _handle_title_command: an explicit /title is
clear user intent, not an abandoned draft, so create the row up front
(_ensure_session_db_row) and set the title immediately via the profile-aware
_session_db handle, returning pending:false. This also fixes the frontend
symptom for free — the desktop handler's immediate refreshSessions() now pulls
the correct persisted title instead of clobbering the optimistic value with a
still-NULL row.

If row creation can't take (DB unavailable / racing writer), fall back to the
existing pending_title queue so the post-turn apply block remains a recovery
path. The sidebar's min-messages filter keeps a titled 0-message row hidden, so
a /title'd-but-never-used draft still doesn't clutter the list.

Updates the test that asserted the old queue-on-missing-row behavior and adds a
fallback-to-queue regression test.

Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
…queuing (NousResearch#47987)

A /title typed before any message in a fresh desktop chat could be silently
lost: the session DB row is deferred to the first prompt, so session.title
found no row, only stashed pending_title, and returned pending:true. It then
relied on a post-turn apply block to write the title. When that turn never
landed under the same session_key (or the apply path didn't fire), the title
was dropped and the sidebar fell back to the first-message preview — e.g.
"/title my-custom-name" then "hello" left the session titled "hello".

Mirror the messaging gateway's _handle_title_command: an explicit /title is
clear user intent, not an abandoned draft, so create the row up front
(_ensure_session_db_row) and set the title immediately via the profile-aware
_session_db handle, returning pending:false. This also fixes the frontend
symptom for free — the desktop handler's immediate refreshSessions() now pulls
the correct persisted title instead of clobbering the optimistic value with a
still-NULL row.

If row creation can't take (DB unavailable / racing writer), fall back to the
existing pending_title queue so the post-turn apply block remains a recovery
path. The sidebar's min-messages filter keeps a titled 0-message row hidden, so
a /title'd-but-never-used draft still doesn't clutter the list.

Updates the test that asserted the old queue-on-missing-row behavior and adds a
fallback-to-queue regression test.

Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…queuing (NousResearch#47987)

A /title typed before any message in a fresh desktop chat could be silently
lost: the session DB row is deferred to the first prompt, so session.title
found no row, only stashed pending_title, and returned pending:true. It then
relied on a post-turn apply block to write the title. When that turn never
landed under the same session_key (or the apply path didn't fire), the title
was dropped and the sidebar fell back to the first-message preview — e.g.
"/title my-custom-name" then "hello" left the session titled "hello".

Mirror the messaging gateway's _handle_title_command: an explicit /title is
clear user intent, not an abandoned draft, so create the row up front
(_ensure_session_db_row) and set the title immediately via the profile-aware
_session_db handle, returning pending:false. This also fixes the frontend
symptom for free — the desktop handler's immediate refreshSessions() now pulls
the correct persisted title instead of clobbering the optimistic value with a
still-NULL row.

If row creation can't take (DB unavailable / racing writer), fall back to the
existing pending_title queue so the post-turn apply block remains a recovery
path. The sidebar's min-messages filter keeps a titled 0-message row hidden, so
a /title'd-but-never-used draft still doesn't clutter the list.

Updates the test that asserted the old queue-on-missing-row behavior and adds a
fallback-to-queue regression test.

Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
…queuing (NousResearch#47987)

A /title typed before any message in a fresh desktop chat could be silently
lost: the session DB row is deferred to the first prompt, so session.title
found no row, only stashed pending_title, and returned pending:true. It then
relied on a post-turn apply block to write the title. When that turn never
landed under the same session_key (or the apply path didn't fire), the title
was dropped and the sidebar fell back to the first-message preview — e.g.
"/title my-custom-name" then "hello" left the session titled "hello".

Mirror the messaging gateway's _handle_title_command: an explicit /title is
clear user intent, not an abandoned draft, so create the row up front
(_ensure_session_db_row) and set the title immediately via the profile-aware
_session_db handle, returning pending:false. This also fixes the frontend
symptom for free — the desktop handler's immediate refreshSessions() now pulls
the correct persisted title instead of clobbering the optimistic value with a
still-NULL row.

If row creation can't take (DB unavailable / racing writer), fall back to the
existing pending_title queue so the post-turn apply block remains a recovery
path. The sidebar's min-messages filter keeps a titled 0-message row hidden, so
a /title'd-but-never-used draft still doesn't clutter the list.

Updates the test that asserted the old queue-on-missing-row behavior and adds a
fallback-to-queue regression test.

Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants