Scope workspace tasks to the bot that has them

A workspace's task now carries the bot that has it there (bot_id with
workspace_id), or none while it's shared. A bot reaches its own list and,
in its workspaces, its own tasks plus the shared ones; a workspace admin
every task there. Bots create workspace tasks for themselves by default,
take a shared one by starting it, and hand-overs record the new bot.
Removing or deleting a bot makes its workspace tasks shared again. The
conversation's Tasks modal shows that bot's tasks in the workspace.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-27 19:19:01 +01:00
co-authored by Claude Opus 5.5
parent 184407d76b
commit 864dcce30a
31 changed files with 316 additions and 153 deletions
+62 -36
View File
@@ -11,14 +11,14 @@ type Params = {
user_id: number | string;
bot_id: number | string;
/**
* The workspace the run is in, if any: a bot adding a task there
* defaults to the workspace's shared list.
* The workspace the run is in, if any: a worker's task there stays in
* it, its own by default.
*/
workspace_id?: number | string | null;
is_coordinator: boolean;
/**
* The coordinator's targets: every worker and live workspace. A worker
* gets neither and may only add to its own list or the workspace it
* gets neither and may only add to its own list or in the workspace it
* works in.
*/
workers?: BUN_SQLITE_COBBLE_BOTS[];
@@ -39,16 +39,19 @@ type Args = {
description: string;
bot_id?: number;
workspace_id?: number;
shared?: boolean;
status?: TaskStatus;
};
type Place = { bot_id?: number; workspace_id?: number };
/**
* Worked out from the arguments and who is asking: where the task goes, or
* why it can't. A worker may only add to its own list or the workspace it
* works in; a workspace admin also to its members' own lists there; the
* coordinator to any worker's, any workspace's, or its own.
* Worked out from the arguments and who is asking: where the task goes and
* who has it, or why it can't. Outside a workspace a worker adds only to
* its own list. In one the task stays in that workspace: its own by
* default, a member's as the workspace's admin, or shared (nobody's yet).
* The coordinator adds to any worker's own list or any workspace, for one
* of its bots or shared.
*/
function resolvePlace({
args,
@@ -66,41 +69,53 @@ function resolvePlace({
const target_bot = args.bot_id ? Number(args.bot_id) : null;
const target_workspace = args.workspace_id ? Number(args.workspace_id) : null;
if (target_bot && target_workspace) {
return `A task belongs to a bot or a workspace, not both. Pick one.`;
if (args.shared && target_bot) {
return `A shared task is nobody's yet: leave bot_id out, or drop shared to give it to that bot.`;
}
if (is_coordinator) {
if (target_workspace) return { workspace_id: target_workspace };
if (target_bot) return { bot_id: target_bot };
const place_workspace = target_workspace || (workspace_id ? Number(workspace_id) : null);
if (place_workspace) {
return target_bot ? { workspace_id: place_workspace, bot_id: target_bot } : { workspace_id: place_workspace };
}
return { bot_id: target_bot || Number(bot_id) };
}
if (!workspace_id) {
if (target_workspace || args.shared) {
return `You're not in a workspace here, so a task can only go on your own list (bot_id ${bot_id}).`;
}
if (target_bot && target_bot != Number(bot_id)) {
return `You can only add tasks to your own list (#${bot_id}).`;
}
return { bot_id: Number(bot_id) };
}
if (target_workspace && target_workspace != Number(workspace_id)) {
return `You can only add tasks in this workspace (#${workspace_id}).`;
}
if (args.shared) return { workspace_id: Number(workspace_id) };
const is_member = members.some((member) => member.id == target_bot);
if (target_bot && target_bot != Number(bot_id) && !is_member) {
return members.length
? `You can only add tasks to your own list (#${bot_id}), this workspace's, or a member's here (${members.map((m) => m.id).join(", ")}).`
: `You can only add tasks to your own list (#${bot_id}).`;
? `You can only give a task here to yourself (#${bot_id}) or a member (${members.map((m) => m.id).join(", ")}), or leave it shared.`
: `You can only give a task here to yourself (#${bot_id}), or leave it shared.`;
}
if (target_workspace && Number(workspace_id || 0) != target_workspace) {
return `You can only add to this workspace's list (#${workspace_id || "none"}) or your own.`;
}
if (target_workspace) return { workspace_id: target_workspace };
if (target_bot) return { bot_id: target_bot };
/**
* In a workspace, the workspace's list is the right home: work there is
* shared work.
*/
return workspace_id ? { workspace_id: Number(workspace_id) } : { bot_id: Number(bot_id) };
return { workspace_id: Number(workspace_id), bot_id: target_bot || Number(bot_id) };
}
/**
* Lets a bot add a task to a list it works on: its own, or a workspace's.
* The coordinator can add to any worker's list and any workspace's.
* Lets a bot add a task to a list it works on: its own, or in a workspace
* its own there (a member's as its admin) or a shared one. The coordinator
* can add to any worker's list and any workspace.
*/
export default function createTaskTool({
user_id,
@@ -119,8 +134,8 @@ export default function createTaskTool({
return {
name: "create_task",
description: is_coordinator
? `Add a task to a list: a worker's own (${crew || "no workers yet"}), a workspace's shared one (${places || "no workspaces yet"}), or your own (leave both ids out). Write the description as a brief: what to do, where, and how you'll know it's done. A workspace's tasks are visible to every bot working there.`
: `Add a task to a list: ${workspace_id ? `this workspace's shared one (leave the ids out), or your own (bot_id: ${bot_id})${team ? `, or, as this workspace's admin, a member's own (${team})` : ""}` : `your own, bot_id ${bot_id} (or leave the id out, since it defaults to yours)`}. Write the description as a brief: what to do, where, and how you'll know it's done.`,
? `Add a task: to a worker's own list (bot_id: ${crew || "no workers yet"}), to a workspace (workspace_id: ${places || "no workspaces yet"}) for one of its bots (with bot_id) or shared (without), or to your own list (leave both ids out${workspace_id ? "; here, leaving them out makes it a shared task in this workspace" : ""}). Write the description as a brief: what to do, where, and how you'll know it's done.`
: `Add a task: ${workspace_id ? `in this workspace, yours by default${team ? `, a member's as this workspace's admin (bot_id: ${team})` : ""}, or shared (shared: true) for whichever bot here takes it up` : `on your own list (bot_id ${bot_id}, the default)`}. Write the description as a brief: what to do, where, and how you'll know it's done.`,
schema: z.object({
title: z.string().trim().min(1).max(AppData.TaskTitleMaxLength).describe(`A short label`),
description: z
@@ -129,10 +144,10 @@ export default function createTaskTool({
.describe(`The brief, in markdown: what to do, where, and what done looks like`),
bot_id: z.number().int().optional().describe(
is_coordinator
? `Whose own list, when it isn't a workspace's. Leave out for your own.`
? `Who has it: a worker's own list, or with workspace_id a bot working there. Leave out for your own list or a shared workspace task.`
: team
? `Your own id (${bot_id}) or a member's here (${team}), to put it on that list rather than the workspace's.`
: `Your own id (${bot_id}), to keep it in your list rather than the workspace's; leaving it out has the same effect here.`,
? `Who has it here: you (${bot_id}, the default) or a member (${team}).`
: `Your own id (${bot_id}); leaving it out has the same effect.`,
),
workspace_id: z
.number()
@@ -140,9 +155,13 @@ export default function createTaskTool({
.optional()
.describe(
is_coordinator
? `A workspace's shared list, when it isn't a bot's own.`
: `This workspace's shared list (${workspace_id || "none"}).`,
? `The workspace it's in, when it isn't a bot's own list.`
: `This workspace (${workspace_id || "none"}); leaving it out has the same effect.`,
),
shared: z
.boolean()
.optional()
.describe(`In a workspace: nobody's yet, for whichever bot there takes it up. Defaults to false.`),
status: z.enum(TaskStatusValues).optional().describe(`Defaults to to do`),
}),
run: async ({ args }) => {
@@ -154,8 +173,9 @@ export default function createTaskTool({
bot_id,
is_coordinator,
admin_workspace_ids,
only_bot_id: place.bot_id,
only_bot_id: place.workspace_id ? undefined : place.bot_id,
only_workspace_id: place.workspace_id,
own_list: !place.workspace_id,
open_only: true,
});
@@ -180,7 +200,13 @@ export default function createTaskTool({
created_by_bot_id: bot_id,
});
return `Task #${task.id} "${task.title}" added ${place.workspace_id ? "to this workspace's list" : place.bot_id == Number(bot_id) ? "to your list" : `to bot ${place.bot_id}'s list`}. Check it with list_tasks before you mention it to the user.`;
const where = place.workspace_id
? `in workspace ${place.workspace_id}, ${!place.bot_id ? "shared" : place.bot_id == Number(bot_id) ? "yours" : `bot ${place.bot_id}'s`}`
: place.bot_id == Number(bot_id)
? "to your list"
: `to bot ${place.bot_id}'s list`;
return `Task #${task.id} "${task.title}" added ${where}. Check it with list_tasks before you mention it to the user.`;
},
};
}