Let a bot answer its sessions side by side, and show a due reply in each
Replies were locked per bot, so a long reply in one session held up every other session's reply with nothing shown. The lock is now per session. A chat shows the thinking row whenever the user has the last word, rather than from the bot's face (one per user, shared by every session), so it shows even while the bot works elsewhere. A task waiting for its busy computer says so, and a job finishing while another still runs puts back the running one's face. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -9,7 +9,12 @@ import { logError } from "@/src/functions/backend/logger";
|
||||
import createKeyLock from "@/src/utils/create-key-lock";
|
||||
import HarnessError from "@/src/harness/harness-error";
|
||||
import deferBotJob from "@/src/functions/backend/db/bots-queue/defer-bot-job";
|
||||
import { BotReplyJobTitle, BotTaskStatusLine, BotWaitingStatusLine } from "@/src/dict/bot-queue-dict";
|
||||
import {
|
||||
BotComputerBusyStatusLine,
|
||||
BotReplyJobTitle,
|
||||
BotTaskStatusLine,
|
||||
BotWaitingStatusLine,
|
||||
} from "@/src/dict/bot-queue-dict";
|
||||
import BotJobFatalError from "./bot-job-fatal-error";
|
||||
import describeJobStep from "./describe-job-step";
|
||||
import pushToSessionUser from "./push-to-session-user";
|
||||
@@ -26,7 +31,8 @@ import type { BotJobContext, BotJobRunner } from "./types";
|
||||
const computer_lock = createKeyLock();
|
||||
|
||||
/**
|
||||
* One reply per bot at a time, so replies never cross in the chat.
|
||||
* One reply per conversation at a time, so replies never cross in the
|
||||
* chat. A bot's other sessions answer side by side.
|
||||
*/
|
||||
const reply_lock = createKeyLock();
|
||||
|
||||
@@ -133,11 +139,23 @@ export default async function processBotJob({
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* A task whose computer is busy says so while it waits, so the
|
||||
* user doesn't see silence.
|
||||
*/
|
||||
const runOnComputer = async (computer_id: number) => {
|
||||
if (computer_lock.busy({ key: computer_id })) {
|
||||
await setFace({ face: "asleep", status_line: BotComputerBusyStatusLine });
|
||||
}
|
||||
|
||||
return await computer_lock.run({ key: computer_id, task: run });
|
||||
};
|
||||
|
||||
const outcome =
|
||||
job.kind == "reply"
|
||||
? await reply_lock.run({ key: bot_id, task: run })
|
||||
? await reply_lock.run({ key: job.session_id || `bot-${bot_id}`, task: run })
|
||||
: job.computer_id
|
||||
? await computer_lock.run({ key: job.computer_id, task: run })
|
||||
? await runOnComputer(job.computer_id)
|
||||
: await run();
|
||||
|
||||
if (controller.signal.aborted) return;
|
||||
|
||||
@@ -55,6 +55,12 @@ export const BotNeedsYouStatusLine = "Waiting on you";
|
||||
*/
|
||||
export const BotWaitingStatusLine = "Waiting for a free model";
|
||||
|
||||
/**
|
||||
* The bot's status line while its task waits for another job on the same
|
||||
* computer to finish.
|
||||
*/
|
||||
export const BotComputerBusyStatusLine = "Waiting for its computer";
|
||||
|
||||
/**
|
||||
* How a job step that called a tool reads in the chat's working row, by
|
||||
* tool name. Tools not listed read "Using <name>".
|
||||
|
||||
@@ -8,6 +8,7 @@ import grabLockedBotJob from "./grab-locked-bot-job";
|
||||
import relayJobToRequester from "./relay-job-to-requester";
|
||||
import jobSessionId from "./job-session-id";
|
||||
import grabJobUserId from "./grab-job-user-id";
|
||||
import grabRunningBotJobFace from "./grab-running-bot-job-face";
|
||||
|
||||
type Params = {
|
||||
job_id: number | string;
|
||||
@@ -31,7 +32,9 @@ type Params = {
|
||||
* Marks a job done, closes its task and sends the completion message back
|
||||
* to the user in the bot's conversation. Unless it has other jobs open
|
||||
* for that user, the bot shows that user needs-you if asked, else done
|
||||
* after a task and idle after a reply; nobody else's face changes. A handed-over job's result also goes to the bot that handed
|
||||
* 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.
|
||||
*/
|
||||
export default async function completeBotJob({
|
||||
@@ -92,7 +95,7 @@ export default async function completeBotJob({
|
||||
status_line: BotNeedsYouStatusLine,
|
||||
})
|
||||
: (await countOpenBotJobs({ bot_id: job.bot_id, user_id }))
|
||||
? null
|
||||
? await restoreRunningFace({ bot_id: job.bot_id, user_id })
|
||||
: await setBotFace({
|
||||
bot_id: job.bot_id,
|
||||
user_id,
|
||||
@@ -103,3 +106,12 @@ export default async function completeBotJob({
|
||||
|
||||
return { message, bot, relay };
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts back the face of a job still running for the user, if any.
|
||||
*/
|
||||
async function restoreRunningFace({ bot_id, user_id }: { bot_id: number; user_id: number }) {
|
||||
const running = await grabRunningBotJobFace({ bot_id, user_id });
|
||||
|
||||
return running ? await setBotFace({ bot_id, user_id, ...running }) : null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BotFace } from "@/src/dict/bot-dict";
|
||||
import { BotReplyJobTitle, BotTaskStatusLine } from "@/src/dict/bot-queue-dict";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
/**
|
||||
* Only jobs in this user's conversations.
|
||||
*/
|
||||
user_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The face a bot should show one user while other jobs for them are
|
||||
* still running: thinking while a reply runs (it's short, and its chat
|
||||
* needs the row), else working on a task. Null when none is running.
|
||||
*/
|
||||
export default async function grabRunningBotJobFace({
|
||||
bot_id,
|
||||
user_id,
|
||||
}: Params): Promise<{ face: BotFace; status_line: string } | null> {
|
||||
const kinds = (
|
||||
await BunSQLite.sql<{ kind: string }>({
|
||||
sql: `SELECT DISTINCT kind FROM bots_queue WHERE bot_id = ? AND status = 'processing'
|
||||
AND session_id IN (SELECT id FROM bot_sessions WHERE user_id = ?)`,
|
||||
values: [bot_id, user_id],
|
||||
})
|
||||
).payload?.map((row) => row.kind) || [];
|
||||
|
||||
if (kinds.includes("reply")) return { face: "working", status_line: BotReplyJobTitle };
|
||||
if (kinds.includes("task")) return { face: "working", status_line: BotTaskStatusLine };
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -3,7 +3,11 @@ import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import BotAvatar from "@/src/components/general/bot-avatar";
|
||||
import type { BotHue } from "@/src/dict/bot-dict";
|
||||
import { BotReplyJobTitle, BotWaitingStatusLine } from "@/src/dict/bot-queue-dict";
|
||||
import {
|
||||
BotComputerBusyStatusLine,
|
||||
BotReplyJobTitle,
|
||||
BotWaitingStatusLine,
|
||||
} from "@/src/dict/bot-queue-dict";
|
||||
import type { BotWithLastMessage } from "@/src/types";
|
||||
|
||||
type Props = {
|
||||
@@ -24,17 +28,23 @@ type Props = {
|
||||
|
||||
/**
|
||||
* Under the last message while the bot is working on a reply or a task
|
||||
* (or waiting for a model to come back), so slow work doesn't look like
|
||||
* none. A task shows its latest step. The bot's face is shared by
|
||||
* everyone who sees it, so the row only shows for work in this
|
||||
* conversation: a reply while the user has the last word, or its own
|
||||
* task.
|
||||
* (or waiting for a model or its computer), so slow work doesn't look like
|
||||
* none. A task shows its latest step. The bot's face is one per user,
|
||||
* shared by all their sessions with it, so a reply shows from the thread
|
||||
* alone: every user message queues one, so while the user has the last
|
||||
* word a reply is due here, even while the bot works elsewhere. Only the
|
||||
* waits (for a model, for its computer) come from the face.
|
||||
*/
|
||||
export default function BotThinkingRow({ bot, progress, awaiting_reply, has_task }: Props) {
|
||||
const name = bot.name || "The bot";
|
||||
const thinking = bot.face == "working" && bot.status_line == BotReplyJobTitle && Boolean(awaiting_reply);
|
||||
const working = bot.face == "working" && bot.status_line != BotReplyJobTitle && Boolean(has_task);
|
||||
const waiting = bot.face == "asleep" && bot.status_line == BotWaitingStatusLine && Boolean(awaiting_reply || has_task);
|
||||
const asleep = bot.face == "asleep";
|
||||
const waiting_model = asleep && bot.status_line == BotWaitingStatusLine && Boolean(awaiting_reply || has_task);
|
||||
const waiting_computer =
|
||||
asleep && bot.status_line == BotComputerBusyStatusLine && Boolean(has_task) && !awaiting_reply;
|
||||
const waiting = waiting_model || waiting_computer;
|
||||
const thinking = Boolean(awaiting_reply) && !waiting;
|
||||
const working =
|
||||
!awaiting_reply && bot.face == "working" && bot.status_line != BotReplyJobTitle && Boolean(has_task);
|
||||
|
||||
if (!thinking && !working && !waiting) return null;
|
||||
|
||||
@@ -42,7 +52,7 @@ export default function BotThinkingRow({ bot, progress, awaiting_reply, has_task
|
||||
? `${name} is thinking…`
|
||||
: working
|
||||
? `${name} is working…`
|
||||
: `${BotWaitingStatusLine}…`;
|
||||
: `${bot.status_line}…`;
|
||||
const detail = working ? progress : null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -65,4 +65,19 @@ describe("createKeyLock", () => {
|
||||
expect(failed).rejects.toThrow("boom");
|
||||
expect(await next).toBe("ok");
|
||||
});
|
||||
|
||||
test("is busy while a task with the key is running", async () => {
|
||||
const lock = createKeyLock();
|
||||
|
||||
expect(lock.busy({ key: 1 })).toBe(false);
|
||||
|
||||
const running = lock.run({ key: 1, task: () => wait(10) });
|
||||
|
||||
expect(lock.busy({ key: 1 })).toBe(true);
|
||||
expect(lock.busy({ key: 2 })).toBe(false);
|
||||
|
||||
await running;
|
||||
|
||||
expect(lock.busy({ key: 1 })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,5 +28,13 @@ export default function createKeyLock() {
|
||||
}
|
||||
}
|
||||
|
||||
return { run, size: () => tails.size };
|
||||
/**
|
||||
* Whether a task with this key is running or queued, so a caller can
|
||||
* say it's waiting before it joins the line.
|
||||
*/
|
||||
function busy({ key }: { key: string | number }) {
|
||||
return tails.has(String(key));
|
||||
}
|
||||
|
||||
return { run, busy, size: () => tails.size };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user