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
+189
View File
@@ -23,6 +23,7 @@ import type { BUN_SQLITE_DatabaseSchemaType } from "@moduletrace/bun-sqlite/type
import { EnvParadigms } from "@/src/dict/env-dict";
import { McpServerStatuses, McpTransports } from "@/src/dict/mcp-dict";
import { CustomToolKinds, CustomToolStatuses } from "@/src/dict/custom-tool-dict";
import { TaskAuthors, TaskMediaKinds, TaskOrigins, TaskStatuses } from "@/src/dict/task-dict";
const schema: BUN_SQLITE_DatabaseSchemaType = {
tables: [
@@ -603,6 +604,194 @@ const schema: BUN_SQLITE_DatabaseSchemaType = {
},
],
},
{
tableName: "tasks",
tableDescription: `The crew's to-do list: work that is tracked until it's done, one collection per bot and one per workspace (exactly one of \`bot_id\` and \`workspace_id\` is set). A workspace's tasks are visible to every bot working there and to the coordinator; the coordinator reaches every task. Not the same thing as \`bot_tasks\` or \`bots_queue\`: those record a queued job, this records the work`,
fields: [
{
fieldName: "bot_id",
fieldDescription: `The bot the task belongs to; null for a workspace's task`,
dataType: "INTEGER",
foreignKey: {
cascadeDelete: true,
destinationTableName: "bots",
destinationTableColumnName: "id",
destinationTableColumnType: "INTEGER",
foreignKeyName: "tasks_bot_fk",
},
},
{
fieldName: "workspace_id",
fieldDescription: `The workspace the task belongs to (its bots all see it); null for a bot's own`,
dataType: "INTEGER",
foreignKey: {
cascadeDelete: true,
destinationTableName: "workspaces",
destinationTableColumnName: "id",
destinationTableColumnType: "INTEGER",
foreignKeyName: "tasks_workspace_fk",
},
},
{
fieldName: "title",
dataType: "TEXT",
},
{
fieldName: "description",
fieldDescription: `The brief (markdown): what to do, where, and how you'll know it's done`,
dataType: "TEXT",
},
{
fieldName: "status",
dataType: "TEXT",
options: TaskStatuses.map((ts) => ts.value),
defaultValue: "todo",
},
{
fieldName: "blocked_reason",
fieldDescription: `What the user has to do to unblock it; only meaningful while \`status\` is blocked`,
dataType: "TEXT",
},
{
fieldName: "origin",
fieldDescription: `Whether the user added it by hand or a bot created it`,
dataType: "TEXT",
options: TaskOrigins.map((to) => to.value),
defaultValue: "user",
},
{
fieldName: "created_by_bot_id",
fieldDescription: `The bot that created it, when a bot did. Not a foreign key: the task stays if the bot goes`,
dataType: "INTEGER",
},
{
fieldName: "created_by_user_id",
fieldDescription: `The user who created it by hand, or whose run created it. Not a foreign key`,
dataType: "INTEGER",
},
{
fieldName: "completed_at",
fieldDescription: `Unix ms when it was done or cancelled; null while it's open`,
dataType: "INTEGER",
},
],
indexes: [
{
indexName: "idx_tasks_bot_id_status",
indexTableFields: ["bot_id", "status"],
},
{
indexName: "idx_tasks_workspace_id_status",
indexTableFields: ["workspace_id", "status"],
},
],
},
{
tableName: "task_media",
tableDescription: `A file linked to a task: the deliverable it produced, or something it needs to do the work. The bytes are a private \`media\` row (paradigm \`task-media\`), served at \`/media/private/<id>\` to whoever reaches the task`,
fields: [
{
fieldName: "task_id",
dataType: "INTEGER",
foreignKey: {
cascadeDelete: true,
destinationTableName: "tasks",
destinationTableColumnName: "id",
destinationTableColumnType: "INTEGER",
foreignKeyName: "task_media_task_fk",
},
},
{
fieldName: "media_id",
fieldDescription: `The private \`media\` row holding the bytes`,
dataType: "INTEGER",
foreignKey: {
cascadeDelete: true,
destinationTableName: "media",
destinationTableColumnName: "id",
destinationTableColumnType: "INTEGER",
foreignKeyName: "task_media_media_fk",
},
},
{
fieldName: "bot_file_id",
fieldDescription: `The \`bot_files\` row the bytes came from, when they live in a bot's or workspace's folder. Not a foreign key: the file's own cleanup handles its bytes`,
dataType: "INTEGER",
},
{
fieldName: "kind",
fieldDescription: `What the file is for: a deliverable the task produced, or a reference it works from`,
dataType: "TEXT",
options: TaskMediaKinds.map((tmk) => tmk.value),
defaultValue: "deliverable",
},
{
fieldName: "name",
fieldDescription: `The name shown to the user`,
dataType: "TEXT",
},
{
fieldName: "added_by_bot_id",
fieldDescription: `The bot that linked it, when a bot did. Not a foreign key`,
dataType: "INTEGER",
},
],
uniqueConstraints: [
{
alias: "one_link_per_task_media",
constraintName: "A file is linked to a task once",
constraintTableFields: [{ value: "task_id" }, { value: "media_id" }],
},
],
indexes: [
{
indexName: "idx_task_media_task_id",
indexTableFields: ["task_id"],
},
{
indexName: "idx_task_media_media_id",
indexTableFields: ["media_id"],
},
],
},
{
tableName: "task_notes",
tableDescription: `A task's progress log: what the user or a bot did or found, oldest first. A note is how a bot keeps the user up to date between status changes`,
fields: [
{
fieldName: "task_id",
dataType: "INTEGER",
foreignKey: {
cascadeDelete: true,
destinationTableName: "tasks",
destinationTableColumnName: "id",
destinationTableColumnType: "INTEGER",
foreignKeyName: "task_note_task_fk",
},
},
{
fieldName: "author",
dataType: "TEXT",
options: TaskAuthors.map((ta) => ta.value),
defaultValue: "bot",
},
{
fieldName: "bot_id",
fieldDescription: `The bot that wrote it, when a bot did. Not a foreign key`,
dataType: "INTEGER",
},
{
fieldName: "text",
dataType: "TEXT",
},
],
indexes: [
{
indexName: "idx_task_notes_task_id",
indexTableFields: ["task_id"],
},
],
},
{
tableName: "bots_queue",
isQueue: {
+112 -2
View File
@@ -11,6 +11,9 @@ export const BunSQLiteTables = [
"bot_message_reactions",
"bot_tasks",
"bot_task_steps",
"tasks",
"task_media",
"task_notes",
"bots_queue",
"bots_queue_updates",
"cron_jobs",
@@ -161,7 +164,7 @@ export type BUN_SQLITE_COBBLE_MEDIA_PARADIGMS = {
*/
updated_at?: number | "";
media_id?: number | "";
media_paradigm?: "user-profile-image" | "generic" | "event" | "bot-attachment" | "bot-image" | "workspace-image" | "bot-file" | "";
media_paradigm?: "user-profile-image" | "generic" | "event" | "bot-attachment" | "bot-image" | "workspace-image" | "bot-file" | "task-media" | "";
}
export type BUN_SQLITE_COBBLE_BOTS = {
@@ -375,6 +378,113 @@ export type BUN_SQLITE_COBBLE_BOT_TASK_STEPS = {
position?: number | "";
}
export type BUN_SQLITE_COBBLE_TASKS = {
/**
* The unique identifier of the record.
*/
id?: number | "";
/**
* The time when the record was created. (Unix Timestamp)
*/
created_at?: number | "";
/**
* The time when the record was updated. (Unix Timestamp)
*/
updated_at?: number | "";
/**
* The bot the task belongs to; null for a workspace's task
*/
bot_id?: number | "";
/**
* The workspace the task belongs to (its bots all see it); null for a bot's own
*/
workspace_id?: number | "";
title?: string;
/**
* The brief (markdown): what to do, where, and how you'll know it's done
*/
description?: string;
status?: "todo" | "doing" | "blocked" | "done" | "cancelled" | "";
/**
* What the user has to do to unblock it; only meaningful while `status` is blocked
*/
blocked_reason?: string;
/**
* Whether the user added it by hand or a bot created it
*/
origin?: "user" | "bot" | "";
/**
* The bot that created it, when a bot did. Not a foreign key: the task stays if the bot goes
*/
created_by_bot_id?: number | "";
/**
* The user who created it by hand, or whose run created it. Not a foreign key
*/
created_by_user_id?: number | "";
/**
* Unix ms when it was done or cancelled; null while it's open
*/
completed_at?: number | "";
}
export type BUN_SQLITE_COBBLE_TASK_MEDIA = {
/**
* The unique identifier of the record.
*/
id?: number | "";
/**
* The time when the record was created. (Unix Timestamp)
*/
created_at?: number | "";
/**
* The time when the record was updated. (Unix Timestamp)
*/
updated_at?: number | "";
task_id?: number | "";
/**
* The private `media` row holding the bytes
*/
media_id?: number | "";
/**
* The `bot_files` row the bytes came from, when they live in a bot's or workspace's folder. Not a foreign key: the file's own cleanup handles its bytes
*/
bot_file_id?: number | "";
/**
* What the file is for: a deliverable the task produced, or a reference it works from
*/
kind?: "deliverable" | "reference" | "";
/**
* The name shown to the user
*/
name?: string;
/**
* The bot that linked it, when a bot did. Not a foreign key
*/
added_by_bot_id?: number | "";
}
export type BUN_SQLITE_COBBLE_TASK_NOTES = {
/**
* The unique identifier of the record.
*/
id?: number | "";
/**
* The time when the record was created. (Unix Timestamp)
*/
created_at?: number | "";
/**
* The time when the record was updated. (Unix Timestamp)
*/
updated_at?: number | "";
task_id?: number | "";
author?: "user" | "bot" | "";
/**
* The bot that wrote it, when a bot did. Not a foreign key
*/
bot_id?: number | "";
text?: string;
}
export type BUN_SQLITE_COBBLE_BOTS_QUEUE = {
/**
* The unique identifier of the record.
@@ -1599,4 +1709,4 @@ export type BUN_SQLITE_COBBLE_APP_SETTINGS = {
value?: string;
}
export type BUN_SQLITE_COBBLE_ALL_TYPEDEFS = BUN_SQLITE_COBBLE_USERS & BUN_SQLITE_COBBLE_USER_TYPES & BUN_SQLITE_COBBLE_SSO_LOGIN_CODES & BUN_SQLITE_COBBLE_USER_PASSWORDS & BUN_SQLITE_COBBLE_MEDIA & BUN_SQLITE_COBBLE_MEDIA_PARADIGMS & BUN_SQLITE_COBBLE_BOTS & BUN_SQLITE_COBBLE_BOT_SESSIONS & BUN_SQLITE_COBBLE_BOT_MESSAGES & BUN_SQLITE_COBBLE_BOT_MESSAGE_REACTIONS & BUN_SQLITE_COBBLE_BOT_TASKS & BUN_SQLITE_COBBLE_BOT_TASK_STEPS & BUN_SQLITE_COBBLE_BOTS_QUEUE & BUN_SQLITE_COBBLE_BOTS_QUEUE_UPDATES & BUN_SQLITE_COBBLE_CRON_JOBS & BUN_SQLITE_COBBLE_WORKSPACES & BUN_SQLITE_COBBLE_WORKSPACE_BOTS & BUN_SQLITE_COBBLE_WORKSPACE_USERS & BUN_SQLITE_COBBLE_BOT_USERS & BUN_SQLITE_COBBLE_BOT_USER_MEMORIES & BUN_SQLITE_COBBLE_BOT_USER_FACES & BUN_SQLITE_COBBLE_BOT_MESSAGE_MEDIA & BUN_SQLITE_COBBLE_CONTAINERS & BUN_SQLITE_COBBLE_COMPUTERS & BUN_SQLITE_COBBLE_BOT_COMPUTERS & BUN_SQLITE_COBBLE_WORKSPACE_COMPUTERS & BUN_SQLITE_COBBLE_HARNESS_PROVIDERS & BUN_SQLITE_COBBLE_HARNESS_MODELS & BUN_SQLITE_COBBLE_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_BOT_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_WORKSPACE_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_MCP_SERVERS & BUN_SQLITE_COBBLE_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_BOT_MCP_SERVERS & BUN_SQLITE_COBBLE_WORKSPACE_MCP_SERVERS & BUN_SQLITE_COBBLE_BOT_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_WORKSPACE_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_LOGS & BUN_SQLITE_COBBLE_USER_PRESENCE & BUN_SQLITE_COBBLE_PUSH_SUBSCRIPTIONS & BUN_SQLITE_COBBLE_ENVS & BUN_SQLITE_COBBLE_BOT_FILES & BUN_SQLITE_COBBLE_BOT_FILE_CHUNKS & BUN_SQLITE_COBBLE_BOT_FILE_VECTORS & BUN_SQLITE_COBBLE_FILE_INGEST_QUEUE & BUN_SQLITE_COBBLE_APP_SETTINGS
export type BUN_SQLITE_COBBLE_ALL_TYPEDEFS = BUN_SQLITE_COBBLE_USERS & BUN_SQLITE_COBBLE_USER_TYPES & BUN_SQLITE_COBBLE_SSO_LOGIN_CODES & BUN_SQLITE_COBBLE_USER_PASSWORDS & BUN_SQLITE_COBBLE_MEDIA & BUN_SQLITE_COBBLE_MEDIA_PARADIGMS & BUN_SQLITE_COBBLE_BOTS & BUN_SQLITE_COBBLE_BOT_SESSIONS & BUN_SQLITE_COBBLE_BOT_MESSAGES & BUN_SQLITE_COBBLE_BOT_MESSAGE_REACTIONS & BUN_SQLITE_COBBLE_BOT_TASKS & BUN_SQLITE_COBBLE_BOT_TASK_STEPS & BUN_SQLITE_COBBLE_TASKS & BUN_SQLITE_COBBLE_TASK_MEDIA & BUN_SQLITE_COBBLE_TASK_NOTES & BUN_SQLITE_COBBLE_BOTS_QUEUE & BUN_SQLITE_COBBLE_BOTS_QUEUE_UPDATES & BUN_SQLITE_COBBLE_CRON_JOBS & BUN_SQLITE_COBBLE_WORKSPACES & BUN_SQLITE_COBBLE_WORKSPACE_BOTS & BUN_SQLITE_COBBLE_WORKSPACE_USERS & BUN_SQLITE_COBBLE_BOT_USERS & BUN_SQLITE_COBBLE_BOT_USER_MEMORIES & BUN_SQLITE_COBBLE_BOT_USER_FACES & BUN_SQLITE_COBBLE_BOT_MESSAGE_MEDIA & BUN_SQLITE_COBBLE_CONTAINERS & BUN_SQLITE_COBBLE_COMPUTERS & BUN_SQLITE_COBBLE_BOT_COMPUTERS & BUN_SQLITE_COBBLE_WORKSPACE_COMPUTERS & BUN_SQLITE_COBBLE_HARNESS_PROVIDERS & BUN_SQLITE_COBBLE_HARNESS_MODELS & BUN_SQLITE_COBBLE_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_BOT_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_WORKSPACE_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_MCP_SERVERS & BUN_SQLITE_COBBLE_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_BOT_MCP_SERVERS & BUN_SQLITE_COBBLE_WORKSPACE_MCP_SERVERS & BUN_SQLITE_COBBLE_BOT_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_WORKSPACE_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_LOGS & BUN_SQLITE_COBBLE_USER_PRESENCE & BUN_SQLITE_COBBLE_PUSH_SUBSCRIPTIONS & BUN_SQLITE_COBBLE_ENVS & BUN_SQLITE_COBBLE_BOT_FILES & BUN_SQLITE_COBBLE_BOT_FILE_CHUNKS & BUN_SQLITE_COBBLE_BOT_FILE_VECTORS & BUN_SQLITE_COBBLE_FILE_INGEST_QUEUE & BUN_SQLITE_COBBLE_APP_SETTINGS