From 5967e7e06d4b7a8e709fca80b923ae1066ebe0c3 Mon Sep 17 00:00:00 2001
From: Roomote
Date: Wed, 26 Aug 2026 16:40:05 +0000
Subject: [PATCH 01/26] feat(db): add unified session schema foundation
---
.../src/__tests__/schema-constraints.test.ts | 136 +++++++++
packages/db/src/fixtures/factories/index.ts | 1 +
.../src/fixtures/factories/session.factory.ts | 36 +++
.../db/src/lib/__tests__/sessions.test.ts | 248 ++++++++++++++++
packages/db/src/lib/sessions.ts | 269 ++++++++++++++++++
packages/db/src/schema.ts | 201 +++++++++++++
packages/db/src/server.ts | 12 +
packages/db/src/types.ts | 25 ++
8 files changed, 928 insertions(+)
create mode 100644 packages/db/src/fixtures/factories/session.factory.ts
create mode 100644 packages/db/src/lib/__tests__/sessions.test.ts
create mode 100644 packages/db/src/lib/sessions.ts
diff --git a/packages/db/src/__tests__/schema-constraints.test.ts b/packages/db/src/__tests__/schema-constraints.test.ts
index 088202a4b..630c51dd6 100644
--- a/packages/db/src/__tests__/schema-constraints.test.ts
+++ b/packages/db/src/__tests__/schema-constraints.test.ts
@@ -28,10 +28,15 @@ import {
import {
db,
+ fastAgentConversations,
inArray,
repositories,
repositoryFactory,
runFactory,
+ sessionFactory,
+ sessionParticipants,
+ sessions,
+ sessionTasks,
taskFactory,
tasks,
userFactory,
@@ -43,6 +48,7 @@ const createdTaskIds: string[] = [];
const createdUserIds: string[] = [];
const createdRepositoryIds: string[] = [];
const createdWebhookIds: string[] = [];
+const createdSessionIds: string[] = [];
afterAll(async () => {
if (createdWebhookIds.length > 0) {
@@ -55,6 +61,10 @@ afterAll(async () => {
.where(inArray(repositories.id, createdRepositoryIds));
}
+ if (createdSessionIds.length > 0) {
+ await db.delete(sessions).where(inArray(sessions.id, createdSessionIds));
+ }
+
if (createdTaskIds.length > 0) {
// task_runs cascade from tasks.
await db.delete(tasks).where(inArray(tasks.id, createdTaskIds));
@@ -71,6 +81,14 @@ async function createTask(overrides: Parameters[0]) {
return task;
}
+async function createSession(
+ overrides: Parameters[0],
+) {
+ const session = await sessionFactory.create(overrides);
+ createdSessionIds.push(session.id);
+ return session;
+}
+
/**
* Asserts the promise rejects with a Postgres violation of the named
* constraint. Drizzle wraps driver errors in DrizzleQueryError, so the
@@ -167,6 +185,124 @@ describe('tasks classification CHECK constraints', () => {
);
});
+describe('sessions CHECK and uniqueness constraints', () => {
+ it.each([
+ ['ownerKind', 'sessions_owner_kind_check'],
+ ['sourceSurface', 'sessions_source_surface_check'],
+ ['sourceTrigger', 'sessions_source_trigger_check'],
+ ['visibility', 'sessions_visibility_check'],
+ ['cachedStatus', 'sessions_cached_status_check'],
+ ] as const)(
+ 'rejects an unknown %s value via %s',
+ async (field, constraintName) => {
+ const overrides = {
+ [field]: 'not-a-real-vocabulary-value',
+ } as unknown as Parameters[0];
+
+ await expectConstraintViolation(createSession(overrides), constraintName);
+ },
+ );
+
+ it('enforces the owner shape', async () => {
+ const user = await userFactory.create();
+ createdUserIds.push(user.id);
+
+ await expectConstraintViolation(
+ createSession({ ownerKind: 'system', ownerUserId: user.id }),
+ 'sessions_owner_shape_check',
+ );
+ });
+
+ it('allows only one canonical session per task', async () => {
+ const task = await createTask({});
+ const first = await createSession({});
+ const second = await createSession({});
+
+ await db.insert(sessionTasks).values({
+ sessionId: first.id,
+ taskId: task.id,
+ origin: 'direct_launch',
+ });
+ await expectConstraintViolation(
+ db.insert(sessionTasks).values({
+ sessionId: second.id,
+ taskId: task.id,
+ origin: 'follow_up',
+ }),
+ 'session_tasks_task_id_unique',
+ );
+ });
+
+ it('rejects unknown task-link origins', async () => {
+ const task = await createTask({});
+ const session = await createSession({});
+
+ await expectConstraintViolation(
+ db.insert(sessionTasks).values({
+ sessionId: session.id,
+ taskId: task.id,
+ origin: 'not-a-real-origin' as 'direct_launch',
+ }),
+ 'session_tasks_origin_check',
+ );
+ });
+
+ it('allows only one participant row per session and user', async () => {
+ const user = await userFactory.create();
+ createdUserIds.push(user.id);
+ const session = await createSession({});
+
+ await db.insert(sessionParticipants).values({
+ sessionId: session.id,
+ userId: user.id,
+ role: 'member',
+ });
+ await expectConstraintViolation(
+ db.insert(sessionParticipants).values({
+ sessionId: session.id,
+ userId: user.id,
+ role: 'owner',
+ }),
+ 'session_participants_session_user_unique',
+ );
+ });
+
+ it('rejects unknown participant roles', async () => {
+ const user = await userFactory.create();
+ createdUserIds.push(user.id);
+ const session = await createSession({});
+
+ await expectConstraintViolation(
+ db.insert(sessionParticipants).values({
+ sessionId: session.id,
+ userId: user.id,
+ role: 'not-a-real-role' as 'member',
+ }),
+ 'session_participants_role_check',
+ );
+ });
+
+ it('allows only one session per Fast conversation', async () => {
+ const user = await userFactory.create();
+ createdUserIds.push(user.id);
+ const [conversation] = await db
+ .insert(fastAgentConversations)
+ .values({
+ userId: user.id,
+ surface: 'web',
+ workspaceId: `workspace-${randomUUID()}`,
+ conversationId: `conversation-${randomUUID()}`,
+ })
+ .returning();
+
+ await createSession({ fastConversationId: conversation!.id });
+ await expectConstraintViolation(
+ createSession({ fastConversationId: conversation!.id }),
+ 'sessions_fast_conversation_id_unique',
+ );
+ });
+});
+
describe('task_runs classification CHECK constraints', () => {
it('accepts every run kind and harness', async () => {
for (const kind of RUN_KINDS) {
diff --git a/packages/db/src/fixtures/factories/index.ts b/packages/db/src/fixtures/factories/index.ts
index aaa18bf5c..f1e3374a1 100644
--- a/packages/db/src/fixtures/factories/index.ts
+++ b/packages/db/src/fixtures/factories/index.ts
@@ -1,5 +1,6 @@
export { userFactory } from './user.factory';
export { taskFactory } from './task.factory';
+export { sessionFactory } from './session.factory';
export { githubInstallationFactory } from './githubInstallation.factory';
export { slackInstallationFactory } from './slackInstallation.factory';
export { slackUserMappingFactory } from './slackUserMapping.factory';
diff --git a/packages/db/src/fixtures/factories/session.factory.ts b/packages/db/src/fixtures/factories/session.factory.ts
new file mode 100644
index 000000000..b93d3bc02
--- /dev/null
+++ b/packages/db/src/fixtures/factories/session.factory.ts
@@ -0,0 +1,36 @@
+import { faker } from '@faker-js/faker';
+import { Factory } from 'fishery';
+
+import { type DatabaseOrTransaction, db } from '../../db';
+import { sessions } from '../../schema';
+import type { CreateSession, Session } from '../../types';
+
+export const sessionFactory = Factory.define<
+ CreateSession,
+ { db?: DatabaseOrTransaction },
+ Session
+>(({ params, onCreate, transientParams }) => {
+ onCreate(async (values) => {
+ const [inserted] = await (transientParams.db || db)
+ .insert(sessions)
+ .values(values)
+ .returning();
+
+ if (!inserted) {
+ throw new Error('Failed to insert session');
+ }
+
+ return inserted;
+ });
+
+ return {
+ title: faker.lorem.sentence(),
+ ownerKind: 'system',
+ sourceSurface: 'system',
+ sourceTrigger: 'manual',
+ visibility: 'visible',
+ activityAt: Math.floor(Date.now() / 1000),
+ cachedStatus: 'ready',
+ ...params,
+ } satisfies CreateSession;
+});
diff --git a/packages/db/src/lib/__tests__/sessions.test.ts b/packages/db/src/lib/__tests__/sessions.test.ts
new file mode 100644
index 000000000..ac93b97d1
--- /dev/null
+++ b/packages/db/src/lib/__tests__/sessions.test.ts
@@ -0,0 +1,248 @@
+import {
+ db,
+ eq,
+ fastAgentConversations,
+ sessionFactory,
+ sessionParticipants,
+ sessions,
+ sessionTasks,
+ taskFactory,
+ tasks,
+ userFactory,
+ users,
+} from '../../server';
+
+import {
+ deriveSessionStatus,
+ ensureSessionForTask,
+ touchSessionActivity,
+} from '../sessions';
+
+const createdTaskIds: string[] = [];
+const createdSessionIds: string[] = [];
+const createdConversationIds: string[] = [];
+const createdUserIds: string[] = [];
+
+afterEach(async () => {
+ if (createdSessionIds.length > 0) {
+ await db.delete(sessions).where(eq(sessions.id, createdSessionIds.pop()!));
+ }
+ while (createdTaskIds.length > 0) {
+ await db.delete(tasks).where(eq(tasks.id, createdTaskIds.pop()!));
+ }
+ while (createdConversationIds.length > 0) {
+ await db
+ .delete(fastAgentConversations)
+ .where(eq(fastAgentConversations.id, createdConversationIds.pop()!));
+ }
+ while (createdUserIds.length > 0) {
+ await db.delete(users).where(eq(users.id, createdUserIds.pop()!));
+ }
+});
+
+describe('deriveSessionStatus', () => {
+ const task = (
+ overrides: Partial<
+ Parameters[0]['tasks'][number]
+ > = {},
+ ) => ({
+ state: 'completed' as const,
+ taskPhase: null,
+ goalStatus: null,
+ ...overrides,
+ });
+
+ it('prioritizes needs input over responding conversation and active work', () => {
+ expect(
+ deriveSessionStatus({
+ conversationResponding: true,
+ tasks: [task({ state: 'active', taskPhase: 'waiting_for_user_input' })],
+ }),
+ ).toBe('needs_input');
+ });
+
+ it.each([
+ ['a responding conversation', true, [task()], 'active'],
+ ['an active task', false, [task({ state: 'active' })], 'active'],
+ ['a failed task', false, [task({ state: 'failed' })], 'blocked'],
+ ['a blocked goal', false, [task({ goalStatus: 'blocked' })], 'blocked'],
+ [
+ 'a budget-limited goal',
+ false,
+ [task({ goalStatus: 'budget_limited' })],
+ 'blocked',
+ ],
+ ['only settled work', false, [task()], 'ready'],
+ ['no work', false, [], 'ready'],
+ ] as const)('derives %s as %s', (_label, responding, taskRows, expected) => {
+ expect(
+ deriveSessionStatus({
+ conversationResponding: responding,
+ tasks: [...taskRows],
+ }),
+ ).toBe(expected);
+ });
+
+ it('prioritizes active work over blocked settled work', () => {
+ expect(
+ deriveSessionStatus({
+ conversationResponding: false,
+ tasks: [task({ state: 'failed' }), task({ state: 'active' })],
+ }),
+ ).toBe('active');
+ });
+});
+
+describe('session helpers', () => {
+ it('updates activity monotonically', async () => {
+ const session = await sessionFactory.create({ activityAt: 100 });
+ createdSessionIds.push(session.id);
+
+ await touchSessionActivity(db, session.id, 200);
+ const updated = await touchSessionActivity(db, session.id, 150);
+
+ expect(updated.activityAt).toBe(200);
+ });
+
+ it('recomputes cached status from linked tasks while touching activity', async () => {
+ const session = await sessionFactory.create({
+ activityAt: 100,
+ cachedStatus: 'ready',
+ });
+ createdSessionIds.push(session.id);
+ const task = await taskFactory.create({
+ state: 'active',
+ activityAt: 200,
+ });
+ createdTaskIds.push(task.id);
+ await db.insert(sessionTasks).values({
+ sessionId: session.id,
+ taskId: task.id,
+ origin: 'direct_launch',
+ });
+
+ const updated = await touchSessionActivity(db, session.id, 200);
+
+ expect(updated).toEqual(
+ expect.objectContaining({ activityAt: 200, cachedStatus: 'active' }),
+ );
+ });
+
+ it('creates one canonical session and owner participant for a visible task', async () => {
+ const user = await userFactory.create();
+ createdUserIds.push(user.id);
+ const task = await taskFactory.create({ initiatorUserId: user.id });
+ createdTaskIds.push(task.id);
+
+ const first = await db.transaction((tx) =>
+ ensureSessionForTask(tx, { taskId: task.id }),
+ );
+ const second = await db.transaction((tx) =>
+ ensureSessionForTask(tx, { taskId: task.id, existingTaskReused: true }),
+ );
+
+ expect(first).not.toBeNull();
+ expect(second?.id).toBe(first?.id);
+ if (first) createdSessionIds.push(first.id);
+
+ const links = await db
+ .select()
+ .from(sessionTasks)
+ .where(eq(sessionTasks.taskId, task.id));
+ const participants = await db
+ .select()
+ .from(sessionParticipants)
+ .where(eq(sessionParticipants.sessionId, first!.id));
+
+ expect(links).toHaveLength(1);
+ expect(participants).toEqual([
+ expect.objectContaining({ userId: user.id, role: 'owner' }),
+ ]);
+ });
+
+ it('does not create a session for a hidden task', async () => {
+ const task = await taskFactory.create({ visibility: 'hidden' });
+ createdTaskIds.push(task.id);
+
+ const result = await db.transaction((tx) =>
+ ensureSessionForTask(tx, { taskId: task.id }),
+ );
+
+ expect(result).toBeNull();
+ expect(
+ await db
+ .select()
+ .from(sessionTasks)
+ .where(eq(sessionTasks.taskId, task.id)),
+ ).toEqual([]);
+ });
+
+ it('retains a session when its owner user is deleted', async () => {
+ const user = await userFactory.create();
+ const session = await sessionFactory.create({
+ ownerKind: 'user',
+ ownerUserId: user.id,
+ });
+ createdSessionIds.push(session.id);
+
+ await db.delete(users).where(eq(users.id, user.id));
+
+ const [retained] = await db
+ .select()
+ .from(sessions)
+ .where(eq(sessions.id, session.id));
+ expect(retained).toEqual(
+ expect.objectContaining({ ownerKind: 'user', ownerUserId: null }),
+ );
+ });
+
+ it('attaches Fast-delegated tasks to the conversation session', async () => {
+ const user = await userFactory.create();
+ createdUserIds.push(user.id);
+ const [conversation] = await db
+ .insert(fastAgentConversations)
+ .values({
+ userId: user.id,
+ surface: 'web',
+ workspaceId: `workspace-${crypto.randomUUID()}`,
+ conversationId: `conversation-${crypto.randomUUID()}`,
+ })
+ .returning();
+ createdConversationIds.push(conversation!.id);
+
+ const firstTask = await taskFactory.create({
+ initiatorUserId: user.id,
+ activityAt: 100,
+ });
+ const secondTask = await taskFactory.create({
+ initiatorUserId: user.id,
+ activityAt: 200,
+ });
+ createdTaskIds.push(firstTask.id, secondTask.id);
+
+ const first = await db.transaction((tx) =>
+ ensureSessionForTask(tx, {
+ taskId: firstTask.id,
+ fastConversationId: conversation!.id,
+ origin: 'fast_delegation',
+ }),
+ );
+ const second = await db.transaction((tx) =>
+ ensureSessionForTask(tx, {
+ taskId: secondTask.id,
+ fastConversationId: conversation!.id,
+ origin: 'fast_delegation',
+ }),
+ );
+
+ expect(second?.id).toBe(first?.id);
+ expect(second?.activityAt).toBe(200);
+ if (first) createdSessionIds.push(first.id);
+ expect(
+ await db
+ .select()
+ .from(sessionTasks)
+ .where(eq(sessionTasks.sessionId, first!.id)),
+ ).toHaveLength(2);
+ });
+});
diff --git a/packages/db/src/lib/sessions.ts b/packages/db/src/lib/sessions.ts
new file mode 100644
index 000000000..0db3daa32
--- /dev/null
+++ b/packages/db/src/lib/sessions.ts
@@ -0,0 +1,269 @@
+import { and, desc, eq, sql } from 'drizzle-orm';
+
+import type { TaskGoalStatus, TaskState } from '@roomote/types';
+
+import type { DatabaseOrTransaction } from '../db';
+import {
+ sessionParticipants,
+ sessions,
+ sessionTasks,
+ taskRuns,
+ tasks,
+ type SessionStatus,
+ type SessionTaskOrigin,
+} from '../schema';
+import type { Session } from '../types';
+
+export type SessionStatusInput = {
+ conversationResponding: boolean;
+ tasks: Array<{
+ state: TaskState;
+ taskPhase: string | null;
+ goalStatus: TaskGoalStatus | null;
+ }>;
+};
+
+export function deriveSessionStatus(input: SessionStatusInput): SessionStatus {
+ if (
+ input.tasks.some(
+ (task) =>
+ task.state === 'active' && task.taskPhase === 'waiting_for_user_input',
+ )
+ ) {
+ return 'needs_input';
+ }
+
+ if (
+ input.conversationResponding ||
+ input.tasks.some((task) => task.state === 'active')
+ ) {
+ return 'active';
+ }
+
+ if (
+ input.tasks.some(
+ (task) =>
+ task.state === 'failed' ||
+ task.goalStatus === 'blocked' ||
+ task.goalStatus === 'budget_limited',
+ )
+ ) {
+ return 'blocked';
+ }
+
+ return 'ready';
+}
+
+export async function touchSessionActivity(
+ tx: DatabaseOrTransaction,
+ sessionId: string,
+ at: number,
+ options: { conversationResponding?: boolean } = {},
+): Promise {
+ const linkedTasks = await tx
+ .selectDistinctOn([tasks.id], {
+ state: tasks.state,
+ taskPhase: taskRuns.taskPhase,
+ goalStatus: tasks.goalStatus,
+ })
+ .from(sessionTasks)
+ .innerJoin(tasks, eq(tasks.id, sessionTasks.taskId))
+ .leftJoin(taskRuns, eq(taskRuns.taskId, tasks.id))
+ .where(eq(sessionTasks.sessionId, sessionId))
+ .orderBy(tasks.id, desc(taskRuns.id));
+
+ const [updated] = await tx
+ .update(sessions)
+ .set({
+ activityAt: sql`GREATEST(${sessions.activityAt}, ${at})`,
+ cachedStatus: deriveSessionStatus({
+ conversationResponding: options.conversationResponding ?? false,
+ tasks: linkedTasks,
+ }),
+ updatedAt: new Date(),
+ })
+ .where(eq(sessions.id, sessionId))
+ .returning();
+
+ if (!updated) {
+ throw new Error(`Session ${sessionId} does not exist.`);
+ }
+
+ return updated;
+}
+
+export type EnsureSessionForTaskInput = {
+ taskId: string;
+ fastConversationId?: string | null;
+ origin?: SessionTaskOrigin;
+ existingTaskReused?: boolean;
+};
+
+/**
+ * Ensures a visible task has one canonical Session inside the caller's
+ * transaction. The tables are additive and ignored by N-1 application code.
+ */
+export async function ensureSessionForTask(
+ tx: DatabaseOrTransaction,
+ input: EnsureSessionForTaskInput,
+): Promise {
+ const [task] = await tx
+ .select({
+ id: tasks.id,
+ title: tasks.title,
+ state: tasks.state,
+ goalStatus: tasks.goalStatus,
+ initiatorKind: tasks.initiatorKind,
+ initiatorUserId: tasks.initiatorUserId,
+ initiatorAutomation: tasks.initiatorAutomation,
+ surface: tasks.surface,
+ trigger: tasks.trigger,
+ visibility: tasks.visibility,
+ activityAt: tasks.activityAt,
+ })
+ .from(tasks)
+ .where(eq(tasks.id, input.taskId))
+ .for('update');
+
+ if (!task) {
+ throw new Error(`Task ${input.taskId} does not exist.`);
+ }
+
+ if (task.visibility !== 'visible') {
+ return null;
+ }
+
+ const existing = await findSessionForTask(tx, task.id);
+ if (existing) {
+ return existing;
+ }
+
+ let session = input.fastConversationId
+ ? await findSessionForFastConversation(tx, input.fastConversationId)
+ : null;
+ let createdCandidate = false;
+
+ if (!session) {
+ const owner =
+ task.initiatorKind === 'user' && task.initiatorUserId
+ ? {
+ ownerKind: 'user' as const,
+ ownerUserId: task.initiatorUserId,
+ ownerAutomation: null,
+ }
+ : task.initiatorKind === 'automation' && task.initiatorAutomation
+ ? {
+ ownerKind: 'automation' as const,
+ ownerUserId: null,
+ ownerAutomation: task.initiatorAutomation,
+ }
+ : {
+ ownerKind: 'system' as const,
+ ownerUserId: null,
+ ownerAutomation: null,
+ };
+
+ const [inserted] = await tx
+ .insert(sessions)
+ .values({
+ title: task.title,
+ ...owner,
+ sourceSurface: task.surface,
+ sourceTrigger: task.trigger,
+ fastConversationId: input.fastConversationId ?? null,
+ visibility: task.visibility,
+ activityAt: task.activityAt,
+ cachedStatus: deriveSessionStatus({
+ conversationResponding: false,
+ tasks: [
+ {
+ state: task.state,
+ taskPhase: null,
+ goalStatus: task.goalStatus,
+ },
+ ],
+ }),
+ })
+ .onConflictDoNothing()
+ .returning();
+
+ session =
+ inserted ??
+ (input.fastConversationId
+ ? await findSessionForFastConversation(tx, input.fastConversationId)
+ : null);
+ createdCandidate = inserted !== undefined;
+ }
+
+ if (!session) {
+ throw new Error(`Failed to create a Session for task ${task.id}.`);
+ }
+
+ const [attached] = await tx
+ .insert(sessionTasks)
+ .values({
+ sessionId: session.id,
+ taskId: task.id,
+ origin: input.origin ?? 'direct_launch',
+ })
+ .onConflictDoNothing({ target: sessionTasks.taskId })
+ .returning({ sessionId: sessionTasks.sessionId });
+
+ if (!attached) {
+ const canonical = await findSessionForTask(tx, task.id);
+ if (!canonical) {
+ throw new Error(`Failed to attach task ${task.id} to a Session.`);
+ }
+
+ if (createdCandidate && canonical.id !== session.id) {
+ await tx.delete(sessions).where(eq(sessions.id, session.id));
+ }
+
+ return touchSessionActivity(tx, canonical.id, task.activityAt);
+ }
+
+ if (session.ownerUserId) {
+ await tx
+ .insert(sessionParticipants)
+ .values({
+ sessionId: session.id,
+ userId: session.ownerUserId,
+ role: 'owner',
+ })
+ .onConflictDoNothing();
+ }
+
+ return touchSessionActivity(tx, session.id, task.activityAt);
+}
+
+async function findSessionForTask(
+ tx: DatabaseOrTransaction,
+ taskId: string,
+): Promise {
+ const [session] = await tx
+ .select({ session: sessions })
+ .from(sessionTasks)
+ .innerJoin(sessions, eq(sessions.id, sessionTasks.sessionId))
+ .where(eq(sessionTasks.taskId, taskId))
+ .limit(1);
+
+ return session?.session ?? null;
+}
+
+async function findSessionForFastConversation(
+ tx: DatabaseOrTransaction,
+ fastConversationId: string,
+): Promise {
+ const [session] = await tx
+ .select()
+ .from(sessions)
+ .where(
+ and(
+ eq(sessions.fastConversationId, fastConversationId),
+ eq(sessions.visibility, 'visible'),
+ ),
+ )
+ .limit(1);
+
+ return session ?? null;
+}
diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts
index f23ac5f45..76ec5af03 100644
--- a/packages/db/src/schema.ts
+++ b/packages/db/src/schema.ts
@@ -150,6 +150,8 @@ export const users = pgTable(
export const userRelations = relations(users, ({ many }) => ({
tasks: many(tasks, { relationName: 'taskInitiatorUser' }),
taskPins: many(taskPins),
+ ownedSessions: many(sessions, { relationName: 'sessionOwnerUser' }),
+ sessionParticipants: many(sessionParticipants),
slackFastIntegrationCalls: many(slackFastIntegrationCalls),
workItems: many(workItems),
setupQualificationBlocks: many(setupQualificationBlocks),
@@ -840,6 +842,7 @@ export const tasksRelations = relations(tasks, ({ one, many }) => ({
relationName: 'taskCommitAuthorUser',
}),
taskPins: many(taskPins),
+ sessionTasks: many(sessionTasks),
runs: many(taskRuns),
inferenceUsageEvents: many(llmUsageEvents),
workItemsAsSource: many(workItems, {
@@ -3184,6 +3187,7 @@ export const fastAgentConversationsRelations = relations(
}),
messages: many(fastAgentMessages),
prFeedbackDeliveries: many(fastAgentPrFeedbackDeliveries),
+ session: one(sessions),
}),
);
@@ -3447,10 +3451,207 @@ export const automations = pgTable('automations', {
export const automationsRelations = relations(automations, ({ many }) => ({
tasks: many(tasks),
+ sessions: many(sessions),
workItems: many(workItems),
trackedMessages: many(trackedMessages),
}));
+export type SessionOwnerKind = 'user' | 'automation' | 'system';
+export type SessionSourceSurface = TaskSurface | FastAgentSurface;
+export type SessionStatus = 'active' | 'needs_input' | 'blocked' | 'ready';
+export type SessionTaskOrigin =
+ | 'direct_launch'
+ | 'fast_delegation'
+ | 'backfill'
+ | 'follow_up';
+export type SessionParticipantRole = 'owner' | 'member';
+
+/**
+ * sessions
+ *
+ * Additive Session storage is intentionally separate from tasks and Fast
+ * conversations so the previous release remains safe against this schema for
+ * N-1 rollback. Existing operational records remain canonical.
+ */
+export const sessions = pgTable(
+ 'sessions',
+ {
+ id: uuid('id').primaryKey().defaultRandom(),
+ title: text('title').notNull(),
+ ownerKind: text('owner_kind').notNull().$type(),
+ ownerUserId: text('owner_user_id').references(() => users.id, {
+ onDelete: 'set null',
+ }),
+ ownerAutomation: text('owner_automation')
+ .$type()
+ .references(() => automations.key, { onDelete: 'set null' }),
+ sourceSurface: text('source_surface')
+ .notNull()
+ .$type(),
+ sourceTrigger: text('source_trigger').notNull().$type(),
+ fastConversationId: uuid('fast_conversation_id').references(
+ () => fastAgentConversations.id,
+ { onDelete: 'set null' },
+ ),
+ visibility: text('visibility')
+ .notNull()
+ .default('visible')
+ .$type(),
+ activityAt: bigint('activity_at', { mode: 'number' }).notNull(),
+ cachedStatus: text('cached_status').$type(),
+ archivedAt: timestamp('archived_at'),
+ createdAt: timestamp('created_at').notNull().defaultNow(),
+ updatedAt: timestamp('updated_at').notNull().defaultNow(),
+ },
+ (table) => [
+ index('sessions_visibility_activity_at_idx').on(
+ table.visibility,
+ table.activityAt.desc(),
+ table.id.desc(),
+ ),
+ index('sessions_owner_user_id_idx').on(table.ownerUserId),
+ uniqueIndex('sessions_fast_conversation_id_unique')
+ .on(table.fastConversationId)
+ .where(sql`${table.fastConversationId} IS NOT NULL`),
+ check(
+ 'sessions_owner_shape_check',
+ // Owner FKs use ON DELETE SET NULL so retained Sessions can outlive
+ // deleted users and automation definitions. The shape still prevents a
+ // value from being stored in the wrong owner column.
+ sql`(${table.ownerKind} = 'user' AND ${table.ownerAutomation} IS NULL) OR (${table.ownerKind} = 'automation' AND ${table.ownerUserId} IS NULL) OR (${table.ownerKind} = 'system' AND ${table.ownerUserId} IS NULL AND ${table.ownerAutomation} IS NULL)`,
+ ),
+ check(
+ 'sessions_owner_kind_check',
+ sql`${table.ownerKind} in ('user', 'automation', 'system')`,
+ ),
+ check(
+ 'sessions_source_surface_check',
+ sql`${table.sourceSurface} in ('web', 'api', 'slack', 'teams', 'telegram', 'discord', 'linear', 'github', 'gitlab', 'gitea', 'ado', 'bitbucket', 'system', 'automation')`,
+ ),
+ check(
+ 'sessions_source_trigger_check',
+ sql`${table.sourceTrigger} in ('message', 'webhook', 'schedule', 'manual')`,
+ ),
+ check(
+ 'sessions_visibility_check',
+ sql`${table.visibility} in ('visible', 'hidden')`,
+ ),
+ check(
+ 'sessions_cached_status_check',
+ sql`${table.cachedStatus} IS NULL OR ${table.cachedStatus} in ('active', 'needs_input', 'blocked', 'ready')`,
+ ),
+ ],
+);
+
+/** Additive task linkage retained independently for N-1 rollback safety. */
+export const sessionTasks = pgTable(
+ 'session_tasks',
+ {
+ sessionId: uuid('session_id')
+ .notNull()
+ .references(() => sessions.id, { onDelete: 'cascade' }),
+ taskId: text('task_id')
+ .notNull()
+ .references(() => tasks.id, { onDelete: 'cascade' }),
+ attachedAt: timestamp('attached_at').notNull().defaultNow(),
+ origin: text('origin').notNull().$type(),
+ },
+ (table) => [
+ primaryKey({
+ name: 'session_tasks_session_id_task_id_pk',
+ columns: [table.sessionId, table.taskId],
+ }),
+ uniqueIndex('session_tasks_task_id_unique').on(table.taskId),
+ index('session_tasks_session_attached_at_idx').on(
+ table.sessionId,
+ table.attachedAt.desc(),
+ ),
+ check(
+ 'session_tasks_origin_check',
+ sql`${table.origin} in ('direct_launch', 'fast_delegation', 'backfill', 'follow_up')`,
+ ),
+ ],
+);
+
+/** Additive read-state storage retained independently for N-1 rollback safety. */
+export const sessionParticipants = pgTable(
+ 'session_participants',
+ {
+ id: uuid('id').primaryKey().defaultRandom(),
+ sessionId: uuid('session_id')
+ .notNull()
+ .references(() => sessions.id, { onDelete: 'cascade' }),
+ userId: text('user_id')
+ .notNull()
+ .references(() => users.id, {
+ onDelete: 'cascade',
+ }),
+ role: text('role')
+ .notNull()
+ .default('member')
+ .$type(),
+ lastReadEventAt: bigint('last_read_event_at', { mode: 'number' }),
+ lastReadEventId: text('last_read_event_id'),
+ lastNotifiedEventAt: bigint('last_notified_event_at', { mode: 'number' }),
+ createdAt: timestamp('created_at').notNull().defaultNow(),
+ updatedAt: timestamp('updated_at').notNull().defaultNow(),
+ },
+ (table) => [
+ uniqueIndex('session_participants_session_user_unique').on(
+ table.sessionId,
+ table.userId,
+ ),
+ index('session_participants_user_id_idx').on(table.userId),
+ check(
+ 'session_participants_role_check',
+ sql`${table.role} in ('owner', 'member')`,
+ ),
+ ],
+);
+
+export const sessionsRelations = relations(sessions, ({ one, many }) => ({
+ ownerUser: one(users, {
+ fields: [sessions.ownerUserId],
+ references: [users.id],
+ relationName: 'sessionOwnerUser',
+ }),
+ ownerAutomationRow: one(automations, {
+ fields: [sessions.ownerAutomation],
+ references: [automations.key],
+ }),
+ fastConversation: one(fastAgentConversations, {
+ fields: [sessions.fastConversationId],
+ references: [fastAgentConversations.id],
+ }),
+ tasks: many(sessionTasks),
+ participants: many(sessionParticipants),
+}));
+
+export const sessionTasksRelations = relations(sessionTasks, ({ one }) => ({
+ session: one(sessions, {
+ fields: [sessionTasks.sessionId],
+ references: [sessions.id],
+ }),
+ task: one(tasks, {
+ fields: [sessionTasks.taskId],
+ references: [tasks.id],
+ }),
+}));
+
+export const sessionParticipantsRelations = relations(
+ sessionParticipants,
+ ({ one }) => ({
+ session: one(sessions, {
+ fields: [sessionParticipants.sessionId],
+ references: [sessions.id],
+ }),
+ user: one(users, {
+ fields: [sessionParticipants.userId],
+ references: [users.id],
+ }),
+ }),
+);
+
/**
* custom_automations
*
diff --git a/packages/db/src/server.ts b/packages/db/src/server.ts
index c171152d6..eba4174d1 100644
--- a/packages/db/src/server.ts
+++ b/packages/db/src/server.ts
@@ -50,6 +50,7 @@ export * from './lib/task-suggestion-content-hash';
export * from './lib/work-item-claims';
export * from './lib/task-start-parallel-counts';
export * from './lib/tasks';
+export * from './lib/sessions';
export * from './lib/task-goals';
export * from './lib/source-control-provider';
export * from './lib/sync-task-state';
@@ -119,6 +120,12 @@ export {
tasksRelations,
taskPins,
taskPinsRelations,
+ sessions,
+ sessionsRelations,
+ sessionTasks,
+ sessionTasksRelations,
+ sessionParticipants,
+ sessionParticipantsRelations,
taskArtifacts,
taskArtifactsRelations,
taskPullRequests,
@@ -240,5 +247,10 @@ export type {
SuggestionType,
ManagerMcpSetupNotificationReason,
EnvironmentConfigVersionSource,
+ SessionOwnerKind,
+ SessionSourceSurface,
+ SessionStatus,
+ SessionTaskOrigin,
+ SessionParticipantRole,
} from './schema';
export type { AutomationWorkItemDisposition } from '@roomote/types';
diff --git a/packages/db/src/types.ts b/packages/db/src/types.ts
index 863c6d9df..f93a1333d 100644
--- a/packages/db/src/types.ts
+++ b/packages/db/src/types.ts
@@ -23,6 +23,9 @@ import type {
deploymentSettings,
tasks,
taskPins,
+ sessions,
+ sessionTasks,
+ sessionParticipants,
taskPullRequests,
taskRuns,
taskRunEvents,
@@ -99,6 +102,28 @@ export type TaskPin = typeof taskPins.$inferSelect;
export type CreateTaskPin = Omit;
+/**
+ * sessions
+ */
+
+export type Session = typeof sessions.$inferSelect;
+
+export type CreateSession = Omit;
+
+export type SessionTask = typeof sessionTasks.$inferSelect;
+
+export type CreateSessionTask = Omit<
+ typeof sessionTasks.$inferInsert,
+ 'attachedAt'
+>;
+
+export type SessionParticipant = typeof sessionParticipants.$inferSelect;
+
+export type CreateSessionParticipant = Omit<
+ typeof sessionParticipants.$inferInsert,
+ Generated
+>;
+
/**
* taskPullRequests
*/
From 8c891d6513de3a3c0488b585ceb4d1ad275a3630 Mon Sep 17 00:00:00 2001
From: Roomote
Date: Wed, 26 Aug 2026 16:48:33 +0000
Subject: [PATCH 02/26] fix(db): serialize session status refreshes
---
.../db/src/lib/__tests__/sessions.test.ts | 66 +++++++++++++++++++
packages/db/src/lib/sessions.ts | 29 ++++++--
2 files changed, 91 insertions(+), 4 deletions(-)
diff --git a/packages/db/src/lib/__tests__/sessions.test.ts b/packages/db/src/lib/__tests__/sessions.test.ts
index ac93b97d1..9754c6d62 100644
--- a/packages/db/src/lib/__tests__/sessions.test.ts
+++ b/packages/db/src/lib/__tests__/sessions.test.ts
@@ -128,6 +128,72 @@ describe('session helpers', () => {
);
});
+ it('serializes concurrent status refreshes before reading linked tasks', async () => {
+ const session = await sessionFactory.create({
+ activityAt: 100,
+ cachedStatus: 'active',
+ });
+ createdSessionIds.push(session.id);
+ const firstTask = await taskFactory.create({ state: 'active' });
+ const secondTask = await taskFactory.create({ state: 'active' });
+ createdTaskIds.push(firstTask.id, secondTask.id);
+ await db.insert(sessionTasks).values([
+ {
+ sessionId: session.id,
+ taskId: firstTask.id,
+ origin: 'direct_launch',
+ },
+ {
+ sessionId: session.id,
+ taskId: secondTask.id,
+ origin: 'follow_up',
+ },
+ ]);
+
+ let releaseFirst!: () => void;
+ const firstCanCommit = new Promise((resolve) => {
+ releaseFirst = resolve;
+ });
+ let firstRefreshed!: () => void;
+ const firstRefreshComplete = new Promise((resolve) => {
+ firstRefreshed = resolve;
+ });
+
+ const first = db.transaction(async (tx) => {
+ await tx
+ .update(tasks)
+ .set({ state: 'completed' })
+ .where(eq(tasks.id, firstTask.id));
+ await touchSessionActivity(tx, session.id, 200);
+ firstRefreshed();
+ await firstCanCommit;
+ });
+ await firstRefreshComplete;
+
+ let secondUpdated!: () => void;
+ const secondTaskUpdated = new Promise((resolve) => {
+ secondUpdated = resolve;
+ });
+ const second = db.transaction(async (tx) => {
+ await tx
+ .update(tasks)
+ .set({ state: 'completed' })
+ .where(eq(tasks.id, secondTask.id));
+ secondUpdated();
+ await touchSessionActivity(tx, session.id, 300);
+ });
+ await secondTaskUpdated;
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ releaseFirst();
+ await Promise.all([first, second]);
+
+ const [refreshed] = await db
+ .select({ cachedStatus: sessions.cachedStatus })
+ .from(sessions)
+ .where(eq(sessions.id, session.id));
+ expect(refreshed?.cachedStatus).toBe('ready');
+ });
+
it('creates one canonical session and owner participant for a visible task', async () => {
const user = await userFactory.create();
createdUserIds.push(user.id);
diff --git a/packages/db/src/lib/sessions.ts b/packages/db/src/lib/sessions.ts
index 0db3daa32..25e591572 100644
--- a/packages/db/src/lib/sessions.ts
+++ b/packages/db/src/lib/sessions.ts
@@ -14,6 +14,8 @@ import {
} from '../schema';
import type { Session } from '../types';
+import { runInTransactionIfAvailable } from './transaction-utils';
+
export type SessionStatusInput = {
conversationResponding: boolean;
tasks: Array<{
@@ -55,10 +57,31 @@ export function deriveSessionStatus(input: SessionStatusInput): SessionStatus {
}
export async function touchSessionActivity(
- tx: DatabaseOrTransaction,
+ dbOrTx: DatabaseOrTransaction,
sessionId: string,
at: number,
options: { conversationResponding?: boolean } = {},
+): Promise {
+ return runInTransactionIfAvailable(dbOrTx, async (tx) => {
+ const [lockedSession] = await tx
+ .select({ id: sessions.id })
+ .from(sessions)
+ .where(eq(sessions.id, sessionId))
+ .for('update');
+
+ if (!lockedSession) {
+ throw new Error(`Session ${sessionId} does not exist.`);
+ }
+
+ return refreshLockedSession(tx, sessionId, at, options);
+ });
+}
+
+async function refreshLockedSession(
+ tx: DatabaseOrTransaction,
+ sessionId: string,
+ at: number,
+ options: { conversationResponding?: boolean },
): Promise {
const linkedTasks = await tx
.selectDistinctOn([tasks.id], {
@@ -85,9 +108,7 @@ export async function touchSessionActivity(
.where(eq(sessions.id, sessionId))
.returning();
- if (!updated) {
- throw new Error(`Session ${sessionId} does not exist.`);
- }
+ if (!updated) throw new Error(`Session ${sessionId} does not exist.`);
return updated;
}
From c22e9d8e1ac2a1924dd2b3e18f7cbc4fd04c80e2 Mon Sep 17 00:00:00 2001
From: Roomote
Date: Wed, 26 Aug 2026 19:17:52 +0000
Subject: [PATCH 03/26] feat: complete unified sessions rollout
---
apps/bullmq/package.json | 1 +
.../__tests__/sessions-reconcile.test.ts | 62 ++
apps/bullmq/src/scheduled-jobs/index.ts | 1 +
.../src/scheduled-jobs/sessions-reconcile.ts | 250 +++++++
apps/bullmq/src/scheduler.ts | 7 +
apps/bullmq/src/types.ts | 1 +
apps/docs/fast-sessions.mdx | 54 +-
apps/docs/tasks.mdx | 26 +-
.../(authenticated)/analytics/Analytics.tsx | 10 +-
.../analytics/AnalyticsDetailsDialog.tsx | 2 +
.../analytics/AnalyticsDimensionIcons.ts | 3 +
.../analytics/AnalyticsFilterBar.tsx | 2 +
.../analytics/AnalyticsShell.tsx | 4 +
.../web/src/app/(authenticated)/home/Home.tsx | 22 +-
.../(authenticated)/sessions/SessionCard.tsx | 87 +++
.../sessions/SessionsFilters.tsx | 222 +++++-
.../src/app/(authenticated)/sessions/page.tsx | 124 +++-
.../src/app/(authenticated)/tasks/page.tsx | 26 +-
.../[sessionId]/FastSessionTranscript.tsx | 15 +-
.../[sessionId]/SessionReadTracker.tsx | 38 +
.../sessions/[sessionId]/SessionTaskCards.tsx | 169 +++++
.../sessions/[sessionId]/SessionWorkspace.tsx | 239 ++++++-
.../sessions/[sessionId]/page.test.tsx | 4 +
.../(sandbox)/sessions/[sessionId]/page.tsx | 97 ++-
.../app/(sandbox)/task/[taskId]/Header.tsx | 86 ++-
.../task/[taskId]/TaskSessionReadTracker.tsx | 28 +
.../src/components/layout/CommandPalette.tsx | 34 +-
.../components/layout/navbar/NavbarDrawer.tsx | 7 +-
.../src/components/layout/navigation-items.ts | 16 +-
.../components/layout/side-nav/SideNav.tsx | 10 +-
apps/web/src/hooks/useRecentSessions.ts | 42 ++
apps/web/src/lib/server/analytics/index.ts | 14 +
.../src/lib/server/analytics/session-rows.ts | 84 +++
apps/web/src/lib/server/auth-context.test.ts | 10 +-
apps/web/src/lib/server/sessions.test.ts | 146 ++++
apps/web/src/lib/server/sessions.ts | 660 ++++++++++++++++++
apps/web/src/lib/telemetry/normalize-path.ts | 7 +-
.../src/trpc/commands/fast-sessions/index.ts | 17 +-
.../trpc/commands/feature-flags/index.test.ts | 22 +-
.../src/trpc/commands/sessions/index.test.ts | 63 ++
apps/web/src/trpc/commands/sessions/index.ts | 104 +++
apps/web/src/trpc/commands/task-runs/index.ts | 10 +-
apps/web/src/trpc/routers/_app.ts | 79 +++
apps/web/src/types/analytics.ts | 34 +-
packages/cloud-agents/package.json | 1 +
.../src/server/__tests__/enqueue-task.test.ts | 56 ++
.../__tests__/fast-agent-service.test.ts | 3 +
.../fast-agent-conversation-repository.ts | 69 ++
.../server/fast-agent/fast-agent-service.ts | 64 +-
.../src/server/non-task-provider-usage.ts | 4 +
.../cloud-agents/src/server/task-run-queue.ts | 48 ++
.../communication/src/fast-session-footer.ts | 16 +-
.../db/src/lib/__tests__/sessions.test.ts | 135 ++++
packages/db/src/lib/llm-usage.ts | 19 +-
packages/db/src/lib/sessions.ts | 202 +++++-
packages/db/src/lib/sync-task-state.ts | 3 +
packages/db/src/schema.ts | 79 +++
packages/db/src/server.ts | 4 +
packages/db/src/types.ts | 5 +
.../src/__tests__/config.test.ts | 16 +-
.../evaluateFlagFromMetadata.test.ts | 22 +-
packages/feature-flags/src/config.ts | 21 +-
packages/feature-flags/src/types.ts | 8 +-
packages/sdk/src/server/routers/task-runs.ts | 6 +
packages/slack/package.json | 1 +
.../src/fast-agent-live-task-launcher.ts | 54 +-
packages/slack/src/live-task-card-blocks.ts | 19 +-
packages/slack/src/live-task-stream.ts | 1 +
packages/slack/src/settle-live-task-card.ts | 16 +-
pnpm-lock.yaml | 9 +
70 files changed, 3640 insertions(+), 180 deletions(-)
create mode 100644 apps/bullmq/src/scheduled-jobs/__tests__/sessions-reconcile.test.ts
create mode 100644 apps/bullmq/src/scheduled-jobs/sessions-reconcile.ts
create mode 100644 apps/web/src/app/(authenticated)/sessions/SessionCard.tsx
create mode 100644 apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionReadTracker.tsx
create mode 100644 apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionTaskCards.tsx
create mode 100644 apps/web/src/app/(sandbox)/task/[taskId]/TaskSessionReadTracker.tsx
create mode 100644 apps/web/src/hooks/useRecentSessions.ts
create mode 100644 apps/web/src/lib/server/analytics/session-rows.ts
create mode 100644 apps/web/src/lib/server/sessions.test.ts
create mode 100644 apps/web/src/lib/server/sessions.ts
create mode 100644 apps/web/src/trpc/commands/sessions/index.test.ts
create mode 100644 apps/web/src/trpc/commands/sessions/index.ts
diff --git a/apps/bullmq/package.json b/apps/bullmq/package.json
index 3858ea93b..abbc24cdf 100644
--- a/apps/bullmq/package.json
+++ b/apps/bullmq/package.json
@@ -27,6 +27,7 @@
"@roomote/db": "workspace:^",
"@roomote/discord-gateway": "workspace:^",
"@roomote/env": "workspace:^",
+ "@roomote/feature-flags": "workspace:^",
"@roomote/github": "workspace:^",
"@roomote/linear": "workspace:^",
"@roomote/sdk": "workspace:^",
diff --git a/apps/bullmq/src/scheduled-jobs/__tests__/sessions-reconcile.test.ts b/apps/bullmq/src/scheduled-jobs/__tests__/sessions-reconcile.test.ts
new file mode 100644
index 000000000..76438cd00
--- /dev/null
+++ b/apps/bullmq/src/scheduled-jobs/__tests__/sessions-reconcile.test.ts
@@ -0,0 +1,62 @@
+import {
+ db,
+ deploymentSettings,
+ eq,
+ fastAgentConversations,
+ sessionTasks,
+ sessions,
+ taskFactory,
+ userFactory,
+} from '@roomote/db/server';
+import { getFeatureFlagEvaluator } from '@roomote/feature-flags/server';
+
+import { getRedis } from '../../redis';
+import { sessionsReconcileJob } from '../sessions-reconcile';
+
+describe('sessionsReconcileJob', () => {
+ beforeEach(async () => {
+ await db
+ .insert(deploymentSettings)
+ .values({ id: 'default', metadata: { sessions_data: true } })
+ .onConflictDoUpdate({
+ target: deploymentSettings.id,
+ set: { metadata: { sessions_data: true } },
+ });
+ await getFeatureFlagEvaluator(getRedis()).invalidateDeploymentCache();
+ });
+
+ afterEach(async () => {
+ await db
+ .update(deploymentSettings)
+ .set({ metadata: {} })
+ .where(eq(deploymentSettings.id, 'default'));
+ await getFeatureFlagEvaluator(getRedis()).invalidateDeploymentCache();
+ });
+
+ it('backfills Fast conversations and visible tasks idempotently', async () => {
+ const user = await userFactory.create();
+ const [conversation] = await db
+ .insert(fastAgentConversations)
+ .values({
+ userId: user.id,
+ surface: 'web',
+ workspaceId: user.id,
+ conversationId: crypto.randomUUID(),
+ })
+ .returning();
+ const task = await taskFactory.create({ initiatorUserId: user.id });
+
+ await sessionsReconcileJob();
+ await sessionsReconcileJob();
+
+ await expect(
+ db
+ .select()
+ .from(sessions)
+ .where(eq(sessions.fastConversationId, conversation!.id)),
+ ).resolves.toHaveLength(1);
+ await expect(
+ db.select().from(sessionTasks).where(eq(sessionTasks.taskId, task.id)),
+ ).resolves.toHaveLength(1);
+ });
+});
diff --git a/apps/bullmq/src/scheduled-jobs/index.ts b/apps/bullmq/src/scheduled-jobs/index.ts
index 6be114ec8..a2b2061f0 100644
--- a/apps/bullmq/src/scheduled-jobs/index.ts
+++ b/apps/bullmq/src/scheduled-jobs/index.ts
@@ -9,3 +9,4 @@ export { standbyRetentionJob } from './standby-retention';
export { prReviewNotificationDispatchJob } from './pr-review-notification-dispatch';
export { brainOutboxDrainJob, brainCollectorsJob } from './brain-outbox-drain';
export { brainMaintenanceJob } from './brain-maintenance';
+export { sessionsReconcileJob } from './sessions-reconcile';
diff --git a/apps/bullmq/src/scheduled-jobs/sessions-reconcile.ts b/apps/bullmq/src/scheduled-jobs/sessions-reconcile.ts
new file mode 100644
index 000000000..3f40866f9
--- /dev/null
+++ b/apps/bullmq/src/scheduled-jobs/sessions-reconcile.ts
@@ -0,0 +1,250 @@
+import {
+ and,
+ db,
+ desc,
+ ensureSessionForFastConversation,
+ ensureSessionForTask,
+ eq,
+ fastAgentConversations,
+ gt,
+ isNull,
+ or,
+ sessionBackfillState,
+ sessions,
+ sessionTasks,
+ sql,
+ taskRuns,
+ tasks,
+ touchSessionActivity,
+} from '@roomote/db/server';
+import {
+ FeatureFlag,
+ getFeatureFlagEvaluator,
+} from '@roomote/feature-flags/server';
+
+import { getRedis } from '../redis';
+
+const LOG_PREFIX = '[sessions]';
+const BACKFILL_KEY = 'unified-sessions-v1';
+const BATCH_SIZE = 100;
+
+type Cursor = { createdAt: Date; id: string } | null;
+
+function afterCursor(
+ createdAt: TCreatedAt,
+ id: TId,
+ cursor: Cursor,
+) {
+ return cursor
+ ? or(
+ gt(createdAt as never, cursor.createdAt),
+ and(
+ eq(createdAt as never, cursor.createdAt),
+ gt(id as never, cursor.id),
+ ),
+ )
+ : undefined;
+}
+
+async function updateState(input: {
+ phase: 'fast_conversations' | 'tasks' | 'participants';
+ cursor?: Cursor;
+ completed?: boolean;
+}) {
+ await db
+ .insert(sessionBackfillState)
+ .values({
+ key: BACKFILL_KEY,
+ phase: input.phase,
+ cursorCreatedAt: input.cursor?.createdAt ?? null,
+ cursorId: input.cursor?.id ?? null,
+ completedAt: input.completed ? new Date() : null,
+ lastRunAt: new Date(),
+ })
+ .onConflictDoUpdate({
+ target: sessionBackfillState.key,
+ set: {
+ phase: input.phase,
+ cursorCreatedAt: input.cursor?.createdAt ?? null,
+ cursorId: input.cursor?.id ?? null,
+ completedAt: input.completed ? new Date() : null,
+ lastRunAt: new Date(),
+ updatedAt: new Date(),
+ },
+ });
+}
+
+async function backfillFastConversations(cursor: Cursor): Promise {
+ const rows = await db
+ .select({
+ id: fastAgentConversations.id,
+ createdAt: fastAgentConversations.createdAt,
+ })
+ .from(fastAgentConversations)
+ .leftJoin(
+ sessions,
+ eq(sessions.fastConversationId, fastAgentConversations.id),
+ )
+ .where(
+ and(
+ isNull(sessions.id),
+ afterCursor(
+ fastAgentConversations.createdAt,
+ fastAgentConversations.id,
+ cursor,
+ ),
+ ),
+ )
+ .orderBy(fastAgentConversations.createdAt, fastAgentConversations.id)
+ .limit(BATCH_SIZE);
+
+ for (const row of rows) {
+ await db.transaction((tx) => ensureSessionForFastConversation(tx, row.id));
+ }
+
+ const last = rows.at(-1);
+ await updateState({
+ phase: last && rows.length === BATCH_SIZE ? 'fast_conversations' : 'tasks',
+ cursor:
+ last && rows.length === BATCH_SIZE
+ ? { createdAt: last.createdAt, id: last.id }
+ : null,
+ });
+ console.info(`${LOG_PREFIX} backfill fast conversations`, {
+ processed: rows.length,
+ });
+ return rows.length < BATCH_SIZE;
+}
+
+async function backfillTasks(cursor: Cursor): Promise {
+ const rows = await db
+ .select({ id: tasks.id, createdAt: tasks.createdAt })
+ .from(tasks)
+ .leftJoin(sessionTasks, eq(sessionTasks.taskId, tasks.id))
+ .where(
+ and(
+ eq(tasks.visibility, 'visible'),
+ isNull(tasks.deletedAt),
+ isNull(sessionTasks.taskId),
+ afterCursor(tasks.createdAt, tasks.id, cursor),
+ ),
+ )
+ .orderBy(tasks.createdAt, tasks.id)
+ .limit(BATCH_SIZE);
+
+ for (const row of rows) {
+ const latestFastRun = await db.query.taskRuns.findFirst({
+ where: and(
+ eq(taskRuns.taskId, row.id),
+ sql`${taskRuns.fastAgentSessionId} IS NOT NULL`,
+ ),
+ columns: { fastAgentSessionId: true },
+ orderBy: desc(taskRuns.id),
+ });
+ await db.transaction((tx) =>
+ ensureSessionForTask(tx, {
+ taskId: row.id,
+ fastConversationId: latestFastRun?.fastAgentSessionId ?? null,
+ origin: 'backfill',
+ }),
+ );
+ }
+
+ const last = rows.at(-1);
+ await updateState({
+ phase: last && rows.length === BATCH_SIZE ? 'tasks' : 'participants',
+ cursor:
+ last && rows.length === BATCH_SIZE
+ ? { createdAt: last.createdAt, id: last.id }
+ : null,
+ });
+ console.info(`${LOG_PREFIX} backfill tasks`, { processed: rows.length });
+ return rows.length < BATCH_SIZE;
+}
+
+async function backfillParticipants(): Promise {
+ await db.execute(sql`
+ INSERT INTO session_participants (session_id, user_id, role)
+ SELECT DISTINCT s.id, fam.metadata->>'userId', 'member'
+ FROM sessions s
+ JOIN fast_agent_messages fam ON fam.conversation_id = s.fast_conversation_id
+ JOIN users u ON u.id = fam.metadata->>'userId' AND u.deleted_at IS NULL
+ WHERE fam.metadata->>'userId' IS NOT NULL
+ ON CONFLICT (session_id, user_id) DO NOTHING
+ `);
+ await updateState({ phase: 'participants', completed: true });
+ console.info(`${LOG_PREFIX} backfill participants complete`);
+}
+
+async function reconcileRecentSessions(): Promise {
+ const orphanTasks = await db
+ .select({ id: tasks.id })
+ .from(tasks)
+ .leftJoin(sessionTasks, eq(sessionTasks.taskId, tasks.id))
+ .where(
+ and(
+ eq(tasks.visibility, 'visible'),
+ isNull(tasks.deletedAt),
+ isNull(sessionTasks.taskId),
+ ),
+ )
+ .orderBy(desc(tasks.activityAt))
+ .limit(BATCH_SIZE);
+
+ for (const task of orphanTasks) {
+ await db.transaction((tx) =>
+ ensureSessionForTask(tx, { taskId: task.id, origin: 'backfill' }),
+ );
+ }
+
+ const recent = await db
+ .select({ id: sessions.id, activityAt: sessions.activityAt })
+ .from(sessions)
+ .where(eq(sessions.visibility, 'visible'))
+ .orderBy(desc(sessions.activityAt))
+ .limit(BATCH_SIZE);
+ for (const session of recent) {
+ await touchSessionActivity(db, session.id, session.activityAt);
+ }
+
+ console.info(`${LOG_PREFIX} reconciliation`, {
+ orphanVisibleTasks: orphanTasks.length,
+ refreshedSessions: recent.length,
+ });
+}
+
+export async function sessionsReconcileJob(): Promise {
+ const enabled = await getFeatureFlagEvaluator(getRedis()).evaluate(
+ FeatureFlag.SessionsData,
+ { isDeploymentContext: true },
+ );
+ if (!enabled) return;
+
+ const state = await db.query.sessionBackfillState.findFirst({
+ where: eq(sessionBackfillState.key, BACKFILL_KEY),
+ });
+ if (state?.completedAt) {
+ await reconcileRecentSessions();
+ return;
+ }
+
+ const phase = state?.phase ?? 'fast_conversations';
+ const cursor =
+ state?.cursorCreatedAt && state.cursorId
+ ? { createdAt: state.cursorCreatedAt, id: state.cursorId }
+ : null;
+
+ if (phase === 'fast_conversations') {
+ const complete = await backfillFastConversations(cursor);
+ if (!complete) return;
+ }
+ if (
+ phase === 'fast_conversations' ||
+ phase === 'fast_tasks' ||
+ phase === 'tasks'
+ ) {
+ const complete = await backfillTasks(phase === 'tasks' ? cursor : null);
+ if (!complete) return;
+ }
+ await backfillParticipants();
+}
diff --git a/apps/bullmq/src/scheduler.ts b/apps/bullmq/src/scheduler.ts
index 360d37127..44b34dfa5 100644
--- a/apps/bullmq/src/scheduler.ts
+++ b/apps/bullmq/src/scheduler.ts
@@ -35,6 +35,7 @@ import {
brainOutboxDrainJob,
brainCollectorsJob,
brainMaintenanceJob,
+ sessionsReconcileJob,
} from './scheduled-jobs';
const QUEUE_NAME = 'scheduled-jobs';
@@ -225,6 +226,10 @@ async function createJobs(queue: Queue): Promise {
{ pattern: '0 7 * * *' },
);
+ await queue.upsertJobScheduler(ScheduledJobName.SessionsReconcile, {
+ every: 60 * 1000,
+ });
+
const schedulers = await queue.getJobSchedulers();
console.log('[createJobs] getJobSchedulers ->', schedulers);
}
@@ -266,6 +271,8 @@ const runJobs = async (job: ScheduledJob): Promise => {
return brainCollectorsJob();
case ScheduledJobName.BrainMaintenance:
return brainMaintenanceJob();
+ case ScheduledJobName.SessionsReconcile:
+ return sessionsReconcileJob();
case ScheduledJobName.CustomAutomations:
await customAutomationsJob();
return;
diff --git a/apps/bullmq/src/types.ts b/apps/bullmq/src/types.ts
index 6393c98a6..9e3730035 100644
--- a/apps/bullmq/src/types.ts
+++ b/apps/bullmq/src/types.ts
@@ -18,6 +18,7 @@ export enum ScheduledJobName {
BrainOutboxDrain = 'BrainOutboxDrain',
BrainCollectors = 'BrainCollectors',
BrainMaintenance = 'BrainMaintenance',
+ SessionsReconcile = 'SessionsReconcile',
}
/**
diff --git a/apps/docs/fast-sessions.mdx b/apps/docs/fast-sessions.mdx
index 45b4d93e9..ef0c684c4 100644
--- a/apps/docs/fast-sessions.mdx
+++ b/apps/docs/fast-sessions.mdx
@@ -1,39 +1,47 @@
---
-title: Fast sessions
-icon: zap
-description: Chat with the fast orchestrator from the dashboard and review every Fast conversation's transcript.
+title: Sessions
+icon: messages-square
+description: Follow a conversation and every execution it delegates from one continuous Roomote workspace.
---
-Fast is Roomote's conversational orchestrator: it answers directly when it can
-and delegates execution work into tasks when needed. A Fast session is one
-persisted Fast conversation, whether it started in Slack, Discord, an
-automation, or the web dashboard.
+Sessions are the primary way to follow work in Roomote. A Session keeps the
+conversation, delegated executions, review activity, artifacts, pull requests,
+cost, and unread state together, whether it started in chat, source control, an
+automation, the API, or the web dashboard.
-## Start a Fast session from the dashboard
+## Start a Session from the dashboard
-On the home page, open the workspace selector next to the prompt box and choose
-**Fast**. Your prompt starts a Fast session instead of a sandbox task, and
-Roomote takes you straight to the session view, where the response streams in
-as it is produced.
+On the home page, leave the workspace selector on **Auto** to start a
+conversation. Roomote answers directly when it can and delegates execution
+when the request needs a repository workspace. Selecting an environment or
+repository starts the execution directly, but Roomote still creates the
+owning Session and opens it with that execution selected.
-Use Fast when you want an answer, a decision, or a delegation rather than a
-full sandbox run. Fast can still launch tasks on your behalf; delegated tasks
-appear in the transcript with links to their task pages.
+You do not need to choose a separate conversation mode. The Session grows from
+conversation to execution to review without changing identity.
## The session view
-A session's transcript shows prompts, replies, and the tool activity behind
-them, rendered with the same transcript view as tasks, with a generated title
-that updates as the conversation evolves. The view updates in real time while
-a turn is running, so you can watch tool calls complete and replies land
-without refreshing.
+A Session timeline shows prompts, replies, and delegated execution activity.
+Execution cards show their status, workspace, pull requests, artifacts, latest
+error, and cost. Select a card to open the lightweight details panel, or choose
+**Open full workspace** for terminal, logs, diff, and preview tools.
+
+The Sessions page supports list and board views, filters, search, pins, recent
+Sessions, and unread indicators. **Ready** is not a terminal state: you can
+reply or start another execution in the same Session later.
## Reply to a session
-Every session has a reply box at the bottom of the transcript; follow-ups
+Conversational Sessions have a reply box at the bottom of the transcript; follow-ups
continue the same conversation with full context. For conversations that live
on another surface, such as a Slack thread, Roomote's answer is posted back
into the originating thread with a quoted copy of your web message, so the
conversation stays in one place for everyone following it there. Fast replies
-in Slack and Discord carry the same "Reply or use the web app" footer as task
-replies, linking to the session view.
+in supported communications providers link back with **Open in Roomote**.
+
+## Execution access
+
+Session participants can see timeline summaries. Full execution details keep
+the existing task permissions, so joining a shared channel does not grant
+access to logs, terminals, diffs, previews, or private artifacts.
diff --git a/apps/docs/tasks.mdx b/apps/docs/tasks.mdx
index 0bac4e8b7..ca6852ee0 100644
--- a/apps/docs/tasks.mdx
+++ b/apps/docs/tasks.mdx
@@ -4,9 +4,10 @@ icon: clipboard-check
description: Inspect the transcript, logs, diffs, previews, and follow-up path before you trust the result.
---
-A task is a single unit of Roomote work. It may start from chat, source
-control, Linear, or the web dashboard, but the task view gives your team one
-shared place to inspect what happened and decide what should happen next.
+A task is one independently controllable execution inside a Session. It may
+start from chat, source control, Linear, the API, or the web dashboard. The
+task workspace remains the place to inspect operational details such as logs,
+terminal output, diffs, previews, retries, and artifacts.
Use the task view as the handoff point between Roomote and your normal review
process. A task is complete only when the evidence is clear enough for a
@@ -24,19 +25,17 @@ Before you dive into details, check the basics:
- whether the end state matches the kind of outcome you wanted: answer, plan,
patch, branch, or PR
-## Task board
+## Sessions and the task board
-Use the board view on the Tasks page to scan shared work by lifecycle. Roomote
-places tasks in **Active**, **Needs input**, **Blocked / failed**, or **Done**
+Use the board view on the Sessions page to scan shared work by lifecycle.
+Roomote places Sessions in **Active**, **Needs input**, **Blocked**, or **Ready**
from their current task, goal, and run state, so your team does not need to
maintain a separate status field.
-Each card shows who started the task, participant avatars, recent activity, and
-available workspace or pull-request context. The Done column keeps the six most
-recent completed tasks so finished work does not overwhelm active work. Board
-and list choices remain in the URL so views are shareable. Roomote also restores
-the most recently selected layout from browser storage when you return; if
-browser storage is unavailable, the Tasks page falls back to list view.
+Each Session card shows its owner and participants, recent activity, delegated
+execution count, workspace or pull-request context, aggregate cost, and unread
+state. Use the **Tasks** scope when you only want Sessions containing execution
+work. Board and list choices remain in the URL so views are shareable.
## Recover from a failed start
@@ -50,6 +49,9 @@ reattach any files the new task needs.
The task view gives you the working context for a run:
+The header breadcrumb links back to the owning Session. When you opened the
+workspace from a filtered Sessions view, browser Back returns to that view.
+
- conversation history and Roomote updates
- inline widgets for structured tables, status cards, plans, and other
presentational results an agent chooses to show
diff --git a/apps/web/src/app/(authenticated)/analytics/Analytics.tsx b/apps/web/src/app/(authenticated)/analytics/Analytics.tsx
index 47013ad37..f3451106b 100644
--- a/apps/web/src/app/(authenticated)/analytics/Analytics.tsx
+++ b/apps/web/src/app/(authenticated)/analytics/Analytics.tsx
@@ -56,6 +56,8 @@ const analyticsFilterKeys = [
'taskType',
'provider',
'model',
+ 'ownerKind',
+ 'hasExecution',
] as const;
type SelectedAnalyticsSegment = {
@@ -65,7 +67,11 @@ type SelectedAnalyticsSegment = {
seriesLabel: string;
};
-const GENERIC_ANALYTICS_OBJECTS: AnalyticsObject[] = ['tasks', 'pullRequests'];
+const GENERIC_ANALYTICS_OBJECTS: AnalyticsObject[] = [
+ 'tasks',
+ 'sessions',
+ 'pullRequests',
+];
function parseAnalyticsObject(
value: string | null,
@@ -75,7 +81,7 @@ function parseAnalyticsObject(
return value as AnalyticsObject;
}
- return allowedObjects[0] ?? analyticsObjects[0];
+ return allowedObjects[0] ?? analyticsObjects[0] ?? 'tasks';
}
function getFiltersFromSearchParams(
diff --git a/apps/web/src/app/(authenticated)/analytics/AnalyticsDetailsDialog.tsx b/apps/web/src/app/(authenticated)/analytics/AnalyticsDetailsDialog.tsx
index 7f0acd1fd..c0513d34c 100644
--- a/apps/web/src/app/(authenticated)/analytics/AnalyticsDetailsDialog.tsx
+++ b/apps/web/src/app/(authenticated)/analytics/AnalyticsDetailsDialog.tsx
@@ -42,12 +42,14 @@ type AnalyticsDetailsDialogProps = {
};
const DIALOG_WIDTH_BY_OBJECT: Record = {
+ sessions: 'md:w-[min(96vw,1160px)] md:max-w-[1160px]',
tasks: 'md:w-[min(96vw,1160px)] md:max-w-[1160px]',
pullRequests: 'md:w-[min(96vw,1240px)] md:max-w-[1240px]',
costs: 'md:w-[min(96vw,1240px)] md:max-w-[1240px]',
};
const TABLE_MIN_WIDTH_BY_OBJECT: Record = {
+ sessions: 'min-w-[900px] md:min-w-[1040px]',
tasks: 'min-w-[980px] md:min-w-[1100px]',
pullRequests: 'min-w-[1140px] md:min-w-[1220px]',
costs: 'min-w-[1140px] md:min-w-[1220px]',
diff --git a/apps/web/src/app/(authenticated)/analytics/AnalyticsDimensionIcons.ts b/apps/web/src/app/(authenticated)/analytics/AnalyticsDimensionIcons.ts
index 0e440a02d..35a5b9c57 100644
--- a/apps/web/src/app/(authenticated)/analytics/AnalyticsDimensionIcons.ts
+++ b/apps/web/src/app/(authenticated)/analytics/AnalyticsDimensionIcons.ts
@@ -10,6 +10,7 @@ import {
GitPullRequest,
RadioTower,
VectorSquare,
+ Rows4,
} from '@/components/system';
export const ANALYTICS_DIMENSION_ICONS: Record<
@@ -25,4 +26,6 @@ export const ANALYTICS_DIMENSION_ICONS: Record<
taskType: Bot,
provider: Cpu,
model: Brain,
+ ownerKind: Bot,
+ hasExecution: Rows4,
};
diff --git a/apps/web/src/app/(authenticated)/analytics/AnalyticsFilterBar.tsx b/apps/web/src/app/(authenticated)/analytics/AnalyticsFilterBar.tsx
index c2dcda2d3..a7b2e2c7f 100644
--- a/apps/web/src/app/(authenticated)/analytics/AnalyticsFilterBar.tsx
+++ b/apps/web/src/app/(authenticated)/analytics/AnalyticsFilterBar.tsx
@@ -39,6 +39,8 @@ const ANALYTICS_DIMENSION_PLURAL_LABELS: Record = {
taskType: 'Task Types',
provider: 'Providers',
model: 'Models',
+ ownerKind: 'Owner kinds',
+ hasExecution: 'Execution states',
};
type AnalyticsFilterBarProps = {
diff --git a/apps/web/src/app/(authenticated)/analytics/AnalyticsShell.tsx b/apps/web/src/app/(authenticated)/analytics/AnalyticsShell.tsx
index 2311c1ac7..f5caa6d23 100644
--- a/apps/web/src/app/(authenticated)/analytics/AnalyticsShell.tsx
+++ b/apps/web/src/app/(authenticated)/analytics/AnalyticsShell.tsx
@@ -16,6 +16,8 @@ type AnalyticsShellItemId = AnalyticsObject;
export function getAnalyticsHref(itemId: AnalyticsShellItemId) {
switch (itemId) {
+ case 'sessions':
+ return '/analytics?object=sessions';
case 'tasks':
return '/analytics';
case 'pullRequests':
@@ -26,6 +28,7 @@ export function getAnalyticsHref(itemId: AnalyticsShellItemId) {
}
const ANALYTICS_SHELL_ITEMS = [
+ { id: 'sessions', label: 'Sessions', icon: ChartColumnIncreasing },
{ id: 'tasks', label: 'Tasks', icon: ChartColumnIncreasing },
{ id: 'costs', label: 'Costs', icon: CircleDollarSign },
] as const satisfies Array<{
@@ -35,6 +38,7 @@ const ANALYTICS_SHELL_ITEMS = [
}>;
const ANALYTICS_DESCRIPTIONS: Record = {
+ sessions: 'Track Session activity by owner, status, and source.',
pullRequests:
'Track pull request activity by user, status, repository, and author.',
tasks: 'Track task activity by user, environment, source, and task type.',
diff --git a/apps/web/src/app/(authenticated)/home/Home.tsx b/apps/web/src/app/(authenticated)/home/Home.tsx
index 803e5b274..3b493ab95 100644
--- a/apps/web/src/app/(authenticated)/home/Home.tsx
+++ b/apps/web/src/app/(authenticated)/home/Home.tsx
@@ -148,8 +148,10 @@ export function Home({
const {
cloudEnabled,
isAdmin,
+ featureFlags,
managedAccess = DEFAULT_MANAGED_DEPLOYMENT_ACCESS,
} = useAuthorizedUser();
+ const sessionsUiEnabled = featureFlags?.sessions_ui === true;
const canSelectBranch = false;
@@ -406,11 +408,16 @@ export function Home({
const navigateToTaskRun = (result: {
success: boolean;
taskId?: string;
+ sessionId?: string;
error?: string;
}) => {
if (result.success && 'taskId' in result) {
setIsExiting(true);
- router.push(`/task/${result.taskId}`);
+ router.push(
+ sessionsUiEnabled && result.sessionId
+ ? `/sessions/${result.sessionId}?task=${result.taskId}`
+ : `/task/${result.taskId}`,
+ );
} else if ('error' in result) {
toast.error(result.error);
}
@@ -655,6 +662,16 @@ export function Home({
return;
}
+ if (isAutoWorkspace && sessionsUiEnabled) {
+ if (!submission.description && !submission.images?.length) return;
+ await startFastSession({
+ text: submission.description ?? '',
+ images: submission.images,
+ model: selectedModelId,
+ });
+ return;
+ }
+
if (isAutoWorkspace) {
await handleAutoSubmit(submission);
return;
@@ -688,6 +705,7 @@ export function Home({
wiggleWorkspace,
startFastSession,
selectedModelId,
+ sessionsUiEnabled,
],
);
@@ -722,7 +740,7 @@ export function Home({
diff --git a/apps/web/src/app/(authenticated)/sessions/SessionCard.tsx b/apps/web/src/app/(authenticated)/sessions/SessionCard.tsx
new file mode 100644
index 000000000..e0fb69848
--- /dev/null
+++ b/apps/web/src/app/(authenticated)/sessions/SessionCard.tsx
@@ -0,0 +1,87 @@
+import Link from 'next/link';
+import { formatDistanceToNow } from 'date-fns';
+
+import { getUserDisplayName } from '@/lib';
+import { Avatar, Badge } from '@/components/system';
+
+type SessionCardData = {
+ id: string;
+ title: string;
+ ownerName: string | null;
+ ownerEmail: string | null;
+ ownerImageUrl: string | null;
+ sourceSurface: string;
+ activityAt: number;
+ cachedStatus: 'active' | 'needs_input' | 'blocked' | 'ready' | null;
+ executionCount: number;
+ inferenceCostMicroUsd: number;
+ unread: boolean;
+ tasks: Array<{
+ taskId: string;
+ workflow: string;
+ repositoryName: string | null;
+ }>;
+};
+
+const STATUS_VARIANTS = {
+ active: 'success',
+ needs_input: 'warning',
+ blocked: 'destructive',
+ ready: 'secondary',
+} as const;
+
+export function SessionCard({ session }: { session: SessionCardData }) {
+ const owner =
+ getUserDisplayName({
+ name: session.ownerName,
+ email: session.ownerEmail,
+ }) ?? 'Roomote';
+ const primaryTask = session.tasks[0];
+ const status = session.cachedStatus ?? 'ready';
+
+ return (
+
+
+
+ {session.unread ? (
+
+ ) : null}
+
+
+
+
+ {session.title}
+
+
+ {formatDistanceToNow(new Date(session.activityAt * 1000), {
+ addSuffix: true,
+ })}
+
+
+
+
+ {status.replace('_', ' ')}
+
+ {session.executionCount} executions
+ {session.sourceSurface}
+ {primaryTask?.repositoryName ? (
+ {primaryTask.repositoryName}
+ ) : null}
+ ${(session.inferenceCostMicroUsd / 1_000_000).toFixed(4)}
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(authenticated)/sessions/SessionsFilters.tsx b/apps/web/src/app/(authenticated)/sessions/SessionsFilters.tsx
index 51b8c9fb2..60452c892 100644
--- a/apps/web/src/app/(authenticated)/sessions/SessionsFilters.tsx
+++ b/apps/web/src/app/(authenticated)/sessions/SessionsFilters.tsx
@@ -5,13 +5,42 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import type { TimePeriodFilter } from '@/types';
import { TaskFilters } from '@/components/tasks';
+import {
+ Button,
+ Input,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/system';
export function SessionsFilters({
userId,
timePeriod,
+ unified = false,
+ scope = 'all',
+ status = 'all',
+ view = 'list',
+ query = '',
+ repository = null,
+ pullRequest = null,
+ model = null,
+ source = 'all',
+ environment = '',
}: {
userId: string | null;
timePeriod: TimePeriodFilter;
+ unified?: boolean;
+ scope?: string;
+ status?: string;
+ view?: string;
+ query?: string;
+ repository?: string | null;
+ pullRequest?: string | null;
+ model?: string | null;
+ source?: string;
+ environment?: string;
}) {
const router = useRouter();
const pathname = usePathname();
@@ -30,37 +59,166 @@ export function SessionsFilters({
);
return (
-
- updateParams((params) => {
- if (id && id !== 'all') {
- params.set('user', id);
- } else {
- params.delete('user');
- }
- })
- }
- onRepositoryChange={() => {}}
- onPullRequestChange={() => {}}
- onModelChange={() => {}}
- onTimePeriodChange={(period) =>
- updateParams((params) => {
- if (period === 'all') {
- params.delete('period');
- } else {
- params.set('period', String(period));
- }
- })
- }
- showRepository={false}
- showPullRequest={false}
- showModel={false}
- showTaskType={false}
- />
+
+ {unified ? (
+ <>
+
+
+
+
+
+ >
+ ) : null}
+
+ updateParams((params) => {
+ if (id && id !== 'all') {
+ params.set('user', id);
+ } else {
+ params.delete('user');
+ }
+ })
+ }
+ onRepositoryChange={(value) =>
+ updateParams((params) => {
+ if (value) params.set('repository', value);
+ else params.delete('repository');
+ })
+ }
+ onPullRequestChange={(value) =>
+ updateParams((params) => {
+ if (value) params.set('pullRequest', value);
+ else params.delete('pullRequest');
+ })
+ }
+ onModelChange={(value) =>
+ updateParams((params) => {
+ if (value) params.set('model', value);
+ else params.delete('model');
+ })
+ }
+ onTimePeriodChange={(period) =>
+ updateParams((params) => {
+ if (period === 'all') {
+ params.delete('period');
+ } else {
+ params.set('period', String(period));
+ }
+ })
+ }
+ showRepository={unified}
+ showPullRequest={unified}
+ showModel={unified}
+ showTaskType={false}
+ />
+
);
}
diff --git a/apps/web/src/app/(authenticated)/sessions/page.tsx b/apps/web/src/app/(authenticated)/sessions/page.tsx
index 8a794905c..e7abfa15b 100644
--- a/apps/web/src/app/(authenticated)/sessions/page.tsx
+++ b/apps/web/src/app/(authenticated)/sessions/page.tsx
@@ -4,25 +4,145 @@ import { notFound } from 'next/navigation';
import { parseTimePeriodParam } from '@/types';
import { authorize } from '@/lib/server/auth-context';
import { getFastSessions } from '@/lib/server/fast-sessions';
+import { getSessions, type SessionScope } from '@/lib/server/sessions';
import { Empty, EmptyDescription, EmptyHeader } from '@/components/system';
import { FastSessionCard } from './FastSessionCard';
import { SessionsFilters } from './SessionsFilters';
+import { SessionCard } from './SessionCard';
export default async function SessionsPage({
searchParams,
}: {
- searchParams?: Promise<{ before?: string; user?: string; period?: string }>;
+ searchParams?: Promise<{
+ before?: string;
+ user?: string;
+ period?: string;
+ scope?: string;
+ status?: string;
+ view?: string;
+ q?: string;
+ repository?: string;
+ environment?: string;
+ pullRequest?: string;
+ source?: string;
+ model?: string;
+ }>;
}) {
- const [authorizedUser, { before, user, period } = {}] = await Promise.all([
+ const [authorizedUser, params = {}] = await Promise.all([
authorize(),
searchParams,
]);
if (!authorizedUser.success) {
notFound();
}
+ const { before, user, period, q } = params;
+ const unified = authorizedUser.featureFlags.sessions_ui === true;
+ const scope = ['all', 'tasks', 'reviews', 'automations'].includes(
+ params.scope ?? '',
+ )
+ ? (params.scope as SessionScope)
+ : 'all';
+ const status = ['active', 'needs_input', 'blocked', 'ready'].includes(
+ params.status ?? '',
+ )
+ ? (params.status as 'active' | 'needs_input' | 'blocked' | 'ready')
+ : undefined;
+ const view = params.view === 'board' ? 'board' : 'list';
const timePeriod = parseTimePeriodParam(period ?? null, 'all');
+ if (unified) {
+ const result = await getSessions(authorizedUser, {
+ before,
+ user,
+ period: timePeriod,
+ scope,
+ status,
+ q,
+ repository: params.repository,
+ environment: params.environment,
+ pullRequest: params.pullRequest,
+ source: params.source,
+ model: params.model,
+ });
+ const olderParams = new URLSearchParams();
+ Object.entries(params).forEach(([key, value]) => {
+ if (value && key !== 'before') olderParams.set(key, value);
+ });
+ if (result.nextCursor) olderParams.set('before', result.nextCursor);
+ const columns = ['active', 'needs_input', 'blocked', 'ready'] as const;
+
+ return (
+
+
+
+
+
+ {result.sessions.length === 0 ? (
+
+
+ No sessions found.
+
+
+ ) : view === 'board' ? (
+
+ {columns.map((column) => (
+
+
+ {column.replace('_', ' ')}
+
+
+ {result.sessions
+ .filter((session) =>
+ column === 'ready'
+ ? !session.cachedStatus ||
+ session.cachedStatus === column
+ : session.cachedStatus === column,
+ )
+ .map((session) => (
+
+ ))}
+
+
+ ))}
+
+ ) : (
+
+ {result.sessions.map((session) => (
+
+ ))}
+
+ )}
+ {result.nextCursor ? (
+
+
+ Show older sessions
+
+
+ ) : null}
+
+
+ );
+ }
const { sessions, nextCursor } = await getFastSessions(authorizedUser, {
before,
filterUserId: user ?? null,
diff --git a/apps/web/src/app/(authenticated)/tasks/page.tsx b/apps/web/src/app/(authenticated)/tasks/page.tsx
index 32b5c5f48..72895ac17 100644
--- a/apps/web/src/app/(authenticated)/tasks/page.tsx
+++ b/apps/web/src/app/(authenticated)/tasks/page.tsx
@@ -1,13 +1,16 @@
'use client';
import { useEffect } from 'react';
-import { useSearchParams } from 'next/navigation';
+import { useRouter, useSearchParams } from 'next/navigation';
import { toast } from 'sonner';
import { Tasks } from './Tasks';
+import { useAuthorizedUser } from '@/hooks/useUser';
export default function Page() {
const searchParams = useSearchParams();
+ const router = useRouter();
+ const { featureFlags } = useAuthorizedUser();
const error = searchParams.get('error');
useEffect(() => {
@@ -16,5 +19,26 @@ export default function Page() {
}
}, [error]);
+ useEffect(() => {
+ if (featureFlags?.sessions_ui !== true) return;
+ const mapped = new URLSearchParams();
+ mapped.set('scope', 'tasks');
+ const mappings = [
+ ['userId', 'user'],
+ ['timePeriod', 'period'],
+ ['repositoryName', 'repository'],
+ ['pullRequest', 'pullRequest'],
+ ['model', 'model'],
+ ['view', 'view'],
+ ] as const;
+ for (const [from, to] of mappings) {
+ const value = searchParams.get(from);
+ if (value) mapped.set(to, value);
+ }
+ router.replace(`/sessions?${mapped.toString()}`);
+ }, [featureFlags?.sessions_ui, router, searchParams]);
+
+ if (featureFlags?.sessions_ui === true) return null;
+
return ;
}
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/FastSessionTranscript.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/FastSessionTranscript.tsx
index 8066694fc..2b1bd801f 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/FastSessionTranscript.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/FastSessionTranscript.tsx
@@ -1,6 +1,13 @@
'use client';
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+ type ReactNode,
+} from 'react';
import {
ACP_ENVELOPE_EVENT_TYPES,
getImageUrisFromContentBlocks,
@@ -61,6 +68,8 @@ export function FastSessionTranscript({
sessionReasoningEffort = null,
defaultModelId = null,
defaultReasoningEffort = null,
+ headerExtras,
+ timelineExtras,
}: {
sessionId: string;
initialMessages: FastSessionMessage[];
@@ -72,6 +81,8 @@ export function FastSessionTranscript({
sessionReasoningEffort?: ReasoningEffort | null;
defaultModelId?: string | null;
defaultReasoningEffort?: ReasoningEffort | null;
+ headerExtras?: ReactNode;
+ timelineExtras?: ReactNode;
}) {
const trpcClient = useTRPCClient();
const [serverMessages, setServerMessages] = useState<
@@ -259,6 +270,7 @@ export function FastSessionTranscript({
{title ?? fallbackTitle}
+ {headerExtras}
@@ -267,6 +279,7 @@ export function FastSessionTranscript({
Older messages in this session are not shown.
) : null}
+ {timelineExtras}
{
+ recordVisit(sessionId);
+ capture('session_opened', { surface: 'web', outcome: 'opened' });
+ const markRead = async () => {
+ if (document.visibilityState !== 'visible') return;
+ const timeline = await trpc.sessions.timeline.query({ sessionId });
+ const last = timeline?.events.findLast((event) => !event.own);
+ if (!last) return;
+ await trpc.sessions.markRead.mutate({
+ sessionId,
+ throughEventAt: last.at,
+ throughEventId: last.id,
+ });
+ };
+ void markRead();
+ window.addEventListener('focus', markRead);
+ document.addEventListener('visibilitychange', markRead);
+ return () => {
+ window.removeEventListener('focus', markRead);
+ document.removeEventListener('visibilitychange', markRead);
+ };
+ }, [capture, recordVisit, sessionId, trpc]);
+
+ return null;
+}
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionTaskCards.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionTaskCards.tsx
new file mode 100644
index 000000000..dc1615898
--- /dev/null
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionTaskCards.tsx
@@ -0,0 +1,169 @@
+'use client';
+
+import Link from 'next/link';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { useMutation } from '@tanstack/react-query';
+import { toast } from 'sonner';
+
+import {
+ Badge,
+ Button,
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from '@/components/system';
+import { useTRPC } from '@/trpc/client';
+
+export type SessionTaskSummary = {
+ taskId: string;
+ title: string;
+ workflow: string;
+ state: string;
+ repositoryName: string | null;
+ latestOutput: string | null;
+ inferenceCostMicroUsd: number;
+ canAccessDetails?: boolean;
+ latestRun: {
+ id: number;
+ status: string;
+ taskPhase: string | null;
+ error: string | null;
+ result: unknown;
+ } | null;
+ artifacts: Array<{
+ id: string;
+ path: string;
+ artifactType: string;
+ }>;
+ pullRequests: Array<{
+ id: string;
+ url: string;
+ number: number | null;
+ title: string | null;
+ repository: string | null;
+ status: string | null;
+ }>;
+};
+
+export function SessionTaskCards({
+ sessionId,
+ tasks,
+}: {
+ sessionId: string;
+ tasks: SessionTaskSummary[];
+}) {
+ const trpc = useTRPC();
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const cancel = useMutation(trpc.taskRuns.cancel.mutationOptions());
+ const retry = useMutation(trpc.taskRuns.retryFailedStart.mutationOptions());
+
+ if (tasks.length === 0) return null;
+
+ const selectTask = (taskId: string) => {
+ const params = new URLSearchParams(searchParams);
+ params.set('task', taskId);
+ router.replace(`/sessions/${sessionId}?${params.toString()}`);
+ };
+
+ return (
+
+
+ Executions
+
+
+ {tasks.map((task) => (
+
+
+
+
+ {task.title}
+
+
+ {task.state}
+
+
+
+
+ {task.repositoryName ?? task.workflow}
+ {task.latestRun?.error ? (
+
+ {task.latestRun.error}
+
+ ) : null}
+ {task.latestOutput ? (
+ {task.latestOutput}
+ ) : null}
+
+ ${(task.inferenceCostMicroUsd / 1_000_000).toFixed(4)} inference
+
+ {task.canAccessDetails === false ? (
+ Execution details require task access.
+ ) : null}
+
+
+ {task.canAccessDetails === false ? null : task.state ===
+ 'active' ? (
+
+ ) : task.state === 'failed' ? (
+
+ ) : null}
+ {task.canAccessDetails === false ? null : (
+ <>
+
+
+ >
+ )}
+
+
+ ))}
+
+
+ );
+}
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
index ade5d409c..d60249c0e 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
@@ -1,10 +1,13 @@
'use client';
-import { useState, type ReactNode } from 'react';
+import Link from 'next/link';
+import { useCallback, useEffect, useState, type ReactNode } from 'react';
+import { useRouter, useSearchParams } from 'next/navigation';
import { formatDistanceToNow } from 'date-fns';
import { formatInferenceCost, getUserDisplayName } from '@/lib';
import { useLaunchTaskModels } from '@/hooks/task-models/useLaunchTaskModels';
+import { useIsMobile } from '@/hooks/useIsMobile';
import { WorkspaceSurface } from '@/components/layout';
import { SideNavItem } from '@/components/layout/side-nav/SideNavItem';
import {
@@ -17,7 +20,18 @@ import {
ResizablePanel,
ResizablePanelGroup,
X,
+ Rows4,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+ Drawer,
+ DrawerContent,
+ DrawerHeader,
+ DrawerTitle,
} from '@/components/system';
+import type { SessionTaskSummary } from './SessionTaskCards';
export type SessionInfo = {
id: string;
@@ -29,6 +43,7 @@ export type SessionInfo = {
model: string | null;
inferenceCostMicroUsd: number;
createdAt: Date;
+ tasks: SessionTaskSummary[];
};
const SURFACE_LABELS: Record = {
@@ -47,6 +62,110 @@ function InfoRow({ label, children }: { label: string; children: ReactNode }) {
);
}
+function SessionTaskPanel({
+ sessionId,
+ task,
+ tasks,
+ onSelect,
+ onClose,
+}: {
+ sessionId: string;
+ task: SessionTaskSummary;
+ tasks: SessionTaskSummary[];
+ onSelect: (taskId: string) => void;
+ onClose: () => void;
+}) {
+ return (
+ <>
+
+
Execution details
+
+
+
+
+
+ {tasks.length > 1 ? (
+
+ ) : null}
+
+
{task.title}
+
{task.state}
+ {task.repositoryName ? (
+
{task.repositoryName}
+ ) : null}
+
+ {task.canAccessDetails === false ? (
+
+ Execution details require task access.
+
+ ) : null}
+ {task.latestRun?.error ? (
+
+ {task.latestRun.error}
+
+ ) : null}
+ {task.pullRequests.length ? (
+
+ ) : null}
+ {task.artifacts.length ? (
+
+ Artifacts
+ {task.artifacts.map((artifact) => (
+
+ {artifact.path}
+
+ ))}
+
+ ) : null}
+ {task.canAccessDetails === false ? null : (
+
+ )}
+
+ >
+ );
+}
+
function SessionInfoPanel({
session,
onClose,
@@ -128,6 +247,47 @@ export function SessionWorkspace({
children: ReactNode;
}) {
const [isInfoOpen, setIsInfoOpen] = useState(false);
+ const isMobile = useIsMobile();
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const selectedTaskId = searchParams.get('task');
+ const selectedTask = session.tasks.find(
+ (task) => task.taskId === selectedTaskId,
+ );
+ const panelOpen = isInfoOpen || Boolean(selectedTask);
+
+ const selectTask = useCallback(
+ (taskId: string | null) => {
+ const params = new URLSearchParams(searchParams);
+ if (taskId) params.set('task', taskId);
+ else params.delete('task');
+ const query = params.toString();
+ router.replace(`/sessions/${session.id}${query ? `?${query}` : ''}`);
+ },
+ [router, searchParams, session.id],
+ );
+
+ useEffect(() => {
+ if (!selectedTaskId && session.tasks.length === 1) {
+ selectTask(session.tasks[0]!.taskId);
+ }
+ }, [selectTask, selectedTaskId, session.tasks]);
+
+ const closePanel = () => {
+ setIsInfoOpen(false);
+ selectTask(null);
+ };
+ const panelContent = selectedTask ? (
+
+ ) : (
+
+ );
return (
setIsInfoOpen((previous) => !previous)}
/>
+ {session.tasks.length ? (
+
+ selectTask(selectedTask ? null : session.tasks[0]!.taskId)
+ }
+ />
+ ) : null}
}
>
-
-
- {children}
-
- {isInfoOpen && (
- <>
-
-
- setIsInfoOpen(false)}
- />
-
- >
- )}
-
+ {isMobile ? (
+ <>
+ {children}
+ {
+ if (!open) closePanel();
+ }}
+ >
+
+
+ Session execution details
+
+ {panelContent}
+
+
+ >
+ ) : (
+
+
+ {children}
+
+ {panelOpen ? (
+ <>
+
+
+ {panelContent}
+
+ >
+ ) : null}
+
+ )}
);
}
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.test.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.test.tsx
index 8d8250dc6..93aebc7ea 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.test.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.test.tsx
@@ -14,6 +14,10 @@ const { authorizeMock, getFastSessionByIdMock, transcriptMock } = vi.hoisted(
);
vi.mock('@/lib/server/auth-context', () => ({ authorize: authorizeMock }));
+vi.mock('next/navigation', () => ({
+ useRouter: () => ({ replace: vi.fn() }),
+ useSearchParams: () => new URLSearchParams(),
+}));
vi.mock('@/lib/server/fast-sessions', () => ({
getFastSessionById: getFastSessionByIdMock,
}));
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx
index c042a4893..9e37bb646 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx
@@ -9,9 +9,14 @@ import {
import { authorize } from '@/lib/server/auth-context';
import { getFastSessionById } from '@/lib/server/fast-sessions';
+import { getSessionByIdCommand } from '@/trpc/commands/sessions';
+import { Badge } from '@/components/system';
+import { WorkspaceHeader } from '@/components/layout';
import { FastSessionTranscript } from './FastSessionTranscript';
import { SessionWorkspace, type SessionInfo } from './SessionWorkspace';
+import { SessionTaskCards } from './SessionTaskCards';
+import { SessionReadTracker } from './SessionReadTracker';
export default async function SessionDetailPage({
params,
@@ -26,7 +31,96 @@ export default async function SessionDetailPage({
notFound();
}
- const session = await getFastSessionById(authorizedUser, sessionId);
+ const unifiedSession = authorizedUser.featureFlags?.sessions_ui
+ ? await getSessionByIdCommand(authorizedUser, sessionId)
+ : null;
+ const session = unifiedSession?.fastConversationId
+ ? await getFastSessionById(
+ authorizedUser,
+ unifiedSession.fastConversationId,
+ )
+ : unifiedSession
+ ? null
+ : await getFastSessionById(authorizedUser, sessionId);
+ if (unifiedSession) {
+ const modelEnv: Record =
+ await resolveEffectiveModelRuntimeEnv().catch(() => ({}));
+ const defaultModelId =
+ modelEnv.R_ORCHESTRATION_MODEL || modelEnv.R_MODEL || null;
+ const rawDefaultEffort = modelEnv.R_ORCHESTRATION_MODEL_REASONING_EFFORT;
+ const defaultReasoningEffort = REASONING_EFFORT_VALUES.includes(
+ rawDefaultEffort as ReasoningEffort,
+ )
+ ? (rawDefaultEffort as ReasoningEffort)
+ : null;
+ const sessionInfo: SessionInfo = {
+ id: unifiedSession.id,
+ ownerName: unifiedSession.ownerName,
+ ownerEmail: unifiedSession.ownerEmail,
+ ownerImageUrl: unifiedSession.ownerImageUrl,
+ surface: unifiedSession.sourceSurface,
+ model: session?.model ?? defaultModelId,
+ inferenceCostMicroUsd: unifiedSession.inferenceCostMicroUsd,
+ createdAt: unifiedSession.createdAt,
+ tasks: unifiedSession.tasks,
+ };
+ const statusVariant =
+ unifiedSession.status === 'active'
+ ? 'success'
+ : unifiedSession.status === 'needs_input'
+ ? 'warning'
+ : unifiedSession.status === 'blocked'
+ ? 'destructive'
+ : 'secondary';
+ const taskCards = (
+
+ );
+
+ return (
+
+
+
+ {session ? (
+
+ {unifiedSession.status.replace('_', ' ')}
+
+ }
+ timelineExtras={taskCards}
+ />
+ ) : (
+ <>
+
+
+ {unifiedSession.title}
+
+
+ {unifiedSession.status.replace('_', ' ')}
+
+
+
+ >
+ )}
+
+
+ );
+ }
if (!session) {
notFound();
}
@@ -53,6 +147,7 @@ export default async function SessionDetailPage({
model: session.model ?? defaultModelId,
inferenceCostMicroUsd: session.inferenceCostMicroUsd,
createdAt: session.createdAt,
+ tasks: [],
};
const initialUserMessage = session.messages.find(
(message) => message.role === 'user',
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/Header.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/Header.tsx
index 282df1030..e1dd8f108 100644
--- a/apps/web/src/app/(sandbox)/task/[taskId]/Header.tsx
+++ b/apps/web/src/app/(sandbox)/task/[taskId]/Header.tsx
@@ -1,7 +1,9 @@
'use client';
import { useEffect, useState, type KeyboardEvent } from 'react';
-import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import Link from 'next/link';
+import { useSearchParams } from 'next/navigation';
import { toast } from 'sonner';
import {
ArrowLeftFromLine,
@@ -12,6 +14,12 @@ import {
DialogHeader,
DialogTitle,
Input,
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
} from '@/components/system';
import { PullRequestBadge, WorkspaceBadge } from '@/components/sandbox';
import { WorkspaceHeader } from '@/components/layout';
@@ -20,6 +28,7 @@ import { useTRPC } from '@/trpc/client';
import { useSandboxLayout } from '../../use-sandbox-layout';
import { type TaskSession } from './hooks';
+import { TaskSessionReadTracker } from './TaskSessionReadTracker';
interface HeaderProps {
session: TaskSession;
@@ -28,9 +37,18 @@ interface HeaderProps {
export const Header = ({ session: { taskRun, task, taskId } }: HeaderProps) => {
const { isSidebarVisible, toggleSidebar } = useSandboxLayout();
const trpc = useTRPC();
+ const searchParams = useSearchParams();
const queryClient = useQueryClient();
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
const [titleDraft, setTitleDraft] = useState(task?.title ?? '');
+ const parentSessionOptions = trpc.sessions?.forTask?.queryOptions({
+ taskId,
+ }) ?? {
+ queryKey: ['sessions', 'for-task', 'disabled', taskId],
+ queryFn: async () => null,
+ enabled: false,
+ };
+ const { data: parentSession } = useQuery(parentSessionOptions);
const environmentId = taskRun?.payload?.environmentId;
const repo = taskRun?.payload?.repo;
@@ -150,21 +168,65 @@ export const Header = ({ session: { taskRun, task, taskId } }: HeaderProps) => {
};
const title = task?.title || 'Untitled task';
+ const returnTo = searchParams?.get('returnTo');
+ const safeReturnTo =
+ returnTo?.startsWith('/sessions') && !returnTo.startsWith('//')
+ ? returnTo
+ : '/sessions';
return (
<>
+ {parentSession ? (
+
+ ) : null}
-
- {title}
-
+ {parentSession ? (
+
+
+
+
+ Sessions
+
+
+
+
+
+
+ {parentSession.title}
+
+
+
+
+
+
+ {title}
+
+
+
+
+ ) : (
+
+ {title}
+
+ )}
{badges.length > 0 && (
{badges.map((badge, index) => (
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/TaskSessionReadTracker.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/TaskSessionReadTracker.tsx
new file mode 100644
index 000000000..cff3edc13
--- /dev/null
+++ b/apps/web/src/app/(sandbox)/task/[taskId]/TaskSessionReadTracker.tsx
@@ -0,0 +1,28 @@
+'use client';
+
+import { useEffect } from 'react';
+
+import { useTRPCClient } from '@/trpc/client';
+
+export function TaskSessionReadTracker({ sessionId }: { sessionId: string }) {
+ const trpc = useTRPCClient();
+
+ useEffect(() => {
+ const markRead = async () => {
+ if (document.visibilityState !== 'visible') return;
+ const timeline = await trpc.sessions.timeline.query({ sessionId });
+ const last = timeline?.events.findLast((event) => !event.own);
+ if (!last) return;
+ await trpc.sessions.markRead.mutate({
+ sessionId,
+ throughEventAt: last.at,
+ throughEventId: last.id,
+ });
+ };
+ void markRead();
+ window.addEventListener('focus', markRead);
+ return () => window.removeEventListener('focus', markRead);
+ }, [sessionId, trpc]);
+
+ return null;
+}
diff --git a/apps/web/src/components/layout/CommandPalette.tsx b/apps/web/src/components/layout/CommandPalette.tsx
index b45af7eb7..bc0d521c5 100644
--- a/apps/web/src/components/layout/CommandPalette.tsx
+++ b/apps/web/src/components/layout/CommandPalette.tsx
@@ -111,7 +111,11 @@ function AuthorizedCommandPalette() {
const navItems = useMemo
(() => {
const items: NavItem[] = [
{ icon: Plus, label: 'New Task', href: '/' },
- { icon: GalleryVerticalEnd, label: 'Tasks', href: '/tasks' },
+ {
+ icon: GalleryVerticalEnd,
+ label: user?.featureFlags?.sessions_ui ? 'Sessions' : 'Tasks',
+ href: user?.featureFlags?.sessions_ui ? '/sessions' : '/tasks',
+ },
{ icon: Settings, label: 'Settings', href: '/settings' },
{ icon: HelpCircle, label: 'Help', action: 'contact-support' },
];
@@ -133,7 +137,7 @@ function AuthorizedCommandPalette() {
);
}
return items;
- }, [user?.isAdmin]);
+ }, [user?.featureFlags?.sessions_ui, user?.isAdmin]);
// Debounce search input
useEffect(() => {
@@ -157,6 +161,15 @@ function AuthorizedCommandPalette() {
{ enabled: open },
),
);
+ const sessionSearchOptions = trpc.sessions?.search?.queryOptions(
+ { query: debouncedSearch, limit: SEARCH_TASKS_LIMIT },
+ { enabled: open && user?.featureFlags?.sessions_ui === true },
+ ) ?? {
+ queryKey: ['sessions', 'search', 'disabled'],
+ queryFn: async () => null,
+ enabled: false,
+ };
+ const { data: sessionResults } = useQuery(sessionSearchOptions);
// Promote recently-visited tasks to the top of the list
const sortedTasks = useMemo(() => {
@@ -281,6 +294,23 @@ function AuthorizedCommandPalette() {
>
)}
+ {(sessionResults?.sessions?.length ?? 0) > 0 ? (
+
+ {sessionResults!.sessions.map((session) => (
+ navigate(`/sessions/${session.id}`)}
+ >
+ {session.title}
+
+ {session.executionCount} executions
+
+
+ ))}
+
+ ) : null}
+
{commandGroups.size > 0 &&
Array.from(commandGroups.entries()).map(([group, cmds]) => (
diff --git a/apps/web/src/components/layout/navbar/NavbarDrawer.tsx b/apps/web/src/components/layout/navbar/NavbarDrawer.tsx
index e821b965c..066357025 100644
--- a/apps/web/src/components/layout/navbar/NavbarDrawer.tsx
+++ b/apps/web/src/components/layout/navbar/NavbarDrawer.tsx
@@ -17,8 +17,11 @@ import { getVisiblePrimaryNavItems } from '../navigation-items';
export const NavbarDrawer = () => {
const pathname = usePathname();
- const { isAdmin } = useAuthorizedUser();
- const visibleNavItems = getVisiblePrimaryNavItems({ isAdmin });
+ const { isAdmin, featureFlags } = useAuthorizedUser();
+ const visibleNavItems = getVisiblePrimaryNavItems({
+ isAdmin,
+ sessionsUi: featureFlags?.sessions_ui === true,
+ });
const [open, setOpen] = useState(false);
diff --git a/apps/web/src/components/layout/navigation-items.ts b/apps/web/src/components/layout/navigation-items.ts
index da059f972..3436fd698 100644
--- a/apps/web/src/components/layout/navigation-items.ts
+++ b/apps/web/src/components/layout/navigation-items.ts
@@ -1,6 +1,5 @@
import { type LucideIcon } from '@/components/system';
-import { ChartColumnIncreasing, House, Zap } from '@/components/system';
-import { Rows4 } from 'lucide-react';
+import { ChartColumnIncreasing, House, Rows4, Zap } from '@/components/system';
interface PrimaryNavItem {
icon: LucideIcon;
@@ -52,6 +51,17 @@ const PRIMARY_NAV_ITEMS: PrimaryNavItem[] = [
export function getVisiblePrimaryNavItems(opts: {
isAdmin: boolean;
+ sessionsUi?: boolean;
}): PrimaryNavItem[] {
- return PRIMARY_NAV_ITEMS.filter((item) => !item.adminOnly || opts.isAdmin);
+ return PRIMARY_NAV_ITEMS.map((item) =>
+ item.href === '/tasks' && opts.sessionsUi
+ ? {
+ ...item,
+ href: '/sessions',
+ label: 'Sessions',
+ description: 'View conversations and delegated work',
+ matchPaths: ['/sessions', '/tasks', '/cloud-agents'],
+ }
+ : item,
+ ).filter((item) => !item.adminOnly || opts.isAdmin);
}
diff --git a/apps/web/src/components/layout/side-nav/SideNav.tsx b/apps/web/src/components/layout/side-nav/SideNav.tsx
index de06fdb48..24f1aafc6 100644
--- a/apps/web/src/components/layout/side-nav/SideNav.tsx
+++ b/apps/web/src/components/layout/side-nav/SideNav.tsx
@@ -71,7 +71,7 @@ export const SideNav = () => {
const pathname = usePathname();
const { setOpen: openCommandPalette } = useCommandPalette();
- const { isAdmin } = useAuthorizedUser();
+ const { isAdmin, featureFlags } = useAuthorizedUser();
const hasHydrated = useLayoutStore((state) => state.hasHydrated);
const persistedIsSideNavExpanded = useLayoutStore(
(state) => state.isSideNavExpanded,
@@ -204,8 +204,12 @@ export const SideNav = () => {
return Array.from(groups.values());
}, [environmentNameById, nonPinnedQuickAccessTasks]);
const visibleNavItems = useMemo(
- () => getVisiblePrimaryNavItems({ isAdmin }),
- [isAdmin],
+ () =>
+ getVisiblePrimaryNavItems({
+ isAdmin,
+ sessionsUi: featureFlags?.sessions_ui === true,
+ }),
+ [featureFlags?.sessions_ui, isAdmin],
);
return (
diff --git a/apps/web/src/hooks/useRecentSessions.ts b/apps/web/src/hooks/useRecentSessions.ts
new file mode 100644
index 000000000..b109a171b
--- /dev/null
+++ b/apps/web/src/hooks/useRecentSessions.ts
@@ -0,0 +1,42 @@
+'use client';
+
+import { useCallback, useEffect, useState } from 'react';
+
+import { useAuthorizedUser } from './useUser';
+
+const MAX_RECENT_SESSIONS = 20;
+
+export function useRecentSessions() {
+ const { userId } = useAuthorizedUser();
+ const storageKey = `roomote-recent-sessions:${userId}`;
+ const [recentSessionIds, setRecentSessionIds] = useState([]);
+
+ useEffect(() => {
+ try {
+ const stored = JSON.parse(localStorage.getItem(storageKey) ?? '[]');
+ setRecentSessionIds(Array.isArray(stored) ? stored.slice(0, 20) : []);
+ } catch {
+ setRecentSessionIds([]);
+ }
+ }, [storageKey]);
+
+ const recordVisit = useCallback(
+ (sessionId: string) => {
+ setRecentSessionIds((current) => {
+ const next = [
+ sessionId,
+ ...current.filter((id) => id !== sessionId),
+ ].slice(0, MAX_RECENT_SESSIONS);
+ try {
+ localStorage.setItem(storageKey, JSON.stringify(next));
+ } catch {
+ // Local recents are best-effort.
+ }
+ return next;
+ });
+ },
+ [storageKey],
+ );
+
+ return { recentSessionIds, recordVisit };
+}
diff --git a/apps/web/src/lib/server/analytics/index.ts b/apps/web/src/lib/server/analytics/index.ts
index 456650d9d..cf003c225 100644
--- a/apps/web/src/lib/server/analytics/index.ts
+++ b/apps/web/src/lib/server/analytics/index.ts
@@ -23,6 +23,7 @@ import {
getCostAnalyticsRows,
} from './cost-rows';
import { getTaskAnalyticsRows } from './task-rows';
+import { getSessionAnalyticsRows } from './session-rows';
import {
buildPullRequestAnalyticsSummary,
getPullRequestAnalyticsRows,
@@ -39,6 +40,8 @@ async function getAnalyticsRows(
metric: AnalyticsMetric = getDefaultAnalyticsMetric(object),
) {
switch (object) {
+ case 'sessions':
+ return getSessionAnalyticsRows(auth, timePeriod, now);
case 'tasks':
return getTaskAnalyticsRows(auth, timePeriod, now, metric);
case 'pullRequests':
@@ -53,6 +56,17 @@ function getAnalyticsDetailsColumns(
metric: AnalyticsMetric = getDefaultAnalyticsMetric(object),
): AnalyticsDetailsColumn[] {
switch (object) {
+ case 'sessions':
+ return [
+ { key: 'date', label: 'Date' },
+ { key: 'user', label: 'User' },
+ { key: 'source', label: 'Source' },
+ { key: 'status', label: 'Status' },
+ { key: 'ownerKind', label: 'Owner kind' },
+ { key: 'hasExecution', label: 'Has execution' },
+ { key: 'sessionTitle', label: 'Session' },
+ { key: 'session', label: 'Session Link' },
+ ];
case 'tasks': {
const columns: AnalyticsDetailsColumn[] = [
{ key: 'date', label: 'Date' },
diff --git a/apps/web/src/lib/server/analytics/session-rows.ts b/apps/web/src/lib/server/analytics/session-rows.ts
new file mode 100644
index 000000000..9cc0a9d77
--- /dev/null
+++ b/apps/web/src/lib/server/analytics/session-rows.ts
@@ -0,0 +1,84 @@
+import {
+ and,
+ db,
+ eq,
+ gte,
+ sessions,
+ sessionTasks,
+ sql,
+ users,
+} from '@roomote/db/server';
+
+import type { TimePeriodFilter, UserAuthSuccess } from '@/types';
+import { getUserDisplayName } from '@/lib';
+
+import type { AnalyticsRow } from './types';
+
+export async function getSessionAnalyticsRows(
+ _auth: UserAuthSuccess,
+ timePeriod: TimePeriodFilter | undefined,
+ now: Date,
+): Promise {
+ const rows = await db
+ .select({
+ id: sessions.id,
+ title: sessions.title,
+ ownerName: users.name,
+ ownerEmail: users.email,
+ source: sessions.sourceSurface,
+ ownerKind: sessions.ownerKind,
+ executionCount: sql`(
+ select count(*)::int from ${sessionTasks}
+ where ${sessionTasks.sessionId} = ${sessions.id}
+ )`,
+ status: sessions.cachedStatus,
+ createdAt: sessions.createdAt,
+ })
+ .from(sessions)
+ .leftJoin(users, eq(users.id, sessions.ownerUserId))
+ .where(
+ and(
+ eq(sessions.visibility, 'visible'),
+ timePeriod && timePeriod !== 'all'
+ ? gte(
+ sessions.createdAt,
+ new Date(now.getTime() - timePeriod * 24 * 60 * 60 * 1000),
+ )
+ : undefined,
+ ),
+ );
+
+ return rows.map((row) => {
+ const owner =
+ getUserDisplayName({ name: row.ownerName, email: row.ownerEmail }) ??
+ 'System';
+ const status = row.status ?? 'ready';
+ const hasExecution = row.executionCount > 0 ? 'yes' : 'no';
+ return {
+ id: row.id,
+ timestamp: row.createdAt,
+ value: 1,
+ dimensions: {
+ user: { key: owner, label: owner },
+ status: { key: status, label: status.replace('_', ' ') },
+ source: { key: row.source, label: row.source },
+ ownerKind: { key: row.ownerKind, label: row.ownerKind },
+ hasExecution: { key: hasExecution, label: hasExecution },
+ },
+ details: {
+ id: row.id,
+ values: {
+ date: row.createdAt.toISOString(),
+ user: owner,
+ source: row.source,
+ status,
+ ownerKind: row.ownerKind,
+ hasExecution,
+ sessionTitle: row.title,
+ session: 'Open',
+ },
+ links: { session: `/sessions/${row.id}` },
+ },
+ };
+ });
+}
diff --git a/apps/web/src/lib/server/auth-context.test.ts b/apps/web/src/lib/server/auth-context.test.ts
index 524ce83f8..434b2dc43 100644
--- a/apps/web/src/lib/server/auth-context.test.ts
+++ b/apps/web/src/lib/server/auth-context.test.ts
@@ -209,7 +209,7 @@ describe('authorize', () => {
expect(mockUpdateSet).not.toHaveBeenCalled();
});
- it('hydrates an empty feature flag map from stale deployment metadata', async () => {
+ it('ignores stale metadata and hydrates disabled Sessions flags', async () => {
mockDeploymentFindFirst.mockResolvedValue({
metadata: { suggestion_routing: true },
});
@@ -217,7 +217,13 @@ describe('authorize', () => {
const result = await authorize();
expect(result.success).toBe(true);
- if (result.success) expect(result.featureFlags).toEqual({});
+ if (result.success) {
+ expect(result.featureFlags).toEqual({
+ sessions_data: false,
+ sessions_ui: false,
+ sessions_comms: false,
+ });
+ }
});
it('keeps an unchanged member with incomplete onboarding read-only', async () => {
diff --git a/apps/web/src/lib/server/sessions.test.ts b/apps/web/src/lib/server/sessions.test.ts
new file mode 100644
index 000000000..4160f8d23
--- /dev/null
+++ b/apps/web/src/lib/server/sessions.test.ts
@@ -0,0 +1,146 @@
+import {
+ db,
+ fastAgentConversations,
+ fastAgentMessages,
+ sessionFactory,
+ sessionTasks,
+ taskFactory,
+ userFactory,
+} from '@roomote/db/server';
+
+import {
+ findAccessibleSession,
+ getSessionById,
+ getSessionForTask,
+ getSessions,
+ getSessionTimeline,
+ setSessionPinned,
+ updateSessionMetadata,
+} from './sessions';
+
+describe('unified Session queries', () => {
+ it('scopes list and detail reads to owners, participants, and admins', async () => {
+ const owner = await userFactory.create();
+ const stranger = await userFactory.create();
+ const session = await sessionFactory.create({
+ ownerKind: 'user',
+ ownerUserId: owner.id,
+ title: 'Visible Session',
+ });
+
+ await expect(
+ findAccessibleSession({ userId: owner.id, isAdmin: false }, session.id),
+ ).resolves.toMatchObject({ id: session.id });
+ await expect(
+ findAccessibleSession(
+ { userId: stranger.id, isAdmin: false },
+ session.id,
+ ),
+ ).resolves.toBeNull();
+ await expect(
+ findAccessibleSession({ userId: stranger.id, isAdmin: true }, session.id),
+ ).resolves.toMatchObject({ id: session.id });
+
+ const list = await getSessions(
+ { userId: owner.id, isAdmin: false },
+ { scope: 'all' },
+ );
+ expect(list.sessions.map((row) => row.id)).toContain(session.id);
+ });
+
+ it('returns task rollups, task resolution, and deterministic timeline events', async () => {
+ const owner = await userFactory.create();
+ const [conversation] = await db
+ .insert(fastAgentConversations)
+ .values({
+ userId: owner.id,
+ surface: 'web',
+ workspaceId: owner.id,
+ conversationId: crypto.randomUUID(),
+ })
+ .returning();
+ const session = await sessionFactory.create({
+ ownerKind: 'user',
+ ownerUserId: owner.id,
+ title: 'Composed Session',
+ fastConversationId: conversation!.id,
+ });
+ const task = await taskFactory.create({
+ initiatorUserId: owner.id,
+ title: 'Delegated work',
+ });
+ await db.insert(sessionTasks).values({
+ sessionId: session.id,
+ taskId: task.id,
+ origin: 'fast_delegation',
+ });
+ await db.insert(fastAgentMessages).values({
+ conversationId: conversation!.id,
+ eventId: 'message-1',
+ turnId: 'turn-1',
+ turnSeq: 0,
+ ts: 100,
+ eventType: 'roomote_runtime.user_prompt',
+ role: 'user',
+ contentBlocks: [{ type: 'text', text: 'Please delegate this' }],
+ metadata: { userId: owner.id, visibleInTranscript: true },
+ payload: {},
+ });
+
+ const detail = await getSessionById(
+ { userId: owner.id, isAdmin: false },
+ session.id,
+ );
+ expect(detail?.tasks).toEqual([
+ expect.objectContaining({ taskId: task.id, title: 'Delegated work' }),
+ ]);
+ await expect(
+ getSessionForTask({ userId: owner.id, isAdmin: false }, task.id),
+ ).resolves.toEqual({ sessionId: session.id, title: 'Composed Session' });
+ const timeline = await getSessionTimeline(
+ { userId: owner.id, isAdmin: false },
+ session.id,
+ );
+ expect(timeline?.events.map((event) => event.id)).toEqual(
+ expect.arrayContaining([
+ 'fast:message-1',
+ `task:${task.id}:delegated`,
+ `task:${task.id}:${task.state}`,
+ ]),
+ );
+ const taskEvent = timeline?.events.find(
+ (event) => event.type === 'task_delegated',
+ );
+ expect(taskEvent).not.toHaveProperty('task.latestRun');
+ expect(taskEvent).not.toHaveProperty('task.artifacts');
+ expect(taskEvent).not.toHaveProperty('task.pullRequests');
+ });
+
+ it('keeps metadata changes owner-only and stores per-user pins', async () => {
+ const owner = await userFactory.create();
+ const stranger = await userFactory.create();
+ const session = await sessionFactory.create({
+ ownerKind: 'user',
+ ownerUserId: owner.id,
+ });
+
+ await expect(
+ updateSessionMetadata(
+ { userId: stranger.id, isAdmin: false },
+ session.id,
+ { title: 'Nope' },
+ ),
+ ).resolves.toBeNull();
+ await expect(
+ updateSessionMetadata({ userId: owner.id, isAdmin: false }, session.id, {
+ title: 'Renamed',
+ }),
+ ).resolves.toMatchObject({ title: 'Renamed' });
+ await expect(
+ setSessionPinned(
+ { userId: owner.id, isAdmin: false },
+ { sessionId: session.id, pinned: true },
+ ),
+ ).resolves.toEqual({ success: true, pinned: true });
+ });
+});
diff --git a/apps/web/src/lib/server/sessions.ts b/apps/web/src/lib/server/sessions.ts
new file mode 100644
index 000000000..c57df6bb8
--- /dev/null
+++ b/apps/web/src/lib/server/sessions.ts
@@ -0,0 +1,660 @@
+import {
+ and,
+ count,
+ db,
+ desc,
+ deriveSessionStatus,
+ eq,
+ exists,
+ fastAgentMessages,
+ gte,
+ ilike,
+ inArray,
+ isNull,
+ llmUsageEvents,
+ lt,
+ or,
+ sessionParticipants,
+ sessionPins,
+ sessions,
+ sessionTasks,
+ sql,
+ taskArtifacts,
+ taskPullRequests,
+ taskRuns,
+ tasks,
+ users,
+} from '@roomote/db/server';
+
+import type { UserAuthSuccess } from '@/types';
+
+import { getFastSessionById } from './fast-sessions';
+
+type SessionAuth = Pick;
+export type SessionScope = 'all' | 'tasks' | 'reviews' | 'automations';
+
+type SessionListInput = {
+ scope?: SessionScope;
+ status?: 'active' | 'needs_input' | 'blocked' | 'ready';
+ user?: string | null;
+ repository?: string | null;
+ environment?: string | null;
+ pullRequest?: string | null;
+ source?: string | null;
+ model?: string | null;
+ period?: number | 'all';
+ q?: string | null;
+ before?: string | null;
+ limit?: number;
+};
+
+const DEFAULT_LIMIT = 50;
+const MAX_LIMIT = 200;
+
+function sessionScope(auth: SessionAuth) {
+ if (auth.isAdmin) return undefined;
+ return or(
+ eq(sessions.ownerUserId, auth.userId),
+ exists(
+ db
+ .select({ one: sql`1` })
+ .from(sessionParticipants)
+ .where(
+ and(
+ eq(sessionParticipants.sessionId, sessions.id),
+ eq(sessionParticipants.userId, auth.userId),
+ ),
+ ),
+ ),
+ exists(
+ db
+ .select({ one: sql`1` })
+ .from(fastAgentMessages)
+ .where(
+ and(
+ eq(fastAgentMessages.conversationId, sessions.fastConversationId),
+ sql`${fastAgentMessages.metadata} ->> 'userId' = ${auth.userId}`,
+ ),
+ ),
+ ),
+ );
+}
+
+function encodeCursor(row: { activityAt: number; id: string }): string {
+ return `${row.activityAt}:${row.id}`;
+}
+
+function decodeCursor(cursor?: string | null) {
+ if (!cursor) return null;
+ const separator = cursor.indexOf(':');
+ const activityAt = Number(cursor.slice(0, separator));
+ const id = cursor.slice(separator + 1);
+ return separator > 0 && Number.isFinite(activityAt) && id
+ ? { activityAt, id }
+ : null;
+}
+
+function taskExistsCondition(condition?: ReturnType) {
+ return exists(
+ db
+ .select({ one: sql`1` })
+ .from(sessionTasks)
+ .innerJoin(tasks, eq(tasks.id, sessionTasks.taskId))
+ .where(
+ and(
+ eq(sessionTasks.sessionId, sessions.id),
+ isNull(tasks.deletedAt),
+ condition,
+ ),
+ ),
+ );
+}
+
+function listConditions(auth: SessionAuth, input: SessionListInput) {
+ const cursor = decodeCursor(input.before);
+ const scope = input.scope ?? 'all';
+ const query = input.q?.trim();
+ const period = input.period ?? 'all';
+ const pullRequestNumber = Number(input.pullRequest);
+
+ return and(
+ sessionScope(auth),
+ eq(sessions.visibility, 'visible'),
+ isNull(sessions.archivedAt),
+ input.status ? eq(sessions.cachedStatus, input.status) : undefined,
+ input.user ? eq(sessions.ownerUserId, input.user) : undefined,
+ input.source
+ ? eq(sessions.sourceSurface, input.source as never)
+ : undefined,
+ period === 'all'
+ ? undefined
+ : gte(
+ sessions.activityAt,
+ Math.floor(Date.now() / 1000) - period * 24 * 60 * 60,
+ ),
+ cursor
+ ? or(
+ lt(sessions.activityAt, cursor.activityAt),
+ and(
+ eq(sessions.activityAt, cursor.activityAt),
+ lt(sessions.id, cursor.id),
+ ),
+ )
+ : undefined,
+ scope === 'tasks' ? taskExistsCondition() : undefined,
+ scope === 'reviews'
+ ? taskExistsCondition(eq(tasks.workflow, 'pr_review'))
+ : undefined,
+ scope === 'automations' ? eq(sessions.ownerKind, 'automation') : undefined,
+ input.repository
+ ? taskExistsCondition(eq(tasks.repositoryName, input.repository))
+ : undefined,
+ input.environment
+ ? exists(
+ db
+ .select({ one: sql`1` })
+ .from(sessionTasks)
+ .innerJoin(taskRuns, eq(taskRuns.taskId, sessionTasks.taskId))
+ .where(
+ and(
+ eq(sessionTasks.sessionId, sessions.id),
+ sql`${taskRuns.payload} ->> 'environmentId' = ${input.environment}`,
+ ),
+ ),
+ )
+ : undefined,
+ input.model ? taskExistsCondition(eq(tasks.model, input.model)) : undefined,
+ input.pullRequest && Number.isFinite(pullRequestNumber)
+ ? exists(
+ db
+ .select({ one: sql`1` })
+ .from(sessionTasks)
+ .innerJoin(
+ taskPullRequests,
+ eq(taskPullRequests.taskId, sessionTasks.taskId),
+ )
+ .where(
+ and(
+ eq(sessionTasks.sessionId, sessions.id),
+ eq(taskPullRequests.prNumber, pullRequestNumber),
+ ),
+ ),
+ )
+ : undefined,
+ query
+ ? or(
+ ilike(sessions.title, `%${query.replaceAll('%', '\\%')}%`),
+ exists(
+ db
+ .select({ one: sql`1` })
+ .from(sessionTasks)
+ .innerJoin(tasks, eq(tasks.id, sessionTasks.taskId))
+ .where(
+ and(
+ eq(sessionTasks.sessionId, sessions.id),
+ or(
+ ilike(tasks.title, `%${query.replaceAll('%', '\\%')}%`),
+ ilike(
+ tasks.repositoryName,
+ `%${query.replaceAll('%', '\\%')}%`,
+ ),
+ ),
+ ),
+ ),
+ ),
+ )
+ : undefined,
+ );
+}
+
+const baseSelection = {
+ id: sessions.id,
+ title: sessions.title,
+ ownerKind: sessions.ownerKind,
+ ownerUserId: sessions.ownerUserId,
+ ownerAutomation: sessions.ownerAutomation,
+ ownerName: users.name,
+ ownerEmail: users.email,
+ ownerImageUrl: users.imageUrl,
+ sourceSurface: sessions.sourceSurface,
+ sourceTrigger: sessions.sourceTrigger,
+ fastConversationId: sessions.fastConversationId,
+ visibility: sessions.visibility,
+ activityAt: sessions.activityAt,
+ cachedStatus: sessions.cachedStatus,
+ archivedAt: sessions.archivedAt,
+ createdAt: sessions.createdAt,
+ updatedAt: sessions.updatedAt,
+};
+
+async function hydrateSessionRows(
+ auth: SessionAuth,
+ rows: Array<
+ typeof sessions.$inferSelect & {
+ ownerName: string | null;
+ ownerEmail: string | null;
+ ownerImageUrl: string | null;
+ }
+ >,
+) {
+ if (rows.length === 0) return [];
+ const ids = rows.map((row) => row.id);
+ const [
+ linkedTasks,
+ participants,
+ usage,
+ legacyTaskUsage,
+ legacyFastUsage,
+ externalFastActivity,
+ pins,
+ ] = await Promise.all([
+ db
+ .select({
+ sessionId: sessionTasks.sessionId,
+ taskId: tasks.id,
+ title: tasks.title,
+ workflow: tasks.workflow,
+ state: tasks.state,
+ repositoryName: tasks.repositoryName,
+ model: tasks.model,
+ activityAt: tasks.activityAt,
+ })
+ .from(sessionTasks)
+ .innerJoin(tasks, eq(tasks.id, sessionTasks.taskId))
+ .where(
+ and(inArray(sessionTasks.sessionId, ids), isNull(tasks.deletedAt)),
+ ),
+ db
+ .select({
+ sessionId: sessionParticipants.sessionId,
+ userId: sessionParticipants.userId,
+ role: sessionParticipants.role,
+ lastReadEventAt: sessionParticipants.lastReadEventAt,
+ lastReadEventId: sessionParticipants.lastReadEventId,
+ })
+ .from(sessionParticipants)
+ .where(inArray(sessionParticipants.sessionId, ids)),
+ db
+ .select({
+ sessionId: llmUsageEvents.sessionId,
+ costMicroUsd: sql`coalesce(sum(${llmUsageEvents.costMicroUsd}), 0)::bigint`,
+ })
+ .from(llmUsageEvents)
+ .where(inArray(llmUsageEvents.sessionId, ids))
+ .groupBy(llmUsageEvents.sessionId),
+ db
+ .select({
+ sessionId: sessionTasks.sessionId,
+ costMicroUsd: sql`coalesce(sum(${llmUsageEvents.costMicroUsd}), 0)::bigint`,
+ })
+ .from(sessionTasks)
+ .innerJoin(llmUsageEvents, eq(llmUsageEvents.taskId, sessionTasks.taskId))
+ .where(
+ and(
+ inArray(sessionTasks.sessionId, ids),
+ isNull(llmUsageEvents.sessionId),
+ ),
+ )
+ .groupBy(sessionTasks.sessionId),
+ db
+ .select({
+ sessionId: sessions.id,
+ costMicroUsd: sql`(
+ select coalesce(sum(legacy_usage.cost_micro_usd), 0)::bigint
+ from task_inference_usage_events legacy_usage
+ where legacy_usage.session_id is null
+ and legacy_usage.harness_session_id in (
+ select distinct ${fastAgentMessages.nativeSessionId}
+ from ${fastAgentMessages}
+ where ${fastAgentMessages.conversationId} = ${sessions.fastConversationId}
+ and ${fastAgentMessages.nativeSessionId} is not null
+ )
+ )`,
+ })
+ .from(sessions)
+ .where(inArray(sessions.id, ids)),
+ db
+ .select({
+ sessionId: sessions.id,
+ eventAt: sql`coalesce(max(${fastAgentMessages.ts}), 0)::bigint`,
+ })
+ .from(sessions)
+ .innerJoin(
+ fastAgentMessages,
+ eq(fastAgentMessages.conversationId, sessions.fastConversationId),
+ )
+ .where(
+ and(
+ inArray(sessions.id, ids),
+ or(
+ sql`${fastAgentMessages.metadata} ->> 'userId' IS NULL`,
+ sql`${fastAgentMessages.metadata} ->> 'userId' <> ${auth.userId}`,
+ ),
+ ),
+ )
+ .groupBy(sessions.id),
+ db
+ .select({ sessionId: sessionPins.sessionId })
+ .from(sessionPins)
+ .where(
+ and(
+ eq(sessionPins.userId, auth.userId),
+ inArray(sessionPins.sessionId, ids),
+ ),
+ ),
+ ]);
+
+ const pinned = new Set(pins.map((pin) => pin.sessionId));
+ return rows.map((row) => {
+ const tasksForSession = linkedTasks.filter(
+ (task) => task.sessionId === row.id,
+ );
+ const sessionParticipantsRows = participants.filter(
+ (participant) => participant.sessionId === row.id,
+ );
+ const cursor = sessionParticipantsRows.find(
+ (participant) => participant.userId === auth.userId,
+ );
+ const latestTaskEventAt = tasksForSession.reduce(
+ (latest, task) => Math.max(latest, task.activityAt * 1000),
+ 0,
+ );
+ const latestExternalEventAt = Math.max(
+ latestTaskEventAt,
+ Number(
+ externalFastActivity.find((event) => event.sessionId === row.id)
+ ?.eventAt ?? 0,
+ ),
+ );
+ return {
+ ...row,
+ tasks: tasksForSession,
+ executionCount: tasksForSession.length,
+ participants: sessionParticipantsRows,
+ inferenceCostMicroUsd:
+ Number(
+ usage.find((event) => event.sessionId === row.id)?.costMicroUsd ?? 0,
+ ) +
+ Number(
+ legacyTaskUsage.find((event) => event.sessionId === row.id)
+ ?.costMicroUsd ?? 0,
+ ) +
+ Number(
+ legacyFastUsage.find((event) => event.sessionId === row.id)
+ ?.costMicroUsd ?? 0,
+ ),
+ unread: latestExternalEventAt > Number(cursor?.lastReadEventAt ?? 0),
+ pinned: pinned.has(row.id),
+ };
+ });
+}
+
+export async function getSessions(auth: SessionAuth, input: SessionListInput) {
+ const limit = Math.min(Math.max(input.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT);
+ const rows = await db
+ .select(baseSelection)
+ .from(sessions)
+ .leftJoin(users, eq(users.id, sessions.ownerUserId))
+ .where(listConditions(auth, input))
+ .orderBy(desc(sessions.activityAt), desc(sessions.id))
+ .limit(limit + 1);
+ const page = rows.slice(0, limit);
+ const last = page.at(-1);
+ return {
+ sessions: await hydrateSessionRows(auth, page),
+ nextCursor: rows.length > limit && last ? encodeCursor(last) : null,
+ };
+}
+
+export async function findAccessibleSession(
+ auth: SessionAuth,
+ sessionId: string,
+) {
+ const [session] = await db
+ .select(baseSelection)
+ .from(sessions)
+ .leftJoin(users, eq(users.id, sessions.ownerUserId))
+ .where(and(eq(sessions.id, sessionId), sessionScope(auth)))
+ .limit(1);
+ return session ?? null;
+}
+
+async function getSessionTasks(sessionId: string) {
+ const linked = await db
+ .select({
+ sessionId: sessionTasks.sessionId,
+ taskId: tasks.id,
+ attachedAt: sessionTasks.attachedAt,
+ origin: sessionTasks.origin,
+ title: tasks.title,
+ workflow: tasks.workflow,
+ state: tasks.state,
+ goalStatus: tasks.goalStatus,
+ repositoryName: tasks.repositoryName,
+ model: tasks.model,
+ activityAt: tasks.activityAt,
+ deletedAt: tasks.deletedAt,
+ })
+ .from(sessionTasks)
+ .innerJoin(tasks, eq(tasks.id, sessionTasks.taskId))
+ .where(eq(sessionTasks.sessionId, sessionId))
+ .orderBy(sessionTasks.attachedAt);
+
+ return Promise.all(
+ linked.map(async (task) => {
+ const [latestRun, artifacts, pullRequests, usage] = await Promise.all([
+ db.query.taskRuns.findFirst({
+ where: eq(taskRuns.taskId, task.taskId),
+ orderBy: desc(taskRuns.id),
+ columns: {
+ id: true,
+ status: true,
+ taskPhase: true,
+ error: true,
+ result: true,
+ },
+ }),
+ db
+ .select({
+ id: taskArtifacts.id,
+ path: taskArtifacts.path,
+ artifactType: taskArtifacts.artifactType,
+ contentType: taskArtifacts.contentType,
+ size: taskArtifacts.size,
+ })
+ .from(taskArtifacts)
+ .where(eq(taskArtifacts.taskId, task.taskId))
+ .orderBy(desc(taskArtifacts.createdAt)),
+ db
+ .select({
+ id: taskPullRequests.id,
+ url: taskPullRequests.prUrl,
+ number: taskPullRequests.prNumber,
+ title: taskPullRequests.prTitle,
+ repository: taskPullRequests.repository,
+ status: taskPullRequests.status,
+ })
+ .from(taskPullRequests)
+ .where(eq(taskPullRequests.taskId, task.taskId)),
+ db
+ .select({
+ costMicroUsd: sql`coalesce(sum(${llmUsageEvents.costMicroUsd}), 0)::bigint`,
+ })
+ .from(llmUsageEvents)
+ .where(eq(llmUsageEvents.taskId, task.taskId)),
+ ]);
+ const result = latestRun?.result;
+ const latestOutput =
+ result && typeof result === 'object'
+ ? String(
+ (result as Record).summary ??
+ (result as Record).message ??
+ '',
+ )
+ .trim()
+ .slice(0, 240) || null
+ : null;
+ return {
+ ...task,
+ latestRun: latestRun ?? null,
+ latestOutput,
+ inferenceCostMicroUsd: Number(usage[0]?.costMicroUsd ?? 0),
+ artifacts,
+ pullRequests,
+ };
+ }),
+ );
+}
+
+export async function getSessionById(auth: SessionAuth, sessionId: string) {
+ const session = await findAccessibleSession(auth, sessionId);
+ if (!session) return null;
+ const [hydrated] = await hydrateSessionRows(auth, [session]);
+ const sessionTaskDetails = await getSessionTasks(sessionId);
+ const liveStatus = deriveSessionStatus({
+ conversationResponding:
+ Boolean(session.fastConversationId) && session.cachedStatus === 'active',
+ tasks: sessionTaskDetails.map((task) => ({
+ state: task.state,
+ taskPhase: task.latestRun?.taskPhase ?? null,
+ goalStatus: task.goalStatus,
+ })),
+ });
+ return { ...hydrated!, tasks: sessionTaskDetails, status: liveStatus };
+}
+
+export async function getSessionTimeline(
+ auth: SessionAuth,
+ sessionId: string,
+ since = 0,
+) {
+ const session = await findAccessibleSession(auth, sessionId);
+ if (!session) return null;
+ const taskRows = await getSessionTasks(sessionId);
+ const fast = session.fastConversationId
+ ? await getFastSessionById(auth, session.fastConversationId)
+ : null;
+ const timelineTasks = taskRows.map((task) => ({
+ taskId: task.taskId,
+ title: task.title,
+ workflow: task.workflow,
+ state: task.state,
+ goalStatus: task.goalStatus,
+ repositoryName: task.repositoryName,
+ activityAt: task.activityAt,
+ attachedAt: task.attachedAt,
+ origin: task.origin,
+ }));
+ const events = [
+ ...(fast?.messages ?? []).map((message) => ({
+ id: `fast:${message.eventId}`,
+ at: message.ts,
+ type: 'message' as const,
+ own: message.metadata?.userId === auth.userId,
+ message,
+ })),
+ ...timelineTasks.flatMap((task) => [
+ {
+ id: `task:${task.taskId}:delegated`,
+ at: task.attachedAt.getTime(),
+ type: 'task_delegated' as const,
+ own: false,
+ task,
+ },
+ {
+ id: `task:${task.taskId}:${task.state}`,
+ at: task.activityAt * 1000,
+ type: 'task_state' as const,
+ own: false,
+ task,
+ },
+ ]),
+ ]
+ .filter((event) => event.at > since)
+ .sort(
+ (left, right) => left.at - right.at || left.id.localeCompare(right.id),
+ );
+ return { events, cursor: events.at(-1)?.at ?? since };
+}
+
+export async function getSessionForTask(auth: SessionAuth, taskId: string) {
+ const [row] = await db
+ .select({ sessionId: sessions.id, title: sessions.title })
+ .from(sessionTasks)
+ .innerJoin(sessions, eq(sessions.id, sessionTasks.sessionId))
+ .where(and(eq(sessionTasks.taskId, taskId), sessionScope(auth)))
+ .limit(1);
+ return row ?? null;
+}
+
+export async function updateSessionMetadata(
+ auth: SessionAuth,
+ sessionId: string,
+ changes: { title?: string; archivedAt?: Date | null },
+) {
+ const [updated] = await db
+ .update(sessions)
+ .set({ ...changes, updatedAt: new Date() })
+ .where(
+ and(
+ eq(sessions.id, sessionId),
+ auth.isAdmin ? undefined : eq(sessions.ownerUserId, auth.userId),
+ ),
+ )
+ .returning();
+ return updated ?? null;
+}
+
+export async function listSessionPins(auth: SessionAuth) {
+ return db
+ .select({
+ sessionId: sessionPins.sessionId,
+ updatedAt: sessionPins.updatedAt,
+ })
+ .from(sessionPins)
+ .innerJoin(sessions, eq(sessions.id, sessionPins.sessionId))
+ .where(and(eq(sessionPins.userId, auth.userId), sessionScope(auth)))
+ .orderBy(desc(sessionPins.updatedAt));
+}
+
+export async function setSessionPinned(
+ auth: SessionAuth,
+ input: { sessionId: string; pinned: boolean },
+) {
+ if (!input.pinned) {
+ await db
+ .delete(sessionPins)
+ .where(
+ and(
+ eq(sessionPins.sessionId, input.sessionId),
+ eq(sessionPins.userId, auth.userId),
+ ),
+ );
+ return { success: true as const, pinned: false };
+ }
+ if (!(await findAccessibleSession(auth, input.sessionId))) {
+ return { success: false as const, error: 'session_not_found' as const };
+ }
+ const [existing] = await db
+ .select({ id: sessionPins.id })
+ .from(sessionPins)
+ .where(
+ and(
+ eq(sessionPins.sessionId, input.sessionId),
+ eq(sessionPins.userId, auth.userId),
+ ),
+ );
+ if (existing) return { success: true as const, pinned: true };
+ const [total] = await db
+ .select({ value: count() })
+ .from(sessionPins)
+ .where(eq(sessionPins.userId, auth.userId));
+ if ((total?.value ?? 0) >= 5) {
+ return { success: false as const, error: 'pin_limit_reached' as const };
+ }
+ await db.insert(sessionPins).values({
+ sessionId: input.sessionId,
+ userId: auth.userId,
+ });
+ return { success: true as const, pinned: true };
+}
diff --git a/apps/web/src/lib/telemetry/normalize-path.ts b/apps/web/src/lib/telemetry/normalize-path.ts
index dde94dd27..b95c5f251 100644
--- a/apps/web/src/lib/telemetry/normalize-path.ts
+++ b/apps/web/src/lib/telemetry/normalize-path.ts
@@ -69,6 +69,10 @@ function normalizeTaskPath(pathname: string): string | null {
return ['/task/[taskId]', ...restSegments].join('/');
}
+function normalizeSessionPath(pathname: string): string | null {
+ return /^\/sessions\/[^/]+$/.test(pathname) ? '/sessions/[sessionId]' : null;
+}
+
/** @public */
export interface NormalizedPath {
path: string;
@@ -81,7 +85,8 @@ export function normalizePath(
): NormalizedPath {
const pathname = rawPathname.split('?')[0] ?? '/';
- let path: string | null = normalizeTaskPath(pathname);
+ let path: string | null =
+ normalizeTaskPath(pathname) ?? normalizeSessionPath(pathname);
if (path === null) {
for (const matcher of DYNAMIC_ROUTE_MATCHERS) {
diff --git a/apps/web/src/trpc/commands/fast-sessions/index.ts b/apps/web/src/trpc/commands/fast-sessions/index.ts
index 218c2dcdc..20f808fbf 100644
--- a/apps/web/src/trpc/commands/fast-sessions/index.ts
+++ b/apps/web/src/trpc/commands/fast-sessions/index.ts
@@ -12,7 +12,12 @@ import {
resolveUserMcpServerConfigs,
type FastAgentSurfaceReplyDelivery,
} from '@roomote/sdk/server';
-import { db, eq, fastAgentConversations } from '@roomote/db/server';
+import {
+ db,
+ eq,
+ fastAgentConversations,
+ getSessionForFastConversation,
+} from '@roomote/db/server';
import {
formatErrorForLog,
getUserDisplayName,
@@ -148,7 +153,7 @@ export async function startFastSessionCommand(
model?: string | null;
reasoningEffort?: ReasoningEffort | null;
},
-): Promise<{ sessionId: string }> {
+): Promise<{ sessionId: string; fastConversationId?: string }> {
const conversation: WebFastAgentConversation = {
surface: 'web',
workspaceId: auth.userId,
@@ -182,7 +187,13 @@ export async function startFastSessionCommand(
reasoningEffort: settings.reasoningEffort,
});
- return { sessionId: session.id };
+ const unifiedSession = auth.featureFlags.sessions_ui
+ ? await getSessionForFastConversation(db, session.id)
+ : null;
+ return {
+ sessionId: unifiedSession?.id ?? session.id,
+ fastConversationId: session.id,
+ };
}
export async function replyToFastSessionCommand(
diff --git a/apps/web/src/trpc/commands/feature-flags/index.test.ts b/apps/web/src/trpc/commands/feature-flags/index.test.ts
index a2f9fa882..60aaa9b80 100644
--- a/apps/web/src/trpc/commands/feature-flags/index.test.ts
+++ b/apps/web/src/trpc/commands/feature-flags/index.test.ts
@@ -52,11 +52,27 @@ function buildAuth(isAdmin: boolean): UserAuthSuccess {
describe('feature-flags commands', () => {
beforeEach(() => vi.clearAllMocks());
- it('returns no experimental flags', async () => {
+ it('returns the default-off Sessions rollout flags', async () => {
await expect(getExperimentalFlagsCommand(buildAuth(true))).resolves.toEqual(
- [],
+ [
+ expect.objectContaining({
+ id: 'sessions_data',
+ value: false,
+ explicitlySet: false,
+ }),
+ expect.objectContaining({
+ id: 'sessions_ui',
+ value: false,
+ explicitlySet: false,
+ }),
+ expect.objectContaining({
+ id: 'sessions_comms',
+ value: false,
+ explicitlySet: false,
+ }),
+ ],
);
- expect(mockFindFirst).not.toHaveBeenCalled();
+ expect(mockFindFirst).toHaveBeenCalledOnce();
});
it('rejects stale flags before metadata lookup or a database write', async () => {
diff --git a/apps/web/src/trpc/commands/sessions/index.test.ts b/apps/web/src/trpc/commands/sessions/index.test.ts
new file mode 100644
index 000000000..a325348a1
--- /dev/null
+++ b/apps/web/src/trpc/commands/sessions/index.test.ts
@@ -0,0 +1,63 @@
+import type { UserAuthSuccess } from '@/types';
+
+const { getSessionByIdMock, resolveTaskAccessMock } = vi.hoisted(() => ({
+ getSessionByIdMock: vi.fn(),
+ resolveTaskAccessMock: vi.fn(),
+}));
+
+vi.mock('@/lib/server/sessions', () => ({
+ findAccessibleSession: vi.fn(),
+ getSessionById: getSessionByIdMock,
+ getSessionForTask: vi.fn(),
+ getSessions: vi.fn(),
+ getSessionTimeline: vi.fn(),
+ listSessionPins: vi.fn(),
+ setSessionPinned: vi.fn(),
+ updateSessionMetadata: vi.fn(),
+}));
+vi.mock('../tasks/by-id', () => ({
+ resolveTaskByIdAccessCommand: resolveTaskAccessMock,
+}));
+vi.mock('@roomote/db/server', () => ({
+ advanceSessionReadCursor: vi.fn(),
+ db: {},
+}));
+vi.mock('@roomote/telemetry/server', () => ({ captureEvent: vi.fn() }));
+
+import { getSessionByIdCommand } from './index';
+
+describe('getSessionByIdCommand', () => {
+ it('redacts execution details when Session access exceeds task access', async () => {
+ getSessionByIdMock.mockResolvedValue({
+ id: 'session-1',
+ tasks: [
+ {
+ taskId: 'task-1',
+ title: 'Private execution',
+ latestRun: { id: 1, error: 'private error', result: {} },
+ latestOutput: 'private output',
+ inferenceCostMicroUsd: 123,
+ artifacts: [{ id: 'artifact-1', path: 'private.txt' }],
+ pullRequests: [{ id: 'pr-1', url: 'https://example.com/private' }],
+ },
+ ],
+ });
+ resolveTaskAccessMock.mockResolvedValue({ kind: 'not-found' });
+
+ const result = await getSessionByIdCommand(
+ { userId: 'user-1', isAdmin: false } as UserAuthSuccess,
+ 'session-1',
+ );
+
+ expect(result?.tasks[0]).toEqual(
+ expect.objectContaining({
+ canAccessDetails: false,
+ latestRun: null,
+ latestOutput: null,
+ inferenceCostMicroUsd: 0,
+ artifacts: [],
+ pullRequests: [],
+ }),
+ );
+ });
+});
diff --git a/apps/web/src/trpc/commands/sessions/index.ts b/apps/web/src/trpc/commands/sessions/index.ts
new file mode 100644
index 000000000..d2e8ba13e
--- /dev/null
+++ b/apps/web/src/trpc/commands/sessions/index.ts
@@ -0,0 +1,104 @@
+import { z } from 'zod';
+import { advanceSessionReadCursor, db } from '@roomote/db/server';
+import { captureEvent } from '@roomote/telemetry/server';
+
+import type { UserAuthSuccess } from '@/types';
+import {
+ findAccessibleSession,
+ getSessionById,
+ getSessionForTask,
+ getSessions,
+ getSessionTimeline,
+ listSessionPins,
+ setSessionPinned,
+ updateSessionMetadata,
+} from '@/lib/server/sessions';
+import { resolveTaskByIdAccessCommand } from '../tasks/by-id';
+
+export const sessionIdInputSchema = z.object({ sessionId: z.string().uuid() });
+export const sessionsListInputSchema = z.object({
+ scope: z.enum(['all', 'tasks', 'reviews', 'automations']).optional(),
+ status: z.enum(['active', 'needs_input', 'blocked', 'ready']).optional(),
+ user: z.string().nullish(),
+ repository: z.string().nullish(),
+ environment: z.string().nullish(),
+ pullRequest: z.string().nullish(),
+ source: z.string().nullish(),
+ model: z.string().nullish(),
+ period: z.union([z.literal('all'), z.number().int().positive()]).optional(),
+ q: z.string().max(200).nullish(),
+ before: z.string().nullish(),
+ limit: z.number().int().min(1).max(200).optional(),
+});
+
+export async function markSessionReadCommand(
+ auth: UserAuthSuccess,
+ input: { sessionId: string; throughEventAt: number; throughEventId: string },
+) {
+ if (!(await findAccessibleSession(auth, input.sessionId))) return null;
+ return advanceSessionReadCursor(db, {
+ sessionId: input.sessionId,
+ userId: auth.userId,
+ eventAt: input.throughEventAt,
+ eventId: input.throughEventId,
+ });
+}
+
+export async function getSessionByIdCommand(
+ auth: UserAuthSuccess,
+ sessionId: string,
+) {
+ const session = await getSessionById(auth, sessionId);
+ if (!session) return null;
+
+ const taskAccess = await Promise.all(
+ session.tasks.map((task) =>
+ resolveTaskByIdAccessCommand(auth, {
+ taskId: task.taskId,
+ includeArtifacts: true,
+ }),
+ ),
+ );
+
+ return {
+ ...session,
+ tasks: session.tasks.map((task, index) =>
+ taskAccess[index]?.kind === 'resolved'
+ ? { ...task, canAccessDetails: true as const }
+ : {
+ ...task,
+ canAccessDetails: false as const,
+ latestRun: null,
+ latestOutput: null,
+ inferenceCostMicroUsd: 0,
+ artifacts: [],
+ pullRequests: [],
+ },
+ ),
+ };
+}
+
+export async function archiveSessionCommand(
+ auth: UserAuthSuccess,
+ sessionId: string,
+) {
+ const archived = await updateSessionMetadata(auth, sessionId, {
+ archivedAt: new Date(),
+ });
+ if (archived) {
+ void captureEvent('session_archived', {
+ userId: auth.userId,
+ properties: { surface: 'web', outcome: 'archived' },
+ });
+ }
+ return archived;
+}
+
+export {
+ getSessionForTask,
+ getSessions,
+ getSessionTimeline,
+ listSessionPins,
+ setSessionPinned,
+ updateSessionMetadata,
+};
diff --git a/apps/web/src/trpc/commands/task-runs/index.ts b/apps/web/src/trpc/commands/task-runs/index.ts
index a05deb488..add345283 100644
--- a/apps/web/src/trpc/commands/task-runs/index.ts
+++ b/apps/web/src/trpc/commands/task-runs/index.ts
@@ -28,6 +28,7 @@ import {
inArray,
markTaskStartParallelCountEndedAt,
prepareTaskGoalActivation,
+ sessionTasks,
slackInstallations,
taskRuns,
tasks,
@@ -45,7 +46,7 @@ import { sendSandboxPromptCommand } from '../sandbox-session';
import { resolveTaskByIdAccessCommand } from '../tasks/by-id';
export type CreateTaskRunResult =
- | { success: true; id: number; taskId: string }
+ | { success: true; id: number; taskId: string; sessionId?: string }
| { success: false; error: string };
export async function startTaskGoalCommand(
@@ -464,6 +465,12 @@ export async function createStandardTaskRunCommand(
surface: 'web',
trigger: 'manual',
});
+ const linkedSession = auth.featureFlags.sessions_ui
+ ? await db.query.sessionTasks.findFirst({
+ where: eq(sessionTasks.taskId, launchResult.taskId),
+ columns: { sessionId: true },
+ })
+ : null;
try {
await notifySourceTaskArtifactBuild({
@@ -486,6 +493,7 @@ export async function createStandardTaskRunCommand(
success: true,
id: launchResult.id,
taskId: launchResult.taskId,
+ sessionId: linkedSession?.sessionId,
};
} catch (error) {
console.error(error);
diff --git a/apps/web/src/trpc/routers/_app.ts b/apps/web/src/trpc/routers/_app.ts
index 4e1c286ac..56396abb6 100644
--- a/apps/web/src/trpc/routers/_app.ts
+++ b/apps/web/src/trpc/routers/_app.ts
@@ -39,6 +39,19 @@ import {
replyToFastSessionInputSchema,
startFastSessionInputSchema,
} from '../commands/fast-sessions/input';
+import {
+ getSessionByIdCommand,
+ getSessionForTask,
+ getSessions,
+ getSessionTimeline,
+ archiveSessionCommand,
+ listSessionPins,
+ markSessionReadCommand,
+ sessionIdInputSchema,
+ sessionsListInputSchema,
+ setSessionPinned,
+ updateSessionMetadata,
+} from '../commands/sessions';
import {
analyticsChartInputSchema,
analyticsDetailsInputSchema,
@@ -2825,6 +2838,72 @@ export const appRouter = createRouter({
),
}),
+ sessions: createRouter({
+ list: protectedProcedure
+ .input(sessionsListInputSchema)
+ .query(({ ctx: { auth }, input }) => getSessions(auth, input)),
+ byId: protectedProcedure
+ .input(sessionIdInputSchema)
+ .query(({ ctx: { auth }, input }) =>
+ getSessionByIdCommand(auth, input.sessionId),
+ ),
+ timeline: protectedProcedure
+ .input(sessionIdInputSchema.extend({ since: z.number().optional() }))
+ .query(({ ctx: { auth }, input }) =>
+ getSessionTimeline(auth, input.sessionId, input.since),
+ ),
+ forTask: protectedProcedure
+ .input(z.object({ taskId: z.string().min(1) }))
+ .query(({ ctx: { auth }, input }) =>
+ getSessionForTask(auth, input.taskId),
+ ),
+ markRead: protectedProcedure
+ .input(
+ sessionIdInputSchema.extend({
+ throughEventAt: z.number().nonnegative(),
+ throughEventId: z.string().min(1),
+ }),
+ )
+ .mutation(({ ctx: { auth }, input }) =>
+ markSessionReadCommand(auth, input),
+ ),
+ rename: protectedProcedure
+ .input(
+ sessionIdInputSchema.extend({
+ title: z.string().trim().min(1).max(500),
+ }),
+ )
+ .mutation(({ ctx: { auth }, input }) =>
+ updateSessionMetadata(auth, input.sessionId, { title: input.title }),
+ ),
+ archive: protectedProcedure
+ .input(sessionIdInputSchema)
+ .mutation(({ ctx: { auth }, input }) =>
+ archiveSessionCommand(auth, input.sessionId),
+ ),
+ unarchive: protectedProcedure
+ .input(sessionIdInputSchema)
+ .mutation(({ ctx: { auth }, input }) =>
+ updateSessionMetadata(auth, input.sessionId, { archivedAt: null }),
+ ),
+ pins: protectedProcedure.query(({ ctx: { auth } }) =>
+ listSessionPins(auth),
+ ),
+ setPinned: protectedProcedure
+ .input(sessionIdInputSchema.extend({ pinned: z.boolean() }))
+ .mutation(({ ctx: { auth }, input }) => setSessionPinned(auth, input)),
+ search: protectedProcedure
+ .input(
+ z.object({
+ query: z.string().max(200),
+ limit: z.number().int().min(1).max(50).optional(),
+ }),
+ )
+ .query(({ ctx: { auth }, input }) =>
+ getSessions(auth, { q: input.query, limit: input.limit ?? 20 }),
+ ),
+ }),
+
agentBehavior: createRouter({
get: protectedProcedure.query(({ ctx: { auth } }) =>
getAgentBehaviorSettingsCommand(auth),
diff --git a/apps/web/src/types/analytics.ts b/apps/web/src/types/analytics.ts
index 50f8406e1..74a1a5b2d 100644
--- a/apps/web/src/types/analytics.ts
+++ b/apps/web/src/types/analytics.ts
@@ -2,7 +2,12 @@ import { z } from 'zod';
import { timePeriodFilterSchema, type TimePeriodFilter } from './time-period';
-export const analyticsObjects = ['tasks', 'pullRequests', 'costs'] as const;
+export const analyticsObjects = [
+ 'sessions',
+ 'tasks',
+ 'pullRequests',
+ 'costs',
+] as const;
export const analyticsObjectSchema = z.enum(analyticsObjects);
export type AnalyticsObject = z.infer;
@@ -21,6 +26,8 @@ const analyticsDimensions = [
'taskType',
'provider',
'model',
+ 'ownerKind',
+ 'hasExecution',
] as const;
export const analyticsDimensionSchema = z.enum(analyticsDimensions);
export type AnalyticsDimension = z.infer;
@@ -42,6 +49,8 @@ export const analyticsFiltersSchema = z
taskType: analyticsFilterValueSchema,
provider: analyticsFilterValueSchema,
model: analyticsFilterValueSchema,
+ ownerKind: analyticsFilterValueSchema,
+ hasExecution: analyticsFilterValueSchema,
})
.partial();
export type AnalyticsFilters = z.infer;
@@ -202,6 +211,27 @@ export type PullRequestAnalyticsOverviewResponse = {
};
export const ANALYTICS_OBJECT_CONFIG = {
+ sessions: {
+ label: 'Sessions',
+ axisLabel: 'Sessions',
+ filterDimensions: [
+ 'user',
+ 'status',
+ 'source',
+ 'ownerKind',
+ 'hasExecution',
+ ] as AnalyticsDimension[],
+ viewByDimensions: [
+ 'user',
+ 'status',
+ 'source',
+ 'ownerKind',
+ 'hasExecution',
+ ] as AnalyticsDimension[],
+ defaultViewBy: 'status' as AnalyticsDimension,
+ supportedMetrics: ['tasks'] as readonly AnalyticsMetric[],
+ defaultMetric: 'tasks' as AnalyticsMetric,
+ },
tasks: {
label: 'Tasks',
axisLabel: 'Tasks',
@@ -286,6 +316,8 @@ export const ANALYTICS_DIMENSION_LABELS: Record = {
taskType: 'Task Type',
provider: 'Provider',
model: 'Model',
+ ownerKind: 'Owner kind',
+ hasExecution: 'Has execution',
};
export const ANALYTICS_METRIC_LABELS: Record = {
diff --git a/packages/cloud-agents/package.json b/packages/cloud-agents/package.json
index 6e0220d91..7c854cfd0 100644
--- a/packages/cloud-agents/package.json
+++ b/packages/cloud-agents/package.json
@@ -64,6 +64,7 @@
"@roomote/communication": "workspace:^",
"@roomote/db": "workspace:^",
"@roomote/env": "workspace:^",
+ "@roomote/feature-flags": "workspace:^",
"@roomote/github": "workspace:^",
"@roomote/gitea": "workspace:^",
"@roomote/gitlab": "workspace:^",
diff --git a/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts b/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts
index cce56bc67..a81f38008 100644
--- a/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts
+++ b/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts
@@ -39,10 +39,13 @@ import {
environments,
environmentRepositoryMappings,
repositories,
+ sessionTasks,
userFactory,
environmentFactory,
repositoryFactory,
} from '@roomote/db/server';
+import { getFeatureFlagEvaluator } from '@roomote/feature-flags/server';
+import { getRedis } from '@roomote/redis';
import {
TaskRunQueue,
@@ -909,6 +912,59 @@ describe('enqueueTask initiator stamping', () => {
});
});
+describe('enqueueTask Session linkage', () => {
+ beforeEach(async () => {
+ await db
+ .insert(deploymentSettings)
+ .values({ id: 'default', metadata: { sessions_data: true } })
+ .onConflictDoUpdate({
+ target: deploymentSettings.id,
+ set: { metadata: { sessions_data: true } },
+ });
+ await getFeatureFlagEvaluator(getRedis()).invalidateDeploymentCache();
+ });
+
+ afterEach(async () => {
+ await db
+ .update(deploymentSettings)
+ .set({ metadata: {} })
+ .where(eq(deploymentSettings.id, 'default'));
+ await getFeatureFlagEvaluator(getRedis()).invalidateDeploymentCache();
+ });
+
+ it('creates exactly one Session link for a visible fresh task', async () => {
+ const userId = await createUser();
+ const run = await launchFresh({
+ initiator: { kind: 'user', userId },
+ workflow: 'standard',
+ surface: 'web',
+ trigger: 'manual',
+ });
+
+ const links = await db
+ .select()
+ .from(sessionTasks)
+ .where(eq(sessionTasks.taskId, run.taskId));
+ expect(links).toHaveLength(1);
+ expect(links[0]?.origin).toBe('direct_launch');
+ });
+
+ it('does not create Session links for hidden tasks', async () => {
+ const userId = await createUser();
+ const run = await launchFresh({
+ initiator: { kind: 'user', userId },
+ workflow: 'scan',
+ surface: 'system',
+ trigger: 'schedule',
+ visibility: 'hidden',
+ });
+
+ await expect(
+ db.select().from(sessionTasks).where(eq(sessionTasks.taskId, run.taskId)),
+ ).resolves.toEqual([]);
+ });
+});
+
describe('enqueueTask snapshot resume', () => {
it('atomically rejects concurrent resumes from the same source run', async () => {
const userId = await createUser();
diff --git a/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-service.test.ts b/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-service.test.ts
index 4ba076f97..00cd06ab6 100644
--- a/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-service.test.ts
+++ b/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-service.test.ts
@@ -77,6 +77,9 @@ vi.mock('@roomote/db/server', () => ({
appendFastAgentMemory: mocks.appendMemory,
isBrainProviderConfigured: mocks.isBrainProviderConfigured,
db: {},
+ getSessionForFastConversation: vi.fn().mockResolvedValue(null),
+ getSessionForTask: vi.fn().mockResolvedValue(null),
+ touchSessionActivity: vi.fn().mockResolvedValue(undefined),
}));
vi.mock('../../non-task-provider-usage', () => ({
diff --git a/packages/cloud-agents/src/server/fast-agent/fast-agent-conversation-repository.ts b/packages/cloud-agents/src/server/fast-agent/fast-agent-conversation-repository.ts
index dfa628abe..bef16b9c1 100644
--- a/packages/cloud-agents/src/server/fast-agent/fast-agent-conversation-repository.ts
+++ b/packages/cloud-agents/src/server/fast-agent/fast-agent-conversation-repository.ts
@@ -6,9 +6,19 @@ import {
eq,
fastAgentConversations,
fastAgentMessages,
+ ensureSessionForFastConversation,
+ advanceSessionNotifiedCursor,
+ advanceSessionReadCursor,
+ getSessionForFastConversation,
sql,
+ touchSessionActivity,
type DatabaseOrTransaction,
} from '@roomote/db/server';
+import {
+ FeatureFlag,
+ getFeatureFlagEvaluator,
+} from '@roomote/feature-flags/server';
+import { getRedis } from '@roomote/redis';
import { fastAgentConversationSchema } from '@roomote/types';
import type { FastAgentConversation } from './fast-agent-conversation';
@@ -56,6 +66,15 @@ export interface FastAgentConversationRepository {
}): Promise;
}
+async function sessionsDataEnabled(): Promise {
+ return getFeatureFlagEvaluator(getRedis()).evaluate(
+ FeatureFlag.SessionsData,
+ {
+ isDeploymentContext: true,
+ },
+ );
+}
+
function buildIdentityKey(conversation: FastAgentConversation): string {
return `${conversation.surface}:${conversation.workspaceId}:${conversation.conversationId}`;
}
@@ -151,6 +170,7 @@ async function loadConversationRecord(
export const fastAgentConversationRepository: FastAgentConversationRepository =
{
async getOrCreate({ userId, conversation }) {
+ const createSession = await sessionsDataEnabled();
return db.transaction(async (tx) => {
await tx.execute(
sql`select pg_advisory_xact_lock(hashtextextended(${buildIdentityKey(conversation)}, 0))`,
@@ -207,6 +227,10 @@ export const fastAgentConversationRepository: FastAgentConversationRepository =
.where(eq(fastAgentConversations.id, record.id))
.returning();
+ if (createSession) {
+ await ensureSessionForFastConversation(tx, updated?.id ?? record.id);
+ }
+
return loadConversationRecord(tx, updated?.id ?? record.id);
});
},
@@ -274,6 +298,7 @@ export const fastAgentConversationRepository: FastAgentConversationRepository =
return;
}
+ const touchSession = await sessionsDataEnabled();
await db.transaction(async (tx) => {
const conversationId = await resolveCanonicalId(tx, requestedId);
await tx.execute(
@@ -290,10 +315,25 @@ export const fastAgentConversationRepository: FastAgentConversationRepository =
if (!updated) {
throw new Error('Fast conversation was not found.');
}
+ if (touchSession) {
+ const session = await getSessionForFastConversation(
+ tx,
+ conversationId,
+ );
+ if (session) {
+ await touchSessionActivity(
+ tx,
+ session.id,
+ Math.floor(Date.now() / 1000),
+ { recomputeStatus: false },
+ );
+ }
+ }
});
},
async upsertMessage({ conversationId: requestedId, message }) {
+ const touchSession = await sessionsDataEnabled();
await db.transaction(async (tx) => {
const conversationId = await resolveCanonicalId(tx, requestedId);
await tx.execute(
@@ -335,6 +375,35 @@ export const fastAgentConversationRepository: FastAgentConversationRepository =
.update(fastAgentConversations)
.set({ updatedAt: sql`now()` })
.where(eq(fastAgentConversations.id, conversationId));
+ if (touchSession) {
+ const session = await getSessionForFastConversation(
+ tx,
+ conversationId,
+ );
+ if (session) {
+ await touchSessionActivity(
+ tx,
+ session.id,
+ Math.floor(message.ts / 1000),
+ { recomputeStatus: false },
+ );
+ const messageUserId = message.metadata?.userId;
+ if (message.role === 'user' && typeof messageUserId === 'string') {
+ await advanceSessionReadCursor(tx, {
+ sessionId: session.id,
+ userId: messageUserId,
+ eventAt: message.ts,
+ eventId: message.eventId,
+ });
+ } else if (message.role === 'assistant') {
+ await advanceSessionNotifiedCursor(tx, {
+ sessionId: session.id,
+ eventAt: message.ts,
+ eventId: message.eventId,
+ });
+ }
+ }
+ }
});
},
diff --git a/packages/cloud-agents/src/server/fast-agent/fast-agent-service.ts b/packages/cloud-agents/src/server/fast-agent/fast-agent-service.ts
index 73568576a..5522bf3b5 100644
--- a/packages/cloud-agents/src/server/fast-agent/fast-agent-service.ts
+++ b/packages/cloud-agents/src/server/fast-agent/fast-agent-service.ts
@@ -22,8 +22,16 @@ import {
appendFastAgentMemory,
db,
getDeploymentTaskModelOptions,
+ getSessionForFastConversation,
+ getSessionForTask,
isBrainProviderConfigured,
+ touchSessionActivity,
} from '@roomote/db/server';
+import {
+ FeatureFlag,
+ getFeatureFlagEvaluator,
+} from '@roomote/feature-flags/server';
+import { getRedis } from '@roomote/redis';
import { Env } from '@roomote/env';
import { z } from 'zod';
@@ -109,6 +117,17 @@ const chatReactionArgsSchema = z.object({
const FAST_AGENT_DEFAULT_SLACK_HISTORY_LOOKBACK_MS = 24 * 60 * 60 * 1000;
const FAST_AGENT_CANONICAL_TOOL_OUTPUT_MAX_CHARS = 50_000;
+async function setFastSessionResponding(
+ fastConversationId: string,
+ responding: boolean,
+): Promise {
+ const session = await getSessionForFastConversation(db, fastConversationId);
+ if (!session) return;
+ await touchSessionActivity(db, session.id, Math.floor(Date.now() / 1000), {
+ conversationResponding: responding,
+ });
+}
+
function buildFastAgentTurnId({
currentMessageId,
conversation,
@@ -956,6 +975,11 @@ export async function answerFastAgentQuestion({
}),
]);
canonicalConversationId = session.id;
+ await setFastSessionResponding(session.id, true).catch((error) => {
+ console.warn(
+ `[sessions] Failed to mark Fast Session active: ${formatErrorForLog(error)}`,
+ );
+ });
durableOpenCodeSessionId = session.openCodeSessionId;
activeOpenCodeSessionId = session.openCodeSessionId;
diagnostics.setCanonicalConversationId(session.id);
@@ -1450,12 +1474,34 @@ export async function answerFastAgentQuestion({
taskUrl?: string;
taskLinkRendered?: boolean;
}) => {
+ let sessionCommsEnabled = false;
+ let linkedSession: Awaited> =
+ null;
+ try {
+ sessionCommsEnabled = await getFeatureFlagEvaluator(
+ getRedis(),
+ ).evaluate(FeatureFlag.SessionsComms, {
+ isDeploymentContext: true,
+ });
+ linkedSession = sessionCommsEnabled
+ ? await getSessionForTask(db, task.taskId)
+ : null;
+ } catch (error) {
+ console.warn(
+ `[sessions] Failed to resolve Session kickoff link: ${formatErrorForLog(error)}`,
+ );
+ }
+ const destinationUrl = linkedSession
+ ? `${Env.R_APP_URL}/sessions/${linkedSession.id}?task=${task.taskId}`
+ : task.taskUrl;
const message = [
- args.kickoffMessage,
- task.taskUrl &&
+ sessionCommsEnabled
+ ? `Preparing workspace…\n\n${args.kickoffMessage}`
+ : args.kickoffMessage,
+ destinationUrl &&
!task.taskLinkRendered &&
- !args.kickoffMessage.includes(task.taskUrl)
- ? `[Open the task](${task.taskUrl})`
+ !args.kickoffMessage.includes(destinationUrl)
+ ? `[${sessionCommsEnabled ? 'Open in Roomote' : 'Open the task'}](${destinationUrl})`
: undefined,
]
.filter((part): part is string => Boolean(part))
@@ -1703,6 +1749,7 @@ export async function answerFastAgentQuestion({
return await generateTrackedNonTaskTextInOpenCodeSession(
{
userId,
+ fastConversationId: session.id,
surface:
NON_TASK_INFERENCE_SURFACES.fastAgentQuestionAnswering,
modelRole: FAST_AGENT_MODEL_ROLE,
@@ -1963,6 +2010,15 @@ export async function answerFastAgentQuestion({
}
return lastVisibleMessage || message;
} finally {
+ if (canonicalConversationId) {
+ await setFastSessionResponding(canonicalConversationId, false).catch(
+ (error) => {
+ console.warn(
+ `[sessions] Failed to settle Fast Session status: ${formatErrorForLog(error)}`,
+ );
+ },
+ );
+ }
diagnostics.finish();
}
}
diff --git a/packages/cloud-agents/src/server/non-task-provider-usage.ts b/packages/cloud-agents/src/server/non-task-provider-usage.ts
index 273f0fbf7..4e8d96394 100644
--- a/packages/cloud-agents/src/server/non-task-provider-usage.ts
+++ b/packages/cloud-agents/src/server/non-task-provider-usage.ts
@@ -91,6 +91,7 @@ export type NonTaskInferenceTrackingInput = {
surface: string;
userId?: string | null;
taskId?: string | null;
+ fastConversationId?: string | null;
provider?: string;
};
@@ -359,6 +360,9 @@ async function recordNonTaskOpenCodeUsage(
usageType: 'inference',
eventKey: `non-task:${params.surface}:${harnessSessionId}:${messageId}`,
taskId: params.taskId ?? null,
+ ...(params.fastConversationId
+ ? { fastConversationId: params.fastConversationId }
+ : {}),
userId: params.userId ?? null,
harnessSessionId,
messageId,
diff --git a/packages/cloud-agents/src/server/task-run-queue.ts b/packages/cloud-agents/src/server/task-run-queue.ts
index 28553bb41..63b94befb 100644
--- a/packages/cloud-agents/src/server/task-run-queue.ts
+++ b/packages/cloud-agents/src/server/task-run-queue.ts
@@ -48,6 +48,7 @@ import {
db,
deploymentSettings,
ensureAutomationRowsOnce,
+ ensureSessionForTask,
isChatGptSubscriptionConnected,
createTaskWithRetry,
markTaskStartParallelCountEndedAt,
@@ -71,6 +72,10 @@ import {
resolveWorkspaceRepositoryProviders,
sql,
} from '@roomote/db/server';
+import {
+ FeatureFlag,
+ getFeatureFlagEvaluator,
+} from '@roomote/feature-flags/server';
import { type Redis, getRedis } from '@roomote/redis';
import {
captureActivationTaskCreated,
@@ -1385,6 +1390,9 @@ async function enqueueFreshLaunch(
const { task, initiator, workflow, surface, trigger } = input;
const visibility: TaskVisibility = input.visibility ?? 'visible';
const linkedUserId = getTaskInitiatorLinkedUserId(initiator);
+ const sessionsDataEnabled = await getFeatureFlagEvaluator(
+ getRedis(),
+ ).evaluate(FeatureFlag.SessionsData, { isDeploymentContext: true });
await assertUserIsNotDeleted(linkedUserId);
@@ -1637,6 +1645,16 @@ async function enqueueFreshLaunch(
});
if (activeRun) {
+ if (sessionsDataEnabled) {
+ await ensureSessionForTask(tx, {
+ taskId: existingTask.id,
+ fastConversationId:
+ getFastAgentParentFromPayload(taskWithHarnessOverrides.payload)
+ ?.sessionId ?? null,
+ origin: 'follow_up',
+ existingTaskReused: true,
+ });
+ }
return { taskRun: activeRun, createdRun: false, reusedTask: true };
}
@@ -1686,6 +1704,22 @@ async function enqueueFreshLaunch(
taskId = createdTask.id;
}
+ if (sessionsDataEnabled) {
+ const fastParent = getFastAgentParentFromPayload(
+ taskWithHarnessOverrides.payload,
+ );
+ await ensureSessionForTask(tx, {
+ taskId,
+ fastConversationId: fastParent?.sessionId ?? null,
+ origin: fastParent
+ ? 'fast_delegation'
+ : existingTask
+ ? 'follow_up'
+ : 'direct_launch',
+ existingTaskReused: Boolean(existingTask),
+ });
+ }
+
if (input.prLinkage) {
const prLinkage = {
sourceControlProvider: input.prLinkage.provider,
@@ -1802,6 +1836,20 @@ async function enqueueFreshLaunch(
return taskRun;
}
+ if (sessionsDataEnabled) {
+ const delegated = Boolean(
+ reusedTask ||
+ getFastAgentParentFromPayload(taskWithHarnessOverrides.payload),
+ );
+ void captureEvent(
+ delegated ? 'session_task_delegated' : 'session_created',
+ {
+ ...(linkedUserId ? { userId: linkedUserId } : {}),
+ properties: { surface, outcome: 'created' },
+ },
+ );
+ }
+
if (shouldCaptureTaskCreatedEvent(taskRun.payloadKind)) {
// Anonymous analytics (no-op unless enabled): task creation with
// non-identifying routing facts only.
diff --git a/packages/communication/src/fast-session-footer.ts b/packages/communication/src/fast-session-footer.ts
index 131d24fc0..919586109 100644
--- a/packages/communication/src/fast-session-footer.ts
+++ b/packages/communication/src/fast-session-footer.ts
@@ -6,7 +6,11 @@ import {
} from './chat-messages';
import { chunkDiscordMessage } from './discord-provider';
-export type FastSessionFooterProvider = 'slack' | 'discord';
+export type FastSessionFooterProvider =
+ | 'slack'
+ | 'discord'
+ | 'teams'
+ | 'telegram';
export function buildFastSessionUrl(
provider: FastSessionFooterProvider,
@@ -36,10 +40,12 @@ export function buildFastSessionReplyFooterText(params: {
explicitMentionRequired: false,
...(params.provider === 'slack'
? { formatLink: (label: string, url: string) => `<${url}|${label}>` }
- : {
- formatLink: formatMarkdownLink,
- formatFooterText: (text: string) => `-# ${text}`,
- }),
+ : params.provider === 'telegram'
+ ? { formatLink: (label: string, url: string) => `${label} (${url})` }
+ : {
+ formatLink: formatMarkdownLink,
+ formatFooterText: (text: string) => `-# ${text}`,
+ }),
});
}
diff --git a/packages/db/src/lib/__tests__/sessions.test.ts b/packages/db/src/lib/__tests__/sessions.test.ts
index 9754c6d62..342e5da13 100644
--- a/packages/db/src/lib/__tests__/sessions.test.ts
+++ b/packages/db/src/lib/__tests__/sessions.test.ts
@@ -2,6 +2,8 @@ import {
db,
eq,
fastAgentConversations,
+ llmUsageEvents,
+ recordLlmUsage,
sessionFactory,
sessionParticipants,
sessions,
@@ -13,7 +15,10 @@ import {
} from '../../server';
import {
+ advanceSessionReadCursor,
+ advanceSessionNotifiedCursor,
deriveSessionStatus,
+ ensureSessionForFastConversation,
ensureSessionForTask,
touchSessionActivity,
} from '../sessions';
@@ -104,6 +109,21 @@ describe('session helpers', () => {
expect(updated.activityAt).toBe(200);
});
+ it('keeps Fast-only Sessions active while a conversation is responding', async () => {
+ const session = await sessionFactory.create({ cachedStatus: 'ready' });
+ createdSessionIds.push(session.id);
+
+ const active = await touchSessionActivity(db, session.id, 200, {
+ conversationResponding: true,
+ });
+ const ready = await touchSessionActivity(db, session.id, 201, {
+ conversationResponding: false,
+ });
+
+ expect(active.cachedStatus).toBe('active');
+ expect(ready.cachedStatus).toBe('ready');
+ });
+
it('recomputes cached status from linked tasks while touching activity', async () => {
const session = await sessionFactory.create({
activityAt: 100,
@@ -311,4 +331,119 @@ describe('session helpers', () => {
.where(eq(sessionTasks.sessionId, first!.id)),
).toHaveLength(2);
});
+
+ it('creates one Session when a Fast conversation is created repeatedly', async () => {
+ const user = await userFactory.create();
+ createdUserIds.push(user.id);
+ const [conversation] = await db
+ .insert(fastAgentConversations)
+ .values({
+ userId: user.id,
+ surface: 'web',
+ workspaceId: `workspace-${crypto.randomUUID()}`,
+ conversationId: `conversation-${crypto.randomUUID()}`,
+ })
+ .returning();
+ createdConversationIds.push(conversation!.id);
+
+ const first = await db.transaction((tx) =>
+ ensureSessionForFastConversation(tx, conversation!.id),
+ );
+ const second = await db.transaction((tx) =>
+ ensureSessionForFastConversation(tx, conversation!.id),
+ );
+ createdSessionIds.push(first.id);
+
+ expect(second.id).toBe(first.id);
+ expect(
+ await db
+ .select()
+ .from(sessions)
+ .where(eq(sessions.fastConversationId, conversation!.id)),
+ ).toHaveLength(1);
+ });
+
+ it('never regresses a participant read cursor', async () => {
+ const user = await userFactory.create();
+ createdUserIds.push(user.id);
+ const session = await sessionFactory.create({
+ ownerKind: 'user',
+ ownerUserId: user.id,
+ });
+ createdSessionIds.push(session.id);
+
+ await advanceSessionReadCursor(db, {
+ sessionId: session.id,
+ userId: user.id,
+ eventAt: 200,
+ eventId: 'event-b',
+ });
+ const current = await advanceSessionReadCursor(db, {
+ sessionId: session.id,
+ userId: user.id,
+ eventAt: 100,
+ eventId: 'event-a',
+ });
+
+ expect(current.lastReadEventAt).toBe(200);
+ expect(current.lastReadEventId).toBe('event-b');
+ });
+
+ it('advances participant notification cursors monotonically', async () => {
+ const user = await userFactory.create();
+ createdUserIds.push(user.id);
+ const session = await sessionFactory.create({
+ ownerKind: 'user',
+ ownerUserId: user.id,
+ });
+ createdSessionIds.push(session.id);
+ await db.insert(sessionParticipants).values({
+ sessionId: session.id,
+ userId: user.id,
+ role: 'owner',
+ });
+
+ await advanceSessionNotifiedCursor(db, {
+ sessionId: session.id,
+ eventAt: 200,
+ eventId: 'event-b',
+ });
+ await advanceSessionNotifiedCursor(db, {
+ sessionId: session.id,
+ eventAt: 100,
+ eventId: 'event-a',
+ });
+
+ const [participant] = await db
+ .select()
+ .from(sessionParticipants)
+ .where(eq(sessionParticipants.sessionId, session.id));
+ expect(participant?.lastNotifiedEventAt).toBe(200);
+ expect(participant?.lastNotifiedEventId).toBe('event-b');
+ });
+
+ it('stamps new task usage with the owning Session', async () => {
+ const task = await taskFactory.create();
+ createdTaskIds.push(task.id);
+ const session = await sessionFactory.create();
+ createdSessionIds.push(session.id);
+ await db.insert(sessionTasks).values({
+ sessionId: session.id,
+ taskId: task.id,
+ origin: 'direct_launch',
+ });
+
+ await recordLlmUsage({
+ taskId: task.id,
+ eventKey: `session-usage:${crypto.randomUUID()}`,
+ inputTokens: 10,
+ outputTokens: 5,
+ });
+
+ const [usage] = await db
+ .select({ sessionId: llmUsageEvents.sessionId })
+ .from(llmUsageEvents)
+ .where(eq(llmUsageEvents.taskId, task.id));
+ expect(usage?.sessionId).toBe(session.id);
+ });
});
diff --git a/packages/db/src/lib/llm-usage.ts b/packages/db/src/lib/llm-usage.ts
index 0b5c7e70b..77bff508b 100644
--- a/packages/db/src/lib/llm-usage.ts
+++ b/packages/db/src/lib/llm-usage.ts
@@ -1,7 +1,9 @@
+import { eq } from 'drizzle-orm';
+
import type { LlmUsageCostSource } from '@roomote/types';
import { db } from '../db';
-import { llmUsageEvents } from '../schema';
+import { llmUsageEvents, sessions, sessionTasks } from '../schema';
export interface RecordLlmUsageInput {
source?: string;
@@ -11,6 +13,7 @@ export interface RecordLlmUsageInput {
runId?: number | null;
userId?: string | null;
environmentId?: string | null;
+ fastConversationId?: string | null;
harnessSessionId?: string | null;
messageId?: string | null;
providerId?: string | null;
@@ -100,6 +103,19 @@ export async function recordLlmUsage(
: clampOptionalInteger(input.contextTokens);
const costSource = input.costSource ?? 'missing';
const agent = normalizeAgent(input.agent);
+ const sessionId = input.fastConversationId
+ ? await db.query.sessions.findFirst({
+ where: eq(sessions.fastConversationId, input.fastConversationId),
+ columns: { id: true },
+ })
+ : input.taskId
+ ? await db
+ .select({ id: sessionTasks.sessionId })
+ .from(sessionTasks)
+ .where(eq(sessionTasks.taskId, input.taskId))
+ .limit(1)
+ .then((rows) => rows[0])
+ : null;
const values = {
source: input.source ?? 'roomote',
@@ -108,6 +124,7 @@ export async function recordLlmUsage(
runId: input.runId ?? null,
userId: input.userId ?? null,
environmentId: input.environmentId ?? null,
+ sessionId: sessionId?.id ?? null,
eventKey: input.eventKey ?? null,
harnessSessionId: input.harnessSessionId ?? null,
messageId: input.messageId ?? null,
diff --git a/packages/db/src/lib/sessions.ts b/packages/db/src/lib/sessions.ts
index 25e591572..44ab88d72 100644
--- a/packages/db/src/lib/sessions.ts
+++ b/packages/db/src/lib/sessions.ts
@@ -1,4 +1,4 @@
-import { and, desc, eq, sql } from 'drizzle-orm';
+import { and, desc, eq, or, sql } from 'drizzle-orm';
import type { TaskGoalStatus, TaskState } from '@roomote/types';
@@ -7,6 +7,7 @@ import {
sessionParticipants,
sessions,
sessionTasks,
+ fastAgentConversations,
taskRuns,
tasks,
type SessionStatus,
@@ -60,7 +61,10 @@ export async function touchSessionActivity(
dbOrTx: DatabaseOrTransaction,
sessionId: string,
at: number,
- options: { conversationResponding?: boolean } = {},
+ options: {
+ conversationResponding?: boolean;
+ recomputeStatus?: boolean;
+ } = {},
): Promise {
return runInTransactionIfAvailable(dbOrTx, async (tx) => {
const [lockedSession] = await tx
@@ -81,7 +85,7 @@ async function refreshLockedSession(
tx: DatabaseOrTransaction,
sessionId: string,
at: number,
- options: { conversationResponding?: boolean },
+ options: { conversationResponding?: boolean; recomputeStatus?: boolean },
): Promise {
const linkedTasks = await tx
.selectDistinctOn([tasks.id], {
@@ -99,10 +103,14 @@ async function refreshLockedSession(
.update(sessions)
.set({
activityAt: sql`GREATEST(${sessions.activityAt}, ${at})`,
- cachedStatus: deriveSessionStatus({
- conversationResponding: options.conversationResponding ?? false,
- tasks: linkedTasks,
- }),
+ ...(options.recomputeStatus === false
+ ? {}
+ : {
+ cachedStatus: deriveSessionStatus({
+ conversationResponding: options.conversationResponding ?? false,
+ tasks: linkedTasks,
+ }),
+ }),
updatedAt: new Date(),
})
.where(eq(sessions.id, sessionId))
@@ -120,6 +128,69 @@ export type EnsureSessionForTaskInput = {
existingTaskReused?: boolean;
};
+export async function ensureSessionForFastConversation(
+ tx: DatabaseOrTransaction,
+ fastConversationId: string,
+): Promise {
+ const [conversation] = await tx
+ .select({
+ id: fastAgentConversations.id,
+ userId: fastAgentConversations.userId,
+ surface: fastAgentConversations.surface,
+ title: fastAgentConversations.title,
+ updatedAt: fastAgentConversations.updatedAt,
+ })
+ .from(fastAgentConversations)
+ .where(eq(fastAgentConversations.id, fastConversationId))
+ .for('update');
+
+ if (!conversation) {
+ throw new Error(`Fast conversation ${fastConversationId} does not exist.`);
+ }
+
+ const existing = await getSessionForFastConversation(tx, conversation.id);
+ if (existing) {
+ return existing;
+ }
+
+ const activityAt = Math.floor(conversation.updatedAt.getTime() / 1000);
+ const [inserted] = await tx
+ .insert(sessions)
+ .values({
+ title: conversation.title?.trim() || 'New session',
+ ownerKind: 'user',
+ ownerUserId: conversation.userId,
+ sourceSurface: conversation.surface,
+ sourceTrigger:
+ conversation.surface === 'automation' ? 'schedule' : 'message',
+ fastConversationId: conversation.id,
+ visibility: 'visible',
+ activityAt,
+ cachedStatus: 'ready',
+ })
+ .onConflictDoNothing()
+ .returning();
+
+ const session =
+ inserted ?? (await getSessionForFastConversation(tx, conversation.id));
+ if (!session) {
+ throw new Error(
+ `Failed to create a Session for Fast conversation ${conversation.id}.`,
+ );
+ }
+
+ await tx
+ .insert(sessionParticipants)
+ .values({
+ sessionId: session.id,
+ userId: conversation.userId,
+ role: 'owner',
+ })
+ .onConflictDoNothing();
+
+ return session;
+}
+
/**
* Ensures a visible task has one canonical Session inside the caller's
* transaction. The tables are additive and ignored by N-1 application code.
@@ -154,13 +225,13 @@ export async function ensureSessionForTask(
return null;
}
- const existing = await findSessionForTask(tx, task.id);
+ const existing = await getSessionForTask(tx, task.id);
if (existing) {
return existing;
}
let session = input.fastConversationId
- ? await findSessionForFastConversation(tx, input.fastConversationId)
+ ? await getSessionForFastConversation(tx, input.fastConversationId)
: null;
let createdCandidate = false;
@@ -211,7 +282,7 @@ export async function ensureSessionForTask(
session =
inserted ??
(input.fastConversationId
- ? await findSessionForFastConversation(tx, input.fastConversationId)
+ ? await getSessionForFastConversation(tx, input.fastConversationId)
: null);
createdCandidate = inserted !== undefined;
}
@@ -231,7 +302,7 @@ export async function ensureSessionForTask(
.returning({ sessionId: sessionTasks.sessionId });
if (!attached) {
- const canonical = await findSessionForTask(tx, task.id);
+ const canonical = await getSessionForTask(tx, task.id);
if (!canonical) {
throw new Error(`Failed to attach task ${task.id} to a Session.`);
}
@@ -257,7 +328,7 @@ export async function ensureSessionForTask(
return touchSessionActivity(tx, session.id, task.activityAt);
}
-async function findSessionForTask(
+export async function getSessionForTask(
tx: DatabaseOrTransaction,
taskId: string,
): Promise {
@@ -271,7 +342,7 @@ async function findSessionForTask(
return session?.session ?? null;
}
-async function findSessionForFastConversation(
+export async function getSessionForFastConversation(
tx: DatabaseOrTransaction,
fastConversationId: string,
): Promise {
@@ -288,3 +359,108 @@ async function findSessionForFastConversation(
return session ?? null;
}
+
+export async function touchSessionForTask(
+ tx: DatabaseOrTransaction,
+ taskId: string,
+ at: number,
+): Promise {
+ const session = await getSessionForTask(tx, taskId);
+ return session ? touchSessionActivity(tx, session.id, at) : null;
+}
+
+export async function advanceSessionReadCursor(
+ dbOrTx: DatabaseOrTransaction,
+ input: {
+ sessionId: string;
+ userId: string;
+ eventAt: number;
+ eventId: string;
+ },
+) {
+ return runInTransactionIfAvailable(dbOrTx, async (tx) => {
+ const [lockedSession] = await tx
+ .select({ id: sessions.id })
+ .from(sessions)
+ .where(eq(sessions.id, input.sessionId))
+ .for('update');
+ if (!lockedSession) {
+ throw new Error(`Session ${input.sessionId} does not exist.`);
+ }
+
+ const [participant] = await tx
+ .insert(sessionParticipants)
+ .values({
+ sessionId: input.sessionId,
+ userId: input.userId,
+ role: 'member',
+ lastReadEventAt: input.eventAt,
+ lastReadEventId: input.eventId,
+ })
+ .onConflictDoUpdate({
+ target: [sessionParticipants.sessionId, sessionParticipants.userId],
+ set: {
+ lastReadEventAt: input.eventAt,
+ lastReadEventId: input.eventId,
+ updatedAt: new Date(),
+ },
+ setWhere: or(
+ sql`${sessionParticipants.lastReadEventAt} IS NULL`,
+ sql`${sessionParticipants.lastReadEventAt} < ${input.eventAt}`,
+ and(
+ eq(sessionParticipants.lastReadEventAt, input.eventAt),
+ or(
+ sql`${sessionParticipants.lastReadEventId} IS NULL`,
+ sql`${sessionParticipants.lastReadEventId} < ${input.eventId}`,
+ ),
+ ),
+ ),
+ })
+ .returning();
+
+ if (participant) return participant;
+
+ const [current] = await tx
+ .select()
+ .from(sessionParticipants)
+ .where(
+ and(
+ eq(sessionParticipants.sessionId, input.sessionId),
+ eq(sessionParticipants.userId, input.userId),
+ ),
+ );
+ if (!current) {
+ throw new Error('Failed to advance Session read cursor.');
+ }
+ return current;
+ });
+}
+
+export async function advanceSessionNotifiedCursor(
+ tx: DatabaseOrTransaction,
+ input: { sessionId: string; eventAt: number; eventId: string },
+): Promise {
+ await tx
+ .update(sessionParticipants)
+ .set({
+ lastNotifiedEventAt: input.eventAt,
+ lastNotifiedEventId: input.eventId,
+ updatedAt: new Date(),
+ })
+ .where(
+ and(
+ eq(sessionParticipants.sessionId, input.sessionId),
+ or(
+ sql`${sessionParticipants.lastNotifiedEventAt} IS NULL`,
+ sql`${sessionParticipants.lastNotifiedEventAt} < ${input.eventAt}`,
+ and(
+ eq(sessionParticipants.lastNotifiedEventAt, input.eventAt),
+ or(
+ sql`${sessionParticipants.lastNotifiedEventId} IS NULL`,
+ sql`${sessionParticipants.lastNotifiedEventId} < ${input.eventId}`,
+ ),
+ ),
+ ),
+ ),
+ );
+}
diff --git a/packages/db/src/lib/sync-task-state.ts b/packages/db/src/lib/sync-task-state.ts
index 87cc00160..6d0d92a9a 100644
--- a/packages/db/src/lib/sync-task-state.ts
+++ b/packages/db/src/lib/sync-task-state.ts
@@ -3,6 +3,7 @@ import { RunStatus, type TaskState } from '@roomote/types';
import { type DatabaseOrTransaction } from '../db';
import { taskRuns, tasks } from '../schema';
+import { touchSessionForTask } from './sessions';
/**
* Run statuses that keep the owning task 'active': the sandbox is still (or
@@ -134,4 +135,6 @@ export async function syncTaskStateFromRuns(
.update(tasks)
.set({ state: nextState, updatedAt: new Date() })
.where(and(eq(tasks.id, taskId), ne(tasks.state, nextState)));
+
+ await touchSessionForTask(tx, taskId, Math.floor(Date.now() / 1000));
}
diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts
index 76ec5af03..0ca976309 100644
--- a/packages/db/src/schema.ts
+++ b/packages/db/src/schema.ts
@@ -1795,6 +1795,9 @@ export const llmUsageEvents = pgTable(
environmentId: uuid('environment_id').references(() => environments.id, {
onDelete: 'set null',
}),
+ sessionId: uuid('session_id').references(() => sessions.id, {
+ onDelete: 'set null',
+ }),
// Non-task producers use eventKey for idempotency. Task harness events use
// the session/message pair below because a message may be retried with
// progressively richer usage data.
@@ -1862,6 +1865,7 @@ export const llmUsageEvents = pgTable(
index('task_inference_usage_events_environment_id_idx').on(
table.environmentId,
),
+ index('task_inference_usage_events_session_id_idx').on(table.sessionId),
index('task_inference_usage_events_provider_model_idx').on(
table.providerId,
table.modelId,
@@ -1887,6 +1891,10 @@ export const llmUsageEventsRelations = relations(llmUsageEvents, ({ one }) => ({
fields: [llmUsageEvents.environmentId],
references: [environments.id],
}),
+ session: one(sessions, {
+ fields: [llmUsageEvents.sessionId],
+ references: [sessions.id],
+ }),
}));
export const taskSlackReplyDetails = pgTable(
@@ -3465,6 +3473,11 @@ export type SessionTaskOrigin =
| 'backfill'
| 'follow_up';
export type SessionParticipantRole = 'owner' | 'member';
+export type SessionBackfillPhase =
+ | 'fast_conversations'
+ | 'fast_tasks'
+ | 'tasks'
+ | 'participants';
/**
* sessions
@@ -3593,6 +3606,7 @@ export const sessionParticipants = pgTable(
lastReadEventAt: bigint('last_read_event_at', { mode: 'number' }),
lastReadEventId: text('last_read_event_id'),
lastNotifiedEventAt: bigint('last_notified_event_at', { mode: 'number' }),
+ lastNotifiedEventId: text('last_notified_event_id'),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
@@ -3609,6 +3623,58 @@ export const sessionParticipants = pgTable(
],
);
+/** User-scoped Session pins mirror task pins without changing task storage. */
+export const sessionPins = pgTable(
+ 'session_pins',
+ {
+ id: uuid('id').primaryKey().defaultRandom(),
+ sessionId: uuid('session_id')
+ .notNull()
+ .references(() => sessions.id, { onDelete: 'cascade' }),
+ userId: text('user_id')
+ .notNull()
+ .references(() => users.id, { onDelete: 'cascade' }),
+ createdAt: timestamp('created_at').notNull().defaultNow(),
+ updatedAt: timestamp('updated_at').notNull().defaultNow(),
+ },
+ (table) => [
+ uniqueIndex('session_pins_user_session_unique').on(
+ table.userId,
+ table.sessionId,
+ ),
+ index('session_pins_user_updated_at_idx').on(table.userId, table.updatedAt),
+ index('session_pins_session_id_idx').on(table.sessionId),
+ ],
+);
+
+/** Durable bounded-backfill position retained independently for N-1 safety. */
+export const sessionBackfillState = pgTable(
+ 'session_backfill_state',
+ {
+ key: text('key').primaryKey(),
+ phase: text('phase')
+ .notNull()
+ .default('fast_conversations')
+ .$type(),
+ cursorCreatedAt: timestamp('cursor_created_at'),
+ cursorId: text('cursor_id'),
+ completedAt: timestamp('completed_at'),
+ lastRunAt: timestamp('last_run_at'),
+ createdAt: timestamp('created_at').notNull().defaultNow(),
+ updatedAt: timestamp('updated_at').notNull().defaultNow(),
+ },
+ (table) => [
+ check(
+ 'session_backfill_state_phase_check',
+ sql`${table.phase} in ('fast_conversations', 'fast_tasks', 'tasks', 'participants')`,
+ ),
+ check(
+ 'session_backfill_state_cursor_shape_check',
+ sql`(${table.cursorCreatedAt} IS NULL) = (${table.cursorId} IS NULL)`,
+ ),
+ ],
+);
+
export const sessionsRelations = relations(sessions, ({ one, many }) => ({
ownerUser: one(users, {
fields: [sessions.ownerUserId],
@@ -3625,6 +3691,8 @@ export const sessionsRelations = relations(sessions, ({ one, many }) => ({
}),
tasks: many(sessionTasks),
participants: many(sessionParticipants),
+ pins: many(sessionPins),
+ usageEvents: many(llmUsageEvents),
}));
export const sessionTasksRelations = relations(sessionTasks, ({ one }) => ({
@@ -3652,6 +3720,17 @@ export const sessionParticipantsRelations = relations(
}),
);
+export const sessionPinsRelations = relations(sessionPins, ({ one }) => ({
+ session: one(sessions, {
+ fields: [sessionPins.sessionId],
+ references: [sessions.id],
+ }),
+ user: one(users, {
+ fields: [sessionPins.userId],
+ references: [users.id],
+ }),
+}));
+
/**
* custom_automations
*
diff --git a/packages/db/src/server.ts b/packages/db/src/server.ts
index eba4174d1..a617550ba 100644
--- a/packages/db/src/server.ts
+++ b/packages/db/src/server.ts
@@ -126,6 +126,9 @@ export {
sessionTasksRelations,
sessionParticipants,
sessionParticipantsRelations,
+ sessionPins,
+ sessionPinsRelations,
+ sessionBackfillState,
taskArtifacts,
taskArtifactsRelations,
taskPullRequests,
@@ -252,5 +255,6 @@ export type {
SessionStatus,
SessionTaskOrigin,
SessionParticipantRole,
+ SessionBackfillPhase,
} from './schema';
export type { AutomationWorkItemDisposition } from '@roomote/types';
diff --git a/packages/db/src/types.ts b/packages/db/src/types.ts
index f93a1333d..67ab8e2f1 100644
--- a/packages/db/src/types.ts
+++ b/packages/db/src/types.ts
@@ -26,6 +26,8 @@ import type {
sessions,
sessionTasks,
sessionParticipants,
+ sessionPins,
+ sessionBackfillState,
taskPullRequests,
taskRuns,
taskRunEvents,
@@ -124,6 +126,9 @@ export type CreateSessionParticipant = Omit<
Generated
>;
+export type SessionBackfillState = typeof sessionBackfillState.$inferSelect;
+export type SessionPin = typeof sessionPins.$inferSelect;
+
/**
* taskPullRequests
*/
diff --git a/packages/feature-flags/src/__tests__/config.test.ts b/packages/feature-flags/src/__tests__/config.test.ts
index 0a0e08a52..ad1ebcacd 100644
--- a/packages/feature-flags/src/__tests__/config.test.ts
+++ b/packages/feature-flags/src/__tests__/config.test.ts
@@ -4,8 +4,18 @@ import { FEATURE_FLAG_CONFIG } from '../config';
import { FeatureFlag } from '../types';
describe('feature flags', () => {
- it('defines zero recognized flags and zero config entries', () => {
- expect(FeatureFlag).toEqual({});
- expect(FEATURE_FLAG_CONFIG).toEqual({});
+ it('defines the independently reversible Sessions rollout flags', () => {
+ expect(FeatureFlag).toEqual({
+ SessionsData: 'sessions_data',
+ SessionsUi: 'sessions_ui',
+ SessionsComms: 'sessions_comms',
+ });
+ expect(FEATURE_FLAG_CONFIG).toEqual(
+ expect.objectContaining({
+ sessions_data: expect.objectContaining({ defaultValue: false }),
+ sessions_ui: expect.objectContaining({ defaultValue: false }),
+ sessions_comms: expect.objectContaining({ defaultValue: false }),
+ }),
+ );
});
});
diff --git a/packages/feature-flags/src/__tests__/evaluateFlagFromMetadata.test.ts b/packages/feature-flags/src/__tests__/evaluateFlagFromMetadata.test.ts
index f4b67ed32..324d148e6 100644
--- a/packages/feature-flags/src/__tests__/evaluateFlagFromMetadata.test.ts
+++ b/packages/feature-flags/src/__tests__/evaluateFlagFromMetadata.test.ts
@@ -23,7 +23,7 @@ describe('generic feature flag evaluation', () => {
});
});
- it('evaluates all flags to an empty object even with stale metadata', () => {
+ it('ignores stale metadata and keeps Sessions flags disabled by default', () => {
expect(
evaluateFeatureFlagsFromMetadata({
slack_eval_launcher: true,
@@ -33,6 +33,24 @@ describe('generic feature flag evaluation', () => {
background_subagents: true,
opencode_code_mode: true,
}),
- ).toEqual({});
+ ).toEqual({
+ sessions_data: false,
+ sessions_ui: false,
+ sessions_comms: false,
+ });
+ });
+
+ it('evaluates each Sessions rollout flag independently', () => {
+ expect(
+ evaluateFeatureFlagsFromMetadata({
+ sessions_data: true,
+ sessions_ui: 'true',
+ sessions_comms: false,
+ }),
+ ).toEqual({
+ sessions_data: true,
+ sessions_ui: true,
+ sessions_comms: false,
+ });
});
});
diff --git a/packages/feature-flags/src/config.ts b/packages/feature-flags/src/config.ts
index 107bb2cd6..9c94f5f4d 100644
--- a/packages/feature-flags/src/config.ts
+++ b/packages/feature-flags/src/config.ts
@@ -1,6 +1,25 @@
import type { FeatureFlagConfigMap, MetadataBooleanDescriptor } from './types';
-export const FEATURE_FLAG_CONFIG: FeatureFlagConfigMap = {};
+export const FEATURE_FLAG_CONFIG: FeatureFlagConfigMap = {
+ sessions_data: {
+ defaultValue: false,
+ metadataKey: 'sessions_data',
+ description: 'Create and reconcile unified Session records',
+ group: 'Sessions',
+ },
+ sessions_ui: {
+ defaultValue: false,
+ metadataKey: 'sessions_ui',
+ description: 'Use Sessions as the primary dashboard navigation unit',
+ group: 'Sessions',
+ },
+ sessions_comms: {
+ defaultValue: false,
+ metadataKey: 'sessions_comms',
+ description: 'Use Session-aware communication wording and links',
+ group: 'Sessions',
+ },
+};
/**
* Non-feature-flag boolean deployment metadata that is still actively read in
diff --git a/packages/feature-flags/src/types.ts b/packages/feature-flags/src/types.ts
index eb8b981a9..649b2f060 100644
--- a/packages/feature-flags/src/types.ts
+++ b/packages/feature-flags/src/types.ts
@@ -2,7 +2,11 @@
* Feature flag types and configuration
*/
-export const FeatureFlag = {} as const;
+export const FeatureFlag = {
+ SessionsData: 'sessions_data',
+ SessionsUi: 'sessions_ui',
+ SessionsComms: 'sessions_comms',
+} as const;
export type FeatureFlag = (typeof FeatureFlag)[keyof typeof FeatureFlag];
@@ -37,7 +41,7 @@ export type FeatureFlagConfigMap = {
};
export type FeatureFlagValues = {
- [K in FeatureFlag]: boolean;
+ [K in FeatureFlag]?: boolean;
};
export type FeatureFlagContext =
diff --git a/packages/sdk/src/server/routers/task-runs.ts b/packages/sdk/src/server/routers/task-runs.ts
index 51807dbff..6ec0a5fab 100644
--- a/packages/sdk/src/server/routers/task-runs.ts
+++ b/packages/sdk/src/server/routers/task-runs.ts
@@ -8,6 +8,7 @@ import {
getTaskGoalForRun,
isNotNull,
releaseTaskGoalContinuationForRun,
+ sessionTasks,
slackInstallations,
taskPullRequests,
} from '@roomote/db/server';
@@ -401,10 +402,15 @@ export const taskRunsRouter = router({
.input(enqueueTaskInputSchema)
.mutation(async ({ input }) => {
const launchResult = await enqueueTask(input as EnqueueTaskInput);
+ const linkedSession = await db.query.sessionTasks.findFirst({
+ where: eq(sessionTasks.taskId, launchResult.taskId),
+ columns: { sessionId: true },
+ });
return {
id: launchResult.id,
taskId: launchResult.taskId,
+ sessionId: linkedSession?.sessionId,
};
}),
dequeue: runScoped(
diff --git a/packages/slack/package.json b/packages/slack/package.json
index 1789320c0..d4aea38fb 100644
--- a/packages/slack/package.json
+++ b/packages/slack/package.json
@@ -23,6 +23,7 @@
"@roomote/communication": "workspace:^",
"@roomote/db": "workspace:^",
"@roomote/env": "workspace:^",
+ "@roomote/feature-flags": "workspace:^",
"@roomote/redis": "workspace:^",
"@roomote/types": "workspace:^",
"@slack/web-api": "^7.19.0",
diff --git a/packages/slack/src/fast-agent-live-task-launcher.ts b/packages/slack/src/fast-agent-live-task-launcher.ts
index c533965b0..e767da0b0 100644
--- a/packages/slack/src/fast-agent-live-task-launcher.ts
+++ b/packages/slack/src/fast-agent-live-task-launcher.ts
@@ -4,9 +4,17 @@ import {
type LaunchFastAgentTask,
} from '@roomote/cloud-agents/server';
import { RunStatus } from '@roomote/types';
+import { Env } from '@roomote/env';
+import { db, getSessionForTask } from '@roomote/db/server';
+import {
+ FeatureFlag,
+ getFeatureFlagEvaluator,
+} from '@roomote/feature-flags/server';
+import { getRedis } from '@roomote/redis';
import {
buildSlackLiveTaskCardBlocks,
SLACK_LIVE_TASK_CARD_MESSAGES,
+ SLACK_SESSION_LIVE_TASK_CARD_MESSAGES,
} from './live-task-card-blocks';
import {
buildSlackLiveTaskTitle,
@@ -22,6 +30,7 @@ type SlackLiveTaskCardNotifier = Pick<
>;
export const STARTING_TASK_TITLE = 'Starting task…';
+export const PREPARING_WORKSPACE_TITLE = 'Preparing workspace…';
function describeError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
@@ -49,13 +58,17 @@ export function createFastAgentSlackLiveTaskLauncher(
): LaunchFastAgentTask {
const { slack, ...launcherParams } = params;
- const postTaskLink = async (taskUrl: string): Promise => {
+ const postTaskLink = async (
+ taskUrl: string,
+ sessionMode = false,
+ ): Promise => {
+ const label = sessionMode ? 'Open in Roomote' : 'Open the task';
try {
await slack.postMessage({
channel: launcherParams.channelId,
thread_ts: launcherParams.threadTs,
- text: `Open the task: ${taskUrl}`,
- blocks: [{ type: 'markdown', text: `[Open the task](${taskUrl})` }],
+ text: `${label}: ${taskUrl}`,
+ blocks: [{ type: 'markdown', text: `[${label}](${taskUrl})` }],
unfurl_links: false,
unfurl_media: false,
});
@@ -72,8 +85,20 @@ export function createFastAgentSlackLiveTaskLauncher(
): Promise => {
const taskUpdateId = `roomote-task-${taskRun.taskId}`;
let messageTs: string | undefined;
+ let sessionMode = false;
+ let destinationUrl = context.taskUrl;
try {
+ sessionMode = await getFeatureFlagEvaluator(getRedis()).evaluate(
+ FeatureFlag.SessionsComms,
+ { isDeploymentContext: true },
+ );
+ const linkedSession = sessionMode
+ ? await getSessionForTask(db, taskRun.taskId)
+ : null;
+ destinationUrl = linkedSession
+ ? `${Env.R_APP_URL}/sessions/${linkedSession.id}?task=${taskRun.taskId}`
+ : context.taskUrl;
// A card for this task already exists (for example an idempotent
// relaunch of the same task); keep updating it instead of posting
// a second card in the thread.
@@ -86,9 +111,10 @@ export function createFastAgentSlackLiveTaskLauncher(
thread_ts: launcherParams.threadTs,
...buildSlackLiveTaskCardBlocks({
taskUpdateId,
- title: STARTING_TASK_TITLE,
+ title: sessionMode ? PREPARING_WORKSPACE_TITLE : STARTING_TASK_TITLE,
status: 'in_progress',
- taskUrl: context.taskUrl,
+ taskUrl: destinationUrl,
+ sessionMode,
}),
unfurl_links: false,
unfurl_media: false,
@@ -104,7 +130,7 @@ export function createFastAgentSlackLiveTaskLauncher(
console.warn(
`[Fast Agent] Slack rejected the task card for run ${taskRun.id} (${posted.slackErrorCode ?? (posted.transportError ? 'transport error' : 'unknown')}); posting the task link instead.`,
);
- await postTaskLink(context.taskUrl);
+ await postTaskLink(destinationUrl, sessionMode);
return;
}
@@ -118,7 +144,8 @@ export function createFastAgentSlackLiveTaskLauncher(
taskUpdateId,
threadTs: launcherParams.threadTs,
title: buildSlackLiveTaskTitle(context.prompt),
- taskUrl: context.taskUrl,
+ taskUrl: destinationUrl,
+ ...(sessionMode ? { sessionMode: true } : {}),
});
} catch (error) {
console.error(
@@ -140,10 +167,15 @@ export function createFastAgentSlackLiveTaskLauncher(
ts: messageTs,
message: buildSlackLiveTaskCardBlocks({
taskUpdateId,
- title: STARTING_TASK_TITLE,
+ title: sessionMode
+ ? PREPARING_WORKSPACE_TITLE
+ : STARTING_TASK_TITLE,
status: 'error',
- message: SLACK_LIVE_TASK_CARD_MESSAGES.trackingUnavailable,
- taskUrl: context.taskUrl,
+ message: sessionMode
+ ? SLACK_SESSION_LIVE_TASK_CARD_MESSAGES.trackingUnavailable
+ : SLACK_LIVE_TASK_CARD_MESSAGES.trackingUnavailable,
+ taskUrl: destinationUrl,
+ sessionMode,
}),
});
} catch (updateError) {
@@ -152,7 +184,7 @@ export function createFastAgentSlackLiveTaskLauncher(
);
}
if (!settled) {
- await postTaskLink(context.taskUrl);
+ await postTaskLink(destinationUrl, sessionMode);
}
}
};
diff --git a/packages/slack/src/live-task-card-blocks.ts b/packages/slack/src/live-task-card-blocks.ts
index 81fdea7df..df83b16de 100644
--- a/packages/slack/src/live-task-card-blocks.ts
+++ b/packages/slack/src/live-task-card-blocks.ts
@@ -16,6 +16,14 @@ export const SLACK_LIVE_TASK_CARD_MESSAGES = {
'Live updates are unavailable for this task; open it to follow progress.',
} as const;
+export const SLACK_SESSION_LIVE_TASK_CARD_MESSAGES = {
+ completed: 'Ready.',
+ canceled: 'Stopped.',
+ failed: 'Stopped because of an error.',
+ trackingUnavailable:
+ 'Live updates are unavailable; open Roomote to follow progress.',
+} as const;
+
export interface SlackLiveTaskCardContent {
taskUpdateId: string;
title: string;
@@ -24,6 +32,7 @@ export interface SlackLiveTaskCardContent {
* the card output. Always the latest one, never accumulated. */
message?: string;
taskUrl?: string;
+ sessionMode?: boolean;
}
/**
@@ -53,7 +62,9 @@ export function buildSlackLiveTaskCardBlocks(
text: [
content.title,
message,
- content.taskUrl ? `<${content.taskUrl}|Open the task>` : undefined,
+ content.taskUrl
+ ? `<${content.taskUrl}|${content.sessionMode ? 'Open in Roomote' : 'Open the task'}>`
+ : undefined,
]
.filter((line): line is string => Boolean(line))
.join('\n'),
@@ -68,7 +79,11 @@ export function buildSlackLiveTaskCardBlocks(
...(content.taskUrl
? {
sources: [
- { type: 'url', url: content.taskUrl, text: 'View task' },
+ {
+ type: 'url',
+ url: content.taskUrl,
+ text: content.sessionMode ? 'Open in Roomote' : 'View task',
+ },
],
}
: {}),
diff --git a/packages/slack/src/live-task-stream.ts b/packages/slack/src/live-task-stream.ts
index 20aaf9005..f4bfdac2b 100644
--- a/packages/slack/src/live-task-stream.ts
+++ b/packages/slack/src/live-task-stream.ts
@@ -15,6 +15,7 @@ export interface SlackLiveTaskStreamData {
threadTs: string;
title: string;
taskUrl?: string;
+ sessionMode?: boolean;
}
// Keyed by task id: runs are replaced on snapshot resume, but the card in the
diff --git a/packages/slack/src/settle-live-task-card.ts b/packages/slack/src/settle-live-task-card.ts
index ca039a551..a9312e3b4 100644
--- a/packages/slack/src/settle-live-task-card.ts
+++ b/packages/slack/src/settle-live-task-card.ts
@@ -4,6 +4,7 @@ import { and, db, eq, slackInstallations } from '@roomote/db/server';
import {
buildSlackLiveTaskCardBlocks,
SLACK_LIVE_TASK_CARD_MESSAGES,
+ SLACK_SESSION_LIVE_TASK_CARD_MESSAGES,
} from './live-task-card-blocks';
import {
buildSlackLiveTaskTitle,
@@ -74,6 +75,7 @@ export async function renderSlackLiveTaskCard(input: {
status: input.status,
...(input.message ? { message: input.message } : {}),
...(data.taskUrl ? { taskUrl: data.taskUrl } : {}),
+ sessionMode: data.sessionMode === true,
}),
});
@@ -105,8 +107,8 @@ export async function settleSlackLiveTaskCardForRun(input: {
status: 'error',
message:
input.status === RunStatus.Canceled
- ? SLACK_LIVE_TASK_CARD_MESSAGES.canceled
- : SLACK_LIVE_TASK_CARD_MESSAGES.failed,
+ ? await dataSessionMessages(input.taskId, 'canceled')
+ : await dataSessionMessages(input.taskId, 'failed'),
taskTitle: input.taskTitle,
});
} catch (error) {
@@ -115,3 +117,13 @@ export async function settleSlackLiveTaskCardForRun(input: {
);
}
}
+
+async function dataSessionMessages(
+ taskId: string,
+ state: 'canceled' | 'failed',
+): Promise {
+ const data = await getSlackLiveTaskStreamData(taskId);
+ return data?.sessionMode
+ ? SLACK_SESSION_LIVE_TASK_CARD_MESSAGES[state]
+ : SLACK_LIVE_TASK_CARD_MESSAGES[state];
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bd0da3dbc..2309d1aff 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -277,6 +277,9 @@ importers:
'@roomote/env':
specifier: workspace:^
version: link:../../packages/env
+ '@roomote/feature-flags':
+ specifier: workspace:^
+ version: link:../../packages/feature-flags
'@roomote/github':
specifier: workspace:^
version: link:../../packages/github
@@ -1139,6 +1142,9 @@ importers:
'@roomote/env':
specifier: workspace:^
version: link:../env
+ '@roomote/feature-flags':
+ specifier: workspace:^
+ version: link:../feature-flags
'@roomote/gitea':
specifier: workspace:^
version: link:../gitea
@@ -1639,6 +1645,9 @@ importers:
'@roomote/env':
specifier: workspace:^
version: link:../env
+ '@roomote/feature-flags':
+ specifier: workspace:^
+ version: link:../feature-flags
'@roomote/redis':
specifier: workspace:^
version: link:../redis
From 7e5240f842c9da50e4b82170d1d9e4f82651f4ec Mon Sep 17 00:00:00 2001
From: Roomote
Date: Wed, 26 Aug 2026 19:24:40 +0000
Subject: [PATCH 04/26] chore: regenerate unified sessions migration
---
.../drizzle/0061_careful_lily_hollister.sql | 89 +
packages/db/drizzle/meta/0061_snapshot.json | 13717 ++++++++++++++++
packages/db/drizzle/meta/_journal.json | 7 +
3 files changed, 13813 insertions(+)
create mode 100644 packages/db/drizzle/0061_careful_lily_hollister.sql
create mode 100644 packages/db/drizzle/meta/0061_snapshot.json
diff --git a/packages/db/drizzle/0061_careful_lily_hollister.sql b/packages/db/drizzle/0061_careful_lily_hollister.sql
new file mode 100644
index 000000000..446cd427a
--- /dev/null
+++ b/packages/db/drizzle/0061_careful_lily_hollister.sql
@@ -0,0 +1,89 @@
+CREATE TABLE "session_backfill_state" (
+ "key" text PRIMARY KEY NOT NULL,
+ "phase" text DEFAULT 'fast_conversations' NOT NULL,
+ "cursor_created_at" timestamp,
+ "cursor_id" text,
+ "completed_at" timestamp,
+ "last_run_at" timestamp,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL,
+ CONSTRAINT "session_backfill_state_phase_check" CHECK ("session_backfill_state"."phase" in ('fast_conversations', 'fast_tasks', 'tasks', 'participants')),
+ CONSTRAINT "session_backfill_state_cursor_shape_check" CHECK (("session_backfill_state"."cursor_created_at" IS NULL) = ("session_backfill_state"."cursor_id" IS NULL))
+);
+--> statement-breakpoint
+CREATE TABLE "session_participants" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "session_id" uuid NOT NULL,
+ "user_id" text NOT NULL,
+ "role" text DEFAULT 'member' NOT NULL,
+ "last_read_event_at" bigint,
+ "last_read_event_id" text,
+ "last_notified_event_at" bigint,
+ "last_notified_event_id" text,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL,
+ CONSTRAINT "session_participants_role_check" CHECK ("session_participants"."role" in ('owner', 'member'))
+);
+--> statement-breakpoint
+CREATE TABLE "session_pins" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "session_id" uuid NOT NULL,
+ "user_id" text NOT NULL,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "session_tasks" (
+ "session_id" uuid NOT NULL,
+ "task_id" text NOT NULL,
+ "attached_at" timestamp DEFAULT now() NOT NULL,
+ "origin" text NOT NULL,
+ CONSTRAINT "session_tasks_session_id_task_id_pk" PRIMARY KEY("session_id","task_id"),
+ CONSTRAINT "session_tasks_origin_check" CHECK ("session_tasks"."origin" in ('direct_launch', 'fast_delegation', 'backfill', 'follow_up'))
+);
+--> statement-breakpoint
+CREATE TABLE "sessions" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "title" text NOT NULL,
+ "owner_kind" text NOT NULL,
+ "owner_user_id" text,
+ "owner_automation" text,
+ "source_surface" text NOT NULL,
+ "source_trigger" text NOT NULL,
+ "fast_conversation_id" uuid,
+ "visibility" text DEFAULT 'visible' NOT NULL,
+ "activity_at" bigint NOT NULL,
+ "cached_status" text,
+ "archived_at" timestamp,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL,
+ CONSTRAINT "sessions_owner_shape_check" CHECK (("sessions"."owner_kind" = 'user' AND "sessions"."owner_automation" IS NULL) OR ("sessions"."owner_kind" = 'automation' AND "sessions"."owner_user_id" IS NULL) OR ("sessions"."owner_kind" = 'system' AND "sessions"."owner_user_id" IS NULL AND "sessions"."owner_automation" IS NULL)),
+ CONSTRAINT "sessions_owner_kind_check" CHECK ("sessions"."owner_kind" in ('user', 'automation', 'system')),
+ CONSTRAINT "sessions_source_surface_check" CHECK ("sessions"."source_surface" in ('web', 'api', 'slack', 'teams', 'telegram', 'discord', 'linear', 'github', 'gitlab', 'gitea', 'ado', 'bitbucket', 'system', 'automation')),
+ CONSTRAINT "sessions_source_trigger_check" CHECK ("sessions"."source_trigger" in ('message', 'webhook', 'schedule', 'manual')),
+ CONSTRAINT "sessions_visibility_check" CHECK ("sessions"."visibility" in ('visible', 'hidden')),
+ CONSTRAINT "sessions_cached_status_check" CHECK ("sessions"."cached_status" IS NULL OR "sessions"."cached_status" in ('active', 'needs_input', 'blocked', 'ready'))
+);
+--> statement-breakpoint
+ALTER TABLE "task_inference_usage_events" ADD COLUMN "session_id" uuid;--> statement-breakpoint
+ALTER TABLE "session_participants" ADD CONSTRAINT "session_participants_session_id_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "session_participants" ADD CONSTRAINT "session_participants_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "session_pins" ADD CONSTRAINT "session_pins_session_id_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "session_pins" ADD CONSTRAINT "session_pins_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "session_tasks" ADD CONSTRAINT "session_tasks_session_id_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "session_tasks" ADD CONSTRAINT "session_tasks_task_id_tasks_id_fk" FOREIGN KEY ("task_id") REFERENCES "public"."tasks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "sessions" ADD CONSTRAINT "sessions_owner_user_id_users_id_fk" FOREIGN KEY ("owner_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "sessions" ADD CONSTRAINT "sessions_owner_automation_automations_key_fk" FOREIGN KEY ("owner_automation") REFERENCES "public"."automations"("key") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "sessions" ADD CONSTRAINT "sessions_fast_conversation_id_fast_agent_conversations_id_fk" FOREIGN KEY ("fast_conversation_id") REFERENCES "public"."fast_agent_conversations"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+CREATE UNIQUE INDEX "session_participants_session_user_unique" ON "session_participants" USING btree ("session_id","user_id");--> statement-breakpoint
+CREATE INDEX "session_participants_user_id_idx" ON "session_participants" USING btree ("user_id");--> statement-breakpoint
+CREATE UNIQUE INDEX "session_pins_user_session_unique" ON "session_pins" USING btree ("user_id","session_id");--> statement-breakpoint
+CREATE INDEX "session_pins_user_updated_at_idx" ON "session_pins" USING btree ("user_id","updated_at");--> statement-breakpoint
+CREATE INDEX "session_pins_session_id_idx" ON "session_pins" USING btree ("session_id");--> statement-breakpoint
+CREATE UNIQUE INDEX "session_tasks_task_id_unique" ON "session_tasks" USING btree ("task_id");--> statement-breakpoint
+CREATE INDEX "session_tasks_session_attached_at_idx" ON "session_tasks" USING btree ("session_id","attached_at" DESC NULLS LAST);--> statement-breakpoint
+CREATE INDEX "sessions_visibility_activity_at_idx" ON "sessions" USING btree ("visibility","activity_at" DESC NULLS LAST,"id" DESC NULLS LAST);--> statement-breakpoint
+CREATE INDEX "sessions_owner_user_id_idx" ON "sessions" USING btree ("owner_user_id");--> statement-breakpoint
+CREATE UNIQUE INDEX "sessions_fast_conversation_id_unique" ON "sessions" USING btree ("fast_conversation_id") WHERE "sessions"."fast_conversation_id" IS NOT NULL;--> statement-breakpoint
+ALTER TABLE "task_inference_usage_events" ADD CONSTRAINT "task_inference_usage_events_session_id_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."sessions"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "task_inference_usage_events_session_id_idx" ON "task_inference_usage_events" USING btree ("session_id");
\ No newline at end of file
diff --git a/packages/db/drizzle/meta/0061_snapshot.json b/packages/db/drizzle/meta/0061_snapshot.json
new file mode 100644
index 000000000..cc86040ba
--- /dev/null
+++ b/packages/db/drizzle/meta/0061_snapshot.json
@@ -0,0 +1,13717 @@
+{
+ "id": "7fb9bf55-5bfb-4854-a7c0-348575cb6c30",
+ "prevId": "df5ebdeb-4055-46c6-87fc-a10c618013f3",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.auth_accounts": {
+ "name": "auth_accounts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_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
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "auth_accounts_user_id_idx": {
+ "name": "auth_accounts_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "auth_accounts_provider_account_unique": {
+ "name": "auth_accounts_provider_account_unique",
+ "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": {
+ "auth_accounts_user_id_auth_users_id_fk": {
+ "name": "auth_accounts_user_id_auth_users_id_fk",
+ "tableFrom": "auth_accounts",
+ "tableTo": "auth_users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.auth_sessions": {
+ "name": "auth_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",
+ "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",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "auth_sessions_token_unique": {
+ "name": "auth_sessions_token_unique",
+ "columns": [
+ {
+ "expression": "token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "auth_sessions_user_id_idx": {
+ "name": "auth_sessions_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "auth_sessions_user_id_auth_users_id_fk": {
+ "name": "auth_sessions_user_id_auth_users_id_fk",
+ "tableFrom": "auth_sessions",
+ "tableTo": "auth_users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.auth_users": {
+ "name": "auth_users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "auth_users_email_unique": {
+ "name": "auth_users_email_unique",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "auth_users_created_at_idx": {
+ "name": "auth_users_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.auth_verifications": {
+ "name": "auth_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",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "auth_verifications_identifier_idx": {
+ "name": "auth_verifications_identifier_idx",
+ "columns": [
+ {
+ "expression": "identifier",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.automations": {
+ "name": "automations",
+ "schema": "",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "internal": {
+ "name": "internal",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "schedule": {
+ "name": "schedule",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "instructions": {
+ "name": "instructions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "targets": {
+ "name": "targets",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "last_run_at": {
+ "name": "last_run_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_succeeded_at": {
+ "name": "last_succeeded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_failed_at": {
+ "name": "last_failed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scan_cursor": {
+ "name": "scan_cursor",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.brain_collector_items": {
+ "name": "brain_collector_items",
+ "schema": "",
+ "columns": {
+ "collector_id": {
+ "name": "collector_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_id": {
+ "name": "item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "brain_collector_items_collector_seen_idx": {
+ "name": "brain_collector_items_collector_seen_idx",
+ "columns": [
+ {
+ "expression": "collector_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_seen_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "brain_collector_items_collector_item_pk": {
+ "name": "brain_collector_items_collector_item_pk",
+ "columns": ["collector_id", "item_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.brain_memory_events": {
+ "name": "brain_memory_events",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "agent_summary": {
+ "name": "agent_summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revision": {
+ "name": "revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processed_at": {
+ "name": "processed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "brain_memory_events_status_created_idx": {
+ "name": "brain_memory_events_status_created_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": {
+ "brain_memory_events_run_id_task_runs_id_fk": {
+ "name": "brain_memory_events_run_id_task_runs_id_fk",
+ "tableFrom": "brain_memory_events",
+ "tableTo": "task_runs",
+ "columnsFrom": ["run_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "brain_memory_events_run_unique": {
+ "name": "brain_memory_events_run_unique",
+ "nullsNotDistinct": false,
+ "columns": ["run_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.brain_sync_state": {
+ "name": "brain_sync_state",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "collector_id": {
+ "name": "collector_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "watermark": {
+ "name": "watermark",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "backfill_cursor": {
+ "name": "backfill_cursor",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "backfill_completed_at": {
+ "name": "backfill_completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "brain_sync_state_collector_id_unique": {
+ "name": "brain_sync_state_collector_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["collector_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.compute_provider_usage": {
+ "name": "compute_provider_usage",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_usage_id": {
+ "name": "provider_usage_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "auth_kind": {
+ "name": "auth_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "instance_id": {
+ "name": "instance_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "launch_mode": {
+ "name": "launch_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lifecycle_action": {
+ "name": "lifecycle_action",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "measurement_source": {
+ "name": "measurement_source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "configured_vcpus": {
+ "name": "configured_vcpus",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "configured_cpu_cores": {
+ "name": "configured_cpu_cores",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "configured_memory_mib": {
+ "name": "configured_memory_mib",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "wall_clock_duration_ms": {
+ "name": "wall_clock_duration_ms",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "active_cpu_duration_ms": {
+ "name": "active_cpu_duration_ms",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "observed_memory_mib_milliseconds": {
+ "name": "observed_memory_mib_milliseconds",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "network_ingress_bytes": {
+ "name": "network_ingress_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "network_egress_bytes": {
+ "name": "network_egress_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "details": {
+ "name": "details",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "compute_provider_usage_provider_usage_id_unique": {
+ "name": "compute_provider_usage_provider_usage_id_unique",
+ "columns": [
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_usage_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "compute_provider_usage_run_id_idx": {
+ "name": "compute_provider_usage_run_id_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "compute_provider_usage_task_id_idx": {
+ "name": "compute_provider_usage_task_id_idx",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "compute_provider_usage_created_at_idx": {
+ "name": "compute_provider_usage_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "compute_provider_usage_run_id_task_runs_id_fk": {
+ "name": "compute_provider_usage_run_id_task_runs_id_fk",
+ "tableFrom": "compute_provider_usage",
+ "tableTo": "task_runs",
+ "columnsFrom": ["run_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "compute_provider_usage_task_id_tasks_id_fk": {
+ "name": "compute_provider_usage_task_id_tasks_id_fk",
+ "tableFrom": "compute_provider_usage",
+ "tableTo": "tasks",
+ "columnsFrom": ["task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.compute_provider_usage_samples": {
+ "name": "compute_provider_usage_samples",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_usage_id": {
+ "name": "provider_usage_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "instance_id": {
+ "name": "instance_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sampled_at": {
+ "name": "sampled_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cpu_usage_ns_total": {
+ "name": "cpu_usage_ns_total",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "memory_usage_bytes": {
+ "name": "memory_usage_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "memory_peak_usage_bytes": {
+ "name": "memory_peak_usage_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "details": {
+ "name": "details",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "compute_provider_usage_samples_provider_usage_sampled_at_unique": {
+ "name": "compute_provider_usage_samples_provider_usage_sampled_at_unique",
+ "columns": [
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_usage_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sampled_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "compute_provider_usage_samples_run_id_idx": {
+ "name": "compute_provider_usage_samples_run_id_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "compute_provider_usage_samples_task_id_idx": {
+ "name": "compute_provider_usage_samples_task_id_idx",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "compute_provider_usage_samples_created_at_idx": {
+ "name": "compute_provider_usage_samples_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "compute_provider_usage_samples_run_id_task_runs_id_fk": {
+ "name": "compute_provider_usage_samples_run_id_task_runs_id_fk",
+ "tableFrom": "compute_provider_usage_samples",
+ "tableTo": "task_runs",
+ "columnsFrom": ["run_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "compute_provider_usage_samples_task_id_tasks_id_fk": {
+ "name": "compute_provider_usage_samples_task_id_tasks_id_fk",
+ "tableFrom": "compute_provider_usage_samples",
+ "tableTo": "tasks",
+ "columnsFrom": ["task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.custom_automations": {
+ "name": "custom_automations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "prompt": {
+ "name": "prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "schedule_mode": {
+ "name": "schedule_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'off'"
+ },
+ "cron_expression": {
+ "name": "cron_expression",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "environment_id": {
+ "name": "environment_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "all_repositories": {
+ "name": "all_repositories",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "execution_mode": {
+ "name": "execution_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'sandbox_task'"
+ },
+ "target": {
+ "name": "target",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_run_at": {
+ "name": "last_run_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_succeeded_at": {
+ "name": "last_succeeded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_failed_at": {
+ "name": "last_failed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_launched_task_id": {
+ "name": "last_launched_task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "launch_claimed_at": {
+ "name": "launch_claimed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "custom_automations_name_unique_idx": {
+ "name": "custom_automations_name_unique_idx",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "custom_automations_enabled_idx": {
+ "name": "custom_automations_enabled_idx",
+ "columns": [
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "custom_automations_environment_id_idx": {
+ "name": "custom_automations_environment_id_idx",
+ "columns": [
+ {
+ "expression": "environment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "custom_automations_environment_id_environments_id_fk": {
+ "name": "custom_automations_environment_id_environments_id_fk",
+ "tableFrom": "custom_automations",
+ "tableTo": "environments",
+ "columnsFrom": ["environment_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "custom_automations_created_by_user_id_users_id_fk": {
+ "name": "custom_automations_created_by_user_id_users_id_fk",
+ "tableFrom": "custom_automations",
+ "tableTo": "users",
+ "columnsFrom": ["created_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "custom_automations_last_launched_task_id_tasks_id_fk": {
+ "name": "custom_automations_last_launched_task_id_tasks_id_fk",
+ "tableFrom": "custom_automations",
+ "tableTo": "tasks",
+ "columnsFrom": ["last_launched_task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.custom_mcp_servers": {
+ "name": "custom_mcp_servers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auth_type": {
+ "name": "auth_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'none'"
+ },
+ "headers": {
+ "name": "headers",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stdio": {
+ "name": "stdio",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "disabled_tools": {
+ "name": "disabled_tools",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "manual_client_id": {
+ "name": "manual_client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "manual_client_secret": {
+ "name": "manual_client_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauth_server_metadata": {
+ "name": "oauth_server_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauth_server_metadata_fetched_at": {
+ "name": "oauth_server_metadata_fetched_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauth_resource_indicator_disabled": {
+ "name": "oauth_resource_indicator_disabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "custom_mcp_servers_created_by_user_id_users_id_fk": {
+ "name": "custom_mcp_servers_created_by_user_id_users_id_fk",
+ "tableFrom": "custom_mcp_servers",
+ "tableTo": "users",
+ "columnsFrom": ["created_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "custom_mcp_servers_name_unique": {
+ "name": "custom_mcp_servers_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.deployment_mcp_enablements": {
+ "name": "deployment_mcp_enablements",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "mcp_id": {
+ "name": "mcp_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "enabled_by_user_id": {
+ "name": "enabled_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "disabled_tools": {
+ "name": "disabled_tools",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tool_access_mode": {
+ "name": "tool_access_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "deployment_mcp_enablements_enabled_by_user_id_users_id_fk": {
+ "name": "deployment_mcp_enablements_enabled_by_user_id_users_id_fk",
+ "tableFrom": "deployment_mcp_enablements",
+ "tableTo": "users",
+ "columnsFrom": ["enabled_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "deployment_mcp_enablements_mcp_unique": {
+ "name": "deployment_mcp_enablements_mcp_unique",
+ "nullsNotDistinct": false,
+ "columns": ["mcp_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.deployment_secrets": {
+ "name": "deployment_secrets",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "deployment_secrets_name_unique": {
+ "name": "deployment_secrets_name_unique",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.deployment_settings": {
+ "name": "deployment_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "'default'"
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "task_model_settings": {
+ "name": "task_model_settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workspace_routing_settings": {
+ "name": "workspace_routing_settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "router_debug_provider": {
+ "name": "router_debug_provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "router_debug_channel_id": {
+ "name": "router_debug_channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "router_debug_disabled": {
+ "name": "router_debug_disabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "router_debug_slack_channel_id": {
+ "name": "router_debug_slack_channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "runtime_model_config": {
+ "name": "runtime_model_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "runtime_compute_config": {
+ "name": "runtime_compute_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_policy": {
+ "name": "access_policy",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "license_key": {
+ "name": "license_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "license_cloud_state": {
+ "name": "license_cloud_state",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "instance_analytics_id": {
+ "name": "instance_analytics_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_known_version": {
+ "name": "latest_known_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latest_version_checked_at": {
+ "name": "latest_version_checked_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "setup_completed_at": {
+ "name": "setup_completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "setup_new_state": {
+ "name": "setup_new_state",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "slack_onboarding_stage": {
+ "name": "slack_onboarding_stage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "manager_slack_channel_id": {
+ "name": "manager_slack_channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "manager_discord_channel_id": {
+ "name": "manager_discord_channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "global_agent_instructions": {
+ "name": "global_agent_instructions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "time_zone": {
+ "name": "time_zone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "time_zone_updated_at": {
+ "name": "time_zone_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "authorship_instructions": {
+ "name": "authorship_instructions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "compiled_authorship_rules": {
+ "name": "compiled_authorship_rules",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "compiled_authorship_issues": {
+ "name": "compiled_authorship_issues",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "compiled_authorship_at": {
+ "name": "compiled_authorship_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "style_guidance": {
+ "name": "style_guidance",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "slack_summon_emoji": {
+ "name": "slack_summon_emoji",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "slack_ack_emoji": {
+ "name": "slack_ack_emoji",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'eyes'"
+ },
+ "slack_completion_emoji": {
+ "name": "slack_completion_emoji",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'white_check_mark'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.discord_gateway_sessions": {
+ "name": "discord_gateway_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "resume_gateway_url": {
+ "name": "resume_gateway_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sequence": {
+ "name": "sequence",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "shard_count": {
+ "name": "shard_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_connected_at": {
+ "name": "last_connected_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_heartbeat_ack_at": {
+ "name": "last_heartbeat_ack_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "disconnected_at": {
+ "name": "disconnected_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.discord_installation_channels": {
+ "name": "discord_installation_channels",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "discord_installation_id": {
+ "name": "discord_installation_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "channel_id": {
+ "name": "channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "channel_name": {
+ "name": "channel_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "channel_type": {
+ "name": "channel_type",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "position": {
+ "name": "position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "permissions": {
+ "name": "permissions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_available": {
+ "name": "is_available",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "discord_installation_channels_installation_id_idx": {
+ "name": "discord_installation_channels_installation_id_idx",
+ "columns": [
+ {
+ "expression": "discord_installation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "discord_installation_channels_unique": {
+ "name": "discord_installation_channels_unique",
+ "columns": [
+ {
+ "expression": "discord_installation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "channel_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "discord_installation_channels_discord_installation_id_discord_installations_id_fk": {
+ "name": "discord_installation_channels_discord_installation_id_discord_installations_id_fk",
+ "tableFrom": "discord_installation_channels",
+ "tableTo": "discord_installations",
+ "columnsFrom": ["discord_installation_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.discord_installations": {
+ "name": "discord_installations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "guild_id": {
+ "name": "guild_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "guild_name": {
+ "name": "guild_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "application_id": {
+ "name": "application_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "bot_user_id": {
+ "name": "bot_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "installed_by_user_id": {
+ "name": "installed_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "default_channel_id": {
+ "name": "default_channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "default_channel_name": {
+ "name": "default_channel_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "default_channel_type": {
+ "name": "default_channel_type",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "discord_installations_guild_id_unique": {
+ "name": "discord_installations_guild_id_unique",
+ "columns": [
+ {
+ "expression": "guild_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "discord_installations_active_idx": {
+ "name": "discord_installations_active_idx",
+ "columns": [
+ {
+ "expression": "is_active",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "discord_installations_default_channel_idx": {
+ "name": "discord_installations_default_channel_idx",
+ "columns": [
+ {
+ "expression": "default_channel_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "discord_installations_installed_by_user_id_users_id_fk": {
+ "name": "discord_installations_installed_by_user_id_users_id_fk",
+ "tableFrom": "discord_installations",
+ "tableTo": "users",
+ "columnsFrom": ["installed_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.discord_user_mappings": {
+ "name": "discord_user_mappings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "discord_user_id": {
+ "name": "discord_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "discord_username": {
+ "name": "discord_username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discord_global_name": {
+ "name": "discord_global_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discord_dm_channel_id": {
+ "name": "discord_dm_channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "discord_user_mappings_user_id_idx": {
+ "name": "discord_user_mappings_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "discord_user_mappings_discord_user_id_unique": {
+ "name": "discord_user_mappings_discord_user_id_unique",
+ "columns": [
+ {
+ "expression": "discord_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "discord_user_mappings_user_id_users_id_fk": {
+ "name": "discord_user_mappings_user_id_users_id_fk",
+ "tableFrom": "discord_user_mappings",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.environment_config_versions": {
+ "name": "environment_config_versions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "environment_id": {
+ "name": "environment_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "environment_config_versions_environment_id_idx": {
+ "name": "environment_config_versions_environment_id_idx",
+ "columns": [
+ {
+ "expression": "environment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "environment_config_versions_environment_version_unique": {
+ "name": "environment_config_versions_environment_version_unique",
+ "columns": [
+ {
+ "expression": "environment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "version",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "environment_config_versions_environment_id_environments_id_fk": {
+ "name": "environment_config_versions_environment_id_environments_id_fk",
+ "tableFrom": "environment_config_versions",
+ "tableTo": "environments",
+ "columnsFrom": ["environment_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "environment_config_versions_created_by_user_id_users_id_fk": {
+ "name": "environment_config_versions_created_by_user_id_users_id_fk",
+ "tableFrom": "environment_config_versions",
+ "tableTo": "users",
+ "columnsFrom": ["created_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.environment_repository_mappings": {
+ "name": "environment_repository_mappings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "environment_id": {
+ "name": "environment_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "repository_id": {
+ "name": "repository_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "env_repo_mappings_env_id_idx": {
+ "name": "env_repo_mappings_env_id_idx",
+ "columns": [
+ {
+ "expression": "environment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "env_repo_mappings_repo_id_idx": {
+ "name": "env_repo_mappings_repo_id_idx",
+ "columns": [
+ {
+ "expression": "repository_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "environment_repository_mappings_environment_id_environments_id_fk": {
+ "name": "environment_repository_mappings_environment_id_environments_id_fk",
+ "tableFrom": "environment_repository_mappings",
+ "tableTo": "environments",
+ "columnsFrom": ["environment_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "environment_repository_mappings_repository_id_repositories_id_fk": {
+ "name": "environment_repository_mappings_repository_id_repositories_id_fk",
+ "tableFrom": "environment_repository_mappings",
+ "tableTo": "repositories",
+ "columnsFrom": ["repository_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "env_repo_mappings_unique": {
+ "name": "env_repo_mappings_unique",
+ "nullsNotDistinct": false,
+ "columns": ["environment_id", "repository_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.environment_snapshots": {
+ "name": "environment_snapshots",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "environment_id": {
+ "name": "environment_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "snapshot_id": {
+ "name": "snapshot_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "snapshot_created_at": {
+ "name": "snapshot_created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "snapshot_expires_at": {
+ "name": "snapshot_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "snapshot_status": {
+ "name": "snapshot_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "environment_snapshots_environment_id_idx": {
+ "name": "environment_snapshots_environment_id_idx",
+ "columns": [
+ {
+ "expression": "environment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "environment_snapshots_env_provider_unique": {
+ "name": "environment_snapshots_env_provider_unique",
+ "columns": [
+ {
+ "expression": "environment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"environment_snapshots\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "environment_snapshots_environment_id_environments_id_fk": {
+ "name": "environment_snapshots_environment_id_environments_id_fk",
+ "tableFrom": "environment_snapshots",
+ "tableTo": "environments",
+ "columnsFrom": ["environment_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.environment_variables": {
+ "name": "environment_variables",
+ "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
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_updated_by_user_id": {
+ "name": "last_updated_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "environment_variables_user_id_idx": {
+ "name": "environment_variables_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "environment_variables_name_unique": {
+ "name": "environment_variables_name_unique",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "environment_variables_user_id_users_id_fk": {
+ "name": "environment_variables_user_id_users_id_fk",
+ "tableFrom": "environment_variables",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "environment_variables_created_by_user_id_users_id_fk": {
+ "name": "environment_variables_created_by_user_id_users_id_fk",
+ "tableFrom": "environment_variables",
+ "tableTo": "users",
+ "columnsFrom": ["created_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "environment_variables_last_updated_by_user_id_users_id_fk": {
+ "name": "environment_variables_last_updated_by_user_id_users_id_fk",
+ "tableFrom": "environment_variables",
+ "tableTo": "users",
+ "columnsFrom": ["last_updated_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.environments": {
+ "name": "environments",
+ "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
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_eval": {
+ "name": "is_eval",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "declarative_source": {
+ "name": "declarative_source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_verified": {
+ "name": "is_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "verification_task_id": {
+ "name": "verification_task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "verified_at": {
+ "name": "verified_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "verification_error": {
+ "name": "verification_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "snapshot_id": {
+ "name": "snapshot_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "snapshot_created_at": {
+ "name": "snapshot_created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "snapshot_expires_at": {
+ "name": "snapshot_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "snapshot_status": {
+ "name": "snapshot_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "environments_user_id_idx": {
+ "name": "environments_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "environments_created_by_user_id_idx": {
+ "name": "environments_created_by_user_id_idx",
+ "columns": [
+ {
+ "expression": "created_by_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "environments_snapshot_expires_at_idx": {
+ "name": "environments_snapshot_expires_at_idx",
+ "columns": [
+ {
+ "expression": "snapshot_expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "environments_name_unique": {
+ "name": "environments_name_unique",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "environments_user_id_users_id_fk": {
+ "name": "environments_user_id_users_id_fk",
+ "tableFrom": "environments",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "environments_created_by_user_id_users_id_fk": {
+ "name": "environments_created_by_user_id_users_id_fk",
+ "tableFrom": "environments",
+ "tableTo": "users",
+ "columnsFrom": ["created_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.fast_agent_conversations": {
+ "name": "fast_agent_conversations",
+ "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": true
+ },
+ "surface": {
+ "name": "surface",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "conversation_id": {
+ "name": "conversation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "current_reply_channel_id": {
+ "name": "current_reply_channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_reply_thread_id": {
+ "name": "current_reply_thread_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reply_target_verified": {
+ "name": "reply_target_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "compatibility_messages": {
+ "name": "compatibility_messages",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "opencode_session_id": {
+ "name": "opencode_session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reasoning_effort": {
+ "name": "reasoning_effort",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title_edited_by_user_at": {
+ "name": "title_edited_by_user_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "llm_title_checkpoint": {
+ "name": "llm_title_checkpoint",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "legacy_conversation_ids": {
+ "name": "legacy_conversation_ids",
+ "type": "uuid[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::uuid[]"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "fast_agent_conversations_identity_unique": {
+ "name": "fast_agent_conversations_identity_unique",
+ "columns": [
+ {
+ "expression": "surface",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "conversation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "fast_agent_conversations_user_idx": {
+ "name": "fast_agent_conversations_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "fast_agent_conversations_legacy_ids_idx": {
+ "name": "fast_agent_conversations_legacy_ids_idx",
+ "columns": [
+ {
+ "expression": "legacy_conversation_ids",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fast_agent_conversations_user_id_users_id_fk": {
+ "name": "fast_agent_conversations_user_id_users_id_fk",
+ "tableFrom": "fast_agent_conversations",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.fast_agent_memory_events": {
+ "name": "fast_agent_memory_events",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "conversation_id": {
+ "name": "conversation_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "memory": {
+ "name": "memory",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revision": {
+ "name": "revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processed_at": {
+ "name": "processed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "fast_agent_memory_events_status_created_idx": {
+ "name": "fast_agent_memory_events_status_created_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": {
+ "fast_agent_memory_events_conversation_id_fast_agent_conversations_id_fk": {
+ "name": "fast_agent_memory_events_conversation_id_fast_agent_conversations_id_fk",
+ "tableFrom": "fast_agent_memory_events",
+ "tableTo": "fast_agent_conversations",
+ "columnsFrom": ["conversation_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "fast_agent_memory_events_conversation_unique": {
+ "name": "fast_agent_memory_events_conversation_unique",
+ "nullsNotDistinct": false,
+ "columns": ["conversation_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.fast_agent_messages": {
+ "name": "fast_agent_messages",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "conversation_id": {
+ "name": "conversation_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "turn_id": {
+ "name": "turn_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "turn_seq": {
+ "name": "turn_seq",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ts": {
+ "name": "ts",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_type": {
+ "name": "event_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content_blocks": {
+ "name": "content_blocks",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "native_session_id": {
+ "name": "native_session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "native_message_id": {
+ "name": "native_message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "fast_agent_messages_conversation_event_unique": {
+ "name": "fast_agent_messages_conversation_event_unique",
+ "columns": [
+ {
+ "expression": "conversation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "fast_agent_messages_conversation_order_idx": {
+ "name": "fast_agent_messages_conversation_order_idx",
+ "columns": [
+ {
+ "expression": "conversation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "ts",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "turn_seq",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fast_agent_messages_conversation_id_fast_agent_conversations_id_fk": {
+ "name": "fast_agent_messages_conversation_id_fast_agent_conversations_id_fk",
+ "tableFrom": "fast_agent_messages",
+ "tableTo": "fast_agent_conversations",
+ "columnsFrom": ["conversation_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.fast_agent_pr_feedback_deliveries": {
+ "name": "fast_agent_pr_feedback_deliveries",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "conversation_id": {
+ "name": "conversation_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "feedback_id": {
+ "name": "feedback_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lease_token": {
+ "name": "lease_token",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lease_expires_at": {
+ "name": "lease_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivered_at": {
+ "name": "delivered_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "fast_agent_pr_feedback_deliveries_identity_unique": {
+ "name": "fast_agent_pr_feedback_deliveries_identity_unique",
+ "columns": [
+ {
+ "expression": "conversation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "feedback_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "fast_agent_pr_feedback_deliveries_task_idx": {
+ "name": "fast_agent_pr_feedback_deliveries_task_idx",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "fast_agent_pr_feedback_deliveries_conversation_id_fast_agent_conversations_id_fk": {
+ "name": "fast_agent_pr_feedback_deliveries_conversation_id_fast_agent_conversations_id_fk",
+ "tableFrom": "fast_agent_pr_feedback_deliveries",
+ "tableTo": "fast_agent_conversations",
+ "columnsFrom": ["conversation_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "fast_agent_pr_feedback_deliveries_task_id_tasks_id_fk": {
+ "name": "fast_agent_pr_feedback_deliveries_task_id_tasks_id_fk",
+ "tableFrom": "fast_agent_pr_feedback_deliveries",
+ "tableTo": "tasks",
+ "columnsFrom": ["task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.github_installations": {
+ "name": "github_installations",
+ "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
+ },
+ "installation_id": {
+ "name": "installation_id",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "app_id": {
+ "name": "app_id",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "account_login": {
+ "name": "account_login",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "account_type": {
+ "name": "account_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permissions": {
+ "name": "permissions",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "installed_by_user_id": {
+ "name": "installed_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "members_count": {
+ "name": "members_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "suspended_at": {
+ "name": "suspended_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "github_installations_account_login_idx": {
+ "name": "github_installations_account_login_idx",
+ "columns": [
+ {
+ "expression": "account_login",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "github_installations_deployment_installation_unique": {
+ "name": "github_installations_deployment_installation_unique",
+ "columns": [
+ {
+ "expression": "installation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "github_installations_user_id_users_id_fk": {
+ "name": "github_installations_user_id_users_id_fk",
+ "tableFrom": "github_installations",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "github_installations_installed_by_user_id_users_id_fk": {
+ "name": "github_installations_installed_by_user_id_users_id_fk",
+ "tableFrom": "github_installations",
+ "tableTo": "users",
+ "columnsFrom": ["installed_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.github_pending_installations": {
+ "name": "github_pending_installations",
+ "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
+ },
+ "requested_by_user_id": {
+ "name": "requested_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "app_id": {
+ "name": "app_id",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "github_pending_installations_requested_by_user_id_idx": {
+ "name": "github_pending_installations_requested_by_user_id_idx",
+ "columns": [
+ {
+ "expression": "requested_by_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "github_pending_installations_user_id_users_id_fk": {
+ "name": "github_pending_installations_user_id_users_id_fk",
+ "tableFrom": "github_pending_installations",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "github_pending_installations_requested_by_user_id_users_id_fk": {
+ "name": "github_pending_installations_requested_by_user_id_users_id_fk",
+ "tableFrom": "github_pending_installations",
+ "tableTo": "users",
+ "columnsFrom": ["requested_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.github_user_mappings": {
+ "name": "github_user_mappings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "github_login": {
+ "name": "github_login",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "github_user_id": {
+ "name": "github_user_id",
+ "type": "bigint",
+ "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
+ },
+ "token_expires_at": {
+ "name": "token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "github_user_mappings_github_login_idx": {
+ "name": "github_user_mappings_github_login_idx",
+ "columns": [
+ {
+ "expression": "github_login",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "github_user_mappings_user_id_idx": {
+ "name": "github_user_mappings_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "github_user_mappings_user_id_users_id_fk": {
+ "name": "github_user_mappings_user_id_users_id_fk",
+ "tableFrom": "github_user_mappings",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "github_user_mappings_unique": {
+ "name": "github_user_mappings_unique",
+ "nullsNotDistinct": false,
+ "columns": ["github_user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.invites": {
+ "name": "invites",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "label": {
+ "name": "label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "invited_by_user_id": {
+ "name": "invited_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'member'"
+ },
+ "max_uses": {
+ "name": "max_uses",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "used_count": {
+ "name": "used_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "invites_token_hash_unique": {
+ "name": "invites_token_hash_unique",
+ "columns": [
+ {
+ "expression": "token_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "invites_created_at_idx": {
+ "name": "invites_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "invites_invited_by_user_id_users_id_fk": {
+ "name": "invites_invited_by_user_id_users_id_fk",
+ "tableFrom": "invites",
+ "tableTo": "users",
+ "columnsFrom": ["invited_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.license_usage_observations": {
+ "name": "license_usage_observations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "observed_at": {
+ "name": "observed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "active_users": {
+ "name": "active_users",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "delivered_at": {
+ "name": "delivered_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_attempted_at": {
+ "name": "last_attempted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "license_usage_observations_pending_idx": {
+ "name": "license_usage_observations_pending_idx",
+ "columns": [
+ {
+ "expression": "delivered_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "observed_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.linear_pending_selections": {
+ "name": "linear_pending_selections",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "linear_organization_id": {
+ "name": "linear_organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "step": {
+ "name": "step",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'awaiting_workspace'"
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "selected_repo": {
+ "name": "selected_repo",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workspace_options": {
+ "name": "workspace_options",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "linear_pending_selections_expires_at_idx": {
+ "name": "linear_pending_selections_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "linear_pending_selections_step_idx": {
+ "name": "linear_pending_selections_step_idx",
+ "columns": [
+ {
+ "expression": "step",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "linear_pending_selections_user_id_users_id_fk": {
+ "name": "linear_pending_selections_user_id_users_id_fk",
+ "tableFrom": "linear_pending_selections",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "linear_pending_selections_session_id_unique": {
+ "name": "linear_pending_selections_session_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["session_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.task_inference_usage_events": {
+ "name": "task_inference_usage_events",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'opencode'"
+ },
+ "usage_type": {
+ "name": "usage_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'inference'"
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "environment_id": {
+ "name": "environment_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "event_key": {
+ "name": "event_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "harness_session_id": {
+ "name": "harness_session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "message_id": {
+ "name": "message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "model_id": {
+ "name": "model_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "agent": {
+ "name": "agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "input_tokens": {
+ "name": "input_tokens",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "output_tokens": {
+ "name": "output_tokens",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "reasoning_tokens": {
+ "name": "reasoning_tokens",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "cache_read_tokens": {
+ "name": "cache_read_tokens",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "cache_write_tokens": {
+ "name": "cache_write_tokens",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_tokens": {
+ "name": "total_tokens",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "context_tokens": {
+ "name": "context_tokens",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "cost_micro_usd": {
+ "name": "cost_micro_usd",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "cost_source": {
+ "name": "cost_source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pricing_metadata": {
+ "name": "pricing_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "message_created_at": {
+ "name": "message_created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "message_completed_at": {
+ "name": "message_completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "details": {
+ "name": "details",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "task_inference_usage_events_session_message_unique": {
+ "name": "task_inference_usage_events_session_message_unique",
+ "columns": [
+ {
+ "expression": "harness_session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "message_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_inference_usage_events_event_key_unique": {
+ "name": "task_inference_usage_events_event_key_unique",
+ "columns": [
+ {
+ "expression": "event_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_inference_usage_events_task_id_idx": {
+ "name": "task_inference_usage_events_task_id_idx",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_inference_usage_events_run_id_idx": {
+ "name": "task_inference_usage_events_run_id_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_inference_usage_events_user_id_idx": {
+ "name": "task_inference_usage_events_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_inference_usage_events_environment_id_idx": {
+ "name": "task_inference_usage_events_environment_id_idx",
+ "columns": [
+ {
+ "expression": "environment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_inference_usage_events_session_id_idx": {
+ "name": "task_inference_usage_events_session_id_idx",
+ "columns": [
+ {
+ "expression": "session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_inference_usage_events_provider_model_idx": {
+ "name": "task_inference_usage_events_provider_model_idx",
+ "columns": [
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "model_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_inference_usage_events_created_at_idx": {
+ "name": "task_inference_usage_events_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "task_inference_usage_events_task_id_tasks_id_fk": {
+ "name": "task_inference_usage_events_task_id_tasks_id_fk",
+ "tableFrom": "task_inference_usage_events",
+ "tableTo": "tasks",
+ "columnsFrom": ["task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "task_inference_usage_events_run_id_task_runs_id_fk": {
+ "name": "task_inference_usage_events_run_id_task_runs_id_fk",
+ "tableFrom": "task_inference_usage_events",
+ "tableTo": "task_runs",
+ "columnsFrom": ["run_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "task_inference_usage_events_user_id_users_id_fk": {
+ "name": "task_inference_usage_events_user_id_users_id_fk",
+ "tableFrom": "task_inference_usage_events",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "task_inference_usage_events_environment_id_environments_id_fk": {
+ "name": "task_inference_usage_events_environment_id_environments_id_fk",
+ "tableFrom": "task_inference_usage_events",
+ "tableTo": "environments",
+ "columnsFrom": ["environment_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "task_inference_usage_events_session_id_sessions_id_fk": {
+ "name": "task_inference_usage_events_session_id_sessions_id_fk",
+ "tableFrom": "task_inference_usage_events",
+ "tableTo": "sessions",
+ "columnsFrom": ["session_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_connections": {
+ "name": "mcp_connections",
+ "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
+ },
+ "mcp_id": {
+ "name": "mcp_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connection_role": {
+ "name": "connection_role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'default'"
+ },
+ "auth_config": {
+ "name": "auth_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "auth_status": {
+ "name": "auth_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token_expires_at": {
+ "name": "token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "mcp_connections_user_id_idx": {
+ "name": "mcp_connections_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcp_connections_role_idx": {
+ "name": "mcp_connections_role_idx",
+ "columns": [
+ {
+ "expression": "mcp_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "connection_role",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "mcp_connections_user_id_users_id_fk": {
+ "name": "mcp_connections_user_id_users_id_fk",
+ "tableFrom": "mcp_connections",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mcp_connections_user_mcp_id_unique": {
+ "name": "mcp_connections_user_mcp_id_unique",
+ "nullsNotDistinct": true,
+ "columns": ["user_id", "mcp_id", "connection_role"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_oauth_replays": {
+ "name": "mcp_oauth_replays",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mcp_id": {
+ "name": "mcp_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connection_role": {
+ "name": "connection_role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'default'"
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "redirect_to": {
+ "name": "redirect_to",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "mcp_oauth_replays_connection_id_idx": {
+ "name": "mcp_oauth_replays_connection_id_idx",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcp_oauth_replays_user_id_idx": {
+ "name": "mcp_oauth_replays_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcp_oauth_replays_expires_at_idx": {
+ "name": "mcp_oauth_replays_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "mcp_oauth_replays_connection_id_mcp_connections_id_fk": {
+ "name": "mcp_oauth_replays_connection_id_mcp_connections_id_fk",
+ "tableFrom": "mcp_oauth_replays",
+ "tableTo": "mcp_connections",
+ "columnsFrom": ["connection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_oauth_replays_user_id_users_id_fk": {
+ "name": "mcp_oauth_replays_user_id_users_id_fk",
+ "tableFrom": "mcp_oauth_replays",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mcp_oauth_replays_token_unique": {
+ "name": "mcp_oauth_replays_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.microsoft_auth_user_mappings": {
+ "name": "microsoft_auth_user_mappings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "auth_account_id": {
+ "name": "auth_account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "microsoft_tenant_id": {
+ "name": "microsoft_tenant_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "microsoft_aad_object_id": {
+ "name": "microsoft_aad_object_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "microsoft_auth_user_mappings_user_id_idx": {
+ "name": "microsoft_auth_user_mappings_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "microsoft_auth_user_mappings_account_id_idx": {
+ "name": "microsoft_auth_user_mappings_account_id_idx",
+ "columns": [
+ {
+ "expression": "account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "microsoft_auth_user_mappings_auth_account_idx": {
+ "name": "microsoft_auth_user_mappings_auth_account_idx",
+ "columns": [
+ {
+ "expression": "auth_account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "microsoft_auth_user_mappings_aad_object_unique": {
+ "name": "microsoft_auth_user_mappings_aad_object_unique",
+ "columns": [
+ {
+ "expression": "microsoft_tenant_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "microsoft_aad_object_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "microsoft_auth_user_mappings_auth_account_id_auth_accounts_id_fk": {
+ "name": "microsoft_auth_user_mappings_auth_account_id_auth_accounts_id_fk",
+ "tableFrom": "microsoft_auth_user_mappings",
+ "tableTo": "auth_accounts",
+ "columnsFrom": ["auth_account_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "microsoft_auth_user_mappings_user_id_auth_users_id_fk": {
+ "name": "microsoft_auth_user_mappings_user_id_auth_users_id_fk",
+ "tableFrom": "microsoft_auth_user_mappings",
+ "tableTo": "auth_users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.notion_directory_users": {
+ "name": "notion_directory_users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "notion_user_id": {
+ "name": "notion_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_deleted": {
+ "name": "is_deleted",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "notion_directory_users_unique": {
+ "name": "notion_directory_users_unique",
+ "nullsNotDistinct": false,
+ "columns": ["notion_user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_state": {
+ "name": "oauth_state",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "code_verifier": {
+ "name": "code_verifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "replay_token": {
+ "name": "replay_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "oauth_state_connection_id_idx": {
+ "name": "oauth_state_connection_id_idx",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_state_replay_token_idx": {
+ "name": "oauth_state_replay_token_idx",
+ "columns": [
+ {
+ "expression": "replay_token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_state_expires_at_idx": {
+ "name": "oauth_state_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oauth_state_connection_id_mcp_connections_id_fk": {
+ "name": "oauth_state_connection_id_mcp_connections_id_fk",
+ "tableFrom": "oauth_state",
+ "tableTo": "mcp_connections",
+ "columnsFrom": ["connection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.pr_review_auto_preferences": {
+ "name": "pr_review_auto_preferences",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "source_control_provider": {
+ "name": "source_control_provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "host": {
+ "name": "host",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "repository_id": {
+ "name": "repository_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "repository_identity_key": {
+ "name": "repository_identity_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "repository": {
+ "name": "repository",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pr_number": {
+ "name": "pr_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled_by_user_id": {
+ "name": "enabled_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled_at": {
+ "name": "enabled_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "source_task_id": {
+ "name": "source_task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_destination_key": {
+ "name": "source_destination_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "pr_review_auto_preferences_identity_unique": {
+ "name": "pr_review_auto_preferences_identity_unique",
+ "columns": [
+ {
+ "expression": "source_control_provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "repository_identity_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "pr_number",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pr_review_auto_preferences_repository_idx": {
+ "name": "pr_review_auto_preferences_repository_idx",
+ "columns": [
+ {
+ "expression": "source_control_provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "repository",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "pr_number",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "pr_review_auto_preferences_repository_id_repositories_id_fk": {
+ "name": "pr_review_auto_preferences_repository_id_repositories_id_fk",
+ "tableFrom": "pr_review_auto_preferences",
+ "tableTo": "repositories",
+ "columnsFrom": ["repository_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "pr_review_auto_preferences_enabled_by_user_id_users_id_fk": {
+ "name": "pr_review_auto_preferences_enabled_by_user_id_users_id_fk",
+ "tableFrom": "pr_review_auto_preferences",
+ "tableTo": "users",
+ "columnsFrom": ["enabled_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "pr_review_auto_preferences_source_task_id_tasks_id_fk": {
+ "name": "pr_review_auto_preferences_source_task_id_tasks_id_fk",
+ "tableFrom": "pr_review_auto_preferences",
+ "tableTo": "tasks",
+ "columnsFrom": ["source_task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.pr_review_cycles": {
+ "name": "pr_review_cycles",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "source_control_provider": {
+ "name": "source_control_provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "repository": {
+ "name": "repository",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pr_number": {
+ "name": "pr_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "review_head_sha": {
+ "name": "review_head_sha",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cycle_id": {
+ "name": "cycle_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "pr_review_cycles_source_unique": {
+ "name": "pr_review_cycles_source_unique",
+ "columns": [
+ {
+ "expression": "source_control_provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "repository",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "pr_number",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "review_head_sha",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "cycle_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.pr_review_event_deliveries": {
+ "name": "pr_review_event_deliveries",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "due_at": {
+ "name": "due_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "deferrals": {
+ "name": "deferrals",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "lease_token": {
+ "name": "lease_token",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lease_expires_at": {
+ "name": "lease_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "pr_review_event_deliveries_event_task_unique": {
+ "name": "pr_review_event_deliveries_event_task_unique",
+ "columns": [
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pr_review_event_deliveries_due_idx": {
+ "name": "pr_review_event_deliveries_due_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "due_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lease_expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "pr_review_event_deliveries_event_id_pr_review_events_id_fk": {
+ "name": "pr_review_event_deliveries_event_id_pr_review_events_id_fk",
+ "tableFrom": "pr_review_event_deliveries",
+ "tableTo": "pr_review_events",
+ "columnsFrom": ["event_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "pr_review_event_deliveries_task_id_tasks_id_fk": {
+ "name": "pr_review_event_deliveries_task_id_tasks_id_fk",
+ "tableFrom": "pr_review_event_deliveries",
+ "tableTo": "tasks",
+ "columnsFrom": ["task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "pr_review_event_deliveries_status_check": {
+ "name": "pr_review_event_deliveries_status_check",
+ "value": "\"pr_review_event_deliveries\".\"status\" in ('pending', 'processing', 'delivered', 'suppressed')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.pr_review_events": {
+ "name": "pr_review_events",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "event_key": {
+ "name": "event_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_control_provider": {
+ "name": "source_control_provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "repository": {
+ "name": "repository",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pr_number": {
+ "name": "pr_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pr_url": {
+ "name": "pr_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "event": {
+ "name": "event",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "batch_kind": {
+ "name": "batch_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "batch_id": {
+ "name": "batch_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "review_head_sha": {
+ "name": "review_head_sha",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sealed_at": {
+ "name": "sealed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "superseded": {
+ "name": "superseded",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "available_at": {
+ "name": "available_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "observed_at": {
+ "name": "observed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "pr_review_events_source_unique": {
+ "name": "pr_review_events_source_unique",
+ "columns": [
+ {
+ "expression": "source_control_provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "repository",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "pr_number",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "event_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pr_review_events_pr_idx": {
+ "name": "pr_review_events_pr_idx",
+ "columns": [
+ {
+ "expression": "source_control_provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "repository",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "pr_number",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "pr_review_events_batch_kind_check": {
+ "name": "pr_review_events_batch_kind_check",
+ "value": "\"pr_review_events\".\"batch_kind\" in ('human', 'roomote')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.pr_review_notification_deliveries": {
+ "name": "pr_review_notification_deliveries",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "notification_unit_id": {
+ "name": "notification_unit_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "destination_kind": {
+ "name": "destination_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "destination_key": {
+ "name": "destination_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "due_at": {
+ "name": "due_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "deferrals": {
+ "name": "deferrals",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "attempt": {
+ "name": "attempt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "lease_token": {
+ "name": "lease_token",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lease_expires_at": {
+ "name": "lease_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "route_provider": {
+ "name": "route_provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "route_workspace_id": {
+ "name": "route_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "route_channel_id": {
+ "name": "route_channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "route_thread_id": {
+ "name": "route_thread_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "follow_up_prompt": {
+ "name": "follow_up_prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_task_id": {
+ "name": "target_task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "acting_user_id": {
+ "name": "acting_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_message_id": {
+ "name": "provider_message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action_claimed_at": {
+ "name": "action_claimed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dispatch_key": {
+ "name": "dispatch_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dispatched_run_id": {
+ "name": "dispatched_run_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "pr_review_notification_deliveries_destination_unique": {
+ "name": "pr_review_notification_deliveries_destination_unique",
+ "columns": [
+ {
+ "expression": "notification_unit_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "destination_kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "destination_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pr_review_notification_deliveries_dispatch_key_unique": {
+ "name": "pr_review_notification_deliveries_dispatch_key_unique",
+ "columns": [
+ {
+ "expression": "dispatch_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pr_review_notification_deliveries_due_idx": {
+ "name": "pr_review_notification_deliveries_due_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "due_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lease_expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pr_review_notification_deliveries_destination_idx": {
+ "name": "pr_review_notification_deliveries_destination_idx",
+ "columns": [
+ {
+ "expression": "destination_kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "destination_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "pr_review_notification_deliveries_notification_unit_id_pr_review_notification_units_id_fk": {
+ "name": "pr_review_notification_deliveries_notification_unit_id_pr_review_notification_units_id_fk",
+ "tableFrom": "pr_review_notification_deliveries",
+ "tableTo": "pr_review_notification_units",
+ "columnsFrom": ["notification_unit_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "pr_review_notification_deliveries_task_id_tasks_id_fk": {
+ "name": "pr_review_notification_deliveries_task_id_tasks_id_fk",
+ "tableFrom": "pr_review_notification_deliveries",
+ "tableTo": "tasks",
+ "columnsFrom": ["task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "pr_review_notification_deliveries_target_task_id_tasks_id_fk": {
+ "name": "pr_review_notification_deliveries_target_task_id_tasks_id_fk",
+ "tableFrom": "pr_review_notification_deliveries",
+ "tableTo": "tasks",
+ "columnsFrom": ["target_task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "pr_review_notification_deliveries_acting_user_id_users_id_fk": {
+ "name": "pr_review_notification_deliveries_acting_user_id_users_id_fk",
+ "tableFrom": "pr_review_notification_deliveries",
+ "tableTo": "users",
+ "columnsFrom": ["acting_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "pr_review_notification_deliveries_destination_kind_check": {
+ "name": "pr_review_notification_deliveries_destination_kind_check",
+ "value": "\"pr_review_notification_deliveries\".\"destination_kind\" in ('fast_conversation', 'task')"
+ },
+ "pr_review_notification_deliveries_status_check": {
+ "name": "pr_review_notification_deliveries_status_check",
+ "value": "\"pr_review_notification_deliveries\".\"status\" in ('pending', 'claimed', 'prepared', 'prompt_posting', 'awaiting_user_action', 'auto_dispatch_pending', 'completed', 'suppressed', 'dismissed')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.pr_review_notification_unit_events": {
+ "name": "pr_review_notification_unit_events",
+ "schema": "",
+ "columns": {
+ "unit_id": {
+ "name": "unit_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "attached_at": {
+ "name": "attached_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "pr_review_notification_unit_events_event_unique": {
+ "name": "pr_review_notification_unit_events_event_unique",
+ "columns": [
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "pr_review_notification_unit_events_unit_id_pr_review_notification_units_id_fk": {
+ "name": "pr_review_notification_unit_events_unit_id_pr_review_notification_units_id_fk",
+ "tableFrom": "pr_review_notification_unit_events",
+ "tableTo": "pr_review_notification_units",
+ "columnsFrom": ["unit_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "pr_review_notification_unit_events_event_id_pr_review_events_id_fk": {
+ "name": "pr_review_notification_unit_events_event_id_pr_review_events_id_fk",
+ "tableFrom": "pr_review_notification_unit_events",
+ "tableTo": "pr_review_events",
+ "columnsFrom": ["event_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "pr_review_notification_unit_events_pk": {
+ "name": "pr_review_notification_unit_events_pk",
+ "columns": ["unit_id", "event_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.pr_review_notification_units": {
+ "name": "pr_review_notification_units",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "source_control_provider": {
+ "name": "source_control_provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "host": {
+ "name": "host",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "repository_id": {
+ "name": "repository_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "repository_identity_key": {
+ "name": "repository_identity_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "repository": {
+ "name": "repository",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pr_number": {
+ "name": "pr_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pr_url": {
+ "name": "pr_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "head_sha": {
+ "name": "head_sha",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "head_identity_key": {
+ "name": "head_identity_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "episode_kind": {
+ "name": "episode_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "episode_id": {
+ "name": "episode_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "due_at": {
+ "name": "due_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "first_observed_at": {
+ "name": "first_observed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_observed_at": {
+ "name": "last_observed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sealed_at": {
+ "name": "sealed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "pr_review_notification_units_identity_unique": {
+ "name": "pr_review_notification_units_identity_unique",
+ "columns": [
+ {
+ "expression": "source_control_provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "repository_identity_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "pr_number",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "head_identity_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "episode_kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "episode_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pr_review_notification_units_open_head_idx": {
+ "name": "pr_review_notification_units_open_head_idx",
+ "columns": [
+ {
+ "expression": "source_control_provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "repository",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "pr_number",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "head_sha",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sealed_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "pr_review_notification_units_repository_id_repositories_id_fk": {
+ "name": "pr_review_notification_units_repository_id_repositories_id_fk",
+ "tableFrom": "pr_review_notification_units",
+ "tableTo": "repositories",
+ "columnsFrom": ["repository_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "pr_review_notification_units_episode_kind_check": {
+ "name": "pr_review_notification_units_episode_kind_check",
+ "value": "\"pr_review_notification_units\".\"episode_kind\" in ('roomote_cycle', 'human', 'automated', 'ci')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.pull_request_facts": {
+ "name": "pull_request_facts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "repository_id": {
+ "name": "repository_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "repository_full_name": {
+ "name": "repository_full_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_control_provider": {
+ "name": "source_control_provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'github'"
+ },
+ "external_pull_request_id": {
+ "name": "external_pull_request_id",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pr_number": {
+ "name": "pr_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "html_url": {
+ "name": "html_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "author_login": {
+ "name": "author_login",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "state": {
+ "name": "state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "labels": {
+ "name": "labels",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "changed_files": {
+ "name": "changed_files",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "changed_file_count": {
+ "name": "changed_file_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "files_capped": {
+ "name": "files_capped",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reviews_capped": {
+ "name": "reviews_capped",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "additions": {
+ "name": "additions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deletions": {
+ "name": "deletions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reviews": {
+ "name": "reviews",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enriched_at": {
+ "name": "enriched_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enriched_for_updated_at": {
+ "name": "enriched_for_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enrichment_failed_at": {
+ "name": "enrichment_failed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at_remote": {
+ "name": "created_at_remote",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at_remote": {
+ "name": "updated_at_remote",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "closed_at_remote": {
+ "name": "closed_at_remote",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "merged_at_remote": {
+ "name": "merged_at_remote",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "first_seen_at": {
+ "name": "first_seen_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "synced_at": {
+ "name": "synced_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "pull_request_facts_deployment_repo_pr_unique": {
+ "name": "pull_request_facts_deployment_repo_pr_unique",
+ "columns": [
+ {
+ "expression": "repository_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "pr_number",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pull_request_facts_deployment_created_idx": {
+ "name": "pull_request_facts_deployment_created_idx",
+ "columns": [
+ {
+ "expression": "created_at_remote",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pull_request_facts_deployment_repo_created_idx": {
+ "name": "pull_request_facts_deployment_repo_created_idx",
+ "columns": [
+ {
+ "expression": "repository_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at_remote",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pull_request_facts_deployment_state_created_idx": {
+ "name": "pull_request_facts_deployment_state_created_idx",
+ "columns": [
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at_remote",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pull_request_facts_deployment_author_created_idx": {
+ "name": "pull_request_facts_deployment_author_created_idx",
+ "columns": [
+ {
+ "expression": "author_login",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at_remote",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pull_request_facts_deployment_updated_idx": {
+ "name": "pull_request_facts_deployment_updated_idx",
+ "columns": [
+ {
+ "expression": "updated_at_remote",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "pull_request_facts_repository_id_repositories_id_fk": {
+ "name": "pull_request_facts_repository_id_repositories_id_fk",
+ "tableFrom": "pull_request_facts",
+ "tableTo": "repositories",
+ "columnsFrom": ["repository_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "pull_request_facts_source_control_provider_check": {
+ "name": "pull_request_facts_source_control_provider_check",
+ "value": "\"pull_request_facts\".\"source_control_provider\" in ('github', 'gitlab', 'gitea', 'ado', 'bitbucket')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.pull_request_sync_states": {
+ "name": "pull_request_sync_states",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "repository_id": {
+ "name": "repository_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_incremental_updated_at": {
+ "name": "last_incremental_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "backfill_completed_at": {
+ "name": "backfill_completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cooldown_until": {
+ "name": "cooldown_until",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_successful_sync_at": {
+ "name": "last_successful_sync_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_attempted_sync_at": {
+ "name": "last_attempted_sync_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error_at": {
+ "name": "last_error_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error_message": {
+ "name": "last_error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "pull_request_sync_states_repo_unique": {
+ "name": "pull_request_sync_states_repo_unique",
+ "columns": [
+ {
+ "expression": "repository_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pull_request_sync_states_deployment_updated_idx": {
+ "name": "pull_request_sync_states_deployment_updated_idx",
+ "columns": [
+ {
+ "expression": "last_successful_sync_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pull_request_sync_states_cooldown_idx": {
+ "name": "pull_request_sync_states_cooldown_idx",
+ "columns": [
+ {
+ "expression": "cooldown_until",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "pull_request_sync_states_repository_id_repositories_id_fk": {
+ "name": "pull_request_sync_states_repository_id_repositories_id_fk",
+ "tableFrom": "pull_request_sync_states",
+ "tableTo": "repositories",
+ "columnsFrom": ["repository_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.repositories": {
+ "name": "repositories",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "source_control_provider": {
+ "name": "source_control_provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'github'"
+ },
+ "installation_id": {
+ "name": "installation_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "github_repo_id": {
+ "name": "github_repo_id",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "external_repo_id": {
+ "name": "external_repo_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "host": {
+ "name": "host",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "full_name": {
+ "name": "full_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "private": {
+ "name": "private",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "default_branch": {
+ "name": "default_branch",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'main'"
+ },
+ "clone_url": {
+ "name": "clone_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "html_url": {
+ "name": "html_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permissions": {
+ "name": "permissions",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "linked_by_user_id": {
+ "name": "linked_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "repositories_source_control_provider_idx": {
+ "name": "repositories_source_control_provider_idx",
+ "columns": [
+ {
+ "expression": "source_control_provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "repositories_installation_id_idx": {
+ "name": "repositories_installation_id_idx",
+ "columns": [
+ {
+ "expression": "installation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "repositories_full_name_idx": {
+ "name": "repositories_full_name_idx",
+ "columns": [
+ {
+ "expression": "full_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "repositories_provider_host_full_name_idx": {
+ "name": "repositories_provider_host_full_name_idx",
+ "columns": [
+ {
+ "expression": "source_control_provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "host",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "full_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "repositories_deployment_active_installation_idx": {
+ "name": "repositories_deployment_active_installation_idx",
+ "columns": [
+ {
+ "expression": "is_active",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "installation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "repositories_deployment_github_repo_unique": {
+ "name": "repositories_deployment_github_repo_unique",
+ "columns": [
+ {
+ "expression": "github_repo_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "repositories_provider_host_external_repo_unique": {
+ "name": "repositories_provider_host_external_repo_unique",
+ "columns": [
+ {
+ "expression": "source_control_provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"host\", '')",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_repo_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "repositories_provider_host_full_name_unique": {
+ "name": "repositories_provider_host_full_name_unique",
+ "columns": [
+ {
+ "expression": "source_control_provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"host\", '')",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "full_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "repositories_installation_id_github_installations_id_fk": {
+ "name": "repositories_installation_id_github_installations_id_fk",
+ "tableFrom": "repositories",
+ "tableTo": "github_installations",
+ "columnsFrom": ["installation_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "repositories_user_id_users_id_fk": {
+ "name": "repositories_user_id_users_id_fk",
+ "tableFrom": "repositories",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "repositories_linked_by_user_id_users_id_fk": {
+ "name": "repositories_linked_by_user_id_users_id_fk",
+ "tableFrom": "repositories",
+ "tableTo": "users",
+ "columnsFrom": ["linked_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "repositories_source_control_provider_check": {
+ "name": "repositories_source_control_provider_check",
+ "value": "\"repositories\".\"source_control_provider\" in ('github', 'gitlab', 'gitea', 'ado', 'bitbucket')"
+ },
+ "repositories_github_shape_check": {
+ "name": "repositories_github_shape_check",
+ "value": "\"repositories\".\"source_control_provider\" != 'github' OR (\"repositories\".\"installation_id\" IS NOT NULL AND \"repositories\".\"github_repo_id\" IS NOT NULL)"
+ },
+ "repositories_gitlab_shape_check": {
+ "name": "repositories_gitlab_shape_check",
+ "value": "\"repositories\".\"source_control_provider\" != 'gitlab' OR \"repositories\".\"external_repo_id\" IS NOT NULL"
+ },
+ "repositories_gitea_shape_check": {
+ "name": "repositories_gitea_shape_check",
+ "value": "\"repositories\".\"source_control_provider\" != 'gitea' OR \"repositories\".\"external_repo_id\" IS NOT NULL"
+ },
+ "repositories_ado_shape_check": {
+ "name": "repositories_ado_shape_check",
+ "value": "\"repositories\".\"source_control_provider\" != 'ado' OR \"repositories\".\"external_repo_id\" IS NOT NULL"
+ },
+ "repositories_bitbucket_shape_check": {
+ "name": "repositories_bitbucket_shape_check",
+ "value": "\"repositories\".\"source_control_provider\" != 'bitbucket' OR \"repositories\".\"external_repo_id\" IS NOT NULL"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.repository_automation_signals": {
+ "name": "repository_automation_signals",
+ "schema": "",
+ "columns": {
+ "repository_id": {
+ "name": "repository_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "signals_version": {
+ "name": "signals_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "collected_at": {
+ "name": "collected_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "partial": {
+ "name": "partial",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {
+ "repository_automation_signals_collected_idx": {
+ "name": "repository_automation_signals_collected_idx",
+ "columns": [
+ {
+ "expression": "collected_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "repository_automation_signals_repository_id_repositories_id_fk": {
+ "name": "repository_automation_signals_repository_id_repositories_id_fk",
+ "tableFrom": "repository_automation_signals",
+ "tableTo": "repositories",
+ "columnsFrom": ["repository_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "repository_automation_signals_repository_id_signals_version_pk": {
+ "name": "repository_automation_signals_repository_id_signals_version_pk",
+ "columns": ["repository_id", "signals_version"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sandbox_oidc_targets": {
+ "name": "sandbox_oidc_targets",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "environment_id": {
+ "name": "environment_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "compute_provider": {
+ "name": "compute_provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "compute_provider_id": {
+ "name": "compute_provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_kind": {
+ "name": "target_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "audience": {
+ "name": "audience",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_file": {
+ "name": "token_file",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "aws_role_arn": {
+ "name": "aws_role_arn",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "aws_region": {
+ "name": "aws_region",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_at": {
+ "name": "refresh_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "sandbox_oidc_targets_environment_id_idx": {
+ "name": "sandbox_oidc_targets_environment_id_idx",
+ "columns": [
+ {
+ "expression": "environment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sandbox_oidc_targets_run_id_idx": {
+ "name": "sandbox_oidc_targets_run_id_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sandbox_oidc_targets_refresh_at_idx": {
+ "name": "sandbox_oidc_targets_refresh_at_idx",
+ "columns": [
+ {
+ "expression": "refresh_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sandbox_oidc_targets_provider_target_file_unique": {
+ "name": "sandbox_oidc_targets_provider_target_file_unique",
+ "columns": [
+ {
+ "expression": "compute_provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "compute_provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "token_file",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "sandbox_oidc_targets_environment_id_environments_id_fk": {
+ "name": "sandbox_oidc_targets_environment_id_environments_id_fk",
+ "tableFrom": "sandbox_oidc_targets",
+ "tableTo": "environments",
+ "columnsFrom": ["environment_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "sandbox_oidc_targets_run_id_task_runs_id_fk": {
+ "name": "sandbox_oidc_targets_run_id_task_runs_id_fk",
+ "tableFrom": "sandbox_oidc_targets",
+ "tableTo": "task_runs",
+ "columnsFrom": ["run_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "sandbox_oidc_targets_owner_required": {
+ "name": "sandbox_oidc_targets_owner_required",
+ "value": "run_id IS NOT NULL"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.session_backfill_state": {
+ "name": "session_backfill_state",
+ "schema": "",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "phase": {
+ "name": "phase",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'fast_conversations'"
+ },
+ "cursor_created_at": {
+ "name": "cursor_created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cursor_id": {
+ "name": "cursor_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_run_at": {
+ "name": "last_run_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "session_backfill_state_phase_check": {
+ "name": "session_backfill_state_phase_check",
+ "value": "\"session_backfill_state\".\"phase\" in ('fast_conversations', 'fast_tasks', 'tasks', 'participants')"
+ },
+ "session_backfill_state_cursor_shape_check": {
+ "name": "session_backfill_state_cursor_shape_check",
+ "value": "(\"session_backfill_state\".\"cursor_created_at\" IS NULL) = (\"session_backfill_state\".\"cursor_id\" IS NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.session_participants": {
+ "name": "session_participants",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'member'"
+ },
+ "last_read_event_at": {
+ "name": "last_read_event_at",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_read_event_id": {
+ "name": "last_read_event_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_notified_event_at": {
+ "name": "last_notified_event_at",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_notified_event_id": {
+ "name": "last_notified_event_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "session_participants_session_user_unique": {
+ "name": "session_participants_session_user_unique",
+ "columns": [
+ {
+ "expression": "session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "session_participants_user_id_idx": {
+ "name": "session_participants_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "session_participants_session_id_sessions_id_fk": {
+ "name": "session_participants_session_id_sessions_id_fk",
+ "tableFrom": "session_participants",
+ "tableTo": "sessions",
+ "columnsFrom": ["session_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "session_participants_user_id_users_id_fk": {
+ "name": "session_participants_user_id_users_id_fk",
+ "tableFrom": "session_participants",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "session_participants_role_check": {
+ "name": "session_participants_role_check",
+ "value": "\"session_participants\".\"role\" in ('owner', 'member')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.session_pins": {
+ "name": "session_pins",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "session_pins_user_session_unique": {
+ "name": "session_pins_user_session_unique",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "session_pins_user_updated_at_idx": {
+ "name": "session_pins_user_updated_at_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "session_pins_session_id_idx": {
+ "name": "session_pins_session_id_idx",
+ "columns": [
+ {
+ "expression": "session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "session_pins_session_id_sessions_id_fk": {
+ "name": "session_pins_session_id_sessions_id_fk",
+ "tableFrom": "session_pins",
+ "tableTo": "sessions",
+ "columnsFrom": ["session_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "session_pins_user_id_users_id_fk": {
+ "name": "session_pins_user_id_users_id_fk",
+ "tableFrom": "session_pins",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.session_tasks": {
+ "name": "session_tasks",
+ "schema": "",
+ "columns": {
+ "session_id": {
+ "name": "session_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "attached_at": {
+ "name": "attached_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "origin": {
+ "name": "origin",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "session_tasks_task_id_unique": {
+ "name": "session_tasks_task_id_unique",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "session_tasks_session_attached_at_idx": {
+ "name": "session_tasks_session_attached_at_idx",
+ "columns": [
+ {
+ "expression": "session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "attached_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "session_tasks_session_id_sessions_id_fk": {
+ "name": "session_tasks_session_id_sessions_id_fk",
+ "tableFrom": "session_tasks",
+ "tableTo": "sessions",
+ "columnsFrom": ["session_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "session_tasks_task_id_tasks_id_fk": {
+ "name": "session_tasks_task_id_tasks_id_fk",
+ "tableFrom": "session_tasks",
+ "tableTo": "tasks",
+ "columnsFrom": ["task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "session_tasks_session_id_task_id_pk": {
+ "name": "session_tasks_session_id_task_id_pk",
+ "columns": ["session_id", "task_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "session_tasks_origin_check": {
+ "name": "session_tasks_origin_check",
+ "value": "\"session_tasks\".\"origin\" in ('direct_launch', 'fast_delegation', 'backfill', 'follow_up')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.sessions": {
+ "name": "sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owner_kind": {
+ "name": "owner_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owner_user_id": {
+ "name": "owner_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "owner_automation": {
+ "name": "owner_automation",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_surface": {
+ "name": "source_surface",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_trigger": {
+ "name": "source_trigger",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "fast_conversation_id": {
+ "name": "fast_conversation_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "visibility": {
+ "name": "visibility",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'visible'"
+ },
+ "activity_at": {
+ "name": "activity_at",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cached_status": {
+ "name": "cached_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "sessions_visibility_activity_at_idx": {
+ "name": "sessions_visibility_activity_at_idx",
+ "columns": [
+ {
+ "expression": "visibility",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "activity_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sessions_owner_user_id_idx": {
+ "name": "sessions_owner_user_id_idx",
+ "columns": [
+ {
+ "expression": "owner_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sessions_fast_conversation_id_unique": {
+ "name": "sessions_fast_conversation_id_unique",
+ "columns": [
+ {
+ "expression": "fast_conversation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"sessions\".\"fast_conversation_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "sessions_owner_user_id_users_id_fk": {
+ "name": "sessions_owner_user_id_users_id_fk",
+ "tableFrom": "sessions",
+ "tableTo": "users",
+ "columnsFrom": ["owner_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "sessions_owner_automation_automations_key_fk": {
+ "name": "sessions_owner_automation_automations_key_fk",
+ "tableFrom": "sessions",
+ "tableTo": "automations",
+ "columnsFrom": ["owner_automation"],
+ "columnsTo": ["key"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "sessions_fast_conversation_id_fast_agent_conversations_id_fk": {
+ "name": "sessions_fast_conversation_id_fast_agent_conversations_id_fk",
+ "tableFrom": "sessions",
+ "tableTo": "fast_agent_conversations",
+ "columnsFrom": ["fast_conversation_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "sessions_owner_shape_check": {
+ "name": "sessions_owner_shape_check",
+ "value": "(\"sessions\".\"owner_kind\" = 'user' AND \"sessions\".\"owner_automation\" IS NULL) OR (\"sessions\".\"owner_kind\" = 'automation' AND \"sessions\".\"owner_user_id\" IS NULL) OR (\"sessions\".\"owner_kind\" = 'system' AND \"sessions\".\"owner_user_id\" IS NULL AND \"sessions\".\"owner_automation\" IS NULL)"
+ },
+ "sessions_owner_kind_check": {
+ "name": "sessions_owner_kind_check",
+ "value": "\"sessions\".\"owner_kind\" in ('user', 'automation', 'system')"
+ },
+ "sessions_source_surface_check": {
+ "name": "sessions_source_surface_check",
+ "value": "\"sessions\".\"source_surface\" in ('web', 'api', 'slack', 'teams', 'telegram', 'discord', 'linear', 'github', 'gitlab', 'gitea', 'ado', 'bitbucket', 'system', 'automation')"
+ },
+ "sessions_source_trigger_check": {
+ "name": "sessions_source_trigger_check",
+ "value": "\"sessions\".\"source_trigger\" in ('message', 'webhook', 'schedule', 'manual')"
+ },
+ "sessions_visibility_check": {
+ "name": "sessions_visibility_check",
+ "value": "\"sessions\".\"visibility\" in ('visible', 'hidden')"
+ },
+ "sessions_cached_status_check": {
+ "name": "sessions_cached_status_check",
+ "value": "\"sessions\".\"cached_status\" IS NULL OR \"sessions\".\"cached_status\" in ('active', 'needs_input', 'blocked', 'ready')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.setup_qualification_blocks": {
+ "name": "setup_qualification_blocks",
+ "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": true
+ },
+ "reason": {
+ "name": "reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'blocked'"
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email_domain": {
+ "name": "email_domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "github_account_login": {
+ "name": "github_account_login",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "github_account_type": {
+ "name": "github_account_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "first_blocked_at": {
+ "name": "first_blocked_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "last_blocked_at": {
+ "name": "last_blocked_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "resolved_at": {
+ "name": "resolved_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lifted_by_admin_user_id": {
+ "name": "lifted_by_admin_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lifted_by_admin_email": {
+ "name": "lifted_by_admin_email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "setup_qualification_blocks_deployment_user_reason_unique": {
+ "name": "setup_qualification_blocks_deployment_user_reason_unique",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "reason",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "setup_qualification_blocks_deployment_status_idx": {
+ "name": "setup_qualification_blocks_deployment_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "setup_qualification_blocks_user_status_idx": {
+ "name": "setup_qualification_blocks_user_status_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "setup_qualification_blocks_user_id_users_id_fk": {
+ "name": "setup_qualification_blocks_user_id_users_id_fk",
+ "tableFrom": "setup_qualification_blocks",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.slack_auth_tokens": {
+ "name": "slack_auth_tokens",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slack_user_id": {
+ "name": "slack_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slack_team_id": {
+ "name": "slack_team_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "channel": {
+ "name": "channel",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "thread_ts": {
+ "name": "thread_ts",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_text": {
+ "name": "original_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "slack_auth_tokens_expires_at_idx": {
+ "name": "slack_auth_tokens_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "slack_auth_tokens_token_unique": {
+ "name": "slack_auth_tokens_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.slack_conversation_messages": {
+ "name": "slack_conversation_messages",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "subject_user_id": {
+ "name": "subject_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "slack_team_id": {
+ "name": "slack_team_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject_slack_user_id": {
+ "name": "subject_slack_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sender_user_id": {
+ "name": "sender_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sender_slack_user_id": {
+ "name": "sender_slack_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "slack_channel_id": {
+ "name": "slack_channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "conversation_kind": {
+ "name": "conversation_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "thread_ts": {
+ "name": "thread_ts",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "message_ts": {
+ "name": "message_ts",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "message_at": {
+ "name": "message_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "direction": {
+ "name": "direction",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "author_kind": {
+ "name": "author_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "text": {
+ "name": "text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "slack_conversation_messages_deployment_user_message_at_idx": {
+ "name": "slack_conversation_messages_deployment_user_message_at_idx",
+ "columns": [
+ {
+ "expression": "subject_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "message_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "slack_conversation_messages_deployment_user_thread_idx": {
+ "name": "slack_conversation_messages_deployment_user_thread_idx",
+ "columns": [
+ {
+ "expression": "subject_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "slack_channel_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "thread_ts",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "message_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "slack_conversation_messages_task_id_idx": {
+ "name": "slack_conversation_messages_task_id_idx",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "slack_conversation_messages_run_id_idx": {
+ "name": "slack_conversation_messages_run_id_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "slack_conversation_messages_team_channel_message_unique": {
+ "name": "slack_conversation_messages_team_channel_message_unique",
+ "columns": [
+ {
+ "expression": "slack_team_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "slack_channel_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "message_ts",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "slack_conversation_messages_subject_user_id_users_id_fk": {
+ "name": "slack_conversation_messages_subject_user_id_users_id_fk",
+ "tableFrom": "slack_conversation_messages",
+ "tableTo": "users",
+ "columnsFrom": ["subject_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "slack_conversation_messages_sender_user_id_users_id_fk": {
+ "name": "slack_conversation_messages_sender_user_id_users_id_fk",
+ "tableFrom": "slack_conversation_messages",
+ "tableTo": "users",
+ "columnsFrom": ["sender_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "slack_conversation_messages_task_id_tasks_id_fk": {
+ "name": "slack_conversation_messages_task_id_tasks_id_fk",
+ "tableFrom": "slack_conversation_messages",
+ "tableTo": "tasks",
+ "columnsFrom": ["task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "slack_conversation_messages_run_id_task_runs_id_fk": {
+ "name": "slack_conversation_messages_run_id_task_runs_id_fk",
+ "tableFrom": "slack_conversation_messages",
+ "tableTo": "task_runs",
+ "columnsFrom": ["run_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.slack_directory_users": {
+ "name": "slack_directory_users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "slack_user_id": {
+ "name": "slack_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slack_team_id": {
+ "name": "slack_team_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "real_name": {
+ "name": "real_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_deleted": {
+ "name": "is_deleted",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_bot": {
+ "name": "is_bot",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_app_user": {
+ "name": "is_app_user",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "profile_updated_at": {
+ "name": "profile_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "slack_directory_users_team_id_idx": {
+ "name": "slack_directory_users_team_id_idx",
+ "columns": [
+ {
+ "expression": "slack_team_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "slack_directory_users_unique": {
+ "name": "slack_directory_users_unique",
+ "nullsNotDistinct": false,
+ "columns": ["slack_user_id", "slack_team_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.slack_fast_integration_calls": {
+ "name": "slack_fast_integration_calls",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "fast_agent_conversation_id": {
+ "name": "fast_agent_conversation_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slack_team_id": {
+ "name": "slack_team_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slack_channel": {
+ "name": "slack_channel",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slack_thread_ts": {
+ "name": "slack_thread_ts",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slack_message_ts": {
+ "name": "slack_message_ts",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "integration_id": {
+ "name": "integration_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tool_name": {
+ "name": "tool_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "arguments": {
+ "name": "arguments",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "result_preview": {
+ "name": "result_preview",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "duration_ms": {
+ "name": "duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "slack_fast_integration_calls_conversation_idx": {
+ "name": "slack_fast_integration_calls_conversation_idx",
+ "columns": [
+ {
+ "expression": "fast_agent_conversation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "slack_fast_integration_calls_user_idx": {
+ "name": "slack_fast_integration_calls_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "slack_fast_integration_calls_status_idx": {
+ "name": "slack_fast_integration_calls_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": {
+ "slack_fast_integration_calls_fast_agent_conversation_id_fast_agent_conversations_id_fk": {
+ "name": "slack_fast_integration_calls_fast_agent_conversation_id_fast_agent_conversations_id_fk",
+ "tableFrom": "slack_fast_integration_calls",
+ "tableTo": "fast_agent_conversations",
+ "columnsFrom": ["fast_agent_conversation_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "slack_fast_integration_calls_user_id_users_id_fk": {
+ "name": "slack_fast_integration_calls_user_id_users_id_fk",
+ "tableFrom": "slack_fast_integration_calls",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.slack_installation_channels": {
+ "name": "slack_installation_channels",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "slack_installation_id": {
+ "name": "slack_installation_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "channel_id": {
+ "name": "channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "slack_installation_channels_installation_id_idx": {
+ "name": "slack_installation_channels_installation_id_idx",
+ "columns": [
+ {
+ "expression": "slack_installation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "slack_installation_channels_slack_installation_id_slack_installations_id_fk": {
+ "name": "slack_installation_channels_slack_installation_id_slack_installations_id_fk",
+ "tableFrom": "slack_installation_channels",
+ "tableTo": "slack_installations",
+ "columnsFrom": ["slack_installation_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "slack_installation_channels_unique": {
+ "name": "slack_installation_channels_unique",
+ "nullsNotDistinct": false,
+ "columns": ["slack_installation_id", "channel_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.slack_installations": {
+ "name": "slack_installations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "team_id": {
+ "name": "team_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "team_name": {
+ "name": "team_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "team_domain": {
+ "name": "team_domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enterprise_id": {
+ "name": "enterprise_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enterprise_name": {
+ "name": "enterprise_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "app_id": {
+ "name": "app_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "bot_user_id": {
+ "name": "bot_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "bot_name": {
+ "name": "bot_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "app_name": {
+ "name": "app_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "bot_access_token": {
+ "name": "bot_access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_access_token": {
+ "name": "user_access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_type": {
+ "name": "token_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'bot'"
+ },
+ "installed_by_user_id": {
+ "name": "installed_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "member_count_snapshot": {
+ "name": "member_count_snapshot",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "member_count_snapshot_at": {
+ "name": "member_count_snapshot_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "slack_installations_bot_user_id_idx": {
+ "name": "slack_installations_bot_user_id_idx",
+ "columns": [
+ {
+ "expression": "bot_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "slack_installations_active_idx": {
+ "name": "slack_installations_active_idx",
+ "columns": [
+ {
+ "expression": "is_active",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "slack_installations_installed_by_user_id_users_id_fk": {
+ "name": "slack_installations_installed_by_user_id_users_id_fk",
+ "tableFrom": "slack_installations",
+ "tableTo": "users",
+ "columnsFrom": ["installed_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "slack_installations_team_id_unique": {
+ "name": "slack_installations_team_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["team_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.slack_user_mappings": {
+ "name": "slack_user_mappings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "slack_user_id": {
+ "name": "slack_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slack_team_id": {
+ "name": "slack_team_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "slack_user_mappings_user_id_idx": {
+ "name": "slack_user_mappings_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "slack_user_mappings_user_id_users_id_fk": {
+ "name": "slack_user_mappings_user_id_users_id_fk",
+ "tableFrom": "slack_user_mappings",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "slack_user_mappings_unique": {
+ "name": "slack_user_mappings_unique",
+ "nullsNotDistinct": false,
+ "columns": ["slack_user_id", "slack_team_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.source_control_user_mappings": {
+ "name": "source_control_user_mappings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "auth_account_id": {
+ "name": "auth_account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_control_provider": {
+ "name": "source_control_provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "host": {
+ "name": "host",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_account_id": {
+ "name": "external_account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "source_control_user_mappings_auth_account_unique": {
+ "name": "source_control_user_mappings_auth_account_unique",
+ "columns": [
+ {
+ "expression": "auth_account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "source_control_user_mappings_user_provider_host_idx": {
+ "name": "source_control_user_mappings_user_provider_host_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_control_provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "host",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "source_control_user_mappings_provider_identity_unique": {
+ "name": "source_control_user_mappings_provider_identity_unique",
+ "columns": [
+ {
+ "expression": "source_control_provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "host",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "source_control_user_mappings_auth_account_id_auth_accounts_id_fk": {
+ "name": "source_control_user_mappings_auth_account_id_auth_accounts_id_fk",
+ "tableFrom": "source_control_user_mappings",
+ "tableTo": "auth_accounts",
+ "columnsFrom": ["auth_account_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "source_control_user_mappings_user_id_auth_users_id_fk": {
+ "name": "source_control_user_mappings_user_id_auth_users_id_fk",
+ "tableFrom": "source_control_user_mappings",
+ "tableTo": "auth_users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.task_artifacts": {
+ "name": "task_artifacts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "artifact_type": {
+ "name": "artifact_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'general'"
+ },
+ "content_type": {
+ "name": "content_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "size": {
+ "name": "size",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "uploaded": {
+ "name": "uploaded",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "task_artifacts_task_id_idx": {
+ "name": "task_artifacts_task_id_idx",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_artifacts_run_id_idx": {
+ "name": "task_artifacts_run_id_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_artifacts_uploaded_idx": {
+ "name": "task_artifacts_uploaded_idx",
+ "columns": [
+ {
+ "expression": "uploaded",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_artifacts_created_at_idx": {
+ "name": "task_artifacts_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_artifacts_path_idx": {
+ "name": "task_artifacts_path_idx",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "task_artifacts_task_id_tasks_id_fk": {
+ "name": "task_artifacts_task_id_tasks_id_fk",
+ "tableFrom": "task_artifacts",
+ "tableTo": "tasks",
+ "columnsFrom": ["task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "task_artifacts_run_id_task_runs_id_fk": {
+ "name": "task_artifacts_run_id_task_runs_id_fk",
+ "tableFrom": "task_artifacts",
+ "tableTo": "task_runs",
+ "columnsFrom": ["run_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "task_artifacts_task_id_path_version_unique": {
+ "name": "task_artifacts_task_id_path_version_unique",
+ "nullsNotDistinct": false,
+ "columns": ["task_id", "path", "version"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.task_messages": {
+ "name": "task_messages",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ts": {
+ "name": "ts",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_type": {
+ "name": "event_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "protocol": {
+ "name": "protocol",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_blocks": {
+ "name": "content_blocks",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "task_messages_task_id_ts_idx": {
+ "name": "task_messages_task_id_ts_idx",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "ts",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_messages_run_id_idx": {
+ "name": "task_messages_run_id_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_messages_created_at_idx": {
+ "name": "task_messages_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "task_messages_run_id_task_runs_id_fk": {
+ "name": "task_messages_run_id_task_runs_id_fk",
+ "tableFrom": "task_messages",
+ "tableTo": "task_runs",
+ "columnsFrom": ["run_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "task_messages_task_id_tasks_id_fk": {
+ "name": "task_messages_task_id_tasks_id_fk",
+ "tableFrom": "task_messages",
+ "tableTo": "tasks",
+ "columnsFrom": ["task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "task_messages_user_id_users_id_fk": {
+ "name": "task_messages_user_id_users_id_fk",
+ "tableFrom": "task_messages",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "task_messages_task_protocol_ts_event_type_unique": {
+ "name": "task_messages_task_protocol_ts_event_type_unique",
+ "nullsNotDistinct": false,
+ "columns": ["task_id", "protocol", "ts", "event_type"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.task_pins": {
+ "name": "task_pins",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "task_pins_deployment_user_task_unique": {
+ "name": "task_pins_deployment_user_task_unique",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_pins_deployment_user_updated_at_idx": {
+ "name": "task_pins_deployment_user_updated_at_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_pins_task_id_idx": {
+ "name": "task_pins_task_id_idx",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "task_pins_task_id_tasks_id_fk": {
+ "name": "task_pins_task_id_tasks_id_fk",
+ "tableFrom": "task_pins",
+ "tableTo": "tasks",
+ "columnsFrom": ["task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "task_pins_user_id_users_id_fk": {
+ "name": "task_pins_user_id_users_id_fk",
+ "tableFrom": "task_pins",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.task_platform_issue_reports": {
+ "name": "task_platform_issue_reports",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "task_message_id": {
+ "name": "task_message_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "report": {
+ "name": "report",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slack_posted_at": {
+ "name": "slack_posted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "task_platform_issue_reports_created_at_idx": {
+ "name": "task_platform_issue_reports_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_platform_issue_reports_task_id_created_at_idx": {
+ "name": "task_platform_issue_reports_task_id_created_at_idx",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_platform_issue_reports_run_id_created_at_idx": {
+ "name": "task_platform_issue_reports_run_id_created_at_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_platform_issue_reports_task_message_id_unique": {
+ "name": "task_platform_issue_reports_task_message_id_unique",
+ "columns": [
+ {
+ "expression": "task_message_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "task_platform_issue_reports_task_id_tasks_id_fk": {
+ "name": "task_platform_issue_reports_task_id_tasks_id_fk",
+ "tableFrom": "task_platform_issue_reports",
+ "tableTo": "tasks",
+ "columnsFrom": ["task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "task_platform_issue_reports_run_id_task_runs_id_fk": {
+ "name": "task_platform_issue_reports_run_id_task_runs_id_fk",
+ "tableFrom": "task_platform_issue_reports",
+ "tableTo": "task_runs",
+ "columnsFrom": ["run_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "task_platform_issue_reports_task_message_id_task_messages_id_fk": {
+ "name": "task_platform_issue_reports_task_message_id_task_messages_id_fk",
+ "tableFrom": "task_platform_issue_reports",
+ "tableTo": "task_messages",
+ "columnsFrom": ["task_message_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.task_pull_requests": {
+ "name": "task_pull_requests",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_control_provider": {
+ "name": "source_control_provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'github'"
+ },
+ "host": {
+ "name": "host",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "repository_id": {
+ "name": "repository_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pr_url": {
+ "name": "pr_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pr_number": {
+ "name": "pr_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pr_title": {
+ "name": "pr_title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "repository": {
+ "name": "repository",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pr_sha": {
+ "name": "pr_sha",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pr_base_ref": {
+ "name": "pr_base_ref",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pr_base_sha": {
+ "name": "pr_base_sha",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "github_reaction_id": {
+ "name": "github_reaction_id",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "github_check_run_id": {
+ "name": "github_check_run_id",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "github_review_comment_id": {
+ "name": "github_review_comment_id",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by_roomote": {
+ "name": "created_by_roomote",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mergeability_status": {
+ "name": "mergeability_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'unknown'"
+ },
+ "conflict_detected_at": {
+ "name": "conflict_detected_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "conflict_notification_claimed_at": {
+ "name": "conflict_notification_claimed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "conflict_notified_at": {
+ "name": "conflict_notified_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auto_handle_feedback_by_user_id": {
+ "name": "auto_handle_feedback_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "detected_at": {
+ "name": "detected_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "task_pull_requests_task_id_idx": {
+ "name": "task_pull_requests_task_id_idx",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_pull_requests_repository_id_idx": {
+ "name": "task_pull_requests_repository_id_idx",
+ "columns": [
+ {
+ "expression": "repository_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_pull_requests_provider_repository_pr_number_idx": {
+ "name": "task_pull_requests_provider_repository_pr_number_idx",
+ "columns": [
+ {
+ "expression": "source_control_provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "repository",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "pr_number",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_pull_requests_mergeability_lookup_idx": {
+ "name": "task_pull_requests_mergeability_lookup_idx",
+ "columns": [
+ {
+ "expression": "source_control_provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "repository",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_by_roomote",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "pr_base_ref",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "task_pull_requests_task_id_tasks_id_fk": {
+ "name": "task_pull_requests_task_id_tasks_id_fk",
+ "tableFrom": "task_pull_requests",
+ "tableTo": "tasks",
+ "columnsFrom": ["task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "task_pull_requests_repository_id_repositories_id_fk": {
+ "name": "task_pull_requests_repository_id_repositories_id_fk",
+ "tableFrom": "task_pull_requests",
+ "tableTo": "repositories",
+ "columnsFrom": ["repository_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "task_pull_requests_auto_handle_feedback_by_user_id_users_id_fk": {
+ "name": "task_pull_requests_auto_handle_feedback_by_user_id_users_id_fk",
+ "tableFrom": "task_pull_requests",
+ "tableTo": "users",
+ "columnsFrom": ["auto_handle_feedback_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "task_pull_requests_task_pr_unique": {
+ "name": "task_pull_requests_task_pr_unique",
+ "nullsNotDistinct": false,
+ "columns": ["task_id", "pr_url"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "task_pull_requests_source_control_provider_check": {
+ "name": "task_pull_requests_source_control_provider_check",
+ "value": "\"task_pull_requests\".\"source_control_provider\" in ('github', 'gitlab', 'gitea', 'ado', 'bitbucket')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.task_run_events": {
+ "name": "task_run_events",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_type": {
+ "name": "event_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "details": {
+ "name": "details",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "task_run_events_run_id_created_at_idx": {
+ "name": "task_run_events_run_id_created_at_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_run_events_task_id_created_at_idx": {
+ "name": "task_run_events_task_id_created_at_idx",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_run_events_created_at_idx": {
+ "name": "task_run_events_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_run_events_source_created_at_idx": {
+ "name": "task_run_events_source_created_at_idx",
+ "columns": [
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "task_run_events_run_id_task_runs_id_fk": {
+ "name": "task_run_events_run_id_task_runs_id_fk",
+ "tableFrom": "task_run_events",
+ "tableTo": "task_runs",
+ "columnsFrom": ["run_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "task_run_events_task_id_tasks_id_fk": {
+ "name": "task_run_events_task_id_tasks_id_fk",
+ "tableFrom": "task_run_events",
+ "tableTo": "tasks",
+ "columnsFrom": ["task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.task_runs": {
+ "name": "task_runs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "task_runs_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "2147483647",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'fresh'"
+ },
+ "source_run_id": {
+ "name": "source_run_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "acting_user_id": {
+ "name": "acting_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "payload_kind": {
+ "name": "payload_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "harness": {
+ "name": "harness",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'opencode-server'"
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "queue_scope": {
+ "name": "queue_scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "task_phase": {
+ "name": "task_phase",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "fast_agent_session_id": {
+ "name": "fast_agent_session_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "((payload ->> 'fastAgentSessionId')::uuid)",
+ "type": "stored"
+ }
+ },
+ "prompt": {
+ "name": "prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "log": {
+ "name": "log",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "artifacts": {
+ "name": "artifacts",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "result": {
+ "name": "result",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_code": {
+ "name": "error_code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "machine_id": {
+ "name": "machine_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sandbox_cmd_id": {
+ "name": "sandbox_cmd_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "machine_domain": {
+ "name": "machine_domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "machine_domains": {
+ "name": "machine_domains",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "initial_paths": {
+ "name": "initial_paths",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "primary_port_name": {
+ "name": "primary_port_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sandbox_server_url": {
+ "name": "sandbox_server_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "proxy_ports": {
+ "name": "proxy_ports",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "worker_release_tag": {
+ "name": "worker_release_tag",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "worker_version": {
+ "name": "worker_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "worker_commit": {
+ "name": "worker_commit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vendor": {
+ "name": "vendor",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "port": {
+ "name": "port",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "configured_vcpus": {
+ "name": "configured_vcpus",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "configured_cpu_cores": {
+ "name": "configured_cpu_cores",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "configured_memory_mib": {
+ "name": "configured_memory_mib",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "snapshot_id": {
+ "name": "snapshot_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "snapshot_requested_at": {
+ "name": "snapshot_requested_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "snapshot_created_at": {
+ "name": "snapshot_created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "snapshot_failed_at": {
+ "name": "snapshot_failed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "keepalive_ms": {
+ "name": "keepalive_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sleep_at": {
+ "name": "sleep_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sleep_requested_at": {
+ "name": "sleep_requested_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "worker_heartbeat_at": {
+ "name": "worker_heartbeat_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_snapshot_id": {
+ "name": "source_snapshot_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auth_bypass_value": {
+ "name": "auth_bypass_value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auth_bypass_header_name": {
+ "name": "auth_bypass_header_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "dequeued_at": {
+ "name": "dequeued_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provision_started_at": {
+ "name": "provision_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provision_ready_at": {
+ "name": "provision_ready_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "setup_completed_at": {
+ "name": "setup_completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "environment_setup_state": {
+ "name": "environment_setup_state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "environment_setup_completed_at": {
+ "name": "environment_setup_completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "harness_started_at": {
+ "name": "harness_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "runtime_task_started_at": {
+ "name": "runtime_task_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "first_assistant_output_at": {
+ "name": "first_assistant_output_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cancel_requested_at": {
+ "name": "cancel_requested_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "canceled_at": {
+ "name": "canceled_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "launch_mode": {
+ "name": "launch_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "task_runs_task_id_idx": {
+ "name": "task_runs_task_id_idx",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_runs_fast_agent_session_id_idx": {
+ "name": "task_runs_fast_agent_session_id_idx",
+ "columns": [
+ {
+ "expression": "fast_agent_session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_runs_queue_scope_idx": {
+ "name": "task_runs_queue_scope_idx",
+ "columns": [
+ {
+ "expression": "queue_scope",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_runs_acting_user_id_idx": {
+ "name": "task_runs_acting_user_id_idx",
+ "columns": [
+ {
+ "expression": "acting_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_runs_snapshot_id_idx": {
+ "name": "task_runs_snapshot_id_idx",
+ "columns": [
+ {
+ "expression": "snapshot_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_runs_sleep_at_idx": {
+ "name": "task_runs_sleep_at_idx",
+ "columns": [
+ {
+ "expression": "sleep_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_runs_worker_heartbeat_at_idx": {
+ "name": "task_runs_worker_heartbeat_at_idx",
+ "columns": [
+ {
+ "expression": "worker_heartbeat_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_runs_sleep_check_due_v2_idx": {
+ "name": "task_runs_sleep_check_due_v2_idx",
+ "columns": [
+ {
+ "expression": "sleep_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "vendor",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"task_runs\".\"status\" IN ('running', 'idle') AND \"task_runs\".\"machine_id\" IS NOT NULL AND \"task_runs\".\"sleep_at\" IS NOT NULL AND \"task_runs\".\"sleep_requested_at\" IS NULL AND \"task_runs\".\"snapshot_id\" IS NULL AND \"task_runs\".\"snapshot_requested_at\" IS NULL AND \"task_runs\".\"vendor\" IN ('modal', 'daytona', 'e2b', 'docker', 'blaxel', 'box', 'roomote', 'azure')",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_runs_sleep_check_stale_worker_v2_idx": {
+ "name": "task_runs_sleep_check_stale_worker_v2_idx",
+ "columns": [
+ {
+ "expression": "worker_heartbeat_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "vendor",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"task_runs\".\"status\" IN ('running', 'idle') AND \"task_runs\".\"machine_id\" IS NOT NULL AND \"task_runs\".\"worker_heartbeat_at\" IS NOT NULL AND \"task_runs\".\"sleep_requested_at\" IS NULL AND \"task_runs\".\"snapshot_id\" IS NULL AND \"task_runs\".\"snapshot_requested_at\" IS NULL AND \"task_runs\".\"vendor\" IN ('modal', 'daytona', 'e2b', 'docker', 'blaxel', 'box', 'roomote', 'azure')",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_runs_sleep_check_active_v2_idx": {
+ "name": "task_runs_sleep_check_active_v2_idx",
+ "columns": [
+ {
+ "expression": "vendor",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"task_runs\".\"status\" IN ('running', 'idle') AND \"task_runs\".\"machine_id\" IS NOT NULL AND \"task_runs\".\"sleep_requested_at\" IS NULL AND \"task_runs\".\"snapshot_id\" IS NULL AND \"task_runs\".\"snapshot_requested_at\" IS NULL AND \"task_runs\".\"vendor\" IN ('modal', 'daytona', 'e2b', 'docker', 'blaxel', 'box', 'roomote', 'azure')",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_runs_source_snapshot_id_idx": {
+ "name": "task_runs_source_snapshot_id_idx",
+ "columns": [
+ {
+ "expression": "source_snapshot_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_runs_source_run_id_idx": {
+ "name": "task_runs_source_run_id_idx",
+ "columns": [
+ {
+ "expression": "source_run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_runs_discord_source_event_unique": {
+ "name": "task_runs_discord_source_event_unique",
+ "columns": [
+ {
+ "expression": "(\"payload\"->>'communicationSourceEventId')",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"task_runs\".\"payload\"->>'communicationProvider' = 'discord' AND \"task_runs\".\"payload\"->>'communicationSourceEventId' IS NOT NULL AND \"task_runs\".\"canceled_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_runs_launch_idempotency_key_unique": {
+ "name": "task_runs_launch_idempotency_key_unique",
+ "columns": [
+ {
+ "expression": "(\"payload\"->>'launchIdempotencyKey')",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"task_runs\".\"payload\"->>'launchIdempotencyKey' IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_runs_first_assistant_output_at_idx": {
+ "name": "task_runs_first_assistant_output_at_idx",
+ "columns": [
+ {
+ "expression": "first_assistant_output_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "task_runs_task_id_tasks_id_fk": {
+ "name": "task_runs_task_id_tasks_id_fk",
+ "tableFrom": "task_runs",
+ "tableTo": "tasks",
+ "columnsFrom": ["task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "task_runs_source_run_id_task_runs_id_fk": {
+ "name": "task_runs_source_run_id_task_runs_id_fk",
+ "tableFrom": "task_runs",
+ "tableTo": "task_runs",
+ "columnsFrom": ["source_run_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "task_runs_acting_user_id_users_id_fk": {
+ "name": "task_runs_acting_user_id_users_id_fk",
+ "tableFrom": "task_runs",
+ "tableTo": "users",
+ "columnsFrom": ["acting_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "task_runs_kind_check": {
+ "name": "task_runs_kind_check",
+ "value": "\"task_runs\".\"kind\" in ('fresh', 'resume')"
+ },
+ "task_runs_harness_check": {
+ "name": "task_runs_harness_check",
+ "value": "\"task_runs\".\"harness\" in ('opencode-server')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.task_slack_reply_details": {
+ "name": "task_slack_reply_details",
+ "schema": "",
+ "columns": {
+ "detail_id": {
+ "name": "detail_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "summary": {
+ "name": "summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "findings": {
+ "name": "findings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "task_slack_reply_details_task_id_idx": {
+ "name": "task_slack_reply_details_task_id_idx",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_slack_reply_details_deployment_task_detail_unique": {
+ "name": "task_slack_reply_details_deployment_task_detail_unique",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "detail_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "task_slack_reply_details_task_id_tasks_id_fk": {
+ "name": "task_slack_reply_details_task_id_tasks_id_fk",
+ "tableFrom": "task_slack_reply_details",
+ "tableTo": "tasks",
+ "columnsFrom": ["task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.task_start_parallel_counts": {
+ "name": "task_start_parallel_counts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payload_kind": {
+ "name": "payload_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parallel_count": {
+ "name": "parallel_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "activity_window_seconds": {
+ "name": "activity_window_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ended_at": {
+ "name": "ended_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "task_start_parallel_counts_run_id_unique": {
+ "name": "task_start_parallel_counts_run_id_unique",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_start_parallel_counts_task_id_started_at_idx": {
+ "name": "task_start_parallel_counts_task_id_started_at_idx",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "task_start_parallel_counts_started_at_idx": {
+ "name": "task_start_parallel_counts_started_at_idx",
+ "columns": [
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "task_start_parallel_counts_task_id_tasks_id_fk": {
+ "name": "task_start_parallel_counts_task_id_tasks_id_fk",
+ "tableFrom": "task_start_parallel_counts",
+ "tableTo": "tasks",
+ "columnsFrom": ["task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "task_start_parallel_counts_run_id_task_runs_id_fk": {
+ "name": "task_start_parallel_counts_run_id_task_runs_id_fk",
+ "tableFrom": "task_start_parallel_counts",
+ "tableTo": "task_runs",
+ "columnsFrom": ["run_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tasks": {
+ "name": "tasks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow": {
+ "name": "workflow",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "surface": {
+ "name": "surface",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "trigger": {
+ "name": "trigger",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "visibility": {
+ "name": "visibility",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'visible'"
+ },
+ "state": {
+ "name": "state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "initiator_kind": {
+ "name": "initiator_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "initiator_user_id": {
+ "name": "initiator_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "initiator_automation": {
+ "name": "initiator_automation",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_external_id": {
+ "name": "actor_external_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_display_name": {
+ "name": "actor_display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "commit_author_kind": {
+ "name": "commit_author_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "commit_author_user_id": {
+ "name": "commit_author_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "commit_author_login": {
+ "name": "commit_author_login",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "commit_author_external_id": {
+ "name": "commit_author_external_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pr_assignee_login": {
+ "name": "pr_assignee_login",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "slack_channel_id": {
+ "name": "slack_channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "slack_thread_ts": {
+ "name": "slack_thread_ts",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "linear_session_id": {
+ "name": "linear_session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "linear_issue_id": {
+ "name": "linear_issue_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "linear_organization_id": {
+ "name": "linear_organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "harness": {
+ "name": "harness",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'opencode-server'"
+ },
+ "harness_session_id": {
+ "name": "harness_session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "model_provider": {
+ "name": "model_provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title_edited_by_user_at": {
+ "name": "title_edited_by_user_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "llm_title_checkpoint": {
+ "name": "llm_title_checkpoint",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "mode": {
+ "name": "mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "prompt": {
+ "name": "prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "goal_objective": {
+ "name": "goal_objective",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "goal_status": {
+ "name": "goal_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "goal_max_continuations": {
+ "name": "goal_max_continuations",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "goal_continuations_used": {
+ "name": "goal_continuations_used",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "goal_blocked_reason": {
+ "name": "goal_blocked_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "goal_completed_at": {
+ "name": "goal_completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "goal_last_continuation_id": {
+ "name": "goal_last_continuation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "goal_continuation_ids": {
+ "name": "goal_continuation_ids",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::text[]"
+ },
+ "goal_generation_ids": {
+ "name": "goal_generation_ids",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::text[]"
+ },
+ "goal_blocker_candidate_reason": {
+ "name": "goal_blocker_candidate_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "goal_blocker_candidate_count": {
+ "name": "goal_blocker_candidate_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "goal_blocker_last_continuation_used": {
+ "name": "goal_blocker_last_continuation_used",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "draft_prompt": {
+ "name": "draft_prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "requested_work_kind": {
+ "name": "requested_work_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'unknown'"
+ },
+ "requested_work_kind_source": {
+ "name": "requested_work_kind_source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'system_default'"
+ },
+ "requested_work_kind_confidence": {
+ "name": "requested_work_kind_confidence",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "harness_instructions": {
+ "name": "harness_instructions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "compute_duration_ms": {
+ "name": "compute_duration_ms",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "activity_at": {
+ "name": "activity_at",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "repository_url": {
+ "name": "repository_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "repository_name": {
+ "name": "repository_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "default_branch": {
+ "name": "default_branch",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "tasks_initiator_user_id_idx": {
+ "name": "tasks_initiator_user_id_idx",
+ "columns": [
+ {
+ "expression": "initiator_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tasks_initiator_automation_idx": {
+ "name": "tasks_initiator_automation_idx",
+ "columns": [
+ {
+ "expression": "initiator_automation",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tasks_workflow_idx": {
+ "name": "tasks_workflow_idx",
+ "columns": [
+ {
+ "expression": "workflow",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tasks_visibility_activity_at_idx": {
+ "name": "tasks_visibility_activity_at_idx",
+ "columns": [
+ {
+ "expression": "visibility",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "activity_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tasks_harness_session_id_idx": {
+ "name": "tasks_harness_session_id_idx",
+ "columns": [
+ {
+ "expression": "harness_session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tasks_timestamp_idx": {
+ "name": "tasks_timestamp_idx",
+ "columns": [
+ {
+ "expression": "timestamp",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tasks_deployment_activity_at_idx": {
+ "name": "tasks_deployment_activity_at_idx",
+ "columns": [
+ {
+ "expression": "activity_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tasks_created_at_idx": {
+ "name": "tasks_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tasks_initiator_user_id_users_id_fk": {
+ "name": "tasks_initiator_user_id_users_id_fk",
+ "tableFrom": "tasks",
+ "tableTo": "users",
+ "columnsFrom": ["initiator_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "tasks_initiator_automation_automations_key_fk": {
+ "name": "tasks_initiator_automation_automations_key_fk",
+ "tableFrom": "tasks",
+ "tableTo": "automations",
+ "columnsFrom": ["initiator_automation"],
+ "columnsTo": ["key"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "tasks_commit_author_user_id_users_id_fk": {
+ "name": "tasks_commit_author_user_id_users_id_fk",
+ "tableFrom": "tasks",
+ "tableTo": "users",
+ "columnsFrom": ["commit_author_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "tasks_initiator_shape_check": {
+ "name": "tasks_initiator_shape_check",
+ "value": "(\"tasks\".\"initiator_kind\" = 'user' AND \"tasks\".\"initiator_automation\" IS NULL AND (\"tasks\".\"initiator_user_id\" IS NOT NULL OR \"tasks\".\"actor_external_id\" IS NOT NULL)) OR (\"tasks\".\"initiator_kind\" = 'automation' AND \"tasks\".\"initiator_automation\" IS NOT NULL AND \"tasks\".\"initiator_user_id\" IS NULL)"
+ },
+ "tasks_workflow_check": {
+ "name": "tasks_workflow_check",
+ "value": "\"tasks\".\"workflow\" in ('standard', 'pr_review', 'pr_conflict_resolve', 'scan', 'mcp_recommendations', 'setup_onboarding', 'env_snapshot', 'eval')"
+ },
+ "tasks_surface_check": {
+ "name": "tasks_surface_check",
+ "value": "\"tasks\".\"surface\" in ('web', 'api', 'slack', 'teams', 'telegram', 'discord', 'linear', 'github', 'gitlab', 'gitea', 'ado', 'bitbucket', 'system')"
+ },
+ "tasks_trigger_check": {
+ "name": "tasks_trigger_check",
+ "value": "\"tasks\".\"trigger\" in ('message', 'webhook', 'schedule', 'manual')"
+ },
+ "tasks_visibility_check": {
+ "name": "tasks_visibility_check",
+ "value": "\"tasks\".\"visibility\" in ('visible', 'hidden')"
+ },
+ "tasks_state_check": {
+ "name": "tasks_state_check",
+ "value": "\"tasks\".\"state\" in ('active', 'completed', 'failed', 'canceled')"
+ },
+ "tasks_goal_status_check": {
+ "name": "tasks_goal_status_check",
+ "value": "\"tasks\".\"goal_status\" IS NULL OR \"tasks\".\"goal_status\" in ('active', 'complete', 'blocked', 'budget_limited')"
+ },
+ "tasks_goal_continuations_check": {
+ "name": "tasks_goal_continuations_check",
+ "value": "\"tasks\".\"goal_continuations_used\" >= 0 AND (\"tasks\".\"goal_max_continuations\" IS NULL OR \"tasks\".\"goal_max_continuations\" > 0)"
+ },
+ "tasks_goal_blocker_candidate_count_check": {
+ "name": "tasks_goal_blocker_candidate_count_check",
+ "value": "\"tasks\".\"goal_blocker_candidate_count\" >= 0"
+ },
+ "tasks_harness_check": {
+ "name": "tasks_harness_check",
+ "value": "\"tasks\".\"harness\" in ('opencode-server')"
+ },
+ "tasks_requested_work_kind_check": {
+ "name": "tasks_requested_work_kind_check",
+ "value": "\"tasks\".\"requested_work_kind\" in ('question', 'plan', 'implement', 'unknown')"
+ },
+ "tasks_requested_work_kind_source_check": {
+ "name": "tasks_requested_work_kind_source_check",
+ "value": "\"tasks\".\"requested_work_kind_source\" in ('explicit_bootstrap', 'task_tool', 'llm_classifier', 'inherited', 'system_default')"
+ },
+ "tasks_commit_author_kind_check": {
+ "name": "tasks_commit_author_kind_check",
+ "value": "\"tasks\".\"commit_author_kind\" IS NULL OR \"tasks\".\"commit_author_kind\" in ('roomote', 'user', 'external')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.teams_installations": {
+ "name": "teams_installations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "installation_key": {
+ "name": "installation_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tenant_id": {
+ "name": "tenant_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "team_id": {
+ "name": "team_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "team_name": {
+ "name": "team_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "channel_id": {
+ "name": "channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "channel_name": {
+ "name": "channel_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "conversation_id": {
+ "name": "conversation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "conversation_type": {
+ "name": "conversation_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "bot_app_id": {
+ "name": "bot_app_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "bot_user_id": {
+ "name": "bot_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "bot_name": {
+ "name": "bot_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "service_url": {
+ "name": "service_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "last_activity_at": {
+ "name": "last_activity_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "teams_installations_tenant_id_idx": {
+ "name": "teams_installations_tenant_id_idx",
+ "columns": [
+ {
+ "expression": "tenant_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "teams_installations_team_id_idx": {
+ "name": "teams_installations_team_id_idx",
+ "columns": [
+ {
+ "expression": "team_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "teams_installations_conversation_id_idx": {
+ "name": "teams_installations_conversation_id_idx",
+ "columns": [
+ {
+ "expression": "conversation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "teams_installations_active_idx": {
+ "name": "teams_installations_active_idx",
+ "columns": [
+ {
+ "expression": "is_active",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "teams_installations_installation_key_unique": {
+ "name": "teams_installations_installation_key_unique",
+ "nullsNotDistinct": false,
+ "columns": ["installation_key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.teams_user_mappings": {
+ "name": "teams_user_mappings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "teams_user_id": {
+ "name": "teams_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "teams_tenant_id": {
+ "name": "teams_tenant_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "teams_aad_object_id": {
+ "name": "teams_aad_object_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "teams_user_mappings_aad_object_idx": {
+ "name": "teams_user_mappings_aad_object_idx",
+ "columns": [
+ {
+ "expression": "teams_aad_object_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "teams_tenant_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "teams_user_mappings_user_id_idx": {
+ "name": "teams_user_mappings_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "teams_user_mappings_user_id_users_id_fk": {
+ "name": "teams_user_mappings_user_id_users_id_fk",
+ "tableFrom": "teams_user_mappings",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "teams_user_mappings_unique": {
+ "name": "teams_user_mappings_unique",
+ "nullsNotDistinct": false,
+ "columns": ["teams_user_id", "teams_tenant_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.telegram_user_mappings": {
+ "name": "telegram_user_mappings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "telegram_user_id": {
+ "name": "telegram_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "telegram_chat_id": {
+ "name": "telegram_chat_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "telegram_username": {
+ "name": "telegram_username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "telegram_user_mappings_user_id_idx": {
+ "name": "telegram_user_mappings_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "telegram_user_mappings_user_id_users_id_fk": {
+ "name": "telegram_user_mappings_user_id_users_id_fk",
+ "tableFrom": "telegram_user_mappings",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "telegram_user_mappings_unique": {
+ "name": "telegram_user_mappings_unique",
+ "nullsNotDistinct": false,
+ "columns": ["telegram_user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tracked_messages": {
+ "name": "tracked_messages",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "surface": {
+ "name": "surface",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dedupe_key": {
+ "name": "dedupe_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "channel_id": {
+ "name": "channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "message_ts": {
+ "name": "message_ts",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "thread_ts": {
+ "name": "thread_ts",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "work_item_id": {
+ "name": "work_item_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "automation_key": {
+ "name": "automation_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "summary_text": {
+ "name": "summary_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "posted_at": {
+ "name": "posted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "dismissed_at": {
+ "name": "dismissed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "tracked_messages_kind_dedupe_key_unique": {
+ "name": "tracked_messages_kind_dedupe_key_unique",
+ "columns": [
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "dedupe_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tracked_messages_work_item_id_idx": {
+ "name": "tracked_messages_work_item_id_idx",
+ "columns": [
+ {
+ "expression": "work_item_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tracked_messages_channel_message_idx": {
+ "name": "tracked_messages_channel_message_idx",
+ "columns": [
+ {
+ "expression": "channel_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "message_ts",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tracked_messages_automation_channel_posted_idx": {
+ "name": "tracked_messages_automation_channel_posted_idx",
+ "columns": [
+ {
+ "expression": "automation_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "channel_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "posted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tracked_messages_work_item_id_work_items_id_fk": {
+ "name": "tracked_messages_work_item_id_work_items_id_fk",
+ "tableFrom": "tracked_messages",
+ "tableTo": "work_items",
+ "columnsFrom": ["work_item_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "tracked_messages_automation_key_automations_key_fk": {
+ "name": "tracked_messages_automation_key_automations_key_fk",
+ "tableFrom": "tracked_messages",
+ "tableTo": "automations",
+ "columnsFrom": ["automation_key"],
+ "columnsTo": ["key"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "tracked_messages_created_by_user_id_users_id_fk": {
+ "name": "tracked_messages_created_by_user_id_users_id_fk",
+ "tableFrom": "tracked_messages",
+ "tableTo": "users",
+ "columnsFrom": ["created_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_api_keys": {
+ "name": "user_api_keys",
+ "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": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "api_key": {
+ "name": "api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "user_api_keys_user_id_idx": {
+ "name": "user_api_keys_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_api_keys_user_deployment_provider_unique": {
+ "name": "user_api_keys_user_deployment_provider_unique",
+ "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": {
+ "user_api_keys_user_id_users_id_fk": {
+ "name": "user_api_keys_user_id_users_id_fk",
+ "tableFrom": "user_api_keys",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "image_url": {
+ "name": "image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity": {
+ "name": "entity",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'member'"
+ },
+ "analytics_id": {
+ "name": "analytics_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cookie_consented_at": {
+ "name": "cookie_consented_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "onboarding_completed_at": {
+ "name": "onboarding_completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "invited_by_invite_id": {
+ "name": "invited_by_invite_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_sync_at": {
+ "name": "last_sync_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "users_email_idx": {
+ "name": "users_email_idx",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_created_at_idx": {
+ "name": "users_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_analytics_id_unique_idx": {
+ "name": "users_analytics_id_unique_idx",
+ "columns": [
+ {
+ "expression": "analytics_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.webhooks": {
+ "name": "webhooks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "delivery_id": {
+ "name": "delivery_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event": {
+ "name": "event",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "succeeded_at": {
+ "name": "succeeded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "failed_at": {
+ "name": "failed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "webhooks_provider_delivery_id_unique": {
+ "name": "webhooks_provider_delivery_id_unique",
+ "columns": [
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "delivery_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "webhooks_event_idx": {
+ "name": "webhooks_event_idx",
+ "columns": [
+ {
+ "expression": "event",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "webhooks_created_at_idx": {
+ "name": "webhooks_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "webhooks_status_exclusive": {
+ "name": "webhooks_status_exclusive",
+ "value": "(\n (succeeded_at IS NOT NULL)::int +\n (failed_at IS NOT NULL)::int\n ) <= 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.work_items": {
+ "name": "work_items",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "automation_key": {
+ "name": "automation_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_task_id": {
+ "name": "source_task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "selected_by_user_id": {
+ "name": "selected_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_work_item_id": {
+ "name": "source_work_item_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "brief": {
+ "name": "brief",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "execution_prompt": {
+ "name": "execution_prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "investigation_context": {
+ "name": "investigation_context",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "category": {
+ "name": "category",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "priority": {
+ "name": "priority",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action_kind": {
+ "name": "action_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "disposition": {
+ "name": "disposition",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "repository_ids": {
+ "name": "repository_ids",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "target_repository_full_name": {
+ "name": "target_repository_full_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_environment_id": {
+ "name": "target_environment_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workspace_readiness": {
+ "name": "workspace_readiness",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "readiness_message": {
+ "name": "readiness_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "fingerprint": {
+ "name": "fingerprint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'open'"
+ },
+ "launch_claimed_at": {
+ "name": "launch_claimed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "launched_task_id": {
+ "name": "launched_task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "launched_at": {
+ "name": "launched_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "failed_at": {
+ "name": "failed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "launch_error": {
+ "name": "launch_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dismissed_at": {
+ "name": "dismissed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "work_items_source_task_idx": {
+ "name": "work_items_source_task_idx",
+ "columns": [
+ {
+ "expression": "source_task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "work_items_kind_status_idx": {
+ "name": "work_items_kind_status_idx",
+ "columns": [
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "work_items_automation_key_fingerprint_idx": {
+ "name": "work_items_automation_key_fingerprint_idx",
+ "columns": [
+ {
+ "expression": "automation_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "fingerprint",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "work_items_fingerprint_idx": {
+ "name": "work_items_fingerprint_idx",
+ "columns": [
+ {
+ "expression": "fingerprint",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "work_items_launched_task_id_idx": {
+ "name": "work_items_launched_task_id_idx",
+ "columns": [
+ {
+ "expression": "launched_task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "work_items_source_task_kind_sort_order_unique": {
+ "name": "work_items_source_task_kind_sort_order_unique",
+ "columns": [
+ {
+ "expression": "source_task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "work_items_automation_key_automations_key_fk": {
+ "name": "work_items_automation_key_automations_key_fk",
+ "tableFrom": "work_items",
+ "tableTo": "automations",
+ "columnsFrom": ["automation_key"],
+ "columnsTo": ["key"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "work_items_source_task_id_tasks_id_fk": {
+ "name": "work_items_source_task_id_tasks_id_fk",
+ "tableFrom": "work_items",
+ "tableTo": "tasks",
+ "columnsFrom": ["source_task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "work_items_selected_by_user_id_users_id_fk": {
+ "name": "work_items_selected_by_user_id_users_id_fk",
+ "tableFrom": "work_items",
+ "tableTo": "users",
+ "columnsFrom": ["selected_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "work_items_source_work_item_id_work_items_id_fk": {
+ "name": "work_items_source_work_item_id_work_items_id_fk",
+ "tableFrom": "work_items",
+ "tableTo": "work_items",
+ "columnsFrom": ["source_work_item_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "work_items_target_environment_id_environments_id_fk": {
+ "name": "work_items_target_environment_id_environments_id_fk",
+ "tableFrom": "work_items",
+ "tableTo": "environments",
+ "columnsFrom": ["target_environment_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "work_items_launched_task_id_tasks_id_fk": {
+ "name": "work_items_launched_task_id_tasks_id_fk",
+ "tableFrom": "work_items",
+ "tableTo": "tasks",
+ "columnsFrom": ["launched_task_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json
index 0e44b199a..3a228ef2b 100644
--- a/packages/db/drizzle/meta/_journal.json
+++ b/packages/db/drizzle/meta/_journal.json
@@ -428,6 +428,13 @@
"when": 1787765765044,
"tag": "0060_organic_harrier",
"breakpoints": true
+ },
+ {
+ "idx": 61,
+ "version": "7",
+ "when": 1787772010337,
+ "tag": "0061_careful_lily_hollister",
+ "breakpoints": true
}
]
}
From f82c6ff8bbbd7471a527327cd6c11f9701ec6ad0 Mon Sep 17 00:00:00 2001
From: Roomote
Date: Thu, 27 Aug 2026 09:35:50 +0000
Subject: [PATCH 05/26] fix: avoid Redis dependency in session flag checks
---
.../__tests__/sessions-reconcile.test.ts | 5 -----
.../src/scheduled-jobs/sessions-reconcile.ts | 9 ++------
.../src/server/__tests__/enqueue-task.test.ts | 4 ----
.../fast-agent-conversation-repository.ts | 10 ++-------
.../server/fast-agent/fast-agent-service.ts | 11 ++++------
.../cloud-agents/src/server/task-run-queue.ts | 8 +++----
.../feature-flags/src/server/deployment.ts | 21 +++++++++++++++++++
packages/feature-flags/src/server/index.ts | 1 +
.../src/fast-agent-live-task-launcher.ts | 6 ++----
9 files changed, 36 insertions(+), 39 deletions(-)
create mode 100644 packages/feature-flags/src/server/deployment.ts
diff --git a/apps/bullmq/src/scheduled-jobs/__tests__/sessions-reconcile.test.ts b/apps/bullmq/src/scheduled-jobs/__tests__/sessions-reconcile.test.ts
index 76438cd00..ccdc8aab2 100644
--- a/apps/bullmq/src/scheduled-jobs/__tests__/sessions-reconcile.test.ts
+++ b/apps/bullmq/src/scheduled-jobs/__tests__/sessions-reconcile.test.ts
@@ -8,9 +8,6 @@ import {
taskFactory,
userFactory,
} from '@roomote/db/server';
-import { getFeatureFlagEvaluator } from '@roomote/feature-flags/server';
-
-import { getRedis } from '../../redis';
import { sessionsReconcileJob } from '../sessions-reconcile';
describe('sessionsReconcileJob', () => {
@@ -22,7 +19,6 @@ describe('sessionsReconcileJob', () => {
target: deploymentSettings.id,
set: { metadata: { sessions_data: true } },
});
- await getFeatureFlagEvaluator(getRedis()).invalidateDeploymentCache();
});
afterEach(async () => {
@@ -30,7 +26,6 @@ describe('sessionsReconcileJob', () => {
.update(deploymentSettings)
.set({ metadata: {} })
.where(eq(deploymentSettings.id, 'default'));
- await getFeatureFlagEvaluator(getRedis()).invalidateDeploymentCache();
});
it('backfills Fast conversations and visible tasks idempotently', async () => {
diff --git a/apps/bullmq/src/scheduled-jobs/sessions-reconcile.ts b/apps/bullmq/src/scheduled-jobs/sessions-reconcile.ts
index 3f40866f9..470217c30 100644
--- a/apps/bullmq/src/scheduled-jobs/sessions-reconcile.ts
+++ b/apps/bullmq/src/scheduled-jobs/sessions-reconcile.ts
@@ -18,12 +18,10 @@ import {
touchSessionActivity,
} from '@roomote/db/server';
import {
+ evaluateDeploymentFeatureFlag,
FeatureFlag,
- getFeatureFlagEvaluator,
} from '@roomote/feature-flags/server';
-import { getRedis } from '../redis';
-
const LOG_PREFIX = '[sessions]';
const BACKFILL_KEY = 'unified-sessions-v1';
const BATCH_SIZE = 100;
@@ -214,10 +212,7 @@ async function reconcileRecentSessions(): Promise {
}
export async function sessionsReconcileJob(): Promise {
- const enabled = await getFeatureFlagEvaluator(getRedis()).evaluate(
- FeatureFlag.SessionsData,
- { isDeploymentContext: true },
- );
+ const enabled = await evaluateDeploymentFeatureFlag(FeatureFlag.SessionsData);
if (!enabled) return;
const state = await db.query.sessionBackfillState.findFirst({
diff --git a/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts b/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts
index a81f38008..a28e5ac20 100644
--- a/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts
+++ b/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts
@@ -44,8 +44,6 @@ import {
environmentFactory,
repositoryFactory,
} from '@roomote/db/server';
-import { getFeatureFlagEvaluator } from '@roomote/feature-flags/server';
-import { getRedis } from '@roomote/redis';
import {
TaskRunQueue,
@@ -921,7 +919,6 @@ describe('enqueueTask Session linkage', () => {
target: deploymentSettings.id,
set: { metadata: { sessions_data: true } },
});
- await getFeatureFlagEvaluator(getRedis()).invalidateDeploymentCache();
});
afterEach(async () => {
@@ -929,7 +926,6 @@ describe('enqueueTask Session linkage', () => {
.update(deploymentSettings)
.set({ metadata: {} })
.where(eq(deploymentSettings.id, 'default'));
- await getFeatureFlagEvaluator(getRedis()).invalidateDeploymentCache();
});
it('creates exactly one Session link for a visible fresh task', async () => {
diff --git a/packages/cloud-agents/src/server/fast-agent/fast-agent-conversation-repository.ts b/packages/cloud-agents/src/server/fast-agent/fast-agent-conversation-repository.ts
index bef16b9c1..1d4fa192e 100644
--- a/packages/cloud-agents/src/server/fast-agent/fast-agent-conversation-repository.ts
+++ b/packages/cloud-agents/src/server/fast-agent/fast-agent-conversation-repository.ts
@@ -15,10 +15,9 @@ import {
type DatabaseOrTransaction,
} from '@roomote/db/server';
import {
+ evaluateDeploymentFeatureFlag,
FeatureFlag,
- getFeatureFlagEvaluator,
} from '@roomote/feature-flags/server';
-import { getRedis } from '@roomote/redis';
import { fastAgentConversationSchema } from '@roomote/types';
import type { FastAgentConversation } from './fast-agent-conversation';
@@ -67,12 +66,7 @@ export interface FastAgentConversationRepository {
}
async function sessionsDataEnabled(): Promise {
- return getFeatureFlagEvaluator(getRedis()).evaluate(
- FeatureFlag.SessionsData,
- {
- isDeploymentContext: true,
- },
- );
+ return evaluateDeploymentFeatureFlag(FeatureFlag.SessionsData);
}
function buildIdentityKey(conversation: FastAgentConversation): string {
diff --git a/packages/cloud-agents/src/server/fast-agent/fast-agent-service.ts b/packages/cloud-agents/src/server/fast-agent/fast-agent-service.ts
index 5522bf3b5..edf295b45 100644
--- a/packages/cloud-agents/src/server/fast-agent/fast-agent-service.ts
+++ b/packages/cloud-agents/src/server/fast-agent/fast-agent-service.ts
@@ -28,10 +28,9 @@ import {
touchSessionActivity,
} from '@roomote/db/server';
import {
+ evaluateDeploymentFeatureFlag,
FeatureFlag,
- getFeatureFlagEvaluator,
} from '@roomote/feature-flags/server';
-import { getRedis } from '@roomote/redis';
import { Env } from '@roomote/env';
import { z } from 'zod';
@@ -1478,11 +1477,9 @@ export async function answerFastAgentQuestion({
let linkedSession: Awaited> =
null;
try {
- sessionCommsEnabled = await getFeatureFlagEvaluator(
- getRedis(),
- ).evaluate(FeatureFlag.SessionsComms, {
- isDeploymentContext: true,
- });
+ sessionCommsEnabled = await evaluateDeploymentFeatureFlag(
+ FeatureFlag.SessionsComms,
+ );
linkedSession = sessionCommsEnabled
? await getSessionForTask(db, task.taskId)
: null;
diff --git a/packages/cloud-agents/src/server/task-run-queue.ts b/packages/cloud-agents/src/server/task-run-queue.ts
index 63b94befb..5466f6fa7 100644
--- a/packages/cloud-agents/src/server/task-run-queue.ts
+++ b/packages/cloud-agents/src/server/task-run-queue.ts
@@ -73,8 +73,8 @@ import {
sql,
} from '@roomote/db/server';
import {
+ evaluateDeploymentFeatureFlag,
FeatureFlag,
- getFeatureFlagEvaluator,
} from '@roomote/feature-flags/server';
import { type Redis, getRedis } from '@roomote/redis';
import {
@@ -1390,9 +1390,9 @@ async function enqueueFreshLaunch(
const { task, initiator, workflow, surface, trigger } = input;
const visibility: TaskVisibility = input.visibility ?? 'visible';
const linkedUserId = getTaskInitiatorLinkedUserId(initiator);
- const sessionsDataEnabled = await getFeatureFlagEvaluator(
- getRedis(),
- ).evaluate(FeatureFlag.SessionsData, { isDeploymentContext: true });
+ const sessionsDataEnabled = await evaluateDeploymentFeatureFlag(
+ FeatureFlag.SessionsData,
+ );
await assertUserIsNotDeleted(linkedUserId);
diff --git a/packages/feature-flags/src/server/deployment.ts b/packages/feature-flags/src/server/deployment.ts
new file mode 100644
index 000000000..668e82b67
--- /dev/null
+++ b/packages/feature-flags/src/server/deployment.ts
@@ -0,0 +1,21 @@
+import { db, deploymentSettings, eq } from '@roomote/db/server';
+
+import { evaluateFeatureFlagFromMetadata } from '../index';
+import type { FeatureFlag } from '../types';
+
+const DEFAULT_DEPLOYMENT_ID = 'default';
+
+/**
+ * Evaluates a deployment-wide flag without requiring Redis. Runtime write
+ * paths use this when cache availability must not gate task or Session writes.
+ */
+export async function evaluateDeploymentFeatureFlag(
+ flag: FeatureFlag,
+): Promise {
+ const deployment = await db.query.deploymentSettings.findFirst({
+ where: eq(deploymentSettings.id, DEFAULT_DEPLOYMENT_ID),
+ columns: { metadata: true },
+ });
+
+ return evaluateFeatureFlagFromMetadata(flag, deployment?.metadata);
+}
diff --git a/packages/feature-flags/src/server/index.ts b/packages/feature-flags/src/server/index.ts
index d17f03f23..e51415ce2 100644
--- a/packages/feature-flags/src/server/index.ts
+++ b/packages/feature-flags/src/server/index.ts
@@ -4,4 +4,5 @@ export {
resetFeatureFlagEvaluatorForTests,
} from '../evaluator';
export { MetadataCache } from '../cache';
+export { evaluateDeploymentFeatureFlag } from './deployment';
export * from '../index';
diff --git a/packages/slack/src/fast-agent-live-task-launcher.ts b/packages/slack/src/fast-agent-live-task-launcher.ts
index e767da0b0..f23431910 100644
--- a/packages/slack/src/fast-agent-live-task-launcher.ts
+++ b/packages/slack/src/fast-agent-live-task-launcher.ts
@@ -7,10 +7,9 @@ import { RunStatus } from '@roomote/types';
import { Env } from '@roomote/env';
import { db, getSessionForTask } from '@roomote/db/server';
import {
+ evaluateDeploymentFeatureFlag,
FeatureFlag,
- getFeatureFlagEvaluator,
} from '@roomote/feature-flags/server';
-import { getRedis } from '@roomote/redis';
import {
buildSlackLiveTaskCardBlocks,
SLACK_LIVE_TASK_CARD_MESSAGES,
@@ -89,9 +88,8 @@ export function createFastAgentSlackLiveTaskLauncher(
let destinationUrl = context.taskUrl;
try {
- sessionMode = await getFeatureFlagEvaluator(getRedis()).evaluate(
+ sessionMode = await evaluateDeploymentFeatureFlag(
FeatureFlag.SessionsComms,
- { isDeploymentContext: true },
);
const linkedSession = sessionMode
? await getSessionForTask(db, taskRun.taskId)
From 4cc33a3294fcbcc5efe4b9c4a0ba05774f66f0a5 Mon Sep 17 00:00:00 2001
From: Roomote
Date: Thu, 27 Aug 2026 10:04:35 +0000
Subject: [PATCH 06/26] fix: cache deployment feature flag metadata
---
.../src/server/__tests__/enqueue-task.test.ts | 3 +
packages/feature-flags/src/evaluator.ts | 3 +
.../src/server/deployment.test.ts | 85 +++++++++++++++++++
.../feature-flags/src/server/deployment.ts | 57 +++++++++++--
packages/feature-flags/src/server/index.ts | 5 +-
5 files changed, 146 insertions(+), 7 deletions(-)
create mode 100644 packages/feature-flags/src/server/deployment.test.ts
diff --git a/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts b/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts
index a28e5ac20..b2b0a0ba1 100644
--- a/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts
+++ b/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts
@@ -4,6 +4,7 @@
// stamping, resume semantics, enqueue-time PR linkage, and pr_review queue
// scope dedup.
import Redis from 'ioredis-mock';
+import { invalidateDeploymentFeatureFlagCache } from '@roomote/feature-flags/server';
const { mockGenerateLlmTaskTitle } = vi.hoisted(() => ({
mockGenerateLlmTaskTitle: vi.fn().mockResolvedValue('Generated title'),
@@ -919,6 +920,7 @@ describe('enqueueTask Session linkage', () => {
target: deploymentSettings.id,
set: { metadata: { sessions_data: true } },
});
+ invalidateDeploymentFeatureFlagCache();
});
afterEach(async () => {
@@ -926,6 +928,7 @@ describe('enqueueTask Session linkage', () => {
.update(deploymentSettings)
.set({ metadata: {} })
.where(eq(deploymentSettings.id, 'default'));
+ invalidateDeploymentFeatureFlagCache();
});
it('creates exactly one Session link for a visible fresh task', async () => {
diff --git a/packages/feature-flags/src/evaluator.ts b/packages/feature-flags/src/evaluator.ts
index 919641c6f..fa4fb6ac9 100644
--- a/packages/feature-flags/src/evaluator.ts
+++ b/packages/feature-flags/src/evaluator.ts
@@ -13,6 +13,7 @@ import {
normalizeMetadataRecord,
} from './index';
import { MetadataCache } from './cache';
+import { invalidateDeploymentFeatureFlagCache } from './server/deployment';
import type {
FeatureFlag,
FeatureFlagContext,
@@ -72,6 +73,7 @@ export class FeatureFlagEvaluator {
}
async invalidateDeploymentCache(): Promise {
+ invalidateDeploymentFeatureFlagCache();
await this.cache.invalidate('deployment', 'default');
}
}
@@ -85,4 +87,5 @@ export function getFeatureFlagEvaluator(redis: Redis): FeatureFlagEvaluator {
export function resetFeatureFlagEvaluatorForTests(): void {
evaluatorInstance = null;
+ invalidateDeploymentFeatureFlagCache();
}
diff --git a/packages/feature-flags/src/server/deployment.test.ts b/packages/feature-flags/src/server/deployment.test.ts
new file mode 100644
index 000000000..c0b15e8d3
--- /dev/null
+++ b/packages/feature-flags/src/server/deployment.test.ts
@@ -0,0 +1,85 @@
+const { findDeploymentSettings } = vi.hoisted(() => ({
+ findDeploymentSettings: vi.fn(),
+}));
+
+vi.mock('@roomote/db/server', () => ({
+ db: {
+ query: {
+ deploymentSettings: { findFirst: findDeploymentSettings },
+ },
+ },
+ deploymentSettings: { id: 'id' },
+ eq: vi.fn(),
+}));
+
+describe('evaluateDeploymentFeatureFlag', () => {
+ beforeEach(() => {
+ vi.resetModules();
+ vi.useFakeTimers();
+ findDeploymentSettings.mockReset();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('reuses deployment metadata until the bounded cache expires', async () => {
+ findDeploymentSettings
+ .mockResolvedValueOnce({ metadata: { sessions_data: true } })
+ .mockResolvedValueOnce({ metadata: { sessions_data: false } });
+
+ const { evaluateDeploymentFeatureFlag } = await import('./deployment');
+
+ await expect(evaluateDeploymentFeatureFlag('sessions_data')).resolves.toBe(
+ true,
+ );
+ await expect(evaluateDeploymentFeatureFlag('sessions_data')).resolves.toBe(
+ true,
+ );
+ expect(findDeploymentSettings).toHaveBeenCalledTimes(1);
+
+ vi.advanceTimersByTime(30_001);
+
+ await expect(evaluateDeploymentFeatureFlag('sessions_data')).resolves.toBe(
+ false,
+ );
+ expect(findDeploymentSettings).toHaveBeenCalledTimes(2);
+ });
+
+ it('coalesces concurrent metadata reads', async () => {
+ findDeploymentSettings.mockResolvedValue({
+ metadata: { sessions_data: true, sessions_comms: true },
+ });
+
+ const { evaluateDeploymentFeatureFlag } = await import('./deployment');
+
+ await expect(
+ Promise.all([
+ evaluateDeploymentFeatureFlag('sessions_data'),
+ evaluateDeploymentFeatureFlag('sessions_comms'),
+ ]),
+ ).resolves.toEqual([true, true]);
+ expect(findDeploymentSettings).toHaveBeenCalledTimes(1);
+ });
+
+ it('refreshes immediately after explicit invalidation', async () => {
+ findDeploymentSettings
+ .mockResolvedValueOnce({ metadata: { sessions_data: false } })
+ .mockResolvedValueOnce({ metadata: { sessions_data: true } });
+
+ const {
+ evaluateDeploymentFeatureFlag,
+ invalidateDeploymentFeatureFlagCache,
+ } = await import('./deployment');
+
+ await expect(evaluateDeploymentFeatureFlag('sessions_data')).resolves.toBe(
+ false,
+ );
+ invalidateDeploymentFeatureFlagCache();
+ await expect(evaluateDeploymentFeatureFlag('sessions_data')).resolves.toBe(
+ true,
+ );
+
+ expect(findDeploymentSettings).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/packages/feature-flags/src/server/deployment.ts b/packages/feature-flags/src/server/deployment.ts
index 668e82b67..caf3560d5 100644
--- a/packages/feature-flags/src/server/deployment.ts
+++ b/packages/feature-flags/src/server/deployment.ts
@@ -4,6 +4,56 @@ import { evaluateFeatureFlagFromMetadata } from '../index';
import type { FeatureFlag } from '../types';
const DEFAULT_DEPLOYMENT_ID = 'default';
+const DEPLOYMENT_METADATA_CACHE_TTL_MS = 30_000;
+
+let cachedDeploymentMetadata: { value: unknown; expiresAt: number } | null =
+ null;
+let pendingDeploymentMetadata: Promise | null = null;
+let cacheGeneration = 0;
+
+export function invalidateDeploymentFeatureFlagCache(): void {
+ cachedDeploymentMetadata = null;
+ pendingDeploymentMetadata = null;
+ cacheGeneration += 1;
+}
+
+async function getDeploymentMetadata(): Promise {
+ if (
+ cachedDeploymentMetadata &&
+ cachedDeploymentMetadata.expiresAt > Date.now()
+ ) {
+ return cachedDeploymentMetadata.value;
+ }
+
+ if (pendingDeploymentMetadata) {
+ return pendingDeploymentMetadata;
+ }
+
+ const generation = cacheGeneration;
+ const request = db.query.deploymentSettings
+ .findFirst({
+ where: eq(deploymentSettings.id, DEFAULT_DEPLOYMENT_ID),
+ columns: { metadata: true },
+ })
+ .then((deployment) => {
+ const value = deployment?.metadata;
+ if (cacheGeneration === generation) {
+ cachedDeploymentMetadata = {
+ value,
+ expiresAt: Date.now() + DEPLOYMENT_METADATA_CACHE_TTL_MS,
+ };
+ }
+ return value;
+ })
+ .finally(() => {
+ if (pendingDeploymentMetadata === request) {
+ pendingDeploymentMetadata = null;
+ }
+ });
+ pendingDeploymentMetadata = request;
+
+ return request;
+}
/**
* Evaluates a deployment-wide flag without requiring Redis. Runtime write
@@ -12,10 +62,5 @@ const DEFAULT_DEPLOYMENT_ID = 'default';
export async function evaluateDeploymentFeatureFlag(
flag: FeatureFlag,
): Promise {
- const deployment = await db.query.deploymentSettings.findFirst({
- where: eq(deploymentSettings.id, DEFAULT_DEPLOYMENT_ID),
- columns: { metadata: true },
- });
-
- return evaluateFeatureFlagFromMetadata(flag, deployment?.metadata);
+ return evaluateFeatureFlagFromMetadata(flag, await getDeploymentMetadata());
}
diff --git a/packages/feature-flags/src/server/index.ts b/packages/feature-flags/src/server/index.ts
index e51415ce2..753eb31f1 100644
--- a/packages/feature-flags/src/server/index.ts
+++ b/packages/feature-flags/src/server/index.ts
@@ -4,5 +4,8 @@ export {
resetFeatureFlagEvaluatorForTests,
} from '../evaluator';
export { MetadataCache } from '../cache';
-export { evaluateDeploymentFeatureFlag } from './deployment';
+export {
+ evaluateDeploymentFeatureFlag,
+ invalidateDeploymentFeatureFlagCache,
+} from './deployment';
export * from '../index';
From d127a47ca8e0cf177087d1713f875952a3c8660c Mon Sep 17 00:00:00 2001
From: Roomote
Date: Thu, 27 Aug 2026 10:43:09 +0000
Subject: [PATCH 07/26] fix: resolve session review findings
---
.../(authenticated)/home/Home.client.test.tsx | 24 +++++++
.../web/src/app/(authenticated)/home/Home.tsx | 6 +-
.../task/[taskId]/Header.client.test.tsx | 63 +++++++++++++++++--
.../app/(sandbox)/task/[taskId]/Header.tsx | 13 ++--
apps/web/src/lib/server/sessions.test.ts | 34 ++++++++++
apps/web/src/lib/server/sessions.ts | 3 +-
.../commands/tasks/__tests__/delete.test.ts | 16 +++++
apps/web/src/trpc/commands/tasks/delete.ts | 14 +++++
.../db/src/lib/__tests__/sessions.test.ts | 22 +++++++
packages/db/src/lib/sessions.ts | 4 +-
10 files changed, 184 insertions(+), 15 deletions(-)
diff --git a/apps/web/src/app/(authenticated)/home/Home.client.test.tsx b/apps/web/src/app/(authenticated)/home/Home.client.test.tsx
index a49ad66e2..0b5a17fbd 100644
--- a/apps/web/src/app/(authenticated)/home/Home.client.test.tsx
+++ b/apps/web/src/app/(authenticated)/home/Home.client.test.tsx
@@ -21,6 +21,7 @@ let currentEnvironments: Array<{ id: string; name: string }> | undefined = [
let currentEnvironmentsPending = false;
let currentCommunicationsFastModeDefault = false;
let currentPersonalPreferencesLoading = false;
+let currentSessionsUiEnabled = false;
const {
mockPush,
@@ -74,6 +75,7 @@ vi.mock('@/hooks/useUser', () => ({
name: 'Test User',
primaryEmail: 'test@example.com',
cloudEnabled: currentCloudEnabled,
+ featureFlags: { sessions_ui: currentSessionsUiEnabled },
resource: {
username: 'tester',
fullName: 'Test User',
@@ -398,6 +400,7 @@ describe('Home', () => {
currentEnvironmentsPending = false;
currentCommunicationsFastModeDefault = false;
currentPersonalPreferencesLoading = false;
+ currentSessionsUiEnabled = false;
localStorage.clear();
vi.clearAllMocks();
@@ -1139,6 +1142,27 @@ describe('Home', () => {
).toBeDisabled();
});
+ it('starts an Auto Session without an environment when Sessions UI is enabled', async () => {
+ currentEnvironments = [];
+ currentSessionsUiEnabled = true;
+
+ render();
+
+ const submitButton = screen.getByRole('button', { name: 'Submit prompt' });
+ expect(submitButton).toBeEnabled();
+ fireEvent.click(submitButton);
+
+ await waitFor(() => {
+ expect(mockStartFastSession).toHaveBeenCalledWith({
+ text: 'Test prompt',
+ images: undefined,
+ model: 'openrouter/openai/gpt-5.4',
+ });
+ });
+ expect(mockRouteHomeTask).not.toHaveBeenCalled();
+ expect(mockCreateStandardTaskRun).not.toHaveBeenCalled();
+ });
+
it('does not show the empty-environments warning while environments are loading', () => {
currentEnvironments = undefined;
currentEnvironmentsPending = true;
diff --git a/apps/web/src/app/(authenticated)/home/Home.tsx b/apps/web/src/app/(authenticated)/home/Home.tsx
index 10393dd16..f18cadd36 100644
--- a/apps/web/src/app/(authenticated)/home/Home.tsx
+++ b/apps/web/src/app/(authenticated)/home/Home.tsx
@@ -569,9 +569,13 @@ export function Home({
const hasAnyEnvironments = (environments.data?.length ?? 0) > 0;
const showNoEnvironmentsWarning =
isAdmin && !environments.isPending && !hasAnyEnvironments;
+ const autoRoutingNeedsEnvironment =
+ !sessionsUiEnabled &&
+ !hasAnyEnvironments &&
+ watchedRepository === AUTO_WORKSPACE_VALUE;
const submitDisabledReason =
getTaskLaunchDisabledReason(managedAccess) ??
- (!hasAnyEnvironments && watchedRepository === AUTO_WORKSPACE_VALUE
+ (autoRoutingNeedsEnvironment
? 'Auto routing needs an environment. Create one, or select All Repositories to work without one.'
: undefined);
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/Header.client.test.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/Header.client.test.tsx
index 8dd0401c4..3858daa04 100644
--- a/apps/web/src/app/(sandbox)/task/[taskId]/Header.client.test.tsx
+++ b/apps/web/src/app/(sandbox)/task/[taskId]/Header.client.test.tsx
@@ -1,12 +1,19 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-const { useSandboxLayoutMock, useTRPCMock, updateTitleMutationMock } =
- vi.hoisted(() => ({
- useSandboxLayoutMock: vi.fn(),
- useTRPCMock: vi.fn(),
- updateTitleMutationMock: vi.fn(async () => undefined),
- }));
+const {
+ useSandboxLayoutMock,
+ useTRPCMock,
+ updateTitleMutationMock,
+ parentSessionQueryMock,
+ featureFlagState,
+} = vi.hoisted(() => ({
+ useSandboxLayoutMock: vi.fn(),
+ useTRPCMock: vi.fn(),
+ updateTitleMutationMock: vi.fn(async () => undefined),
+ parentSessionQueryMock: vi.fn(),
+ featureFlagState: { sessionsUiEnabled: false },
+}));
vi.mock('../../use-sandbox-layout', () => ({
useSandboxLayout: useSandboxLayoutMock,
@@ -16,6 +23,16 @@ vi.mock('@/trpc/client', () => ({
useTRPC: useTRPCMock,
}));
+vi.mock('@/hooks/useUser', () => ({
+ useAuthorizedUser: () => ({
+ featureFlags: { sessions_ui: featureFlagState.sessionsUiEnabled },
+ }),
+}));
+
+vi.mock('./TaskSessionReadTracker', () => ({
+ TaskSessionReadTracker: () => null,
+}));
+
vi.mock('@/components/sandbox', () => ({
WorkspaceBadge: ({
environmentId,
@@ -78,6 +95,11 @@ function renderHeader(
describe('Header', () => {
beforeEach(() => {
vi.clearAllMocks();
+ featureFlagState.sessionsUiEnabled = false;
+ parentSessionQueryMock.mockResolvedValue({
+ sessionId: 'session-1',
+ title: 'Parent Session',
+ });
useSandboxLayoutMock.mockReturnValue({
isSidebarVisible: true,
@@ -93,6 +115,18 @@ describe('Header', () => {
],
},
},
+ sessions: {
+ forTask: {
+ queryOptions: (
+ _input: { taskId: string },
+ options?: { enabled?: boolean },
+ ) => ({
+ queryKey: ['sessions.forTask'],
+ queryFn: parentSessionQueryMock,
+ enabled: options?.enabled,
+ }),
+ },
+ },
tasks: {
updateTitle: {
mutationOptions: () => ({
@@ -142,6 +176,23 @@ describe('Header', () => {
expect(screen.queryByText('OpenCode')).not.toBeInTheDocument();
});
+ it('does not query or render Session breadcrumbs while Sessions UI is disabled', () => {
+ renderHeader();
+
+ expect(parentSessionQueryMock).not.toHaveBeenCalled();
+ expect(screen.queryByRole('link', { name: 'Sessions' })).toBeNull();
+ });
+
+ it('renders Session breadcrumbs while Sessions UI is enabled', async () => {
+ featureFlagState.sessionsUiEnabled = true;
+
+ renderHeader();
+
+ expect(
+ await screen.findByRole('link', { name: 'Parent Session' }),
+ ).toHaveAttribute('href', '/sessions/session-1?task=task-123');
+ });
+
it('refreshes task lists after renaming a task', async () => {
const { queryClient } = renderHeader();
const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries');
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/Header.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/Header.tsx
index e1dd8f108..74f22f331 100644
--- a/apps/web/src/app/(sandbox)/task/[taskId]/Header.tsx
+++ b/apps/web/src/app/(sandbox)/task/[taskId]/Header.tsx
@@ -25,6 +25,7 @@ import { PullRequestBadge, WorkspaceBadge } from '@/components/sandbox';
import { WorkspaceHeader } from '@/components/layout';
import { useTRPC } from '@/trpc/client';
+import { useAuthorizedUser } from '@/hooks/useUser';
import { useSandboxLayout } from '../../use-sandbox-layout';
import { type TaskSession } from './hooks';
@@ -37,18 +38,22 @@ interface HeaderProps {
export const Header = ({ session: { taskRun, task, taskId } }: HeaderProps) => {
const { isSidebarVisible, toggleSidebar } = useSandboxLayout();
const trpc = useTRPC();
+ const { featureFlags } = useAuthorizedUser();
+ const sessionsUiEnabled = featureFlags?.sessions_ui === true;
const searchParams = useSearchParams();
const queryClient = useQueryClient();
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
const [titleDraft, setTitleDraft] = useState(task?.title ?? '');
- const parentSessionOptions = trpc.sessions?.forTask?.queryOptions({
- taskId,
- }) ?? {
+ const parentSessionOptions = trpc.sessions?.forTask?.queryOptions(
+ { taskId },
+ { enabled: sessionsUiEnabled },
+ ) ?? {
queryKey: ['sessions', 'for-task', 'disabled', taskId],
queryFn: async () => null,
enabled: false,
};
- const { data: parentSession } = useQuery(parentSessionOptions);
+ const { data: queriedParentSession } = useQuery(parentSessionOptions);
+ const parentSession = sessionsUiEnabled ? queriedParentSession : null;
const environmentId = taskRun?.payload?.environmentId;
const repo = taskRun?.payload?.repo;
diff --git a/apps/web/src/lib/server/sessions.test.ts b/apps/web/src/lib/server/sessions.test.ts
index 4160f8d23..a0e9239ad 100644
--- a/apps/web/src/lib/server/sessions.test.ts
+++ b/apps/web/src/lib/server/sessions.test.ts
@@ -116,6 +116,40 @@ describe('unified Session queries', () => {
expect(taskEvent).not.toHaveProperty('task.pullRequests');
});
+ it('excludes soft-deleted tasks from Session detail, timeline, and live status', async () => {
+ const owner = await userFactory.create();
+ const session = await sessionFactory.create({
+ ownerKind: 'user',
+ ownerUserId: owner.id,
+ cachedStatus: 'blocked',
+ });
+ const task = await taskFactory.create({
+ initiatorUserId: owner.id,
+ state: 'failed',
+ deletedAt: new Date(),
+ });
+ await db.insert(sessionTasks).values({
+ sessionId: session.id,
+ taskId: task.id,
+ origin: 'direct_launch',
+ });
+
+ const detail = await getSessionById(
+ { userId: owner.id, isAdmin: false },
+ session.id,
+ );
+ const timeline = await getSessionTimeline(
+ { userId: owner.id, isAdmin: false },
+ session.id,
+ );
+
+ expect(detail?.tasks).toEqual([]);
+ expect(detail?.status).toBe('ready');
+ expect(
+ timeline?.events.some((event) => event.id.startsWith(`task:${task.id}:`)),
+ ).toBe(false);
+ });
+
it('keeps metadata changes owner-only and stores per-user pins', async () => {
const owner = await userFactory.create();
const stranger = await userFactory.create();
diff --git a/apps/web/src/lib/server/sessions.ts b/apps/web/src/lib/server/sessions.ts
index c57df6bb8..3c7cc4644 100644
--- a/apps/web/src/lib/server/sessions.ts
+++ b/apps/web/src/lib/server/sessions.ts
@@ -433,11 +433,10 @@ async function getSessionTasks(sessionId: string) {
repositoryName: tasks.repositoryName,
model: tasks.model,
activityAt: tasks.activityAt,
- deletedAt: tasks.deletedAt,
})
.from(sessionTasks)
.innerJoin(tasks, eq(tasks.id, sessionTasks.taskId))
- .where(eq(sessionTasks.sessionId, sessionId))
+ .where(and(eq(sessionTasks.sessionId, sessionId), isNull(tasks.deletedAt)))
.orderBy(sessionTasks.attachedAt);
return Promise.all(
diff --git a/apps/web/src/trpc/commands/tasks/__tests__/delete.test.ts b/apps/web/src/trpc/commands/tasks/__tests__/delete.test.ts
index 8b53ef589..8fd780327 100644
--- a/apps/web/src/trpc/commands/tasks/__tests__/delete.test.ts
+++ b/apps/web/src/trpc/commands/tasks/__tests__/delete.test.ts
@@ -3,6 +3,8 @@ import type { UserAuthSuccess } from '@/types';
const {
mockDeleteArtifactsBatch,
mockMarkParallelCounts,
+ mockGetSessionForTask,
+ mockTouchSessionActivity,
tasksTable,
taskArtifactsTable,
deleteCalls,
@@ -10,6 +12,8 @@ const {
} = vi.hoisted(() => ({
mockDeleteArtifactsBatch: vi.fn(),
mockMarkParallelCounts: vi.fn(),
+ mockGetSessionForTask: vi.fn(),
+ mockTouchSessionActivity: vi.fn(),
tasksTable: { id: 'tasks.id', deletedAt: 'tasks.deletedAt' },
taskArtifactsTable: {
id: 'taskArtifacts.id',
@@ -64,6 +68,8 @@ vi.mock('@roomote/db/server', () => ({
tasks: tasksTable,
taskArtifacts: taskArtifactsTable,
markTaskStartParallelCountsEndedAtForTaskIds: mockMarkParallelCounts,
+ getSessionForTask: mockGetSessionForTask,
+ touchSessionActivity: mockTouchSessionActivity,
and: (...conditions: unknown[]) => ({ and: conditions }),
inArray: (column: unknown, values: unknown) => ({
inArray: [column, values],
@@ -89,6 +95,10 @@ describe('deleteTasksCommand', () => {
vi.clearAllMocks();
deleteCalls.length = 0;
mockDeleteArtifactsBatch.mockResolvedValue({ deleted: 1, errors: 0 });
+ mockGetSessionForTask.mockResolvedValue({
+ id: 'session-1',
+ activityAt: 100,
+ });
});
it('deletes taskArtifacts rows inside the soft-delete transaction', async () => {
@@ -114,5 +124,11 @@ describe('deleteTasksCommand', () => {
(call as { table: unknown }).table === taskArtifactsTable,
);
expect(artifactDelete).toBeDefined();
+ expect(mockGetSessionForTask).toHaveBeenCalledWith(fakeTx, 'task-1');
+ expect(mockTouchSessionActivity).toHaveBeenCalledWith(
+ fakeTx,
+ 'session-1',
+ 100,
+ );
});
});
diff --git a/apps/web/src/trpc/commands/tasks/delete.ts b/apps/web/src/trpc/commands/tasks/delete.ts
index 3e95b621d..3c41429d9 100644
--- a/apps/web/src/trpc/commands/tasks/delete.ts
+++ b/apps/web/src/trpc/commands/tasks/delete.ts
@@ -2,6 +2,8 @@ import {
db,
tasks,
markTaskStartParallelCountsEndedAtForTaskIds,
+ getSessionForTask,
+ touchSessionActivity,
taskArtifacts,
and,
inArray,
@@ -95,6 +97,18 @@ export async function deleteTasksCommand(
.where(and(...whereConditions))
.returning({ id: tasks.id });
+ const affectedSessions = new Map<
+ string,
+ NonNullable>>
+ >();
+ for (const deletedTask of deletedTasksResult) {
+ const session = await getSessionForTask(tx, deletedTask.id);
+ if (session) affectedSessions.set(session.id, session);
+ }
+ for (const session of affectedSessions.values()) {
+ await touchSessionActivity(tx, session.id, session.activityAt);
+ }
+
return {
deletedTasks: deletedTasksResult,
artifactsDeleted: s3Result.deleted,
diff --git a/packages/db/src/lib/__tests__/sessions.test.ts b/packages/db/src/lib/__tests__/sessions.test.ts
index 342e5da13..a4f88241f 100644
--- a/packages/db/src/lib/__tests__/sessions.test.ts
+++ b/packages/db/src/lib/__tests__/sessions.test.ts
@@ -148,6 +148,28 @@ describe('session helpers', () => {
);
});
+ it('excludes soft-deleted tasks when recomputing cached status', async () => {
+ const session = await sessionFactory.create({
+ activityAt: 100,
+ cachedStatus: 'blocked',
+ });
+ createdSessionIds.push(session.id);
+ const task = await taskFactory.create({
+ state: 'failed',
+ deletedAt: new Date(),
+ });
+ createdTaskIds.push(task.id);
+ await db.insert(sessionTasks).values({
+ sessionId: session.id,
+ taskId: task.id,
+ origin: 'direct_launch',
+ });
+
+ const updated = await touchSessionActivity(db, session.id, 100);
+
+ expect(updated.cachedStatus).toBe('ready');
+ });
+
it('serializes concurrent status refreshes before reading linked tasks', async () => {
const session = await sessionFactory.create({
activityAt: 100,
diff --git a/packages/db/src/lib/sessions.ts b/packages/db/src/lib/sessions.ts
index 44ab88d72..b53c51461 100644
--- a/packages/db/src/lib/sessions.ts
+++ b/packages/db/src/lib/sessions.ts
@@ -1,4 +1,4 @@
-import { and, desc, eq, or, sql } from 'drizzle-orm';
+import { and, desc, eq, isNull, or, sql } from 'drizzle-orm';
import type { TaskGoalStatus, TaskState } from '@roomote/types';
@@ -96,7 +96,7 @@ async function refreshLockedSession(
.from(sessionTasks)
.innerJoin(tasks, eq(tasks.id, sessionTasks.taskId))
.leftJoin(taskRuns, eq(taskRuns.taskId, tasks.id))
- .where(eq(sessionTasks.sessionId, sessionId))
+ .where(and(eq(sessionTasks.sessionId, sessionId), isNull(tasks.deletedAt)))
.orderBy(tasks.id, desc(taskRuns.id));
const [updated] = await tx
From d411f66d791546c5f4b43b73320f206f9843f498 Mon Sep 17 00:00:00 2001
From: Roomote
Date: Wed, 26 Aug 2026 13:17:25 +0000
Subject: [PATCH 08/26] feat: add live nested task panels to web sessions
---
.../FastSessionTranscript.client.test.tsx | 92 ++++++++++++-
.../[sessionId]/FastSessionTranscript.tsx | 4 +
.../NestedTaskSidePanel.client.test.tsx | 93 +++++++++++++
.../[sessionId]/NestedTaskSidePanel.tsx | 124 ++++++++++++++++++
.../SessionWorkspace.client.test.tsx | 37 +++++-
.../sessions/[sessionId]/SessionWorkspace.tsx | 117 ++++++++++-------
.../[sessionId]/session-task-panel-context.ts | 11 ++
.../[taskId]/messages/acp/AcpMessageItem.tsx | 12 ++
.../messages/acp/AcpTranscriptBlocks.tsx | 8 ++
.../acp/DelegatedTaskCard.client.test.tsx | 98 ++++++++++++++
.../messages/acp/DelegatedTaskCard.tsx | 60 +++++++++
.../tool-call-grouping.client.test.ts | 23 ++++
.../[taskId]/messages/acp/activity-groups.ts | 23 +++-
.../[taskId]/messages/acp/delegated-task.ts | 50 +++++++
.../[taskId]/messages/acp/render-blocks.ts | 7 +-
15 files changed, 699 insertions(+), 60 deletions(-)
create mode 100644 apps/web/src/app/(sandbox)/sessions/[sessionId]/NestedTaskSidePanel.client.test.tsx
create mode 100644 apps/web/src/app/(sandbox)/sessions/[sessionId]/NestedTaskSidePanel.tsx
create mode 100644 apps/web/src/app/(sandbox)/sessions/[sessionId]/session-task-panel-context.ts
create mode 100644 apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/DelegatedTaskCard.client.test.tsx
create mode 100644 apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/DelegatedTaskCard.tsx
create mode 100644 apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/delegated-task.ts
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/FastSessionTranscript.client.test.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/FastSessionTranscript.client.test.tsx
index 5d8930f6d..deb6c375b 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/FastSessionTranscript.client.test.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/FastSessionTranscript.client.test.tsx
@@ -9,10 +9,13 @@ import { ACP_ENVELOPE_EVENT_TYPES } from '@roomote/types';
import { FastSessionTranscript } from './FastSessionTranscript';
-const { replyMutate, preparePromptAttachments } = vi.hoisted(() => ({
- replyMutate: vi.fn(),
- preparePromptAttachments: vi.fn(),
-}));
+const { replyMutate, preparePromptAttachments, openTaskPanel } = vi.hoisted(
+ () => ({
+ replyMutate: vi.fn(),
+ preparePromptAttachments: vi.fn(),
+ openTaskPanel: vi.fn(),
+ }),
+);
vi.mock('@/trpc/client', () => ({
useTRPCClient: () => ({
@@ -38,6 +41,24 @@ vi.mock('@/hooks/task-models/useLaunchTaskModels', () => ({
}),
}));
+vi.mock('./session-task-panel-context', () => ({
+ useOpenSessionTaskPanel: () => openTaskPanel,
+}));
+
+vi.mock('../../task/[taskId]/messages/acp/DelegatedTaskCard', () => ({
+ DelegatedTaskCard: ({
+ taskId,
+ onOpen,
+ }: {
+ taskId: string;
+ onOpen: (taskId: string) => void;
+ }) => (
+
+ ),
+}));
+
class FakeEventSource {
static instances: FakeEventSource[] = [];
listeners = new Map void>>();
@@ -71,6 +92,7 @@ beforeEach(() => {
preparePromptAttachments.mockImplementation(({ text }: { text: string }) =>
Promise.resolve({ text }),
);
+ openTaskPanel.mockReset();
vi.stubGlobal('EventSource', FakeEventSource);
});
@@ -279,6 +301,68 @@ describe('FastSessionTranscript', () => {
);
});
+ it('opens a launched child task in the session side panel', () => {
+ render(
+ ,
+ );
+
+ fireEvent.click(screen.getByRole('button', { name: /Delegated task/ }));
+
+ expect(openTaskPanel).toHaveBeenCalledWith('child-1');
+ });
+
it('cold-loads one completed tool row before an intervening kickoff', () => {
render(
>(
@@ -185,6 +187,7 @@ export function FastSessionTranscript({
shouldHideFirstMessage: false,
showInternalMessages: false,
hasLeadingTextBoundary: false,
+ keepDelegatedTasksVisible: true,
resetKey: `${messages.length}:${messages[0]?.eventId ?? ''}:${messages.at(-1)?.eventId ?? ''}`,
});
@@ -284,6 +287,7 @@ export function FastSessionTranscript({
blocks={renderBlocks}
showInternalMessages={false}
onSuppress={suppressMessage}
+ onOpenDelegatedTask={openTaskPanel ?? undefined}
/>
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/NestedTaskSidePanel.client.test.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/NestedTaskSidePanel.client.test.tsx
new file mode 100644
index 000000000..6e663264b
--- /dev/null
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/NestedTaskSidePanel.client.test.tsx
@@ -0,0 +1,93 @@
+import { render, screen } from '@testing-library/react';
+import type { ReactNode } from 'react';
+import { RunStatus } from '@roomote/types';
+
+const useTaskSessionMock = vi.fn();
+
+vi.mock('../../task/[taskId]/hooks/use-task-session', () => ({
+ useTaskSession: (...args: unknown[]) => useTaskSessionMock(...args),
+}));
+
+vi.mock('../../task/[taskId]/hooks/use-task-message-envelopes', () => ({
+ useTaskMessageEnvelopes: () => ({
+ data: [],
+ isPending: false,
+ isSuccess: true,
+ isError: false,
+ }),
+}));
+
+vi.mock('../../task/[taskId]/hooks/ArtifactLinkProvider', () => ({
+ ArtifactLinkProvider: ({ children }: { children: ReactNode }) => children,
+}));
+
+vi.mock('../../task/[taskId]/hooks/HistoricalSandboxProvider', () => ({
+ HistoricalSandboxProvider: ({ children }: { children: ReactNode }) => (
+ {children}
+ ),
+}));
+
+vi.mock('../../task/[taskId]/hooks/SandboxProvider', () => ({
+ SandboxProvider: ({ children }: { children: ReactNode }) => (
+ {children}
+ ),
+}));
+
+vi.mock('../../task/[taskId]/Messages', () => ({
+ Messages: () => Child transcript
,
+}));
+
+vi.mock('../../task/[taskId]/sidebar-panels/SidePanelHeader', () => ({
+ SidePanelHeader: ({
+ title,
+ actions,
+ }: {
+ title: string;
+ actions: ReactNode;
+ }) => (
+
+ ),
+}));
+
+import { NestedTaskSidePanel } from './NestedTaskSidePanel';
+
+describe('NestedTaskSidePanel', () => {
+ beforeEach(() => {
+ useTaskSessionMock.mockReturnValue({
+ taskId: 'child-1',
+ task: { title: 'Fix checkout' },
+ taskRun: {
+ id: 42,
+ harness: 'opencode-server',
+ status: RunStatus.Running,
+ taskPhase: 'running',
+ sandboxServerUrl: 'http://sandbox.test',
+ },
+ artifacts: [],
+ prompt: null,
+ token: 'token',
+ refreshConnection: vi.fn(),
+ sessionState: 'interactive',
+ isSessionLoading: false,
+ });
+ });
+
+ it('renders the focused live transcript and full-task navigation without task chrome', () => {
+ render();
+
+ expect(screen.getByText('Fix checkout')).toBeInTheDocument();
+ expect(screen.getByTestId('live-provider')).toBeInTheDocument();
+ expect(screen.getByText('Child transcript')).toBeInTheDocument();
+ expect(screen.getByRole('link', { name: /Go to task/ })).toHaveAttribute(
+ 'href',
+ '/task/child-1',
+ );
+ expect(screen.queryByText('Task actions')).not.toBeInTheDocument();
+ expect(useTaskSessionMock).toHaveBeenCalledWith('child-1', {
+ refetchInterval: 2_000,
+ });
+ });
+});
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/NestedTaskSidePanel.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/NestedTaskSidePanel.tsx
new file mode 100644
index 000000000..0ec29566d
--- /dev/null
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/NestedTaskSidePanel.tsx
@@ -0,0 +1,124 @@
+'use client';
+
+import Link from 'next/link';
+
+import { DEFAULT_CODING_HARNESS, type TaskPhase } from '@roomote/types';
+
+import {
+ Button,
+ ErrorState,
+ ExternalLink,
+ Skeleton,
+} from '@/components/system';
+
+import { ArtifactLinkProvider } from '../../task/[taskId]/hooks/ArtifactLinkProvider';
+import { HistoricalSandboxProvider } from '../../task/[taskId]/hooks/HistoricalSandboxProvider';
+import { SandboxProvider } from '../../task/[taskId]/hooks/SandboxProvider';
+import { useTaskMessageEnvelopes } from '../../task/[taskId]/hooks/use-task-message-envelopes';
+import {
+ useTaskSession,
+ type TaskSession,
+} from '../../task/[taskId]/hooks/use-task-session';
+import { Messages } from '../../task/[taskId]/Messages';
+import { SidePanelHeader } from '../../task/[taskId]/sidebar-panels/SidePanelHeader';
+
+function NestedTaskTranscript({ session }: { session: TaskSession }) {
+ const history = useTaskMessageEnvelopes(session.taskId);
+
+ if (session.isSessionLoading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (
+ session.sessionState === 'error' ||
+ session.sessionState === 'not-found'
+ ) {
+ return ;
+ }
+
+ if (!session.taskRun) {
+ return ;
+ }
+
+ const transcript = (
+
+
+
+ );
+
+ if (
+ session.sessionState === 'historical' ||
+ session.sessionState === 'resuming' ||
+ session.sessionState === 'boot-failed'
+ ) {
+ return (
+
+ {transcript}
+
+ );
+ }
+
+ return (
+
+ {transcript}
+
+ );
+}
+
+export function NestedTaskSidePanel({
+ taskId,
+ onClose,
+}: {
+ taskId: string;
+ onClose: () => void;
+}) {
+ const session = useTaskSession(taskId, { refetchInterval: 2_000 });
+ const title = session.task?.title?.trim() || 'Task';
+
+ return (
+
+
+
+ Go to task
+
+
+
+ }
+ />
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx
index 169a932c0..b3703e869 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx
@@ -3,6 +3,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { SandboxLayoutContext } from '../../use-sandbox-layout';
import { SessionWorkspace, type SessionInfo } from './SessionWorkspace';
+import { useOpenSessionTaskPanel } from './session-task-panel-context';
const { useMediaQueryMock } = vi.hoisted(() => ({
useMediaQueryMock: vi.fn(),
@@ -23,6 +24,12 @@ vi.mock('@/hooks/task-models/useLaunchTaskModels', () => ({
}),
}));
+vi.mock('./NestedTaskSidePanel', () => ({
+ NestedTaskSidePanel: ({ taskId }: { taskId: string }) => (
+ Nested panel {taskId}
+ ),
+}));
+
const session: SessionInfo = {
id: 'session-1',
ownerName: 'Test User',
@@ -51,18 +58,32 @@ function SandboxLayoutProvider({ children }: { children: ReactNode }) {
);
}
-function renderWorkspace({ isMobile }: { isMobile: boolean }) {
+function renderWorkspace({
+ isMobile,
+ children = Session transcript
,
+}: {
+ isMobile: boolean;
+ children?: ReactNode;
+}) {
useMediaQueryMock.mockReturnValue(!isMobile);
render(
-
- Session transcript
-
+ {children}
,
);
}
+function OpenNestedTask() {
+ const openTaskPanel = useOpenSessionTaskPanel();
+
+ return (
+
+ );
+}
+
describe('SessionWorkspace', () => {
it('matches the task sidebar replacement behavior and controls on mobile', () => {
renderWorkspace({ isMobile: true });
@@ -109,4 +130,12 @@ describe('SessionWorkspace', () => {
screen.getByRole('button', { name: 'Close session info' }),
).toBeInTheDocument();
});
+
+ it('opens delegated tasks in the existing session side-panel slot', () => {
+ renderWorkspace({ isMobile: false, children: });
+
+ fireEvent.click(screen.getByRole('button', { name: 'Open child' }));
+
+ expect(screen.getByText('Nested panel child-1')).toBeInTheDocument();
+ });
});
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
index 30b254ef8..15826ebb1 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
@@ -32,6 +32,8 @@ import {
SandboxSideActions,
} from '../../SandboxWorkspacePanels';
import { useSandboxLayout } from '../../use-sandbox-layout';
+import { NestedTaskSidePanel } from './NestedTaskSidePanel';
+import { OpenSessionTaskPanelContext } from './session-task-panel-context';
export type SessionInfo = {
id: string;
@@ -241,13 +243,15 @@ export function SessionWorkspace({
children: ReactNode;
}) {
const [isInfoOpen, setIsInfoOpen] = useState(false);
+ const [nestedTaskId, setNestedTaskId] = useState(null);
const router = useRouter();
const searchParams = useSearchParams();
const selectedTaskId = searchParams.get('task');
const selectedTask = session.tasks.find(
(task) => task.taskId === selectedTaskId,
);
- const panelOpen = isInfoOpen || Boolean(selectedTask);
+ const panelOpen =
+ isInfoOpen || Boolean(selectedTask) || Boolean(nestedTaskId);
const selectTask = useCallback(
(taskId: string | null) => {
@@ -266,11 +270,22 @@ export function SessionWorkspace({
}
}, [selectTask, selectedTaskId, session.tasks]);
+ const openTaskPanel = useCallback(
+ (taskId: string) => {
+ setIsInfoOpen(false);
+ setNestedTaskId(taskId);
+ selectTask(null);
+ },
+ [selectTask],
+ );
const closePanel = () => {
setIsInfoOpen(false);
+ setNestedTaskId(null);
selectTask(null);
};
- const panelContent = selectedTask ? (
+ const panelContent = nestedTaskId ? (
+
+ ) : selectedTask ? (
-
- {
- selectTask(null);
- setIsInfoOpen((previous) => !previous);
- }}
- />
- {session.tasks.length ? (
+
+
+
{
- setIsInfoOpen(false);
- selectTask(selectedTask ? null : session.tasks[0]!.taskId);
+ setNestedTaskId(null);
+ selectTask(null);
+ setIsInfoOpen((previous) => !previous);
}}
/>
+ {session.tasks.length ? (
+ {
+ setNestedTaskId(null);
+ setIsInfoOpen(false);
+ selectTask(selectedTask ? null : session.tasks[0]!.taskId);
+ }}
+ />
+ ) : null}
+
+ {!isSidebarVisible && !panelOpen ? (
+
+
+
) : null}
-
- {!isSidebarVisible && !panelOpen ? (
-
-
-
- ) : null}
- >
- }
- >
-
-
+ >
+ }
+ >
+
+
+
);
}
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/session-task-panel-context.ts b/apps/web/src/app/(sandbox)/sessions/[sessionId]/session-task-panel-context.ts
new file mode 100644
index 000000000..8fd19b8cf
--- /dev/null
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/session-task-panel-context.ts
@@ -0,0 +1,11 @@
+'use client';
+
+import { createContext, useContext } from 'react';
+
+export const OpenSessionTaskPanelContext = createContext<
+ ((taskId: string) => void) | null
+>(null);
+
+export function useOpenSessionTaskPanel() {
+ return useContext(OpenSessionTaskPanelContext);
+}
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/AcpMessageItem.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/AcpMessageItem.tsx
index ddc9bdcf1..d2f0ebc86 100644
--- a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/AcpMessageItem.tsx
+++ b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/AcpMessageItem.tsx
@@ -8,11 +8,14 @@ import { AcpTodoSectionMessage } from './AcpTodoSectionMessage';
import { AcpTextMessage } from './AcpTextMessage';
import { AcpToolMessage } from './AcpToolMessage';
import { AcpUnknownMessage } from './AcpUnknownMessage';
+import { DelegatedTaskCard } from './DelegatedTaskCard';
+import { getDelegatedTaskDetails } from './delegated-task';
interface AcpMessageItemProps {
msg: AcpUiMessage;
onSuppress?: (messageId: string) => void;
showSubagentPayload?: boolean;
+ onOpenDelegatedTask?: (taskId: string) => void;
children?: ReactNode;
}
@@ -20,6 +23,7 @@ function AcpMessageItemBase({
msg,
onSuppress,
showSubagentPayload = false,
+ onOpenDelegatedTask,
children,
}: AcpMessageItemProps) {
switch (msg.kind) {
@@ -31,6 +35,14 @@ function AcpMessageItemBase({
return ;
case 'tool_call':
case 'tool_result': {
+ const delegatedTask = getDelegatedTaskDetails(msg);
+
+ if (delegatedTask && onOpenDelegatedTask) {
+ return (
+
+ );
+ }
+
return msg.data.kind === 'execute' ? (
>(
@@ -63,6 +65,7 @@ export function useAcpTranscriptBlocks({
initialPrompt,
shouldHideFirstMessage,
showInternalMessages,
+ keepDelegatedTasksVisible,
suppressedMessageIds,
});
@@ -70,12 +73,14 @@ export function useAcpTranscriptBlocks({
artifacts,
displayMode,
hasLeadingTextBoundary,
+ keepDelegatedTasksVisible,
});
}, [
artifacts,
displayMode,
hasLeadingTextBoundary,
initialPrompt,
+ keepDelegatedTasksVisible,
messages,
shouldHideFirstMessage,
showInternalMessages,
@@ -106,10 +111,12 @@ export function AcpTranscriptBlockList({
blocks,
showInternalMessages,
onSuppress,
+ onOpenDelegatedTask,
}: {
blocks: AcpConversationRenderBlock[];
showInternalMessages: boolean;
onSuppress: (messageId: string) => void;
+ onOpenDelegatedTask?: (taskId: string) => void;
}) {
function renderNestedBlocks(nestedBlocks: AcpRenderBlock[]) {
return nestedBlocks.map((block) => (
@@ -196,6 +203,7 @@ export function AcpTranscriptBlockList({
msg={block.msg}
onSuppress={onSuppress}
showSubagentPayload={showInternalMessages}
+ onOpenDelegatedTask={onOpenDelegatedTask}
>
{block.childBlocks?.length
? renderNestedBlocks(block.childBlocks)
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/DelegatedTaskCard.client.test.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/DelegatedTaskCard.client.test.tsx
new file mode 100644
index 000000000..0205c52ca
--- /dev/null
+++ b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/DelegatedTaskCard.client.test.tsx
@@ -0,0 +1,98 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { RunStatus } from '@roomote/types';
+
+const useQueryMock = vi.fn();
+const queryOptionsMock = vi.fn((input, options) => ({ input, ...options }));
+
+vi.mock('@tanstack/react-query', () => ({
+ useQuery: (...args: unknown[]) => useQueryMock(...args),
+}));
+
+vi.mock('@/trpc/client', () => ({
+ useTRPC: () => ({
+ sandboxSession: { byTaskId: { queryOptions: queryOptionsMock } },
+ }),
+}));
+
+import { DelegatedTaskCard } from './DelegatedTaskCard';
+
+describe('DelegatedTaskCard', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ useQueryMock.mockReturnValue({
+ isPending: false,
+ data: {
+ task: { title: 'Fix checkout' },
+ taskRun: {
+ status: RunStatus.Running,
+ taskPhase: 'running',
+ error: null,
+ },
+ },
+ });
+ });
+
+ it('renders live task state and opens the selected child', () => {
+ const onOpen = vi.fn();
+ render(
+ ,
+ );
+
+ expect(screen.getByText('Fix checkout')).toBeInTheDocument();
+ expect(screen.getByText('Working')).toBeInTheDocument();
+ fireEvent.click(
+ screen.getByRole('button', { name: 'View delegated task: Fix checkout' }),
+ );
+ expect(onOpen).toHaveBeenCalledWith('child-1');
+
+ const queryOptions = queryOptionsMock.mock.calls[0]![1];
+ expect(queryOptions.refetchInterval({ state: { data: undefined } })).toBe(
+ 2_000,
+ );
+ expect(queryOptionsMock).toHaveBeenCalledWith(
+ { taskId: 'child-1' },
+ expect.any(Object),
+ );
+ });
+
+ it('updates when the child transitions to a terminal state', () => {
+ let queryResult = {
+ isPending: false,
+ data: {
+ task: { title: 'Fix checkout' },
+ taskRun: {
+ status: RunStatus.Running,
+ taskPhase: 'running',
+ error: null as string | null,
+ },
+ },
+ };
+ useQueryMock.mockImplementation(() => queryResult);
+
+ const { rerender } = render(
+ ,
+ );
+ expect(screen.getByText('Working')).toBeInTheDocument();
+
+ queryResult = {
+ ...queryResult,
+ data: {
+ ...queryResult.data,
+ taskRun: {
+ status: RunStatus.Failed,
+ taskPhase: 'shutting_down',
+ error: 'Task failed',
+ },
+ },
+ };
+ rerender(
+ ,
+ );
+
+ expect(screen.getByText('Error')).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/DelegatedTaskCard.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/DelegatedTaskCard.tsx
new file mode 100644
index 000000000..dc2834459
--- /dev/null
+++ b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/DelegatedTaskCard.tsx
@@ -0,0 +1,60 @@
+'use client';
+
+import { useQuery } from '@tanstack/react-query';
+
+import { Bot, ChevronRight, Skeleton } from '@/components/system';
+import { TaskStatusIndicator } from '@/components/sandbox';
+import { useTRPC } from '@/trpc/client';
+
+export function DelegatedTaskCard({
+ taskId,
+ prompt,
+ onOpen,
+}: {
+ taskId: string;
+ prompt: string | null;
+ onOpen: (taskId: string) => void;
+}) {
+ const trpc = useTRPC();
+ const { data, isPending } = useQuery(
+ trpc.sandboxSession.byTaskId.queryOptions(
+ { taskId },
+ {
+ refetchInterval: (query) => query.state.data?.refetchInterval ?? 2_000,
+ },
+ ),
+ );
+ const title = data?.task?.title?.trim() || prompt || 'Delegated task';
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/__tests__/tool-call-grouping.client.test.ts b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/__tests__/tool-call-grouping.client.test.ts
index 29b7fd169..0ea0c5827 100644
--- a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/__tests__/tool-call-grouping.client.test.ts
+++ b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/__tests__/tool-call-grouping.client.test.ts
@@ -2014,4 +2014,27 @@ describe('buildAcpRenderBlocks', () => {
},
});
});
+
+ it('keeps multiple delegated tasks as standalone cards when requested', () => {
+ const delegatedTask = (id: string, ts: number) =>
+ explorationToolMessage({
+ id,
+ ts,
+ title: 'launch_task',
+ kind: 'tool',
+ mcp: false,
+ payload: {
+ toolName: 'launch_task',
+ output: JSON.stringify({ success: true, taskId: id }),
+ },
+ });
+
+ const entries = buildAcpRenderBlocks(
+ [delegatedTask('child-1', 1), delegatedTask('child-2', 2)],
+ { keepDelegatedTasksVisible: true },
+ );
+
+ expect(entries).toHaveLength(2);
+ expect(entries.every((entry) => entry.kind === 'message')).toBe(true);
+ });
});
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/activity-groups.ts b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/activity-groups.ts
index c899e451e..f1391c2f6 100644
--- a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/activity-groups.ts
+++ b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/activity-groups.ts
@@ -10,6 +10,7 @@ import type {
import type { AcpRenderBlock } from './render-blocks';
import { resolveShowWidgetForToolMessage } from './show-widget-tool-result';
import { resolveVisualProofMediaForToolMessage } from './visual-proof-tool-result';
+import { getDelegatedTaskDetails } from './delegated-task';
const COLLAPSIBLE_ACP_MESSAGE_KINDS = [
'reasoning',
@@ -42,6 +43,7 @@ interface BuildAcpActivityRenderBlocksOptions {
displayMode?: 'default' | 'narration';
hasLeadingTextBoundary?: boolean;
collapseLeadingActivity?: boolean;
+ keepDelegatedTasksVisible?: boolean;
}
function isToolMessage(
@@ -152,6 +154,7 @@ function isLivePartialBlock(block: AcpRenderBlock): boolean {
export function isActivityCollapsibleBlock(
block: AcpRenderBlock,
artifacts?: readonly TaskArtifact[] | null,
+ keepDelegatedTasksVisible = false,
): boolean {
// Keep in-flight reasoning/tool rows outside default-closed groups so current
// activity stays visible without a manual expand.
@@ -179,6 +182,14 @@ export function isActivityCollapsibleBlock(
return false;
}
+ if (
+ keepDelegatedTasksVisible &&
+ isToolMessage(msg) &&
+ getDelegatedTaskDetails(msg)
+ ) {
+ return false;
+ }
+
return true;
}
@@ -215,7 +226,11 @@ export function buildAcpActivityRenderBlocks(
if (
!hasLeftTextBoundary ||
- !isActivityCollapsibleBlock(current, options.artifacts)
+ !isActivityCollapsibleBlock(
+ current,
+ options.artifacts,
+ options.keepDelegatedTasksVisible,
+ )
) {
groupedBlocks.push(current);
hasLeftTextBoundary = false;
@@ -228,7 +243,11 @@ export function buildAcpActivityRenderBlocks(
while (
activityEnd < blocks.length &&
- isActivityCollapsibleBlock(blocks[activityEnd]!, options.artifacts)
+ isActivityCollapsibleBlock(
+ blocks[activityEnd]!,
+ options.artifacts,
+ options.keepDelegatedTasksVisible,
+ )
) {
activityEnd += 1;
}
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/delegated-task.ts b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/delegated-task.ts
new file mode 100644
index 000000000..65148e00d
--- /dev/null
+++ b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/delegated-task.ts
@@ -0,0 +1,50 @@
+import type { AcpToolCallUiMessage, AcpToolResultUiMessage } from './types';
+
+type ToolMessage = AcpToolCallUiMessage | AcpToolResultUiMessage;
+
+export interface DelegatedTaskDetails {
+ taskId: string;
+ prompt: string | null;
+}
+
+function asRecord(value: unknown): Record | null {
+ return value && typeof value === 'object' && !Array.isArray(value)
+ ? (value as Record)
+ : null;
+}
+
+export function getDelegatedTaskDetails(
+ msg: ToolMessage,
+): DelegatedTaskDetails | null {
+ const toolName = (msg.data.toolName ?? msg.data.mcpToolName)
+ ?.trim()
+ .toLowerCase();
+
+ if (msg.kind !== 'tool_result' || toolName !== 'launch_task') {
+ return null;
+ }
+
+ try {
+ const parsed = asRecord(JSON.parse(msg.data.output));
+ const result = asRecord(parsed?.result) ?? asRecord(parsed?.data) ?? parsed;
+ const taskId = result?.taskId;
+
+ if (typeof taskId !== 'string' || taskId.length === 0) {
+ return null;
+ }
+
+ const rawInput = asRecord(
+ (msg.data as unknown as Record).rawInput,
+ );
+ const args = asRecord(rawInput?.arguments);
+ const prompt = args?.prompt;
+
+ return {
+ taskId,
+ prompt:
+ typeof prompt === 'string' && prompt.trim() ? prompt.trim() : null,
+ };
+ } catch {
+ return null;
+ }
+}
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/render-blocks.ts b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/render-blocks.ts
index 3cedf9ef0..b82defe01 100644
--- a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/render-blocks.ts
+++ b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/render-blocks.ts
@@ -20,6 +20,7 @@ import {
isSubagentToolPayload,
} from './subagent-tool';
import { resolveShowWidgetForToolMessage } from './show-widget-tool-result';
+import { getDelegatedTaskDetails } from './delegated-task';
export type ExplorationStepKind = 'list' | 'read' | 'search';
@@ -113,6 +114,7 @@ interface BuildAcpRenderBlocksOptions {
initialPrompt?: Pick | null;
shouldHideFirstMessage?: boolean;
showInternalMessages?: boolean;
+ keepDelegatedTasksVisible?: boolean;
suppressedMessageIds?: ReadonlySet;
}
@@ -759,7 +761,10 @@ function resolveMessageRenderState(
return {
visibility: 'render',
- groupKey: resolveToolGroupKey(msg),
+ groupKey:
+ options.keepDelegatedTasksVisible && getDelegatedTaskDetails(msg)
+ ? null
+ : resolveToolGroupKey(msg),
};
}
From 6adcd59cd82c04f581c72d2d0f71837a836986ae Mon Sep 17 00:00:00 2001
From: Roomote
Date: Wed, 26 Aug 2026 13:19:04 +0000
Subject: [PATCH 09/26] chore: keep delegated task details private
---
.../app/(sandbox)/task/[taskId]/messages/acp/delegated-task.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/delegated-task.ts b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/delegated-task.ts
index 65148e00d..488e97bc8 100644
--- a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/delegated-task.ts
+++ b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/delegated-task.ts
@@ -2,7 +2,7 @@ import type { AcpToolCallUiMessage, AcpToolResultUiMessage } from './types';
type ToolMessage = AcpToolCallUiMessage | AcpToolResultUiMessage;
-export interface DelegatedTaskDetails {
+interface DelegatedTaskDetails {
taskId: string;
prompt: string | null;
}
From 3e4f8ed39c53f739b02c3c707bf78cb7b36e8c9b Mon Sep 17 00:00:00 2001
From: Bruno Bergher
Date: Thu, 27 Aug 2026 18:22:50 +0100
Subject: [PATCH 10/26] refine delegated task card
---
.../acp/DelegatedTaskCard.client.test.tsx | 3 +-
.../messages/acp/DelegatedTaskCard.tsx | 11 ++---
.../fast-agent-task-launcher.test.ts | 46 ++++++++++++++++++-
.../fast-agent/fast-agent-task-launcher.ts | 1 +
4 files changed, 52 insertions(+), 9 deletions(-)
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/DelegatedTaskCard.client.test.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/DelegatedTaskCard.client.test.tsx
index 0205c52ca..f6d845cae 100644
--- a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/DelegatedTaskCard.client.test.tsx
+++ b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/DelegatedTaskCard.client.test.tsx
@@ -43,9 +43,10 @@ describe('DelegatedTaskCard', () => {
);
expect(screen.getByText('Fix checkout')).toBeInTheDocument();
+ expect(screen.getByText('Started coding task')).toBeInTheDocument();
expect(screen.getByText('Working')).toBeInTheDocument();
fireEvent.click(
- screen.getByRole('button', { name: 'View delegated task: Fix checkout' }),
+ screen.getByRole('button', { name: 'View coding task: Fix checkout' }),
);
expect(onOpen).toHaveBeenCalledWith('child-1');
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/DelegatedTaskCard.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/DelegatedTaskCard.tsx
index dc2834459..8047cea93 100644
--- a/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/DelegatedTaskCard.tsx
+++ b/apps/web/src/app/(sandbox)/task/[taskId]/messages/acp/DelegatedTaskCard.tsx
@@ -2,7 +2,7 @@
import { useQuery } from '@tanstack/react-query';
-import { Bot, ChevronRight, Skeleton } from '@/components/system';
+import { ChevronRight, Skeleton } from '@/components/system';
import { TaskStatusIndicator } from '@/components/sandbox';
import { useTRPC } from '@/trpc/client';
@@ -29,16 +29,13 @@ export function DelegatedTaskCard({
return (
)}
- {parentSession ? (
+ {sessionHref ? (
-
+
Go to session
From 3c32f89081adbe98e545056bde297a1e156caefd Mon Sep 17 00:00:00 2001
From: Bruno Bergher
Date: Thu, 27 Aug 2026 18:38:22 +0100
Subject: [PATCH 16/26] style: space standalone task header actions
---
apps/web/src/app/(sandbox)/task/[taskId]/Header.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/Header.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/Header.tsx
index d5fa728cd..749a07a0a 100644
--- a/apps/web/src/app/(sandbox)/task/[taskId]/Header.tsx
+++ b/apps/web/src/app/(sandbox)/task/[taskId]/Header.tsx
@@ -185,7 +185,7 @@ export const Header = ({ session: { taskRun, task, taskId } }: HeaderProps) => {
{parentSession ? (
) : null}
-
+
{parentSession ? (
From 33d03f0bacdd8e9b416be1f6aa55faa97a1da1fc Mon Sep 17 00:00:00 2001
From: Bruno Bergher
Date: Thu, 27 Aug 2026 18:46:56 +0100
Subject: [PATCH 17/26] refine session info panel
---
.../src/app/(sandbox)/SandboxInfoPanel.tsx | 55 ++
.../SessionWorkspace.client.test.tsx | 7 +-
.../sessions/[sessionId]/SessionWorkspace.tsx | 176 ++++--
.../(sandbox)/sessions/[sessionId]/page.tsx | 4 +
.../[taskId]/sidebar-panels/TaskInfoPanel.tsx | 533 +++++++++---------
5 files changed, 445 insertions(+), 330 deletions(-)
create mode 100644 apps/web/src/app/(sandbox)/SandboxInfoPanel.tsx
diff --git a/apps/web/src/app/(sandbox)/SandboxInfoPanel.tsx b/apps/web/src/app/(sandbox)/SandboxInfoPanel.tsx
new file mode 100644
index 000000000..967f94001
--- /dev/null
+++ b/apps/web/src/app/(sandbox)/SandboxInfoPanel.tsx
@@ -0,0 +1,55 @@
+import type { ReactNode } from 'react';
+
+import { SandboxSidePanelHeader } from './SandboxSidePanelHeader';
+
+export function SandboxInfoPanel({
+ title,
+ onClose,
+ closeLabel,
+ header,
+ children,
+}: {
+ title: string;
+ onClose: () => void;
+ closeLabel?: string;
+ header?: ReactNode;
+ children: ReactNode;
+}) {
+ return (
+ <>
+ {header ?? (
+
+ )}
+
+ >
+ );
+}
+
+export function SandboxInfoRow({
+ label,
+ children,
+}: {
+ label: string;
+ children: ReactNode;
+}) {
+ return (
+
+ | {label} |
+ {children} |
+
+ );
+}
+
+export function SandboxInfoTable({ children }: { children: ReactNode }) {
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx
index b3703e869..ba2c9fe09 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx
@@ -37,8 +37,10 @@ const session: SessionInfo = {
ownerImageUrl: null,
surface: 'slack',
model: 'model-1',
+ reasoningEffort: null,
inferenceCostMicroUsd: 1_000_000,
createdAt: new Date('2026-01-01T00:00:00.000Z'),
+ status: 'needs_input',
tasks: [],
};
@@ -95,8 +97,9 @@ describe('SessionWorkspace', () => {
expect(screen.queryByText('Session transcript')).not.toBeInTheDocument();
expect(
- screen.getByRole('heading', { name: 'Session info' }),
+ screen.getByRole('heading', { name: 'Session Info' }),
).toBeInTheDocument();
+ expect(screen.getByText('needs input')).toBeInTheDocument();
expect(
screen.queryByRole('button', { name: 'Close session info' }),
).toBeNull();
@@ -116,7 +119,7 @@ describe('SessionWorkspace', () => {
fireEvent.click(screen.getByRole('button', { name: 'Session info' }));
expect(
- screen.getByRole('heading', { name: 'Session info' }),
+ screen.getByRole('heading', { name: 'Session Info' }),
).toBeInTheDocument();
});
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
index 3b9488d06..89977e198 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
@@ -3,7 +3,7 @@
import Link from 'next/link';
import { useCallback, useEffect, useState, type ReactNode } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
-import { formatDistanceToNow } from 'date-fns';
+import { getReasoningEffortLabel, type ReasoningEffort } from '@roomote/types';
import { formatInferenceCost, getUserDisplayName } from '@/lib';
import { useLaunchTaskModels } from '@/hooks/task-models/useLaunchTaskModels';
@@ -13,9 +13,15 @@ import {
ArrowLeftFromLine,
Avatar,
BasicTooltip,
+ Badge,
+ BrandIcon,
+ Brain,
Button,
+ Calendar,
DollarSign,
+ Globe,
Info,
+ Slack,
X,
Rows4,
Select,
@@ -27,6 +33,11 @@ import {
import type { SessionTaskSummary } from './SessionTaskCards';
import { SandboxSidePanelHeader } from '../../SandboxSidePanelHeader';
+import {
+ SandboxInfoPanel,
+ SandboxInfoRow,
+ SandboxInfoTable,
+} from '../../SandboxInfoPanel';
import {
ResponsiveWorkspacePanels,
SandboxSideActions,
@@ -43,27 +54,56 @@ export type SessionInfo = {
surface: string;
/** Effective model for the session's turns (stored override or default). */
model: string | null;
+ reasoningEffort: ReasoningEffort | null;
inferenceCostMicroUsd: number;
createdAt: Date;
+ status: string | null;
tasks: SessionTaskSummary[];
};
const SURFACE_LABELS: Record = {
slack: 'Slack',
+ linear: 'Linear',
+ github: 'GitHub',
+ gitlab: 'GitLab',
+ gitea: 'Gitea',
+ bitbucket: 'Bitbucket',
+ ado: 'Azure DevOps',
discord: 'Discord',
- teams: 'Microsoft Teams',
+ teams: 'Teams',
telegram: 'Telegram',
automation: 'Automation',
web: 'Web',
};
-function InfoRow({ label, children }: { label: string; children: ReactNode }) {
- return (
-
- | {label} |
- {children} |
-
- );
+type SessionSurfaceBrandIcon =
+ | 'linear'
+ | 'github'
+ | 'gitlab'
+ | 'gitea'
+ | 'bitbucket'
+ | 'ado'
+ | 'discord'
+ | 'teams'
+ | 'telegram';
+
+const SURFACE_BRAND_ICONS: Partial> = {
+ linear: 'linear',
+ github: 'github',
+ gitlab: 'gitlab',
+ gitea: 'gitea',
+ bitbucket: 'bitbucket',
+ ado: 'ado',
+ discord: 'discord',
+ teams: 'teams',
+ telegram: 'telegram',
+};
+
+function getSessionStatusVariant(status: string) {
+ if (status === 'active') return 'success';
+ if (status === 'needs_input') return 'warning';
+ if (status === 'blocked') return 'destructive';
+ return 'secondary';
}
function SessionTaskPanel({
@@ -187,51 +227,85 @@ function SessionInfoPanel({
? (modelData?.models.find(({ id }) => id === session.model)?.displayName ??
session.model)
: null;
+ const modelAndReasoningLabel = [
+ modelLabel ?? 'Default model',
+ session.reasoningEffort
+ ? getReasoningEffortLabel(session.reasoningEffort)
+ : null,
+ ]
+ .filter(Boolean)
+ .join(' • ');
const inferenceCostLabel = formatInferenceCost(session.inferenceCostMicroUsd);
+ const surfaceLabel = SURFACE_LABELS[session.surface] ?? session.surface;
+ const surfaceBrandIcon = SURFACE_BRAND_ICONS[session.surface];
return (
- <>
-
-
-
-
-
-
-
- {ownerDisplayName}
-
-
- {modelLabel ?? 'Default model'}
-
-
-
- {inferenceCostLabel}
-
-
-
-
-
- {formatDistanceToNow(session.createdAt, { addSuffix: true })}
-
-
-
-
- {SURFACE_LABELS[session.surface] ?? session.surface}
-
-
-
-
- >
+
+
+
+
+
+ {ownerDisplayName}
+
+
+
+
+
+ {modelAndReasoningLabel}
+
+
+
+
+
+ {inferenceCostLabel}
+
+
+
+
+
+
+ {session.createdAt.toLocaleString(undefined, {
+ dateStyle: 'medium',
+ timeStyle: 'short',
+ })}
+
+
+
+
+
+ {session.surface === 'slack' ? (
+
+ ) : surfaceBrandIcon ? (
+
+ ) : (
+
+ )}
+ {surfaceLabel}
+
+
+ {session.status ? (
+
+
+ {session.status.replace('_', ' ')}
+
+
+ ) : null}
+
+
);
}
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx
index 9a1c56648..ffa2e6d9b 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx
@@ -60,8 +60,10 @@ export default async function SessionDetailPage({
ownerImageUrl: unifiedSession.ownerImageUrl,
surface: unifiedSession.sourceSurface,
model: session?.model ?? defaultModelId,
+ reasoningEffort: session?.reasoningEffort ?? defaultReasoningEffort,
inferenceCostMicroUsd: unifiedSession.inferenceCostMicroUsd,
createdAt: unifiedSession.createdAt,
+ status: unifiedSession.status,
tasks: unifiedSession.tasks,
};
const statusVariant =
@@ -148,8 +150,10 @@ export default async function SessionDetailPage({
ownerImageUrl: session.ownerImageUrl,
surface: session.surface,
model: session.model ?? defaultModelId,
+ reasoningEffort: session.reasoningEffort ?? defaultReasoningEffort,
inferenceCostMicroUsd: session.inferenceCostMicroUsd,
createdAt: session.createdAt,
+ status: null,
tasks: [],
};
const initialUserMessage = session.messages.find(
diff --git a/apps/web/src/app/(sandbox)/task/[taskId]/sidebar-panels/TaskInfoPanel.tsx b/apps/web/src/app/(sandbox)/task/[taskId]/sidebar-panels/TaskInfoPanel.tsx
index 61d3e12ff..d40f13449 100644
--- a/apps/web/src/app/(sandbox)/task/[taskId]/sidebar-panels/TaskInfoPanel.tsx
+++ b/apps/web/src/app/(sandbox)/task/[taskId]/sidebar-panels/TaskInfoPanel.tsx
@@ -52,6 +52,11 @@ import {
useTaskSummary,
} from '../hooks';
+import {
+ SandboxInfoPanel,
+ SandboxInfoRow,
+ SandboxInfoTable,
+} from '../../../SandboxInfoPanel';
import { SidePanelHeader } from './SidePanelHeader';
import { getTaskParticipants } from './task-participants';
@@ -299,296 +304,270 @@ export function TaskInfoPanel({
PRODUCT_NAME;
return (
- <>
-
-
-
-
-
-
- |
- Creator
- |
-
- {task.user && task.attributionKind === 'user' ? (
- <>
- {task.user.imageUrl ? (
-
- ) : null}
- {taskCreatorDisplayName}
- >
- ) : (
- taskCreatorDisplayName
- )}
- |
-
-
- {participants.length > 0 && (
-
- |
- Participants
- |
-
-
- {participants.map((participant) => (
-
-
- {participant.displayName}
-
- ))}
-
- |
-
- )}
-
- {(taskRun.payload?.environmentId || taskRun.payload?.repo) && (
-
- |
- Workspace
- |
-
-
- |
-
- )}
-
-
- |
- Sandbox Provider
- |
-
-
-
- {sandboxProviderLabel}
-
- |
-
-
- {taskModelLabel && (
-
- |
- Model
- |
-
-
-
- {taskModelLabel}
- {taskRun.payload?.modelRoleOverrides && (
-
- Customized
-
- )}
-
- |
-
- )}
-
-
- |
- Inference Cost
- |
-
-
-
- {inferenceCostLabel}
-
- |
-
-
- {showRuntimeRow && (
-
- |
- Runtime
- |
-
-
-
-
- {HARNESS_LABELS[effectiveHarness]}
-
-
- |
-
- )}
-
- {(taskRun.pullRequests?.length ?? 0) > 0 ? (
-
- |
- Pull Requests
- |
-
-
- {taskRun.pullRequests?.map((pullRequest) => (
-
- ))}
-
- |
-
- ) : taskRun.prRepo && taskRun.prNumber ? (
-
- |
- Pull Request
- |
-
- }
+ >
+
+
+ | Creator |
+
+ {task.user && task.attributionKind === 'user' ? (
+ <>
+ {task.user.imageUrl ? (
+
+ ) : null}
+ {taskCreatorDisplayName}
+ >
+ ) : (
+ taskCreatorDisplayName
+ )}
+ |
+
+
+ {participants.length > 0 && (
+
+ |
+ Participants
+ |
+
+
+ {participants.map((participant) => (
+
+
- |
-
- ) : null}
-
- {linkedWorkItems.length > 0 ? (
-
- |
- Linked Work
- |
-
-
- {linkedWorkItems.map((item, index) => (
-
- ))}
-
- |
-
- ) : null}
-
-
- |
- Started At
- |
-
-
-
-
- {formatStartedAt(taskRun.startedAt)}
-
+ {participant.displayName}
- |
-
-
-
- |
- Started From
- |
-
-
- {startedFrom.brandIcon ? (
- startedFrom.brandIcon === 'slack' ? (
-
- ) : (
-
- )
- ) : (
-
- )}
- {startedFrom.label}
-
- |
-
- |
-
-
- {taskRunError && (
-
-
-
Last Error
-
+ ))}
-
- {taskRunError}
-
-
- )}
+
+
+ )}
+
+ {(taskRun.payload?.environmentId || taskRun.payload?.repo) && (
+
+ | Workspace |
+
+
+ |
+
+ )}
+
+
+ |
+ Sandbox Provider
+ |
+
+
+
+ {sandboxProviderLabel}
+
+ |
+
+
+ {taskModelLabel && (
+
+ | Model |
+
+
+
+ {taskModelLabel}
+ {taskRun.payload?.modelRoleOverrides && (
+
+ Customized
+
+ )}
+
+ |
+
+ )}
+
+
+
+
+ {inferenceCostLabel}
+
+
+
+ {showRuntimeRow && (
+
+ | Runtime |
+
+
+
+
+ {HARNESS_LABELS[effectiveHarness]}
+
+
+ |
+
+ )}
+
+ {(taskRun.pullRequests?.length ?? 0) > 0 ? (
+
+ |
+ Pull Requests
+ |
+
+
+ {taskRun.pullRequests?.map((pullRequest) => (
+
+ ))}
+
+ |
+
+ ) : taskRun.prRepo && taskRun.prNumber ? (
+
+ |
+ Pull Request
+ |
+
+
+ |
+
+ ) : null}
- {summaryEnabled && (
-
-
-
Summary
+ {linkedWorkItems.length > 0 ? (
+
+ |
+ Linked Work
+ |
+
+
+ {linkedWorkItems.map((item, index) => (
+
+ ))}
+ |
+
+ ) : null}
- {isLoadingSummary ? (
-
-
- Generating...
-
- ) : summary ? (
- <>
- {isSummaryStale && (
-
- New messages since last summarized.
- regenerateSummary()}
- >
-
- Regenerate
-
-
- )}
-
-
- {summary}
-
-
- >
- ) : summaryErrorMessage ? (
-
-
{summaryErrorMessage}
+
+
+
+
+ {formatStartedAt(taskRun.startedAt)}
+
+
+
+
+
+
+ {startedFrom.brandIcon ? (
+ startedFrom.brandIcon === 'slack' ? (
+
+ ) : (
+
+ )
+ ) : (
+
+ )}
+ {startedFrom.label}
+
+
+
+
+ {taskRunError && (
+
+
+
Last Error
+
+
+
+ {taskRunError}
+
+
+ )}
+
+ {summaryEnabled && (
+
+
+
Summary
+
+
+ {isLoadingSummary ? (
+
+
+ Generating...
+
+ ) : summary ? (
+ <>
+ {isSummaryStale && (
+
+ New messages since last summarized.
regenerateSummary()}
- className="mt-2"
>
-
- Try again
+
+ Regenerate
- ) : null}
+ )}
+
+
+ {summary}
+
+
+ >
+ ) : summaryErrorMessage ? (
+
+
{summaryErrorMessage}
+
regenerateSummary()}
+ className="mt-2"
+ >
+
+ Try again
+
- )}
+ ) : null}
-
- >
+ )}
+
);
}
From 2c3347cf466594ce53dc0964b00697754bd9e6d0 Mon Sep 17 00:00:00 2001
From: Bruno Bergher
Date: Thu, 27 Aug 2026 18:48:29 +0100
Subject: [PATCH 18/26] fix: frame session info panel
---
.../sessions/[sessionId]/SessionWorkspace.tsx | 133 +++++++++---------
1 file changed, 69 insertions(+), 64 deletions(-)
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
index 89977e198..dd400f38a 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
@@ -7,7 +7,7 @@ import { getReasoningEffortLabel, type ReasoningEffort } from '@roomote/types';
import { formatInferenceCost, getUserDisplayName } from '@/lib';
import { useLaunchTaskModels } from '@/hooks/task-models/useLaunchTaskModels';
-import { WorkspaceSurface } from '@/components/layout';
+import { FramedSurface, WorkspaceSurface } from '@/components/layout';
import { SideNavItem } from '@/components/layout/side-nav/SideNavItem';
import {
ArrowLeftFromLine,
@@ -240,72 +240,77 @@ function SessionInfoPanel({
const surfaceBrandIcon = SURFACE_BRAND_ICONS[session.surface];
return (
-
-
-
-
-
- {ownerDisplayName}
-
-
-
-
-
- {modelAndReasoningLabel}
-
-
-
-
-
- {inferenceCostLabel}
-
-
-
-
-
-
- {session.createdAt.toLocaleString(undefined, {
- dateStyle: 'medium',
- timeStyle: 'short',
- })}
-
-
-
-
-
- {session.surface === 'slack' ? (
-
- ) : surfaceBrandIcon ? (
-
+
+
+
+
- ) : (
-
- )}
- {surfaceLabel}
-
-
- {session.status ? (
-
-
- {session.status.replace('_', ' ')}
-
+ {ownerDisplayName}
+
- ) : null}
-
-
+
+
+
+ {modelAndReasoningLabel}
+
+
+
+
+
+ {inferenceCostLabel}
+
+
+
+
+
+
+ {session.createdAt.toLocaleString(undefined, {
+ dateStyle: 'medium',
+ timeStyle: 'short',
+ })}
+
+
+
+
+
+ {session.surface === 'slack' ? (
+
+ ) : surfaceBrandIcon ? (
+
+ ) : (
+
+ )}
+ {surfaceLabel}
+
+
+ {session.status ? (
+
+
+ {session.status.replace('_', ' ')}
+
+
+ ) : null}
+
+
+
);
}
From 020f83d2c695f08ad35514c98b346b73e3500e21 Mon Sep 17 00:00:00 2001
From: Bruno Bergher
Date: Thu, 27 Aug 2026 18:52:54 +0100
Subject: [PATCH 19/26] feat: make sessions the primary navigation
---
.../layout/CommandPalette.client.test.tsx | 12 +++++------
.../src/components/layout/CommandPalette.tsx | 6 +++---
apps/web/src/components/layout/RouteTitle.tsx | 2 +-
.../components/layout/navbar/NavbarDrawer.tsx | 7 ++-----
.../layout/navigation-items.test.ts | 8 +++----
.../src/components/layout/navigation-items.ts | 21 +++++--------------
.../components/layout/side-nav/SideNav.tsx | 10 +++------
7 files changed, 24 insertions(+), 42 deletions(-)
diff --git a/apps/web/src/components/layout/CommandPalette.client.test.tsx b/apps/web/src/components/layout/CommandPalette.client.test.tsx
index 913c854a9..3fe9f5198 100644
--- a/apps/web/src/components/layout/CommandPalette.client.test.tsx
+++ b/apps/web/src/components/layout/CommandPalette.client.test.tsx
@@ -201,10 +201,10 @@ describe('CommandPalette', () => {
it('navigates using static navigation items', () => {
render();
- fireEvent.click(screen.getByRole('button', { name: 'Tasks' }));
+ fireEvent.click(screen.getByRole('button', { name: 'Sessions' }));
expect(setOpen).toHaveBeenCalledWith(false);
- expect(push).toHaveBeenCalledWith('/tasks');
+ expect(push).toHaveBeenCalledWith('/sessions');
});
it('lists navigation items in the expected order', () => {
@@ -216,7 +216,7 @@ describe('CommandPalette', () => {
.filter((label): label is string =>
[
'New Task',
- 'Tasks',
+ 'Sessions',
'Automations',
'Analytics',
'Settings',
@@ -224,7 +224,7 @@ describe('CommandPalette', () => {
].includes(label ?? ''),
);
- expect(navItems).toEqual(['New Task', 'Tasks', 'Settings', 'Help']);
+ expect(navItems).toEqual(['New Task', 'Sessions', 'Settings', 'Help']);
});
it('lets admins find and open recurring automations', () => {
@@ -247,7 +247,7 @@ describe('CommandPalette', () => {
.filter((label): label is string =>
[
'New Task',
- 'Tasks',
+ 'Sessions',
'Automations',
'Analytics',
'Settings',
@@ -256,7 +256,7 @@ describe('CommandPalette', () => {
);
expect(navItems).toEqual([
'New Task',
- 'Tasks',
+ 'Sessions',
'Automations',
'Analytics',
'Settings',
diff --git a/apps/web/src/components/layout/CommandPalette.tsx b/apps/web/src/components/layout/CommandPalette.tsx
index bc0d521c5..99ff4caa6 100644
--- a/apps/web/src/components/layout/CommandPalette.tsx
+++ b/apps/web/src/components/layout/CommandPalette.tsx
@@ -113,8 +113,8 @@ function AuthorizedCommandPalette() {
{ icon: Plus, label: 'New Task', href: '/' },
{
icon: GalleryVerticalEnd,
- label: user?.featureFlags?.sessions_ui ? 'Sessions' : 'Tasks',
- href: user?.featureFlags?.sessions_ui ? '/sessions' : '/tasks',
+ label: 'Sessions',
+ href: '/sessions',
},
{ icon: Settings, label: 'Settings', href: '/settings' },
{ icon: HelpCircle, label: 'Help', action: 'contact-support' },
@@ -137,7 +137,7 @@ function AuthorizedCommandPalette() {
);
}
return items;
- }, [user?.featureFlags?.sessions_ui, user?.isAdmin]);
+ }, [user?.isAdmin]);
// Debounce search input
useEffect(() => {
diff --git a/apps/web/src/components/layout/RouteTitle.tsx b/apps/web/src/components/layout/RouteTitle.tsx
index d186a58bc..da7c01229 100644
--- a/apps/web/src/components/layout/RouteTitle.tsx
+++ b/apps/web/src/components/layout/RouteTitle.tsx
@@ -7,7 +7,7 @@ import { getSettingsTitleForPath } from '@/components/settings/settings-navigati
const ROUTE_TITLES: [RegExp, string][] = [
[/^\/analytics$/, 'Analytics'],
- [/^\/tasks$/, 'Tasks'],
+ [/^\/sessions$/, 'Sessions'],
[/^\/$/, 'Home'],
];
diff --git a/apps/web/src/components/layout/navbar/NavbarDrawer.tsx b/apps/web/src/components/layout/navbar/NavbarDrawer.tsx
index 066357025..e821b965c 100644
--- a/apps/web/src/components/layout/navbar/NavbarDrawer.tsx
+++ b/apps/web/src/components/layout/navbar/NavbarDrawer.tsx
@@ -17,11 +17,8 @@ import { getVisiblePrimaryNavItems } from '../navigation-items';
export const NavbarDrawer = () => {
const pathname = usePathname();
- const { isAdmin, featureFlags } = useAuthorizedUser();
- const visibleNavItems = getVisiblePrimaryNavItems({
- isAdmin,
- sessionsUi: featureFlags?.sessions_ui === true,
- });
+ const { isAdmin } = useAuthorizedUser();
+ const visibleNavItems = getVisiblePrimaryNavItems({ isAdmin });
const [open, setOpen] = useState(false);
diff --git a/apps/web/src/components/layout/navigation-items.test.ts b/apps/web/src/components/layout/navigation-items.test.ts
index 6d8aab869..f5c6726a2 100644
--- a/apps/web/src/components/layout/navigation-items.test.ts
+++ b/apps/web/src/components/layout/navigation-items.test.ts
@@ -1,12 +1,12 @@
import { getVisiblePrimaryNavItems } from './navigation-items';
describe('getVisiblePrimaryNavItems', () => {
- it('places task history before automations for admins', () => {
+ it('places sessions before automations for admins', () => {
const items = getVisiblePrimaryNavItems({ isAdmin: true });
expect(items.map((item) => item.href)).toEqual([
'/',
- '/tasks',
+ '/sessions',
'/automations',
'/analytics',
]);
@@ -17,7 +17,7 @@ describe('getVisiblePrimaryNavItems', () => {
isAdmin: false,
});
- expect(items.map((item) => item.href)).toEqual(['/', '/tasks']);
+ expect(items.map((item) => item.href)).toEqual(['/', '/sessions']);
});
it('hides automations from non-admins', () => {
@@ -25,6 +25,6 @@ describe('getVisiblePrimaryNavItems', () => {
isAdmin: false,
});
- expect(items.map((item) => item.href)).toEqual(['/', '/tasks']);
+ expect(items.map((item) => item.href)).toEqual(['/', '/sessions']);
});
});
diff --git a/apps/web/src/components/layout/navigation-items.ts b/apps/web/src/components/layout/navigation-items.ts
index 3436fd698..1df2022dd 100644
--- a/apps/web/src/components/layout/navigation-items.ts
+++ b/apps/web/src/components/layout/navigation-items.ts
@@ -23,11 +23,11 @@ const PRIMARY_NAV_ITEMS: PrimaryNavItem[] = [
},
{
icon: Rows4,
- href: '/tasks',
- label: 'Tasks',
- description: 'View current and past tasks',
+ href: '/sessions',
+ label: 'Sessions',
+ description: 'View conversations and delegated work',
matchExact: false,
- matchPaths: ['/tasks', '/cloud-agents'],
+ matchPaths: ['/sessions', '/tasks', '/cloud-agents'],
},
{
icon: Zap,
@@ -51,17 +51,6 @@ const PRIMARY_NAV_ITEMS: PrimaryNavItem[] = [
export function getVisiblePrimaryNavItems(opts: {
isAdmin: boolean;
- sessionsUi?: boolean;
}): PrimaryNavItem[] {
- return PRIMARY_NAV_ITEMS.map((item) =>
- item.href === '/tasks' && opts.sessionsUi
- ? {
- ...item,
- href: '/sessions',
- label: 'Sessions',
- description: 'View conversations and delegated work',
- matchPaths: ['/sessions', '/tasks', '/cloud-agents'],
- }
- : item,
- ).filter((item) => !item.adminOnly || opts.isAdmin);
+ return PRIMARY_NAV_ITEMS.filter((item) => !item.adminOnly || opts.isAdmin);
}
diff --git a/apps/web/src/components/layout/side-nav/SideNav.tsx b/apps/web/src/components/layout/side-nav/SideNav.tsx
index 24f1aafc6..de06fdb48 100644
--- a/apps/web/src/components/layout/side-nav/SideNav.tsx
+++ b/apps/web/src/components/layout/side-nav/SideNav.tsx
@@ -71,7 +71,7 @@ export const SideNav = () => {
const pathname = usePathname();
const { setOpen: openCommandPalette } = useCommandPalette();
- const { isAdmin, featureFlags } = useAuthorizedUser();
+ const { isAdmin } = useAuthorizedUser();
const hasHydrated = useLayoutStore((state) => state.hasHydrated);
const persistedIsSideNavExpanded = useLayoutStore(
(state) => state.isSideNavExpanded,
@@ -204,12 +204,8 @@ export const SideNav = () => {
return Array.from(groups.values());
}, [environmentNameById, nonPinnedQuickAccessTasks]);
const visibleNavItems = useMemo(
- () =>
- getVisiblePrimaryNavItems({
- isAdmin,
- sessionsUi: featureFlags?.sessions_ui === true,
- }),
- [featureFlags?.sessions_ui, isAdmin],
+ () => getVisiblePrimaryNavItems({ isAdmin }),
+ [isAdmin],
);
return (
From a0e470aafdda7389a60e1d32a7a3ea3b90481dc4 Mon Sep 17 00:00:00 2001
From: Bruno Bergher
Date: Thu, 27 Aug 2026 19:00:22 +0100
Subject: [PATCH 20/26] feat: list session tasks in side panel
---
.../SessionWorkspace.client.test.tsx | 66 +++++++++++++++-
.../sessions/[sessionId]/SessionWorkspace.tsx | 78 +++++++++++++++----
2 files changed, 126 insertions(+), 18 deletions(-)
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx
index ba2c9fe09..88f74019e 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx
@@ -30,6 +30,26 @@ vi.mock('./NestedTaskSidePanel', () => ({
),
}));
+vi.mock('../../task/[taskId]/messages/acp/DelegatedTaskCard', () => ({
+ DelegatedTaskCard: ({
+ taskId,
+ prompt,
+ onOpen,
+ }: {
+ taskId: string;
+ prompt: string | null;
+ onOpen: (taskId: string) => void;
+ }) => (
+ onOpen(taskId)}
+ >
+ {prompt}
+
+ ),
+}));
+
const session: SessionInfo = {
id: 'session-1',
ownerName: 'Test User',
@@ -63,15 +83,19 @@ function SandboxLayoutProvider({ children }: { children: ReactNode }) {
function renderWorkspace({
isMobile,
children = Session transcript
,
+ sessionOverride,
}: {
isMobile: boolean;
children?: ReactNode;
+ sessionOverride?: Partial;
}) {
useMediaQueryMock.mockReturnValue(!isMobile);
render(
- {children}
+
+ {children}
+
,
);
}
@@ -134,6 +158,46 @@ describe('SessionWorkspace', () => {
).toBeInTheDocument();
});
+ it('disables the Tasks panel button until the session has a task', () => {
+ renderWorkspace({ isMobile: false });
+
+ expect(screen.getByRole('button', { name: 'Tasks' })).toBeDisabled();
+ });
+
+ it('lists session tasks with delegated task cards', () => {
+ renderWorkspace({
+ isMobile: false,
+ sessionOverride: {
+ tasks: [
+ {
+ taskId: 'task-1',
+ title: 'Update homepage background',
+ workflow: 'standard',
+ state: 'active',
+ repositoryName: null,
+ latestOutput: null,
+ inferenceCostMicroUsd: 0,
+ canAccessDetails: true,
+ latestRun: null,
+ artifacts: [],
+ pullRequests: [],
+ },
+ ],
+ },
+ });
+
+ fireEvent.click(screen.getByRole('button', { name: 'Tasks' }));
+
+ expect(screen.getByRole('heading', { name: 'Tasks' })).toBeInTheDocument();
+ fireEvent.click(
+ screen.getByRole('button', {
+ name: 'View coding task: Update homepage background',
+ }),
+ );
+
+ expect(screen.getByText('Nested panel task-1')).toBeInTheDocument();
+ });
+
it('opens delegated tasks in the existing session side-panel slot', () => {
renderWorkspace({ isMobile: false, children: });
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
index dd400f38a..2231f329e 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
@@ -45,6 +45,7 @@ import {
import { useSandboxLayout } from '../../use-sandbox-layout';
import { NestedTaskSidePanel } from './NestedTaskSidePanel';
import { OpenSessionTaskPanelContext } from './session-task-panel-context';
+import { DelegatedTaskCard } from '../../task/[taskId]/messages/acp/DelegatedTaskCard';
export type SessionInfo = {
id: string;
@@ -210,6 +211,39 @@ function SessionTaskPanel({
);
}
+function SessionTasksPanel({
+ tasks,
+ onOpenTask,
+ onClose,
+}: {
+ tasks: SessionTaskSummary[];
+ onOpenTask: (taskId: string) => void;
+ onClose: () => void;
+}) {
+ return (
+
+
+
+ {tasks.map((task) => (
+
+ ))}
+
+
+ );
+}
+
function SessionInfoPanel({
session,
onClose,
@@ -322,6 +356,7 @@ export function SessionWorkspace({
children: ReactNode;
}) {
const [isInfoOpen, setIsInfoOpen] = useState(false);
+ const [isTasksOpen, setIsTasksOpen] = useState(false);
const [nestedTaskId, setNestedTaskId] = useState(null);
const router = useRouter();
const searchParams = useSearchParams();
@@ -330,7 +365,7 @@ export function SessionWorkspace({
(task) => task.taskId === selectedTaskId,
);
const panelOpen =
- isInfoOpen || Boolean(selectedTask) || Boolean(nestedTaskId);
+ isInfoOpen || isTasksOpen || Boolean(selectedTask) || Boolean(nestedTaskId);
const selectTask = useCallback(
(taskId: string | null) => {
@@ -344,14 +379,15 @@ export function SessionWorkspace({
);
useEffect(() => {
- if (!selectedTaskId && session.tasks.length === 1) {
+ if (!isTasksOpen && !selectedTaskId && session.tasks.length === 1) {
selectTask(session.tasks[0]!.taskId);
}
- }, [selectTask, selectedTaskId, session.tasks]);
+ }, [isTasksOpen, selectTask, selectedTaskId, session.tasks]);
const openTaskPanel = useCallback(
(taskId: string) => {
setIsInfoOpen(false);
+ setIsTasksOpen(false);
setNestedTaskId(taskId);
selectTask(null);
},
@@ -359,6 +395,7 @@ export function SessionWorkspace({
);
const closePanel = () => {
setIsInfoOpen(false);
+ setIsTasksOpen(false);
setNestedTaskId(null);
selectTask(null);
};
@@ -372,6 +409,12 @@ export function SessionWorkspace({
onSelect={selectTask}
onClose={closePanel}
/>
+ ) : isTasksOpen ? (
+
) : (
);
@@ -391,25 +434,26 @@ export function SessionWorkspace({
active={isInfoOpen && !selectedTask && !nestedTaskId}
icon={Info}
onClick={() => {
+ setIsTasksOpen(false);
setNestedTaskId(null);
selectTask(null);
setIsInfoOpen((previous) => !previous);
}}
/>
- {session.tasks.length ? (
- {
- setNestedTaskId(null);
- setIsInfoOpen(false);
- selectTask(selectedTask ? null : session.tasks[0]!.taskId);
- }}
- />
- ) : null}
+ {
+ setNestedTaskId(null);
+ setIsInfoOpen(false);
+ selectTask(null);
+ setIsTasksOpen((previous) => !previous);
+ }}
+ />
{!isSidebarVisible && !panelOpen ? (
From 7f4474256befaa069448bc954138123782b7a969 Mon Sep 17 00:00:00 2001
From: Bruno Bergher
Date: Thu, 27 Aug 2026 19:14:00 +0100
Subject: [PATCH 21/26] fix: load tasks for fast session URLs
---
.../sessions/[sessionId]/page.test.tsx | 101 ++++++++++++++++--
.../(sandbox)/sessions/[sessionId]/page.tsx | 4 +-
apps/web/src/lib/server/sessions.ts | 24 ++++-
3 files changed, 114 insertions(+), 15 deletions(-)
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.test.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.test.tsx
index fca2933e1..dfcc6574b 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.test.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.test.tsx
@@ -1,17 +1,25 @@
import type { ReactNode } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
-const { authorizeMock, getFastSessionByIdMock, transcriptMock } = vi.hoisted(
- () => ({
- authorizeMock: vi.fn(),
- getFastSessionByIdMock: vi.fn(),
- transcriptMock: vi.fn(
- ({ footer }: { messages: unknown[]; footer?: ReactNode }) => (
- {footer}
- ),
+const {
+ authorizeMock,
+ getFastSessionByIdMock,
+ getSessionByIdCommandMock,
+ transcriptMock,
+ sessionWorkspaceMock,
+} = vi.hoisted(() => ({
+ authorizeMock: vi.fn(),
+ getFastSessionByIdMock: vi.fn(),
+ getSessionByIdCommandMock: vi.fn(),
+ transcriptMock: vi.fn(
+ ({ footer }: { messages: unknown[]; footer?: ReactNode }) => (
+ {footer}
),
- }),
-);
+ ),
+ sessionWorkspaceMock: vi.fn(({ children }: { children: ReactNode }) => (
+ {children}
+ )),
+}));
vi.mock('@/lib/server/auth-context', () => ({ authorize: authorizeMock }));
vi.mock('next/navigation', () => ({
@@ -21,6 +29,9 @@ vi.mock('next/navigation', () => ({
vi.mock('@/lib/server/fast-sessions', () => ({
getFastSessionById: getFastSessionByIdMock,
}));
+vi.mock('@/trpc/commands/sessions', () => ({
+ getSessionByIdCommand: getSessionByIdCommandMock,
+}));
vi.mock('../../use-sandbox-layout', () => ({
useSandboxLayout: () => ({
isSidebarVisible: true,
@@ -39,10 +50,21 @@ vi.mock('@/components/layout', () => ({
vi.mock('./FastSessionTranscript', () => ({
FastSessionTranscript: transcriptMock,
}));
+vi.mock('./SessionWorkspace', () => ({
+ SessionWorkspace: sessionWorkspaceMock,
+}));
+vi.mock('./SessionReadTracker', () => ({
+ SessionReadTracker: () => null,
+}));
import SessionDetailPage from './page';
describe('Fast session detail page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ getSessionByIdCommandMock.mockResolvedValue(null);
+ });
+
it('uses the shared task workspace and renders supported session data', async () => {
authorizeMock.mockResolvedValue({
success: true,
@@ -165,4 +187,63 @@ describe('Fast session detail page', () => {
undefined,
);
});
+
+ it('loads linked tasks for Fast session URLs when Sessions UI is disabled', async () => {
+ authorizeMock.mockResolvedValue({
+ success: true,
+ userId: 'user-1',
+ isAdmin: false,
+ featureFlags: { sessions_ui: false },
+ });
+ getSessionByIdCommandMock.mockResolvedValue({
+ id: 'unified-session-1',
+ title: 'Session title',
+ ownerName: 'User',
+ ownerEmail: 'user@example.com',
+ ownerImageUrl: null,
+ sourceSurface: 'slack',
+ fastConversationId: 'fast-session-3',
+ inferenceCostMicroUsd: 0,
+ createdAt: new Date('2026-01-01T00:00:00.000Z'),
+ status: 'active',
+ tasks: [
+ {
+ taskId: 'task-1',
+ title: 'Delegated task',
+ },
+ ],
+ });
+ getFastSessionByIdMock.mockResolvedValue({
+ id: 'fast-session-3',
+ ownerName: 'User',
+ ownerEmail: 'user@example.com',
+ surface: 'slack',
+ model: null,
+ reasoningEffort: null,
+ inferenceCostMicroUsd: 0,
+ createdAt: new Date('2026-01-01T00:00:00.000Z'),
+ messages: [],
+ hasOlderMessages: false,
+ });
+
+ renderToStaticMarkup(
+ await SessionDetailPage({
+ params: Promise.resolve({ sessionId: 'fast-session-3' }),
+ }),
+ );
+
+ expect(getSessionByIdCommandMock).toHaveBeenCalledWith(
+ expect.objectContaining({ userId: 'user-1' }),
+ 'fast-session-3',
+ );
+ expect(sessionWorkspaceMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ session: expect.objectContaining({
+ id: 'unified-session-1',
+ tasks: [expect.objectContaining({ taskId: 'task-1' })],
+ }),
+ }),
+ undefined,
+ );
+ });
});
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx
index ffa2e6d9b..6e137dc1f 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx
@@ -31,9 +31,7 @@ export default async function SessionDetailPage({
notFound();
}
- const unifiedSession = authorizedUser.featureFlags?.sessions_ui
- ? await getSessionByIdCommand(authorizedUser, sessionId)
- : null;
+ const unifiedSession = await getSessionByIdCommand(authorizedUser, sessionId);
const session = unifiedSession?.fastConversationId
? await getFastSessionById(
authorizedUser,
diff --git a/apps/web/src/lib/server/sessions.ts b/apps/web/src/lib/server/sessions.ts
index 3c7cc4644..4f3f6c304 100644
--- a/apps/web/src/lib/server/sessions.ts
+++ b/apps/web/src/lib/server/sessions.ts
@@ -419,6 +419,24 @@ export async function findAccessibleSession(
return session ?? null;
}
+async function findAccessibleSessionByFastConversationId(
+ auth: SessionAuth,
+ fastConversationId: string,
+) {
+ const [session] = await db
+ .select(baseSelection)
+ .from(sessions)
+ .leftJoin(users, eq(users.id, sessions.ownerUserId))
+ .where(
+ and(
+ eq(sessions.fastConversationId, fastConversationId),
+ sessionScope(auth),
+ ),
+ )
+ .limit(1);
+ return session ?? null;
+}
+
async function getSessionTasks(sessionId: string) {
const linked = await db
.select({
@@ -506,10 +524,12 @@ async function getSessionTasks(sessionId: string) {
}
export async function getSessionById(auth: SessionAuth, sessionId: string) {
- const session = await findAccessibleSession(auth, sessionId);
+ const session =
+ (await findAccessibleSession(auth, sessionId)) ??
+ (await findAccessibleSessionByFastConversationId(auth, sessionId));
if (!session) return null;
const [hydrated] = await hydrateSessionRows(auth, [session]);
- const sessionTaskDetails = await getSessionTasks(sessionId);
+ const sessionTaskDetails = await getSessionTasks(session.id);
const liveStatus = deriveSessionStatus({
conversationResponding:
Boolean(session.fastConversationId) && session.cachedStatus === 'active',
From b1f38c51634683da5259de7a968a53aaf3be6d30 Mon Sep 17 00:00:00 2001
From: Bruno Bergher
Date: Thu, 27 Aug 2026 19:18:51 +0100
Subject: [PATCH 22/26] fix: label untitled sessions as new
---
apps/web/src/app/(authenticated)/sessions/FastSessionCard.tsx | 2 +-
.../[sessionId]/FastSessionTranscript.client.test.tsx | 4 ++--
.../(sandbox)/sessions/[sessionId]/FastSessionTranscript.tsx | 2 +-
apps/web/src/app/(sandbox)/sessions/[sessionId]/page.test.tsx | 2 +-
apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx | 2 +-
5 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/apps/web/src/app/(authenticated)/sessions/FastSessionCard.tsx b/apps/web/src/app/(authenticated)/sessions/FastSessionCard.tsx
index 76afd2cd7..848b14aa4 100644
--- a/apps/web/src/app/(authenticated)/sessions/FastSessionCard.tsx
+++ b/apps/web/src/app/(authenticated)/sessions/FastSessionCard.tsx
@@ -39,7 +39,7 @@ export function FastSessionCard({
const activityDate = new Date(session.updatedAt);
const title =
session.title ??
- (session.surface === 'web' ? 'Session' : session.conversationId);
+ (session.surface === 'web' ? 'New session' : session.conversationId);
return (
{
,
);
- expect(screen.getByText('Session')).toBeInTheDocument();
+ expect(screen.getByText('New session')).toBeInTheDocument();
act(() => {
FakeEventSource.instances[0]!.emit('session', {
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/FastSessionTranscript.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/FastSessionTranscript.tsx
index e84fc5666..dffe01ec1 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/FastSessionTranscript.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/FastSessionTranscript.tsx
@@ -64,7 +64,7 @@ export function FastSessionTranscript({
hasOlderMessages,
canReply,
initialTitle = null,
- fallbackTitle = 'Session',
+ fallbackTitle = 'New session',
sessionModel = null,
sessionReasoningEffort = null,
defaultModelId = null,
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.test.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.test.tsx
index dfcc6574b..6726138a0 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.test.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.test.tsx
@@ -182,7 +182,7 @@ describe('Fast session detail page', () => {
sessionId: 'session-2',
canReply: true,
initialTitle: 'Rotate the API keys',
- fallbackTitle: 'Session',
+ fallbackTitle: 'New session',
}),
undefined,
);
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx
index 6e137dc1f..5e63f5f4d 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx
@@ -159,7 +159,7 @@ export default async function SessionDetailPage({
);
const fallbackTitle =
getTextFromContentBlocks(initialUserMessage?.contentBlocks ?? [])?.trim() ||
- 'Session';
+ 'New session';
return (
From 6035b167c24c4f16eaf52a1920dce89687dbe373 Mon Sep 17 00:00:00 2001
From: Bruno Bergher
Date: Thu, 27 Aug 2026 19:24:23 +0100
Subject: [PATCH 23/26] fix: link delegated fast tasks to sessions
---
.../src/server/__tests__/enqueue-task.test.ts | 50 +++++++++++++++++++
.../cloud-agents/src/server/task-run-queue.ts | 27 ++++++----
2 files changed, 68 insertions(+), 9 deletions(-)
diff --git a/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts b/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts
index b2b0a0ba1..42a876a9c 100644
--- a/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts
+++ b/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts
@@ -24,6 +24,7 @@ import {
TASK_KICKOFF_MESSAGE_SOURCE,
ACP_ENVELOPE_EVENT_TYPES,
ROOMOTE_RUNTIME_TASK_MESSAGE_PROTOCOL,
+ buildFastAgentChildTaskMetadata,
} from '@roomote/types';
import {
db,
@@ -36,6 +37,7 @@ import {
taskPullRequests,
taskRunEvents,
deploymentSettings,
+ sessions,
users,
environments,
environmentRepositoryMappings,
@@ -63,6 +65,7 @@ import {
} from '../task-run-queue';
import { LLM_TITLE_LOCKED_CHECKPOINT } from '../llm-task-title';
import { applyTaskModelSelectionToRun } from '../task-model-selection';
+import { fastAgentConversationRepository } from '../fast-agent/fast-agent-conversation-repository';
const createdTaskIds: string[] = [];
const createdUserIds: string[] = [];
@@ -962,6 +965,53 @@ describe('enqueueTask Session linkage', () => {
db.select().from(sessionTasks).where(eq(sessionTasks.taskId, run.taskId)),
).resolves.toEqual([]);
});
+
+ it('links a delegated Fast task when the general Sessions data rollout is off', async () => {
+ await db
+ .update(deploymentSettings)
+ .set({ metadata: {} })
+ .where(eq(deploymentSettings.id, 'default'));
+ invalidateDeploymentFeatureFlagCache();
+
+ const userId = await createUser();
+ const conversation = {
+ surface: 'web' as const,
+ workspaceId: userId,
+ conversationId: `session-link-${userId}`,
+ };
+ const fastSession = await fastAgentConversationRepository.getOrCreate({
+ userId,
+ conversation,
+ });
+ const run = await launchFresh({
+ task: standardTaskInput({
+ payload: {
+ repo: 'acme/widgets',
+ description: 'Delegated Fast task',
+ ...buildFastAgentChildTaskMetadata({
+ sessionId: fastSession.id,
+ conversation,
+ }),
+ },
+ }),
+ initiator: { kind: 'user', userId },
+ workflow: 'standard',
+ surface: 'web',
+ trigger: 'message',
+ });
+
+ const [link] = await db
+ .select()
+ .from(sessionTasks)
+ .where(eq(sessionTasks.taskId, run.taskId));
+ const [session] = await db
+ .select({ fastConversationId: sessions.fastConversationId })
+ .from(sessions)
+ .where(eq(sessions.id, link!.sessionId));
+
+ expect(link?.origin).toBe('fast_delegation');
+ expect(session?.fastConversationId).toBe(fastSession.id);
+ });
});
describe('enqueueTask snapshot resume', () => {
diff --git a/packages/cloud-agents/src/server/task-run-queue.ts b/packages/cloud-agents/src/server/task-run-queue.ts
index 5466f6fa7..7b55b373e 100644
--- a/packages/cloud-agents/src/server/task-run-queue.ts
+++ b/packages/cloud-agents/src/server/task-run-queue.ts
@@ -51,6 +51,7 @@ import {
ensureSessionForTask,
isChatGptSubscriptionConnected,
createTaskWithRetry,
+ fastAgentConversations,
markTaskStartParallelCountEndedAt,
projectPendingPrReviewEventsForAssociation,
recordTaskStartParallelCount,
@@ -1487,6 +1488,19 @@ async function enqueueFreshLaunch(
});
const repositoryName = taskWithHarnessOverrides.payload.repo || null;
+ const fastParent = getFastAgentParentFromPayload(
+ taskWithHarnessOverrides.payload,
+ );
+ const hasFastParent = fastParent
+ ? await db.query.fastAgentConversations.findFirst({
+ where: eq(fastAgentConversations.id, fastParent.sessionId),
+ columns: { id: true },
+ })
+ : null;
+ const fastConversationId = hasFastParent ? fastParent?.sessionId : null;
+ // Fast task cards are part of the primary Sessions UI, so their relationship
+ // must be available immediately even while the general data rollout is off.
+ const shouldLinkSession = sessionsDataEnabled || fastConversationId !== null;
const resolvedTaskPolicy = resolveTaskRuntimePolicy({
taskType: taskWithHarnessOverrides.type,
launchClass:
@@ -1645,12 +1659,10 @@ async function enqueueFreshLaunch(
});
if (activeRun) {
- if (sessionsDataEnabled) {
+ if (shouldLinkSession) {
await ensureSessionForTask(tx, {
taskId: existingTask.id,
- fastConversationId:
- getFastAgentParentFromPayload(taskWithHarnessOverrides.payload)
- ?.sessionId ?? null,
+ fastConversationId,
origin: 'follow_up',
existingTaskReused: true,
});
@@ -1704,13 +1716,10 @@ async function enqueueFreshLaunch(
taskId = createdTask.id;
}
- if (sessionsDataEnabled) {
- const fastParent = getFastAgentParentFromPayload(
- taskWithHarnessOverrides.payload,
- );
+ if (shouldLinkSession) {
await ensureSessionForTask(tx, {
taskId,
- fastConversationId: fastParent?.sessionId ?? null,
+ fastConversationId,
origin: fastParent
? 'fast_delegation'
: existingTask
From a9e86926cdc4f9f4f8bf04dc6005f852338a30b9 Mon Sep 17 00:00:00 2001
From: Bruno Bergher
Date: Thu, 27 Aug 2026 19:27:37 +0100
Subject: [PATCH 24/26] Revert "fix: link delegated fast tasks to sessions"
This reverts commit 6035b167c24c4f16eaf52a1920dce89687dbe373.
---
.../src/server/__tests__/enqueue-task.test.ts | 50 -------------------
.../cloud-agents/src/server/task-run-queue.ts | 27 ++++------
2 files changed, 9 insertions(+), 68 deletions(-)
diff --git a/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts b/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts
index 42a876a9c..b2b0a0ba1 100644
--- a/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts
+++ b/packages/cloud-agents/src/server/__tests__/enqueue-task.test.ts
@@ -24,7 +24,6 @@ import {
TASK_KICKOFF_MESSAGE_SOURCE,
ACP_ENVELOPE_EVENT_TYPES,
ROOMOTE_RUNTIME_TASK_MESSAGE_PROTOCOL,
- buildFastAgentChildTaskMetadata,
} from '@roomote/types';
import {
db,
@@ -37,7 +36,6 @@ import {
taskPullRequests,
taskRunEvents,
deploymentSettings,
- sessions,
users,
environments,
environmentRepositoryMappings,
@@ -65,7 +63,6 @@ import {
} from '../task-run-queue';
import { LLM_TITLE_LOCKED_CHECKPOINT } from '../llm-task-title';
import { applyTaskModelSelectionToRun } from '../task-model-selection';
-import { fastAgentConversationRepository } from '../fast-agent/fast-agent-conversation-repository';
const createdTaskIds: string[] = [];
const createdUserIds: string[] = [];
@@ -965,53 +962,6 @@ describe('enqueueTask Session linkage', () => {
db.select().from(sessionTasks).where(eq(sessionTasks.taskId, run.taskId)),
).resolves.toEqual([]);
});
-
- it('links a delegated Fast task when the general Sessions data rollout is off', async () => {
- await db
- .update(deploymentSettings)
- .set({ metadata: {} })
- .where(eq(deploymentSettings.id, 'default'));
- invalidateDeploymentFeatureFlagCache();
-
- const userId = await createUser();
- const conversation = {
- surface: 'web' as const,
- workspaceId: userId,
- conversationId: `session-link-${userId}`,
- };
- const fastSession = await fastAgentConversationRepository.getOrCreate({
- userId,
- conversation,
- });
- const run = await launchFresh({
- task: standardTaskInput({
- payload: {
- repo: 'acme/widgets',
- description: 'Delegated Fast task',
- ...buildFastAgentChildTaskMetadata({
- sessionId: fastSession.id,
- conversation,
- }),
- },
- }),
- initiator: { kind: 'user', userId },
- workflow: 'standard',
- surface: 'web',
- trigger: 'message',
- });
-
- const [link] = await db
- .select()
- .from(sessionTasks)
- .where(eq(sessionTasks.taskId, run.taskId));
- const [session] = await db
- .select({ fastConversationId: sessions.fastConversationId })
- .from(sessions)
- .where(eq(sessions.id, link!.sessionId));
-
- expect(link?.origin).toBe('fast_delegation');
- expect(session?.fastConversationId).toBe(fastSession.id);
- });
});
describe('enqueueTask snapshot resume', () => {
diff --git a/packages/cloud-agents/src/server/task-run-queue.ts b/packages/cloud-agents/src/server/task-run-queue.ts
index 7b55b373e..5466f6fa7 100644
--- a/packages/cloud-agents/src/server/task-run-queue.ts
+++ b/packages/cloud-agents/src/server/task-run-queue.ts
@@ -51,7 +51,6 @@ import {
ensureSessionForTask,
isChatGptSubscriptionConnected,
createTaskWithRetry,
- fastAgentConversations,
markTaskStartParallelCountEndedAt,
projectPendingPrReviewEventsForAssociation,
recordTaskStartParallelCount,
@@ -1488,19 +1487,6 @@ async function enqueueFreshLaunch(
});
const repositoryName = taskWithHarnessOverrides.payload.repo || null;
- const fastParent = getFastAgentParentFromPayload(
- taskWithHarnessOverrides.payload,
- );
- const hasFastParent = fastParent
- ? await db.query.fastAgentConversations.findFirst({
- where: eq(fastAgentConversations.id, fastParent.sessionId),
- columns: { id: true },
- })
- : null;
- const fastConversationId = hasFastParent ? fastParent?.sessionId : null;
- // Fast task cards are part of the primary Sessions UI, so their relationship
- // must be available immediately even while the general data rollout is off.
- const shouldLinkSession = sessionsDataEnabled || fastConversationId !== null;
const resolvedTaskPolicy = resolveTaskRuntimePolicy({
taskType: taskWithHarnessOverrides.type,
launchClass:
@@ -1659,10 +1645,12 @@ async function enqueueFreshLaunch(
});
if (activeRun) {
- if (shouldLinkSession) {
+ if (sessionsDataEnabled) {
await ensureSessionForTask(tx, {
taskId: existingTask.id,
- fastConversationId,
+ fastConversationId:
+ getFastAgentParentFromPayload(taskWithHarnessOverrides.payload)
+ ?.sessionId ?? null,
origin: 'follow_up',
existingTaskReused: true,
});
@@ -1716,10 +1704,13 @@ async function enqueueFreshLaunch(
taskId = createdTask.id;
}
- if (shouldLinkSession) {
+ if (sessionsDataEnabled) {
+ const fastParent = getFastAgentParentFromPayload(
+ taskWithHarnessOverrides.payload,
+ );
await ensureSessionForTask(tx, {
taskId,
- fastConversationId,
+ fastConversationId: fastParent?.sessionId ?? null,
origin: fastParent
? 'fast_delegation'
: existingTask
From beb7796f50ce760787acaa83b4a4feb420d38f5a Mon Sep 17 00:00:00 2001
From: Bruno Bergher
Date: Thu, 27 Aug 2026 19:30:42 +0100
Subject: [PATCH 25/26] fix: refresh session task sidebar
---
.../SessionWorkspace.client.test.tsx | 74 +++++++++++++++++--
.../sessions/[sessionId]/SessionWorkspace.tsx | 20 ++++-
2 files changed, 83 insertions(+), 11 deletions(-)
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx
index 88f74019e..1e3739462 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx
@@ -1,12 +1,14 @@
import { useState, type ReactNode } from 'react';
-import { fireEvent, render, screen } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { SandboxLayoutContext } from '../../use-sandbox-layout';
import { SessionWorkspace, type SessionInfo } from './SessionWorkspace';
import { useOpenSessionTaskPanel } from './session-task-panel-context';
-const { useMediaQueryMock } = vi.hoisted(() => ({
+const { useMediaQueryMock, sessionQueryState } = vi.hoisted(() => ({
useMediaQueryMock: vi.fn(),
+ sessionQueryState: { data: null as unknown },
}));
vi.mock('usehooks-ts', () => ({
@@ -24,6 +26,23 @@ vi.mock('@/hooks/task-models/useLaunchTaskModels', () => ({
}),
}));
+vi.mock('@/trpc/client', () => ({
+ useTRPC: () => ({
+ sessions: {
+ byId: {
+ queryOptions: (
+ input: { sessionId: string },
+ options?: Record,
+ ) => ({
+ queryKey: ['sessions', 'byId', input.sessionId],
+ queryFn: async () => sessionQueryState.data,
+ ...options,
+ }),
+ },
+ },
+ }),
+}));
+
vi.mock('./NestedTaskSidePanel', () => ({
NestedTaskSidePanel: ({ taskId }: { taskId: string }) => (
Nested panel {taskId}
@@ -84,19 +103,29 @@ function renderWorkspace({
isMobile,
children = Session transcript
,
sessionOverride,
+ queriedTasks,
}: {
isMobile: boolean;
children?: ReactNode;
sessionOverride?: Partial;
+ queriedTasks?: SessionInfo['tasks'];
}) {
useMediaQueryMock.mockReturnValue(!isMobile);
+ const initialSession = { ...session, ...sessionOverride };
+ sessionQueryState.data = {
+ ...initialSession,
+ tasks: queriedTasks ?? initialSession.tasks,
+ };
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+ });
render(
-
-
- {children}
-
- ,
+
+
+ {children}
+
+ ,
);
}
@@ -198,6 +227,37 @@ describe('SessionWorkspace', () => {
expect(screen.getByText('Nested panel task-1')).toBeInTheDocument();
});
+ it('enables and populates the Tasks panel from refreshed session tasks', async () => {
+ const delegatedTask = {
+ taskId: 'task-2',
+ title: 'Refreshed coding task',
+ workflow: 'standard',
+ state: 'active',
+ repositoryName: null,
+ latestOutput: null,
+ inferenceCostMicroUsd: 0,
+ canAccessDetails: true,
+ latestRun: null,
+ artifacts: [],
+ pullRequests: [],
+ };
+ renderWorkspace({
+ isMobile: false,
+ queriedTasks: [delegatedTask],
+ });
+
+ await waitFor(() => {
+ expect(screen.getByRole('button', { name: 'Tasks' })).toBeEnabled();
+ });
+ fireEvent.click(screen.getByRole('button', { name: 'Tasks' }));
+
+ expect(
+ screen.getByRole('button', {
+ name: 'View coding task: Refreshed coding task',
+ }),
+ ).toBeInTheDocument();
+ });
+
it('opens delegated tasks in the existing session side-panel slot', () => {
renderWorkspace({ isMobile: false, children: });
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
index 2231f329e..e0dd83e6f 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
@@ -3,10 +3,12 @@
import Link from 'next/link';
import { useCallback, useEffect, useState, type ReactNode } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
+import { useQuery } from '@tanstack/react-query';
import { getReasoningEffortLabel, type ReasoningEffort } from '@roomote/types';
import { formatInferenceCost, getUserDisplayName } from '@/lib';
import { useLaunchTaskModels } from '@/hooks/task-models/useLaunchTaskModels';
+import { useTRPC } from '@/trpc/client';
import { FramedSurface, WorkspaceSurface } from '@/components/layout';
import { SideNavItem } from '@/components/layout/side-nav/SideNavItem';
import {
@@ -358,10 +360,20 @@ export function SessionWorkspace({
const [isInfoOpen, setIsInfoOpen] = useState(false);
const [isTasksOpen, setIsTasksOpen] = useState(false);
const [nestedTaskId, setNestedTaskId] = useState(null);
+ const trpc = useTRPC();
const router = useRouter();
const searchParams = useSearchParams();
+ const { data: currentSession } = useQuery(
+ trpc.sessions.byId.queryOptions(
+ { sessionId: session.id },
+ {
+ refetchInterval: 2_000,
+ },
+ ),
+ );
+ const sessionTasks = currentSession?.tasks ?? session.tasks;
const selectedTaskId = searchParams.get('task');
- const selectedTask = session.tasks.find(
+ const selectedTask = sessionTasks.find(
(task) => task.taskId === selectedTaskId,
);
const panelOpen =
@@ -405,13 +417,13 @@ export function SessionWorkspace({
) : isTasksOpen ? (
@@ -445,7 +457,7 @@ export function SessionWorkspace({
label="Tasks"
tooltip="Tasks"
active={isTasksOpen}
- disabled={session.tasks.length === 0}
+ disabled={sessionTasks.length === 0}
icon={Rows4}
onClick={() => {
setNestedTaskId(null);
From d8efdac81b2935dd1a7cf83a78e7adeca2ffdbef Mon Sep 17 00:00:00 2001
From: Bruno Bergher
Date: Thu, 27 Aug 2026 19:38:05 +0100
Subject: [PATCH 26/26] fix: list tasks for fast sessions
---
.../SessionWorkspace.client.test.tsx | 31 ++++++++--
.../sessions/[sessionId]/SessionWorkspace.tsx | 22 ++++++-
.../(sandbox)/sessions/[sessionId]/page.tsx | 7 ++-
apps/web/src/lib/server/fast-sessions.test.ts | 28 +++++++++
apps/web/src/lib/server/fast-sessions.ts | 59 +++++++++++++++++++
.../src/trpc/commands/fast-sessions/index.ts | 12 +++-
apps/web/src/trpc/routers/_app.ts | 6 ++
7 files changed, 155 insertions(+), 10 deletions(-)
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx
index 1e3739462..4614115cb 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.client.test.tsx
@@ -6,10 +6,13 @@ import { SandboxLayoutContext } from '../../use-sandbox-layout';
import { SessionWorkspace, type SessionInfo } from './SessionWorkspace';
import { useOpenSessionTaskPanel } from './session-task-panel-context';
-const { useMediaQueryMock, sessionQueryState } = vi.hoisted(() => ({
- useMediaQueryMock: vi.fn(),
- sessionQueryState: { data: null as unknown },
-}));
+const { useMediaQueryMock, sessionQueryState, fastTaskQueryState } = vi.hoisted(
+ () => ({
+ useMediaQueryMock: vi.fn(),
+ sessionQueryState: { data: null as unknown },
+ fastTaskQueryState: { data: null as unknown },
+ }),
+);
vi.mock('usehooks-ts', () => ({
useMediaQuery: useMediaQueryMock,
@@ -40,6 +43,18 @@ vi.mock('@/trpc/client', () => ({
}),
},
},
+ fastSessions: {
+ tasks: {
+ queryOptions: (
+ input: { sessionId: string },
+ options?: Record,
+ ) => ({
+ queryKey: ['fastSessions', 'tasks', input.sessionId],
+ queryFn: async () => fastTaskQueryState.data,
+ ...options,
+ }),
+ },
+ },
}),
}));
@@ -104,11 +119,15 @@ function renderWorkspace({
children = Session transcript
,
sessionOverride,
queriedTasks,
+ queriedFastTasks,
}: {
isMobile: boolean;
children?: ReactNode;
sessionOverride?: Partial;
queriedTasks?: SessionInfo['tasks'];
+ queriedFastTasks?: Array<
+ Pick
+ >;
}) {
useMediaQueryMock.mockReturnValue(!isMobile);
const initialSession = { ...session, ...sessionOverride };
@@ -116,6 +135,7 @@ function renderWorkspace({
...initialSession,
tasks: queriedTasks ?? initialSession.tasks,
};
+ fastTaskQueryState.data = queriedFastTasks ?? initialSession.taskCards ?? [];
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
@@ -243,7 +263,8 @@ describe('SessionWorkspace', () => {
};
renderWorkspace({
isMobile: false,
- queriedTasks: [delegatedTask],
+ sessionOverride: { taskSource: 'fast', taskCards: [] },
+ queriedFastTasks: [delegatedTask],
});
await waitFor(() => {
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
index e0dd83e6f..ddf049d09 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/SessionWorkspace.tsx
@@ -62,6 +62,8 @@ export type SessionInfo = {
createdAt: Date;
status: string | null;
tasks: SessionTaskSummary[];
+ taskSource?: 'unified' | 'fast';
+ taskCards?: Array>;
};
const SURFACE_LABELS: Record = {
@@ -218,7 +220,7 @@ function SessionTasksPanel({
onOpenTask,
onClose,
}: {
- tasks: SessionTaskSummary[];
+ tasks: Array>;
onOpenTask: (taskId: string) => void;
onClose: () => void;
}) {
@@ -363,15 +365,29 @@ export function SessionWorkspace({
const trpc = useTRPC();
const router = useRouter();
const searchParams = useSearchParams();
+ const isFastTaskSource = session.taskSource === 'fast';
const { data: currentSession } = useQuery(
trpc.sessions.byId.queryOptions(
{ sessionId: session.id },
{
+ enabled: !isFastTaskSource,
+ refetchInterval: 2_000,
+ },
+ ),
+ );
+ const { data: currentFastTasks } = useQuery(
+ trpc.fastSessions.tasks.queryOptions(
+ { sessionId: session.id },
+ {
+ enabled: isFastTaskSource,
refetchInterval: 2_000,
},
),
);
const sessionTasks = currentSession?.tasks ?? session.tasks;
+ const taskCards = isFastTaskSource
+ ? (currentFastTasks ?? session.taskCards ?? session.tasks)
+ : sessionTasks;
const selectedTaskId = searchParams.get('task');
const selectedTask = sessionTasks.find(
(task) => task.taskId === selectedTaskId,
@@ -423,7 +439,7 @@ export function SessionWorkspace({
/>
) : isTasksOpen ? (
@@ -457,7 +473,7 @@ export function SessionWorkspace({
label="Tasks"
tooltip="Tasks"
active={isTasksOpen}
- disabled={sessionTasks.length === 0}
+ disabled={taskCards.length === 0}
icon={Rows4}
onClick={() => {
setNestedTaskId(null);
diff --git a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx
index 5e63f5f4d..dc9d0e2f7 100644
--- a/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx
+++ b/apps/web/src/app/(sandbox)/sessions/[sessionId]/page.tsx
@@ -8,7 +8,10 @@ import {
} from '@roomote/types';
import { authorize } from '@/lib/server/auth-context';
-import { getFastSessionById } from '@/lib/server/fast-sessions';
+import {
+ getFastSessionById,
+ getFastSessionTasks,
+} from '@/lib/server/fast-sessions';
import { getSessionByIdCommand } from '@/trpc/commands/sessions';
import { Badge } from '@/components/system';
import { WorkspaceHeader } from '@/components/layout';
@@ -153,6 +156,8 @@ export default async function SessionDetailPage({
createdAt: session.createdAt,
status: null,
tasks: [],
+ taskSource: 'fast',
+ taskCards: (await getFastSessionTasks(authorizedUser, session.id)) ?? [],
};
const initialUserMessage = session.messages.find(
(message) => message.role === 'user',
diff --git a/apps/web/src/lib/server/fast-sessions.test.ts b/apps/web/src/lib/server/fast-sessions.test.ts
index a4dcdd1e2..6b67fa247 100644
--- a/apps/web/src/lib/server/fast-sessions.test.ts
+++ b/apps/web/src/lib/server/fast-sessions.test.ts
@@ -2,6 +2,8 @@ import {
db,
fastAgentConversations,
fastAgentMessages,
+ runFactory,
+ taskFactory,
userFactory,
} from '@roomote/db/server';
@@ -9,6 +11,7 @@ import {
encodeFastSessionCursor,
findAccessibleFastSession,
getFastSessionById,
+ getFastSessionTasks,
getFastSessionMessagesSince,
getFastSessions,
} from './fast-sessions';
@@ -183,6 +186,31 @@ describe('Fast session queries', () => {
).resolves.toMatchObject({ id: session.id, userId: owner.id });
});
+ it('lists every task associated with a Fast session', async () => {
+ const owner = await userFactory.create();
+ const session = await createFastSession({
+ userId: owner.id,
+ conversationId: 'tasks-session',
+ updatedAt: new Date('2026-01-01T00:00:00.000Z'),
+ });
+ const delegatedTask = await taskFactory.create({
+ title: 'Delegated task',
+ state: 'active',
+ });
+ await runFactory.create({
+ taskId: delegatedTask.id,
+ payload: {
+ repo: 'acme/widgets',
+ description: 'Delegated Fast task',
+ fastAgentSessionId: session.id,
+ },
+ });
+
+ await expect(
+ getFastSessionTasks({ userId: owner.id, isAdmin: false }, session.id),
+ ).resolves.toEqual([{ taskId: delegatedTask.id, title: 'Delegated task' }]);
+ });
+
it('reads canonical messages in timestamp and turn sequence order', async () => {
const owner = await userFactory.create();
const session = await createFastSession({
diff --git a/apps/web/src/lib/server/fast-sessions.ts b/apps/web/src/lib/server/fast-sessions.ts
index 96eccfc9a..41a17e5fa 100644
--- a/apps/web/src/lib/server/fast-sessions.ts
+++ b/apps/web/src/lib/server/fast-sessions.ts
@@ -13,9 +13,13 @@ import {
fastAgentConversations,
fastAgentMessages,
llmUsageEvents,
+ inArray,
+ isNull,
lt,
or,
sql,
+ taskRuns,
+ tasks,
users,
} from '@roomote/db/server';
import type { FastAgentMessage } from '@roomote/db';
@@ -24,6 +28,11 @@ import type { TimePeriodFilter, UserAuthSuccess } from '@/types';
type FastSessionAuth = Pick;
+type FastSessionTaskSummary = {
+ taskId: string;
+ title: string;
+};
+
export type FastSessionMessage = Pick<
FastAgentMessage,
| 'id'
@@ -120,6 +129,56 @@ export async function findAccessibleFastSession(
return session ?? null;
}
+/**
+ * Fast conversations predate the unified Session tables. Their delegated tasks
+ * are linked directly from task runs, rather than through session_tasks.
+ */
+export async function getFastSessionTasks(
+ auth: FastSessionAuth,
+ sessionId: string,
+): Promise {
+ const session = await findAccessibleFastSession(auth, sessionId);
+ if (!session) return null;
+
+ const [conversation] = await db
+ .select({
+ legacyConversationIds: fastAgentConversations.legacyConversationIds,
+ })
+ .from(fastAgentConversations)
+ .where(eq(fastAgentConversations.id, session.id))
+ .limit(1);
+ const lookupIds = [
+ session.id,
+ ...(conversation?.legacyConversationIds ?? []),
+ ];
+ const latestRunPerTask = db.$with('latest_fast_session_task_runs').as(
+ db
+ .selectDistinctOn([taskRuns.taskId], {
+ taskId: taskRuns.taskId,
+ title: tasks.title,
+ latestRunId: taskRuns.id,
+ })
+ .from(taskRuns)
+ .innerJoin(tasks, eq(tasks.id, taskRuns.taskId))
+ .where(
+ and(
+ inArray(taskRuns.fastAgentSessionId, lookupIds),
+ isNull(tasks.deletedAt),
+ ),
+ )
+ .orderBy(taskRuns.taskId, desc(taskRuns.id)),
+ );
+
+ return db
+ .with(latestRunPerTask)
+ .select({
+ taskId: latestRunPerTask.taskId,
+ title: latestRunPerTask.title,
+ })
+ .from(latestRunPerTask)
+ .orderBy(desc(latestRunPerTask.latestRunId));
+}
+
function sanitizeFastSessionMessageRow<
T extends Pick<
FastSessionMessage,
diff --git a/apps/web/src/trpc/commands/fast-sessions/index.ts b/apps/web/src/trpc/commands/fast-sessions/index.ts
index a6d62e524..7461a75ea 100644
--- a/apps/web/src/trpc/commands/fast-sessions/index.ts
+++ b/apps/web/src/trpc/commands/fast-sessions/index.ts
@@ -26,7 +26,10 @@ import {
} from '@roomote/types';
import type { UserAuthSuccess } from '@/types';
-import { findAccessibleFastSession } from '@/lib/server/fast-sessions';
+import {
+ findAccessibleFastSession,
+ getFastSessionTasks,
+} from '@/lib/server/fast-sessions';
/**
* Persist the session's model settings when the caller sent an explicit
@@ -205,6 +208,13 @@ export async function startFastSessionCommand(
};
}
+export async function getFastSessionTasksCommand(
+ auth: UserAuthSuccess,
+ sessionId: string,
+) {
+ return getFastSessionTasks(auth, sessionId);
+}
+
export async function replyToFastSessionCommand(
auth: UserAuthSuccess,
input: {
diff --git a/apps/web/src/trpc/routers/_app.ts b/apps/web/src/trpc/routers/_app.ts
index 5f7208343..23825e807 100644
--- a/apps/web/src/trpc/routers/_app.ts
+++ b/apps/web/src/trpc/routers/_app.ts
@@ -32,6 +32,7 @@ import {
} from '@roomote/types';
import {
+ getFastSessionTasksCommand,
replyToFastSessionCommand,
startFastSessionCommand,
} from '../commands/fast-sessions';
@@ -2822,6 +2823,11 @@ export const appRouter = createRouter({
.mutation(({ ctx: { auth }, input }) =>
replyToFastSessionCommand(auth, input),
),
+ tasks: protectedProcedure
+ .input(z.object({ sessionId: z.string().uuid() }))
+ .query(({ ctx: { auth }, input }) =>
+ getFastSessionTasksCommand(auth, input.sessionId),
+ ),
}),
sessions: createRouter({