Files
cobble/src/functions/backend/db/bots-queue/complete-bot-job.ts
T
tbenandClaude Opus 5.5 d7f88d8dd3 Keep a record of each run's tool calls so bots can't claim work they never did
The wrap-up turn now gets the run's actual tool calls, each bot message
keeps them as bot_messages.tool_log, and history follows recorded bot
messages with an [App record] line. The coordinator also gets update_bot
in workspaces, so it can fill a bot's core prompt from there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 11:32:56 +01:00

155 lines
5.0 KiB
TypeScript

import BunSQLite from "@moduletrace/bun-sqlite";
import AppError from "@/src/utils/app-error";
import { BotNeedsYouStatusLine } from "@/src/dict/bot-queue-dict";
import insertBotMessage from "../bots/insert-bot-message";
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";
import withMessageUsage from "../usage/with-message-usage";
import type { HarnessToolUse } from "@/src/harness/types";
type Params = {
job_id: number | string;
worker: string;
/**
* The completion message for the user (markdown). Also kept as the
* job's `result`.
*/
result: string;
/**
* Flag the message as needing the user.
*/
needs_you?: boolean;
/**
* Finish without posting a message.
*/
silent?: boolean;
/**
* Files the bot saved and asked to send with it (`media` ids).
*/
attachments?: number[];
/**
* Every tool call the run made, kept on the message.
*/
tool_log?: HarnessToolUse[];
};
/**
* 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; 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`), 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,
worker,
result,
needs_you,
silent,
attachments,
tool_log,
}: Params) {
const job = await grabLockedBotJob({ job_id, worker });
if (!job?.id || !job.bot_id) {
throw new AppError(`This job is no longer held by ${worker}.`);
}
const res = await BunSQLite.queue.complete({
table: "bots_queue",
id: job.id,
});
if (!res.success) {
throw new Error(`Couldn't complete the job: ${res.msg || res.error}`);
}
const now = Date.now();
await BunSQLite.sql({
sql: `UPDATE bots_queue SET result = ?, finished_at = ? WHERE id = ?`,
values: [result, now, job.id],
});
if (job.task_id) {
await BunSQLite.sql({
sql: `UPDATE bot_tasks SET status = 'done', updated_at = ? WHERE id = ?`,
values: [now, job.task_id],
});
}
const inserted = silent
? null
: await insertBotMessage({
bot_id: job.bot_id,
session_id: await jobSessionId({ job }),
workspace_id: job.workspace_id || null,
text: result,
needs_you,
media_ids: attachments,
tool_log,
});
if (job.id && inserted?.id) {
/**
* The job's model calls were written down as they happened, before
* its message existed. Now that it does, they belong to it, so the
* thread can show what the answer cost.
*/
await BunSQLite.sql({
sql: `UPDATE token_usages SET message_id = ? WHERE job_id = ? AND message_id IS NULL`,
values: [inserted.id, job.id],
}).catch(() => {});
}
/**
* Pushed live as it is returned, so it carries its usage now: the
* thread shows the tokens without a reload.
*/
const message = inserted
? ((await withMessageUsage({ messages: [inserted] }).catch(() => [inserted]))[0] ?? inserted)
: null;
const user_id = await grabJobUserId({ job });
const bot = !user_id
? null
: needs_you
? await setBotFace({
bot_id: job.bot_id,
user_id,
face: "needs-you",
status_line: BotNeedsYouStatusLine,
})
: (await countOpenBotJobs({ bot_id: job.bot_id, user_id }))
? await restoreRunningFace({ bot_id: job.bot_id, user_id })
: await setBotFace({
bot_id: job.bot_id,
user_id,
face: job.kind == "reply" ? "idle" : "done",
});
await noteTrackerTask({ job });
const relay = silent ? null : await relayJobToRequester({ job, text: result });
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;
}