Add the harness, envs, per-bot models and chat attachments

- Harness: providers, model catalog synced hourly, automatic free picks,
  cooldowns, tool loop with context trimming, MCP computer tools
- Bots queue runs replies and tasks through the harness; jobs wait
  (attempt refunded) while every model is resting
- Envs: Settings > Environment, scoped general/workspace/computer,
  written to computers and redacted from tool output
- Models: Settings > Models, per-harness provider select defaulting to
  OpenRouter, and a models modal on each bot's chat (the coordinator's
  edits the defaults)
- Chat: thinking row while a reply runs, file attachments (upload,
  paste, drop) that replies read as images, audio and document text
- Fix media uploads failing after a user's first file, and private
  files being served from the public media path

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-19 03:38:38 +01:00
co-authored by Claude Opus 5.5
parent d49f524fe8
commit 6a2665dae9
223 changed files with 12342 additions and 162 deletions
+46 -23
View File
@@ -1,18 +1,23 @@
import bunext from "@moduletrace/bunext";
import { AppData } from "@/src/data/app-data";
import type { BotJobKind } from "@/src/dict/bot-queue-dict";
import claimBotJobs from "@/src/functions/backend/db/bots-queue/claim-bot-jobs";
import releaseWorkerBotJobs from "@/src/functions/backend/db/bots-queue/release-worker-bot-jobs";
import { logError } from "@/src/functions/backend/logger";
import processBotJob from "./process-bot-job";
import reportLostBotJobs from "./report-lost-bot-jobs";
import { setBotsQueueWaker } from "./wake-bots-queue";
const worker = AppData.BotQueueWorker;
let running = 0;
const running = { task: 0, reply: 0 };
let polling = false;
let timer: ReturnType<typeof setTimeout> | undefined;
/**
* Starts the `bots_queue` handler: releases jobs a restart cut off, then
* polls every `AppData.BotQueuePollMs`, reporting lost jobs and claiming
* up to `AppData.BotQueueConcurrency` at once. Gated in production to the
* up to `AppData.BotQueueConcurrency` tasks and, on their own slots,
* `AppData.BotQueueReplyConcurrency` chat replies. Gated in production to the
* `MAIN_SERVER` container, like cron.
*/
export default async function botsQueue() {
@@ -28,38 +33,56 @@ export default async function botsQueue() {
bunext.bunextLog.info(`Bots queue: released ${released} cut-off job(s)`);
}
setBotsQueueWaker({ waker: wake });
poll();
}
/**
* Polls now, unless a poll is already running (`wakeBotsQueue`).
*/
function wake() {
if (polling) return;
clearTimeout(timer);
poll();
}
async function claimAndRun({ kind, slots }: { kind: BotJobKind; slots: number }) {
const free = slots - running[kind];
if (free < 1) return;
for (const job of await claimBotJobs({ worker, limit: free, kind })) {
running[kind]++;
processBotJob({ job, worker })
.catch((error) =>
logError({
title: `Bot job #${job.id} crashed`,
error,
}).catch(() => {}),
)
.finally(() => {
running[kind]--;
});
}
}
async function poll() {
polling = true;
try {
await reportLostBotJobs();
const free = AppData.BotQueueConcurrency - running;
if (free > 0) {
const jobs = await claimBotJobs({ worker, limit: free });
for (const job of jobs) {
running++;
processBotJob({ job, worker })
.catch((error) =>
logError({
title: `Bot job #${job.id} crashed`,
error,
}).catch(() => {}),
)
.finally(() => {
running--;
});
}
}
await claimAndRun({ kind: "reply", slots: AppData.BotQueueReplyConcurrency });
await claimAndRun({ kind: "task", slots: AppData.BotQueueConcurrency });
} catch (error) {
await logError({ title: "Bots queue poll failed", error }).catch(
() => {},
);
} finally {
setTimeout(poll, AppData.BotQueuePollMs);
polling = false;
clearTimeout(timer);
timer = setTimeout(poll, AppData.BotQueuePollMs);
}
}