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:
@@ -0,0 +1,40 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
|
||||
import grabBotUserMemory from "@/src/functions/backend/db/bots/grab-bot-user-memory";
|
||||
import updateBotUserMemory from "@/src/functions/backend/db/bots/update-bot-user-memory";
|
||||
|
||||
/**
|
||||
* One-off: before `bot_user_memories`, bots saved what they learned about
|
||||
* their owner in their core memory (`bots.memory`), which every user they
|
||||
* work for would see. This moves each bot's core memory into its owner's
|
||||
* memory (after anything already there) and empties the core one.
|
||||
*
|
||||
* Run once per install: `bun src/scripts/move-bot-memory-to-owners.ts`.
|
||||
* Run again later it would move real core memory too.
|
||||
*/
|
||||
async function moveBotMemoryToOwners() {
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS>({
|
||||
sql: `SELECT id, user_id, memory FROM bots WHERE TRIM(COALESCE(memory, '')) != ''`,
|
||||
});
|
||||
|
||||
const bots = res.payload || [];
|
||||
|
||||
for (const bot of bots) {
|
||||
const existing = await grabBotUserMemory({ bot_id: bot.id!, user_id: bot.user_id! });
|
||||
const memory = [existing?.trim(), bot.memory!.trim()].filter(Boolean).join("\n\n");
|
||||
|
||||
await updateBotUserMemory({ user_id: bot.user_id!, bot_id: bot.id!, memory });
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bots SET memory = '', updated_at = ? WHERE id = ?`,
|
||||
values: [Date.now(), bot.id!],
|
||||
});
|
||||
|
||||
console.log(`Moved bot ${bot.id}'s memory to user ${bot.user_id}.`);
|
||||
}
|
||||
|
||||
console.log(`Done: ${bots.length} bot(s).`);
|
||||
}
|
||||
|
||||
await moveBotMemoryToOwners();
|
||||
process.exit(0);
|
||||
@@ -210,7 +210,7 @@ async function insertBot({
|
||||
values: [bot.face, bot.status_line, coordinator_id],
|
||||
});
|
||||
|
||||
await insertMessages({ bot_id: coordinator_id, bot, now, last_ago });
|
||||
await insertMessages({ user_id, bot_id: coordinator_id, bot, now, last_ago });
|
||||
|
||||
console.log(` ${bot.name} (#${coordinator_id}, existing)`);
|
||||
return;
|
||||
@@ -237,7 +237,16 @@ async function insertBot({
|
||||
throw new Error(`Failed to create bot ${bot.name}: ${bot_res.msg}`);
|
||||
}
|
||||
|
||||
const session_id = await insertMessages({ bot_id, bot, now, last_ago });
|
||||
/**
|
||||
* Bots belong to the install: the user reaches a sample worker once
|
||||
* it's assigned to them.
|
||||
*/
|
||||
await BunSQLite.sql({
|
||||
sql: `INSERT OR IGNORE INTO bot_users (created_at, updated_at, bot_id, user_id) VALUES (?, ?, ?, ?)`,
|
||||
values: [now, now, bot_id, user_id],
|
||||
});
|
||||
|
||||
const session_id = await insertMessages({ user_id, bot_id, bot, now, last_ago });
|
||||
|
||||
if (bot.task) {
|
||||
await insertTask({ bot_id, session_id, task: bot.task });
|
||||
@@ -247,21 +256,23 @@ async function insertBot({
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the bot's sample messages to its own chat's active session and
|
||||
* returns the session's id.
|
||||
* Adds the bot's sample messages to the user's own chat with it (its
|
||||
* active session) and returns the session's id.
|
||||
*/
|
||||
async function insertMessages({
|
||||
user_id,
|
||||
bot_id,
|
||||
bot,
|
||||
now,
|
||||
last_ago,
|
||||
}: {
|
||||
user_id: number;
|
||||
bot_id: number;
|
||||
bot: SeedBot;
|
||||
now: number;
|
||||
last_ago: number;
|
||||
}) {
|
||||
const session = await ensureBotSession({ bot_id });
|
||||
const session = await ensureBotSession({ user_id, bot_id });
|
||||
const session_id = Number(session.id);
|
||||
|
||||
await BunSQLite.sql({
|
||||
@@ -363,7 +374,7 @@ async function seedBots() {
|
||||
|
||||
if (existing_workers?.length) {
|
||||
console.log(
|
||||
`User #${user.id} <${EMAIL}> already has ${existing_workers.length} worker bots. Skipping.`,
|
||||
`User #${user.id} <${EMAIL}> already created ${existing_workers.length} worker bots. Skipping.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_BOTS, BUN_SQLITE_COBBLE_COMPUTERS } from "@/db/types/db";
|
||||
import { MainComputerName } from "@/src/dict/computer-dict";
|
||||
|
||||
/**
|
||||
* One-off, safe to run again: moves an install from one crew per user to
|
||||
* one shared crew. Before, every user owned their bots, workspaces and
|
||||
* computers, with a coordinator and a main computer each; now they belong
|
||||
* to the install, with one coordinator (the super admin's alone) and one
|
||||
* main computer, and users reach bots through `bot_users` and
|
||||
* `workspace_users`.
|
||||
*
|
||||
* - Every conversation is given to the user who owned its bot.
|
||||
* - The super admin's coordinator is kept; every other one is folded into
|
||||
* it (conversations, jobs, routines, memories kept), then deleted.
|
||||
* - The super admin's main computer is kept; the others become sandboxes.
|
||||
* - A member keeps reaching what they had: their workers are assigned to
|
||||
* them and they're added to their workspaces.
|
||||
*
|
||||
* Run once per install: `bun src/scripts/share-crew.ts`.
|
||||
*/
|
||||
async function shareCrew() {
|
||||
const now = Date.now();
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bot_sessions SET user_id = (SELECT user_id FROM bots WHERE bots.id = bot_sessions.bot_id)
|
||||
WHERE user_id IS NULL`,
|
||||
});
|
||||
|
||||
const super_admin_id = Number(
|
||||
(
|
||||
await BunSQLite.sql<{ user_id: number }>({
|
||||
sql: `SELECT user_id FROM user_types WHERE user_type = 'super_admin' ORDER BY user_id ASC LIMIT 1`,
|
||||
})
|
||||
).payload?.[0]?.user_id,
|
||||
);
|
||||
|
||||
if (!super_admin_id) {
|
||||
console.log(`No super admin yet: nothing to share.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await foldCoordinators({ super_admin_id, now });
|
||||
await keepOneMainComputer({ super_admin_id, now });
|
||||
await keepMembersAccess({ now });
|
||||
await keepOneActiveSession({ now });
|
||||
|
||||
console.log(`Done.`);
|
||||
}
|
||||
|
||||
async function foldCoordinators({ super_admin_id, now }: { super_admin_id: number; now: number }) {
|
||||
const coordinators =
|
||||
(
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS>({
|
||||
sql: `SELECT id, user_id FROM bots WHERE kind = 'coordinator'
|
||||
ORDER BY user_id = ? DESC, id ASC`,
|
||||
values: [super_admin_id],
|
||||
})
|
||||
).payload || [];
|
||||
|
||||
const [kept, ...extra] = coordinators;
|
||||
|
||||
if (!kept?.id) return;
|
||||
|
||||
for (const bot of extra) {
|
||||
for (const [table, column] of [
|
||||
["bot_sessions", "bot_id"],
|
||||
["bot_messages", "bot_id"],
|
||||
["bot_tasks", "bot_id"],
|
||||
["bots_queue", "bot_id"],
|
||||
["bots_queue", "requested_by_bot_id"],
|
||||
["bots_queue_updates", "bot_id"],
|
||||
["cron_jobs", "bot_id"],
|
||||
] as const) {
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE ${table} SET ${column} = ? WHERE ${column} = ?`,
|
||||
values: [kept.id, bot.id!],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* What it remembered about a user joins what the kept one has.
|
||||
*/
|
||||
await BunSQLite.sql({
|
||||
sql: `INSERT OR IGNORE INTO bot_user_memories (created_at, updated_at, bot_id, user_id, memory)
|
||||
SELECT ?, ?, ?, user_id, '' FROM bot_user_memories WHERE bot_id = ?`,
|
||||
values: [now, now, kept.id, bot.id!],
|
||||
});
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bot_user_memories SET updated_at = ?, memory = TRIM(memory || char(10) || char(10) || (
|
||||
SELECT old.memory FROM bot_user_memories old WHERE old.bot_id = ? AND old.user_id = bot_user_memories.user_id
|
||||
), char(10))
|
||||
WHERE bot_id = ? AND user_id IN (SELECT user_id FROM bot_user_memories WHERE bot_id = ? AND TRIM(COALESCE(memory, '')) != '')`,
|
||||
values: [now, bot.id!, kept.id, bot.id!],
|
||||
});
|
||||
|
||||
await BunSQLite.sql({ sql: `DELETE FROM bots WHERE id = ?`, values: [bot.id!] });
|
||||
|
||||
console.log(`Folded coordinator #${bot.id} (user #${bot.user_id}) into #${kept.id}.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function keepOneMainComputer({ super_admin_id, now }: { super_admin_id: number; now: number }) {
|
||||
const mains =
|
||||
(
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_COMPUTERS>({
|
||||
sql: `SELECT id, user_id, name FROM computers WHERE kind = 'main' ORDER BY user_id = ? DESC, id ASC`,
|
||||
values: [super_admin_id],
|
||||
})
|
||||
).payload || [];
|
||||
|
||||
for (const computer of mains.slice(1)) {
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE computers SET kind = 'sandbox', name = ?, updated_at = ? WHERE id = ?`,
|
||||
values: [
|
||||
computer.name == MainComputerName ? `Computer ${computer.id}` : computer.name || `Computer ${computer.id}`,
|
||||
now,
|
||||
computer.id!,
|
||||
],
|
||||
});
|
||||
|
||||
console.log(`Computer #${computer.id} (user #${computer.user_id}) is a sandbox now.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Members (not super admins or admins, who reach everything) keep the
|
||||
* workers and workspaces that were theirs.
|
||||
*/
|
||||
async function keepMembersAccess({ now }: { now: number }) {
|
||||
const members = `user_id NOT IN (SELECT user_id FROM user_types WHERE user_type IN ('super_admin', 'admin'))`;
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `INSERT OR IGNORE INTO bot_users (created_at, updated_at, bot_id, user_id)
|
||||
SELECT ?, ?, id, user_id FROM bots WHERE kind = 'worker' AND user_id IS NOT NULL AND ${members}`,
|
||||
values: [now, now],
|
||||
});
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `INSERT OR IGNORE INTO workspace_users (created_at, updated_at, workspace_id, user_id)
|
||||
SELECT ?, ?, id, user_id FROM workspaces WHERE user_id IS NOT NULL AND ${members}`,
|
||||
values: [now, now],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* One active session per user's conversation: an empty extra one is
|
||||
* deleted, and of the rest only the latest stays active.
|
||||
*/
|
||||
async function keepOneActiveSession({ now }: { now: number }) {
|
||||
const same = `s2.user_id IS s.user_id AND s2.bot_id = s.bot_id AND s2.workspace_id IS s.workspace_id
|
||||
AND s2.status = 'active' AND s2.id != s.id`;
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `DELETE FROM bot_sessions WHERE id IN (
|
||||
SELECT s.id FROM bot_sessions s
|
||||
WHERE s.status = 'active'
|
||||
AND NOT EXISTS (SELECT 1 FROM bot_messages m WHERE m.session_id = s.id)
|
||||
AND EXISTS (SELECT 1 FROM bot_sessions s2 WHERE ${same})
|
||||
)`,
|
||||
});
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bot_sessions SET status = 'closed', updated_at = ? WHERE id IN (
|
||||
SELECT s.id FROM bot_sessions s
|
||||
WHERE s.status = 'active'
|
||||
AND EXISTS (SELECT 1 FROM bot_sessions s2 WHERE ${same}
|
||||
AND (s2.last_activity_at > s.last_activity_at OR (s2.last_activity_at = s.last_activity_at AND s2.id > s.id)))
|
||||
)`,
|
||||
values: [now],
|
||||
});
|
||||
}
|
||||
|
||||
await shareCrew();
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user