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,80 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import AppError from "@/src/utils/app-error";
import insertBotMessage from "../bots/insert-bot-message";
import setBotFace from "../bots/set-bot-face";
import countOpenBotJobs from "./count-open-bot-jobs";
import grabLockedBotJob from "./grab-locked-bot-job";
type Params = {
job_id: number | string;
worker: string;
/**
* The completion message for the user (markdown). Also kept as the
* job's `result`.
*/
result: string;
/**
* Flag the message as needing the user.
*/
needs_you?: boolean;
};
/**
* Marks a job done, closes its task and sends the completion message back
* to the user in the bot's conversation. The bot shows done (or needs-you)
* unless it has other jobs open. Throws if this handler no longer holds
* the job.
*/
export default async function completeBotJob({
job_id,
worker,
result,
needs_you,
}: Params) {
const job = await grabLockedBotJob({ job_id, worker });
if (!job?.id || !job.bot_id) {
throw new AppError(`This job is no longer held by ${worker}.`);
}
const res = await BunSQLite.queue.complete({
table: "bots_queue",
id: job.id,
});
if (!res.success) {
throw new Error(`Couldn't complete the job: ${res.msg || res.error}`);
}
const now = Date.now();
await BunSQLite.sql({
sql: `UPDATE bots_queue SET result = ?, finished_at = ? WHERE id = ?`,
values: [result, now, job.id],
});
if (job.task_id) {
await BunSQLite.sql({
sql: `UPDATE bot_tasks SET status = 'done', updated_at = ? WHERE id = ?`,
values: [now, job.task_id],
});
}
const message = await insertBotMessage({
bot_id: job.bot_id,
text: result,
needs_you,
});
const bot = needs_you
? await setBotFace({
bot_id: job.bot_id,
face: "needs-you",
status_line: "Waiting on you",
})
: (await countOpenBotJobs({ bot_id: job.bot_id }))
? null
: await setBotFace({ bot_id: job.bot_id, face: "done" });
return { message, bot };
}