Add workspaces, computers, bot memory and the bots queue
- Schema: workspaces (+ workspace_bots, workspace_computers), containers, computers, bot_computers, bots.memory, workspaces.system_prompt, and bots_queue as a bun-sqlite isQueue table - Main computer (and its container record) ensured with the coordinator on signup, seed and /admin/bots; the coordinator's access to it is locked - Effective computer access: direct grants plus workspace computers - Full bot prompt: core instructions, workspace prompts, then memory; bots can rewrite their memory and system prompt - Queue helpers on the native BunSQLite.queue API, plus a handler that heartbeats, serializes per computer, recovers after restarts, reports lost jobs, supports cancelling and pushes outcomes over the WebSocket - Enable SQLite foreign keys at startup so schema cascades fire - Prune finished jobs daily Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -9,7 +9,7 @@ type Params = {
|
||||
|
||||
/**
|
||||
* Returns the id of the user's coordinator, creating it first (with the
|
||||
* default `CoordinatorSystemPrompt`) if the workspace has none. A
|
||||
* default `CoordinatorSystemPrompt`) if the user has none. A
|
||||
* coordinator without a prompt gets the default; an edited prompt is never
|
||||
* overwritten. Safe to call on every visit.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import composeBotPrompt from "@/src/utils/compose-bot-prompt";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import grabUserBot from "./grab-user-bot";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
user_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The full prompt for one of the user's bots: its core instructions, the
|
||||
* system prompt of every non-archived workspace it's in, and its memory.
|
||||
*/
|
||||
export default async function grabBotPrompt({ bot_id, user_id }: Params) {
|
||||
const bot = await grabUserBot({ bot_id, user_id });
|
||||
|
||||
if (!bot?.id) {
|
||||
throw new AppError(`Bot not found.`);
|
||||
}
|
||||
|
||||
const workspaces =
|
||||
(
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_WORKSPACES>({
|
||||
sql: `SELECT w.name, w.system_prompt FROM workspaces w
|
||||
JOIN workspace_bots wb ON wb.workspace_id = w.id
|
||||
WHERE wb.bot_id = ? AND w.user_id = ? AND w.archived = 0
|
||||
ORDER BY w.id ASC`,
|
||||
values: [bot.id, user_id],
|
||||
})
|
||||
).payload || [];
|
||||
|
||||
return composeBotPrompt({
|
||||
system_prompt: bot.system_prompt,
|
||||
memory: bot.memory,
|
||||
workspaces,
|
||||
});
|
||||
}
|
||||
@@ -6,7 +6,7 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A workspace's crew for the rail: the coordinator pinned first, then
|
||||
* A user's crew for the rail: the coordinator pinned first, then
|
||||
* workers by latest activity. Each row carries its latest message.
|
||||
*/
|
||||
export default async function grabBots({
|
||||
|
||||
@@ -7,7 +7,7 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* A bot only if it belongs to the given workspace owner.
|
||||
* A bot only if it belongs to the given owner.
|
||||
*/
|
||||
export default async function grabUserBot({
|
||||
bot_id,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import type {
|
||||
BUN_SQLITE_COBBLE_BOT_MESSAGES,
|
||||
BunSQLiteTables,
|
||||
} from "@/db/types/db";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type Params = {
|
||||
bot_id: number;
|
||||
text: string;
|
||||
needs_you?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Appends a bot message to its conversation, marks the bot unread and
|
||||
* bumps its activity. Callers must check the bot first.
|
||||
*/
|
||||
export default async function insertBotMessage({
|
||||
bot_id,
|
||||
text,
|
||||
needs_you,
|
||||
}: Params): Promise<BUN_SQLITE_COBBLE_BOT_MESSAGES> {
|
||||
const now = Date.now();
|
||||
const needs_you_flag = needs_you ? 1 : 0;
|
||||
|
||||
const res = await BunSQLite.insert<
|
||||
BUN_SQLITE_COBBLE_BOT_MESSAGES,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "bot_messages",
|
||||
data: [{ bot_id, author: "bot", text, needs_you: needs_you_flag }],
|
||||
});
|
||||
|
||||
const id = res.postInsertReturn?.insertId;
|
||||
|
||||
if (!res.success || !id) {
|
||||
throw new Error(`Failed to save message: ${res.msg}`);
|
||||
}
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bots SET last_activity_at = ?, unread = 1 WHERE id = ?`,
|
||||
values: [now, bot_id],
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
bot_id,
|
||||
author: "bot",
|
||||
text,
|
||||
needs_you: needs_you_flag,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BotFace } from "@/src/dict/bot-dict";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
face: BotFace;
|
||||
status_line?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets a bot's face (its status) and status line. Callers must check the
|
||||
* bot first.
|
||||
*/
|
||||
export default async function setBotFace({
|
||||
bot_id,
|
||||
face,
|
||||
status_line = "",
|
||||
}: Params) {
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bots SET face = ?, status_line = ?, updated_at = ? WHERE id = ?`,
|
||||
values: [face, status_line, Date.now(), bot_id],
|
||||
});
|
||||
|
||||
return { id: Number(bot_id), face, status_line };
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import assertPromptLength from "@/src/utils/assert-prompt-length";
|
||||
import grabUserBot from "./grab-user-bot";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
bot_id: number | string;
|
||||
/**
|
||||
* The whole new memory (markdown), replacing the old one.
|
||||
*/
|
||||
memory: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces a bot's memory. The bot calls this as it learns what it must
|
||||
* keep in mind; the user can edit it too.
|
||||
*/
|
||||
export default async function updateBotMemory({
|
||||
user_id,
|
||||
bot_id,
|
||||
memory,
|
||||
}: Params) {
|
||||
assertPromptLength({ label: "Memory", text: memory });
|
||||
|
||||
const bot = await grabUserBot({ bot_id, user_id });
|
||||
|
||||
if (!bot?.id) {
|
||||
throw new AppError(`Bot not found.`);
|
||||
}
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bots SET memory = ?, updated_at = ? WHERE id = ?`,
|
||||
values: [memory, Date.now(), bot.id],
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import assertPromptLength from "@/src/utils/assert-prompt-length";
|
||||
import grabUserBot from "./grab-user-bot";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
bot_id: number | string;
|
||||
/**
|
||||
* The whole new core instructions (markdown), replacing the old ones.
|
||||
*/
|
||||
system_prompt: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces a bot's core instructions. The bot calls this to adapt how it
|
||||
* works to the user's workflow; the user can edit it too.
|
||||
*/
|
||||
export default async function updateBotSystemPrompt({
|
||||
user_id,
|
||||
bot_id,
|
||||
system_prompt,
|
||||
}: Params) {
|
||||
if (!system_prompt.trim()) {
|
||||
throw new AppError(`A bot's core instructions can't be empty.`);
|
||||
}
|
||||
|
||||
assertPromptLength({ label: "System prompt", text: system_prompt });
|
||||
|
||||
const bot = await grabUserBot({ bot_id, user_id });
|
||||
|
||||
if (!bot?.id) {
|
||||
throw new AppError(`Bot not found.`);
|
||||
}
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bots SET system_prompt = ?, updated_at = ? WHERE id = ?`,
|
||||
values: [system_prompt, Date.now(), bot.id],
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user