Give every user their own bot face

A bot's face and status line live in bot_user_faces, one row per bot and
user, set by jobs for the user whose conversation they're in. Needs-you
and working only show to that user; every bot read overlays the viewer's
face (botFaceSql) and pushes go to the user a face change is for.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-19 13:58:26 +01:00
co-authored by Claude Opus 5.5
parent 47c58bc76b
commit ab25158d87
32 changed files with 448 additions and 181 deletions
@@ -1,6 +1,7 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import { AppData } from "@/src/data/app-data";
import type { WorkspaceActivityItem } from "@/src/types";
import botFaceSql from "@/src/utils/bot-face-sql";
type Params = {
user_id: number | string;
@@ -9,19 +10,21 @@ type Params = {
/**
* The user's latest conversations in a workspace, most recent first, each
* with the bot it's with, its message count and latest message. Empty
* with the bot it's with (its face as the user sees it), its message count and latest message. Empty
* sessions are left out unless active.
*/
export default async function grabWorkspaceActivity({
user_id,
workspace_id,
}: Params): Promise<WorkspaceActivityItem[]> {
const face = botFaceSql({ user_id, prefix: "bot_" });
const res = await BunSQLite.sql<WorkspaceActivityItem>({
sql: `SELECT s.*,
b.name AS bot_name,
b.image_media_id AS bot_image_media_id,
b.hue AS bot_hue,
b.face AS bot_face,
${face.sql},
(SELECT COUNT(*) FROM bot_messages m WHERE m.session_id = s.id) AS message_count,
(SELECT m.text FROM bot_messages m WHERE m.session_id = s.id ORDER BY m.created_at DESC, m.id DESC LIMIT 1) AS last_message_text
FROM bot_sessions s
@@ -33,7 +36,7 @@ export default async function grabWorkspaceActivity({
))
ORDER BY COALESCE(s.last_activity_at, s.created_at) DESC, s.id DESC
LIMIT ?`,
values: [workspace_id, user_id, AppData.WorkspaceActivityItems],
values: [...face.values, workspace_id, user_id, AppData.WorkspaceActivityItems],
});
return res.payload || [];
@@ -1,11 +1,12 @@
import type { BotWithFace } from "@/src/types";
import type {
BUN_SQLITE_COBBLE_BOTS,
BUN_SQLITE_COBBLE_WORKSPACE_BOTS,
BUN_SQLITE_COBBLE_WORKSPACES,
} from "@/db/types/db";
import BunSQLite from "@moduletrace/bun-sqlite";
import workspaceBotsSql from "@/src/utils/crew-access/workspace-bots-sql";
import workspacesSql from "@/src/utils/crew-access/workspaces-sql";
import botFaceSql from "@/src/utils/bot-face-sql";
import grabCrewRole from "../access/grab-crew-role";
type Params = {
@@ -16,7 +17,10 @@ type Params = {
export type WorkspaceBotMembership = {
workspace: BUN_SQLITE_COBBLE_WORKSPACES;
bot: BUN_SQLITE_COBBLE_BOTS;
/**
* With the user's own face (`botFaceSql`).
*/
bot: BotWithFace;
/**
* Its `workspace_bots` row. Null for the coordinator, which works in
* every workspace without one until it's given instructions there.
@@ -47,11 +51,12 @@ export default async function grabWorkspaceBot({
if (!workspace?.id) return null;
const bot_access = workspaceBotsSql({ role, workspace_id: workspace.id });
const face = botFaceSql({ user_id });
const bot = (
await BunSQLite.sql<BUN_SQLITE_COBBLE_BOTS>({
sql: `SELECT b.* FROM bots b WHERE b.id = ? AND ${bot_access.sql} LIMIT 1`,
values: [bot_id, ...bot_access.values],
await BunSQLite.sql<BotWithFace>({
sql: `SELECT b.*, ${face.sql} FROM bots b WHERE b.id = ? AND ${bot_access.sql} LIMIT 1`,
values: [...face.values, bot_id, ...bot_access.values],
})
).payload?.[0];