Let bots take in messages sent while they reply
A reply read the session once at the start, so a message sent while it ran was never seen, and the follow-up reply queued for it then finished silently: the message was saved before the reply, so the thread ended with the bot. Those messages went unanswered. The text loop now reads new user messages after every round of tools and before an answer goes out, and the bot decides from what each one means whether it changes the work (folded in, or the unsent answer redrafted) or is a separate request, which it acknowledges at once with the new send_message tool and does after the current work. Messages sent that way are marked interim, so the thinking row stays up while the bot works. A reply job records the newest user message it read, and a queued reply only stays silent when a finished reply already read everything. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import answeredMessageIdSql from "./answered-message-id-sql";
|
||||
|
||||
/**
|
||||
* What decides whether a queued reply still has something to answer, run
|
||||
* against real SQLite: only finished replies in the same session count, so
|
||||
* a failed or still running one never hides a message.
|
||||
*/
|
||||
const db = new Database(":memory:");
|
||||
|
||||
db.run(
|
||||
`CREATE TABLE bots_queue (id INTEGER PRIMARY KEY, session_id INTEGER, kind TEXT, status TEXT, message_id INTEGER)`,
|
||||
);
|
||||
db.run(`INSERT INTO bots_queue VALUES
|
||||
(1, 100, 'reply', 'completed', 10),
|
||||
(2, 100, 'reply', 'completed', 14),
|
||||
(3, 100, 'reply', 'processing', 20),
|
||||
(4, 100, 'reply', 'failed', 22),
|
||||
(5, 100, 'task', 'completed', 30),
|
||||
(6, 101, 'reply', 'completed', 40),
|
||||
(7, 102, 'reply', 'pending', 50)`);
|
||||
|
||||
function answered(session_id: number) {
|
||||
const { sql, values } = answeredMessageIdSql({ session_id });
|
||||
|
||||
return (db.query(sql).get(...values) as { id: number | null }).id;
|
||||
}
|
||||
|
||||
describe("answeredMessageIdSql", () => {
|
||||
test("is the newest message a finished reply in the session read", () => {
|
||||
expect(answered(100)).toBe(14);
|
||||
expect(answered(101)).toBe(40);
|
||||
});
|
||||
|
||||
test("ignores running and failed replies, and tasks", () => {
|
||||
expect(answered(100)).not.toBe(20);
|
||||
expect(answered(100)).not.toBe(22);
|
||||
expect(answered(100)).not.toBe(30);
|
||||
});
|
||||
|
||||
test("is null where no reply has finished", () => {
|
||||
expect(answered(102)).toBe(null);
|
||||
expect(answered(999)).toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { SqlCondition } from "@/src/utils/crew-access/types";
|
||||
|
||||
type Params = {
|
||||
session_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The SQL for the newest user message a finished reply in the session
|
||||
* answered: a reply job's `message_id` is the newest one it read
|
||||
* (`markReplyAnswered`), so a message with a higher id came in after the
|
||||
* bot last looked and still needs an answer, wherever its reply landed in
|
||||
* the thread. Selects one `id` column, null when no reply has finished.
|
||||
*/
|
||||
export default function answeredMessageIdSql({ session_id }: Params): SqlCondition {
|
||||
return {
|
||||
sql: `SELECT MAX(message_id) AS id FROM bots_queue
|
||||
WHERE session_id = ? AND kind = 'reply' AND status = 'completed'`,
|
||||
values: [Number(session_id)],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import answeredMessageIdSql from "./answered-message-id-sql";
|
||||
|
||||
type Params = {
|
||||
session_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The newest user message a finished reply in the session answered
|
||||
* (`answeredMessageIdSql`), or null when none is on record.
|
||||
*/
|
||||
export default async function grabAnsweredMessageId({ session_id }: Params) {
|
||||
const { sql, values } = answeredMessageIdSql({ session_id });
|
||||
|
||||
const res = await BunSQLite.sql<{ id: number | null }>({ sql, values });
|
||||
const id = res.payload?.[0]?.id;
|
||||
|
||||
return id ? Number(id) : null;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
type Params = {
|
||||
job_id: number | string;
|
||||
/**
|
||||
* The newest user message the reply read.
|
||||
*/
|
||||
message_id: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Records the newest user message a running reply read, so a reply queued
|
||||
* for a message it already took in finishes silently and one it missed is
|
||||
* still answered (`grabAnsweredMessageId`). Never lowers it.
|
||||
*/
|
||||
export default async function markReplyAnswered({ job_id, message_id }: Params) {
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bots_queue SET message_id = ?
|
||||
WHERE id = ? AND status = 'processing' AND (message_id IS NULL OR message_id < ?)`,
|
||||
values: [message_id, job_id, message_id],
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user