Give every task a bot: drop shared workspace tasks
A workspace task with no bot was seen by every bot working there. Now a task can't be saved without a bot, a worker reaches only its own (the workspace admin and the coordinator still reach all), the workspace page picks who has a new task, and a bot leaving a workspace or deleted passes its tasks there to the workspace's admin bot, else the coordinator (also at boot for rows without a bot). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -2,6 +2,7 @@ import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import deleteMedia from "@/src/functions/backend/db/media/delete-media";
|
||||
import purgeStorage, { grabStorageMediaIds } from "@/src/functions/backend/db/bot-files/purge-storage";
|
||||
import { grabTaskMediaIds } from "@/src/functions/backend/db/tasks/purge-task-media";
|
||||
import passOnBotTasks from "@/src/functions/backend/db/tasks/pass-on-bot-tasks";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import { clearEnvCache } from "../envs/grab-all-envs";
|
||||
import grabUserBot from "./grab-user-bot";
|
||||
@@ -40,19 +41,18 @@ export default async function deleteBot({ user_id, bot_id }: Params) {
|
||||
);
|
||||
const task_media_ids = await grabTaskMediaIds({ bot_id: bot.id! }).catch(() => []);
|
||||
|
||||
/**
|
||||
* Its tasks in workspaces belong to the workspace: they go to each one's
|
||||
* admin bot (else the coordinator) instead of cascading away with its
|
||||
* own list.
|
||||
*/
|
||||
await passOnBotTasks({ bot_id: bot.id, bot_name: bot.name });
|
||||
|
||||
/**
|
||||
* Explicitly, since foreign keys are off outside the server process.
|
||||
*/
|
||||
await BunSQLite.sql({ sql: `DELETE FROM bot_types WHERE bot_id = ?`, values: [bot.id] });
|
||||
|
||||
/**
|
||||
* Its tasks in workspaces belong to the workspace: they stay, shared
|
||||
* again, instead of cascading away with its own list.
|
||||
*/
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE tasks SET bot_id = NULL, updated_at = ? WHERE bot_id = ? AND workspace_id IS NOT NULL`,
|
||||
values: [Date.now(), bot.id],
|
||||
});
|
||||
await BunSQLite.sql({ sql: `DELETE FROM envs WHERE bot_id = ?`, values: [bot.id] });
|
||||
await BunSQLite.sql({ sql: `DELETE FROM secret_requests WHERE bot_id = ?`, values: [bot.id] });
|
||||
await BunSQLite.sql({ sql: `DELETE FROM bot_graphics WHERE bot_id = ?`, values: [bot.id] });
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import passOnBotTasks from "./pass-on-bot-tasks";
|
||||
|
||||
/**
|
||||
* At boot: every task belongs to a bot. Workspace tasks left without one
|
||||
* by earlier versions ("shared") go to the workspace's admin bot, else the
|
||||
* coordinator.
|
||||
*/
|
||||
export default async function assignOwnerlessTasks() {
|
||||
return await passOnBotTasks({});
|
||||
}
|
||||
@@ -27,7 +27,7 @@ type Params = {
|
||||
|
||||
/**
|
||||
* Adds a task to a bot's own list or a workspace's, there for one of its
|
||||
* bots (`bot_id` too) or shared. A user adds one to a bot they chat with or
|
||||
* bots (`bot_id` too): every task belongs to a bot. 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). A workspace's task can
|
||||
* only go to a bot working there. Caps a list at `TaskMaxPerScope` open and
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
|
||||
import grabWorkspaceAdmins from "../bot-types/grab-workspace-admins";
|
||||
|
||||
type Params = {
|
||||
workspace_id: number | string;
|
||||
/**
|
||||
* The bot leaving, never its own heir.
|
||||
*/
|
||||
except_bot_id?: number | string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Who takes over a workspace task whose bot is gone: the workspace's admin
|
||||
* bot, else the coordinator. Every task belongs to a bot, so none is left
|
||||
* without one.
|
||||
*/
|
||||
export default async function grabTaskHeir({ workspace_id, except_bot_id }: Params) {
|
||||
const admins = await grabWorkspaceAdmins({ workspace_id });
|
||||
const admin = admins.find((bot) => Number(bot.id) != Number(except_bot_id || 0));
|
||||
|
||||
if (admin?.id) return admin;
|
||||
|
||||
const res = await BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS>({
|
||||
sql: `SELECT * FROM bots WHERE kind = 'coordinator' ORDER BY id ASC LIMIT 1`,
|
||||
});
|
||||
|
||||
return res.payload?.[0] || null;
|
||||
}
|
||||
@@ -27,10 +27,6 @@ type Params = {
|
||||
* Only the tasks this bot has (anywhere, unless narrowed).
|
||||
*/
|
||||
only_bot_id?: number | string | null;
|
||||
/**
|
||||
* With `only_bot_id`: also the shared ones nobody has yet.
|
||||
*/
|
||||
with_unassigned?: boolean;
|
||||
/**
|
||||
* Only one workspace's tasks.
|
||||
*/
|
||||
@@ -61,7 +57,6 @@ export default async function grabTasks({
|
||||
is_coordinator,
|
||||
admin_workspace_ids,
|
||||
only_bot_id,
|
||||
with_unassigned,
|
||||
only_workspace_id,
|
||||
own_list,
|
||||
statuses,
|
||||
@@ -82,7 +77,7 @@ export default async function grabTasks({
|
||||
}
|
||||
|
||||
if (only_bot_id) {
|
||||
where.push(with_unassigned ? `(t.bot_id = ? OR t.bot_id IS NULL)` : `t.bot_id = ?`);
|
||||
where.push(`t.bot_id = ?`);
|
||||
values.push(Number(only_bot_id));
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ type Params = {
|
||||
task_id: number | string;
|
||||
/**
|
||||
* Where it goes: a bot's own list (`bot_id` alone), or a workspace,
|
||||
* with the bot that has it there (none: unassigned).
|
||||
* with the bot that has it there (always one).
|
||||
*/
|
||||
bot_id?: number | string | null;
|
||||
workspace_id?: number | string | null;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_TASKS } from "@/db/types/db";
|
||||
import addTaskNote from "./add-task-note";
|
||||
import grabTaskHeir from "./grab-task-heir";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The bot leaving; none for tasks that have no bot at all.
|
||||
*/
|
||||
bot_id?: number | string | null;
|
||||
/**
|
||||
* Only its tasks in this workspace; none for every workspace.
|
||||
*/
|
||||
workspace_id?: number | string | null;
|
||||
/**
|
||||
* Who is leaving, for the note on each task.
|
||||
*/
|
||||
bot_name?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gives a leaving bot's workspace tasks (or ones with no bot) to the
|
||||
* workspace's admin bot, else the coordinator, with a note on each. Its own
|
||||
* list is left alone. Returns how many moved.
|
||||
*/
|
||||
export default async function passOnBotTasks({ bot_id, workspace_id, bot_name }: Params) {
|
||||
const where = [`workspace_id IS NOT NULL`, bot_id ? `bot_id = ?` : `bot_id IS NULL`];
|
||||
const values: number[] = bot_id ? [Number(bot_id)] : [];
|
||||
|
||||
if (workspace_id) {
|
||||
where.push(`workspace_id = ?`);
|
||||
values.push(Number(workspace_id));
|
||||
}
|
||||
|
||||
const tasks =
|
||||
(
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_TASKS>({
|
||||
sql: `SELECT * FROM tasks WHERE ${where.join(" AND ")}`,
|
||||
values,
|
||||
})
|
||||
).payload || [];
|
||||
|
||||
let moved = 0;
|
||||
|
||||
for (const task of tasks) {
|
||||
const heir = await grabTaskHeir({ workspace_id: task.workspace_id!, except_bot_id: bot_id });
|
||||
|
||||
if (!heir?.id) continue;
|
||||
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE tasks SET bot_id = ?, updated_at = ? WHERE id = ?`,
|
||||
values: [Number(heir.id), Date.now(), Number(task.id)],
|
||||
});
|
||||
|
||||
await addTaskNote({
|
||||
task_id: task.id!,
|
||||
text: bot_name ? `${bot_name} left; this is now ${heir.name}'s.` : `This is now ${heir.name}'s.`,
|
||||
author: "user",
|
||||
}).catch(() => {});
|
||||
|
||||
moved++;
|
||||
}
|
||||
|
||||
return moved;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import taskScopeValues from "./task-scope-values";
|
||||
|
||||
describe("taskScopeValues", () => {
|
||||
test("a task on a bot's own list or in a workspace has its bot", () => {
|
||||
expect(taskScopeValues({ bot_id: 3 })).toEqual({ bot_id: 3, workspace_id: null });
|
||||
expect(taskScopeValues({ bot_id: "3", workspace_id: "11" })).toEqual({ bot_id: 3, workspace_id: 11 });
|
||||
});
|
||||
|
||||
test("a workspace task without a bot is refused", () => {
|
||||
expect(() => taskScopeValues({ workspace_id: 11 })).toThrow();
|
||||
expect(() => taskScopeValues({})).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -6,17 +6,17 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Where a task lives and who has it. A bot's own list is `bot_id` alone; a
|
||||
* workspace's task has `workspace_id`, plus `bot_id` when a bot there has
|
||||
* it (none: shared, not yet anyone's). Checked in the helpers rather than
|
||||
* the schema.
|
||||
* Where a task lives and who has it. Every task belongs to a bot: a bot's
|
||||
* own list is `bot_id` alone, a workspace's task has `workspace_id` plus
|
||||
* the `bot_id` that has it there. Checked in the helpers rather than the
|
||||
* schema.
|
||||
*/
|
||||
export default function taskScopeValues({ bot_id, workspace_id }: Params) {
|
||||
const bot = bot_id ? Number(bot_id) : null;
|
||||
const workspace = workspace_id ? Number(workspace_id) : null;
|
||||
|
||||
if (!bot && !workspace) {
|
||||
throw new AppError(`Say which bot or workspace the task is for.`);
|
||||
if (!bot) {
|
||||
throw new AppError(`Say which bot has the task.`);
|
||||
}
|
||||
|
||||
return { bot_id: bot, workspace_id: workspace };
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import grabUserWorkspace from "./grab-user-workspace";
|
||||
import grabBotById from "../bots/grab-bot-by-id";
|
||||
import passOnBotTasks from "../tasks/pass-on-bot-tasks";
|
||||
import assertCrewManager from "@/src/functions/backend/db/access/assert-crew-manager";
|
||||
|
||||
type Params = {
|
||||
@@ -31,12 +33,11 @@ export default async function removeBotFromWorkspace({
|
||||
});
|
||||
|
||||
/**
|
||||
* The tasks it had there stay in the workspace, shared again.
|
||||
* The tasks it had there stay in the workspace, passed to its admin bot
|
||||
* (else the coordinator), read before its admin type goes below.
|
||||
*/
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE tasks SET bot_id = NULL, updated_at = ? WHERE workspace_id = ? AND bot_id = ?`,
|
||||
values: [Date.now(), workspace.id, bot_id],
|
||||
});
|
||||
const bot = await grabBotById({ bot_id });
|
||||
await passOnBotTasks({ bot_id, workspace_id: workspace.id, bot_name: bot?.name });
|
||||
|
||||
/**
|
||||
* Its types there (workspace admin) go with its place.
|
||||
|
||||
Reference in New Issue
Block a user