Name sessions opened by handed-off work, and hide empty ones from activity
A session the coordinator's delegation, a routine or a hook opened has no user message and no reply, so it was never named and showed as "Untitled conversation". Tasks now name their session too, and a session with no user message is named from the bot's own messages. Until a session is named, Latest activity shows what it opened with, and sessions with no messages are left out of it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -7,6 +7,7 @@ import generateText from "@/src/harness/text/generate-text";
|
||||
import { runWithHarnessUsageScope } from "@/src/harness/usage/harness-usage-scope";
|
||||
import parseSessionName from "./parse-session-name";
|
||||
import sessionNameTranscript from "./session-name-transcript";
|
||||
import sessionReadyToName from "./session-ready-to-name";
|
||||
|
||||
type Params = {
|
||||
session_id: number | string;
|
||||
@@ -16,7 +17,8 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Names a session once its first exchange is in: the bot writes the title
|
||||
* Names a session once its first exchange is in (or, with no user message,
|
||||
* once the bot has written in it: work handed to it): the bot writes the title
|
||||
* the rail and sessions list show and a short summary of it, from the
|
||||
* session's first messages (however late the first reply came). The summary
|
||||
* says how many messages it covers (`summary_message_count`), so
|
||||
@@ -51,12 +53,7 @@ export default async function nameBotSession({
|
||||
limit: AppData.BotSessionNameMessages,
|
||||
});
|
||||
|
||||
if (
|
||||
!messages.some((message) => message.author == "user") ||
|
||||
!messages.some((message) => message.author == "bot" && !message.carried_from_session_id)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!sessionReadyToName({ messages })) return false;
|
||||
|
||||
const transcript = sessionNameTranscript({ messages });
|
||||
|
||||
|
||||
@@ -235,9 +235,10 @@ export default async function processBotJob({
|
||||
* Once the first exchange is sent, the bot names the session and
|
||||
* previews it. After the reply is pushed, so it never waits on it;
|
||||
* a model that can't answer now leaves the session for the next
|
||||
* reply to name.
|
||||
* job to name. Tasks name too: a session the coordinator's
|
||||
* delegation or a routine opened has no reply to name it.
|
||||
*/
|
||||
if (job.kind == "reply" && !in_room && completion.message?.session_id) {
|
||||
if (!in_room && completion.message?.session_id) {
|
||||
/**
|
||||
* The message's session, not the job's as it was claimed: a
|
||||
* reply that moved the conversation on answered in the new one.
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import sessionReadyToName from "./session-ready-to-name";
|
||||
|
||||
describe("sessionReadyToName", () => {
|
||||
test("waits for the bot to answer the user", () => {
|
||||
expect(sessionReadyToName({ messages: [{ id: 1, author: "user", text: "Hi" }] })).toBe(false);
|
||||
});
|
||||
|
||||
test("names a first exchange", () => {
|
||||
expect(
|
||||
sessionReadyToName({
|
||||
messages: [
|
||||
{ id: 1, author: "user", text: "Hi" },
|
||||
{ id: 2, author: "bot", text: "Hello" },
|
||||
],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("names work handed to the bot, with no user message", () => {
|
||||
expect(
|
||||
sessionReadyToName({
|
||||
messages: [{ id: 1, author: "bot", text: "Coordinator asked me to: **SEO lookup**" }],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("a carried summary alone is not the bot's answer", () => {
|
||||
expect(
|
||||
sessionReadyToName({
|
||||
messages: [
|
||||
{ id: 1, author: "bot", text: "- Crew set up", carried_from_session_id: 9 },
|
||||
{ id: 2, author: "user", text: "Continue here" },
|
||||
],
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
sessionReadyToName({
|
||||
messages: [{ id: 1, author: "bot", text: "- Crew set up", carried_from_session_id: 9 }],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { SessionOpeningMessage } from "@/src/functions/backend/db/bot-sessions/grab-session-opening";
|
||||
|
||||
type Params = {
|
||||
messages: SessionOpeningMessage[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether a session's opening says enough to name it: the bot has said
|
||||
* something of its own (a carried summary doesn't count), and, when the
|
||||
* user wrote, after the user's first message. A session with no user
|
||||
* message at all is work handed to the bot (the coordinator's delegations,
|
||||
* routines, hooks), named from the bot's own messages.
|
||||
*/
|
||||
export default function sessionReadyToName({ messages }: Params) {
|
||||
const has_bot_message = messages.some(
|
||||
(message) => message.author == "bot" && !message.carried_from_session_id,
|
||||
);
|
||||
|
||||
if (!has_bot_message) return false;
|
||||
|
||||
const first_user = messages.findIndex((message) => message.author == "user");
|
||||
|
||||
if (first_user < 0) return true;
|
||||
|
||||
return messages
|
||||
.slice(first_user + 1)
|
||||
.some((message) => message.author == "bot" && !message.carried_from_session_id);
|
||||
}
|
||||
@@ -279,8 +279,8 @@ update_memory and update_instructions save to this workspace by default: what yo
|
||||
export const SessionNamePrompt = `Name and summarize this conversation from its first exchange. Answer with a JSON object only, no markdown fence and no other text:
|
||||
{"title": "...", "summary": "..."}
|
||||
|
||||
- title: what the conversation is about, at most 60 characters, no quotes and no trailing period. Say what the user wanted, not that it is a conversation. When it opens with a summary of an earlier session, name what this one carries on with, not every topic the summary recaps.
|
||||
- summary: what the user wanted and what was done so far. Keep names, numbers, paths and links exactly. Plain markdown bullets, no preamble.`;
|
||||
- title: what the conversation is about, at most 60 characters, no quotes and no trailing period. Say what the user wanted, not that it is a conversation. With no user message, it is work handed to you (by the coordinator, a schedule or a hook): name that work. When it opens with a summary of an earlier session, name what this one carries on with, not every topic the summary recaps.
|
||||
- summary: what the user (or whoever handed you the work) wanted and what was done so far. Keep names, numbers, paths and links exactly. Plain markdown bullets, no preamble.`;
|
||||
|
||||
/**
|
||||
* Asks the text model to carry a conversation into a new session when the
|
||||
|
||||
@@ -10,8 +10,8 @@ type Params = {
|
||||
|
||||
/**
|
||||
* The user's latest conversations in a workspace, most recent first, each
|
||||
* with the bot it's with (its face as the user sees it), its message count and latest message. Empty
|
||||
* sessions are left out unless active.
|
||||
* with the bot it's with (its face as the user sees it), its message count, first and latest message. Sessions
|
||||
* with no messages yet are left out, active or not.
|
||||
*/
|
||||
export default async function grabWorkspaceActivity({
|
||||
user_id,
|
||||
@@ -28,11 +28,12 @@ export default async function grabWorkspaceActivity({
|
||||
b.stone AS bot_stone,
|
||||
${face.sql},
|
||||
(SELECT COUNT(*) FROM bot_messages m WHERE m.session_id = s.id) AS message_count,
|
||||
(SELECT m.text FROM bot_messages m WHERE m.session_id = s.id ORDER BY m.created_at DESC, m.id DESC LIMIT 1) AS last_message_text
|
||||
(SELECT m.text FROM bot_messages m WHERE m.session_id = s.id ORDER BY m.created_at DESC, m.id DESC LIMIT 1) AS last_message_text,
|
||||
(SELECT m.text FROM bot_messages m WHERE m.session_id = s.id ORDER BY m.created_at ASC, m.id ASC LIMIT 1) AS first_message_text
|
||||
FROM bot_sessions s
|
||||
JOIN bots b ON b.id = s.bot_id
|
||||
WHERE s.workspace_id = ? AND s.user_id = ? AND s.chatroom_id IS NULL
|
||||
AND (s.status = 'active' OR EXISTS (SELECT 1 FROM bot_messages m WHERE m.session_id = s.id))
|
||||
AND EXISTS (SELECT 1 FROM bot_messages m WHERE m.session_id = s.id)
|
||||
AND (b.kind = 'coordinator' OR EXISTS (
|
||||
SELECT 1 FROM workspace_bots wb WHERE wb.workspace_id = s.workspace_id AND wb.bot_id = s.bot_id
|
||||
))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import BotAvatar from "@/src/components/general/bot-avatar";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import type { BotFace } from "@/src/dict/bot-dict";
|
||||
import botStoneProps from "@/src/utils/bot-stone-props";
|
||||
import SettingsListRow from "@/src/pages/admin/settings/(partials)/settings-list-row";
|
||||
@@ -13,14 +14,18 @@ type Props = {
|
||||
};
|
||||
|
||||
/**
|
||||
* One recent conversation in the workspace: the bot, the session's title,
|
||||
* a preview of its latest message and when it last moved.
|
||||
* One recent conversation in the workspace: the bot, the session's title
|
||||
* (what it opened with until it's named), a preview of its latest message
|
||||
* and when it last moved.
|
||||
*/
|
||||
export default function WorkspaceActivityRow({ workspace_id, item }: Props) {
|
||||
const at = item.last_activity_at || item.created_at;
|
||||
const preview = item.last_message_text
|
||||
? messagePreview({ text: item.last_message_text })
|
||||
: "";
|
||||
const opening = item.first_message_text
|
||||
? messagePreview({ text: item.first_message_text }).slice(0, AppData.BotSessionTitleMaxChars)
|
||||
: "";
|
||||
|
||||
return (
|
||||
<SettingsListRow
|
||||
@@ -31,7 +36,7 @@ export default function WorkspaceActivityRow({ workspace_id, item }: Props) {
|
||||
face={(item.bot_face || "idle") as BotFace}
|
||||
/>
|
||||
}
|
||||
title={item.title || "Untitled conversation"}
|
||||
title={item.title || opening || "Untitled conversation"}
|
||||
href={`/admin/workspaces/${workspace_id}/bots/${item.bot_id}`}
|
||||
badge={item.unread ? "New" : undefined}
|
||||
subtitle={`${item.bot_name || "A bot"}${preview ? ` · ${preview}` : ""}`}
|
||||
|
||||
@@ -275,6 +275,10 @@ export type WorkspaceWithActivity = BUN_SQLITE_COBBLE_WORKSPACES & {
|
||||
*/
|
||||
export type WorkspaceActivityItem = BUN_SQLITE_COBBLE_BOT_SESSIONS & {
|
||||
message_count: number;
|
||||
/**
|
||||
* What the session opened with, shown while it has no title yet.
|
||||
*/
|
||||
first_message_text?: string | null;
|
||||
last_message_text?: string | null;
|
||||
bot_name?: string | null;
|
||||
bot_image_media_id?: number | null;
|
||||
|
||||
Reference in New Issue
Block a user