Share one crew across the install, keep user memory per user
Bots, workspaces and computers now belong to the install. Users reach workers only through bot_users (assigned by a super admin or admin) and workspace_users; the coordinator and main computer are one per install, and only the super admin sees the coordinator. Conversations are per user (bot_sessions.user_id), jobs run for the session's user, and live updates go only to who should see them. A bot's core memory is now general knowledge shared by every user it works for; what it learns about a user lives in bot_user_memories. One-off scripts move existing installs over: move-bot-memory-to-owners (run first), then share-crew. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -5,6 +5,10 @@ import type {
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The user the conversation is with.
|
||||
*/
|
||||
user_id: number | string;
|
||||
bot_id: number | string;
|
||||
workspace_id?: number | string | null;
|
||||
/**
|
||||
@@ -14,10 +18,11 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Starts an active session in a conversation. Callers close the old one
|
||||
* Starts an active session in a user's conversation. Callers close the old one
|
||||
* first (`startBotSession`).
|
||||
*/
|
||||
export default async function createBotSession({
|
||||
user_id,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
previous_session_id,
|
||||
@@ -25,6 +30,7 @@ export default async function createBotSession({
|
||||
const now = Date.now();
|
||||
|
||||
const row: BUN_SQLITE_COBBLE_BOT_SESSIONS = {
|
||||
user_id: Number(user_id),
|
||||
bot_id: Number(bot_id),
|
||||
workspace_id: workspace_id ? Number(workspace_id) : undefined,
|
||||
previous_session_id: previous_session_id || undefined,
|
||||
|
||||
@@ -6,6 +6,10 @@ import grabActiveBotSession from "./grab-active-bot-session";
|
||||
import startBotSession from "./start-bot-session";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The user the conversation is with.
|
||||
*/
|
||||
user_id: number | string;
|
||||
bot_id: number | string;
|
||||
workspace_id?: number | string | null;
|
||||
/**
|
||||
@@ -17,16 +21,17 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* The conversation's active session, started first when there is none
|
||||
* The user's conversation's active session, started first when there is none
|
||||
* (carrying on from its latest closed one) or, with `rollover`, when the
|
||||
* active one is full.
|
||||
*/
|
||||
export default async function ensureBotSession({
|
||||
user_id,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
rollover,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_BOT_SESSIONS> {
|
||||
const active = await grabActiveBotSession({ bot_id, workspace_id });
|
||||
const active = await grabActiveBotSession({ user_id, bot_id, workspace_id });
|
||||
|
||||
if (active?.id) {
|
||||
if (!rollover) return active;
|
||||
@@ -43,18 +48,19 @@ export default async function ensureBotSession({
|
||||
|
||||
return count < AppData.BotSessionMaxMessages
|
||||
? active
|
||||
: await startBotSession({ bot_id, workspace_id });
|
||||
: await startBotSession({ user_id, bot_id, workspace_id });
|
||||
}
|
||||
|
||||
const latest = (
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_SESSIONS>({
|
||||
sql: `SELECT id FROM bot_sessions WHERE bot_id = ? AND workspace_id IS ?
|
||||
sql: `SELECT id FROM bot_sessions WHERE user_id = ? AND bot_id = ? AND workspace_id IS ?
|
||||
ORDER BY last_activity_at DESC, id DESC LIMIT 1`,
|
||||
values: [bot_id, workspace_id ? Number(workspace_id) : null],
|
||||
values: [user_id, bot_id, workspace_id ? Number(workspace_id) : null],
|
||||
})
|
||||
).payload?.[0];
|
||||
|
||||
return await createBotSession({
|
||||
user_id,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
previous_session_id: latest?.id || null,
|
||||
|
||||
@@ -2,6 +2,10 @@ import type { BUN_SQLITE_COBBLE_BOT_SESSIONS } from "@/db/types/db";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The user the conversation is with.
|
||||
*/
|
||||
user_id: number | string;
|
||||
bot_id: number | string;
|
||||
/**
|
||||
* Null or undefined: the bot's own chat.
|
||||
@@ -10,18 +14,19 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* The active session of one conversation (a bot in its own chat or in one
|
||||
* workspace), or null when it has none yet.
|
||||
* The active session of one user's conversation (with a bot in its own
|
||||
* chat or in one workspace), or null when it has none yet.
|
||||
*/
|
||||
export default async function grabActiveBotSession({
|
||||
user_id,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_BOT_SESSIONS | null> {
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_SESSIONS>({
|
||||
sql: `SELECT * FROM bot_sessions
|
||||
WHERE bot_id = ? AND workspace_id IS ? AND status = 'active'
|
||||
WHERE user_id = ? AND bot_id = ? AND workspace_id IS ? AND status = 'active'
|
||||
ORDER BY id DESC LIMIT 1`,
|
||||
values: [bot_id, workspace_id ? Number(workspace_id) : null],
|
||||
values: [user_id, bot_id, workspace_id ? Number(workspace_id) : null],
|
||||
});
|
||||
|
||||
return res.payload?.[0] || null;
|
||||
|
||||
@@ -3,6 +3,10 @@ import { AppData } from "@/src/data/app-data";
|
||||
import type { BotSessionListItem } from "@/src/types";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The user the conversation is with.
|
||||
*/
|
||||
user_id: number | string;
|
||||
bot_id: number | string;
|
||||
workspace_id?: number | string | null;
|
||||
/**
|
||||
@@ -13,11 +17,12 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A conversation's sessions for the sessions modal, latest activity first,
|
||||
* A user's conversation's sessions for the sessions modal, latest activity first,
|
||||
* each with its message count and latest message. Empty sessions are left
|
||||
* out unless active.
|
||||
*/
|
||||
export default async function grabBotSessions({
|
||||
user_id,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
query,
|
||||
@@ -33,7 +38,7 @@ export default async function grabBotSessions({
|
||||
sql += ` (SELECT COUNT(*) FROM bot_messages m WHERE m.session_id = s.id) AS message_count,\n`;
|
||||
sql += ` (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\n`;
|
||||
sql += `FROM bot_sessions s\n`;
|
||||
sql += `WHERE s.bot_id = ? AND s.workspace_id IS ?\n`;
|
||||
sql += `WHERE s.user_id = ? AND s.bot_id = ? AND s.workspace_id IS ?\n`;
|
||||
sql += `AND (s.status = 'active' OR EXISTS (SELECT 1 FROM bot_messages m WHERE m.session_id = s.id))\n`;
|
||||
|
||||
if (like) {
|
||||
@@ -47,6 +52,7 @@ export default async function grabBotSessions({
|
||||
const res = await BunSQLite.sql<BotSessionListItem>({
|
||||
sql,
|
||||
values: [
|
||||
user_id,
|
||||
bot_id,
|
||||
workspace_id ? Number(workspace_id) : null,
|
||||
...(like ? [like, like, like] : []),
|
||||
|
||||
@@ -24,7 +24,7 @@ type Params = {
|
||||
* `/admin/workspaces/<id>/bots/<id>`): the rail's crew (the workspace's
|
||||
* in a workspace) and workspaces, the bot, its active session (marked
|
||||
* read) and its messages, its models, computers and active task there.
|
||||
* Null when the bot isn't the user's or isn't in the workspace.
|
||||
* Null when the user can't chat with the bot there.
|
||||
*/
|
||||
export default async function grabConversationProps({
|
||||
user_id,
|
||||
@@ -38,7 +38,7 @@ export default async function grabConversationProps({
|
||||
if (!conversation) return null;
|
||||
|
||||
const scope = conversation.workspace_id;
|
||||
const session = await ensureBotSession({ bot_id: conversation.bot.id, workspace_id: scope });
|
||||
const session = await ensureBotSession({ user_id, bot_id: conversation.bot.id, workspace_id: scope });
|
||||
|
||||
if (session.unread) {
|
||||
await markBotSessionRead({ session_id: session.id!, user_id });
|
||||
@@ -71,7 +71,7 @@ export default async function grabConversationProps({
|
||||
workspace_id: scope || undefined,
|
||||
})
|
||||
).catch(() => null),
|
||||
grabBotComputers({ bot_id: bot.id, user_id, workspace_id: scope }),
|
||||
grabBotComputers({ bot_id: bot.id, workspace_id: scope }),
|
||||
grabActiveBotTask({ bot_id: bot.id, session_id: session.id }),
|
||||
]);
|
||||
|
||||
|
||||
@@ -7,15 +7,14 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A session only if its bot belongs to the given owner.
|
||||
* A session only if it's the given user's own.
|
||||
*/
|
||||
export default async function grabUserBotSession({
|
||||
session_id,
|
||||
user_id,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_BOT_SESSIONS | null> {
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_SESSIONS>({
|
||||
sql: `SELECT s.* FROM bot_sessions s JOIN bots b ON b.id = s.bot_id
|
||||
WHERE s.id = ? AND b.user_id = ? LIMIT 1`,
|
||||
sql: `SELECT s.* FROM bot_sessions s WHERE s.id = ? AND s.user_id = ? LIMIT 1`,
|
||||
values: [session_id, user_id],
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { BUN_SQLITE_COBBLE_BOTS, BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import grabUserBot from "../bots/grab-user-bot";
|
||||
import grabChatBot from "../bots/grab-chat-bot";
|
||||
import grabWorkspaceBot from "../workspaces/grab-workspace-bot";
|
||||
|
||||
type Params = {
|
||||
@@ -19,9 +19,9 @@ export type UserConversation = {
|
||||
};
|
||||
|
||||
/**
|
||||
* One of the user's conversations: their bot, in its own chat or in one of
|
||||
* their workspaces it works in. Throws when the bot isn't theirs or isn't
|
||||
* in that workspace.
|
||||
* One of the user's conversations: a bot they chat with in its own chat
|
||||
* (`grabChatBot`) or in a workspace they're in (`grabWorkspaceBot`).
|
||||
* Throws when they can't.
|
||||
*/
|
||||
export default async function grabUserConversation({
|
||||
user_id,
|
||||
@@ -42,7 +42,7 @@ export default async function grabUserConversation({
|
||||
};
|
||||
}
|
||||
|
||||
const bot = await grabUserBot({ bot_id, user_id });
|
||||
const bot = await grabChatBot({ bot_id, user_id });
|
||||
|
||||
if (!bot?.id) {
|
||||
throw new AppError(`Bot not found`);
|
||||
|
||||
@@ -6,12 +6,12 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears a session's unread flag when its owner has the conversation open.
|
||||
* Clears a session's unread flag when its user has the conversation open.
|
||||
*/
|
||||
export default async function markBotSessionRead({ session_id, user_id }: Params) {
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bot_sessions SET unread = 0
|
||||
WHERE id = ? AND unread = 1 AND bot_id IN (SELECT id FROM bots WHERE user_id = ?)`,
|
||||
WHERE id = ? AND unread = 1 AND user_id = ?`,
|
||||
values: [session_id, user_id],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,15 +29,15 @@ export default async function reopenBotSession({
|
||||
const workspace_id = session.workspace_id || null;
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `DELETE FROM bot_sessions WHERE bot_id = ? AND workspace_id IS ? AND status = 'active'
|
||||
sql: `DELETE FROM bot_sessions WHERE user_id = ? AND bot_id = ? AND workspace_id IS ? AND status = 'active'
|
||||
AND NOT EXISTS (SELECT 1 FROM bot_messages m WHERE m.session_id = bot_sessions.id)`,
|
||||
values: [session.bot_id!, workspace_id],
|
||||
values: [session.user_id!, session.bot_id!, workspace_id],
|
||||
});
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bot_sessions SET status = 'closed', unread = 0, updated_at = ?
|
||||
WHERE bot_id = ? AND workspace_id IS ? AND status = 'active'`,
|
||||
values: [now, session.bot_id!, workspace_id],
|
||||
WHERE user_id = ? AND bot_id = ? AND workspace_id IS ? AND status = 'active'`,
|
||||
values: [now, session.user_id!, session.bot_id!, workspace_id],
|
||||
});
|
||||
|
||||
await BunSQLite.sql({
|
||||
|
||||
@@ -4,6 +4,10 @@ import createBotSession from "./create-bot-session";
|
||||
import grabActiveBotSession from "./grab-active-bot-session";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The user the conversation is with.
|
||||
*/
|
||||
user_id: number | string;
|
||||
bot_id: number | string;
|
||||
workspace_id?: number | string | null;
|
||||
};
|
||||
@@ -14,10 +18,11 @@ type Params = {
|
||||
* An active session with no messages yet is kept as it is.
|
||||
*/
|
||||
export default async function startBotSession({
|
||||
user_id,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_BOT_SESSIONS> {
|
||||
const active = await grabActiveBotSession({ bot_id, workspace_id });
|
||||
const active = await grabActiveBotSession({ user_id, bot_id, workspace_id });
|
||||
|
||||
if (active?.id) {
|
||||
const has_messages = (
|
||||
@@ -36,6 +41,7 @@ export default async function startBotSession({
|
||||
}
|
||||
|
||||
return await createBotSession({
|
||||
user_id,
|
||||
bot_id,
|
||||
workspace_id,
|
||||
previous_session_id: active?.id || null,
|
||||
|
||||
Reference in New Issue
Block a user