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:
@@ -0,0 +1,47 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import mergeHookCallSql from "./merge-hook-call-sql";
|
||||
|
||||
/**
|
||||
* A burst of hook calls makes one waiting run: a call joins the last run
|
||||
* only while it waits untried. Run against real SQLite, since a wrong
|
||||
* condition would either change the brief of a run already working or
|
||||
* pile every call into a run that is retrying.
|
||||
*/
|
||||
const db = new Database(":memory:");
|
||||
|
||||
db.run(`CREATE TABLE bots_queue (id INTEGER PRIMARY KEY, status TEXT, attempts INTEGER, instructions TEXT, updated_at INTEGER)`);
|
||||
db.run(`INSERT INTO bots_queue VALUES
|
||||
(1, 'pending', 0, 'Brief', 0),
|
||||
(2, 'processing', 1, 'Brief', 0),
|
||||
(3, 'pending', 1, 'Brief', 0),
|
||||
(4, 'completed', 1, 'Brief', 0),
|
||||
(5, 'pending', NULL, NULL, 0)`);
|
||||
|
||||
function merge(job_id: number) {
|
||||
const { sql, values } = mergeHookCallSql({ job_id, block: "CALL", now: 42 });
|
||||
|
||||
return db.query(sql).all(...values).length > 0;
|
||||
}
|
||||
|
||||
function brief(job_id: number) {
|
||||
return (db.query(`SELECT instructions FROM bots_queue WHERE id = ?`).get(job_id) as { instructions: string })
|
||||
.instructions;
|
||||
}
|
||||
|
||||
describe("mergeHookCallSql", () => {
|
||||
test("joins a run still waiting, untried", () => {
|
||||
expect(merge(1)).toBe(true);
|
||||
expect(brief(1)).toBe("Brief\n\nAnother call came in before this run started:\n\nCALL");
|
||||
expect(merge(5)).toBe(true);
|
||||
expect(brief(5)).toContain("CALL");
|
||||
});
|
||||
|
||||
test("leaves a working, retrying or finished run alone", () => {
|
||||
expect(merge(2)).toBe(false);
|
||||
expect(merge(3)).toBe(false);
|
||||
expect(merge(4)).toBe(false);
|
||||
expect(brief(2)).toBe("Brief");
|
||||
expect(brief(3)).toBe("Brief");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user