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:
@@ -2,6 +2,7 @@ import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import grabUserBot from "../bots/grab-user-bot";
|
||||
import grabUserComputer from "./grab-user-computer";
|
||||
import assertCrewManager from "@/src/functions/backend/db/access/assert-crew-manager";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
@@ -18,6 +19,8 @@ export default async function addBotToComputer({
|
||||
bot_id,
|
||||
computer_id,
|
||||
}: Params) {
|
||||
await assertCrewManager({ user_id });
|
||||
|
||||
const bot = await grabUserBot({ bot_id, user_id });
|
||||
const computer = await grabUserComputer({ computer_id, user_id });
|
||||
|
||||
|
||||
@@ -10,11 +10,14 @@ import grabEnv from "../envs/grab-env";
|
||||
import provisionComputer from "./provision-computer";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* Who creates it. Computers belong to the install.
|
||||
*/
|
||||
user_id: number | string;
|
||||
name: string;
|
||||
description?: string;
|
||||
/**
|
||||
* `main` only for the owner's one main computer (`ensureMainComputer`).
|
||||
* `main` only for the install's one main computer (`ensureMainComputer`).
|
||||
*/
|
||||
kind?: ComputerKind;
|
||||
/**
|
||||
@@ -29,7 +32,7 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Records a new computer for the user and its container
|
||||
* Records a new computer and its container
|
||||
* (`<AppData.ComputerContainerPrefix>-<id>`), then sets the container up
|
||||
* in the background (`provisionComputer`). Returns the computer at once,
|
||||
* still `creating`, and the setup to await when the caller must (a CLI
|
||||
|
||||
@@ -10,9 +10,12 @@ import createComputer from "./create-computer";
|
||||
import provisionComputer from "./provision-computer";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* Who it's created by when it's missing (the super admin).
|
||||
*/
|
||||
user_id: number | string;
|
||||
/**
|
||||
* The owner's coordinator (from `ensureCoordinatorBot`). It always has
|
||||
* The coordinator (from `ensureCoordinatorBot`). It always has
|
||||
* access to the main computer.
|
||||
*/
|
||||
coordinator_id: number | string;
|
||||
@@ -24,7 +27,7 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the id of the user's main computer, creating it first if
|
||||
* Returns the id of the install's one main computer, creating it first if
|
||||
* missing (`createComputer`, which sets its container up in the
|
||||
* background), and makes sure the coordinator has access to it. A main
|
||||
* computer recorded before the app managed containers (no docker id, no
|
||||
@@ -44,7 +47,6 @@ export default async function ensureMainComputer({
|
||||
table: "computers",
|
||||
query: {
|
||||
query: {
|
||||
user_id: { value: user_id },
|
||||
kind: { value: "main" },
|
||||
},
|
||||
order: { field: "id", strategy: "ASC" },
|
||||
|
||||
@@ -3,7 +3,6 @@ import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
user_id: number | string;
|
||||
/**
|
||||
* The workspace the bot is working in: then its computers are the
|
||||
* workspace's, the default first, and nothing else.
|
||||
@@ -15,27 +14,26 @@ type Params = {
|
||||
* Every computer a bot can use where it's working. In a workspace, the
|
||||
* workspace's computers (none linked: none at all), its default first. In
|
||||
* its own chat, the ones it was given directly (`bot_computers`); the
|
||||
* coordinator can use all of the owner's. There the one it works on now
|
||||
* (`bots.computer_id`) comes first, else its main one, so `computers[0]`
|
||||
* is where its tasks run by default.
|
||||
* coordinator can use all of them. There the one it works on now
|
||||
* (`bots.computer_id`) comes first, else the main one, so `computers[0]`
|
||||
* is where its tasks run by default. The same for every user it works
|
||||
* for: callers check the user reaches the bot.
|
||||
*/
|
||||
export default async function grabBotComputers({
|
||||
bot_id,
|
||||
user_id,
|
||||
workspace_id,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_COMPUTERS[]> {
|
||||
if (workspace_id) {
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_COMPUTERS>({
|
||||
sql: `SELECT c.* FROM computers c
|
||||
JOIN workspace_computers wc ON wc.computer_id = c.id AND wc.workspace_id = ?
|
||||
JOIN workspaces w ON w.id = wc.workspace_id AND w.user_id = c.user_id AND w.archived = 0
|
||||
WHERE c.user_id = ?
|
||||
AND (
|
||||
(SELECT kind FROM bots WHERE id = ? AND user_id = c.user_id) = 'coordinator'
|
||||
JOIN workspaces w ON w.id = wc.workspace_id AND w.archived = 0
|
||||
WHERE (
|
||||
(SELECT kind FROM bots WHERE id = ?) = 'coordinator'
|
||||
OR ? IN (SELECT bot_id FROM workspace_bots WHERE workspace_id = w.id)
|
||||
)
|
||||
ORDER BY c.id = w.computer_id DESC, wc.id ASC`,
|
||||
values: [workspace_id, user_id, bot_id, Number(bot_id)],
|
||||
values: [workspace_id, bot_id, Number(bot_id)],
|
||||
});
|
||||
|
||||
return res.payload || [];
|
||||
@@ -43,14 +41,13 @@ export default async function grabBotComputers({
|
||||
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_COMPUTERS>({
|
||||
sql: `SELECT c.* FROM computers c
|
||||
WHERE c.user_id = ?
|
||||
AND (
|
||||
(SELECT kind FROM bots WHERE id = ? AND user_id = c.user_id) = 'coordinator'
|
||||
WHERE (
|
||||
(SELECT kind FROM bots WHERE id = ?) = 'coordinator'
|
||||
OR c.id IN (SELECT computer_id FROM bot_computers WHERE bot_id = ?)
|
||||
)
|
||||
ORDER BY c.id = (SELECT computer_id FROM bots WHERE id = ?) DESC,
|
||||
c.kind = 'main' DESC, c.id ASC`,
|
||||
values: [user_id, bot_id, bot_id, bot_id],
|
||||
values: [bot_id, bot_id, bot_id],
|
||||
});
|
||||
|
||||
return res.payload || [];
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { BUN_SQLITE_COBBLE_COMPUTERS } from "@/db/types/db";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import computersSql from "@/src/utils/crew-access/computers-sql";
|
||||
import grabCrewRole from "../access/grab-crew-role";
|
||||
|
||||
type Params = {
|
||||
computer_id: number | string;
|
||||
@@ -7,15 +9,17 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A computer only if it belongs to the given owner.
|
||||
* A computer only if the user sees it (`computersSql`).
|
||||
*/
|
||||
export default async function grabUserComputer({
|
||||
computer_id,
|
||||
user_id,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_COMPUTERS | null> {
|
||||
const access = computersSql({ role: await grabCrewRole({ user_id }), user_id });
|
||||
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_COMPUTERS>({
|
||||
sql: `SELECT * FROM computers WHERE id = ? AND user_id = ? LIMIT 1`,
|
||||
values: [computer_id, user_id],
|
||||
sql: `SELECT c.* FROM computers c WHERE c.id = ? AND ${access.sql} LIMIT 1`,
|
||||
values: [computer_id, ...access.values],
|
||||
});
|
||||
|
||||
return res.payload?.[0] || null;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import parseJobJson from "@/src/bots-queue/parse-job-json";
|
||||
import type { ComputerWithStatus } from "@/src/types";
|
||||
import computersSql from "@/src/utils/crew-access/computers-sql";
|
||||
import grabCrewRole from "../access/grab-crew-role";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* Every computer of this owner, main first.
|
||||
* Every computer this user sees (`computersSql`), main first.
|
||||
*/
|
||||
user_id?: number | string;
|
||||
/**
|
||||
@@ -22,14 +24,18 @@ export default async function grabUserComputers({
|
||||
}: Params): Promise<ComputerWithStatus[]> {
|
||||
if (!user_id && !computer_id) return [];
|
||||
|
||||
const access = computer_id
|
||||
? { sql: `c.id = ?`, values: [Number(computer_id)] }
|
||||
: computersSql({ role: await grabCrewRole({ user_id: user_id! }), user_id: user_id! });
|
||||
|
||||
const rows =
|
||||
(
|
||||
await BunSQLite.sql<ComputerWithStatus & { metadata?: string | null }>({
|
||||
sql: `SELECT c.*, co.status AS status, co.metadata AS metadata FROM computers c
|
||||
LEFT JOIN containers co ON co.id = c.container_id
|
||||
WHERE ${computer_id ? `c.id = ?` : `c.user_id = ?`}
|
||||
WHERE ${access.sql}
|
||||
ORDER BY c.kind = 'main' DESC, c.id ASC`,
|
||||
values: [computer_id || user_id!],
|
||||
values: access.values,
|
||||
})
|
||||
).payload || [];
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { sendToUser } from "@/src/websocket/(functions)/send-to-user";
|
||||
import grabComputerAudience from "../access/grab-computer-audience";
|
||||
import grabUserComputers from "./grab-user-computers";
|
||||
|
||||
type Params = {
|
||||
@@ -6,16 +7,15 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Pushes a computer's current state (`server:computer-updated`) to its
|
||||
* owner's open tabs. Best effort.
|
||||
* Pushes a computer's current state (`server:computer-updated`) to the
|
||||
* open tabs of everyone who sees it (`grabComputerAudience`). Best effort.
|
||||
*/
|
||||
export default async function pushComputerUpdate({ computer_id }: Params) {
|
||||
const computer = (await grabUserComputers({ computer_id }))[0];
|
||||
|
||||
if (!computer?.user_id) return;
|
||||
if (!computer?.id) return;
|
||||
|
||||
sendToUser({
|
||||
user_id: computer.user_id,
|
||||
data: { event: "server:computer-updated", data: computer },
|
||||
});
|
||||
for (const user_id of await grabComputerAudience({ computer_id: computer.id })) {
|
||||
sendToUser({ user_id, data: { event: "server:computer-updated", data: computer } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import AppError from "@/src/utils/app-error";
|
||||
import isLockedComputerAccess from "@/src/utils/is-locked-computer-access";
|
||||
import grabUserBot from "../bots/grab-user-bot";
|
||||
import grabUserComputer from "./grab-user-computer";
|
||||
import assertCrewManager from "@/src/functions/backend/db/access/assert-crew-manager";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
@@ -19,6 +20,8 @@ export default async function removeBotFromComputer({
|
||||
bot_id,
|
||||
computer_id,
|
||||
}: Params) {
|
||||
await assertCrewManager({ user_id });
|
||||
|
||||
const bot = await grabUserBot({ bot_id, user_id });
|
||||
const computer = await grabUserComputer({ computer_id, user_id });
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export default async function setBotComputer({ bot_id, user_id, computer_id }: P
|
||||
throw new AppError(`Bot not found.`);
|
||||
}
|
||||
|
||||
const computers = await grabBotComputers({ bot_id: bot.id, user_id });
|
||||
const computers = await grabBotComputers({ bot_id: bot.id });
|
||||
const computer = computers.find((c) => String(c.id) == String(computer_id));
|
||||
|
||||
if (!computer?.id) {
|
||||
|
||||
Reference in New Issue
Block a user