Let outside services and computers' browser notifications give bots work
A bot hook (bot_hooks) is a secret URL, POST /api/hooks/<token>, whose calls queue the hook's brief plus the raw body as a task, per user and bot like a routine. Only the token's hash is kept (shown once, rotatable); an optional HMAC-SHA256 signing secret is stored encrypted. Calls are rate limited per address and per hook, recorded in bot_hook_events (pruned daily), deduplicated by delivery id, and a call while the last run still waits joins its brief instead of queueing another. The payload is fenced data and HookRunNote tells the bot never to take instructions from it; failed runs retry like routines. A hook can instead watch a computer's browser notifications: the app installs a small watcher that owns org.freedesktop.Notifications on the sandbox's session bus, so Chrome hands it every notification (it only looks for that service at start-up, so Chrome is restarted once, never mid-task), and it posts each one to its hooks, optionally filtered. Computers reach the app at COMPUTER_APP_URL or the app container's name. Users make hooks in the conversation's Hooks modal and manage them all on /admin/routines; bots never create or change hooks. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -10,6 +10,7 @@ import createKeyLock from "@/src/utils/create-key-lock";
|
||||
import withTimeout from "@/src/utils/with-timeout";
|
||||
import HarnessError from "@/src/harness/harness-error";
|
||||
import deferBotJob from "@/src/functions/backend/db/bots-queue/defer-bot-job";
|
||||
import recordBotHookFailure from "@/src/functions/backend/db/bot-hooks/record-bot-hook-failure";
|
||||
import recordCronJobFailure from "@/src/functions/backend/db/cron-jobs/record-cron-job-failure";
|
||||
import scheduledRunRetryAt from "@/src/utils/scheduled-run-retry-at";
|
||||
import {
|
||||
@@ -62,7 +63,8 @@ type Params = {
|
||||
* the user. If the job is cancelled or lost mid-way, its result is
|
||||
* dropped. A failed scheduled run (a routine) is retried with longer gaps
|
||||
* (`scheduledRunRetryAt`); once out of tries the user is told, and the
|
||||
* schedule keeps why (`recordCronJobFailure`).
|
||||
* schedule keeps why (`recordCronJobFailure`). A hook call's run is
|
||||
* retried the same way and its hook keeps why (`recordBotHookFailure`).
|
||||
*/
|
||||
export default async function processBotJob({
|
||||
job,
|
||||
@@ -75,6 +77,7 @@ export default async function processBotJob({
|
||||
const bot_id = job.bot_id;
|
||||
const params = parseJobJson({ text: job.params }) || {};
|
||||
const schedule_id = Number(params.schedule_id) || null;
|
||||
const hook_id = Number(params.hook_id) || null;
|
||||
|
||||
/**
|
||||
* Its face changes only for the user it works for.
|
||||
@@ -282,7 +285,9 @@ export default async function processBotJob({
|
||||
fatal: error instanceof BotJobFatalError,
|
||||
failure_message: schedule_id
|
||||
? `My scheduled run "${job.title || "Scheduled task"}" failed after ${tries} ${tries == 1 ? "try" : "tries"}: ${message}`
|
||||
: undefined,
|
||||
: hook_id
|
||||
? `My run for the hook "${job.title || "Hook call"}" failed after ${tries} ${tries == 1 ? "try" : "tries"}: ${message}`
|
||||
: undefined,
|
||||
}).catch(async (fail_error) => {
|
||||
await logError({
|
||||
title: `Bot job #${job_id} couldn't be failed`,
|
||||
@@ -317,6 +322,7 @@ export default async function processBotJob({
|
||||
session_id: job.session_id,
|
||||
workspace_id: job.workspace_id,
|
||||
schedule_id,
|
||||
hook_id,
|
||||
fatal: error instanceof BotJobFatalError,
|
||||
harness_code: error instanceof HarnessError ? error.code : undefined,
|
||||
},
|
||||
@@ -333,10 +339,15 @@ export default async function processBotJob({
|
||||
);
|
||||
}
|
||||
|
||||
if (hook_id && failed?.status == "failed") {
|
||||
await recordBotHookFailure({ hook_id, job_id, error: message }).catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* A routine's retry waits long enough for an outage to pass.
|
||||
* A routine's or hook call's retry waits long enough for an outage
|
||||
* to pass.
|
||||
*/
|
||||
if (schedule_id && failed?.status == "pending") {
|
||||
if ((schedule_id || hook_id) && failed?.status == "pending") {
|
||||
await deferBotJob({ job_id, worker, until: scheduledRunRetryAt({ attempts: failed.attempts }) });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user