Name sessions with the model and keep the rail list live
Sessions no longer take their title from the first message: once the first exchange is sent, the text model writes the title and summary, which the rail and sessions list show. The summary records how many messages it covers so a stale one is rewritten before it is carried into the next session. The sessions hook now reloads on live message and session events, so a session started by the first message, a rollover or another tab appears without a reload. New sessions are pushed as server:bot-session, both from the process that changed them and through the live relay.
This commit is contained in:
@@ -14,8 +14,10 @@ type Params = {
|
||||
|
||||
/**
|
||||
* The previous session's summary for a session's prompt (markdown), or ""
|
||||
* when it doesn't carry on from one. The summary is written the first
|
||||
* time it's needed and kept on that session.
|
||||
* when it doesn't carry on from one. The summary written as the session
|
||||
* went covers only its first exchange, so a closed session whose summary
|
||||
* is stale (it covers fewer messages than the session ended with) is
|
||||
* summarized again from its latest messages, and kept.
|
||||
*/
|
||||
export default async function grabSessionCarryOver({
|
||||
session_id,
|
||||
@@ -24,8 +26,10 @@ export default async function grabSessionCarryOver({
|
||||
signal,
|
||||
}: Params) {
|
||||
const previous = (
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_SESSIONS>({
|
||||
sql: `SELECT p.* FROM bot_sessions s JOIN bot_sessions p ON p.id = s.previous_session_id
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_SESSIONS & { message_count: number }>({
|
||||
sql: `SELECT p.*,
|
||||
(SELECT COUNT(*) FROM bot_messages m WHERE m.session_id = p.id) AS message_count
|
||||
FROM bot_sessions s JOIN bot_sessions p ON p.id = s.previous_session_id
|
||||
WHERE s.id = ? AND p.bot_id = s.bot_id LIMIT 1`,
|
||||
values: [session_id],
|
||||
})
|
||||
@@ -33,7 +37,12 @@ export default async function grabSessionCarryOver({
|
||||
|
||||
if (!previous?.id) return "";
|
||||
|
||||
if (previous.summary?.trim()) {
|
||||
const message_count = Number(previous.message_count) || 0;
|
||||
|
||||
if (
|
||||
previous.summary?.trim() &&
|
||||
Number(previous.summary_message_count) >= message_count
|
||||
) {
|
||||
return `${PreviousSessionNote}\n\n${previous.summary.trim()}`;
|
||||
}
|
||||
|
||||
@@ -56,15 +65,26 @@ export default async function grabSessionCarryOver({
|
||||
bot_id,
|
||||
workspace_id,
|
||||
signal,
|
||||
}).catch((error: unknown) => {
|
||||
if (signal?.aborted) throw error;
|
||||
return null;
|
||||
});
|
||||
|
||||
const summary = (result.text || "").trim().slice(0, AppData.BotSessionSummaryMaxChars);
|
||||
const summary = (result?.text || "").trim().slice(0, AppData.BotSessionSummaryMaxChars);
|
||||
|
||||
if (!summary) return "";
|
||||
/**
|
||||
* A model that can't rewrite it now must not lose what the earlier
|
||||
* one already said.
|
||||
*/
|
||||
if (!summary) {
|
||||
return previous.summary?.trim()
|
||||
? `${PreviousSessionNote}\n\n${previous.summary.trim()}`
|
||||
: "";
|
||||
}
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bot_sessions SET summary = ?, updated_at = ? WHERE id = ?`,
|
||||
values: [summary, Date.now(), previous.id],
|
||||
sql: `UPDATE bot_sessions SET summary = ?, summary_message_count = ?, updated_at = ? WHERE id = ?`,
|
||||
values: [summary, message_count, Date.now(), previous.id],
|
||||
});
|
||||
|
||||
return `${PreviousSessionNote}\n\n${summary}`;
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { BUN_SQLITE_COBBLE_BOT_SESSIONS } from "@/db/types/db";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import { SessionNamePrompt } from "@/src/data/harness-prompts";
|
||||
import grabBotMessages from "@/src/functions/backend/db/bots/grab-bot-messages";
|
||||
import generateText from "@/src/harness/text/generate-text";
|
||||
import parseSessionName from "./parse-session-name";
|
||||
|
||||
type Params = {
|
||||
session_id: number | string;
|
||||
bot_id: number | string;
|
||||
workspace_id?: number | string | null;
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
/**
|
||||
* Names a session once its first exchange is in: the bot writes the title
|
||||
* the rail and sessions list show and a short summary of it. The summary
|
||||
* says how many messages it covers (`summary_message_count`), so
|
||||
* `grabSessionCarryOver` rewrites it before carrying a closed session into
|
||||
* the next one. Already named sessions are left as they are.
|
||||
*
|
||||
* Returns whether it wrote something.
|
||||
*/
|
||||
export default async function nameBotSession({
|
||||
session_id,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
signal,
|
||||
}: Params): Promise<boolean> {
|
||||
const session = (
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_SESSIONS>({
|
||||
sql: `SELECT id, title, summary FROM bot_sessions WHERE id = ? LIMIT 1`,
|
||||
values: [session_id],
|
||||
})
|
||||
).payload?.[0];
|
||||
|
||||
if (!session?.id) return false;
|
||||
if (session.title?.trim() && session.summary?.trim()) return false;
|
||||
|
||||
const messages = await grabBotMessages({
|
||||
session_id,
|
||||
limit: AppData.BotSessionNameMessages,
|
||||
});
|
||||
|
||||
if (
|
||||
!messages.some((message) => message.author == "user") ||
|
||||
!messages.some((message) => message.author == "bot")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const transcript = messages
|
||||
.map(
|
||||
(message) =>
|
||||
`${message.author == "user" ? "User" : "You"}: ${message.text || "(files only)"}`,
|
||||
)
|
||||
.join("\n\n");
|
||||
|
||||
const result = await generateText({
|
||||
messages: [
|
||||
{ role: "system", content: SessionNamePrompt },
|
||||
{ role: "user", content: transcript },
|
||||
],
|
||||
bot_id,
|
||||
workspace_id,
|
||||
signal,
|
||||
});
|
||||
|
||||
const name = parseSessionName({ text: result.text });
|
||||
|
||||
if (!name) return false;
|
||||
|
||||
const now = Date.now();
|
||||
const title = name.title.replace(/\s+/g, " ").trim().slice(0, AppData.BotSessionTitleMaxChars);
|
||||
const summary = name.summary.slice(0, AppData.BotSessionSummaryMaxChars);
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bot_sessions SET
|
||||
title = CASE WHEN COALESCE(title, '') = '' THEN ? ELSE title END,
|
||||
summary = ?,
|
||||
summary_message_count = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?`,
|
||||
values: [title, summary, messages.length, now, session_id],
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import parseSessionName from "./parse-session-name";
|
||||
|
||||
describe("parseSessionName", () => {
|
||||
test("reads a title and summary", () => {
|
||||
expect(parseSessionName({ text: `{"title":"Tidy invoices","summary":"- sorted them"}` })).toEqual({
|
||||
title: "Tidy invoices",
|
||||
summary: "- sorted them",
|
||||
});
|
||||
});
|
||||
|
||||
test("tolerates a code fence around the object", () => {
|
||||
expect(
|
||||
parseSessionName({
|
||||
text: "```json\n{\"title\":\"Plans\",\"summary\":\"- a trip\"}\n```",
|
||||
}),
|
||||
).toEqual({ title: "Plans", summary: "- a trip" });
|
||||
});
|
||||
|
||||
test("is null when either part is missing or the JSON is bad", () => {
|
||||
expect(parseSessionName({})).toBeNull();
|
||||
expect(parseSessionName({ text: "" })).toBeNull();
|
||||
expect(parseSessionName({ text: `{"title":"Only a title"}` })).toBeNull();
|
||||
expect(parseSessionName({ text: `{"title":"","summary":"- a trip"}` })).toBeNull();
|
||||
expect(parseSessionName({ text: "{nope" })).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
type Params = {
|
||||
text?: string | null;
|
||||
};
|
||||
|
||||
export type SessionName = {
|
||||
title: string;
|
||||
summary: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads the JSON a naming model answers with: its title and summary.
|
||||
* Tolerates a surrounding code fence or a sentence around the object.
|
||||
* Null when either is missing.
|
||||
*/
|
||||
export default function parseSessionName({ text }: Params): SessionName | null {
|
||||
if (!text) return null;
|
||||
|
||||
const start = text.indexOf("{");
|
||||
const end = text.lastIndexOf("}");
|
||||
|
||||
if (start < 0 || end <= start) return null;
|
||||
|
||||
try {
|
||||
const value = JSON.parse(text.slice(start, end + 1));
|
||||
|
||||
if (!value || typeof value != "object" || Array.isArray(value)) return null;
|
||||
|
||||
const title = typeof value.title == "string" ? value.title.trim() : "";
|
||||
const summary = typeof value.summary == "string" ? value.summary.trim() : "";
|
||||
|
||||
return title && summary ? { title, summary } : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import pushBotNotice from "./push-bot-notice";
|
||||
import grabJobUserId from "@/src/functions/backend/db/bots-queue/grab-job-user-id";
|
||||
import runBotJob from "./run-bot-job";
|
||||
import parseJobJson from "./parse-job-json";
|
||||
import nameBotSession from "./name-bot-session";
|
||||
import pushBotSession from "./push-bot-session";
|
||||
import type { BotJobUpdateData } from "@/src/types";
|
||||
import type { BotJobContext, BotJobRunner } from "./types";
|
||||
|
||||
@@ -160,16 +162,48 @@ export default async function processBotJob({
|
||||
|
||||
if (controller.signal.aborted) return;
|
||||
|
||||
await pushBotNotice({
|
||||
bot_id,
|
||||
...(await completeBotJob({
|
||||
job_id,
|
||||
worker,
|
||||
result: outcome.result,
|
||||
needs_you: outcome.needs_you,
|
||||
silent: outcome.silent,
|
||||
})),
|
||||
const completion = await completeBotJob({
|
||||
job_id,
|
||||
worker,
|
||||
result: outcome.result,
|
||||
needs_you: outcome.needs_you,
|
||||
silent: outcome.silent,
|
||||
});
|
||||
|
||||
await pushBotNotice({ bot_id, ...completion });
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
if (job.kind == "reply" && completion.message && job.session_id) {
|
||||
const session_id = Number(job.session_id);
|
||||
|
||||
/**
|
||||
* No job signal: the job is already complete, so its heartbeat
|
||||
* no longer holds a lock and would abort this naming turn.
|
||||
*/
|
||||
const named = await nameBotSession({
|
||||
session_id,
|
||||
bot_id,
|
||||
workspace_id: job.workspace_id,
|
||||
}).catch(async (error: unknown) => {
|
||||
await logError({ title: `Bot session #${session_id} not named`, error }).catch(
|
||||
() => {},
|
||||
);
|
||||
return false;
|
||||
});
|
||||
|
||||
if (named) {
|
||||
await pushBotSession({
|
||||
session_id,
|
||||
bot_id,
|
||||
workspace_id: job.workspace_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (controller.signal.aborted) return;
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { BotSessionChangedData } from "@/src/types";
|
||||
import pushToSessionUser from "./push-to-session-user";
|
||||
|
||||
type Params = {
|
||||
session_id: number | string;
|
||||
bot_id: number | string;
|
||||
workspace_id?: number | string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tells the open tabs of the user a session belongs to that it started or
|
||||
* changed, so the rail and the sessions list reload it.
|
||||
*/
|
||||
export default async function pushBotSession({
|
||||
session_id,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
}: Params) {
|
||||
const data: BotSessionChangedData = {
|
||||
session_id: Number(session_id),
|
||||
bot_id: Number(bot_id),
|
||||
workspace_id: workspace_id ? Number(workspace_id) : null,
|
||||
};
|
||||
|
||||
await pushToSessionUser({
|
||||
session_id,
|
||||
data: { event: "server:bot-session", data },
|
||||
});
|
||||
}
|
||||
@@ -238,6 +238,12 @@ export const AppData = {
|
||||
*/
|
||||
BotSessionSummaryMessages: 80,
|
||||
BotSessionSummaryMaxChars: 4000,
|
||||
/**
|
||||
* The first messages of a session the bot reads to name and summarize
|
||||
* it once the first reply is in, and how long its title may be.
|
||||
*/
|
||||
BotSessionNameMessages: 4,
|
||||
BotSessionTitleMaxChars: 80,
|
||||
/**
|
||||
* Sessions listed per page in a conversation's sessions modal.
|
||||
*/
|
||||
|
||||
@@ -61,9 +61,21 @@ export const WorkspaceLearningNote = `# Learning in this workspace
|
||||
|
||||
update_memory and update_instructions save to this workspace by default: what you learn here applies here. What you learn about the user that holds outside this workspace goes in your memory of them (update_memory, scope "user"). Save to your core memory or instructions (scope "core") only when the user asks for it to apply in every workspace, or when it's plainly about how you work everywhere, not about this workspace. Your core memory is shared with every user you work for, so nothing about this user goes there. When something learned here looks useful everywhere, suggest saving it and do it once the user agrees.`;
|
||||
|
||||
/**
|
||||
* Asks the text model for a session's name and summary, written once the
|
||||
* first exchange is in. The title labels it in the rail and the sessions
|
||||
* list; the summary previews it and is carried into the next session.
|
||||
*/
|
||||
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.
|
||||
- summary: what the user wanted and what was done so far. Keep names, numbers, paths and links exactly. Plain markdown bullets, no preamble.`;
|
||||
|
||||
/**
|
||||
* Asks the text model for a closed session's summary, carried into the
|
||||
* next session's prompt.
|
||||
* next session's prompt. Used when the session's own summary is stale
|
||||
* (it covers fewer messages than the session now has).
|
||||
*/
|
||||
export const SessionSummaryPrompt = `Summarize this conversation for whoever picks it up next: what the user wanted, what was done, decisions made, open questions and anything still in progress. Keep names, numbers, paths and links exactly. Plain markdown bullets, no preamble.`;
|
||||
|
||||
|
||||
@@ -14,8 +14,9 @@ type Params = {
|
||||
|
||||
/**
|
||||
* Closes the conversation's active session and starts the next one, which
|
||||
* carries on from it (its summary is written when the next reply runs).
|
||||
* An active session with no messages yet is kept as it is.
|
||||
* carries on from it (its summary goes into the next reply's prompt,
|
||||
* rewritten first when it covers fewer messages than the session ended
|
||||
* with). An active session with no messages yet is kept as it is.
|
||||
*/
|
||||
export default async function startBotSession({
|
||||
user_id,
|
||||
|
||||
@@ -24,8 +24,9 @@ type Params = {
|
||||
|
||||
/**
|
||||
* Appends a user message (and its files) to a session and bumps its
|
||||
* activity. The first one titles the session. Callers must check the bot
|
||||
* and session belong to the session user first.
|
||||
* activity. The session is named later, from its first exchange with the
|
||||
* bot (`nameBotSession`). Callers must check the bot and session belong to
|
||||
* the session user first.
|
||||
*/
|
||||
export default async function insertUserMessage({
|
||||
bot_id,
|
||||
@@ -66,13 +67,9 @@ export default async function insertUserMessage({
|
||||
});
|
||||
}
|
||||
|
||||
const title = (text || media[0]?.title || "").replace(/\s+/g, " ").trim().slice(0, 80);
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bot_sessions SET last_activity_at = ?,
|
||||
title = CASE WHEN COALESCE(title, '') = '' THEN ? ELSE title END
|
||||
WHERE id = ?`,
|
||||
values: [now, title, session_id],
|
||||
sql: `UPDATE bot_sessions SET last_activity_at = ? WHERE id = ?`,
|
||||
values: [now, session_id],
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { BotSessionListItem } from "@/src/types";
|
||||
import type { BotSessionChangedData, BotSessionListItem } from "@/src/types";
|
||||
import postSessionApi from "@/src/functions/frontend/post-session-api";
|
||||
import useServerEvent from "./use-server-event";
|
||||
|
||||
type Params = {
|
||||
bot_id: number;
|
||||
@@ -15,9 +16,37 @@ type Params = {
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
type ChangedSession = {
|
||||
bot_id?: number | string | null;
|
||||
workspace_id?: number | string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether a pushed message or session change belongs to this conversation:
|
||||
* the same bot, in its own chat or in the same workspace.
|
||||
*/
|
||||
function belongsToConversation({
|
||||
session,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
}: {
|
||||
session: ChangedSession;
|
||||
bot_id: number;
|
||||
workspace_id?: number | null;
|
||||
}) {
|
||||
return (
|
||||
Number(session.bot_id) == bot_id &&
|
||||
(session.workspace_id ? Number(session.workspace_id) : null) ==
|
||||
(workspace_id ? Number(workspace_id) : null)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A conversation's sessions, latest first, searched as the user types
|
||||
* (debounced). Starting or reopening a session reloads the page onto it.
|
||||
* New messages and session changes pushed live reload the list, so a
|
||||
* session started in the conversation (or named once its first reply is
|
||||
* in) shows without a reload.
|
||||
*/
|
||||
export default function useBotSessions({ bot_id, workspace_id, open, limit }: Params) {
|
||||
const [query, setQuery] = useState("");
|
||||
@@ -25,6 +54,31 @@ export default function useBotSessions({ bot_id, workspace_id, open, limit }: Pa
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [busy_id, setBusyId] = useState<number | "new">();
|
||||
const [error, setError] = useState<string>();
|
||||
const [revision, setRevision] = useState(0);
|
||||
|
||||
useServerEvent({
|
||||
event: "server:bot-message",
|
||||
onData: (message: BotSessionListItem) => {
|
||||
if (
|
||||
message &&
|
||||
belongsToConversation({ session: message, bot_id, workspace_id })
|
||||
) {
|
||||
setRevision((value) => value + 1);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useServerEvent({
|
||||
event: "server:bot-session",
|
||||
onData: (session: BotSessionChangedData) => {
|
||||
if (
|
||||
session &&
|
||||
belongsToConversation({ session, bot_id, workspace_id })
|
||||
) {
|
||||
setRevision((value) => value + 1);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -54,7 +108,7 @@ export default function useBotSessions({ bot_id, workspace_id, open, limit }: Pa
|
||||
stale = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [open, query, bot_id, workspace_id, limit]);
|
||||
}, [open, query, bot_id, workspace_id, limit, revision]);
|
||||
|
||||
const act = async ({ id, path, body }: { id: number | "new"; path: "new-session" | "reopen-session"; body: Record<string, unknown> }) => {
|
||||
if (busy_id) return;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AppData } from "@/src/data/app-data";
|
||||
import { logError } from "@/src/functions/backend/logger";
|
||||
import grabConnectedUserIds from "@/src/websocket/(functions)/grab-connected-user-ids";
|
||||
import relayChangedBots from "./relay-changed-bots";
|
||||
import relayChangedSessions from "./relay-changed-sessions";
|
||||
import relayCreatedBots from "./relay-created-bots";
|
||||
import relayCreatedWorkspaces from "./relay-created-workspaces";
|
||||
import relayNewBotMessages from "./relay-new-bot-messages";
|
||||
@@ -32,6 +33,7 @@ export default async function liveRelay() {
|
||||
message_id: await latest("bot_messages"),
|
||||
update_id: await latest("bots_queue_updates"),
|
||||
bots_at: Date.now(),
|
||||
sessions_at: Date.now(),
|
||||
};
|
||||
|
||||
let running = false;
|
||||
@@ -58,6 +60,12 @@ export default async function liveRelay() {
|
||||
await relayCreatedWorkspaces({ since: bots_since, users });
|
||||
await relayChangedBots({ since: bots_since, users });
|
||||
cursor.bots_at = now;
|
||||
|
||||
await relayChangedSessions({
|
||||
since: cursor.sessions_at - AppData.LiveRelayPollMs,
|
||||
users,
|
||||
});
|
||||
cursor.sessions_at = now;
|
||||
} catch (error) {
|
||||
await logError({ title: `Live relay failed`, error }).catch(() => {});
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_BOT_SESSIONS } from "@/db/types/db";
|
||||
import type { BotSessionChangedData } from "@/src/types";
|
||||
import { sendToUser } from "@/src/websocket/(functions)/send-to-user";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* Unix ms: sessions updated since then are pushed.
|
||||
*/
|
||||
since: number;
|
||||
users: Set<string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pushes `server:bot-session` for every session created or changed since
|
||||
* `since` (named, reopened, marked read) to the connected user whose
|
||||
* conversation it's in. Pushes are idempotent, so overlapping windows only
|
||||
* repeat them.
|
||||
*/
|
||||
export default async function relayChangedSessions({ since, users }: Params) {
|
||||
if (!users.size) return;
|
||||
|
||||
const sessions =
|
||||
(
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_SESSIONS>({
|
||||
sql: `SELECT id, user_id, bot_id, workspace_id FROM bot_sessions WHERE updated_at > ?`,
|
||||
values: [since],
|
||||
})
|
||||
).payload || [];
|
||||
|
||||
for (const session of sessions) {
|
||||
if (!session.id || !session.user_id || !users.has(String(session.user_id))) continue;
|
||||
|
||||
const data: BotSessionChangedData = {
|
||||
session_id: Number(session.id),
|
||||
bot_id: Number(session.bot_id) || 0,
|
||||
workspace_id: session.workspace_id ? Number(session.workspace_id) : null,
|
||||
};
|
||||
|
||||
sendToUser({
|
||||
user_id: session.user_id,
|
||||
data: { event: "server:bot-session", data },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* How far the relay has read: the newest message and job step it pushed,
|
||||
* and when it last looked for changed bots.
|
||||
* and when it last looked for changed bots and sessions.
|
||||
*/
|
||||
export type LiveRelayCursor = {
|
||||
message_id: number;
|
||||
update_id: number;
|
||||
bots_at: number;
|
||||
sessions_at: number;
|
||||
};
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function ComposerTools({ bot, workspace_id, initial }: Props) {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{has_tools ? (
|
||||
{has_tools && computers.length ? (
|
||||
<Span className="-mr-2 opacity-40">
|
||||
<Dot size={20} />
|
||||
</Span>
|
||||
|
||||
@@ -164,6 +164,16 @@ export type BotJobUpdateData = BUN_SQLITE_COBBLE_BOTS_QUEUE_UPDATES & {
|
||||
session_id: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* `server:bot-session` data: one of the user's conversation sessions was
|
||||
* started, named or otherwise changed.
|
||||
*/
|
||||
export type BotSessionChangedData = {
|
||||
session_id: number;
|
||||
bot_id: number;
|
||||
workspace_id: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* `server:bot-created` data: a bot the user can now see, the workspaces
|
||||
* it's in, and whether it belongs in their own chat list (the main rail).
|
||||
@@ -812,6 +822,11 @@ export const WebSocketEvents = [
|
||||
* `bot_messages` row)
|
||||
*/
|
||||
"server:bot-message",
|
||||
/**
|
||||
* A session in one of the user's bot conversations started, was named
|
||||
* or otherwise changed (`data`: `BotSessionChangedData`)
|
||||
*/
|
||||
"server:bot-session",
|
||||
/**
|
||||
* A bot joined the crew or one of the user's workspaces (`data`:
|
||||
* `BotCreatedData`)
|
||||
|
||||
Reference in New Issue
Block a user