session title should be set by the AI on first chat #4

Closed
opened 2026-07-25 08:55:45 +00:00 by hugo · 1 comment
Owner

when a user starts a session, that session should get a title from the first chat of the user, decided by the AI - so a separate one-shot AI integration is done to summarize the first chat of the user into a session title.

Session title discovery must be a separate one-shot communication with the AI model that does not leave a session behind, as it is only meant to get a quick summary to show in the title. The one-shot communication happens outside the current session, so is not added there, too. this is the same one-shot mechanism as in #1

when a user starts a session, that session should get a title from the first chat of the user, decided by the AI - so a separate one-shot AI integration is done to summarize the first chat of the user into a session title. Session title discovery must be a separate one-shot communication with the AI model that does not leave a session behind, as it is only meant to get a quick summary to show in the title. The one-shot communication happens outside the current session, so is not added there, too. this is the same one-shot mechanism as in #1
hugo added the enhancement label 2026-07-25 08:55:45 +00:00
hugo changed title from session title should be set by the AI on first chat and sessions should only store in database with first chat to session title should be set by the AI on first chat 2026-07-25 09:48:56 +00:00
Author
Owner

Implemented in d456458.

The deferred-persistence half of this issue was already done in 51c1b2a: a new session lives only in the UI until its first chat turn is stored, so empty sessions never reach the database and are gone on restart. What this commit adds is the AI title on top of that, reusing the one-shot mechanism built for #1 exactly as this issue asks.

Trigger. start_generation already had to distinguish a draft from a stored session in order to persist it; that same branch now records whether this is the opening turn, and once the reply is queued it calls request_first_title. Titling therefore happens exactly once per session, on the turn that brings it into existence.

The one-shot is unchanged from #1. Same CheckpointTarget::OneShot against the shared transient KV cache, same reasoning_mode = Direct and 48-token cap, same session_title sanitizer (first non-empty line, quotes and markdown stripped, 60 characters max). No session row, no per-session checkpoint, nothing added to the conversation being titled.

Quiet and loud paths are now separated. request_title returns a Result instead of writing to the error banner, with two callers: retitle_session for the #1 menu action, which reports failures, and request_first_title for this automatic path, which swallows them. This mattered more than it first looked — without the split, a user who never asked for a title would have been shown "A title is already being generated." because a background job could not get a slot.

Empty turns are filtered out of the summary context. start_chat_turn inserts the assistant row empty, so at trigger time the stored conversation is the user's message plus a blank placeholder. Skipping empty content is correct in general, and it is what makes this pass summarize the opening message on its own, matching "summarize the first chat of the user".

A manual rename beats a queued title. TitleRequest now carries the title the session had when the request was queued; if it differs when the result arrives, the generated title is dropped. Without this, renaming a session while its title was in flight would have had the edit silently overwritten a minute later.

Three consequences worth recording for whoever touches this next:

  1. The one-shot evicts the session's in-memory KV state. Running outside the session means the executor resets, so the next turn in that session reloads its checkpoint from disk. This follows directly from the requirement that the one-shot happen outside the session, on a single-threaded runtime. It is a disk read rather than a re-prefill, since the checkpoint is saved after every turn. If it becomes noticeable on large contexts, the fix is to defer titling until the runtime is idle instead of queueing it immediately.

  2. Titles arrive after the reply, not during it. The request queues behind the generation on the single worker. This is deliberate: the alternative delays the user's answer in order to compute a title. The sidebar shows the "Session N" placeholder until the title lands.

  3. Concurrent first chats can skip a title. There is one titling slot, so starting a second new session while the first one's title is still generating skips the second silently. The window is a few seconds. Queueing them is easy if it ever matters; a queue seemed like the wrong thing to build for that window.

Gates: cargo fmt --check, cargo clippy --all-targets --all-features -D warnings, make bundle, cargo test --all-features (45 passing), all re-run on top of af0de86.

No new tests. The additions are App state transitions and database-backed lookups, and App can only be built through App::load, which opens the real database under Application Support — testing them would need a dependency-injection seam larger than the change. The sanitizer they feed is already covered by one_shot_titles_are_reduced_to_a_single_sidebar_line.

Not verified on screen: the app was not launched, so the title replacing the placeholder in the sidebar has not been watched happening.

Implemented in d456458. The deferred-persistence half of this issue was already done in 51c1b2a: a new session lives only in the UI until its first chat turn is stored, so empty sessions never reach the database and are gone on restart. What this commit adds is the AI title on top of that, reusing the one-shot mechanism built for #1 exactly as this issue asks. **Trigger.** `start_generation` already had to distinguish a draft from a stored session in order to persist it; that same branch now records whether this is the opening turn, and once the reply is queued it calls `request_first_title`. Titling therefore happens exactly once per session, on the turn that brings it into existence. **The one-shot is unchanged from #1.** Same `CheckpointTarget::OneShot` against the shared transient KV cache, same `reasoning_mode = Direct` and 48-token cap, same `session_title` sanitizer (first non-empty line, quotes and markdown stripped, 60 characters max). No session row, no per-session checkpoint, nothing added to the conversation being titled. **Quiet and loud paths are now separated.** `request_title` returns a `Result` instead of writing to the error banner, with two callers: `retitle_session` for the #1 menu action, which reports failures, and `request_first_title` for this automatic path, which swallows them. This mattered more than it first looked — without the split, a user who never asked for a title would have been shown "A title is already being generated." because a background job could not get a slot. **Empty turns are filtered out of the summary context.** `start_chat_turn` inserts the assistant row empty, so at trigger time the stored conversation is the user's message plus a blank placeholder. Skipping empty content is correct in general, and it is what makes this pass summarize the opening message on its own, matching "summarize the first chat of the user". **A manual rename beats a queued title.** `TitleRequest` now carries the title the session had when the request was queued; if it differs when the result arrives, the generated title is dropped. Without this, renaming a session while its title was in flight would have had the edit silently overwritten a minute later. Three consequences worth recording for whoever touches this next: 1. **The one-shot evicts the session's in-memory KV state.** Running outside the session means the executor resets, so the next turn in that session reloads its checkpoint from disk. This follows directly from the requirement that the one-shot happen outside the session, on a single-threaded runtime. It is a disk read rather than a re-prefill, since the checkpoint is saved after every turn. If it becomes noticeable on large contexts, the fix is to defer titling until the runtime is idle instead of queueing it immediately. 2. **Titles arrive after the reply, not during it.** The request queues behind the generation on the single worker. This is deliberate: the alternative delays the user's answer in order to compute a title. The sidebar shows the "Session N" placeholder until the title lands. 3. **Concurrent first chats can skip a title.** There is one titling slot, so starting a second new session while the first one's title is still generating skips the second silently. The window is a few seconds. Queueing them is easy if it ever matters; a queue seemed like the wrong thing to build for that window. Gates: `cargo fmt --check`, `cargo clippy --all-targets --all-features -D warnings`, `make bundle`, `cargo test --all-features` (45 passing), all re-run on top of af0de86. No new tests. The additions are `App` state transitions and database-backed lookups, and `App` can only be built through `App::load`, which opens the real database under Application Support — testing them would need a dependency-injection seam larger than the change. The sanitizer they feed is already covered by `one_shot_titles_are_reduced_to_a_single_sidebar_line`. Not verified on screen: the app was not launched, so the title replacing the placeholder in the sidebar has not been watched happening.
hugo closed this issue 2026-07-25 10:33:49 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: hugo/DS4Server#4