From e95a2f838a5ee3090f7b3d7d2132489429864956 Mon Sep 17 00:00:00 2001 From: Guido Vizoso Date: Mon, 26 Oct 2026 12:26:05 -0300 Subject: [PATCH 01/13] Say what a conversation is waiting on, on reads and on every busy event The busy signal said only that something was running, on the socket and nowhere else. Read what a conversation waits on from the rows that already say so: an unfinished hop it asked, folded with its relay, and an open routine run. The channel read and the roster carry the list, and every busy event carries the whole list at the moment it is sent. --- server/src/channels/events.ts | 10 + server/src/channels/routes.ts | 146 ++++++-- server/src/channels/working.ts | 226 +++++++++++ server/tests/channel-routes.test.ts | 89 ++++- server/tests/channel-work.integration.test.ts | 353 ++++++++++++++++++ 5 files changed, 782 insertions(+), 42 deletions(-) create mode 100644 server/src/channels/working.ts create mode 100644 server/tests/channel-work.integration.test.ts diff --git a/server/src/channels/events.ts b/server/src/channels/events.ts index ebdd596653..c4c2dc6466 100644 --- a/server/src/channels/events.ts +++ b/server/src/channels/events.ts @@ -1,4 +1,5 @@ import postgres from "postgres"; +import type { ChannelWorkWire } from "./working"; /** * Live channel activity, from whoever ran an agent to everybody else in the channel. @@ -45,6 +46,15 @@ export type ChannelActivityEvent = { * streams. A missed one costs at most a stuck-looking dot until the next real event, never data. */ busy?: boolean; + /** + * Everything this channel is waiting on right now: a Bot it asked, a routine firing into it. + * Absent on an ordinary activity event. + * + * The WHOLE list, never a change to it, so the newest event is the answer and a missed one is + * corrected by the next. Read from the queue and the run history at the moment of sending, which + * is also what the roster and the channel read return. See `working.ts`. + */ + work?: ChannelWorkWire[]; }; /** "The roster you hold may be wrong." Carries no delta, because what was lost is not recoverable. */ diff --git a/server/src/channels/routes.ts b/server/src/channels/routes.ts index 2798aca78a..dbc855c1b6 100644 --- a/server/src/channels/routes.ts +++ b/server/src/channels/routes.ts @@ -37,6 +37,7 @@ import { import { upgradeWebSocket } from "./socket"; import { oneLine } from "./text"; import type { ThreadIdentity } from "./thread-identity"; +import { type ChannelWork, channelWorkDto, workIn } from "./working"; export type AgentChannel = { id: string; @@ -235,6 +236,17 @@ export type ChannelStore = { channelId: string, busy: boolean, ): Promise; + /** + * Tell a channel's members what it is waiting on now, without saying a turn started or ended. + * + * For the moments the queue moves and no turn does: a hop finished, released for another try, or + * turned into a relay. Every busy signal carries the same list, so this is only for those. + */ + signalWork(threadId: string): Promise; + /** What each of these channels is waiting on. See `workIn` in working.ts. */ + workIn( + channels: readonly { id: string; threadId: string }[], + ): Promise>; }; const PRIVATE_AGENT_CHANNEL_DESCRIPTION = "Private agent channel."; @@ -319,6 +331,44 @@ export function createChannelStore( return { id, name, agentIds, threadId, active: true, lastMessageAt: null }; }; + /** + * Tell a channel's members what it is waiting on, and whether a turn just started or ended. + * + * No table write: busy is a moment, and the rows the work list is read from stay the source of + * truth. Just the announcement, carrying the members the same way recordActivity does. The list + * rides on every one of these so a client never merges a delta into what it holds: the newest + * event is the whole answer, and the same answer a reload gets. + */ + const announceWork = async ( + channelId: string, + threadId: string | null, + busy?: boolean, + ): Promise => { + const members = await database + .select({ userId: channelMemberships.userId }) + .from(channelMemberships) + .where(eq(channelMemberships.channelId, channelId)); + if (members.length === 0) return; + + const work = threadId + ? ((await workIn(database, [{ id: channelId, threadId }])).get( + channelId, + ) ?? []) + : []; + const event: ChannelActivityEvent = { + channelId, + memberIds: members.map((member) => member.userId), + lastMessage: null, + lastMessageAt: null, + lastMessageAgentId: null, + ...(busy === undefined ? {} : { busy }), + work: work.map(channelWorkDto), + }; + await database.execute( + sql`select pg_notify(${CHANNEL_ACTIVITY_TOPIC}, ${JSON.stringify(event)})`, + ); + }; + const store: ChannelStore = { create(actor, agentIds) { return database.transaction( @@ -839,31 +889,25 @@ export function createChannelStore( .where(eq(intelligenceChannelMappings.threadId, threadId)) .limit(1); if (!mapped) return; + await announceWork(mapped.channelId, threadId, busy); + }, - const members = await database - .select({ userId: channelMemberships.userId }) - .from(channelMemberships) - .where(eq(channelMemberships.channelId, mapped.channelId)); - if (members.length === 0) return; - - // No table write: busy is a moment, and the roster query stays the source of truth. Just the - // announcement, carrying the members the same way recordActivity does. - const event: ChannelActivityEvent = { - channelId: mapped.channelId, - memberIds: members.map((member) => member.userId), - lastMessage: null, - lastMessageAt: null, - lastMessageAgentId: null, - busy, - }; - await database.execute( - sql`select pg_notify(${CHANNEL_ACTIVITY_TOPIC}, ${JSON.stringify(event)})`, - ); + async signalWork(threadId) { + const [mapped] = await database + .select({ channelId: intelligenceChannelMappings.channelId }) + .from(intelligenceChannelMappings) + .where(eq(intelligenceChannelMappings.threadId, threadId)) + .limit(1); + if (!mapped) return; + await announceWork(mapped.channelId, threadId); }, async signalChannelBusy(actor, channelId, busy) { const [membership] = await database - .select({ userId: channelMemberships.userId }) + .select({ + userId: channelMemberships.userId, + threadId: intelligenceChannelMappings.threadId, + }) .from(channelMemberships) .innerJoin( channels, @@ -872,33 +916,32 @@ export function createChannelStore( isNull(channels.deletedAt), ), ) + // The caller's own thread for the channel, which is the one their hops are offered from. + .leftJoin( + intelligenceChannelMappings, + and( + eq( + intelligenceChannelMappings.channelId, + channelMemberships.channelId, + ), + eq(intelligenceChannelMappings.userId, channelMemberships.userId), + ), + ) .where( and( eq(channelMemberships.channelId, channelId), eq(channelMemberships.userId, actor.id), ), - ); + ) + .limit(1); // Not a member, no such channel, or a deleted one: the same refusal every way, so belonging // to a channel is not something an outsider can probe for. if (!membership) throw new ChannelNotFoundError(channelId); - const members = await database - .select({ userId: channelMemberships.userId }) - .from(channelMemberships) - .where(eq(channelMemberships.channelId, channelId)); - - const event: ChannelActivityEvent = { - channelId, - memberIds: members.map((member) => member.userId), - lastMessage: null, - lastMessageAt: null, - lastMessageAgentId: null, - busy, - }; - await database.execute( - sql`select pg_notify(${CHANNEL_ACTIVITY_TOPIC}, ${JSON.stringify(event)})`, - ); + await announceWork(channelId, membership.threadId, busy); }, + + workIn: (list) => workIn(database, list), }; return store; } @@ -1127,8 +1170,16 @@ export function createChannelRoutes( ...(parsed.limit !== undefined ? { limit: parsed.limit } : {}), }); + /* + * What each row is waiting on, read with the page rather than left to the socket. A busy + * flag that only ever arrived as an event was gone on every reload, so a conversation a Bot + * was still answering in looked finished to anybody who refreshed. + */ + const work = await store.workIn(page.channels); return context.json({ - channels: page.channels.map(channelSummaryDto), + channels: page.channels.map((channel) => + channelSummaryDto(channel, work.get(channel.id) ?? []), + ), nextCursor: page.nextCursor, }); } catch (error) { @@ -1231,7 +1282,13 @@ export function createChannelRoutes( if (!channel) { return context.json({ error: "Couldn't find that conversation." }, 404); } - return context.json({ channel: channelDto(channel) }); + const work = await store.workIn([channel]); + return context.json({ + channel: { + ...channelDto(channel), + work: (work.get(channel.id) ?? []).map(channelWorkDto), + }, + }); } catch (error) { return mapStoreError(context, error); } @@ -1257,7 +1314,10 @@ function channelDto(channel: AgentChannel): ChannelWire { }; } -function channelSummaryDto(channel: ChannelSummary) { +function channelSummaryDto( + channel: ChannelSummary, + work: readonly ChannelWork[], +) { return { ...channelDto(channel), summary: channel.summary, @@ -1267,6 +1327,12 @@ function channelSummaryDto(channel: ChannelSummary) { pinned: channel.pinned, // Serialised as ISO-8601 like lastMessageAt, so the browser can compare the two as strings. lastReadAt: channel.lastReadAt?.toISOString() ?? null, + work: work.map(channelWorkDto), + /* + * Only ever true from here. A person's own run is not on any row this can read, so a roster + * that said false would put out the dot another tab's run had just lit. + */ + ...(work.length > 0 ? { busy: true } : {}), }; } diff --git a/server/src/channels/working.ts b/server/src/channels/working.ts new file mode 100644 index 0000000000..60b5b48182 --- /dev/null +++ b/server/src/channels/working.ts @@ -0,0 +1,226 @@ +/** + * What is working on a conversation right now, when nobody's browser is running it. + * + * A conversation can be waiting on work its person cannot see: a Bot it asked, answering in a + * scratch thread nobody is shown, or a routine firing into it. The busy signal used to say only + * "something is running", on the socket and nowhere else, so an open conversation looked idle and a + * reload forgot even that much. + * + * READ FROM THE ROWS THAT ALREADY SAY SO, never written here. A hop is a `work_items` row until it + * is finished and a routine firing is a `routine_runs` row until it has a status, so the queue and + * the run history are the record. A second table of "busy reasons" would be a copy of them that a + * crashed pod leaves behind: the hop would be retried and finished, and the copy would still say the + * conversation was waiting. Derived, the answer a reload gets and the answer a socket event carries + * are the same query. + * + * ONE ENTRY PER PIECE OF WORK, keyed by the work. A hop retried five times is one row on the queue + * and one entry here. A hop whose answer is on its way back is the same entry too: the relay is + * keyed `relay:`, so it is folded into the hop it carries the answer for, and the timer a + * person is watching does not restart when the answer turns around. + */ +import { TURN_LIMITS } from "@openbot/toolkit/limits"; +import { + and, + eq, + gt, + inArray, + isNull, + lt, + lte, + notLike, + or, + sql, +} from "drizzle-orm"; +import { alias } from "drizzle-orm/pg-core"; +import { HANDOFF_KIND } from "../agents/handoff"; +import type { Database } from "../db/client"; +import { agents, routineRuns, routines, workItems } from "../db/schema"; +import { DEFAULT_MAX_ATTEMPTS } from "../work/queue"; + +/** Something a conversation is waiting on. */ +export type ChannelWork = + | { + /** A Bot this conversation's Bot asked, which has not answered here yet. */ + kind: "handoff"; + /** The hop's own key, so two asks of the same Bot are two entries. */ + key: string; + /** The Bot that was asked. */ + botId: string; + /** Its name when the hop was offered. Null on a hop written before names were carried. */ + botName: string | null; + /** When it was asked. */ + since: Date; + } + | { + /** A routine firing into this conversation. */ + kind: "routine"; + /** The run's id. */ + key: string; + routineId: string; + /** The Bot the routine runs as. */ + botId: string; + botName: string | null; + since: Date; + }; + +/** The same, on the wire. */ +export type ChannelWorkWire = Omit & { since: string }; + +export function channelWorkDto(work: ChannelWork): ChannelWorkWire { + return { ...work, since: work.since.toISOString() }; +} + +/** + * How long a routine run with no outcome still counts as running. + * + * The routine sweep's own cutoff for an abandoned run, so this never says a routine is running for + * longer than the sweep would leave its row open. See `ABANDONED_RUN_MS` in routines/sweep.ts. + */ +const ROUTINE_IN_FLIGHT_MS = 2 * TURN_LIMITS.turnTimeoutMs; + +const RELAY_PREFIX = "relay:"; + +type HandoffPayload = { + threadId?: unknown; + toBotId?: unknown; + toName?: unknown; + fromBotId?: unknown; + answerIn?: unknown; +}; + +const text = (value: unknown): string | null => + typeof value === "string" && value.length > 0 ? value : null; + +/** + * What each of these conversations is waiting on, oldest first. A conversation waiting on nothing + * is absent from the map. + */ +export async function workIn( + database: Database, + channels: readonly { id: string; threadId: string }[], +): Promise> { + const found = new Map(); + if (channels.length === 0) return found; + const channelByThread = new Map( + channels.map((channel) => [channel.threadId, channel.id]), + ); + const add = (channelId: string, work: ChannelWork) => { + const list = found.get(channelId) ?? []; + list.push(work); + found.set(channelId, list); + }; + + /* + * The hop that asked, joined to a relay so the relay can be read as the hop it answers: who was + * asked and when. The relay's own payload names the Bot that answered as its sender and nothing + * of when the question was put. + */ + const asked = alias(workItems, "asked"); + const threadOf = sql`${workItems.payload}->>'threadId'`; + const hops = await database + .select({ + key: workItems.key, + payload: workItems.payload, + createdAt: workItems.createdAt, + askedPayload: asked.payload, + askedAt: asked.createdAt, + }) + .from(workItems) + .leftJoin( + asked, + and( + eq(asked.kind, workItems.kind), + sql`${workItems.key} = ${RELAY_PREFIX} || ${asked.key}`, + ), + ) + .where( + and( + eq(workItems.kind, HANDOFF_KIND), + isNull(workItems.finishedAt), + inArray(threadOf, [...channelByThread.keys()]), + // A notice says a hop failed for good. Nothing is coming, so nothing is waited on. + notLike(workItems.key, "notice:%"), + // Given up on: still a row, never claimed again. Unless its last attempt is still running. + or( + lt(workItems.attempts, DEFAULT_MAX_ATTEMPTS), + gt(workItems.leaseUntil, sql`now()`), + ), + /* + * A relay that is running is the asking Bot speaking in this conversation, which is the + * answer arriving rather than a wait for it. Only a relay still queued is a wait. + */ + or( + sql`${workItems.payload}->>'answerIn' is null`, + isNull(workItems.claimedBy), + isNull(workItems.leaseUntil), + lte(workItems.leaseUntil, sql`now()`), + ), + ), + ); + + const seen = new Set(); + for (const hop of hops.sort( + (left, right) => left.createdAt.getTime() - right.createdAt.getTime(), + )) { + const payload = hop.payload as HandoffPayload; + const channelId = channelByThread.get(String(payload.threadId)); + if (!channelId) continue; + const isRelay = text(payload.answerIn) !== null; + const original = (hop.askedPayload ?? null) as HandoffPayload | null; + const key = isRelay ? hop.key.slice(RELAY_PREFIX.length) : hop.key; + if (seen.has(key)) continue; + seen.add(key); + // A relay is sent BY the Bot that was asked, so its sender is the one being waited on. + const botId = isRelay + ? (text(original?.toBotId) ?? text(payload.fromBotId)) + : text(payload.toBotId); + if (!botId) continue; + add(channelId, { + kind: "handoff", + key, + botId, + botName: text(isRelay ? original?.toName : payload.toName), + since: (isRelay ? hop.askedAt : null) ?? hop.createdAt, + }); + } + + const runs = await database + .select({ + runId: routineRuns.id, + routineId: routines.id, + channelId: routines.channelId, + botId: routines.agentId, + botName: agents.name, + startedAt: routineRuns.startedAt, + }) + .from(routineRuns) + .innerJoin(routines, eq(routines.id, routineRuns.routineId)) + .leftJoin(agents, eq(agents.id, routines.agentId)) + .where( + and( + isNull(routineRuns.status), + inArray( + routines.channelId, + channels.map((channel) => channel.id), + ), + gt( + routineRuns.startedAt, + sql`now() - make_interval(secs => ${ROUTINE_IN_FLIGHT_MS / 1000})`, + ), + ), + ); + for (const run of runs) { + add(run.channelId, { + kind: "routine", + key: run.runId, + routineId: run.routineId, + botId: run.botId, + botName: run.botName ?? null, + since: run.startedAt, + }); + } + + for (const list of found.values()) + list.sort((left, right) => left.since.getTime() - right.since.getTime()); + return found; +} diff --git a/server/tests/channel-routes.test.ts b/server/tests/channel-routes.test.ts index ffee18aee7..18bbd9cc85 100644 --- a/server/tests/channel-routes.test.ts +++ b/server/tests/channel-routes.test.ts @@ -86,6 +86,10 @@ function fakeStore( async softDelete(receivedActor, id) { calls.push(["softDelete", receivedActor, id]); }, + // A read beside the one the route was asked for, so it is not a call worth asserting on. + async workIn() { + return new Map(); + }, }; return Object.assign(base, overrides, { calls }); @@ -158,6 +162,81 @@ describe("channel input parser", () => { }); }); +/* + * What a conversation is waiting on comes back with the read, so a reload shows it. It used to live + * on the socket and nowhere else, and a refresh in the middle of a hop forgot it. + */ +describe("what a channel is waiting on", () => { + const since = new Date("2026-10-26T09:00:00.000Z"); + const waiting = new Map([ + [ + "channel-1", + [ + { + kind: "handoff" as const, + key: "hop-1", + botId: "ana", + botName: "Ana", + since, + }, + ], + ], + ]); + + test("the channel read carries the list", async () => { + const store = fakeStore({ workIn: async () => waiting }); + + const fetched = await appFor(store).request( + "http://openbot.test/channel-1", + ); + + expect(await json(fetched)).toEqual({ + channel: { + ...channel(), + work: [ + { + kind: "handoff", + key: "hop-1", + botId: "ana", + botName: "Ana", + since: since.toISOString(), + }, + ], + }, + }); + }); + + test("the roster carries it too, and lights only the rows that have some", async () => { + const summary = (id: string) => ({ + ...channel({ id }), + summary: null, + lastMessage: null, + lastMessageAgentId: null, + createdAt: since, + pinned: false, + lastReadAt: null, + }); + const store = fakeStore({ + list: async () => ({ + channels: [summary("channel-1"), summary("channel-2")], + nextCursor: null, + }), + workIn: async () => waiting, + }); + + const response = await appFor(store).request("http://openbot.test/"); + const body = (await response.json()) as { + channels: Array<{ id: string; busy?: boolean; work: unknown[] }>; + }; + + expect(body.channels[0]).toMatchObject({ id: "channel-1", busy: true }); + expect(body.channels[0]?.work).toHaveLength(1); + // Absent rather than false: a person's own run in another tab is not on any row this reads. + expect(body.channels[1]?.busy).toBeUndefined(); + expect(body.channels[1]?.work).toEqual([]); + }); +}); + describe("channel list limit", () => { /** * `?limit=` used to be read with `Number.parseInt`, which coerces: `"12abc"` arrived as 12 @@ -282,7 +361,9 @@ describe("channel routes", () => { }, }); expect(fetched.status).toBe(200); - expect(await json(fetched)).toEqual({ channel: channel() }); + expect(await json(fetched)).toEqual({ + channel: { ...channel(), work: [] }, + }); }); /** @@ -307,7 +388,11 @@ describe("channel routes", () => { expect(fetched.status).toBe(200); expect(await json(fetched)).toEqual({ - channel: { ...channel(), lastMessageAt: spokenAt.toISOString() }, + channel: { + ...channel(), + lastMessageAt: spokenAt.toISOString(), + work: [], + }, }); }); diff --git a/server/tests/channel-work.integration.test.ts b/server/tests/channel-work.integration.test.ts new file mode 100644 index 0000000000..babc3db3b9 --- /dev/null +++ b/server/tests/channel-work.integration.test.ts @@ -0,0 +1,353 @@ +import { afterAll, afterEach, describe, expect, test } from "bun:test"; +import { randomUUID } from "node:crypto"; +import { eq, like, sql } from "drizzle-orm"; +import { HANDOFF_KIND } from "../src/agents/handoff"; +import { createAgentProfileStore } from "../src/agents/profile-store"; +import type { AgentActor } from "../src/agents/profile-types"; +import { + type ChannelActivityEvent, + createChannelEventHub, + startChannelActivityListener, +} from "../src/channels/events"; +import { createChannelStore } from "../src/channels/routes"; +import { createThreadIdentity } from "../src/channels/thread-identity"; +import { createDatabase } from "../src/db/client"; +import { + agentProfiles, + agents, + channels, + intelligenceChannelMappings, + routineRuns, + routines, + users, + workItems, +} from "../src/db/schema"; +import { TEST_POOL, testDatabaseUrl } from "./support/database"; + +/** + * What a conversation is waiting on, read from the rows that already say so. + * + * A Bot it asked is a `work_items` row until the hop is finished; a routine firing into it is a + * `routine_runs` row until it has an outcome. These tests write those rows the way the queue and + * the routine sweep do and read the answer back, because the list is a query over them and the + * only way to know the query is right is to run it. + */ + +const databaseUrl = testDatabaseUrl(); +const database = createDatabase(databaseUrl, TEST_POOL); +const profileStore = createAgentProfileStore( + database, + new URL("https://managed.example.test/ag-ui"), +); +const store = createChannelStore( + database, + profileStore, + createThreadIdentity("test-deployment"), +); + +const testPrefix = `channel-work-${randomUUID()}`; +const createdUserIds: string[] = []; +const createdAgentIds: string[] = []; +const createdChannelIds: string[] = []; +const createdRoutineIds: string[] = []; + +afterEach(async () => { + await database + .delete(workItems) + .where(like(workItems.key, `%${testPrefix}%`)); + for (const routineId of createdRoutineIds.splice(0)) { + await database.delete(routines).where(eq(routines.id, routineId)); + } + for (const channelId of createdChannelIds.splice(0)) { + await database + .delete(intelligenceChannelMappings) + .where(eq(intelligenceChannelMappings.channelId, channelId)); + await database.delete(channels).where(eq(channels.id, channelId)); + } + for (const agentId of createdAgentIds.splice(0)) { + await database + .delete(agentProfiles) + .where(eq(agentProfiles.agentId, agentId)); + await database.delete(agents).where(eq(agents.id, agentId)); + } + for (const userId of createdUserIds.splice(0)) { + await database.delete(users).where(eq(users.id, userId)); + } +}); + +afterAll(async () => { + await database.$client.close(); +}); + +async function createUser(): Promise { + const id = `${testPrefix}-user-${randomUUID()}`; + await database.insert(users).values({ + id, + email: `${id}@example.test`, + name: "Channel Work Test User", + }); + createdUserIds.push(id); + return { id, role: "user" }; +} + +async function createAgent(owner: AgentActor, name: string) { + const profile = await profileStore.create(owner, { + name, + title: "Research", + roleDescription: "Find things out.", + visibility: "private", + }); + createdAgentIds.push(profile.id); + return profile.id; +} + +async function setup() { + const owner = await createUser(); + const asker = await createAgent(owner, "Assistant"); + const channel = await store.create(owner, [asker]); + createdChannelIds.push(channel.id); + return { owner, asker, channel }; +} + +/** A hop, as `handoff.ts` offers it and the queue then moves it. */ +async function hop(input: { + threadId: string; + toBotId?: string; + toName?: string; + key?: string; + answerIn?: string; + fromBotId?: string; + attempts?: number; + claimed?: boolean; + finished?: boolean; + createdAt?: Date; +}) { + const key = input.key ?? `hop:${testPrefix}:${randomUUID()}`; + await database.insert(workItems).values({ + kind: HANDOFF_KIND, + key, + payload: { + fromBotId: input.fromBotId ?? "assistant", + toBotId: input.toBotId ?? "ana", + ...(input.toName ? { toName: input.toName } : {}), + actorId: "someone", + threadId: input.threadId, + runId: "run-1", + depth: 1, + task: "find the outage window", + ...(input.answerIn ? { answerIn: input.answerIn } : {}), + }, + attempts: input.attempts ?? 0, + ...(input.claimed + ? { + claimedBy: "replica-a", + leaseUntil: sql`now() + interval '1 minute'`, + } + : {}), + ...(input.finished ? { finishedAt: new Date() } : {}), + ...(input.createdAt ? { createdAt: input.createdAt } : {}), + }); + return key; +} + +async function workFor(channel: { id: string; threadId: string }) { + return (await store.workIn([channel])).get(channel.id) ?? []; +} + +describe("what a conversation is waiting on", () => { + test("a Bot it asked, from the moment the hop is queued", async () => { + const { channel } = await setup(); + const key = await hop({ threadId: channel.threadId, toName: "Ana" }); + + const work = await workFor(channel); + + expect(work).toHaveLength(1); + expect(work[0]).toMatchObject({ + kind: "handoff", + key, + botId: "ana", + botName: "Ana", + }); + expect(work[0]?.since).toBeInstanceOf(Date); + }); + + test("nothing, once the hop is finished", async () => { + const { channel } = await setup(); + await hop({ threadId: channel.threadId, finished: true }); + + expect(await workFor(channel)).toEqual([]); + }); + + test("one entry for a hop however many times it was tried", async () => { + const { channel } = await setup(); + await hop({ threadId: channel.threadId, attempts: 3 }); + + expect(await workFor(channel)).toHaveLength(1); + }); + + test("two Bots asked at once are two entries, oldest first", async () => { + const { channel } = await setup(); + await hop({ + threadId: channel.threadId, + toBotId: "ben", + createdAt: new Date(Date.now() - 1_000), + }); + await hop({ + threadId: channel.threadId, + toBotId: "ana", + createdAt: new Date(Date.now() - 5_000), + }); + + const work = await workFor(channel); + + expect(work.map((entry) => entry.botId)).toEqual(["ana", "ben"]); + }); + + test("not a hop that was given up on, nor the notice saying so", async () => { + const { channel } = await setup(); + const key = await hop({ threadId: channel.threadId, attempts: 5 }); + await hop({ + threadId: channel.threadId, + key: `notice:${key}`, + answerIn: channel.threadId, + }); + + expect(await workFor(channel)).toEqual([]); + }); + + test("a given-up hop's last attempt, while it is still running", async () => { + const { channel } = await setup(); + await hop({ threadId: channel.threadId, attempts: 5, claimed: true }); + + expect(await workFor(channel)).toHaveLength(1); + }); + + test("an answer on its way back is the same wait, with the same clock", async () => { + const { channel } = await setup(); + const askedAt = new Date(Date.now() - 30_000); + const key = await hop({ + threadId: channel.threadId, + toName: "Ana", + finished: true, + createdAt: askedAt, + }); + await hop({ + threadId: channel.threadId, + key: `relay:${key}`, + fromBotId: "ana", + toBotId: "assistant", + answerIn: channel.threadId, + }); + + const work = await workFor(channel); + + expect(work).toHaveLength(1); + expect(work[0]).toMatchObject({ + kind: "handoff", + key, + botId: "ana", + botName: "Ana", + }); + expect(work[0]?.since.getTime()).toBe(askedAt.getTime()); + }); + + test("not once the answer is being given", async () => { + const { channel } = await setup(); + const key = await hop({ threadId: channel.threadId, finished: true }); + await hop({ + threadId: channel.threadId, + key: `relay:${key}`, + answerIn: channel.threadId, + claimed: true, + }); + + expect(await workFor(channel)).toEqual([]); + }); + + test("not a hop asked from a thread no conversation shows", async () => { + const { channel } = await setup(); + await hop({ threadId: `scratch-${testPrefix}` }); + + expect(await workFor(channel)).toEqual([]); + }); + + test("a routine firing into it, until the run has an outcome", async () => { + const { owner, asker, channel } = await setup(); + const routineId = `${testPrefix}-routine`; + await database.insert(routines).values({ + id: routineId, + ownerUserId: owner.id, + agentId: asker, + channelId: channel.id, + instruction: "Post the standup summary.", + cron: "0 9 * * *", + nextRunAt: new Date(Date.now() + 86_400_000), + }); + createdRoutineIds.push(routineId); + const runId = `${testPrefix}-run`; + await database.insert(routineRuns).values({ id: runId, routineId }); + + const running = await workFor(channel); + expect(running).toHaveLength(1); + expect(running[0]).toMatchObject({ + kind: "routine", + key: runId, + routineId, + botId: asker, + }); + + await database + .update(routineRuns) + .set({ status: "succeeded", finishedAt: new Date() }) + .where(eq(routineRuns.id, runId)); + expect(await workFor(channel)).toEqual([]); + }); +}); + +/* + * Every busy event carries the whole list, read at the moment it is sent, so the newest event is + * the answer a reload would get. Heard through the real NOTIFY path, because that is the path that + * has a size limit and a serialiser. + */ +describe("announcing it", () => { + test("a busy signal carries the list, and the settle after the hop clears it", async () => { + const { owner, channel } = await setup(); + const heard: ChannelActivityEvent[] = []; + const hub = createChannelEventHub(); + const detach = hub.register(owner.id, (payload) => { + heard.push(JSON.parse(payload) as ChannelActivityEvent); + }); + const listener = await startChannelActivityListener(databaseUrl, hub); + try { + const key = await hop({ threadId: channel.threadId, toName: "Ana" }); + await store.signalBusy(channel.threadId, true); + await database + .update(workItems) + .set({ finishedAt: new Date() }) + .where(eq(workItems.key, key)); + await store.signalWork(channel.threadId); + + const deadline = Date.now() + 5_000; + while (heard.length < 2 && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 20)); + } + + expect(heard).toHaveLength(2); + expect(heard[0]).toMatchObject({ channelId: channel.id, busy: true }); + expect(heard[0]?.work).toEqual([ + expect.objectContaining({ + kind: "handoff", + key, + botName: "Ana", + since: expect.any(String), + }), + ]); + // A settle says what is left without claiming a turn started or ended. + expect(heard[1]).toMatchObject({ channelId: channel.id, work: [] }); + expect(heard[1]?.busy).toBeUndefined(); + } finally { + detach(); + await listener.stop(); + } + }); +}); From 1608651c3ad52037a11f3375a7074ef13fadcb21 Mon Sep 17 00:00:00 2001 From: Guido Vizoso Date: Mon, 26 Oct 2026 12:26:09 -0300 Subject: [PATCH 02/13] Tell the asking conversation when a hop is queued and when the queue moves The delivery's busy signal fires inside the run, while the row still says the hop is in flight, so it cannot be what says the hop is over. The runner now settles after finish or release, with the relay already queued, and the desk announces a hop the moment it is offered. --- server/src/agents/handoff-runner.ts | 21 +++++++++ server/src/agents/handoff-wiring.ts | 2 + server/src/agents/handoff.ts | 15 +++++- server/src/index.ts | 2 + server/tests/agent-handoff-runner.test.ts | 57 +++++++++++++++++++++++ server/tests/agent-handoff.test.ts | 43 +++++++++++++++++ 6 files changed, 139 insertions(+), 1 deletion(-) diff --git a/server/src/agents/handoff-runner.ts b/server/src/agents/handoff-runner.ts index 2227bdd862..44add93283 100644 --- a/server/src/agents/handoff-runner.ts +++ b/server/src/agents/handoff-runner.ts @@ -128,6 +128,13 @@ export function createHandoffRunner(options: { * which is a matter of one duration outrunning another and does not care about the scale. */ renewEveryMs?: number; + /** + * The queue moved for a hop: finished, released for another try, or dropped. Told after the row + * changed, with the relay already queued, so the conversation that asked can be told what it is + * waiting on now. The delivery's own busy signal fires inside the run, while the row still says + * the hop is in flight, so it cannot be the one that says it is over. Best-effort: never thrown. + */ + onSettled?: (work: HandoffWork) => Promise; }) { const { queue, @@ -139,8 +146,18 @@ export function createHandoffRunner(options: { limit = 5, maxAttempts = DEFAULT_MAX_ATTEMPTS, renewEveryMs = RENEW_EVERY_MS, + onSettled, } = options; + const settled = async (work: HandoffWork) => { + try { + await onSettled?.(work); + } catch { + // A conversation that has not heard is corrected by its next event or its next read; a hop + // reported as failed for it would be run again. + } + }; + /** * Put the failure in front of the person, by running the Bot that asked in the conversation they * are watching. @@ -301,6 +318,7 @@ export function createHandoffRunner(options: { await queue.finish({ kind: HANDOFF_KIND, key: item.key, owner }); ours.delete(item.key); report.skipped.push({ key: item.key, reason: BOT_PAUSED_REASON }); + await settled(work); continue; } @@ -425,6 +443,8 @@ export function createHandoffRunner(options: { ); }); } + // After the relay is queued, so the conversation reads as still waiting on the answer. + await settled(work); await recordAuditEvent(auditStore, { eventType: "agent.handoff_delivered", targetType: "agent", @@ -490,6 +510,7 @@ export function createHandoffRunner(options: { }); ours.delete(item.key); report.skipped.push({ key: item.key, reason }); + await settled(work); await recordAuditEvent(auditStore, { eventType: "agent.handoff_failed", targetType: "agent", diff --git a/server/src/agents/handoff-wiring.ts b/server/src/agents/handoff-wiring.ts index bce0c3e254..b5e3b13d7f 100644 --- a/server/src/agents/handoff-wiring.ts +++ b/server/src/agents/handoff-wiring.ts @@ -60,6 +60,8 @@ export async function startHandoffDelivery(deps: { */ sign: (work, claim) => signHandoffDeliveryRun(work, config.keyEncryptionKey, undefined, claim), + // The asking conversation, told what it is waiting on once the queue has moved for a hop. + onSettled: (work) => channelStore.signalWork(work.threadId), delivery: createHandoffDelivery({ admission: serverTurnAdmission, /* diff --git a/server/src/agents/handoff.ts b/server/src/agents/handoff.ts index ea0a3878fc..babdcdc36b 100644 --- a/server/src/agents/handoff.ts +++ b/server/src/agents/handoff.ts @@ -95,8 +95,15 @@ export function createHandoffDesk(options: { actorFor: (userId: string) => Promise; auditStore: AuditStore; caps: HandoffCaps; + /** + * A hop was queued from this thread. Lets the conversation that asked say what it is waiting on + * before the hop is picked up, which on a busy replica can be minutes. Not awaited: the Bot that + * asked is mid-turn, and a conversation told late is worth less than a tool call kept waiting. + */ + onOffered?: (threadId: string) => Promise; }): HandoffDesk { - const { queue, profiles, mayAddress, actorFor, auditStore, caps } = options; + const { queue, profiles, mayAddress, actorFor, auditStore, caps, onOffered } = + options; /** Said once, so the trail carries the same words the Bot was given. */ async function refuse( @@ -417,6 +424,12 @@ export function createHandoffDesk(options: { }, }); + try { + void onOffered?.(from.threadId).catch(() => {}); + } catch { + // Told on the next event instead. The hop is queued either way. + } + return { ok: true, to: found.id, toName: found.name }; }, }; diff --git a/server/src/index.ts b/server/src/index.ts index 77faeca575..254c6ce953 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1098,6 +1098,8 @@ const handoffDesk = createHandoffDesk({ actorFor(userId).catch(() => null), auditStore: bootAuditStore, caps: config.handoff, + // The asking conversation, told it is now waiting on the Bot it asked. See `onOffered`. + onOffered: (threadId) => channelStore.signalWork(threadId), }); void recordAuditEvent(bootAuditStore, { diff --git a/server/tests/agent-handoff-runner.test.ts b/server/tests/agent-handoff-runner.test.ts index bbbb08788d..b3bda1f248 100644 --- a/server/tests/agent-handoff-runner.test.ts +++ b/server/tests/agent-handoff-runner.test.ts @@ -38,6 +38,7 @@ function runner(options?: { }) => Promise; /** What the delivery says the Bot answered. Null — nothing worth relaying — unless a test cares. */ answer?: string | null; + onSettled?: () => Promise; }) { const calls: Array<{ verb: string; key: string; owner?: string }> = []; const events: string[] = []; @@ -48,6 +49,8 @@ function runner(options?: { const delivered: Array<{ message: string; assertion: string }> = []; const offered: HandoffWork[] = []; const delays: number[] = []; + /** Each time the asking conversation was told the queue moved, with what the queue had seen by then. */ + const settled: Array<{ threadId: string; after: string[] }> = []; const queue = { claim: async () => @@ -101,12 +104,20 @@ function runner(options?: { delivered, offered, delays, + settled, runner: createHandoffRunner({ queue, owner: "replica-a", sign: (work, claim) => signHandoffDeliveryRun(work, KEY, "delivery-run", claim), auditStore, + onSettled: async (work) => { + settled.push({ + threadId: work.threadId, + after: calls.map((call) => call.verb), + }); + await options?.onSettled?.(); + }, delivery: { deliver: async ({ work, message, shown, assertion }) => { delivered.push({ message, assertion }); @@ -654,3 +665,49 @@ describe("what a notice shows", () => { ); }); }); + +/* + * The asking conversation reads what it is waiting on from the queue, so it is told after the queue + * moved and not before. Told from inside the delivery, the row would still say the hop was in + * flight, and "Asking Ana…" would stay on screen for a hop that had finished. + */ +describe("telling the asking conversation", () => { + test("after a delivered hop is finished and its answer is on its way back", async () => { + const { runner: sweep, settled } = runner({ + answer: "Tuesday 02:00 to 04:00.", + }); + + await sweep.sweep(); + + expect(settled).toEqual([ + { threadId: WORK.threadId, after: ["finish", "offer"] }, + ]); + }); + + test("after a failed delivery is released for another try", async () => { + const { runner: sweep, settled } = runner({ + deliver: async () => { + throw new Error("the model stopped"); + }, + }); + + await sweep.sweep(); + + expect(settled).toEqual([{ threadId: WORK.threadId, after: ["release"] }]); + }); + + test("a conversation that cannot be told does not fail the hop", async () => { + const { runner: sweep, calls } = runner({ + onSettled: async () => { + throw new Error("the socket is down"); + }, + }); + + const report = await sweep.sweep(); + + expect(report.delivered).toEqual(["researcher"]); + expect(calls).toEqual([ + { verb: "finish", key: "run-1:abc", owner: "replica-a" }, + ]); + }); +}); diff --git a/server/tests/agent-handoff.test.ts b/server/tests/agent-handoff.test.ts index 97a98e6d95..5d469b315c 100644 --- a/server/tests/agent-handoff.test.ts +++ b/server/tests/agent-handoff.test.ts @@ -47,6 +47,7 @@ function desk(options?: { offered?: number; caps?: HandoffCaps; role?: "admin" | "user"; + onOffered?: (threadId: string) => Promise; }) { const rows: Array<{ kind: string; key: string; payload: unknown }> = []; const events: Array<{ @@ -103,6 +104,7 @@ function desk(options?: { }), auditStore, caps: options?.caps ?? CAPS, + ...(options?.onOffered ? { onOffered: options.onOffered } : {}), }), }; } @@ -137,6 +139,47 @@ describe("handing work to another Bot", () => { }); }); + /* + * The conversation that asked is told at once, so it shows who it is waiting on before the hop is + * picked up. Only for a hop that was queued: a refusal or a repeat starts no wait. + */ + test("a queued hop tells the conversation that asked, and a repeat does not", async () => { + const told: string[] = []; + const { desk: handoff } = desk({ + onOffered: async (threadId) => { + told.push(threadId); + }, + }); + const send = () => + handoff.send({ + from: FROM, + target: "researcher", + envelope: { task: "find the outage window" }, + }); + + await send(); + await send(); + + expect(told).toEqual(["thread-1"]); + }); + + test("a conversation that cannot be told does not refuse the hop", async () => { + const { desk: handoff, rows } = desk({ + onOffered: async () => { + throw new Error("the socket is down"); + }, + }); + + const outcome = await handoff.send({ + from: FROM, + target: "researcher", + envelope: { task: "find the outage window" }, + }); + + expect(outcome).toMatchObject({ ok: true }); + expect(rows).toHaveLength(1); + }); + /* * The key is what stops a retried delivery running the other Bot twice, so the same envelope sent * twice in one run has to land on the same key. A fresh id per attempt is at-least-once with no From a541d6f0f88e4940fbafc2be7d81c4476844ed59 Mon Sep 17 00:00:00 2001 From: Guido Vizoso Date: Mon, 26 Oct 2026 12:26:09 -0300 Subject: [PATCH 03/13] Show a conversation as working while a routine fires into it Signalled around the firing and cleared after the run row has its outcome, since the open run row is what the conversation reads. --- server/src/routines/runner.ts | 131 ++++++++++++++++------------ server/tests/routine-runner.test.ts | 67 ++++++++++++++ server/tests/typecheck-baseline.txt | 1 - 3 files changed, 140 insertions(+), 59 deletions(-) diff --git a/server/src/routines/runner.ts b/server/src/routines/runner.ts index 9c8fd627c5..10333db16f 100644 --- a/server/src/routines/runner.ts +++ b/server/src/routines/runner.ts @@ -140,71 +140,86 @@ export function createRoutineRunner(options: { await routineStore.finishRun(routineRunId, "skipped", BOT_PAUSED_REASON); return; } - let replyText: string; + /* + * The conversation shown as working while the firing runs, and told it is over once the run + * row has its outcome. The run row is what the conversation reads "a routine is running" from + * (see channels/working.ts), so the signal that ends it comes after `finishRun`, not after the + * turn. Best-effort both ways: a firing is not worth failing over a dot. + */ + await channelStore.signalBusy(channel.threadId, true).catch(() => {}); try { - ({ replyText } = await runTurn({ - ownerUserId, - routineId, - agentId, - threadId: channel.threadId, - instruction, - })); - } catch (error) { - if (error instanceof HeadlessToolSuspension) { - await routineStore.finishRun( - routineRunId, - "waiting", - error.message, - error.waiting, - ); - await say(error.message); + await fire(channel.threadId); + } finally { + await channelStore.signalBusy(channel.threadId, false).catch(() => {}); + } + + async function fire(threadId: string): Promise { + let replyText: string; + try { + ({ replyText } = await runTurn({ + ownerUserId, + routineId, + agentId, + threadId, + instruction, + })); + } catch (error) { + if (error instanceof HeadlessToolSuspension) { + await routineStore.finishRun( + routineRunId, + "waiting", + error.message, + error.waiting, + ); + await say(error.message); + return; + } + const reason = reasonOf(error); + await routineStore.finishRun(routineRunId, "failed", reason); + + /* + * THE FATIGUE RULE, read after the failure is recorded — the count has to include this + * firing, or the tenth failure in a row reads as the ninth and the routine keeps going. + * + * Its own try/catch, around the read and the switching-off as well as the message: this all + * happens after `finishRun`, and nothing in it is allowed to throw its way out of a firing + * whose failure is already on the row. + */ + try { + const failures = await routineStore.consecutiveFailures(routineId); + if (failures === 1) { + // Only the first failure after a success. A routine that fails every night at three has + // one line in the channel, not one line a night for a month. + await say(`This routine failed: ${shorten(reason)}`); + } else if (failures >= FATIGUE_LIMIT) { + // A dead integration should not burn model spend for ever. Switched off, and said out + // loud, because a routine that goes quiet without explaining itself is worse than one + // that fails. + await routineStore.setEnabled(ownerUserId, routineId, false); + await say(SWITCHED_OFF); + } + // In between, nothing is said. The run rows carry it, and the routines page reads them. + } catch (fatigueError) { + console.error( + JSON.stringify({ + type: "routine-fatigue-rule-failed", + routineId, + routineRunId, + reason: reasonOf(fatigueError), + }), + ); + } return; } - const reason = reasonOf(error); - await routineStore.finishRun(routineRunId, "failed", reason); /* - * THE FATIGUE RULE, read after the failure is recorded — the count has to include this - * firing, or the tenth failure in a row reads as the ninth and the routine keeps going. - * - * Its own try/catch, around the read and the switching-off as well as the message: this all - * happens after `finishRun`, and nothing in it is allowed to throw its way out of a firing - * whose failure is already on the row. + * The reply, then the outcome. `say` cannot throw, and `finishRun` is finish-once, so neither + * order can lose the run row — this one is the browser's order: what was said lands in the + * channel, and then the firing is closed. */ - try { - const failures = await routineStore.consecutiveFailures(routineId); - if (failures === 1) { - // Only the first failure after a success. A routine that fails every night at three has - // one line in the channel, not one line a night for a month. - await say(`This routine failed: ${shorten(reason)}`); - } else if (failures >= FATIGUE_LIMIT) { - // A dead integration should not burn model spend for ever. Switched off, and said out - // loud, because a routine that goes quiet without explaining itself is worse than one - // that fails. - await routineStore.setEnabled(ownerUserId, routineId, false); - await say(SWITCHED_OFF); - } - // In between, nothing is said. The run rows carry it, and the routines page reads them. - } catch (fatigueError) { - console.error( - JSON.stringify({ - type: "routine-fatigue-rule-failed", - routineId, - routineRunId, - reason: reasonOf(fatigueError), - }), - ); - } - return; + await say(replyText); + await routineStore.finishRun(routineRunId, "succeeded"); } - - /* - * The reply, then the outcome. `say` cannot throw, and `finishRun` is finish-once, so neither - * order can lose the run row — this one is the browser's order: what was said lands in the - * channel, and then the firing is closed. - */ - await say(replyText); - await routineStore.finishRun(routineRunId, "succeeded"); } return { diff --git a/server/tests/routine-runner.test.ts b/server/tests/routine-runner.test.ts index bb12a31408..91cf8bd954 100644 --- a/server/tests/routine-runner.test.ts +++ b/server/tests/routine-runner.test.ts @@ -56,6 +56,8 @@ type Recorded = { }[]; enabled: { ownerUserId: string; id: string; enabled: boolean }[]; turns: Parameters[0][]; + /** Each busy signal, with how many run rows had been closed when it was sent. */ + busy: { threadId: string; busy: boolean; finishedBefore: number }[]; }; function unreachable(method: string): never { @@ -68,12 +70,14 @@ function harness(options: { failures?: number; runTurn?: TurnRunner; recordActivity?: () => Promise; + signalBusy?: () => Promise; }) { const recorded: Recorded = { finished: [], activity: [], enabled: [], turns: [], + busy: [], }; const routineStore: RoutineStore = { @@ -114,6 +118,10 @@ function harness(options: { setPinned: () => unreachable("channels.setPinned"), markRead: () => unreachable("channels.markRead"), softDelete: () => unreachable("channels.softDelete"), + direct: () => unreachable("channels.direct"), + signalChannelBusy: () => unreachable("channels.signalChannelBusy"), + signalWork: () => unreachable("channels.signalWork"), + workIn: () => unreachable("channels.workIn"), async get(actor, channelId) { expect(actor).toEqual(OWNER); @@ -124,6 +132,14 @@ function harness(options: { recorded.activity.push({ actor, channelId, activity }); if (options.recordActivity) await options.recordActivity(); }, + async signalBusy(threadId, busy) { + recorded.busy.push({ + threadId, + busy, + finishedBefore: recorded.finished.length, + }); + if (options.signalBusy) await options.signalBusy(); + }, }; const runTurn: TurnRunner = async (input) => { @@ -257,6 +273,7 @@ describe("createRoutineRunner", () => { activity: [], enabled: [], turns: [], + busy: [], }); }); @@ -338,4 +355,54 @@ describe("createRoutineRunner", () => { "This routine failed: not an Error object", ); }); + /* + * The conversation reads "a routine is running" from the open run row, so the signal that says + * it is over has to come after the row is closed. Sent before, the event would still list the + * routine and the row would stay on screen until something else happened in the conversation. + */ + test("shows the conversation working while it fires, and clears it after the run is closed", async () => { + const { runner, recorded } = harness({}); + + await runner.run(RUN_ID); + + expect(recorded.busy).toEqual([ + { threadId: CHANNEL.threadId, busy: true, finishedBefore: 0 }, + { threadId: CHANNEL.threadId, busy: false, finishedBefore: 1 }, + ]); + }); + + test("clears the working signal when the turn fails", async () => { + const { runner, recorded } = harness({ + runTurn: throwingTurn, + failures: 2, + }); + + await runner.run(RUN_ID); + + expect(recorded.busy.map((signal) => signal.busy)).toEqual([true, false]); + expect(recorded.busy.at(-1)?.finishedBefore).toBe(1); + }); + + test("still runs and records the firing when the working signal cannot be sent", async () => { + const { runner, recorded } = harness({ + signalBusy: async () => { + throw new Error("the socket is down"); + }, + }); + + await runner.run(RUN_ID); + + expect(recorded.turns).toHaveLength(1); + expect(recorded.finished).toEqual([ + { runId: RUN_ID, status: "succeeded", error: undefined }, + ]); + }); + + test("signals nothing for a firing that never runs", async () => { + const { runner, recorded } = harness({ channel: null }); + + await runner.run(RUN_ID); + + expect(recorded.busy).toEqual([]); + }); }); diff --git a/server/tests/typecheck-baseline.txt b/server/tests/typecheck-baseline.txt index ce21d9f4e6..6b8a092993 100644 --- a/server/tests/typecheck-baseline.txt +++ b/server/tests/typecheck-baseline.txt @@ -206,7 +206,6 @@ server/tests/remote-coordination.integration.test.ts: error TS2769: No overload server/tests/responsibility-triggers.test.ts: error TS2353: Object literal may only specify known properties, and 'agentId' does not exist in type 'ResolvedTrigger'. server/tests/routine-routes.test.ts: error TS2739: Type '{ create(): Promise; listFor(ownerUserId: string): Promise; update(): Promise; remove(ownerUserId: string, id: string): Promise<...>; ... 9 more ...; consecutiveFailures(): Promise<...>; }' is missing the following properties from type 'RoutineStore': startManualRun, listRuns, reapAbandonedRuns, markUnschedulable server/tests/routine-run-now.test.ts: error TS2322: Type '() => Promise<({ id: string; status: null; startedAt: Date; finishedAt: null; error: null; } | { id: string; status: "failed"; startedAt: Date; finishedAt: Date; error: string; })[]>' is not assignable to type '(ownerUserId: string, id: string, limit?: number | undefined) => Promise'. -server/tests/routine-runner.test.ts: error TS2739: Type '{ create: () => never; list: () => never; setPinned: () => never; markRead: () => never; softDelete: () => never; get(actor: AgentActor, channelId: string): Promise; recordActivity(actor: AgentActor, channelId: string, activity: ChannelActivity): Promise<...>; }' is missing the following properties from type 'ChannelStore': direct, signalBusy, signalChannelBusy server/tests/routine-runner.test.ts: error TS2740: Type '{ create: () => never; listFor: () => never; update: () => never; remove: () => never; dueRoutines: () => never; advanceNextRun: () => never; insertRun: () => never; runContext(runId: string): Promise; finishRun(runId: string, status: RoutineRunOutcome, error: string | undefined): Promise<....' is missing the following properties from type 'RoutineStore': startManualRun, listRuns, routineForFiring, reapAbandonedRuns, and 3 more. server/tests/routine-runner.test.ts: error TS2741: Property 'lastMessageAt' is missing in type '{ id: string; name: string; agentIds: string[]; threadId: string; active: true; }' but required in type 'AgentChannel'. server/tests/routing-routes.test.ts: error TS2345: Argument of type '(input: { id: string; name: string; roleDescription: string; visibility: AgentVisibility; }) => AgentProfile' is not assignable to parameter of type '(value: { id: string; name: string; roleDescription: string; visibility: string; }, index: number, array: { id: string; name: string; roleDescription: string; visibility: string; }[]) => AgentProfile'. From c2c0543b762c486319c074dff109f8a0ec090e9c Mon Sep 17 00:00:00 2001 From: Guido Vizoso Date: Mon, 26 Oct 2026 12:30:50 -0300 Subject: [PATCH 04/13] Keep what a conversation waits on in the roster and in the open channel's read Busy events now carry the whole work list. It replaces the row's list in the roster and the channel read the conversation screen uses, and both are refetched with the roster after a reconnect or a resync. The sidebar stays lit while a hop or a routine is pending between turns. --- .../components/app-sidebar/app-sidebar.tsx | 3 +- app/src/lib/channels/queries.ts | 46 ++++++++++- app/src/lib/channels/use-channel-events.ts | 79 ++++++++++++++++++- app/tests/channel-event-patch.test.ts | 62 +++++++++++++++ 4 files changed, 181 insertions(+), 9 deletions(-) diff --git a/app/src/components/app-sidebar/app-sidebar.tsx b/app/src/components/app-sidebar/app-sidebar.tsx index 47e336b056..30087fc613 100644 --- a/app/src/components/app-sidebar/app-sidebar.tsx +++ b/app/src/components/app-sidebar/app-sidebar.tsx @@ -210,7 +210,8 @@ function ChannelRow({ } pinned={channel.pinned} unread={unread} - busy={channel.busy ?? false} + // A turn running, or work the conversation waits on between turns: a hop, a routine. + busy={(channel.busy ?? false) || (channel.work?.length ?? 0) > 0} /> ); diff --git a/app/src/lib/channels/queries.ts b/app/src/lib/channels/queries.ts index e8ef1a2f08..b7e2b9ee15 100644 --- a/app/src/lib/channels/queries.ts +++ b/app/src/lib/channels/queries.ts @@ -21,8 +21,44 @@ export type AgentChannel = { * history, and one whose history this deployment cannot reach. See `channel-chat.tsx`. */ lastMessageAt: string | null; + /** + * What this conversation is waiting on right now, oldest first. See `ChannelWork`. + * + * Read with the channel and replaced whole by every busy event, so a reload shows the same thing + * the socket last said. Absent from a server that predates it, which reads as nothing. + */ + work?: ChannelWork[]; }; +/** + * Something a conversation is waiting on that nobody's browser is running: a Bot its Bot asked, + * answering out of sight, or a routine firing into it. + * + * The server reads it from the hop queue and the routine runs, so it holds for as long as the work + * does, across reloads and replicas. `since` is when it started, for the timer a person watches. + */ +export type ChannelWork = + | { + kind: "handoff"; + key: string; + /** The Bot that was asked. */ + botId: string; + /** Its name when it was asked, or null. Prefer the roster's name when the person has one. */ + botName: string | null; + /** ISO-8601. */ + since: string; + } + | { + kind: "routine"; + key: string; + routineId: string; + /** The Bot the routine runs as. */ + botId: string; + botName: string | null; + /** ISO-8601. */ + since: string; + }; + /** A channel plus what the roster renders about it. */ export type ChannelSummary = AgentChannel & { /** A few words about the conversation, or null. The roster falls back to `name`. */ @@ -38,10 +74,10 @@ export type ChannelSummary = AgentChannel & { /** * Whether a turn is running in this channel right now. * - * Socket-only and transient: the server never persists it and the roster query never returns it, - * so it is undefined until a busy event arrives and is dropped whenever the roster is refetched. - * A headless turn — a handoff hop, a relay — sets it, which is how the roster shows work the - * browser never streamed. + * Mostly socket-only and transient: a person's own run is announced and never stored, so a + * refetch drops it. The roster returns it true for a channel with `work`, which is stored, so a + * conversation a Bot is still answering in stays lit across a reload. A headless turn — a handoff + * hop, a relay — sets it, which is how the roster shows work the browser never streamed. */ busy?: boolean; }; @@ -49,6 +85,8 @@ export type ChannelSummary = AgentChannel & { export const channelKeys = { all: ["channels"] as const, list: () => ["channels", "list"] as const, + /** Every channel read by id, to refetch them together. */ + details: () => ["channels", "detail"] as const, detail: (channelId: string) => ["channels", "detail", channelId] as const, }; diff --git a/app/src/lib/channels/use-channel-events.ts b/app/src/lib/channels/use-channel-events.ts index 3c8a9dd332..8b3a07819a 100644 --- a/app/src/lib/channels/use-channel-events.ts +++ b/app/src/lib/channels/use-channel-events.ts @@ -1,7 +1,13 @@ import { useQueryClient } from "@tanstack/react-query"; import { useRouter } from "@tanstack/react-router"; import { useEffect } from "react"; -import { type ChannelPage, type ChannelSummary, channelKeys } from "./queries"; +import { + type AgentChannel, + type ChannelPage, + type ChannelSummary, + type ChannelWork, + channelKeys, +} from "./queries"; import { socketUrl as buildSocketUrl } from "@/lib/socket-url"; /** @@ -90,6 +96,12 @@ export type ChannelActivityEvent = { * indicator without disturbing the preview or the order. */ busy?: boolean; + /** + * Everything the channel is waiting on now, replacing what the row held. Absent on an ordinary + * activity event. Rides on every busy event, and alone on the one the server sends when a hop + * settles without a turn starting or ending. + */ + work?: ChannelWork[]; }; /** The infinite query's cache, which holds pages rather than one array. */ @@ -171,10 +183,20 @@ export function applyChannelEvent( * is also not activity — a channel does not jump to the top of the roster because a turn started * in it — so the order is left exactly as it was. */ - if (activity.busy !== undefined) { - if ((previous.busy ?? false) === activity.busy) return data; + if (activity.busy !== undefined || activity.work !== undefined) { + const busy = activity.busy ?? previous.busy; + const work = activity.work ?? previous.work; + if ( + (previous.busy ?? false) === (busy ?? false) && + sameWork(previous.work, work) + ) + return data; const channels = page.channels.slice(); - channels[index] = { ...previous, busy: activity.busy }; + channels[index] = { + ...previous, + ...(busy === undefined ? {} : { busy }), + ...(work === undefined ? {} : { work }), + }; const pages = data.pages.slice(); pages[holdingPage] = { ...page, channels }; return { ...data, pages }; @@ -194,6 +216,43 @@ export function applyChannelEvent( return { ...data, pages }; } +/** + * The same list, entry for entry. Compared rather than replaced so an event that changes nothing + * leaves the cache alone and the transcript does not re-render to draw the same rows. + */ +function sameWork( + left: readonly ChannelWork[] | undefined, + right: readonly ChannelWork[] | undefined, +): boolean { + const a = left ?? []; + const b = right ?? []; + return ( + a.length === b.length && + a.every( + (entry, at) => + entry.key === b[at]?.key && + entry.since === b[at]?.since && + entry.botName === b[at]?.botName, + ) + ); +} + +/** + * Apply one event's work list to the open conversation's own read. + * + * The conversation screen reads the channel by id rather than from the roster: the roster is paged, + * and the conversation somebody has open need not be on a page they have loaded. So the list is + * patched there too, and only there when the event carries one. + */ +export function applyChannelWork( + channel: AgentChannel | undefined, + activity: ChannelActivityEvent, +): AgentChannel | undefined { + if (!channel || activity.work === undefined) return channel; + if (sameWork(channel.work, activity.work)) return channel; + return { ...channel, work: activity.work }; +} + const FIRST_RETRY_MS = 500; const MAX_RETRY_MS = 30_000; @@ -219,6 +278,7 @@ export function useChannelEvents() { retryDelay = FIRST_RETRY_MS; // Recover events missed while the socket was disconnected. void queryClient.invalidateQueries({ queryKey: channelKeys.list() }); + void queryClient.invalidateQueries({ queryKey: channelKeys.details() }); }; socket.onmessage = (message) => { @@ -229,6 +289,9 @@ export function useChannelEvents() { // `channelId`, because this message has none. if (isResync(parsed)) { void queryClient.invalidateQueries({ queryKey: channelKeys.list() }); + void queryClient.invalidateQueries({ + queryKey: channelKeys.details(), + }); return; } @@ -260,6 +323,14 @@ export function useChannelEvents() { }, ); + if (activity.work !== undefined) { + queryClient.setQueryData( + channelKeys.detail(activity.channelId), + (channel: AgentChannel | undefined) => + applyChannelWork(channel, activity), + ); + } + /* * A tab looking at the channel somebody just deleted in another tab. * diff --git a/app/tests/channel-event-patch.test.ts b/app/tests/channel-event-patch.test.ts index cc850820d7..93c7ae2bc4 100644 --- a/app/tests/channel-event-patch.test.ts +++ b/app/tests/channel-event-patch.test.ts @@ -225,6 +225,68 @@ describe("a busy signal", () => { }); }); +/** + * What the channel is waiting on, carried whole on every busy event and alone on a settle. + * + * Replaced rather than merged, so the newest event is the answer and a retried hop never shows + * twice. The sidebar lights from it as well as from `busy`, so a hop still running after a turn + * ended keeps the row lit. + */ +describe("a work list", () => { + const work = [ + { + kind: "handoff" as const, + key: "hop-1", + botId: "ana", + botName: "Ana", + since: "2026-10-26T10:00:00.000Z", + }, + ]; + + test("rides on a busy event and patches only the busy flag and the list", () => { + const data = cache([ + channel("a", { lastMessage: "Said something." }), + channel("b"), + ]); + + const patched = applyChannelEvent( + data, + event({ channelId: "a", busy: true, work }), + ); + + expect(patched).not.toBe("unknown"); + if (patched === "unknown") return; + expect(patched.pages[0]?.channels[0]).toEqual({ + ...(data.pages[0]?.channels[0] as ChannelSummary), + busy: true, + work, + }); + }); + + test("a settle replaces the list and leaves the busy flag as it was", () => { + const patched = applyChannelEvent( + cache([channel("a", { busy: true, work })]), + event({ channelId: "a", work: [] }), + ); + + expect(patched).not.toBe("unknown"); + if (patched === "unknown") return; + expect(patched.pages[0]?.channels[0]?.busy).toBe(true); + expect(patched.pages[0]?.channels[0]?.work).toEqual([]); + }); + + test("the same list again changes nothing", () => { + const data = cache([channel("a", { busy: true, work })]); + + expect( + applyChannelEvent( + data, + event({ channelId: "a", busy: true, work: [...work] }), + ), + ).toBe(data); + }); +}); + /** * A conversation the server has just named. * From 9a0f6c246e5c8fef1c0185b80ef4c3e5fb3ae00f Mon Sep 17 00:00:00 2001 From: Guido Vizoso Date: Mon, 26 Oct 2026 12:30:50 -0300 Subject: [PATCH 05/13] Show who a conversation is waiting on, with a running clock, where the answer will land MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Asking Ana… 0:12" for a Bot its Bot asked, "Ana is running a routine…" for a routine, at the end of the transcript, from the server's list. Hidden while this tab's own turn runs. ProgressRow is the reusable line for later waits. --- app/src/components/channels/channel-chat.tsx | 18 ++ app/src/components/channels/channel-work.tsx | 42 +++ .../components/channels/chat-transcript.tsx | 8 + .../components/channels/conversation-view.tsx | 4 + app/src/components/channels/progress-row.tsx | 60 +++++ app/tests/channel-work-row.test.tsx | 240 ++++++++++++++++++ 6 files changed, 372 insertions(+) create mode 100644 app/src/components/channels/channel-work.tsx create mode 100644 app/src/components/channels/progress-row.tsx create mode 100644 app/tests/channel-work-row.test.tsx diff --git a/app/src/components/channels/channel-chat.tsx b/app/src/components/channels/channel-chat.tsx index 06bb23cdb9..74d67cd8d7 100644 --- a/app/src/components/channels/channel-chat.tsx +++ b/app/src/components/channels/channel-chat.tsx @@ -12,6 +12,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { HandoffResumeNotice } from "@/components/computer/handoff-resume-notice"; import { attachmentModality } from "@/components/channels/chat-messages"; import { toAgentOptions } from "@/components/channels/composer"; +import { ChannelWorkRows } from "@/components/channels/channel-work"; import { ConversationView } from "@/components/channels/conversation-view"; import { VoiceCallWidget } from "@/components/channels/voice-call-widget"; import { @@ -1081,6 +1082,23 @@ export function ChannelChat({ * run before closing it; see server/src/channels/stall-guard.ts. */ stopped={runError ?? undefined} + /* + * What the server says this conversation waits on: a Bot its Bot asked, a routine. + * Hidden while this tab's own turn is going, which already shows itself and would + * otherwise draw "Asking Ana…" under the sentence saying it had asked. The composer + * stays open either way: a person may keep talking while a hop runs, and a relay that + * finds them mid-turn waits and is tried again. + */ + working={ + agent.isRunning || turnsInFlight > 0 ? null : ( + + agentProfiles?.find((profile) => profile.id === botId)?.name + } + work={channel.work} + /> + ) + } /> diff --git a/app/src/components/channels/channel-work.tsx b/app/src/components/channels/channel-work.tsx new file mode 100644 index 0000000000..43c32464fd --- /dev/null +++ b/app/src/components/channels/channel-work.tsx @@ -0,0 +1,42 @@ +import type { ChannelWork } from "@/lib/channels/queries"; +import { ProgressRow } from "./progress-row"; + +/** + * What a conversation is waiting on, one row each, at the end of the transcript. + * + * Driven by the server's list rather than this tab's run, because the work is not this tab's: a Bot + * the conversation's Bot asked answers out of sight, and a routine fires with nobody's browser + * open. Without these the conversation looks finished while something is still working on it. + * + * Gone when the list empties, which is when the answer is being written into the conversation or + * the work is over. The caller hides it while its own run is going, so a turn that has just asked + * another Bot does not show "Asking Ana…" under its own half-written reply. + */ +export function ChannelWorkRows({ + work, + nameOf, +}: { + work: readonly ChannelWork[] | undefined; + /** The name this person knows a Bot by, when they know it at all. */ + nameOf?: (botId: string) => string | undefined; +}) { + if (!work || work.length === 0) return null; + return ( +
+ {work.map((entry) => ( + + ))} +
+ ); +} + +function labelFor(entry: ChannelWork, name: string | null): string { + if (entry.kind === "handoff") { + return name ? `Asking ${name}…` : "Asking another Bot…"; + } + return name ? `${name} is running a routine…` : "A routine is running…"; +} diff --git a/app/src/components/channels/chat-transcript.tsx b/app/src/components/channels/chat-transcript.tsx index b439b5c5d7..d8466cce92 100644 --- a/app/src/components/channels/chat-transcript.tsx +++ b/app/src/components/channels/chat-transcript.tsx @@ -106,6 +106,12 @@ type ChatTranscriptProps = { * be told, and only the thing that ended the turn knows which one happened. */ stopped?: string; + /** + * Work this conversation is waiting on that is not this tab's run: a Bot it asked, a routine. + * Drawn in the same slot as `Thinking`, under the last thing said, because that is where the + * answer will appear. + */ + working?: React.ReactNode; }; /** One shared empty array, so a screen without a queue does not hand down a new one per render. */ @@ -1549,6 +1555,7 @@ export function ChatTranscript({ queued = EMPTY_QUEUE, restoring = false, stopped, + working, }: ChatTranscriptProps) { const isDrawn = useDrawnTool(); /* @@ -1692,6 +1699,7 @@ export function ChatTranscript({ ) : waitingOnFirstToken ? ( ) : null} + {working} {queued.map((message) => (
diff --git a/app/src/components/channels/progress-row.tsx b/app/src/components/channels/progress-row.tsx new file mode 100644 index 0000000000..3160ebfac5 --- /dev/null +++ b/app/src/components/channels/progress-row.tsx @@ -0,0 +1,60 @@ +import { useEffect, useState } from "react"; + +/** + * One line saying something is under way, and for how long. + * + * For a wait the person did not start and cannot watch: a Bot asked by their Bot, a routine firing, + * a sign-in or an approval somebody else has to finish. Drawn where the result will appear, so the + * person looks at the right spot, and with a running clock, because "still working" and "stuck" + * look the same without one. + * + * It borrows the shimmer a running tool line uses, so every kind of "working on it" in the + * transcript reads the same. + */ +export function ProgressRow({ + label, + since, +}: { + /** What is under way, as a person would say it: "Asking Ana…". */ + label: string; + /** When it started. ISO-8601, or epoch milliseconds. */ + since: string | number; +}) { + const elapsed = useElapsed(since); + return ( +

+ {label} + +

+ ); +} + +/** "0:12", "4:05", "1:02:07". Never negative: a server clock a little ahead reads as zero. */ +export function formatElapsed(milliseconds: number): string { + const total = Math.max(0, Math.floor(milliseconds / 1000)); + const hours = Math.floor(total / 3600); + const minutes = Math.floor((total % 3600) / 60); + const seconds = String(total % 60).padStart(2, "0"); + return hours > 0 + ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}` + : `${minutes}:${seconds}`; +} + +/** The time since `since`, as text, moving once a second while it is on screen. */ +function useElapsed(since: string | number): string { + const start = typeof since === "number" ? since : Date.parse(since); + const [now, setNow] = useState(() => Date.now()); + useEffect(() => { + const timer = window.setInterval(() => setNow(Date.now()), 1000); + return () => window.clearInterval(timer); + }, []); + return formatElapsed(Number.isNaN(start) ? 0 : now - start); +} diff --git a/app/tests/channel-work-row.test.tsx b/app/tests/channel-work-row.test.tsx new file mode 100644 index 0000000000..9ef09032e2 --- /dev/null +++ b/app/tests/channel-work-row.test.tsx @@ -0,0 +1,240 @@ +import { + afterAll, + afterEach, + beforeAll, + expect, + jest, + setSystemTime, + test, +} from "bun:test"; +import { GlobalRegistrator } from "@happy-dom/global-registrator"; +import { + QueryClient, + QueryClientProvider, + useQuery, +} from "@tanstack/react-query"; +import { act, cleanup, render, waitFor } from "@testing-library/react"; +import { ChannelWorkRows } from "@/components/channels/channel-work"; +import { formatElapsed } from "@/components/channels/progress-row"; +import { + type AgentChannel, + type ChannelWork, + channelKeys, + channelQueryOptions, +} from "@/lib/channels/queries"; +import { applyChannelWork } from "@/lib/channels/use-channel-events"; +import { settleReactWork } from "./settle-react-work"; + +/** + * The row a conversation shows while it waits on work nobody's browser is running: "Asking Ana… + * 0:12" for a Bot its Bot asked, and the same for a routine. Driven by the server's list, so it + * has to appear from a fetched channel as well as from a socket event, tick while it is up, and go + * when the list empties. + */ + +beforeAll(() => GlobalRegistrator.register()); +afterEach(() => { + cleanup(); + jest.useRealTimers(); + setSystemTime(); +}); +afterAll(async () => { + await settleReactWork(); + GlobalRegistrator.unregister(); +}); + +const originalFetch = global.fetch; +afterEach(() => { + global.fetch = originalFetch; +}); + +const NOW = new Date("2026-10-26T10:00:12.000Z"); + +const asking = (overrides: Partial = {}): ChannelWork => + ({ + kind: "handoff", + key: "hop-1", + botId: "ana", + botName: "Ana", + since: "2026-10-26T10:00:00.000Z", + ...overrides, + }) as ChannelWork; + +test("says who was asked and for how long", () => { + setSystemTime(NOW); + + const view = render(); + + const row = view.getByTestId("progress-row"); + expect(row.textContent).toBe("Asking Ana…0:12"); + // Announced as progress, and only by its words: the clock is hidden from a screen reader. + expect(row.getAttribute("role")).toBe("status"); + expect(row.querySelector("[aria-hidden='true']")?.textContent).toBe("0:12"); +}); + +test("the clock moves while the row is up", () => { + jest.useFakeTimers(); + setSystemTime(NOW); + const view = render(); + + // The fake clock moves `Date.now()` with the timers, as a real second would. + act(() => { + jest.advanceTimersByTime(3_000); + }); + + expect(view.getByTestId("progress-row").textContent).toBe("Asking Ana…0:15"); +}); + +test("prefers the name the person knows the Bot by", () => { + setSystemTime(NOW); + + const view = render( + (botId === "ana" ? "Ana Research" : undefined)} + work={[asking()]} + />, + ); + + expect(view.getByTestId("progress-row").textContent).toContain( + "Asking Ana Research…", + ); +}); + +test("one row for each Bot asked, and one for a routine", () => { + setSystemTime(NOW); + + const view = render( + , + ); + + expect( + view.getAllByTestId("progress-row").map((row) => row.textContent), + ).toEqual([ + "Asking Ana…0:12", + "Asking another Bot…0:12", + "Assistant is running a routine…1:12", + ]); +}); + +test("goes when the list empties", () => { + setSystemTime(NOW); + const view = render(); + + view.rerender(); + + expect(view.queryByTestId("progress-row")).toBeNull(); + expect(view.queryByTestId("channel-work")).toBeNull(); +}); + +/* + * A reload is a fetch of the channel, not a socket event. The list has to come back with it, or a + * conversation a Bot is still answering in looks finished to anybody who refreshed. + */ +test("comes back from the channel read after a reload", async () => { + setSystemTime(NOW); + global.fetch = (async (input: RequestInfo | URL) => { + if (String(input) === "/api/channels/channel-1") { + return Response.json({ + channel: { + id: "channel-1", + name: "Assistant", + agentIds: ["assistant"], + threadId: "thread-1", + active: true, + lastMessageAt: null, + work: [asking()], + }, + }); + } + return Response.json({ error: "unexpected" }, { status: 500 }); + }) as typeof fetch; + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + function Conversation() { + const channel = useQuery(channelQueryOptions("channel-1")); + return ; + } + + const view = render( + + + , + ); + + await waitFor(() => { + expect(view.getByTestId("progress-row").textContent).toBe( + "Asking Ana…0:12", + ); + }); + + // The socket then says the answer is being given, and the row goes. + act(() => { + queryClient.setQueryData( + channelKeys.detail("channel-1"), + (channel: AgentChannel | undefined) => + applyChannelWork(channel, { + channelId: "channel-1", + lastMessage: null, + lastMessageAt: null, + lastMessageAgentId: null, + work: [], + }), + ); + }); + await waitFor(() => { + expect(view.queryByTestId("progress-row")).toBeNull(); + }); +}); + +test("an event with no list leaves the conversation's list alone", () => { + const channel: AgentChannel = { + id: "channel-1", + name: "Assistant", + agentIds: [], + threadId: "thread-1", + active: true, + lastMessageAt: null, + work: [asking()], + }; + + expect( + applyChannelWork(channel, { + channelId: "channel-1", + lastMessage: null, + lastMessageAt: null, + lastMessageAgentId: null, + busy: true, + }), + ).toBe(channel); + // And the same list again changes nothing either, so nothing re-renders. + expect( + applyChannelWork(channel, { + channelId: "channel-1", + lastMessage: null, + lastMessageAt: null, + lastMessageAgentId: null, + work: [asking()], + }), + ).toBe(channel); +}); + +test("the clock reads minutes, then hours, and never runs backwards", () => { + expect(formatElapsed(12_000)).toBe("0:12"); + expect(formatElapsed(245_000)).toBe("4:05"); + expect(formatElapsed(3_727_000)).toBe("1:02:07"); + expect(formatElapsed(-4_000)).toBe("0:00"); +}); From 2982a030ee138e48d29a166b4afd909fbbdb0a54 Mon Sep 17 00:00:00 2001 From: Guido Vizoso Date: Mon, 26 Oct 2026 13:03:27 -0300 Subject: [PATCH 06/13] Read a conversation's open hops through an index instead of scanning every hop The work list is read on every roster page and every busy event. It filtered work_items on payload->>'threadId' with no index, and joined each relay to its hop by building 'relay:' || key for every row, so both sides scanned a day of finished hops. A partial index on unfinished rows by thread, and a join that cuts the hop key out of the relay key, turn it into two index lookups. --- .../drizzle/0063_work_items_open_thread.sql | 1 + server/drizzle/meta/0063_snapshot.json | 10205 ++++++++++++++++ server/drizzle/meta/_journal.json | 7 + server/src/channels/working.ts | 15 +- server/src/db/schema/work.ts | 9 + 5 files changed, 10235 insertions(+), 2 deletions(-) create mode 100644 server/drizzle/0063_work_items_open_thread.sql create mode 100644 server/drizzle/meta/0063_snapshot.json diff --git a/server/drizzle/0063_work_items_open_thread.sql b/server/drizzle/0063_work_items_open_thread.sql new file mode 100644 index 0000000000..82f3b30a95 --- /dev/null +++ b/server/drizzle/0063_work_items_open_thread.sql @@ -0,0 +1 @@ +CREATE INDEX "work_items_open_thread_idx" ON "work_items" USING btree ("kind",("payload"->>'threadId')) WHERE "work_items"."finished_at" IS NULL; \ No newline at end of file diff --git a/server/drizzle/meta/0063_snapshot.json b/server/drizzle/meta/0063_snapshot.json new file mode 100644 index 0000000000..9d9918d32b --- /dev/null +++ b/server/drizzle/meta/0063_snapshot.json @@ -0,0 +1,10205 @@ +{ + "id": "6fe50b30-070c-422e-9cd4-fbb9a492da1f", + "prevId": "20465172-4c5a-4639-919a-d1f8d832e6ed", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_provider_account_idx": { + "name": "accounts_provider_account_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "agent_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "configuration": { + "name": "configuration", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "package_id": { + "name": "package_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "override": { + "name": "override", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "agents_package_id_deployment_packages_id_fk": { + "name": "agents_package_id_deployment_packages_id_fk", + "tableFrom": "agents", + "tableTo": "deployment_packages", + "columnsFrom": [ + "package_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.attachments": { + "name": "attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bytes": { + "name": "bytes", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attached_at": { + "name": "attached_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "upload_group": { + "name": "upload_group", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "attachments_channel_idx": { + "name": "attachments_channel_idx", + "columns": [ + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "attachments_uploaded_by_idx": { + "name": "attachments_uploaded_by_idx", + "columns": [ + { + "expression": "uploaded_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "attachments_upload_group_idx": { + "name": "attachments_upload_group_idx", + "columns": [ + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uploaded_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "upload_group", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"attachments\".\"attached_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "attachments_staged_idx": { + "name": "attachments_staged_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"attachments\".\"attached_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "attachments_channel_id_channels_id_fk": { + "name": "attachments_channel_id_channels_id_fk", + "tableFrom": "attachments", + "tableTo": "channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "attachments_uploaded_by_users_id_fk": { + "name": "attachments_uploaded_by_users_id_fk", + "tableFrom": "attachments", + "tableTo": "users", + "columnsFrom": [ + "uploaded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_events": { + "name": "audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "initiator_kind": { + "name": "initiator_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'person'" + }, + "initiator_id": { + "name": "initiator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_events_created_at_idx": { + "name": "audit_events_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_events_type_time_idx": { + "name": "audit_events_type_time_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_events_actor_time_idx": { + "name": "audit_events_actor_time_idx", + "columns": [ + { + "expression": "actor_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_events_target_time_idx": { + "name": "audit_events_target_time_idx", + "columns": [ + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_events_initiator_time_idx": { + "name": "audit_events_initiator_time_idx", + "columns": [ + { + "expression": "initiator_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.channel_agents": { + "name": "channel_agents", + "schema": "", + "columns": { + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "channel_agents_channel_id_channels_id_fk": { + "name": "channel_agents_channel_id_channels_id_fk", + "tableFrom": "channel_agents", + "tableTo": "channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "channel_agents_agent_id_agents_id_fk": { + "name": "channel_agents_agent_id_agents_id_fk", + "tableFrom": "channel_agents", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "channel_agents_channel_id_agent_id_pk": { + "name": "channel_agents_channel_id_agent_id_pk", + "columns": [ + "channel_id", + "agent_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.channel_memberships": { + "name": "channel_memberships", + "schema": "", + "columns": { + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "channel_memberships_channel_id_channels_id_fk": { + "name": "channel_memberships_channel_id_channels_id_fk", + "tableFrom": "channel_memberships", + "tableTo": "channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "channel_memberships_user_id_users_id_fk": { + "name": "channel_memberships_user_id_users_id_fk", + "tableFrom": "channel_memberships", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "channel_memberships_channel_id_user_id_pk": { + "name": "channel_memberships_channel_id_user_id_pk", + "columns": [ + "channel_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.channels": { + "name": "channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suggested_prompts": { + "name": "suggested_prompts", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "allowed_groups": { + "name": "allowed_groups", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "package_id": { + "name": "package_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "override": { + "name": "override", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_at": { + "name": "summary_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_message": { + "name": "last_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_message_source_id": { + "name": "last_message_source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_message_agent_id": { + "name": "last_message_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "channels_recent_activity_idx": { + "name": "channels_recent_activity_idx", + "columns": [ + { + "expression": "COALESCE(\"last_message_at\", \"created_at\") DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "channels_awaiting_summary_idx": { + "name": "channels_awaiting_summary_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"channels\".\"summary\" is null and \"channels\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "channels_package_id_deployment_packages_id_fk": { + "name": "channels_package_id_deployment_packages_id_fk", + "tableFrom": "channels", + "tableTo": "deployment_packages", + "columnsFrom": [ + "package_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "channels_last_message_agent_id_agents_id_fk": { + "name": "channels_last_message_agent_id_agents_id_fk", + "tableFrom": "channels", + "tableTo": "agents", + "columnsFrom": [ + "last_message_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentials": { + "name": "credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kind": { + "name": "kind", + "type": "credential_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credentials_active_key_idx": { + "name": "credentials_active_key_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credentials\".\"revoked_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_packages": { + "name": "deployment_packages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "loaded_at": { + "name": "loaded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "deployment_packages_tenant_id_unique": { + "name": "deployment_packages_tenant_id_unique", + "nullsNotDistinct": false, + "columns": [ + "tenant_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.intelligence_channel_mappings": { + "name": "intelligence_channel_mappings", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "intelligence_channel_mappings_thread_idx": { + "name": "intelligence_channel_mappings_thread_idx", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "intelligence_channel_mappings_user_id_users_id_fk": { + "name": "intelligence_channel_mappings_user_id_users_id_fk", + "tableFrom": "intelligence_channel_mappings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "intelligence_channel_mappings_channel_id_channels_id_fk": { + "name": "intelligence_channel_mappings_channel_id_channels_id_fk", + "tableFrom": "intelligence_channel_mappings", + "tableTo": "channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "intelligence_channel_mappings_user_id_channel_id_pk": { + "name": "intelligence_channel_mappings_user_id_channel_id_pk", + "columns": [ + "user_id", + "channel_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.revoked_access": { + "name": "revoked_access", + "schema": "", + "columns": { + "email": { + "name": "email", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sso_providers_user_id_users_id_fk": { + "name": "sso_providers_user_id_users_id_fk", + "tableFrom": "sso_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sso_providers_provider_id_unique": { + "name": "sso_providers_provider_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_instructions": { + "name": "user_instructions", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_instructions_user_id_users_id_fk": { + "name": "user_instructions_user_id_users_id_fk", + "tableFrom": "user_instructions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_user_id_role_pk": { + "name": "user_roles_user_id_role_pk", + "columns": [ + "user_id", + "role" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "preferences": { + "name": "preferences", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "groups": { + "name": "groups", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "onboarding_step": { + "name": "onboarding_step", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "onboarding_completed_at": { + "name": "onboarding_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_signed_in_at": { + "name": "last_signed_in_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.action_policy": { + "name": "action_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deny": { + "name": "deny", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "allow": { + "name": "allow", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "labels": { + "name": "labels", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.computer_page_frame": { + "name": "computer_page_frame", + "schema": "", + "columns": { + "computer_id": { + "name": "computer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frame": { + "name": "frame", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "captured_at": { + "name": "captured_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "computer_page_frame_captured_idx": { + "name": "computer_page_frame_captured_idx", + "columns": [ + { + "expression": "captured_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "computer_page_frame_computer_id_tool_call_id_pk": { + "name": "computer_page_frame_computer_id_tool_call_id_pk", + "columns": [ + "computer_id", + "tool_call_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.computer_snapshot": { + "name": "computer_snapshot", + "schema": "", + "columns": { + "computer_id": { + "name": "computer_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "elements": { + "name": "elements", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "taken_at": { + "name": "taken_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "session": { + "name": "session", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_preferences": { + "name": "agent_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hidden_at": { + "name": "hidden_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_preferences_user_id_users_id_fk": { + "name": "agent_preferences_user_id_users_id_fk", + "tableFrom": "agent_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_preferences_agent_id_agents_id_fk": { + "name": "agent_preferences_agent_id_agents_id_fk", + "tableFrom": "agent_preferences", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_preferences_user_id_agent_id_pk": { + "name": "agent_preferences_user_id_agent_id_pk", + "columns": [ + "user_id", + "agent_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_profiles": { + "name": "agent_profiles", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_description": { + "name": "role_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_seed": { + "name": "avatar_seed", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "agent_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "callback_token_hash": { + "name": "callback_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "callback_token_issued_at": { + "name": "callback_token_issued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_profiles_visibility_deleted_idx": { + "name": "agent_profiles_visibility_deleted_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_profiles_agent_id_agents_id_fk": { + "name": "agent_profiles_agent_id_agents_id_fk", + "tableFrom": "agent_profiles", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_profiles_owner_user_id_users_id_fk": { + "name": "agent_profiles_owner_user_id_users_id_fk", + "tableFrom": "agent_profiles", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_runs": { + "name": "routine_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "routine_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "waiting": { + "name": "waiting", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'schedule'" + } + }, + "indexes": { + "routine_runs_by_routine_idx": { + "name": "routine_runs_by_routine_idx", + "columns": [ + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_runs_routine_id_routines_id_fk": { + "name": "routine_runs_routine_id_routines_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_sweeps": { + "name": "routine_sweeps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "swept_at": { + "name": "swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routines": { + "name": "routines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instruction": { + "name": "instruction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "enabled_at": { + "name": "enabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routines_due_idx": { + "name": "routines_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_by_owner_idx": { + "name": "routines_by_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routines_owner_user_id_users_id_fk": { + "name": "routines_owner_user_id_users_id_fk", + "tableFrom": "routines", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routines_agent_id_agents_id_fk": { + "name": "routines_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.component_exclusions": { + "name": "component_exclusions", + "schema": "", + "columns": { + "component_name": { + "name": "component_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "withheld_by": { + "name": "withheld_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "component_exclusions_component_name_components_name_fk": { + "name": "component_exclusions_component_name_components_name_fk", + "tableFrom": "component_exclusions", + "tableTo": "components", + "columnsFrom": [ + "component_name" + ], + "columnsTo": [ + "name" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "component_exclusions_agent_id_agents_id_fk": { + "name": "component_exclusions_agent_id_agents_id_fk", + "tableFrom": "component_exclusions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "component_exclusions_component_name_agent_id_pk": { + "name": "component_exclusions_component_name_agent_id_pk", + "columns": [ + "component_name", + "agent_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.component_functions": { + "name": "component_functions", + "schema": "", + "columns": { + "component_name": { + "name": "component_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "function_name": { + "name": "function_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "component_functions_component_name_components_name_fk": { + "name": "component_functions_component_name_components_name_fk", + "tableFrom": "component_functions", + "tableTo": "components", + "columnsFrom": [ + "component_name" + ], + "columnsTo": [ + "name" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "component_functions_component_name_function_name_pk": { + "name": "component_functions_component_name_function_name_pk", + "columns": [ + "component_name", + "function_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.components": { + "name": "components", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "draft_description": { + "name": "draft_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "published_description": { + "name": "published_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_access_requests": { + "name": "app_access_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "app_key": { + "name": "app_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_title": { + "name": "app_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "app_access_requests_pending_idx": { + "name": "app_access_requests_pending_idx", + "columns": [ + { + "expression": "requested_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "app_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"agent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"app_access_requests\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "app_access_requests_status_idx": { + "name": "app_access_requests_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "app_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "app_access_requests_requested_by_users_id_fk": { + "name": "app_access_requests_requested_by_users_id_fk", + "tableFrom": "app_access_requests", + "tableTo": "users", + "columnsFrom": [ + "requested_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "app_access_requests_agent_id_agents_id_fk": { + "name": "app_access_requests_agent_id_agents_id_fk", + "tableFrom": "app_access_requests", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "app_access_requests_kind_check": { + "name": "app_access_requests_kind_check", + "value": "\"app_access_requests\".\"kind\" IN ('enable', 'grant', 'connect_shared')" + }, + "app_access_requests_status_check": { + "name": "app_access_requests_status_check", + "value": "\"app_access_requests\".\"status\" IN ('pending', 'approved', 'dismissed', 'cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.brokered_connections": { + "name": "brokered_connections", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app": { + "name": "app", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "holder": { + "name": "holder", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vendor_user_id": { + "name": "vendor_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connected_by": { + "name": "connected_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "probe_action": { + "name": "probe_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "brokered_connections_person_idx": { + "name": "brokered_connections_person_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "app", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"brokered_connections\".\"holder\" = 'person'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "brokered_connections_deployment_idx": { + "name": "brokered_connections_deployment_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "app", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"brokered_connections\".\"holder\" = 'deployment'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "brokered_connections_user_idx": { + "name": "brokered_connections_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "brokered_connections_holder_check": { + "name": "brokered_connections_holder_check", + "value": "(\"brokered_connections\".\"holder\" = 'person' AND \"brokered_connections\".\"user_id\" IS NOT NULL) OR (\"brokered_connections\".\"holder\" = 'deployment' AND \"brokered_connections\".\"user_id\" IS NULL)" + }, + "brokered_connections_provider_check": { + "name": "brokered_connections_provider_check", + "value": "\"brokered_connections\".\"provider\" IN ('composio', 'arcade')" + } + }, + "isRLSEnabled": false + }, + "public.composio_connections": { + "name": "composio_connections", + "schema": "", + "columns": { + "toolkit": { + "name": "toolkit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "probe_action": { + "name": "probe_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "composio_connections_user_idx": { + "name": "composio_connections_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "composio_connections_toolkit_user_id_pk": { + "name": "composio_connections_toolkit_user_id_pk", + "columns": [ + "toolkit", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vendor": { + "name": "vendor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provenance": { + "name": "provenance", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'first-party'" + }, + "credential_id": { + "name": "credential_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "auth_scheme": { + "name": "auth_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_mode": { + "name": "account_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_vendor_user_id": { + "name": "shared_vendor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tools_refreshed_at": { + "name": "tools_refreshed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "skipped_tools": { + "name": "skipped_tools", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_servers_credential_id_credentials_id_fk": { + "name": "mcp_servers_credential_id_credentials_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_servers_account_mode_check": { + "name": "mcp_servers_account_mode_check", + "value": "\"mcp_servers\".\"account_mode\" IS NULL OR \"mcp_servers\".\"account_mode\" IN ('personal', 'shared')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_tools": { + "name": "mcp_tools", + "schema": "", + "columns": { + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vendor_name": { + "name": "vendor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "input_schema": { + "name": "input_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "effect": { + "name": "effect", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "destructive": { + "name": "destructive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_tools_server_id_mcp_servers_id_fk": { + "name": "mcp_tools_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_tools", + "tableTo": "mcp_servers", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mcp_tools_server_id_name_pk": { + "name": "mcp_tools_server_id_name_pk", + "columns": [ + "server_id", + "name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_user_credentials": { + "name": "mcp_user_credentials", + "schema": "", + "columns": { + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_user_credentials_user_idx": { + "name": "mcp_user_credentials_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_user_credentials_server_id_mcp_servers_id_fk": { + "name": "mcp_user_credentials_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_user_credentials", + "tableTo": "mcp_servers", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_user_credentials_user_id_users_id_fk": { + "name": "mcp_user_credentials_user_id_users_id_fk", + "tableFrom": "mcp_user_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_user_credentials_credential_id_credentials_id_fk": { + "name": "mcp_user_credentials_credential_id_credentials_id_fk", + "tableFrom": "mcp_user_credentials", + "tableTo": "credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mcp_user_credentials_server_id_user_id_pk": { + "name": "mcp_user_credentials_server_id_user_id_pk", + "columns": [ + "server_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_authorizations": { + "name": "pending_authorizations", + "schema": "", + "columns": { + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "holder": { + "name": "holder", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vendor_user_id": { + "name": "vendor_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_id": { + "name": "auth_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "pending_authorizations_server_idx": { + "name": "pending_authorizations_server_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_authorizations_actor_id_users_id_fk": { + "name": "pending_authorizations_actor_id_users_id_fk", + "tableFrom": "pending_authorizations", + "tableTo": "users", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_authorizations_server_id_mcp_servers_id_fk": { + "name": "pending_authorizations_server_id_mcp_servers_id_fk", + "tableFrom": "pending_authorizations", + "tableTo": "mcp_servers", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pending_authorizations_holder_check": { + "name": "pending_authorizations_holder_check", + "value": "(\"pending_authorizations\".\"holder\" = 'person' AND \"pending_authorizations\".\"user_id\" IS NOT NULL) OR (\"pending_authorizations\".\"holder\" = 'deployment' AND \"pending_authorizations\".\"user_id\" IS NULL)" + }, + "pending_authorizations_outcome_check": { + "name": "pending_authorizations_outcome_check", + "value": "\"pending_authorizations\".\"outcome\" IS NULL OR \"pending_authorizations\".\"outcome\" IN ('completed', 'failed', 'unreconciled', 'withdrawn')" + }, + "pending_authorizations_completion_check": { + "name": "pending_authorizations_completion_check", + "value": "(\"pending_authorizations\".\"completed_at\" IS NULL) = (\"pending_authorizations\".\"outcome\" IS NULL)" + }, + "pending_authorizations_user_is_actor_check": { + "name": "pending_authorizations_user_is_actor_check", + "value": "\"pending_authorizations\".\"user_id\" IS NULL OR \"pending_authorizations\".\"user_id\" = \"pending_authorizations\".\"actor_id\"" + } + }, + "isRLSEnabled": false + }, + "public.plugin_grants": { + "name": "plugin_grants", + "schema": "", + "columns": { + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ref": { + "name": "ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_grants_agent_idx": { + "name": "plugin_grants_agent_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_grants_agent_id_agents_id_fk": { + "name": "plugin_grants_agent_id_agents_id_fk", + "tableFrom": "plugin_grants", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_grants_kind_ref_agent_id_pk": { + "name": "plugin_grants_kind_ref_agent_id_pk", + "columns": [ + "kind", + "ref", + "agent_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sandboxed_components": { + "name": "sandboxed_components", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "draft_description": { + "name": "draft_description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "draft_html": { + "name": "draft_html", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "draft_css": { + "name": "draft_css", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "draft_js_functions": { + "name": "draft_js_functions", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "draft_argument_schema": { + "name": "draft_argument_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "published_description": { + "name": "published_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_html": { + "name": "published_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_css": { + "name": "published_css", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_js_functions": { + "name": "published_js_functions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_argument_schema": { + "name": "published_argument_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sample_arguments": { + "name": "sample_arguments", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "authored_by": { + "name": "authored_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill_tools": { + "name": "skill_tools", + "schema": "", + "columns": { + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ref": { + "name": "ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "declared_by": { + "name": "declared_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_tools_ref_idx": { + "name": "skill_tools_ref_idx", + "columns": [ + { + "expression": "ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_tools_skill_id_skills_id_fk": { + "name": "skill_tools_skill_id_skills_id_fk", + "tableFrom": "skill_tools", + "tableTo": "skills", + "columnsFrom": [ + "skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "skill_tools_skill_id_ref_pk": { + "name": "skill_tools_skill_id_ref_pk", + "columns": [ + "skill_id", + "ref" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skills": { + "name": "skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'yours'" + }, + "installed_by": { + "name": "installed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skills_slug_key": { + "name": "skills_slug_key", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "skills_owner_idx": { + "name": "skills_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skills_owner_user_id_users_id_fk": { + "name": "skills_owner_user_id_users_id_fk", + "tableFrom": "skills", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.work_items": { + "name": "work_items", + "schema": "", + "columns": { + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_at": { + "name": "run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "work_items_claimable_idx": { + "name": "work_items_claimable_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "work_items_open_thread_idx": { + "name": "work_items_open_thread_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "(\"payload\"->>'threadId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"work_items\".\"finished_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "work_items_kind_key_pk": { + "name": "work_items_kind_key_pk", + "columns": [ + "kind", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.voice_sessions": { + "name": "voice_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor_message_id": { + "name": "anchor_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "duration_seconds": { + "name": "duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "transcript": { + "name": "transcript", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_status": { + "name": "summary_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'failed'" + } + }, + "indexes": { + "voice_sessions_channel_started_idx": { + "name": "voice_sessions_channel_started_idx", + "columns": [ + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "voice_sessions_channel_id_channels_id_fk": { + "name": "voice_sessions_channel_id_channels_id_fk", + "tableFrom": "voice_sessions", + "tableTo": "channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "voice_sessions_user_id_users_id_fk": { + "name": "voice_sessions_user_id_users_id_fk", + "tableFrom": "voice_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "voice_sessions_duration_check": { + "name": "voice_sessions_duration_check", + "value": "\"voice_sessions\".\"duration_seconds\" >= 0" + }, + "voice_sessions_summary_check": { + "name": "voice_sessions_summary_check", + "value": "(\"voice_sessions\".\"summary_status\" = 'ready' AND \"voice_sessions\".\"summary\" IS NOT NULL) OR (\"voice_sessions\".\"summary_status\" = 'failed' AND \"voice_sessions\".\"summary\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.learning_settings": { + "name": "learning_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.learning_thread_bindings": { + "name": "learning_thread_bindings", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "learning_thread_bindings_user_id_thread_id_pk": { + "name": "learning_thread_bindings_user_id_thread_id_pk", + "columns": [ + "user_id", + "thread_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.responsibilities": { + "name": "responsibilities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instruction": { + "name": "instruction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "success_criteria": { + "name": "success_criteria", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "responsibility_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "progress": { + "name": "progress", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_result": { + "name": "last_result", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subscriptions": { + "name": "subscriptions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "responsibilities_owner_status_idx": { + "name": "responsibilities_owner_status_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "responsibilities_owner_user_id_users_id_fk": { + "name": "responsibilities_owner_user_id_users_id_fk", + "tableFrom": "responsibilities", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "responsibilities_agent_id_agents_id_fk": { + "name": "responsibilities_agent_id_agents_id_fk", + "tableFrom": "responsibilities", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.responsibility_events": { + "name": "responsibility_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "responsibility_events_source_id_idx": { + "name": "responsibility_events_source_id_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "responsibility_events_owner_user_id_users_id_fk": { + "name": "responsibility_events_owner_user_id_users_id_fk", + "tableFrom": "responsibility_events", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.responsibility_runs": { + "name": "responsibility_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "responsibility_id": { + "name": "responsibility_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "responsibility_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "reply_text": { + "name": "reply_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "waiting": { + "name": "waiting", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "continuation": { + "name": "continuation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "responsibility_runs_event_idx": { + "name": "responsibility_runs_event_idx", + "columns": [ + { + "expression": "responsibility_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "responsibility_runs_status_idx": { + "name": "responsibility_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "responsibility_runs_responsibility_id_responsibilities_id_fk": { + "name": "responsibility_runs_responsibility_id_responsibilities_id_fk", + "tableFrom": "responsibility_runs", + "tableTo": "responsibilities", + "columnsFrom": [ + "responsibility_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "responsibility_runs_event_id_responsibility_events_id_fk": { + "name": "responsibility_runs_event_id_responsibility_events_id_fk", + "tableFrom": "responsibility_runs", + "tableTo": "responsibility_events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.responsibility_source_bindings": { + "name": "responsibility_source_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "responsibility_source_bindings_owner_idx": { + "name": "responsibility_source_bindings_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "responsibility_source_bindings_owner_user_id_users_id_fk": { + "name": "responsibility_source_bindings_owner_user_id_users_id_fk", + "tableFrom": "responsibility_source_bindings", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "responsibility_source_bindings_credential_id_credentials_id_fk": { + "name": "responsibility_source_bindings_credential_id_credentials_id_fk", + "tableFrom": "responsibility_source_bindings", + "tableTo": "credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.responsibility_triggers": { + "name": "responsibility_triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "responsibility_id": { + "name": "responsibility_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "responsibility_triggers_owner_idx": { + "name": "responsibility_triggers_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "responsibility_triggers_responsibility_idx": { + "name": "responsibility_triggers_responsibility_idx", + "columns": [ + { + "expression": "responsibility_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "responsibility_triggers_kind_idx": { + "name": "responsibility_triggers_kind_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "responsibility_triggers_owner_user_id_users_id_fk": { + "name": "responsibility_triggers_owner_user_id_users_id_fk", + "tableFrom": "responsibility_triggers", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "responsibility_triggers_responsibility_id_responsibilities_id_fk": { + "name": "responsibility_triggers_responsibility_id_responsibilities_id_fk", + "tableFrom": "responsibility_triggers", + "tableTo": "responsibilities", + "columnsFrom": [ + "responsibility_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "responsibility_triggers_credential_id_credentials_id_fk": { + "name": "responsibility_triggers_credential_id_credentials_id_fk", + "tableFrom": "responsibility_triggers", + "tableTo": "credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approval_preferences": { + "name": "approval_preferences", + "schema": "", + "columns": { + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_review": { + "name": "auto_review", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "host_commands": { + "name": "host_commands", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ask'" + } + }, + "indexes": {}, + "foreignKeys": { + "approval_preferences_owner_user_id_users_id_fk": { + "name": "approval_preferences_owner_user_id_users_id_fk", + "tableFrom": "approval_preferences", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "approval_preferences_host_commands_check": { + "name": "approval_preferences_host_commands_check", + "value": "\"approval_preferences\".\"host_commands\" IN ('ask', 'allow', 'never')" + } + }, + "isRLSEnabled": false + }, + "public.approval_requests": { + "name": "approval_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action_digest": { + "name": "action_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "approval_requests_action_idx": { + "name": "approval_requests_action_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_requests_inbox_idx": { + "name": "approval_requests_inbox_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approval_requests_owner_user_id_users_id_fk": { + "name": "approval_requests_owner_user_id_users_id_fk", + "tableFrom": "approval_requests", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "approval_status_check": { + "name": "approval_status_check", + "value": "\"approval_requests\".\"status\" IN ('pending', 'approved', 'denied', 'consumed', 'completed')" + }, + "approval_decision_check": { + "name": "approval_decision_check", + "value": "\"approval_requests\".\"decision\" IN ('allow_once', 'allow_always', 'deny', 'handled')" + } + }, + "isRLSEnabled": false + }, + "public.approval_rules": { + "name": "approval_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bot_id": { + "name": "bot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_ref": { + "name": "tool_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effect": { + "name": "effect", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "behaviour": { + "name": "behaviour", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'allow'" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "approval_rules_owner_idx": { + "name": "approval_rules_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approval_rules_owner_user_id_users_id_fk": { + "name": "approval_rules_owner_user_id_users_id_fk", + "tableFrom": "approval_rules", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "approval_rules_behaviour_check": { + "name": "approval_rules_behaviour_check", + "value": "\"approval_rules\".\"behaviour\" IN ('allow', 'pre_approved', 'ask', 'hand_off')" + } + }, + "isRLSEnabled": false + }, + "public.approval_team_rules": { + "name": "approval_team_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "bot_id": { + "name": "bot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_ref": { + "name": "tool_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effect": { + "name": "effect", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "behaviour": { + "name": "behaviour", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "approval_team_rules_created_by_users_id_fk": { + "name": "approval_team_rules_created_by_users_id_fk", + "tableFrom": "approval_team_rules", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "approval_team_rules_behaviour_check": { + "name": "approval_team_rules_behaviour_check", + "value": "\"approval_team_rules\".\"behaviour\" IN ('allow', 'pre_approved', 'ask', 'hand_off')" + } + }, + "isRLSEnabled": false + }, + "public.approval_team_settings": { + "name": "approval_team_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enforce_auto_review": { + "name": "enforce_auto_review", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "custom_rules_enabled": { + "name": "custom_rules_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "host_commands_cap": { + "name": "host_commands_cap", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'allow'" + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "approval_team_settings_updated_by_users_id_fk": { + "name": "approval_team_settings_updated_by_users_id_fk", + "tableFrom": "approval_team_settings", + "tableTo": "users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "approval_team_settings_host_commands_cap_check": { + "name": "approval_team_settings_host_commands_cap_check", + "value": "\"approval_team_settings\".\"host_commands_cap\" IN ('ask', 'allow', 'never')" + } + }, + "isRLSEnabled": false + }, + "public.memory_sources": { + "name": "memory_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_ref": { + "name": "tool_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "memory_sources_owner_idx": { + "name": "memory_sources_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_sources_due_idx": { + "name": "memory_sources_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_sources_owner_user_id_users_id_fk": { + "name": "memory_sources_owner_user_id_users_id_fk", + "tableFrom": "memory_sources", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memory_sources_agent_id_agents_id_fk": { + "name": "memory_sources_agent_id_agents_id_fk", + "tableFrom": "memory_sources", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.personal_memories": { + "name": "personal_memories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "import_digest": { + "name": "import_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provenance": { + "name": "provenance", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "review_state": { + "name": "review_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unreviewed'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "formed_by": { + "name": "formed_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'person'" + }, + "formed_by_agent_id": { + "name": "formed_by_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_app": { + "name": "source_app", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "intelligence_memory_id": { + "name": "intelligence_memory_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intelligence_digest": { + "name": "intelligence_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "personal_memories_owner_idx": { + "name": "personal_memories_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "personal_memories_intelligence_idx": { + "name": "personal_memories_intelligence_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "intelligence_memory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "personal_memories_formed_idx": { + "name": "personal_memories_formed_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "formed_by_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "personal_memories_import_idx": { + "name": "personal_memories_import_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "personal_memories_formed_digest_idx": { + "name": "personal_memories_formed_digest_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "import_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"personal_memories\".\"formed_by\" = 'bot'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "personal_memories_owner_user_id_users_id_fk": { + "name": "personal_memories_owner_user_id_users_id_fk", + "tableFrom": "personal_memories", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "personal_memories_source_id_memory_sources_id_fk": { + "name": "personal_memories_source_id_memory_sources_id_fk", + "tableFrom": "personal_memories", + "tableTo": "memory_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "personal_memories_formed_by_agent_id_agents_id_fk": { + "name": "personal_memories_formed_by_agent_id_agents_id_fk", + "tableFrom": "personal_memories", + "tableTo": "agents", + "columnsFrom": [ + "formed_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.default_opt_outs": { + "name": "default_opt_outs", + "schema": "", + "columns": { + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "default_opt_outs_owner_user_id_users_id_fk": { + "name": "default_opt_outs_owner_user_id_users_id_fk", + "tableFrom": "default_opt_outs", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "default_opt_outs_agent_id_agents_id_fk": { + "name": "default_opt_outs_agent_id_agents_id_fk", + "tableFrom": "default_opt_outs", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "default_opt_outs_owner_user_id_agent_id_feature_scope_pk": { + "name": "default_opt_outs_owner_user_id_agent_id_feature_scope_pk", + "columns": [ + "owner_user_id", + "agent_id", + "feature", + "scope" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "default_opt_outs_feature_check": { + "name": "default_opt_outs_feature_check", + "value": "\"default_opt_outs\".\"feature\" IN ('memory_source', 'proactive_research')" + } + }, + "isRLSEnabled": false + }, + "public.proactive_settings": { + "name": "proactive_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "focus": { + "name": "focus", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "interval_minutes": { + "name": "interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 240 + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_status": { + "name": "last_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "proactive_settings_owner_agent_idx": { + "name": "proactive_settings_owner_agent_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "proactive_settings_due_idx": { + "name": "proactive_settings_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "proactive_settings_owner_user_id_users_id_fk": { + "name": "proactive_settings_owner_user_id_users_id_fk", + "tableFrom": "proactive_settings", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proactive_settings_agent_id_agents_id_fk": { + "name": "proactive_settings_agent_id_agents_id_fk", + "tableFrom": "proactive_settings", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "proactive_settings_interval_check": { + "name": "proactive_settings_interval_check", + "value": "\"proactive_settings\".\"interval_minutes\" >= 60 AND \"proactive_settings\".\"interval_minutes\" <= 1440" + }, + "proactive_settings_status_check": { + "name": "proactive_settings_status_check", + "value": "\"proactive_settings\".\"last_status\" IN ('idle', 'running', 'succeeded', 'error')" + } + }, + "isRLSEnabled": false + }, + "public.proactive_suggestions": { + "name": "proactive_suggestions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "setting_id": { + "name": "setting_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_app": { + "name": "source_app", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "proactive_suggestions_owner_idx": { + "name": "proactive_suggestions_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "proactive_suggestions_run_idx": { + "name": "proactive_suggestions_run_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "proactive_suggestions_owner_user_id_users_id_fk": { + "name": "proactive_suggestions_owner_user_id_users_id_fk", + "tableFrom": "proactive_suggestions", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proactive_suggestions_agent_id_agents_id_fk": { + "name": "proactive_suggestions_agent_id_agents_id_fk", + "tableFrom": "proactive_suggestions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "proactive_suggestions_setting_id_proactive_settings_id_fk": { + "name": "proactive_suggestions_setting_id_proactive_settings_id_fk", + "tableFrom": "proactive_suggestions", + "tableTo": "proactive_settings", + "columnsFrom": [ + "setting_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "proactive_suggestions_status_check": { + "name": "proactive_suggestions_status_check", + "value": "\"proactive_suggestions\".\"status\" IN ('open', 'dismissed', 'started', 'superseded')" + } + }, + "isRLSEnabled": false + }, + "public.demonstrations": { + "name": "demonstrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bot_id": { + "name": "bot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'recording'" + }, + "actions": { + "name": "actions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "draft": { + "name": "draft", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "skill_slug": { + "name": "skill_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "demonstrations_owner_idx": { + "name": "demonstrations_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "demonstrations_active_bot_idx": { + "name": "demonstrations_active_bot_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"demonstrations\".\"status\" = 'recording'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "demonstrations_owner_user_id_users_id_fk": { + "name": "demonstrations_owner_user_id_users_id_fk", + "tableFrom": "demonstrations", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "demonstrations_bot_id_agents_id_fk": { + "name": "demonstrations_bot_id_agents_id_fk", + "tableFrom": "demonstrations", + "tableTo": "agents", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.delivery_bindings": { + "name": "delivery_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identity": { + "name": "identity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "realm": { + "name": "realm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mention_id": { + "name": "mention_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "opted_out_at": { + "name": "opted_out_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "delivery_binding_identity_idx": { + "name": "delivery_binding_identity_idx", + "columns": [ + { + "expression": "transport", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "realm", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "identity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "delivery_bindings_owner_idx": { + "name": "delivery_bindings_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "delivery_bindings_owner_user_id_users_id_fk": { + "name": "delivery_bindings_owner_user_id_users_id_fk", + "tableFrom": "delivery_bindings", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "delivery_bindings_channel_id_channels_id_fk": { + "name": "delivery_bindings_channel_id_channels_id_fk", + "tableFrom": "delivery_bindings", + "tableTo": "channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "delivery_bindings_agent_id_agents_id_fk": { + "name": "delivery_bindings_agent_id_agents_id_fk", + "tableFrom": "delivery_bindings", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.delivery_challenges": { + "name": "delivery_challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "delivery_challenges_owner_user_id_users_id_fk": { + "name": "delivery_challenges_owner_user_id_users_id_fk", + "tableFrom": "delivery_challenges", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "delivery_challenges_channel_id_channels_id_fk": { + "name": "delivery_challenges_channel_id_channels_id_fk", + "tableFrom": "delivery_challenges", + "tableTo": "channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "delivery_challenges_agent_id_agents_id_fk": { + "name": "delivery_challenges_agent_id_agents_id_fk", + "tableFrom": "delivery_challenges", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.delivery_inbox": { + "name": "delivery_inbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding_id": { + "name": "binding_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "realm": { + "name": "realm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "delivery_inbox_dedupe_idx": { + "name": "delivery_inbox_dedupe_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "realm", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "delivery_inbox_owner_idx": { + "name": "delivery_inbox_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "delivery_inbox_owner_user_id_users_id_fk": { + "name": "delivery_inbox_owner_user_id_users_id_fk", + "tableFrom": "delivery_inbox", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "delivery_inbox_channel_id_channels_id_fk": { + "name": "delivery_inbox_channel_id_channels_id_fk", + "tableFrom": "delivery_inbox", + "tableTo": "channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "delivery_inbox_agent_id_agents_id_fk": { + "name": "delivery_inbox_agent_id_agents_id_fk", + "tableFrom": "delivery_inbox", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.delivery_outbox": { + "name": "delivery_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding_id": { + "name": "binding_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "delivery_outbox_owner_idx": { + "name": "delivery_outbox_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "delivery_outbox_provider_idx": { + "name": "delivery_outbox_provider_idx", + "columns": [ + { + "expression": "transport", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "delivery_outbox_owner_user_id_users_id_fk": { + "name": "delivery_outbox_owner_user_id_users_id_fk", + "tableFrom": "delivery_outbox", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "delivery_outbox_channel_id_channels_id_fk": { + "name": "delivery_outbox_channel_id_channels_id_fk", + "tableFrom": "delivery_outbox", + "tableTo": "channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "delivery_outbox_agent_id_agents_id_fk": { + "name": "delivery_outbox_agent_id_agents_id_fk", + "tableFrom": "delivery_outbox", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "delivery_outbox_dedupe_key_unique": { + "name": "delivery_outbox_dedupe_key_unique", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.push_devices": { + "name": "push_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "push_devices_owner_idx": { + "name": "push_devices_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "push_devices_owner_user_id_users_id_fk": { + "name": "push_devices_owner_user_id_users_id_fk", + "tableFrom": "push_devices", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "push_devices_token_unique": { + "name": "push_devices_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.group_bot_threads": { + "name": "group_bot_threads", + "schema": "", + "columns": { + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "group_bot_threads_thread_idx": { + "name": "group_bot_threads_thread_idx", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "group_bot_threads_owner_user_id_users_id_fk": { + "name": "group_bot_threads_owner_user_id_users_id_fk", + "tableFrom": "group_bot_threads", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "group_bot_threads_channel_id_channels_id_fk": { + "name": "group_bot_threads_channel_id_channels_id_fk", + "tableFrom": "group_bot_threads", + "tableTo": "channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "group_bot_threads_owner_user_id_channel_id_agent_id_pk": { + "name": "group_bot_threads_owner_user_id_channel_id_agent_id_pk", + "columns": [ + "owner_user_id", + "channel_id", + "agent_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.group_messages": { + "name": "group_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'completed'" + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "group_messages_channel_idx": { + "name": "group_messages_channel_idx", + "columns": [ + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "group_messages_channel_id_channels_id_fk": { + "name": "group_messages_channel_id_channels_id_fk", + "tableFrom": "group_messages", + "tableTo": "channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "group_messages_owner_user_id_users_id_fk": { + "name": "group_messages_owner_user_id_users_id_fk", + "tableFrom": "group_messages", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "group_messages_status_check": { + "name": "group_messages_status_check", + "value": "\"group_messages\".\"status\" IN ('queued', 'running', 'waiting', 'completed', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.action_records": { + "name": "action_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bot_id": { + "name": "bot_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "action_records_created_idx": { + "name": "action_records_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "action_records_bot_idx": { + "name": "action_records_bot_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "action_records_surface_check": { + "name": "action_records_surface_check", + "value": "\"action_records\".\"surface\" IN ('cloud', 'local')" + } + }, + "isRLSEnabled": false + }, + "public.capability_settings": { + "name": "capability_settings", + "schema": "", + "columns": { + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "capability": { + "name": "capability", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed": { + "name": "allowed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "capability_settings_scope_kind_scope_id_capability_pk": { + "name": "capability_settings_scope_kind_scope_id_capability_pk", + "columns": [ + "scope_kind", + "scope_id", + "capability" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "capability_settings_scope_kind_check": { + "name": "capability_settings_scope_kind_check", + "value": "\"capability_settings\".\"scope_kind\" IN ('organization', 'role', 'group')" + } + }, + "isRLSEnabled": false + }, + "public.enterprise_settings": { + "name": "enterprise_settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_usage": { + "name": "model_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed": { + "name": "allowed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "model_usage_created_idx": { + "name": "model_usage_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "model_usage_model_idx": { + "name": "model_usage_model_idx", + "columns": [ + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "model_usage_source_check": { + "name": "model_usage_source_check", + "value": "\"model_usage\".\"source\" IN ('configured', 'observed', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.network_policies": { + "name": "network_policies", + "schema": "", + "columns": { + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rules": { + "name": "rules", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"entries\":[]}'::jsonb" + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "network_policies_scope_kind_scope_id_pk": { + "name": "network_policies_scope_kind_scope_id_pk", + "columns": [ + "scope_kind", + "scope_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "network_policies_scope_kind_check": { + "name": "network_policies_scope_kind_check", + "value": "\"network_policies\".\"scope_kind\" IN ('organization', 'group')" + }, + "network_policies_mode_check": { + "name": "network_policies_mode_check", + "value": "\"network_policies\".\"mode\" IN ('allow_all', 'defaults_plus_allowlist', 'allowlist_only')" + } + }, + "isRLSEnabled": false + }, + "public.scim_connection_bindings": { + "name": "scim_connection_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_key": { + "name": "connection_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "decommissioned_at": { + "name": "decommissioned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "decommission_status": { + "name": "decommission_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "decommission_cursor_user_id": { + "name": "decommission_cursor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decommission_reconciled_user_count": { + "name": "decommission_reconciled_user_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "decommission_batch_count": { + "name": "decommission_batch_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "decommission_revision": { + "name": "decommission_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "decommission_completed_at": { + "name": "decommission_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "decommission_lease_id": { + "name": "decommission_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decommission_lease_expires_at": { + "name": "decommission_lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scim_connection_bindings_connection_idx": { + "name": "scim_connection_bindings_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_connection_bindings_connection_key_unique": { + "name": "scim_connection_bindings_connection_key_unique", + "nullsNotDistinct": false, + "columns": [ + "connection_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group_members": { + "name": "scim_group_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "membership_key": { + "name": "membership_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scim_group_members_connection_idx": { + "name": "scim_group_members_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_members_group_idx": { + "name": "scim_group_members_group_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_members_scim_user_idx": { + "name": "scim_group_members_scim_user_idx", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_members_group_id_scim_groups_id_fk": { + "name": "scim_group_members_group_id_scim_groups_id_fk", + "tableFrom": "scim_group_members", + "tableTo": "scim_groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_members_scim_user_id_scim_users_id_fk": { + "name": "scim_group_members_scim_user_id_scim_users_id_fk", + "tableFrom": "scim_group_members", + "tableTo": "scim_users", + "columnsFrom": [ + "scim_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_group_members_membership_key_unique": { + "name": "scim_group_members_membership_key_unique", + "nullsNotDistinct": false, + "columns": [ + "membership_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_groups": { + "name": "scim_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name_key": { + "name": "display_name_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id_key": { + "name": "external_id_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scim_groups_connection_idx": { + "name": "scim_groups_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_groups_domain_idx": { + "name": "scim_groups_domain_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_groups_display_name_key_unique": { + "name": "scim_groups_display_name_key_unique", + "nullsNotDistinct": false, + "columns": [ + "display_name_key" + ] + }, + "scim_groups_external_id_key_unique": { + "name": "scim_groups_external_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id_key" + ] + }, + "scim_groups_order_key_unique": { + "name": "scim_groups_order_key_unique", + "nullsNotDistinct": false, + "columns": [ + "order_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_identity_tombstones": { + "name": "scim_identity_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id_key": { + "name": "external_id_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile": { + "name": "profile", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scim_identity_tombstones_connection_idx": { + "name": "scim_identity_tombstones_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_identity_tombstones_domain_idx": { + "name": "scim_identity_tombstones_domain_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_identity_tombstones_user_idx": { + "name": "scim_identity_tombstones_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_identity_tombstones_user_id_users_id_fk": { + "name": "scim_identity_tombstones_user_id_users_id_fk", + "tableFrom": "scim_identity_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_identity_tombstones_external_id_key_unique": { + "name": "scim_identity_tombstones_external_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_projection_grants": { + "name": "scim_projection_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_value": { + "name": "source_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "grant_key": { + "name": "grant_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scim_projection_grants_connection_idx": { + "name": "scim_projection_grants_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_projection_grants_domain_idx": { + "name": "scim_projection_grants_domain_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_projection_grants_scim_user_idx": { + "name": "scim_projection_grants_scim_user_idx", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_projection_grants_user_idx": { + "name": "scim_projection_grants_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_projection_grants_scim_user_id_scim_users_id_fk": { + "name": "scim_projection_grants_scim_user_id_scim_users_id_fk", + "tableFrom": "scim_projection_grants", + "tableTo": "scim_users", + "columnsFrom": [ + "scim_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_projection_grants_user_id_users_id_fk": { + "name": "scim_projection_grants_user_id_users_id_fk", + "tableFrom": "scim_projection_grants", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_projection_grants_grant_key_unique": { + "name": "scim_projection_grants_grant_key_unique", + "nullsNotDistinct": false, + "columns": [ + "grant_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_subjects": { + "name": "scim_subjects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_source_id": { + "name": "profile_source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scim_subjects_profile_source_idx": { + "name": "scim_subjects_profile_source_idx", + "columns": [ + { + "expression": "profile_source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_subjects_user_id_users_id_fk": { + "name": "scim_subjects_user_id_users_id_fk", + "tableFrom": "scim_subjects", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_subjects_user_id_unique": { + "name": "scim_subjects_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_users": { + "name": "scim_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_user_key": { + "name": "connection_user_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_name": { + "name": "user_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_name_key": { + "name": "user_name_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_email": { + "name": "primary_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "work_email_value_index": { + "name": "work_email_value_index", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_value_index": { + "name": "email_value_index", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "formatted_name": { + "name": "formatted_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serialized_emails": { + "name": "serialized_emails", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serialized_attributes": { + "name": "serialized_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id_key": { + "name": "external_id_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scim_users_connection_idx": { + "name": "scim_users_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_users_domain_idx": { + "name": "scim_users_domain_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_users_user_idx": { + "name": "scim_users_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_users_user_id_users_id_fk": { + "name": "scim_users_user_id_users_id_fk", + "tableFrom": "scim_users", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_users_connection_user_key_unique": { + "name": "scim_users_connection_user_key_unique", + "nullsNotDistinct": false, + "columns": [ + "connection_user_key" + ] + }, + "scim_users_user_name_key_unique": { + "name": "scim_users_user_name_key_unique", + "nullsNotDistinct": false, + "columns": [ + "user_name_key" + ] + }, + "scim_users_external_id_key_unique": { + "name": "scim_users_external_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id_key" + ] + }, + "scim_users_order_key_unique": { + "name": "scim_users_order_key_unique", + "nullsNotDistinct": false, + "columns": [ + "order_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_lifecycle": { + "name": "bot_lifecycle", + "schema": "", + "columns": { + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "notify": { + "name": "notify", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "bot_lifecycle_owner_user_id_users_id_fk": { + "name": "bot_lifecycle_owner_user_id_users_id_fk", + "tableFrom": "bot_lifecycle", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_lifecycle_agent_id_agents_id_fk": { + "name": "bot_lifecycle_agent_id_agents_id_fk", + "tableFrom": "bot_lifecycle", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bot_lifecycle_owner_user_id_agent_id_pk": { + "name": "bot_lifecycle_owner_user_id_agent_id_pk", + "columns": [ + "owner_user_id", + "agent_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.update_routing_preferences": { + "name": "update_routing_preferences", + "schema": "", + "columns": { + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "update_routing_preferences_owner_user_id_users_id_fk": { + "name": "update_routing_preferences_owner_user_id_users_id_fk", + "tableFrom": "update_routing_preferences", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "update_routing_preferences_owner_user_id_kind_pk": { + "name": "update_routing_preferences_owner_user_id_kind_pk", + "columns": [ + "owner_user_id", + "kind" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_logins": { + "name": "saved_logins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "saved_logins_owner_origin_username_idx": { + "name": "saved_logins_owner_origin_username_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "saved_logins_owner_idx": { + "name": "saved_logins_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saved_logins_owner_user_id_users_id_fk": { + "name": "saved_logins_owner_user_id_users_id_fk", + "tableFrom": "saved_logins", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_in_requests": { + "name": "sign_in_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bot_id": { + "name": "bot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "control_request_id": { + "name": "control_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "continuation": { + "name": "continuation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "filling_until": { + "name": "filling_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sign_in_requests_owner_idx": { + "name": "sign_in_requests_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_in_requests_owner_user_id_users_id_fk": { + "name": "sign_in_requests_owner_user_id_users_id_fk", + "tableFrom": "sign_in_requests", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "sign_in_requests_status_check": { + "name": "sign_in_requests_status_check", + "value": "\"sign_in_requests\".\"status\" IN ('pending', 'filling', 'code_required', 'taken_over', 'signed_in', 'failed', 'cancelled', 'expired')" + }, + "sign_in_requests_method_check": { + "name": "sign_in_requests_method_check", + "value": "\"sign_in_requests\".\"method\" IS NULL OR \"sign_in_requests\".\"method\" IN ('typed', 'saved', 'takeover')" + } + }, + "isRLSEnabled": false + }, + "public.team_bot_assignments": { + "name": "team_bot_assignments", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_bot_assignments_agent_id_agents_id_fk": { + "name": "team_bot_assignments_agent_id_agents_id_fk", + "tableFrom": "team_bot_assignments", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_bot_assignments_assigned_by_users_id_fk": { + "name": "team_bot_assignments_assigned_by_users_id_fk", + "tableFrom": "team_bot_assignments", + "tableTo": "users", + "columnsFrom": [ + "assigned_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "team_bot_assignments_agent_id_group_name_pk": { + "name": "team_bot_assignments_agent_id_group_name_pk", + "columns": [ + "agent_id", + "group_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_bot_audience": { + "name": "team_bot_audience", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_bot_audience_agent_id_agents_id_fk": { + "name": "team_bot_audience_agent_id_agents_id_fk", + "tableFrom": "team_bot_audience", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "team_bot_audience_agent_id_kind_value_pk": { + "name": "team_bot_audience_agent_id_kind_value_pk", + "columns": [ + "agent_id", + "kind", + "value" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "team_bot_audience_kind_check": { + "name": "team_bot_audience_kind_check", + "value": "\"team_bot_audience\".\"kind\" IN ('user', 'group')" + } + }, + "isRLSEnabled": false + }, + "public.team_bot_consent_defaults": { + "name": "team_bot_consent_defaults", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_bot_consent_defaults_user_id_users_id_fk": { + "name": "team_bot_consent_defaults_user_id_users_id_fk", + "tableFrom": "team_bot_consent_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "team_bot_consent_defaults_user_id_server_id_pk": { + "name": "team_bot_consent_defaults_user_id_server_id_pk", + "columns": [ + "user_id", + "server_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_bot_consents": { + "name": "team_bot_consents", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_bot_consents_user_id_users_id_fk": { + "name": "team_bot_consents_user_id_users_id_fk", + "tableFrom": "team_bot_consents", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_bot_consents_agent_id_agents_id_fk": { + "name": "team_bot_consents_agent_id_agents_id_fk", + "tableFrom": "team_bot_consents", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "team_bot_consents_user_id_agent_id_server_id_pk": { + "name": "team_bot_consents_user_id_agent_id_server_id_pk", + "columns": [ + "user_id", + "agent_id", + "server_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "team_bot_consents_decision_check": { + "name": "team_bot_consents_decision_check", + "value": "\"team_bot_consents\".\"decision\" IN ('always', 'once')" + } + }, + "isRLSEnabled": false + }, + "public.team_bot_publications": { + "name": "team_bot_publications", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "published_by": { + "name": "published_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_bot_publications_agent_id_agents_id_fk": { + "name": "team_bot_publications_agent_id_agents_id_fk", + "tableFrom": "team_bot_publications", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_bot_publications_published_by_users_id_fk": { + "name": "team_bot_publications_published_by_users_id_fk", + "tableFrom": "team_bot_publications", + "tableTo": "users", + "columnsFrom": [ + "published_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "team_bot_publications_audience_check": { + "name": "team_bot_publications_audience_check", + "value": "\"team_bot_publications\".\"audience\" IN ('team', 'people')" + } + }, + "isRLSEnabled": false + }, + "public.shared_use_approval_members": { + "name": "shared_use_approval_members", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "shared_use_approval_members_approval_fk": { + "name": "shared_use_approval_members_approval_fk", + "tableFrom": "shared_use_approval_members", + "tableTo": "shared_use_approvals", + "columnsFrom": [ + "agent_id", + "server_id" + ], + "columnsTo": [ + "agent_id", + "server_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_use_approval_members_agent_id_server_id_kind_value_pk": { + "name": "shared_use_approval_members_agent_id_server_id_kind_value_pk", + "columns": [ + "agent_id", + "server_id", + "kind", + "value" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "shared_use_approval_members_kind_check": { + "name": "shared_use_approval_members_kind_check", + "value": "\"shared_use_approval_members\".\"kind\" IN ('user', 'group')" + } + }, + "isRLSEnabled": false + }, + "public.shared_use_approvals": { + "name": "shared_use_approvals", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outside_input": { + "name": "outside_input", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "approved_by": { + "name": "approved_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shared_use_approvals_agent_id_agents_id_fk": { + "name": "shared_use_approvals_agent_id_agents_id_fk", + "tableFrom": "shared_use_approvals", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_use_approvals_server_id_mcp_servers_id_fk": { + "name": "shared_use_approvals_server_id_mcp_servers_id_fk", + "tableFrom": "shared_use_approvals", + "tableTo": "mcp_servers", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_use_approvals_agent_id_server_id_pk": { + "name": "shared_use_approvals_agent_id_server_id_pk", + "columns": [ + "agent_id", + "server_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "shared_use_approvals_audience_check": { + "name": "shared_use_approvals_audience_check", + "value": "\"shared_use_approvals\".\"audience\" IN ('owner', 'people', 'team')" + } + }, + "isRLSEnabled": false + }, + "public.shared_use_requests": { + "name": "shared_use_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_audience": { + "name": "proposed_audience", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_outside_input": { + "name": "proposed_outside_input", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "proposed_members": { + "name": "proposed_members", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "decided_by": { + "name": "decided_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "shared_use_requests_pending_idx": { + "name": "shared_use_requests_pending_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"shared_use_requests\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "shared_use_requests_status_idx": { + "name": "shared_use_requests_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_use_requests_agent_id_agents_id_fk": { + "name": "shared_use_requests_agent_id_agents_id_fk", + "tableFrom": "shared_use_requests", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_use_requests_server_id_mcp_servers_id_fk": { + "name": "shared_use_requests_server_id_mcp_servers_id_fk", + "tableFrom": "shared_use_requests", + "tableTo": "mcp_servers", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "shared_use_requests_audience_check": { + "name": "shared_use_requests_audience_check", + "value": "\"shared_use_requests\".\"proposed_audience\" IN ('owner', 'people', 'team')" + }, + "shared_use_requests_reason_check": { + "name": "shared_use_requests_reason_check", + "value": "\"shared_use_requests\".\"reason\" IN ('publish', 'trigger', 'grant', 'refused_call')" + }, + "shared_use_requests_status_check": { + "name": "shared_use_requests_status_check", + "value": "\"shared_use_requests\".\"status\" IN ('pending', 'approved', 'declined', 'superseded')" + } + }, + "isRLSEnabled": false + }, + "public.identity_link_challenges": { + "name": "identity_link_challenges", + "schema": "", + "columns": { + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "identity_link_challenges_expiry_idx": { + "name": "identity_link_challenges_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "identity_link_challenges_user_provider_idx": { + "name": "identity_link_challenges_user_provider_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.identity_links": { + "name": "identity_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "realm": { + "name": "realm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified_by": { + "name": "verified_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identity_links_identity_idx": { + "name": "identity_links_identity_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "realm", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "identity_links_user_realm_idx": { + "name": "identity_links_user_realm_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "realm", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "identity_links_credential_idx": { + "name": "identity_links_credential_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"identity_links\".\"credential_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "identity_links_credential_id_credentials_id_fk": { + "name": "identity_links_credential_id_credentials_id_fk", + "tableFrom": "identity_links", + "tableTo": "credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "identity_links_verified_by_check": { + "name": "identity_links_verified_by_check", + "value": "\"identity_links\".\"verified_by\" IN ('challenge', 'oauth')" + }, + "identity_links_status_check": { + "name": "identity_links_status_check", + "value": "\"identity_links\".\"status\" IN ('active', 'needs_reconnect')" + } + }, + "isRLSEnabled": false + } + }, + "enums": { + "public.agent_type": { + "name": "agent_type", + "schema": "public", + "values": [ + "built_in", + "remote_ag_ui", + "remote_mastra" + ] + }, + "public.credential_kind": { + "name": "credential_kind", + "schema": "public", + "values": [ + "model", + "connector", + "agent", + "mcp", + "mcp_oauth_client", + "mcp_user_token" + ] + }, + "public.role": { + "name": "role", + "schema": "public", + "values": [ + "admin", + "user" + ] + }, + "public.agent_visibility": { + "name": "agent_visibility", + "schema": "public", + "values": [ + "public", + "private" + ] + }, + "public.routine_run_status": { + "name": "routine_run_status", + "schema": "public", + "values": [ + "succeeded", + "failed", + "skipped", + "waiting" + ] + }, + "public.responsibility_run_status": { + "name": "responsibility_run_status", + "schema": "public", + "values": [ + "queued", + "running", + "waiting", + "succeeded", + "failed", + "skipped" + ] + }, + "public.responsibility_status": { + "name": "responsibility_status", + "schema": "public", + "values": [ + "active", + "paused", + "completed" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/server/drizzle/meta/_journal.json b/server/drizzle/meta/_journal.json index a3b2138466..d36dc6bd7c 100644 --- a/server/drizzle/meta/_journal.json +++ b/server/drizzle/meta/_journal.json @@ -442,6 +442,13 @@ "when": 1793016088183, "tag": "0062_action_policy_labels", "breakpoints": true + }, + { + "idx": 63, + "version": "7", + "when": 1793030330893, + "tag": "0063_work_items_open_thread", + "breakpoints": true } ] } \ No newline at end of file diff --git a/server/src/channels/working.ts b/server/src/channels/working.ts index 60b5b48182..8b74f02c0c 100644 --- a/server/src/channels/working.ts +++ b/server/src/channels/working.ts @@ -25,6 +25,7 @@ import { gt, inArray, isNull, + like, lt, lte, notLike, @@ -116,7 +117,8 @@ export async function workIn( * of when the question was put. */ const asked = alias(workItems, "asked"); - const threadOf = sql`${workItems.payload}->>'threadId'`; + // The same expression as `work_items_open_thread_idx`, so the read uses it. See work.ts. + const threadOf = sql`(${workItems.payload}->>'threadId')`; const hops = await database .select({ key: workItems.key, @@ -130,7 +132,16 @@ export async function workIn( asked, and( eq(asked.kind, workItems.kind), - sql`${workItems.key} = ${RELAY_PREFIX} || ${asked.key}`, + like(workItems.key, `${RELAY_PREFIX}%`), + /* + * The asked key cut out of the relay's, rather than the relay's built from every asked + * key: this way round it is a primary-key lookup per relay, and the other way round is a + * scan of every hop the table holds. + */ + eq( + asked.key, + sql`substr(${workItems.key}, ${RELAY_PREFIX.length + 1})`, + ), ), ) .where( diff --git a/server/src/db/schema/work.ts b/server/src/db/schema/work.ts index 6f6cb18a25..00d49a3cd5 100644 --- a/server/src/db/schema/work.ts +++ b/server/src/db/schema/work.ts @@ -1,3 +1,4 @@ +import { sql } from "drizzle-orm"; import { index, integer, @@ -94,5 +95,13 @@ export const workItems = pgTable( primaryKey({ columns: [table.kind, table.key] }), // The claim's own query: due, unclaimed or expired, not yet finished, oldest first. index("work_items_claimable_idx").on(table.kind, table.runAt), + /* + * What a conversation is waiting on: the open hops asked from a thread. Read on every roster + * page and every busy event, so it cannot scan a day of finished hops each time. Partial on + * unfinished rows, which keeps it as small as the queue's live work. See channels/working.ts. + */ + index("work_items_open_thread_idx") + .on(table.kind, sql`(${table.payload}->>'threadId')`) + .where(sql`${table.finishedAt} IS NULL`), ], ); From 821597a57b99216207b0e94c60f1bbc8f9975e91 Mon Sep 17 00:00:00 2001 From: Guido Vizoso Date: Mon, 26 Oct 2026 13:03:35 -0300 Subject: [PATCH 07/13] Keep what a conversation waits on to the person whose thread it is Every member of a shared channel has a thread of their own in it. The work list was broadcast to all members on every busy event, read from whichever thread the signal came from, so another member learned which Bot somebody asked and lost their own list to it; the roster also listed every member's routine runs. Hops are now read from the reader's thread and routines from the reader's own; the list goes only to the thread's owner, and the other members hear the busy flag alone, as before. Each list is stamped with the database clock just before it is read, so a browser can tell an older list that arrived late from a newer one. --- server/src/channels/events.ts | 6 + server/src/channels/routes.ts | 98 +++++++++--- server/src/channels/working.ts | 21 ++- server/tests/channel-work.integration.test.ts | 148 +++++++++++++++--- 4 files changed, 229 insertions(+), 44 deletions(-) diff --git a/server/src/channels/events.ts b/server/src/channels/events.ts index c4c2dc6466..9dc26eeb08 100644 --- a/server/src/channels/events.ts +++ b/server/src/channels/events.ts @@ -55,6 +55,12 @@ export type ChannelActivityEvent = { * is also what the roster and the channel read return. See `working.ts`. */ work?: ChannelWorkWire[]; + /** + * When `work` was read, by the database's clock: ISO-8601 in UTC to the microsecond, fixed width + * so two compare as strings. Present whenever `work` is. Events + * can arrive out of order across replicas, so a browser keeps the list with the latest stamp. + */ + workAt?: string; }; /** "The roster you hold may be wrong." Carries no delta, because what was lost is not recoverable. */ diff --git a/server/src/channels/routes.ts b/server/src/channels/routes.ts index dbc855c1b6..f1226d76c6 100644 --- a/server/src/channels/routes.ts +++ b/server/src/channels/routes.ts @@ -243,8 +243,12 @@ export type ChannelStore = { * turned into a relay. Every busy signal carries the same list, so this is only for those. */ signalWork(threadId: string): Promise; - /** What each of these channels is waiting on. See `workIn` in working.ts. */ + /** + * What each of these channels is waiting on, for this person: their own thread's hops and their + * own routines. See `workIn` in working.ts. + */ workIn( + actor: AgentActor, channels: readonly { id: string; threadId: string }[], ): Promise>; }; @@ -338,10 +342,18 @@ export function createChannelStore( * truth. Just the announcement, carrying the members the same way recordActivity does. The list * rides on every one of these so a client never merges a delta into what it holds: the newest * event is the whole answer, and the same answer a reload gets. + * + * THE LIST GOES ONLY TO THE PERSON WHOSE THREAD IT IS. Each member has a thread of their own in + * a channel, and what that thread waits on names the Bots its owner asked. The other members + * hear that a turn started or ended, as they always have, and nothing about whom it asked. + * + * `workAt` is the database's clock just before the list was read. Two announcements for one + * channel can be read on two replicas and delivered in either order, so the browser keeps the + * list with the latest `workAt` rather than the one that happened to arrive last. */ const announceWork = async ( channelId: string, - threadId: string | null, + thread: { threadId: string; userId: string } | null, busy?: boolean, ): Promise => { const members = await database @@ -350,23 +362,47 @@ export function createChannelStore( .where(eq(channelMemberships.channelId, channelId)); if (members.length === 0) return; - const work = threadId - ? ((await workIn(database, [{ id: channelId, threadId }])).get( - channelId, - ) ?? []) - : []; - const event: ChannelActivityEvent = { + const notify = (event: ChannelActivityEvent) => + database.execute( + sql`select pg_notify(${CHANNEL_ACTIVITY_TOPIC}, ${JSON.stringify(event)})`, + ); + const base = { channelId, - memberIds: members.map((member) => member.userId), lastMessage: null, lastMessageAt: null, lastMessageAgentId: null, ...(busy === undefined ? {} : { busy }), - work: work.map(channelWorkDto), }; - await database.execute( - sql`select pg_notify(${CHANNEL_ACTIVITY_TOPIC}, ${JSON.stringify(event)})`, - ); + + const owner = + thread && members.some((member) => member.userId === thread.userId) + ? thread + : null; + const others = members + .map((member) => member.userId) + .filter((userId) => userId !== owner?.userId); + + if (owner) { + // Fixed-width UTC to the microsecond, so two stamps compare as strings. + const [stamp] = await database.execute<{ at: string }>( + sql`select to_char(clock_timestamp() at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') as at`, + ); + const work = + ( + await workIn(database, [ + { id: channelId, threadId: owner.threadId, userId: owner.userId }, + ]) + ).get(channelId) ?? []; + await notify({ + ...base, + memberIds: [owner.userId], + work: work.map(channelWorkDto), + ...(stamp ? { workAt: stamp.at } : {}), + }); + } + // A settle with no turn moving has nothing to tell anybody but the thread's owner. + if (busy !== undefined && others.length > 0) + await notify({ ...base, memberIds: others }); }; const store: ChannelStore = { @@ -884,22 +920,32 @@ export function createChannelStore( // The channel this thread is shown in, if any. A scratch thread maps to nothing, so a hop // running there signals nowhere and the branch below returns without announcing. const [mapped] = await database - .select({ channelId: intelligenceChannelMappings.channelId }) + .select({ + channelId: intelligenceChannelMappings.channelId, + userId: intelligenceChannelMappings.userId, + }) .from(intelligenceChannelMappings) .where(eq(intelligenceChannelMappings.threadId, threadId)) .limit(1); if (!mapped) return; - await announceWork(mapped.channelId, threadId, busy); + await announceWork( + mapped.channelId, + { threadId, userId: mapped.userId }, + busy, + ); }, async signalWork(threadId) { const [mapped] = await database - .select({ channelId: intelligenceChannelMappings.channelId }) + .select({ + channelId: intelligenceChannelMappings.channelId, + userId: intelligenceChannelMappings.userId, + }) .from(intelligenceChannelMappings) .where(eq(intelligenceChannelMappings.threadId, threadId)) .limit(1); if (!mapped) return; - await announceWork(mapped.channelId, threadId); + await announceWork(mapped.channelId, { threadId, userId: mapped.userId }); }, async signalChannelBusy(actor, channelId, busy) { @@ -938,10 +984,20 @@ export function createChannelStore( // to a channel is not something an outsider can probe for. if (!membership) throw new ChannelNotFoundError(channelId); - await announceWork(channelId, membership.threadId, busy); + await announceWork( + channelId, + membership.threadId + ? { threadId: membership.threadId, userId: membership.userId } + : null, + busy, + ); }, - workIn: (list) => workIn(database, list), + workIn: (actor, list) => + workIn( + database, + list.map((channel) => ({ ...channel, userId: actor.id })), + ), }; return store; } @@ -1175,7 +1231,7 @@ export function createChannelRoutes( * flag that only ever arrived as an event was gone on every reload, so a conversation a Bot * was still answering in looked finished to anybody who refreshed. */ - const work = await store.workIn(page.channels); + const work = await store.workIn(context.var.actor, page.channels); return context.json({ channels: page.channels.map((channel) => channelSummaryDto(channel, work.get(channel.id) ?? []), @@ -1282,7 +1338,7 @@ export function createChannelRoutes( if (!channel) { return context.json({ error: "Couldn't find that conversation." }, 404); } - const work = await store.workIn([channel]); + const work = await store.workIn(context.var.actor, [channel]); return context.json({ channel: { ...channelDto(channel), diff --git a/server/src/channels/working.ts b/server/src/channels/working.ts index 8b74f02c0c..19e735f360 100644 --- a/server/src/channels/working.ts +++ b/server/src/channels/working.ts @@ -92,19 +92,35 @@ type HandoffPayload = { const text = (value: unknown): string | null => typeof value === "string" && value.length > 0 ? value : null; +/** A conversation as one person holds it: the channel, and that person's own thread in it. */ +export type ChannelThread = { + id: string; + /** The thread this person's Bot runs in. Hops are offered from it, so it scopes theirs. */ + threadId: string; + /** The person. Their routines fire into this thread; another member's do not. */ + userId: string; +}; + /** * What each of these conversations is waiting on, oldest first. A conversation waiting on nothing * is absent from the map. + * + * SCOPED TO THE PERSON, never to the channel. Every member of a channel has a thread of their own + * in it, and the hops and routines in that thread are theirs: another member reading the same + * channel must not learn which Bot somebody else asked, or which Bot their routine runs as. */ export async function workIn( database: Database, - channels: readonly { id: string; threadId: string }[], + channels: readonly ChannelThread[], ): Promise> { const found = new Map(); if (channels.length === 0) return found; const channelByThread = new Map( channels.map((channel) => [channel.threadId, channel.id]), ); + const ownerByChannel = new Map( + channels.map((channel) => [channel.id, channel.userId]), + ); const add = (channelId: string, work: ChannelWork) => { const list = found.get(channelId) ?? []; list.push(work); @@ -200,6 +216,7 @@ export async function workIn( runId: routineRuns.id, routineId: routines.id, channelId: routines.channelId, + ownerUserId: routines.ownerUserId, botId: routines.agentId, botName: agents.name, startedAt: routineRuns.startedAt, @@ -221,6 +238,8 @@ export async function workIn( ), ); for (const run of runs) { + // Somebody else's routine, firing into their own thread in a channel this person shares. + if (ownerByChannel.get(run.channelId) !== run.ownerUserId) continue; add(run.channelId, { kind: "routine", key: run.runId, diff --git a/server/tests/channel-work.integration.test.ts b/server/tests/channel-work.integration.test.ts index babc3db3b9..425dcf1589 100644 --- a/server/tests/channel-work.integration.test.ts +++ b/server/tests/channel-work.integration.test.ts @@ -15,6 +15,7 @@ import { createDatabase } from "../src/db/client"; import { agentProfiles, agents, + channelMemberships, channels, intelligenceChannelMappings, routineRuns, @@ -150,16 +151,61 @@ async function hop(input: { return key; } -async function workFor(channel: { id: string; threadId: string }) { - return (await store.workIn([channel])).get(channel.id) ?? []; +async function workFor( + owner: AgentActor, + channel: { id: string; threadId: string }, +) { + return (await store.workIn(owner, [channel])).get(channel.id) ?? []; +} + +/** A second person in the channel, with a thread of their own in it, as `group.ts` adds one. */ +async function addMember(channelId: string) { + const member = await createUser(); + const threadId = `${testPrefix}-thread-${randomUUID()}`; + await database + .insert(channelMemberships) + .values({ channelId, userId: member.id }); + await database + .insert(intelligenceChannelMappings) + .values({ userId: member.id, channelId, threadId }); + return { member, channel: { id: channelId, threadId } }; +} + +/** Hear what the activity socket would carry to each of these people. */ +async function listen(people: AgentActor[]) { + const heard = new Map( + people.map((person) => [person.id, []]), + ); + const hub = createChannelEventHub(); + const detachers = people.map((person) => + hub.register(person.id, (payload) => { + heard.get(person.id)?.push(JSON.parse(payload) as ChannelActivityEvent); + }), + ); + const listener = await startChannelActivityListener(databaseUrl, hub); + return { + heard, + async until( + count: (heard: Map) => boolean, + ) { + const deadline = Date.now() + 5_000; + while (!count(heard) && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 20)); + } + }, + async stop() { + for (const detach of detachers) detach(); + await listener.stop(); + }, + }; } describe("what a conversation is waiting on", () => { test("a Bot it asked, from the moment the hop is queued", async () => { - const { channel } = await setup(); + const { owner, channel } = await setup(); const key = await hop({ threadId: channel.threadId, toName: "Ana" }); - const work = await workFor(channel); + const work = await workFor(owner, channel); expect(work).toHaveLength(1); expect(work[0]).toMatchObject({ @@ -172,21 +218,21 @@ describe("what a conversation is waiting on", () => { }); test("nothing, once the hop is finished", async () => { - const { channel } = await setup(); + const { owner, channel } = await setup(); await hop({ threadId: channel.threadId, finished: true }); - expect(await workFor(channel)).toEqual([]); + expect(await workFor(owner, channel)).toEqual([]); }); test("one entry for a hop however many times it was tried", async () => { - const { channel } = await setup(); + const { owner, channel } = await setup(); await hop({ threadId: channel.threadId, attempts: 3 }); - expect(await workFor(channel)).toHaveLength(1); + expect(await workFor(owner, channel)).toHaveLength(1); }); test("two Bots asked at once are two entries, oldest first", async () => { - const { channel } = await setup(); + const { owner, channel } = await setup(); await hop({ threadId: channel.threadId, toBotId: "ben", @@ -198,13 +244,13 @@ describe("what a conversation is waiting on", () => { createdAt: new Date(Date.now() - 5_000), }); - const work = await workFor(channel); + const work = await workFor(owner, channel); expect(work.map((entry) => entry.botId)).toEqual(["ana", "ben"]); }); test("not a hop that was given up on, nor the notice saying so", async () => { - const { channel } = await setup(); + const { owner, channel } = await setup(); const key = await hop({ threadId: channel.threadId, attempts: 5 }); await hop({ threadId: channel.threadId, @@ -212,18 +258,18 @@ describe("what a conversation is waiting on", () => { answerIn: channel.threadId, }); - expect(await workFor(channel)).toEqual([]); + expect(await workFor(owner, channel)).toEqual([]); }); test("a given-up hop's last attempt, while it is still running", async () => { - const { channel } = await setup(); + const { owner, channel } = await setup(); await hop({ threadId: channel.threadId, attempts: 5, claimed: true }); - expect(await workFor(channel)).toHaveLength(1); + expect(await workFor(owner, channel)).toHaveLength(1); }); test("an answer on its way back is the same wait, with the same clock", async () => { - const { channel } = await setup(); + const { owner, channel } = await setup(); const askedAt = new Date(Date.now() - 30_000); const key = await hop({ threadId: channel.threadId, @@ -239,7 +285,7 @@ describe("what a conversation is waiting on", () => { answerIn: channel.threadId, }); - const work = await workFor(channel); + const work = await workFor(owner, channel); expect(work).toHaveLength(1); expect(work[0]).toMatchObject({ @@ -252,7 +298,7 @@ describe("what a conversation is waiting on", () => { }); test("not once the answer is being given", async () => { - const { channel } = await setup(); + const { owner, channel } = await setup(); const key = await hop({ threadId: channel.threadId, finished: true }); await hop({ threadId: channel.threadId, @@ -261,14 +307,14 @@ describe("what a conversation is waiting on", () => { claimed: true, }); - expect(await workFor(channel)).toEqual([]); + expect(await workFor(owner, channel)).toEqual([]); }); test("not a hop asked from a thread no conversation shows", async () => { - const { channel } = await setup(); + const { owner, channel } = await setup(); await hop({ threadId: `scratch-${testPrefix}` }); - expect(await workFor(channel)).toEqual([]); + expect(await workFor(owner, channel)).toEqual([]); }); test("a routine firing into it, until the run has an outcome", async () => { @@ -287,7 +333,7 @@ describe("what a conversation is waiting on", () => { const runId = `${testPrefix}-run`; await database.insert(routineRuns).values({ id: runId, routineId }); - const running = await workFor(channel); + const running = await workFor(owner, channel); expect(running).toHaveLength(1); expect(running[0]).toMatchObject({ kind: "routine", @@ -300,7 +346,30 @@ describe("what a conversation is waiting on", () => { .update(routineRuns) .set({ status: "succeeded", finishedAt: new Date() }) .where(eq(routineRuns.id, runId)); - expect(await workFor(channel)).toEqual([]); + expect(await workFor(owner, channel)).toEqual([]); + }); + test("only what this person's own thread and routines wait on, in a shared channel", async () => { + const { owner, asker, channel } = await setup(); + const { member, channel: theirs } = await addMember(channel.id); + await hop({ threadId: channel.threadId, toName: "Ana" }); + const routineId = `${testPrefix}-routine-shared`; + await database.insert(routines).values({ + id: routineId, + ownerUserId: owner.id, + agentId: asker, + channelId: channel.id, + instruction: "Post the standup summary.", + cron: "0 9 * * *", + nextRunAt: new Date(Date.now() + 86_400_000), + }); + createdRoutineIds.push(routineId); + await database + .insert(routineRuns) + .values({ id: `${testPrefix}-run-shared`, routineId }); + + expect(await workFor(owner, channel)).toHaveLength(2); + // The other member's read names neither the Bot the owner asked nor the owner's routine. + expect(await workFor(member, theirs)).toEqual([]); }); }); @@ -350,4 +419,39 @@ describe("announcing it", () => { await listener.stop(); } }); + + test("another member hears that a turn moved, and nothing about whom it asked", async () => { + const { owner, channel } = await setup(); + const { member } = await addMember(channel.id); + const socket = await listen([owner, member]); + try { + await hop({ threadId: channel.threadId, toName: "Ana" }); + await store.signalBusy(channel.threadId, true); + await store.signalWork(channel.threadId); + await socket.until( + (heard) => + (heard.get(owner.id)?.length ?? 0) >= 2 && + (heard.get(member.id)?.length ?? 0) >= 1, + ); + + const ownerHeard = socket.heard.get(owner.id) ?? []; + const memberHeard = socket.heard.get(member.id) ?? []; + expect(ownerHeard).toHaveLength(2); + expect(ownerHeard[0]?.work).toHaveLength(1); + expect(ownerHeard[0]?.workAt).toMatch(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/); + // Stamped in the order they were read, so a browser can drop the older of two. + expect( + (ownerHeard[0]?.workAt ?? "") < (ownerHeard[1]?.workAt ?? ""), + ).toBe(true); + // The settle is the owner's alone; the busy signal reaches the member without the list. + expect(memberHeard).toHaveLength(1); + expect(memberHeard[0]).toMatchObject({ + channelId: channel.id, + busy: true, + }); + expect(memberHeard[0]?.work).toBeUndefined(); + } finally { + await socket.stop(); + } + }); }); From 758de811886a759ae1a3fcbd57489e46993af912 Mon Sep 17 00:00:00 2001 From: Guido Vizoso Date: Mon, 26 Oct 2026 13:03:35 -0300 Subject: [PATCH 08/13] Ignore a work list read before the one a conversation already holds Busy events are read on whichever replica sent them and can arrive out of order. The roster and the open conversation now keep the list with the latest stamp, so a late event no longer puts back a hop that has already settled. --- app/src/lib/channels/queries.ts | 5 ++ app/src/lib/channels/use-channel-events.ts | 34 ++++++++++-- app/tests/channel-event-patch.test.ts | 60 +++++++++++++++++++++- 3 files changed, 95 insertions(+), 4 deletions(-) diff --git a/app/src/lib/channels/queries.ts b/app/src/lib/channels/queries.ts index b7e2b9ee15..a7a32fc88f 100644 --- a/app/src/lib/channels/queries.ts +++ b/app/src/lib/channels/queries.ts @@ -28,6 +28,11 @@ export type AgentChannel = { * the socket last said. Absent from a server that predates it, which reads as nothing. */ work?: ChannelWork[]; + /** + * When the server read `work`, from the socket event that last set it. Absent on a fetched read. + * Kept so an older event arriving late does not put back a list that has moved on. + */ + workAt?: string; }; /** diff --git a/app/src/lib/channels/use-channel-events.ts b/app/src/lib/channels/use-channel-events.ts index 8b3a07819a..9e0d4d79c3 100644 --- a/app/src/lib/channels/use-channel-events.ts +++ b/app/src/lib/channels/use-channel-events.ts @@ -102,6 +102,12 @@ export type ChannelActivityEvent = { * settles without a turn starting or ending. */ work?: ChannelWork[]; + /** + * When the server read `work`, as fixed-width UTC that compares as a string. Two events can + * arrive in the other order from the one they were read in, so a list older than the one already + * held is ignored. See `newerWork`. + */ + workAt?: string; }; /** The infinite query's cache, which holds pages rather than one array. */ @@ -185,7 +191,8 @@ export function applyChannelEvent( */ if (activity.busy !== undefined || activity.work !== undefined) { const busy = activity.busy ?? previous.busy; - const work = activity.work ?? previous.work; + const fresh = newerWork(previous, activity); + const work = fresh ? activity.work : previous.work; if ( (previous.busy ?? false) === (busy ?? false) && sameWork(previous.work, work) @@ -196,6 +203,7 @@ export function applyChannelEvent( ...previous, ...(busy === undefined ? {} : { busy }), ...(work === undefined ? {} : { work }), + ...(fresh && activity.workAt ? { workAt: activity.workAt } : {}), }; const pages = data.pages.slice(); pages[holdingPage] = { ...page, channels }; @@ -237,6 +245,22 @@ function sameWork( ); } +/** + * Whether this event's work list should replace the one held. + * + * Only when it carries one, and was not read before the one already held. A list from a fetch has + * no stamp and gives way to any event; an event without a stamp, from an older server, always + * wins, as every event did before stamps. + */ +function newerWork( + held: { workAt?: string }, + activity: ChannelActivityEvent, +): boolean { + if (activity.work === undefined) return false; + if (!held.workAt || !activity.workAt) return true; + return activity.workAt >= held.workAt; +} + /** * Apply one event's work list to the open conversation's own read. * @@ -248,9 +272,13 @@ export function applyChannelWork( channel: AgentChannel | undefined, activity: ChannelActivityEvent, ): AgentChannel | undefined { - if (!channel || activity.work === undefined) return channel; + if (!channel || !newerWork(channel, activity)) return channel; if (sameWork(channel.work, activity.work)) return channel; - return { ...channel, work: activity.work }; + return { + ...channel, + work: activity.work, + ...(activity.workAt ? { workAt: activity.workAt } : {}), + }; } const FIRST_RETRY_MS = 500; diff --git a/app/tests/channel-event-patch.test.ts b/app/tests/channel-event-patch.test.ts index 93c7ae2bc4..7907406934 100644 --- a/app/tests/channel-event-patch.test.ts +++ b/app/tests/channel-event-patch.test.ts @@ -1,7 +1,12 @@ import { describe, expect, test } from "bun:test"; -import type { ChannelPage, ChannelSummary } from "../src/lib/channels/queries"; +import type { + AgentChannel, + ChannelPage, + ChannelSummary, +} from "../src/lib/channels/queries"; import { applyChannelEvent, + applyChannelWork, type ChannelActivityEvent, } from "../src/lib/channels/use-channel-events"; @@ -285,6 +290,59 @@ describe("a work list", () => { ), ).toBe(data); }); + + test("an older list arriving late does not undo a newer one", () => { + const settled = applyChannelEvent( + cache([channel("a", { busy: true, work })]), + event({ + channelId: "a", + work: [], + workAt: "2026-10-26T10:00:02.000000Z", + }), + ); + if (settled === "unknown") throw new Error("expected a patch"); + + const late = applyChannelEvent( + settled, + event({ + channelId: "a", + busy: false, + work, + workAt: "2026-10-26T10:00:01.000000Z", + }), + ); + + if (late === "unknown") throw new Error("expected a patch"); + // The busy flag still moves; the list read before the settle is ignored. + expect(late.pages[0]?.channels[0]?.busy).toBe(false); + expect(late.pages[0]?.channels[0]?.work).toEqual([]); + }); + + test("the open conversation's read ignores an older list too", () => { + const held: AgentChannel = { + id: "a", + name: "A", + agentIds: [], + threadId: "thread-a", + active: true, + lastMessageAt: null, + work: [], + workAt: "2026-10-26T10:00:02.000000Z", + }; + + expect( + applyChannelWork( + held, + event({ channelId: "a", work, workAt: "2026-10-26T10:00:01.000000Z" }), + ), + ).toBe(held); + expect( + applyChannelWork( + held, + event({ channelId: "a", work, workAt: "2026-10-26T10:00:03.000000Z" }), + )?.work, + ).toEqual(work); + }); }); /** From 7444d7948e8543947569fe88afb45f457e15d1a1 Mon Sep 17 00:00:00 2001 From: Guido Vizoso Date: Mon, 26 Oct 2026 13:04:28 -0300 Subject: [PATCH 09/13] Keep the assertion-shape guard green across the work-list tests The branch moved four allow-listed offences down their files, and its own tests added four new ones: an absent busy flag and work list asserted through a ?. chain, and an expected undefined key in a toEqual. The moved entries are re-keyed; the new ones assert the missing key directly instead. --- server/tests/assertion-shape.test.ts | 16 ++++++++++++---- server/tests/channel-routes.test.ts | 3 ++- server/tests/channel-work.integration.test.ts | 5 +++-- server/tests/routine-runner.test.ts | 4 +--- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/server/tests/assertion-shape.test.ts b/server/tests/assertion-shape.test.ts index 83f13618c2..8aaedeeff2 100644 --- a/server/tests/assertion-shape.test.ts +++ b/server/tests/assertion-shape.test.ts @@ -625,7 +625,9 @@ const ALLOW: Allowed[] = [ }, { file: "server/tests/agent-handoff.test.ts", - line: 366, + // Moved from 366 by the work-list change, which added tests above it; the offence is the same one and + // is re-keyed rather than re-found. + line: 409, shape: "b", finding: "NEW (round 2 did not find this)", why: "expect(plain.events[0]?.initiator).toBe(undefined);", @@ -814,7 +816,9 @@ const ALLOW: Allowed[] = [ }, { file: "server/tests/agent-handoff.test.ts", - line: 620, + // Moved from 620 by the work-list change, which added tests above it; the offence is the same one and + // is re-keyed rather than re-found. + line: 663, shape: "c", finding: "NEW (round 2 did not find this)", why: ').toEqual([ { eventType: "agent.handoff_offered", reason: undefined }, { eventType: "agent.handoff_refused", r', @@ -869,14 +873,18 @@ const ALLOW: Allowed[] = [ }, { file: "server/tests/routine-runner.test.ts", - line: 169, + // Moved from 169 by the work-list change, which added tests above it; the offence is the same one and + // is re-keyed rather than re-found. + line: 185, shape: "c", finding: "NEW (round 2 did not find this)", why: 'expect(recorded.finished).toEqual([ { runId: RUN_ID, status: "succeeded", error: undefined }, ]);', }, { file: "server/tests/routine-runner.test.ts", - line: 272, + // Moved from 272 by the work-list change, which added tests above it; the offence is the same one and + // is re-keyed rather than re-found. + line: 289, shape: "c", finding: "NEW (round 2 did not find this)", why: 'expect(recorded.finished).toEqual([ { runId: RUN_ID, status: "succeeded", error: undefined }, ]);', diff --git a/server/tests/channel-routes.test.ts b/server/tests/channel-routes.test.ts index 18bbd9cc85..2109e1ac3a 100644 --- a/server/tests/channel-routes.test.ts +++ b/server/tests/channel-routes.test.ts @@ -232,7 +232,8 @@ describe("what a channel is waiting on", () => { expect(body.channels[0]).toMatchObject({ id: "channel-1", busy: true }); expect(body.channels[0]?.work).toHaveLength(1); // Absent rather than false: a person's own run in another tab is not on any row this reads. - expect(body.channels[1]?.busy).toBeUndefined(); + expect(body.channels[1]).toBeDefined(); + expect(body.channels[1]).not.toHaveProperty("busy"); expect(body.channels[1]?.work).toEqual([]); }); }); diff --git a/server/tests/channel-work.integration.test.ts b/server/tests/channel-work.integration.test.ts index 425dcf1589..5e2a721fc1 100644 --- a/server/tests/channel-work.integration.test.ts +++ b/server/tests/channel-work.integration.test.ts @@ -413,7 +413,8 @@ describe("announcing it", () => { ]); // A settle says what is left without claiming a turn started or ended. expect(heard[1]).toMatchObject({ channelId: channel.id, work: [] }); - expect(heard[1]?.busy).toBeUndefined(); + expect(heard[1]).toBeDefined(); + expect(heard[1]).not.toHaveProperty("busy"); } finally { detach(); await listener.stop(); @@ -449,7 +450,7 @@ describe("announcing it", () => { channelId: channel.id, busy: true, }); - expect(memberHeard[0]?.work).toBeUndefined(); + expect(memberHeard[0]).not.toHaveProperty("work"); } finally { await socket.stop(); } diff --git a/server/tests/routine-runner.test.ts b/server/tests/routine-runner.test.ts index 91cf8bd954..5e01693070 100644 --- a/server/tests/routine-runner.test.ts +++ b/server/tests/routine-runner.test.ts @@ -393,9 +393,7 @@ describe("createRoutineRunner", () => { await runner.run(RUN_ID); expect(recorded.turns).toHaveLength(1); - expect(recorded.finished).toEqual([ - { runId: RUN_ID, status: "succeeded", error: undefined }, - ]); + expect(recorded.finished).toEqual([{ runId: RUN_ID, status: "succeeded" }]); }); test("signals nothing for a firing that never runs", async () => { From b98019221f4a4fc5d1ace661b9ca4d81b2cd1a42 Mon Sep 17 00:00:00 2001 From: Guido Vizoso Date: Mon, 26 Oct 2026 13:23:06 -0300 Subject: [PATCH 10/13] Add a changelog line for the handoff progress row --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85bcf0b573..67cd8e16a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,14 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged. - Migration `0062_action_policy_labels` adds a `labels` column to `action_policies`, defaulting to empty. Existing boundary rules keep working and show as "Custom rule" until somebody names them. +### A conversation shows who it is waiting on + +When a Bot asks another Bot, the end of the conversation now reads "Asking Risk Analyst… 0:13", with +the clock running, until the answer comes back; a routine firing into it reads "Market Scout is +running a routine…". The sidebar keeps the conversation lit while it waits, and both survive a +reload. In a shared conversation only the person whose Bot asked sees whom it asked; the others see +only that it is busy. Migration `0063_work_items_open_thread` adds one partial index to `work_items`. + ### Screens share one spacing, row and empty-state design Every configuration screen now takes its spacing from the page frame: 32px under the title, 48px From 8fd3ff4ad7e3507dc1251ec7c1947eb612287885 Mon Sep 17 00:00:00 2001 From: Guido Vizoso Date: Mon, 26 Oct 2026 15:25:38 -0300 Subject: [PATCH 11/13] End a hop at once when the other Bot's address is one this deployment will not dial --- server/src/agents/handoff-runner.ts | 10 ++++++--- server/tests/agent-handoff-runner.test.ts | 26 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/server/src/agents/handoff-runner.ts b/server/src/agents/handoff-runner.ts index 44add93283..9da2c30eee 100644 --- a/server/src/agents/handoff-runner.ts +++ b/server/src/agents/handoff-runner.ts @@ -473,10 +473,14 @@ export function createHandoffRunner(options: { * this hop. A person who was told their question had been handed on, and then hears * nothing for ever, has no way to tell a slow Bot from a broken one. */ - // The deployment's controls refused it (Use Bots, the model allowlist). Trying again in a - // minute gets the same answer, so this is the last try, whatever the count says. + // The deployment's controls refused it (Use Bots, the model allowlist), or the other + // Bot's address is one this deployment will not dial. Trying again in a minute gets the + // same answer, so this is the last try, whatever the count says. Retrying a refused + // address left the person watching "Asking …" for five minutes before being told. const refused = - error instanceof Error && error.name === "CapabilityRefusedError"; + error instanceof Error && + (error.name === "CapabilityRefusedError" || + error.name === "EndpointNotAllowedError"); if ((refused || item.attempts >= maxAttempts) && !work.answerIn) { await tell(work, item.key, reason).catch((failure) => { // A notice that cannot be queued must not take the release with it: leaving the row diff --git a/server/tests/agent-handoff-runner.test.ts b/server/tests/agent-handoff-runner.test.ts index b3bda1f248..d42fdd559e 100644 --- a/server/tests/agent-handoff-runner.test.ts +++ b/server/tests/agent-handoff-runner.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; import { readRunAssertion } from "../src/agents/callback-token"; +import { EndpointNotAllowedError } from "../src/agents/endpoint"; import { ThreadBusyError } from "../src/agents/handoff-delivery"; import { createHandoffRunner, @@ -411,6 +412,31 @@ describe("a hop that failed for good", () => { expect(offered[0]?.task).toContain("Use Bots is turned off"); }); + test("a hop to an address this deployment will not dial ends on its first try", async () => { + const { + runner: sweeper, + offered, + calls, + } = runner({ + deliver: async () => { + throw new EndpointNotAllowedError( + "This deployment will not dial http://localhost:4201/ag-ui: that address is inside OpenBot's own network, so an agent may not live there.", + ); + }, + }); + + await sweeper.sweep(); + + expect(calls.filter((call) => call.verb === "release")).toEqual([]); + expect(calls).toContainEqual({ + verb: "finish", + key: "run-1:abc", + owner: "replica-a", + }); + expect(offered).toHaveLength(1); + expect(offered[0]?.task).toContain("will not dial"); + }); + /* * The fan-out cap counts every row whose key starts with the run's own prefix. A notice is not one * of the Bots this run asked for, and a run long enough to see a hop fail for good is exactly the From a0070bb017f5865333f483b0298b40ae3f51e91f Mon Sep 17 00:00:00 2001 From: Guido Vizoso Date: Mon, 26 Oct 2026 15:36:21 -0300 Subject: [PATCH 12/13] Recognise a refused address by its message, since the AG-UI client keeps only that --- server/src/agents/endpoint.ts | 20 +++++++++++++++++++- server/src/agents/handoff-runner.ts | 7 ++++--- server/tests/agent-handoff-runner.test.ts | 5 +++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/server/src/agents/endpoint.ts b/server/src/agents/endpoint.ts index 52074f657e..133c00759e 100644 --- a/server/src/agents/endpoint.ts +++ b/server/src/agents/endpoint.ts @@ -129,6 +129,24 @@ export class EndpointNotAllowedError extends Error { } } +/** How a refusal of the stored address begins, whoever ends up holding its message. */ +const NOT_DIALLED = "This deployment will not dial "; + +/** + * Whether a run failed because its stored address is one this deployment will not dial. + * + * By message as well as by type, because the AG-UI client turns a fetch that throws into a RUN_ERROR + * event carrying only the message, so the error a caller finally holds is a plain `Error`. Only the + * stored address counts: a redirect can stop redirecting, and that address cannot change on its own. + */ +export function isEndpointRefusal(error: unknown): boolean { + return ( + error instanceof Error && + (error.name === "EndpointNotAllowedError" || + error.message.startsWith(NOT_DIALLED)) + ); +} + const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]); /** @@ -282,7 +300,7 @@ export function createAgentFetch( if (!stored.allowed) { throw refuse( url, - `This deployment will not dial ${url}: ${stored.reason.charAt(0).toLowerCase()}${stored.reason.slice(1)}`, + `${NOT_DIALLED}${url}: ${stored.reason.charAt(0).toLowerCase()}${stored.reason.slice(1)}`, ); } diff --git a/server/src/agents/handoff-runner.ts b/server/src/agents/handoff-runner.ts index 9da2c30eee..db052a2ac1 100644 --- a/server/src/agents/handoff-runner.ts +++ b/server/src/agents/handoff-runner.ts @@ -17,6 +17,7 @@ import { } from "../audit"; import { cutAtCodeUnits } from "../channels/text"; import { DEFAULT_MAX_ATTEMPTS, type WorkQueue } from "../work/queue"; +import { isEndpointRefusal } from "./endpoint"; import { HANDOFF_KIND } from "./handoff"; import { ThreadBusyError } from "./handoff-delivery"; import { BOT_PAUSED_REASON, isBotPaused } from "./lifecycle"; @@ -478,9 +479,9 @@ export function createHandoffRunner(options: { // same answer, so this is the last try, whatever the count says. Retrying a refused // address left the person watching "Asking …" for five minutes before being told. const refused = - error instanceof Error && - (error.name === "CapabilityRefusedError" || - error.name === "EndpointNotAllowedError"); + (error instanceof Error && + error.name === "CapabilityRefusedError") || + isEndpointRefusal(error); if ((refused || item.attempts >= maxAttempts) && !work.answerIn) { await tell(work, item.key, reason).catch((failure) => { // A notice that cannot be queued must not take the release with it: leaving the row diff --git a/server/tests/agent-handoff-runner.test.ts b/server/tests/agent-handoff-runner.test.ts index d42fdd559e..01972b5dba 100644 --- a/server/tests/agent-handoff-runner.test.ts +++ b/server/tests/agent-handoff-runner.test.ts @@ -1,6 +1,5 @@ import { describe, expect, test } from "bun:test"; import { readRunAssertion } from "../src/agents/callback-token"; -import { EndpointNotAllowedError } from "../src/agents/endpoint"; import { ThreadBusyError } from "../src/agents/handoff-delivery"; import { createHandoffRunner, @@ -418,8 +417,10 @@ describe("a hop that failed for good", () => { offered, calls, } = runner({ + // As it arrives: the AG-UI client rebuilds the refusal from its RUN_ERROR, so only the + // message survives. deliver: async () => { - throw new EndpointNotAllowedError( + throw new Error( "This deployment will not dial http://localhost:4201/ag-ui: that address is inside OpenBot's own network, so an agent may not live there.", ); }, From 8ad807e2d774d62c115dc359a8b3cca40c39fcda Mon Sep 17 00:00:00 2001 From: Guido Vizoso Date: Mon, 26 Oct 2026 16:19:10 -0300 Subject: [PATCH 13/13] Show the asked Bot's avatar on the waiting row, and drop the clock --- CHANGELOG.md | 7 +- app/src/components/channels/channel-chat.tsx | 4 +- app/src/components/channels/channel-work.tsx | 36 +++++--- app/src/components/channels/progress-row.tsx | 45 ++-------- app/tests/channel-work-row.test.tsx | 90 +++++++------------- 5 files changed, 70 insertions(+), 112 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67cd8e16a0..1d452d0975 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,9 +42,10 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged. ### A conversation shows who it is waiting on -When a Bot asks another Bot, the end of the conversation now reads "Asking Risk Analyst… 0:13", with -the clock running, until the answer comes back; a routine firing into it reads "Market Scout is -running a routine…". The sidebar keeps the conversation lit while it waits, and both survive a +When a Bot asks another Bot, the end of the conversation now shows that Bot's avatar and "Asking +Risk Analyst…" until the answer comes back; a routine firing into it reads "Market Scout is running a +routine…". A Bot whose address this deployment will not dial is reported at once, not after five +minutes of retries. The sidebar keeps the conversation lit while it waits, and both survive a reload. In a shared conversation only the person whose Bot asked sees whom it asked; the others see only that it is busy. Migration `0063_work_items_open_thread` adds one partial index to `work_items`. diff --git a/app/src/components/channels/channel-chat.tsx b/app/src/components/channels/channel-chat.tsx index 74d67cd8d7..14a91aa4dc 100644 --- a/app/src/components/channels/channel-chat.tsx +++ b/app/src/components/channels/channel-chat.tsx @@ -1092,8 +1092,8 @@ export function ChannelChat({ working={ agent.isRunning || turnsInFlight > 0 ? null : ( - agentProfiles?.find((profile) => profile.id === botId)?.name + botOf={(botId) => + agentProfiles?.find((profile) => profile.id === botId) } work={channel.work} /> diff --git a/app/src/components/channels/channel-work.tsx b/app/src/components/channels/channel-work.tsx index 43c32464fd..c78531cf81 100644 --- a/app/src/components/channels/channel-work.tsx +++ b/app/src/components/channels/channel-work.tsx @@ -1,6 +1,10 @@ +import { AbstractAvatar } from "@/components/agents/abstract-avatar"; import type { ChannelWork } from "@/lib/channels/queries"; import { ProgressRow } from "./progress-row"; +/** A Bot as this person knows it: its name and the seed its avatar is drawn from. */ +export type KnownBot = { name: string; avatarSeed: string }; + /** * What a conversation is waiting on, one row each, at the end of the transcript. * @@ -14,22 +18,34 @@ import { ProgressRow } from "./progress-row"; */ export function ChannelWorkRows({ work, - nameOf, + botOf, }: { work: readonly ChannelWork[] | undefined; - /** The name this person knows a Bot by, when they know it at all. */ - nameOf?: (botId: string) => string | undefined; + /** The Bot this person knows by that id, when they know it at all. */ + botOf?: (botId: string) => KnownBot | undefined; }) { if (!work || work.length === 0) return null; return (
- {work.map((entry) => ( - - ))} + {work.map((entry) => { + const known = botOf?.(entry.botId); + const name = known?.name ?? entry.botName; + return ( + + } + /> + ); + })}
); } diff --git a/app/src/components/channels/progress-row.tsx b/app/src/components/channels/progress-row.tsx index 3160ebfac5..e63c4a2323 100644 --- a/app/src/components/channels/progress-row.tsx +++ b/app/src/components/channels/progress-row.tsx @@ -1,60 +1,33 @@ -import { useEffect, useState } from "react"; +import type { ReactNode } from "react"; /** - * One line saying something is under way, and for how long. + * One line saying something is under way, with who is doing it. * * For a wait the person did not start and cannot watch: a Bot asked by their Bot, a routine firing, * a sign-in or an approval somebody else has to finish. Drawn where the result will appear, so the - * person looks at the right spot, and with a running clock, because "still working" and "stuck" - * look the same without one. + * person looks at the right spot. * * It borrows the shimmer a running tool line uses, so every kind of "working on it" in the * transcript reads the same. */ export function ProgressRow({ label, - since, + media, }: { /** What is under way, as a person would say it: "Asking Ana…". */ label: string; - /** When it started. ISO-8601, or epoch milliseconds. */ - since: string | number; + /** Who is doing it, drawn before the words: a Bot's avatar. */ + media?: ReactNode; }) { - const elapsed = useElapsed(since); return (

+ {media ? : null} {label} -

); } - -/** "0:12", "4:05", "1:02:07". Never negative: a server clock a little ahead reads as zero. */ -export function formatElapsed(milliseconds: number): string { - const total = Math.max(0, Math.floor(milliseconds / 1000)); - const hours = Math.floor(total / 3600); - const minutes = Math.floor((total % 3600) / 60); - const seconds = String(total % 60).padStart(2, "0"); - return hours > 0 - ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}` - : `${minutes}:${seconds}`; -} - -/** The time since `since`, as text, moving once a second while it is on screen. */ -function useElapsed(since: string | number): string { - const start = typeof since === "number" ? since : Date.parse(since); - const [now, setNow] = useState(() => Date.now()); - useEffect(() => { - const timer = window.setInterval(() => setNow(Date.now()), 1000); - return () => window.clearInterval(timer); - }, []); - return formatElapsed(Number.isNaN(start) ? 0 : now - start); -} diff --git a/app/tests/channel-work-row.test.tsx b/app/tests/channel-work-row.test.tsx index 9ef09032e2..a37164c9f9 100644 --- a/app/tests/channel-work-row.test.tsx +++ b/app/tests/channel-work-row.test.tsx @@ -1,12 +1,4 @@ -import { - afterAll, - afterEach, - beforeAll, - expect, - jest, - setSystemTime, - test, -} from "bun:test"; +import { afterAll, afterEach, beforeAll, expect, test } from "bun:test"; import { GlobalRegistrator } from "@happy-dom/global-registrator"; import { QueryClient, @@ -15,7 +7,6 @@ import { } from "@tanstack/react-query"; import { act, cleanup, render, waitFor } from "@testing-library/react"; import { ChannelWorkRows } from "@/components/channels/channel-work"; -import { formatElapsed } from "@/components/channels/progress-row"; import { type AgentChannel, type ChannelWork, @@ -26,17 +17,15 @@ import { applyChannelWork } from "@/lib/channels/use-channel-events"; import { settleReactWork } from "./settle-react-work"; /** - * The row a conversation shows while it waits on work nobody's browser is running: "Asking Ana… - * 0:12" for a Bot its Bot asked, and the same for a routine. Driven by the server's list, so it - * has to appear from a fetched channel as well as from a socket event, tick while it is up, and go - * when the list empties. + * The row a conversation shows while it waits on work nobody's browser is running: the Bot's avatar + * and "Asking Ana…" for a Bot its Bot asked, and the same for a routine. Driven by the server's + * list, so it has to appear from a fetched channel as well as from a socket event, and go when the + * list empties. */ beforeAll(() => GlobalRegistrator.register()); afterEach(() => { cleanup(); - jest.useRealTimers(); - setSystemTime(); }); afterAll(async () => { await settleReactWork(); @@ -48,8 +37,6 @@ afterEach(() => { global.fetch = originalFetch; }); -const NOW = new Date("2026-10-26T10:00:12.000Z"); - const asking = (overrides: Partial = {}): ChannelWork => ({ kind: "handoff", @@ -60,49 +47,41 @@ const asking = (overrides: Partial = {}): ChannelWork => ...overrides, }) as ChannelWork; -test("says who was asked and for how long", () => { - setSystemTime(NOW); - +test("says who was asked, with their avatar and no clock", () => { const view = render(); const row = view.getByTestId("progress-row"); - expect(row.textContent).toBe("Asking Ana…0:12"); - // Announced as progress, and only by its words: the clock is hidden from a screen reader. + expect(row.textContent).toBe("Asking Ana…"); expect(row.getAttribute("role")).toBe("status"); - expect(row.querySelector("[aria-hidden='true']")?.textContent).toBe("0:12"); + // The avatar is decoration beside a label that already names the Bot, so it is announced once. + const avatar = row.querySelector("[aria-hidden='true'] [role='img']"); + expect(avatar?.getAttribute("aria-label")).toBe("Ana"); + expect(row.querySelector("svg")).not.toBeNull(); }); -test("the clock moves while the row is up", () => { - jest.useFakeTimers(); - setSystemTime(NOW); - const view = render(); - - // The fake clock moves `Date.now()` with the timers, as a real second would. - act(() => { - jest.advanceTimersByTime(3_000); - }); - - expect(view.getByTestId("progress-row").textContent).toBe("Asking Ana…0:15"); -}); - -test("prefers the name the person knows the Bot by", () => { - setSystemTime(NOW); - +test("prefers the name and avatar the person knows the Bot by", () => { const view = render( (botId === "ana" ? "Ana Research" : undefined)} + botOf={(botId) => + botId === "ana" + ? { name: "Ana Research", avatarSeed: "seed-ana" } + : undefined + } work={[asking()]} />, ); + const known = view.getByTestId("progress-row").innerHTML; + cleanup(); + const unknown = render().getByTestId( + "progress-row", + ).innerHTML; - expect(view.getByTestId("progress-row").textContent).toContain( - "Asking Ana Research…", - ); + expect(known).toContain("Asking Ana Research…"); + // Drawn from the Bot's own seed, so it is the same face the Bot has everywhere else. + expect(known.replace("Ana Research", "Ana")).not.toBe(unknown); }); test("one row for each Bot asked, and one for a routine", () => { - setSystemTime(NOW); - const view = render( { expect( view.getAllByTestId("progress-row").map((row) => row.textContent), ).toEqual([ - "Asking Ana…0:12", - "Asking another Bot…0:12", - "Assistant is running a routine…1:12", + "Asking Ana…", + "Asking another Bot…", + "Assistant is running a routine…", ]); }); test("goes when the list empties", () => { - setSystemTime(NOW); const view = render(); view.rerender(); @@ -144,7 +122,6 @@ test("goes when the list empties", () => { * conversation a Bot is still answering in looks finished to anybody who refreshed. */ test("comes back from the channel read after a reload", async () => { - setSystemTime(NOW); global.fetch = (async (input: RequestInfo | URL) => { if (String(input) === "/api/channels/channel-1") { return Response.json({ @@ -176,9 +153,7 @@ test("comes back from the channel read after a reload", async () => { ); await waitFor(() => { - expect(view.getByTestId("progress-row").textContent).toBe( - "Asking Ana…0:12", - ); + expect(view.getByTestId("progress-row").textContent).toBe("Asking Ana…"); }); // The socket then says the answer is being given, and the row goes. @@ -231,10 +206,3 @@ test("an event with no list leaves the conversation's list alone", () => { }), ).toBe(channel); }); - -test("the clock reads minutes, then hours, and never runs backwards", () => { - expect(formatElapsed(12_000)).toBe("0:12"); - expect(formatElapsed(245_000)).toBe("4:05"); - expect(formatElapsed(3_727_000)).toBe("1:02:07"); - expect(formatElapsed(-4_000)).toBe("0:00"); -});