Add tasks: the crew's shared to-do list

Bots and workspaces each keep a list of work tracked until it's done, and
the bots move it along themselves as they work.

- tasks (one collection per bot or per workspace), task_notes (the
  progress log) and task_media (a deliverable or a reference file, its
  bytes a private media row served at /media/private/<id>)
- statuses todo/doing/blocked/done/cancelled, with blocked_reason while
  only the user can unblock it
- visibility as SQL in src/utils/task-access: a user's own bots and
  workspaces, a run's own list plus its workspaces, everything for the
  coordinator; every tool and endpoint resolves through it
- list_tasks, create_task and update_task on every run, so a chat reply,
  a task and a routine all read the list, log progress and file what they
  produced
- TasksNote on every run and TaskReminderNote in chat, so a greeting or a
  proposed next step brings pending work up in one measured line
- tasks group on the workspace dashboard and a tasks modal in the
  conversation header, sharing one form, row and detail panel
- a bot's or workspace's own chat uploads are read before the delete so
  none are stranded; bot files keep their folder

Also corrects the README's RAM guidance and table alignment.
This commit is contained in:
2026-10-22 05:57:50 +01:00
parent 2d108b05bf
commit 7cb26c4e36
60 changed files with 3793 additions and 52 deletions
@@ -0,0 +1,87 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_TASKS, BunSQLiteTables } from "@/db/types/db";
import { AppData } from "@/src/data/app-data";
import type { TaskStatus } from "@/src/dict/task-dict";
import type { TaskDetail } from "@/src/types";
import AppError from "@/src/utils/app-error";
import grabCrewRole from "../access/grab-crew-role";
import grabTask from "./grab-task";
import taskScopeValues from "./task-scope-values";
type Params = {
/**
* The user whose run or request creates it.
*/
user_id: number | string;
bot_id?: number | string | null;
workspace_id?: number | string | null;
title: string;
description?: string;
status?: TaskStatus;
/**
* The bot creating it with its tool; none for a user's manual add.
*/
created_by_bot_id?: number | string | null;
};
/**
* Adds a task to a bot's own list or a workspace's shared one. A user adds
* one to a bot they chat with or a workspace they're in; a bot's run adds
* one to its own list or to a workspace it works in (its tool checks
* that). Caps a collection at `TaskMaxPerScope` open and finished tasks.
*/
export default async function createTask({
user_id,
bot_id,
workspace_id,
title,
description,
status,
created_by_bot_id,
}: Params): Promise<TaskDetail> {
const scope = taskScopeValues({ bot_id, workspace_id });
const role = await grabCrewRole({ user_id });
if (!created_by_bot_id && role == "member") {
throw new AppError(`Only an admin can add a task.`);
}
const count = Number(
(
await BunSQLite.sql<{ n: number }>({
sql: `SELECT COUNT(*) AS n FROM tasks WHERE ${
scope.bot_id ? `bot_id = ?` : `workspace_id = ?`
}`,
values: [Number(scope.bot_id || scope.workspace_id)],
})
).payload?.[0]?.n || 0,
);
if (count >= AppData.TaskMaxPerScope) {
throw new AppError(`That list is full (${AppData.TaskMaxPerScope} tasks). Finish or delete one first.`);
}
const res = await BunSQLite.insert<BUN_SQLITE_COBBLE_TASKS, (typeof BunSQLiteTables)[number]>({
table: "tasks",
data: [
{
bot_id: scope.bot_id || undefined,
workspace_id: scope.workspace_id || undefined,
title: title.trim().slice(0, AppData.TaskTitleMaxLength),
description: description || undefined,
status: status || "todo",
origin: created_by_bot_id ? "bot" : "user",
created_by_bot_id: created_by_bot_id ? Number(created_by_bot_id) : undefined,
created_by_user_id: Number(user_id),
},
],
});
const task_id = res.singleRes?.id || res.postInsertReturn?.insertId;
if (!res.success || !task_id) {
throw new Error(`Couldn't save the task: ${res.msg}`);
}
return await grabTask({ user_id, task_id });
}