Carry conversations into new sessions with a visible summary

- Bots get continue_in_new_session: the bot writes a title and summary,
  the session closes for good (read-only) and the new one opens with the
  summary as a "Carried over from the last session" message that links
  back to the old session.
- Continue in new session (conversation menu, sessions modal) and the
  automatic rollover at the session length do the same; New session now
  starts a fresh, unlinked session.
- Session length is an app setting in Settings > General.
- ?session_id= opens a session; a carried-over one shows a bar linking to
  where the conversation went on instead of the composer.
- The thread can open the previous session above the current one (shown
  only, never in the prompt), and replies get search_past_sessions and
  read_past_session.
- Sessions are named from their opening messages, not their latest, and
  a carried summary is labelled as the earlier session's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-26 05:35:52 +01:00
co-authored by Claude Opus 5.5
parent 1e6833b5d8
commit cc109bc361
64 changed files with 2074 additions and 128 deletions
@@ -3,6 +3,9 @@ import type { ConversationHistory } from "@/src/types";
import grabActiveBotTask from "../bots/grab-active-bot-task";
import grabBotMessages from "../bots/grab-bot-messages";
import ensureBotSession from "./ensure-bot-session";
import grabCarriedToSessionId from "./grab-carried-to-session-id";
import reopenBotSession from "./reopen-bot-session";
import grabUserBotSession from "./grab-user-bot-session";
import grabUserConversation from "./grab-user-conversation";
import markBotSessionRead from "./mark-bot-session-read";
@@ -13,12 +16,20 @@ type Params = {
* The workspace the conversation is in; none for the bot's own chat.
*/
workspace_id?: number | string | null;
/**
* A session of the conversation to show instead of the active one
* (the page's `?session_id=`). One that isn't the user's, or is in
* another conversation, is ignored.
*/
session_id?: number | string | null;
};
/**
* A conversation's history, which the conversation pages load on the
* client once they render (`api/admin/bots/conversation-history`): the
* bot's active session there (marked read), its latest
* bot's active session there, or the one asked for (marked read; a closed
* one is reopened, unless it was carried over, which only shows, with
* where the conversation went on), its latest
* `AppData.ConversationFirstMessages` messages (the thread loads older
* ones as it scrolls) and the bot's active task in it. Everything else on
* the page comes with it from the server (`grabConversationProps`).
@@ -28,6 +39,7 @@ export default async function grabConversationHistory({
user_id,
bot_id,
workspace_id,
session_id,
}: Params): Promise<ConversationHistory | null> {
const conversation = await grabUserConversation({ user_id, bot_id, workspace_id }).catch(
() => null,
@@ -36,11 +48,37 @@ export default async function grabConversationHistory({
if (!conversation?.bot?.id) return null;
const open_bot_id = conversation.bot.id;
const session = await ensureBotSession({
user_id,
bot_id: open_bot_id,
workspace_id: conversation.workspace_id,
});
const conversation_workspace_id = conversation.workspace_id || null;
const asked = session_id
? await grabUserBotSession({ session_id, user_id })
: null;
const in_conversation =
asked?.id &&
Number(asked.bot_id) == Number(open_bot_id) &&
(asked.workspace_id ? Number(asked.workspace_id) : null) ==
(conversation_workspace_id ? Number(conversation_workspace_id) : null);
/**
* A closed session asked for is reopened, as clicking it in a list
* does, unless it was carried over into a newer one: that one only
* shows, with the way to where the conversation went on.
*/
const carried_to =
in_conversation && asked.status != "active"
? await grabCarriedToSessionId({ session_id: asked.id!, user_id })
: null;
const session = !in_conversation
? await ensureBotSession({
user_id,
bot_id: open_bot_id,
workspace_id: conversation.workspace_id,
})
: asked.status == "active" || carried_to
? asked
: await reopenBotSession({ session_id: asked.id!, user_id });
if (session.unread) {
await markBotSessionRead({ session_id: session.id!, user_id });
@@ -63,5 +101,6 @@ export default async function grabConversationHistory({
messages: has_more ? messages.slice(1) : messages,
has_more,
task,
moved_to_session_id: carried_to,
};
}