Let the coordinator and workspace admins hand over tasks and routines
delegate_task takes a task_id to hand over a task already on a list: it moves to the worker (a workspace's task stays on its list), goes to doing, and the job is linked back so its end is noted on the task. hand_over_schedule moves a bot's own routine to another bot, keeping its workspace. Users get Hand over in the Tasks UI and both routine lists. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -6,6 +6,7 @@ import setBotFace from "../bots/set-bot-face";
|
||||
import countOpenBotJobs from "./count-open-bot-jobs";
|
||||
import grabLockedBotJob from "./grab-locked-bot-job";
|
||||
import relayJobToRequester from "./relay-job-to-requester";
|
||||
import noteTrackerTask from "./note-tracker-task";
|
||||
import jobSessionId from "./job-session-id";
|
||||
import grabJobUserId from "./grab-job-user-id";
|
||||
import grabRunningBotJobFace from "./grab-running-bot-job-face";
|
||||
@@ -40,7 +41,7 @@ type Params = {
|
||||
* after a task and idle after a reply; with others open, it shows the one
|
||||
* still running (a reply in another session mustn't hide a task's row).
|
||||
* Nobody else's face changes. A handed-over job's result also goes to the bot that handed
|
||||
* it over (`relay`). Throws if this handler no longer holds the job.
|
||||
* it over (`relay`), and a handed-over task left open says so (`noteTrackerTask`). Throws if this handler no longer holds the job.
|
||||
*/
|
||||
export default async function completeBotJob({
|
||||
job_id,
|
||||
@@ -129,6 +130,8 @@ export default async function completeBotJob({
|
||||
face: job.kind == "reply" ? "idle" : "done",
|
||||
});
|
||||
|
||||
await noteTrackerTask({ job });
|
||||
|
||||
const relay = silent ? null : await relayJobToRequester({ job, text: result });
|
||||
|
||||
return { message, bot, relay };
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
/**
|
||||
* Whether a job is still queued or working.
|
||||
*/
|
||||
export default async function isBotJobOpen({ job_id }: { job_id: number | string }) {
|
||||
const res = await BunSQLite.sql({
|
||||
sql: `SELECT id FROM bots_queue WHERE id = ? AND status IN ('pending', 'processing') LIMIT 1`,
|
||||
values: [job_id],
|
||||
});
|
||||
|
||||
return Boolean(res.payload?.[0]);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { BUN_SQLITE_COBBLE_BOTS_QUEUE } from "@/db/types/db";
|
||||
import parseJobJson from "@/src/bots-queue/parse-job-json";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import { logError } from "@/src/functions/backend/logger";
|
||||
import grabBotById from "../bots/grab-bot-by-id";
|
||||
import addTaskNote from "../tasks/add-task-note";
|
||||
import grabTaskRow, { isOpenTask } from "../tasks/grab-task-scope";
|
||||
import updateTask from "../tasks/update-task";
|
||||
import grabJobUserId from "./grab-job-user-id";
|
||||
|
||||
type Params = {
|
||||
job: BUN_SQLITE_COBBLE_BOTS_QUEUE;
|
||||
/**
|
||||
* Why it failed; left out when it finished.
|
||||
*/
|
||||
failure?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The end of a job worked on a handed-over task (`params.tracker_task_id`,
|
||||
* set by `handOverTask`), written on that task while it's still open: a
|
||||
* run that finished without closing it says so, a failed one blocks it
|
||||
* with the reason. The bot moves the task along itself as it works; this
|
||||
* only keeps the task honest when it didn't. Best effort: never fails
|
||||
* the job's wrap-up.
|
||||
*/
|
||||
export default async function noteTrackerTask({ job, failure }: Params) {
|
||||
const tracker_task_id = Number(parseJobJson({ text: job.params })?.tracker_task_id || 0);
|
||||
|
||||
if (!tracker_task_id || !job.bot_id) return;
|
||||
|
||||
try {
|
||||
const task = await grabTaskRow({ task_id: tracker_task_id });
|
||||
|
||||
if (!isOpenTask({ status: task.status })) return;
|
||||
|
||||
const name = (await grabBotById({ bot_id: job.bot_id }))?.name || "The bot";
|
||||
|
||||
if (failure === undefined) {
|
||||
await addTaskNote({
|
||||
task_id: tracker_task_id,
|
||||
text: `${name}'s run finished; the task is still ${task.status}.`,
|
||||
bot_id: job.bot_id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const reason = `${name} couldn't finish it. ${failure}`.trim().slice(0, AppData.TaskNoteMaxLength);
|
||||
const user_id = await grabJobUserId({ job });
|
||||
|
||||
if (user_id) {
|
||||
await updateTask({
|
||||
user_id,
|
||||
task_id: tracker_task_id,
|
||||
status: "blocked",
|
||||
blocked_reason: reason,
|
||||
updated_by_bot_id: job.bot_id,
|
||||
});
|
||||
}
|
||||
|
||||
await addTaskNote({ task_id: tracker_task_id, text: reason, bot_id: job.bot_id });
|
||||
} catch (error) {
|
||||
await logError({
|
||||
title: `Couldn't note job #${job.id} on task #${tracker_task_id}`,
|
||||
error,
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import type { BUN_SQLITE_COBBLE_BOTS_QUEUE } from "@/db/types/db";
|
||||
import insertBotMessage from "../bots/insert-bot-message";
|
||||
import setBotFace from "../bots/set-bot-face";
|
||||
import relayJobToRequester from "./relay-job-to-requester";
|
||||
import noteTrackerTask from "./note-tracker-task";
|
||||
import jobSessionId from "./job-session-id";
|
||||
import grabJobUserId from "./grab-job-user-id";
|
||||
import { BotNeedsYouStatusLine } from "@/src/dict/bot-queue-dict";
|
||||
@@ -16,7 +17,8 @@ type Params = {
|
||||
* Wraps up a job that is out of attempts: stamps `finished_at` (so it's
|
||||
* reported once), tells the user in the bot's conversation and sets the
|
||||
* bot to needs-you for that user. Its task stays active, since the work isn't done. A
|
||||
* handed-over job's failure also goes to the bot that handed it over.
|
||||
* handed-over job's failure also goes to the bot that handed it over, and
|
||||
* blocks the handed-over task it worked on (`noteTrackerTask`).
|
||||
*/
|
||||
export default async function notifyBotJobFailed({
|
||||
job,
|
||||
@@ -59,6 +61,8 @@ export default async function notifyBotJobFailed({
|
||||
})
|
||||
: null;
|
||||
|
||||
await noteTrackerTask({ job, failure: failure_message || job.last_error || "" });
|
||||
|
||||
const relay = await relayJobToRequester({
|
||||
job,
|
||||
text: failure_message || job.last_error || "",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { HandOverTarget } from "@/src/types";
|
||||
import grabBots from "./grab-bots";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
/**
|
||||
* Where the work runs: the bots the user chats with there, or in their
|
||||
* own chat when none.
|
||||
*/
|
||||
workspace_id?: number | string | null;
|
||||
/**
|
||||
* The bot it's with now.
|
||||
*/
|
||||
except_bot_id?: number | string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The bots a user can hand a task or routine to where it runs, for the
|
||||
* picker. Only ones already there: joining a workspace is the
|
||||
* coordinator's call, not a picker's side effect.
|
||||
*/
|
||||
export default async function grabHandOverTargets({ user_id, workspace_id, except_bot_id }: Params): Promise<HandOverTarget[]> {
|
||||
const bots = await grabBots({ user_id, workspace_id });
|
||||
|
||||
return bots
|
||||
.filter((bot) => bot.id && bot.id != Number(except_bot_id || 0))
|
||||
.map((bot) => ({ id: Number(bot.id), name: bot.name || `Bot ${bot.id}` }));
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import nextCronRun from "@/src/utils/next-cron-run";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
import ensureBotSession from "../bot-sessions/ensure-bot-session";
|
||||
import enqueueBotJob from "../bots-queue/enqueue-bot-job";
|
||||
import isBotJobOpen from "../bots-queue/is-bot-job-open";
|
||||
import grabBotComputers from "../computers/grab-bot-computers";
|
||||
|
||||
type Params = {
|
||||
@@ -42,7 +43,7 @@ export default async function fireCronJob({ cron_job, now = Date.now() }: Params
|
||||
if (!claimed.payload?.[0]) return;
|
||||
|
||||
try {
|
||||
if (cron_job.last_job_id && (await isJobOpen({ job_id: cron_job.last_job_id }))) {
|
||||
if (cron_job.last_job_id && (await isBotJobOpen({ job_id: cron_job.last_job_id }))) {
|
||||
await saveRun({ id, last_error: `Skipped a run: the last one was still going.` });
|
||||
return;
|
||||
}
|
||||
@@ -99,15 +100,6 @@ export default async function fireCronJob({ cron_job, now = Date.now() }: Params
|
||||
}
|
||||
}
|
||||
|
||||
async function isJobOpen({ job_id }: { job_id: number }) {
|
||||
const res = await BunSQLite.sql({
|
||||
sql: `SELECT id FROM bots_queue WHERE id = ? AND status IN ('pending', 'processing') LIMIT 1`,
|
||||
values: [job_id],
|
||||
});
|
||||
|
||||
return Boolean(res.payload?.[0]);
|
||||
}
|
||||
|
||||
async function saveRun({
|
||||
id,
|
||||
last_run_at,
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import pushBotNotice from "@/src/bots-queue/push-bot-notice";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import handOverPlace from "@/src/utils/hand-over-place";
|
||||
import grabUserConversation from "../bot-sessions/grab-user-conversation";
|
||||
import ensureBotSession from "../bot-sessions/ensure-bot-session";
|
||||
import isBotJobOpen from "../bots-queue/is-bot-job-open";
|
||||
import grabBotById from "../bots/grab-bot-by-id";
|
||||
import insertBotMessage from "../bots/insert-bot-message";
|
||||
import grabBotComputers from "../computers/grab-bot-computers";
|
||||
import ensureBotInWorkspace from "../workspaces/ensure-bot-in-workspace";
|
||||
import grabUserCronJob from "./grab-user-cron-job";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The user the routine belongs to.
|
||||
*/
|
||||
user_id: number | string;
|
||||
cron_job_id: number | string;
|
||||
/**
|
||||
* Only if it's this bot's (a bot handing over its own).
|
||||
*/
|
||||
from_bot_id?: number | string;
|
||||
to_bot_id: number | string;
|
||||
/**
|
||||
* Where it runs from now, for a routine in the bot's own chat: left
|
||||
* out, the new bot's own chat. A routine in a workspace stays there.
|
||||
*/
|
||||
workspace_id?: number | string | null;
|
||||
/**
|
||||
* Who hands it over, shown in the new bot's conversation.
|
||||
*/
|
||||
handed_by: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Moves one of the user's routines to another bot. A routine in a
|
||||
* workspace keeps running there (the bot is added first when it isn't in
|
||||
* it); one in a bot's own chat runs in the new bot's, or in the workspace
|
||||
* given. Its computer stays only if the new bot reaches it there; its
|
||||
* schedule, count and next run are kept. Refused while a run is going,
|
||||
* or when the new bot has no room for another. Returns the routine and
|
||||
* what was said about joining.
|
||||
*/
|
||||
export default async function handOverCronJob({
|
||||
user_id,
|
||||
cron_job_id,
|
||||
from_bot_id,
|
||||
to_bot_id,
|
||||
workspace_id,
|
||||
handed_by,
|
||||
}: Params) {
|
||||
const cron_job = await grabUserCronJob({ user_id, cron_job_id, bot_id: from_bot_id });
|
||||
|
||||
if (Number(cron_job.bot_id) == Number(to_bot_id)) {
|
||||
throw new AppError(`${cron_job.bot_name || "That bot"} already runs "${cron_job.title}".`);
|
||||
}
|
||||
|
||||
const run_workspace_id = handOverPlace({
|
||||
workspace_id: cron_job.workspace_id,
|
||||
asked_workspace_id: workspace_id,
|
||||
label: `Routine #${cron_job.id} "${cron_job.title}"`,
|
||||
});
|
||||
|
||||
if (cron_job.last_job_id && (await isBotJobOpen({ job_id: cron_job.last_job_id }))) {
|
||||
throw new AppError(`"${cron_job.title}" has a run going. Hand it over once that's done.`);
|
||||
}
|
||||
|
||||
const bot = await grabBotById({ bot_id: to_bot_id });
|
||||
|
||||
if (!bot?.id) {
|
||||
throw new AppError(`Bot not found.`);
|
||||
}
|
||||
|
||||
const count = Number(
|
||||
(
|
||||
await BunSQLite.sql<{ n: number }>({
|
||||
sql: `SELECT COUNT(*) AS n FROM cron_jobs WHERE bot_id = ? AND user_id = ?`,
|
||||
values: [bot.id, user_id],
|
||||
})
|
||||
).payload?.[0]?.n || 0,
|
||||
);
|
||||
|
||||
if (count >= AppData.CronJobMaxPerBot) {
|
||||
throw new AppError(`${bot.name || "That bot"} already has ${AppData.CronJobMaxPerBot} routines.`);
|
||||
}
|
||||
|
||||
const joined = run_workspace_id ? await ensureBotInWorkspace({ user_id, bot, workspace_id: run_workspace_id }) : "";
|
||||
|
||||
/**
|
||||
* The user must reach the bot there: its own chat, or the workspace.
|
||||
*/
|
||||
await grabUserConversation({ user_id, bot_id: bot.id, workspace_id: run_workspace_id });
|
||||
|
||||
const computers = await grabBotComputers({ bot_id: bot.id, workspace_id: run_workspace_id });
|
||||
const computer_id =
|
||||
cron_job.computer_id && computers.some((computer) => computer.id == cron_job.computer_id)
|
||||
? cron_job.computer_id
|
||||
: null;
|
||||
|
||||
const res = await BunSQLite.sql({
|
||||
sql: `UPDATE cron_jobs SET bot_id = ?, workspace_id = ?, computer_id = ?, last_error = NULL, updated_at = ? WHERE id = ?`,
|
||||
values: [bot.id, run_workspace_id, computer_id, Date.now(), cron_job.id!],
|
||||
});
|
||||
|
||||
if (!res.success) {
|
||||
throw new AppError(`Couldn't hand the routine over: ${res.msg || res.error}`);
|
||||
}
|
||||
|
||||
const session = await ensureBotSession({ user_id, bot_id: bot.id, workspace_id: run_workspace_id });
|
||||
|
||||
await pushBotNotice({
|
||||
bot_id: bot.id,
|
||||
message: await insertBotMessage({
|
||||
bot_id: bot.id,
|
||||
session_id: Number(session.id),
|
||||
workspace_id: run_workspace_id,
|
||||
text: `${handed_by} handed me the routine **${cron_job.title}** (\`${cron_job.schedule}\`, UTC). It runs here from now on.`,
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
cron_job: await grabUserCronJob({ user_id, cron_job_id: cron_job.id! }),
|
||||
bot_name: bot.name || "The bot",
|
||||
joined,
|
||||
computer_dropped: Boolean(cron_job.computer_id && !computer_id),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import type { BUN_SQLITE_COBBLE_TASKS } from "@/db/types/db";
|
||||
import pushBotNotice from "@/src/bots-queue/push-bot-notice";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import handOverPlace from "@/src/utils/hand-over-place";
|
||||
import grabUserConversation from "../bot-sessions/grab-user-conversation";
|
||||
import ensureBotSession from "../bot-sessions/ensure-bot-session";
|
||||
import enqueueBotJob from "../bots-queue/enqueue-bot-job";
|
||||
import grabOpenBotTask from "../bots-queue/grab-open-bot-task";
|
||||
import grabBotById from "../bots/grab-bot-by-id";
|
||||
import insertBotMessage from "../bots/insert-bot-message";
|
||||
import grabBotComputers from "../computers/grab-bot-computers";
|
||||
import ensureBotInWorkspace from "../workspaces/ensure-bot-in-workspace";
|
||||
import addTaskNote from "./add-task-note";
|
||||
import grabTaskRow, { isOpenTask } from "./grab-task-scope";
|
||||
import moveTask from "./move-task";
|
||||
import updateTask from "./update-task";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The user the work runs for. The caller checked they (or the bot's
|
||||
* run) reach the task.
|
||||
*/
|
||||
user_id: number | string;
|
||||
task_id: number | string;
|
||||
/**
|
||||
* The bot taking it over.
|
||||
*/
|
||||
to_bot_id: number | string;
|
||||
/**
|
||||
* Where the work runs, for a task on a bot's own list: left out, the
|
||||
* new bot's own chat. A task on a workspace's list always runs there.
|
||||
*/
|
||||
workspace_id?: number | string | null;
|
||||
/**
|
||||
* More for the brief; the task's own brief comes first.
|
||||
*/
|
||||
instructions?: string;
|
||||
/**
|
||||
* Who hands it over, shown in the new bot's conversation and the
|
||||
* task's log.
|
||||
*/
|
||||
handed_by: string;
|
||||
/**
|
||||
* The bot handing it over and the session it did from: the result is
|
||||
* reported back there (`relayJobToRequester`). None when a user does.
|
||||
*/
|
||||
requested_by_bot_id?: number | string | null;
|
||||
requested_by_session_id?: number | string | null;
|
||||
};
|
||||
|
||||
export type HandedOverTask = {
|
||||
task: BUN_SQLITE_COBBLE_TASKS;
|
||||
job_id: number | string;
|
||||
bot_name: string;
|
||||
workspace_id: number | null;
|
||||
/**
|
||||
* Says the bot was added to the workspace first, or "".
|
||||
*/
|
||||
joined: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hands an open task to another bot and starts the work: a task on a
|
||||
* workspace's list stays there (the work runs in that workspace), one on
|
||||
* a bot's own list moves to the new bot's. The task goes to doing, its log
|
||||
* says who handed it over, and a job is queued in the new bot's
|
||||
* conversation with the task's brief and `params.tracker_task_id`, so the
|
||||
* bot moves this same task along and the job's end is noted on it.
|
||||
*/
|
||||
export default async function handOverTask({
|
||||
user_id,
|
||||
task_id,
|
||||
to_bot_id,
|
||||
workspace_id,
|
||||
instructions,
|
||||
handed_by,
|
||||
requested_by_bot_id,
|
||||
requested_by_session_id,
|
||||
}: Params): Promise<HandedOverTask> {
|
||||
const task = await grabTaskRow({ task_id });
|
||||
|
||||
if (!isOpenTask({ status: task.status })) {
|
||||
throw new AppError(`Task #${task.id} is ${task.status}. Only an open task can be handed over.`);
|
||||
}
|
||||
|
||||
if (requested_by_bot_id && Number(to_bot_id) == Number(requested_by_bot_id)) {
|
||||
throw new AppError(`Hand the task to another bot, not yourself.`);
|
||||
}
|
||||
|
||||
const run_workspace_id = handOverPlace({
|
||||
workspace_id: task.workspace_id,
|
||||
asked_workspace_id: workspace_id,
|
||||
label: `Task #${task.id}`,
|
||||
});
|
||||
|
||||
const bot = await grabBotById({ bot_id: to_bot_id });
|
||||
|
||||
if (!bot?.id) {
|
||||
throw new AppError(`Bot not found.`);
|
||||
}
|
||||
|
||||
const joined = run_workspace_id ? await ensureBotInWorkspace({ user_id, bot, workspace_id: run_workspace_id }) : "";
|
||||
|
||||
/**
|
||||
* The user must reach the bot there: its own chat, or the workspace.
|
||||
*/
|
||||
const conversation = await grabUserConversation({ user_id, bot_id: bot.id, workspace_id: run_workspace_id });
|
||||
const session = await ensureBotSession({ user_id, bot_id: bot.id, workspace_id: run_workspace_id });
|
||||
const title = (task.title || "Task").slice(0, 80);
|
||||
const open = await grabOpenBotTask({ bot_id: bot.id, session_id: Number(session.id), title });
|
||||
|
||||
if (open) {
|
||||
throw new AppError(
|
||||
`${bot.name} already has "${title}" ${open.status == "pending" ? "queued" : "under way"} (job #${open.id}).`,
|
||||
);
|
||||
}
|
||||
|
||||
const moved = task.workspace_id ? task : await moveTask({ task_id: task.id!, bot_id: bot.id });
|
||||
const computers = await grabBotComputers({ bot_id: bot.id, workspace_id: run_workspace_id });
|
||||
|
||||
const brief = [
|
||||
task.description?.trim(),
|
||||
instructions?.trim(),
|
||||
`This is task #${task.id} on ${task.workspace_id ? "this workspace's" : "your"} list, handed over by ${handed_by}. Move it along with update_task as you work, file what it produces on it, and mark it done once you've verified the work.`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
|
||||
const job_id = await enqueueBotJob({
|
||||
user_id,
|
||||
bot_id: bot.id,
|
||||
session_id: Number(session.id),
|
||||
title,
|
||||
instructions: brief,
|
||||
params: { tracker_task_id: Number(task.id) },
|
||||
computer_id: computers[0]?.id || undefined,
|
||||
requested_by_bot_id: requested_by_bot_id || undefined,
|
||||
requested_by_session_id: requested_by_session_id || undefined,
|
||||
});
|
||||
|
||||
await updateTask({
|
||||
user_id,
|
||||
task_id: task.id!,
|
||||
status: "doing",
|
||||
updated_by_bot_id: requested_by_bot_id || null,
|
||||
});
|
||||
|
||||
await addTaskNote({
|
||||
task_id: task.id!,
|
||||
text: `${handed_by} handed this over to ${bot.name}.`,
|
||||
author: requested_by_bot_id ? "bot" : "user",
|
||||
bot_id: requested_by_bot_id || null,
|
||||
});
|
||||
|
||||
await pushBotNotice({
|
||||
bot_id: bot.id,
|
||||
message: await insertBotMessage({
|
||||
bot_id: bot.id,
|
||||
session_id: Number(session.id),
|
||||
workspace_id: run_workspace_id,
|
||||
text: `${handed_by} handed me task #${task.id}: **${title}**`,
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
task: { ...moved, status: "doing" },
|
||||
job_id,
|
||||
bot_name: conversation.bot.name || bot.name || "The bot",
|
||||
workspace_id: run_workspace_id,
|
||||
joined,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_TASKS } from "@/db/types/db";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import findDuplicateTask from "./find-duplicate-task";
|
||||
import grabTaskRow from "./grab-task-scope";
|
||||
import taskScopeValues from "./task-scope-values";
|
||||
|
||||
type Params = {
|
||||
task_id: number | string;
|
||||
/**
|
||||
* The collection it moves to: a bot's own list, or a workspace's.
|
||||
*/
|
||||
bot_id?: number | string | null;
|
||||
workspace_id?: number | string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Moves a task to another collection, with its notes and files. Refused
|
||||
* when the destination already has an open task with that title, so a
|
||||
* hand-over never leaves two of the same. The caller checked who is
|
||||
* asking reaches both. Returns the row as it is now.
|
||||
*/
|
||||
export default async function moveTask({ task_id, bot_id, workspace_id }: Params): Promise<BUN_SQLITE_COBBLE_TASKS> {
|
||||
const task = await grabTaskRow({ task_id });
|
||||
const place = taskScopeValues({ bot_id, workspace_id });
|
||||
|
||||
if (Number(task.bot_id || 0) == Number(place.bot_id || 0) && Number(task.workspace_id || 0) == Number(place.workspace_id || 0)) {
|
||||
return task;
|
||||
}
|
||||
|
||||
const there = (
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_TASKS>({
|
||||
sql: place.workspace_id
|
||||
? `SELECT * FROM tasks WHERE workspace_id = ? AND id != ?`
|
||||
: `SELECT * FROM tasks WHERE bot_id = ? AND id != ?`,
|
||||
values: [Number(place.workspace_id || place.bot_id), Number(task.id)],
|
||||
})
|
||||
).payload || [];
|
||||
|
||||
const duplicate = findDuplicateTask({ tasks: there, title: task.title || "", ...place });
|
||||
|
||||
if (duplicate) {
|
||||
throw new AppError(`That list already has task #${duplicate.id} "${duplicate.title}" open. Hand that one over instead.`);
|
||||
}
|
||||
|
||||
const res = await BunSQLite.sql({
|
||||
sql: `UPDATE tasks SET bot_id = ?, workspace_id = ?, updated_at = ? WHERE id = ?`,
|
||||
values: [place.bot_id, place.workspace_id, Date.now(), Number(task.id)],
|
||||
});
|
||||
|
||||
if (!res.success) {
|
||||
throw new AppError(`Couldn't move the task: ${res.msg || res.error}`);
|
||||
}
|
||||
|
||||
return await grabTaskRow({ task_id });
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
|
||||
import pushBotCreated from "@/src/bots-queue/push-bot-created";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import addBotToWorkspace from "./add-bot-to-workspace";
|
||||
import grabUserWorkspaces from "./grab-user-workspaces";
|
||||
import grabWorkspaceBots from "./grab-workspace-bots";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
bot: BUN_SQLITE_COBBLE_BOTS;
|
||||
workspace_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A bot can only work in a workspace it's in. Anyone else is added first
|
||||
* (crew managers only, checked by `addBotToWorkspace`), since handing work
|
||||
* over is exactly the moment someone decides who does the work where.
|
||||
* Returns the sentence saying so, or "" when it was already there.
|
||||
*/
|
||||
export default async function ensureBotInWorkspace({ user_id, bot, workspace_id }: Params) {
|
||||
if (!bot.id) return "";
|
||||
|
||||
const workspaces = (await grabUserWorkspaces({ user_id })).filter((w) => !w.archived);
|
||||
const workspace = workspaces.find((w) => w.id == Number(workspace_id));
|
||||
|
||||
if (!workspace) {
|
||||
throw new AppError(
|
||||
`No workspace has id ${workspace_id}. Workspaces: ${
|
||||
workspaces.map((w) => `${w.id}: ${w.name}`).join("; ") || "none"
|
||||
}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const members = await grabWorkspaceBots({ user_id, workspace_id: Number(workspace.id) });
|
||||
|
||||
if (members.some((member) => member.id == bot.id)) return "";
|
||||
|
||||
await addBotToWorkspace({ user_id, workspace_id: Number(workspace.id), bot_id: Number(bot.id) });
|
||||
await pushBotCreated({ bot });
|
||||
|
||||
return ` ${bot.name} wasn't in "${workspace.name}", so it was added to it first.`;
|
||||
}
|
||||
Reference in New Issue
Block a user