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
@@ -17,12 +17,17 @@ type Params = {
* Flag the message as needing the user.
*/
needs_you?: boolean;
/**
* Finish without posting a message.
*/
silent?: 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
* to the user in the bot's conversation. Unless it has other jobs open,
* the bot shows needs-you if asked, else done after a task and idle after
* a reply. Throws if this handler no longer holds
* the job.
*/
export default async function completeBotJob({
@@ -30,6 +35,7 @@ export default async function completeBotJob({
worker,
result,
needs_you,
silent,
}: Params) {
const job = await grabLockedBotJob({ job_id, worker });
@@ -60,11 +66,13 @@ export default async function completeBotJob({
});
}
const message = await insertBotMessage({
bot_id: job.bot_id,
text: result,
needs_you,
});
const message = silent
? null
: await insertBotMessage({
bot_id: job.bot_id,
text: result,
needs_you,
});
const bot = needs_you
? await setBotFace({
@@ -74,7 +82,10 @@ export default async function completeBotJob({
})
: (await countOpenBotJobs({ bot_id: job.bot_id }))
? null
: await setBotFace({ bot_id: job.bot_id, face: "done" });
: await setBotFace({
bot_id: job.bot_id,
face: job.kind == "reply" ? "idle" : "done",
});
return { message, bot };
}