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:
2026-10-18 18:33:54 +01:00
co-authored by Claude Opus 5.5
parent 694c83346f
commit d49f524fe8
72 changed files with 2961 additions and 31 deletions
@@ -0,0 +1,46 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_BOTS_QUEUE } from "@/db/types/db";
import insertBotMessage from "../bots/insert-bot-message";
import setBotFace from "../bots/set-bot-face";
type Params = {
job: BUN_SQLITE_COBBLE_BOTS_QUEUE;
failure_message?: string;
};
/**
* Wraps up a job that is out of attempts: stamps `finished_at` (so it's
* reported once), tells the user in the bot's conversation and sets the
* bot to needs-you. Its task stays active, since the work isn't done.
*/
export default async function notifyBotJobFailed({
job,
failure_message,
}: Params) {
if (!job.id || !job.bot_id) {
throw new Error(`Job has no id or bot.`);
}
await BunSQLite.sql({
sql: `UPDATE bots_queue SET finished_at = ? WHERE id = ?`,
values: [Date.now(), job.id],
});
const message = await insertBotMessage({
bot_id: job.bot_id,
text:
failure_message ||
[`I couldn't finish "${job.title || "the job"}".`, job.last_error]
.filter(Boolean)
.join(" "),
needs_you: true,
});
const bot = await setBotFace({
bot_id: job.bot_id,
face: "needs-you",
status_line: "Waiting on you",
});
return { message, bot };
}