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
@@ -0,0 +1,59 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_ENVS } from "@/db/types/db";
import type { EnvParadigm } from "@/src/dict/env-dict";
import { decrypt } from "@/src/utils/crypt";
export type DecryptedEnv = {
id: number;
key: string;
value: string;
description: string;
paradigm: EnvParadigm;
workspace_id: number | null;
computer_id: number | null;
secret: boolean;
expose_to_computers: boolean;
};
let cache: Promise<DecryptedEnv[]> | undefined;
/**
* Every env, decrypted, cached in this process until `clearEnvCache` (every
* save and delete calls it). A value that can't be decrypted (the
* encryption key changed) reads as empty.
*/
export default function grabAllEnvs(): Promise<DecryptedEnv[]> {
cache ||= load().catch((error) => {
cache = undefined;
throw error;
});
return cache;
}
export function clearEnvCache() {
cache = undefined;
}
async function load() {
const rows =
(
await BunSQLite.sql<BUN_SQLITE_COBBLE_ENVS>({
sql: `SELECT * FROM envs ORDER BY id ASC`,
})
).payload || [];
return await Promise.all(
rows.map(async (row): Promise<DecryptedEnv> => ({
id: Number(row.id),
key: row.key || "",
value: row.value ? await decrypt(row.value).catch(() => "") : "",
description: row.description || "",
paradigm: (row.paradigm || "general") as EnvParadigm,
workspace_id: typeof row.workspace_id == "number" ? row.workspace_id : null,
computer_id: typeof row.computer_id == "number" ? row.computer_id : null,
secret: row.secret != 0,
expose_to_computers: row.expose_to_computers != 0,
})),
);
}